@mitcsutt/kiln-ui 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,12 @@ export interface ThemeProviderProps {
18
18
  onModeChange?: (mode: ColorMode) => void;
19
19
  /** Persist the user's mode choice in localStorage. Default `true`. */
20
20
  persistMode?: boolean;
21
+ /**
22
+ * The localStorage key the mode is stored under (default `kiln-color-mode`). Give each
23
+ * app on one origin its own key, or keep an existing one so readers' choices carry over.
24
+ * Pass the same key to `themeScript`.
25
+ */
26
+ storageKey?: string;
21
27
  /**
22
28
  * Where to write `data-theme` / `data-mode`. `document` (default) targets `<html>`,
23
29
  * which is what an app wants. Use `<ThemeScope>` for nested, local themes.
@@ -32,7 +38,7 @@ export interface ThemeProviderProps {
32
38
  * For SSR without a flash, also render `<script dangerouslySetInnerHTML={{ __html: themeScript() }} />`
33
39
  * in the document head (see `themeScript`).
34
40
  */
35
- export declare function ThemeProvider({ theme: themeProp, mode: modeProp, defaultMode, onThemeChange, onModeChange, persistMode, target, children, }: ThemeProviderProps): import("react").JSX.Element;
41
+ export declare function ThemeProvider({ theme: themeProp, mode: modeProp, defaultMode, onThemeChange, onModeChange, persistMode, storageKey, target, children, }: ThemeProviderProps): import("react").JSX.Element;
36
42
  export interface ThemeScopeProps extends HTMLAttributes<HTMLDivElement> {
37
43
  theme: ThemeName;
38
44
  /** Omit to inherit the surrounding colour mode (the usual case). */
@@ -1,13 +1,14 @@
1
1
  import { DEFAULT_THEME as e } from "./themes.js";
2
- import { STORAGE_KEY as t, ThemeContext as n } from "./context.js";
2
+ import { ThemeContext as t } from "./context.js";
3
+ import { DEFAULT_STORAGE_KEY as n } from "./script.js";
3
4
  import { forwardRef as r, useCallback as i, useEffect as a, useMemo as o, useState as s, useSyncExternalStore as c } from "react";
4
5
  import { jsx as l } from "react/jsx-runtime";
5
6
  //#region src/theme/ThemeProvider.tsx
6
7
  var u = "(prefers-color-scheme: dark)";
7
- function d() {
8
+ function d(e) {
8
9
  try {
9
- let e = typeof localStorage > "u" ? null : localStorage.getItem(t);
10
- return e === "light" || e === "dark" || e === "system" ? e : void 0;
10
+ let t = typeof localStorage > "u" ? null : localStorage.getItem(e);
11
+ return t === "light" || t === "dark" || t === "system" ? t : void 0;
11
12
  } catch {
12
13
  return;
13
14
  }
@@ -17,8 +18,8 @@ function f(e) {
17
18
  window.removeEventListener("storage", e);
18
19
  };
19
20
  }
20
- function p(e) {
21
- return c(f, () => e ? d() : void 0, () => void 0);
21
+ function p(e, t) {
22
+ return c(f, () => e ? d(t) : void 0, () => void 0);
22
23
  }
23
24
  function m(e) {
24
25
  if (typeof matchMedia > "u") return () => void 0;
@@ -30,44 +31,48 @@ function m(e) {
30
31
  function h() {
31
32
  return c(m, () => typeof matchMedia < "u" && matchMedia(u).matches, () => !1);
32
33
  }
33
- function g({ theme: r = e, mode: c, defaultMode: u = "system", onThemeChange: d, onModeChange: f, persistMode: m = !0, target: g = "document", children: _ }) {
34
- let [v, y] = s(r), [b, x] = s(r);
35
- r !== b && (x(r), y(r));
36
- let [S, C] = s(c), [w, T] = s(c);
37
- c !== w && (T(c), c && C(c));
38
- let E = p(m && !c), D = S ?? E ?? u;
34
+ function g({ theme: r = e, mode: c, defaultMode: u = "system", onThemeChange: d, onModeChange: f, persistMode: m = !0, storageKey: g = n, target: _ = "document", children: v }) {
35
+ let [y, b] = s(r), [x, S] = s(r);
36
+ r !== x && (S(r), b(r));
37
+ let [C, w] = s(c), [T, E] = s(c);
38
+ c !== T && (E(c), c && w(c));
39
+ let D = p(m && !c, g), O = C ?? D ?? u;
39
40
  a(() => {
40
- if (g !== "document" || typeof document > "u") return;
41
+ if (_ !== "document" || typeof document > "u") return;
41
42
  let e = document.documentElement;
42
- e.dataset.theme = v, e.dataset.mode = D;
43
+ e.dataset.theme = y, e.dataset.mode = O;
43
44
  }, [
44
- g,
45
- v,
46
- D
45
+ _,
46
+ y,
47
+ O
47
48
  ]);
48
- let O = h(), k = D === "system" ? O ? "dark" : "light" : D, A = i((e) => {
49
- y(e), d?.(e);
50
- }, [d]), j = i((e) => {
51
- if (C(e), m && typeof localStorage < "u") try {
52
- localStorage.setItem(t, e);
49
+ let k = h(), A = O === "system" ? k ? "dark" : "light" : O, j = i((e) => {
50
+ b(e), d?.(e);
51
+ }, [d]), M = i((e) => {
52
+ if (w(e), m && typeof localStorage < "u") try {
53
+ localStorage.setItem(g, e);
53
54
  } catch {}
54
55
  f?.(e);
55
- }, [f, m]), M = o(() => ({
56
- theme: v,
57
- mode: D,
58
- resolvedMode: k,
59
- setTheme: A,
60
- setMode: j
56
+ }, [
57
+ f,
58
+ m,
59
+ g
60
+ ]), N = o(() => ({
61
+ theme: y,
62
+ mode: O,
63
+ resolvedMode: A,
64
+ setTheme: j,
65
+ setMode: M
61
66
  }), [
62
- v,
63
- D,
64
- k,
67
+ y,
68
+ O,
65
69
  A,
66
- j
70
+ j,
71
+ M
67
72
  ]);
68
- return /* @__PURE__ */ l(n.Provider, {
69
- value: M,
70
- children: _
73
+ return /* @__PURE__ */ l(t.Provider, {
74
+ value: N,
75
+ children: v
71
76
  });
72
77
  }
73
78
  var _ = r(function({ theme: e, mode: t, paint: n = !0, ...r }, i) {
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeProvider.js","names":["DARK_QUERY","readStoredMode","v","STORAGE_KEY","subscribeToStorage","onChange","useStoredMode","useSyncExternalStore","enabled","subscribeToSystemDark","mq","useSystemDark","ThemeProvider","themeProp","DEFAULT_THEME","modeProp","themeState","setThemeState","useState","prevThemeProp","setPrevThemeProp","chosenMode","setChosenMode","prevModeProp","setPrevModeProp","storedMode","persistMode","modeState","defaultMode","target","el","systemDark","resolvedMode","setTheme","useCallback","t","onThemeChange","setMode","m","onModeChange","value","useMemo","ThemeContext","ThemeScope","forwardRef","rest","theme","mode","paint"],"sources":["../../src/theme/ThemeProvider.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n type HTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { DEFAULT_THEME } from './themes'\nimport type { ColorMode, ThemeName } from './themes'\nimport type { ThemeContextValue } from './context'\n\nimport { ThemeContext, STORAGE_KEY } from './context'\n\nconst DARK_QUERY = '(prefers-color-scheme: dark)'\n\nfunction readStoredMode(): ColorMode | undefined {\n try {\n const v = typeof localStorage === 'undefined' ? null : localStorage.getItem(STORAGE_KEY)\n return v === 'light' || v === 'dark' || v === 'system' ? v : undefined\n } catch {\n return undefined\n }\n}\n\nfunction subscribeToStorage(onChange: () => void): () => void {\n window.addEventListener('storage', onChange)\n return () => {\n window.removeEventListener('storage', onChange)\n }\n}\n\n/**\n * The mode stored by an earlier visit, read only on the client (`undefined` while\n * rendering on the server and during hydration, so markup always matches).\n */\nfunction useStoredMode(enabled: boolean): ColorMode | undefined {\n return useSyncExternalStore(\n subscribeToStorage,\n () => (enabled ? readStoredMode() : undefined),\n () => undefined,\n )\n}\n\nfunction subscribeToSystemDark(onChange: () => void): () => void {\n if (typeof matchMedia === 'undefined') return () => undefined\n const mq = matchMedia(DARK_QUERY)\n mq.addEventListener('change', onChange)\n return () => {\n mq.removeEventListener('change', onChange)\n }\n}\n\nfunction useSystemDark(): boolean {\n return useSyncExternalStore(\n subscribeToSystemDark,\n () => typeof matchMedia !== 'undefined' && matchMedia(DARK_QUERY).matches,\n () => false,\n )\n}\n\nexport interface ThemeProviderProps {\n /**\n * Which theme to render (default `paper`). A built-in name, or any name your own\n * `[data-theme='<name>']` stylesheet defines. Controlled if `onThemeChange` is also supplied.\n */\n theme?: ThemeName\n /** Initial/controlled colour mode. `system` follows the OS via CSS alone — no flash. */\n mode?: ColorMode\n /**\n * Uncontrolled starting mode when the user hasn't chosen one yet (default `system`).\n * A stored choice (see `persistMode`) always wins. Monograph is dark-first, so apps\n * using it usually pass `defaultMode=\"dark\"`. Pair with `themeScript(theme, defaultMode)`.\n */\n defaultMode?: ColorMode\n onThemeChange?: (theme: ThemeName) => void\n onModeChange?: (mode: ColorMode) => void\n /** Persist the user's mode choice in localStorage. Default `true`. */\n persistMode?: boolean\n /**\n * Where to write `data-theme` / `data-mode`. `document` (default) targets `<html>`,\n * which is what an app wants. Use `<ThemeScope>` for nested, local themes.\n */\n target?: 'document' | 'none'\n children: ReactNode\n}\n\n/**\n * App-level theme state. Writes `data-theme` + `data-mode` to `<html>` and exposes\n * `useTheme()`. All visual work happens in CSS; this component only flips attributes.\n *\n * For SSR without a flash, also render `<script dangerouslySetInnerHTML={{ __html: themeScript() }} />`\n * in the document head (see `themeScript`).\n */\nexport function ThemeProvider({\n theme: themeProp = DEFAULT_THEME,\n mode: modeProp,\n defaultMode = 'system',\n onThemeChange,\n onModeChange,\n persistMode = true,\n target = 'document',\n children,\n}: ThemeProviderProps) {\n const [themeState, setThemeState] = useState<ThemeName>(themeProp)\n const [prevThemeProp, setPrevThemeProp] = useState(themeProp)\n // Follow the prop when it changes (React's \"adjust state on a prop change\" pattern).\n if (themeProp !== prevThemeProp) {\n setPrevThemeProp(themeProp)\n setThemeState(themeProp)\n }\n\n // The latest explicit choice: a `mode` prop, or a `setMode` call. Unset until one happens.\n const [chosenMode, setChosenMode] = useState<ColorMode | undefined>(modeProp)\n const [prevModeProp, setPrevModeProp] = useState(modeProp)\n if (modeProp !== prevModeProp) {\n setPrevModeProp(modeProp)\n if (modeProp) setChosenMode(modeProp)\n }\n // A stored choice wins over `defaultMode`, uncontrolled only.\n const storedMode = useStoredMode(persistMode && !modeProp)\n const modeState = chosenMode ?? storedMode ?? defaultMode\n\n useEffect(() => {\n if (target !== 'document' || typeof document === 'undefined') return\n const el = document.documentElement\n el.dataset.theme = themeState\n el.dataset.mode = modeState\n }, [target, themeState, modeState])\n\n const systemDark = useSystemDark()\n const resolvedMode = modeState === 'system' ? (systemDark ? 'dark' : 'light') : modeState\n\n const setTheme = useCallback(\n (t: ThemeName) => {\n setThemeState(t)\n onThemeChange?.(t)\n },\n [onThemeChange],\n )\n\n const setMode = useCallback(\n (m: ColorMode) => {\n setChosenMode(m)\n if (persistMode && typeof localStorage !== 'undefined') {\n try {\n localStorage.setItem(STORAGE_KEY, m)\n } catch {\n /* storage unavailable — ignore */\n }\n }\n onModeChange?.(m)\n },\n [onModeChange, persistMode],\n )\n\n const value = useMemo<ThemeContextValue>(\n () => ({ theme: themeState, mode: modeState, resolvedMode, setTheme, setMode }),\n [themeState, modeState, resolvedMode, setTheme, setMode],\n )\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n}\n\nexport interface ThemeScopeProps extends HTMLAttributes<HTMLDivElement> {\n theme: ThemeName\n /** Omit to inherit the surrounding colour mode (the usual case). */\n mode?: ColorMode\n /**\n * Paint the theme's canvas (background, text colour, base type) on the scope.\n * Default `true`. Set `false` to drop a themed component into a rounded frame or\n * a surface without a square background.\n */\n paint?: boolean\n}\n\n/**\n * Renders a subtree in a different theme (and optionally mode). Themes are plain CSS\n * scoped to `[data-theme]`, so scopes nest freely — e.g. a Fiesta card previewed on a\n * Monograph page. Also paints the theme's page background and text colour.\n */\nexport const ThemeScope = forwardRef<HTMLDivElement, ThemeScopeProps>(function ThemeScope(\n { theme, mode, paint = true, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n data-theme={theme}\n data-mode={mode}\n data-kiln-scope={paint ? '' : undefined}\n data-kiln-scope-bare={paint ? undefined : ''}\n {...rest}\n />\n )\n})\n"],"mappings":";;;;;AAgBA,IAAMA,IAAa;AAEnB,SAASC,IAAwC;CAC/C,IAAI;EACF,IAAMC,IAAI,OAAO,eAAiB,MAAc,OAAO,aAAa,QAAQC,CAAW;EACvF,OAAOD,MAAM,WAAWA,MAAM,UAAUA,MAAM,WAAWA,IAAI,KAAA;CAC/D,QAAQ;EACN;CACF;AACF;AAEA,SAASE,EAAmB,GAAkC;CAE5D,OADA,OAAO,iBAAiB,WAAWC,CAAQ,SAC9B;EACX,OAAO,oBAAoB,WAAWA,CAAQ;CAChD;AACF;AAMA,SAASC,EAAc,GAAyC;CAC9D,OAAOC,EACLH,SACOI,IAAUP,EAAe,IAAI,KAAA,SAC9B,KAAA,CACR;AACF;AAEA,SAASQ,EAAsB,GAAkC;CAC/D,IAAI,OAAO,aAAe,KAAa,aAAa,KAAA;CACpD,IAAMC,IAAK,WAAWV,CAAU;CAEhC,OADAU,EAAG,iBAAiB,UAAUL,CAAQ,SACzB;EACX,EAAG,oBAAoB,UAAUA,CAAQ;CAC3C;AACF;AAEA,SAASM,IAAyB;CAChC,OAAOJ,EACLE,SACM,OAAO,aAAe,OAAe,WAAWT,CAAU,CAAC,CAAC,eAC5D,EACR;AACF;AAmCA,SAAgBY,EAAc,EAC5B,OAAOC,IAAYC,GACnB,MAAMC,GACN,iBAAc,UACd,kBACA,iBACA,iBAAc,IACd,YAAS,YACT,eACqB;CACrB,IAAM,CAACC,GAAYC,KAAiBC,EAAoBL,CAAS,GAC3D,CAACM,GAAeC,KAAoBF,EAASL,CAAS;CAE5D,AAAIA,MAAcM,MAChBC,EAAiBP,CAAS,GAC1BI,EAAcJ,CAAS;CAIzB,IAAM,CAACQ,GAAYC,KAAiBJ,EAAgCH,CAAQ,GACtE,CAACQ,GAAcC,KAAmBN,EAASH,CAAQ;CACzD,AAAIA,MAAaQ,MACfC,EAAgBT,CAAQ,GACpBA,KAAUO,EAAcP,CAAQ;CAGtC,IAAMU,IAAanB,EAAcoB,KAAe,CAACX,CAAQ,GACnDY,IAAYN,KAAcI,KAAcG;CAE9C,QAAgB;EACd,IAAIC,MAAW,cAAc,OAAO,WAAa,KAAa;EAC9D,IAAMC,IAAK,SAAS;EAEpB,AADA,EAAG,QAAQ,QAAQd,GACnB,EAAG,QAAQ,OAAOW;CACpB,GAAG;EAACE;EAAQb;EAAYW;CAAS,CAAC;CAElC,IAAMI,IAAapB,EAAc,GAC3BqB,IAAeL,MAAc,WAAYI,IAAa,SAAS,UAAWJ,GAE1EM,IAAWC,GACd,MAAiB;EAEhB,AADAjB,EAAckB,CAAC,GACfC,IAAgBD,CAAC;CACnB,GACA,CAACC,CAAa,CAChB,GAEMC,IAAUH,GACb,MAAiB;EAEhB,IADAZ,EAAcgB,CAAC,GACXZ,KAAe,OAAO,eAAiB,KACzC,IAAI;GACF,aAAa,QAAQvB,GAAamC,CAAC;EACrC,QAAQ,CAER;EAEF,IAAeA,CAAC;CAClB,GACA,CAACC,GAAcb,CAAW,CAC5B,GAEMc,IAAQC,SACL;EAAE,OAAOzB;EAAY,MAAMW;EAAW;EAAc;EAAU;CAAQ,IAC7E;EAACX;EAAYW;EAAWK;EAAcC;EAAUI;CAAO,CACzD;CAEA,OAAO,kBAACK,EAAa,UAAd;EAA8B;EAAQ;CAAgC,CAAA;AAC/E;AAmBA,IAAaC,IAAaC,EAA4C,SACpE,EAAE,UAAO,SAAM,WAAQ,IAAM,GAAGC,KAChC,GACA;CACA,OACE,kBAAC,OAAD;EACO;EACL,cAAYC;EACZ,aAAWC;EACX,mBAAiBC,IAAQ,KAAK,KAAA;EAC9B,wBAAsBA,IAAQ,KAAA,IAAY;EAC1C,GAAIH;CACL,CAAA;AAEL,CAAC"}
1
+ {"version":3,"file":"ThemeProvider.js","names":["DARK_QUERY","readStoredMode","v","storageKey","subscribeToStorage","onChange","useStoredMode","useSyncExternalStore","enabled","subscribeToSystemDark","mq","useSystemDark","ThemeProvider","themeProp","DEFAULT_THEME","modeProp","DEFAULT_STORAGE_KEY","themeState","setThemeState","useState","prevThemeProp","setPrevThemeProp","chosenMode","setChosenMode","prevModeProp","setPrevModeProp","storedMode","persistMode","modeState","defaultMode","target","el","systemDark","resolvedMode","setTheme","useCallback","t","onThemeChange","setMode","m","onModeChange","value","useMemo","ThemeContext","ThemeScope","forwardRef","rest","theme","mode","paint"],"sources":["../../src/theme/ThemeProvider.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n type HTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { DEFAULT_THEME } from './themes'\nimport type { ColorMode, ThemeName } from './themes'\nimport type { ThemeContextValue } from './context'\n\nimport { ThemeContext } from './context'\nimport { DEFAULT_STORAGE_KEY } from './script'\n\nconst DARK_QUERY = '(prefers-color-scheme: dark)'\n\nfunction readStoredMode(storageKey: string): ColorMode | undefined {\n try {\n const v = typeof localStorage === 'undefined' ? null : localStorage.getItem(storageKey)\n return v === 'light' || v === 'dark' || v === 'system' ? v : undefined\n } catch {\n return undefined\n }\n}\n\nfunction subscribeToStorage(onChange: () => void): () => void {\n window.addEventListener('storage', onChange)\n return () => {\n window.removeEventListener('storage', onChange)\n }\n}\n\n/**\n * The mode stored by an earlier visit, read only on the client (`undefined` while\n * rendering on the server and during hydration, so markup always matches).\n */\nfunction useStoredMode(enabled: boolean, storageKey: string): ColorMode | undefined {\n return useSyncExternalStore(\n subscribeToStorage,\n () => (enabled ? readStoredMode(storageKey) : undefined),\n () => undefined,\n )\n}\n\nfunction subscribeToSystemDark(onChange: () => void): () => void {\n if (typeof matchMedia === 'undefined') return () => undefined\n const mq = matchMedia(DARK_QUERY)\n mq.addEventListener('change', onChange)\n return () => {\n mq.removeEventListener('change', onChange)\n }\n}\n\nfunction useSystemDark(): boolean {\n return useSyncExternalStore(\n subscribeToSystemDark,\n () => typeof matchMedia !== 'undefined' && matchMedia(DARK_QUERY).matches,\n () => false,\n )\n}\n\nexport interface ThemeProviderProps {\n /**\n * Which theme to render (default `paper`). A built-in name, or any name your own\n * `[data-theme='<name>']` stylesheet defines. Controlled if `onThemeChange` is also supplied.\n */\n theme?: ThemeName\n /** Initial/controlled colour mode. `system` follows the OS via CSS alone — no flash. */\n mode?: ColorMode\n /**\n * Uncontrolled starting mode when the user hasn't chosen one yet (default `system`).\n * A stored choice (see `persistMode`) always wins. Monograph is dark-first, so apps\n * using it usually pass `defaultMode=\"dark\"`. Pair with `themeScript(theme, defaultMode)`.\n */\n defaultMode?: ColorMode\n onThemeChange?: (theme: ThemeName) => void\n onModeChange?: (mode: ColorMode) => void\n /** Persist the user's mode choice in localStorage. Default `true`. */\n persistMode?: boolean\n /**\n * The localStorage key the mode is stored under (default `kiln-color-mode`). Give each\n * app on one origin its own key, or keep an existing one so readers' choices carry over.\n * Pass the same key to `themeScript`.\n */\n storageKey?: string\n /**\n * Where to write `data-theme` / `data-mode`. `document` (default) targets `<html>`,\n * which is what an app wants. Use `<ThemeScope>` for nested, local themes.\n */\n target?: 'document' | 'none'\n children: ReactNode\n}\n\n/**\n * App-level theme state. Writes `data-theme` + `data-mode` to `<html>` and exposes\n * `useTheme()`. All visual work happens in CSS; this component only flips attributes.\n *\n * For SSR without a flash, also render `<script dangerouslySetInnerHTML={{ __html: themeScript() }} />`\n * in the document head (see `themeScript`).\n */\nexport function ThemeProvider({\n theme: themeProp = DEFAULT_THEME,\n mode: modeProp,\n defaultMode = 'system',\n onThemeChange,\n onModeChange,\n persistMode = true,\n storageKey = DEFAULT_STORAGE_KEY,\n target = 'document',\n children,\n}: ThemeProviderProps) {\n const [themeState, setThemeState] = useState<ThemeName>(themeProp)\n const [prevThemeProp, setPrevThemeProp] = useState(themeProp)\n // Follow the prop when it changes (React's \"adjust state on a prop change\" pattern).\n if (themeProp !== prevThemeProp) {\n setPrevThemeProp(themeProp)\n setThemeState(themeProp)\n }\n\n // The latest explicit choice: a `mode` prop, or a `setMode` call. Unset until one happens.\n const [chosenMode, setChosenMode] = useState<ColorMode | undefined>(modeProp)\n const [prevModeProp, setPrevModeProp] = useState(modeProp)\n if (modeProp !== prevModeProp) {\n setPrevModeProp(modeProp)\n if (modeProp) setChosenMode(modeProp)\n }\n // A stored choice wins over `defaultMode`, uncontrolled only.\n const storedMode = useStoredMode(persistMode && !modeProp, storageKey)\n const modeState = chosenMode ?? storedMode ?? defaultMode\n\n useEffect(() => {\n if (target !== 'document' || typeof document === 'undefined') return\n const el = document.documentElement\n el.dataset.theme = themeState\n el.dataset.mode = modeState\n }, [target, themeState, modeState])\n\n const systemDark = useSystemDark()\n const resolvedMode = modeState === 'system' ? (systemDark ? 'dark' : 'light') : modeState\n\n const setTheme = useCallback(\n (t: ThemeName) => {\n setThemeState(t)\n onThemeChange?.(t)\n },\n [onThemeChange],\n )\n\n const setMode = useCallback(\n (m: ColorMode) => {\n setChosenMode(m)\n if (persistMode && typeof localStorage !== 'undefined') {\n try {\n localStorage.setItem(storageKey, m)\n } catch {\n /* storage unavailable — ignore */\n }\n }\n onModeChange?.(m)\n },\n [onModeChange, persistMode, storageKey],\n )\n\n const value = useMemo<ThemeContextValue>(\n () => ({ theme: themeState, mode: modeState, resolvedMode, setTheme, setMode }),\n [themeState, modeState, resolvedMode, setTheme, setMode],\n )\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n}\n\nexport interface ThemeScopeProps extends HTMLAttributes<HTMLDivElement> {\n theme: ThemeName\n /** Omit to inherit the surrounding colour mode (the usual case). */\n mode?: ColorMode\n /**\n * Paint the theme's canvas (background, text colour, base type) on the scope.\n * Default `true`. Set `false` to drop a themed component into a rounded frame or\n * a surface without a square background.\n */\n paint?: boolean\n}\n\n/**\n * Renders a subtree in a different theme (and optionally mode). Themes are plain CSS\n * scoped to `[data-theme]`, so scopes nest freely — e.g. a Fiesta card previewed on a\n * Monograph page. Also paints the theme's page background and text colour.\n */\nexport const ThemeScope = forwardRef<HTMLDivElement, ThemeScopeProps>(function ThemeScope(\n { theme, mode, paint = true, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n data-theme={theme}\n data-mode={mode}\n data-kiln-scope={paint ? '' : undefined}\n data-kiln-scope-bare={paint ? undefined : ''}\n {...rest}\n />\n )\n})\n"],"mappings":";;;;;;AAiBA,IAAMA,IAAa;AAEnB,SAASC,EAAe,GAA2C;CACjE,IAAI;EACF,IAAMC,IAAI,OAAO,eAAiB,MAAc,OAAO,aAAa,QAAQC,CAAU;EACtF,OAAOD,MAAM,WAAWA,MAAM,UAAUA,MAAM,WAAWA,IAAI,KAAA;CAC/D,QAAQ;EACN;CACF;AACF;AAEA,SAASE,EAAmB,GAAkC;CAE5D,OADA,OAAO,iBAAiB,WAAWC,CAAQ,SAC9B;EACX,OAAO,oBAAoB,WAAWA,CAAQ;CAChD;AACF;AAMA,SAASC,EAAc,GAAkB,GAA2C;CAClF,OAAOC,EACLH,SACOI,IAAUP,EAAeE,CAAU,IAAI,KAAA,SACxC,KAAA,CACR;AACF;AAEA,SAASM,EAAsB,GAAkC;CAC/D,IAAI,OAAO,aAAe,KAAa,aAAa,KAAA;CACpD,IAAMC,IAAK,WAAWV,CAAU;CAEhC,OADAU,EAAG,iBAAiB,UAAUL,CAAQ,SACzB;EACX,EAAG,oBAAoB,UAAUA,CAAQ;CAC3C;AACF;AAEA,SAASM,IAAyB;CAChC,OAAOJ,EACLE,SACM,OAAO,aAAe,OAAe,WAAWT,CAAU,CAAC,CAAC,eAC5D,EACR;AACF;AAyCA,SAAgBY,EAAc,EAC5B,OAAOC,IAAYC,GACnB,MAAMC,GACN,iBAAc,UACd,kBACA,iBACA,iBAAc,IACd,gBAAaC,GACb,YAAS,YACT,eACqB;CACrB,IAAM,CAACC,GAAYC,KAAiBC,EAAoBN,CAAS,GAC3D,CAACO,GAAeC,KAAoBF,EAASN,CAAS;CAE5D,AAAIA,MAAcO,MAChBC,EAAiBR,CAAS,GAC1BK,EAAcL,CAAS;CAIzB,IAAM,CAACS,GAAYC,KAAiBJ,EAAgCJ,CAAQ,GACtE,CAACS,GAAcC,KAAmBN,EAASJ,CAAQ;CACzD,AAAIA,MAAaS,MACfC,EAAgBV,CAAQ,GACpBA,KAAUQ,EAAcR,CAAQ;CAGtC,IAAMW,IAAapB,EAAcqB,KAAe,CAACZ,GAAUZ,CAAU,GAC/DyB,IAAYN,KAAcI,KAAcG;CAE9C,QAAgB;EACd,IAAIC,MAAW,cAAc,OAAO,WAAa,KAAa;EAC9D,IAAMC,IAAK,SAAS;EAEpB,AADA,EAAG,QAAQ,QAAQd,GACnB,EAAG,QAAQ,OAAOW;CACpB,GAAG;EAACE;EAAQb;EAAYW;CAAS,CAAC;CAElC,IAAMI,IAAarB,EAAc,GAC3BsB,IAAeL,MAAc,WAAYI,IAAa,SAAS,UAAWJ,GAE1EM,IAAWC,GACd,MAAiB;EAEhB,AADAjB,EAAckB,CAAC,GACfC,IAAgBD,CAAC;CACnB,GACA,CAACC,CAAa,CAChB,GAEMC,IAAUH,GACb,MAAiB;EAEhB,IADAZ,EAAcgB,CAAC,GACXZ,KAAe,OAAO,eAAiB,KACzC,IAAI;GACF,aAAa,QAAQxB,GAAYoC,CAAC;EACpC,QAAQ,CAER;EAEF,IAAeA,CAAC;CAClB,GACA;EAACC;EAAcb;EAAaxB;CAAU,CACxC,GAEMsC,IAAQC,SACL;EAAE,OAAOzB;EAAY,MAAMW;EAAW;EAAc;EAAU;CAAQ,IAC7E;EAACX;EAAYW;EAAWK;EAAcC;EAAUI;CAAO,CACzD;CAEA,OAAO,kBAACK,EAAa,UAAd;EAA8B;EAAQ;CAAgC,CAAA;AAC/E;AAmBA,IAAaC,IAAaC,EAA4C,SACpE,EAAE,UAAO,SAAM,WAAQ,IAAM,GAAGC,KAChC,GACA;CACA,OACE,kBAAC,OAAD;EACO;EACL,cAAYC;EACZ,aAAWC;EACX,mBAAiBC,IAAQ,KAAK,KAAA;EAC9B,wBAAsBA,IAAQ,KAAA,IAAY;EAC1C,GAAIH;CACL,CAAA;AAEL,CAAC"}
@@ -8,13 +8,5 @@ export interface ThemeContextValue {
8
8
  setMode: (mode: ColorMode) => void;
9
9
  }
10
10
  export declare const ThemeContext: import("react").Context<ThemeContextValue | null>;
11
- export declare const STORAGE_KEY = "kiln-color-mode";
12
11
  /** Read and change the current theme/mode. Must be inside `<ThemeProvider>`. */
13
12
  export declare function useTheme(): ThemeContextValue;
14
- /**
15
- * Inline script for the document `<head>` that applies the theme and the stored colour
16
- * mode (or `defaultMode` when nothing is stored) before first paint. Use the same `theme`
17
- * and `defaultMode` as `<ThemeProvider>`. Only needed with SSR + a user-selectable mode.
18
- * Any theme name works, built-in or your own.
19
- */
20
- export declare function themeScript(theme?: ThemeName, defaultMode?: ColorMode): string;
@@ -1,19 +1,12 @@
1
- import { DEFAULT_THEME as e } from "./themes.js";
2
- import { createContext as t, useContext as n } from "react";
1
+ import { createContext as e, useContext as t } from "react";
3
2
  //#region src/theme/context.ts
4
- var r = t(null), i = "kiln-color-mode";
5
- function a() {
6
- let e = n(r);
3
+ var n = e(null);
4
+ function r() {
5
+ let e = t(n);
7
6
  if (!e) throw Error("useTheme() must be used inside <ThemeProvider>.");
8
7
  return e;
9
8
  }
10
- function o(e) {
11
- return JSON.stringify(e).replace(/</g, "\\u003c");
12
- }
13
- function s(t = e, n = "system") {
14
- return `(function(){try{var d=document.documentElement;d.dataset.theme=${o(t)};var m=localStorage.getItem(${o(i)});d.dataset.mode=(m==='light'||m==='dark'||m==='system')?m:${o(n)};}catch(e){}})();`;
15
- }
16
9
  //#endregion
17
- export { i as STORAGE_KEY, r as ThemeContext, s as themeScript, a as useTheme };
10
+ export { n as ThemeContext, r as useTheme };
18
11
 
19
12
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":["ThemeContext","createContext","STORAGE_KEY","useTheme","ctx","useContext","scriptLiteral","value","themeScript","DEFAULT_THEME","theme","defaultMode"],"sources":["../../src/theme/context.ts"],"sourcesContent":["import { createContext, useContext } from 'react'\nimport { DEFAULT_THEME } from './themes'\nimport type { ColorMode, ThemeName } from './themes'\n\nexport interface ThemeContextValue {\n theme: ThemeName\n mode: ColorMode\n /** The mode actually rendered once `system` is resolved (`light` during SSR). */\n resolvedMode: 'light' | 'dark'\n setTheme: (theme: ThemeName) => void\n setMode: (mode: ColorMode) => void\n}\n\nexport const ThemeContext = createContext<ThemeContextValue | null>(null)\n\nexport const STORAGE_KEY = 'kiln-color-mode'\n\n/** Read and change the current theme/mode. Must be inside `<ThemeProvider>`. */\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext)\n if (!ctx) throw new Error('useTheme() must be used inside <ThemeProvider>.')\n return ctx\n}\n\n/** JSON for an inline `<script>`: `<` is escaped so a name can never close the tag. */\nfunction scriptLiteral(value: string): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c')\n}\n\n/**\n * Inline script for the document `<head>` that applies the theme and the stored colour\n * mode (or `defaultMode` when nothing is stored) before first paint. Use the same `theme`\n * and `defaultMode` as `<ThemeProvider>`. Only needed with SSR + a user-selectable mode.\n * Any theme name works, built-in or your own.\n */\nexport function themeScript(\n theme: ThemeName = DEFAULT_THEME,\n defaultMode: ColorMode = 'system',\n): string {\n return `(function(){try{var d=document.documentElement;d.dataset.theme=${scriptLiteral(\n theme,\n )};var m=localStorage.getItem(${scriptLiteral(STORAGE_KEY)});d.dataset.mode=(m==='light'||m==='dark'||m==='system')?m:${scriptLiteral(\n defaultMode,\n )};}catch(e){}})();`\n}\n"],"mappings":";;;AAaA,IAAaA,IAAeC,EAAwC,IAAI,GAE3DC,IAAc;AAG3B,SAAgBC,IAA8B;CAC5C,IAAMC,IAAMC,EAAWL,CAAY;CACnC,IAAI,CAACI,GAAK,MAAU,MAAM,iDAAiD;CAC3E,OAAOA;AACT;AAGA,SAASE,EAAc,GAAuB;CAC5C,OAAO,KAAK,UAAUC,CAAK,CAAC,CAAC,QAAQ,MAAM,SAAS;AACtD;AAQA,SAAgBC,EACd,IAAmBC,GACnB,IAAyB,UACjB;CACR,OAAO,kEAAkEH,EACvEI,CACF,EAAE,8BAA8BJ,EAAcJ,CAAW,EAAE,6DAA6DI,EACtHK,CACF,EAAE;AACJ"}
1
+ {"version":3,"file":"context.js","names":["ThemeContext","createContext","useTheme","ctx","useContext"],"sources":["../../src/theme/context.ts"],"sourcesContent":["import { createContext, useContext } from 'react'\nimport type { ColorMode, ThemeName } from './themes'\n\nexport interface ThemeContextValue {\n theme: ThemeName\n mode: ColorMode\n /** The mode actually rendered once `system` is resolved (`light` during SSR). */\n resolvedMode: 'light' | 'dark'\n setTheme: (theme: ThemeName) => void\n setMode: (mode: ColorMode) => void\n}\n\nexport const ThemeContext = createContext<ThemeContextValue | null>(null)\n\n/** Read and change the current theme/mode. Must be inside `<ThemeProvider>`. */\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext)\n if (!ctx) throw new Error('useTheme() must be used inside <ThemeProvider>.')\n return ctx\n}\n"],"mappings":";;AAYA,IAAaA,IAAeC,EAAwC,IAAI;AAGxE,SAAgBC,IAA8B;CAC5C,IAAMC,IAAMC,EAAWJ,CAAY;CACnC,IAAI,CAACG,GAAK,MAAU,MAAM,iDAAiD;CAC3E,OAAOA;AACT"}
@@ -1,6 +1,8 @@
1
1
  export { ThemeProvider, ThemeScope } from './ThemeProvider.js';
2
2
  export type { ThemeProviderProps, ThemeScopeProps } from './ThemeProvider.js';
3
- export { useTheme, themeScript } from './context.js';
3
+ export { useTheme } from './context.js';
4
4
  export type { ThemeContextValue } from './context.js';
5
+ export { themeScript } from './script.js';
6
+ export type { ThemeScriptOptions } from './script.js';
5
7
  export { THEMES, THEME_META, MODES, DEFAULT_THEME } from './themes.js';
6
8
  export type { ThemeName, BuiltInThemeName, ThemeMeta, ColorMode } from './themes.js';
@@ -0,0 +1,14 @@
1
+ import type { ColorMode, ThemeName } from './themes.js';
2
+ /** The localStorage key for the reader's colour mode, unless `storageKey` names another. */
3
+ export declare const DEFAULT_STORAGE_KEY = "kiln-color-mode";
4
+ export interface ThemeScriptOptions {
5
+ /** The localStorage key the mode is read from. Use the same key as `<ThemeProvider>`. */
6
+ storageKey?: string;
7
+ }
8
+ /**
9
+ * Inline script for the document `<head>` that applies the theme and the stored colour
10
+ * mode (or `defaultMode` when nothing is stored) before first paint. Use the same `theme`,
11
+ * `defaultMode` and `storageKey` as `<ThemeProvider>`. Only needed with SSR (or a static
12
+ * `index.html`) + a user-selectable mode. Any theme name works, built-in or your own.
13
+ */
14
+ export declare function themeScript(theme?: ThemeName, defaultMode?: ColorMode, { storageKey }?: ThemeScriptOptions): string;
@@ -0,0 +1,13 @@
1
+ import { DEFAULT_THEME as e } from "./themes.js";
2
+ //#region src/theme/script.ts
3
+ var t = "kiln-color-mode";
4
+ function n(e) {
5
+ return JSON.stringify(e).replace(/</g, "\\u003c");
6
+ }
7
+ function r(r = e, i = "system", { storageKey: a = t } = {}) {
8
+ return `(function(){try{var d=document.documentElement;d.dataset.theme=${n(r)};var m=localStorage.getItem(${n(a)});d.dataset.mode=(m==='light'||m==='dark'||m==='system')?m:${n(i)};}catch(e){}})();`;
9
+ }
10
+ //#endregion
11
+ export { t as DEFAULT_STORAGE_KEY, r as themeScript };
12
+
13
+ //# sourceMappingURL=script.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"script.js","names":["DEFAULT_STORAGE_KEY","scriptLiteral","value","themeScript","DEFAULT_THEME","theme","storageKey","defaultMode"],"sources":["../../src/theme/script.ts"],"sourcesContent":["/**\n * `themeScript`, on its own so it can load without React: the root entry re-exports it, and\n * `@mitcsutt/kiln-ui/theme-script` serves it to Node-side tooling, such as a Vite config that\n * writes it into a static `index.html` (ADR 0023).\n */\nimport { DEFAULT_THEME } from './themes'\nimport type { ColorMode, ThemeName } from './themes'\n\n/** The localStorage key for the reader's colour mode, unless `storageKey` names another. */\nexport const DEFAULT_STORAGE_KEY = 'kiln-color-mode'\n\n/** JSON for an inline `<script>`: `<` is escaped so a name can never close the tag. */\nfunction scriptLiteral(value: string): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c')\n}\n\nexport interface ThemeScriptOptions {\n /** The localStorage key the mode is read from. Use the same key as `<ThemeProvider>`. */\n storageKey?: string\n}\n\n/**\n * Inline script for the document `<head>` that applies the theme and the stored colour\n * mode (or `defaultMode` when nothing is stored) before first paint. Use the same `theme`,\n * `defaultMode` and `storageKey` as `<ThemeProvider>`. Only needed with SSR (or a static\n * `index.html`) + a user-selectable mode. Any theme name works, built-in or your own.\n */\nexport function themeScript(\n theme: ThemeName = DEFAULT_THEME,\n defaultMode: ColorMode = 'system',\n { storageKey = DEFAULT_STORAGE_KEY }: ThemeScriptOptions = {},\n): string {\n return `(function(){try{var d=document.documentElement;d.dataset.theme=${scriptLiteral(\n theme,\n )};var m=localStorage.getItem(${scriptLiteral(storageKey)});d.dataset.mode=(m==='light'||m==='dark'||m==='system')?m:${scriptLiteral(\n defaultMode,\n )};}catch(e){}})();`\n}\n"],"mappings":";;AASA,IAAaA,IAAsB;AAGnC,SAASC,EAAc,GAAuB;CAC5C,OAAO,KAAK,UAAUC,CAAK,CAAC,CAAC,QAAQ,MAAM,SAAS;AACtD;AAaA,SAAgBC,EACd,IAAmBC,GACnB,IAAyB,UACzB,EAAE,gBAAaJ,MAA4C,CAAC,GACpD;CACR,OAAO,kEAAkEC,EACvEI,CACF,EAAE,8BAA8BJ,EAAcK,CAAU,EAAE,6DAA6DL,EACrHM,CACF,EAAE;AACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mitcsutt/kiln-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Kiln: a themeable React design system on Radix primitives, plain CSS and design tokens",
5
5
  "keywords": [
6
6
  "kiln",
@@ -32,6 +32,10 @@
32
32
  "types": "./dist/index.d.ts",
33
33
  "default": "./dist/index.js"
34
34
  },
35
+ "./theme-script": {
36
+ "types": "./dist/theme/script.d.ts",
37
+ "default": "./dist/theme/script.js"
38
+ },
35
39
  "./styles.css": "./dist/styles.css",
36
40
  "./themes/monograph.css": "./dist/themes/monograph.css",
37
41
  "./themes/ledger.css": "./dist/themes/ledger.css",
@@ -80,7 +84,8 @@
80
84
  },
81
85
  "scripts": {
82
86
  "build": "vite build",
83
- "check:package": "pnpm pack --out node_modules/.cache/kiln-pack/package.tgz > /dev/null && publint --pack pnpm --strict && attw node_modules/.cache/kiln-pack/package.tgz --profile esm-only --exclude-entrypoints ./styles.css ./themes/monograph.css ./themes/ledger.css ./themes/fiesta.css",
87
+ "build:watch": "vite build --watch",
88
+ "check:package": "pnpm pack --out node_modules/.cache/kiln-pack/package.tgz > /dev/null && publint --pack pnpm --strict && attw node_modules/.cache/kiln-pack/package.tgz --profile esm-only --exclude-entrypoints ./styles.css ./themes/monograph.css ./themes/ledger.css ./themes/fiesta.css && node scripts/check-theme-script-entry.ts node_modules/.cache/kiln-pack/package.tgz",
84
89
  "check:skills": "intent validate",
85
90
  "lint": "eslint --max-warnings=0 .",
86
91
  "size": "node ../../size-report.ts",
@@ -24,7 +24,7 @@ A Kiln theme is one CSS file. It sets the tokens in the [contract](references/to
24
24
  ```tsx
25
25
  import { Box, Button, Heading, Inline, Stack, Text, ThemeScope } from '@mitcsutt/kiln-ui'
26
26
 
27
- export default function CustomTheme() {
27
+ export function Usage() {
28
28
  return (
29
29
  <ThemeScope theme="harbour">
30
30
  <Box padding={6}>
@@ -50,7 +50,7 @@ Vary the rhythm. Adjacent sections shouldn't share a `space` step, and a page us
50
50
  ```tsx
51
51
  import { Button, Inline, Stack, Text, TextField } from '@mitcsutt/kiln-ui'
52
52
 
53
- export default function Density() {
53
+ export function Density() {
54
54
  return (
55
55
  <Inline gap={7} align="start">
56
56
  {(['compact', undefined, 'comfortable'] as const).map((density) => (
@@ -96,7 +96,7 @@ import { Box, Grid, Text } from '@mitcsutt/kiln-ui'
96
96
 
97
97
  const STOPS = ['Harbour', 'Northpoint', 'Kelso Bay', 'Ferry Lane', 'Old Quay', 'Marram Point']
98
98
 
99
- export default function ResponsiveProps() {
99
+ export function ResponsiveProps() {
100
100
  return (
101
101
  <Grid columns={{ base: 1, sm: 2, lg: 3 }} gap={{ base: 3, md: 5 }}>
102
102
  {STOPS.map((stop) => (
@@ -124,7 +124,7 @@ A column of things with consistent space between them. The layout you'll reach f
124
124
  ```tsx
125
125
  import { Button, Heading, Stack, Text } from '@mitcsutt/kiln-ui'
126
126
 
127
- export default function Usage() {
127
+ export function Usage() {
128
128
  return (
129
129
  <Stack gap={4} align="start">
130
130
  <Heading level={3} size="xl">
@@ -152,7 +152,7 @@ const STOPS = [
152
152
  { name: 'Kelso Bay Pier', time: '23:31' },
153
153
  ]
154
154
 
155
- export default function Dividers() {
155
+ export function Dividers() {
156
156
  return (
157
157
  <Stack gap={{ base: 3, md: 4 }} dividers>
158
158
  {STOPS.map((stop) => (
@@ -196,7 +196,7 @@ Things in a row that wrap when they run out of room. Button rows, tag lists, too
196
196
  ```tsx
197
197
  import { Badge, Button, Inline, Text } from '@mitcsutt/kiln-ui'
198
198
 
199
- export default function Usage() {
199
+ export function Usage() {
200
200
  return (
201
201
  <Inline gap={3} justify={{ base: 'start', md: 'between' }}>
202
202
  <Inline gap={2}>
@@ -244,7 +244,7 @@ import { Box, Grid, Text } from '@mitcsutt/kiln-ui'
244
244
 
245
245
  const LINES = ['Red', 'Harbour', 'Coastal', 'Night', 'Airport', 'Orbital', 'Market', 'University']
246
246
 
247
- export default function Columns() {
247
+ export function Columns() {
248
248
  return (
249
249
  <Grid columns={{ base: 1, sm: 2, lg: 4 }} gap={4}>
250
250
  {LINES.map((line) => (
@@ -266,7 +266,7 @@ import { Box, Grid, Text } from '@mitcsutt/kiln-ui'
266
266
 
267
267
  const PIERS = ['North pier', 'South pier', 'Ferry terminal', 'Lifeboat station', 'Fish market']
268
268
 
269
- export default function AutoFill() {
269
+ export function AutoFill() {
270
270
  return (
271
271
  <Grid minItemWidth="xs" gap={4}>
272
272
  {PIERS.map((pier) => (
@@ -286,7 +286,7 @@ export default function AutoFill() {
286
286
  ```tsx
287
287
  import { Box, Grid, Text } from '@mitcsutt/kiln-ui'
288
288
 
289
- export default function Items() {
289
+ export function Items() {
290
290
  return (
291
291
  <Grid columns={{ base: 1, md: 3 }} gap={4}>
292
292
  <Grid.Item span={{ base: 1, md: 2 }}>
@@ -357,7 +357,7 @@ const CHANGES = [
357
357
  'Kelso Bay Pier reopens on 3 November.',
358
358
  ]
359
359
 
360
- export default function Usage() {
360
+ export function Usage() {
361
361
  return (
362
362
  <Split ratio="5/7" gap={{ base: 5, md: 7 }}>
363
363
  <Heading level={3} size="2xl">
@@ -380,7 +380,7 @@ export default function Usage() {
380
380
  ```tsx
381
381
  import { Box, Split, Stack, Text } from '@mitcsutt/kiln-ui'
382
382
 
383
- export default function Ratios() {
383
+ export function Ratios() {
384
384
  return (
385
385
  <Stack gap={4}>
386
386
  {(['1/1', '1/2', '1/3', '5/7'] as const).map((ratio) => (
@@ -425,7 +425,7 @@ A full-bleed band of vertical space, with an optional surface and rules. Put a C
425
425
  ```tsx
426
426
  import { Button, Container, Heading, Section, Stack, Text } from '@mitcsutt/kiln-ui'
427
427
 
428
- export default function Usage() {
428
+ export function Usage() {
429
429
  return (
430
430
  <>
431
431
  <Section space={7}>
@@ -11,7 +11,7 @@ Source: https://kiln.mitchellsutton.com/docs/ui/layout/action-bar
11
11
  ```tsx
12
12
  import { ActionBar, Button, Stack, TextField } from '@mitcsutt/kiln-ui'
13
13
 
14
- export default function Usage() {
14
+ export function Usage() {
15
15
  return (
16
16
  <Stack gap={5}>
17
17
  <TextField label="Route name" defaultValue="Morning commute" />
@@ -32,7 +32,7 @@ const ICONS = [
32
32
  <SystemIcon key="a" />,
33
33
  ]
34
34
 
35
- export default function Usage() {
35
+ export function Usage() {
36
36
  return (
37
37
  <AppShell navBreakpoint="md">
38
38
  <AppShell.Header>
@@ -25,7 +25,7 @@ function Chart({ label }: { label: string }) {
25
25
  )
26
26
  }
27
27
 
28
- export default function Usage() {
28
+ export function Usage() {
29
29
  return (
30
30
  <Grid columns={{ base: 1, sm: 3 }} gap={4}>
31
31
  {(['16/9', '4/3', '1/1'] as const).map((ratio) => (
@@ -11,7 +11,7 @@ Source: https://kiln.mitchellsutton.com/docs/ui/layout/box
11
11
  ```tsx
12
12
  import { Box, Stack, Text } from '@mitcsutt/kiln-ui'
13
13
 
14
- export default function Usage() {
14
+ export function Usage() {
15
15
  return (
16
16
  <Stack gap={4}>
17
17
  <Box padding={{ base: 4, md: 5 }} surface="sunken" radius="surface">
@@ -33,7 +33,7 @@ const BASKET = [
33
33
 
34
34
  const subtotal = BASKET.reduce((sum, item) => sum + item.price, 0)
35
35
 
36
- export default function Checkout() {
36
+ export function Usage() {
37
37
  return (
38
38
  <Section space={7}>
39
39
  <Container width="content">
@@ -11,7 +11,7 @@ Source: https://kiln.mitchellsutton.com/docs/ui/layout/container
11
11
  ```tsx
12
12
  import { Container, Stack, Text } from '@mitcsutt/kiln-ui'
13
13
 
14
- export default function Usage() {
14
+ export function Usage() {
15
15
  return (
16
16
  <Stack gap={4}>
17
17
  {(['narrow', 'text', 'content'] as const).map((width) => (
@@ -41,7 +41,7 @@ const STATUS = {
41
41
  empty: { tone: 'critical', label: 'Empty' },
42
42
  } as const
43
43
 
44
- export default function Dashboard() {
44
+ export function Usage() {
45
45
  return (
46
46
  <Section space={7}>
47
47
  <Container width="wide">
@@ -11,7 +11,7 @@ Source: https://kiln.mitchellsutton.com/docs/ui/layout/divider
11
11
  ```tsx
12
12
  import { Divider, Inline, Stack, Text } from '@mitcsutt/kiln-ui'
13
13
 
14
- export default function Usage() {
14
+ export function Usage() {
15
15
  return (
16
16
  <Stack gap={4}>
17
17
  <Text>Morning departures</Text>
@@ -28,7 +28,7 @@ import {
28
28
  TextField,
29
29
  } from '@mitcsutt/kiln-ui'
30
30
 
31
- export default function Settings() {
31
+ export function Usage() {
32
32
  return (
33
33
  <Section space={7}>
34
34
  <Container width="content">
@@ -11,7 +11,7 @@ Source: https://kiln.mitchellsutton.com/docs/ui/layout/visually-hidden
11
11
  ```tsx
12
12
  import { Inline, Text, VisuallyHidden } from '@mitcsutt/kiln-ui'
13
13
 
14
- export default function Usage() {
14
+ export function Usage() {
15
15
  return (
16
16
  <Inline gap={2}>
17
17
  <Text numeric weight="strong">
@@ -70,7 +70,7 @@ Screens are built from layout primitives and typed props, never utility classes
70
70
  ```tsx
71
71
  import { Button, Container, Heading, Inline, Section, Stack, Text } from '@mitcsutt/kiln-ui'
72
72
 
73
- export default function FirstScreen() {
73
+ export function Usage() {
74
74
  return (
75
75
  <Section space={7}>
76
76
  <Container width="text">
@@ -121,6 +121,44 @@ export default function RootLayout({ children }: { children: React.ReactNode })
121
121
 
122
122
  `suppressHydrationWarning` on `<html>` is needed because the script changes its attributes before React hydrates.
123
123
 
124
+ ### Name the storage key
125
+
126
+ The reader's mode is stored in `localStorage` under `kiln-color-mode`. Every app on one origin shares that key, so a choice made in one carries into the next. Give each app its own key with `storageKey`, or keep a key the app already used so returning readers keep their choice. Pass the same key to `themeScript`:
127
+
128
+ ```tsx
129
+ ;<ThemeProvider theme="ledger" defaultMode="light" storageKey="budget:color-mode">
130
+ …
131
+ </ThemeProvider>
132
+
133
+ themeScript('ledger', 'light', { storageKey: 'budget:color-mode' })
134
+ ```
135
+
136
+ ### Without server rendering
137
+
138
+ A single-page app served from a static `index.html` still paints before React loads, so it needs the script too. Generate it at build time instead of copying it by hand. The config runs in Node, so import `themeScript` from `@mitcsutt/kiln-ui/theme-script`: that entry has no React and nothing else to load. With Vite, a small plugin in the config puts it at the top of `<head>`:
139
+
140
+ ```ts title="vite.config.ts"
141
+ import { themeScript } from '@mitcsutt/kiln-ui/theme-script'
142
+ import react from '@vitejs/plugin-react'
143
+ import { defineConfig } from 'vite'
144
+
145
+ export default defineConfig({
146
+ plugins: [
147
+ react(),
148
+ {
149
+ name: 'kiln-theme-script',
150
+ transformIndexHtml: () => [
151
+ {
152
+ tag: 'script',
153
+ children: themeScript('ledger', 'light', { storageKey: 'budget:color-mode' }),
154
+ injectTo: 'head-prepend',
155
+ },
156
+ ],
157
+ },
158
+ ],
159
+ })
160
+ ```
161
+
124
162
  ### With the Next.js App Router
125
163
 
126
164
  Kiln's components use React state and context, so they render in client components. Put `ThemeProvider` in a file marked `'use client'`, and import Kiln components from client components: