@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
package/src/rule.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Bot, Eye, Hash, ShieldCheck } from "lucide-react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { CodeView } from "./code-view";
|
|
6
|
+
import { cx } from "./lib/cx";
|
|
7
|
+
import { ruleMarkdown } from "./lib/markdown";
|
|
8
|
+
import type { RuleEntry } from "./lib/manifest-types";
|
|
9
|
+
import { StatusBadge } from "./lib/status";
|
|
10
|
+
import { inline, Prose } from "./lib/text";
|
|
11
|
+
import { Eyebrow, Segmented, SegmentedList, SegmentedPanel, Stage } from "./lib/ui";
|
|
12
|
+
|
|
13
|
+
const CHECK: Record<string, string> = { test: "Tested", lint: "Linted", audit: "Audited", manual: "Checked by a person" };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One rule, brand or interface, written for people: the statement set large, the
|
|
17
|
+
* line it draws, what it applies to and the check that proves it, then why. A
|
|
18
|
+
* switch shows the same rule as an agent reads it. Pass a demonstration as
|
|
19
|
+
* children where the rule needs persuading; it is shown on a stage.
|
|
20
|
+
*/
|
|
21
|
+
export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode }) {
|
|
22
|
+
const chips = (items: string[], mono = false) => (
|
|
23
|
+
<span className="flex flex-wrap gap-1.5">
|
|
24
|
+
{items.map((t) => (
|
|
25
|
+
<span key={t} className={cx("inline-flex min-h-6 items-center rounded-sm bg-muted px-1.5 text-xs", mono && "font-mono")}>
|
|
26
|
+
{t}
|
|
27
|
+
</span>
|
|
28
|
+
))}
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
const facts: [string, ReactNode][] = [];
|
|
32
|
+
if (rule.threshold) facts.push(["Threshold", <span key="t" className="font-medium">{rule.threshold}</span>]);
|
|
33
|
+
if (rule.appliesTo?.length) facts.push(["Applies to", chips(rule.appliesTo)]);
|
|
34
|
+
if (rule.exceptions?.length) facts.push(["Exceptions", <span key="e">{rule.exceptions.join("; ")}</span>]);
|
|
35
|
+
if (rule.tokens?.length) facts.push(["Tokens", chips(rule.tokens, true)]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<article id={rule.id} data-slot="rule" className="scroll-mt-24 border-t border-border pt-8">
|
|
39
|
+
<Segmented defaultValue="rendered">
|
|
40
|
+
<header className="flex flex-wrap items-start justify-between gap-3">
|
|
41
|
+
<div className="flex min-w-0 flex-col gap-2">
|
|
42
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
43
|
+
<StatusBadge status={rule.status} />
|
|
44
|
+
<span className="text-xs font-medium text-muted-foreground">{rule.kind === "brand" ? "Brand rule" : "Interface rule"}</span>
|
|
45
|
+
</span>
|
|
46
|
+
<h3 className="group flex items-center gap-2 text-3xl tracking-tight">
|
|
47
|
+
{rule.title}
|
|
48
|
+
<a
|
|
49
|
+
href={`#${rule.id}`}
|
|
50
|
+
aria-label={`Link to ${rule.title}`}
|
|
51
|
+
className="relative inline-flex size-7 items-center justify-center rounded-md text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-muted focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-2 after:content-['']"
|
|
52
|
+
>
|
|
53
|
+
<Hash aria-hidden className="size-4" />
|
|
54
|
+
</a>
|
|
55
|
+
</h3>
|
|
56
|
+
</div>
|
|
57
|
+
<SegmentedList
|
|
58
|
+
label={`${rule.title}, two ways`}
|
|
59
|
+
items={[
|
|
60
|
+
{ value: "rendered", label: "For people", icon: Eye },
|
|
61
|
+
{ value: "agent", label: "For agents", icon: Bot },
|
|
62
|
+
]}
|
|
63
|
+
/>
|
|
64
|
+
</header>
|
|
65
|
+
<SegmentedPanel value="rendered" className="flex flex-col gap-6 pt-4">
|
|
66
|
+
<p data-slot="rule-statement" className="max-w-3xl text-xl leading-snug tracking-tight">
|
|
67
|
+
{inline(rule.statement)}
|
|
68
|
+
</p>
|
|
69
|
+
{children && (
|
|
70
|
+
<div data-slot="rule-demonstration" className="overflow-hidden rounded-xl border border-border">
|
|
71
|
+
<Stage className="min-h-40">{children}</Stage>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
{facts.length > 0 && (
|
|
75
|
+
<dl className="grid gap-x-10 gap-y-5 sm:grid-cols-2">
|
|
76
|
+
{facts.map(([term, detail]) => (
|
|
77
|
+
<div key={term} className="flex flex-col gap-1.5">
|
|
78
|
+
<dt className="text-xs font-medium text-muted-foreground">{term}</dt>
|
|
79
|
+
<dd className="text-sm">{detail}</dd>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</dl>
|
|
83
|
+
)}
|
|
84
|
+
{rule.check && (
|
|
85
|
+
<div className="flex items-start gap-2.5 text-sm">
|
|
86
|
+
<ShieldCheck aria-hidden className="mt-0.5 size-(--dt-size-xs) shrink-0 text-muted-foreground" />
|
|
87
|
+
<p className="flex flex-col gap-0.5">
|
|
88
|
+
<span className="font-medium">
|
|
89
|
+
{CHECK[rule.check.kind] ?? rule.check.kind}
|
|
90
|
+
{rule.check.ref && (
|
|
91
|
+
<>
|
|
92
|
+
{" "}
|
|
93
|
+
by <code className="font-mono text-xs">{rule.check.ref}</code>
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
</span>
|
|
97
|
+
{rule.check.description && <span className="text-muted-foreground">{inline(rule.check.description)}</span>}
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
{rule.rationale && (
|
|
102
|
+
<div className="flex max-w-3xl flex-col gap-2">
|
|
103
|
+
<Eyebrow>Why</Eyebrow>
|
|
104
|
+
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
|
|
105
|
+
<Prose text={rule.rationale} />
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</SegmentedPanel>
|
|
110
|
+
<SegmentedPanel value="agent" className="pt-4">
|
|
111
|
+
<CodeView code={ruleMarkdown(rule)} lang="markdown" label={`${rule.id}.md`} wrap />
|
|
112
|
+
</SegmentedPanel>
|
|
113
|
+
</Segmented>
|
|
114
|
+
</article>
|
|
115
|
+
);
|
|
116
|
+
}
|
package/src/scale.tsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
const TITLE: Partial<Record<TokenTier, [string, string]>> = {
|
|
9
|
+
spacing: ["Space", "The gap between things: padding, margin and gap."],
|
|
10
|
+
size: ["Size", "The extent of a thing: icons, avatars and control heights."],
|
|
11
|
+
radius: ["Radius", "Corners, scaled to the element: bigger box, bigger step."],
|
|
12
|
+
shadow: ["Elevation", "How far a thing lifts from the page."],
|
|
13
|
+
duration: ["Duration", "How long a change takes. Point at a row to play it."],
|
|
14
|
+
ease: ["Easing", "How a change moves. Point at a row to play it."],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Everything that is not colour or type: space, size, radius, elevation and
|
|
19
|
+
* motion, each tier drawn the way it is used. Pass `tiers` to show only some, in your order.
|
|
20
|
+
*/
|
|
21
|
+
export function Scale({
|
|
22
|
+
tokens,
|
|
23
|
+
tiers = ["spacing", "size", "radius", "shadow", "duration", "ease"],
|
|
24
|
+
}: {
|
|
25
|
+
tokens: TokensManifest;
|
|
26
|
+
tiers?: TokenTier[];
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<div data-slot="scale" className="flex flex-col gap-14">
|
|
30
|
+
{tiers.map((tier) => {
|
|
31
|
+
const rows = tokensOfTier(tokens, tier);
|
|
32
|
+
if (rows.length === 0) return null;
|
|
33
|
+
const [title, lead] = TITLE[tier] ?? [tier, ""];
|
|
34
|
+
return (
|
|
35
|
+
<section key={tier} data-slot="scale-tier" className="flex flex-col gap-4">
|
|
36
|
+
<header className="flex flex-col gap-1">
|
|
37
|
+
<h3 className="text-2xl tracking-tight capitalize">{title}</h3>
|
|
38
|
+
{lead && <p className="text-sm text-muted-foreground">{lead}</p>}
|
|
39
|
+
</header>
|
|
40
|
+
{tier === "spacing" ? (
|
|
41
|
+
<Spacing rows={rows} />
|
|
42
|
+
) : tier === "duration" || tier === "ease" ? (
|
|
43
|
+
<MotionRows rows={rows} tier={tier} />
|
|
44
|
+
) : (
|
|
45
|
+
<Tiles rows={rows} tier={tier} />
|
|
46
|
+
)}
|
|
47
|
+
</section>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const card = "rounded-2xl border border-border bg-surface text-surface-foreground";
|
|
55
|
+
|
|
56
|
+
function Spacing({ rows }: { rows: TokenEntry[] }) {
|
|
57
|
+
const multiplier = rows.find((t) => t.name === "--spacing");
|
|
58
|
+
const named = rows.filter((t) => t !== multiplier);
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex flex-col gap-4">
|
|
61
|
+
{multiplier && (
|
|
62
|
+
<div className={`${card} flex flex-col gap-1 p-5`}>
|
|
63
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
64
|
+
{multiplier.description ?? "One multiplier"} · <code className="font-mono">--spacing: {multiplier.values.default}</code>
|
|
65
|
+
</p>
|
|
66
|
+
<ul className="flex flex-col gap-2">
|
|
67
|
+
{SPACING_STEPS.map((n) => (
|
|
68
|
+
<li key={n} className="grid grid-cols-[3rem_4rem_1fr] items-center gap-3 font-mono text-xs">
|
|
69
|
+
<span className="font-medium">{n}</span>
|
|
70
|
+
<span className="text-muted-foreground">{n * 4}px</span>
|
|
71
|
+
<span
|
|
72
|
+
data-slot="scale-bar"
|
|
73
|
+
className="h-5 rounded-sm border border-primary-subdued-foreground/40 bg-primary-subdued"
|
|
74
|
+
style={{ width: `calc(var(--spacing) * ${n})` }}
|
|
75
|
+
/>
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{named.length > 0 && <Tiles rows={named} tier="spacing" />}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Tiles({ rows, tier }: { rows: TokenEntry[]; tier: TokenTier }) {
|
|
87
|
+
return (
|
|
88
|
+
<ul className={tier === "shadow" ? "grid grid-cols-2 gap-4 lg:grid-cols-3" : "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4"}>
|
|
89
|
+
{rows.map((t) => (
|
|
90
|
+
<li key={t.name} data-slot="scale-row" className={`${card} flex flex-col overflow-hidden`}>
|
|
91
|
+
<div
|
|
92
|
+
className={
|
|
93
|
+
tier === "shadow"
|
|
94
|
+
? "flex h-36 items-center justify-center bg-canvas p-6"
|
|
95
|
+
: "flex h-28 items-center justify-center bg-[radial-gradient(color-mix(in_oklch,var(--border)_85%,transparent)_1px,transparent_1px)] [background-size:14px_14px] p-4"
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
{sample(t, tier)}
|
|
99
|
+
</div>
|
|
100
|
+
<div className="flex flex-col gap-0.5 border-t border-border px-4 py-3 font-mono text-xs">
|
|
101
|
+
<span className="font-medium">{shortName(t, tier)}</span>
|
|
102
|
+
{tier !== "shadow" && (
|
|
103
|
+
<span className="truncate text-muted-foreground" title={t.values.default}>
|
|
104
|
+
{t.values.default}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
{t.description && (
|
|
108
|
+
<span className="truncate font-sans text-muted-foreground" title={t.description}>
|
|
109
|
+
{t.description}
|
|
110
|
+
</span>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function MotionRows({ rows, tier }: { rows: TokenEntry[]; tier: TokenTier }) {
|
|
120
|
+
return (
|
|
121
|
+
<ul className={`${card} flex flex-col divide-y divide-border overflow-hidden`}>
|
|
122
|
+
{withGroupDescriptions(rows).map((t) => (
|
|
123
|
+
// focusable, so a keyboard can play a row as a pointer does
|
|
124
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: a row a keyboard can focus to play its motion
|
|
125
|
+
<li key={t.name} data-slot="scale-row" tabIndex={0} className="group grid grid-cols-1 items-center gap-3 px-5 py-4 outline-none hover:bg-muted/50 focus-visible:bg-muted/50 md:grid-cols-[12rem_minmax(0,1fr)]">
|
|
126
|
+
<span className="flex min-w-0 flex-col font-mono text-xs">
|
|
127
|
+
<span className="font-medium">{shortName(t, tier)}</span>
|
|
128
|
+
<span className="truncate text-muted-foreground" title={t.values.default}>
|
|
129
|
+
{t.values.default}
|
|
130
|
+
</span>
|
|
131
|
+
{t.description && <span className="truncate font-sans text-muted-foreground">{t.description}</span>}
|
|
132
|
+
</span>
|
|
133
|
+
<span className="min-w-0">
|
|
134
|
+
{tier === "duration" ? (
|
|
135
|
+
<Motion duration={`var(${t.name})`} easing="var(--dt-ease-out, ease-out)" />
|
|
136
|
+
) : (
|
|
137
|
+
<Motion duration="var(--dt-duration-slow, 400ms)" easing={`var(${t.name})`} />
|
|
138
|
+
)}
|
|
139
|
+
</span>
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sample(t: TokenEntry, tier: TokenTier): ReactNode {
|
|
147
|
+
const v = `var(${t.name})`;
|
|
148
|
+
switch (tier) {
|
|
149
|
+
case "spacing":
|
|
150
|
+
return <span className="block h-5 rounded-sm bg-primary-subdued" style={{ width: v }} />;
|
|
151
|
+
case "size":
|
|
152
|
+
return <span className="block rounded-md border border-primary-subdued-foreground/40 bg-primary-subdued" style={{ width: v, height: v }} />;
|
|
153
|
+
case "radius":
|
|
154
|
+
return <span className="block size-16 border-2 border-primary-subdued-foreground/60 bg-primary-subdued" style={{ borderRadius: v }} />;
|
|
155
|
+
case "shadow":
|
|
156
|
+
return <span className="block h-16 w-28 rounded-lg bg-surface" style={{ boxShadow: v }} />;
|
|
157
|
+
default:
|
|
158
|
+
return <span className="font-mono text-xs">{t.values.default}</span>;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A dot that crosses its track when the row is pointed at or focused. Reduced motion jumps instead. */
|
|
163
|
+
function Motion({ duration, easing }: { duration: string; easing: string }) {
|
|
164
|
+
return (
|
|
165
|
+
<span data-slot="scale-motion" className="relative block h-6 w-full rounded-full bg-muted">
|
|
166
|
+
{/* the runner is the track less the dot, so translating it by its own width lands the dot at the end */}
|
|
167
|
+
<span
|
|
168
|
+
className="absolute inset-y-1 right-5 left-1 block transition-transform group-hover:translate-x-full group-focus-visible:translate-x-full motion-reduce:transition-none"
|
|
169
|
+
style={{ transitionDuration: duration, transitionTimingFunction: easing }}
|
|
170
|
+
>
|
|
171
|
+
<span className="block size-4 rounded-full bg-primary shadow-sm" />
|
|
172
|
+
</span>
|
|
173
|
+
</span>
|
|
174
|
+
);
|
|
175
|
+
}
|
package/src/search.tsx
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Autocomplete } from "@base-ui/react/autocomplete";
|
|
4
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
5
|
+
import { Search } from "lucide-react";
|
|
6
|
+
import { useRouter } from "next/navigation";
|
|
7
|
+
import { useEffect, useId, useMemo, useState } from "react";
|
|
8
|
+
import { cx, target } from "./lib/cx";
|
|
9
|
+
import { search, type SearchGroup, type SearchItem } from "./lib/search";
|
|
10
|
+
|
|
11
|
+
const PLURAL: Record<SearchGroup["kind"], string> = { Component: "Components", Rule: "Rules", Pattern: "Patterns", Page: "Pages" };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find a component, a part, a rule, a pattern or a page by name, fuzzily, and go
|
|
15
|
+
* there. A wrong name finds the right thing ("Dropdown" finds Select, and says
|
|
16
|
+
* so). Opens with ⌘K or Ctrl+K anywhere, or / when you are not typing. Build the
|
|
17
|
+
* items with `searchIndex` from `lib/search`; the shell does, when you pass it the
|
|
18
|
+
* taxonomy. Navigation only: it does not search page text.
|
|
19
|
+
*/
|
|
20
|
+
export function DocsSearch({
|
|
21
|
+
items,
|
|
22
|
+
shortcuts = true,
|
|
23
|
+
variant = "field",
|
|
24
|
+
className,
|
|
25
|
+
}: {
|
|
26
|
+
items: SearchItem[];
|
|
27
|
+
/** Listen for ⌘K, Ctrl+K and /. Give one search on a page the shortcuts. */
|
|
28
|
+
shortcuts?: boolean;
|
|
29
|
+
/** A field with its shortcut, or an icon on its own for a phone's top bar. */
|
|
30
|
+
variant?: "field" | "icon";
|
|
31
|
+
className?: string;
|
|
32
|
+
}) {
|
|
33
|
+
const router = useRouter();
|
|
34
|
+
const [open, setOpen] = useState(false);
|
|
35
|
+
const [query, setQuery] = useState("");
|
|
36
|
+
const [shortcut, setShortcut] = useState("Ctrl K");
|
|
37
|
+
const hintId = useId();
|
|
38
|
+
const groups = useMemo(() => search(items, query), [items, query]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (/Mac|iPhone|iPad/.test(navigator.platform)) setShortcut("⌘K");
|
|
42
|
+
if (!shortcuts) return;
|
|
43
|
+
const onKey = (e: KeyboardEvent) => {
|
|
44
|
+
const typing = e.target instanceof HTMLElement && (e.target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName));
|
|
45
|
+
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || (e.key === "/" && !typing && !e.metaKey && !e.ctrlKey && !e.altKey)) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setOpen(true);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
window.addEventListener("keydown", onKey);
|
|
51
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
52
|
+
}, [shortcuts]);
|
|
53
|
+
|
|
54
|
+
const go = (item: SearchItem) => {
|
|
55
|
+
setOpen(false);
|
|
56
|
+
setQuery("");
|
|
57
|
+
router.push(item.href);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Dialog.Root open={open} onOpenChange={(next) => (setOpen(next), next || setQuery(""))}>
|
|
62
|
+
{variant === "icon" ? (
|
|
63
|
+
<Dialog.Trigger
|
|
64
|
+
data-slot="docs-search-trigger"
|
|
65
|
+
aria-label="Search the design system"
|
|
66
|
+
className={cx(
|
|
67
|
+
"inline-flex size-(--docs-target,2.75rem) shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<Search aria-hidden className="size-(--dt-size-sm)" />
|
|
72
|
+
</Dialog.Trigger>
|
|
73
|
+
) : (
|
|
74
|
+
<Dialog.Trigger
|
|
75
|
+
data-slot="docs-search-trigger"
|
|
76
|
+
className={cx(
|
|
77
|
+
target,
|
|
78
|
+
"flex w-full items-center gap-2.5 rounded-md border border-border bg-canvas px-3 text-sm text-muted-foreground transition-colors duration-(--dt-duration-fast) hover:border-input hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
<Search aria-hidden className="size-(--dt-size-xs) shrink-0" />
|
|
83
|
+
<span className="flex-1 text-left">Search</span>
|
|
84
|
+
<kbd className="rounded-sm border border-border bg-surface px-1.5 py-0.5 font-mono text-xs pointer-coarse:hidden">{shortcut}</kbd>
|
|
85
|
+
</Dialog.Trigger>
|
|
86
|
+
)}
|
|
87
|
+
<Dialog.Portal>
|
|
88
|
+
<Dialog.Backdrop data-designtools="" className="fixed inset-0 z-40 bg-canvas-foreground/30 backdrop-blur-xs transition-opacity duration-(--dt-duration-base) data-ending-style:opacity-0 data-starting-style:opacity-0" />
|
|
89
|
+
<Dialog.Viewport data-designtools="" className="fixed inset-0 z-50 flex items-start justify-center px-4 pt-[12dvh]">
|
|
90
|
+
<Dialog.Popup
|
|
91
|
+
data-slot="docs-search"
|
|
92
|
+
aria-label="Search the design system"
|
|
93
|
+
className="flex max-h-[min(36rem,80dvh)] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-border bg-overlay text-overlay-foreground shadow-xl transition-[opacity,scale] duration-(--dt-duration-base) ease-(--dt-ease-out) data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-starting-style:scale-[0.98] data-starting-style:opacity-0"
|
|
94
|
+
>
|
|
95
|
+
<Autocomplete.Root
|
|
96
|
+
open
|
|
97
|
+
inline
|
|
98
|
+
items={groups}
|
|
99
|
+
filter={null}
|
|
100
|
+
value={query}
|
|
101
|
+
onValueChange={(next) => setQuery(next)}
|
|
102
|
+
itemToStringValue={(item: SearchItem) => item.label}
|
|
103
|
+
autoHighlight="always"
|
|
104
|
+
keepHighlight
|
|
105
|
+
>
|
|
106
|
+
<div className="flex items-center gap-3 border-b border-border px-4">
|
|
107
|
+
<Search aria-hidden className="size-(--dt-size-sm) shrink-0 text-muted-foreground" />
|
|
108
|
+
<Autocomplete.Input
|
|
109
|
+
aria-label="Search components, rules, patterns and pages"
|
|
110
|
+
aria-describedby={hintId}
|
|
111
|
+
placeholder="Search components, rules, patterns…"
|
|
112
|
+
className="h-(--dt-size-3xl) w-full bg-transparent text-lg outline-none placeholder:text-muted-foreground"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
<Dialog.Close className="sr-only">Close search</Dialog.Close>
|
|
116
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2">
|
|
117
|
+
<Autocomplete.Empty>
|
|
118
|
+
<p className="px-4 py-6 text-sm text-muted-foreground">{query.trim() ? `Nothing called “${query.trim()}”.` : "Type a name, or a name you half remember."}</p>
|
|
119
|
+
</Autocomplete.Empty>
|
|
120
|
+
<Autocomplete.List>
|
|
121
|
+
{(group: SearchGroup) => (
|
|
122
|
+
<Autocomplete.Group key={group.kind} items={group.items} className="not-last:mb-2">
|
|
123
|
+
<Autocomplete.GroupLabel className="px-3 pt-2 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">{PLURAL[group.kind]}</Autocomplete.GroupLabel>
|
|
124
|
+
<Autocomplete.Collection>
|
|
125
|
+
{(item: SearchGroup["items"][number]) => (
|
|
126
|
+
<Autocomplete.Item
|
|
127
|
+
key={item.href}
|
|
128
|
+
value={item}
|
|
129
|
+
onClick={() => go(item)}
|
|
130
|
+
className={cx(
|
|
131
|
+
target,
|
|
132
|
+
"flex cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2 text-sm outline-none select-none data-highlighted:bg-muted",
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
<span className="min-w-0 truncate font-medium">{item.label}</span>
|
|
136
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
137
|
+
{item.matched ? `for “${item.matched}”` : item.detail}
|
|
138
|
+
</span>
|
|
139
|
+
</Autocomplete.Item>
|
|
140
|
+
)}
|
|
141
|
+
</Autocomplete.Collection>
|
|
142
|
+
</Autocomplete.Group>
|
|
143
|
+
)}
|
|
144
|
+
</Autocomplete.List>
|
|
145
|
+
</div>
|
|
146
|
+
<p id={hintId} className="border-t border-border px-4 py-2 text-xs text-muted-foreground">
|
|
147
|
+
Arrow keys to move, Enter to go, Escape to close.
|
|
148
|
+
</p>
|
|
149
|
+
</Autocomplete.Root>
|
|
150
|
+
</Dialog.Popup>
|
|
151
|
+
</Dialog.Viewport>
|
|
152
|
+
</Dialog.Portal>
|
|
153
|
+
</Dialog.Root>
|
|
154
|
+
);
|
|
155
|
+
}
|