'use client' import { useLayoutEffect } from 'react' import { persistThemePreference, normalizeThemeId } from '@/lib/apply-document-theme' /** * Empêche le flash clair en mode sombre. * * Au changement de route, le layout racine Next peut réécrire la classe de * depuis le serveur (souvent « light »). Sans garde, une frame claire * apparaît avant le prochain effet React. * * useLayoutEffect = avant le paint navigateur. * MutationObserver = remet le thème si React retire « dark » à tort. */ export function ThemeClassGuard() { useLayoutEffect(() => { let applying = false const preferredTheme = (): string => { try { const local = localStorage.getItem('theme-preference') if (local) return normalizeThemeId(local) } catch { /* localStorage indisponible */ } const server = document.documentElement.getAttribute('data-server-theme') || 'light' return normalizeThemeId(server) } const wantsDarkClass = (theme: string): boolean => { const t = normalizeThemeId(theme) if (t === 'dark' || t === 'midnight') return true if (t === 'auto') { return window.matchMedia('(prefers-color-scheme: dark)').matches } return false } const sync = () => { if (applying) return applying = true try { persistThemePreference(preferredTheme()) } finally { // Laisser le navigateur appliquer le DOM avant de réarmer queueMicrotask(() => { applying = false }) } } // Immédiat (avant paint) sync() // Après commit React éventuel qui a écrasé const raf1 = requestAnimationFrame(() => { sync() requestAnimationFrame(sync) }) const root = document.documentElement const observer = new MutationObserver(() => { if (applying) return const theme = preferredTheme() const needDark = wantsDarkClass(theme) const hasDark = root.classList.contains('dark') if (needDark && !hasDark) { sync() return } // data-theme retiré à tort pour une palette nommée if ( theme !== 'light' && theme !== 'dark' && theme !== 'auto' && root.getAttribute('data-theme') !== theme ) { sync() } }) observer.observe(root, { attributes: true, attributeFilter: ['class', 'data-theme'], }) const onStorage = (e: StorageEvent) => { if (e.key === 'theme-preference') sync() } window.addEventListener('storage', onStorage) return () => { cancelAnimationFrame(raf1) observer.disconnect() window.removeEventListener('storage', onStorage) } }, []) return null }