diff --git a/src/client/App.tsx b/src/client/App.tsx
index 5082ff2..d2f6dfc 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -31,6 +31,7 @@ import {
import { clockFor, formatRemaining } from "../shared/time.ts";
import { dailySummary, isDaily, resolveDaily } from "../shared/daily.ts";
import { GameMetaProvider, type MetaResolver } from "./state/gameMeta.tsx";
+import { metaOnTheme, useTheme } from "./state/theme.ts";
import {
isCustomGameId,
type CustomEvents,
@@ -108,16 +109,26 @@ export function App() {
const prog = useProgress();
const daily = useDailyLog();
const custom = useCustom();
+ // Colour only: which ground the page is drawn on, written to the document by
+ // the hook. Nothing else in the app asks what it is — the tokens in
+ // styles.css answer for every component — except the hues below.
+ const theme = useTheme(prefs.theme);
/**
* How every lane in this tree is named and coloured.
*
* App owns it because App is the only thing holding the reader's own games,
* and hands it down rather than letting components import a lookup that can
* only ever answer for the tracked ones.
+ *
+ * It is also where a hue meets the theme. A hue is data — ours in `games.ts`,
+ * theirs in their browser — and all of it was picked against the dark ground,
+ * so on paper the bright ones need darkening to stay readable. Doing it here
+ * means every lane label, chip, rail and bar in the tree gets the adjusted
+ * answer without a single component knowing a theme exists.
*/
const gameMeta = useMemo
+ Additional ideas and design from{" "} + {IDEA_CREDITS.map((c, i) => ( + + {i > 0 && (i === IDEA_CREDITS.length - 1 ? " and " : ", ")} + + {c.handle} + + + ))} + {"."} +
+ )} + - {IDEA_CREDITS.length > 0 && ( -- Additional ideas and design from{" "} - {IDEA_CREDITS.map((c, i) => ( - - {i > 0 && (i === IDEA_CREDITS.length - 1 ? " and " : ", ")} - - {c.handle} - - - ))} - {"."} -
- )} - {/* Placed under the disclaimer that admits dates can be wrong, because that paragraph is where a reader who has just found one is looking. The bug diff --git a/src/client/state/usePrefs.ts b/src/client/state/usePrefs.ts index 0cd94e7..58eb8cd 100644 --- a/src/client/state/usePrefs.ts +++ b/src/client/state/usePrefs.ts @@ -5,6 +5,7 @@ import { guessRegion } from "../../shared/time.ts"; import type { SortMode } from "./sort.ts"; import { KEYS, readJson, writeJson } from "./storage.ts"; import type { TimelineGroup } from "./lanes.ts"; +import { DEFAULT_THEME_CHOICE, type ThemeChoice } from "./theme.ts"; import { DEFAULT_DAY_WIDTH } from "./zoom.ts"; /** @@ -98,6 +99,16 @@ export interface Prefs { showCompleted: boolean; /** Reveal events the reader has ignored, so they can be restored. */ showIgnored: boolean; + /** + * Which ground the app is drawn on: `dark`, `light`, or `system` to follow + * the device. + * + * Dark is the default and not a placeholder — see `DEFAULT_THEME_CHOICE`. The + * value only decides colour: nothing about what is shown, sorted, counted or + * stored changes with it, which is why it can be flipped mid-read with + * nothing to save. + */ + theme: ThemeChoice; /** False until the reader confirms or changes the guessed region. */ regionConfirmed: boolean; /** False until the reader has picked their games on first run. */ @@ -116,6 +127,7 @@ function defaults(): Prefs { detectDaily: false, showCompleted: true, showIgnored: false, + theme: DEFAULT_THEME_CHOICE, regionConfirmed: false, onboarded: false, };