@mitcsutt/kiln-ui 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -50,7 +50,7 @@ export function App() {
50
50
 
51
51
  - **Your own theme** is one CSS file against the token contract, selected the same way (`theme="harbour"`).
52
52
 
53
- Each theme has light and dark modes (`data-mode`, or `mode`/`defaultMode` on `ThemeProvider`), and `data-density` makes any subtree denser or roomier. For SSR without a flash of the wrong mode, render `themeScript(theme, defaultMode)` in your document `<head>`.
53
+ Each theme has light and dark modes (`data-mode`, or `mode`/`defaultMode` on `ThemeProvider`), and `data-density` makes any subtree denser or roomier. For SSR (or a static `index.html`) without a flash of the wrong mode, render `themeScript(theme, defaultMode)` in your document `<head>`. Node-side tooling, like a Vite config that writes it into `index.html`, imports it from `@mitcsutt/kiln-ui/theme-script`, which loads without React. The mode is stored under `kiln-color-mode`; pass `storageKey` to `ThemeProvider` and `themeScript` to give each app on one origin its own key.
54
54
 
55
55
  ## Fonts
56
56
 
@@ -64,6 +64,8 @@ The package ships agent skills in `skills/`, built from the docs pages: setting
64
64
  npx @tanstack/intent@latest install
65
65
  ```
66
66
 
67
+ If your `package.json` already has an `intent.skills` list, Intent loads only the packages it names: add `@mitcsutt/kiln-ui` to it, then check with `npx @tanstack/intent@latest list`.
68
+
67
69
  ## Docs
68
70
 
69
71
  Full documentation, including the token contract and how to write a theme, lives on the [Kiln docs site](https://kiln.mitchellsutton.com). Until it's live, see [`DESIGN.md`](https://github.com/mitcsutt/kiln/blob/main/DESIGN.md) in the repository.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference path="./stylesheets.d.ts" preserve="true" />
2
2
  export { ThemeProvider, ThemeScope, useTheme, themeScript, THEMES, THEME_META, MODES, DEFAULT_THEME, } from './theme/index.js';
3
- export type { ThemeProviderProps, ThemeScopeProps, ThemeContextValue, ThemeName, BuiltInThemeName, ThemeMeta, ColorMode, } from './theme/index.js';
3
+ export type { ThemeProviderProps, ThemeScopeProps, ThemeContextValue, ThemeScriptOptions, ThemeName, BuiltInThemeName, ThemeMeta, ColorMode, } from './theme/index.js';
4
4
  export { cx } from './utils/cx.js';
5
5
  export type { Responsive, Breakpoint } from './utils/responsive.js';
6
6
  export type { Space, Tone, Size, Width, Align, Justify } from './utils/tokens.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_THEME as e, MODES as t, THEMES as n, THEME_META as r } from "./theme/themes.js";
2
- import { themeScript as i, useTheme as a } from "./theme/context.js";
2
+ import { useTheme as i } from "./theme/context.js";
3
+ import { themeScript as a } from "./theme/script.js";
3
4
  import { ThemeProvider as o, ThemeScope as s } from "./theme/ThemeProvider.js";
4
5
  import { cx as c } from "./utils/cx.js";
5
6
  import { createIcon as l } from "./icons/Icon.js";
@@ -114,4 +115,4 @@ import { Sheet as zt } from "./components/overlays/Sheet/Sheet.js";
114
115
  import { Popover as Bt } from "./components/overlays/Popover/Popover.js";
115
116
  import { DropdownMenu as Vt } from "./components/overlays/DropdownMenu/DropdownMenu.js";
116
117
  import { Tooltip as Ht, TooltipProvider as Ut } from "./components/overlays/Tooltip/Tooltip.js";
117
- export { At as Accordion, q as ActionBar, Mt as Alert, oe as Amount, Ne as AmountField, Me as AmountInput, K as AppShell, u as ArrowLeftIcon, d as ArrowRightIcon, f as ArrowUpRightIcon, W as AspectRatio, dt as Avatar, ft as AvatarGroup, ct as Badge, kt as BottomNav, R as Box, le as Button, st as Card, p as CheckIcon, be as Checkbox, De as CheckboxField, Be as CheckboxGroup, Ve as CheckboxGroupField, m as ChevronDownIcon, h as ChevronLeftIcon, g as ChevronRightIcon, _ as ChevronUpIcon, pe as ChipGroup, We as ChipGroupField, Ge as ChoiceCards, Ke as ChoiceCardsField, v as CircleCheckIcon, y as CloseIcon, Q as Code, bt as CodeBlock, Le as ColorField, Ie as ColorInput, tt as Combobox, nt as ComboboxField, V as Container, b as CopyIcon, J as DEFAULT_HEADING_SIZE, e as DEFAULT_THEME, ht as DataList, ze as DateRangeField, pt as Delta, Rt as Dialog, U as Divider, Vt as DropdownMenu, Et as EmptyState, x as ErrorIcon, S as EyeIcon, C as EyeOffIcon, ge as Field, Ce as Fieldset, at as FileDrop, ot as FileField, z as Grid, Y as Heading, ue as IconButton, w as InfoIcon, L as Inline, _e as Input, $ as Kbd, Z as Link, gt as List, Nt as LiveIndicator, t as MODES, yt as Marquee, vt as Media, T as MenuIcon, wt as Meter, E as MinusIcon, he as ModeToggle, D as MoonIcon, O as MoreIcon, Ot as NavLinks, je as NumberField, Ae as NumberInput, ae as Numeral, Fe as OneTimeCodeField, Pe as OneTimeCodeInput, ke as PasswordField, Oe as PasswordInput, k as PlusIcon, Bt as Popover, St as Progress, ee as Prose, te as Quote, xe as RadioGroup, He as RadioGroupField, Ye as RangeSlider, Xe as RangeSliderField, Ze as Rating, Qe as RatingField, Lt as RelativeTime, A as SearchIcon, H as Section, se as SectionHeader, me as SegmentedControl, Ue as SegmentedField, ye as Select, Ee as SelectField, zt as Sheet, Tt as Skeleton, qe as Slider, Je as SliderField, ce as Spinner, B as Split, I as Stack, xt as Stamp, j as StarIcon, mt as Stat, Pt as StatusDot, jt as Stepper, M as SunIcon, Se as Switch, Re as SwitchField, N as SystemIcon, n as THEMES, r as THEME_META, _t as Table, Dt as Tabs, $e as Tag, et as TagList, it as TagsField, rt as TagsInput, X as Text, we as TextField, ve as Textarea, Te as TextareaField, o as ThemeProvider, s as ThemeScope, de as ToggleChip, Ht as Tooltip, Ut as TooltipProvider, P as UploadIcon, G as VisuallyHidden, F as WarningIcon, lt as avatarColor, l as createIcon, c as cx, Ft as formatAbsoluteTime, ne as formatNumeral, re as formatNumeralParts, It as formatRelativeTime, ut as getInitials, Ct as meterTone, ie as signOf, i as themeScript, fe as useFieldControl, a as useTheme };
118
+ export { At as Accordion, q as ActionBar, Mt as Alert, oe as Amount, Ne as AmountField, Me as AmountInput, K as AppShell, u as ArrowLeftIcon, d as ArrowRightIcon, f as ArrowUpRightIcon, W as AspectRatio, dt as Avatar, ft as AvatarGroup, ct as Badge, kt as BottomNav, R as Box, le as Button, st as Card, p as CheckIcon, be as Checkbox, De as CheckboxField, Be as CheckboxGroup, Ve as CheckboxGroupField, m as ChevronDownIcon, h as ChevronLeftIcon, g as ChevronRightIcon, _ as ChevronUpIcon, pe as ChipGroup, We as ChipGroupField, Ge as ChoiceCards, Ke as ChoiceCardsField, v as CircleCheckIcon, y as CloseIcon, Q as Code, bt as CodeBlock, Le as ColorField, Ie as ColorInput, tt as Combobox, nt as ComboboxField, V as Container, b as CopyIcon, J as DEFAULT_HEADING_SIZE, e as DEFAULT_THEME, ht as DataList, ze as DateRangeField, pt as Delta, Rt as Dialog, U as Divider, Vt as DropdownMenu, Et as EmptyState, x as ErrorIcon, S as EyeIcon, C as EyeOffIcon, ge as Field, Ce as Fieldset, at as FileDrop, ot as FileField, z as Grid, Y as Heading, ue as IconButton, w as InfoIcon, L as Inline, _e as Input, $ as Kbd, Z as Link, gt as List, Nt as LiveIndicator, t as MODES, yt as Marquee, vt as Media, T as MenuIcon, wt as Meter, E as MinusIcon, he as ModeToggle, D as MoonIcon, O as MoreIcon, Ot as NavLinks, je as NumberField, Ae as NumberInput, ae as Numeral, Fe as OneTimeCodeField, Pe as OneTimeCodeInput, ke as PasswordField, Oe as PasswordInput, k as PlusIcon, Bt as Popover, St as Progress, ee as Prose, te as Quote, xe as RadioGroup, He as RadioGroupField, Ye as RangeSlider, Xe as RangeSliderField, Ze as Rating, Qe as RatingField, Lt as RelativeTime, A as SearchIcon, H as Section, se as SectionHeader, me as SegmentedControl, Ue as SegmentedField, ye as Select, Ee as SelectField, zt as Sheet, Tt as Skeleton, qe as Slider, Je as SliderField, ce as Spinner, B as Split, I as Stack, xt as Stamp, j as StarIcon, mt as Stat, Pt as StatusDot, jt as Stepper, M as SunIcon, Se as Switch, Re as SwitchField, N as SystemIcon, n as THEMES, r as THEME_META, _t as Table, Dt as Tabs, $e as Tag, et as TagList, it as TagsField, rt as TagsInput, X as Text, we as TextField, ve as Textarea, Te as TextareaField, o as ThemeProvider, s as ThemeScope, de as ToggleChip, Ht as Tooltip, Ut as TooltipProvider, P as UploadIcon, G as VisuallyHidden, F as WarningIcon, lt as avatarColor, l as createIcon, c as cx, Ft as formatAbsoluteTime, ne as formatNumeral, re as formatNumeralParts, It as formatRelativeTime, ut as getInitials, Ct as meterTone, ie as signOf, a as themeScript, fe as useFieldControl, i as useTheme };
@@ -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.0",
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",
@@ -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: