Files
Momento/memento-note/components/theme-class-guard.tsx
Antigravity 556a0b2f3f feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace
les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux
legacy, usage multi-features, hints de coût, anti-flash thème et hydratation
admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
2026-07-16 20:43:18 +00:00

103 lines
2.7 KiB
TypeScript

'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
* <html> 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é <html>
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
}