Theme & Storage¶
Two small but load-bearing core modules: theme.ts (colors + UI vars → CSS variables) and storageKey.ts (namespaced localStorage).
Theme — core/theme.ts¶
applyTheme(colors)¶
Writes every color token to a --color-<token> CSS variable on :root. Each token's value is a Tailwind color name ('indigo-700') resolved to that palette's variable (var(--color-indigo-700)), so both Tailwind utilities (bg-primary, bg-danger) and direct var(--color-*) CSS read the live value.
Resolution: { ...DEFAULT_COLORS, ...userColors } — missing tokens fall back to DEFAULT_COLORS, so nothing is required. Two sentinels keep relationships live:
- A token whose value is another token name (
'primary','surface','mutedText') points at that token's var. Sosave: 'primary'tracks whateverprimaryis set to — changeprimaryand the save button follows. 'primaryDarker'emits acolor-mix(in srgb, var(--color-primary) 85%, black)— a hover shade that's "primary, ~15% darker" regardless of which shade primary is. Used for action/save hovers.
DEFAULT_COLORS is the locked default palette; it mirrors the @theme block in index.css (which provides the boot defaults so the first paint is correct).
applyUiVars(ui, isMobile)¶
Writes non-color layout variables, kept separate from applyTheme to make clear these aren't brand colors:
--floating-control-sizeand--top-bar-button-size(+ a derived--top-bar-icon-sizeat 50%) — both switch to theirmobileSizebelow the breakpoint, which is why this is re-called whenisMobileflips.--widget-button-cursor— fromui.buttonCursor.--top-bar-shadow-height/--top-bar-shadow-opacity— the map-window drop shadow;strength(0–1, clamped) scales both. Height collapses to 0 when disabled.
Both are called synchronously in main.tsx before the first React render, so there's no color/size flash on boot.
Why map paint is different¶
MapLibre style values (layer paint) can't read CSS variables. Colors that feed map rendering — the hover-highlight color, default label colors — are resolved to a computed value at runtime via getComputedStyle(:root), not var(). This is the one place the token system doesn't reach directly.
Storage — core/storageKey.ts¶
Every localStorage key the app writes routes through one helper so the namespace can't drift between modules and one setting controls all of it.
<appId> is branding.appId if set, else a slug of branding.title. Consumers:
| Consumer | Key |
|---|---|
| Bookmarks | storageKey('bookmarks') |
| Drawing sets | storageKey('drawings') |
| Info-dismiss | storageKey('info-dismissed', <content-hash>) |
The info-dismiss key embeds a hash of info.md's content as the suffix, so editing the file invalidates a prior "don't show again" and re-greets returning users.
This matters when several kit apps are served from one origin (e.g. multiple demos on one Pages site) — distinct appIds keep their saved state from colliding.