Files
siftlode/frontend/src/lib/theme.ts
T

68 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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);
}