feat: thème dark/light/system, taille police, page config, paste photo todo
ThemeContext - ThemeMode dark/light/system persisté en localStorage - fontScale 0.8–1.4 appliqué via CSS zoom sur <html> - Écoute prefers-color-scheme pour le mode système index.html - Script anti-flash : applique thème et zoom synchrone avant le premier rendu Layout - TopBar fixe 44px : bouton icône qui cycle dark→light→system→dark - Contenu décalé de 44px vers le bas ConfigPage (/config) - Sélecteur de thème (3 boutons avec icônes) - Slider taille de texte avec aperçu temps réel - Bouton Réinitialiser HomePage - Tuile Paramètres (fa-sliders) → /config TodoForm - Paste Ctrl+V pour ajouter une photo (même mécanique que CatalogueModal) - Indice visuel "ou Ctrl+V" v0.5.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||
|
||||
export type ThemeMode = 'dark' | 'light' | 'system'
|
||||
|
||||
interface ThemeCtx {
|
||||
theme: ThemeMode
|
||||
setTheme: (t: ThemeMode) => void
|
||||
resolved: 'dark' | 'light'
|
||||
fontScale: number
|
||||
setFontScale: (s: number) => void
|
||||
}
|
||||
|
||||
const Ctx = createContext<ThemeCtx>({
|
||||
theme: 'system', setTheme: () => {}, resolved: 'dark',
|
||||
fontScale: 1, setFontScale: () => {},
|
||||
})
|
||||
|
||||
function systemTheme(): 'dark' | 'light' {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
function resolveTheme(t: ThemeMode): 'dark' | 'light' {
|
||||
return t === 'system' ? systemTheme() : t
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<ThemeMode>(
|
||||
() => (localStorage.getItem('theme') as ThemeMode) ?? 'system'
|
||||
)
|
||||
const [resolved, setResolved] = useState<'dark' | 'light'>(
|
||||
() => resolveTheme((localStorage.getItem('theme') as ThemeMode) ?? 'system')
|
||||
)
|
||||
const [fontScale, setFontScaleState] = useState<number>(
|
||||
() => parseFloat(localStorage.getItem('fontScale') ?? '1')
|
||||
)
|
||||
|
||||
function setTheme(t: ThemeMode) {
|
||||
localStorage.setItem('theme', t)
|
||||
setThemeState(t)
|
||||
}
|
||||
|
||||
function setFontScale(s: number) {
|
||||
const clamped = Math.round(s * 100) / 100
|
||||
localStorage.setItem('fontScale', String(clamped))
|
||||
setFontScaleState(clamped)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const r = resolveTheme(theme)
|
||||
setResolved(r)
|
||||
document.documentElement.setAttribute('data-theme', r)
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
if (theme !== 'system') return
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
function onChange() {
|
||||
const r = systemTheme()
|
||||
setResolved(r)
|
||||
document.documentElement.setAttribute('data-theme', r)
|
||||
}
|
||||
mq.addEventListener('change', onChange)
|
||||
return () => mq.removeEventListener('change', onChange)
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.style.zoom = String(fontScale)
|
||||
}, [fontScale])
|
||||
|
||||
return <Ctx.Provider value={{ theme, setTheme, resolved, fontScale, setFontScale }}>{children}</Ctx.Provider>
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(Ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user