@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,173 @@
1
+ import type { DocsLink } from "../shell";
2
+ import { findBySlug, readComponents, readTokens, slugOf } from "../lib/manifest";
3
+ import type {
4
+ AssetsManifest,
5
+ ComponentEntry,
6
+ ComponentsManifest,
7
+ PatternEntry,
8
+ PatternsManifest,
9
+ RulesManifest,
10
+ TaxonomyManifest,
11
+ TokensManifest,
12
+ } from "../lib/manifest-types";
13
+ import type { BuiltInPage, DocsConfig, EditorialPage } from "./config";
14
+
15
+ /** The config, read once: the manifest typed and checked, and what is shown. */
16
+ export interface Site {
17
+ config: DocsConfig;
18
+ base: string;
19
+ title: string;
20
+ components: ComponentsManifest;
21
+ tokens: TokensManifest;
22
+ rules: RulesManifest;
23
+ patterns: PatternsManifest;
24
+ taxonomy: TaxonomyManifest;
25
+ assets: AssetsManifest;
26
+ editorial: EditorialPage[];
27
+ shows: (page: BuiltInPage) => boolean;
28
+ }
29
+
30
+ export function siteOf(config: DocsConfig): Site {
31
+ const hidden = new Set(config.hide ?? []);
32
+ return {
33
+ config,
34
+ base: (config.basePath ?? "/design/system").replace(/\/$/, ""),
35
+ title: config.title,
36
+ components: readComponents(config.manifest.components),
37
+ tokens: readTokens(config.manifest.tokens),
38
+ rules: (config.manifest.rules as RulesManifest | undefined) ?? ({ rules: [] } as unknown as RulesManifest),
39
+ patterns: (config.manifest.patterns as PatternsManifest | undefined) ?? ({ patterns: [] } as unknown as PatternsManifest),
40
+ taxonomy: (config.manifest.taxonomy as TaxonomyManifest | undefined) ?? ({ terms: [] } as unknown as TaxonomyManifest),
41
+ assets: (config.manifest.assets as AssetsManifest | undefined) ?? ({ assets: [] } as unknown as AssetsManifest),
42
+ editorial: config.editorial ?? [],
43
+ shows: (page) => !hidden.has(page),
44
+ };
45
+ }
46
+
47
+ /** One page of the docs, by what it shows. */
48
+ export type PageRef =
49
+ | { kind: "overview" }
50
+ | { kind: "editorial"; page: EditorialPage }
51
+ | { kind: "logo" | "imagery" | "voice" | "colour" | "type" | "scale" | "rules" | "standards" | "vocabulary" }
52
+ | { kind: "component"; entry: ComponentEntry }
53
+ | { kind: "pattern"; pattern: PatternEntry };
54
+
55
+ const BUILT_IN: Record<string, Exclude<PageRef["kind"], "overview" | "editorial" | "component" | "pattern">> = {
56
+ "brand/logo": "logo",
57
+ "brand/imagery": "imagery",
58
+ "brand/voice": "voice",
59
+ colour: "colour",
60
+ type: "type",
61
+ scale: "scale",
62
+ rules: "rules",
63
+ standards: "standards",
64
+ vocabulary: "vocabulary",
65
+ };
66
+
67
+ /** Whether a built-in page has anything to show, as well as not being hidden. */
68
+ function has(site: Site, kind: keyof typeof KIND_NEEDS): boolean {
69
+ return site.shows(kind as BuiltInPage) && KIND_NEEDS[kind](site);
70
+ }
71
+
72
+ const KIND_NEEDS = {
73
+ logo: (s: Site) => s.assets.assets.some((a) => a.kind === "logo"),
74
+ imagery: (s: Site) => s.assets.assets.some((a) => a.kind !== "logo"),
75
+ voice: (s: Site) => s.taxonomy.terms.some((t) => t.kind === "product" || t.wrong?.length),
76
+ colour: () => true,
77
+ type: () => true,
78
+ scale: () => true,
79
+ rules: (s: Site) => s.rules.rules.length > 0,
80
+ standards: (s: Site) => !!s.config.baselines || !!s.config.adherence,
81
+ vocabulary: (s: Site) => s.taxonomy.terms.length > 0,
82
+ };
83
+
84
+ /** The page at a path under the docs, or nothing. */
85
+ export function resolve(site: Site, slug: string[] = []): PageRef | undefined {
86
+ const path = slug.join("/");
87
+ if (!path) return { kind: "overview" };
88
+ const editorial = site.editorial.find((p) => p.slug === path);
89
+ if (editorial) return { kind: "editorial", page: editorial };
90
+ const builtIn = BUILT_IN[path];
91
+ if (builtIn) return has(site, builtIn) ? { kind: builtIn } : undefined;
92
+ const [section, id] = slug;
93
+ if (section === "components" && id && site.shows("components")) {
94
+ const entry = findBySlug(site.components, id);
95
+ return entry ? { kind: "component", entry } : undefined;
96
+ }
97
+ if (section === "patterns" && id && site.shows("patterns")) {
98
+ const pattern = site.patterns.patterns.find((p) => p.id === id);
99
+ return pattern ? { kind: "pattern", pattern } : undefined;
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ /** Every page's path, for static generation. */
105
+ export function allSlugs(site: Site): string[][] {
106
+ const out: string[][] = [[]];
107
+ for (const p of site.editorial) out.push(p.slug.split("/"));
108
+ for (const [path, kind] of Object.entries(BUILT_IN)) if (has(site, kind)) out.push(path.split("/"));
109
+ if (site.shows("components")) for (const c of site.components.components) out.push(["components", slugOf(c)]);
110
+ if (site.shows("patterns")) for (const p of site.patterns.patterns) out.push(["patterns", p.id]);
111
+ return out;
112
+ }
113
+
114
+ /** The navigation's editorial and generated groups: brand, foundations, standards. Rules, patterns and components the shell adds from the manifest. */
115
+ export function navOf(site: Site): { brand: DocsLink[]; foundations: DocsLink[]; standards: DocsLink[] } {
116
+ const link = (label: string, path: string) => ({ label, href: `${site.base}/${path}` });
117
+ const written = (section: EditorialPage["section"]) =>
118
+ site.editorial.filter((p) => (p.section ?? "brand") === section).map((p) => link(p.nav ?? p.title, p.slug));
119
+ return {
120
+ brand: [
121
+ ...written("brand"),
122
+ ...(has(site, "logo") ? [link("Logo", "brand/logo")] : []),
123
+ ...(has(site, "imagery") ? [link("Imagery and icons", "brand/imagery")] : []),
124
+ ...(has(site, "voice") ? [link("Voice and terms", "brand/voice")] : []),
125
+ ],
126
+ foundations: [...written("foundations"), link("Colour", "colour"), link("Type", "type"), link("Scale", "scale")].filter(
127
+ (l) => !["colour", "type", "scale"].some((k) => l.href.endsWith(`/${k}`) && !site.shows(k as BuiltInPage)),
128
+ ),
129
+ standards: [
130
+ ...written("standards"),
131
+ ...(has(site, "standards") ? [link("Standards", "standards")] : []),
132
+ ...(has(site, "vocabulary") ? [link("Vocabulary", "vocabulary")] : []),
133
+ ],
134
+ };
135
+ }
136
+
137
+ /** Each built-in page's title and lead: what the page says it is, which is also its description. */
138
+ export const COPY: Record<Exclude<PageRef["kind"], "overview" | "editorial" | "component" | "pattern">, { title: string; lead: string }> = {
139
+ logo: { title: "Logo", lead: "Each logo on the surfaces it may sit on, at its smallest, with its clear space." },
140
+ imagery: { title: "Imagery and icons", lead: "The approved icons and imagery, and how to use each." },
141
+ voice: { title: "Voice and terms", lead: "The words we use, beside the ones we do not." },
142
+ colour: {
143
+ title: "Colour",
144
+ lead: "Generated from the brand colour by @designtools/tokens. Write the semantic names in components; the ramps are what they point at.",
145
+ },
146
+ type: { title: "Type", lead: "Each family, then the scale: a size is never set without its leading." },
147
+ scale: { title: "Scale", lead: "Space, size, radius, elevation and motion, each drawn the way it is used." },
148
+ rules: {
149
+ title: "Rules",
150
+ lead: "Brand and interface rules in one format, each with the check that proves it. Written for people; agents read the same rule.",
151
+ },
152
+ standards: {
153
+ title: "Standards",
154
+ lead: "The budgets and targets the project holds itself to, read from .mxa/stack.json, where the checks read them too.",
155
+ },
156
+ vocabulary: { title: "Vocabulary", lead: "Every name in the system. Search by a wrong name and it finds the right one." },
157
+ };
158
+
159
+ /** A page's title and description, for its metadata. */
160
+ export function metaOf(site: Site, ref: PageRef): { title: string; description?: string } {
161
+ switch (ref.kind) {
162
+ case "overview":
163
+ return { title: site.title, description: site.config.lead ?? `${site.title}: the brand and the design system in one place.` };
164
+ case "editorial":
165
+ return { title: ref.page.title, description: ref.page.lead };
166
+ case "component":
167
+ return { title: `${ref.entry.name} · Components`, description: ref.entry.description };
168
+ case "pattern":
169
+ return { title: `${ref.pattern.name} · Patterns`, description: ref.pattern.description };
170
+ default:
171
+ return { title: COPY[ref.kind].title, description: COPY[ref.kind].lead };
172
+ }
173
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import { Menu } from "@base-ui/react/menu";
4
+ import { Braces, Check, ChevronDown, Copy, FileText, Files } from "lucide-react";
5
+ import { usePathname } from "next/navigation";
6
+ import { useState } from "react";
7
+
8
+ /**
9
+ * What a reader can take from a page, as one split button: copy the page as
10
+ * markdown, the same markdown any agent gets at `.md`, with a menu beside it to
11
+ * view that markdown, copy the whole system as one file, or open the manifest.
12
+ * Put it in a `DocsHeader`.
13
+ */
14
+ export function PageActions({ basePath = "/design/system" }: { basePath?: string }) {
15
+ const path = usePathname().replace(/\/$/, "");
16
+ const [done, setDone] = useState<string | null>(null);
17
+
18
+ const copyFrom = async (url: string, what: string) => {
19
+ try {
20
+ const res = await fetch(url);
21
+ if (!res.ok) throw new Error(String(res.status));
22
+ await navigator.clipboard.writeText(await res.text());
23
+ setDone(what);
24
+ } catch {
25
+ setDone("Could not copy");
26
+ }
27
+ setTimeout(() => setDone(null), 1600);
28
+ };
29
+
30
+ const item =
31
+ "flex min-h-(--docs-target,2.75rem) cursor-pointer items-center gap-3 rounded-md px-3 text-sm outline-none select-none data-highlighted:bg-muted";
32
+ const icon = "size-4 shrink-0 text-muted-foreground";
33
+
34
+ return (
35
+ <div data-slot="page-actions" className="inline-flex items-stretch rounded-lg border border-border bg-canvas text-sm shadow-xs">
36
+ <button
37
+ type="button"
38
+ onClick={() => copyFrom(`${path}.md`, "Page copied")}
39
+ className="inline-flex min-h-9 cursor-pointer items-center gap-2 rounded-l-lg py-1.5 pr-3 pl-3 font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring"
40
+ >
41
+ {done === "Page copied" ? <Check aria-hidden className="size-4" /> : <Copy aria-hidden className={icon} />}
42
+ {done === "Page copied" ? "Copied" : "Copy page"}
43
+ </button>
44
+ <Menu.Root>
45
+ <Menu.Trigger
46
+ aria-label="More ways to take this page"
47
+ className="inline-flex min-h-9 w-9 cursor-pointer items-center justify-center rounded-r-lg border-l border-border text-muted-foreground hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-popup-open:bg-muted"
48
+ >
49
+ <ChevronDown aria-hidden className="size-4" />
50
+ </Menu.Trigger>
51
+ <Menu.Portal>
52
+ <Menu.Positioner data-designtools="" align="end" sideOffset={6} className="z-50">
53
+ <Menu.Popup className="w-72 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">
54
+ <Menu.Item className={item} onClick={() => copyFrom(`${path}.md`, "Page copied")}>
55
+ <Copy aria-hidden className={icon} />
56
+ <span className="flex flex-col">
57
+ Copy page
58
+ <span className="text-xs text-muted-foreground">As markdown, for an assistant</span>
59
+ </span>
60
+ </Menu.Item>
61
+ <Menu.LinkItem className={item} href={`${path}.md`} target="_blank" rel="noreferrer">
62
+ <FileText aria-hidden className={icon} />
63
+ <span className="flex flex-col">
64
+ View as markdown
65
+ <span className="text-xs text-muted-foreground">What an agent reads at this address</span>
66
+ </span>
67
+ </Menu.LinkItem>
68
+ <Menu.Separator className="my-1.5 h-px bg-border" />
69
+ <Menu.Item className={item} onClick={() => copyFrom(`${basePath}/context.md`, "System copied")}>
70
+ <Files aria-hidden className={icon} />
71
+ <span className="flex flex-col">
72
+ Copy the whole system
73
+ <span className="text-xs text-muted-foreground">Every page in one markdown file</span>
74
+ </span>
75
+ </Menu.Item>
76
+ <Menu.LinkItem className={item} href={`${basePath}/manifest/components.json`} target="_blank" rel="noreferrer">
77
+ <Braces aria-hidden className={icon} />
78
+ <span className="flex flex-col">
79
+ Open the manifest
80
+ <span className="text-xs text-muted-foreground">Components, tokens and rules as JSON</span>
81
+ </span>
82
+ </Menu.LinkItem>
83
+ </Menu.Popup>
84
+ </Menu.Positioner>
85
+ </Menu.Portal>
86
+ </Menu.Root>
87
+ <span className="sr-only" aria-live="polite">
88
+ {done ?? ""}
89
+ </span>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { ArrowUpRight } from "lucide-react";
4
+ import Link from "next/link";
5
+ import { ExampleFrame } from "./examples";
6
+ import { Missing, renderExample, useLookup } from "./lib/lookup";
7
+ import { slugOf } from "./lib/manifest";
8
+ import type { ComponentsManifest, PatternEntry } from "./lib/manifest-types";
9
+ import { StatusBadge } from "./lib/status";
10
+
11
+ /** A recurring screen composition: what it is made of, then each composition live in its frame, with its code. */
12
+ export function Pattern({
13
+ pattern,
14
+ manifest,
15
+ basePath = "/design/system",
16
+ }: {
17
+ pattern: PatternEntry;
18
+ /** To link the components it uses to their pages. */
19
+ manifest?: ComponentsManifest;
20
+ basePath?: string;
21
+ }) {
22
+ const lookup = useLookup();
23
+ const module = lookup.compositionsFor(pattern);
24
+ const known = new Map((manifest?.components ?? []).map((c) => [c.name, c]));
25
+ return (
26
+ <div data-slot="pattern" className="flex flex-col gap-10">
27
+ <div className="flex flex-col gap-3 rounded-xl border border-border bg-surface p-4 text-surface-foreground sm:flex-row sm:items-center sm:gap-6">
28
+ <span className="flex items-center gap-2">
29
+ <span className="text-xs font-medium text-muted-foreground">Status</span>
30
+ <StatusBadge status={pattern.status} />
31
+ </span>
32
+ {pattern.components.length > 0 && (
33
+ <span className="flex flex-wrap items-center gap-2">
34
+ <span className="text-xs font-medium text-muted-foreground">Made of</span>
35
+ {pattern.components.map((name) => {
36
+ const c = known.get(name);
37
+ const chip = "inline-flex min-h-8 items-center gap-1 rounded-full border border-border px-3 text-xs font-medium";
38
+ return c ? (
39
+ <Link
40
+ key={name}
41
+ href={`${basePath}/components/${slugOf(c)}`}
42
+ className={`${chip} relative hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-y-1.5 after:inset-x-0 after:content-['']`}
43
+ >
44
+ {name}
45
+ <ArrowUpRight aria-hidden className="size-3.5 text-muted-foreground" />
46
+ </Link>
47
+ ) : (
48
+ <span key={name} className={chip}>
49
+ {name}
50
+ </span>
51
+ );
52
+ })}
53
+ </span>
54
+ )}
55
+ </div>
56
+ {!module ? (
57
+ <Missing name={pattern.name} what="compositions" />
58
+ ) : (
59
+ pattern.examples.map((ex, i) => (
60
+ <ExampleFrame key={ex.name} name={ex.name} kind={i === 0 ? "canonical" : "other"} description={ex.description} code={ex.code}>
61
+ {renderExample(module[ex.name], ex.name)}
62
+ </ExampleFrame>
63
+ ))
64
+ )}
65
+ </div>
66
+ );
67
+ }