@designtools/docs 0.2.2 → 0.3.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 (58) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +27 -3
  3. package/dist/adherence-summary.js +2 -2
  4. package/dist/anatomy.d.ts +6 -2
  5. package/dist/anatomy.js +3 -2
  6. package/dist/glossary.d.ts +1 -1
  7. package/dist/glossary.js +3 -8
  8. package/dist/lib/adherence-types.d.ts +2 -0
  9. package/dist/lib/colour.d.ts +16 -0
  10. package/dist/lib/colour.js +99 -0
  11. package/dist/lib/contrast.js +1 -7
  12. package/dist/lib/design-md.d.ts +27 -0
  13. package/dist/lib/design-md.js +176 -0
  14. package/dist/lib/markdown.js +1 -0
  15. package/dist/lib/terms.d.ts +1 -1
  16. package/dist/lib/terms.js +1 -1
  17. package/dist/lib/wrap.d.ts +5 -0
  18. package/dist/lib/wrap.js +5 -0
  19. package/dist/logo-usage.js +3 -3
  20. package/dist/next/config.d.ts +9 -0
  21. package/dist/next/markdown.d.ts +1 -1
  22. package/dist/next/markdown.js +15 -1
  23. package/dist/next/pages.js +5 -12
  24. package/dist/next/proxy.js +3 -2
  25. package/dist/page-actions.d.ts +2 -1
  26. package/dist/page-actions.js +4 -3
  27. package/dist/playground.js +1 -1
  28. package/dist/shell.js +6 -4
  29. package/dist/standards.js +1 -1
  30. package/dist/styles.css +65 -43
  31. package/dist/type-ramp.js +1 -1
  32. package/dist/variant-matrix.d.ts +6 -2
  33. package/dist/variant-matrix.js +3 -2
  34. package/dist/workbench.js +3 -2
  35. package/package.json +1 -1
  36. package/src/adherence-summary.tsx +7 -2
  37. package/src/anatomy.tsx +6 -4
  38. package/src/glossary.tsx +10 -22
  39. package/src/lib/adherence-types.ts +2 -0
  40. package/src/lib/colour.ts +104 -0
  41. package/src/lib/contrast.ts +2 -8
  42. package/src/lib/design-md.ts +193 -0
  43. package/src/lib/markdown.ts +1 -0
  44. package/src/lib/terms.tsx +1 -1
  45. package/src/lib/wrap.tsx +6 -0
  46. package/src/logo-usage.tsx +20 -12
  47. package/src/next/config.ts +7 -0
  48. package/src/next/markdown.ts +16 -1
  49. package/src/next/pages.tsx +21 -20
  50. package/src/next/proxy.ts +3 -2
  51. package/src/page-actions.tsx +10 -2
  52. package/src/playground.tsx +2 -2
  53. package/src/shell.tsx +6 -4
  54. package/src/standards.tsx +2 -2
  55. package/src/styles.css +9 -8
  56. package/src/type-ramp.tsx +3 -6
  57. package/src/variant-matrix.tsx +11 -4
  58. package/src/workbench.tsx +2 -1
@@ -24,8 +24,7 @@ export function LogoUsage({ assets, rules = [], rulesHref }: { assets: AssetEntr
24
24
  const brand = rules.filter((r) => r.kind === "brand");
25
25
  const general = brand.filter((r) => (r.appliesTo ?? []).some((x) => ["logo", "logos"].includes(x.toLowerCase())));
26
26
  return (
27
- <div data-slot="logo-usage" className="flex flex-col gap-16">
28
- {general.length > 0 && <RuleList rules={general} rulesHref={rulesHref} />}
27
+ <div data-slot="logo-usage" className="flex flex-col gap-16 lg:gap-24">
29
28
  {logos.map((logo) => {
30
29
  const src = lookup.assetUrl(logo);
31
30
  const names = new Set([logo.name.toLowerCase(), logo.file.toLowerCase()]);
@@ -111,27 +110,36 @@ export function LogoUsage({ assets, rules = [], rulesHref }: { assets: AssetEntr
111
110
  </section>
112
111
  );
113
112
  })}
113
+ {general.length > 0 && (
114
+ <section data-slot="logo-rules" aria-labelledby="logo-rules" className="flex flex-col gap-5">
115
+ <header className="flex flex-col gap-1.5">
116
+ <h3 id="logo-rules" className="text-3xl tracking-tight">
117
+ Rules for the logo
118
+ </h3>
119
+ <p className="max-w-2xl text-base text-muted-foreground">What holds for every logo above, each with how it is checked.</p>
120
+ </header>
121
+ <RuleList rules={general} rulesHref={rulesHref} />
122
+ </section>
123
+ )}
114
124
  </div>
115
125
  );
116
126
  }
117
127
 
118
128
  function RuleList({ rules, rulesHref }: { rules: RuleEntry[]; rulesHref?: string }) {
119
129
  return (
120
- <ul data-slot="logo-rules" className="grid gap-x-12 sm:grid-cols-2">
130
+ <ul data-slot="logo-rules" className="flex flex-col divide-y divide-border border-y border-border">
121
131
  {rules.map((r) => (
122
- <li key={r.id} className="border-t border-border">
132
+ <li key={r.id}>
123
133
  <a
124
134
  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"
135
+ className="group grid items-baseline gap-x-8 gap-y-1.5 py-5 focus-visible:outline-2 focus-visible:outline-ring md:grid-cols-[14rem_minmax(0,1fr)_auto]"
126
136
  >
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} />
137
+ <span className="flex items-center gap-2 text-base font-medium underline-offset-4 group-hover:underline">
138
+ <Scale aria-hidden className="size-(--dt-size-xs) shrink-0 text-muted-foreground" />
139
+ {r.title}
133
140
  </span>
134
- <span className="text-muted-foreground">{inline(r.statement)}</span>
141
+ <span className="text-sm leading-relaxed text-muted-foreground">{inline(r.statement)}</span>
142
+ <StatusBadge status={r.status} />
135
143
  </a>
136
144
  </li>
137
145
  ))}
@@ -85,6 +85,13 @@ export interface DocsConfig {
85
85
  provenance?: string;
86
86
  /** `designtools-adherence --json`, for the standards page. */
87
87
  adherence?: AdherenceReport;
88
+ /**
89
+ * design.md, served at `<base>/design.md`: the brand in one file for anything made outside the repo's
90
+ * components (a page built elsewhere, a slide, a document). `body` is its hand-written half, in markdown
91
+ * `##` sections: what comes first when guidance conflicts, how to work, the reflexes to avoid, a review
92
+ * list. The values after it are generated. Without a body the file still serves, with the values alone.
93
+ */
94
+ designMd?: { body: string };
88
95
  /** Pages people write, in the order they appear. */
89
96
  editorial?: EditorialPage[];
90
97
  /** Built-in pages to leave out. */
@@ -15,12 +15,13 @@ import {
15
15
  systemMarkdown,
16
16
  typeMarkdown,
17
17
  } from "../lib/markdown";
18
+ import { designMarkdown } from "../lib/design-md";
18
19
  import { navOf, resolve, type Site } from "./site";
19
20
 
20
21
  /**
21
22
  * The markdown form of a docs page, generated from the manifest rather than
22
23
  * converted from HTML, or nothing when there is no such page. `context` is the
23
- * whole system in one file.
24
+ * whole system in one file; `design` is design.md, the brand for work outside the repo.
24
25
  */
25
26
  export function markdownFor(site: Site, path: string): string | undefined {
26
27
  const { base, title, components, tokens, rules, patterns, taxonomy, assets, config } = site;
@@ -28,6 +29,19 @@ export function markdownFor(site: Site, path: string): string | undefined {
28
29
  if (path === "context")
29
30
  return systemMarkdown({ components, tokens, rules: rules.rules, patterns: patterns.patterns, terms: taxonomy.terms }, title, config.provenance ?? "");
30
31
 
32
+ if (path === "design")
33
+ return designMarkdown({
34
+ title,
35
+ base,
36
+ provenance: config.provenance,
37
+ components,
38
+ tokens,
39
+ rules: rules.rules,
40
+ assets: assets.assets,
41
+ terms: taxonomy.terms,
42
+ body: config.designMd?.body,
43
+ });
44
+
31
45
  const page = resolve(site, path ? path.split("/") : []);
32
46
  if (!page) return undefined;
33
47
  switch (page.kind) {
@@ -51,6 +65,7 @@ export function markdownFor(site: Site, path: string): string | undefined {
51
65
  ),
52
66
  ...(nav.standards.length ? [`- Standards: ${links(nav.standards)}`] : []),
53
67
  `- Everything in [one file](${base}/context.md)`,
68
+ `- The brand for work outside the repo, a page, a slide or a document: [design.md](${base}/design.md)`,
54
69
  "",
55
70
  manifestLinks(`${base}/manifest`),
56
71
  ].join("\n"),
@@ -1,5 +1,6 @@
1
1
  import { ArrowRight, ArrowUpRight } from "lucide-react";
2
2
  import Link from "next/link";
3
+ import { cx, display } from "../lib/cx";
3
4
  import { AdherenceSummary } from "../adherence-summary";
4
5
  import { Anatomy } from "../anatomy";
5
6
  import { Examples } from "../examples";
@@ -269,7 +270,7 @@ function ComponentPage({ site, blocks: B, entry }: { site: Site; blocks: Blocks;
269
270
  </B.Workbench>
270
271
 
271
272
  <DocsSection title="Anatomy">
272
- <B.Anatomy entry={entry} />
273
+ <B.Anatomy entry={entry} wrap={setup.wrap} />
273
274
  </DocsSection>
274
275
 
275
276
  <DocsSection title="Examples" lead="The usual use first, then each do beside its don't. Every one is the real component.">
@@ -281,7 +282,7 @@ function ComponentPage({ site, blocks: B, entry }: { site: Site; blocks: Blocks;
281
282
  title="Variants"
282
283
  lead={`Every combination, from the ${entry.variants.from === "props" ? "props' types" : `${entry.variants.from}() config`}, then each state.`}
283
284
  >
284
- <B.VariantMatrix entry={entry} props={setup.props}>
285
+ <B.VariantMatrix entry={entry} props={setup.props} wrap={setup.wrap}>
285
286
  {setup.children === "" ? undefined : (setup.content ?? entry.name)}
286
287
  </B.VariantMatrix>
287
288
  </DocsSection>
@@ -305,57 +306,49 @@ function Overview({ site }: { site: Site }) {
305
306
  const count = families.reduce((n, g) => n + g.families.length, 0);
306
307
  const brandPages = site.editorial.filter((p) => (p.section ?? "brand") === "brand");
307
308
  const brandFirst = brandPages[0];
308
- const share = config.adherence?.summary.share;
309
309
 
310
- // each section of the system, why it exists and how much is in it
311
- const sections: { title: string; href: string; why: string; count: string }[] = [];
310
+ // each section of the system, and why it exists
311
+ const sections: { title: string; href: string; why: string }[] = [];
312
312
  if (brandFirst || site.shows("logo"))
313
313
  sections.push({
314
314
  title: "Brand",
315
315
  href: `${base}/${brandFirst?.slug ?? "brand/logo"}`,
316
316
  why: "What the brand believes, how it sounds and how it looks, so every screen and sentence comes from the same place.",
317
- count: `${brandPages.length + (site.assets.assets.some((a) => a.kind === "logo") ? 1 : 0)} pages`
318
317
  });
319
318
  sections.push({
320
319
  title: "Foundations",
321
320
  href: `${base}/colour`,
322
321
  why: "Colour, type and the scale, generated from the tokens and measured in both modes, so nothing is a guess.",
323
- count: `${site.tokens.tokens.length} tokens`,
324
322
  });
325
323
  if (site.rules.rules.length)
326
324
  sections.push({
327
325
  title: "Rules",
328
326
  href: `${base}/rules`,
329
327
  why: "The lines the brand and the interface hold, each written for people, with the check that proves it.",
330
- count: `${site.rules.rules.length} rules, ${site.rules.rules.filter((r) => r.status === "confirmed").length} confirmed`
331
328
  });
332
329
  if (site.shows("components") && count)
333
330
  sections.push({
334
331
  title: "Components",
335
332
  href: `${base}/components/${slugOf(families[0].families[0].head)}`,
336
333
  why: "The real components the product is built from, each with when to use it, a live workbench and an accessibility report.",
337
- count: `${count} components`
338
334
  });
339
335
  if (site.patterns.patterns.length)
340
336
  sections.push({
341
337
  title: "Patterns",
342
338
  href: `${base}/patterns/${site.patterns.patterns[0].id}`,
343
339
  why: "The screens the product keeps making, composed of the components, so a recurring problem is solved once.",
344
- count: `${site.patterns.patterns.length} patterns`
345
340
  });
346
341
  if (site.shows("standards") && (config.baselines || config.adherence))
347
342
  sections.push({
348
343
  title: "Standards",
349
344
  href: `${base}/standards`,
350
345
  why: "The bar the product is held to, and how closely it follows the system today, reported on every change.",
351
- count: share == null ? "Baselines agreed" : `${Math.round(share * 100)}% of the product on the system`
352
346
  });
353
347
  if (site.shows("vocabulary") && site.taxonomy.terms.length)
354
348
  sections.push({
355
349
  title: "Vocabulary",
356
350
  href: `${base}/vocabulary`,
357
351
  why: "One name for each thing, and the names people reach for instead, so people and agents mean the same thing.",
358
- count: `${site.taxonomy.terms.length} terms`
359
352
  });
360
353
 
361
354
  const loop = [
@@ -375,15 +368,19 @@ function Overview({ site }: { site: Site }) {
375
368
  )}
376
369
 
377
370
  <DocsSection title="What's here, and why" lead="Each part of the system has one job. Together they mean the product, its people and its agents work from the same source.">
378
- <ul data-slot="overview-sections" className="grid gap-x-12 md:grid-cols-2">
371
+ <ul data-slot="overview-sections" className="flex flex-col border-b border-border">
379
372
  {sections.map((sec) => (
380
373
  <li key={sec.title} className="border-t border-border">
381
- <Link href={sec.href} className="group flex h-full flex-col gap-4 py-7 focus-visible:outline-2 focus-visible:outline-ring">
382
- <span className="flex items-baseline justify-between gap-4">
383
- <span className="text-2xl leading-tight tracking-tight underline-offset-[6px] group-hover:underline">{sec.title}</span>
384
- <span className="shrink-0 text-xs text-muted-foreground tabular-nums">{sec.count}</span>
385
- </span>
386
- <span className="max-w-md text-base leading-relaxed text-muted-foreground">{sec.why}</span>
374
+ <Link
375
+ href={sec.href}
376
+ className="group grid items-baseline gap-x-10 gap-y-2 py-6 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring md:grid-cols-[13rem_minmax(0,1fr)_auto]"
377
+ >
378
+ <span className={cx(display, "text-2xl leading-snug tracking-tight underline-offset-4 group-hover:underline")}>{sec.title}</span>
379
+ <span className="max-w-2xl text-base leading-relaxed text-muted-foreground">{sec.why}</span>
380
+ <ArrowUpRight
381
+ aria-hidden
382
+ className="hidden size-(--dt-size-sm) translate-y-0.5 self-start text-muted-foreground transition duration-(--dt-duration-base) ease-(--dt-ease-out) group-hover:translate-x-1 group-hover:-translate-y-0.5 group-hover:text-canvas-foreground md:mt-1.5 md:block"
383
+ />
387
384
  </Link>
388
385
  </li>
389
386
  ))}
@@ -412,9 +409,13 @@ function Overview({ site }: { site: Site }) {
412
409
  </ol>
413
410
  <p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground">
414
411
  See it the way an agent does:
415
- <a href={`${base}.md`} className="inline-flex items-center gap-1 font-medium text-canvas-foreground underline underline-offset-4 hover:decoration-2">
412
+ <a href={base ? `${base}.md` : "/.md"} className="inline-flex items-center gap-1 font-medium text-canvas-foreground underline underline-offset-4 hover:decoration-2">
416
413
  this page as markdown <ArrowUpRight aria-hidden className="size-3.5" />
417
414
  </a>
415
+ or, for a page, slide or document made outside the repo,
416
+ <a href={`${base}/design.md`} className="inline-flex items-center gap-1 font-medium text-canvas-foreground underline underline-offset-4 hover:decoration-2">
417
+ design.md <ArrowUpRight aria-hidden className="size-3.5" />
418
+ </a>
418
419
  </p>
419
420
  </DocsSection>
420
421
  )}
package/src/next/proxy.ts CHANGED
@@ -27,7 +27,7 @@ export function docsProxy({ basePath = "/design/system", production }: { basePat
27
27
  // the markdown responses keep theirs.
28
28
  const res = NextResponse.next();
29
29
  res.headers.set("vary", "Accept");
30
- res.headers.set("link", `<${page}.md>; rel="alternate"; type="text/markdown"`);
30
+ res.headers.set("link", `<${page ? `${page}.md` : "/.md"}>; rel="alternate"; type="text/markdown"`);
31
31
  res.headers.set("x-robots-tag", "noindex");
32
32
  return res;
33
33
  };
@@ -36,7 +36,8 @@ export function docsProxy({ basePath = "/design/system", production }: { basePat
36
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
37
  export function docsMatcher(basePath = "/design/system"): string[] {
38
38
  const base = basePath.replace(/\/$/, "");
39
- return [base, `${base}.md`, `${base}/:path*`];
39
+ // served at the site root, every path is a docs path
40
+ return base ? [base, `${base}.md`, `${base}/:path*`] : ["/", "/:path*"];
40
41
  }
41
42
 
42
43
  /** text/markdown ranked above text/html in the Accept header. */
@@ -1,14 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  import { Menu } from "@base-ui/react/menu";
4
- import { Braces, Check, ChevronDown, Copy, FileText, Files } from "lucide-react";
4
+ import { Braces, Check, ChevronDown, Copy, FileText, Files, Palette } from "lucide-react";
5
5
  import { usePathname } from "next/navigation";
6
6
  import { useState } from "react";
7
7
 
8
8
  /**
9
9
  * What a reader can take from a page, as one split button: copy the page as
10
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.
11
+ * view that markdown, copy the whole system as one file, copy design.md (the
12
+ * brand for work outside the repo), or open the manifest.
12
13
  * Put it in a `DocsHeader`.
13
14
  */
14
15
  export function PageActions({ basePath = "/design/system" }: { basePath?: string }) {
@@ -73,6 +74,13 @@ export function PageActions({ basePath = "/design/system" }: { basePath?: string
73
74
  <span className="text-xs text-muted-foreground">Every page in one markdown file</span>
74
75
  </span>
75
76
  </Menu.Item>
77
+ <Menu.Item className={item} onClick={() => copyFrom(`${basePath}/design.md`, "design.md copied")}>
78
+ <Palette aria-hidden className={icon} />
79
+ <span className="flex flex-col">
80
+ Copy design.md
81
+ <span className="text-xs text-muted-foreground">The brand for a page, slide or document made elsewhere</span>
82
+ </span>
83
+ </Menu.Item>
76
84
  <Menu.LinkItem className={item} href={`${basePath}/manifest/components.json`} target="_blank" rel="noreferrer">
77
85
  <Braces aria-hidden className={icon} />
78
86
  <span className="flex flex-col">
@@ -250,9 +250,9 @@ export function FlagControl({ name, checked, onChange }: { name: string; checked
250
250
  checked={checked}
251
251
  onCheckedChange={onChange}
252
252
  // drawn small, pressed large: the hit area reaches the docs' target without the switch growing
253
- className="relative h-6 w-10 shrink-0 cursor-pointer rounded-full bg-input p-0.5 transition-colors duration-(--dt-duration-fast) after:absolute after:-inset-2.5 after:content-[''] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[checked]:bg-primary"
253
+ className="relative h-6 w-10 shrink-0 cursor-pointer rounded-full bg-input p-0.5 transition-colors duration-(--dt-duration-fast) after:absolute after:-inset-2.5 after:content-[''] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[checked]:bg-[var(--docs-accent,var(--primary))]"
254
254
  >
255
- <Switch.Thumb className="block size-5 rounded-full bg-primary-foreground shadow-xs transition-transform duration-(--dt-duration-fast) data-[checked]:translate-x-4" />
255
+ <Switch.Thumb className="block size-5 rounded-full bg-canvas shadow-xs ring-1 ring-border transition-transform duration-(--dt-duration-fast) data-[checked]:translate-x-4" />
256
256
  </Switch.Root>
257
257
  </div>
258
258
  );
package/src/shell.tsx CHANGED
@@ -93,6 +93,8 @@ export function DocsShell({
93
93
  children: ReactNode;
94
94
  }) {
95
95
  const current = usePathname();
96
+ // the overview's own address: the base path, or the site root when the docs are served there (basePath "")
97
+ const home = basePath || "/";
96
98
  const searchItems = useMemo(
97
99
  () =>
98
100
  searchIndex({
@@ -109,7 +111,7 @@ export function DocsShell({
109
111
  const sections = useMemo<Section[]>(() => {
110
112
  const families = groupFamilies(manifest);
111
113
  const all: Section[] = [
112
- { id: "overview", label: "Overview", icon: House, href: basePath, groups: [] },
114
+ { id: "overview", label: "Overview", icon: House, href: home, groups: [] },
113
115
  { id: "brand", label: "Brand", icon: BookOpen, groups: [{ links: brand }] },
114
116
  { id: "foundations", label: "Foundations", icon: Palette, groups: [{ links: foundations }] },
115
117
  ...(rules?.rules.length ? [{ id: "rules", label: "Rules", icon: Scale, href: `${basePath}/rules`, groups: [] }] : []),
@@ -135,7 +137,7 @@ export function DocsShell({
135
137
  { id: "standards", label: "Standards", icon: Gauge, groups: [{ links: standards }] },
136
138
  ];
137
139
  return all.filter((s) => s.href || s.groups.some((g) => g.links.length));
138
- }, [manifest, rules, patterns, brand, foundations, standards, basePath]);
140
+ }, [manifest, rules, patterns, brand, foundations, standards, basePath, home]);
139
141
 
140
142
  // every page in navigation order, for the section a page sits in and its neighbours
141
143
  const page = useMemo(() => {
@@ -156,7 +158,7 @@ export function DocsShell({
156
158
  useEffect(() => setMenu(false), [current]);
157
159
 
158
160
  const brandLink = (
159
- <Link href={basePath} data-slot="docs-title" className={cx(target, "flex min-w-0 items-center gap-3 rounded-md focus-visible:outline-2 focus-visible:outline-ring")}>
161
+ <Link href={home} data-slot="docs-title" className={cx(target, "flex min-w-0 items-center gap-3 rounded-md focus-visible:outline-2 focus-visible:outline-ring")}>
160
162
  {logo && (
161
163
  <span data-slot="docs-logo" className="shrink-0">
162
164
  <Live>{logo}</Live>
@@ -224,7 +226,7 @@ export function DocsShell({
224
226
  {search && <DocsSearch items={searchItems} shortcuts={false} variant="icon" />}
225
227
  </header>
226
228
 
227
- {hero && current === basePath && (
229
+ {hero && current === home && (
228
230
  <div data-slot="docs-hero" className="w-full">
229
231
  <Live>{hero}</Live>
230
232
  </div>
package/src/standards.tsx CHANGED
@@ -26,13 +26,13 @@ export function Standards({ baselines, children }: { baselines: Record<string, u
26
26
  {section.title}
27
27
  </h3>
28
28
  {single ? (
29
- <p className="text-5xl font-light tracking-tight">{section.rows[0].value}</p>
29
+ <p className="text-xl leading-snug tracking-tight text-pretty">{section.rows[0].value}</p>
30
30
  ) : section.rows.length ? (
31
31
  <dl className="flex flex-col gap-3">
32
32
  {section.rows.map((row) => (
33
33
  <div key={row.label} className="flex flex-col gap-0.5">
34
34
  <dt className="text-xs text-muted-foreground">{row.label}</dt>
35
- <dd className="text-2xl font-light tracking-tight tabular-nums">{row.value}</dd>
35
+ <dd className="text-lg leading-snug tracking-tight tabular-nums">{row.value}</dd>
36
36
  </div>
37
37
  ))}
38
38
  </dl>
package/src/styles.css CHANGED
@@ -9,8 +9,9 @@
9
9
  * semantic names (--canvas, --surface, --primary, …), so a project on our tokens
10
10
  * needs nothing; any other project sets those names on the docs. Fonts take
11
11
  * --docs-font-sans / --docs-font-mono, else the project's --font-sans and
12
- * --font-mono; headings --docs-display, else --font-display. Sizes, radii,
13
- * shadows, leading and tracking follow the project's scale where it has one.
12
+ * --font-mono; headings --docs-display, else --font-display. Sizes, shadows,
13
+ * leading and tracking follow the project's scale where it has one; corners
14
+ * are the docs' own (--docs-radius-*), never the project's.
14
15
  * Every value has a fallback, so the docs render on a project with none of it.
15
16
  */
16
17
  @layer theme, base, components, utilities;
@@ -109,12 +110,12 @@
109
110
  --tracking-wide: var(--tracking-wide, 0.04em);
110
111
  --tracking-widest: var(--tracking-widest, 0.16em);
111
112
 
112
- --radius-xs: var(--docs-radius-xs, var(--radius-xs, 0.125rem));
113
- --radius-sm: var(--docs-radius-sm, var(--radius-sm, 0.25rem));
114
- --radius-md: var(--docs-radius-md, var(--radius-md, 0.375rem));
115
- --radius-lg: var(--docs-radius-lg, var(--radius-lg, 0.5rem));
116
- --radius-xl: var(--docs-radius-xl, var(--radius-xl, 0.75rem));
117
- --radius-2xl: var(--docs-radius-2xl, var(--radius-2xl, 1rem));
113
+ --radius-xs: var(--docs-radius-xs, 0.125rem);
114
+ --radius-sm: var(--docs-radius-sm, 0.25rem);
115
+ --radius-md: var(--docs-radius-md, 0.375rem);
116
+ --radius-lg: var(--docs-radius-lg, 0.5rem);
117
+ --radius-xl: var(--docs-radius-xl, 0.75rem);
118
+ --radius-2xl: var(--docs-radius-2xl, 1rem);
118
119
 
119
120
  --shadow-xs: var(--shadow-xs, 0 1px 2px 0 color-mix(in oklch, var(--shadow, #0b0d12) 6%, transparent));
120
121
  --shadow-sm: var(--shadow-sm, 0 1px 3px 0 color-mix(in oklch, var(--shadow, #0b0d12) 10%, transparent));
package/src/type-ramp.tsx CHANGED
@@ -34,12 +34,9 @@ export function TypeRamp({
34
34
  <div className="grid gap-x-12 gap-y-10 md:grid-cols-2">
35
35
  {families.map((f) => (
36
36
  <figure key={f.name} data-slot="type-family" className="flex flex-col gap-5 border-t border-border pt-6">
37
- <div className="flex flex-col gap-6" style={{ fontFamily: `var(${f.name})` }}>
38
- <span className="text-8xl leading-none font-light tracking-tighter">Aa</span>
39
- <span className="text-sm leading-relaxed break-all text-muted-foreground">
40
- ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 &amp;?!
41
- </span>
42
- </div>
37
+ <span className="text-8xl leading-none font-light tracking-tighter" style={{ fontFamily: `var(${f.name})` }}>
38
+ Aa
39
+ </span>
43
40
  <figcaption className="flex flex-col gap-0.5">
44
41
  <code className="font-mono text-xs font-medium">{shortName(f, "font")}</code>
45
42
  <span className="truncate font-mono text-xs text-muted-foreground" title={f.values.default}>
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import { Live } from "./lib/live";
4
+ import { wrapIn } from "./lib/wrap";
4
5
  import { Grid3x3, Moon, Sun } from "lucide-react";
5
- import { type ReactNode, useEffect, useState } from "react";
6
+ import { type ReactElement, type ReactNode, useEffect, useState } from "react";
6
7
  import { cx } from "./lib/cx";
7
8
  import { coerce, Missing, useLookup } from "./lib/lookup";
8
9
  import type { ComponentEntry, VariantAxis } from "./lib/manifest-types";
@@ -19,9 +20,12 @@ export function VariantMatrix({
19
20
  props = {},
20
21
  children,
21
22
  axes: only,
23
+ wrap,
22
24
  }: {
23
25
  entry: ComponentEntry;
24
26
  props?: Record<string, unknown>;
27
+ /** What each instance must sit in to work: a provider, its root. */
28
+ wrap?: ReactElement<{ children?: ReactNode }>;
25
29
  /** What to render inside each instance. Defaults to the component's name. */
26
30
  children?: ReactNode;
27
31
  /** Limit and order the axes shown. */
@@ -38,9 +42,12 @@ export function VariantMatrix({
38
42
  const content = children ?? entry.name;
39
43
  const render = (extra: Record<string, string>) => (
40
44
  <Live>
41
- <Component {...props} {...coerce(extra)}>
42
- {content}
43
- </Component>
45
+ {wrapIn(
46
+ wrap,
47
+ <Component {...props} {...coerce(extra)}>
48
+ {content}
49
+ </Component>,
50
+ )}
44
51
  </Live>
45
52
  );
46
53
  const [rows, cols, ...rest] = axes;
package/src/workbench.tsx CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { handlerNames } from "./lib/handlers";
4
4
  import { Live } from "./lib/live";
5
+ import { wrapIn } from "./lib/wrap";
5
6
  import { Tabs } from "@base-ui/react/tabs";
6
7
  import { Toggle } from "@base-ui/react/toggle";
7
8
  import { ToggleGroup } from "@base-ui/react/toggle-group";
@@ -381,7 +382,7 @@ export function Workbench({
381
382
  }}
382
383
  >
383
384
  <div ref={target} data-slot="workbench-target" className="flex max-w-full items-center justify-center">
384
- <Boundary label={entry.name}>{wrap && isValidElement(wrap) ? cloneElement(wrap, undefined, node) : node}</Boundary>
385
+ <Boundary label={entry.name}>{wrapIn(wrap, node)}</Boundary>
385
386
  </div>
386
387
  </div>
387
388
  </div>