Skip to content

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. So save: 'primary' tracks whatever primary is set to — change primary and the save button follows.
  • 'primaryDarker' emits a color-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-size and --top-bar-button-size (+ a derived --top-bar-icon-size at 50%) — both switch to their mobileSize below the breakpoint, which is why this is re-called when isMobile flips.
  • --widget-button-cursor — from ui.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.

storageKey(kind, suffix?)    `mak:<appId>:<kind>[:<suffix>]`

<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.