@designtools/docs 0.1.0 → 0.2.1
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 +16 -0
- package/README.md +24 -2
- package/dist/anatomy.js +2 -1
- package/dist/cli.js +1 -1
- package/dist/glossary.d.ts +1 -1
- package/dist/glossary.js +13 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/live.d.ts +10 -0
- package/dist/lib/live.js +10 -0
- package/dist/lib/lookup.js +3 -2
- package/dist/lib/status.d.ts +2 -3
- package/dist/lib/terms.d.ts +15 -0
- package/dist/lib/terms.js +20 -0
- package/dist/next/config.d.ts +24 -4
- package/dist/next/index.d.ts +3 -0
- package/dist/next/index.js +3 -0
- package/dist/next/markdown.js +1 -1
- package/dist/next/pages.js +82 -16
- package/dist/next/prose.d.ts +2 -2
- package/dist/next/prose.js +6 -4
- package/dist/pattern.d.ts +4 -1
- package/dist/pattern.js +11 -8
- package/dist/playground.js +1 -1
- package/dist/rule.d.ts +9 -5
- package/dist/rule.js +14 -9
- package/dist/shell.d.ts +4 -1
- package/dist/shell.js +13 -11
- package/dist/styles.css +961 -754
- package/dist/swatches.d.ts +7 -6
- package/dist/swatches.js +50 -16
- package/dist/variant-matrix.js +11 -6
- package/dist/voice-terms.js +6 -5
- package/dist/workbench.js +2 -1
- package/package.json +1 -1
- package/src/anatomy.tsx +6 -1
- package/src/glossary.tsx +39 -40
- package/src/index.ts +2 -1
- package/src/lib/live.tsx +15 -0
- package/src/lib/lookup.tsx +3 -2
- package/src/lib/status.tsx +3 -3
- package/src/lib/terms.tsx +53 -0
- package/src/next/config.ts +25 -4
- package/src/next/index.tsx +3 -0
- package/src/next/markdown.ts +1 -1
- package/src/next/pages.tsx +172 -59
- package/src/next/prose.tsx +16 -6
- package/src/pattern.tsx +42 -37
- package/src/playground.tsx +7 -2
- package/src/rule.tsx +108 -59
- package/src/shell.tsx +30 -16
- package/src/styles.css +4 -2
- package/src/swatches.tsx +189 -120
- package/src/variant-matrix.tsx +63 -80
- package/src/voice-terms.tsx +20 -29
- package/src/workbench.tsx +6 -3
package/src/swatches.tsx
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Check, Square, X } from "lucide-react";
|
|
4
|
+
import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
|
|
4
5
|
import { contrastRatio, wcagGrade } from "./lib/contrast";
|
|
5
6
|
import { cx } from "./lib/cx";
|
|
6
7
|
import { ramps, semanticColours, valueIn, type Mode } from "./lib/manifest";
|
|
7
8
|
import type { TokensManifest } from "./lib/manifest-types";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Colour, painted from the tokens rather than restated.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* every pair's contrast is measured on the colours
|
|
11
|
+
* Colour, painted from the tokens rather than restated. No panels and no cards around the samples:
|
|
12
|
+
* each sample is its own colour, with one hairline round it, set on the page in a light and a dark
|
|
13
|
+
* column. Each colour with a subdued pair shows its fill large, then subdued, hover and press; then
|
|
14
|
+
* the surfaces and the utility colours, one row each. Every sample sits inside its own `data-theme`,
|
|
15
|
+
* so dark is shown as the dark tokens resolve, and every pair's contrast is measured on the colours
|
|
16
|
+
* the browser actually paints.
|
|
15
17
|
*/
|
|
16
18
|
export function SemanticSwatches({
|
|
17
19
|
tokens,
|
|
@@ -30,78 +32,111 @@ export function SemanticSwatches({
|
|
|
30
32
|
const token = byName.get(name)?.background;
|
|
31
33
|
return token && pointsAt(valueIn(token, mode));
|
|
32
34
|
};
|
|
35
|
+
const columns = cx("grid gap-x-8 gap-y-6", modes.length > 1 && "md:grid-cols-2");
|
|
36
|
+
const modeLabel = (mode: Mode) => <p className="text-xs font-medium text-muted-foreground capitalize">{mode}</p>;
|
|
37
|
+
const title = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
33
38
|
|
|
34
39
|
return (
|
|
35
|
-
<div data-slot="swatches" className="flex flex-col gap-
|
|
36
|
-
<div className="
|
|
40
|
+
<div data-slot="swatches" className="flex flex-col gap-16">
|
|
41
|
+
<div className="flex flex-col gap-14">
|
|
37
42
|
{families.map((f) => (
|
|
38
|
-
<
|
|
39
|
-
<header className="flex items-baseline
|
|
40
|
-
<h3 className="text-
|
|
43
|
+
<section key={f} data-slot="swatch-family" className="flex flex-col gap-5">
|
|
44
|
+
<header className="flex items-baseline gap-3">
|
|
45
|
+
<h3 className="text-2xl leading-tight tracking-tight">{title(f.slice(2).replace(/-/g, " "))}</h3>
|
|
41
46
|
<code className="font-mono text-xs text-muted-foreground">{f}</code>
|
|
42
47
|
</header>
|
|
43
|
-
<div className={
|
|
48
|
+
<div className={columns}>
|
|
44
49
|
{modes.map((mode) => (
|
|
45
|
-
<div key={mode}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<div className="
|
|
49
|
-
<Chip name={
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
<div key={mode} className="flex flex-col gap-2.5">
|
|
51
|
+
{modeLabel(mode)}
|
|
52
|
+
{/* only the samples take the mode; the label is read on the page */}
|
|
53
|
+
<div data-theme={mode} className="flex flex-col gap-2.5">
|
|
54
|
+
<Chip name={f} label="fill" step={step(f, mode)} size="lg" />
|
|
55
|
+
<div className="grid grid-cols-3 gap-2.5">
|
|
56
|
+
<Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
|
|
57
|
+
{byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
|
|
58
|
+
{byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
|
|
59
|
+
</div>
|
|
52
60
|
</div>
|
|
53
61
|
</div>
|
|
54
62
|
))}
|
|
55
63
|
</div>
|
|
56
|
-
</
|
|
64
|
+
</section>
|
|
57
65
|
))}
|
|
58
66
|
</div>
|
|
59
67
|
|
|
60
68
|
{surfaces.length > 0 && (
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
|
68
|
-
{surfaces.map((name) => (
|
|
69
|
-
<Chip key={name} name={name} label={name.slice(2)} step={step(name, mode)} outlined />
|
|
70
|
-
))}
|
|
69
|
+
<Rows title="Surfaces" lead="The page, panels and popups, and the brand's own backgrounds, each with its text." modes={modes}>
|
|
70
|
+
{surfaces.map((name) => (
|
|
71
|
+
<Row key={name} name={name.slice(2)} modes={modes}>
|
|
72
|
+
{(mode) => (
|
|
73
|
+
<div data-theme={mode}>
|
|
74
|
+
<Chip name={name} label={step(name, mode) ?? ""} size="wide" />
|
|
71
75
|
</div>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</
|
|
76
|
+
)}
|
|
77
|
+
</Row>
|
|
78
|
+
))}
|
|
79
|
+
</Rows>
|
|
76
80
|
)}
|
|
77
81
|
|
|
78
82
|
{utility.length > 0 && (
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<span className="truncate text-muted-foreground">{step(name, mode)}</span>
|
|
92
|
-
</span>
|
|
93
|
-
</li>
|
|
94
|
-
))}
|
|
95
|
-
</ul>
|
|
96
|
-
</div>
|
|
97
|
-
))}
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
83
|
+
<Rows title="Lines, focus and shadow" lead="Borders, input edges, the focus ring and the shadow colour." modes={modes}>
|
|
84
|
+
{utility.map((name) => (
|
|
85
|
+
<Row key={name} name={name.slice(2)} modes={modes}>
|
|
86
|
+
{(mode) => (
|
|
87
|
+
<span className="flex h-14 items-center gap-3">
|
|
88
|
+
<span data-theme={mode} aria-hidden className="size-10 shrink-0 rounded-lg ring-1 ring-(--canvas-foreground)/15 ring-inset" style={{ background: `var(${name})` }} />
|
|
89
|
+
<span className="font-mono text-xs text-muted-foreground">{step(name, mode)}</span>
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</Row>
|
|
93
|
+
))}
|
|
94
|
+
</Rows>
|
|
100
95
|
)}
|
|
101
96
|
</div>
|
|
102
97
|
);
|
|
103
98
|
}
|
|
104
99
|
|
|
100
|
+
/** A titled list of rows, with the mode names once over the columns. */
|
|
101
|
+
function Rows({ title, lead, modes, children }: { title: string; lead: string; modes: Mode[]; children: ReactNode }) {
|
|
102
|
+
return (
|
|
103
|
+
<section className="flex flex-col gap-5">
|
|
104
|
+
<header className="flex flex-col gap-1.5">
|
|
105
|
+
<h3 className="text-2xl leading-tight tracking-tight">{title}</h3>
|
|
106
|
+
<p className="text-sm text-muted-foreground">{lead}</p>
|
|
107
|
+
</header>
|
|
108
|
+
<div>
|
|
109
|
+
<div aria-hidden className="hidden gap-x-8 pb-3 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]" style={{ "--modes": modes.length } as CSSProperties}>
|
|
110
|
+
<span />
|
|
111
|
+
{modes.map((m) => (
|
|
112
|
+
<span key={m} className="capitalize">
|
|
113
|
+
{m}
|
|
114
|
+
</span>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
{children}
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Row({ name, modes, children }: { name: string; modes: Mode[]; children: (mode: Mode) => ReactNode }) {
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
className="grid items-center gap-x-8 gap-y-2 border-t border-border py-3 md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]"
|
|
127
|
+
style={{ "--modes": modes.length } as CSSProperties}
|
|
128
|
+
>
|
|
129
|
+
<code className="font-mono text-sm">{name}</code>
|
|
130
|
+
{modes.map((mode) => (
|
|
131
|
+
<div key={mode} className="flex flex-col gap-1">
|
|
132
|
+
<span className="text-xs text-muted-foreground capitalize md:hidden">{mode}</span>
|
|
133
|
+
{children(mode)}
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
/** `var(--color-primary-500)` → `primary-500`: the step it points at, which is what a person compares. */
|
|
106
141
|
function pointsAt(value: string | undefined): string | undefined {
|
|
107
142
|
const m = value && /^var\(--color-([\w-]+)\)$/.exec(value);
|
|
@@ -110,9 +145,9 @@ function pointsAt(value: string | undefined): string | undefined {
|
|
|
110
145
|
}
|
|
111
146
|
|
|
112
147
|
/**
|
|
113
|
-
* One background with its foreground on it, labelled in that foreground, with the
|
|
114
|
-
*
|
|
115
|
-
*
|
|
148
|
+
* One background with its foreground on it, labelled in that foreground, with the contrast measured on
|
|
149
|
+
* what the browser paints. It reads its mode from the nearest `data-theme` around it. One hairline round
|
|
150
|
+
* it, so a white sample shows on a white page, and nothing else.
|
|
116
151
|
*/
|
|
117
152
|
function Chip({
|
|
118
153
|
name,
|
|
@@ -120,14 +155,12 @@ function Chip({
|
|
|
120
155
|
label,
|
|
121
156
|
step,
|
|
122
157
|
size = "md",
|
|
123
|
-
outlined,
|
|
124
158
|
}: {
|
|
125
159
|
name: string;
|
|
126
160
|
foreground?: string;
|
|
127
161
|
label: string;
|
|
128
162
|
step?: string;
|
|
129
|
-
size?: "md" | "lg";
|
|
130
|
-
outlined?: boolean;
|
|
163
|
+
size?: "md" | "lg" | "wide";
|
|
131
164
|
}) {
|
|
132
165
|
const chip = useRef<HTMLDivElement>(null);
|
|
133
166
|
const [ratio, setRatio] = useState<number | null>(null);
|
|
@@ -140,40 +173,45 @@ function Chip({
|
|
|
140
173
|
const grade = ratio === null ? null : wcagGrade(ratio);
|
|
141
174
|
const fails = grade === "Fail";
|
|
142
175
|
const measured = grade && ratio !== null ? `${ratio.toFixed(1)}:1 ${grade}` : undefined;
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{measured}
|
|
160
|
-
</span>
|
|
161
|
-
)}
|
|
176
|
+
const contrast = measured && (
|
|
177
|
+
<span data-slot="swatch-contrast" className={cx("font-mono text-xs whitespace-nowrap", fails ? "rounded-sm bg-destructive px-1.5 py-0.5 font-medium text-destructive-foreground" : "opacity-80")}>
|
|
178
|
+
{measured}
|
|
179
|
+
</span>
|
|
180
|
+
);
|
|
181
|
+
const paint = { background: `var(${name})`, color: `var(${foreground})` };
|
|
182
|
+
// a hairline in the sample's own text colour: it shows a white sample on a white page and disappears into a strong fill
|
|
183
|
+
const base = "min-w-0 rounded-xl ring-1 ring-current/15 ring-inset";
|
|
184
|
+
const tip = `${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`;
|
|
185
|
+
|
|
186
|
+
if (size === "wide")
|
|
187
|
+
return (
|
|
188
|
+
<div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex h-14 items-center justify-between gap-4 px-4")} style={paint}>
|
|
189
|
+
<span className="flex min-w-0 items-baseline gap-3">
|
|
190
|
+
<span className="text-xl leading-none font-medium">Aa</span>
|
|
191
|
+
<span className="font-mono text-xs opacity-80">{label}</span>
|
|
162
192
|
</span>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
193
|
+
{contrast}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
if (size === "lg")
|
|
197
|
+
return (
|
|
198
|
+
<div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex min-h-36 flex-col justify-between gap-6 p-5")} style={paint}>
|
|
199
|
+
<span className="flex items-start justify-between gap-3">
|
|
200
|
+
<span className="text-5xl leading-none tracking-tight">Aa</span>
|
|
201
|
+
{contrast}
|
|
202
|
+
</span>
|
|
203
|
+
<span className="flex flex-col gap-0.5 text-sm leading-snug">
|
|
204
|
+
<span className="font-medium">{label}</span>
|
|
205
|
+
{step && <span className="font-mono text-xs opacity-80">{step}</span>}
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
return (
|
|
210
|
+
<div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex min-h-28 flex-col justify-between gap-4 p-4")} style={paint}>
|
|
211
|
+
<span className="text-2xl leading-none font-medium">Aa</span>
|
|
212
|
+
<span className="flex flex-col gap-1 leading-snug">
|
|
213
|
+
<span className="text-sm font-medium">{label}</span>
|
|
214
|
+
{contrast}
|
|
177
215
|
</span>
|
|
178
216
|
</div>
|
|
179
217
|
);
|
|
@@ -183,7 +221,7 @@ function Chip({
|
|
|
183
221
|
* Each colour on each background, per mode: a label is measured on its own fill, and this is
|
|
184
222
|
* everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
|
|
185
223
|
* canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
|
|
186
|
-
* and muted text.
|
|
224
|
+
* and muted text. Text: fine as text (4.5:1); shape: fine as a shape only (3:1); none: neither.
|
|
187
225
|
* Measured on what the browser paints, inside each mode's `data-theme`.
|
|
188
226
|
*/
|
|
189
227
|
export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: TokensManifest; modes?: Mode[] }) {
|
|
@@ -191,7 +229,21 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
|
|
|
191
229
|
const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
|
|
192
230
|
const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
|
|
193
231
|
return (
|
|
194
|
-
<div data-slot="page-contrast" className="flex flex-col gap-
|
|
232
|
+
<div data-slot="page-contrast" className="flex flex-col gap-14">
|
|
233
|
+
<p className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
|
|
234
|
+
{(
|
|
235
|
+
[
|
|
236
|
+
["text", "fine as text, 4.5:1"],
|
|
237
|
+
["shape", "fine as a shape only, 3:1"],
|
|
238
|
+
["none", "neither"],
|
|
239
|
+
] as const
|
|
240
|
+
).map(([mark, words]) => (
|
|
241
|
+
<span key={mark} className="inline-flex items-center gap-2">
|
|
242
|
+
<Verdict mark={mark} />
|
|
243
|
+
{words}
|
|
244
|
+
</span>
|
|
245
|
+
))}
|
|
246
|
+
</p>
|
|
195
247
|
{modes.map((mode) => (
|
|
196
248
|
<PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
|
|
197
249
|
))}
|
|
@@ -199,6 +251,23 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
|
|
|
199
251
|
);
|
|
200
252
|
}
|
|
201
253
|
|
|
254
|
+
const VERDICT = {
|
|
255
|
+
text: { Icon: Check, className: "text-success-subdued-foreground", label: "text" },
|
|
256
|
+
shape: { Icon: Square, className: "text-muted-foreground", label: "shape" },
|
|
257
|
+
none: { Icon: X, className: "text-destructive-subdued-foreground", label: "none" },
|
|
258
|
+
} as const;
|
|
259
|
+
|
|
260
|
+
function Verdict({ mark, ratio }: { mark: keyof typeof VERDICT; ratio?: number }) {
|
|
261
|
+
const { Icon, className, label } = VERDICT[mark];
|
|
262
|
+
return (
|
|
263
|
+
<span className="inline-flex items-center gap-1.5 whitespace-nowrap">
|
|
264
|
+
<Icon aria-hidden className={cx("size-3.5 shrink-0", className)} strokeWidth={2.5} />
|
|
265
|
+
{ratio != null && <span className="font-mono text-xs text-canvas-foreground tabular-nums">{ratio.toFixed(1)}</span>}
|
|
266
|
+
<span className={cx(ratio != null && "text-xs text-muted-foreground")}>{ratio != null ? label : null}</span>
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
202
271
|
function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; grounds: string[] }) {
|
|
203
272
|
const probe = useRef<HTMLDivElement>(null);
|
|
204
273
|
const [ratios, setRatios] = useState<Record<string, number | null>>({});
|
|
@@ -215,44 +284,44 @@ function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; g
|
|
|
215
284
|
setRatios(out);
|
|
216
285
|
}, [colours, grounds]);
|
|
217
286
|
return (
|
|
218
|
-
<div className="overflow-x-auto
|
|
287
|
+
<div className="-mx-1 overflow-x-auto px-1 pb-2" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
|
|
219
288
|
<div ref={probe} data-theme={mode} hidden />
|
|
220
|
-
<table className="w-full min-w-[
|
|
221
|
-
<caption className="
|
|
289
|
+
<table className="w-full min-w-[46rem] table-fixed border-collapse text-left text-sm">
|
|
290
|
+
<caption className="pb-5 text-left text-lg font-medium capitalize">{mode}</caption>
|
|
222
291
|
<thead>
|
|
223
|
-
<tr className="
|
|
224
|
-
<th scope="col" className="
|
|
292
|
+
<tr className="text-xs text-muted-foreground">
|
|
293
|
+
<th scope="col" className="w-32 pr-4 pb-3 font-medium">
|
|
294
|
+
Colour
|
|
295
|
+
</th>
|
|
225
296
|
{grounds.map((g) => (
|
|
226
|
-
<th key={g} scope="col" className="px-
|
|
297
|
+
<th key={g} scope="col" className="px-2 pb-3 font-mono font-normal">
|
|
298
|
+
{g.slice(2)}
|
|
299
|
+
</th>
|
|
227
300
|
))}
|
|
228
301
|
</tr>
|
|
229
302
|
</thead>
|
|
230
303
|
<tbody>
|
|
231
304
|
{colours.map((c) => (
|
|
232
|
-
<tr key={c} className="border-
|
|
233
|
-
<th scope="row" className="
|
|
305
|
+
<tr key={c} className="border-t border-border">
|
|
306
|
+
<th scope="row" className="py-4 pr-4 align-top font-mono text-xs font-medium break-words">
|
|
307
|
+
{c.slice(2)}
|
|
308
|
+
</th>
|
|
234
309
|
{grounds.map((g) => {
|
|
235
310
|
const r = ratios[`${c}|${g}`];
|
|
236
|
-
const mark = r == null ?
|
|
311
|
+
const mark = r == null ? undefined : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
|
|
237
312
|
return (
|
|
238
|
-
<td key={g} className="px-
|
|
239
|
-
<div className="flex
|
|
313
|
+
<td key={g} className="px-2 py-4 align-top">
|
|
314
|
+
<div className="flex flex-col gap-2">
|
|
240
315
|
{/* only the sample takes the mode; the number is read on the page */}
|
|
241
|
-
<span
|
|
316
|
+
<span
|
|
317
|
+
data-theme={mode}
|
|
318
|
+
aria-hidden
|
|
319
|
+
className="flex h-12 items-center justify-center rounded-lg text-lg font-bold ring-1 ring-(--border) ring-inset"
|
|
320
|
+
style={{ background: `var(${g})`, color: `var(${c})` }}
|
|
321
|
+
>
|
|
242
322
|
Aa
|
|
243
323
|
</span>
|
|
244
|
-
{r != null &&
|
|
245
|
-
<span
|
|
246
|
-
className={cx(
|
|
247
|
-
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-xs",
|
|
248
|
-
mark === "text" && "bg-success-subdued text-success-subdued-foreground",
|
|
249
|
-
mark === "shape" && "bg-muted text-muted-foreground",
|
|
250
|
-
mark === "none" && "bg-destructive-subdued text-destructive-subdued-foreground",
|
|
251
|
-
)}
|
|
252
|
-
>
|
|
253
|
-
{r.toFixed(1)} {mark}
|
|
254
|
-
</span>
|
|
255
|
-
)}
|
|
324
|
+
{mark && r != null && <Verdict mark={mark} ratio={r} />}
|
|
256
325
|
</div>
|
|
257
326
|
</td>
|
|
258
327
|
);
|
package/src/variant-matrix.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Live } from "./lib/live";
|
|
3
4
|
import { Grid3x3, Moon, Sun } from "lucide-react";
|
|
4
5
|
import { type ReactNode, useEffect, useState } from "react";
|
|
5
6
|
import { cx } from "./lib/cx";
|
|
@@ -36,9 +37,11 @@ export function VariantMatrix({
|
|
|
36
37
|
|
|
37
38
|
const content = children ?? entry.name;
|
|
38
39
|
const render = (extra: Record<string, string>) => (
|
|
39
|
-
<
|
|
40
|
-
{
|
|
41
|
-
|
|
40
|
+
<Live>
|
|
41
|
+
<Component {...props} {...coerce(extra)}>
|
|
42
|
+
{content}
|
|
43
|
+
</Component>
|
|
44
|
+
</Live>
|
|
42
45
|
);
|
|
43
46
|
const [rows, cols, ...rest] = axes;
|
|
44
47
|
const toggle = (
|
|
@@ -51,26 +54,28 @@ export function VariantMatrix({
|
|
|
51
54
|
);
|
|
52
55
|
|
|
53
56
|
return (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
<Frame data-slot="variant-matrix">
|
|
58
|
+
<Toolbar>
|
|
59
|
+
<span className="flex items-center gap-2 pl-2 font-mono text-xs text-muted-foreground">
|
|
60
|
+
<Grid3x3 aria-hidden className="size-(--dt-size-xs)" />
|
|
61
|
+
{cols ? `${rows.name} × ${cols.name}` : rows.name}
|
|
62
|
+
{rest.length > 0 && `, ${rest.map((a) => a.name).join(", ")}`}
|
|
63
|
+
</span>
|
|
64
|
+
{toggle}
|
|
65
|
+
</Toolbar>
|
|
66
|
+
{/* one stage: the matrix, any further axes and the states, separated by space rather than rules */}
|
|
67
|
+
<div data-docs-stage="" data-theme={theme} className="flex flex-col gap-12 bg-canvas px-6 py-8 text-canvas-foreground sm:px-8">
|
|
68
|
+
{cols ? (
|
|
69
|
+
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="Variants">
|
|
65
70
|
<table className="w-full border-collapse text-sm">
|
|
66
71
|
<caption className="sr-only">
|
|
67
72
|
{entry.name}: {rows.name} by {cols.name}
|
|
68
73
|
</caption>
|
|
69
74
|
<thead>
|
|
70
|
-
<tr
|
|
75
|
+
<tr>
|
|
71
76
|
<th scope="col" className="w-0 p-0" />
|
|
72
77
|
{cols.options.map((c) => (
|
|
73
|
-
<th key={c.name} scope="col" title={c.description} className="px-4
|
|
78
|
+
<th key={c.name} scope="col" title={c.description} className="px-4 pb-4 text-center font-mono text-xs font-normal text-muted-foreground">
|
|
74
79
|
<OptionName axis={cols} option={c.name} />
|
|
75
80
|
</th>
|
|
76
81
|
))}
|
|
@@ -78,12 +83,12 @@ export function VariantMatrix({
|
|
|
78
83
|
</thead>
|
|
79
84
|
<tbody>
|
|
80
85
|
{rows.options.map((r) => (
|
|
81
|
-
<tr key={r.name}
|
|
82
|
-
<th scope="row" title={r.description} className="
|
|
86
|
+
<tr key={r.name}>
|
|
87
|
+
<th scope="row" title={r.description} className="py-4 pr-6 text-left font-mono text-xs font-normal whitespace-nowrap text-muted-foreground">
|
|
83
88
|
<OptionName axis={rows} option={r.name} />
|
|
84
89
|
</th>
|
|
85
90
|
{cols.options.map((c) => (
|
|
86
|
-
<td key={c.name} data-slot="variant-cell" className="px-4 py-
|
|
91
|
+
<td key={c.name} data-slot="variant-cell" className="px-4 py-4 text-center align-middle">
|
|
87
92
|
<span className="inline-flex justify-center">{render({ [rows.name]: r.name, [cols.name]: c.name })}</span>
|
|
88
93
|
</td>
|
|
89
94
|
))}
|
|
@@ -92,15 +97,25 @@ export function VariantMatrix({
|
|
|
92
97
|
</tbody>
|
|
93
98
|
</table>
|
|
94
99
|
</div>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
</
|
|
100
|
+
) : (
|
|
101
|
+
<AxisRow axis={rows} render={render} />
|
|
102
|
+
)}
|
|
103
|
+
{rest.map((axis) => (
|
|
104
|
+
<AxisRow key={axis.name} axis={axis} render={render} />
|
|
105
|
+
))}
|
|
106
|
+
<States render={render} canDisable={"disabled" in entry.props} />
|
|
107
|
+
</div>
|
|
108
|
+
</Frame>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A titled row of figures inside the variants stage. */
|
|
113
|
+
function Group({ title, slot, children }: { title: string; slot: string; children: ReactNode }) {
|
|
114
|
+
return (
|
|
115
|
+
<section data-slot={slot} className="flex flex-col gap-5">
|
|
116
|
+
<h3 className="font-mono text-xs text-muted-foreground">{title}</h3>
|
|
117
|
+
<div className="flex flex-wrap items-end gap-x-10 gap-y-6">{children}</div>
|
|
118
|
+
</section>
|
|
104
119
|
);
|
|
105
120
|
}
|
|
106
121
|
|
|
@@ -109,17 +124,7 @@ export function VariantMatrix({
|
|
|
109
124
|
* pressed and disabled, pinned without a pointer or a keyboard, so states are
|
|
110
125
|
* reviewed side by side rather than by mousing over each.
|
|
111
126
|
*/
|
|
112
|
-
function States({
|
|
113
|
-
render,
|
|
114
|
-
theme,
|
|
115
|
-
toggle,
|
|
116
|
-
canDisable,
|
|
117
|
-
}: {
|
|
118
|
-
render: (extra: Record<string, string>) => ReactNode;
|
|
119
|
-
theme?: "light" | "dark";
|
|
120
|
-
toggle: ReactNode;
|
|
121
|
-
canDisable: boolean;
|
|
122
|
-
}) {
|
|
127
|
+
function States({ render, canDisable }: { render: (extra: Record<string, string>) => ReactNode; canDisable: boolean }) {
|
|
123
128
|
useEffect(() => installPseudoStates(), []);
|
|
124
129
|
const states: { name: string; pseudo?: string; extra?: Record<string, string> }[] = [
|
|
125
130
|
{ name: "rest" },
|
|
@@ -129,51 +134,29 @@ function States({
|
|
|
129
134
|
...(canDisable ? [{ name: "disabled", extra: { disabled: "true" } }] : []),
|
|
130
135
|
];
|
|
131
136
|
return (
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<div data-pseudo={s.pseudo}>{render(s.extra ?? {})}</div>
|
|
141
|
-
<figcaption className="font-mono text-xs text-muted-foreground">{s.name}</figcaption>
|
|
142
|
-
</figure>
|
|
143
|
-
))}
|
|
144
|
-
</div>
|
|
145
|
-
</Frame>
|
|
137
|
+
<Group title="state" slot="variant-states">
|
|
138
|
+
{states.map((s) => (
|
|
139
|
+
<figure key={s.name} className="flex flex-col items-center gap-3">
|
|
140
|
+
<div data-pseudo={s.pseudo}>{render(s.extra ?? {})}</div>
|
|
141
|
+
<figcaption className="font-mono text-xs text-muted-foreground">{s.name}</figcaption>
|
|
142
|
+
</figure>
|
|
143
|
+
))}
|
|
144
|
+
</Group>
|
|
146
145
|
);
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
function AxisRow({
|
|
150
|
-
axis,
|
|
151
|
-
render,
|
|
152
|
-
theme,
|
|
153
|
-
toggle,
|
|
154
|
-
}: {
|
|
155
|
-
axis: VariantAxis;
|
|
156
|
-
render: (extra: Record<string, string>) => ReactNode;
|
|
157
|
-
theme?: "light" | "dark";
|
|
158
|
-
toggle: ReactNode;
|
|
159
|
-
}) {
|
|
148
|
+
function AxisRow({ axis, render }: { axis: VariantAxis; render: (extra: Record<string, string>) => ReactNode }) {
|
|
160
149
|
return (
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<OptionName axis={axis} option={o.name} />
|
|
172
|
-
</figcaption>
|
|
173
|
-
</figure>
|
|
174
|
-
))}
|
|
175
|
-
</div>
|
|
176
|
-
</Frame>
|
|
150
|
+
<Group title={axis.name} slot="variant-axis">
|
|
151
|
+
{axis.options.map((o) => (
|
|
152
|
+
<figure key={o.name} className="flex flex-col items-center gap-3">
|
|
153
|
+
{render({ [axis.name]: o.name })}
|
|
154
|
+
<figcaption className="font-mono text-xs text-muted-foreground" title={o.description}>
|
|
155
|
+
<OptionName axis={axis} option={o.name} />
|
|
156
|
+
</figcaption>
|
|
157
|
+
</figure>
|
|
158
|
+
))}
|
|
159
|
+
</Group>
|
|
177
160
|
);
|
|
178
161
|
}
|
|
179
162
|
|