import * as storage from '#lib/util/safeLocalStorage.js'; const STORAGE_KEY = 'minstrel:theme'; export type ThemePreference = 'dark' | 'light' | 'system'; export type ResolvedTheme = 'dark' | 'light'; function readPref(): ThemePreference { const v = storage.read(STORAGE_KEY); return v === 'dark' || v === 'light' || v === 'system' ? v : 'system'; } function systemResolved(): ResolvedTheme { if (typeof window === 'undefined' || !window.matchMedia) return 'dark'; return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } function resolve(pref: ThemePreference): ResolvedTheme { return pref === 'system' ? systemResolved() : pref; } function applyToDOM(resolved: ResolvedTheme): void { if (typeof document === 'undefined') return; document.documentElement.setAttribute('data-theme', resolved); } let _theme = $state(readPref()); // svelte-ignore state_referenced_locally // Module-scope state with manual sync — _theme is read once at module // init to seed _resolved; subsequent updates flow through setTheme() // (which writes both) and the matchMedia listener (which writes // _resolved when prefers-color-scheme flips). Not a missed-reactivity // case; $derived doesn't apply at module scope here. let _resolved = $state(resolve(_theme)); export const theme = { get value(): ThemePreference { return _theme; } }; export const resolvedTheme = { get value(): ResolvedTheme { return _resolved; } }; export function setTheme(pref: ThemePreference): void { _theme = pref; _resolved = resolve(pref); storage.write(STORAGE_KEY, pref); applyToDOM(_resolved); } if (typeof window !== 'undefined' && window.matchMedia) { const mq = window.matchMedia('(prefers-color-scheme: light)'); mq.addEventListener('change', () => { if (_theme === 'system') { _resolved = systemResolved(); applyToDOM(_resolved); } }); }