113 lines
3.0 KiB
TypeScript
113 lines
3.0 KiB
TypeScript
import { browser } from "$app/environment";
|
|||
|
|
|
||
|
|
/** Canonical preference key (app-wide: marketing, dashboard, admin, auth). */
|
||
|
|
export const THEME_KEY = "descrybe-theme";
|
||
|
|
/** Pre-unification marketing key — read once, then migrate to THEME_KEY. */
|
||
|
|
export const LEGACY_MARKETING_THEME_KEY = "descrybe-marketing-theme";
|
||
|
|
/** Brief dual-store key from ui-theme experiment — migrate to THEME_KEY. */
|
||
|
|
export const LEGACY_UI_THEME_KEY = "descrybe-ui-theme";
|
||
|
|
|
||
|
|
export type Theme = "light" | "dark";
|
||
|
|
|
||
|
|
/** SSR / first paint / unset storage — always light (no OS prefers-color-scheme). */
|
||
|
|
export const THEME_DEFAULT: Theme = "light";
|
||
|
|
|
||
|
|
/** Match layout.css --background (FOUC paint before CSS loads). */
|
||
|
|
const DARK_PAINT = "hsl(252 22% 9%)";
|
||
|
|
const LIGHT_PAINT = "hsl(240 20% 98%)";
|
||
|
|
|
||
|
|
function normalize(value: string | null | undefined): Theme | null {
|
||
|
|
if (value === "light" || value === "dark") return value;
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function readStored(): Theme {
|
||
|
|
if (!browser) return THEME_DEFAULT;
|
||
|
|
try {
|
||
|
|
const fromWindow =
|
||
|
|
normalize(window.__THEME__) ??
|
||
|
|
normalize(window.__MARKETING_THEME__) ??
|
||
|
|
normalize(window.__UI_THEME__);
|
||
|
|
if (fromWindow) return fromWindow;
|
||
|
|
const fromStorage =
|
||
|
|
normalize(localStorage.getItem(THEME_KEY)) ??
|
||
|
|
normalize(localStorage.getItem(LEGACY_MARKETING_THEME_KEY)) ??
|
||
|
|
normalize(localStorage.getItem(LEGACY_UI_THEME_KEY));
|
||
|
|
if (fromStorage) return fromStorage;
|
||
|
|
} catch {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
return THEME_DEFAULT;
|
||
|
|
}
|
||
|
|
|
||
|
|
function paintDocument(next: Theme): void {
|
||
|
|
if (!browser) return;
|
||
|
|
const root = document.documentElement;
|
||
|
|
root.classList.toggle("dark", next === "dark");
|
||
|
|
root.dataset.theme = next;
|
||
|
|
root.style.colorScheme = next;
|
||
|
|
root.style.backgroundColor = next === "dark" ? DARK_PAINT : LIGHT_PAINT;
|
||
|
|
document.body?.classList.remove("dark");
|
||
|
|
for (const id of ["marketing-shell", "auth-shell", "admin-shell", "app-shell"] as const) {
|
||
|
|
const shell = document.getElementById(id);
|
||
|
|
if (shell) {
|
||
|
|
shell.classList.toggle("dark", next === "dark");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function createTheme() {
|
||
|
|
let theme = $state<Theme>(readStored());
|
||
|
|
|
||
|
|
function setTheme(next: Theme) {
|
||
|
|
theme = next;
|
||
|
|
if (!browser) return;
|
||
|
|
try {
|
||
|
|
localStorage.setItem(THEME_KEY, next);
|
||
|
|
localStorage.removeItem(LEGACY_MARKETING_THEME_KEY);
|
||
|
|
localStorage.removeItem(LEGACY_UI_THEME_KEY);
|
||
|
|
} catch {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
window.__THEME__ = next;
|
||
|
|
window.__MARKETING_THEME__ = next;
|
||
|
|
window.__UI_THEME__ = next;
|
||
|
|
paintDocument(next);
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggle() {
|
||
|
|
setTheme(theme === "dark" ? "light" : "dark");
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncFromStorage() {
|
||
|
|
const next = readStored();
|
||
|
|
theme = next;
|
||
|
|
paintDocument(next);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
get theme() {
|
||
|
|
return theme;
|
||
|
|
},
|
||
|
|
get isDark() {
|
||
|
|
return theme === "dark";
|
||
|
|
},
|
||
|
|
setTheme,
|
||
|
|
toggle,
|
||
|
|
syncFromStorage,
|
||
|
|
paintDocument
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export const theme = createTheme();
|
||
|
|
|
||
|
|
declare global {
|
||
|
|
interface Window {
|
||
|
|
__THEME__?: Theme;
|
||
|
|
/** @deprecated Prefer __THEME__; kept for FOUC / docs lockstep. */
|
||
|
|
__MARKETING_THEME__?: Theme;
|
||
|
|
/** @deprecated Prefer __THEME__; kept for ui-theme migration. */
|
||
|
|
__UI_THEME__?: Theme;
|
||
|
|
}
|
||
|
|
}
|