import { useCallback, useEffect, useState } from 'react' /** * Ключ localStorage для явного выбора темы пользователем. ДОЛЖЕН совпадать с * инлайн-скриптом в `frontend/index.html` (защита от FOUC — атрибут * `data-theme` проставляется на `` ДО первого рендера React). */ const STORAGE_KEY = 'vidconf-theme' type ExplicitTheme = 'light' | 'dark' function readStoredTheme(): ExplicitTheme | null { try { const value = localStorage.getItem(STORAGE_KEY) return value === 'light' || value === 'dark' ? value : null } catch { // localStorage недоступен (приватный режим/политики браузера) — считаем, // что явного выбора нет, тема определится системной prefers-color-scheme. return null } } function applyThemeAttribute(theme: ExplicitTheme | null) { if (theme) { document.documentElement.setAttribute('data-theme', theme) } else { document.documentElement.removeAttribute('data-theme') } } /** * Переключатель темы оболочки (см. design/DESIGN_SYSTEM.md §0.1, * design/mockups/dark/README.md «Переключатель темы»). * * Контракт: явный выбор пользователя (`data-theme="light"`/`"dark"` на * ``) сохраняется в localStorage и переживает перезагрузку; пока * выбора нет — действует системная тема (`prefers-color-scheme`), атрибут не * проставлен вообще. В макетах переключатель — сегментированный контрол из * двух явных кнопок «Светлая»/«Тёмная» (не циклический тумблер и без * третьего состояния «системная» в UI) — клик по любой из них фиксирует * выбор навсегда (до следующего клика по другой кнопке); вернуться к * автодетекту без явного выбора через этот контрол нельзя, ровно как в * утверждённых макетах. */ export function useTheme() { const [explicit, setExplicit] = useState(() => readStoredTheme()) const [systemPrefersDark, setSystemPrefersDark] = useState( () => window.matchMedia('(prefers-color-scheme: dark)').matches, ) // Живое отслеживание системной темы — чтобы подсветка активной кнопки // оставалась верной, если пользователь ещё не делал явного выбора и сменил // тему ОС, не покидая вкладку. useEffect(() => { const media = window.matchMedia('(prefers-color-scheme: dark)') const handleChange = (event: MediaQueryListEvent) => setSystemPrefersDark(event.matches) media.addEventListener('change', handleChange) return () => media.removeEventListener('change', handleChange) }, []) useEffect(() => { applyThemeAttribute(explicit) }, [explicit]) const setTheme = useCallback((theme: ExplicitTheme) => { try { localStorage.setItem(STORAGE_KEY, theme) } catch { // Сохранение недоступно — тема продержится до перезагрузки страницы. } setExplicit(theme) }, []) const resolved: ExplicitTheme = explicit ?? (systemPrefersDark ? 'dark' : 'light') return { resolved, setTheme } }