import { Alpine } from "./alpine"; // Theme management functionality type ThemeType = | "tokyo-night" | "dracula" | "nord" | "solarized-dark" | "monokai" | "one-dark-pro" | "material-dark" | "catppuccin-mocha" | "catppuccin-macchiato" | "catppuccin-frappe" // Omarchy dark themes | "vantablack" | "last-horizon" | "ethereal" | "hackerman" | "matte-black" | "solitude" | "retro-82" | "osaka-jade" | "kanagawa" | "miasma" | "lumon" | "ristretto" | "gruvbox" | "everforest" // Light themes | "tokyo-night-day" | "solarized-light" | "one-light" | "ayu-light" | "github-light" | "gruvbox-light" | "everforest-light" | "kanagawa-lotus" | "catppuccin-latte" | "rose-pine" | "lupine" | "flexoki-light" | "white"; const DEFAULT_THEME: ThemeType = "tokyo-night"; const THEME_STORAGE_KEY = "theme"; const TOKEN_STORAGE_KEY = "token"; // Apply theme to document body. Replaces only `theme-*` classes and keeps // the class in sync on as well (ThemeFlashGuard may have put one // there on public pages), so unrelated classes like `min-h-screen` on the // auth pages survive a theme change. const applyTheme = (theme: string): void => { const setThemeClass = (el: Element): void => { Array.from(el.classList).forEach((c) => { if (c.startsWith("theme-")) el.classList.remove(c); }); el.classList.add(`theme-${theme}`); }; setThemeClass(document.documentElement); setThemeClass(document.body); document.body.style.background = ""; document.body.style.backgroundSize = ""; document.body.style.backgroundAttachment = ""; localStorage.setItem(THEME_STORAGE_KEY, theme); }; // Load theme from localStorage or use default. // // Do NOT blindly applyTheme() here: signed-in pages server-render the // user's theme into the body class, and re-applying the localStorage // value caused the admin/docs "flash to tokyo-night then settle" bug — // applyTheme records even the default, so any signed-out visit bakes // "tokyo-night" into localStorage and every signed-in page painted the // SSR theme, flipped to the stale stored one, then loadUserTheme() // flipped back. The server-rendered class is authoritative; the async // loadUserTheme() re-syncs localStorage to it. Signed-out pages carry // no meaningful body theme ("theme-" with an empty user) and fall back // to localStorage — which ThemeFlashGuard has already applied to // pre-paint, so this is visually a no-op for them. const loadTheme = (): void => { if (!/(?:^|\s)theme-[\w-]+/.test(document.body.className)) { const storedTheme = localStorage.getItem(THEME_STORAGE_KEY); applyTheme(storedTheme || DEFAULT_THEME); } }; // Change: Remove DOM element lookup, accept theme as parameter const changeTheme = async (theme: string): Promise => { applyTheme(theme); updateThemeIndicators(); // Save to server if logged in const token = localStorage.getItem(TOKEN_STORAGE_KEY); if (!token) return; try { const response = await fetch("/api/auth/theme", { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ theme }), }); if (!response.ok) { console.log("Theme save failed"); } } catch (error) { console.log("Theme save failed", error); } }; // Load user's theme from server if logged in const loadUserTheme = async (): Promise => { const token = localStorage.getItem(TOKEN_STORAGE_KEY); if (!token) return; try { const response = await fetch("/api/auth/profile", { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const data = await response.json(); if (data.theme) { applyTheme(data.theme as string); updateThemeIndicators(); syncThemeSelect(data.theme as string); } } } catch { // Silently fail - user will get default theme } }; // Keep the docs-topbar theme