Files
opsxshell/src/hooks/useTheme.ts
T

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 }
}