nandi/oripublic Fork 0
34e69b510306161654f26903a269247aefa9c94d
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

theme.ts · 87 lines · 2.6 KBTypeScript Blame HistoryRaw
 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);