import { useEffect, useMemo } from 'react' import { useSettingsStore } from '../stores/settingsStore' import { darkColors, lightColors, type AppThemeMode, type ColorMap } from '../themes/app-theme' /** * Resolves the *effective* theme ('dark' | 'light') from the user preference, * including the 'system' → prefers-color-scheme mapping. */ function resolveEffectiveTheme(preference: AppThemeMode): 'dark' | 'light' { if (preference === 'system') { if (typeof window === 'undefined') return 'dark' return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } return preference } /** * Apply a colour map to `document.documentElement` as CSS custom properties. */ function applyCSSVars(colors: ColorMap) { const root = document.documentElement for (const [key, value] of Object.entries(colors)) { root.style.setProperty(`--color-${key}`, value) } } /** * useTheme — keeps the DOM in sync with the user's theme preference. * * • Reads `settings.theme` from the settings store. * • Listens to `prefers-color-scheme` changes when set to 'system'. * • Writes `data-theme="dark" | "light"` on ``. * • Writes `--color-*` CSS custom properties on ``. */ export function useTheme() { const theme = useSettingsStore((s) => s.settings.theme) const effective = useMemo(() => resolveEffectiveTheme(theme), [theme]) // Apply theme to DOM whenever effective theme changes useEffect(() => { const colors = effective === 'dark' ? darkColors : lightColors // Set data attribute for CSS selectors document.documentElement.setAttribute('data-theme', effective) // Toggle Tailwind dark class if (effective === 'dark') { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } // Apply CSS custom properties applyCSSVars(colors) // Hairline aliases for legacy border-dark-* remaps in index.css const root = document.documentElement root.style.setProperty('--hairline-soft', colors['hairline-soft'] || colors['border-subtle'] || 'rgba(148,163,184,0.06)') root.style.setProperty('--hairline', colors['hairline'] || colors['border-default'] || 'rgba(148,163,184,0.10)') root.style.setProperty('--hairline-strong', colors['hairline-strong'] || colors['border-strong'] || 'rgba(148,163,184,0.16)') // Update body bg & colour from tokens (not hardcoded) document.body.style.backgroundColor = colors['surface-base'] || '#0f172a' document.body.style.color = colors['text-primary'] || '#e2e8f0' }, [effective]) // Listen for system colour-scheme changes when preference is 'system' useEffect(() => { if (theme !== 'system' || typeof window === 'undefined') return const mql = window.matchMedia('(prefers-color-scheme: dark)') const handler = (e: MediaQueryListEvent) => { const newEffective = e.matches ? 'dark' : 'light' const colors = e.matches ? darkColors : lightColors document.documentElement.setAttribute('data-theme', newEffective) if (newEffective === 'dark') { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } applyCSSVars(colors) document.body.style.backgroundColor = colors['surface-base'] || '#0f172a' document.body.style.color = colors['text-primary'] || '#e2e8f0' } mql.addEventListener('change', handler) return () => mql.removeEventListener('change', handler) }, [theme]) return { theme, effective } }