/** * 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(() => ({ 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 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);