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 const loadTheme = (): void => { const storedTheme = localStorage.getItem(THEME_STORAGE_KEY) as ThemeType | null; const theme = storedTheme || DEFAULT_THEME; applyTheme(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(); } } } catch { // Silently fail - user will get default theme } }; // Update the active-theme checkmark in the Appearance menu to follow the // theme currently applied to . Each theme button carries its theme name // in data-theme and a .theme-check element that is shown for the active theme // and hidden for the rest. const updateThemeIndicators = (): void => { const match = document.body.className.match(/theme-([\w-]+)/); const current = match ? match[1] : DEFAULT_THEME; document.querySelectorAll(".theme-btn").forEach((btn) => { const check = btn.querySelector(".theme-check"); if (!check) return; const theme = btn.getAttribute("data-theme"); check.classList.toggle("hidden", theme !== current); }); }; // Scroll the active theme's row into view inside the Appearance panel's // theme list, for the given mode. No-op when the active theme lives in the // other mode (its row is filtered out there). const revealCurrentTheme = (mode: string): void => { const match = document.body.className.match(/theme-([\w-]+)/); const current = match ? match[1] : DEFAULT_THEME; const btn = document.querySelector(`.theme-btn[data-theme="${current}"]`); if (btn && btn.dataset.mode === mode) { btn.scrollIntoView({ block: "nearest" }); } }; // Initialize theme system const initializeTheme = (): void => { loadTheme(); loadUserTheme(); updateThemeIndicators(); // Set theme select value to current theme const themeSelect = document.getElementById("theme-select") as HTMLSelectElement; if (themeSelect) { const currentTheme = localStorage.getItem(THEME_STORAGE_KEY) || DEFAULT_THEME; themeSelect.value = currentTheme; } // Setup smooth scroll for anchor links setupSmoothScroll(); }; // Setup smooth scrolling for anchor links const setupSmoothScroll = (): void => { document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", (e) => { e.preventDefault(); const href = anchor.getAttribute("href"); if (!href) return; const target = document.querySelector(href); if (target) { target.scrollIntoView({ behavior: "smooth", block: "start", }); } }); }); }; // Wood paneling management functionality type WoodPanelingType = "none" | "wood-light" | "wood-dark" | "wood-mahogany"; const WOOD_STORAGE_KEY = "wood-paneling"; // Apply wood paneling to collections container const applyWoodPaneling = (paneling: WoodPanelingType): void => { const container = document.getElementById("collections-container"); const body = document.body; if (!container || !body) return; // Remove all wood background classes from container container.classList.remove("bg-wood-light", "bg-wood-dark", "bg-wood-mahogany"); container.removeAttribute("data-wood"); // Remove from body body.classList.remove("bg-wood-light", "bg-wood-dark", "bg-wood-mahogany"); if (paneling !== "none") { // Add selected wood background class to both container and body const woodClass = `bg-${paneling}`; container.classList.add(woodClass); container.setAttribute("data-wood", paneling); // Add to body for overscroll area body.classList.add(woodClass); } // Save to localStorage localStorage.setItem(WOOD_STORAGE_KEY, paneling); }; // Load wood paneling from localStorage on page load const loadWoodPaneling = (): void => { const stored = localStorage.getItem(WOOD_STORAGE_KEY) as WoodPanelingType | null; if (stored) { applyWoodPaneling(stored); } else { // Default to none applyWoodPaneling("none"); } }; // Change wood paneling (called from theme dropdown) const changeWoodPaneling = (paneling: WoodPanelingType): void => { applyWoodPaneling(paneling); // Update active indicators updateWoodPanelingIndicators(); // Alpine closes dropdown automatically via template state }; // Update visual indicators for wood paneling buttons const updateWoodPanelingIndicators = (): void => { const currentWood = localStorage.getItem(WOOD_STORAGE_KEY) || "none"; // Update wood paneling buttons document.querySelectorAll(".wood-paneling-btn").forEach((btn) => { const wood = btn.getAttribute("data-wood"); if (wood === currentWood) { // Active state - use CSS class instead of inline style btn.classList.add("bg-wood-active"); btn.classList.remove("bg-wood-inactive"); } else { // Inactive state btn.classList.remove("bg-wood-active"); btn.classList.add("bg-wood-inactive"); } }); }; Alpine.data("theme", () => ({ applyTheme, changeTheme, changeWoodPaneling, initializeTheme, loadTheme, loadUserTheme, loadWoodPaneling, revealCurrentTheme, updateThemeIndicators, updateWoodPanelingIndicators, })); export { applyTheme, changeTheme, changeWoodPaneling, initializeTheme, loadTheme, loadUserTheme, loadWoodPaneling, revealCurrentTheme, updateThemeIndicators, updateWoodPanelingIndicators, }; export type { ThemeType };