// Theme management — side-effect-free at import time.
// Call bootstrapTheme() once from main.tsx before render.
export type ThemePreference = 'light' | 'dark' | 'system'
export function getThemePreference(): ThemePreference {
const stored = localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark') return stored
return 'system'
}
const MEDIA_QUERY = '(prefers-color-scheme: dark)'
function getOSDark(): boolean {
return window.matchMedia(MEDIA_QUERY).matches
}
function applyTheme(theme: 'light' | 'dark'): void {
document.documentElement.dataset['theme'] = theme
}
// Listener reference so the media-query change handler can check localStorage
// at call time (not at setup time).
function handleOSChange(e: MediaQueryListEvent): void {
if (localStorage.getItem('theme') !== null) return
applyTheme(e.matches ? 'dark' : 'light')
}
/**
* bootstrapTheme — reads stored preference (or falls back to OS),
* applies data-theme, and registers a media-query change listener.
* Call exactly once from main.tsx.
*/
export function bootstrapTheme(): void {
const stored = localStorage.getItem('theme')
const isDark = stored !== null ? stored === 'dark' : getOSDark()
applyTheme(isDark ? 'dark' : 'light')
window.matchMedia(MEDIA_QUERY).addEventListener('change', handleOSChange)
}
/**
* setTheme — explicit override.
* null: clears override, re-syncs with current OS preference.
* 'light' | 'dark': stores override, applies immediately.
*/
export function setTheme(theme: 'light' | 'dark' | null): void {
if (theme === null) {
localStorage.removeItem('theme')
applyTheme(getOSDark() ? 'dark' : 'light')
} else {
localStorage.setItem('theme', theme)
applyTheme(theme)
}
}