tmp/webapp-docs/frontend/theme/ThemeProvider.js

/**
 * Theme context: applies the chosen base mode + accent to `<html>` and keeps
 * them live. Mirrors the i18n provider — the App owns the persisted values
 * (`settings.themeMode` / `settings.accent`) and the user-theme list, and passes
 * them in. Consumers (the ThemePicker) read the merged theme registry + actions
 * via `useTheme()`.
 *
 * `system` mode follows the OS (a `matchMedia` listener re-applies live). User
 * themes are merged over the built-ins (override by id / add new) and applied at
 * runtime via an adopted stylesheet (see runtimeAccents.js), so a user theme
 * behaves exactly like a built-in one.
 * @module gui/theme/ThemeProvider
 */
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { applyAccent, applyTheme, resolveMode } from "./applyTheme.js";
import { applyUserThemes } from "./runtimeAccents.js";
import { DEFAULT_MODE, normalizeMode } from "./config.js";
import { ACCENTS, DEFAULT_ACCENT, normalizeAccent } from "./presets.js";
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
const ThemeContext = /*#__PURE__*/createContext(null);

/**
 * @param {object} props
 * @param {string} props.mode Persisted base mode (`system` | `dark` | `light`).
 * @param {(mode: string) => void} [props.setMode] Persist a new mode.
 * @param {string} props.accent Persisted accent id (built-in or user theme).
 * @param {(accent: string) => void} [props.setAccent] Persist a new accent.
 * @param {object[]} [props.userThemes] Imported user themes.
 * @param {(theme: object) => void} [props.addUserTheme] Add/overwrite a user theme.
 * @param {(id: string) => void} [props.removeUserTheme] Remove a user theme by id.
 * @param {React.ReactNode} props.children
 */
export function ThemeProvider({
  mode = DEFAULT_MODE,
  setMode,
  accent = DEFAULT_ACCENT,
  setAccent,
  userThemes = [],
  addUserTheme,
  removeUserTheme,
  children
}) {
  const safeMode = normalizeMode(mode);

  // The full theme registry: built-ins, with user themes overriding by id (in
  // place) and new user themes appended. Plus the set of user ids (deletable).
  const userThemeIds = useMemo(() => userThemes.map(t => t.id), [userThemes]);
  const themes = useMemo(() => {
    const byId = new Map(ACCENTS.map(t => [t.id, t]));
    for (const t of userThemes) byId.set(t.id, t);
    return [...byId.values()];
  }, [userThemes]);
  const safeAccent = normalizeAccent(accent, userThemeIds);
  const [resolvedMode, setResolvedMode] = useState(() => resolveMode(safeMode));

  // Apply whenever the chosen mode changes (also on first mount — the inline
  // boot script set a first guess, this reconciles it with the saved choice).
  useEffect(() => {
    setResolvedMode(applyTheme(safeMode));
  }, [safeMode]);

  // While following the OS, re-apply when the OS scheme flips.
  useEffect(() => {
    if (safeMode !== "system") return undefined;
    if (typeof window === "undefined" || !window.matchMedia) return undefined;
    const mq = window.matchMedia("(prefers-color-scheme: light)");
    const onChange = () => setResolvedMode(applyTheme("system"));
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, [safeMode]);

  // Push the user themes into the runtime stylesheet BEFORE the accent is applied
  // (so selecting a just-imported theme finds its rules already present).
  useEffect(() => {
    applyUserThemes(userThemes);
  }, [userThemes]);

  // Apply the accent whenever it (or the valid-id set) changes.
  useEffect(() => {
    applyAccent(safeAccent);
  }, [safeAccent]);
  const changeMode = useCallback(next => {
    if (setMode) setMode(normalizeMode(next));
  }, [setMode]);
  const changeAccent = useCallback(next => {
    if (setAccent) setAccent(normalizeAccent(next, userThemeIds));
  }, [setAccent, userThemeIds]);
  const value = {
    mode: safeMode,
    resolvedMode,
    setMode: changeMode,
    accent: safeAccent,
    setAccent: changeAccent,
    themes,
    userThemeIds,
    addUserTheme,
    removeUserTheme
  };
  return /*#__PURE__*/_jsxDEV(ThemeContext.Provider, {
    value: value,
    children: children
  }, void 0, false);
}

/** Read the current theme registry + setters. Must be used inside a ThemeProvider. */
export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
  return ctx;
}