@designtools/docs 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +24 -2
  3. package/dist/anatomy.js +2 -1
  4. package/dist/cli.js +1 -1
  5. package/dist/glossary.d.ts +1 -1
  6. package/dist/glossary.js +13 -6
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/lib/live.d.ts +10 -0
  10. package/dist/lib/live.js +10 -0
  11. package/dist/lib/lookup.js +3 -2
  12. package/dist/lib/status.d.ts +2 -3
  13. package/dist/lib/terms.d.ts +15 -0
  14. package/dist/lib/terms.js +20 -0
  15. package/dist/next/config.d.ts +24 -4
  16. package/dist/next/index.d.ts +3 -0
  17. package/dist/next/index.js +3 -0
  18. package/dist/next/markdown.js +1 -1
  19. package/dist/next/pages.js +82 -16
  20. package/dist/next/prose.d.ts +2 -2
  21. package/dist/next/prose.js +6 -4
  22. package/dist/pattern.d.ts +4 -1
  23. package/dist/pattern.js +11 -8
  24. package/dist/playground.js +1 -1
  25. package/dist/rule.d.ts +9 -5
  26. package/dist/rule.js +14 -9
  27. package/dist/shell.d.ts +4 -1
  28. package/dist/shell.js +13 -11
  29. package/dist/styles.css +961 -754
  30. package/dist/swatches.d.ts +7 -6
  31. package/dist/swatches.js +50 -16
  32. package/dist/variant-matrix.js +11 -6
  33. package/dist/voice-terms.js +6 -5
  34. package/dist/workbench.js +2 -1
  35. package/package.json +1 -1
  36. package/src/anatomy.tsx +6 -1
  37. package/src/glossary.tsx +39 -40
  38. package/src/index.ts +2 -1
  39. package/src/lib/live.tsx +15 -0
  40. package/src/lib/lookup.tsx +3 -2
  41. package/src/lib/status.tsx +3 -3
  42. package/src/lib/terms.tsx +53 -0
  43. package/src/next/config.ts +25 -4
  44. package/src/next/index.tsx +3 -0
  45. package/src/next/markdown.ts +1 -1
  46. package/src/next/pages.tsx +172 -59
  47. package/src/next/prose.tsx +16 -6
  48. package/src/pattern.tsx +42 -37
  49. package/src/playground.tsx +7 -2
  50. package/src/rule.tsx +108 -59
  51. package/src/shell.tsx +30 -16
  52. package/src/styles.css +4 -2
  53. package/src/swatches.tsx +189 -120
  54. package/src/variant-matrix.tsx +63 -80
  55. package/src/voice-terms.tsx +20 -29
  56. package/src/workbench.tsx +6 -3
@@ -1,11 +1,12 @@
1
1
  import { type Mode } from "./lib/manifest.js";
2
2
  import type { TokensManifest } from "./lib/manifest-types.js";
3
3
  /**
4
- * Colour, painted from the tokens rather than restated. Each colour with a
5
- * subdued pair is a card: its fill, subdued, hover and press, in light and dark
6
- * side by side. Then the surfaces, then the utility colours. Every sample sits
7
- * inside its own `data-theme`, so dark is shown as the dark tokens resolve, and
8
- * every pair's contrast is measured on the colours the browser actually paints.
4
+ * Colour, painted from the tokens rather than restated. No panels and no cards around the samples:
5
+ * each sample is its own colour, with one hairline round it, set on the page in a light and a dark
6
+ * column. Each colour with a subdued pair shows its fill large, then subdued, hover and press; then
7
+ * the surfaces and the utility colours, one row each. Every sample sits inside its own `data-theme`,
8
+ * so dark is shown as the dark tokens resolve, and every pair's contrast is measured on the colours
9
+ * the browser actually paints.
9
10
  */
10
11
  export declare function SemanticSwatches({ tokens, modes, }: {
11
12
  tokens: TokensManifest;
@@ -15,7 +16,7 @@ export declare function SemanticSwatches({ tokens, modes, }: {
15
16
  * Each colour on each background, per mode: a label is measured on its own fill, and this is
16
17
  * everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
17
18
  * canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
18
- * and muted text. T: fine as text (4.5:1); S: fine as a shape only (3:1); none: neither.
19
+ * and muted text. Text: fine as text (4.5:1); shape: fine as a shape only (3:1); none: neither.
19
20
  * Measured on what the browser paints, inside each mode's `data-theme`.
20
21
  */
21
22
  export declare function PageContrast({ tokens, modes }: {
package/dist/swatches.js CHANGED
@@ -1,15 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, Square, X } from "lucide-react";
3
4
  import { useEffect, useRef, useState } from "react";
4
5
  import { contrastRatio, wcagGrade } from "./lib/contrast.js";
5
6
  import { cx } from "./lib/cx.js";
6
7
  import { ramps, semanticColours, valueIn } from "./lib/manifest.js";
7
8
  /**
8
- * Colour, painted from the tokens rather than restated. Each colour with a
9
- * subdued pair is a card: its fill, subdued, hover and press, in light and dark
10
- * side by side. Then the surfaces, then the utility colours. Every sample sits
11
- * inside its own `data-theme`, so dark is shown as the dark tokens resolve, and
12
- * every pair's contrast is measured on the colours the browser actually paints.
9
+ * Colour, painted from the tokens rather than restated. No panels and no cards around the samples:
10
+ * each sample is its own colour, with one hairline round it, set on the page in a light and a dark
11
+ * column. Each colour with a subdued pair shows its fill large, then subdued, hover and press; then
12
+ * the surfaces and the utility colours, one row each. Every sample sits inside its own `data-theme`,
13
+ * so dark is shown as the dark tokens resolve, and every pair's contrast is measured on the colours
14
+ * the browser actually paints.
13
15
  */
14
16
  export function SemanticSwatches({ tokens, modes = ["light", "dark"], }) {
15
17
  const colours = semanticColours(tokens);
@@ -22,7 +24,17 @@ export function SemanticSwatches({ tokens, modes = ["light", "dark"], }) {
22
24
  const token = byName.get(name)?.background;
23
25
  return token && pointsAt(valueIn(token, mode));
24
26
  };
25
- return (_jsxs("div", { "data-slot": "swatches", className: "flex flex-col gap-10", children: [_jsx("div", { className: "grid gap-4 xl:grid-cols-2", children: families.map((f) => (_jsxs("article", { "data-slot": "swatch-family", className: "flex flex-col overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground", children: [_jsxs("header", { className: "flex items-baseline justify-between gap-3 px-4 py-3", children: [_jsx("h3", { className: "text-lg capitalize", children: f.slice(2).replace(/-/g, " ") }), _jsx("code", { className: "font-mono text-xs text-muted-foreground", children: f })] }), _jsx("div", { className: cx("grid border-t border-border", modes.length > 1 && "sm:grid-cols-2"), children: modes.map((mode) => (_jsxs("div", { "data-theme": mode, className: "flex flex-col gap-2 bg-canvas p-3 text-canvas-foreground not-first:border-t not-first:border-border sm:not-first:border-t-0 sm:not-first:border-l", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground capitalize", children: mode }), _jsx(Chip, { name: f, label: "fill", step: step(f, mode), size: "lg" }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsx(Chip, { name: `${f}-subdued`, label: "subdued", step: step(`${f}-subdued`, mode) }), byName.has(`${f}-hover`) && _jsx(Chip, { name: `${f}-hover`, foreground: `${f}-foreground`, label: "hover" }), byName.has(`${f}-active`) && _jsx(Chip, { name: `${f}-active`, foreground: `${f}-foreground`, label: "press" })] })] }, mode))) })] }, f))) }), surfaces.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-2xl tracking-tight", children: "Surfaces" }), _jsx("div", { className: cx("grid gap-4", modes.length > 1 && "lg:grid-cols-2"), children: modes.map((mode) => (_jsxs("div", { "data-theme": mode, className: "flex flex-col gap-2 rounded-xl border border-border bg-canvas p-3 text-canvas-foreground", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground capitalize", children: mode }), _jsx("div", { className: "grid grid-cols-2 gap-2 sm:grid-cols-3", children: surfaces.map((name) => (_jsx(Chip, { name: name, label: name.slice(2), step: step(name, mode), outlined: true }, name))) })] }, mode))) })] })), utility.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-2xl tracking-tight", children: "Lines, focus and shadow" }), _jsx("div", { className: cx("grid gap-4", modes.length > 1 && "lg:grid-cols-2"), children: modes.map((mode) => (_jsxs("div", { "data-theme": mode, className: "flex flex-col gap-2 rounded-xl border border-border bg-canvas p-3 text-canvas-foreground", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground capitalize", children: mode }), _jsx("ul", { className: "grid grid-cols-2 gap-2", children: utility.map((name) => (_jsxs("li", { "data-slot": "swatch", className: "flex items-center gap-2 rounded-lg bg-surface p-2 text-surface-foreground", children: [_jsx("span", { "aria-hidden": true, className: "size-8 shrink-0 rounded-md border border-border", style: { background: `var(${name})` } }), _jsxs("span", { className: "flex min-w-0 flex-col font-mono text-xs", children: [_jsx("span", { className: "truncate font-medium", children: name.slice(2) }), _jsx("span", { className: "truncate text-muted-foreground", children: step(name, mode) })] })] }, name))) })] }, mode))) })] }))] }));
27
+ const columns = cx("grid gap-x-8 gap-y-6", modes.length > 1 && "md:grid-cols-2");
28
+ const modeLabel = (mode) => _jsx("p", { className: "text-xs font-medium text-muted-foreground capitalize", children: mode });
29
+ const title = (text) => text.charAt(0).toUpperCase() + text.slice(1);
30
+ return (_jsxs("div", { "data-slot": "swatches", className: "flex flex-col gap-16", children: [_jsx("div", { className: "flex flex-col gap-14", children: families.map((f) => (_jsxs("section", { "data-slot": "swatch-family", className: "flex flex-col gap-5", children: [_jsxs("header", { className: "flex items-baseline gap-3", children: [_jsx("h3", { className: "text-2xl leading-tight tracking-tight", children: title(f.slice(2).replace(/-/g, " ")) }), _jsx("code", { className: "font-mono text-xs text-muted-foreground", children: f })] }), _jsx("div", { className: columns, children: modes.map((mode) => (_jsxs("div", { className: "flex flex-col gap-2.5", children: [modeLabel(mode), _jsxs("div", { "data-theme": mode, className: "flex flex-col gap-2.5", children: [_jsx(Chip, { name: f, label: "fill", step: step(f, mode), size: "lg" }), _jsxs("div", { className: "grid grid-cols-3 gap-2.5", children: [_jsx(Chip, { name: `${f}-subdued`, label: "subdued", step: step(`${f}-subdued`, mode) }), byName.has(`${f}-hover`) && _jsx(Chip, { name: `${f}-hover`, foreground: `${f}-foreground`, label: "hover" }), byName.has(`${f}-active`) && _jsx(Chip, { name: `${f}-active`, foreground: `${f}-foreground`, label: "press" })] })] })] }, mode))) })] }, f))) }), surfaces.length > 0 && (_jsx(Rows, { title: "Surfaces", lead: "The page, panels and popups, and the brand's own backgrounds, each with its text.", modes: modes, children: surfaces.map((name) => (_jsx(Row, { name: name.slice(2), modes: modes, children: (mode) => (_jsx("div", { "data-theme": mode, children: _jsx(Chip, { name: name, label: step(name, mode) ?? "", size: "wide" }) })) }, name))) })), utility.length > 0 && (_jsx(Rows, { title: "Lines, focus and shadow", lead: "Borders, input edges, the focus ring and the shadow colour.", modes: modes, children: utility.map((name) => (_jsx(Row, { name: name.slice(2), modes: modes, children: (mode) => (_jsxs("span", { className: "flex h-14 items-center gap-3", children: [_jsx("span", { "data-theme": mode, "aria-hidden": true, className: "size-10 shrink-0 rounded-lg ring-1 ring-(--canvas-foreground)/15 ring-inset", style: { background: `var(${name})` } }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: step(name, mode) })] })) }, name))) }))] }));
31
+ }
32
+ /** A titled list of rows, with the mode names once over the columns. */
33
+ function Rows({ title, lead, modes, children }) {
34
+ return (_jsxs("section", { className: "flex flex-col gap-5", children: [_jsxs("header", { className: "flex flex-col gap-1.5", children: [_jsx("h3", { className: "text-2xl leading-tight tracking-tight", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: lead })] }), _jsxs("div", { children: [_jsxs("div", { "aria-hidden": true, className: "hidden gap-x-8 pb-3 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]", style: { "--modes": modes.length }, children: [_jsx("span", {}), modes.map((m) => (_jsx("span", { className: "capitalize", children: m }, m)))] }), children] })] }));
35
+ }
36
+ function Row({ name, modes, children }) {
37
+ return (_jsxs("div", { className: "grid items-center gap-x-8 gap-y-2 border-t border-border py-3 md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]", style: { "--modes": modes.length }, children: [_jsx("code", { className: "font-mono text-sm", children: name }), modes.map((mode) => (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-xs text-muted-foreground capitalize md:hidden", children: mode }), children(mode)] }, mode)))] }));
26
38
  }
27
39
  /** `var(--color-primary-500)` → `primary-500`: the step it points at, which is what a person compares. */
28
40
  function pointsAt(value) {
@@ -32,11 +44,11 @@ function pointsAt(value) {
32
44
  return value?.startsWith("oklch(") ? "generated" : value;
33
45
  }
34
46
  /**
35
- * One background with its foreground on it, labelled in that foreground, with the
36
- * contrast measured on what the browser paints. It reads its mode from the nearest
37
- * `data-theme` around it.
47
+ * One background with its foreground on it, labelled in that foreground, with the contrast measured on
48
+ * what the browser paints. It reads its mode from the nearest `data-theme` around it. One hairline round
49
+ * it, so a white sample shows on a white page, and nothing else.
38
50
  */
39
- function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md", outlined, }) {
51
+ function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md", }) {
40
52
  const chip = useRef(null);
41
53
  const [ratio, setRatio] = useState(null);
42
54
  useEffect(() => {
@@ -49,20 +61,42 @@ function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md
49
61
  const grade = ratio === null ? null : wcagGrade(ratio);
50
62
  const fails = grade === "Fail";
51
63
  const measured = grade && ratio !== null ? `${ratio.toFixed(1)}:1 ${grade}` : undefined;
52
- return (_jsxs("div", { ref: chip, "data-slot": "swatch", title: `${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`, className: cx("flex min-w-0 flex-col justify-between gap-3 rounded-lg p-3", size === "lg" ? "min-h-28" : "min-h-20", outlined && "border border-border"), style: { background: `var(${name})`, color: `var(${foreground})` }, children: [size === "lg" ? (_jsxs("span", { className: "flex items-start justify-between gap-2", children: [_jsx("span", { className: "text-3xl leading-none tracking-tight", children: "Aa" }), measured && (_jsx("span", { "data-slot": "swatch-contrast", className: cx("rounded-full px-2 py-0.5 font-mono text-xs whitespace-nowrap", fails ? "bg-destructive font-medium text-destructive-foreground" : "ring-1 ring-current/30"), children: measured }))] })) : (_jsx("span", { className: "text-base leading-none font-medium", children: "Aa" })), _jsxs("span", { className: "flex min-w-0 flex-col font-mono text-xs leading-snug", children: [_jsx("span", { className: "truncate font-medium", children: label }), size === "lg" ? (step && _jsx("span", { className: "truncate opacity-75", children: step })) : (measured && (_jsx("span", { "data-slot": "swatch-contrast", className: cx("truncate", fails ? "w-fit rounded-sm bg-destructive px-1 text-destructive-foreground" : "opacity-75"), children: measured })))] })] }));
64
+ const contrast = measured && (_jsx("span", { "data-slot": "swatch-contrast", className: cx("font-mono text-xs whitespace-nowrap", fails ? "rounded-sm bg-destructive px-1.5 py-0.5 font-medium text-destructive-foreground" : "opacity-80"), children: measured }));
65
+ const paint = { background: `var(${name})`, color: `var(${foreground})` };
66
+ // a hairline in the sample's own text colour: it shows a white sample on a white page and disappears into a strong fill
67
+ const base = "min-w-0 rounded-xl ring-1 ring-current/15 ring-inset";
68
+ const tip = `${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`;
69
+ if (size === "wide")
70
+ return (_jsxs("div", { ref: chip, "data-slot": "swatch", title: tip, className: cx(base, "flex h-14 items-center justify-between gap-4 px-4"), style: paint, children: [_jsxs("span", { className: "flex min-w-0 items-baseline gap-3", children: [_jsx("span", { className: "text-xl leading-none font-medium", children: "Aa" }), _jsx("span", { className: "font-mono text-xs opacity-80", children: label })] }), contrast] }));
71
+ if (size === "lg")
72
+ return (_jsxs("div", { ref: chip, "data-slot": "swatch", title: tip, className: cx(base, "flex min-h-36 flex-col justify-between gap-6 p-5"), style: paint, children: [_jsxs("span", { className: "flex items-start justify-between gap-3", children: [_jsx("span", { className: "text-5xl leading-none tracking-tight", children: "Aa" }), contrast] }), _jsxs("span", { className: "flex flex-col gap-0.5 text-sm leading-snug", children: [_jsx("span", { className: "font-medium", children: label }), step && _jsx("span", { className: "font-mono text-xs opacity-80", children: step })] })] }));
73
+ return (_jsxs("div", { ref: chip, "data-slot": "swatch", title: tip, className: cx(base, "flex min-h-28 flex-col justify-between gap-4 p-4"), style: paint, children: [_jsx("span", { className: "text-2xl leading-none font-medium", children: "Aa" }), _jsxs("span", { className: "flex flex-col gap-1 leading-snug", children: [_jsx("span", { className: "text-sm font-medium", children: label }), contrast] })] }));
53
74
  }
54
75
  /**
55
76
  * Each colour on each background, per mode: a label is measured on its own fill, and this is
56
77
  * everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
57
78
  * canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
58
- * and muted text. T: fine as text (4.5:1); S: fine as a shape only (3:1); none: neither.
79
+ * and muted text. Text: fine as text (4.5:1); shape: fine as a shape only (3:1); none: neither.
59
80
  * Measured on what the browser paints, inside each mode's `data-theme`.
60
81
  */
61
82
  export function PageContrast({ tokens, modes = ["light", "dark"] }) {
62
83
  const names = new Set(semanticColours(tokens).map((c) => c.name));
63
84
  const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
64
85
  const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
65
- return (_jsx("div", { "data-slot": "page-contrast", className: "flex flex-col gap-6", children: modes.map((mode) => (_jsx(PageGrid, { mode: mode, colours: colours, grounds: grounds }, mode))) }));
86
+ return (_jsxs("div", { "data-slot": "page-contrast", className: "flex flex-col gap-14", children: [_jsx("p", { className: "flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground", children: [
87
+ ["text", "fine as text, 4.5:1"],
88
+ ["shape", "fine as a shape only, 3:1"],
89
+ ["none", "neither"],
90
+ ].map(([mark, words]) => (_jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx(Verdict, { mark: mark }), words] }, mark))) }), modes.map((mode) => (_jsx(PageGrid, { mode: mode, colours: colours, grounds: grounds }, mode)))] }));
91
+ }
92
+ const VERDICT = {
93
+ text: { Icon: Check, className: "text-success-subdued-foreground", label: "text" },
94
+ shape: { Icon: Square, className: "text-muted-foreground", label: "shape" },
95
+ none: { Icon: X, className: "text-destructive-subdued-foreground", label: "none" },
96
+ };
97
+ function Verdict({ mark, ratio }) {
98
+ const { Icon, className, label } = VERDICT[mark];
99
+ return (_jsxs("span", { className: "inline-flex items-center gap-1.5 whitespace-nowrap", children: [_jsx(Icon, { "aria-hidden": true, className: cx("size-3.5 shrink-0", className), strokeWidth: 2.5 }), ratio != null && _jsx("span", { className: "font-mono text-xs text-canvas-foreground tabular-nums", children: ratio.toFixed(1) }), _jsx("span", { className: cx(ratio != null && "text-xs text-muted-foreground"), children: ratio != null ? label : null })] }));
66
100
  }
67
101
  function PageGrid({ mode, colours, grounds }) {
68
102
  const probe = useRef(null);
@@ -82,10 +116,10 @@ function PageGrid({ mode, colours, grounds }) {
82
116
  out[`${c}|${g}`] = contrastRatio(painted[c], painted[g]);
83
117
  setRatios(out);
84
118
  }, [colours, grounds]);
85
- return (_jsxs("div", { className: "overflow-x-auto rounded-2xl border border-border bg-surface text-surface-foreground", tabIndex: 0, role: "region", "aria-label": `Colours on each background, ${mode} mode`, children: [_jsx("div", { ref: probe, "data-theme": mode, hidden: true }), _jsxs("table", { className: "w-full min-w-[32rem] border-collapse text-left text-sm", children: [_jsx("caption", { className: "border-b border-border px-4 py-3 text-left text-sm font-semibold capitalize", children: mode }), _jsx("thead", { children: _jsxs("tr", { className: "border-b border-border bg-muted/50 text-xs text-muted-foreground", children: [_jsx("th", { scope: "col", className: "px-4 py-2.5 font-medium", children: "Colour" }), grounds.map((g) => (_jsx("th", { scope: "col", className: "px-4 py-2.5 font-mono font-medium", children: g.slice(2) }, g)))] }) }), _jsx("tbody", { children: colours.map((c) => (_jsxs("tr", { className: "border-b border-border last:border-b-0", children: [_jsx("th", { scope: "row", className: "px-4 py-2.5 font-mono text-xs font-medium", children: c.slice(2) }), grounds.map((g) => {
119
+ return (_jsxs("div", { className: "-mx-1 overflow-x-auto px-1 pb-2", tabIndex: 0, role: "region", "aria-label": `Colours on each background, ${mode} mode`, children: [_jsx("div", { ref: probe, "data-theme": mode, hidden: true }), _jsxs("table", { className: "w-full min-w-[46rem] table-fixed border-collapse text-left text-sm", children: [_jsx("caption", { className: "pb-5 text-left text-lg font-medium capitalize", children: mode }), _jsx("thead", { children: _jsxs("tr", { className: "text-xs text-muted-foreground", children: [_jsx("th", { scope: "col", className: "w-32 pr-4 pb-3 font-medium", children: "Colour" }), grounds.map((g) => (_jsx("th", { scope: "col", className: "px-2 pb-3 font-mono font-normal", children: g.slice(2) }, g)))] }) }), _jsx("tbody", { children: colours.map((c) => (_jsxs("tr", { className: "border-t border-border", children: [_jsx("th", { scope: "row", className: "py-4 pr-4 align-top font-mono text-xs font-medium break-words", children: c.slice(2) }), grounds.map((g) => {
86
120
  const r = ratios[`${c}|${g}`];
87
- const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
88
- return (_jsx("td", { className: "px-4 py-2.5", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { "data-theme": mode, className: "flex h-9 w-12 items-center justify-center rounded-lg border border-(--border) text-base font-bold", style: { background: `var(${g})`, color: `var(${c})` }, "aria-hidden": true, children: "Aa" }), r != null && (_jsxs("span", { className: cx("inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-xs", mark === "text" && "bg-success-subdued text-success-subdued-foreground", mark === "shape" && "bg-muted text-muted-foreground", mark === "none" && "bg-destructive-subdued text-destructive-subdued-foreground"), children: [r.toFixed(1), " ", mark] }))] }) }, g));
121
+ const mark = r == null ? undefined : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
122
+ return (_jsx("td", { className: "px-2 py-4 align-top", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { "data-theme": mode, "aria-hidden": true, className: "flex h-12 items-center justify-center rounded-lg text-lg font-bold ring-1 ring-(--border) ring-inset", style: { background: `var(${g})`, color: `var(${c})` }, children: "Aa" }), mark && r != null && _jsx(Verdict, { mark: mark, ratio: r })] }) }, g));
89
123
  })] }, c))) })] })] }));
90
124
  }
91
125
  /** The primitive ramps, one row per colour family, each step painted from its own token. */
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Live } from "./lib/live.js";
3
4
  import { Grid3x3, Moon, Sun } from "lucide-react";
4
5
  import { useEffect, useState } from "react";
5
6
  import { cx } from "./lib/cx.js";
@@ -22,17 +23,21 @@ export function VariantMatrix({ entry, props = {}, children, axes: only, }) {
22
23
  if (axes.length === 0)
23
24
  return _jsxs("p", { className: "text-sm text-muted-foreground", children: [entry.name, " has no variants."] });
24
25
  const content = children ?? entry.name;
25
- const render = (extra) => (_jsx(Component, { ...props, ...coerce(extra), children: content }));
26
+ const render = (extra) => (_jsx(Live, { children: _jsx(Component, { ...props, ...coerce(extra), children: content }) }));
26
27
  const [rows, cols, ...rest] = axes;
27
28
  const toggle = (_jsx(IconButton, { icon: theme === "dark" ? Sun : Moon, label: theme === "dark" ? "Show in light mode" : "Show in dark mode", pressed: theme !== undefined, onClick: () => setTheme(theme === "dark" ? "light" : "dark") }));
28
- return (_jsxs("div", { "data-slot": "variant-matrix", className: "flex flex-col gap-6", children: [cols ? (_jsxs(Frame, { children: [_jsxs(Toolbar, { children: [_jsxs("span", { className: "flex items-center gap-2 pl-2 font-mono text-xs text-muted-foreground", children: [_jsx(Grid3x3, { "aria-hidden": true, className: "size-(--dt-size-xs)" }), rows.name, " \u00D7 ", cols.name] }), toggle] }), _jsx("div", { "data-docs-stage": "", "data-theme": theme, className: "overflow-x-auto bg-canvas text-canvas-foreground", tabIndex: 0, role: "region", "aria-label": "Variants", children: _jsxs("table", { className: "w-full border-collapse text-sm", children: [_jsxs("caption", { className: "sr-only", children: [entry.name, ": ", rows.name, " by ", cols.name] }), _jsx("thead", { children: _jsxs("tr", { className: "border-b border-border", children: [_jsx("th", { scope: "col", className: "w-0 p-0" }), cols.options.map((c) => (_jsx("th", { scope: "col", title: c.description, className: "px-4 py-3 text-center font-mono text-xs font-medium text-muted-foreground", children: _jsx(OptionName, { axis: cols, option: c.name }) }, c.name)))] }) }), _jsx("tbody", { children: rows.options.map((r) => (_jsxs("tr", { className: "border-b border-border last:border-b-0", children: [_jsx("th", { scope: "row", title: r.description, className: "border-r border-border px-4 py-5 text-left font-mono text-xs font-medium whitespace-nowrap text-muted-foreground", children: _jsx(OptionName, { axis: rows, option: r.name }) }), cols.options.map((c) => (_jsx("td", { "data-slot": "variant-cell", className: "px-4 py-5 text-center align-middle", children: _jsx("span", { className: "inline-flex justify-center", children: render({ [rows.name]: r.name, [cols.name]: c.name }) }) }, c.name)))] }, r.name))) })] }) })] })) : (_jsx(AxisRow, { axis: rows, render: render, theme: theme, toggle: toggle })), rest.map((axis) => (_jsx(AxisRow, { axis: axis, render: render, theme: theme, toggle: toggle }, axis.name))), _jsx(States, { render: render, theme: theme, toggle: toggle, canDisable: "disabled" in entry.props })] }));
29
+ return (_jsxs(Frame, { "data-slot": "variant-matrix", children: [_jsxs(Toolbar, { children: [_jsxs("span", { className: "flex items-center gap-2 pl-2 font-mono text-xs text-muted-foreground", children: [_jsx(Grid3x3, { "aria-hidden": true, className: "size-(--dt-size-xs)" }), cols ? `${rows.name} × ${cols.name}` : rows.name, rest.length > 0 && `, ${rest.map((a) => a.name).join(", ")}`] }), toggle] }), _jsxs("div", { "data-docs-stage": "", "data-theme": theme, className: "flex flex-col gap-12 bg-canvas px-6 py-8 text-canvas-foreground sm:px-8", children: [cols ? (_jsx("div", { className: "overflow-x-auto", tabIndex: 0, role: "region", "aria-label": "Variants", children: _jsxs("table", { className: "w-full border-collapse text-sm", children: [_jsxs("caption", { className: "sr-only", children: [entry.name, ": ", rows.name, " by ", cols.name] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", className: "w-0 p-0" }), cols.options.map((c) => (_jsx("th", { scope: "col", title: c.description, className: "px-4 pb-4 text-center font-mono text-xs font-normal text-muted-foreground", children: _jsx(OptionName, { axis: cols, option: c.name }) }, c.name)))] }) }), _jsx("tbody", { children: rows.options.map((r) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", title: r.description, className: "py-4 pr-6 text-left font-mono text-xs font-normal whitespace-nowrap text-muted-foreground", children: _jsx(OptionName, { axis: rows, option: r.name }) }), cols.options.map((c) => (_jsx("td", { "data-slot": "variant-cell", className: "px-4 py-4 text-center align-middle", children: _jsx("span", { className: "inline-flex justify-center", children: render({ [rows.name]: r.name, [cols.name]: c.name }) }) }, c.name)))] }, r.name))) })] }) })) : (_jsx(AxisRow, { axis: rows, render: render })), rest.map((axis) => (_jsx(AxisRow, { axis: axis, render: render }, axis.name))), _jsx(States, { render: render, canDisable: "disabled" in entry.props })] })] }));
30
+ }
31
+ /** A titled row of figures inside the variants stage. */
32
+ function Group({ title, slot, children }) {
33
+ return (_jsxs("section", { "data-slot": slot, className: "flex flex-col gap-5", children: [_jsx("h3", { className: "font-mono text-xs text-muted-foreground", children: title }), _jsx("div", { className: "flex flex-wrap items-end gap-x-10 gap-y-6", children: children })] }));
29
34
  }
30
35
  /**
31
36
  * The component at its defaults in each state at once: at rest, hovered, focused,
32
37
  * pressed and disabled, pinned without a pointer or a keyboard, so states are
33
38
  * reviewed side by side rather than by mousing over each.
34
39
  */
35
- function States({ render, theme, toggle, canDisable, }) {
40
+ function States({ render, canDisable }) {
36
41
  useEffect(() => installPseudoStates(), []);
37
42
  const states = [
38
43
  { name: "rest" },
@@ -41,10 +46,10 @@ function States({ render, theme, toggle, canDisable, }) {
41
46
  { name: "press", pseudo: "active" },
42
47
  ...(canDisable ? [{ name: "disabled", extra: { disabled: "true" } }] : []),
43
48
  ];
44
- return (_jsxs(Frame, { "data-slot": "variant-states", children: [_jsxs(Toolbar, { children: [_jsx("span", { className: "pl-2 font-mono text-xs text-muted-foreground", children: "state" }), toggle] }), _jsx("div", { "data-docs-stage": "", "data-theme": theme, className: "flex flex-wrap items-end justify-center gap-x-10 gap-y-6 bg-canvas px-6 py-8 text-canvas-foreground", children: states.map((s) => (_jsxs("figure", { className: "flex flex-col items-center gap-3", children: [_jsx("div", { "data-pseudo": s.pseudo, children: render(s.extra ?? {}) }), _jsx("figcaption", { className: "font-mono text-xs text-muted-foreground", children: s.name })] }, s.name))) })] }));
49
+ return (_jsx(Group, { title: "state", slot: "variant-states", children: states.map((s) => (_jsxs("figure", { className: "flex flex-col items-center gap-3", children: [_jsx("div", { "data-pseudo": s.pseudo, children: render(s.extra ?? {}) }), _jsx("figcaption", { className: "font-mono text-xs text-muted-foreground", children: s.name })] }, s.name))) }));
45
50
  }
46
- function AxisRow({ axis, render, theme, toggle, }) {
47
- return (_jsxs(Frame, { "data-slot": "variant-axis", children: [_jsxs(Toolbar, { children: [_jsx("span", { className: "pl-2 font-mono text-xs text-muted-foreground", children: axis.name }), toggle] }), _jsx("div", { "data-docs-stage": "", "data-theme": theme, className: "flex flex-wrap items-end justify-center gap-x-10 gap-y-6 bg-canvas px-6 py-8 text-canvas-foreground", children: axis.options.map((o) => (_jsxs("figure", { className: "flex flex-col items-center gap-3", children: [render({ [axis.name]: o.name }), _jsx("figcaption", { className: "font-mono text-xs text-muted-foreground", title: o.description, children: _jsx(OptionName, { axis: axis, option: o.name }) })] }, o.name))) })] }));
51
+ function AxisRow({ axis, render }) {
52
+ return (_jsx(Group, { title: axis.name, slot: "variant-axis", children: axis.options.map((o) => (_jsxs("figure", { className: "flex flex-col items-center gap-3", children: [render({ [axis.name]: o.name }), _jsx("figcaption", { className: "font-mono text-xs text-muted-foreground", title: o.description, children: _jsx(OptionName, { axis: axis, option: o.name }) })] }, o.name))) }));
48
53
  }
49
54
  /** An option's name, with the default marked by a dot and said for screen readers. */
50
55
  function OptionName({ axis, option }) {
@@ -1,13 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { inline } from "./lib/text.js";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TermHead, TermRow } from "./lib/terms.js";
3
+ import { DocsSection } from "./shell.js";
3
4
  /** The words to use, beside the ones not to: the product's words first, then the system's names that have wrong forms. */
4
5
  export function VoiceTerms({ terms }) {
5
6
  const product = terms.filter((t) => t.kind === "product");
6
7
  const system = terms.filter((t) => t.kind !== "product" && t.wrong?.length);
7
8
  if (product.length + system.length === 0)
8
9
  return _jsx("p", { className: "text-sm text-muted-foreground", children: "No terms yet. Add them to the taxonomy file." });
9
- return (_jsxs("div", { "data-slot": "voice-terms", className: "flex flex-col gap-12", children: [product.length > 0 && _jsx(Group, { title: "In the product", terms: product }), system.length > 0 && _jsx(Group, { title: "In the system", terms: system })] }));
10
+ return (_jsxs(_Fragment, { children: [product.length > 0 && (_jsx(DocsSection, { title: "In the product", lead: "The words the product uses with the people it serves, in copy, on screens and in emails.", children: _jsx(Terms, { terms: product }) })), system.length > 0 && (_jsx(DocsSection, { title: "In the system", lead: "The names the design system gives its parts, and the ones people reach for instead.", children: _jsx(Terms, { terms: system }) }))] }));
10
11
  }
11
- function Group({ title, terms }) {
12
- return (_jsxs("section", { className: "flex flex-col gap-2", children: [_jsx("h3", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: title }), _jsx("dl", { className: "grid gap-x-12 sm:grid-cols-2", children: terms.map((t) => (_jsxs("div", { "data-slot": "term", className: "flex flex-col gap-1.5 border-t border-border py-5", children: [_jsxs("dt", { className: "text-3xl tracking-tight", children: [t.name, _jsx("span", { className: "sr-only", children: ", say this" })] }), (t.wrong ?? []).length > 0 && (_jsxs("dd", { className: "flex flex-wrap gap-x-3 text-base text-muted-foreground", children: [_jsx("span", { className: "sr-only", children: "not " }), (t.wrong ?? []).map((w) => (_jsx("s", { className: "decoration-destructive decoration-2", children: w }, w)))] })), (t.note || t.kind !== "product") && (_jsx("dd", { className: "text-sm text-muted-foreground", children: t.note ? inline(t.note) : `The ${t.kind}'s name in the code.` }))] }, `${t.kind}:${t.name}`))) })] }));
12
+ function Terms({ terms }) {
13
+ return (_jsxs("div", { "data-slot": "voice-terms", children: [_jsx(TermHead, {}), _jsx("dl", { children: terms.map((t) => (_jsx(TermRow, { term: t }, `${t.kind}:${t.name}`))) })] }));
13
14
  }
package/dist/workbench.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Live } from "./lib/live.js";
3
4
  import { Tabs } from "@base-ui/react/tabs";
4
5
  import { Toggle } from "@base-ui/react/toggle";
5
6
  import { ToggleGroup } from "@base-ui/react/toggle-group";
@@ -213,7 +214,7 @@ export function Workbench({ entry, props: base = {}, children: initialText = ent
213
214
  ? { background: "repeating-conic-gradient(var(--muted) 0 25%, var(--canvas) 0 50%) 0 0 / 16px 16px", color: "var(--canvas-foreground)" }
214
215
  : { background: `var(--${view.bg})`, color: `var(--${view.bg}-foreground, var(--canvas-foreground))` };
215
216
  const dotted = view.bg === "canvas";
216
- const node = (_jsx(Component, { ...base, ...logged, ...chosen, children: content ?? (text || undefined) }));
217
+ const node = (_jsx(Live, { children: _jsx(Component, { ...base, ...logged, ...chosen, children: content ?? (text || undefined) }) }));
217
218
  const scope = [
218
219
  entry.name,
219
220
  ...Object.entries(variants).map(([k, v]) => `${k} ${v}`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designtools/docs",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Design system docs as a package: pages, navigation, search, a workbench per component, rules, patterns, brand pages and markdown for agents, generated from the @designtools/manifest and themed by your tokens. One Next adapter serves them inside your app or as a standalone site.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/anatomy.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { Live } from "./lib/live";
3
4
  import { Layers } from "lucide-react";
4
5
  import { useEffect, useId, useRef, useState, type ReactNode } from "react";
5
6
  import { cx, target } from "./lib/cx";
@@ -45,7 +46,11 @@ export function Anatomy({
45
46
  const module = lookup.examplesFor(entry);
46
47
  const first = entry.examples?.canonical?.name ?? entry.examples?.do[0]?.name ?? entry.examples?.other[0]?.name;
47
48
  const example =
48
- children ?? (module && first ? renderExample(module[first], first) : Component ? <Component>{entry.name}</Component> : null);
49
+ children ?? (module && first ? renderExample(module[first], first) : Component ? (
50
+ <Live>
51
+ <Component>{entry.name}</Component>
52
+ </Live>
53
+ ) : null);
49
54
 
50
55
  const shown = entry.slots.filter((slot) => counts[slot]).length;
51
56
  return (
package/src/glossary.tsx CHANGED
@@ -1,71 +1,70 @@
1
1
  "use client";
2
2
 
3
- import { Search } from "lucide-react";
3
+ import { ArrowRight, Search } from "lucide-react";
4
4
  import { useId, useState } from "react";
5
5
  import type { TermEntry } from "./lib/manifest-types";
6
- import { inline } from "./lib/text";
6
+ import { TermRow } from "./lib/terms";
7
7
 
8
- /** Every name in the system, searchable by its wrong forms too, so a wrong name finds the right one. */
8
+ /** Every name in the system, searchable by its wrong forms too, so a wrong name finds the right one. A to Z. */
9
9
  export function Glossary({ terms }: { terms: TermEntry[] }) {
10
10
  const id = useId();
11
11
  const [query, setQuery] = useState("");
12
12
  const q = query.trim().toLowerCase();
13
- const matches = q ? terms.filter((t) => t.name.toLowerCase().includes(q) || t.wrong?.some((w) => w.toLowerCase().includes(q))) : terms;
13
+ const matches = (q ? terms.filter((t) => t.name.toLowerCase().includes(q) || t.wrong?.some((w) => w.toLowerCase().includes(q))) : terms)
14
+ .slice()
15
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
14
16
  const corrected = q ? terms.find((t) => t.wrong?.some((w) => w.toLowerCase() === q)) : undefined;
17
+ const letters = new Map<string, TermEntry[]>();
18
+ for (const t of matches) {
19
+ const l = /[a-z]/i.test(t.name[0]) ? t.name[0].toUpperCase() : "#";
20
+ letters.set(l, [...(letters.get(l) ?? []), t]);
21
+ }
15
22
 
16
23
  return (
17
- <div data-slot="glossary" className="flex flex-col gap-5">
18
- <div className="flex flex-col gap-2">
24
+ <div data-slot="glossary" className="flex flex-col gap-10">
25
+ <div className="flex max-w-3xl flex-col gap-4">
19
26
  <label htmlFor={id} className="sr-only">
20
27
  Find a term
21
28
  </label>
22
- <div className="relative max-w-xl">
23
- <Search aria-hidden className="pointer-events-none absolute top-1/2 left-4 size-(--dt-size-sm) -translate-y-1/2 text-muted-foreground" />
29
+ <div className="relative">
30
+ <Search aria-hidden className="pointer-events-none absolute top-1/2 left-0 size-(--dt-size-md) -translate-y-1/2 text-muted-foreground" />
24
31
  <input
25
32
  id={id}
26
33
  type="search"
27
34
  value={query}
28
35
  onChange={(e) => setQuery(e.target.value)}
29
36
  placeholder="Find a term, or a name you half remember"
30
- className="h-(--dt-size-2xl) w-full rounded-lg border border-border bg-canvas pr-4 pl-12 text-base placeholder:text-muted-foreground hover:border-input focus-visible:outline-2 focus-visible:outline-ring"
37
+ className="h-16 w-full border-0 border-b border-border bg-transparent pr-2 pl-10 text-2xl tracking-tight placeholder:text-muted-foreground/70 hover:border-input focus-visible:border-b-2 focus-visible:border-canvas-foreground focus-visible:outline-none"
31
38
  />
32
39
  </div>
33
- <p aria-live="polite" className="text-sm text-muted-foreground">
40
+ <div aria-live="polite">
34
41
  {corrected ? (
35
- <>
36
- The system calls “{query.trim()}” <strong className="text-canvas-foreground">{corrected.name}</strong>.
37
- </>
38
- ) : q ? (
39
- `${matches.length} ${matches.length === 1 ? "term" : "terms"}`
42
+ <p data-slot="glossary-correction" className="flex flex-wrap items-center gap-2 rounded-xl bg-muted/60 px-5 py-4 text-lg">
43
+ Say <strong className="font-semibold">{corrected.name}</strong>
44
+ <ArrowRight aria-hidden className="size-4 text-muted-foreground" />
45
+ <span className="text-muted-foreground">not “{query.trim()}”</span>
46
+ </p>
40
47
  ) : (
41
- `${terms.length} terms`
48
+ <p className="text-sm text-muted-foreground">
49
+ {q ? `${matches.length} ${matches.length === 1 ? "term" : "terms"}` : `${terms.length} terms, A to Z. Search by the right name or a wrong one.`}
50
+ </p>
42
51
  )}
43
- </p>
52
+ </div>
44
53
  </div>
45
- <dl data-slot="glossary-list" className="grid gap-x-12 sm:grid-cols-2">
46
- {matches.map((t) => (
47
- <div key={`${t.kind}:${t.name}`} className="flex flex-col gap-1.5 border-t border-border py-4">
48
- <dt className="flex items-center justify-between gap-2">
49
- <span className="text-lg">{t.name}</span>
50
- <span className="font-mono text-xs text-muted-foreground">{t.kind}</span>
51
- </dt>
52
- <dd className="flex flex-col gap-1 text-sm text-muted-foreground">
53
- {t.wrong?.length ? (
54
- <span className="flex flex-wrap items-center gap-1">
55
- Not
56
- {t.wrong.map((w) => (
57
- <s key={w} className="decoration-destructive">
58
- {w}
59
- </s>
60
- ))}
61
- </span>
62
- ) : null}
63
- {t.note && <span>{inline(t.note)}</span>}
64
- {t.ref && <code className="font-mono text-xs">{t.ref}</code>}
65
- </dd>
66
- </div>
54
+ <div data-slot="glossary-list" className="flex flex-col gap-8">
55
+ {[...letters].map(([letter, list]) => (
56
+ <section key={letter} aria-label={letter} className="grid gap-x-8 lg:grid-cols-[3rem_minmax(0,1fr)]">
57
+ <h3 className="pt-5 text-2xl tracking-tight text-muted-foreground lg:sticky lg:top-24 lg:self-start">{letter}</h3>
58
+ <div>
59
+ <dl>
60
+ {list.map((t) => (
61
+ <TermRow key={`${t.kind}:${t.name}`} term={t} kind />
62
+ ))}
63
+ </dl>
64
+ </div>
65
+ </section>
67
66
  ))}
68
- </dl>
67
+ </div>
69
68
  </div>
70
69
  );
71
70
  }
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export { Pattern } from "./pattern";
17
17
  export { Playground } from "./playground";
18
18
  export { PreviewComposition, PreviewExample, PreviewFrame, PreviewSurface } from "./preview-frame";
19
19
  export { PropsTable } from "./props-table";
20
- export { Rule } from "./rule";
20
+ export { Rule, RuleIndex } from "./rule";
21
21
  export { Scale } from "./scale";
22
22
  export { DocsSearch } from "./search";
23
23
  export { type DocsLink, DocsFooter, DocsHeader, DocsSection, DocsShell } from "./shell";
@@ -29,6 +29,7 @@ export { Usage } from "./usage";
29
29
  export { VariantMatrix } from "./variant-matrix";
30
30
  export { VoiceTerms } from "./voice-terms";
31
31
  export { Workbench } from "./workbench";
32
+ export { Live } from "./lib/live";
32
33
  export { type Registry, RegistryProvider, useRegistry } from "./lib/registry";
33
34
  export * from "./lib/manifest-types";
34
35
  export type { AdherenceReport } from "./lib/adherence-types";
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Marks the project's own components inside the docs: an example, a workbench, the hero, a rule's
5
+ * demonstration. The docs' styles never match inside it, so the project's own CSS, its responsive
6
+ * utilities included, is all that styles its components, whichever stylesheet loads last. It draws
7
+ * nothing itself (`display: contents`). Wrap the project's components in it on an editorial page.
8
+ */
9
+ export function Live({ children }: { children: ReactNode }) {
10
+ return (
11
+ <div data-docs-content="" style={{ display: "contents" }}>
12
+ {children}
13
+ </div>
14
+ );
15
+ }
@@ -1,3 +1,4 @@
1
+ import { Live } from "./live";
1
2
  import { createElement, isValidElement, type ComponentType, type ReactNode } from "react";
2
3
  import { componentKey } from "./manifest";
3
4
  import type { AssetEntry, ComponentEntry, PatternEntry } from "./manifest-types";
@@ -43,7 +44,7 @@ export function Missing({ name, what = "component" }: { name: string; what?: str
43
44
 
44
45
  /** An example export is a component that takes no props, or an element. */
45
46
  export function renderExample(value: unknown, name: string): ReactNode {
46
- if (isValidElement(value)) return value;
47
- if (typeof value === "function") return createElement(value as ComponentType);
47
+ if (isValidElement(value)) return <Live>{value}</Live>;
48
+ if (typeof value === "function") return <Live>{createElement(value as ComponentType)}</Live>;
48
49
  return <span className="text-xs text-muted-foreground">{name} is not a component or an element.</span>;
49
50
  }
@@ -1,8 +1,8 @@
1
1
  import { cx } from "./cx";
2
2
 
3
- type Any = "stable" | "emerging" | "deprecated" | "confirmed" | "assumption" | "measured" | "draft" | "published";
3
+ export type Status = "stable" | "emerging" | "deprecated" | "confirmed" | "assumption" | "measured" | "draft" | "published";
4
4
 
5
- const LOOK: Record<Any, string> = {
5
+ const LOOK: Record<Status, string> = {
6
6
  stable: "bg-success-subdued text-success-subdued-foreground",
7
7
  confirmed: "bg-success-subdued text-success-subdued-foreground",
8
8
  published: "bg-success-subdued text-success-subdued-foreground",
@@ -15,7 +15,7 @@ const LOOK: Record<Any, string> = {
15
15
  };
16
16
 
17
17
  /** A status, everywhere one is shown: components, patterns, rules and editorial pages. Unset says so. */
18
- export function StatusBadge({ status, className }: { status?: Any; className?: string }) {
18
+ export function StatusBadge({ status, className }: { status?: Status; className?: string }) {
19
19
  return (
20
20
  <span
21
21
  data-slot="status-badge"
@@ -0,0 +1,53 @@
1
+ import { X } from "lucide-react";
2
+ import type { TermEntry } from "./manifest-types";
3
+ import { inline } from "./text";
4
+
5
+ /** The wrong forms of a term: quiet chips, each with a cross, so "not this" reads without shouting. */
6
+ export function WrongForms({ wrong }: { wrong?: string[] }) {
7
+ if (!wrong?.length) return <span className="text-sm text-muted-foreground">None</span>;
8
+ return (
9
+ <span className="flex flex-wrap gap-1.5">
10
+ <span className="sr-only">Not </span>
11
+ {wrong.map((w) => (
12
+ <span key={w} data-slot="term-wrong" className="inline-flex min-h-7 items-center gap-1 rounded-md bg-muted px-2 text-sm text-muted-foreground">
13
+ <X aria-hidden className="size-3 shrink-0" />
14
+ <span className="line-through decoration-current/40">{w}</span>
15
+ </span>
16
+ ))}
17
+ </span>
18
+ );
19
+ }
20
+
21
+ /**
22
+ * One term on one row: the word to use, the words not to, and why. Three columns on a wide screen, under
23
+ * the labels `TermHead` draws; stacked on a phone.
24
+ */
25
+ export function TermRow({ term, kind }: { term: TermEntry; kind?: boolean }) {
26
+ return (
27
+ <div data-slot="term" className="grid gap-x-8 gap-y-2 border-t border-border py-5 md:grid-cols-[minmax(10rem,14rem)_minmax(0,1fr)_minmax(0,1.3fr)] md:items-baseline">
28
+ <dt className="flex items-baseline gap-2">
29
+ <span className="text-xl leading-snug tracking-tight">{term.name}</span>
30
+ {kind && <span className="font-mono text-xs text-muted-foreground">{term.kind}</span>}
31
+ <span className="sr-only">, say this</span>
32
+ </dt>
33
+ <dd>
34
+ <WrongForms wrong={term.wrong} />
35
+ </dd>
36
+ <dd className="text-sm leading-relaxed text-muted-foreground">
37
+ {term.note ? inline(term.note) : term.kind !== "product" ? `The ${term.kind}'s name in the code.` : null}
38
+ {term.ref && <code className="ml-1 font-mono text-xs">{term.ref}</code>}
39
+ </dd>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ /** The column labels over a list of `TermRow`s, on a wide screen. */
45
+ export function TermHead() {
46
+ return (
47
+ <div aria-hidden className="hidden gap-x-8 pb-3 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[minmax(10rem,14rem)_minmax(0,1fr)_minmax(0,1.3fr)]">
48
+ <span>Say</span>
49
+ <span>Not</span>
50
+ <span>Why</span>
51
+ </div>
52
+ );
53
+ }
@@ -1,14 +1,30 @@
1
1
  import type { ComponentType, ReactElement, ReactNode } from "react";
2
2
  import type { AdherenceReport } from "../lib/adherence-types";
3
3
 
4
- /** A page people write: brand, principles, a design language. Markdown, with its author and how settled it is. */
5
- export interface EditorialPage {
4
+ /** A page people write: brand, principles, a design language, with its author and how settled it is. */
5
+ export type EditorialPage = EditorialBase &
6
+ (
7
+ | {
8
+ /** Markdown: `##` and `###` sections, paragraphs, bullet lists, bold, code and links; a list whose items open in bold is a set of principles. */
9
+ body: string;
10
+ markdown?: undefined;
11
+ }
12
+ | {
13
+ /**
14
+ * The page as React: real components from the project's system beside the docs' own `DocsSection`
15
+ * and `Prose`, for a page that has to show rather than tell. Rendered as is, under the page header.
16
+ */
17
+ body: ReactNode;
18
+ /** What agents read instead, at the page's `.md`: the same page, in words. */
19
+ markdown: string;
20
+ }
21
+ );
22
+
23
+ interface EditorialBase {
6
24
  /** Its path under the docs, e.g. `brand` or `brand/accessibility`. */
7
25
  slug: string;
8
26
  title: string;
9
27
  lead?: string;
10
- /** Markdown: `##` sections, paragraphs, bullet lists; a list whose items open in bold is a set of principles. */
11
- body: string;
12
28
  by: { author: string; reviewer?: string; reviewed: string; status: "draft" | "published" };
13
29
  /** The navigation group it sits in. Default: brand. */
14
30
  section?: "brand" | "foundations" | "standards";
@@ -47,6 +63,11 @@ export interface DocsConfig {
47
63
  hero?: ReactNode;
48
64
  /** The overview's lead, when there is no hero. */
49
65
  lead?: string;
66
+ /**
67
+ * The overview after its hero or header: `agentic: false` drops "Why this is agentic", and `agenticLead`
68
+ * replaces the line that opens it.
69
+ */
70
+ overview?: { agentic?: boolean; agenticLead?: string };
50
71
  /** The generated registry (`designtools-docs map`): a provider for the project's components, examples and assets. */
51
72
  registry: ComponentType<{ children: ReactNode }>;
52
73
  /** The manifest, as `designtools-manifest build` writes it. */
@@ -10,7 +10,10 @@ import { manifestFiles, manifestRoute, markdownRoute } from "./markdown";
10
10
  import { DocsPageView } from "./pages";
11
11
  import { allSlugs, metaOf, navOf, resolve, siteOf } from "./site";
12
12
 
13
+ export { Live } from "../lib/live";
14
+ export { DocsSection } from "../shell";
13
15
  export type { BuiltInPage, DocsConfig, EditorialPage, WorkbenchSetup } from "./config";
16
+ export { Markdown as Prose } from "./prose";
14
17
  export { markdownFor } from "./markdown";
15
18
  export { docsMatcher, docsProxy, prefersMarkdown } from "./proxy";
16
19