Anti-flash dark (color-scheme + applyDocumentTheme), scroll pages publiques, prompts wizard/slides moins génériques, cron agents rattrapage nextRun null, slides carnet via notebookId, graphiques à 2 crédits + hint, audit dashboard, packs Stripe marqués non configurés sans price ID.
111 lines
3.0 KiB
TypeScript
111 lines
3.0 KiB
TypeScript
/**
|
||
* Valeurs persistées pour User.theme / localStorage `theme-preference`.
|
||
* Une seule source de vérité pour le DOM : évite les écarts entre header, settings et hydratation.
|
||
*/
|
||
const THEME_IDS = [
|
||
'light',
|
||
'dark',
|
||
'auto',
|
||
'sepia',
|
||
'midnight',
|
||
'rose',
|
||
'green',
|
||
'lavender',
|
||
'sand',
|
||
'ocean',
|
||
'sunset',
|
||
'blue',
|
||
] as const
|
||
|
||
export type ThemeId = (typeof THEME_IDS)[number]
|
||
|
||
const NAMED_PALETTE_IDS: ThemeId[] = [
|
||
'sepia',
|
||
'midnight',
|
||
'rose',
|
||
'green',
|
||
'lavender',
|
||
'sand',
|
||
'ocean',
|
||
'sunset',
|
||
'blue',
|
||
]
|
||
|
||
export function isThemeId(value: string | null | undefined): value is ThemeId {
|
||
return value !== undefined && value !== null && (THEME_IDS as readonly string[]).includes(value)
|
||
}
|
||
|
||
/** Corrige les anciennes valeurs (ex. formulaire « slate ») vers un thème supporté. */
|
||
export function normalizeThemeId(raw: string | null | undefined): ThemeId {
|
||
if (!raw) return 'light'
|
||
if (raw === 'slate') return 'light'
|
||
if (isThemeId(raw)) return raw
|
||
return 'light'
|
||
}
|
||
|
||
/** Cookie lu par le layout serveur pour éviter le flash clair au changement de route. */
|
||
export const THEME_COOKIE_NAME = 'memento-theme'
|
||
|
||
/**
|
||
* Applique le thème sur `<html>` : `class="dark"` et/ou `data-theme`.
|
||
* - `light` → papier par défaut (`:root`), pas de `data-theme`
|
||
* - `dark` → sombre global (`.dark`)
|
||
* - `auto` → `.dark` si prefers-color-scheme: dark
|
||
* - palettes nommées → `data-theme="<id>"` ; `midnight` force aussi `.dark` (variante sombre du thème)
|
||
*
|
||
* Important : on n’enlève JAMAIS `dark` avant de le remettre (sinon flash clair
|
||
* d’une frame à chaque navigation / re-render de ThemeInitializer).
|
||
*/
|
||
export function applyDocumentTheme(theme: string): void {
|
||
if (typeof document === 'undefined') return
|
||
|
||
const t = normalizeThemeId(theme)
|
||
const root = document.documentElement
|
||
|
||
const wantsDark =
|
||
t === 'dark' ||
|
||
t === 'midnight' ||
|
||
(t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||
|
||
// Classe dark : bascule atomique (pas de remove puis add)
|
||
if (wantsDark) {
|
||
root.classList.add('dark')
|
||
} else {
|
||
root.classList.remove('dark')
|
||
}
|
||
|
||
try {
|
||
root.style.colorScheme = wantsDark ? 'dark' : 'light'
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
// data-theme pour les palettes nommées uniquement
|
||
if (t === 'auto' || t === 'dark' || t === 'light') {
|
||
root.removeAttribute('data-theme')
|
||
} else if ((NAMED_PALETTE_IDS as readonly string[]).includes(t)) {
|
||
root.setAttribute('data-theme', t)
|
||
} else {
|
||
root.removeAttribute('data-theme')
|
||
}
|
||
|
||
root.dataset.appliedTheme = t
|
||
}
|
||
|
||
/** Persiste le thème (localStorage + cookie) pour que le serveur connaisse le dark. */
|
||
export function persistThemePreference(theme: string): void {
|
||
const t = normalizeThemeId(theme)
|
||
try {
|
||
localStorage.setItem('theme-preference', t)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
// 1 an — lu par le layout racine via cookies()
|
||
document.cookie = `${THEME_COOKIE_NAME}=${encodeURIComponent(t)}; path=/; max-age=31536000; SameSite=Lax`
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
applyDocumentTheme(t)
|
||
}
|