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:
co-authored by
Claude Opus 5
parent
eca5ad7a0a
commit
c1cd1148d9
@@ -115,9 +115,10 @@ src/client/ React app, service worker, manifest
|
|||||||
lens.ts — who sees which rows (focus, outstanding, next-to-expire); pure
|
lens.ts — who sees which rows (focus, outstanding, next-to-expire); pure
|
||||||
zoom.ts — the timeline's scale ladder; pure
|
zoom.ts — the timeline's scale ladder; pure
|
||||||
lanes.ts — how the timeline stacks: a lane per game, or one deadline queue; pure
|
lanes.ts — how the timeline stacks: a lane per game, or one deadline queue; pure
|
||||||
|
theme.ts — dark or light, and what a game hue reads as on each
|
||||||
scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
|
scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
|
||||||
serve.ts static server + /api/health
|
serve.ts static server + /api/health
|
||||||
test/ 607 tests
|
test/ 620 tests
|
||||||
fixtures/<game>/ raw HTML + .expected.json per source — pinned, kept forever
|
fixtures/<game>/ raw HTML + .expected.json per source — pinned, kept forever
|
||||||
snapshots/ current page per source, rewritten by refresh — see its README
|
snapshots/ current page per source, rewritten by refresh — see its README
|
||||||
```
|
```
|
||||||
@@ -510,6 +511,18 @@ to an open page). Four things hold it up:
|
|||||||
- Keep old fixtures when a source changes shape — the old one is the regression test proving the
|
- Keep old fixtures when a source changes shape — the old one is the regression test proving the
|
||||||
parser still handles the previous format. Fixtures are pinned and permanent; `snapshots/` is the
|
parser still handles the previous format. Fixtures are pinned and permanent; `snapshots/` is the
|
||||||
current page and gets overwritten. Do not conflate them.
|
current page and gets overwritten. Do not conflate them.
|
||||||
|
- **Colour is a token, and every token has two answers.** The app ships dark and offers light
|
||||||
|
(PRD F15), and the whole difference is a set of custom properties re-struck under
|
||||||
|
`:root[data-theme="light"]` in `styles.css`. So a component names `ink`, `hairline` or `soon` and
|
||||||
|
never a colour: a literal — `bg-white`, a hex, a hardcoded scrim — is a component that looks
|
||||||
|
right in one theme and wrong in the other, and nothing will tell you which. The two things that
|
||||||
|
genuinely cannot be tokens are the game hues, which are *data* (`games.ts`, and the reader's own),
|
||||||
|
and the pre-paint script in `index.html`; both are handled in `src/client/state/theme.ts` and
|
||||||
|
pinned by `test/theme.test.ts`.
|
||||||
|
Two rules follow. **The heat ramp must stay readable on whichever ground it is on** — it carries
|
||||||
|
meaning, and the dark theme's amber is 1.9:1 on paper, so each step is re-struck rather than
|
||||||
|
reused. And **the dark theme does not move**: `readableHue` returns dark untouched by
|
||||||
|
construction, because adding a theme is not a licence to redraw the one that shipped.
|
||||||
- **The page is two columns past `lg`, and the split is the one below.** What the page *tells* the
|
- **The page is two columns past `lg`, and the split is the one below.** What the page *tells* the
|
||||||
reader to do — the next deadlines, tonight's dailies — pins to a rail on the left and stays put
|
reader to do — the next deadlines, tonight's dailies — pins to a rail on the left and stays put
|
||||||
while the lists it *shows* them scroll beside it. Below that breakpoint it is one column in the
|
while the lists it *shows* them scroll beside it. Below that breakpoint it is one column in the
|
||||||
|
|||||||
+20
-1
@@ -120,7 +120,8 @@ src/
|
|||||||
useMarkSet.ts ignores (and the superseded completions shape)
|
useMarkSet.ts ignores (and the superseded completions shape)
|
||||||
useProgress.ts status, effort, note, daily override (F12)
|
useProgress.ts status, effort, note, daily override (F12)
|
||||||
useDailyLog.ts which game-days are ticked off
|
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)
|
useCustom.ts the reader's own games and events (F13)
|
||||||
gameMeta.tsx lane id → name, label, hue; resolves custom lanes too
|
gameMeta.tsx lane id → name, label, hue; resolves custom lanes too
|
||||||
sort.ts deadline order, or what you're partway through
|
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
|
network. Offline state is surfaced in the header and above the footer — stale data must never be
|
||||||
presented as current.
|
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
|
### 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
|
A cache-first shell is what makes the offline story work and what makes a deploy invisible: the
|
||||||
|
|||||||
+5
-1
@@ -219,7 +219,7 @@ Namespaced, versioned, and small. Nothing here ever goes to the server.
|
|||||||
"gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this"
|
"gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this"
|
||||||
"gacha-tracker:v1:prefs" // { region, hiddenGames[], knownGames[]?, focusGame, sort, view,
|
"gacha-tracker:v1:prefs" // { region, hiddenGames[], knownGames[]?, focusGame, sort, view,
|
||||||
// timelineDayWidth, timelineGroup, detectDaily, showCompleted,
|
// timelineDayWidth, timelineGroup, detectDaily, showCompleted,
|
||||||
// showIgnored, regionConfirmed, onboarded }
|
// showIgnored, theme, regionConfirmed, onboarded }
|
||||||
// timelineDayWidth is px per day on the board, stored as the
|
// timelineDayWidth is px per day on the board, stored as the
|
||||||
// measurement rather than a step number and read through
|
// measurement rather than a step number and read through
|
||||||
// snapDayWidth — so a value from an older ladder still opens
|
// snapDayWidth — so a value from an older ladder still opens
|
||||||
@@ -230,6 +230,10 @@ Namespaced, versioned, and small. Nothing here ever goes to the server.
|
|||||||
// knownGames is every lane the reader has been offered. Absent
|
// knownGames is every lane the reader has been offered. Absent
|
||||||
// means unrecorded, not "offered nothing" — see PRD F8; a lane
|
// means unrecorded, not "offered nothing" — see PRD F8; a lane
|
||||||
// missing from it is new to them and arrives switched off.
|
// missing from it is new to them and arrives switched off.
|
||||||
|
// theme is "dark" | "light" | "system", defaulting to dark — see
|
||||||
|
// PRD F15. It is read by the app *and* by a pre-paint script in
|
||||||
|
// index.html, which is the only thing outside the client bundle
|
||||||
|
// that touches a key in this space.
|
||||||
"gacha-tracker:v1:completions" // SUPERSEDED — read once to migrate, never written
|
"gacha-tracker:v1:completions" // SUPERSEDED — read once to migrate, never written
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+31
@@ -284,6 +284,37 @@ The footer shows when the feed was last updated, per game. If a game's data is m
|
|||||||
stale, its lane carries a warning badge. Never present stale data as current — the whole value
|
stale, its lane carries a warning badge. Never present stale data as current — the whole value
|
||||||
proposition is trust in the dates.
|
proposition is trust in the dates.
|
||||||
|
|
||||||
|
**F15 — Light mode, with dark still the default.**
|
||||||
|
The app is a lit instrument panel and that is what it should be on first sight, but it is also read
|
||||||
|
on a train in daylight and by people who find a dark UI harder rather than moodier. A public fork
|
||||||
|
worked a theme toggle out first and is credited for the idea in the colophon. So the ground is
|
||||||
|
the reader's choice: **Dark**, **Light**, or **System** to follow the device, in settings next to the
|
||||||
|
region — both are "how do I read this?" and neither changes what the page knows.
|
||||||
|
|
||||||
|
Dark is what it ships as, and deliberately not `System`. A reader whose laptop is in light mode has
|
||||||
|
said something about their laptop, not about this page; defaulting to the device would also move
|
||||||
|
every existing reader the first time they loaded a build that had this, which is the `knownGames`
|
||||||
|
mistake in a different costume (F8). Choosing `System` is one tap, and from then on it *is* their
|
||||||
|
answer.
|
||||||
|
|
||||||
|
Three things the light theme has to get right, because they are what a second palette usually gets
|
||||||
|
wrong:
|
||||||
|
|
||||||
|
- **It is a re-strike, not an inversion.** Every colour in the UI is a token, and light redefines the
|
||||||
|
tokens rather than adding a second set of rules to components. Nothing in the app asks which theme
|
||||||
|
it is in, so a new component cannot forget to support one.
|
||||||
|
- **Urgency still reads.** The heat ramp carries meaning, and the dark theme's amber is 1.9:1 on
|
||||||
|
paper — an urgency the reader cannot read is not urgency. Every step is re-struck to clear 4.5:1
|
||||||
|
against the light ground, keeping its order and its meaning.
|
||||||
|
- **A game keeps its colour.** The hues are identity (F1) and were all picked against a near-black
|
||||||
|
ground; on paper the bright ones vanish. They are darkened until they read, along the same hue, so
|
||||||
|
Wuthering Waves is still the green one — including the hues a reader picked for a game they
|
||||||
|
invented (F13).
|
||||||
|
|
||||||
|
Switching is instant, costs nothing and saves nothing: no reload, and nothing marked, typed or
|
||||||
|
ticked is touched. And it survives the load it is chosen on — the shell sets the theme before first
|
||||||
|
paint, so a reader on light is never shown a dark page while the bundle downloads.
|
||||||
|
|
||||||
## Out of scope for v1
|
## Out of scope for v1
|
||||||
|
|
||||||
Accounts and sync; push notifications; in-game resource or pull tracking; native mobile apps (the
|
Accounts and sync; push notifications; in-game resource or pull tracking; native mobile apps (the
|
||||||
|
|||||||
Reference in New Issue
Block a user