export type ThemePreference = "system" | "light" | "dark"; export const THEME_STORAGE_KEY = "jyotisha-theme"; export function isThemePreference(value: unknown): value is ThemePreference { return value === "system" || value === "light" || value === "dark"; } /** * Runs synchronously in , before the first paint, so a pinned theme never * flashes the other one on load. It only ever writes `data-theme`; "system" is * the absence of the attribute, which is what the CSS media query expects. * Kept as one exported string so the boot script and the runtime below cannot * drift apart on the storage key. */ export const themePreferenceBootScript = `try{var t=localStorage.getItem("${THEME_STORAGE_KEY}");` + `if(t==="dark"||t==="light"){document.documentElement.dataset.theme=t}}catch(e){}`; export function readThemePreference(): ThemePreference { try { const stored = localStorage.getItem(THEME_STORAGE_KEY); return isThemePreference(stored) ? stored : "system"; } catch { // Private mode or blocked storage: fall back to following the OS. return "system"; } } const listeners = new Set<() => void>(); /** * `storage` only fires in *other* tabs, so a local change has to notify this one * explicitly. Subscribing to both keeps every open tab in step. */ export function subscribeThemePreference(onChange: () => void): () => void { listeners.add(onChange); window.addEventListener("storage", onChange); return () => { listeners.delete(onChange); window.removeEventListener("storage", onChange); }; } export function applyThemePreference(preference: ThemePreference): void { const root = document.documentElement; if (preference === "system") delete root.dataset.theme; else root.dataset.theme = preference; try { if (preference === "system") localStorage.removeItem(THEME_STORAGE_KEY); else localStorage.setItem(THEME_STORAGE_KEY, preference); } catch { // The choice still applies to this page; it just will not survive a reload. } for (const listener of listeners) listener(); }