@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,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,80 @@
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. Markdown, with its author and how settled it is. */
5
+ export interface EditorialPage {
6
+ /** Its path under the docs, e.g. `brand` or `brand/accessibility`. */
7
+ slug: string;
8
+ title: string;
9
+ lead?: string;
10
+ /** Markdown: `##` sections, paragraphs, bullet lists; a list whose items open in bold is a set of principles. */
11
+ body: string;
12
+ by: { author: string; reviewer?: string; reviewed: string; status: "draft" | "published" };
13
+ /** The navigation group it sits in. Default: brand. */
14
+ section?: "brand" | "foundations" | "standards";
15
+ /** Its label in the navigation, when shorter than its title. */
16
+ nav?: string;
17
+ }
18
+
19
+ /** What a component needs to render on its workbench. */
20
+ export interface WorkbenchSetup {
21
+ /** Starting props: what it needs to render at all (a Select's options). */
22
+ props?: Record<string, unknown>;
23
+ /** Text inside it. "" for a component that takes none. */
24
+ children?: string;
25
+ /** Children richer than text: a Card's parts, a Tabs' list and panels. */
26
+ content?: ReactNode;
27
+ /** What it must sit inside to work: a provider, the root a part belongs to. */
28
+ wrap?: ReactElement<{ children?: ReactNode }>;
29
+ }
30
+
31
+ /** The built-in generated pages, each of which can be turned off. */
32
+ export type BuiltInPage = "logo" | "imagery" | "voice" | "colour" | "type" | "scale" | "rules" | "patterns" | "components" | "standards" | "vocabulary";
33
+
34
+ /**
35
+ * Everything one site of docs needs: the manifest it renders, the registry that
36
+ * reaches the project's components, the brand around it, and what each part
37
+ * says. The pages, their order, the navigation, search and the markdown for
38
+ * agents all come from this.
39
+ */
40
+ export interface DocsConfig {
41
+ title: string;
42
+ /** Where the docs are served. Default: `/design/system`. */
43
+ basePath?: string;
44
+ /** The client's logo, beside the title in the navigation. */
45
+ logo?: ReactNode;
46
+ /** A full-bleed introduction across the top of the overview. Without one the overview has a plain header. */
47
+ hero?: ReactNode;
48
+ /** The overview's lead, when there is no hero. */
49
+ lead?: string;
50
+ /** The generated registry (`designtools-docs map`): a provider for the project's components, examples and assets. */
51
+ registry: ComponentType<{ children: ReactNode }>;
52
+ /** The manifest, as `designtools-manifest build` writes it. */
53
+ manifest: {
54
+ components: unknown;
55
+ tokens: unknown;
56
+ rules?: unknown;
57
+ patterns?: unknown;
58
+ taxonomy?: unknown;
59
+ assets?: unknown;
60
+ };
61
+ /** `.mxa/stack.json`'s baselines, for the standards page. */
62
+ baselines?: Record<string, unknown>;
63
+ /** `designtools-manifest provenance`'s line: the version and commit the docs were built from. */
64
+ provenance?: string;
65
+ /** `designtools-adherence --json`, for the standards page. */
66
+ adherence?: AdherenceReport;
67
+ /** Pages people write, in the order they appear. */
68
+ editorial?: EditorialPage[];
69
+ /** Built-in pages to leave out. */
70
+ hide?: BuiltInPage[];
71
+ /** Per component, by name: what it needs on its workbench. */
72
+ workbench?: Record<string, WorkbenchSetup>;
73
+ /** Per rule, by id: a demonstration shown on its card, for a rule that needs persuading. */
74
+ demonstrations?: Record<string, ReactNode>;
75
+ /**
76
+ * Blocks replaced by the project's own, by name: `{ Usage: LocalUsage }`.
77
+ * `designtools-docs eject <block>` copies one into the project to start from.
78
+ */
79
+ blocks?: Partial<Record<string, ComponentType<any>>>;
80
+ }
@@ -0,0 +1,100 @@
1
+ import "../styles.css";
2
+ import type { Metadata } from "next";
3
+ import { notFound } from "next/navigation";
4
+ import type { ReactNode } from "react";
5
+ import { findBySlug, slugOf } from "../lib/manifest";
6
+ import { PreviewExample, PreviewSurface } from "../preview-frame";
7
+ import { DocsShell } from "../shell";
8
+ import type { DocsConfig } from "./config";
9
+ import { manifestFiles, manifestRoute, markdownRoute } from "./markdown";
10
+ import { DocsPageView } from "./pages";
11
+ import { allSlugs, metaOf, navOf, resolve, siteOf } from "./site";
12
+
13
+ export type { BuiltInPage, DocsConfig, EditorialPage, WorkbenchSetup } from "./config";
14
+ export { markdownFor } from "./markdown";
15
+ export { docsMatcher, docsProxy, prefersMarkdown } from "./proxy";
16
+
17
+ type SlugParams = { params: Promise<{ slug?: string[] }> };
18
+
19
+ /**
20
+ * A whole docs site from one config, for the Next App Router. Each export is
21
+ * what one route file re-exports, so a project's docs are a handful of
22
+ * one-line files:
23
+ *
24
+ * - `(docs)/layout.tsx`: `export default docs.Layout`
25
+ * - `(docs)/[[...slug]]/page.tsx`: `export default docs.Page`, with `generateStaticParams` and `generateMetadata`
26
+ * - `md/[[...path]]/route.ts`: `export const GET = docs.markdown`
27
+ * - `manifest/[file]/route.ts`: `export const GET = docs.manifest`, with `generateStaticParams = docs.manifestParams`
28
+ * - `preview/[slug]/[example]/page.tsx`: `export default docs.Preview`
29
+ *
30
+ * The same files serve the docs inside a product's app or as a standalone app.
31
+ */
32
+ export function createDocs(config: DocsConfig) {
33
+ const site = siteOf(config);
34
+ const nav = navOf(site);
35
+ const Registry = config.registry;
36
+
37
+ function Layout({ children }: { children: ReactNode }) {
38
+ return (
39
+ <Registry>
40
+ <DocsShell
41
+ manifest={site.components}
42
+ rules={site.rules}
43
+ patterns={site.patterns}
44
+ terms={site.taxonomy}
45
+ title={site.title}
46
+ logo={config.logo}
47
+ basePath={site.base}
48
+ brand={nav.brand}
49
+ foundations={nav.foundations}
50
+ standards={nav.standards}
51
+ hero={config.hero}
52
+ >
53
+ {children}
54
+ </DocsShell>
55
+ </Registry>
56
+ );
57
+ }
58
+
59
+ async function Page({ params }: SlugParams) {
60
+ const page = resolve(site, (await params).slug);
61
+ if (!page) notFound();
62
+ return <DocsPageView site={site} page={page} />;
63
+ }
64
+
65
+ async function generateMetadata({ params }: SlugParams): Promise<Metadata> {
66
+ const page = resolve(site, (await params).slug);
67
+ return page ? metaOf(site, page) : {};
68
+ }
69
+
70
+ /** One example on its own, outside the shell: what the preview frame shows, and the target for visual regression. */
71
+ async function Preview({ params, searchParams }: { params: Promise<{ slug: string; example: string }>; searchParams: Promise<{ theme?: string }> }) {
72
+ const { slug, example } = await params;
73
+ const { theme } = await searchParams;
74
+ const entry = findBySlug(site.components, slug);
75
+ if (!entry?.examples) notFound();
76
+ return (
77
+ <Registry>
78
+ <PreviewSurface theme={theme}>
79
+ <PreviewExample entry={entry} example={decodeURIComponent(example)} />
80
+ </PreviewSurface>
81
+ </Registry>
82
+ );
83
+ }
84
+
85
+ return {
86
+ site,
87
+ Layout,
88
+ Page,
89
+ generateMetadata,
90
+ generateStaticParams: () => allSlugs(site).map((slug) => ({ slug })),
91
+ Preview,
92
+ previewParams: () =>
93
+ site.components.components.flatMap((c) =>
94
+ c.examples ? [c.examples.canonical, ...c.examples.do, ...c.examples.dont, ...c.examples.other].filter(Boolean).map((e) => ({ slug: slugOf(c), example: e!.name })) : [],
95
+ ),
96
+ markdown: markdownRoute(site),
97
+ manifest: manifestRoute(site),
98
+ manifestParams: () => Object.keys(manifestFiles(site)).map((name) => ({ file: `${name}.json` })),
99
+ };
100
+ }
@@ -0,0 +1,119 @@
1
+ import { groupByCategory, slugOf } from "../lib/manifest";
2
+ import {
3
+ adherenceMarkdown,
4
+ assetsMarkdown,
5
+ coloursMarkdown,
6
+ componentMarkdown,
7
+ editorialHeader,
8
+ generatedHeader,
9
+ glossaryMarkdown,
10
+ manifestLinks,
11
+ patternMarkdown,
12
+ ruleMarkdown,
13
+ scaleMarkdown,
14
+ standardsMarkdown,
15
+ systemMarkdown,
16
+ typeMarkdown,
17
+ } from "../lib/markdown";
18
+ import { navOf, resolve, type Site } from "./site";
19
+
20
+ /**
21
+ * The markdown form of a docs page, generated from the manifest rather than
22
+ * converted from HTML, or nothing when there is no such page. `context` is the
23
+ * whole system in one file.
24
+ */
25
+ export function markdownFor(site: Site, path: string): string | undefined {
26
+ const { base, title, components, tokens, rules, patterns, taxonomy, assets, config } = site;
27
+ const generated = (md: string) => generatedHeader(config.provenance ?? "") + md;
28
+ if (path === "context")
29
+ return systemMarkdown({ components, tokens, rules: rules.rules, patterns: patterns.patterns, terms: taxonomy.terms }, title, config.provenance ?? "");
30
+
31
+ const page = resolve(site, path ? path.split("/") : []);
32
+ if (!page) return undefined;
33
+ switch (page.kind) {
34
+ case "overview": {
35
+ const nav = navOf(site);
36
+ const links = (list: { label: string; href: string }[]) => list.map((l) => `[${l.label}](${l.href}.md)`).join(", ");
37
+ return generated(
38
+ [
39
+ `# ${title}`,
40
+ "",
41
+ "The brand and the design system in one place. Every page here has a markdown form: add `.md` to its URL.",
42
+ "",
43
+ "## Pages",
44
+ "",
45
+ ...(nav.brand.length ? [`- Brand: ${links(nav.brand)}`] : []),
46
+ `- Foundations: ${links(nav.foundations)}`,
47
+ ...(rules.rules.length ? [`- [Rules](${base}/rules.md)`] : []),
48
+ ...patterns.patterns.map((p) => `- Pattern: [${p.name}](${base}/patterns/${p.id}.md)`),
49
+ ...groupByCategory(components).flatMap((g) =>
50
+ g.components.map((c) => `- ${g.category}: [${c.name}](${base}/components/${slugOf(c)}.md), ${c.status ?? "status not set"}`),
51
+ ),
52
+ ...(nav.standards.length ? [`- Standards: ${links(nav.standards)}`] : []),
53
+ `- Everything in [one file](${base}/context.md)`,
54
+ "",
55
+ manifestLinks(`${base}/manifest`),
56
+ ].join("\n"),
57
+ );
58
+ }
59
+ case "editorial":
60
+ return `${editorialHeader(page.page.by)}# ${page.page.title}\n\n${page.page.lead ? `${page.page.lead}\n\n` : ""}${page.page.body}\n`;
61
+ case "logo":
62
+ return generated(
63
+ assetsMarkdown(assets.assets.filter((a) => a.kind === "logo")) +
64
+ "\n" +
65
+ rules.rules
66
+ .filter((r) => r.kind === "brand")
67
+ .map(ruleMarkdown)
68
+ .join("\n"),
69
+ );
70
+ case "imagery":
71
+ return generated(assetsMarkdown(assets.assets.filter((a) => a.kind !== "logo")));
72
+ case "voice":
73
+ return generated(glossaryMarkdown(taxonomy.terms.filter((t) => t.kind === "product" || t.wrong?.length)));
74
+ case "colour":
75
+ return generated(coloursMarkdown(tokens));
76
+ case "type":
77
+ return generated(typeMarkdown(tokens));
78
+ case "scale":
79
+ return generated(scaleMarkdown(tokens));
80
+ case "rules":
81
+ return generated(["# Rules", "", ...rules.rules.map(ruleMarkdown)].join("\n"));
82
+ case "pattern":
83
+ return generated(patternMarkdown(page.pattern, 1));
84
+ case "component":
85
+ return generated(componentMarkdown(page.entry, 1));
86
+ case "standards":
87
+ return generated(`${config.baselines ? standardsMarkdown(config.baselines) : ""}\n${config.adherence ? adherenceMarkdown(config.adherence) : ""}`);
88
+ case "vocabulary":
89
+ return generated(glossaryMarkdown(taxonomy.terms));
90
+ }
91
+ }
92
+
93
+ /** The route handler for `md/[[...path]]`: the page's markdown with `Vary: Accept` and its canonical link, or a markdown 404. */
94
+ export function markdownRoute(site: Site) {
95
+ return async function GET(_: Request, { params }: { params: Promise<{ path?: string[] }> }) {
96
+ const path = ((await params).path ?? []).join("/").replace(/\.md$/, "");
97
+ const page = markdownFor(site, path);
98
+ const canonical = `${site.base}${path ? `/${path}` : ""}`;
99
+ return new Response(page ?? `# Not found\n\nThere is no page at \`${canonical}\`. The index is at [${site.base}.md](${site.base}.md).\n`, {
100
+ status: page ? 200 : 404,
101
+ headers: { "content-type": "text/markdown; charset=utf-8", vary: "Accept", link: `<${canonical}>; rel="canonical"` },
102
+ });
103
+ };
104
+ }
105
+
106
+ /** The route handler for `manifest/[file]`: each manifest file as JSON, so an agent outside the repo gets the exact data. */
107
+ export function manifestRoute(site: Site) {
108
+ const files = manifestFiles(site);
109
+ return async function GET(_: Request, { params }: { params: Promise<{ file: string }> }) {
110
+ const { file } = await params;
111
+ const data = files[file.replace(/\.json$/, "")];
112
+ if (!data) return new Response(`# Not found\n\nNo manifest file called ${file}.\n`, { status: 404, headers: { "content-type": "text/markdown; charset=utf-8" } });
113
+ return Response.json(data);
114
+ };
115
+ }
116
+
117
+ export function manifestFiles(site: Site): Record<string, unknown> {
118
+ return Object.fromEntries(Object.entries(site.config.manifest).filter(([, v]) => v !== undefined));
119
+ }