feat: let a stored preference decide the ground

`prefs.theme` is dark, light or system, defaulting to dark: a reader
whose laptop is in light mode has said something about their laptop, not
about this page, and following the device would move every existing
reader the first time they loaded this. No control yet — the value can
only arrive from storage.

App resolves it and writes it to the document. It is also where a hue
meets the theme: doing that in the lane resolver means every label, chip,
rail and bar gets the readable answer without a component knowing a theme
exists, which is why the colophon moves onto that resolver too.

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 448c70968d
commit 3ad74df5ea
3 changed files with 53 additions and 24 deletions
+28 -22
View File
@@ -1,4 +1,4 @@
import { gameMeta } from "../../shared/games.ts";
import { useGameMeta } from "../state/gameMeta.tsx";
import { freshness, type SourceHealth } from "../../shared/feed.ts";
import { formatAbsolute, formatRemaining } from "../../shared/time.ts";
@@ -128,7 +128,13 @@ export function Colophon({
sources: SourceHealth[];
now: number;
}) {
const games = [...new Set(sources.map((s) => s.game))].map(gameMeta);
// The tree's resolver rather than the module lookup: it is the one that
// answers for a reader's own games, and the one that reads a hue for the
// theme the page is in.
const gameMeta = useGameMeta();
const games = [...new Set(sources.map((s) => s.game))].map((id) =>
gameMeta(id),
);
const studios = [...new Set(games.map((g) => g.studio))];
const { refreshedAt, stale } = freshness(sources, now);
@@ -282,6 +288,26 @@ export function Colophon({
{"."}
</p>
{IDEA_CREDITS.length > 0 && (
<p className="mt-2">
Additional ideas and design from{" "}
{IDEA_CREDITS.map((c, i) => (
<span key={c.handle}>
{i > 0 && (i === IDEA_CREDITS.length - 1 ? " and " : ", ")}
<a
href={c.url}
target="_blank"
rel="noreferrer noopener"
className={LINK}
>
{c.handle}
</a>
</span>
))}
{"."}
</p>
)}
<p className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1.5">
<a
href={AUTHOR.github}
@@ -303,26 +329,6 @@ export function Colophon({
</a>
</p>
{IDEA_CREDITS.length > 0 && (
<p className="mt-2">
Additional ideas and design from{" "}
{IDEA_CREDITS.map((c, i) => (
<span key={c.handle}>
{i > 0 && (i === IDEA_CREDITS.length - 1 ? " and " : ", ")}
<a
href={c.url}
target="_blank"
rel="noreferrer noopener"
className={LINK}
>
{c.handle}
</a>
</span>
))}
{"."}
</p>
)}
{/*
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