@designtools/docs 0.2.1 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @designtools/docs
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - c4b61fb: The workbench no longer freezes on a component with a function prop it calls while rendering (Base UI's Meter and Progress, through `getAriaValueText`): only event handlers (`on…`) are logged to Actions now, so logging can never re-render the component into calling it again. The overview's section list loses its glimpses (colour dots, the logo, sample rules): each part is its name, its count and why it exists.
8
+
3
9
  ## 0.2.1
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,7 @@
1
+ import type { PropEntry } from "./manifest-types.js";
2
+ /**
3
+ * The props the workbench logs to its Actions tab: event handlers only. A function prop that is not a
4
+ * handler (getAriaValueText, a formatter, a render function) is called while the component renders, and
5
+ * logging it re-renders the component, which calls it again, for ever.
6
+ */
7
+ export declare function handlerNames(props: Record<string, PropEntry>): string[];
@@ -0,0 +1,11 @@
1
+ const isFunction = (name, p) => /=>|^\(|Function|Handler/.test(p.type) || /^on[A-Z]/.test(name);
2
+ /**
3
+ * The props the workbench logs to its Actions tab: event handlers only. A function prop that is not a
4
+ * handler (getAriaValueText, a formatter, a render function) is called while the component renders, and
5
+ * logging it re-renders the component, which calls it again, for ever.
6
+ */
7
+ export function handlerNames(props) {
8
+ return Object.entries(props)
9
+ .filter(([name, p]) => isFunction(name, p) && /^on[A-Z]/.test(name))
10
+ .map(([name]) => name);
11
+ }
@@ -1,14 +1,12 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowRight, ArrowUpRight } from "lucide-react";
3
3
  import Link from "next/link";
4
- import { cx, display } from "../lib/cx.js";
5
4
  import { AdherenceSummary } from "../adherence-summary.js";
6
5
  import { Anatomy } from "../anatomy.js";
7
6
  import { Examples } from "../examples.js";
8
7
  import { Glossary } from "../glossary.js";
9
8
  import { Imagery } from "../imagery.js";
10
9
  import { groupFamilies, slugOf } from "../lib/manifest.js";
11
- import { StatusBadge } from "../lib/status.js";
12
10
  import { LogoUsage } from "../logo-usage.js";
13
11
  import { PageActions } from "../page-actions.js";
14
12
  import { Pattern } from "../pattern.js";
@@ -112,69 +110,56 @@ function Overview({ site }) {
112
110
  const count = families.reduce((n, g) => n + g.families.length, 0);
113
111
  const brandPages = site.editorial.filter((p) => (p.section ?? "brand") === "brand");
114
112
  const brandFirst = brandPages[0];
115
- const products = site.taxonomy.terms.filter((t) => t.kind === "product" && t.wrong?.length);
116
- const example = products[0] ?? site.taxonomy.terms.find((t) => t.wrong?.length);
117
- const fills = ["--primary", "--neutral", "--success", "--warning", "--destructive"].filter((n) => site.tokens.tokens.some((t) => t.name === n));
118
113
  const share = config.adherence?.summary.share;
119
- // each section of the system, why it exists, how much is in it, and a glimpse of it, live from the manifest
114
+ // each section of the system, why it exists and how much is in it
120
115
  const sections = [];
121
116
  if (brandFirst || site.shows("logo"))
122
117
  sections.push({
123
118
  title: "Brand",
124
119
  href: `${base}/${brandFirst?.slug ?? "brand/logo"}`,
125
120
  why: "What the brand believes, how it sounds and how it looks, so every screen and sentence comes from the same place.",
126
- count: `${brandPages.length + (site.assets.assets.some((a) => a.kind === "logo") ? 1 : 0)} pages`,
127
- preview: config.logo ? _jsx("span", { className: "flex h-10 items-center [&_img]:h-8 [&_svg]:h-8", children: config.logo }) : null,
121
+ count: `${brandPages.length + (site.assets.assets.some((a) => a.kind === "logo") ? 1 : 0)} pages`
128
122
  });
129
123
  sections.push({
130
124
  title: "Foundations",
131
125
  href: `${base}/colour`,
132
126
  why: "Colour, type and the scale, generated from the tokens and measured in both modes, so nothing is a guess.",
133
127
  count: `${site.tokens.tokens.length} tokens`,
134
- preview: (_jsxs("span", { className: "flex items-center gap-4", children: [_jsx("span", { className: "flex -space-x-1.5", children: fills.map((n) => (_jsx("span", { "aria-hidden": true, className: "size-8 rounded-full ring-2 ring-canvas", style: { background: `var(${n})` } }, n))) }), _jsx("span", { "aria-hidden": true, className: cx(display, "text-3xl leading-none tracking-tight"), children: "Aa" })] })),
135
128
  });
136
129
  if (site.rules.rules.length)
137
130
  sections.push({
138
131
  title: "Rules",
139
132
  href: `${base}/rules`,
140
133
  why: "The lines the brand and the interface hold, each written for people, with the check that proves it.",
141
- count: `${site.rules.rules.length} rules, ${site.rules.rules.filter((r) => r.status === "confirmed").length} confirmed`,
142
- preview: (_jsx("span", { className: "flex flex-col gap-1.5 text-sm", children: site.rules.rules
143
- .filter((r) => r.kind === "brand")
144
- .slice(0, 2)
145
- .map((r) => (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(StatusBadge, { status: r.status }), _jsx("span", { className: "truncate", children: r.title })] }, r.id))) })),
134
+ count: `${site.rules.rules.length} rules, ${site.rules.rules.filter((r) => r.status === "confirmed").length} confirmed`
146
135
  });
147
136
  if (site.shows("components") && count)
148
137
  sections.push({
149
138
  title: "Components",
150
139
  href: `${base}/components/${slugOf(families[0].families[0].head)}`,
151
140
  why: "The real components the product is built from, each with when to use it, a live workbench and an accessibility report.",
152
- count: `${count} components`,
153
- preview: (_jsx("span", { className: "flex flex-wrap gap-1.5", children: families.slice(0, 6).map((g) => (_jsxs("span", { className: "inline-flex min-h-7 items-center rounded-md bg-muted px-2 text-xs", children: [g.category, " ", _jsx("span", { className: "ml-1.5 text-muted-foreground tabular-nums", children: g.families.length })] }, g.category))) })),
141
+ count: `${count} components`
154
142
  });
155
143
  if (site.patterns.patterns.length)
156
144
  sections.push({
157
145
  title: "Patterns",
158
146
  href: `${base}/patterns/${site.patterns.patterns[0].id}`,
159
147
  why: "The screens the product keeps making, composed of the components, so a recurring problem is solved once.",
160
- count: `${site.patterns.patterns.length} patterns`,
161
- preview: _jsx("span", { className: "text-sm text-muted-foreground", children: site.patterns.patterns.slice(0, 3).map((p) => p.name).join(" · ") }),
148
+ count: `${site.patterns.patterns.length} patterns`
162
149
  });
163
150
  if (site.shows("standards") && (config.baselines || config.adherence))
164
151
  sections.push({
165
152
  title: "Standards",
166
153
  href: `${base}/standards`,
167
154
  why: "The bar the product is held to, and how closely it follows the system today, reported on every change.",
168
- count: share == null ? "Baselines agreed" : `${Math.round(share * 100)}% of the product on the system`,
169
- preview: share == null ? null : (_jsxs("span", { className: "flex w-full max-w-56 items-center gap-3", children: [_jsx("span", { className: "h-1.5 flex-1 overflow-hidden rounded-full bg-muted", children: _jsx("span", { className: "block h-full rounded-full bg-canvas-foreground", style: { width: `${Math.round(share * 100)}%` } }) }), _jsxs("span", { className: "text-sm tabular-nums", children: [Math.round(share * 100), "%"] })] })),
155
+ count: share == null ? "Baselines agreed" : `${Math.round(share * 100)}% of the product on the system`
170
156
  });
171
157
  if (site.shows("vocabulary") && site.taxonomy.terms.length)
172
158
  sections.push({
173
159
  title: "Vocabulary",
174
160
  href: `${base}/vocabulary`,
175
161
  why: "One name for each thing, and the names people reach for instead, so people and agents mean the same thing.",
176
- count: `${site.taxonomy.terms.length} terms`,
177
- preview: example ? (_jsxs("span", { className: "text-sm", children: ["Say ", _jsx("strong", { className: "font-semibold", children: example.name }), ", ", _jsx("span", { className: "text-muted-foreground line-through decoration-current/40", children: example.wrong?.[0] })] })) : null,
162
+ count: `${site.taxonomy.terms.length} terms`
178
163
  });
179
164
  const loop = [
180
165
  ["Written once", "The components, tokens and rules live in the product's own code. Nobody keeps a second copy in a slide deck or a wiki."],
@@ -183,6 +168,6 @@ function Overview({ site }) {
183
168
  ["Checked", "Each rule names the check that proves it. Stale docs stop a commit; everything else is reported."],
184
169
  ["Measured", "A report on every change shows how closely the product follows the system. It informs the conversation; it never blocks the work."],
185
170
  ];
186
- return (_jsxs(_Fragment, { children: [!config.hero && (_jsx(DocsHeader, { title: site.title, lead: config.lead ?? "The brand and the design system in one place, for the people who make the product and the agents that build it with them.", children: _jsx(PageActions, { basePath: base }) })), _jsx(DocsSection, { title: "What's here, and why", lead: "Each part of the system has one job. Together they mean the product, its people and its agents work from the same source.", children: _jsx("ul", { "data-slot": "overview-sections", className: "grid gap-x-12 md:grid-cols-2", children: sections.map((sec) => (_jsx("li", { className: "border-t border-border", children: _jsxs(Link, { href: sec.href, className: "group flex h-full flex-col gap-4 py-7 focus-visible:outline-2 focus-visible:outline-ring", children: [_jsxs("span", { className: "flex items-baseline justify-between gap-4", children: [_jsx("span", { className: "text-2xl leading-tight tracking-tight underline-offset-[6px] group-hover:underline", children: sec.title }), _jsx("span", { className: "shrink-0 text-xs text-muted-foreground tabular-nums", children: sec.count })] }), _jsx("span", { className: "max-w-md text-base leading-relaxed text-muted-foreground", children: sec.why }), sec.preview && _jsx("span", { className: "mt-auto flex min-h-10 items-center pt-2", children: sec.preview })] }) }, sec.title))) }) }), config.overview?.agentic !== false && (_jsxs(DocsSection, { title: "Why this is agentic", lead: config.overview?.agenticLead ??
171
+ return (_jsxs(_Fragment, { children: [!config.hero && (_jsx(DocsHeader, { title: site.title, lead: config.lead ?? "The brand and the design system in one place, for the people who make the product and the agents that build it with them.", children: _jsx(PageActions, { basePath: base }) })), _jsx(DocsSection, { title: "What's here, and why", lead: "Each part of the system has one job. Together they mean the product, its people and its agents work from the same source.", children: _jsx("ul", { "data-slot": "overview-sections", className: "grid gap-x-12 md:grid-cols-2", children: sections.map((sec) => (_jsx("li", { className: "border-t border-border", children: _jsxs(Link, { href: sec.href, className: "group flex h-full flex-col gap-4 py-7 focus-visible:outline-2 focus-visible:outline-ring", children: [_jsxs("span", { className: "flex items-baseline justify-between gap-4", children: [_jsx("span", { className: "text-2xl leading-tight tracking-tight underline-offset-[6px] group-hover:underline", children: sec.title }), _jsx("span", { className: "shrink-0 text-xs text-muted-foreground tabular-nums", children: sec.count })] }), _jsx("span", { className: "max-w-md text-base leading-relaxed text-muted-foreground", children: sec.why })] }) }, sec.title))) }) }), config.overview?.agentic !== false && (_jsxs(DocsSection, { title: "Why this is agentic", lead: config.overview?.agenticLead ??
187
172
  "A design system used to be a document people were asked to follow. This one is the source the product, its people and its AI agents all work from, and it checks itself.", children: [_jsx("ol", { "data-slot": "overview-loop", className: "grid gap-x-6 gap-y-8 sm:grid-cols-2 xl:grid-cols-5", children: loop.map(([title, body], i) => (_jsxs("li", { className: "relative flex flex-col gap-3 xl:pr-2", children: [_jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-full bg-canvas-foreground text-sm font-medium text-canvas tabular-nums", children: i + 1 }), i < loop.length - 1 && _jsx("span", { "aria-hidden": true, className: "hidden h-px flex-1 bg-border xl:block" })] }), _jsx("span", { className: "text-lg leading-snug tracking-tight", children: title }), _jsx("span", { className: "text-sm leading-relaxed text-muted-foreground", children: body })] }, title))) }), _jsxs("p", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground", children: ["See it the way an agent does:", _jsxs("a", { href: `${base}.md`, className: "inline-flex items-center gap-1 font-medium text-canvas-foreground underline underline-offset-4 hover:decoration-2", children: ["this page as markdown ", _jsx(ArrowUpRight, { "aria-hidden": true, className: "size-3.5" })] })] })] })), _jsx(DocsFooter, { provenance: config.provenance })] }));
188
173
  }
package/dist/styles.css CHANGED
@@ -10,7 +10,6 @@
10
10
  --tw-rotate-z: initial;
11
11
  --tw-skew-x: initial;
12
12
  --tw-skew-y: initial;
13
- --tw-space-x-reverse: 0;
14
13
  --tw-divide-y-reverse: 0;
15
14
  --tw-border-style: solid;
16
15
  --tw-gradient-position: initial;
@@ -630,10 +629,6 @@
630
629
  margin-left: .25rem;
631
630
  }
632
631
 
633
- :where([data-designtools]) .ml-1\.5:not([data-docs-content] *), .ml-1\.5:where([data-designtools]):not([data-docs-content] *) {
634
- margin-left: .375rem;
635
- }
636
-
637
632
  :where([data-designtools]) .ml-3:not([data-docs-content] *), .ml-3:where([data-designtools]):not([data-docs-content] *) {
638
633
  margin-left: .75rem;
639
634
  }
@@ -783,10 +778,6 @@
783
778
  height: .125rem;
784
779
  }
785
780
 
786
- :where([data-designtools]) .h-1\.5:not([data-docs-content] *), .h-1\.5:where([data-designtools]):not([data-docs-content] *) {
787
- height: .375rem;
788
- }
789
-
790
781
  :where([data-designtools]) .h-2:not([data-docs-content] *), .h-2:where([data-designtools]):not([data-docs-content] *) {
791
782
  height: .5rem;
792
783
  }
@@ -903,10 +894,6 @@
903
894
  min-height: 2.25rem;
904
895
  }
905
896
 
906
- :where([data-designtools]) .min-h-10:not([data-docs-content] *), .min-h-10:where([data-designtools]):not([data-docs-content] *) {
907
- min-height: 2.5rem;
908
- }
909
-
910
897
  :where([data-designtools]) .min-h-11:not([data-docs-content] *), .min-h-11:where([data-designtools]):not([data-docs-content] *) {
911
898
  min-height: 2.75rem;
912
899
  }
@@ -1027,10 +1014,6 @@
1027
1014
  max-width: 7rem;
1028
1015
  }
1029
1016
 
1030
- :where([data-designtools]) .max-w-56:not([data-docs-content] *), .max-w-56:where([data-designtools]):not([data-docs-content] *) {
1031
- max-width: 14rem;
1032
- }
1033
-
1034
1017
  :where([data-designtools]) .max-w-64:not([data-docs-content] *), .max-w-64:where([data-designtools]):not([data-docs-content] *) {
1035
1018
  max-width: 16rem;
1036
1019
  }
@@ -1345,12 +1328,6 @@
1345
1328
  column-gap: 3rem;
1346
1329
  }
1347
1330
 
1348
- :where([data-designtools]) :where(.-space-x-1\.5 > :not(:last-child)):not([data-docs-content] *), :where(.-space-x-1\.5 > :not(:last-child)):where([data-designtools]):not([data-docs-content] *) {
1349
- --tw-space-x-reverse: 0;
1350
- margin-inline-start: calc(calc(.25rem * -1.5) * var(--tw-space-x-reverse));
1351
- margin-inline-end: calc(calc(.25rem * -1.5) * calc(1 - var(--tw-space-x-reverse)));
1352
- }
1353
-
1354
1331
  :where([data-designtools]) .gap-y-1:not([data-docs-content] *), .gap-y-1:where([data-designtools]):not([data-docs-content] *) {
1355
1332
  row-gap: .25rem;
1356
1333
  }
@@ -2423,11 +2400,6 @@
2423
2400
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
2424
2401
  }
2425
2402
 
2426
- :where([data-designtools]) .ring-2:not([data-docs-content] *), .ring-2:where([data-designtools]):not([data-docs-content] *) {
2427
- --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
2428
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
2429
- }
2430
-
2431
2403
  :where([data-designtools]) .ring-\(--border\):not([data-docs-content] *), .ring-\(--border\):where([data-designtools]):not([data-docs-content] *) {
2432
2404
  --tw-ring-color: var(--border);
2433
2405
  }
@@ -2446,10 +2418,6 @@
2446
2418
  --tw-ring-color: var(--border, #e2e4e8);
2447
2419
  }
2448
2420
 
2449
- :where([data-designtools]) .ring-canvas:not([data-docs-content] *), .ring-canvas:where([data-designtools]):not([data-docs-content] *) {
2450
- --tw-ring-color: var(--canvas, #fff);
2451
- }
2452
-
2453
2421
  :where([data-designtools]) .ring-current\/15:not([data-docs-content] *), .ring-current\/15:where([data-designtools]):not([data-docs-content] *) {
2454
2422
  --tw-ring-color: currentcolor;
2455
2423
  }
@@ -3312,10 +3280,6 @@
3312
3280
  font-size: .875em;
3313
3281
  }
3314
3282
 
3315
- :where([data-designtools]) .\[\&_img\]\:h-8 img:not([data-docs-content] *), .\[\&_img\]\:h-8 img:where([data-designtools]):not([data-docs-content] *) {
3316
- height: 2rem;
3317
- }
3318
-
3319
3283
  :where([data-designtools]) .\[\&_pre\]\:break-words pre:not([data-docs-content] *), .\[\&_pre\]\:break-words pre:where([data-designtools]):not([data-docs-content] *) {
3320
3284
  overflow-wrap: break-word;
3321
3285
  }
@@ -3324,10 +3288,6 @@
3324
3288
  white-space: pre-wrap;
3325
3289
  }
3326
3290
 
3327
- :where([data-designtools]) .\[\&_svg\]\:h-8 svg:not([data-docs-content] *), .\[\&_svg\]\:h-8 svg:where([data-designtools]):not([data-docs-content] *) {
3328
- height: 2rem;
3329
- }
3330
-
3331
3291
  :where([data-designtools]) .\[\&\:\:-webkit-details-marker\]\:hidden:not([data-docs-content] *)::-webkit-details-marker, .\[\&\:\:-webkit-details-marker\]\:hidden:where([data-designtools]):not([data-docs-content] *)::-webkit-details-marker {
3332
3292
  display: none;
3333
3293
  }
@@ -3380,12 +3340,6 @@
3380
3340
  inherits: false
3381
3341
  }
3382
3342
 
3383
- @property --tw-space-x-reverse {
3384
- syntax: "*";
3385
- inherits: false;
3386
- initial-value: 0;
3387
- }
3388
-
3389
3343
  @property --tw-divide-y-reverse {
3390
3344
  syntax: "*";
3391
3345
  inherits: false;
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.1",
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
  ))}
package/src/workbench.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { handlerNames } from "./lib/handlers";
3
4
  import { Live } from "./lib/live";
4
5
  import { Tabs } from "@base-ui/react/tabs";
5
6
  import { Toggle } from "@base-ui/react/toggle";
@@ -133,7 +134,7 @@ export function Workbench({
133
134
  const own = Object.entries(entry.props).filter(([name, p]) => !axisNames.has(name) && !PLUMBING.has(name) && p.deprecated === undefined);
134
135
  // disabled is a pinned state ("Off"), not a prop control
135
136
  const settable = own.filter(([name, p]) => !isFunction(name, p) && !p.from && name !== "disabled");
136
- const handlers = own.filter(([name, p]) => isFunction(name, p)).map(([name]) => name);
137
+ const handlers = handlerNames(Object.fromEntries(own));
137
138
  const canDisable = "disabled" in entry.props;
138
139
 
139
140
  const [variants, setVariants] = useState<Record<string, string>>({});