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:
co-authored by
Claude Opus 5
parent
448c70968d
commit
3ad74df5ea
+13
-2
@@ -31,6 +31,7 @@ import {
|
|||||||
import { clockFor, formatRemaining } from "../shared/time.ts";
|
import { clockFor, formatRemaining } from "../shared/time.ts";
|
||||||
import { dailySummary, isDaily, resolveDaily } from "../shared/daily.ts";
|
import { dailySummary, isDaily, resolveDaily } from "../shared/daily.ts";
|
||||||
import { GameMetaProvider, type MetaResolver } from "./state/gameMeta.tsx";
|
import { GameMetaProvider, type MetaResolver } from "./state/gameMeta.tsx";
|
||||||
|
import { metaOnTheme, useTheme } from "./state/theme.ts";
|
||||||
import {
|
import {
|
||||||
isCustomGameId,
|
isCustomGameId,
|
||||||
type CustomEvents,
|
type CustomEvents,
|
||||||
@@ -108,16 +109,26 @@ export function App() {
|
|||||||
const prog = useProgress();
|
const prog = useProgress();
|
||||||
const daily = useDailyLog();
|
const daily = useDailyLog();
|
||||||
const custom = useCustom();
|
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.
|
* 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,
|
* 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
|
* and hands it down rather than letting components import a lookup that can
|
||||||
* only ever answer for the tracked ones.
|
* 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<MetaResolver>(
|
const gameMeta = useMemo<MetaResolver>(
|
||||||
() => (id) => metaFor(id, custom.games),
|
() => (id) => metaOnTheme(metaFor(id, custom.games), theme),
|
||||||
[custom.games],
|
[custom.games, theme],
|
||||||
);
|
);
|
||||||
// "Completed" is now one status among several; the rest of the UI still asks
|
// "Completed" is now one status among several; the rest of the UI still asks
|
||||||
// this question a lot, so keep a cheap shorthand.
|
// this question a lot, so keep a cheap shorthand.
|
||||||
|
|||||||
@@ -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 { freshness, type SourceHealth } from "../../shared/feed.ts";
|
||||||
import { formatAbsolute, formatRemaining } from "../../shared/time.ts";
|
import { formatAbsolute, formatRemaining } from "../../shared/time.ts";
|
||||||
|
|
||||||
@@ -128,7 +128,13 @@ export function Colophon({
|
|||||||
sources: SourceHealth[];
|
sources: SourceHealth[];
|
||||||
now: number;
|
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 studios = [...new Set(games.map((g) => g.studio))];
|
||||||
const { refreshedAt, stale } = freshness(sources, now);
|
const { refreshedAt, stale } = freshness(sources, now);
|
||||||
|
|
||||||
@@ -282,6 +288,26 @@ export function Colophon({
|
|||||||
{"."}
|
{"."}
|
||||||
</p>
|
</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">
|
<p className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1.5">
|
||||||
<a
|
<a
|
||||||
href={AUTHOR.github}
|
href={AUTHOR.github}
|
||||||
@@ -303,26 +329,6 @@ export function Colophon({
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</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
|
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
|
paragraph is where a reader who has just found one is looking. The bug
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { guessRegion } from "../../shared/time.ts";
|
|||||||
import type { SortMode } from "./sort.ts";
|
import type { SortMode } from "./sort.ts";
|
||||||
import { KEYS, readJson, writeJson } from "./storage.ts";
|
import { KEYS, readJson, writeJson } from "./storage.ts";
|
||||||
import type { TimelineGroup } from "./lanes.ts";
|
import type { TimelineGroup } from "./lanes.ts";
|
||||||
|
import { DEFAULT_THEME_CHOICE, type ThemeChoice } from "./theme.ts";
|
||||||
import { DEFAULT_DAY_WIDTH } from "./zoom.ts";
|
import { DEFAULT_DAY_WIDTH } from "./zoom.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,6 +99,16 @@ export interface Prefs {
|
|||||||
showCompleted: boolean;
|
showCompleted: boolean;
|
||||||
/** Reveal events the reader has ignored, so they can be restored. */
|
/** Reveal events the reader has ignored, so they can be restored. */
|
||||||
showIgnored: boolean;
|
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. */
|
/** False until the reader confirms or changes the guessed region. */
|
||||||
regionConfirmed: boolean;
|
regionConfirmed: boolean;
|
||||||
/** False until the reader has picked their games on first run. */
|
/** False until the reader has picked their games on first run. */
|
||||||
@@ -116,6 +127,7 @@ function defaults(): Prefs {
|
|||||||
detectDaily: false,
|
detectDaily: false,
|
||||||
showCompleted: true,
|
showCompleted: true,
|
||||||
showIgnored: false,
|
showIgnored: false,
|
||||||
|
theme: DEFAULT_THEME_CHOICE,
|
||||||
regionConfirmed: false,
|
regionConfirmed: false,
|
||||||
onboarded: false,
|
onboarded: false,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user