@designtools/docs 0.2.1 → 0.2.3

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,17 @@
1
1
  # @designtools/docs
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 62af842: 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, so logging can never re-render the component into calling it again. The overview's parts of the system are a typographic list: the name, why it exists and an arrow, with no glimpses or counts.
8
+
9
+ ## 0.2.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 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.
14
+
3
15
  ## 0.2.1
4
16
 
5
17
  ### 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
+ }
@@ -8,7 +8,6 @@ import { Examples } from "../examples.js";
8
8
  import { Glossary } from "../glossary.js";
9
9
  import { Imagery } from "../imagery.js";
10
10
  import { groupFamilies, slugOf } from "../lib/manifest.js";
11
- import { StatusBadge } from "../lib/status.js";
12
11
  import { LogoUsage } from "../logo-usage.js";
13
12
  import { PageActions } from "../page-actions.js";
14
13
  import { Pattern } from "../pattern.js";
@@ -112,69 +111,48 @@ function Overview({ site }) {
112
111
  const count = families.reduce((n, g) => n + g.families.length, 0);
113
112
  const brandPages = site.editorial.filter((p) => (p.section ?? "brand") === "brand");
114
113
  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
- 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, and why it exists
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,
128
121
  });
129
122
  sections.push({
130
123
  title: "Foundations",
131
124
  href: `${base}/colour`,
132
125
  why: "Colour, type and the scale, generated from the tokens and measured in both modes, so nothing is a guess.",
133
- 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
126
  });
136
127
  if (site.rules.rules.length)
137
128
  sections.push({
138
129
  title: "Rules",
139
130
  href: `${base}/rules`,
140
131
  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))) })),
146
132
  });
147
133
  if (site.shows("components") && count)
148
134
  sections.push({
149
135
  title: "Components",
150
136
  href: `${base}/components/${slugOf(families[0].families[0].head)}`,
151
137
  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))) })),
154
138
  });
155
139
  if (site.patterns.patterns.length)
156
140
  sections.push({
157
141
  title: "Patterns",
158
142
  href: `${base}/patterns/${site.patterns.patterns[0].id}`,
159
143
  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(" · ") }),
162
144
  });
163
145
  if (site.shows("standards") && (config.baselines || config.adherence))
164
146
  sections.push({
165
147
  title: "Standards",
166
148
  href: `${base}/standards`,
167
149
  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), "%"] })] })),
170
150
  });
171
151
  if (site.shows("vocabulary") && site.taxonomy.terms.length)
172
152
  sections.push({
173
153
  title: "Vocabulary",
174
154
  href: `${base}/vocabulary`,
175
155
  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,
178
156
  });
179
157
  const loop = [
180
158
  ["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 +161,6 @@ function Overview({ site }) {
183
161
  ["Checked", "Each rule names the check that proves it. Stale docs stop a commit; everything else is reported."],
184
162
  ["Measured", "A report on every change shows how closely the product follows the system. It informs the conversation; it never blocks the work."],
185
163
  ];
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 ??
164
+ 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: "flex flex-col border-b border-border", children: sections.map((sec) => (_jsx("li", { className: "border-t border-border", children: _jsxs(Link, { href: sec.href, className: "group grid items-baseline gap-x-10 gap-y-2 py-6 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring md:grid-cols-[13rem_minmax(0,1fr)_auto]", children: [_jsx("span", { className: cx(display, "text-2xl leading-snug tracking-tight underline-offset-4 group-hover:underline"), children: sec.title }), _jsx("span", { className: "max-w-2xl text-base leading-relaxed text-muted-foreground", children: sec.why }), _jsx(ArrowUpRight, { "aria-hidden": true, className: "hidden size-(--dt-size-sm) translate-y-0.5 self-start text-muted-foreground transition duration-(--dt-duration-base) ease-(--dt-ease-out) group-hover:translate-x-1 group-hover:-translate-y-0.5 group-hover:text-canvas-foreground md:mt-1.5 md:block" })] }) }, sec.title))) }) }), config.overview?.agentic !== false && (_jsxs(DocsSection, { title: "Why this is agentic", lead: config.overview?.agenticLead ??
187
165
  "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
166
  }
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
  }
@@ -1117,6 +1100,11 @@
1117
1100
  translate: var(--tw-translate-x) var(--tw-translate-y);
1118
1101
  }
1119
1102
 
1103
+ :where([data-designtools]) .translate-y-0\.5:not([data-docs-content] *), .translate-y-0\.5:where([data-designtools]):not([data-docs-content] *) {
1104
+ --tw-translate-y: calc(.25rem * .5);
1105
+ translate: var(--tw-translate-x) var(--tw-translate-y);
1106
+ }
1107
+
1120
1108
  :where([data-designtools]) .transform:not([data-docs-content] *), .transform:where([data-designtools]):not([data-docs-content] *) {
1121
1109
  transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
1122
1110
  }
@@ -1345,12 +1333,6 @@
1345
1333
  column-gap: 3rem;
1346
1334
  }
1347
1335
 
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
1336
  :where([data-designtools]) .gap-y-1:not([data-docs-content] *), .gap-y-1:where([data-designtools]):not([data-docs-content] *) {
1355
1337
  row-gap: .25rem;
1356
1338
  }
@@ -1403,6 +1385,10 @@
1403
1385
  align-self: flex-end;
1404
1386
  }
1405
1387
 
1388
+ :where([data-designtools]) .self-start:not([data-docs-content] *), .self-start:where([data-designtools]):not([data-docs-content] *) {
1389
+ align-self: flex-start;
1390
+ }
1391
+
1406
1392
  :where([data-designtools]) .justify-self-end:not([data-docs-content] *), .justify-self-end:where([data-designtools]):not([data-docs-content] *) {
1407
1393
  justify-self: flex-end;
1408
1394
  }
@@ -1865,10 +1851,6 @@
1865
1851
  padding-block: 1.5rem;
1866
1852
  }
1867
1853
 
1868
- :where([data-designtools]) .py-7:not([data-docs-content] *), .py-7:where([data-designtools]):not([data-docs-content] *) {
1869
- padding-block: 1.75rem;
1870
- }
1871
-
1872
1854
  :where([data-designtools]) .py-8:not([data-docs-content] *), .py-8:where([data-designtools]):not([data-docs-content] *) {
1873
1855
  padding-block: 2rem;
1874
1856
  }
@@ -2323,10 +2305,6 @@
2323
2305
  text-underline-offset: 4px;
2324
2306
  }
2325
2307
 
2326
- :where([data-designtools]) .underline-offset-\[6px\]:not([data-docs-content] *), .underline-offset-\[6px\]:where([data-designtools]):not([data-docs-content] *) {
2327
- text-underline-offset: 6px;
2328
- }
2329
-
2330
2308
  :where([data-designtools]) .opacity-0:not([data-docs-content] *), .opacity-0:where([data-designtools]):not([data-docs-content] *) {
2331
2309
  opacity: 0;
2332
2310
  }
@@ -2423,11 +2401,6 @@
2423
2401
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
2424
2402
  }
2425
2403
 
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
2404
  :where([data-designtools]) .ring-\(--border\):not([data-docs-content] *), .ring-\(--border\):where([data-designtools]):not([data-docs-content] *) {
2432
2405
  --tw-ring-color: var(--border);
2433
2406
  }
@@ -2446,10 +2419,6 @@
2446
2419
  --tw-ring-color: var(--border, #e2e4e8);
2447
2420
  }
2448
2421
 
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
2422
  :where([data-designtools]) .ring-current\/15:not([data-docs-content] *), .ring-current\/15:where([data-designtools]):not([data-docs-content] *) {
2454
2423
  --tw-ring-color: currentcolor;
2455
2424
  }
@@ -2490,6 +2459,12 @@
2490
2459
  backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
2491
2460
  }
2492
2461
 
2462
+ :where([data-designtools]) .transition:not([data-docs-content] *), .transition:where([data-designtools]):not([data-docs-content] *) {
2463
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
2464
+ transition-timing-function: var(--tw-ease, cubic-bezier(.4, 0, .2, 1));
2465
+ transition-duration: var(--tw-duration, .15s);
2466
+ }
2467
+
2493
2468
  :where([data-designtools]) .transition-\[height\]:not([data-docs-content] *), .transition-\[height\]:where([data-designtools]):not([data-docs-content] *) {
2494
2469
  transition-property: height;
2495
2470
  transition-timing-function: var(--tw-ease, cubic-bezier(.4, 0, .2, 1));
@@ -2596,11 +2571,21 @@
2596
2571
  translate: var(--tw-translate-x) var(--tw-translate-y);
2597
2572
  }
2598
2573
 
2574
+ :where([data-designtools]) .group-hover\:translate-x-1:is(:where(.group):hover *):not([data-docs-content] *), .group-hover\:translate-x-1:is(:where(.group):hover *):where([data-designtools]):not([data-docs-content] *) {
2575
+ --tw-translate-x: .25rem;
2576
+ translate: var(--tw-translate-x) var(--tw-translate-y);
2577
+ }
2578
+
2599
2579
  :where([data-designtools]) .group-hover\:translate-x-full:is(:where(.group):hover *):not([data-docs-content] *), .group-hover\:translate-x-full:is(:where(.group):hover *):where([data-designtools]):not([data-docs-content] *) {
2600
2580
  --tw-translate-x: 100%;
2601
2581
  translate: var(--tw-translate-x) var(--tw-translate-y);
2602
2582
  }
2603
2583
 
2584
+ :where([data-designtools]) .group-hover\:-translate-y-0\.5:is(:where(.group):hover *):not([data-docs-content] *), .group-hover\:-translate-y-0\.5:is(:where(.group):hover *):where([data-designtools]):not([data-docs-content] *) {
2585
+ --tw-translate-y: calc(.25rem * -.5);
2586
+ translate: var(--tw-translate-x) var(--tw-translate-y);
2587
+ }
2588
+
2604
2589
  :where([data-designtools]) .group-hover\:text-canvas-foreground:is(:where(.group):hover *):not([data-docs-content] *), .group-hover\:text-canvas-foreground:is(:where(.group):hover *):where([data-designtools]):not([data-docs-content] *) {
2605
2590
  color: var(--canvas-foreground, #1c1f26);
2606
2591
  }
@@ -2823,6 +2808,10 @@
2823
2808
  outline-offset: 2px;
2824
2809
  }
2825
2810
 
2811
+ :where([data-designtools]) .focus-visible\:outline-offset-4:focus-visible:not([data-docs-content] *), .focus-visible\:outline-offset-4:focus-visible:where([data-designtools]):not([data-docs-content] *) {
2812
+ outline-offset: 4px;
2813
+ }
2814
+
2826
2815
  :where([data-designtools]) .focus-visible\:outline-offset-\[-2px\]:focus-visible:not([data-docs-content] *), .focus-visible\:outline-offset-\[-2px\]:focus-visible:where([data-designtools]):not([data-docs-content] *) {
2827
2816
  outline-offset: -2px;
2828
2817
  }
@@ -3042,6 +3031,10 @@
3042
3031
  grid-column-start: 4;
3043
3032
  }
3044
3033
 
3034
+ :where([data-designtools]) .md\:mt-1\.5:not([data-docs-content] *), .md\:mt-1\.5:where([data-designtools]):not([data-docs-content] *) {
3035
+ margin-top: .375rem;
3036
+ }
3037
+
3045
3038
  :where([data-designtools]) .md\:block:not([data-docs-content] *), .md\:block:where([data-designtools]):not([data-docs-content] *) {
3046
3039
  display: block;
3047
3040
  }
@@ -3070,6 +3063,10 @@
3070
3063
  grid-template-columns: 12rem minmax(0, 1fr);
3071
3064
  }
3072
3065
 
3066
+ :where([data-designtools]) .md\:grid-cols-\[13rem_minmax\(0\,1fr\)_auto\]:not([data-docs-content] *), .md\:grid-cols-\[13rem_minmax\(0\,1fr\)_auto\]:where([data-designtools]):not([data-docs-content] *) {
3067
+ grid-template-columns: 13rem minmax(0, 1fr) auto;
3068
+ }
3069
+
3073
3070
  :where([data-designtools]) .md\:grid-cols-\[minmax\(9rem\,0\.9fr\)_minmax\(0\,1\.6fr\)_minmax\(5rem\,0\.6fr\)_1\.25rem\]:not([data-docs-content] *), .md\:grid-cols-\[minmax\(9rem\,0\.9fr\)_minmax\(0\,1\.6fr\)_minmax\(5rem\,0\.6fr\)_1\.25rem\]:where([data-designtools]):not([data-docs-content] *) {
3074
3071
  grid-template-columns: minmax(9rem, .9fr) minmax(0, 1.6fr) minmax(5rem, .6fr) 1.25rem;
3075
3072
  }
@@ -3312,10 +3309,6 @@
3312
3309
  font-size: .875em;
3313
3310
  }
3314
3311
 
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
3312
  :where([data-designtools]) .\[\&_pre\]\:break-words pre:not([data-docs-content] *), .\[\&_pre\]\:break-words pre:where([data-designtools]):not([data-docs-content] *) {
3320
3313
  overflow-wrap: break-word;
3321
3314
  }
@@ -3324,10 +3317,6 @@
3324
3317
  white-space: pre-wrap;
3325
3318
  }
3326
3319
 
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
3320
  :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
3321
  display: none;
3333
3322
  }
@@ -3380,12 +3369,6 @@
3380
3369
  inherits: false
3381
3370
  }
3382
3371
 
3383
- @property --tw-space-x-reverse {
3384
- syntax: "*";
3385
- inherits: false;
3386
- initial-value: 0;
3387
- }
3388
-
3389
3372
  @property --tw-divide-y-reverse {
3390
3373
  syntax: "*";
3391
3374
  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.3",
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,6 +1,5 @@
1
1
  import { ArrowRight, ArrowUpRight } from "lucide-react";
2
2
  import Link from "next/link";
3
- import type { ReactNode } from "react";
4
3
  import { cx, display } from "../lib/cx";
5
4
  import { AdherenceSummary } from "../adherence-summary";
6
5
  import { Anatomy } from "../anatomy";
@@ -307,112 +306,49 @@ function Overview({ site }: { site: Site }) {
307
306
  const count = families.reduce((n, g) => n + g.families.length, 0);
308
307
  const brandPages = site.editorial.filter((p) => (p.section ?? "brand") === "brand");
309
308
  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
- 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, and why it exists
311
+ const sections: { title: string; href: string; why: 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,
326
317
  });
327
318
  sections.push({
328
319
  title: "Foundations",
329
320
  href: `${base}/colour`,
330
321
  why: "Colour, type and the scale, generated from the tokens and measured in both modes, so nothing is a guess.",
331
- 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
322
  });
345
323
  if (site.rules.rules.length)
346
324
  sections.push({
347
325
  title: "Rules",
348
326
  href: `${base}/rules`,
349
327
  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
- ),
364
328
  });
365
329
  if (site.shows("components") && count)
366
330
  sections.push({
367
331
  title: "Components",
368
332
  href: `${base}/components/${slugOf(families[0].families[0].head)}`,
369
333
  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
- ),
380
334
  });
381
335
  if (site.patterns.patterns.length)
382
336
  sections.push({
383
337
  title: "Patterns",
384
338
  href: `${base}/patterns/${site.patterns.patterns[0].id}`,
385
339
  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>,
388
340
  });
389
341
  if (site.shows("standards") && (config.baselines || config.adherence))
390
342
  sections.push({
391
343
  title: "Standards",
392
344
  href: `${base}/standards`,
393
345
  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
- ),
404
346
  });
405
347
  if (site.shows("vocabulary") && site.taxonomy.terms.length)
406
348
  sections.push({
407
349
  title: "Vocabulary",
408
350
  href: `${base}/vocabulary`,
409
351
  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,
416
352
  });
417
353
 
418
354
  const loop = [
@@ -432,16 +368,19 @@ function Overview({ site }: { site: Site }) {
432
368
  )}
433
369
 
434
370
  <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.">
435
- <ul data-slot="overview-sections" className="grid gap-x-12 md:grid-cols-2">
371
+ <ul data-slot="overview-sections" className="flex flex-col border-b border-border">
436
372
  {sections.map((sec) => (
437
373
  <li key={sec.title} className="border-t border-border">
438
- <Link href={sec.href} className="group flex h-full flex-col gap-4 py-7 focus-visible:outline-2 focus-visible:outline-ring">
439
- <span className="flex items-baseline justify-between gap-4">
440
- <span className="text-2xl leading-tight tracking-tight underline-offset-[6px] group-hover:underline">{sec.title}</span>
441
- <span className="shrink-0 text-xs text-muted-foreground tabular-nums">{sec.count}</span>
442
- </span>
443
- <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>}
374
+ <Link
375
+ href={sec.href}
376
+ className="group grid items-baseline gap-x-10 gap-y-2 py-6 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring md:grid-cols-[13rem_minmax(0,1fr)_auto]"
377
+ >
378
+ <span className={cx(display, "text-2xl leading-snug tracking-tight underline-offset-4 group-hover:underline")}>{sec.title}</span>
379
+ <span className="max-w-2xl text-base leading-relaxed text-muted-foreground">{sec.why}</span>
380
+ <ArrowUpRight
381
+ aria-hidden
382
+ className="hidden size-(--dt-size-sm) translate-y-0.5 self-start text-muted-foreground transition duration-(--dt-duration-base) ease-(--dt-ease-out) group-hover:translate-x-1 group-hover:-translate-y-0.5 group-hover:text-canvas-foreground md:mt-1.5 md:block"
383
+ />
445
384
  </Link>
446
385
  </li>
447
386
  ))}
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>>({});