@designtools/docs 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 +21 -0
- package/LICENSE +21 -0
- package/README.md +155 -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 +39 -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 +21 -0
- package/dist/imagery.d.ts +6 -0
- package/dist/imagery.js +17 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +34 -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/live.d.ts +10 -0
- package/dist/lib/live.js +10 -0
- package/dist/lib/lookup.d.ts +35 -0
- package/dist/lib/lookup.js +42 -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 +6 -0
- package/dist/lib/status.js +17 -0
- package/dist/lib/terms.d.ts +15 -0
- package/dist/lib/terms.js +20 -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 +105 -0
- package/dist/next/config.js +1 -0
- package/dist/next/index.d.ts +65 -0
- package/dist/next/index.js +66 -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 +188 -0
- package/dist/next/prose.d.ts +9 -0
- package/dist/next/prose.js +39 -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 +11 -0
- package/dist/pattern.js +22 -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 +16 -0
- package/dist/rule.js +37 -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 +92 -0
- package/dist/shell.js +201 -0
- package/dist/standards.d.ts +13 -0
- package/dist/standards.js +19 -0
- package/dist/styles.css +3708 -0
- package/dist/swatches.d.ts +28 -0
- package/dist/swatches.js +100 -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 +58 -0
- package/dist/voice-terms.d.ts +5 -0
- package/dist/voice-terms.js +14 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +274 -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 +119 -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 +70 -0
- package/src/imagery.tsx +64 -0
- package/src/index.ts +35 -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/live.tsx +15 -0
- package/src/lib/lookup.tsx +50 -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/terms.tsx +53 -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 +101 -0
- package/src/next/index.tsx +103 -0
- package/src/next/markdown.ts +119 -0
- package/src/next/pages.tsx +483 -0
- package/src/next/prose.tsx +97 -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 +72 -0
- package/src/playground.tsx +357 -0
- package/src/preview-frame.tsx +131 -0
- package/src/props-table.tsx +154 -0
- package/src/rule.tsx +165 -0
- package/src/scale.tsx +175 -0
- package/src/search.tsx +155 -0
- package/src/shell.tsx +569 -0
- package/src/standards.tsx +58 -0
- package/src/styles.css +157 -0
- package/src/swatches.tsx +298 -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 +178 -0
- package/src/voice-terms.tsx +37 -0
- package/src/workbench.tsx +587 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, Copy, Sparkles } from "lucide-react";
|
|
4
|
+
import { useId, useState } from "react";
|
|
5
|
+
import { importOf } from "./lib/manifest";
|
|
6
|
+
import type { ComponentEntry } from "./lib/manifest-types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Copies a ready-made prompt for Claude: the component, its source file, the
|
|
10
|
+
* variant and example in view, and what should change. Paste it into Claude
|
|
11
|
+
* Code in the project and it starts from the right file.
|
|
12
|
+
*/
|
|
13
|
+
export function AskClaude({
|
|
14
|
+
entry,
|
|
15
|
+
variant,
|
|
16
|
+
example,
|
|
17
|
+
}: {
|
|
18
|
+
entry: ComponentEntry;
|
|
19
|
+
/** The variant in view, e.g. `{ intent: "success" }`. */
|
|
20
|
+
variant?: Record<string, string>;
|
|
21
|
+
/** The example in view, by export name. */
|
|
22
|
+
example?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const id = useId();
|
|
25
|
+
const [request, setRequest] = useState("");
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
|
|
28
|
+
const copy = async () => {
|
|
29
|
+
try {
|
|
30
|
+
await navigator.clipboard.writeText(promptFor(entry, request, variant, example));
|
|
31
|
+
setCopied(true);
|
|
32
|
+
setTimeout(() => setCopied(false), 1500);
|
|
33
|
+
} catch {
|
|
34
|
+
// clipboard refused: nothing changes
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div data-slot="ask-claude" className="flex flex-col gap-4 rounded-2xl border border-border bg-surface p-5 text-surface-foreground shadow-xs sm:p-6">
|
|
40
|
+
<div className="flex items-start gap-3">
|
|
41
|
+
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary-subdued text-primary-subdued-foreground">
|
|
42
|
+
<Sparkles aria-hidden className="size-(--dt-size-xs)" />
|
|
43
|
+
</span>
|
|
44
|
+
<span className="flex flex-col gap-0.5">
|
|
45
|
+
<label htmlFor={id} className="text-base font-semibold tracking-tight">
|
|
46
|
+
Ask Claude to change {entry.name}
|
|
47
|
+
</label>
|
|
48
|
+
<span className="text-sm text-muted-foreground">
|
|
49
|
+
Copies a prompt naming the file{example ? ", the example" : ""}, its parts and the variant, ready to paste into Claude Code.
|
|
50
|
+
</span>
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex flex-col overflow-hidden rounded-xl border border-input bg-canvas focus-within:outline-2 focus-within:outline-ring">
|
|
54
|
+
<textarea
|
|
55
|
+
id={id}
|
|
56
|
+
rows={3}
|
|
57
|
+
value={request}
|
|
58
|
+
onChange={(e) => setRequest(e.target.value)}
|
|
59
|
+
placeholder={`What should change about ${entry.name}?`}
|
|
60
|
+
className="resize-none bg-transparent px-4 py-3 text-base outline-none placeholder:text-muted-foreground"
|
|
61
|
+
/>
|
|
62
|
+
<div className="flex items-center justify-between gap-3 px-2 pb-2 pl-4">
|
|
63
|
+
<code className="truncate font-mono text-xs text-muted-foreground">{entry.source}</code>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={copy}
|
|
67
|
+
className="inline-flex min-h-(--docs-target,2.75rem) shrink-0 cursor-pointer items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
68
|
+
>
|
|
69
|
+
{copied ? <Check aria-hidden className="size-(--dt-size-xs)" /> : <Copy aria-hidden className="size-(--dt-size-xs)" />}
|
|
70
|
+
{copied ? "Copied" : "Copy prompt"}
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<span className="sr-only" aria-live="polite">
|
|
75
|
+
{copied ? "Prompt copied" : ""}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function promptFor(entry: ComponentEntry, request: string, variant?: Record<string, string>, example?: string): string {
|
|
82
|
+
const lines = [`Component: ${entry.name}, exported as ${entry.export} from ${entry.source} (imported from "${importOf(entry)}")`];
|
|
83
|
+
const shown = Object.entries(variant ?? {});
|
|
84
|
+
if (shown.length) lines.push(`Variant in view: ${shown.map(([k, v]) => `${k}="${v}"`).join(", ")}`);
|
|
85
|
+
if (example && entry.examples) lines.push(`Example in view: ${example}, from ${entry.examples.source}`);
|
|
86
|
+
if (entry.status && entry.status !== "stable") lines.push(`Status: ${entry.status}${entry.statusNote ? `. ${entry.statusNote}` : ""}`);
|
|
87
|
+
if (entry.slots.length) lines.push(`Its parts (data-slot): ${entry.slots.join(", ")}`);
|
|
88
|
+
lines.push("", request.trim() || "What should change: ");
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, FileCode } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { cx } from "./lib/cx";
|
|
6
|
+
import { CopyButton } from "./lib/ui";
|
|
7
|
+
|
|
8
|
+
/* Shiki writes both themes as CSS variables; these pick the one for the page's mode, and number the lines. */
|
|
9
|
+
const THEME_CSS = `
|
|
10
|
+
[data-slot="code-view"] .shiki { background: transparent !important; counter-reset: line; }
|
|
11
|
+
[data-slot="code-view"][data-numbered] .shiki .line::before { counter-increment: line; content: counter(line); display: inline-block; width: 2ch; margin-right: 1.25rem; text-align: right; color: var(--muted-foreground); opacity: .6; }
|
|
12
|
+
[data-theme="dark"] [data-slot="code-view"] .shiki, [data-theme="dark"] [data-slot="code-view"] .shiki span { color: var(--shiki-dark) !important; }
|
|
13
|
+
@media (prefers-color-scheme: dark) {
|
|
14
|
+
:root:not([data-theme="light"]) [data-slot="code-view"] .shiki, :root:not([data-theme="light"]) [data-slot="code-view"] .shiki span { color: var(--shiki-dark) !important; }
|
|
15
|
+
}`;
|
|
16
|
+
|
|
17
|
+
/** Lines past which the code folds, with a fade and a "Show all". */
|
|
18
|
+
const FOLD = 14;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Code, highlighted once Shiki has loaded (it loads on first use, so pages
|
|
22
|
+
* without code never pay for it) and readable as plain text before then. A file
|
|
23
|
+
* header with a copy button, numbered lines, and long code folded behind "Show
|
|
24
|
+
* all". `bare` drops the frame, for code inside another frame.
|
|
25
|
+
*/
|
|
26
|
+
export function CodeView({
|
|
27
|
+
code,
|
|
28
|
+
lang = "tsx",
|
|
29
|
+
label = "Code",
|
|
30
|
+
wrap = false,
|
|
31
|
+
bare = false,
|
|
32
|
+
numbered,
|
|
33
|
+
}: {
|
|
34
|
+
code: string;
|
|
35
|
+
lang?: string;
|
|
36
|
+
label?: string;
|
|
37
|
+
/** Wrap long lines, for prose such as markdown, instead of scrolling them. */
|
|
38
|
+
wrap?: boolean;
|
|
39
|
+
/** No border or header of its own: for code inside an example or playground frame. */
|
|
40
|
+
bare?: boolean;
|
|
41
|
+
/** Line numbers. On by default for more than one line of code, off for prose. */
|
|
42
|
+
numbered?: boolean;
|
|
43
|
+
}) {
|
|
44
|
+
const [html, setHtml] = useState<string | null>(null);
|
|
45
|
+
const lines = code.split("\n").length;
|
|
46
|
+
const [open, setOpen] = useState(lines <= FOLD);
|
|
47
|
+
const numbers = numbered ?? (!wrap && lines > 1);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let live = true;
|
|
51
|
+
import("shiki")
|
|
52
|
+
.then(({ codeToHtml }) => codeToHtml(code, { lang, themes: { light: "github-light", dark: "github-dark" } }))
|
|
53
|
+
.then((out) => live && setHtml(out))
|
|
54
|
+
.catch(() => live && setHtml(null));
|
|
55
|
+
return () => {
|
|
56
|
+
live = false;
|
|
57
|
+
};
|
|
58
|
+
}, [code, lang]);
|
|
59
|
+
|
|
60
|
+
const body = cx(
|
|
61
|
+
"overflow-x-auto px-4 py-3.5 font-mono text-sm leading-relaxed",
|
|
62
|
+
wrap && "[&_pre]:break-words [&_pre]:whitespace-pre-wrap",
|
|
63
|
+
// room for the copy button floating at the top right
|
|
64
|
+
bare && "pr-12",
|
|
65
|
+
!open && "max-h-96 overflow-y-hidden",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<figure
|
|
70
|
+
data-slot="code-view"
|
|
71
|
+
data-numbered={numbers || undefined}
|
|
72
|
+
className={cx("relative min-w-0", !bare && "overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground")}
|
|
73
|
+
>
|
|
74
|
+
<style>{THEME_CSS}</style>
|
|
75
|
+
{!bare && (
|
|
76
|
+
<figcaption className="flex min-h-11 items-center justify-between gap-3 border-b border-border bg-muted/40 py-1 pr-1.5 pl-4 text-xs text-muted-foreground">
|
|
77
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
78
|
+
<FileCode aria-hidden className="size-(--dt-size-xs) shrink-0" />
|
|
79
|
+
<span className="truncate font-medium text-canvas-foreground">{label}</span>
|
|
80
|
+
</span>
|
|
81
|
+
<CopyButton text={code} label="Copy code" />
|
|
82
|
+
</figcaption>
|
|
83
|
+
)}
|
|
84
|
+
<div className="relative">
|
|
85
|
+
{bare && (
|
|
86
|
+
<span className="absolute top-1.5 right-1.5 z-10">
|
|
87
|
+
<CopyButton text={code} label="Copy code" />
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
{html ? (
|
|
91
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki's output for code from the manifest, escaped by Shiki
|
|
92
|
+
<div className={body} tabIndex={0} role="region" aria-label={label} dangerouslySetInnerHTML={{ __html: html }} />
|
|
93
|
+
) : (
|
|
94
|
+
<pre tabIndex={0} aria-label={label} className={cx(body, wrap && "break-words whitespace-pre-wrap")}>
|
|
95
|
+
<code>{code}</code>
|
|
96
|
+
</pre>
|
|
97
|
+
)}
|
|
98
|
+
{!open && (
|
|
99
|
+
<div className="absolute inset-x-0 bottom-0 flex h-24 items-end justify-center bg-linear-to-t from-surface via-surface/80 to-transparent pb-3">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => setOpen(true)}
|
|
103
|
+
className="inline-flex min-h-(--docs-target,2.75rem) cursor-pointer items-center gap-1.5 rounded-full border border-border bg-surface px-4 text-xs font-medium text-surface-foreground shadow-sm hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring"
|
|
104
|
+
>
|
|
105
|
+
Show all {lines} lines <ChevronDown aria-hidden className="size-(--dt-size-xs)" />
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</figure>
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/css.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
package/src/examples.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CircleCheck, CircleX, Code, Eye, Maximize2, Moon, Smartphone, Sun } from "lucide-react";
|
|
4
|
+
import { type ReactNode, useState } from "react";
|
|
5
|
+
import { CodeView } from "./code-view";
|
|
6
|
+
import { Boundary } from "./lib/boundary";
|
|
7
|
+
import { cx } from "./lib/cx";
|
|
8
|
+
import { Missing, renderExample, useLookup } from "./lib/lookup";
|
|
9
|
+
import type { ComponentEntry, ExampleRef } from "./lib/manifest-types";
|
|
10
|
+
import { inline } from "./lib/text";
|
|
11
|
+
import { CopyButton, IconButton, IconLink, Segmented, SegmentedList, SegmentedPanel, Stage, Toolbar } from "./lib/ui";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The canonical example first, the one to copy, then each do beside its don't.
|
|
15
|
+
* Every example is the real component; the code behind it is the code that drew it.
|
|
16
|
+
*/
|
|
17
|
+
export function Examples({ entry, previewHref }: { entry: ComponentEntry; previewHref?: string }) {
|
|
18
|
+
const lookup = useLookup();
|
|
19
|
+
const ex = entry.examples;
|
|
20
|
+
if (!ex) {
|
|
21
|
+
return (
|
|
22
|
+
<p data-slot="examples-empty" className="rounded-xl border border-dashed border-border p-6 text-sm text-muted-foreground">
|
|
23
|
+
No examples yet. Add <code className="font-mono text-xs">{entry.source.replace(/\.tsx$/, ".examples.tsx")}</code> with a{" "}
|
|
24
|
+
<code className="font-mono text-xs">canonical</code> export and do and don't pairs.
|
|
25
|
+
</p>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const module = lookup.examplesFor(entry);
|
|
29
|
+
if (!module) return <Missing name={entry.name} what="examples" />;
|
|
30
|
+
|
|
31
|
+
const pairs = Array.from({ length: Math.max(ex.do.length, ex.dont.length) }, (_, i) => [ex.do[i], ex.dont[i]] as const);
|
|
32
|
+
const frame = (ref: ExampleRef, kind: Kind) => (
|
|
33
|
+
<ExampleFrame
|
|
34
|
+
key={ref.name}
|
|
35
|
+
name={ref.name}
|
|
36
|
+
kind={kind}
|
|
37
|
+
description={ref.description}
|
|
38
|
+
code={ref.code}
|
|
39
|
+
href={previewHref && `${previewHref}/${encodeURIComponent(ref.name)}`}
|
|
40
|
+
>
|
|
41
|
+
{renderExample(module[ref.name], ref.name)}
|
|
42
|
+
</ExampleFrame>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div data-slot="examples" className="flex flex-col gap-10">
|
|
47
|
+
{ex.canonical && frame(ex.canonical, "canonical")}
|
|
48
|
+
{pairs.length > 0 && (
|
|
49
|
+
<div className="flex flex-col gap-6">
|
|
50
|
+
{pairs.map(([d, n], i) => (
|
|
51
|
+
<div key={d?.name ?? n?.name ?? i} data-slot="examples-pair" className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
52
|
+
{d ? frame(d, "do") : <span />}
|
|
53
|
+
{n ? frame(n, "dont") : <span />}
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
{ex.other.length > 0 && <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">{ex.other.map((o) => frame(o, "other"))}</div>}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Kind = "canonical" | "do" | "dont" | "other";
|
|
64
|
+
|
|
65
|
+
const MARK: Record<Kind, { label: string; icon?: typeof CircleCheck; tone: string }> = {
|
|
66
|
+
// the canonical example is simply first, the usual use; it needs no badge to say so
|
|
67
|
+
canonical: { label: "", tone: "" },
|
|
68
|
+
// a measured pairing, never a fill colour used as text (text-success read 3.9:1 on the canvas)
|
|
69
|
+
do: { label: "Do", icon: CircleCheck, tone: "bg-success-subdued text-success-subdued-foreground" },
|
|
70
|
+
dont: { label: "Don't", icon: CircleX, tone: "bg-destructive-subdued text-destructive-subdued-foreground" },
|
|
71
|
+
other: { label: "", tone: "" },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One example in its frame: Preview and Code on a switch, the stage in either mode
|
|
76
|
+
* or at a phone's width, a copy button, and a link to it alone. The caption says
|
|
77
|
+
* what kind of example it is and why. Patterns use it for their compositions too.
|
|
78
|
+
*/
|
|
79
|
+
export function ExampleFrame({
|
|
80
|
+
name,
|
|
81
|
+
kind = "other",
|
|
82
|
+
description,
|
|
83
|
+
code,
|
|
84
|
+
href,
|
|
85
|
+
children,
|
|
86
|
+
}: {
|
|
87
|
+
name: string;
|
|
88
|
+
kind?: Kind;
|
|
89
|
+
description?: string;
|
|
90
|
+
code?: string;
|
|
91
|
+
href?: string;
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
}) {
|
|
94
|
+
const [theme, setTheme] = useState<"light" | "dark" | undefined>();
|
|
95
|
+
const [narrow, setNarrow] = useState(false);
|
|
96
|
+
const mark = MARK[kind];
|
|
97
|
+
const Icon = mark.icon;
|
|
98
|
+
const views = [{ value: "preview", label: "Preview", icon: Eye }, ...(code ? [{ value: "code", label: "Code", icon: Code }] : [])];
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<figure data-slot="example" data-kind={kind} className="flex min-w-0 flex-col gap-3">
|
|
102
|
+
<Segmented defaultValue="preview" className="overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground shadow-xs">
|
|
103
|
+
<Toolbar>
|
|
104
|
+
<SegmentedList items={views} label={`${name}: preview or code`} />
|
|
105
|
+
<span className="flex items-center gap-0.5">
|
|
106
|
+
<IconButton icon={Smartphone} label="Phone width" pressed={narrow} onClick={() => setNarrow(!narrow)} />
|
|
107
|
+
<IconButton
|
|
108
|
+
icon={theme === "dark" ? Sun : Moon}
|
|
109
|
+
label={theme === "dark" ? "Show in light mode" : "Show in dark mode"}
|
|
110
|
+
pressed={theme !== undefined}
|
|
111
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
112
|
+
/>
|
|
113
|
+
{code && <CopyButton text={code} label="Copy code" />}
|
|
114
|
+
{href && <IconLink icon={Maximize2} label="Open on its own" href={href} />}
|
|
115
|
+
</span>
|
|
116
|
+
</Toolbar>
|
|
117
|
+
<SegmentedPanel value="preview">
|
|
118
|
+
<Stage theme={theme} width={narrow ? 375 : undefined} className={kind === "canonical" ? "min-h-64" : "min-h-48"}>
|
|
119
|
+
<Boundary label={name}>{children}</Boundary>
|
|
120
|
+
</Stage>
|
|
121
|
+
</SegmentedPanel>
|
|
122
|
+
{code && (
|
|
123
|
+
<SegmentedPanel value="code">
|
|
124
|
+
<CodeView code={code} label={name} bare />
|
|
125
|
+
</SegmentedPanel>
|
|
126
|
+
)}
|
|
127
|
+
</Segmented>
|
|
128
|
+
{(mark.label || description) && (
|
|
129
|
+
<figcaption className="flex items-start gap-2 px-1">
|
|
130
|
+
{mark.label && (
|
|
131
|
+
<span className={cx("inline-flex h-6 shrink-0 items-center gap-1.5 rounded-full pr-2.5 pl-2 text-xs font-medium", mark.tone)}>
|
|
132
|
+
{Icon && <Icon aria-hidden className="size-3.5" />}
|
|
133
|
+
{mark.label}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
{description && <span className="pt-0.5 text-sm">{inline(description)}</span>}
|
|
137
|
+
</figcaption>
|
|
138
|
+
)}
|
|
139
|
+
</figure>
|
|
140
|
+
);
|
|
141
|
+
}
|
package/src/glossary.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowRight, Search } from "lucide-react";
|
|
4
|
+
import { useId, useState } from "react";
|
|
5
|
+
import type { TermEntry } from "./lib/manifest-types";
|
|
6
|
+
import { TermRow } from "./lib/terms";
|
|
7
|
+
|
|
8
|
+
/** Every name in the system, searchable by its wrong forms too, so a wrong name finds the right one. A to Z. */
|
|
9
|
+
export function Glossary({ terms }: { terms: TermEntry[] }) {
|
|
10
|
+
const id = useId();
|
|
11
|
+
const [query, setQuery] = useState("");
|
|
12
|
+
const q = query.trim().toLowerCase();
|
|
13
|
+
const matches = (q ? terms.filter((t) => t.name.toLowerCase().includes(q) || t.wrong?.some((w) => w.toLowerCase().includes(q))) : terms)
|
|
14
|
+
.slice()
|
|
15
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
|
|
16
|
+
const corrected = q ? terms.find((t) => t.wrong?.some((w) => w.toLowerCase() === q)) : undefined;
|
|
17
|
+
const letters = new Map<string, TermEntry[]>();
|
|
18
|
+
for (const t of matches) {
|
|
19
|
+
const l = /[a-z]/i.test(t.name[0]) ? t.name[0].toUpperCase() : "#";
|
|
20
|
+
letters.set(l, [...(letters.get(l) ?? []), t]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div data-slot="glossary" className="flex flex-col gap-10">
|
|
25
|
+
<div className="flex max-w-3xl flex-col gap-4">
|
|
26
|
+
<label htmlFor={id} className="sr-only">
|
|
27
|
+
Find a term
|
|
28
|
+
</label>
|
|
29
|
+
<div className="relative">
|
|
30
|
+
<Search aria-hidden className="pointer-events-none absolute top-1/2 left-0 size-(--dt-size-md) -translate-y-1/2 text-muted-foreground" />
|
|
31
|
+
<input
|
|
32
|
+
id={id}
|
|
33
|
+
type="search"
|
|
34
|
+
value={query}
|
|
35
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
36
|
+
placeholder="Find a term, or a name you half remember"
|
|
37
|
+
className="h-16 w-full border-0 border-b border-border bg-transparent pr-2 pl-10 text-2xl tracking-tight placeholder:text-muted-foreground/70 hover:border-input focus-visible:border-b-2 focus-visible:border-canvas-foreground focus-visible:outline-none"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
<div aria-live="polite">
|
|
41
|
+
{corrected ? (
|
|
42
|
+
<p data-slot="glossary-correction" className="flex flex-wrap items-center gap-2 rounded-xl bg-muted/60 px-5 py-4 text-lg">
|
|
43
|
+
Say <strong className="font-semibold">{corrected.name}</strong>
|
|
44
|
+
<ArrowRight aria-hidden className="size-4 text-muted-foreground" />
|
|
45
|
+
<span className="text-muted-foreground">not “{query.trim()}”</span>
|
|
46
|
+
</p>
|
|
47
|
+
) : (
|
|
48
|
+
<p className="text-sm text-muted-foreground">
|
|
49
|
+
{q ? `${matches.length} ${matches.length === 1 ? "term" : "terms"}` : `${terms.length} terms, A to Z. Search by the right name or a wrong one.`}
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div data-slot="glossary-list" className="flex flex-col gap-8">
|
|
55
|
+
{[...letters].map(([letter, list]) => (
|
|
56
|
+
<section key={letter} aria-label={letter} className="grid gap-x-8 lg:grid-cols-[3rem_minmax(0,1fr)]">
|
|
57
|
+
<h3 className="pt-5 text-2xl tracking-tight text-muted-foreground lg:sticky lg:top-24 lg:self-start">{letter}</h3>
|
|
58
|
+
<div>
|
|
59
|
+
<dl>
|
|
60
|
+
{list.map((t) => (
|
|
61
|
+
<TermRow key={`${t.kind}:${t.name}`} term={t} kind />
|
|
62
|
+
))}
|
|
63
|
+
</dl>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
package/src/imagery.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLookup } from "./lib/lookup";
|
|
4
|
+
import type { AssetEntry } from "./lib/manifest-types";
|
|
5
|
+
import { inline } from "./lib/text";
|
|
6
|
+
import { Stage } from "./lib/ui";
|
|
7
|
+
|
|
8
|
+
/** The approved icons and imagery, each on a stage with how to use it; an icon is shown at its three sizes too. */
|
|
9
|
+
export function Imagery({ assets, kinds = ["icon", "image", "other"] }: { assets: AssetEntry[]; kinds?: AssetEntry["kind"][] }) {
|
|
10
|
+
const lookup = useLookup();
|
|
11
|
+
const shown = assets.filter((a) => kinds.includes(a.kind));
|
|
12
|
+
if (shown.length === 0) return <p className="text-sm text-muted-foreground">No icons or imagery in the brand folder yet.</p>;
|
|
13
|
+
return (
|
|
14
|
+
<ul data-slot="imagery" className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
15
|
+
{shown.map((a) => {
|
|
16
|
+
const src = lookup.assetUrl(a);
|
|
17
|
+
const icon = a.kind === "icon";
|
|
18
|
+
return (
|
|
19
|
+
<li key={a.file} className="min-w-0">
|
|
20
|
+
<figure data-slot="asset" className="flex h-full flex-col overflow-hidden rounded-2xl border border-border bg-surface text-surface-foreground shadow-xs">
|
|
21
|
+
<Stage className="h-44 p-6">
|
|
22
|
+
{src ? (
|
|
23
|
+
icon ? (
|
|
24
|
+
// icons drawn in currentColor show in the stage's text colour, in either mode
|
|
25
|
+
<span
|
|
26
|
+
aria-hidden={!a.alt}
|
|
27
|
+
role={a.alt ? "img" : undefined}
|
|
28
|
+
aria-label={a.alt || undefined}
|
|
29
|
+
className="block size-14 bg-current"
|
|
30
|
+
style={{ mask: `url(${src}) center / contain no-repeat`, WebkitMask: `url(${src}) center / contain no-repeat` }}
|
|
31
|
+
/>
|
|
32
|
+
) : (
|
|
33
|
+
<img src={src} alt={a.alt ?? ""} className="max-h-full max-w-full rounded-md object-contain" />
|
|
34
|
+
)
|
|
35
|
+
) : (
|
|
36
|
+
<span className="font-mono text-xs text-muted-foreground">{a.file}</span>
|
|
37
|
+
)}
|
|
38
|
+
</Stage>
|
|
39
|
+
<figcaption className="flex flex-1 flex-col gap-2 border-t border-border p-4 text-sm">
|
|
40
|
+
<span className="flex items-center justify-between gap-2">
|
|
41
|
+
<span className="text-lg">{a.name}</span>
|
|
42
|
+
{icon && src ? (
|
|
43
|
+
<span aria-hidden className="flex items-end gap-2 text-muted-foreground">
|
|
44
|
+
{[16, 20, 24].map((size) => (
|
|
45
|
+
<span key={size} className="block bg-current" style={{ width: size, height: size, mask: `url(${src}) center / contain no-repeat`, WebkitMask: `url(${src}) center / contain no-repeat` }} />
|
|
46
|
+
))}
|
|
47
|
+
</span>
|
|
48
|
+
) : (
|
|
49
|
+
<span className="rounded-full bg-muted px-2 py-0.5 font-mono text-xs text-muted-foreground">{a.kind}</span>
|
|
50
|
+
)}
|
|
51
|
+
</span>
|
|
52
|
+
<span className="text-muted-foreground">{a.usage ? inline(a.usage) : "No usage notes yet."}</span>
|
|
53
|
+
<span className="mt-auto pt-1 font-mono text-xs text-muted-foreground">
|
|
54
|
+
{a.file}
|
|
55
|
+
{a.width && a.height ? ` · ${a.width} × ${a.height}` : ""}
|
|
56
|
+
</span>
|
|
57
|
+
</figcaption>
|
|
58
|
+
</figure>
|
|
59
|
+
</li>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</ul>
|
|
63
|
+
);
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @designtools/docs: every block, for a project composing its own pages. Most
|
|
3
|
+
* projects use `@designtools/docs/next` instead, which composes them.
|
|
4
|
+
*/
|
|
5
|
+
export { A11yPanel, AxeReport, useAxe } from "./a11y-panel";
|
|
6
|
+
export { AdherenceSummary } from "./adherence-summary";
|
|
7
|
+
export { AgentInstructions, AgentView } from "./agent-view";
|
|
8
|
+
export { Anatomy } from "./anatomy";
|
|
9
|
+
export { AskClaude } from "./ask-claude";
|
|
10
|
+
export { CodeView } from "./code-view";
|
|
11
|
+
export { ExampleFrame, Examples } from "./examples";
|
|
12
|
+
export { Glossary } from "./glossary";
|
|
13
|
+
export { Imagery } from "./imagery";
|
|
14
|
+
export { LogoUsage } from "./logo-usage";
|
|
15
|
+
export { PageActions } from "./page-actions";
|
|
16
|
+
export { Pattern } from "./pattern";
|
|
17
|
+
export { Playground } from "./playground";
|
|
18
|
+
export { PreviewComposition, PreviewExample, PreviewFrame, PreviewSurface } from "./preview-frame";
|
|
19
|
+
export { PropsTable } from "./props-table";
|
|
20
|
+
export { Rule, RuleIndex } from "./rule";
|
|
21
|
+
export { Scale } from "./scale";
|
|
22
|
+
export { DocsSearch } from "./search";
|
|
23
|
+
export { type DocsLink, DocsFooter, DocsHeader, DocsSection, DocsShell } from "./shell";
|
|
24
|
+
export { Standards } from "./standards";
|
|
25
|
+
export { PageContrast, RampSwatches, SemanticSwatches } from "./swatches";
|
|
26
|
+
export { ThemeSwitch, themeScript, useThemeScript } from "./theme-switch";
|
|
27
|
+
export { TypeRamp } from "./type-ramp";
|
|
28
|
+
export { Usage } from "./usage";
|
|
29
|
+
export { VariantMatrix } from "./variant-matrix";
|
|
30
|
+
export { VoiceTerms } from "./voice-terms";
|
|
31
|
+
export { Workbench } from "./workbench";
|
|
32
|
+
export { Live } from "./lib/live";
|
|
33
|
+
export { type Registry, RegistryProvider, useRegistry } from "./lib/registry";
|
|
34
|
+
export * from "./lib/manifest-types";
|
|
35
|
+
export type { AdherenceReport } from "./lib/adherence-types";
|