-
Notifications
You must be signed in to change notification settings - Fork 1
Library API
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:
- Geometry Playground
- VEX V5 Robotics (in development)
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.
| 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 |
| 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.
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)
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.
An empty file. Bypasses Jekyll so all folders and files are served as-is.
<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.
<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.
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.
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.
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'
}
};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.
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
- Do not hardcode theme-sensitive colours (like
background: #ffffff). Usevar(--cu-bg-page)or an aliased site variable. - Do not set
font-sizeonhtml. The library ownshtml { font-size }so it can apply--cu-font-scale. Size things withrem/emrelative to that. - Brand colours (navy, amber, coral) are fine to hardcode. They're the same across themes by design.
Three authoring modes, combined in priority order:
-
Explicit markup: any element with
data-def="..."becomes a vocabulary term.<dfn data-def="A line segment has two endpoints">segment</dfn>. -
Inline dictionary: set
CourseUI.vocab.terms = { 'term': 'definition' }in the config block. -
External file via
src::.json,.yaml, or.mdloaded viafetch(). Works over HTTPS only. -
External JavaScript file loaded via
<script>tag: the most robust pattern. Works under every protocol includingfile://.
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>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.
-
scope: 'page'· indexes only the current page. Works everywhere. Instant first open. -
scope: 'site'· indexes the current page plus every URL insearch.pages, usingfetch()andDOMParser. First open triggers parallel fetches of all listed pages; the result is cached insessionStoragefor the rest of the tab's lifetime.
-
⌘ + KorCtrl + K· open or focus search -
Escape· close search and any open popover -
Tab· navigate result list
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.
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.
-
Required: add an empty
.nojekyllfile at the repo root. This disables Jekyll processing. -
Cache busting: append
?v=Nto 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-facewithfont-display: swap. If the CDN is blocked, the Dyslexic font option falls back gracefully to Comic Sans MS.
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- Copy
course-ui/folder into repo root - Create empty
.nojekyllat 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>withwindow.CourseUI = { ... }config -
<script src="vocab/<subject>.js?v=1">(no defer, synchronous) -
<script src="course-ui/course-ui.js?v=1" defer>
- Load
- Add skip link as the first child of
<body> - Update site
styles.cssto reference--cu-*variables - Verify
selectorsconfig 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-visiblerings are amber and clearly visible - Preferences persist across reloads
- Print preview looks clean
Same as the main project. See the LICENSE file in the main repo.
- Want the live demo? Try Geometry Playground with the library in production.
- Want the source? course-ui on GitHub.
- Have a question about reusing it? Open a GitHub Issue.
Geometry Playground · a Swift Playgrounds curriculum for high school geometry · dbbudd.github.io · built by Daniel Budd