/* * Theme controller. * * Three preferences: "system" (default), "light", "dark". The choice is * stored in localStorage and applied as `data-theme` on ; "system" * removes the attribute so the `prefers-color-scheme` block in tokens.css * takes over. A small inline snippet in each page's
applies the stored * value before first paint, so this module only has to own the control. */ export const THEME_STORAGE_KEY = "jw-theme"; const THEMES = ["system", "light", "dark"]; /* Must match --jw-bg in tokens.css for each theme. */ const BROWSER_CHROME = { light: "#fafafa", dark: "#0b100f" }; const darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); const listeners = new Set(); function readStored() { try { const stored = localStorage.getItem(THEME_STORAGE_KEY); return THEMES.includes(stored) ? stored : "system"; } catch { /* Private mode or blocked storage: fall back to the system preference. */ return "system"; } } /** The preference the user chose ("system" | "light" | "dark"). */ export function getPreference() { return readStored(); } /** The theme actually on screen ("light" | "dark"), resolving "system". */ export function getResolvedTheme(preference = readStored()) { if (preference !== "system") return preference; return darkQuery.matches ? "dark" : "light"; } export function applyTheme(preference) { const next = THEMES.includes(preference) ? preference : "system"; const root = document.documentElement; if (next === "system") root.removeAttribute("data-theme"); else root.setAttribute("data-theme", next); const meta = document.querySelector('meta[name="theme-color"]'); if (meta) meta.setAttribute("content", BROWSER_CHROME[getResolvedTheme(next)]); try { localStorage.setItem(THEME_STORAGE_KEY, next); } catch { /* Not fatal: the theme still applies for this page view. */ } for (const listener of listeners) listener(next, getResolvedTheme(next)); return next; } /** Subscribe to preference changes. Returns an unsubscribe function. */ export function onThemeChange(listener) { listeners.add(listener); return () => listeners.delete(listener); } /* Keep the browser chrome correct when the OS flips while set to "system". */ darkQuery.addEventListener("change", () => { if (readStored() === "system") applyTheme("system"); }); const ICONS = { system: '