import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' /** * Theme: 'light' (Earth) | 'dark' (Claude-warm) | 'system' (follow prefers-color-scheme). * *

Persist'ится в {@code localStorage[STORAGE_KEY]}. На mount читает saved * preference или fallback'ает к 'system'. Меняет {@code data-theme} attr на * {@code } → CSS custom properties в styles.css переключают палитру. * *

{@code system} mode — если user ничего не выбрал, следим за prefers-color-scheme * media query. matchMedia listener обновляет attr реально-time при OS theme change. */ const STORAGE_KEY = 'ord-theme' export type ThemePreference = 'light' | 'dark' | 'system' export type ResolvedTheme = 'light' | 'dark' type ThemeContextValue = { preference: ThemePreference resolved: ResolvedTheme setPreference: (next: ThemePreference) => void } const ThemeContext = createContext(null) function readSystemPreference(): ResolvedTheme { if (typeof window === 'undefined') return 'light' return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } function readSavedPreference(): ThemePreference { if (typeof window === 'undefined') return 'system' const raw = window.localStorage.getItem(STORAGE_KEY) if (raw === 'light' || raw === 'dark' || raw === 'system') return raw return 'system' } function applyToDom(theme: ResolvedTheme) { if (typeof document === 'undefined') return document.documentElement.setAttribute('data-theme', theme) // shadcn convention — добавляем .dark class тоже на случай third-party // компонентов которые ждут именно его. Custom variant в styles.css всё равно // их связывает, но эта мера держит совместимость на корню. if (theme === 'dark') { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } // чтобы system browser UI (scrollbars, autofill) // был в гармонии с темой. let meta = document.querySelector('meta[name="color-scheme"]') if (!meta) { meta = document.createElement('meta') meta.name = 'color-scheme' document.head.appendChild(meta) } meta.content = theme } export function ThemeProvider({ children }: { children: ReactNode }) { const [preference, setPreferenceState] = useState(() => readSavedPreference()) const [systemResolved, setSystemResolved] = useState(() => readSystemPreference()) // Слушаем OS theme change при mode='system'. useEffect(() => { if (typeof window === 'undefined') return const mq = window.matchMedia('(prefers-color-scheme: dark)') const handler = () => setSystemResolved(mq.matches ? 'dark' : 'light') mq.addEventListener('change', handler) return () => mq.removeEventListener('change', handler) }, []) const resolved: ResolvedTheme = preference === 'system' ? systemResolved : preference // Применяем к DOM каждый раз когда resolved меняется. useEffect(() => { applyToDom(resolved) }, [resolved]) const setPreference = (next: ThemePreference) => { setPreferenceState(next) if (typeof window !== 'undefined') { window.localStorage.setItem(STORAGE_KEY, next) } } return ( {children} ) } export function useTheme() { const ctx = useContext(ThemeContext) if (!ctx) { throw new Error('useTheme must be used inside ') } return ctx }