@designtools/docs 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +24 -2
  3. package/dist/anatomy.js +2 -1
  4. package/dist/cli.js +1 -1
  5. package/dist/glossary.d.ts +1 -1
  6. package/dist/glossary.js +13 -6
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/lib/live.d.ts +10 -0
  10. package/dist/lib/live.js +10 -0
  11. package/dist/lib/lookup.js +3 -2
  12. package/dist/lib/status.d.ts +2 -3
  13. package/dist/lib/terms.d.ts +15 -0
  14. package/dist/lib/terms.js +20 -0
  15. package/dist/next/config.d.ts +24 -4
  16. package/dist/next/index.d.ts +3 -0
  17. package/dist/next/index.js +3 -0
  18. package/dist/next/markdown.js +1 -1
  19. package/dist/next/pages.js +82 -16
  20. package/dist/next/prose.d.ts +2 -2
  21. package/dist/next/prose.js +6 -4
  22. package/dist/pattern.d.ts +4 -1
  23. package/dist/pattern.js +11 -8
  24. package/dist/playground.js +1 -1
  25. package/dist/rule.d.ts +9 -5
  26. package/dist/rule.js +14 -9
  27. package/dist/shell.d.ts +4 -1
  28. package/dist/shell.js +13 -11
  29. package/dist/styles.css +932 -743
  30. package/dist/swatches.js +10 -4
  31. package/dist/variant-matrix.js +11 -6
  32. package/dist/voice-terms.js +6 -5
  33. package/dist/workbench.js +2 -1
  34. package/package.json +1 -1
  35. package/src/anatomy.tsx +6 -1
  36. package/src/glossary.tsx +39 -40
  37. package/src/index.ts +2 -1
  38. package/src/lib/live.tsx +15 -0
  39. package/src/lib/lookup.tsx +3 -2
  40. package/src/lib/status.tsx +3 -3
  41. package/src/lib/terms.tsx +53 -0
  42. package/src/next/config.ts +25 -4
  43. package/src/next/index.tsx +3 -0
  44. package/src/next/markdown.ts +1 -1
  45. package/src/next/pages.tsx +172 -59
  46. package/src/next/prose.tsx +16 -6
  47. package/src/pattern.tsx +42 -37
  48. package/src/playground.tsx +7 -2
  49. package/src/rule.tsx +108 -59
  50. package/src/shell.tsx +30 -16
  51. package/src/styles.css +4 -2
  52. package/src/swatches.tsx +68 -57
  53. package/src/variant-matrix.tsx +63 -80
  54. package/src/voice-terms.tsx +20 -29
  55. package/src/workbench.tsx +6 -3
package/src/rule.tsx CHANGED
@@ -1,24 +1,26 @@
1
1
  "use client";
2
2
 
3
+ import { Live } from "./lib/live";
3
4
  import { Bot, Eye, Hash, ShieldCheck } from "lucide-react";
4
- import type { ReactNode } from "react";
5
+ import { type ReactNode, useState } from "react";
5
6
  import { CodeView } from "./code-view";
6
7
  import { cx } from "./lib/cx";
7
8
  import { ruleMarkdown } from "./lib/markdown";
8
9
  import type { RuleEntry } from "./lib/manifest-types";
9
10
  import { StatusBadge } from "./lib/status";
10
11
  import { inline, Prose } from "./lib/text";
11
- import { Eyebrow, Segmented, SegmentedList, SegmentedPanel, Stage } from "./lib/ui";
12
+ import { Eyebrow } from "./lib/ui";
12
13
 
13
14
  const CHECK: Record<string, string> = { test: "Tested", lint: "Linted", audit: "Audited", manual: "Checked by a person" };
14
15
 
15
16
  /**
16
- * One rule, brand or interface, written for people: the statement set large, the
17
- * line it draws, what it applies to and the check that proves it, then why. A
18
- * switch shows the same rule as an agent reads it. Pass a demonstration as
19
- * children where the rule needs persuading; it is shown on a stage.
17
+ * One rule, brand or interface, written for people. On a wide screen, its name, status and the check
18
+ * that proves it hold the left column while the right reads: the statement set large, the
19
+ * demonstration (pass it as children, for a rule that needs persuading), the facts, then why. A small
20
+ * switch shows the same rule as an agent reads it.
20
21
  */
21
22
  export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode }) {
23
+ const [view, setView] = useState<"people" | "agents">("people");
22
24
  const chips = (items: string[], mono = false) => (
23
25
  <span className="flex flex-wrap gap-1.5">
24
26
  {items.map((t) => (
@@ -33,46 +35,75 @@ export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode
33
35
  if (rule.appliesTo?.length) facts.push(["Applies to", chips(rule.appliesTo)]);
34
36
  if (rule.exceptions?.length) facts.push(["Exceptions", <span key="e">{rule.exceptions.join("; ")}</span>]);
35
37
  if (rule.tokens?.length) facts.push(["Tokens", chips(rule.tokens, true)]);
38
+ const toggle = (value: "people" | "agents", label: string, Icon: typeof Eye) => (
39
+ <button
40
+ type="button"
41
+ aria-pressed={view === value}
42
+ onClick={() => setView(value)}
43
+ className={cx(
44
+ "inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs font-medium transition-colors duration-(--dt-duration-fast) focus-visible:outline-2 focus-visible:outline-ring",
45
+ view === value ? "bg-muted text-canvas-foreground" : "text-muted-foreground hover:text-canvas-foreground",
46
+ )}
47
+ >
48
+ <Icon aria-hidden className="size-3.5" />
49
+ {label}
50
+ </button>
51
+ );
36
52
 
37
53
  return (
38
- <article id={rule.id} data-slot="rule" className="scroll-mt-24 border-t border-border pt-8">
39
- <Segmented defaultValue="rendered">
40
- <header className="flex flex-wrap items-start justify-between gap-3">
41
- <div className="flex min-w-0 flex-col gap-2">
42
- <span className="flex flex-wrap items-center gap-2">
43
- <StatusBadge status={rule.status} />
44
- <span className="text-xs font-medium text-muted-foreground">{rule.kind === "brand" ? "Brand rule" : "Interface rule"}</span>
54
+ <article id={rule.id} data-slot="rule" className="grid scroll-mt-24 gap-x-12 gap-y-6 border-t border-border pt-10 xl:grid-cols-[15rem_minmax(0,1fr)]">
55
+ <header data-slot="rule-header" className="flex flex-col gap-4 xl:sticky xl:top-24 xl:self-start">
56
+ <div className="flex flex-col gap-2.5">
57
+ <span className="text-xs font-medium text-muted-foreground">{rule.kind === "brand" ? "Brand rule" : "Interface rule"}</span>
58
+ <h3 className="group flex items-start gap-1 text-2xl leading-tight tracking-tight">
59
+ {rule.title}
60
+ <a
61
+ href={`#${rule.id}`}
62
+ aria-label={`Link to ${rule.title}`}
63
+ className="relative mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-muted focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-2 after:content-['']"
64
+ >
65
+ <Hash aria-hidden className="size-4" />
66
+ </a>
67
+ </h3>
68
+ <span>
69
+ <StatusBadge status={rule.status} />
70
+ </span>
71
+ </div>
72
+ {rule.check && (
73
+ <p data-slot="rule-check" className="flex items-start gap-2 text-sm">
74
+ <ShieldCheck aria-hidden className="mt-0.5 size-(--dt-size-xs) shrink-0 text-muted-foreground" />
75
+ <span className="flex flex-col gap-0.5">
76
+ <span className="font-medium">
77
+ {CHECK[rule.check.kind] ?? rule.check.kind}
78
+ {rule.check.ref && (
79
+ <>
80
+ {" "}
81
+ by <code className="font-mono text-xs">{rule.check.ref}</code>
82
+ </>
83
+ )}
84
+ </span>
85
+ {rule.check.description && <span className="text-muted-foreground">{inline(rule.check.description)}</span>}
45
86
  </span>
46
- <h3 className="group flex items-center gap-2 text-3xl tracking-tight">
47
- {rule.title}
48
- <a
49
- href={`#${rule.id}`}
50
- aria-label={`Link to ${rule.title}`}
51
- className="relative inline-flex size-7 items-center justify-center rounded-md text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-muted focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-2 after:content-['']"
52
- >
53
- <Hash aria-hidden className="size-4" />
54
- </a>
55
- </h3>
56
- </div>
57
- <SegmentedList
58
- label={`${rule.title}, two ways`}
59
- items={[
60
- { value: "rendered", label: "For people", icon: Eye },
61
- { value: "agent", label: "For agents", icon: Bot },
62
- ]}
63
- />
64
- </header>
65
- <SegmentedPanel value="rendered" className="flex flex-col gap-6 pt-4">
66
- <p data-slot="rule-statement" className="max-w-3xl text-xl leading-snug tracking-tight">
87
+ </p>
88
+ )}
89
+ <div role="group" aria-label={`Read ${rule.title}`} className="-ml-2 flex gap-1">
90
+ {toggle("people", "For people", Eye)}
91
+ {toggle("agents", "For agents", Bot)}
92
+ </div>
93
+ </header>
94
+
95
+ {view === "people" ? (
96
+ <div data-slot="rule-body" className="flex min-w-0 flex-col gap-8">
97
+ <p data-slot="rule-statement" className="max-w-3xl text-2xl leading-snug tracking-tight">
67
98
  {inline(rule.statement)}
68
99
  </p>
69
100
  {children && (
70
- <div data-slot="rule-demonstration" className="overflow-hidden rounded-xl border border-border">
71
- <Stage className="min-h-40">{children}</Stage>
101
+ <div data-slot="rule-demonstration" data-docs-stage="" className="flex min-h-40 items-center justify-center rounded-2xl bg-muted/60 p-8 sm:p-10">
102
+ <Live>{children}</Live>
72
103
  </div>
73
104
  )}
74
105
  {facts.length > 0 && (
75
- <dl className="grid gap-x-10 gap-y-5 sm:grid-cols-2">
106
+ <dl className="grid max-w-3xl gap-x-10 gap-y-4 sm:grid-cols-2">
76
107
  {facts.map(([term, detail]) => (
77
108
  <div key={term} className="flex flex-col gap-1.5">
78
109
  <dt className="text-xs font-medium text-muted-foreground">{term}</dt>
@@ -81,36 +112,54 @@ export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode
81
112
  ))}
82
113
  </dl>
83
114
  )}
84
- {rule.check && (
85
- <div className="flex items-start gap-2.5 text-sm">
86
- <ShieldCheck aria-hidden className="mt-0.5 size-(--dt-size-xs) shrink-0 text-muted-foreground" />
87
- <p className="flex flex-col gap-0.5">
88
- <span className="font-medium">
89
- {CHECK[rule.check.kind] ?? rule.check.kind}
90
- {rule.check.ref && (
91
- <>
92
- {" "}
93
- by <code className="font-mono text-xs">{rule.check.ref}</code>
94
- </>
95
- )}
96
- </span>
97
- {rule.check.description && <span className="text-muted-foreground">{inline(rule.check.description)}</span>}
98
- </p>
99
- </div>
100
- )}
101
115
  {rule.rationale && (
102
116
  <div className="flex max-w-3xl flex-col gap-2">
103
117
  <Eyebrow>Why</Eyebrow>
104
- <div className="flex flex-col gap-2 text-sm text-muted-foreground">
118
+ <div className="flex flex-col gap-2 text-base leading-relaxed text-muted-foreground">
105
119
  <Prose text={rule.rationale} />
106
120
  </div>
107
121
  </div>
108
122
  )}
109
- </SegmentedPanel>
110
- <SegmentedPanel value="agent" className="pt-4">
123
+ </div>
124
+ ) : (
125
+ <div className="min-w-0">
111
126
  <CodeView code={ruleMarkdown(rule)} lang="markdown" label={`${rule.id}.md`} wrap />
112
- </SegmentedPanel>
113
- </Segmented>
127
+ </div>
128
+ )}
114
129
  </article>
115
130
  );
116
131
  }
132
+
133
+ /** Every rule on one screen, a line each, linking to the rule: what it is, how settled, how it is checked. */
134
+ export function RuleIndex({ rules }: { rules: RuleEntry[] }) {
135
+ return (
136
+ <div data-slot="rule-index" className="-mx-1 overflow-x-auto px-1">
137
+ <table className="w-full min-w-[34rem] border-collapse text-left text-sm">
138
+ <thead>
139
+ <tr className="text-xs text-muted-foreground">
140
+ <th scope="col" className="pr-4 pb-3 font-medium">Rule</th>
141
+ <th scope="col" className="px-4 pb-3 font-medium">Kind</th>
142
+ <th scope="col" className="px-4 pb-3 font-medium">Status</th>
143
+ <th scope="col" className="pb-3 pl-4 font-medium">Checked</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>
147
+ {rules.map((r) => (
148
+ <tr key={r.id} className="border-t border-border">
149
+ <th scope="row" className="py-3 pr-4 font-normal">
150
+ <a href={`#${r.id}`} className="font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring">
151
+ {r.title}
152
+ </a>
153
+ </th>
154
+ <td className="px-4 py-3 text-muted-foreground capitalize">{r.kind}</td>
155
+ <td className="px-4 py-3">
156
+ <StatusBadge status={r.status} />
157
+ </td>
158
+ <td className="py-3 pl-4 text-muted-foreground">{r.check ? (CHECK[r.check.kind] ?? r.check.kind) : "Not yet"}</td>
159
+ </tr>
160
+ ))}
161
+ </tbody>
162
+ </table>
163
+ </div>
164
+ );
165
+ }
package/src/shell.tsx CHANGED
@@ -6,7 +6,9 @@ import { ArrowLeft, ArrowRight, BookOpen, ChevronRight, Component, Gauge, House,
6
6
  import Link from "next/link";
7
7
  import { usePathname } from "next/navigation";
8
8
  import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
9
+ import { Live } from "./lib/live";
9
10
  import { accent, cx, display, target } from "./lib/cx";
11
+ import { type Status, StatusBadge } from "./lib/status";
10
12
  import { groupFamilies, slugOf } from "./lib/manifest";
11
13
  import type { ComponentsManifest, PatternsManifest, RulesManifest, TaxonomyManifest } from "./lib/manifest-types";
12
14
  import { searchIndex } from "./lib/search";
@@ -157,7 +159,7 @@ export function DocsShell({
157
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")}>
158
160
  {logo && (
159
161
  <span data-slot="docs-logo" className="shrink-0">
160
- {logo}
162
+ <Live>{logo}</Live>
161
163
  </span>
162
164
  )}
163
165
  <span className={cx(logo ? "sr-only" : "truncate text-base font-semibold tracking-tight")}>{title}</span>
@@ -207,7 +209,7 @@ export function DocsShell({
207
209
  <X aria-hidden className="size-(--dt-size-sm)" />
208
210
  </Dialog.Close>
209
211
  </div>
210
- <Nav sections={sections} current={current} title={title} />
212
+ <Nav sections={sections} current={current} title={title} className="pt-4" />
211
213
  {themeSwitch && (
212
214
  <div className="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
213
215
  <span className="text-xs font-medium text-muted-foreground">Appearance</span>
@@ -224,7 +226,7 @@ export function DocsShell({
224
226
 
225
227
  {hero && current === basePath && (
226
228
  <div data-slot="docs-hero" className="w-full">
227
- {hero}
229
+ <Live>{hero}</Live>
228
230
  </div>
229
231
  )}
230
232
  <div className="mx-auto flex w-full max-w-[90rem] gap-16 px-5 pt-12 pb-20 sm:px-8 lg:px-12 lg:pt-20 xl:px-20">
@@ -244,7 +246,7 @@ const iconButton =
244
246
  "inline-flex size-(--docs-target,2.75rem) shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring";
245
247
 
246
248
  /** The sections, each a link or a fold that opens where the current page is. */
247
- function Nav({ sections, current, title }: { sections: Section[]; current: string; title: string }) {
249
+ function Nav({ sections, current, title, className }: { sections: Section[]; current: string; title: string; className?: string }) {
248
250
  const holds = (s: Section) => s.groups.some((g) => g.links.some((l) => l.href === current || l.children?.some((c) => c.href === current)));
249
251
  const [open, setOpen] = useState<Record<string, boolean>>(() => Object.fromEntries(sections.map((s) => [s.id, holds(s)])));
250
252
  // moving to a page opens its section; nothing closes on its own
@@ -255,7 +257,7 @@ function Nav({ sections, current, title }: { sections: Section[]; current: strin
255
257
  }, [current, sections]);
256
258
 
257
259
  return (
258
- <nav aria-label={title} data-slot="docs-nav" className="min-h-0 flex-1 overflow-y-auto px-3 pb-4 text-sm">
260
+ <nav aria-label={title} data-slot="docs-nav" className={cx("min-h-0 flex-1 overflow-y-auto px-3 pb-4 text-sm", className)}>
259
261
  <ul className="flex flex-col gap-0.5">
260
262
  {sections.map((s) => {
261
263
  const Icon = s.icon;
@@ -294,7 +296,7 @@ function Nav({ sections, current, title }: { sections: Section[]; current: strin
294
296
  {s.groups.map((g, i) => (
295
297
  <div key={g.heading ?? i} className="flex flex-col">
296
298
  {g.heading && (
297
- <p data-slot="docs-nav-heading" className="px-2.5 pt-1 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">
299
+ <p data-slot="docs-nav-heading" className="px-2.5 pt-3 pb-1.5 text-[0.6875rem] font-semibold tracking-[0.08em] text-muted-foreground uppercase first:pt-1">
298
300
  {g.heading}
299
301
  </p>
300
302
  )}
@@ -339,7 +341,7 @@ function NavLink({ link, current }: { link: DocsLink; current: string }) {
339
341
  data-slot="docs-nav-link"
340
342
  className={cx(
341
343
  target,
342
- "relative flex items-center rounded-md px-2.5 text-muted-foreground hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring",
344
+ "relative flex items-center rounded-md px-2.5 text-canvas-foreground/80 hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring",
343
345
  active && "bg-muted font-medium text-canvas-foreground",
344
346
  )}
345
347
  >
@@ -416,7 +418,7 @@ function Pager({ prev, next }: { prev?: DocsLink; next?: DocsLink }) {
416
418
  "group flex flex-col gap-1 rounded-lg border border-border p-5 transition-colors duration-(--dt-duration-fast) hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring",
417
419
  );
418
420
  return (
419
- <nav aria-label="Previous and next" data-slot="docs-pager" className="grid gap-3 border-t border-border pt-10 sm:grid-cols-2">
421
+ <nav aria-label="Previous and next" data-slot="docs-pager" className="-mt-4 grid gap-3 sm:grid-cols-2 lg:-mt-8">
420
422
  {prev ? (
421
423
  <Link href={prev.href} className={card}>
422
424
  <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
@@ -448,24 +450,32 @@ export function DocsHeader({
448
450
  title,
449
451
  lead,
450
452
  eyebrow,
453
+ status,
451
454
  children,
452
455
  }: {
453
456
  title: string;
454
457
  lead?: string;
455
458
  /** Above the title. Defaults to the section the page sits in. */
456
459
  eyebrow?: string;
460
+ /** How settled the thing the page documents is, beside the eyebrow: a pattern's status. */
461
+ status?: Status;
457
462
  /** The page's actions, beside the title: `PageActions`. */
458
463
  children?: ReactNode;
459
464
  }) {
460
465
  const { section } = useContext(DocsPage);
461
466
  const over = eyebrow ?? (section && section !== title ? section : undefined);
462
467
  return (
463
- <header data-slot="docs-header" className="flex flex-col gap-6 border-b border-border pb-12 lg:pb-16">
468
+ <header data-slot="docs-header" className="flex flex-col gap-6">
464
469
  <div className="flex flex-wrap items-center justify-between gap-4">
465
- {over ? (
466
- <p data-slot="docs-eyebrow" className="text-xs font-medium tracking-[var(--tracking-widest,0.16em)] text-muted-foreground uppercase">
467
- {over}
468
- </p>
470
+ {over || status ? (
471
+ <span className="flex flex-wrap items-center gap-3">
472
+ {over && (
473
+ <p data-slot="docs-eyebrow" className="text-xs font-medium tracking-[var(--tracking-widest,0.16em)] text-muted-foreground uppercase">
474
+ {over}
475
+ </p>
476
+ )}
477
+ {status && <StatusBadge status={status} />}
478
+ </span>
469
479
  ) : (
470
480
  <span />
471
481
  )}
@@ -508,7 +518,11 @@ export function DocsFooter({
508
518
  }) {
509
519
  if (!provenance && !editorial && !source) return null;
510
520
  return (
511
- <footer data-slot="docs-footer" className="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-muted-foreground">
521
+ // order-last: the page renders its footer before the shell adds the pager, and where a page comes from is the last thing on it
522
+ <footer
523
+ data-slot="docs-footer"
524
+ className="order-last -mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border pt-6 text-xs text-muted-foreground lg:-mt-14"
525
+ >
512
526
  {(provenance || editorial) && (
513
527
  <p data-slot="docs-provenance" className="flex items-center gap-2">
514
528
  <span
@@ -539,11 +553,11 @@ export function DocsSection({ title, lead, children }: { title: string; lead?: s
539
553
  <section data-slot="docs-section" aria-labelledby={id} className="grid gap-x-10 gap-y-8 [counter-increment:docs-section] xl:grid-cols-[5rem_minmax(0,1fr)]">
540
554
  <span
541
555
  aria-hidden
542
- className="pt-2 font-mono text-xs tracking-[var(--tracking-widest,0.16em)] text-muted-foreground before:content-[counter(docs-section,decimal-leading-zero)]"
556
+ className="font-mono text-xs tracking-[var(--tracking-widest,0.16em)] text-muted-foreground [text-box:trim-both_cap_alphabetic] before:content-[counter(docs-section,decimal-leading-zero)]"
543
557
  />
544
558
  <div className="flex min-w-0 flex-col gap-10">
545
559
  <div className="flex flex-col gap-4">
546
- <h2 id={id} data-slot="docs-section-title" className={cx(display, "scroll-mt-24 text-4xl leading-[1.1] tracking-tight md:text-5xl")}>
560
+ <h2 id={id} data-slot="docs-section-title" className={cx(display, "scroll-mt-24 text-4xl leading-[1.1] tracking-tight [text-box:trim-both_cap_alphabetic] md:text-5xl")}>
547
561
  {title}
548
562
  </h2>
549
563
  {lead && <p className="max-w-2xl text-lg leading-relaxed text-muted-foreground">{lead}</p>}
package/src/styles.css CHANGED
@@ -1,7 +1,9 @@
1
1
  /*
2
2
  * The docs' styles, compiled at build and scoped to `[data-designtools]`, so they
3
- * style the docs and nothing else on the page, and add no specificity: a
4
- * project's own CSS always wins a tie.
3
+ * style the docs and nothing else on the page. They never match inside the
4
+ * project's own components shown in the docs (`Live`, `[data-docs-content]`), and
5
+ * on the docs' own elements they carry one attribute more than a project utility,
6
+ * so neither side's utilities leak into the other, whichever loads last.
5
7
  *
6
8
  * Themed by the project's own variables. Colours are @designtools/tokens'
7
9
  * semantic names (--canvas, --surface, --primary, …), so a project on our tokens
package/src/swatches.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
3
+ import { type ReactNode, useEffect, useRef, useState } from "react";
4
4
  import { contrastRatio, wcagGrade } from "./lib/contrast";
5
5
  import { cx } from "./lib/cx";
6
6
  import { ramps, semanticColours, valueIn, type Mode } from "./lib/manifest";
@@ -31,61 +31,73 @@ export function SemanticSwatches({
31
31
  return token && pointsAt(valueIn(token, mode));
32
32
  };
33
33
 
34
+ // one level of framing: a panel per mode (the dark one has to be dark), chips inside it, and nothing around the family
35
+ const panel = (mode: Mode) =>
36
+ cx("flex flex-col gap-3 rounded-2xl bg-canvas p-4 text-canvas-foreground", mode === "light" && "ring-1 ring-border ring-inset");
37
+ const modeLabel = (mode: Mode) => <span className="text-xs font-medium text-muted-foreground capitalize">{mode}</span>;
38
+ const row = (title: string, meta: ReactNode, body: ReactNode, key?: string) => (
39
+ <div key={key ?? title} data-slot="swatch-row" className="grid gap-x-10 gap-y-5 border-t border-border pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[11rem_minmax(0,1fr)]">
40
+ <div className="flex flex-col gap-1.5">
41
+ <h3 className="text-2xl leading-tight tracking-tight">{title}</h3>
42
+ {meta}
43
+ </div>
44
+ {body}
45
+ </div>
46
+ );
47
+
34
48
  return (
35
49
  <div data-slot="swatches" className="flex flex-col gap-10">
36
- <div className="grid gap-4 xl:grid-cols-2">
37
- {families.map((f) => (
38
- <article key={f} data-slot="swatch-family" className="flex flex-col overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground">
39
- <header className="flex items-baseline justify-between gap-3 px-4 py-3">
40
- <h3 className="text-lg capitalize">{f.slice(2).replace(/-/g, " ")}</h3>
41
- <code className="font-mono text-xs text-muted-foreground">{f}</code>
42
- </header>
43
- <div className={cx("grid border-t border-border", modes.length > 1 && "sm:grid-cols-2")}>
44
- {modes.map((mode) => (
45
- <div key={mode} data-theme={mode} className="flex flex-col gap-2 bg-canvas p-3 text-canvas-foreground not-first:border-t not-first:border-border sm:not-first:border-t-0 sm:not-first:border-l">
46
- <span className="text-xs font-medium text-muted-foreground capitalize">{mode}</span>
47
- <Chip name={f} label="fill" step={step(f, mode)} size="lg" />
48
- <div className="grid grid-cols-3 gap-2">
49
- <Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
50
- {byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
51
- {byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
52
- </div>
50
+ {families.map((f) =>
51
+ row(
52
+ (([first, ...rest]) => first.toUpperCase() + rest.join(""))(f.slice(2).replace(/-/g, " ")),
53
+ <code className="font-mono text-xs text-muted-foreground">{f}</code>,
54
+ <div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
55
+ {modes.map((mode) => (
56
+ <div key={mode} data-theme={mode} data-slot="swatch-family" className={panel(mode)}>
57
+ {modeLabel(mode)}
58
+ <Chip name={f} label="fill" step={step(f, mode)} size="lg" />
59
+ <div className="grid grid-cols-3 gap-2">
60
+ <Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
61
+ {byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
62
+ {byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
53
63
  </div>
54
- ))}
55
- </div>
56
- </article>
57
- ))}
58
- </div>
64
+ </div>
65
+ ))}
66
+ </div>,
67
+ f,
68
+ ),
69
+ )}
59
70
 
60
- {surfaces.length > 0 && (
61
- <div className="flex flex-col gap-3">
62
- <h3 className="text-2xl tracking-tight">Surfaces</h3>
63
- <div className={cx("grid gap-4", modes.length > 1 && "lg:grid-cols-2")}>
71
+ {surfaces.length > 0 &&
72
+ row(
73
+ "Surfaces",
74
+ <p className="text-sm text-muted-foreground">The page, panels and popups, each with its text.</p>,
75
+ <div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
64
76
  {modes.map((mode) => (
65
- <div key={mode} data-theme={mode} className="flex flex-col gap-2 rounded-xl border border-border bg-canvas p-3 text-canvas-foreground">
66
- <span className="text-xs font-medium text-muted-foreground capitalize">{mode}</span>
67
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
77
+ <div key={mode} data-theme={mode} className={panel(mode)}>
78
+ {modeLabel(mode)}
79
+ <div className="grid grid-cols-2 gap-2 xl:grid-cols-3">
68
80
  {surfaces.map((name) => (
69
81
  <Chip key={name} name={name} label={name.slice(2)} step={step(name, mode)} outlined />
70
82
  ))}
71
83
  </div>
72
84
  </div>
73
85
  ))}
74
- </div>
75
- </div>
76
- )}
86
+ </div>,
87
+ )}
77
88
 
78
- {utility.length > 0 && (
79
- <div className="flex flex-col gap-3">
80
- <h3 className="text-2xl tracking-tight">Lines, focus and shadow</h3>
81
- <div className={cx("grid gap-4", modes.length > 1 && "lg:grid-cols-2")}>
89
+ {utility.length > 0 &&
90
+ row(
91
+ "Lines, focus and shadow",
92
+ <p className="text-sm text-muted-foreground">Borders, input edges, the focus ring and the shadow colour.</p>,
93
+ <div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
82
94
  {modes.map((mode) => (
83
- <div key={mode} data-theme={mode} className="flex flex-col gap-2 rounded-xl border border-border bg-canvas p-3 text-canvas-foreground">
84
- <span className="text-xs font-medium text-muted-foreground capitalize">{mode}</span>
85
- <ul className="grid grid-cols-2 gap-2">
95
+ <div key={mode} data-theme={mode} className={panel(mode)}>
96
+ {modeLabel(mode)}
97
+ <ul className="grid grid-cols-2 gap-x-4 gap-y-3">
86
98
  {utility.map((name) => (
87
- <li key={name} data-slot="swatch" className="flex items-center gap-2 rounded-lg bg-surface p-2 text-surface-foreground">
88
- <span aria-hidden className="size-8 shrink-0 rounded-md border border-border" style={{ background: `var(${name})` }} />
99
+ <li key={name} data-slot="swatch" className="flex items-center gap-3">
100
+ <span aria-hidden className="size-9 shrink-0 rounded-lg border border-border" style={{ background: `var(${name})` }} />
89
101
  <span className="flex min-w-0 flex-col font-mono text-xs">
90
102
  <span className="truncate font-medium">{name.slice(2)}</span>
91
103
  <span className="truncate text-muted-foreground">{step(name, mode)}</span>
@@ -95,9 +107,8 @@ export function SemanticSwatches({
95
107
  </ul>
96
108
  </div>
97
109
  ))}
98
- </div>
99
- </div>
100
- )}
110
+ </div>,
111
+ )}
101
112
  </div>
102
113
  );
103
114
  }
@@ -191,7 +202,7 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
191
202
  const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
192
203
  const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
193
204
  return (
194
- <div data-slot="page-contrast" className="flex flex-col gap-6">
205
+ <div data-slot="page-contrast" className="flex flex-col gap-12">
195
206
  {modes.map((mode) => (
196
207
  <PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
197
208
  ))}
@@ -215,30 +226,30 @@ function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; g
215
226
  setRatios(out);
216
227
  }, [colours, grounds]);
217
228
  return (
218
- <div className="overflow-x-auto rounded-2xl border border-border bg-surface text-surface-foreground" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
229
+ <div className="-mx-1 overflow-x-auto px-1" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
219
230
  <div ref={probe} data-theme={mode} hidden />
220
- <table className="w-full min-w-[32rem] border-collapse text-left text-sm">
221
- <caption className="border-b border-border px-4 py-3 text-left text-sm font-semibold capitalize">{mode}</caption>
231
+ <table className="w-full min-w-[40rem] border-collapse text-left text-sm">
232
+ <caption className="pb-4 text-left text-lg font-medium capitalize">{mode}</caption>
222
233
  <thead>
223
- <tr className="border-b border-border bg-muted/50 text-xs text-muted-foreground">
224
- <th scope="col" className="px-4 py-2.5 font-medium">Colour</th>
234
+ <tr className="text-xs text-muted-foreground">
235
+ <th scope="col" className="pr-4 pb-3 font-medium">Colour</th>
225
236
  {grounds.map((g) => (
226
- <th key={g} scope="col" className="px-4 py-2.5 font-mono font-medium">{g.slice(2)}</th>
237
+ <th key={g} scope="col" className="px-3 pb-3 font-mono font-normal">{g.slice(2)}</th>
227
238
  ))}
228
239
  </tr>
229
240
  </thead>
230
241
  <tbody>
231
242
  {colours.map((c) => (
232
- <tr key={c} className="border-b border-border last:border-b-0">
233
- <th scope="row" className="px-4 py-2.5 font-mono text-xs font-medium">{c.slice(2)}</th>
243
+ <tr key={c} className="border-t border-border">
244
+ <th scope="row" className="py-3.5 pr-4 font-mono text-xs font-medium">{c.slice(2)}</th>
234
245
  {grounds.map((g) => {
235
246
  const r = ratios[`${c}|${g}`];
236
247
  const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
237
248
  return (
238
- <td key={g} className="px-4 py-2.5">
239
- <div className="flex items-center gap-2">
249
+ <td key={g} className="px-3 py-3.5">
250
+ <div className="flex items-center gap-2.5">
240
251
  {/* only the sample takes the mode; the number is read on the page */}
241
- <span data-theme={mode} className="flex h-9 w-12 items-center justify-center rounded-lg border border-(--border) text-base font-bold" style={{ background: `var(${g})`, color: `var(${c})` }} aria-hidden>
252
+ <span data-theme={mode} className="flex h-10 w-14 items-center justify-center rounded-lg border border-(--border) text-base font-bold" style={{ background: `var(${g})`, color: `var(${c})` }} aria-hidden>
242
253
  Aa
243
254
  </span>
244
255
  {r != null && (