Files
vidconf/frontend/src/hooks/useTheme.ts
Max Ronzhin 8757bec8ac
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
first commit
2026-07-23 02:38:05 +03:00

78 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
/**
* Ключ localStorage для явного выбора темы пользователем. ДОЛЖЕН совпадать с
* инлайн-скриптом в `frontend/index.html` (защита от FOUC — атрибут
* `data-theme` проставляется на `<html>` ДО первого рендера 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"` на
* `<html>`) сохраняется в localStorage и переживает перезагрузку; пока
* выбора нет — действует системная тема (`prefers-color-scheme`), атрибут не
* проставлен вообще. В макетах переключатель — сегментированный контрол из
* двух явных кнопок «Светлая»/«Тёмная» (не циклический тумблер и без
* третьего состояния «системная» в UI) — клик по любой из них фиксирует
* выбор навсегда (до следующего клика по другой кнопке); вернуться к
* автодетекту без явного выбора через этот контрол нельзя, ровно как в
* утверждённых макетах.
*/
export function useTheme() {
const [explicit, setExplicit] = useState<ExplicitTheme | null>(() => 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 }
}