summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/theme.ts
blob: 2b4bad0cb3c1c44070de12e8d021588249bfad26 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
 * Single source of truth for the app's theme picker.
 *
 * Two callers care about themes:
 *  - `App.svelte`'s `onMount`, which applies the persisted theme on boot
 *    so the first paint is the right color.
 *  - `SettingsPanel.svelte`'s theme `<select>`, which lets the user
 *    change theme at runtime.
 *
 * Both used to hand-roll their own `localStorage` key, default value,
 * and DOM-attribute write. That drift produced a real bug: `App.svelte`
 * left the DOM untouched on first load (so daisyUI fell back to the
 * first theme in `app.css`, `light`), while `SettingsPanel` showed
 * `"dark"` as the selected value — UI and reality disagreed until the
 * user manually picked a theme. Centralizing here closes that gap.
 *
 * The theme list also intentionally mirrors the `@plugin "daisyui"`
 * block in `app.css`. Drift between the two is harmless (a theme name
 * present here but not in CSS just falls back to the default daisyUI
 * theme at render time), but they should be kept in sync by hand —
 * daisyUI's plugin config is a CSS-time concern and can't be imported
 * from TS.
 */

export const THEMES = [
	"light",
	"dark",
	"dracula",
	"night",
	"nord",
	"sunset",
	"cyberpunk",
	"forest",
	"cmyk",
	"coffee",
	"caramellatte",
	"garden",
	"luxury",
] as const;

export type Theme = (typeof THEMES)[number];

export const THEME_STORAGE_KEY = "dispatch-theme";

/**
 * The fallback theme used both at first-boot apply (when nothing is
 * persisted) and as the UI's default-selected value. They MUST match —
 * if they ever diverged again, the UI would show one theme and the
 * page would render another.
 */
export const DEFAULT_THEME: Theme = "dark";

/**
 * Read the persisted theme. Returns `DEFAULT_THEME` when nothing is
 * stored, when access throws (private mode / SecurityError), or when
 * the stored value isn't one of the known themes. Never throws.
 *
 * SSR-safe: if `localStorage` is undefined (e.g. `vite build` during
 * prerender, if that's ever wired up), returns the default.
 */
export function loadStoredTheme(): Theme {
	try {
		if (typeof localStorage === "undefined") return DEFAULT_THEME;
		const raw = localStorage.getItem(THEME_STORAGE_KEY);
		if (raw && (THEMES as readonly string[]).includes(raw)) {
			return raw as Theme;
		}
		return DEFAULT_THEME;
	} catch {
		return DEFAULT_THEME;
	}
}

/**
 * Apply a theme to the live document and persist it. The DOM write is
 * unconditional (daisyUI keys off `data-theme` on the `<html>`
 * element); the storage write is best-effort and swallows quota /
 * SecurityError failures because the session continues to work either
 * way — only cross-reload persistence degrades.
 */
export function applyTheme(theme: Theme): void {
	if (typeof document !== "undefined") {
		document.documentElement.setAttribute("data-theme", theme);
	}
	try {
		if (typeof localStorage !== "undefined") {
			localStorage.setItem(THEME_STORAGE_KEY, theme);
		}
	} catch {
		// Best-effort.
	}
}