Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user