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:
Antigravity
2026-07-16 20:43:18 +00:00
parent 704fed1191
commit 556a0b2f3f
77 changed files with 35745 additions and 10430 deletions

View File

@@ -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 nenlève JAMAIS `dark` avant de le remettre (sinon flash clair
* dune 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)
}