@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.
- package/README.md +3 -1
- package/dist/components/display/List/List.module.js.map +1 -1
- package/dist/components/display/Table/Table.module.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/styles.css +1 -1
- 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/custom-theme/SKILL.md +1 -1
- package/skills/layout-composition/SKILL.md +11 -11
- package/skills/layout-composition/references/action-bar.md +1 -1
- package/skills/layout-composition/references/app-shell.md +1 -1
- package/skills/layout-composition/references/aspect-ratio.md +1 -1
- package/skills/layout-composition/references/box.md +1 -1
- package/skills/layout-composition/references/checkout.md +1 -1
- package/skills/layout-composition/references/container.md +1 -1
- package/skills/layout-composition/references/dashboard.md +1 -1
- package/skills/layout-composition/references/divider.md +1 -1
- package/skills/layout-composition/references/settings.md +1 -1
- package/skills/layout-composition/references/visually-hidden.md +1 -1
- package/skills/setup-and-theming/SKILL.md +39 -1
|
@@ -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.1
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14
|
+
export function Usage() {
|
|
15
15
|
return (
|
|
16
16
|
<Stack gap={5}>
|
|
17
17
|
<TextField label="Route name" defaultValue="Morning commute" />
|
|
@@ -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
|
|
14
|
+
export function Usage() {
|
|
15
15
|
return (
|
|
16
16
|
<Stack gap={4}>
|
|
17
17
|
<Box padding={{ base: 4, md: 5 }} surface="sunken" radius="surface">
|
|
@@ -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
|
|
14
|
+
export function Usage() {
|
|
15
15
|
return (
|
|
16
16
|
<Stack gap={4}>
|
|
17
17
|
{(['narrow', 'text', 'content'] as const).map((width) => (
|
|
@@ -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
|
|
14
|
+
export function Usage() {
|
|
15
15
|
return (
|
|
16
16
|
<Stack gap={4}>
|
|
17
17
|
<Text>Morning departures</Text>
|
|
@@ -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
|
|
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
|
|
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:
|