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" }, // A TRIAL outside B2 (Midnight's hue family, DESIGN.md decision log 2026-09-25): folded into // Midnight or removed once the owner has tried it; a stored "fjord" then lands on the default. { 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 in * E28) falls back to the default instead of painting an unstyled page. */ export function normalizeTheme(raw: Partial>): 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); }