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.
This commit is contained in:
@@ -43,41 +43,62 @@ export function normalizeThemeId(raw: string | null | undefined): ThemeId {
|
||||
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
|
||||
root.classList.remove('dark')
|
||||
root.removeAttribute('data-theme')
|
||||
|
||||
if (t === 'auto') {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
root.classList.add('dark')
|
||||
}
|
||||
return
|
||||
}
|
||||
const wantsDark =
|
||||
t === 'dark' ||
|
||||
t === 'midnight' ||
|
||||
(t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
|
||||
if (t === 'dark') {
|
||||
// Classe dark : bascule atomique (pas de remove puis add)
|
||||
if (wantsDark) {
|
||||
root.classList.add('dark')
|
||||
return
|
||||
} else {
|
||||
root.classList.remove('dark')
|
||||
}
|
||||
|
||||
if (t === 'light') {
|
||||
return
|
||||
}
|
||||
|
||||
if ((NAMED_PALETTE_IDS as readonly string[]).includes(t)) {
|
||||
// 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)
|
||||
if (t === 'midnight') {
|
||||
root.classList.add('dark')
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user