96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
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 `<html>`.
|
|
* • Writes `--color-*` CSS custom properties on `<html>`.
|
|
*/
|
|
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 }
|
|
}
|