feat(web/m7-362): theme preference + resolved-theme rune store

This commit is contained in:
2026-05-03 13:43:28 -04:00
parent f0bfac2555
commit ffed19f736
+62
View File
@@ -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<ThemePreference>(readPref());
let _resolved = $state<ResolvedTheme>(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);
}
});
}