/** * Theme management with system preference detection * * Manages light/dark theme state with localStorage persistence * and automatic system preference detection. Themes are applied * via CSS class on the document element. * * Features: * - Persists user preference to localStorage * - Detects OS-level theme preference * - Responds to OS theme changes when in "system" mode * - Toggle between light/dark themes * - Reset to follow system preference * * @example * ```svelte * * * * ``` */ import { createPersistentStore } from '$shared/lib'; type Theme = 'light' | 'dark'; type ThemeSource = 'system' | 'user'; /** * Theme manager singleton * * Call init() on app mount and destroy() on app unmount. * Use isDark property to conditionally apply styles. */ class ThemeManager { // Private reactive state /** Current theme value ('light' or 'dark') */ #theme = $state('light'); /** Whether theme is controlled by user or follows system */ #source = $state('system'); /** MediaQueryList for detecting system theme changes */ #mediaQuery: MediaQueryList | null = null; /** Persistent storage for user's theme preference */ #store = createPersistentStore('glyphdiff:theme', null); /** Bound handler for system theme change events */ #systemChangeHandler = this.#onSystemChange.bind(this); constructor() { // Derive initial values from stored preference or OS const stored = this.#store.value; if (stored === 'dark' || stored === 'light') { this.#theme = stored; this.#source = 'user'; } else { this.#theme = this.#getSystemTheme(); this.#source = 'system'; } } /** Current theme value */ get value(): Theme { return this.#theme; } /** Source of current theme ('system' or 'user') */ get source(): ThemeSource { return this.#source; } /** Whether dark theme is active */ get isDark(): boolean { return this.#theme === 'dark'; } /** Whether theme is controlled by user (not following system) */ get isUserControlled(): boolean { return this.#source === 'user'; } /** * Initialize theme manager * * Applies current theme to DOM and sets up system preference listener. * Call once in root component onMount. */ init(): void { this.#applyToDom(this.#theme); this.#mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); this.#mediaQuery.addEventListener('change', this.#systemChangeHandler); } /** * Clean up theme manager * * Removes system preference listener. * Call in root component onDestroy. */ destroy(): void { this.#mediaQuery?.removeEventListener('change', this.#systemChangeHandler); this.#mediaQuery = null; } /** * Set theme explicitly * * Switches to user control and applies specified theme. * * @param theme - Theme to apply ('light' or 'dark') */ setTheme(theme: Theme): void { this.#source = 'user'; this.#theme = theme; this.#store.value = theme; this.#applyToDom(theme); } /** * Toggle between light and dark themes */ toggle(): void { this.setTheme(this.value === 'dark' ? 'light' : 'dark'); } /** * Reset to follow system preference * * Clears user preference and switches to system theme. */ resetToSystem(): void { this.#store.clear(); this.#theme = this.#getSystemTheme(); this.#source = 'system'; this.#applyToDom(this.#theme); } // Private helpers /** * Detect system theme preference * @returns 'dark' if system prefers dark mode, 'light' otherwise */ #getSystemTheme(): Theme { if (typeof window === 'undefined') { return 'light'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } /** * Apply theme to DOM * @param theme - Theme to apply */ #applyToDom(theme: Theme): void { document.documentElement.classList.toggle('dark', theme === 'dark'); } /** * Handle system theme change * Only updates if currently following system preference */ #onSystemChange(e: MediaQueryListEvent): void { if (this.#source === 'system') { this.#theme = e.matches ? 'dark' : 'light'; this.#applyToDom(this.#theme); } } } /** * Singleton theme manager instance * * Use throughout the app for consistent theme state. */ export const themeManager = new ThemeManager(); /** * ThemeManager class exported for testing purposes * Use the singleton `themeManager` in application code. */ export { ThemeManager };