The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import { LS, readAccountMerged, writeAccount } from "./storage";
|
||
|
||
type Mode = "dark" | "light";
|
||
export type Scheme = "midnight" | "forest" | "slate" | "matrix" | "fjord";
|
||
|
||
// The scheme set (docs/DESIGN.md, decision B2): each earns its place with its own hue family, and its
|
||
// accent stays ≥ 30° of OKLCH hue away from danger / warn / success. theme.contract.test.ts measures
|
||
// every scheme's tokens in index.css against that rule and WCAG contrast.
|
||
export const SCHEMES: { id: Scheme; name: string; swatch: string }[] = [
|
||
{ id: "midnight", name: "Midnight", swatch: "#6d8cff" },
|
||
{ id: "forest", name: "Forest", swatch: "#22d3ee" },
|
||
{ id: "slate", name: "Slate", swatch: "#e879f9" },
|
||
{ id: "matrix", name: "Matrix", swatch: "#86a05a" },
|
||
// The one scheme outside B2's "own hue family": Midnight's navy with a calm steel accent, kept
|
||
// beside Midnight on purpose (DESIGN.md decision log, trial 2026-09-25, kept 2026-09-27).
|
||
{ id: "fjord", name: "Fjord", swatch: "#5aa1cb" },
|
||
];
|
||
|
||
export interface ThemePrefs {
|
||
mode: Mode;
|
||
scheme: Scheme;
|
||
fontScale: number;
|
||
/** Show the per-scheme background image (dark theme only); off = flat colour. */
|
||
bgImage: boolean;
|
||
/** How strongly the background image is faded out, 0–100% (higher = fainter). */
|
||
bgFade: number;
|
||
}
|
||
|
||
export const DEFAULT_THEME: ThemePrefs = {
|
||
mode: "dark",
|
||
scheme: "midnight",
|
||
fontScale: 1.06,
|
||
bgImage: true,
|
||
bgFade: 60,
|
||
};
|
||
|
||
/** A stored theme (this browser's copy or the account's server prefs) as the app can apply it: only
|
||
* the known keys, and a scheme or mode that no longer exists (YouTube and Starship were
|
||
* retired) falls back to the default instead of painting an unstyled page. */
|
||
export function normalizeTheme(raw: Partial<Record<keyof ThemePrefs, unknown>>): ThemePrefs {
|
||
const d = DEFAULT_THEME;
|
||
const num = (v: unknown, fallback: number) =>
|
||
typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
||
return {
|
||
mode: raw.mode === "light" || raw.mode === "dark" ? raw.mode : d.mode,
|
||
scheme: SCHEMES.some((s) => s.id === raw.scheme) ? (raw.scheme as Scheme) : d.scheme,
|
||
fontScale: num(raw.fontScale, d.fontScale),
|
||
bgImage: typeof raw.bgImage === "boolean" ? raw.bgImage : d.bgImage,
|
||
bgFade: num(raw.bgFade, d.bgFade),
|
||
};
|
||
}
|
||
|
||
export function applyTheme(t: ThemePrefs): void {
|
||
const el = document.documentElement;
|
||
el.dataset.theme = t.mode;
|
||
el.dataset.scheme = t.scheme;
|
||
el.style.setProperty("--font-scale", String(t.fontScale));
|
||
el.style.setProperty("--bg-fade", `${t.bgFade}%`);
|
||
}
|
||
|
||
export function loadLocalTheme(): ThemePrefs {
|
||
return normalizeTheme(readAccountMerged(LS.theme, DEFAULT_THEME));
|
||
}
|
||
|
||
export function saveLocalTheme(t: ThemePrefs): void {
|
||
writeAccount(LS.theme, t);
|
||
}
|