@designtools/docs 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +155 -2
  4. package/dist/a11y-panel.d.ts +58 -0
  5. package/dist/a11y-panel.js +75 -0
  6. package/dist/adherence-summary.d.ts +9 -0
  7. package/dist/adherence-summary.js +27 -0
  8. package/dist/agent-view.d.ts +19 -0
  9. package/dist/agent-view.js +23 -0
  10. package/dist/anatomy.d.ts +12 -0
  11. package/dist/anatomy.js +39 -0
  12. package/dist/ask-claude.d.ts +14 -0
  13. package/dist/ask-claude.js +40 -0
  14. package/dist/cli.js +322 -0
  15. package/dist/code-view.d.ts +17 -0
  16. package/dist/code-view.js +42 -0
  17. package/dist/examples.d.ts +25 -0
  18. package/dist/examples.js +48 -0
  19. package/dist/glossary.d.ts +5 -0
  20. package/dist/glossary.js +21 -0
  21. package/dist/imagery.d.ts +6 -0
  22. package/dist/imagery.js +17 -0
  23. package/dist/index.d.ts +35 -0
  24. package/dist/index.js +34 -0
  25. package/dist/lib/adherence-types.d.ts +168 -0
  26. package/dist/lib/adherence-types.js +13 -0
  27. package/dist/lib/boundary.d.ts +16 -0
  28. package/dist/lib/boundary.js +15 -0
  29. package/dist/lib/contrast.d.ts +8 -0
  30. package/dist/lib/contrast.js +42 -0
  31. package/dist/lib/cx.d.ts +24 -0
  32. package/dist/lib/cx.js +26 -0
  33. package/dist/lib/jsx.d.ts +6 -0
  34. package/dist/lib/jsx.js +29 -0
  35. package/dist/lib/live.d.ts +10 -0
  36. package/dist/lib/live.js +10 -0
  37. package/dist/lib/lookup.d.ts +35 -0
  38. package/dist/lib/lookup.js +42 -0
  39. package/dist/lib/manifest-types.d.ts +253 -0
  40. package/dist/lib/manifest-types.js +14 -0
  41. package/dist/lib/manifest.d.ts +98 -0
  42. package/dist/lib/manifest.js +173 -0
  43. package/dist/lib/markdown.d.ts +50 -0
  44. package/dist/lib/markdown.js +286 -0
  45. package/dist/lib/measure.d.ts +12 -0
  46. package/dist/lib/measure.js +62 -0
  47. package/dist/lib/pseudo.d.ts +11 -0
  48. package/dist/lib/pseudo.js +71 -0
  49. package/dist/lib/registry.d.ts +21 -0
  50. package/dist/lib/registry.js +11 -0
  51. package/dist/lib/search.d.ts +44 -0
  52. package/dist/lib/search.js +62 -0
  53. package/dist/lib/standards.d.ts +17 -0
  54. package/dist/lib/standards.js +81 -0
  55. package/dist/lib/status.d.ts +6 -0
  56. package/dist/lib/status.js +17 -0
  57. package/dist/lib/terms.d.ts +15 -0
  58. package/dist/lib/terms.js +20 -0
  59. package/dist/lib/text.d.ts +8 -0
  60. package/dist/lib/text.js +9 -0
  61. package/dist/lib/ui.d.ts +100 -0
  62. package/dist/lib/ui.js +89 -0
  63. package/dist/lib/vision.d.ts +18 -0
  64. package/dist/lib/vision.js +27 -0
  65. package/dist/logo-usage.d.ts +14 -0
  66. package/dist/logo-usage.js +45 -0
  67. package/dist/next/config.d.ts +105 -0
  68. package/dist/next/config.js +1 -0
  69. package/dist/next/index.d.ts +65 -0
  70. package/dist/next/index.js +66 -0
  71. package/dist/next/markdown.d.ts +20 -0
  72. package/dist/next/markdown.js +95 -0
  73. package/dist/next/pages.d.ts +6 -0
  74. package/dist/next/pages.js +188 -0
  75. package/dist/next/prose.d.ts +9 -0
  76. package/dist/next/prose.js +39 -0
  77. package/dist/next/proxy.d.ts +19 -0
  78. package/dist/next/proxy.js +56 -0
  79. package/dist/next/site.d.ts +53 -0
  80. package/dist/next/site.js +136 -0
  81. package/dist/page-actions.d.ts +9 -0
  82. package/dist/page-actions.js +32 -0
  83. package/dist/pattern.d.ts +11 -0
  84. package/dist/pattern.js +22 -0
  85. package/dist/playground.d.ts +62 -0
  86. package/dist/playground.js +111 -0
  87. package/dist/preview-frame.d.ts +31 -0
  88. package/dist/preview-frame.js +63 -0
  89. package/dist/props-table.d.ts +10 -0
  90. package/dist/props-table.js +45 -0
  91. package/dist/rule.d.ts +16 -0
  92. package/dist/rule.js +37 -0
  93. package/dist/scale.d.ts +9 -0
  94. package/dist/scale.js +58 -0
  95. package/dist/search.d.ts +16 -0
  96. package/dist/search.js +46 -0
  97. package/dist/shell.d.ts +92 -0
  98. package/dist/shell.js +201 -0
  99. package/dist/standards.d.ts +13 -0
  100. package/dist/standards.js +19 -0
  101. package/dist/styles.css +3708 -0
  102. package/dist/swatches.d.ts +28 -0
  103. package/dist/swatches.js +100 -0
  104. package/dist/theme-switch.d.ts +20 -0
  105. package/dist/theme-switch.js +68 -0
  106. package/dist/type-ramp.d.ts +10 -0
  107. package/dist/type-ramp.js +31 -0
  108. package/dist/usage.d.ts +13 -0
  109. package/dist/usage.js +55 -0
  110. package/dist/variant-matrix.d.ts +15 -0
  111. package/dist/variant-matrix.js +58 -0
  112. package/dist/voice-terms.d.ts +5 -0
  113. package/dist/voice-terms.js +14 -0
  114. package/dist/workbench.d.ts +33 -0
  115. package/dist/workbench.js +274 -0
  116. package/package.json +107 -3
  117. package/src/a11y-panel.tsx +297 -0
  118. package/src/adherence-summary.tsx +134 -0
  119. package/src/agent-view.tsx +54 -0
  120. package/src/anatomy.tsx +119 -0
  121. package/src/ask-claude.tsx +90 -0
  122. package/src/code-view.tsx +112 -0
  123. package/src/css.d.ts +1 -0
  124. package/src/examples.tsx +141 -0
  125. package/src/glossary.tsx +70 -0
  126. package/src/imagery.tsx +64 -0
  127. package/src/index.ts +35 -0
  128. package/src/lib/adherence-types.ts +181 -0
  129. package/src/lib/boundary.tsx +21 -0
  130. package/src/lib/contrast.ts +44 -0
  131. package/src/lib/cx.ts +30 -0
  132. package/src/lib/jsx.ts +29 -0
  133. package/src/lib/live.tsx +15 -0
  134. package/src/lib/lookup.tsx +50 -0
  135. package/src/lib/manifest-types.ts +284 -0
  136. package/src/lib/manifest.ts +228 -0
  137. package/src/lib/markdown.ts +288 -0
  138. package/src/lib/measure.tsx +101 -0
  139. package/src/lib/pseudo.ts +74 -0
  140. package/src/lib/registry.tsx +29 -0
  141. package/src/lib/search.ts +99 -0
  142. package/src/lib/standards.ts +82 -0
  143. package/src/lib/status.tsx +32 -0
  144. package/src/lib/terms.tsx +53 -0
  145. package/src/lib/text.tsx +27 -0
  146. package/src/lib/ui.tsx +291 -0
  147. package/src/lib/vision.tsx +44 -0
  148. package/src/logo-usage.tsx +140 -0
  149. package/src/next/config.ts +101 -0
  150. package/src/next/index.tsx +103 -0
  151. package/src/next/markdown.ts +119 -0
  152. package/src/next/pages.tsx +483 -0
  153. package/src/next/prose.tsx +97 -0
  154. package/src/next/proxy.ts +55 -0
  155. package/src/next/site.ts +173 -0
  156. package/src/page-actions.tsx +92 -0
  157. package/src/pattern.tsx +72 -0
  158. package/src/playground.tsx +357 -0
  159. package/src/preview-frame.tsx +131 -0
  160. package/src/props-table.tsx +154 -0
  161. package/src/rule.tsx +165 -0
  162. package/src/scale.tsx +175 -0
  163. package/src/search.tsx +155 -0
  164. package/src/shell.tsx +569 -0
  165. package/src/standards.tsx +58 -0
  166. package/src/styles.css +157 -0
  167. package/src/swatches.tsx +298 -0
  168. package/src/theme-switch.tsx +90 -0
  169. package/src/type-ramp.tsx +150 -0
  170. package/src/usage.tsx +148 -0
  171. package/src/variant-matrix.tsx +178 -0
  172. package/src/voice-terms.tsx +37 -0
  173. package/src/workbench.tsx +587 -0
@@ -0,0 +1,150 @@
1
+ import type { ReactNode } from "react";
2
+ import { shortName, tokensOfTier, typeScale, withGroupDescriptions } from "./lib/manifest";
3
+ import type { TokensManifest } from "./lib/manifest-types";
4
+
5
+ type Token = TokensManifest["tokens"][number];
6
+
7
+ /** `1.125rem` → `18px`, so a person reads the size they know. */
8
+ function px(value?: string): string | undefined {
9
+ const m = value && /^(\d*\.?\d+)rem$/.exec(value.trim());
10
+ return m ? `${Math.round(Number(m[1]) * 16 * 100) / 100}px` : undefined;
11
+ }
12
+
13
+ /**
14
+ * Type, set in its own tokens: each family as a specimen, then the scale, each
15
+ * size with the leading and tracking Tailwind pairs with it, then weights,
16
+ * leading and tracking on their own. Every sample reads its token; nothing is restated.
17
+ */
18
+ export function TypeRamp({
19
+ tokens,
20
+ sample = "The quick brown fox jumps over the lazy dog",
21
+ }: {
22
+ tokens: TokensManifest;
23
+ sample?: string;
24
+ }) {
25
+ const steps = withGroupDescriptions(typeScale(tokens).map((s) => ({ ...s, description: s.size.description })));
26
+ const families = tokensOfTier(tokens, "font");
27
+ const weights = tokensOfTier(tokens, "font-weight");
28
+ const leading = tokensOfTier(tokens, "leading");
29
+ const tracking = tokensOfTier(tokens, "tracking");
30
+
31
+ return (
32
+ <div data-slot="type-ramp" className="flex flex-col gap-14">
33
+ {families.length > 0 && (
34
+ <div className="grid gap-x-12 gap-y-10 md:grid-cols-2">
35
+ {families.map((f) => (
36
+ <figure key={f.name} data-slot="type-family" className="flex flex-col gap-5 border-t border-border pt-6">
37
+ <div className="flex flex-col gap-6" style={{ fontFamily: `var(${f.name})` }}>
38
+ <span className="text-8xl leading-none font-light tracking-tighter">Aa</span>
39
+ <span className="text-sm leading-relaxed break-all text-muted-foreground">
40
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 &amp;?!
41
+ </span>
42
+ </div>
43
+ <figcaption className="flex flex-col gap-0.5">
44
+ <code className="font-mono text-xs font-medium">{shortName(f, "font")}</code>
45
+ <span className="truncate font-mono text-xs text-muted-foreground" title={f.values.default}>
46
+ {f.description ?? f.values.default}
47
+ </span>
48
+ </figcaption>
49
+ </figure>
50
+ ))}
51
+ </div>
52
+ )}
53
+
54
+ <ol data-slot="type-scale" className="flex flex-col">
55
+ {steps.map((step) => (
56
+ <li key={step.name} data-slot="type-step" className="grid grid-cols-1 gap-3 border-t border-border py-5 md:grid-cols-[11rem_minmax(0,1fr)] md:items-center md:gap-6">
57
+ <div className="flex flex-col gap-1">
58
+ <span className="flex items-baseline gap-2">
59
+ <code className="font-mono text-xs font-semibold">{shortName(step.size, "text")}</code>
60
+ <span className="font-mono text-xs text-muted-foreground">{px(step.size.values.default) ?? step.size.values.default}</span>
61
+ </span>
62
+ <span className="font-mono text-xs text-muted-foreground">
63
+ {[step.lineHeight && `lh ${step.lineHeight.values.default}`, step.letterSpacing && `tr ${step.letterSpacing.values.default}`].filter(Boolean).join(" · ")}
64
+ </span>
65
+ {step.description && <span className="text-xs text-muted-foreground">{step.description}</span>}
66
+ </div>
67
+ <p
68
+ className="min-w-0 truncate"
69
+ style={{
70
+ fontSize: `var(${step.name})`,
71
+ lineHeight: step.lineHeight ? `var(${step.lineHeight.name})` : undefined,
72
+ letterSpacing: step.letterSpacing ? `var(${step.letterSpacing.name})` : undefined,
73
+ fontWeight: step.fontWeight ? `var(${step.fontWeight.name})` : undefined,
74
+ }}
75
+ >
76
+ {sample}
77
+ </p>
78
+ </li>
79
+ ))}
80
+ </ol>
81
+
82
+ {weights.length > 0 && (
83
+ <Group title="Weights">
84
+ <ul className="grid grid-cols-2 gap-x-10 gap-y-8 sm:grid-cols-4">
85
+ {weights.map((t) => (
86
+ <li key={t.name} className="flex flex-col gap-3 border-t border-border pt-4">
87
+ <span className="text-4xl leading-none tracking-tight" style={{ fontWeight: `var(${t.name})` }}>
88
+ Aa
89
+ </span>
90
+ <Meta token={t} prefix="font-weight" />
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ </Group>
95
+ )}
96
+ {(leading.length > 0 || tracking.length > 0) && (
97
+ <div className="grid gap-10 lg:grid-cols-2">
98
+ {leading.length > 0 && (
99
+ <Group title="Leading">
100
+ <ul className="flex flex-col">
101
+ {leading.map((t) => (
102
+ <li key={t.name} className="grid grid-cols-[6rem_minmax(0,1fr)] gap-4 border-t border-border py-4">
103
+ <Meta token={t} prefix="leading" />
104
+ <span className="line-clamp-2 text-sm" style={{ lineHeight: `var(${t.name})` }}>
105
+ {sample}. {sample}.
106
+ </span>
107
+ </li>
108
+ ))}
109
+ </ul>
110
+ </Group>
111
+ )}
112
+ {tracking.length > 0 && (
113
+ <Group title="Tracking">
114
+ <ul className="flex flex-col">
115
+ {tracking.map((t) => (
116
+ <li key={t.name} className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-4 border-t border-border py-4">
117
+ <Meta token={t} prefix="tracking" />
118
+ <span className="truncate text-lg font-medium" style={{ letterSpacing: `var(${t.name})` }}>
119
+ {sample}
120
+ </span>
121
+ </li>
122
+ ))}
123
+ </ul>
124
+ </Group>
125
+ )}
126
+ </div>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function Group({ title, children }: { title: string; children: ReactNode }) {
133
+ return (
134
+ <div data-slot="type-table" className="flex flex-col gap-4">
135
+ <h3 className="text-2xl tracking-tight">{title}</h3>
136
+ {children}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ function Meta({ token, prefix }: { token: Token; prefix: string }) {
142
+ return (
143
+ <span className="flex min-w-0 flex-col font-mono text-xs">
144
+ <span className="font-medium">{shortName(token, prefix)}</span>
145
+ <span className="truncate text-muted-foreground" title={token.values.default}>
146
+ {token.values.default}
147
+ </span>
148
+ </span>
149
+ );
150
+ }
package/src/usage.tsx ADDED
@@ -0,0 +1,148 @@
1
+ import { ArrowUpRight, CircleCheck, CircleX } from "lucide-react";
2
+ import Link from "next/link";
3
+ import type { ReactNode } from "react";
4
+ import { cx } from "./lib/cx";
5
+ import { slugOf } from "./lib/manifest";
6
+ import type { ComponentEntry, ComponentsManifest } from "./lib/manifest-types";
7
+ import { StatusBadge } from "./lib/status";
8
+ import { inline } from "./lib/text";
9
+
10
+ /**
11
+ * When to use a component and when not, side by side, with what to use instead;
12
+ * then its facts in one strip: status, role, keys, the primitive it wraps and the
13
+ * elements it replaces. Written by people as JSDoc (`@use`, `@avoid`, `@instead`,
14
+ * `@status`, `@role`, `@keyboard`); a component without them says so.
15
+ */
16
+ export function Usage({
17
+ entry,
18
+ manifest,
19
+ basePath = "/design/system",
20
+ }: {
21
+ entry: ComponentEntry;
22
+ /** To link `instead` names that are components. */
23
+ manifest?: ComponentsManifest;
24
+ basePath?: string;
25
+ }) {
26
+ const usage = entry.usage;
27
+ const known = new Map((manifest?.components ?? []).map((c) => [c.name, c]));
28
+ const instead = usage?.instead?.length ? (
29
+ <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground">
30
+ Use instead
31
+ {usage.instead.map((name) => {
32
+ const c = known.get(name);
33
+ return c ? (
34
+ <Link
35
+ key={name}
36
+ href={`${basePath}/components/${slugOf(c)}`}
37
+ className="relative inline-flex items-center gap-0.5 text-canvas-foreground underline decoration-border underline-offset-4 hover:decoration-current focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-3 after:content-['']"
38
+ >
39
+ {name}
40
+ <ArrowUpRight aria-hidden className="size-3.5 text-muted-foreground" />
41
+ </Link>
42
+ ) : (
43
+ <span key={name} className="font-medium text-canvas-foreground">
44
+ {name}
45
+ </span>
46
+ );
47
+ })}
48
+ </p>
49
+ ) : null;
50
+
51
+ const facts: [string, ReactNode][] = [["Status", <Status key="s" entry={entry} />]];
52
+ if (entry.a11y?.role) facts.push(["Role", <code key="r" className="font-mono text-xs">{entry.a11y.role}</code>]);
53
+ if (entry.a11y?.keyboard?.length)
54
+ facts.push([
55
+ "Keyboard",
56
+ <span key="k" className="flex flex-wrap gap-1">
57
+ {entry.a11y.keyboard.map((k) => (
58
+ <kbd key={k} className="rounded-sm border border-border px-1.5 font-mono text-xs leading-5">
59
+ {k}
60
+ </kbd>
61
+ ))}
62
+ </span>,
63
+ ]);
64
+ if (entry.primitive) facts.push(["Built on", <Source key="p" href={entry.primitive} />]);
65
+ if (entry.replaces?.length)
66
+ facts.push([
67
+ "Replaces",
68
+ <span key="x" className="flex flex-wrap gap-1">
69
+ {entry.replaces.map((r) => (
70
+ <code key={r} className="font-mono text-xs">
71
+ {`<${r}>`}
72
+ </code>
73
+ ))}
74
+ </span>,
75
+ ]);
76
+
77
+ return (
78
+ <div data-slot="usage" className="flex flex-col gap-10">
79
+ {usage?.use || usage?.avoid ? (
80
+ <div className="grid gap-10 md:grid-cols-2 md:gap-12">
81
+ <Column tone="do" title="Use it for">
82
+ {usage?.use ? inline(usage.use) : <span className="text-muted-foreground">Not written yet.</span>}
83
+ </Column>
84
+ <Column tone="dont" title="Not for" footer={instead}>
85
+ {usage?.avoid ? inline(usage.avoid) : <span className="text-muted-foreground">Not written yet.</span>}
86
+ </Column>
87
+ </div>
88
+ ) : (
89
+ <p className="text-base text-muted-foreground">
90
+ No usage guidance yet. Add <code className="font-mono text-sm">@use</code>, <code className="font-mono text-sm">@avoid</code> and{" "}
91
+ <code className="font-mono text-sm">@instead</code> to its JSDoc.
92
+ </p>
93
+ )}
94
+ <dl data-slot="usage-facts" className="flex flex-wrap gap-x-12 gap-y-6">
95
+ {facts.map(([term, detail]) => (
96
+ <div key={term} className="flex min-w-0 flex-col gap-2">
97
+ <dt className="text-xs text-muted-foreground">{term}</dt>
98
+ <dd className="text-sm">{detail}</dd>
99
+ </div>
100
+ ))}
101
+ </dl>
102
+ </div>
103
+ );
104
+ }
105
+
106
+ /** One side of when to use it: a hairline, a small label with its icon in the status colour, and the guidance at reading size. */
107
+ function Column({ tone, title, footer, children }: { tone: "do" | "dont"; title: string; footer?: ReactNode; children: ReactNode }) {
108
+ const Icon = tone === "do" ? CircleCheck : CircleX;
109
+ return (
110
+ <div className="flex flex-col gap-3 border-t border-border pt-5">
111
+ <p className="flex items-center gap-2 text-sm text-muted-foreground">
112
+ <Icon aria-hidden className={cx("size-4", tone === "do" ? "text-success-subdued-foreground" : "text-destructive-subdued-foreground")} />
113
+ {title}
114
+ </p>
115
+ <p className="text-lg leading-relaxed text-canvas-foreground/85 [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-[0.875em]">{children}</p>
116
+ {footer}
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function Status({ entry }: { entry: ComponentEntry }) {
122
+ return (
123
+ <span className="flex flex-col items-start gap-1.5">
124
+ <StatusBadge status={entry.status} />
125
+ {entry.statusNote && <span className="text-xs text-muted-foreground">{inline(entry.statusNote)}</span>}
126
+ </span>
127
+ );
128
+ }
129
+
130
+ /** A primitive: a link names the site and the page (`Base UI · avatar`), a package path shows as code. */
131
+ function Source({ href }: { href: string }) {
132
+ if (!/^https?:/.test(href)) return <code className="font-mono text-xs">{href}</code>;
133
+ const url = new URL(href);
134
+ const name = url.pathname.split("/").filter(Boolean).pop() ?? "";
135
+ const host = url.host.replace(/^www\./, "");
136
+ const site = host === "base-ui.com" ? "Base UI" : host.replace(/\.[a-z]+$/, "");
137
+ return (
138
+ <a
139
+ href={href}
140
+ title={href}
141
+ className="group inline-flex items-center gap-1 whitespace-nowrap hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-ring"
142
+ >
143
+ {site}
144
+ {name && <span className="text-muted-foreground">· {name}</span>}
145
+ <ArrowUpRight aria-hidden className="size-3.5 text-muted-foreground" />
146
+ </a>
147
+ );
148
+ }
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { Live } from "./lib/live";
4
+ import { Grid3x3, Moon, Sun } from "lucide-react";
5
+ import { type ReactNode, useEffect, useState } from "react";
6
+ import { cx } from "./lib/cx";
7
+ import { coerce, Missing, useLookup } from "./lib/lookup";
8
+ import type { ComponentEntry, VariantAxis } from "./lib/manifest-types";
9
+ import { installPseudoStates } from "./lib/pseudo";
10
+ import { Frame, IconButton, Toolbar } from "./lib/ui";
11
+
12
+ /**
13
+ * Every variant, rendered live. The first two axes form a grid; each further
14
+ * axis gets its own row, with the others at their defaults. Pass `props` for
15
+ * anything the component needs to render (an `options` list, a `value`).
16
+ */
17
+ export function VariantMatrix({
18
+ entry,
19
+ props = {},
20
+ children,
21
+ axes: only,
22
+ }: {
23
+ entry: ComponentEntry;
24
+ props?: Record<string, unknown>;
25
+ /** What to render inside each instance. Defaults to the component's name. */
26
+ children?: ReactNode;
27
+ /** Limit and order the axes shown. */
28
+ axes?: string[];
29
+ }) {
30
+ const lookup = useLookup();
31
+ const Component = lookup.componentFor(entry);
32
+ const [theme, setTheme] = useState<"light" | "dark" | undefined>();
33
+ const all = entry.variants?.axes ?? [];
34
+ const axes = only ? only.map((n) => all.find((a) => a.name === n)).filter((a): a is VariantAxis => !!a) : all;
35
+ if (!Component) return <Missing name={entry.name} />;
36
+ if (axes.length === 0) return <p className="text-sm text-muted-foreground">{entry.name} has no variants.</p>;
37
+
38
+ const content = children ?? entry.name;
39
+ const render = (extra: Record<string, string>) => (
40
+ <Live>
41
+ <Component {...props} {...coerce(extra)}>
42
+ {content}
43
+ </Component>
44
+ </Live>
45
+ );
46
+ const [rows, cols, ...rest] = axes;
47
+ const toggle = (
48
+ <IconButton
49
+ icon={theme === "dark" ? Sun : Moon}
50
+ label={theme === "dark" ? "Show in light mode" : "Show in dark mode"}
51
+ pressed={theme !== undefined}
52
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
53
+ />
54
+ );
55
+
56
+ return (
57
+ <Frame data-slot="variant-matrix">
58
+ <Toolbar>
59
+ <span className="flex items-center gap-2 pl-2 font-mono text-xs text-muted-foreground">
60
+ <Grid3x3 aria-hidden className="size-(--dt-size-xs)" />
61
+ {cols ? `${rows.name} × ${cols.name}` : rows.name}
62
+ {rest.length > 0 && `, ${rest.map((a) => a.name).join(", ")}`}
63
+ </span>
64
+ {toggle}
65
+ </Toolbar>
66
+ {/* one stage: the matrix, any further axes and the states, separated by space rather than rules */}
67
+ <div data-docs-stage="" data-theme={theme} className="flex flex-col gap-12 bg-canvas px-6 py-8 text-canvas-foreground sm:px-8">
68
+ {cols ? (
69
+ <div className="overflow-x-auto" tabIndex={0} role="region" aria-label="Variants">
70
+ <table className="w-full border-collapse text-sm">
71
+ <caption className="sr-only">
72
+ {entry.name}: {rows.name} by {cols.name}
73
+ </caption>
74
+ <thead>
75
+ <tr>
76
+ <th scope="col" className="w-0 p-0" />
77
+ {cols.options.map((c) => (
78
+ <th key={c.name} scope="col" title={c.description} className="px-4 pb-4 text-center font-mono text-xs font-normal text-muted-foreground">
79
+ <OptionName axis={cols} option={c.name} />
80
+ </th>
81
+ ))}
82
+ </tr>
83
+ </thead>
84
+ <tbody>
85
+ {rows.options.map((r) => (
86
+ <tr key={r.name}>
87
+ <th scope="row" title={r.description} className="py-4 pr-6 text-left font-mono text-xs font-normal whitespace-nowrap text-muted-foreground">
88
+ <OptionName axis={rows} option={r.name} />
89
+ </th>
90
+ {cols.options.map((c) => (
91
+ <td key={c.name} data-slot="variant-cell" className="px-4 py-4 text-center align-middle">
92
+ <span className="inline-flex justify-center">{render({ [rows.name]: r.name, [cols.name]: c.name })}</span>
93
+ </td>
94
+ ))}
95
+ </tr>
96
+ ))}
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ ) : (
101
+ <AxisRow axis={rows} render={render} />
102
+ )}
103
+ {rest.map((axis) => (
104
+ <AxisRow key={axis.name} axis={axis} render={render} />
105
+ ))}
106
+ <States render={render} canDisable={"disabled" in entry.props} />
107
+ </div>
108
+ </Frame>
109
+ );
110
+ }
111
+
112
+ /** A titled row of figures inside the variants stage. */
113
+ function Group({ title, slot, children }: { title: string; slot: string; children: ReactNode }) {
114
+ return (
115
+ <section data-slot={slot} className="flex flex-col gap-5">
116
+ <h3 className="font-mono text-xs text-muted-foreground">{title}</h3>
117
+ <div className="flex flex-wrap items-end gap-x-10 gap-y-6">{children}</div>
118
+ </section>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * The component at its defaults in each state at once: at rest, hovered, focused,
124
+ * pressed and disabled, pinned without a pointer or a keyboard, so states are
125
+ * reviewed side by side rather than by mousing over each.
126
+ */
127
+ function States({ render, canDisable }: { render: (extra: Record<string, string>) => ReactNode; canDisable: boolean }) {
128
+ useEffect(() => installPseudoStates(), []);
129
+ const states: { name: string; pseudo?: string; extra?: Record<string, string> }[] = [
130
+ { name: "rest" },
131
+ { name: "hover", pseudo: "hover" },
132
+ { name: "focus", pseudo: "focus" },
133
+ { name: "press", pseudo: "active" },
134
+ ...(canDisable ? [{ name: "disabled", extra: { disabled: "true" } }] : []),
135
+ ];
136
+ return (
137
+ <Group title="state" slot="variant-states">
138
+ {states.map((s) => (
139
+ <figure key={s.name} className="flex flex-col items-center gap-3">
140
+ <div data-pseudo={s.pseudo}>{render(s.extra ?? {})}</div>
141
+ <figcaption className="font-mono text-xs text-muted-foreground">{s.name}</figcaption>
142
+ </figure>
143
+ ))}
144
+ </Group>
145
+ );
146
+ }
147
+
148
+ function AxisRow({ axis, render }: { axis: VariantAxis; render: (extra: Record<string, string>) => ReactNode }) {
149
+ return (
150
+ <Group title={axis.name} slot="variant-axis">
151
+ {axis.options.map((o) => (
152
+ <figure key={o.name} className="flex flex-col items-center gap-3">
153
+ {render({ [axis.name]: o.name })}
154
+ <figcaption className="font-mono text-xs text-muted-foreground" title={o.description}>
155
+ <OptionName axis={axis} option={o.name} />
156
+ </figcaption>
157
+ </figure>
158
+ ))}
159
+ </Group>
160
+ );
161
+ }
162
+
163
+ /** An option's name, with the default marked by a dot and said for screen readers. */
164
+ function OptionName({ axis, option }: { axis: VariantAxis; option: string }) {
165
+ const isDefault = axis.default === option;
166
+ return (
167
+ <span className={cx("inline-flex items-center gap-1.5", isDefault && "text-canvas-foreground")}>
168
+ {option}
169
+ {isDefault && (
170
+ <>
171
+ <span aria-hidden className="size-1.5 rounded-full bg-[var(--docs-accent,var(--primary))]" />
172
+ <span className="sr-only">(default)</span>
173
+ </>
174
+ )}
175
+ </span>
176
+ );
177
+ }
178
+
@@ -0,0 +1,37 @@
1
+ import type { TermEntry } from "./lib/manifest-types";
2
+ import { TermHead, TermRow } from "./lib/terms";
3
+ import { DocsSection } from "./shell";
4
+
5
+ /** The words to use, beside the ones not to: the product's words first, then the system's names that have wrong forms. */
6
+ export function VoiceTerms({ terms }: { terms: TermEntry[] }) {
7
+ const product = terms.filter((t) => t.kind === "product");
8
+ const system = terms.filter((t) => t.kind !== "product" && t.wrong?.length);
9
+ if (product.length + system.length === 0) return <p className="text-sm text-muted-foreground">No terms yet. Add them to the taxonomy file.</p>;
10
+ return (
11
+ <>
12
+ {product.length > 0 && (
13
+ <DocsSection title="In the product" lead="The words the product uses with the people it serves, in copy, on screens and in emails.">
14
+ <Terms terms={product} />
15
+ </DocsSection>
16
+ )}
17
+ {system.length > 0 && (
18
+ <DocsSection title="In the system" lead="The names the design system gives its parts, and the ones people reach for instead.">
19
+ <Terms terms={system} />
20
+ </DocsSection>
21
+ )}
22
+ </>
23
+ );
24
+ }
25
+
26
+ function Terms({ terms }: { terms: TermEntry[] }) {
27
+ return (
28
+ <div data-slot="voice-terms">
29
+ <TermHead />
30
+ <dl>
31
+ {terms.map((t) => (
32
+ <TermRow key={`${t.kind}:${t.name}`} term={t} />
33
+ ))}
34
+ </dl>
35
+ </div>
36
+ );
37
+ }