diff --git a/web/src/lib/stores/theme.svelte.ts b/web/src/lib/stores/theme.svelte.ts new file mode 100644 index 00000000..3bec8d5d --- /dev/null +++ b/web/src/lib/stores/theme.svelte.ts @@ -0,0 +1,62 @@ +const STORAGE_KEY = 'minstrel:theme'; + +export type ThemePreference = 'dark' | 'light' | 'system'; +export type ResolvedTheme = 'dark' | 'light'; + +function readPref(): ThemePreference { + if (typeof localStorage === 'undefined') return 'system'; + try { + const v = localStorage.getItem(STORAGE_KEY); + return v === 'dark' || v === 'light' || v === 'system' ? v : 'system'; + } catch { + return '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()); +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); + if (typeof localStorage !== 'undefined') { + try { localStorage.setItem(STORAGE_KEY, pref); } catch { /* ignore quota / unavailable */ } + } + 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); + } + }); +}