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
|
/**
* Colour theme of the app: light (the default) or dark. The choice is kept
* apart from the chat and workspace state because it is a per-browser
* preference, remembered in localStorage and applied to the document as a
* `data-theme` attribute that the stylesheet keys its palettes on.
*
* @example
* toggleTheme(); // light → dark, dark → light
* useTheme((t) => t.theme); // inside a component
* document.documentElement.dataset.theme; // "dark"
*/
import { create } from "zustand";
export type Theme = "light" | "dark";
/** Theme used until the user picks one. */
export const DEFAULT_THEME: Theme = "light";
/** localStorage key under which the chosen theme is remembered. */
export const THEME_KEY = "ori.theme";
export interface ThemeState {
/** The active theme. */
theme: Theme;
}
/** isTheme narrows an arbitrary value (e.g. read from storage) to a Theme. */
export function isTheme(value: unknown): value is Theme {
return value === "light" || value === "dark";
}
/** React hook over the theme store. */
export const useTheme = create<ThemeState>(() => ({ theme: loadTheme() }));
/**
* setTheme activates a theme, reflects it on the document and remembers it.
*
* @example
* setTheme("dark");
* useTheme.getState().theme; // "dark"
*/
export function setTheme(theme: Theme): void {
useTheme.setState({ theme });
applyTheme(theme);
saveTheme(theme);
}
/** toggleTheme switches between light and dark. */
export function toggleTheme(): void {
setTheme(useTheme.getState().theme === "dark" ? "light" : "dark");
}
/** resetTheme restores the default theme and forgets the stored one; meant for tests. */
export function resetTheme(): void {
setTheme(DEFAULT_THEME);
}
/** applyTheme stamps the theme on <html> so the CSS palettes switch. */
function applyTheme(theme: Theme): void {
document.documentElement.dataset.theme = theme;
}
/**
* loadTheme reads the remembered theme. Storage can be missing or throw
* (private browsing, blocked site data), and the stored value may be stale,
* so any failure or unknown value yields the default.
*/
function loadTheme(): Theme {
try {
const stored = globalThis.localStorage?.getItem(THEME_KEY);
return isTheme(stored) ? stored : DEFAULT_THEME;
} catch {
return DEFAULT_THEME;
}
}
function saveTheme(theme: Theme): void {
try {
globalThis.localStorage?.setItem(THEME_KEY, theme);
} catch {
// Storage unavailable: the theme still applies for this page.
}
}
// Stamp the document as soon as the module loads, before React renders, so
// the first paint already uses the remembered theme.
applyTheme(useTheme.getState().theme);
|