@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 +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/theme/ThemeProvider.d.ts +7 -1
- package/dist/theme/ThemeProvider.js +40 -35
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/context.d.ts +0 -8
- package/dist/theme/context.js +5 -12
- package/dist/theme/context.js.map +1 -1
- package/dist/theme/index.d.ts +3 -1
- package/dist/theme/script.d.ts +14 -0
- package/dist/theme/script.js +13 -0
- package/dist/theme/script.js.map +1 -0
- package/package.json +7 -2
- package/skills/setup-and-theming/SKILL.md +38 -0
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 {
|
|
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,
|
|
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 {
|
|
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
|
|
10
|
-
return
|
|
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,
|
|
34
|
-
let [
|
|
35
|
-
r !==
|
|
36
|
-
let [
|
|
37
|
-
c !==
|
|
38
|
-
let
|
|
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 (
|
|
41
|
+
if (_ !== "document" || typeof document > "u") return;
|
|
41
42
|
let e = document.documentElement;
|
|
42
|
-
e.dataset.theme =
|
|
43
|
+
e.dataset.theme = y, e.dataset.mode = O;
|
|
43
44
|
}, [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
_,
|
|
46
|
+
y,
|
|
47
|
+
O
|
|
47
48
|
]);
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
}, [d]),
|
|
51
|
-
if (
|
|
52
|
-
localStorage.setItem(
|
|
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
|
-
}, [
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
k,
|
|
67
|
+
y,
|
|
68
|
+
O,
|
|
65
69
|
A,
|
|
66
|
-
j
|
|
70
|
+
j,
|
|
71
|
+
M
|
|
67
72
|
]);
|
|
68
|
-
return /* @__PURE__ */ l(
|
|
69
|
-
value:
|
|
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","
|
|
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"}
|
package/dist/theme/context.d.ts
CHANGED
|
@@ -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;
|
package/dist/theme/context.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
import {
|
|
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
|
|
5
|
-
function
|
|
6
|
-
let e = n
|
|
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 {
|
|
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","
|
|
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"}
|
package/dist/theme/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { ThemeProvider, ThemeScope } from './ThemeProvider.js';
|
|
2
2
|
export type { ThemeProviderProps, ThemeScopeProps } from './ThemeProvider.js';
|
|
3
|
-
export { useTheme
|
|
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.
|
|
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
|
-
"
|
|
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:
|