docs: write down the second ground, and the rule it creates

PRD gets F15 and the fork that asked for it, DATA-MODEL the prefs field
and the fact that a script outside the bundle now reads that key, and
ARCHITECTURE the pre-paint script — the one piece of this that is neither
CSS nor React.

The AGENTS rule is the part worth reading twice: a component that names a
colour instead of a token now looks right in one theme and wrong in the
other, and nothing will tell you which.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-18 23:30:41 +02:00
co-authored by Claude Opus 5
parent eca5ad7a0a
commit c1cd1148d9
4 changed files with 70 additions and 3 deletions
+20 -1
View File
@@ -120,7 +120,8 @@ src/
useMarkSet.ts ignores (and the superseded completions shape)
useProgress.ts status, effort, note, daily override (F12)
useDailyLog.ts which game-days are ticked off
usePrefs.ts region, filters, focus, view, onboarding flags
usePrefs.ts region, filters, focus, view, theme, onboarding flags
theme.ts dark/light: resolving it, applying it, hues on paper
useCustom.ts the reader's own games and events (F13)
gameMeta.tsx lane id → name, label, hue; resolves custom lanes too
sort.ts deadline order, or what you're partway through
@@ -239,6 +240,24 @@ back to the last copy seen). Countdowns run off the device clock, so the app sta
network. Offline state is surfaced in the header and above the footer — stale data must never be
presented as current.
### The theme, before the bundle arrives
The page is drawn dark by default and light when the reader has asked for it (PRD F15). Which one
is decided by one attribute on `<html>`: `styles.css` holds the dark tokens on `:root` and
re-strikes them under `:root[data-theme="light"]`, so nothing in React knows a theme exists and no
component holds a colour of its own.
Setting that attribute is the one part React cannot do in time. It mounts after the bundle has
downloaded and parsed, which on a cold cache is long enough to show a reader who chose light a
dark page, on every single load. So a small inline script in `index.html` reads the same
`localStorage` prefs key the app does, sets the same attribute, and updates `<meta
name="theme-color">` — before first paint, and with a `try`/`catch` so storage being unavailable
costs the reader the default theme rather than the page.
That makes the theme's ground colour a fact written in three files that cannot import each other:
the stylesheet, `state/theme.ts` (which needs it for the meta tag), and the shell. `test/theme.test.ts`
pins the three together rather than trusting them to be edited at the same time.
### Shipping a new version to an open page
A cache-first shell is what makes the offline story work and what makes a deploy invisible: the