@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
package/src/lib/ui.tsx ADDED
@@ -0,0 +1,291 @@
1
+ "use client";
2
+
3
+ import { Menu } from "@base-ui/react/menu";
4
+ import { Tabs } from "@base-ui/react/tabs";
5
+ import { Check, ChevronDown, Copy, type LucideIcon } from "lucide-react";
6
+ import { type ComponentProps, type ReactNode, useState } from "react";
7
+ import { cx } from "./cx";
8
+
9
+ /**
10
+ * The pieces every block is built from, so the docs read as one thing: the stage a
11
+ * component is shown on, the frame around a stage, small icon buttons, a segmented
12
+ * control and a copy button. All drawn from the semantic tokens.
13
+ */
14
+
15
+ /**
16
+ * Where a component is shown: the page's canvas with a faint dot grid, so the
17
+ * component reads as an object on a surface rather than more page. `theme` renders
18
+ * it in one mode whatever the page is in; `width` narrows it to a phone.
19
+ */
20
+ export function Stage({
21
+ theme,
22
+ width,
23
+ className,
24
+ children,
25
+ ...props
26
+ }: { theme?: "light" | "dark"; width?: number; className?: string; children: ReactNode } & Omit<ComponentProps<"div">, "className">) {
27
+ return (
28
+ <div
29
+ data-slot="stage"
30
+ data-docs-stage=""
31
+ data-theme={theme}
32
+ className={cx(
33
+ "relative flex min-w-0 items-center justify-center overflow-auto bg-canvas p-8 text-canvas-foreground sm:p-10",
34
+ "bg-[radial-gradient(color-mix(in_oklch,var(--border)_85%,transparent)_1px,transparent_1px)] [background-size:14px_14px]",
35
+ className,
36
+ )}
37
+ {...props}
38
+ >
39
+ {width ? (
40
+ <div className="w-full rounded-lg border border-dashed border-border p-4" style={{ maxWidth: width }}>
41
+ {children}
42
+ </div>
43
+ ) : (
44
+ children
45
+ )}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ /** A rounded card that frames a stage and its toolbar: every example, playground and preview sits in one. */
51
+ export function Frame({ className, children, ...props }: ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="frame"
55
+ className={cx("flex min-w-0 flex-col overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground shadow-xs", className)}
56
+ {...props}
57
+ >
58
+ {children}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ /** The bar along the top of a frame. */
64
+ export function Toolbar({ className, children }: { className?: string; children: ReactNode }) {
65
+ return (
66
+ <div data-slot="frame-toolbar" className={cx("flex min-h-12 flex-wrap items-center justify-between gap-2 border-b border-border px-2 py-1.5", className)}>
67
+ {children}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ /**
73
+ * A small icon button: drawn at 32px, pressed at the docs' target size (44px by
74
+ * default) through a transparent ring, so a toolbar stays tight and every button
75
+ * is still easy to hit.
76
+ */
77
+ export function IconButton({
78
+ icon: Icon,
79
+ label,
80
+ pressed,
81
+ className,
82
+ ...props
83
+ }: { icon: LucideIcon; label: string; pressed?: boolean } & Omit<ComponentProps<"button">, "children">) {
84
+ return (
85
+ <button
86
+ type="button"
87
+ aria-label={label}
88
+ title={label}
89
+ aria-pressed={pressed}
90
+ className={cx(
91
+ "relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors duration-(--dt-duration-fast)",
92
+ "after:absolute after:-inset-[calc((var(--docs-target,2.75rem)-2rem)/2)] after:content-['']",
93
+ "hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring aria-pressed:bg-muted aria-pressed:text-canvas-foreground",
94
+ className,
95
+ )}
96
+ {...props}
97
+ >
98
+ <Icon aria-hidden className="size-(--dt-size-xs)" />
99
+ </button>
100
+ );
101
+ }
102
+
103
+ /** An `IconButton` that goes somewhere: opening an example on its own, a source file. */
104
+ export function IconLink({ icon: Icon, label, className, ...props }: { icon: LucideIcon; label: string } & Omit<ComponentProps<"a">, "children">) {
105
+ return (
106
+ <a
107
+ aria-label={label}
108
+ title={label}
109
+ className={cx(
110
+ "relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors duration-(--dt-duration-fast)",
111
+ "after:absolute after:-inset-[calc((var(--docs-target,2.75rem)-2rem)/2)] after:content-['']",
112
+ "hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring",
113
+ className,
114
+ )}
115
+ {...props}
116
+ >
117
+ <Icon aria-hidden className="size-(--dt-size-xs)" />
118
+ </a>
119
+ );
120
+ }
121
+
122
+ /** Copies text, and says so: an icon button, or a labelled one with `withLabel`. */
123
+ export function CopyButton({ text, label = "Copy", withLabel, className }: { text: string; label?: string; withLabel?: boolean; className?: string }) {
124
+ const [copied, setCopied] = useState(false);
125
+ const copy = async () => {
126
+ try {
127
+ await navigator.clipboard.writeText(text);
128
+ setCopied(true);
129
+ setTimeout(() => setCopied(false), 1500);
130
+ } catch {
131
+ // clipboard refused (an insecure origin, a denied permission): nothing changes
132
+ }
133
+ };
134
+ return (
135
+ <>
136
+ {withLabel ? (
137
+ <button
138
+ type="button"
139
+ onClick={copy}
140
+ className={cx(
141
+ "inline-flex min-h-(--docs-target,2.75rem) cursor-pointer items-center gap-2 rounded-md border border-border bg-surface px-3 text-sm font-medium text-surface-foreground shadow-xs hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring",
142
+ className,
143
+ )}
144
+ >
145
+ {copied ? <Check aria-hidden className="size-(--dt-size-xs)" /> : <Copy aria-hidden className="size-(--dt-size-xs) text-muted-foreground" />}
146
+ {copied ? "Copied" : label}
147
+ </button>
148
+ ) : (
149
+ <IconButton icon={copied ? Check : Copy} label={copied ? "Copied" : label} onClick={copy} className={className} />
150
+ )}
151
+ <span className="sr-only" aria-live="polite">
152
+ {copied ? "Copied" : ""}
153
+ </span>
154
+ </>
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Two to four views of one thing, as a pill track with the chosen one raised.
160
+ * Tabs underneath, so arrow keys move between them and each panel is labelled by
161
+ * its tab. Put a `SegmentedList` where the switch belongs and a `SegmentedPanel`
162
+ * for each view.
163
+ */
164
+ export function Segmented({ defaultValue, className, children }: { defaultValue: string; className?: string; children: ReactNode }) {
165
+ return (
166
+ <Tabs.Root data-slot="segmented" defaultValue={defaultValue} className={cx("flex min-w-0 flex-col", className)}>
167
+ {children}
168
+ </Tabs.Root>
169
+ );
170
+ }
171
+
172
+ /** The track of a `Segmented`: put it where the switch belongs, usually a toolbar. */
173
+ export function SegmentedList({ items, label }: { items: { value: string; label: string; icon?: LucideIcon }[]; label: string }) {
174
+ return (
175
+ <Tabs.List aria-label={label} data-slot="segmented-list" className="relative flex items-center gap-0.5 rounded-lg border border-border p-0.5">
176
+ {items.map(({ value, label: text, icon: Icon }) => (
177
+ <Tabs.Tab
178
+ key={value}
179
+ value={value}
180
+ className={cx(
181
+ "relative inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-muted-foreground transition-colors duration-(--dt-duration-fast)",
182
+ "after:absolute after:inset-x-0 after:-inset-y-[calc((var(--docs-target,2.75rem)-1.75rem)/2)] after:content-['']",
183
+ "hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-active:bg-muted data-active:text-canvas-foreground",
184
+ )}
185
+ >
186
+ {Icon && <Icon aria-hidden className="size-(--dt-size-xs)" />}
187
+ {text}
188
+ </Tabs.Tab>
189
+ ))}
190
+ </Tabs.List>
191
+ );
192
+ }
193
+
194
+ export function SegmentedPanel({ value, className, children }: { value: string; className?: string; children: ReactNode }) {
195
+ return (
196
+ <Tabs.Panel value={value} className={cx("min-w-0 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring", className)}>
197
+ {children}
198
+ </Tabs.Panel>
199
+ );
200
+ }
201
+
202
+ /** A small label above a group of controls or a column. */
203
+ export function Eyebrow({ className, children }: { className?: string; children: ReactNode }) {
204
+ return <p className={cx("text-xs font-medium tracking-wide text-muted-foreground uppercase", className)}>{children}</p>;
205
+ }
206
+
207
+ /**
208
+ * A toolbar menu for one setting: the trigger shows an icon and, when the setting
209
+ * is not at its default, its current value; the menu lists every option with the
210
+ * chosen one checked. For viewport, background, colour vision and the like.
211
+ */
212
+ export function ToolbarMenu<T extends string>({
213
+ icon: Icon,
214
+ label,
215
+ value,
216
+ fallback,
217
+ options,
218
+ onChange,
219
+ swatch,
220
+ }: {
221
+ icon: LucideIcon;
222
+ /** What the setting is, for the trigger's name and the menu's heading. */
223
+ label: string;
224
+ value: T;
225
+ /** The default: while chosen, the trigger shows only its icon. */
226
+ fallback: T;
227
+ options: { value: T; label: string; note?: string }[];
228
+ onChange: (value: T) => void;
229
+ /** A colour to paint beside each option: for backgrounds. */
230
+ swatch?: (value: T) => string | undefined;
231
+ }) {
232
+ const current = options.find((o) => o.value === value);
233
+ const changed = value !== fallback;
234
+ return (
235
+ <Menu.Root>
236
+ <Menu.Trigger
237
+ aria-label={`${label}: ${current?.label ?? value}`}
238
+ title={label}
239
+ className={cx(
240
+ "relative inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors duration-(--dt-duration-fast)",
241
+ "after:absolute after:inset-x-0 after:-inset-y-[calc((var(--docs-target,2.75rem)-2rem)/2)] after:content-['']",
242
+ "hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-popup-open:bg-muted data-popup-open:text-canvas-foreground",
243
+ changed && "bg-muted text-canvas-foreground",
244
+ )}
245
+ >
246
+ {swatch && swatch(value) ? (
247
+ <span aria-hidden className="size-3.5 rounded-full ring-1 ring-border" style={{ background: swatch(value) }} />
248
+ ) : (
249
+ <Icon aria-hidden className="size-(--dt-size-xs)" />
250
+ )}
251
+ {changed && <span className="max-w-28 truncate">{current?.label ?? value}</span>}
252
+ <ChevronDown aria-hidden className="size-3 opacity-60" />
253
+ </Menu.Trigger>
254
+ <Menu.Portal>
255
+ <Menu.Positioner data-designtools="" sideOffset={6} align="start" className="z-50">
256
+ <Menu.Popup className="min-w-56 rounded-xl border border-border bg-overlay p-1.5 text-overlay-foreground shadow-lg transition-[opacity,scale] duration-(--dt-duration-fast) data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0">
257
+ <Menu.Group>
258
+ <Menu.GroupLabel className="px-3 pt-1.5 pb-1 text-xs text-muted-foreground">{label}</Menu.GroupLabel>
259
+ <Menu.RadioGroup value={value} onValueChange={(v) => onChange(v as T)}>
260
+ {options.map((o) => (
261
+ <Menu.RadioItem
262
+ key={o.value}
263
+ value={o.value}
264
+ closeOnClick
265
+ className="grid min-h-(--docs-target,2.75rem) cursor-pointer grid-cols-[1rem_1fr] items-center gap-3 rounded-md px-3 text-sm outline-none select-none data-highlighted:bg-muted"
266
+ >
267
+ <Menu.RadioItemIndicator keepMounted className="flex items-center justify-center data-unchecked:invisible">
268
+ <Check aria-hidden className="size-4" />
269
+ </Menu.RadioItemIndicator>
270
+ <span className="flex items-center gap-2.5">
271
+ {swatch?.(o.value) && <span aria-hidden className="size-4 shrink-0 rounded-full ring-1 ring-border" style={{ background: swatch(o.value) }} />}
272
+ <span className="flex flex-col">
273
+ {o.label}
274
+ {o.note && <span className="text-xs text-muted-foreground">{o.note}</span>}
275
+ </span>
276
+ </span>
277
+ </Menu.RadioItem>
278
+ ))}
279
+ </Menu.RadioGroup>
280
+ </Menu.Group>
281
+ </Menu.Popup>
282
+ </Menu.Positioner>
283
+ </Menu.Portal>
284
+ </Menu.Root>
285
+ );
286
+ }
287
+
288
+ /** A thin upright rule between groups of toolbar controls. */
289
+ export function ToolbarDivider() {
290
+ return <span aria-hidden className="mx-1 h-5 w-px shrink-0 bg-border" />;
291
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * How the preview looks to people who see colour differently, or less sharply.
3
+ * SVG colour matrices for the three common colour-vision deficiencies and for no
4
+ * colour at all, from Machado, Oliveira and Fernandes (2009) at full severity, and
5
+ * a blur for low acuity. A simulation is a prompt to look again, not a test.
6
+ */
7
+
8
+ export type Vision = "typical" | "protanopia" | "deuteranopia" | "tritanopia" | "achromatopsia" | "blurred";
9
+
10
+ export const VISIONS: { value: Vision; label: string; note: string }[] = [
11
+ { value: "typical", label: "Typical", note: "No simulation" },
12
+ { value: "protanopia", label: "Protanopia", note: "No red cones, about 1% of men" },
13
+ { value: "deuteranopia", label: "Deuteranopia", note: "No green cones, about 1% of men" },
14
+ { value: "tritanopia", label: "Tritanopia", note: "No blue cones, rare" },
15
+ { value: "achromatopsia", label: "Achromatopsia", note: "No colour at all" },
16
+ { value: "blurred", label: "Blurred", note: "Low acuity, or a screen far away" },
17
+ ];
18
+
19
+ const MATRICES: Record<Exclude<Vision, "typical" | "blurred">, string> = {
20
+ protanopia: "0.152 1.053 -0.205 0 0 0.115 0.786 0.099 0 0 -0.004 -0.048 1.052 0 0 0 0 0 1 0",
21
+ deuteranopia: "0.367 0.861 -0.228 0 0 0.280 0.673 0.047 0 0 -0.012 0.043 0.969 0 0 0 0 0 1 0",
22
+ tritanopia: "1.256 -0.077 -0.179 0 0 -0.078 0.931 0.148 0 0 0.005 0.691 0.304 0 0 0 0 0 1 0",
23
+ achromatopsia: "0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0",
24
+ };
25
+
26
+ /** The filters, once per page; `filterFor` points an element at one. */
27
+ export function VisionFilters({ id }: { id: string }) {
28
+ return (
29
+ <svg aria-hidden width="0" height="0" className="absolute">
30
+ {Object.entries(MATRICES).map(([name, values]) => (
31
+ <filter key={name} id={`${id}-${name}`} colorInterpolationFilters="linearRGB">
32
+ <feColorMatrix type="matrix" values={values} />
33
+ </filter>
34
+ ))}
35
+ </svg>
36
+ );
37
+ }
38
+
39
+ /** The CSS `filter` for a vision, given the id the filters were rendered with. */
40
+ export function filterFor(vision: Vision, id: string): string | undefined {
41
+ if (vision === "typical") return undefined;
42
+ if (vision === "blurred") return "blur(1.5px)";
43
+ return `url(#${id}-${vision})`;
44
+ }
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import { Scale } from "lucide-react";
4
+ import type { ReactNode } from "react";
5
+ import { useLookup } from "./lib/lookup";
6
+ import type { AssetEntry, RuleEntry } from "./lib/manifest-types";
7
+ import { StatusBadge } from "./lib/status";
8
+ import { inline } from "./lib/text";
9
+
10
+ const LENGTH = /^\s*(\d+(?:\.\d+)?)(px|rem|em)\b/;
11
+
12
+ /**
13
+ * Each logo on the surfaces it may sit on, at its minimum size, with its clear
14
+ * space drawn, and the brand rules that govern it: rules for every logo once,
15
+ * above them, and a rule naming one logo under that logo. Surfaces are semantic
16
+ * colour names painted from the light-mode tokens whatever the docs' mode, because a
17
+ * brand book's surfaces are fixed colours (white, the brand colour, an off-white),
18
+ * and dark mode would otherwise show the logo on a surface the brand forbids.
19
+ */
20
+ export function LogoUsage({ assets, rules = [], rulesHref }: { assets: AssetEntry[]; rules?: RuleEntry[]; rulesHref?: string }) {
21
+ const lookup = useLookup();
22
+ const logos = assets.filter((a) => a.kind === "logo");
23
+ if (logos.length === 0) return <p className="text-sm text-muted-foreground">No logos in the brand folder yet.</p>;
24
+ const brand = rules.filter((r) => r.kind === "brand");
25
+ const general = brand.filter((r) => (r.appliesTo ?? []).some((x) => ["logo", "logos"].includes(x.toLowerCase())));
26
+ return (
27
+ <div data-slot="logo-usage" className="flex flex-col gap-16">
28
+ {general.length > 0 && <RuleList rules={general} rulesHref={rulesHref} />}
29
+ {logos.map((logo) => {
30
+ const src = lookup.assetUrl(logo);
31
+ const names = new Set([logo.name.toLowerCase(), logo.file.toLowerCase()]);
32
+ const own = brand.filter((r) => !general.includes(r) && (r.appliesTo ?? []).some((x) => names.has(x.toLowerCase())));
33
+ const clear = LENGTH.exec(logo.clearSpace ?? "");
34
+ const min = LENGTH.exec(logo.minSize ?? "");
35
+ const surfaces = logo.surfaces?.length ? logo.surfaces : ["canvas"];
36
+ const paint = (surface: string) => ({ background: `var(--${surface})`, color: `var(--${surface}-foreground, var(--canvas-foreground))` });
37
+ const facts: [string, ReactNode][] = [
38
+ [
39
+ "Minimum size",
40
+ <span key="m" className="flex flex-wrap items-center gap-3">
41
+ {logo.minSize ?? "not set"}
42
+ {src && min && (
43
+ // on the logo's own first surface, so a reversed (white) logo is visible at the size it is shown at
44
+ <span data-slot="logo-min-size" data-theme="light" className="inline-flex rounded-md border border-border p-2" style={paint(surfaces[0])}>
45
+ <img src={src} alt="" aria-hidden className="block w-auto" style={{ height: `${min[1]}${min[2]}` }} />
46
+ </span>
47
+ )}
48
+ </span>,
49
+ ],
50
+ ["Clear space", logo.clearSpace ?? "not set"],
51
+ ];
52
+ if (logo.width && logo.height) facts.push(["Proportions", <span key="p" className="font-mono text-xs">{logo.width} × {logo.height}</span>]);
53
+ return (
54
+ <section key={logo.file} data-slot="logo" className="flex flex-col gap-5">
55
+ <header className="flex flex-col gap-1.5">
56
+ <h3 className="text-3xl tracking-tight">{logo.name}</h3>
57
+ {logo.usage && <p className="max-w-2xl text-base text-muted-foreground">{inline(logo.usage)}</p>}
58
+ </header>
59
+ <div className="grid gap-3 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
60
+ {/* the logo large, on its first surface, with its clear space drawn and named */}
61
+ <div
62
+ data-slot="logo-surface"
63
+ data-theme="light"
64
+ className="relative flex min-h-72 items-center justify-center overflow-hidden rounded-2xl border border-border p-10"
65
+ style={paint(surfaces[0])}
66
+ >
67
+ <span className="absolute top-4 left-4 font-mono text-xs opacity-70">{surfaces[0]}</span>
68
+ <span className="relative inline-flex">
69
+ <span
70
+ data-slot="logo-clear-space"
71
+ className="inline-flex border border-dashed"
72
+ style={{ padding: clear ? `calc(${clear[1]}${clear[2]} * 2)` : "1.5rem", borderColor: "color-mix(in oklch, currentColor 45%, transparent)" }}
73
+ title={`Clear space: ${logo.clearSpace ?? "not set"}`}
74
+ >
75
+ {src ? <img src={src} alt={logo.alt ?? logo.name} className="block h-16 w-auto sm:h-20" /> : <span className="text-xs">{logo.file}</span>}
76
+ </span>
77
+ {clear && (
78
+ <span className="absolute -bottom-6 left-1/2 -translate-x-1/2 font-mono text-xs whitespace-nowrap opacity-70">clear space, shown at 2×</span>
79
+ )}
80
+ </span>
81
+ </div>
82
+ <ul className="grid grid-cols-2 gap-3 lg:grid-cols-1 lg:grid-rows-3">
83
+ {surfaces.slice(1).map((surface) => (
84
+ <li
85
+ key={surface}
86
+ data-slot="logo-surface"
87
+ data-theme="light"
88
+ className="relative flex min-h-28 items-center justify-center rounded-2xl border border-border p-6"
89
+ style={paint(surface)}
90
+ >
91
+ <span className="absolute top-3 left-3 font-mono text-xs opacity-70">{surface}</span>
92
+ {src && <img src={src} alt="" aria-hidden className="block h-9 w-auto" />}
93
+ </li>
94
+ ))}
95
+ {surfaces.length === 1 && (
96
+ <li className="col-span-2 flex items-center rounded-2xl border border-dashed border-border p-6 text-sm text-muted-foreground lg:col-span-1 lg:row-span-3">
97
+ Only on {surfaces[0]}.
98
+ </li>
99
+ )}
100
+ </ul>
101
+ </div>
102
+ <dl className="grid gap-x-10 gap-y-5 sm:grid-cols-3">
103
+ {facts.map(([term, detail]) => (
104
+ <div key={term} className="flex flex-col gap-1.5">
105
+ <dt className="text-xs font-medium text-muted-foreground">{term}</dt>
106
+ <dd className="text-sm">{detail}</dd>
107
+ </div>
108
+ ))}
109
+ </dl>
110
+ {own.length > 0 && <RuleList rules={own} rulesHref={rulesHref} />}
111
+ </section>
112
+ );
113
+ })}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ function RuleList({ rules, rulesHref }: { rules: RuleEntry[]; rulesHref?: string }) {
119
+ return (
120
+ <ul data-slot="logo-rules" className="grid gap-x-12 sm:grid-cols-2">
121
+ {rules.map((r) => (
122
+ <li key={r.id} className="border-t border-border">
123
+ <a
124
+ href={rulesHref ? `${rulesHref}#${r.id}` : `#${r.id}`}
125
+ className="group flex h-full flex-col gap-1.5 py-4 text-sm focus-visible:outline-2 focus-visible:outline-ring"
126
+ >
127
+ <span className="flex items-center justify-between gap-2">
128
+ <span className="flex items-center gap-2 text-base font-medium group-hover:underline group-hover:underline-offset-4">
129
+ <Scale aria-hidden className="size-(--dt-size-xs) text-muted-foreground" />
130
+ {r.title}
131
+ </span>
132
+ <StatusBadge status={r.status} />
133
+ </span>
134
+ <span className="text-muted-foreground">{inline(r.statement)}</span>
135
+ </a>
136
+ </li>
137
+ ))}
138
+ </ul>
139
+ );
140
+ }
@@ -0,0 +1,101 @@
1
+ import type { ComponentType, ReactElement, ReactNode } from "react";
2
+ import type { AdherenceReport } from "../lib/adherence-types";
3
+
4
+ /** A page people write: brand, principles, a design language, with its author and how settled it is. */
5
+ export type EditorialPage = EditorialBase &
6
+ (
7
+ | {
8
+ /** Markdown: `##` and `###` sections, paragraphs, bullet lists, bold, code and links; a list whose items open in bold is a set of principles. */
9
+ body: string;
10
+ markdown?: undefined;
11
+ }
12
+ | {
13
+ /**
14
+ * The page as React: real components from the project's system beside the docs' own `DocsSection`
15
+ * and `Prose`, for a page that has to show rather than tell. Rendered as is, under the page header.
16
+ */
17
+ body: ReactNode;
18
+ /** What agents read instead, at the page's `.md`: the same page, in words. */
19
+ markdown: string;
20
+ }
21
+ );
22
+
23
+ interface EditorialBase {
24
+ /** Its path under the docs, e.g. `brand` or `brand/accessibility`. */
25
+ slug: string;
26
+ title: string;
27
+ lead?: string;
28
+ by: { author: string; reviewer?: string; reviewed: string; status: "draft" | "published" };
29
+ /** The navigation group it sits in. Default: brand. */
30
+ section?: "brand" | "foundations" | "standards";
31
+ /** Its label in the navigation, when shorter than its title. */
32
+ nav?: string;
33
+ }
34
+
35
+ /** What a component needs to render on its workbench. */
36
+ export interface WorkbenchSetup {
37
+ /** Starting props: what it needs to render at all (a Select's options). */
38
+ props?: Record<string, unknown>;
39
+ /** Text inside it. "" for a component that takes none. */
40
+ children?: string;
41
+ /** Children richer than text: a Card's parts, a Tabs' list and panels. */
42
+ content?: ReactNode;
43
+ /** What it must sit inside to work: a provider, the root a part belongs to. */
44
+ wrap?: ReactElement<{ children?: ReactNode }>;
45
+ }
46
+
47
+ /** The built-in generated pages, each of which can be turned off. */
48
+ export type BuiltInPage = "logo" | "imagery" | "voice" | "colour" | "type" | "scale" | "rules" | "patterns" | "components" | "standards" | "vocabulary";
49
+
50
+ /**
51
+ * Everything one site of docs needs: the manifest it renders, the registry that
52
+ * reaches the project's components, the brand around it, and what each part
53
+ * says. The pages, their order, the navigation, search and the markdown for
54
+ * agents all come from this.
55
+ */
56
+ export interface DocsConfig {
57
+ title: string;
58
+ /** Where the docs are served. Default: `/design/system`. */
59
+ basePath?: string;
60
+ /** The client's logo, beside the title in the navigation. */
61
+ logo?: ReactNode;
62
+ /** A full-bleed introduction across the top of the overview. Without one the overview has a plain header. */
63
+ hero?: ReactNode;
64
+ /** The overview's lead, when there is no hero. */
65
+ lead?: string;
66
+ /**
67
+ * The overview after its hero or header: `agentic: false` drops "Why this is agentic", and `agenticLead`
68
+ * replaces the line that opens it.
69
+ */
70
+ overview?: { agentic?: boolean; agenticLead?: string };
71
+ /** The generated registry (`designtools-docs map`): a provider for the project's components, examples and assets. */
72
+ registry: ComponentType<{ children: ReactNode }>;
73
+ /** The manifest, as `designtools-manifest build` writes it. */
74
+ manifest: {
75
+ components: unknown;
76
+ tokens: unknown;
77
+ rules?: unknown;
78
+ patterns?: unknown;
79
+ taxonomy?: unknown;
80
+ assets?: unknown;
81
+ };
82
+ /** `.mxa/stack.json`'s baselines, for the standards page. */
83
+ baselines?: Record<string, unknown>;
84
+ /** `designtools-manifest provenance`'s line: the version and commit the docs were built from. */
85
+ provenance?: string;
86
+ /** `designtools-adherence --json`, for the standards page. */
87
+ adherence?: AdherenceReport;
88
+ /** Pages people write, in the order they appear. */
89
+ editorial?: EditorialPage[];
90
+ /** Built-in pages to leave out. */
91
+ hide?: BuiltInPage[];
92
+ /** Per component, by name: what it needs on its workbench. */
93
+ workbench?: Record<string, WorkbenchSetup>;
94
+ /** Per rule, by id: a demonstration shown on its card, for a rule that needs persuading. */
95
+ demonstrations?: Record<string, ReactNode>;
96
+ /**
97
+ * Blocks replaced by the project's own, by name: `{ Usage: LocalUsage }`.
98
+ * `designtools-docs eject <block>` copies one into the project to start from.
99
+ */
100
+ blocks?: Partial<Record<string, ComponentType<any>>>;
101
+ }