Skip to content

Library API

Daniel Budd edited this page Apr 11, 2026 · 1 revision

Library API: course-ui

course-ui is the small, reusable client-side library that powers the Geometry Playground website. It's a self-contained folder that can be dropped into any other static HTML curriculum or course site to provide the same accessibility, search, vocabulary, and mobile navigation features.

This page is the public reference for anyone who wants to reuse the library in their own project.

Current users:

If you build something on top of course-ui, please let me know (open an issue on the main repo) so I can add it to this list.

What it provides

Feature What it does
Aa popover Text size (75 to 150 percent), font family (Sans, Serif, or Dyslexic), theme (Light, Sepia, Dark)
Focus mode Hides the site sidebar, widens the article column
Reading progress line 3 pixel bar under the top toolbar, fills as the user scrolls
Reading position bar Bottom strip showing "Section 3 of 10 · 27%"
Site wide search Expanding inline input with live results. Can index the current page or crawl every page in a configurable list
Vocabulary popovers Dashed underlined terms with definition popovers on hover / focus / tap
Mobile burger menu Slide in drawer with cloned chapter and section navigation for small screens
Skip link Keyboard accessible "Skip to main content"
Focus ring Visible amber :focus-visible outline in every theme
Reduced motion Respects prefers-reduced-motion for users who want animations disabled
Print Strips all injected UI on print
OS dark mode Respects prefers-color-scheme: dark on first visit

Everything persists to localStorage (user prefs) and sessionStorage (search index cache) under a brand-namespaced key prefix.

File structure

The library is always exactly these files:

course-ui/
├── course-ui.css     (theme tokens + component styles)
└── course-ui.js      (IIFE; reads window.CourseUI, injects UI, wires events)

A typical consuming project looks like:

your-site/
├── .nojekyll                   (required on GitHub Pages)
├── course-ui/                  (copy this folder verbatim)
│   ├── course-ui.css
│   └── course-ui.js
├── vocab/                      (site-specific vocabulary data, optional)
│   └── your-subject.js
├── styles.css                  (site-specific brand + layout styles)
├── index.html
├── 01.html, 02.html, ...       (chapter / lesson pages)

Installation: integrating into a new project

Step 1. Copy the library folder

Copy the course-ui/ folder into the root of your target repo. Do not rename the folder. The current version lives at github.com/dbbudd/dbbudd.github.io/tree/main/course-ui.

Step 2. Add .nojekyll at the repo root (for GitHub Pages)

An empty file. Bypasses Jekyll so all folders and files are served as-is.

Step 3. Wire each page in <head>

<link rel="stylesheet" href="course-ui/course-ui.css?v=1" />
<link rel="stylesheet" href="styles.css?v=1" />

<script>
  window.CourseUI = {
    brand: { name: 'Your Course Name', accent: '#F2B13E' },
    reading: { readingLine: true, readingPos: true, focusMode: true },
    search: {
      enabled: true,
      scope: 'site',
      pages: ['index.html', '01.html', '02.html']
    },
    vocab: {
      enabled: true,
      autoWrap: true,
      contentSelector: '.section-hero .lead, .article p, .article li'
    }
  };
</script>

<script src="vocab/your-subject.js?v=1"></script>
<script src="course-ui/course-ui.js?v=1" defer></script>

Critical load order: course-ui.css first (so site CSS can consume theme tokens), inline config second, vocab data third, library JavaScript last.

Step 4. Add the skip link at the top of <body>

<a class="cu-skip-link" href="#main-content">Skip to main content</a>

The library does not inject this because the anchor target is site-specific.

Step 5. Make sure the site CSS uses library theme tokens

The library exposes CSS custom properties like --cu-bg-page, --cu-text, --cu-border. Your site CSS must reference these variables (directly or via aliases), otherwise dark and sepia themes will not flip your content's background and text colours.

Step 6. Test locally

Site wide search and external vocab data files both use fetch(), which is blocked by browsers when you open HTML files directly via file://. For local development:

  • Recommended: VS Code "Live Server" extension, or npx serve
  • Also works: python3 -m http.server 8080
  • Limited: double-click the HTML file. Most features work; site wide search does not.

On GitHub Pages in production, everything just works.

Configuration reference

Everything goes on window.CourseUI before course-ui.js loads. All fields are optional except where marked.

window.CourseUI = {
  brand: {
    name: 'Geometry Playground',  // (required) used for localStorage namespace
    accent: '#F2B13E'              // accent colour (reading bar, focus ring, highlights)
  },

  reading: {
    readingLine: true,   // top scroll progress line
    readingPos: true,    // bottom reading-position bar
    focusMode: true      // "Focus" button in toolbar cluster
  },

  theme: {
    rememberPrefs: true,
    defaultFont: 'sans'   // 'sans' | 'serif' | 'dyslexic'
  },

  search: {
    enabled: true,
    scope: 'site',        // 'page' (current page only) | 'site' (crawl listed pages)
    pages: [              // only used when scope is 'site'
      'index.html',
      '01.html', '02.html'
    ],
    sectionSelector: 'main > section[id]'
  },

  vocab: {
    enabled: true,
    autoWrap: true,
    contentSelector: '.section-hero .lead, .article p, .article li',
    terms: {              // inline dictionary (highest priority)
      'specific term': 'definition shown in popover'
    },
    src: 'vocab/subject.json'   // or external file(s); array allowed
                                 // supported: .json, .yaml, .yml, .md
                                 // (or load a .js file via a <script> tag instead,
                                 //  which avoids the fetch() requirement)
  },

  mobile: {
    enabled: true,
    drawer: {
      title: 'Navigation',
      sections: [
        { title: 'Chapters', clone: '#chapterPill .nav-dropdown' },
        { title: 'Sections', clone: '.sidebar nav' }
      ]
    }
  },

  selectors: {
    toolbar: '.topbar, header.site-header',
    toolbarInsertBefore: '.topbar-progress, .overall-progress',
    mainContent: 'main.content, main',
    sections: 'main.content > section[id], main > section[id]',
    sidebar: '.sidebar'
  }
};

Theming

How it works

The library defines CSS custom properties under three theme scopes:

:root                     { --cu-bg-page: #ffffff; --cu-text: #1d1d1f; ... }
[data-theme="sepia"]      { --cu-bg-page: #f5edd6; --cu-text: #3e2f1c; ... }
[data-theme="dark"]       { --cu-bg-page: #111317; --cu-text: #e6e6ee; ... }

When the user picks a theme, the library sets data-theme="..." on <html>. Every rule that references a --cu-* variable reflows automatically.

Theme tokens the library exposes

Surfaces           Text                 Borders
--cu-bg-page       --cu-text            --cu-border-soft
--cu-bg-surface    --cu-text-body       --cu-border
--cu-bg-elevated   --cu-text-muted      --cu-border-strong
                   --cu-text-strong

Content components                      Data tables
--cu-callout-bg/border/title            --cu-table-header-bg/text
--cu-blockquote-bg/border               --cu-table-th-bg/text
--cu-code-inline-bg/text                --cu-table-td-text/alt
--cu-solution-bg/border                 --cu-table-divider

Reading UI                              Vocabulary popover
--cu-reading-track                      --cu-term-bg/border
--cu-reading-fill                       --cu-popover-bg/text
--cu-reading-pos-bg/text/strong

Font system                             Site hook-points
--cu-font-scale                         --cu-topbar-h
--cu-body-font                          --cu-accent
--cu-heading-font

What site CSS should NOT do

  • Do not hardcode theme-sensitive colours (like background: #ffffff). Use var(--cu-bg-page) or an aliased site variable.
  • Do not set font-size on html. The library owns html { font-size } so it can apply --cu-font-scale. Size things with rem/em relative to that.
  • Brand colours (navy, amber, coral) are fine to hardcode. They're the same across themes by design.

Vocabulary

Three authoring modes, combined in priority order:

  1. Explicit markup: any element with data-def="..." becomes a vocabulary term. <dfn data-def="A line segment has two endpoints">segment</dfn>.
  2. Inline dictionary: set CourseUI.vocab.terms = { 'term': 'definition' } in the config block.
  3. External file via src:: .json, .yaml, or .md loaded via fetch(). Works over HTTPS only.
  4. External JavaScript file loaded via <script> tag: the most robust pattern. Works under every protocol including file://.

The recommended pattern: a .js vocabulary file

This is the pattern Geometry Playground uses. Create vocab/your-subject.js with this shape:

(function (global) {
  'use strict';

  const terms = [
    {
      term: 'congruent',
      def: 'Two shapes are congruent if they have the same size and shape.',
      aliases: ['congruence']
    },
    {
      term: 'translation',
      def: 'A transformation that slides every point by the same amount.',
      aliases: ['translations', 'translate']
    }
  ];

  const cu = (global.CourseUI = global.CourseUI || {});
  const v  = (cu.vocab = cu.vocab || {});
  const dict = {};
  terms.forEach(e => {
    if (!e || !e.term || !e.def) return;
    dict[e.term] = e.def;
    if (Array.isArray(e.aliases)) {
      e.aliases.forEach(a => { if (a) dict[a] = e.def; });
    }
  });
  v.terms = Object.assign(dict, v.terms || {});
})(window);

Load it before course-ui.js so the dictionary is populated by the time the library boots:

<script>window.CourseUI = { vocab: { enabled: true, autoWrap: true } };</script>
<script src="vocab/your-subject.js?v=1"></script>
<script src="course-ui/course-ui.js?v=1" defer></script>

How auto-wrapping works

On load, the library walks text nodes inside every element matching contentSelector. For each term (sorted longest first, so "interior angle" wins over "angle"), it wraps the first matching occurrence per page in a <span class="cu-term">.

The walker skips <code>, <pre>, <a>, <script>, and <style>. This is intentional. Inside <code>, a word is a type name and should not be decorated.

A second pass decorates every <strong> element whose entire text matches a vocabulary key, so that bolded vocab terms always pop their definition regardless of page position.

Search

Scope modes

  • scope: 'page'  ·  indexes only the current page. Works everywhere. Instant first open.
  • scope: 'site'  ·  indexes the current page plus every URL in search.pages, using fetch() and DOMParser. First open triggers parallel fetches of all listed pages; the result is cached in sessionStorage for the rest of the tab's lifetime.

Keyboard shortcuts

  • ⌘ + K or Ctrl + K  ·  open or focus search
  • Escape  ·  close search and any open popover
  • Tab  ·  navigate result list

What gets indexed

For each page, the library extracts every <section id="..."> that matches sectionSelector, indexing the first <h1> or <h2> as the title and all descendant <p> / <li> / <blockquote> / .lead / <td> text as the body. Vocabulary terms are indexed too.

Storage

All user prefs live in localStorage under keys prefixed with cu_<brand_name_lowercase>_:

Key Value
cu_<brand>_theme light | sepia | dark
cu_<brand>_font sans | serif | dyslexic
cu_<brand>_fontScale 0.75 to 1.5
cu_<brand>_focus true | false

The search index is cached in sessionStorage under cu_<brand>_searchIndex_v<n>, keyed by an internal INDEX_VERSION constant that is bumped when the index schema changes.

GitHub Pages notes

  • Required: add an empty .nojekyll file at the repo root. This disables Jekyll processing.
  • Cache busting: append ?v=N to the library script URLs, and bump the number whenever you ship a library change. The build pattern is:
    <link rel="stylesheet" href="course-ui/course-ui.css?v=2" />
    <script src="course-ui/course-ui.js?v=2" defer></script>
  • OpenDyslexic font: loaded from jsDelivr CDN via @font-face with font-display: swap. If the CDN is blocked, the Dyslexic font option falls back gracefully to Comic Sans MS.

Public API

Available on window.CourseUI after boot:

CourseUI.setTheme('light' | 'sepia' | 'dark')
CourseUI.setFont('sans' | 'serif' | 'dyslexic')
CourseUI.adjustFontSize(-1 | 0 | 1)     // 0 = reset to 100 percent
CourseUI.toggleFocus()
CourseUI.toggleAa()
CourseUI.toggleSearch()
CourseUI.toggleDrawer()
CourseUI.loadVocab('vocab/chapter-05.json')  // load more terms at runtime
CourseUI.vocabReady                           // Promise, resolves when
                                              // initial vocab load is done

Integration checklist (new project)

  • Copy course-ui/ folder into repo root
  • Create empty .nojekyll at repo root
  • Create vocab/<subject>.js (optional but recommended)
  • In every page's <head>:
    • Load course-ui/course-ui.css?v=1
    • Load your site styles.css
    • Inline <script> with window.CourseUI = { ... } config
    • <script src="vocab/<subject>.js?v=1"> (no defer, synchronous)
    • <script src="course-ui/course-ui.js?v=1" defer>
  • Add skip link as the first child of <body>
  • Update site styles.css to reference --cu-* variables
  • Verify selectors config matches your HTML structure
  • Test on a real HTTP origin (GitHub Pages or localhost)
  • Run through the testing checklist:
    • Aa > Dark / Sepia / Light all flip correctly
    • A+ / A- scales text
    • Sans / Serif / Dyslexic swaps the font
    • Focus hides the sidebar
    • Scrolling fills the top progress line and updates the bottom bar
    • Search returns results across all listed pages
    • Hovering a term shows the popover
    • :focus-visible rings are amber and clearly visible
    • Preferences persist across reloads
    • Print preview looks clean

License

Same as the main project. See the LICENSE file in the main repo.

Next

Clone this wiki locally