// Path: Frontend/src/theme/runtime.ts import { defaultThemePresetMeta, defaultThemePresetPath, resolveThemePresetPath } from "./presets"; import { createCssVariableMap, isThemeModeName, validateThemeDefinition, type ThemeDefinition, type ThemeModeName } from "./schema"; export type Theme = ThemeModeName; export const THEME_STORAGE_KEY = "theme"; export const THEME_PRESET_STORAGE_KEY = "theme-preset"; export const DEFAULT_THEME: Theme = "light"; let activeThemeDefinition: ThemeDefinition | null = null; let themeInitializationPromise: Promise | null = null; const canUseDom = (): boolean => typeof document !== "undefined"; const canUseStorage = (): boolean => typeof localStorage !== "undefined"; const canUseMatchMedia = (): boolean => typeof window !== "undefined" && typeof window.matchMedia === "function"; const getRootElement = (): HTMLElement | null => { return canUseDom() ? document.documentElement : null; }; const persistThemePreset = (themeDefinition: ThemeDefinition): void => { if (!canUseStorage()) { return; } localStorage.setItem(THEME_PRESET_STORAGE_KEY, themeDefinition.id); }; const setDocumentThemeMode = (theme: Theme): void => { const rootElement = getRootElement(); rootElement?.setAttribute("data-theme", theme); if (rootElement) { rootElement.style.colorScheme = theme; } if (canUseStorage()) { localStorage.setItem(THEME_STORAGE_KEY, theme); } }; const applyThemeVariables = (themeDefinition: ThemeDefinition, theme: Theme): void => { const rootElement = getRootElement(); if (!rootElement) { return; } const variableMap = createCssVariableMap(themeDefinition, theme); for (const [name, value] of Object.entries(variableMap)) { rootElement.style.setProperty(name, value); } }; export const resolvePreferredTheme = (): Theme => { const stored = canUseStorage() ? localStorage.getItem(THEME_STORAGE_KEY) : null; if (isThemeModeName(stored)) { return stored; } if (canUseMatchMedia()) { return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : DEFAULT_THEME; } return DEFAULT_THEME; }; export const getDocumentTheme = (): Theme => { const theme = getRootElement()?.getAttribute("data-theme"); return isThemeModeName(theme) ? theme : resolvePreferredTheme(); }; export const applyThemeDefinition = (themeDefinition: ThemeDefinition, theme: Theme): void => { activeThemeDefinition = themeDefinition; persistThemePreset(themeDefinition); setDocumentThemeMode(theme); applyThemeVariables(themeDefinition, theme); }; export const resolvePreferredThemePresetId = (): string => { if (!canUseStorage()) { return defaultThemePresetMeta.id; } const stored = localStorage.getItem(THEME_PRESET_STORAGE_KEY); if (stored && resolveThemePresetPath(stored)) { return stored; } return defaultThemePresetMeta.id; }; export const resolvePreferredThemePresetPath = (): string => { const presetId = resolvePreferredThemePresetId(); return resolveThemePresetPath(presetId) ?? defaultThemePresetPath; }; export const initializeThemeRuntime = async (): Promise => { if (typeof window === "undefined") { return null; } if (activeThemeDefinition) { applyThemeDefinition(activeThemeDefinition, getDocumentTheme()); return activeThemeDefinition; } if (!themeInitializationPromise) { themeInitializationPromise = (async (): Promise => { try { const response = await fetch(resolvePreferredThemePresetPath(), { headers: { Accept: "application/json", }, }); if (!response.ok) { throw new Error(`Theme preset request failed with status ${response.status}.`); } const candidate = (await response.json()) as unknown; const result = validateThemeDefinition(candidate); if (!result.success) { throw new Error(result.errors.join(" ")); } applyThemeDefinition(result.data, getDocumentTheme()); return result.data; } catch (error) { console.error("Failed to initialize theme runtime.", error); return null; } finally { themeInitializationPromise = null; } })(); } return themeInitializationPromise; }; export const setTheme = (theme: Theme): void => { setDocumentThemeMode(theme); if (activeThemeDefinition) { applyThemeVariables(activeThemeDefinition, theme); } };