@designtools/docs 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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/a11y-panel.d.ts +58 -0
- package/dist/a11y-panel.js +75 -0
- package/dist/adherence-summary.d.ts +9 -0
- package/dist/adherence-summary.js +27 -0
- package/dist/agent-view.d.ts +19 -0
- package/dist/agent-view.js +23 -0
- package/dist/anatomy.d.ts +12 -0
- package/dist/anatomy.js +38 -0
- package/dist/ask-claude.d.ts +14 -0
- package/dist/ask-claude.js +40 -0
- package/dist/cli.js +322 -0
- package/dist/code-view.d.ts +17 -0
- package/dist/code-view.js +42 -0
- package/dist/examples.d.ts +25 -0
- package/dist/examples.js +48 -0
- package/dist/glossary.d.ts +5 -0
- package/dist/glossary.js +14 -0
- package/dist/imagery.d.ts +6 -0
- package/dist/imagery.js +17 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +33 -0
- package/dist/lib/adherence-types.d.ts +168 -0
- package/dist/lib/adherence-types.js +13 -0
- package/dist/lib/boundary.d.ts +16 -0
- package/dist/lib/boundary.js +15 -0
- package/dist/lib/contrast.d.ts +8 -0
- package/dist/lib/contrast.js +42 -0
- package/dist/lib/cx.d.ts +24 -0
- package/dist/lib/cx.js +26 -0
- package/dist/lib/jsx.d.ts +6 -0
- package/dist/lib/jsx.js +29 -0
- package/dist/lib/lookup.d.ts +35 -0
- package/dist/lib/lookup.js +41 -0
- package/dist/lib/manifest-types.d.ts +253 -0
- package/dist/lib/manifest-types.js +14 -0
- package/dist/lib/manifest.d.ts +98 -0
- package/dist/lib/manifest.js +173 -0
- package/dist/lib/markdown.d.ts +50 -0
- package/dist/lib/markdown.js +286 -0
- package/dist/lib/measure.d.ts +12 -0
- package/dist/lib/measure.js +62 -0
- package/dist/lib/pseudo.d.ts +11 -0
- package/dist/lib/pseudo.js +71 -0
- package/dist/lib/registry.d.ts +21 -0
- package/dist/lib/registry.js +11 -0
- package/dist/lib/search.d.ts +44 -0
- package/dist/lib/search.js +62 -0
- package/dist/lib/standards.d.ts +17 -0
- package/dist/lib/standards.js +81 -0
- package/dist/lib/status.d.ts +7 -0
- package/dist/lib/status.js +17 -0
- package/dist/lib/text.d.ts +8 -0
- package/dist/lib/text.js +9 -0
- package/dist/lib/ui.d.ts +100 -0
- package/dist/lib/ui.js +89 -0
- package/dist/lib/vision.d.ts +18 -0
- package/dist/lib/vision.js +27 -0
- package/dist/logo-usage.d.ts +14 -0
- package/dist/logo-usage.js +45 -0
- package/dist/next/config.d.ts +85 -0
- package/dist/next/config.js +1 -0
- package/dist/next/index.d.ts +62 -0
- package/dist/next/index.js +63 -0
- package/dist/next/markdown.d.ts +20 -0
- package/dist/next/markdown.js +95 -0
- package/dist/next/pages.d.ts +6 -0
- package/dist/next/pages.js +122 -0
- package/dist/next/prose.d.ts +9 -0
- package/dist/next/prose.js +37 -0
- package/dist/next/proxy.d.ts +19 -0
- package/dist/next/proxy.js +56 -0
- package/dist/next/site.d.ts +53 -0
- package/dist/next/site.js +136 -0
- package/dist/page-actions.d.ts +9 -0
- package/dist/page-actions.js +32 -0
- package/dist/pattern.d.ts +8 -0
- package/dist/pattern.js +19 -0
- package/dist/playground.d.ts +62 -0
- package/dist/playground.js +111 -0
- package/dist/preview-frame.d.ts +31 -0
- package/dist/preview-frame.js +63 -0
- package/dist/props-table.d.ts +10 -0
- package/dist/props-table.js +45 -0
- package/dist/rule.d.ts +12 -0
- package/dist/rule.js +32 -0
- package/dist/scale.d.ts +9 -0
- package/dist/scale.js +58 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +46 -0
- package/dist/shell.d.ts +89 -0
- package/dist/shell.js +199 -0
- package/dist/standards.d.ts +13 -0
- package/dist/standards.js +19 -0
- package/dist/styles.css +3519 -0
- package/dist/swatches.d.ts +28 -0
- package/dist/swatches.js +94 -0
- package/dist/theme-switch.d.ts +20 -0
- package/dist/theme-switch.js +68 -0
- package/dist/type-ramp.d.ts +10 -0
- package/dist/type-ramp.js +31 -0
- package/dist/usage.d.ts +13 -0
- package/dist/usage.js +55 -0
- package/dist/variant-matrix.d.ts +15 -0
- package/dist/variant-matrix.js +53 -0
- package/dist/voice-terms.d.ts +5 -0
- package/dist/voice-terms.js +13 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +273 -0
- package/package.json +107 -3
- package/src/a11y-panel.tsx +297 -0
- package/src/adherence-summary.tsx +134 -0
- package/src/agent-view.tsx +54 -0
- package/src/anatomy.tsx +114 -0
- package/src/ask-claude.tsx +90 -0
- package/src/code-view.tsx +112 -0
- package/src/css.d.ts +1 -0
- package/src/examples.tsx +141 -0
- package/src/glossary.tsx +71 -0
- package/src/imagery.tsx +64 -0
- package/src/index.ts +34 -0
- package/src/lib/adherence-types.ts +181 -0
- package/src/lib/boundary.tsx +21 -0
- package/src/lib/contrast.ts +44 -0
- package/src/lib/cx.ts +30 -0
- package/src/lib/jsx.ts +29 -0
- package/src/lib/lookup.tsx +49 -0
- package/src/lib/manifest-types.ts +284 -0
- package/src/lib/manifest.ts +228 -0
- package/src/lib/markdown.ts +288 -0
- package/src/lib/measure.tsx +101 -0
- package/src/lib/pseudo.ts +74 -0
- package/src/lib/registry.tsx +29 -0
- package/src/lib/search.ts +99 -0
- package/src/lib/standards.ts +82 -0
- package/src/lib/status.tsx +32 -0
- package/src/lib/text.tsx +27 -0
- package/src/lib/ui.tsx +291 -0
- package/src/lib/vision.tsx +44 -0
- package/src/logo-usage.tsx +140 -0
- package/src/next/config.ts +80 -0
- package/src/next/index.tsx +100 -0
- package/src/next/markdown.ts +119 -0
- package/src/next/pages.tsx +370 -0
- package/src/next/prose.tsx +87 -0
- package/src/next/proxy.ts +55 -0
- package/src/next/site.ts +173 -0
- package/src/page-actions.tsx +92 -0
- package/src/pattern.tsx +67 -0
- package/src/playground.tsx +352 -0
- package/src/preview-frame.tsx +131 -0
- package/src/props-table.tsx +154 -0
- package/src/rule.tsx +116 -0
- package/src/scale.tsx +175 -0
- package/src/search.tsx +155 -0
- package/src/shell.tsx +555 -0
- package/src/standards.tsx +58 -0
- package/src/styles.css +155 -0
- package/src/swatches.tsx +287 -0
- package/src/theme-switch.tsx +90 -0
- package/src/type-ramp.tsx +150 -0
- package/src/usage.tsx +148 -0
- package/src/variant-matrix.tsx +195 -0
- package/src/voice-terms.tsx +46 -0
- package/src/workbench.tsx +584 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { shortName, tokensOfTier, typeScale, withGroupDescriptions } from "./lib/manifest";
|
|
3
|
+
import type { TokensManifest } from "./lib/manifest-types";
|
|
4
|
+
|
|
5
|
+
type Token = TokensManifest["tokens"][number];
|
|
6
|
+
|
|
7
|
+
/** `1.125rem` → `18px`, so a person reads the size they know. */
|
|
8
|
+
function px(value?: string): string | undefined {
|
|
9
|
+
const m = value && /^(\d*\.?\d+)rem$/.exec(value.trim());
|
|
10
|
+
return m ? `${Math.round(Number(m[1]) * 16 * 100) / 100}px` : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Type, set in its own tokens: each family as a specimen, then the scale, each
|
|
15
|
+
* size with the leading and tracking Tailwind pairs with it, then weights,
|
|
16
|
+
* leading and tracking on their own. Every sample reads its token; nothing is restated.
|
|
17
|
+
*/
|
|
18
|
+
export function TypeRamp({
|
|
19
|
+
tokens,
|
|
20
|
+
sample = "The quick brown fox jumps over the lazy dog",
|
|
21
|
+
}: {
|
|
22
|
+
tokens: TokensManifest;
|
|
23
|
+
sample?: string;
|
|
24
|
+
}) {
|
|
25
|
+
const steps = withGroupDescriptions(typeScale(tokens).map((s) => ({ ...s, description: s.size.description })));
|
|
26
|
+
const families = tokensOfTier(tokens, "font");
|
|
27
|
+
const weights = tokensOfTier(tokens, "font-weight");
|
|
28
|
+
const leading = tokensOfTier(tokens, "leading");
|
|
29
|
+
const tracking = tokensOfTier(tokens, "tracking");
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div data-slot="type-ramp" className="flex flex-col gap-14">
|
|
33
|
+
{families.length > 0 && (
|
|
34
|
+
<div className="grid gap-x-12 gap-y-10 md:grid-cols-2">
|
|
35
|
+
{families.map((f) => (
|
|
36
|
+
<figure key={f.name} data-slot="type-family" className="flex flex-col gap-5 border-t border-border pt-6">
|
|
37
|
+
<div className="flex flex-col gap-6" style={{ fontFamily: `var(${f.name})` }}>
|
|
38
|
+
<span className="text-8xl leading-none font-light tracking-tighter">Aa</span>
|
|
39
|
+
<span className="text-sm leading-relaxed break-all text-muted-foreground">
|
|
40
|
+
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 &?!
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
<figcaption className="flex flex-col gap-0.5">
|
|
44
|
+
<code className="font-mono text-xs font-medium">{shortName(f, "font")}</code>
|
|
45
|
+
<span className="truncate font-mono text-xs text-muted-foreground" title={f.values.default}>
|
|
46
|
+
{f.description ?? f.values.default}
|
|
47
|
+
</span>
|
|
48
|
+
</figcaption>
|
|
49
|
+
</figure>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
<ol data-slot="type-scale" className="flex flex-col">
|
|
55
|
+
{steps.map((step) => (
|
|
56
|
+
<li key={step.name} data-slot="type-step" className="grid grid-cols-1 gap-3 border-t border-border py-5 md:grid-cols-[11rem_minmax(0,1fr)] md:items-center md:gap-6">
|
|
57
|
+
<div className="flex flex-col gap-1">
|
|
58
|
+
<span className="flex items-baseline gap-2">
|
|
59
|
+
<code className="font-mono text-xs font-semibold">{shortName(step.size, "text")}</code>
|
|
60
|
+
<span className="font-mono text-xs text-muted-foreground">{px(step.size.values.default) ?? step.size.values.default}</span>
|
|
61
|
+
</span>
|
|
62
|
+
<span className="font-mono text-xs text-muted-foreground">
|
|
63
|
+
{[step.lineHeight && `lh ${step.lineHeight.values.default}`, step.letterSpacing && `tr ${step.letterSpacing.values.default}`].filter(Boolean).join(" · ")}
|
|
64
|
+
</span>
|
|
65
|
+
{step.description && <span className="text-xs text-muted-foreground">{step.description}</span>}
|
|
66
|
+
</div>
|
|
67
|
+
<p
|
|
68
|
+
className="min-w-0 truncate"
|
|
69
|
+
style={{
|
|
70
|
+
fontSize: `var(${step.name})`,
|
|
71
|
+
lineHeight: step.lineHeight ? `var(${step.lineHeight.name})` : undefined,
|
|
72
|
+
letterSpacing: step.letterSpacing ? `var(${step.letterSpacing.name})` : undefined,
|
|
73
|
+
fontWeight: step.fontWeight ? `var(${step.fontWeight.name})` : undefined,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{sample}
|
|
77
|
+
</p>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ol>
|
|
81
|
+
|
|
82
|
+
{weights.length > 0 && (
|
|
83
|
+
<Group title="Weights">
|
|
84
|
+
<ul className="grid grid-cols-2 gap-x-10 gap-y-8 sm:grid-cols-4">
|
|
85
|
+
{weights.map((t) => (
|
|
86
|
+
<li key={t.name} className="flex flex-col gap-3 border-t border-border pt-4">
|
|
87
|
+
<span className="text-4xl leading-none tracking-tight" style={{ fontWeight: `var(${t.name})` }}>
|
|
88
|
+
Aa
|
|
89
|
+
</span>
|
|
90
|
+
<Meta token={t} prefix="font-weight" />
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
</Group>
|
|
95
|
+
)}
|
|
96
|
+
{(leading.length > 0 || tracking.length > 0) && (
|
|
97
|
+
<div className="grid gap-10 lg:grid-cols-2">
|
|
98
|
+
{leading.length > 0 && (
|
|
99
|
+
<Group title="Leading">
|
|
100
|
+
<ul className="flex flex-col">
|
|
101
|
+
{leading.map((t) => (
|
|
102
|
+
<li key={t.name} className="grid grid-cols-[6rem_minmax(0,1fr)] gap-4 border-t border-border py-4">
|
|
103
|
+
<Meta token={t} prefix="leading" />
|
|
104
|
+
<span className="line-clamp-2 text-sm" style={{ lineHeight: `var(${t.name})` }}>
|
|
105
|
+
{sample}. {sample}.
|
|
106
|
+
</span>
|
|
107
|
+
</li>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
</Group>
|
|
111
|
+
)}
|
|
112
|
+
{tracking.length > 0 && (
|
|
113
|
+
<Group title="Tracking">
|
|
114
|
+
<ul className="flex flex-col">
|
|
115
|
+
{tracking.map((t) => (
|
|
116
|
+
<li key={t.name} className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-4 border-t border-border py-4">
|
|
117
|
+
<Meta token={t} prefix="tracking" />
|
|
118
|
+
<span className="truncate text-lg font-medium" style={{ letterSpacing: `var(${t.name})` }}>
|
|
119
|
+
{sample}
|
|
120
|
+
</span>
|
|
121
|
+
</li>
|
|
122
|
+
))}
|
|
123
|
+
</ul>
|
|
124
|
+
</Group>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Group({ title, children }: { title: string; children: ReactNode }) {
|
|
133
|
+
return (
|
|
134
|
+
<div data-slot="type-table" className="flex flex-col gap-4">
|
|
135
|
+
<h3 className="text-2xl tracking-tight">{title}</h3>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function Meta({ token, prefix }: { token: Token; prefix: string }) {
|
|
142
|
+
return (
|
|
143
|
+
<span className="flex min-w-0 flex-col font-mono text-xs">
|
|
144
|
+
<span className="font-medium">{shortName(token, prefix)}</span>
|
|
145
|
+
<span className="truncate text-muted-foreground" title={token.values.default}>
|
|
146
|
+
{token.values.default}
|
|
147
|
+
</span>
|
|
148
|
+
</span>
|
|
149
|
+
);
|
|
150
|
+
}
|
package/src/usage.tsx
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ArrowUpRight, CircleCheck, CircleX } from "lucide-react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { cx } from "./lib/cx";
|
|
5
|
+
import { slugOf } from "./lib/manifest";
|
|
6
|
+
import type { ComponentEntry, ComponentsManifest } from "./lib/manifest-types";
|
|
7
|
+
import { StatusBadge } from "./lib/status";
|
|
8
|
+
import { inline } from "./lib/text";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* When to use a component and when not, side by side, with what to use instead;
|
|
12
|
+
* then its facts in one strip: status, role, keys, the primitive it wraps and the
|
|
13
|
+
* elements it replaces. Written by people as JSDoc (`@use`, `@avoid`, `@instead`,
|
|
14
|
+
* `@status`, `@role`, `@keyboard`); a component without them says so.
|
|
15
|
+
*/
|
|
16
|
+
export function Usage({
|
|
17
|
+
entry,
|
|
18
|
+
manifest,
|
|
19
|
+
basePath = "/design/system",
|
|
20
|
+
}: {
|
|
21
|
+
entry: ComponentEntry;
|
|
22
|
+
/** To link `instead` names that are components. */
|
|
23
|
+
manifest?: ComponentsManifest;
|
|
24
|
+
basePath?: string;
|
|
25
|
+
}) {
|
|
26
|
+
const usage = entry.usage;
|
|
27
|
+
const known = new Map((manifest?.components ?? []).map((c) => [c.name, c]));
|
|
28
|
+
const instead = usage?.instead?.length ? (
|
|
29
|
+
<p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground">
|
|
30
|
+
Use instead
|
|
31
|
+
{usage.instead.map((name) => {
|
|
32
|
+
const c = known.get(name);
|
|
33
|
+
return c ? (
|
|
34
|
+
<Link
|
|
35
|
+
key={name}
|
|
36
|
+
href={`${basePath}/components/${slugOf(c)}`}
|
|
37
|
+
className="relative inline-flex items-center gap-0.5 text-canvas-foreground underline decoration-border underline-offset-4 hover:decoration-current focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-3 after:content-['']"
|
|
38
|
+
>
|
|
39
|
+
{name}
|
|
40
|
+
<ArrowUpRight aria-hidden className="size-3.5 text-muted-foreground" />
|
|
41
|
+
</Link>
|
|
42
|
+
) : (
|
|
43
|
+
<span key={name} className="font-medium text-canvas-foreground">
|
|
44
|
+
{name}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</p>
|
|
49
|
+
) : null;
|
|
50
|
+
|
|
51
|
+
const facts: [string, ReactNode][] = [["Status", <Status key="s" entry={entry} />]];
|
|
52
|
+
if (entry.a11y?.role) facts.push(["Role", <code key="r" className="font-mono text-xs">{entry.a11y.role}</code>]);
|
|
53
|
+
if (entry.a11y?.keyboard?.length)
|
|
54
|
+
facts.push([
|
|
55
|
+
"Keyboard",
|
|
56
|
+
<span key="k" className="flex flex-wrap gap-1">
|
|
57
|
+
{entry.a11y.keyboard.map((k) => (
|
|
58
|
+
<kbd key={k} className="rounded-sm border border-border px-1.5 font-mono text-xs leading-5">
|
|
59
|
+
{k}
|
|
60
|
+
</kbd>
|
|
61
|
+
))}
|
|
62
|
+
</span>,
|
|
63
|
+
]);
|
|
64
|
+
if (entry.primitive) facts.push(["Built on", <Source key="p" href={entry.primitive} />]);
|
|
65
|
+
if (entry.replaces?.length)
|
|
66
|
+
facts.push([
|
|
67
|
+
"Replaces",
|
|
68
|
+
<span key="x" className="flex flex-wrap gap-1">
|
|
69
|
+
{entry.replaces.map((r) => (
|
|
70
|
+
<code key={r} className="font-mono text-xs">
|
|
71
|
+
{`<${r}>`}
|
|
72
|
+
</code>
|
|
73
|
+
))}
|
|
74
|
+
</span>,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div data-slot="usage" className="flex flex-col gap-10">
|
|
79
|
+
{usage?.use || usage?.avoid ? (
|
|
80
|
+
<div className="grid gap-10 md:grid-cols-2 md:gap-12">
|
|
81
|
+
<Column tone="do" title="Use it for">
|
|
82
|
+
{usage?.use ? inline(usage.use) : <span className="text-muted-foreground">Not written yet.</span>}
|
|
83
|
+
</Column>
|
|
84
|
+
<Column tone="dont" title="Not for" footer={instead}>
|
|
85
|
+
{usage?.avoid ? inline(usage.avoid) : <span className="text-muted-foreground">Not written yet.</span>}
|
|
86
|
+
</Column>
|
|
87
|
+
</div>
|
|
88
|
+
) : (
|
|
89
|
+
<p className="text-base text-muted-foreground">
|
|
90
|
+
No usage guidance yet. Add <code className="font-mono text-sm">@use</code>, <code className="font-mono text-sm">@avoid</code> and{" "}
|
|
91
|
+
<code className="font-mono text-sm">@instead</code> to its JSDoc.
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
94
|
+
<dl data-slot="usage-facts" className="flex flex-wrap gap-x-12 gap-y-6">
|
|
95
|
+
{facts.map(([term, detail]) => (
|
|
96
|
+
<div key={term} className="flex min-w-0 flex-col gap-2">
|
|
97
|
+
<dt className="text-xs text-muted-foreground">{term}</dt>
|
|
98
|
+
<dd className="text-sm">{detail}</dd>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
</dl>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One side of when to use it: a hairline, a small label with its icon in the status colour, and the guidance at reading size. */
|
|
107
|
+
function Column({ tone, title, footer, children }: { tone: "do" | "dont"; title: string; footer?: ReactNode; children: ReactNode }) {
|
|
108
|
+
const Icon = tone === "do" ? CircleCheck : CircleX;
|
|
109
|
+
return (
|
|
110
|
+
<div className="flex flex-col gap-3 border-t border-border pt-5">
|
|
111
|
+
<p className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
112
|
+
<Icon aria-hidden className={cx("size-4", tone === "do" ? "text-success-subdued-foreground" : "text-destructive-subdued-foreground")} />
|
|
113
|
+
{title}
|
|
114
|
+
</p>
|
|
115
|
+
<p className="text-lg leading-relaxed text-canvas-foreground/85 [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-[0.875em]">{children}</p>
|
|
116
|
+
{footer}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Status({ entry }: { entry: ComponentEntry }) {
|
|
122
|
+
return (
|
|
123
|
+
<span className="flex flex-col items-start gap-1.5">
|
|
124
|
+
<StatusBadge status={entry.status} />
|
|
125
|
+
{entry.statusNote && <span className="text-xs text-muted-foreground">{inline(entry.statusNote)}</span>}
|
|
126
|
+
</span>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A primitive: a link names the site and the page (`Base UI · avatar`), a package path shows as code. */
|
|
131
|
+
function Source({ href }: { href: string }) {
|
|
132
|
+
if (!/^https?:/.test(href)) return <code className="font-mono text-xs">{href}</code>;
|
|
133
|
+
const url = new URL(href);
|
|
134
|
+
const name = url.pathname.split("/").filter(Boolean).pop() ?? "";
|
|
135
|
+
const host = url.host.replace(/^www\./, "");
|
|
136
|
+
const site = host === "base-ui.com" ? "Base UI" : host.replace(/\.[a-z]+$/, "");
|
|
137
|
+
return (
|
|
138
|
+
<a
|
|
139
|
+
href={href}
|
|
140
|
+
title={href}
|
|
141
|
+
className="group inline-flex items-center gap-1 whitespace-nowrap hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-ring"
|
|
142
|
+
>
|
|
143
|
+
{site}
|
|
144
|
+
{name && <span className="text-muted-foreground">· {name}</span>}
|
|
145
|
+
<ArrowUpRight aria-hidden className="size-3.5 text-muted-foreground" />
|
|
146
|
+
</a>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Grid3x3, Moon, Sun } from "lucide-react";
|
|
4
|
+
import { type ReactNode, useEffect, useState } from "react";
|
|
5
|
+
import { cx } from "./lib/cx";
|
|
6
|
+
import { coerce, Missing, useLookup } from "./lib/lookup";
|
|
7
|
+
import type { ComponentEntry, VariantAxis } from "./lib/manifest-types";
|
|
8
|
+
import { installPseudoStates } from "./lib/pseudo";
|
|
9
|
+
import { Frame, IconButton, Toolbar } from "./lib/ui";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Every variant, rendered live. The first two axes form a grid; each further
|
|
13
|
+
* axis gets its own row, with the others at their defaults. Pass `props` for
|
|
14
|
+
* anything the component needs to render (an `options` list, a `value`).
|
|
15
|
+
*/
|
|
16
|
+
export function VariantMatrix({
|
|
17
|
+
entry,
|
|
18
|
+
props = {},
|
|
19
|
+
children,
|
|
20
|
+
axes: only,
|
|
21
|
+
}: {
|
|
22
|
+
entry: ComponentEntry;
|
|
23
|
+
props?: Record<string, unknown>;
|
|
24
|
+
/** What to render inside each instance. Defaults to the component's name. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
/** Limit and order the axes shown. */
|
|
27
|
+
axes?: string[];
|
|
28
|
+
}) {
|
|
29
|
+
const lookup = useLookup();
|
|
30
|
+
const Component = lookup.componentFor(entry);
|
|
31
|
+
const [theme, setTheme] = useState<"light" | "dark" | undefined>();
|
|
32
|
+
const all = entry.variants?.axes ?? [];
|
|
33
|
+
const axes = only ? only.map((n) => all.find((a) => a.name === n)).filter((a): a is VariantAxis => !!a) : all;
|
|
34
|
+
if (!Component) return <Missing name={entry.name} />;
|
|
35
|
+
if (axes.length === 0) return <p className="text-sm text-muted-foreground">{entry.name} has no variants.</p>;
|
|
36
|
+
|
|
37
|
+
const content = children ?? entry.name;
|
|
38
|
+
const render = (extra: Record<string, string>) => (
|
|
39
|
+
<Component {...props} {...coerce(extra)}>
|
|
40
|
+
{content}
|
|
41
|
+
</Component>
|
|
42
|
+
);
|
|
43
|
+
const [rows, cols, ...rest] = axes;
|
|
44
|
+
const toggle = (
|
|
45
|
+
<IconButton
|
|
46
|
+
icon={theme === "dark" ? Sun : Moon}
|
|
47
|
+
label={theme === "dark" ? "Show in light mode" : "Show in dark mode"}
|
|
48
|
+
pressed={theme !== undefined}
|
|
49
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div data-slot="variant-matrix" className="flex flex-col gap-6">
|
|
55
|
+
{cols ? (
|
|
56
|
+
<Frame>
|
|
57
|
+
<Toolbar>
|
|
58
|
+
<span className="flex items-center gap-2 pl-2 font-mono text-xs text-muted-foreground">
|
|
59
|
+
<Grid3x3 aria-hidden className="size-(--dt-size-xs)" />
|
|
60
|
+
{rows.name} × {cols.name}
|
|
61
|
+
</span>
|
|
62
|
+
{toggle}
|
|
63
|
+
</Toolbar>
|
|
64
|
+
<div data-docs-stage="" data-theme={theme} className="overflow-x-auto bg-canvas text-canvas-foreground" tabIndex={0} role="region" aria-label="Variants">
|
|
65
|
+
<table className="w-full border-collapse text-sm">
|
|
66
|
+
<caption className="sr-only">
|
|
67
|
+
{entry.name}: {rows.name} by {cols.name}
|
|
68
|
+
</caption>
|
|
69
|
+
<thead>
|
|
70
|
+
<tr className="border-b border-border">
|
|
71
|
+
<th scope="col" className="w-0 p-0" />
|
|
72
|
+
{cols.options.map((c) => (
|
|
73
|
+
<th key={c.name} scope="col" title={c.description} className="px-4 py-3 text-center font-mono text-xs font-medium text-muted-foreground">
|
|
74
|
+
<OptionName axis={cols} option={c.name} />
|
|
75
|
+
</th>
|
|
76
|
+
))}
|
|
77
|
+
</tr>
|
|
78
|
+
</thead>
|
|
79
|
+
<tbody>
|
|
80
|
+
{rows.options.map((r) => (
|
|
81
|
+
<tr key={r.name} className="border-b border-border last:border-b-0">
|
|
82
|
+
<th scope="row" title={r.description} className="border-r border-border px-4 py-5 text-left font-mono text-xs font-medium whitespace-nowrap text-muted-foreground">
|
|
83
|
+
<OptionName axis={rows} option={r.name} />
|
|
84
|
+
</th>
|
|
85
|
+
{cols.options.map((c) => (
|
|
86
|
+
<td key={c.name} data-slot="variant-cell" className="px-4 py-5 text-center align-middle">
|
|
87
|
+
<span className="inline-flex justify-center">{render({ [rows.name]: r.name, [cols.name]: c.name })}</span>
|
|
88
|
+
</td>
|
|
89
|
+
))}
|
|
90
|
+
</tr>
|
|
91
|
+
))}
|
|
92
|
+
</tbody>
|
|
93
|
+
</table>
|
|
94
|
+
</div>
|
|
95
|
+
</Frame>
|
|
96
|
+
) : (
|
|
97
|
+
<AxisRow axis={rows} render={render} theme={theme} toggle={toggle} />
|
|
98
|
+
)}
|
|
99
|
+
{rest.map((axis) => (
|
|
100
|
+
<AxisRow key={axis.name} axis={axis} render={render} theme={theme} toggle={toggle} />
|
|
101
|
+
))}
|
|
102
|
+
<States render={render} theme={theme} toggle={toggle} canDisable={"disabled" in entry.props} />
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The component at its defaults in each state at once: at rest, hovered, focused,
|
|
109
|
+
* pressed and disabled, pinned without a pointer or a keyboard, so states are
|
|
110
|
+
* reviewed side by side rather than by mousing over each.
|
|
111
|
+
*/
|
|
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
|
+
}) {
|
|
123
|
+
useEffect(() => installPseudoStates(), []);
|
|
124
|
+
const states: { name: string; pseudo?: string; extra?: Record<string, string> }[] = [
|
|
125
|
+
{ name: "rest" },
|
|
126
|
+
{ name: "hover", pseudo: "hover" },
|
|
127
|
+
{ name: "focus", pseudo: "focus" },
|
|
128
|
+
{ name: "press", pseudo: "active" },
|
|
129
|
+
...(canDisable ? [{ name: "disabled", extra: { disabled: "true" } }] : []),
|
|
130
|
+
];
|
|
131
|
+
return (
|
|
132
|
+
<Frame data-slot="variant-states">
|
|
133
|
+
<Toolbar>
|
|
134
|
+
<span className="pl-2 font-mono text-xs text-muted-foreground">state</span>
|
|
135
|
+
{toggle}
|
|
136
|
+
</Toolbar>
|
|
137
|
+
<div data-docs-stage="" data-theme={theme} className="flex flex-wrap items-end justify-center gap-x-10 gap-y-6 bg-canvas px-6 py-8 text-canvas-foreground">
|
|
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
|
+
</div>
|
|
145
|
+
</Frame>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
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
|
+
}) {
|
|
160
|
+
return (
|
|
161
|
+
<Frame data-slot="variant-axis">
|
|
162
|
+
<Toolbar>
|
|
163
|
+
<span className="pl-2 font-mono text-xs text-muted-foreground">{axis.name}</span>
|
|
164
|
+
{toggle}
|
|
165
|
+
</Toolbar>
|
|
166
|
+
<div data-docs-stage="" data-theme={theme} className="flex flex-wrap items-end justify-center gap-x-10 gap-y-6 bg-canvas px-6 py-8 text-canvas-foreground">
|
|
167
|
+
{axis.options.map((o) => (
|
|
168
|
+
<figure key={o.name} className="flex flex-col items-center gap-3">
|
|
169
|
+
{render({ [axis.name]: o.name })}
|
|
170
|
+
<figcaption className="font-mono text-xs text-muted-foreground" title={o.description}>
|
|
171
|
+
<OptionName axis={axis} option={o.name} />
|
|
172
|
+
</figcaption>
|
|
173
|
+
</figure>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
</Frame>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** An option's name, with the default marked by a dot and said for screen readers. */
|
|
181
|
+
function OptionName({ axis, option }: { axis: VariantAxis; option: string }) {
|
|
182
|
+
const isDefault = axis.default === option;
|
|
183
|
+
return (
|
|
184
|
+
<span className={cx("inline-flex items-center gap-1.5", isDefault && "text-canvas-foreground")}>
|
|
185
|
+
{option}
|
|
186
|
+
{isDefault && (
|
|
187
|
+
<>
|
|
188
|
+
<span aria-hidden className="size-1.5 rounded-full bg-[var(--docs-accent,var(--primary))]" />
|
|
189
|
+
<span className="sr-only">(default)</span>
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
192
|
+
</span>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
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: the product's words first, then the system's names that have wrong forms. */
|
|
5
|
+
export function VoiceTerms({ terms }: { terms: TermEntry[] }) {
|
|
6
|
+
const product = terms.filter((t) => t.kind === "product");
|
|
7
|
+
const system = terms.filter((t) => t.kind !== "product" && t.wrong?.length);
|
|
8
|
+
if (product.length + system.length === 0) return <p className="text-sm text-muted-foreground">No terms yet. Add them to the taxonomy file.</p>;
|
|
9
|
+
return (
|
|
10
|
+
<div data-slot="voice-terms" className="flex flex-col gap-12">
|
|
11
|
+
{product.length > 0 && <Group title="In the product" terms={product} />}
|
|
12
|
+
{system.length > 0 && <Group title="In the system" terms={system} />}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Group({ title, terms }: { title: string; terms: TermEntry[] }) {
|
|
18
|
+
return (
|
|
19
|
+
<section className="flex flex-col gap-2">
|
|
20
|
+
<h3 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">{title}</h3>
|
|
21
|
+
<dl className="grid gap-x-12 sm:grid-cols-2">
|
|
22
|
+
{terms.map((t) => (
|
|
23
|
+
<div key={`${t.kind}:${t.name}`} data-slot="term" className="flex flex-col gap-1.5 border-t border-border py-5">
|
|
24
|
+
<dt className="text-3xl tracking-tight">
|
|
25
|
+
{t.name}
|
|
26
|
+
<span className="sr-only">, say this</span>
|
|
27
|
+
</dt>
|
|
28
|
+
{(t.wrong ?? []).length > 0 && (
|
|
29
|
+
<dd className="flex flex-wrap gap-x-3 text-base text-muted-foreground">
|
|
30
|
+
<span className="sr-only">not </span>
|
|
31
|
+
{(t.wrong ?? []).map((w) => (
|
|
32
|
+
<s key={w} className="decoration-destructive decoration-2">
|
|
33
|
+
{w}
|
|
34
|
+
</s>
|
|
35
|
+
))}
|
|
36
|
+
</dd>
|
|
37
|
+
)}
|
|
38
|
+
{(t.note || t.kind !== "product") && (
|
|
39
|
+
<dd className="text-sm text-muted-foreground">{t.note ? inline(t.note) : `The ${t.kind}'s name in the code.`}</dd>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</dl>
|
|
44
|
+
</section>
|
|
45
|
+
);
|
|
46
|
+
}
|