@designtools/blocks 0.0.0-stage → 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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/dist/cli.js +385 -0
- package/package.json +61 -3
- package/reference/README.md +20 -0
- package/reference/app/(design)/design/system/(docs)/brand/imagery/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/brand/logo/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/brand/page.tsx +16 -0
- package/reference/app/(design)/design/system/(docs)/brand/voice/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/colour/page.tsx +26 -0
- package/reference/app/(design)/design/system/(docs)/components/[slug]/page.tsx +86 -0
- package/reference/app/(design)/design/system/(docs)/layout.tsx +35 -0
- package/reference/app/(design)/design/system/(docs)/page.tsx +48 -0
- package/reference/app/(design)/design/system/(docs)/patterns/[id]/page.tsx +23 -0
- package/reference/app/(design)/design/system/(docs)/rules/page.tsx +42 -0
- package/reference/app/(design)/design/system/(docs)/scale/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/standards/page.tsx +21 -0
- package/reference/app/(design)/design/system/(docs)/type/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/vocabulary/page.tsx +15 -0
- package/reference/app/(design)/design/system/_components/copy-page.tsx +32 -0
- package/reference/app/(design)/design/system/_components/prose.tsx +26 -0
- package/reference/app/(design)/design/system/_data.ts +53 -0
- package/reference/app/(design)/design/system/manifest/[file]/route.ts +15 -0
- package/reference/app/(design)/design/system/md/[[...path]]/route.ts +94 -0
- package/reference/app/(design)/design/system/preview/[slug]/[example]/page.tsx +23 -0
- package/reference/app/(design)/layout.tsx +16 -0
- package/reference/app/globals.css +14 -0
- package/reference/app/layout.tsx +16 -0
- package/reference/designtools.json +14 -0
- package/reference/package.json +38 -0
- package/reference/proxy.ts +55 -0
- package/registry/a11y-panel.tsx +99 -0
- package/registry/adherence-summary.tsx +110 -0
- package/registry/agent-view.tsx +58 -0
- package/registry/anatomy.tsx +89 -0
- package/registry/ask-claude.tsx +73 -0
- package/registry/code-view.tsx +77 -0
- package/registry/examples.tsx +97 -0
- package/registry/glossary.tsx +59 -0
- package/registry/imagery.tsx +39 -0
- package/registry/lib/adherence-types.ts +181 -0
- package/registry/lib/boundary.tsx +21 -0
- package/registry/lib/contrast.ts +44 -0
- package/registry/lib/cx.ts +12 -0
- package/registry/lib/jsx.ts +29 -0
- package/registry/lib/lookup.tsx +46 -0
- package/registry/lib/manifest-types.ts +284 -0
- package/registry/lib/manifest.ts +228 -0
- package/registry/lib/markdown.ts +288 -0
- package/registry/lib/search.ts +99 -0
- package/registry/lib/standards.ts +82 -0
- package/registry/lib/status.tsx +32 -0
- package/registry/lib/text.tsx +27 -0
- package/registry/logo-usage.tsx +112 -0
- package/registry/pattern.tsx +69 -0
- package/registry/playground.tsx +181 -0
- package/registry/preview-frame.tsx +106 -0
- package/registry/props-table.tsx +110 -0
- package/registry/registry.generated.tsx +9 -0
- package/registry/rule.tsx +99 -0
- package/registry/scale.tsx +115 -0
- package/registry/search.tsx +134 -0
- package/registry/shell.tsx +247 -0
- package/registry/standards.tsx +41 -0
- package/registry/swatches.tsx +225 -0
- package/registry/tsconfig.json +13 -0
- package/registry/type-ramp.tsx +105 -0
- package/registry/usage.tsx +73 -0
- package/registry/variant-matrix.tsx +105 -0
- package/registry/voice-terms.tsx +45 -0
|
@@ -0,0 +1,225 @@
|
|
|
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" tabIndex={0} role="region" aria-label="Colours">
|
|
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" && "w-fit rounded-sm bg-destructive-subdued px-1 font-medium text-destructive-subdued-foreground")}>
|
|
114
|
+
{ratio.toFixed(2)}:1 · {grade}
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Each colour on each background, per mode: a label is measured on its own fill, and this is
|
|
124
|
+
* everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
|
|
125
|
+
* canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
|
|
126
|
+
* and muted text. T: fine as text (4.5:1); S: fine as a shape only (3:1); none: neither.
|
|
127
|
+
* Measured on what the browser paints, inside each mode's `data-theme`.
|
|
128
|
+
*/
|
|
129
|
+
export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: TokensManifest; modes?: Mode[] }) {
|
|
130
|
+
const names = new Set(semanticColours(tokens).map((c) => c.name));
|
|
131
|
+
const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
|
|
132
|
+
const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
|
|
133
|
+
return (
|
|
134
|
+
<div data-slot="page-contrast" className="flex flex-col gap-6">
|
|
135
|
+
{modes.map((mode) => (
|
|
136
|
+
<PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; grounds: string[] }) {
|
|
143
|
+
const probe = useRef<HTMLDivElement>(null);
|
|
144
|
+
const [ratios, setRatios] = useState<Record<string, number | null>>({});
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const node = probe.current;
|
|
147
|
+
if (!node) return;
|
|
148
|
+
const paint = (v: string) => {
|
|
149
|
+
node.style.color = `var(${v})`;
|
|
150
|
+
return getComputedStyle(node).color;
|
|
151
|
+
};
|
|
152
|
+
const painted = Object.fromEntries([...colours, ...grounds].map((v) => [v, paint(v)]));
|
|
153
|
+
const out: Record<string, number | null> = {};
|
|
154
|
+
for (const c of colours) for (const g of grounds) out[`${c}|${g}`] = contrastRatio(painted[c], painted[g]);
|
|
155
|
+
setRatios(out);
|
|
156
|
+
}, [colours, grounds]);
|
|
157
|
+
return (
|
|
158
|
+
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
|
|
159
|
+
<div ref={probe} data-theme={mode} hidden />
|
|
160
|
+
<table className="w-full min-w-[32rem] border-collapse text-left text-sm">
|
|
161
|
+
<caption className="pb-2 text-left text-sm font-medium capitalize">{mode}</caption>
|
|
162
|
+
<thead>
|
|
163
|
+
<tr className="border-b border-border text-xs text-muted-foreground">
|
|
164
|
+
<th scope="col" className="py-2 pr-4 font-medium">Colour</th>
|
|
165
|
+
{grounds.map((g) => (
|
|
166
|
+
<th key={g} scope="col" className="py-2 pr-4 font-mono font-medium">{g.slice(2)}</th>
|
|
167
|
+
))}
|
|
168
|
+
</tr>
|
|
169
|
+
</thead>
|
|
170
|
+
<tbody>
|
|
171
|
+
{colours.map((c) => (
|
|
172
|
+
<tr key={c} className="border-b border-border">
|
|
173
|
+
<th scope="row" className="py-2 pr-4 font-mono text-xs font-medium">{c.slice(2)}</th>
|
|
174
|
+
{grounds.map((g) => {
|
|
175
|
+
const r = ratios[`${c}|${g}`];
|
|
176
|
+
const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
|
|
177
|
+
return (
|
|
178
|
+
<td key={g} className="py-2 pr-4">
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
{/* only the sample takes the mode; the number is read on the page */}
|
|
181
|
+
<span data-theme={mode} className="flex h-9 w-12 items-center justify-center rounded-md border border-(--border) text-base font-bold" style={{ background: `var(${g})`, color: `var(${c})` }} aria-hidden>
|
|
182
|
+
Aa
|
|
183
|
+
</span>
|
|
184
|
+
{r != null && (
|
|
185
|
+
<span className={cx("font-mono text-xs", mark === "none" && "rounded-sm bg-destructive-subdued px-1 text-destructive-subdued-foreground")}>
|
|
186
|
+
{r.toFixed(1)} {mark}
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</td>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
193
|
+
</tr>
|
|
194
|
+
))}
|
|
195
|
+
</tbody>
|
|
196
|
+
</table>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The primitive ramps, one row per colour family, each step painted from its own token. */
|
|
202
|
+
export function RampSwatches({ tokens }: { tokens: TokensManifest }) {
|
|
203
|
+
return (
|
|
204
|
+
<div data-slot="ramps" className="flex flex-col gap-4">
|
|
205
|
+
{ramps(tokens).map((ramp) => (
|
|
206
|
+
<div key={ramp.family} data-slot="ramp" className="flex flex-col gap-1.5">
|
|
207
|
+
<p className="font-mono text-xs font-medium">{ramp.family}</p>
|
|
208
|
+
<ol className="grid grid-flow-col auto-cols-fr overflow-hidden rounded-md border border-border">
|
|
209
|
+
{ramp.steps.map(({ step, token }) => (
|
|
210
|
+
<li
|
|
211
|
+
key={step}
|
|
212
|
+
data-slot="ramp-step"
|
|
213
|
+
title={`${token.name}: ${token.values.default ?? ""}`}
|
|
214
|
+
className="flex h-14 items-end p-1.5"
|
|
215
|
+
style={{ background: `var(${token.name})` }}
|
|
216
|
+
>
|
|
217
|
+
<span className="rounded-sm bg-canvas/80 px-1 font-mono text-xs text-canvas-foreground">{step}</span>
|
|
218
|
+
</li>
|
|
219
|
+
))}
|
|
220
|
+
</ol>
|
|
221
|
+
</div>
|
|
222
|
+
))}
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -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,73 @@
|
|
|
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
|
+
import { target } from "./lib/cx";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* When to use a component, when not, and what to use instead, under its status.
|
|
11
|
+
* Written by people as JSDoc (`@use`, `@avoid`, `@instead`, `@status`); a
|
|
12
|
+
* component without them says so, rather than leaving a gap.
|
|
13
|
+
*/
|
|
14
|
+
export function Usage({
|
|
15
|
+
entry,
|
|
16
|
+
manifest,
|
|
17
|
+
basePath = "/design/system",
|
|
18
|
+
}: {
|
|
19
|
+
entry: ComponentEntry;
|
|
20
|
+
/** To link `instead` names that are components. */
|
|
21
|
+
manifest?: ComponentsManifest;
|
|
22
|
+
basePath?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const usage = entry.usage;
|
|
25
|
+
const known = new Map((manifest?.components ?? []).map((c) => [c.name, c]));
|
|
26
|
+
const rows: [string, ReactNode][] = [];
|
|
27
|
+
if (usage?.use) rows.push(["Use it for", inline(usage.use)]);
|
|
28
|
+
if (usage?.avoid) rows.push(["Not for", inline(usage.avoid)]);
|
|
29
|
+
if (usage?.instead?.length) {
|
|
30
|
+
rows.push([
|
|
31
|
+
"Instead",
|
|
32
|
+
usage.instead.map((name, i) => {
|
|
33
|
+
const c = known.get(name);
|
|
34
|
+
return (
|
|
35
|
+
<span key={name}>
|
|
36
|
+
{i > 0 && ", "}
|
|
37
|
+
{c ? (
|
|
38
|
+
<Link href={`${basePath}/components/${slugOf(c)}`} className={`${target} inline-flex items-center underline underline-offset-2`}>
|
|
39
|
+
{name}
|
|
40
|
+
</Link>
|
|
41
|
+
) : (
|
|
42
|
+
name
|
|
43
|
+
)}
|
|
44
|
+
</span>
|
|
45
|
+
);
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div data-slot="usage" className="flex flex-col gap-4">
|
|
52
|
+
<p className="flex flex-wrap items-center gap-2 text-sm">
|
|
53
|
+
<StatusBadge status={entry.status} />
|
|
54
|
+
{entry.statusNote && <span className="text-muted-foreground">{inline(entry.statusNote)}</span>}
|
|
55
|
+
</p>
|
|
56
|
+
{rows.length ? (
|
|
57
|
+
<dl data-slot="usage-list" className="grid gap-x-6 gap-y-3 text-sm sm:grid-cols-[8rem_1fr]">
|
|
58
|
+
{rows.map(([term, detail]) => (
|
|
59
|
+
<div key={term} className="contents">
|
|
60
|
+
<dt className="font-medium">{term}</dt>
|
|
61
|
+
<dd className="text-muted-foreground">{detail}</dd>
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</dl>
|
|
65
|
+
) : (
|
|
66
|
+
<p className="text-sm text-muted-foreground">
|
|
67
|
+
No usage guidance yet. Add <code className="font-mono text-xs">@use</code>, <code className="font-mono text-xs">@avoid</code> and{" "}
|
|
68
|
+
<code className="font-mono text-xs">@instead</code> to its JSDoc.
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { coerce, componentFor, Missing } from "./lib/lookup";
|
|
5
|
+
import type { ComponentEntry, VariantAxis } from "./lib/manifest-types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every variant, rendered live. The first two axes form a grid; each further
|
|
9
|
+
* axis gets its own row, with the others at their defaults. Pass `props` for
|
|
10
|
+
* anything the component needs to render (an `options` list, a `value`).
|
|
11
|
+
*/
|
|
12
|
+
export function VariantMatrix({
|
|
13
|
+
entry,
|
|
14
|
+
props = {},
|
|
15
|
+
children,
|
|
16
|
+
axes: only,
|
|
17
|
+
}: {
|
|
18
|
+
entry: ComponentEntry;
|
|
19
|
+
props?: Record<string, unknown>;
|
|
20
|
+
/** What to render inside each instance. Defaults to the component's name. */
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
/** Limit and order the axes shown. */
|
|
23
|
+
axes?: string[];
|
|
24
|
+
}) {
|
|
25
|
+
const Component = componentFor(entry);
|
|
26
|
+
const all = entry.variants?.axes ?? [];
|
|
27
|
+
const axes = only ? only.map((n) => all.find((a) => a.name === n)).filter((a): a is VariantAxis => !!a) : all;
|
|
28
|
+
if (!Component) return <Missing name={entry.name} />;
|
|
29
|
+
if (axes.length === 0) return <p className="text-sm text-muted-foreground">{entry.name} has no variants.</p>;
|
|
30
|
+
|
|
31
|
+
const content = children ?? entry.name;
|
|
32
|
+
const render = (extra: Record<string, string>) => (
|
|
33
|
+
<Component {...props} {...coerce(extra)}>
|
|
34
|
+
{content}
|
|
35
|
+
</Component>
|
|
36
|
+
);
|
|
37
|
+
const [rows, cols, ...rest] = axes;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div data-slot="variant-matrix" className="flex flex-col gap-8">
|
|
41
|
+
{cols ? (
|
|
42
|
+
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="Variants">
|
|
43
|
+
<table className="border-collapse text-sm">
|
|
44
|
+
<caption className="sr-only">
|
|
45
|
+
{entry.name}: {rows.name} by {cols.name}
|
|
46
|
+
</caption>
|
|
47
|
+
<thead>
|
|
48
|
+
<tr>
|
|
49
|
+
<th scope="col" className="p-2 text-left text-xs font-normal text-muted-foreground">
|
|
50
|
+
<span className="font-mono">{rows.name}</span> \ <span className="font-mono">{cols.name}</span>
|
|
51
|
+
</th>
|
|
52
|
+
{cols.options.map((c) => (
|
|
53
|
+
<th key={c.name} scope="col" className="p-2 text-left font-mono text-xs font-medium">
|
|
54
|
+
{label(cols, c.name)}
|
|
55
|
+
</th>
|
|
56
|
+
))}
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
{rows.options.map((r) => (
|
|
61
|
+
<tr key={r.name}>
|
|
62
|
+
<th scope="row" className="p-2 text-left font-mono text-xs font-medium">
|
|
63
|
+
{label(rows, r.name)}
|
|
64
|
+
</th>
|
|
65
|
+
{cols.options.map((c) => (
|
|
66
|
+
<td key={c.name} data-slot="variant-cell" className="p-2 align-middle">
|
|
67
|
+
{render({ [rows.name]: r.name, [cols.name]: c.name })}
|
|
68
|
+
</td>
|
|
69
|
+
))}
|
|
70
|
+
</tr>
|
|
71
|
+
))}
|
|
72
|
+
</tbody>
|
|
73
|
+
</table>
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
<AxisRow axis={rows} render={render} />
|
|
77
|
+
)}
|
|
78
|
+
{rest.map((axis) => (
|
|
79
|
+
<AxisRow key={axis.name} axis={axis} render={render} />
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function AxisRow({ axis, render }: { axis: VariantAxis; render: (extra: Record<string, string>) => ReactNode }) {
|
|
86
|
+
return (
|
|
87
|
+
<div data-slot="variant-axis" className="flex flex-col gap-2">
|
|
88
|
+
<p className="font-mono text-xs text-muted-foreground">{axis.name}</p>
|
|
89
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
90
|
+
{axis.options.map((o) => (
|
|
91
|
+
<figure key={o.name} className="flex flex-col items-start gap-1.5">
|
|
92
|
+
{render({ [axis.name]: o.name })}
|
|
93
|
+
<figcaption className="font-mono text-xs text-muted-foreground" title={o.description}>
|
|
94
|
+
{label(axis, o.name)}
|
|
95
|
+
</figcaption>
|
|
96
|
+
</figure>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function label(axis: VariantAxis, option: string): string {
|
|
104
|
+
return axis.default === option ? `${option} (default)` : option;
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TermEntry } from "./lib/manifest-types";
|
|
2
|
+
import { inline } from "./lib/text";
|
|
3
|
+
|
|
4
|
+
/** The words to use, beside the ones not to: product terms first, then the system's names that have wrong forms. */
|
|
5
|
+
export function VoiceTerms({ terms }: { terms: TermEntry[] }) {
|
|
6
|
+
const shown = [...terms.filter((t) => t.kind === "product"), ...terms.filter((t) => t.kind !== "product" && t.wrong?.length)];
|
|
7
|
+
if (shown.length === 0) return <p className="text-sm text-muted-foreground">No terms yet. Add them to the taxonomy file.</p>;
|
|
8
|
+
return (
|
|
9
|
+
<div data-slot="voice-terms" className="overflow-x-auto" tabIndex={0} role="region" aria-label="Terms">
|
|
10
|
+
<table className="w-full min-w-[28rem] border-collapse text-left text-sm">
|
|
11
|
+
<thead>
|
|
12
|
+
<tr className="border-b border-border text-xs text-muted-foreground">
|
|
13
|
+
<th scope="col" className="py-2 pr-4 font-medium">
|
|
14
|
+
Say
|
|
15
|
+
</th>
|
|
16
|
+
<th scope="col" className="py-2 pr-4 font-medium">
|
|
17
|
+
Not
|
|
18
|
+
</th>
|
|
19
|
+
<th scope="col" className="py-2 font-medium">
|
|
20
|
+
Why
|
|
21
|
+
</th>
|
|
22
|
+
</tr>
|
|
23
|
+
</thead>
|
|
24
|
+
<tbody>
|
|
25
|
+
{shown.map((t) => (
|
|
26
|
+
<tr key={`${t.kind}:${t.name}`} className="border-b border-border align-top">
|
|
27
|
+
<th scope="row" className="py-2 pr-4 font-medium">
|
|
28
|
+
{t.name}
|
|
29
|
+
</th>
|
|
30
|
+
<td className="py-2 pr-4 text-muted-foreground">
|
|
31
|
+
{(t.wrong ?? []).map((w, i) => (
|
|
32
|
+
<span key={w}>
|
|
33
|
+
{i > 0 && ", "}
|
|
34
|
+
<s>{w}</s>
|
|
35
|
+
</span>
|
|
36
|
+
))}
|
|
37
|
+
</td>
|
|
38
|
+
<td className="py-2 text-muted-foreground">{t.note ? inline(t.note) : t.kind === "product" ? "" : `The ${t.kind}'s name in the code.`}</td>
|
|
39
|
+
</tr>
|
|
40
|
+
))}
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|