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:
2026-05-25 13:41:42 +02:00
parent 3dbd554eeb
commit bd0e06c5dd
8 changed files with 342 additions and 59 deletions
+75
View File
@@ -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)
}