@designtools/docs 0.2.0 → 0.2.2

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/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,13 +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
- // one level of framing: a panel per mode (the dark one has to be dark), chips inside it, and nothing around the family
26
- const panel = (mode) => cx("flex flex-col gap-3 rounded-2xl bg-canvas p-4 text-canvas-foreground", mode === "light" && "ring-1 ring-border ring-inset");
27
- const modeLabel = (mode) => _jsx("span", { className: "text-xs font-medium text-muted-foreground capitalize", children: mode });
28
- const row = (title, meta, body, key) => (_jsxs("div", { "data-slot": "swatch-row", className: "grid gap-x-10 gap-y-5 border-t border-border pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[11rem_minmax(0,1fr)]", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("h3", { className: "text-2xl leading-tight tracking-tight", children: title }), meta] }), body] }, key ?? title));
29
- return (_jsxs("div", { "data-slot": "swatches", className: "flex flex-col gap-10", children: [families.map((f) => row((([first, ...rest]) => first.toUpperCase() + rest.join(""))(f.slice(2).replace(/-/g, " ")), _jsx("code", { className: "font-mono text-xs text-muted-foreground", children: f }), _jsx("div", { className: cx("grid gap-4", modes.length > 1 && "md:grid-cols-2"), children: modes.map((mode) => (_jsxs("div", { "data-theme": mode, "data-slot": "swatch-family", className: panel(mode), children: [modeLabel(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 &&
30
- row("Surfaces", _jsx("p", { className: "text-sm text-muted-foreground", children: "The page, panels and popups, each with its text." }), _jsx("div", { className: cx("grid gap-4", modes.length > 1 && "md:grid-cols-2"), children: modes.map((mode) => (_jsxs("div", { "data-theme": mode, className: panel(mode), children: [modeLabel(mode), _jsx("div", { className: "grid grid-cols-2 gap-2 xl: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 &&
31
- row("Lines, focus and shadow", _jsx("p", { className: "text-sm text-muted-foreground", children: "Borders, input edges, the focus ring and the shadow colour." }), _jsx("div", { className: cx("grid gap-4", modes.length > 1 && "md:grid-cols-2"), children: modes.map((mode) => (_jsxs("div", { "data-theme": mode, className: panel(mode), children: [modeLabel(mode), _jsx("ul", { className: "grid grid-cols-2 gap-x-4 gap-y-3", children: utility.map((name) => (_jsxs("li", { "data-slot": "swatch", className: "flex items-center gap-3", children: [_jsx("span", { "aria-hidden": true, className: "size-9 shrink-0 rounded-lg 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)))] }));
32
38
  }
33
39
  /** `var(--color-primary-500)` → `primary-500`: the step it points at, which is what a person compares. */
34
40
  function pointsAt(value) {
@@ -38,11 +44,11 @@ function pointsAt(value) {
38
44
  return value?.startsWith("oklch(") ? "generated" : value;
39
45
  }
40
46
  /**
41
- * One background with its foreground on it, labelled in that foreground, with the
42
- * contrast measured on what the browser paints. It reads its mode from the nearest
43
- * `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.
44
50
  */
45
- function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md", outlined, }) {
51
+ function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md", }) {
46
52
  const chip = useRef(null);
47
53
  const [ratio, setRatio] = useState(null);
48
54
  useEffect(() => {
@@ -55,20 +61,42 @@ function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md
55
61
  const grade = ratio === null ? null : wcagGrade(ratio);
56
62
  const fails = grade === "Fail";
57
63
  const measured = grade && ratio !== null ? `${ratio.toFixed(1)}:1 ${grade}` : undefined;
58
- 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] })] }));
59
74
  }
60
75
  /**
61
76
  * Each colour on each background, per mode: a label is measured on its own fill, and this is
62
77
  * everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
63
78
  * canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
64
- * 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.
65
80
  * Measured on what the browser paints, inside each mode's `data-theme`.
66
81
  */
67
82
  export function PageContrast({ tokens, modes = ["light", "dark"] }) {
68
83
  const names = new Set(semanticColours(tokens).map((c) => c.name));
69
84
  const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
70
85
  const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
71
- return (_jsx("div", { "data-slot": "page-contrast", className: "flex flex-col gap-12", 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 })] }));
72
100
  }
73
101
  function PageGrid({ mode, colours, grounds }) {
74
102
  const probe = useRef(null);
@@ -88,10 +116,10 @@ function PageGrid({ mode, colours, grounds }) {
88
116
  out[`${c}|${g}`] = contrastRatio(painted[c], painted[g]);
89
117
  setRatios(out);
90
118
  }, [colours, grounds]);
91
- return (_jsxs("div", { className: "-mx-1 overflow-x-auto px-1", 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-[40rem] border-collapse text-left text-sm", children: [_jsx("caption", { className: "pb-4 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: "pr-4 pb-3 font-medium", children: "Colour" }), grounds.map((g) => (_jsx("th", { scope: "col", className: "px-3 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-3.5 pr-4 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) => {
92
120
  const r = ratios[`${c}|${g}`];
93
- const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
94
- return (_jsx("td", { className: "px-3 py-3.5", children: _jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("span", { "data-theme": mode, className: "flex h-10 w-14 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));
95
123
  })] }, c))) })] })] }));
96
124
  }
97
125
  /** The primitive ramps, one row per colour family, each step painted from its own token. */
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 { handlerNames } from "./lib/handlers.js";
3
4
  import { Live } from "./lib/live.js";
4
5
  import { Tabs } from "@base-ui/react/tabs";
5
6
  import { Toggle } from "@base-ui/react/toggle";
@@ -75,7 +76,7 @@ export function Workbench({ entry, props: base = {}, children: initialText = ent
75
76
  const own = Object.entries(entry.props).filter(([name, p]) => !axisNames.has(name) && !PLUMBING.has(name) && p.deprecated === undefined);
76
77
  // disabled is a pinned state ("Off"), not a prop control
77
78
  const settable = own.filter(([name, p]) => !isFunction(name, p) && !p.from && name !== "disabled");
78
- const handlers = own.filter(([name, p]) => isFunction(name, p)).map(([name]) => name);
79
+ const handlers = handlerNames(Object.fromEntries(own));
79
80
  const canDisable = "disabled" in entry.props;
80
81
  const [variants, setVariants] = useState({});
81
82
  const [values, setValues] = useState({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designtools/docs",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
@@ -0,0 +1,14 @@
1
+ import type { PropEntry } from "./manifest-types";
2
+
3
+ const isFunction = (name: string, p: PropEntry) => /=>|^\(|Function|Handler/.test(p.type) || /^on[A-Z]/.test(name);
4
+
5
+ /**
6
+ * The props the workbench logs to its Actions tab: event handlers only. A function prop that is not a
7
+ * handler (getAriaValueText, a formatter, a render function) is called while the component renders, and
8
+ * logging it re-renders the component, which calls it again, for ever.
9
+ */
10
+ export function handlerNames(props: Record<string, PropEntry>): string[] {
11
+ return Object.entries(props)
12
+ .filter(([name, p]) => isFunction(name, p) && /^on[A-Z]/.test(name))
13
+ .map(([name]) => name);
14
+ }
@@ -1,7 +1,5 @@
1
1
  import { ArrowRight, ArrowUpRight } from "lucide-react";
2
2
  import Link from "next/link";
3
- import type { ReactNode } from "react";
4
- import { cx, display } from "../lib/cx";
5
3
  import { AdherenceSummary } from "../adherence-summary";
6
4
  import { Anatomy } from "../anatomy";
7
5
  import { Examples } from "../examples";
@@ -307,112 +305,57 @@ function Overview({ site }: { site: Site }) {
307
305
  const count = families.reduce((n, g) => n + g.families.length, 0);
308
306
  const brandPages = site.editorial.filter((p) => (p.section ?? "brand") === "brand");
309
307
  const brandFirst = brandPages[0];
310
- const products = site.taxonomy.terms.filter((t) => t.kind === "product" && t.wrong?.length);
311
- const example = products[0] ?? site.taxonomy.terms.find((t) => t.wrong?.length);
312
- const fills = ["--primary", "--neutral", "--success", "--warning", "--destructive"].filter((n) =>
313
- site.tokens.tokens.some((t) => t.name === n),
314
- );
315
308
  const share = config.adherence?.summary.share;
316
309
 
317
- // each section of the system, why it exists, how much is in it, and a glimpse of it, live from the manifest
318
- const sections: { title: string; href: string; why: string; count: string; preview: ReactNode }[] = [];
310
+ // each section of the system, why it exists and how much is in it
311
+ const sections: { title: string; href: string; why: string; count: string }[] = [];
319
312
  if (brandFirst || site.shows("logo"))
320
313
  sections.push({
321
314
  title: "Brand",
322
315
  href: `${base}/${brandFirst?.slug ?? "brand/logo"}`,
323
316
  why: "What the brand believes, how it sounds and how it looks, so every screen and sentence comes from the same place.",
324
- count: `${brandPages.length + (site.assets.assets.some((a) => a.kind === "logo") ? 1 : 0)} pages`,
325
- preview: config.logo ? <span className="flex h-10 items-center [&_img]:h-8 [&_svg]:h-8">{config.logo}</span> : null,
317
+ count: `${brandPages.length + (site.assets.assets.some((a) => a.kind === "logo") ? 1 : 0)} pages`
326
318
  });
327
319
  sections.push({
328
320
  title: "Foundations",
329
321
  href: `${base}/colour`,
330
322
  why: "Colour, type and the scale, generated from the tokens and measured in both modes, so nothing is a guess.",
331
323
  count: `${site.tokens.tokens.length} tokens`,
332
- preview: (
333
- <span className="flex items-center gap-4">
334
- <span className="flex -space-x-1.5">
335
- {fills.map((n) => (
336
- <span key={n} aria-hidden className="size-8 rounded-full ring-2 ring-canvas" style={{ background: `var(${n})` }} />
337
- ))}
338
- </span>
339
- <span aria-hidden className={cx(display, "text-3xl leading-none tracking-tight")}>
340
- Aa
341
- </span>
342
- </span>
343
- ),
344
324
  });
345
325
  if (site.rules.rules.length)
346
326
  sections.push({
347
327
  title: "Rules",
348
328
  href: `${base}/rules`,
349
329
  why: "The lines the brand and the interface hold, each written for people, with the check that proves it.",
350
- count: `${site.rules.rules.length} rules, ${site.rules.rules.filter((r) => r.status === "confirmed").length} confirmed`,
351
- preview: (
352
- <span className="flex flex-col gap-1.5 text-sm">
353
- {site.rules.rules
354
- .filter((r) => r.kind === "brand")
355
- .slice(0, 2)
356
- .map((r) => (
357
- <span key={r.id} className="flex items-center gap-2">
358
- <StatusBadge status={r.status} />
359
- <span className="truncate">{r.title}</span>
360
- </span>
361
- ))}
362
- </span>
363
- ),
330
+ count: `${site.rules.rules.length} rules, ${site.rules.rules.filter((r) => r.status === "confirmed").length} confirmed`
364
331
  });
365
332
  if (site.shows("components") && count)
366
333
  sections.push({
367
334
  title: "Components",
368
335
  href: `${base}/components/${slugOf(families[0].families[0].head)}`,
369
336
  why: "The real components the product is built from, each with when to use it, a live workbench and an accessibility report.",
370
- count: `${count} components`,
371
- preview: (
372
- <span className="flex flex-wrap gap-1.5">
373
- {families.slice(0, 6).map((g) => (
374
- <span key={g.category} className="inline-flex min-h-7 items-center rounded-md bg-muted px-2 text-xs">
375
- {g.category} <span className="ml-1.5 text-muted-foreground tabular-nums">{g.families.length}</span>
376
- </span>
377
- ))}
378
- </span>
379
- ),
337
+ count: `${count} components`
380
338
  });
381
339
  if (site.patterns.patterns.length)
382
340
  sections.push({
383
341
  title: "Patterns",
384
342
  href: `${base}/patterns/${site.patterns.patterns[0].id}`,
385
343
  why: "The screens the product keeps making, composed of the components, so a recurring problem is solved once.",
386
- count: `${site.patterns.patterns.length} patterns`,
387
- preview: <span className="text-sm text-muted-foreground">{site.patterns.patterns.slice(0, 3).map((p) => p.name).join(" · ")}</span>,
344
+ count: `${site.patterns.patterns.length} patterns`
388
345
  });
389
346
  if (site.shows("standards") && (config.baselines || config.adherence))
390
347
  sections.push({
391
348
  title: "Standards",
392
349
  href: `${base}/standards`,
393
350
  why: "The bar the product is held to, and how closely it follows the system today, reported on every change.",
394
- count: share == null ? "Baselines agreed" : `${Math.round(share * 100)}% of the product on the system`,
395
- preview:
396
- share == null ? null : (
397
- <span className="flex w-full max-w-56 items-center gap-3">
398
- <span className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
399
- <span className="block h-full rounded-full bg-canvas-foreground" style={{ width: `${Math.round(share * 100)}%` }} />
400
- </span>
401
- <span className="text-sm tabular-nums">{Math.round(share * 100)}%</span>
402
- </span>
403
- ),
351
+ count: share == null ? "Baselines agreed" : `${Math.round(share * 100)}% of the product on the system`
404
352
  });
405
353
  if (site.shows("vocabulary") && site.taxonomy.terms.length)
406
354
  sections.push({
407
355
  title: "Vocabulary",
408
356
  href: `${base}/vocabulary`,
409
357
  why: "One name for each thing, and the names people reach for instead, so people and agents mean the same thing.",
410
- count: `${site.taxonomy.terms.length} terms`,
411
- preview: example ? (
412
- <span className="text-sm">
413
- Say <strong className="font-semibold">{example.name}</strong>, <span className="text-muted-foreground line-through decoration-current/40">{example.wrong?.[0]}</span>
414
- </span>
415
- ) : null,
358
+ count: `${site.taxonomy.terms.length} terms`
416
359
  });
417
360
 
418
361
  const loop = [
@@ -441,7 +384,6 @@ function Overview({ site }: { site: Site }) {
441
384
  <span className="shrink-0 text-xs text-muted-foreground tabular-nums">{sec.count}</span>
442
385
  </span>
443
386
  <span className="max-w-md text-base leading-relaxed text-muted-foreground">{sec.why}</span>
444
- {sec.preview && <span className="mt-auto flex min-h-10 items-center pt-2">{sec.preview}</span>}
445
387
  </Link>
446
388
  </li>
447
389
  ))}