@designtools/blocks 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 (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +117 -2
  4. package/dist/cli.js +381 -0
  5. package/package.json +59 -3
  6. package/registry/a11y-panel.tsx +99 -0
  7. package/registry/adherence-summary.tsx +110 -0
  8. package/registry/agent-view.tsx +57 -0
  9. package/registry/anatomy.tsx +88 -0
  10. package/registry/ask-claude.tsx +71 -0
  11. package/registry/code-view.tsx +74 -0
  12. package/registry/examples.tsx +96 -0
  13. package/registry/glossary.tsx +58 -0
  14. package/registry/imagery.tsx +39 -0
  15. package/registry/lib/adherence-types.ts +179 -0
  16. package/registry/lib/boundary.tsx +21 -0
  17. package/registry/lib/contrast.ts +44 -0
  18. package/registry/lib/cx.ts +4 -0
  19. package/registry/lib/jsx.ts +29 -0
  20. package/registry/lib/lookup.tsx +46 -0
  21. package/registry/lib/manifest-types.ts +273 -0
  22. package/registry/lib/manifest.ts +182 -0
  23. package/registry/lib/markdown.ts +283 -0
  24. package/registry/lib/status.tsx +30 -0
  25. package/registry/lib/text.tsx +27 -0
  26. package/registry/logo-usage.tsx +87 -0
  27. package/registry/pattern.tsx +68 -0
  28. package/registry/playground.tsx +180 -0
  29. package/registry/preview-frame.tsx +106 -0
  30. package/registry/props-table.tsx +110 -0
  31. package/registry/registry.generated.tsx +9 -0
  32. package/registry/rule.tsx +98 -0
  33. package/registry/scale.tsx +115 -0
  34. package/registry/shell.tsx +184 -0
  35. package/registry/standards.tsx +45 -0
  36. package/registry/swatches.tsx +146 -0
  37. package/registry/tsconfig.json +13 -0
  38. package/registry/type-ramp.tsx +105 -0
  39. package/registry/usage.tsx +72 -0
  40. package/registry/variant-matrix.tsx +105 -0
  41. package/registry/voice-terms.tsx +45 -0
@@ -0,0 +1,115 @@
1
+ import type { ReactNode } from "react";
2
+ import { shortName, tokensOfTier, withGroupDescriptions } from "./lib/manifest";
3
+ import type { TokenEntry, TokenTier, TokensManifest } from "./lib/manifest-types";
4
+
5
+ /** Steps drawn for a single `--spacing` multiplier, as Tailwind's p-1, p-2, … would use it. */
6
+ const SPACING_STEPS = [1, 2, 3, 4, 6, 8, 12, 16, 24];
7
+
8
+ /**
9
+ * Everything that is not colour or type: space, size, radius, elevation and
10
+ * motion, each tier drawn the way it is used. Hover a motion row to play it.
11
+ * Pass `tiers` to show only some, in your order.
12
+ */
13
+ export function Scale({
14
+ tokens,
15
+ tiers = ["spacing", "size", "radius", "shadow", "duration", "ease"],
16
+ }: {
17
+ tokens: TokensManifest;
18
+ tiers?: TokenTier[];
19
+ }) {
20
+ return (
21
+ <div data-slot="scale" className="flex flex-col gap-10">
22
+ {tiers.map((tier) => {
23
+ const rows = tokensOfTier(tokens, tier);
24
+ if (rows.length === 0) return null;
25
+ return (
26
+ <div key={tier} data-slot="scale-tier" className="flex flex-col gap-2">
27
+ <h3 className="text-sm font-medium capitalize">{tier}</h3>
28
+ {tier === "spacing" ? <Spacing rows={rows} /> : <Rows rows={rows} tier={tier} />}
29
+ </div>
30
+ );
31
+ })}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ function Spacing({ rows }: { rows: TokenEntry[] }) {
37
+ const multiplier = rows.find((t) => t.name === "--spacing");
38
+ const named = rows.filter((t) => t !== multiplier);
39
+ return (
40
+ <div className="flex flex-col gap-4">
41
+ {multiplier && (
42
+ <>
43
+ <p className="text-xs text-muted-foreground">
44
+ {multiplier.description ?? "One multiplier"}: <span className="font-mono">--spacing: {multiplier.values.default}</span>
45
+ </p>
46
+ <ul className="flex flex-col gap-1.5">
47
+ {SPACING_STEPS.map((n) => (
48
+ <li key={n} className="grid grid-cols-[4rem_1fr] items-center gap-3 font-mono text-xs">
49
+ <span className="text-muted-foreground">{n}</span>
50
+ <span data-slot="scale-bar" className="h-3 rounded-sm bg-primary" style={{ width: `calc(var(--spacing) * ${n})` }} />
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </>
55
+ )}
56
+ {named.length > 0 && <Rows rows={named} tier="spacing" />}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ function Rows({ rows, tier }: { rows: TokenEntry[]; tier: TokenTier }) {
62
+ return (
63
+ <ul className="flex flex-col divide-y divide-border border-y border-border">
64
+ {withGroupDescriptions(rows).map((t) => (
65
+ <li key={t.name} data-slot="scale-row" className="group flex flex-col gap-2 py-3">
66
+ {t.groupDescription && <p className="text-xs text-muted-foreground">{t.groupDescription}</p>}
67
+ <div className="grid grid-cols-1 items-center gap-3 md:grid-cols-[10rem_minmax(0,1fr)]">
68
+ <span className="flex min-w-0 flex-col font-mono text-xs text-muted-foreground">
69
+ <span className="font-medium text-canvas-foreground">{shortName(t, tier)}</span>
70
+ <span className="truncate" title={t.values.default}>
71
+ {t.values.default}
72
+ </span>
73
+ </span>
74
+ <span className="min-w-0">{sample(t, tier)}</span>
75
+ </div>
76
+ </li>
77
+ ))}
78
+ </ul>
79
+ );
80
+ }
81
+
82
+ function sample(t: TokenEntry, tier: TokenTier): ReactNode {
83
+ const v = `var(${t.name})`;
84
+ switch (tier) {
85
+ case "spacing":
86
+ return <span className="block h-3 rounded-sm bg-primary" style={{ width: v }} />;
87
+ case "size":
88
+ return <span className="block rounded-sm bg-primary" style={{ width: v, height: v }} />;
89
+ case "radius":
90
+ return <span className="block size-16 border-2 border-primary bg-primary-subdued" style={{ borderRadius: v }} />;
91
+ case "shadow":
92
+ return <span className="block h-16 w-32 rounded-md bg-surface" style={{ boxShadow: v }} />;
93
+ case "duration":
94
+ return <Motion duration={v} easing="var(--ease-out, ease-out)" />;
95
+ case "ease":
96
+ return <Motion duration="var(--duration-slow, 400ms)" easing={v} />;
97
+ default:
98
+ return <span className="font-mono text-xs">{t.values.default}</span>;
99
+ }
100
+ }
101
+
102
+ /** A dot that crosses its track on hover, with the token's duration or easing. Reduced motion jumps instead. */
103
+ function Motion({ duration, easing }: { duration: string; easing: string }) {
104
+ return (
105
+ <span data-slot="scale-motion" className="relative block h-4 w-full max-w-sm rounded-full bg-muted">
106
+ {/* the runner is the track less the dot, so translating it by its own width lands the dot at the end */}
107
+ <span
108
+ className="absolute inset-y-0 right-4 left-0 block transition-transform group-hover:translate-x-full motion-reduce:transition-none"
109
+ style={{ transitionDuration: duration, transitionTimingFunction: easing }}
110
+ >
111
+ <span className="block size-4 rounded-full bg-primary" />
112
+ </span>
113
+ </span>
114
+ );
115
+ }
@@ -0,0 +1,184 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import type { ReactNode } from "react";
6
+ import { cx } from "./lib/cx";
7
+ import { groupByCategory, slugOf } from "./lib/manifest";
8
+ import { Prose } from "./lib/text";
9
+ import type { ComponentsManifest, PatternsManifest, RulesManifest } from "./lib/manifest-types";
10
+
11
+ export interface DocsLink {
12
+ label: string;
13
+ href: string;
14
+ }
15
+
16
+ /**
17
+ * The docs frame, in the site's order: brand, foundations, rules, patterns,
18
+ * components by `@category`, standards. Editorial pages are the links you pass;
19
+ * rules, patterns and components come from the manifest. On a narrow screen
20
+ * the navigation folds into a disclosure. Put it in the docs layout; it marks
21
+ * the page you are on.
22
+ */
23
+ export function DocsShell({
24
+ manifest,
25
+ rules,
26
+ patterns,
27
+ title = "Design system",
28
+ basePath = "/design/system",
29
+ brand = [],
30
+ foundations = [],
31
+ standards = [],
32
+ children,
33
+ }: {
34
+ manifest: ComponentsManifest;
35
+ /** Listed under Rules, each linking to its anchor on `${basePath}/rules`. */
36
+ rules?: RulesManifest;
37
+ /** Listed under Patterns, each at `${basePath}/patterns/${id}`. */
38
+ patterns?: PatternsManifest;
39
+ title?: string;
40
+ /** Component pages live at `${basePath}/components/${slug}`. */
41
+ basePath?: string;
42
+ /** Brand pages: principles, design language, voice, logo, imagery. */
43
+ brand?: DocsLink[];
44
+ /** Foundations pages: colour, type, scale. */
45
+ foundations?: DocsLink[];
46
+ /** Standards pages: budgets, accessibility, adherence. */
47
+ standards?: DocsLink[];
48
+ children: ReactNode;
49
+ }) {
50
+ const current = usePathname();
51
+ const groups = [
52
+ ...(brand.length ? [{ heading: "Brand", links: brand }] : []),
53
+ ...(foundations.length ? [{ heading: "Foundations", links: foundations }] : []),
54
+ ...(rules?.rules.length
55
+ ? [{ heading: "Rules", links: [{ label: "All rules", href: `${basePath}/rules` }] }]
56
+ : []),
57
+ ...(patterns?.patterns.length
58
+ ? [{ heading: "Patterns", links: patterns.patterns.map((p) => ({ label: p.name, href: `${basePath}/patterns/${p.id}` })) }]
59
+ : []),
60
+ ...groupByCategory(manifest).map((g) => ({
61
+ heading: g.category,
62
+ links: g.components.map((c) => ({ label: c.name, href: `${basePath}/components/${slugOf(c)}` })),
63
+ })),
64
+ ...(standards.length ? [{ heading: "Standards", links: standards }] : []),
65
+ ];
66
+
67
+ const nav = (
68
+ <nav aria-label={title} data-slot="docs-nav" className="flex flex-col gap-6 p-4 text-sm">
69
+ {groups.map((group) => (
70
+ <div key={group.heading} data-slot="docs-nav-group" className="flex flex-col gap-1">
71
+ <p data-slot="docs-nav-heading" className="px-2 text-xs font-medium text-muted-foreground">
72
+ {group.heading}
73
+ </p>
74
+ <ul className="flex flex-col">
75
+ {group.links.map((link) => {
76
+ const active = current === link.href;
77
+ return (
78
+ <li key={link.href}>
79
+ <Link
80
+ href={link.href}
81
+ aria-current={active ? "page" : undefined}
82
+ data-slot="docs-nav-link"
83
+ className={cx(
84
+ "block rounded-md px-2 py-1.5 hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
85
+ active && "bg-muted font-medium",
86
+ )}
87
+ >
88
+ {link.label}
89
+ </Link>
90
+ </li>
91
+ );
92
+ })}
93
+ </ul>
94
+ </div>
95
+ ))}
96
+ </nav>
97
+ );
98
+
99
+ return (
100
+ <div data-slot="docs-shell" className="min-h-dvh bg-canvas text-canvas-foreground md:grid md:grid-cols-[16rem_1fr]">
101
+ <aside
102
+ data-slot="docs-sidebar"
103
+ className="hidden border-r border-border bg-surface text-surface-foreground md:sticky md:top-0 md:block md:h-dvh md:overflow-y-auto"
104
+ >
105
+ <Link href={basePath} data-slot="docs-title" className="block px-6 pt-6 pb-2 font-semibold">
106
+ {title}
107
+ </Link>
108
+ {nav}
109
+ </aside>
110
+ <details data-slot="docs-nav-disclosure" className="border-b border-border bg-surface text-surface-foreground md:hidden">
111
+ <summary className="cursor-pointer px-4 py-3 font-semibold">{title}</summary>
112
+ {nav}
113
+ </details>
114
+ <main data-slot="docs-main" className="min-w-0 px-4 py-8 md:px-10 md:py-12">
115
+ <div className="mx-auto flex max-w-5xl flex-col gap-12">{children}</div>
116
+ </main>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ /**
122
+ * A page's heading, lead and source, and where it comes from: a generated page
123
+ * names the version and commit it was built from; an editorial page names its
124
+ * author, when it was last reviewed, and whether it is settled. Never both, and
125
+ * never a review date on a generated page, which would be invented.
126
+ */
127
+ export function DocsHeader({
128
+ title,
129
+ lead,
130
+ source,
131
+ provenance,
132
+ editorial,
133
+ children,
134
+ }: {
135
+ title: string;
136
+ lead?: string;
137
+ /** Shown as the file the page documents. */
138
+ source?: string;
139
+ /** For a generated page: `Generated from acme-web@1.4.0 at 3f2a9c1`, from `designtools-manifest provenance`. */
140
+ provenance?: string;
141
+ /** For an editorial page. */
142
+ editorial?: { author: string; reviewed: string; status: "draft" | "published" };
143
+ children?: ReactNode;
144
+ }) {
145
+ return (
146
+ <header data-slot="docs-header" className="flex flex-col gap-3">
147
+ {(provenance || editorial) && (
148
+ <p data-slot="docs-provenance" className="text-xs text-muted-foreground">
149
+ {editorial
150
+ ? `Written by ${editorial.author} · last reviewed ${editorial.reviewed} · ${editorial.status}`
151
+ : `${provenance} · generated from the code`}
152
+ </p>
153
+ )}
154
+ <h1 className="text-3xl font-semibold">{title}</h1>
155
+ {lead && (
156
+ <div data-slot="docs-lead" className="flex max-w-prose flex-col gap-3 text-muted-foreground">
157
+ <Prose text={lead} />
158
+ </div>
159
+ )}
160
+ {source && (
161
+ <p data-slot="docs-source" className="font-mono text-xs text-muted-foreground">
162
+ {source}
163
+ </p>
164
+ )}
165
+ {children}
166
+ </header>
167
+ );
168
+ }
169
+
170
+ /** A titled section of a docs page. */
171
+ export function DocsSection({ title, lead, children }: { title: string; lead?: string; children: ReactNode }) {
172
+ const id = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
173
+ return (
174
+ <section data-slot="docs-section" aria-labelledby={id} className="flex flex-col gap-4">
175
+ <div className="flex flex-col gap-1">
176
+ <h2 id={id} className="text-xl font-semibold">
177
+ {title}
178
+ </h2>
179
+ {lead && <p className="max-w-prose text-sm text-muted-foreground">{lead}</p>}
180
+ </div>
181
+ {children}
182
+ </section>
183
+ );
184
+ }
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * The project's performance budgets, accessibility target and support baseline,
5
+ * rendered from the thresholds it already records (`.mxa/stack.json`
6
+ * `baselines`), so the docs and the checks cannot disagree.
7
+ */
8
+ export function Standards({ baselines, children }: { baselines: Record<string, unknown>; children?: ReactNode }) {
9
+ const sections = Object.entries(baselines).filter(([k]) => !k.startsWith("$"));
10
+ return (
11
+ <div data-slot="standards" className="flex flex-col gap-8">
12
+ {sections.map(([name, value]) => (
13
+ <section key={name} data-slot="standard" className="flex flex-col gap-2">
14
+ <h3 className="text-sm font-medium">{TITLES[name] ?? name}</h3>
15
+ {value && typeof value === "object" && !Array.isArray(value) ? (
16
+ <dl className="grid gap-x-6 gap-y-2 text-sm sm:grid-cols-[12rem_1fr]">
17
+ {Object.entries(value as Record<string, unknown>).map(([k, v]) => (
18
+ <div key={k} className="contents">
19
+ <dt className="font-mono text-xs">{k}</dt>
20
+ <dd className="text-muted-foreground">{show(v)}</dd>
21
+ </div>
22
+ ))}
23
+ </dl>
24
+ ) : (
25
+ <p className="text-sm text-muted-foreground">{show(value)}</p>
26
+ )}
27
+ </section>
28
+ ))}
29
+ {typeof baselines.$note === "string" && <p className="text-sm text-muted-foreground">{baselines.$note}</p>}
30
+ {children}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ const TITLES: Record<string, string> = {
36
+ a11y: "Accessibility",
37
+ performance: "Performance budgets",
38
+ support: "Support",
39
+ };
40
+
41
+ function show(v: unknown): string {
42
+ if (Array.isArray(v)) return v.length ? v.join(", ") : "none set";
43
+ if (v && typeof v === "object") return JSON.stringify(v);
44
+ return String(v);
45
+ }
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { contrastRatio, wcagGrade } from "./lib/contrast";
5
+ import { cx } from "./lib/cx";
6
+ import { ramps, semanticColours, valueIn, type Mode } from "./lib/manifest";
7
+ import type { TokensManifest } from "./lib/manifest-types";
8
+
9
+ /**
10
+ * Colour, painted from the tokens rather than restated: one row per semantic
11
+ * colour, each mode side by side. Each chip renders inside its own
12
+ * `data-theme`, so dark is shown as the dark tokens resolve, and each pair's
13
+ * contrast is measured on the colours the browser actually paints.
14
+ */
15
+ export function SemanticSwatches({
16
+ tokens,
17
+ modes = ["light", "dark"],
18
+ }: {
19
+ tokens: TokensManifest;
20
+ modes?: Mode[];
21
+ }) {
22
+ const colours = semanticColours(tokens);
23
+ return (
24
+ <div data-slot="swatches" className="overflow-x-auto">
25
+ <table className="w-full min-w-[32rem] border-collapse text-left text-sm">
26
+ <thead>
27
+ <tr className="border-b border-border text-xs text-muted-foreground">
28
+ <th scope="col" className="py-2 pr-4 font-medium">
29
+ Token
30
+ </th>
31
+ {modes.map((mode) => (
32
+ <th key={mode} scope="col" className="py-2 pr-4 font-medium capitalize">
33
+ {mode}
34
+ </th>
35
+ ))}
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ {colours.map((c) => (
40
+ <tr key={c.name} data-slot="swatch-row" className="border-b border-border align-middle">
41
+ <th scope="row" className="py-2 pr-4 font-normal">
42
+ <code className="font-mono text-xs font-medium">{c.name}</code>
43
+ {c.foreground && <span className="block font-mono text-xs text-muted-foreground">with {c.foreground.name}</span>}
44
+ </th>
45
+ {modes.map((mode) => (
46
+ <td key={mode} className="py-2 pr-4">
47
+ <Swatch
48
+ mode={mode}
49
+ name={c.name}
50
+ background={valueIn(c.background, mode)}
51
+ foreground={c.foreground && valueIn(c.foreground, mode)}
52
+ paired={!!c.foreground}
53
+ />
54
+ </td>
55
+ ))}
56
+ </tr>
57
+ ))}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /** `var(--color-primary-500)` → `primary-500`: the step it points at, which is what a person compares. */
65
+ function pointsAt(value: string | undefined): string | undefined {
66
+ const m = value && /^var\(--color-([\w-]+)\)$/.exec(value);
67
+ return m ? m[1] : value;
68
+ }
69
+
70
+ function Swatch({
71
+ mode,
72
+ name,
73
+ background,
74
+ foreground,
75
+ paired,
76
+ }: {
77
+ mode: Mode;
78
+ name: string;
79
+ background?: string;
80
+ foreground?: string;
81
+ paired: boolean;
82
+ }) {
83
+ const chip = useRef<HTMLDivElement>(null);
84
+ const [ratio, setRatio] = useState<number | null>(null);
85
+
86
+ useEffect(() => {
87
+ const node = chip.current;
88
+ if (!node || !paired) return;
89
+ const style = getComputedStyle(node);
90
+ setRatio(contrastRatio(style.color, style.backgroundColor));
91
+ }, [paired, background, foreground]);
92
+
93
+ const grade = ratio === null ? null : wcagGrade(ratio);
94
+ return (
95
+ <div data-slot="swatch" className="flex items-center gap-3">
96
+ {/* only the chip switches mode: the label stays in the page's, where it is read */}
97
+ <div
98
+ ref={chip}
99
+ data-theme={mode}
100
+ data-slot="swatch-chip"
101
+ className="flex h-10 w-14 shrink-0 items-center justify-center rounded-md border border-(--border) font-medium"
102
+ style={{ background: `var(${name})`, color: paired ? `var(${name}-foreground)` : "transparent" }}
103
+ aria-hidden
104
+ >
105
+ Aa
106
+ </div>
107
+ <div className="flex min-w-0 flex-col font-mono text-xs">
108
+ <span className="truncate" title={background}>
109
+ {pointsAt(background)}
110
+ {foreground && <span className="text-muted-foreground"> / {pointsAt(foreground)}</span>}
111
+ </span>
112
+ {grade && ratio !== null && (
113
+ <span data-slot="swatch-contrast" className={cx("text-muted-foreground", grade === "Fail" && "font-medium text-destructive")}>
114
+ {ratio.toFixed(2)}:1 · {grade}
115
+ </span>
116
+ )}
117
+ </div>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ /** The primitive ramps, one row per colour family, each step painted from its own token. */
123
+ export function RampSwatches({ tokens }: { tokens: TokensManifest }) {
124
+ return (
125
+ <div data-slot="ramps" className="flex flex-col gap-4">
126
+ {ramps(tokens).map((ramp) => (
127
+ <div key={ramp.family} data-slot="ramp" className="flex flex-col gap-1.5">
128
+ <p className="font-mono text-xs font-medium">{ramp.family}</p>
129
+ <ol className="grid grid-flow-col auto-cols-fr overflow-hidden rounded-md border border-border">
130
+ {ramp.steps.map(({ step, token }) => (
131
+ <li
132
+ key={step}
133
+ data-slot="ramp-step"
134
+ title={`${token.name}: ${token.values.default ?? ""}`}
135
+ className="flex h-14 items-end p-1.5"
136
+ style={{ background: `var(${token.name})` }}
137
+ >
138
+ <span className="rounded-sm bg-canvas/80 px-1 font-mono text-[0.625rem] text-canvas-foreground">{step}</span>
139
+ </li>
140
+ ))}
141
+ </ol>
142
+ </div>
143
+ ))}
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "types": ["react", "react-dom"],
7
+ "noEmit": true,
8
+ "declaration": false,
9
+ "declarationMap": false,
10
+ "ignoreDeprecations": "6.0"
11
+ },
12
+ "include": ["."]
13
+ }
@@ -0,0 +1,105 @@
1
+ import { shortName, tokensOfTier, typeScale, withGroupDescriptions } from "./lib/manifest";
2
+ import type { TokensManifest } from "./lib/manifest-types";
3
+
4
+ /**
5
+ * The type scale, each size set in its own token with the leading and tracking
6
+ * Tailwind pairs with it, then the families, weights, leading and tracking
7
+ * tiers on their own. Every sample reads its token; nothing is restated.
8
+ */
9
+ export function TypeRamp({
10
+ tokens,
11
+ sample = "The quick brown fox jumps over the lazy dog",
12
+ }: {
13
+ tokens: TokensManifest;
14
+ sample?: string;
15
+ }) {
16
+ const steps = withGroupDescriptions(typeScale(tokens).map((s) => ({ ...s, description: s.size.description })));
17
+ const families = tokensOfTier(tokens, "font");
18
+ const weights = tokensOfTier(tokens, "font-weight");
19
+ const leading = tokensOfTier(tokens, "leading");
20
+ const tracking = tokensOfTier(tokens, "tracking");
21
+
22
+ return (
23
+ <div data-slot="type-ramp" className="flex flex-col gap-10">
24
+ <ol data-slot="type-scale" className="flex flex-col divide-y divide-border border-y border-border">
25
+ {steps.map((step) => (
26
+ <li key={step.name} data-slot="type-step" className="flex flex-col gap-2 py-4">
27
+ {step.groupDescription && <p className="text-xs text-muted-foreground">{step.groupDescription}</p>}
28
+ <div className="grid grid-cols-1 gap-2 md:grid-cols-[10rem_minmax(0,1fr)] md:items-baseline">
29
+ <dl className="flex flex-col font-mono text-xs text-muted-foreground">
30
+ <dt className="font-medium text-canvas-foreground">{shortName(step.size, "text")}</dt>
31
+ <dd>{step.size.values.default}</dd>
32
+ {step.lineHeight && <dd>line-height {step.lineHeight.values.default}</dd>}
33
+ {step.letterSpacing && <dd>tracking {step.letterSpacing.values.default}</dd>}
34
+ </dl>
35
+ <p
36
+ className="min-w-0 truncate"
37
+ style={{
38
+ fontSize: `var(${step.name})`,
39
+ lineHeight: step.lineHeight ? `var(${step.lineHeight.name})` : undefined,
40
+ letterSpacing: step.letterSpacing ? `var(${step.letterSpacing.name})` : undefined,
41
+ fontWeight: step.fontWeight ? `var(${step.fontWeight.name})` : undefined,
42
+ }}
43
+ >
44
+ {sample}
45
+ </p>
46
+ </div>
47
+ </li>
48
+ ))}
49
+ </ol>
50
+
51
+ {families.length > 0 && (
52
+ <TokenTable title="Families" rows={families} prefix="font" render={(t) => <span style={{ fontFamily: `var(${t.name})` }}>{sample}</span>} />
53
+ )}
54
+ {weights.length > 0 && (
55
+ <TokenTable title="Weights" rows={weights} prefix="font-weight" render={(t) => <span style={{ fontWeight: `var(${t.name})` }}>{sample}</span>} />
56
+ )}
57
+ {leading.length > 0 && (
58
+ <TokenTable
59
+ title="Leading"
60
+ rows={leading}
61
+ prefix="leading"
62
+ render={(t) => (
63
+ <span className="block max-w-sm text-sm" style={{ lineHeight: `var(${t.name})` }}>
64
+ {sample}. {sample}.
65
+ </span>
66
+ )}
67
+ />
68
+ )}
69
+ {tracking.length > 0 && (
70
+ <TokenTable title="Tracking" rows={tracking} prefix="tracking" render={(t) => <span style={{ letterSpacing: `var(${t.name})` }}>{sample}</span>} />
71
+ )}
72
+ </div>
73
+ );
74
+ }
75
+
76
+ function TokenTable({
77
+ title,
78
+ rows,
79
+ prefix,
80
+ render,
81
+ }: {
82
+ title: string;
83
+ rows: TokensManifest["tokens"];
84
+ prefix: string;
85
+ render: (token: TokensManifest["tokens"][number]) => React.ReactNode;
86
+ }) {
87
+ return (
88
+ <div data-slot="type-table" className="flex flex-col gap-2">
89
+ <h3 className="text-sm font-medium">{title}</h3>
90
+ <ul className="flex flex-col divide-y divide-border border-y border-border">
91
+ {rows.map((t) => (
92
+ <li key={t.name} className="grid grid-cols-1 gap-2 py-3 md:grid-cols-[10rem_minmax(0,1fr)] md:items-baseline">
93
+ <span className="flex flex-col font-mono text-xs text-muted-foreground">
94
+ <span className="font-medium text-canvas-foreground">{shortName(t, prefix)}</span>
95
+ <span className="truncate" title={t.values.default}>
96
+ {t.values.default}
97
+ </span>
98
+ </span>
99
+ <span className="min-w-0 truncate">{render(t)}</span>
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,72 @@
1
+ import Link from "next/link";
2
+ import type { ReactNode } from "react";
3
+ import { slugOf } from "./lib/manifest";
4
+ import type { ComponentEntry, ComponentsManifest } from "./lib/manifest-types";
5
+ import { StatusBadge } from "./lib/status";
6
+ import { inline } from "./lib/text";
7
+
8
+ /**
9
+ * When to use a component, when not, and what to use instead, under its status.
10
+ * Written by people as JSDoc (`@use`, `@avoid`, `@instead`, `@status`); a
11
+ * component without them says so, rather than leaving a gap.
12
+ */
13
+ export function Usage({
14
+ entry,
15
+ manifest,
16
+ basePath = "/design/system",
17
+ }: {
18
+ entry: ComponentEntry;
19
+ /** To link `instead` names that are components. */
20
+ manifest?: ComponentsManifest;
21
+ basePath?: string;
22
+ }) {
23
+ const usage = entry.usage;
24
+ const known = new Map((manifest?.components ?? []).map((c) => [c.name, c]));
25
+ const rows: [string, ReactNode][] = [];
26
+ if (usage?.use) rows.push(["Use it for", inline(usage.use)]);
27
+ if (usage?.avoid) rows.push(["Not for", inline(usage.avoid)]);
28
+ if (usage?.instead?.length) {
29
+ rows.push([
30
+ "Instead",
31
+ usage.instead.map((name, i) => {
32
+ const c = known.get(name);
33
+ return (
34
+ <span key={name}>
35
+ {i > 0 && ", "}
36
+ {c ? (
37
+ <Link href={`${basePath}/components/${slugOf(c)}`} className="underline underline-offset-2">
38
+ {name}
39
+ </Link>
40
+ ) : (
41
+ name
42
+ )}
43
+ </span>
44
+ );
45
+ }),
46
+ ]);
47
+ }
48
+
49
+ return (
50
+ <div data-slot="usage" className="flex flex-col gap-4">
51
+ <p className="flex flex-wrap items-center gap-2 text-sm">
52
+ <StatusBadge status={entry.status} />
53
+ {entry.statusNote && <span className="text-muted-foreground">{inline(entry.statusNote)}</span>}
54
+ </p>
55
+ {rows.length ? (
56
+ <dl data-slot="usage-list" className="grid gap-x-6 gap-y-3 text-sm sm:grid-cols-[8rem_1fr]">
57
+ {rows.map(([term, detail]) => (
58
+ <div key={term} className="contents">
59
+ <dt className="font-medium">{term}</dt>
60
+ <dd className="text-muted-foreground">{detail}</dd>
61
+ </div>
62
+ ))}
63
+ </dl>
64
+ ) : (
65
+ <p className="text-sm text-muted-foreground">
66
+ No usage guidance yet. Add <code className="font-mono text-xs">@use</code>, <code className="font-mono text-xs">@avoid</code> and{" "}
67
+ <code className="font-mono text-xs">@instead</code> to its JSDoc.
68
+ </p>
69
+ )}
70
+ </div>
71
+ );
72
+ }