@designtools/docs 0.0.0-stage → 0.1.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 (167) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -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 +38 -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 +14 -0
  21. package/dist/imagery.d.ts +6 -0
  22. package/dist/imagery.js +17 -0
  23. package/dist/index.d.ts +34 -0
  24. package/dist/index.js +33 -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/lookup.d.ts +35 -0
  36. package/dist/lib/lookup.js +41 -0
  37. package/dist/lib/manifest-types.d.ts +253 -0
  38. package/dist/lib/manifest-types.js +14 -0
  39. package/dist/lib/manifest.d.ts +98 -0
  40. package/dist/lib/manifest.js +173 -0
  41. package/dist/lib/markdown.d.ts +50 -0
  42. package/dist/lib/markdown.js +286 -0
  43. package/dist/lib/measure.d.ts +12 -0
  44. package/dist/lib/measure.js +62 -0
  45. package/dist/lib/pseudo.d.ts +11 -0
  46. package/dist/lib/pseudo.js +71 -0
  47. package/dist/lib/registry.d.ts +21 -0
  48. package/dist/lib/registry.js +11 -0
  49. package/dist/lib/search.d.ts +44 -0
  50. package/dist/lib/search.js +62 -0
  51. package/dist/lib/standards.d.ts +17 -0
  52. package/dist/lib/standards.js +81 -0
  53. package/dist/lib/status.d.ts +7 -0
  54. package/dist/lib/status.js +17 -0
  55. package/dist/lib/text.d.ts +8 -0
  56. package/dist/lib/text.js +9 -0
  57. package/dist/lib/ui.d.ts +100 -0
  58. package/dist/lib/ui.js +89 -0
  59. package/dist/lib/vision.d.ts +18 -0
  60. package/dist/lib/vision.js +27 -0
  61. package/dist/logo-usage.d.ts +14 -0
  62. package/dist/logo-usage.js +45 -0
  63. package/dist/next/config.d.ts +85 -0
  64. package/dist/next/config.js +1 -0
  65. package/dist/next/index.d.ts +62 -0
  66. package/dist/next/index.js +63 -0
  67. package/dist/next/markdown.d.ts +20 -0
  68. package/dist/next/markdown.js +95 -0
  69. package/dist/next/pages.d.ts +6 -0
  70. package/dist/next/pages.js +122 -0
  71. package/dist/next/prose.d.ts +9 -0
  72. package/dist/next/prose.js +37 -0
  73. package/dist/next/proxy.d.ts +19 -0
  74. package/dist/next/proxy.js +56 -0
  75. package/dist/next/site.d.ts +53 -0
  76. package/dist/next/site.js +136 -0
  77. package/dist/page-actions.d.ts +9 -0
  78. package/dist/page-actions.js +32 -0
  79. package/dist/pattern.d.ts +8 -0
  80. package/dist/pattern.js +19 -0
  81. package/dist/playground.d.ts +62 -0
  82. package/dist/playground.js +111 -0
  83. package/dist/preview-frame.d.ts +31 -0
  84. package/dist/preview-frame.js +63 -0
  85. package/dist/props-table.d.ts +10 -0
  86. package/dist/props-table.js +45 -0
  87. package/dist/rule.d.ts +12 -0
  88. package/dist/rule.js +32 -0
  89. package/dist/scale.d.ts +9 -0
  90. package/dist/scale.js +58 -0
  91. package/dist/search.d.ts +16 -0
  92. package/dist/search.js +46 -0
  93. package/dist/shell.d.ts +89 -0
  94. package/dist/shell.js +199 -0
  95. package/dist/standards.d.ts +13 -0
  96. package/dist/standards.js +19 -0
  97. package/dist/styles.css +3519 -0
  98. package/dist/swatches.d.ts +28 -0
  99. package/dist/swatches.js +94 -0
  100. package/dist/theme-switch.d.ts +20 -0
  101. package/dist/theme-switch.js +68 -0
  102. package/dist/type-ramp.d.ts +10 -0
  103. package/dist/type-ramp.js +31 -0
  104. package/dist/usage.d.ts +13 -0
  105. package/dist/usage.js +55 -0
  106. package/dist/variant-matrix.d.ts +15 -0
  107. package/dist/variant-matrix.js +53 -0
  108. package/dist/voice-terms.d.ts +5 -0
  109. package/dist/voice-terms.js +13 -0
  110. package/dist/workbench.d.ts +33 -0
  111. package/dist/workbench.js +273 -0
  112. package/package.json +107 -3
  113. package/src/a11y-panel.tsx +297 -0
  114. package/src/adherence-summary.tsx +134 -0
  115. package/src/agent-view.tsx +54 -0
  116. package/src/anatomy.tsx +114 -0
  117. package/src/ask-claude.tsx +90 -0
  118. package/src/code-view.tsx +112 -0
  119. package/src/css.d.ts +1 -0
  120. package/src/examples.tsx +141 -0
  121. package/src/glossary.tsx +71 -0
  122. package/src/imagery.tsx +64 -0
  123. package/src/index.ts +34 -0
  124. package/src/lib/adherence-types.ts +181 -0
  125. package/src/lib/boundary.tsx +21 -0
  126. package/src/lib/contrast.ts +44 -0
  127. package/src/lib/cx.ts +30 -0
  128. package/src/lib/jsx.ts +29 -0
  129. package/src/lib/lookup.tsx +49 -0
  130. package/src/lib/manifest-types.ts +284 -0
  131. package/src/lib/manifest.ts +228 -0
  132. package/src/lib/markdown.ts +288 -0
  133. package/src/lib/measure.tsx +101 -0
  134. package/src/lib/pseudo.ts +74 -0
  135. package/src/lib/registry.tsx +29 -0
  136. package/src/lib/search.ts +99 -0
  137. package/src/lib/standards.ts +82 -0
  138. package/src/lib/status.tsx +32 -0
  139. package/src/lib/text.tsx +27 -0
  140. package/src/lib/ui.tsx +291 -0
  141. package/src/lib/vision.tsx +44 -0
  142. package/src/logo-usage.tsx +140 -0
  143. package/src/next/config.ts +80 -0
  144. package/src/next/index.tsx +100 -0
  145. package/src/next/markdown.ts +119 -0
  146. package/src/next/pages.tsx +370 -0
  147. package/src/next/prose.tsx +87 -0
  148. package/src/next/proxy.ts +55 -0
  149. package/src/next/site.ts +173 -0
  150. package/src/page-actions.tsx +92 -0
  151. package/src/pattern.tsx +67 -0
  152. package/src/playground.tsx +352 -0
  153. package/src/preview-frame.tsx +131 -0
  154. package/src/props-table.tsx +154 -0
  155. package/src/rule.tsx +116 -0
  156. package/src/scale.tsx +175 -0
  157. package/src/search.tsx +155 -0
  158. package/src/shell.tsx +555 -0
  159. package/src/standards.tsx +58 -0
  160. package/src/styles.css +155 -0
  161. package/src/swatches.tsx +287 -0
  162. package/src/theme-switch.tsx +90 -0
  163. package/src/type-ramp.tsx +150 -0
  164. package/src/usage.tsx +148 -0
  165. package/src/variant-matrix.tsx +195 -0
  166. package/src/voice-terms.tsx +46 -0
  167. package/src/workbench.tsx +584 -0
@@ -0,0 +1,370 @@
1
+ import { ArrowRight, ArrowUpRight } from "lucide-react";
2
+ import Link from "next/link";
3
+ import { AdherenceSummary } from "../adherence-summary";
4
+ import { Anatomy } from "../anatomy";
5
+ import { Examples } from "../examples";
6
+ import { Glossary } from "../glossary";
7
+ import { Imagery } from "../imagery";
8
+ import { groupFamilies, slugOf } from "../lib/manifest";
9
+ import { StatusBadge } from "../lib/status";
10
+ import { LogoUsage } from "../logo-usage";
11
+ import { PageActions } from "../page-actions";
12
+ import { Pattern } from "../pattern";
13
+ import { PreviewFrame } from "../preview-frame";
14
+ import { PropsTable } from "../props-table";
15
+ import { Rule } from "../rule";
16
+ import { Scale } from "../scale";
17
+ import { DocsFooter, DocsHeader, DocsSection } from "../shell";
18
+ import { Standards } from "../standards";
19
+ import { PageContrast, RampSwatches, SemanticSwatches } from "../swatches";
20
+ import { TypeRamp } from "../type-ramp";
21
+ import { Usage } from "../usage";
22
+ import { VariantMatrix } from "../variant-matrix";
23
+ import { VoiceTerms } from "../voice-terms";
24
+ import { Workbench } from "../workbench";
25
+ import { Markdown } from "./prose";
26
+ import { COPY, type PageRef, type Site } from "./site";
27
+
28
+ /** Every block a page renders, by the name `config.blocks` replaces it under. */
29
+ const DEFAULT_BLOCKS = {
30
+ AdherenceSummary,
31
+ Anatomy,
32
+ Examples,
33
+ Glossary,
34
+ Imagery,
35
+ LogoUsage,
36
+ PageContrast,
37
+ Pattern,
38
+ PreviewFrame,
39
+ PropsTable,
40
+ RampSwatches,
41
+ Rule,
42
+ Scale,
43
+ SemanticSwatches,
44
+ Standards,
45
+ TypeRamp,
46
+ Usage,
47
+ VariantMatrix,
48
+ VoiceTerms,
49
+ Workbench,
50
+ };
51
+ type Blocks = typeof DEFAULT_BLOCKS;
52
+
53
+ function blocksOf(site: Site): Blocks {
54
+ return { ...DEFAULT_BLOCKS, ...(site.config.blocks as Partial<Blocks> | undefined) };
55
+ }
56
+
57
+ /** The page for a reference, composed of blocks. */
58
+ export function DocsPageView({ site, page }: { site: Site; page: PageRef }) {
59
+ const B = blocksOf(site);
60
+ const { base, config } = site;
61
+ const footer = <DocsFooter provenance={config.provenance} />;
62
+ const header = (title: string, lead?: string) => (
63
+ <DocsHeader title={title} lead={lead}>
64
+ <PageActions basePath={base} />
65
+ </DocsHeader>
66
+ );
67
+
68
+ switch (page.kind) {
69
+ case "overview":
70
+ return <Overview site={site} />;
71
+
72
+ case "editorial":
73
+ return (
74
+ <>
75
+ {header(page.page.title, page.page.lead)}
76
+ <Markdown text={page.page.body} />
77
+ <DocsFooter editorial={page.page.by} />
78
+ </>
79
+ );
80
+
81
+ case "logo":
82
+ return (
83
+ <>
84
+ {header(COPY.logo.title, COPY.logo.lead)}
85
+ <B.LogoUsage assets={site.assets.assets} rules={site.rules.rules} rulesHref={`${base}/rules`} />
86
+ {footer}
87
+ </>
88
+ );
89
+
90
+ case "imagery":
91
+ return (
92
+ <>
93
+ {header(COPY.imagery.title, COPY.imagery.lead)}
94
+ <B.Imagery assets={site.assets.assets} />
95
+ {footer}
96
+ </>
97
+ );
98
+
99
+ case "voice":
100
+ return (
101
+ <>
102
+ {header(COPY.voice.title, COPY.voice.lead)}
103
+ <B.VoiceTerms terms={site.taxonomy.terms} />
104
+ {footer}
105
+ </>
106
+ );
107
+
108
+ case "colour":
109
+ return (
110
+ <>
111
+ {header(COPY.colour.title, COPY.colour.lead)}
112
+ <DocsSection title="Semantic colours" lead="Each pair is measured on the colours the browser paints, in each mode.">
113
+ <B.SemanticSwatches tokens={site.tokens} />
114
+ </DocsSection>
115
+ <DocsSection
116
+ title="On the page"
117
+ lead="Each colour on each background. A label is measured on its own fill above; this is the rest: a colour used as text on the page needs 4.5:1, as a shape (a meter fill, an icon without words) 3:1."
118
+ >
119
+ <B.PageContrast tokens={site.tokens} />
120
+ </DocsSection>
121
+ <DocsSection title="Ramps">
122
+ <B.RampSwatches tokens={site.tokens} />
123
+ </DocsSection>
124
+ {footer}
125
+ </>
126
+ );
127
+
128
+ case "type":
129
+ return (
130
+ <>
131
+ {header(COPY.type.title, COPY.type.lead)}
132
+ <B.TypeRamp tokens={site.tokens} />
133
+ {footer}
134
+ </>
135
+ );
136
+
137
+ case "scale":
138
+ return (
139
+ <>
140
+ {header(COPY.scale.title, COPY.scale.lead)}
141
+ <B.Scale tokens={site.tokens} />
142
+ {footer}
143
+ </>
144
+ );
145
+
146
+ case "rules":
147
+ return (
148
+ <>
149
+ {header(COPY.rules.title, COPY.rules.lead)}
150
+ {(["brand", "interface"] as const).map((kind) => {
151
+ const rules = site.rules.rules.filter((r) => r.kind === kind);
152
+ return rules.length ? (
153
+ <DocsSection key={kind} title={kind === "brand" ? "Brand" : "Interface"}>
154
+ {rules.map((r) => (
155
+ <B.Rule key={r.id} rule={r}>
156
+ {config.demonstrations?.[r.id]}
157
+ </B.Rule>
158
+ ))}
159
+ </DocsSection>
160
+ ) : null;
161
+ })}
162
+ {footer}
163
+ </>
164
+ );
165
+
166
+ case "standards":
167
+ return (
168
+ <>
169
+ {header(COPY.standards.title, COPY.standards.lead)}
170
+ {config.baselines && (
171
+ <DocsSection title="Targets">
172
+ <B.Standards baselines={config.baselines} />
173
+ </DocsSection>
174
+ )}
175
+ {config.adherence && (
176
+ <DocsSection title="Adherence" lead="How closely the product follows the system, from @designtools/adherence. It informs; it never blocks.">
177
+ <B.AdherenceSummary report={config.adherence} />
178
+ </DocsSection>
179
+ )}
180
+ {footer}
181
+ </>
182
+ );
183
+
184
+ case "vocabulary":
185
+ return (
186
+ <>
187
+ {header(COPY.vocabulary.title, COPY.vocabulary.lead)}
188
+ <B.Glossary terms={site.taxonomy.terms} />
189
+ {footer}
190
+ </>
191
+ );
192
+
193
+ case "pattern":
194
+ return (
195
+ <>
196
+ {header(page.pattern.name, page.pattern.description)}
197
+ <B.Pattern pattern={page.pattern} manifest={site.components} basePath={base} />
198
+ <DocsFooter source={page.pattern.source} provenance={config.provenance} />
199
+ </>
200
+ );
201
+
202
+ case "component":
203
+ return <ComponentPage site={site} blocks={B} entry={page.entry} />;
204
+ }
205
+ }
206
+
207
+ function ComponentPage({ site, blocks: B, entry }: { site: Site; blocks: Blocks; entry: Extract<PageRef, { kind: "component" }>["entry"] }) {
208
+ const { base, config } = site;
209
+ // A part (TabsPanel, CardHeader) renders inside its family's root, so it is shown there, never alone:
210
+ // a part rendered on its own can throw for want of the root's context.
211
+ const head = entry.category ? undefined : site.components.components.find((c) => c.source === entry.source && c.category);
212
+ if (head)
213
+ return (
214
+ <>
215
+ <DocsHeader title={entry.name} lead={entry.description ?? `A part of ${head.name}, used inside it.`}>
216
+ <PageActions basePath={base} />
217
+ </DocsHeader>
218
+ <DocsSection title="Part of" lead={`Its examples, variants and anatomy are on ${head.name}'s page, where it renders inside the whole.`}>
219
+ <Link
220
+ href={`${base}/components/${slugOf(head)}`}
221
+ className="group flex max-w-md items-center justify-between gap-3 border-t border-border py-5 focus-visible:outline-2 focus-visible:outline-ring"
222
+ >
223
+ <span className="flex flex-col gap-1">
224
+ <span className="text-xl tracking-tight underline-offset-4 group-hover:underline">{head.name}</span>
225
+ {head.description && <span className="line-clamp-2 text-sm text-muted-foreground">{head.description}</span>}
226
+ </span>
227
+ <ArrowRight aria-hidden className="size-5 shrink-0 text-muted-foreground" />
228
+ </Link>
229
+ </DocsSection>
230
+ <DocsSection title="Props">
231
+ <B.PropsTable entry={entry} />
232
+ </DocsSection>
233
+ <DocsFooter source={entry.source} provenance={config.provenance} />
234
+ </>
235
+ );
236
+
237
+ const setup = config.workbench?.[entry.name] ?? {};
238
+ const preview = `${base}/preview/${slugOf(entry)}`;
239
+ const first = entry.examples?.canonical?.name;
240
+ // the brand's named backgrounds (--surface-<name>), offered on the workbench's stage
241
+ const backgrounds = site.tokens.tokens.map((t) => t.name.slice(2)).filter((n) => /^surface-[a-z]/.test(n) && !n.endsWith("-foreground"));
242
+
243
+ return (
244
+ <>
245
+ <DocsHeader title={entry.name} lead={entry.description}>
246
+ <PageActions basePath={base} />
247
+ </DocsHeader>
248
+
249
+ <DocsSection title="Usage">
250
+ <B.Usage entry={entry} manifest={site.components} basePath={base} />
251
+ </DocsSection>
252
+
253
+ {/* the component's one main area: live, with its properties, code, props, actions, agent view and accessibility */}
254
+ <B.Workbench
255
+ entry={entry}
256
+ props={setup.props}
257
+ content={setup.content}
258
+ wrap={setup.wrap}
259
+ backgrounds={backgrounds}
260
+ previewHref={first && `${preview}/${first}`}
261
+ >
262
+ {setup.children}
263
+ </B.Workbench>
264
+
265
+ <DocsSection title="Anatomy">
266
+ <B.Anatomy entry={entry} />
267
+ </DocsSection>
268
+
269
+ <DocsSection title="Examples" lead="The usual use first, then each do beside its don't. Every one is the real component.">
270
+ <B.Examples entry={entry} previewHref={preview} />
271
+ </DocsSection>
272
+
273
+ {!!entry.variants?.axes.length && (
274
+ <DocsSection
275
+ title="Variants"
276
+ lead={`Every combination, from the ${entry.variants.from === "props" ? "props' types" : `${entry.variants.from}() config`}, then each state.`}
277
+ >
278
+ <B.VariantMatrix entry={entry} props={setup.props}>
279
+ {setup.children === "" ? undefined : (setup.content ?? entry.name)}
280
+ </B.VariantMatrix>
281
+ </DocsSection>
282
+ )}
283
+
284
+ {first && (
285
+ <DocsSection title="At each width">
286
+ <B.PreviewFrame src={`${preview}/${first}`} title={`${entry.name}: ${first}`} />
287
+ </DocsSection>
288
+ )}
289
+
290
+ <DocsFooter source={entry.source} provenance={config.provenance} />
291
+ </>
292
+ );
293
+ }
294
+
295
+ const row = "group flex h-full flex-col gap-2 border-t border-border py-5 focus-visible:outline-2 focus-visible:outline-ring";
296
+
297
+ /** The overview: where to start, then every component by category. Below the hero when there is one. */
298
+ function Overview({ site }: { site: Site }) {
299
+ const { base, config } = site;
300
+ const families = groupFamilies(site.components);
301
+ const count = families.reduce((n, g) => n + g.families.length, 0);
302
+ const brandFirst = site.editorial.find((p) => (p.section ?? "brand") === "brand");
303
+ const starts = [
304
+ brandFirst || site.shows("logo") ? { title: "Brand", href: `${base}/${brandFirst?.slug ?? "brand/logo"}`, line: "Principles, the logo, imagery and the words we use." } : null,
305
+ { title: "Foundations", href: `${base}/colour`, line: "Colour, type and the scale, rendered from the tokens." },
306
+ site.rules.rules.length ? { title: "Rules", href: `${base}/rules`, line: `${site.rules.rules.length} rules, brand and interface, each with its check.` } : null,
307
+ site.patterns.patterns.length
308
+ ? { title: "Patterns", href: `${base}/patterns/${site.patterns.patterns[0].id}`, line: `${site.patterns.patterns.length} recurring screens, made of the real components.` }
309
+ : null,
310
+ ].filter((s): s is { title: string; href: string; line: string } => !!s);
311
+
312
+ return (
313
+ <>
314
+ {!config.hero && (
315
+ <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."}>
316
+ <PageActions basePath={base} />
317
+ </DocsHeader>
318
+ )}
319
+ <ol className="flex max-w-3xl flex-col">
320
+ {starts.map(({ title, href, line }, n) => (
321
+ <li key={title} className="border-t border-border first:border-t-0">
322
+ <Link
323
+ href={href}
324
+ className="group grid grid-cols-[3rem_minmax(0,1fr)_auto] items-baseline gap-x-4 py-6 focus-visible:outline-2 focus-visible:outline-ring sm:grid-cols-[4rem_minmax(0,1fr)_auto]"
325
+ >
326
+ <span className="text-lg text-muted-foreground tabular-nums">{String(n + 1).padStart(2, "0")}</span>
327
+ <span className="flex flex-col gap-2">
328
+ <span className="text-2xl leading-snug tracking-tight underline-offset-[6px] group-hover:underline md:text-3xl">{title}</span>
329
+ <span className="text-lg leading-relaxed text-muted-foreground">{line}</span>
330
+ </span>
331
+ <ArrowUpRight
332
+ aria-hidden
333
+ className="size-5 self-center text-muted-foreground transition-transform duration-(--dt-duration-fast) group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
334
+ />
335
+ </Link>
336
+ </li>
337
+ ))}
338
+ </ol>
339
+
340
+ {site.shows("components") && (
341
+ <DocsSection title="Components" lead={`${count} components, grouped as the navigation groups them. Each page has its usage, a workbench, anatomy, examples and variants.`}>
342
+ <div className="flex flex-col gap-12">
343
+ {/* a component's parts (CardHeader, CardTitle) are named on its entry, not given entries of their own */}
344
+ {families.map((group) => (
345
+ <div key={group.category} className="flex flex-col gap-1">
346
+ <h3 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">{group.category}</h3>
347
+ <ul className="grid gap-x-10 sm:grid-cols-2 xl:grid-cols-3">
348
+ {group.families.map(({ head: c, parts }) => (
349
+ <li key={c.source + c.export}>
350
+ <Link href={`${base}/components/${slugOf(c)}`} className={row}>
351
+ <span className="flex items-center justify-between gap-2">
352
+ <span className="text-xl tracking-tight underline-offset-4 group-hover:underline">{c.name}</span>
353
+ <StatusBadge status={c.status} />
354
+ </span>
355
+ {c.description && <span className="line-clamp-2 text-sm text-muted-foreground">{c.description}</span>}
356
+ {parts.length > 0 && <span className="mt-auto font-mono text-xs text-muted-foreground">with {parts.map((p) => p.name).join(", ")}</span>}
357
+ </Link>
358
+ </li>
359
+ ))}
360
+ </ul>
361
+ </div>
362
+ ))}
363
+ </div>
364
+ </DocsSection>
365
+ )}
366
+ <DocsFooter provenance={config.provenance} />
367
+ </>
368
+ );
369
+ }
370
+
@@ -0,0 +1,87 @@
1
+ import { DocsSection } from "../shell";
2
+
3
+ /**
4
+ * The few markdown shapes editorial pages use: `##` sections, paragraphs and bullet lists, with bold
5
+ * and code. Each `##` is a docs section, so it reads at the same size as a generated page and is
6
+ * listed in "On this page". A list whose every item opens in bold ("**Say it plainly.** …") is a set
7
+ * of principles, numbered, each a heading over its sentence.
8
+ */
9
+ export function Markdown({ text }: { text: string }) {
10
+ const sections: { title?: string; blocks: string[] }[] = [{ blocks: [] }];
11
+ for (const b of text.split(/\n{2,}/)) {
12
+ if (b.startsWith("## ")) sections.push({ title: b.slice(3), blocks: [] });
13
+ else sections[sections.length - 1].blocks.push(b);
14
+ }
15
+ return (
16
+ <>
17
+ {sections.map((s, i) =>
18
+ s.title ? (
19
+ <DocsSection key={s.title} title={s.title}>
20
+ <Blocks blocks={s.blocks} />
21
+ </DocsSection>
22
+ ) : (
23
+ s.blocks.length > 0 && <Blocks key={i} blocks={s.blocks} />
24
+ ),
25
+ )}
26
+ </>
27
+ );
28
+ }
29
+
30
+ function Blocks({ blocks }: { blocks: string[] }) {
31
+ return (
32
+ <div className="flex flex-col gap-5">
33
+ {blocks.map((b, i) => {
34
+ const lines = b.split("\n");
35
+ if (lines.every((l) => l.startsWith("- "))) {
36
+ const items = lines.map((l) => l.slice(2));
37
+ const principles = items.every((l) => /^\*\*[^*]+\*\*/.test(l));
38
+ if (principles)
39
+ return (
40
+ <ol key={i} className="flex max-w-3xl flex-col gap-12">
41
+ {items.map((l, n) => {
42
+ const [, head, rest] = /^\*\*([^*]+)\*\*\s*(.*)$/.exec(l) ?? [];
43
+ return (
44
+ <li key={l} className="grid grid-cols-[3rem_minmax(0,1fr)] gap-x-4 gap-y-3 sm:grid-cols-[4rem_minmax(0,1fr)]">
45
+ <span className="pt-1 text-lg text-muted-foreground tabular-nums">{String(n + 1).padStart(2, "0")}</span>
46
+ <span className="flex flex-col gap-3">
47
+ <span className="text-2xl leading-snug tracking-tight md:text-3xl">{head.replace(/\.$/, "")}</span>
48
+ <span className="text-lg leading-relaxed text-canvas-foreground/75">{inline(rest)}</span>
49
+ </span>
50
+ </li>
51
+ );
52
+ })}
53
+ </ol>
54
+ );
55
+ return (
56
+ <ul key={i} className="flex max-w-3xl list-disc flex-col gap-2 pl-5 text-lg leading-relaxed">
57
+ {items.map((l) => (
58
+ <li key={l}>{inline(l)}</li>
59
+ ))}
60
+ </ul>
61
+ );
62
+ }
63
+ return (
64
+ <p key={i} className="max-w-3xl text-lg leading-relaxed text-canvas-foreground/80">
65
+ {inline(b)}
66
+ </p>
67
+ );
68
+ })}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ function inline(text: string) {
74
+ return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/).map((part, i) =>
75
+ part.startsWith("**") ? (
76
+ <strong key={i} className="font-semibold">
77
+ {part.slice(2, -2)}
78
+ </strong>
79
+ ) : part.startsWith("`") ? (
80
+ <code key={i} className="font-mono text-[0.9em]">
81
+ {part.slice(1, -1)}
82
+ </code>
83
+ ) : (
84
+ part
85
+ ),
86
+ );
87
+ }
@@ -0,0 +1,55 @@
1
+ import { type NextRequest, NextResponse } from "next/server";
2
+
3
+ /**
4
+ * One URL, two formats, and never public. A docs URL answers `Accept:
5
+ * text/markdown`, or the same path with `.md`, with markdown generated from the
6
+ * manifest; negotiation is on Accept only, never the user agent, so responses say
7
+ * `Vary: Accept` and the HTML links to its markdown. On a production deployment
8
+ * (`VERCEL_ENV`, or `production` you pass) every docs URL, in every format, is a
9
+ * 404, the route handlers included, which no layout wraps.
10
+ *
11
+ * In `proxy.ts`: `export const proxy = docsProxy(); export const config = { matcher: docsMatcher() };`
12
+ */
13
+ export function docsProxy({ basePath = "/design/system", production }: { basePath?: string; production?: () => boolean } = {}) {
14
+ const base = basePath.replace(/\/$/, "");
15
+ const isProduction = production ?? (() => process.env.VERCEL_ENV === "production");
16
+ return function proxy(request: NextRequest) {
17
+ const { pathname } = request.nextUrl;
18
+ if (isProduction()) return new NextResponse("Not found\n", { status: 404, headers: { "x-robots-tag": "noindex" } });
19
+ if (pathname.startsWith(`${base}/md`) || pathname.startsWith(`${base}/manifest/`) || pathname.startsWith(`${base}/preview/`)) return NextResponse.next();
20
+ const page = pathname.replace(/\.md$/, "").replace(/\/$/, "");
21
+ if (pathname.endsWith(".md") || prefersMarkdown(request.headers.get("accept"))) {
22
+ const url = request.nextUrl.clone();
23
+ url.pathname = `${base}/md${page.slice(base.length)}`;
24
+ return NextResponse.rewrite(url, { headers: { vary: "Accept", "x-robots-tag": "noindex" } });
25
+ }
26
+ // Next 16 replaces Vary on page responses with its own, so this one does not survive on HTML;
27
+ // the markdown responses keep theirs.
28
+ const res = NextResponse.next();
29
+ res.headers.set("vary", "Accept");
30
+ res.headers.set("link", `<${page}.md>; rel="alternate"; type="text/markdown"`);
31
+ res.headers.set("x-robots-tag", "noindex");
32
+ return res;
33
+ };
34
+ }
35
+
36
+ /** The proxy's matcher for a base path. Next reads `config.matcher` statically, so write the result out where it must be a literal. */
37
+ export function docsMatcher(basePath = "/design/system"): string[] {
38
+ const base = basePath.replace(/\/$/, "");
39
+ return [base, `${base}.md`, `${base}/:path*`];
40
+ }
41
+
42
+ /** text/markdown ranked above text/html in the Accept header. */
43
+ export function prefersMarkdown(accept: string | null): boolean {
44
+ if (!accept) return false;
45
+ const q = (type: string) => {
46
+ for (const part of accept.split(",")) {
47
+ const [name, ...params] = part.trim().split(";");
48
+ if (name.trim() !== type) continue;
49
+ const weight = params.map((p) => p.trim()).find((p) => p.startsWith("q="));
50
+ return weight ? Number(weight.slice(2)) : 1;
51
+ }
52
+ return 0;
53
+ };
54
+ return q("text/markdown") > 0 && q("text/markdown") >= q("text/html");
55
+ }