How selection works
src/viewer/assets/viewer-theme-boot.js runs as a classic script before the first stylesheet. It validates the stored ID against a closed registry and stamps data-theme on the document root. viewer-theme.js wires the labeled select after the page loads, using that same controller.
Scientific Clay is the default for browsers without a saved preference and stays light. Minimal follows the operating system’s light or dark setting. Nebula Light and Dark are explicit choices.
Preferences use llmwiki.viewer.theme.v1. When that key is absent, a saved light or dark in the older llmwiki-viewer-theme key migrates to the corresponding Nebula choice. An invalid new preference falls back to Scientific Clay. The older key is preserved for older viewer versions. Unavailable storage leaves selection usable for the current page.
Storage belongs to the browser origin, including the port. Reuse the same llmwiki view --port value to keep preferences between launches.
Stylesheet structure
src/viewer/assets/viewer-tokens.css imports these layers in order:
viewer-fonts.css: the thirteen self-hosted font faces.themes/theme-base.css: safe semantic defaults.themes/public-tokens.css: aliases and readable status pairs for public components.themes/minimal.css,themes/scientific-clay.css, andthemes/nebula.css: the selected palettes.
viewer-material.css connects shared elevation and heading tokens to existing components.
Theme files map values; structural styles consume them. For example, public --fg-body reads semantic --text-primary, and --accent-text reads --text-interactive. Nebula maps the public names directly to preserve its existing appearances. Keep aliases on the themed root so palette changes update them together.
Status text needs readable ink, which can differ from a decorative accent. Check warning panels, badges, small metadata, and text on filled buttons against their actual backgrounds. Translucent surfaces require checking the colors beneath them too.
Add a palette
- Copy
themes/minimal.cssto a new file and use your own root selector, such as[data-theme="slate"]. Keep its header and CSS asset marker. Map semantic tokens and any public status extensions your palette needs. - Import it in
viewer-tokens.cssafter the existing maps and before structural styles. - Add a stable ID to
THEME_IDSinviewer-theme-boot.js. IDs are limited to 32 characters; use lowercase kebab-case. Renaming an ID resets its saved preferences. - Add a labeled option to the select in
index.html. - Update
test/viewer-theme.test.ts,test/viewer-theme-css.test.ts, andtest/viewer-pack.test.tsfor the new ID, map, and assets. - Test switching and reloads on every route, including both the compact dashboard graph and full graph, at desktop and narrow widths.
Fonts and verification
Fonts are vendored withnpm run vendor:fonts. Add families and weights to scripts/vendor-fonts.mjs, pin their development packages, add font-face rules to viewer-fonts.css, and preserve their licenses in THIRD_PARTY_NOTICES.txt. The build copies nested CSS and font assets into the npm package.