type Mode = "light" | "dark"; interface Swatch { background: string; text: string; accent: string; } const PRESETS = { dark: { label: "Default Dark", mode: "dark", swatch: { background: "#141414", text: "#fafafa", accent: "#6a63f0" } }, light: { label: "Default Light", mode: "light", swatch: { background: "#ffffff", text: "#0a0a0a", accent: "#5750f1" } }, "classic-light": { label: "Classic Light", mode: "light", swatch: { background: "#ffffff", text: "#111827", accent: "#5750f1" } }, "classic-dark": { label: "Classic Dark", mode: "dark", swatch: { background: "#1f2937", text: "#f3f4f6", accent: "#6a63f0" } }, ash: { label: "Ash", mode: "dark", swatch: { background: "#313338", text: "#eceef0", accent: "#5865f2" } }, snow: { label: "Snow", mode: "light", swatch: { background: "#ffffff", text: "#282a30", accent: "#5e6ad2" } } } as const satisfies Record; export type Preset = keyof typeof PRESETS; export type Theme = "auto" | Preset; export const PRESET_NAMES = Object.keys(PRESETS) as Preset[]; export const THEMES: Theme[] = ["auto", ...PRESET_NAMES]; const COOKIE = "theme"; const YEAR = 60 * 60 * 24 * 365; export const themeLabel = (theme: Theme): string => theme === "auto" ? "System" : PRESETS[theme].label; export const themeSwatch = (theme: Theme, systemDark: boolean): Swatch => PRESETS[theme === "auto" ? (systemDark ? "dark" : "light") : theme].swatch; export const parseTheme = (raw: string | null | undefined): Theme => raw && Object.hasOwn(PRESETS, raw) ? (raw as Preset) : "auto"; // the server reads the raw header so the hook works on any request shape export const themeFromCookies = (header: string | null): Theme => { for (const part of header?.split(";") ?? []) { const [name, value] = part.trim().split("="); if (name === COOKIE) return parseTheme(value); } return "auto"; }; const themeMode = (theme: Theme): Mode | undefined => theme === "auto" ? undefined : PRESETS[theme].mode; const themePalette = (theme: Theme): Preset | undefined => theme === "auto" || theme === themeMode(theme) ? undefined : theme; // auto has no data-theme, so the dark variant uses prefers-color-scheme export const themeAttribute = (theme: Theme): string => { const mode = themeMode(theme); const palette = themePalette(theme); return (mode ? ` data-theme="${mode}"` : "") + (palette ? ` data-palette="${palette}"` : ""); }; export const readTheme = (): Theme => themeFromCookies(document.cookie); export const applyTheme = (theme: Theme): void => { const root = document.documentElement; const mode = themeMode(theme); const palette = themePalette(theme); if (mode) root.dataset.theme = mode; else root.removeAttribute("data-theme"); if (palette) root.dataset.palette = palette; else root.removeAttribute("data-palette"); document.cookie = theme === "auto" ? `${COOKIE}=; path=/; max-age=0; samesite=lax` : `${COOKIE}=${theme}; path=/; max-age=${YEAR}; samesite=lax`; };