@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,136 @@
|
|
|
1
|
+
import { findBySlug, readComponents, readTokens, slugOf } from "../lib/manifest.js";
|
|
2
|
+
export function siteOf(config) {
|
|
3
|
+
const hidden = new Set(config.hide ?? []);
|
|
4
|
+
return {
|
|
5
|
+
config,
|
|
6
|
+
base: (config.basePath ?? "/design/system").replace(/\/$/, ""),
|
|
7
|
+
title: config.title,
|
|
8
|
+
components: readComponents(config.manifest.components),
|
|
9
|
+
tokens: readTokens(config.manifest.tokens),
|
|
10
|
+
rules: config.manifest.rules ?? { rules: [] },
|
|
11
|
+
patterns: config.manifest.patterns ?? { patterns: [] },
|
|
12
|
+
taxonomy: config.manifest.taxonomy ?? { terms: [] },
|
|
13
|
+
assets: config.manifest.assets ?? { assets: [] },
|
|
14
|
+
editorial: config.editorial ?? [],
|
|
15
|
+
shows: (page) => !hidden.has(page),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const BUILT_IN = {
|
|
19
|
+
"brand/logo": "logo",
|
|
20
|
+
"brand/imagery": "imagery",
|
|
21
|
+
"brand/voice": "voice",
|
|
22
|
+
colour: "colour",
|
|
23
|
+
type: "type",
|
|
24
|
+
scale: "scale",
|
|
25
|
+
rules: "rules",
|
|
26
|
+
standards: "standards",
|
|
27
|
+
vocabulary: "vocabulary",
|
|
28
|
+
};
|
|
29
|
+
/** Whether a built-in page has anything to show, as well as not being hidden. */
|
|
30
|
+
function has(site, kind) {
|
|
31
|
+
return site.shows(kind) && KIND_NEEDS[kind](site);
|
|
32
|
+
}
|
|
33
|
+
const KIND_NEEDS = {
|
|
34
|
+
logo: (s) => s.assets.assets.some((a) => a.kind === "logo"),
|
|
35
|
+
imagery: (s) => s.assets.assets.some((a) => a.kind !== "logo"),
|
|
36
|
+
voice: (s) => s.taxonomy.terms.some((t) => t.kind === "product" || t.wrong?.length),
|
|
37
|
+
colour: () => true,
|
|
38
|
+
type: () => true,
|
|
39
|
+
scale: () => true,
|
|
40
|
+
rules: (s) => s.rules.rules.length > 0,
|
|
41
|
+
standards: (s) => !!s.config.baselines || !!s.config.adherence,
|
|
42
|
+
vocabulary: (s) => s.taxonomy.terms.length > 0,
|
|
43
|
+
};
|
|
44
|
+
/** The page at a path under the docs, or nothing. */
|
|
45
|
+
export function resolve(site, slug = []) {
|
|
46
|
+
const path = slug.join("/");
|
|
47
|
+
if (!path)
|
|
48
|
+
return { kind: "overview" };
|
|
49
|
+
const editorial = site.editorial.find((p) => p.slug === path);
|
|
50
|
+
if (editorial)
|
|
51
|
+
return { kind: "editorial", page: editorial };
|
|
52
|
+
const builtIn = BUILT_IN[path];
|
|
53
|
+
if (builtIn)
|
|
54
|
+
return has(site, builtIn) ? { kind: builtIn } : undefined;
|
|
55
|
+
const [section, id] = slug;
|
|
56
|
+
if (section === "components" && id && site.shows("components")) {
|
|
57
|
+
const entry = findBySlug(site.components, id);
|
|
58
|
+
return entry ? { kind: "component", entry } : undefined;
|
|
59
|
+
}
|
|
60
|
+
if (section === "patterns" && id && site.shows("patterns")) {
|
|
61
|
+
const pattern = site.patterns.patterns.find((p) => p.id === id);
|
|
62
|
+
return pattern ? { kind: "pattern", pattern } : undefined;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Every page's path, for static generation. */
|
|
67
|
+
export function allSlugs(site) {
|
|
68
|
+
const out = [[]];
|
|
69
|
+
for (const p of site.editorial)
|
|
70
|
+
out.push(p.slug.split("/"));
|
|
71
|
+
for (const [path, kind] of Object.entries(BUILT_IN))
|
|
72
|
+
if (has(site, kind))
|
|
73
|
+
out.push(path.split("/"));
|
|
74
|
+
if (site.shows("components"))
|
|
75
|
+
for (const c of site.components.components)
|
|
76
|
+
out.push(["components", slugOf(c)]);
|
|
77
|
+
if (site.shows("patterns"))
|
|
78
|
+
for (const p of site.patterns.patterns)
|
|
79
|
+
out.push(["patterns", p.id]);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/** The navigation's editorial and generated groups: brand, foundations, standards. Rules, patterns and components the shell adds from the manifest. */
|
|
83
|
+
export function navOf(site) {
|
|
84
|
+
const link = (label, path) => ({ label, href: `${site.base}/${path}` });
|
|
85
|
+
const written = (section) => site.editorial.filter((p) => (p.section ?? "brand") === section).map((p) => link(p.nav ?? p.title, p.slug));
|
|
86
|
+
return {
|
|
87
|
+
brand: [
|
|
88
|
+
...written("brand"),
|
|
89
|
+
...(has(site, "logo") ? [link("Logo", "brand/logo")] : []),
|
|
90
|
+
...(has(site, "imagery") ? [link("Imagery and icons", "brand/imagery")] : []),
|
|
91
|
+
...(has(site, "voice") ? [link("Voice and terms", "brand/voice")] : []),
|
|
92
|
+
],
|
|
93
|
+
foundations: [...written("foundations"), link("Colour", "colour"), link("Type", "type"), link("Scale", "scale")].filter((l) => !["colour", "type", "scale"].some((k) => l.href.endsWith(`/${k}`) && !site.shows(k))),
|
|
94
|
+
standards: [
|
|
95
|
+
...written("standards"),
|
|
96
|
+
...(has(site, "standards") ? [link("Standards", "standards")] : []),
|
|
97
|
+
...(has(site, "vocabulary") ? [link("Vocabulary", "vocabulary")] : []),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Each built-in page's title and lead: what the page says it is, which is also its description. */
|
|
102
|
+
export const COPY = {
|
|
103
|
+
logo: { title: "Logo", lead: "Each logo on the surfaces it may sit on, at its smallest, with its clear space." },
|
|
104
|
+
imagery: { title: "Imagery and icons", lead: "The approved icons and imagery, and how to use each." },
|
|
105
|
+
voice: { title: "Voice and terms", lead: "The words we use, beside the ones we do not." },
|
|
106
|
+
colour: {
|
|
107
|
+
title: "Colour",
|
|
108
|
+
lead: "Generated from the brand colour by @designtools/tokens. Write the semantic names in components; the ramps are what they point at.",
|
|
109
|
+
},
|
|
110
|
+
type: { title: "Type", lead: "Each family, then the scale: a size is never set without its leading." },
|
|
111
|
+
scale: { title: "Scale", lead: "Space, size, radius, elevation and motion, each drawn the way it is used." },
|
|
112
|
+
rules: {
|
|
113
|
+
title: "Rules",
|
|
114
|
+
lead: "Brand and interface rules in one format, each with the check that proves it. Written for people; agents read the same rule.",
|
|
115
|
+
},
|
|
116
|
+
standards: {
|
|
117
|
+
title: "Standards",
|
|
118
|
+
lead: "The budgets and targets the project holds itself to, read from .mxa/stack.json, where the checks read them too.",
|
|
119
|
+
},
|
|
120
|
+
vocabulary: { title: "Vocabulary", lead: "Every name in the system. Search by a wrong name and it finds the right one." },
|
|
121
|
+
};
|
|
122
|
+
/** A page's title and description, for its metadata. */
|
|
123
|
+
export function metaOf(site, ref) {
|
|
124
|
+
switch (ref.kind) {
|
|
125
|
+
case "overview":
|
|
126
|
+
return { title: site.title, description: site.config.lead ?? `${site.title}: the brand and the design system in one place.` };
|
|
127
|
+
case "editorial":
|
|
128
|
+
return { title: ref.page.title, description: ref.page.lead };
|
|
129
|
+
case "component":
|
|
130
|
+
return { title: `${ref.entry.name} · Components`, description: ref.entry.description };
|
|
131
|
+
case "pattern":
|
|
132
|
+
return { title: `${ref.pattern.name} · Patterns`, description: ref.pattern.description };
|
|
133
|
+
default:
|
|
134
|
+
return { title: COPY[ref.kind].title, description: COPY[ref.kind].lead };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a reader can take from a page, as one split button: copy the page as
|
|
3
|
+
* markdown, the same markdown any agent gets at `.md`, with a menu beside it to
|
|
4
|
+
* view that markdown, copy the whole system as one file, or open the manifest.
|
|
5
|
+
* Put it in a `DocsHeader`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function PageActions({ basePath }: {
|
|
8
|
+
basePath?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Menu } from "@base-ui/react/menu";
|
|
4
|
+
import { Braces, Check, ChevronDown, Copy, FileText, Files } from "lucide-react";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
/**
|
|
8
|
+
* What a reader can take from a page, as one split button: copy the page as
|
|
9
|
+
* markdown, the same markdown any agent gets at `.md`, with a menu beside it to
|
|
10
|
+
* view that markdown, copy the whole system as one file, or open the manifest.
|
|
11
|
+
* Put it in a `DocsHeader`.
|
|
12
|
+
*/
|
|
13
|
+
export function PageActions({ basePath = "/design/system" }) {
|
|
14
|
+
const path = usePathname().replace(/\/$/, "");
|
|
15
|
+
const [done, setDone] = useState(null);
|
|
16
|
+
const copyFrom = async (url, what) => {
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(url);
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
throw new Error(String(res.status));
|
|
21
|
+
await navigator.clipboard.writeText(await res.text());
|
|
22
|
+
setDone(what);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
setDone("Could not copy");
|
|
26
|
+
}
|
|
27
|
+
setTimeout(() => setDone(null), 1600);
|
|
28
|
+
};
|
|
29
|
+
const item = "flex min-h-(--docs-target,2.75rem) cursor-pointer items-center gap-3 rounded-md px-3 text-sm outline-none select-none data-highlighted:bg-muted";
|
|
30
|
+
const icon = "size-4 shrink-0 text-muted-foreground";
|
|
31
|
+
return (_jsxs("div", { "data-slot": "page-actions", className: "inline-flex items-stretch rounded-lg border border-border bg-canvas text-sm shadow-xs", children: [_jsxs("button", { type: "button", onClick: () => copyFrom(`${path}.md`, "Page copied"), className: "inline-flex min-h-9 cursor-pointer items-center gap-2 rounded-l-lg py-1.5 pr-3 pl-3 font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring", children: [done === "Page copied" ? _jsx(Check, { "aria-hidden": true, className: "size-4" }) : _jsx(Copy, { "aria-hidden": true, className: icon }), done === "Page copied" ? "Copied" : "Copy page"] }), _jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { "aria-label": "More ways to take this page", className: "inline-flex min-h-9 w-9 cursor-pointer items-center justify-center rounded-r-lg border-l border-border text-muted-foreground hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-popup-open:bg-muted", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-4" }) }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { "data-designtools": "", align: "end", sideOffset: 6, className: "z-50", children: _jsxs(Menu.Popup, { className: "w-72 rounded-xl border border-border bg-overlay p-1.5 text-overlay-foreground shadow-lg transition-[opacity,scale] duration-(--dt-duration-fast) data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0", children: [_jsxs(Menu.Item, { className: item, onClick: () => copyFrom(`${path}.md`, "Page copied"), children: [_jsx(Copy, { "aria-hidden": true, className: icon }), _jsxs("span", { className: "flex flex-col", children: ["Copy page", _jsx("span", { className: "text-xs text-muted-foreground", children: "As markdown, for an assistant" })] })] }), _jsxs(Menu.LinkItem, { className: item, href: `${path}.md`, target: "_blank", rel: "noreferrer", children: [_jsx(FileText, { "aria-hidden": true, className: icon }), _jsxs("span", { className: "flex flex-col", children: ["View as markdown", _jsx("span", { className: "text-xs text-muted-foreground", children: "What an agent reads at this address" })] })] }), _jsx(Menu.Separator, { className: "my-1.5 h-px bg-border" }), _jsxs(Menu.Item, { className: item, onClick: () => copyFrom(`${basePath}/context.md`, "System copied"), children: [_jsx(Files, { "aria-hidden": true, className: icon }), _jsxs("span", { className: "flex flex-col", children: ["Copy the whole system", _jsx("span", { className: "text-xs text-muted-foreground", children: "Every page in one markdown file" })] })] }), _jsxs(Menu.LinkItem, { className: item, href: `${basePath}/manifest/components.json`, target: "_blank", rel: "noreferrer", children: [_jsx(Braces, { "aria-hidden": true, className: icon }), _jsxs("span", { className: "flex flex-col", children: ["Open the manifest", _jsx("span", { className: "text-xs text-muted-foreground", children: "Components, tokens and rules as JSON" })] })] })] }) }) })] }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: done ?? "" })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentsManifest, PatternEntry } from "./lib/manifest-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A recurring screen composition: each composition live in its frame, with its code, then the
|
|
4
|
+
* components it is made of, each linking to its page. Its status sits in the page header.
|
|
5
|
+
*/
|
|
6
|
+
export declare function Pattern({ pattern, manifest, basePath, }: {
|
|
7
|
+
pattern: PatternEntry;
|
|
8
|
+
/** To link the components it uses to their pages. */
|
|
9
|
+
manifest?: ComponentsManifest;
|
|
10
|
+
basePath?: string;
|
|
11
|
+
}): import("react").JSX.Element;
|
package/dist/pattern.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { ExampleFrame } from "./examples.js";
|
|
5
|
+
import { Missing, renderExample, useLookup } from "./lib/lookup.js";
|
|
6
|
+
import { slugOf } from "./lib/manifest.js";
|
|
7
|
+
import { StatusBadge } from "./lib/status.js";
|
|
8
|
+
import { DocsSection } from "./shell.js";
|
|
9
|
+
/**
|
|
10
|
+
* A recurring screen composition: each composition live in its frame, with its code, then the
|
|
11
|
+
* components it is made of, each linking to its page. Its status sits in the page header.
|
|
12
|
+
*/
|
|
13
|
+
export function Pattern({ pattern, manifest, basePath = "/design/system", }) {
|
|
14
|
+
const lookup = useLookup();
|
|
15
|
+
const module = lookup.compositionsFor(pattern);
|
|
16
|
+
const known = new Map((manifest?.components ?? []).map((c) => [c.name, c]));
|
|
17
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-slot": "pattern", className: "flex flex-col gap-10", children: !module ? (_jsx(Missing, { name: pattern.name, what: "compositions" })) : (pattern.examples.map((ex, i) => (_jsx(ExampleFrame, { name: ex.name, kind: i === 0 ? "canonical" : "other", description: ex.description, code: ex.code, children: renderExample(module[ex.name], ex.name) }, ex.name)))) }), pattern.components.length > 0 && (_jsx(DocsSection, { title: "Made of", lead: "The system's own components, so the pattern changes when they do.", children: _jsx("ul", { "data-slot": "pattern-parts", className: "grid gap-x-10 sm:grid-cols-2", children: pattern.components.map((name) => {
|
|
18
|
+
const c = known.get(name);
|
|
19
|
+
const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-lg tracking-tight underline-offset-4 group-hover:underline", children: name }), c && _jsx(StatusBadge, { status: c.status })] }), c?.description && _jsx("span", { className: "line-clamp-2 text-sm text-muted-foreground", children: c.description })] }));
|
|
20
|
+
return (_jsx("li", { className: "border-t border-border", children: c ? (_jsx(Link, { href: `${basePath}/components/${slugOf(c)}`, className: "group flex flex-col gap-1.5 py-4 focus-visible:outline-2 focus-visible:outline-ring", children: body })) : (_jsx("span", { className: "flex flex-col gap-1.5 py-4", children: body })) }, name));
|
|
21
|
+
}) }) }))] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ComponentEntry, VariantAxis } from "./lib/manifest-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The component, live, with controls generated from its variants and its own
|
|
5
|
+
* boolean props, and the JSX for exactly what is showing. Pass `props` for what
|
|
6
|
+
* it needs to render at all; the controls change everything else.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Playground({ entry, props: base, children: initialText, }: {
|
|
9
|
+
entry: ComponentEntry;
|
|
10
|
+
props?: Record<string, unknown>;
|
|
11
|
+
/** Text content, editable in the playground. Pass "" for a component without children. */
|
|
12
|
+
children?: string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
/** Props passed only so the component renders (handlers, data) are left out of the snippet unless they are short. */
|
|
15
|
+
export declare function visible(props: Record<string, unknown>): Record<string, unknown>;
|
|
16
|
+
/** `variant` → `Variant`, `aria-label` → `Aria label`: a control reads as a setting, not a prop name. */
|
|
17
|
+
export declare function title(name: string): string;
|
|
18
|
+
/** A control's label, with the prop it sets and what its options mean behind an info button. */
|
|
19
|
+
export declare function Label({ id, name, info, htmlFor }: {
|
|
20
|
+
id?: string;
|
|
21
|
+
name: string;
|
|
22
|
+
info?: ReactNode;
|
|
23
|
+
htmlFor?: string;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
export declare function AxisControl({ axis, value, onChange }: {
|
|
26
|
+
axis: VariantAxis;
|
|
27
|
+
value: string;
|
|
28
|
+
onChange: (v: string) => void;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
30
|
+
export declare function FlagControl({ name, checked, onChange }: {
|
|
31
|
+
name: string;
|
|
32
|
+
checked: boolean;
|
|
33
|
+
onChange: (v: boolean) => void;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
35
|
+
export declare function TextControl({ value, onChange }: {
|
|
36
|
+
value: string;
|
|
37
|
+
onChange: (v: string) => void;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
/** A number prop: a field with steppers. */
|
|
40
|
+
export declare function NumberControl({ name, value, onChange, info }: {
|
|
41
|
+
name: string;
|
|
42
|
+
value: number | undefined;
|
|
43
|
+
onChange: (v: number | undefined) => void;
|
|
44
|
+
info?: ReactNode;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
46
|
+
/** A string prop: a text field. */
|
|
47
|
+
export declare function StringControl({ name, value, onChange, info }: {
|
|
48
|
+
name: string;
|
|
49
|
+
value: string;
|
|
50
|
+
onChange: (v: string) => void;
|
|
51
|
+
info?: ReactNode;
|
|
52
|
+
}): import("react").JSX.Element;
|
|
53
|
+
/**
|
|
54
|
+
* An array or object prop (a Select's options): JSON, applied while it parses,
|
|
55
|
+
* and said plainly when it does not, so a half-typed edit never breaks the preview.
|
|
56
|
+
*/
|
|
57
|
+
export declare function JsonControl({ name, value, onChange, info }: {
|
|
58
|
+
name: string;
|
|
59
|
+
value: unknown;
|
|
60
|
+
onChange: (v: unknown) => void;
|
|
61
|
+
info?: ReactNode;
|
|
62
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Select } from "@base-ui/react/select";
|
|
4
|
+
import { Switch } from "@base-ui/react/switch";
|
|
5
|
+
import { Toggle } from "@base-ui/react/toggle";
|
|
6
|
+
import { ToggleGroup } from "@base-ui/react/toggle-group";
|
|
7
|
+
import { Tooltip } from "@base-ui/react/tooltip";
|
|
8
|
+
import { Check, ChevronDown, Info, Moon, RotateCcw, SlidersHorizontal, Sun } from "lucide-react";
|
|
9
|
+
import { useId, useState } from "react";
|
|
10
|
+
import { CodeView } from "./code-view.js";
|
|
11
|
+
import { Boundary } from "./lib/boundary.js";
|
|
12
|
+
import { toJSX } from "./lib/jsx.js";
|
|
13
|
+
import { coerce, Missing, useLookup } from "./lib/lookup.js";
|
|
14
|
+
import { Frame, IconButton, Stage, Toolbar } from "./lib/ui.js";
|
|
15
|
+
/**
|
|
16
|
+
* The component, live, with controls generated from its variants and its own
|
|
17
|
+
* boolean props, and the JSX for exactly what is showing. Pass `props` for what
|
|
18
|
+
* it needs to render at all; the controls change everything else.
|
|
19
|
+
*/
|
|
20
|
+
export function Playground({ entry, props: base = {}, children: initialText = entry.name, }) {
|
|
21
|
+
const lookup = useLookup();
|
|
22
|
+
const Component = lookup.componentFor(entry);
|
|
23
|
+
const axes = entry.variants?.axes ?? [];
|
|
24
|
+
const axisNames = new Set(axes.map((a) => a.name));
|
|
25
|
+
const flags = Object.entries(entry.props)
|
|
26
|
+
.filter(([name, p]) => !p.from && !axisNames.has(name) && p.type === "boolean" && p.deprecated === undefined)
|
|
27
|
+
.map(([name]) => name);
|
|
28
|
+
const [variants, setVariants] = useState({});
|
|
29
|
+
const [on, setOn] = useState({});
|
|
30
|
+
const [text, setText] = useState(initialText);
|
|
31
|
+
const [theme, setTheme] = useState();
|
|
32
|
+
if (!Component)
|
|
33
|
+
return _jsx(Missing, { name: entry.name });
|
|
34
|
+
const chosen = { ...coerce(variants), ...Object.fromEntries(Object.entries(on).filter(([, v]) => v)) };
|
|
35
|
+
const code = toJSX(entry.export, { ...visible(base), ...chosen }, text || undefined);
|
|
36
|
+
const changed = Object.keys(variants).length > 0 || Object.values(on).some(Boolean) || text !== initialText;
|
|
37
|
+
const reset = () => {
|
|
38
|
+
setVariants({});
|
|
39
|
+
setOn({});
|
|
40
|
+
setText(initialText);
|
|
41
|
+
};
|
|
42
|
+
return (_jsxs(Frame, { "data-slot": "playground", children: [_jsxs("div", { className: "grid lg:grid-cols-[minmax(0,1fr)_20rem]", children: [_jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsxs(Toolbar, { children: [_jsxs("span", { className: "flex items-center gap-2 pl-2 text-xs font-medium text-muted-foreground", children: [_jsx(SlidersHorizontal, { "aria-hidden": true, className: "size-(--dt-size-xs)" }), "Live"] }), _jsxs("span", { className: "flex items-center gap-0.5", children: [_jsx(IconButton, { icon: theme === "dark" ? Sun : Moon, label: theme === "dark" ? "Show in light mode" : "Show in dark mode", pressed: theme !== undefined, onClick: () => setTheme(theme === "dark" ? "light" : "dark") }), _jsx(IconButton, { icon: RotateCcw, label: "Reset the controls", onClick: reset, disabled: !changed, className: "disabled:pointer-events-none disabled:opacity-40" })] })] }), _jsx(Stage, { theme: theme, "data-slot": "playground-preview", className: "min-h-72 flex-1", children: _jsx(Boundary, { label: entry.name, children: _jsx(Component, { ...base, ...chosen, children: text || undefined }) }) })] }), _jsxs("div", { "data-slot": "playground-controls", className: "flex flex-col gap-6 border-t border-border p-5 lg:border-t-0 lg:border-l", children: [axes.map((axis) => (_jsx(AxisControl, { axis: axis, value: variants[axis.name] ?? axis.default ?? "", onChange: (v) => setVariants((s) => ({ ...s, [axis.name]: v })) }, axis.name))), flags.map((name) => (_jsx(FlagControl, { name: name, checked: !!on[name], onChange: (v) => setOn((s) => ({ ...s, [name]: v })) }, name))), initialText !== "" && _jsx(TextControl, { value: text, onChange: setText }), axes.length === 0 && flags.length === 0 && initialText === "" && _jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to change: it has no variants or flags." })] })] }), _jsx("div", { className: "border-t border-border", children: _jsx(CodeView, { code: code, label: "JSX", bare: true }) })] }));
|
|
43
|
+
}
|
|
44
|
+
/** Props passed only so the component renders (handlers, data) are left out of the snippet unless they are short. */
|
|
45
|
+
export function visible(props) {
|
|
46
|
+
return Object.fromEntries(Object.entries(props).filter(([, v]) => typeof v !== "function" && JSON.stringify(v ?? "").length <= 40));
|
|
47
|
+
}
|
|
48
|
+
/** `variant` → `Variant`, `aria-label` → `Aria label`: a control reads as a setting, not a prop name. */
|
|
49
|
+
export function title(name) {
|
|
50
|
+
const words = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_]/g, " ").toLowerCase();
|
|
51
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
52
|
+
}
|
|
53
|
+
/** Options fit one segmented row when there are few and their names are short; otherwise a menu. */
|
|
54
|
+
const fitsSegments = (axis) => axis.options.length <= 4 && axis.options.reduce((n, o) => n + o.name.length, 0) <= 26;
|
|
55
|
+
/** A control's label, with the prop it sets and what its options mean behind an info button. */
|
|
56
|
+
export function Label({ id, name, info, htmlFor }) {
|
|
57
|
+
const text = (_jsx("span", { className: "text-sm font-medium", children: title(name) }));
|
|
58
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-2", children: [htmlFor ? (_jsx("label", { id: id, htmlFor: htmlFor, children: text })) : (_jsx("span", { id: id, children: text })), _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { delay: 0, closeDelay: 0, "aria-label": `About ${title(name)}`, className: "relative inline-flex size-6 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-2.5 after:content-['']", children: _jsx(Info, { "aria-hidden": true, className: "size-4" }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Positioner, { "data-designtools": "", side: "left", sideOffset: 8, className: "z-50", children: _jsxs(Tooltip.Popup, { "data-slot": "docs-tooltip", className: "flex max-w-64 flex-col gap-1 rounded-md bg-canvas-foreground px-2.5 py-2 text-xs leading-snug text-canvas shadow-md transition-opacity duration-(--dt-duration-fast) data-ending-style:opacity-0 data-starting-style:opacity-0 [&_*]:text-canvas/80", children: [_jsx("code", { className: "font-mono font-medium text-canvas!", children: name }), info] }) }) })] })] }));
|
|
59
|
+
}
|
|
60
|
+
function AxisInfo({ axis }) {
|
|
61
|
+
const described = axis.options.filter((o) => o.description);
|
|
62
|
+
return (_jsxs(_Fragment, { children: [axis.default && _jsxs("span", { className: "text-muted-foreground", children: ["Default: ", axis.default] }), described.map((o) => (_jsxs("span", { children: [_jsx("span", { className: "font-medium", children: o.name }), ": ", _jsx("span", { className: "text-muted-foreground", children: o.description })] }, o.name)))] }));
|
|
63
|
+
}
|
|
64
|
+
export function AxisControl({ axis, value, onChange }) {
|
|
65
|
+
const id = useId();
|
|
66
|
+
if (fitsSegments(axis)) {
|
|
67
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { id: id, name: axis.name, info: _jsx(AxisInfo, { axis: axis }) }), _jsx(ToggleGroup, { "aria-labelledby": id, value: value ? [value] : [], onValueChange: (next) => next[0] && onChange(String(next[0])), className: "grid grid-flow-col auto-cols-fr gap-0.5 rounded-lg bg-muted p-1", children: axis.options.map((o) => (_jsx(Toggle, { value: o.name, className: "relative h-8 min-w-0 cursor-pointer truncate rounded-md px-2 text-sm text-muted-foreground transition-colors duration-(--dt-duration-fast) after:absolute after:inset-x-0 after:-inset-y-1.5 after:content-[''] hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-[pressed]:bg-canvas data-[pressed]:font-medium data-[pressed]:text-canvas-foreground data-[pressed]:shadow-xs", children: title(o.name) }, o.name))) })] }));
|
|
68
|
+
}
|
|
69
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { id: id, name: axis.name, info: _jsx(AxisInfo, { axis: axis }) }), _jsxs(Select.Root, { value: value, onValueChange: (v) => typeof v === "string" && onChange(v), children: [_jsxs(Select.Trigger, { "aria-labelledby": id, className: "flex h-10 cursor-pointer items-center justify-between rounded-lg bg-muted px-3 text-sm font-medium hover:bg-muted/70 focus-visible:outline-2 focus-visible:outline-ring", children: [_jsx(Select.Value, { children: (v) => title(v) }), _jsx(Select.Icon, { "aria-hidden": true, children: _jsx(ChevronDown, { className: "size-4 text-muted-foreground" }) })] }), _jsx(Select.Portal, { children: _jsx(Select.Positioner, { "data-designtools": "", sideOffset: 4, alignItemWithTrigger: false, className: "z-50", children: _jsx(Select.Popup, { className: "min-w-(--anchor-width) rounded-lg border border-border bg-overlay p-1 text-overlay-foreground shadow-md", children: _jsx(Select.List, { children: axis.options.map((o) => (_jsxs(Select.Item, { value: o.name, className: "grid min-h-(--docs-target,2.75rem) cursor-pointer grid-cols-[1fr_auto] items-center gap-3 rounded-md px-3 text-sm outline-none data-[highlighted]:bg-muted", children: [_jsx(Select.ItemText, { children: title(o.name) }), _jsx(Select.ItemIndicator, { children: _jsx(Check, { "aria-hidden": true, className: "size-4" }) })] }, o.name))) }) }) }) })] })] }));
|
|
70
|
+
}
|
|
71
|
+
export function FlagControl({ name, checked, onChange }) {
|
|
72
|
+
const id = useId();
|
|
73
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("label", { htmlFor: id, className: "text-sm font-medium", children: title(name) }), _jsx(Switch.Root, { id: id, checked: checked, onCheckedChange: onChange,
|
|
74
|
+
// drawn small, pressed large: the hit area reaches the docs' target without the switch growing
|
|
75
|
+
className: "relative h-6 w-10 shrink-0 cursor-pointer rounded-full bg-input p-0.5 transition-colors duration-(--dt-duration-fast) after:absolute after:-inset-2.5 after:content-[''] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[checked]:bg-primary", children: _jsx(Switch.Thumb, { className: "block size-5 rounded-full bg-primary-foreground shadow-xs transition-transform duration-(--dt-duration-fast) data-[checked]:translate-x-4" }) })] }));
|
|
76
|
+
}
|
|
77
|
+
export function TextControl({ value, onChange }) {
|
|
78
|
+
const id = useId();
|
|
79
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { name: "children", htmlFor: id, info: _jsx("span", { className: "text-muted-foreground", children: "The text inside it." }) }), _jsx("input", { id: id, value: value, onChange: (e) => onChange(e.target.value), className: "h-10 rounded-lg bg-muted px-3 text-sm focus-visible:outline-2 focus-visible:outline-ring" })] }));
|
|
80
|
+
}
|
|
81
|
+
/** A number prop: a field with steppers. */
|
|
82
|
+
export function NumberControl({ name, value, onChange, info }) {
|
|
83
|
+
const id = useId();
|
|
84
|
+
const step = (d) => onChange((value ?? 0) + d);
|
|
85
|
+
const button = "relative flex w-9 cursor-pointer items-center justify-center text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-y-1 after:inset-x-0 after:content-['']";
|
|
86
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { name: name, htmlFor: id, info: info }), _jsxs("div", { className: "flex h-10 items-stretch overflow-hidden rounded-lg bg-muted", children: [_jsx("button", { type: "button", "aria-label": `Less ${title(name)}`, onClick: () => step(-1), className: button, children: "\u2212" }), _jsx("input", { id: id, type: "number", inputMode: "decimal", value: value ?? "", onChange: (e) => onChange(e.target.value === "" ? undefined : Number(e.target.value)), className: "min-w-0 flex-1 bg-transparent text-center font-mono text-sm tabular-nums outline-none [appearance:textfield] focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-inner-spin-button]:appearance-none" }), _jsx("button", { type: "button", "aria-label": `More ${title(name)}`, onClick: () => step(1), className: button, children: "+" })] })] }));
|
|
87
|
+
}
|
|
88
|
+
/** A string prop: a text field. */
|
|
89
|
+
export function StringControl({ name, value, onChange, info }) {
|
|
90
|
+
const id = useId();
|
|
91
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { name: name, htmlFor: id, info: info }), _jsx("input", { id: id, value: value, onChange: (e) => onChange(e.target.value), className: "h-10 rounded-lg bg-muted px-3 text-sm focus-visible:outline-2 focus-visible:outline-ring" })] }));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* An array or object prop (a Select's options): JSON, applied while it parses,
|
|
95
|
+
* and said plainly when it does not, so a half-typed edit never breaks the preview.
|
|
96
|
+
*/
|
|
97
|
+
export function JsonControl({ name, value, onChange, info }) {
|
|
98
|
+
const id = useId();
|
|
99
|
+
const [text, setText] = useState(() => JSON.stringify(value ?? null, null, 2));
|
|
100
|
+
const [error, setError] = useState(null);
|
|
101
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { name: name, htmlFor: id, info: info }), _jsx("textarea", { id: id, value: text, rows: Math.min(10, text.split("\n").length), spellCheck: false, "aria-invalid": !!error, "aria-describedby": error ? `${id}-error` : undefined, onChange: (e) => {
|
|
102
|
+
setText(e.target.value);
|
|
103
|
+
try {
|
|
104
|
+
onChange(JSON.parse(e.target.value));
|
|
105
|
+
setError(null);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
setError(err instanceof Error ? err.message : "Not valid JSON");
|
|
109
|
+
}
|
|
110
|
+
}, className: "resize-y rounded-lg bg-muted px-3 py-2 font-mono text-xs leading-relaxed focus-visible:outline-2 focus-visible:outline-ring aria-invalid:outline-2 aria-invalid:outline-destructive" }), error && (_jsxs("p", { id: `${id}-error`, className: "text-xs text-destructive-subdued-foreground", children: ["Not applied: ", error] }))] }));
|
|
111
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ComponentEntry, PatternEntry } from "./lib/manifest-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A preview route in an iframe, at set widths and in either mode, so a
|
|
5
|
+
* component is seen at a phone's width and its media queries actually fire.
|
|
6
|
+
*/
|
|
7
|
+
export declare function PreviewFrame({ src, title, widths, height, }: {
|
|
8
|
+
/** The preview route, e.g. `/design/system/preview/badge/canonical`. */
|
|
9
|
+
src: string;
|
|
10
|
+
title: string;
|
|
11
|
+
widths?: number[];
|
|
12
|
+
height?: number;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* What the preview route renders around a state: the page's canvas in the
|
|
16
|
+
* requested mode, set on the document too, so popups in portals match.
|
|
17
|
+
*/
|
|
18
|
+
export declare function PreviewSurface({ theme, children }: {
|
|
19
|
+
theme?: string;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
/** One example on its own, for the preview route to render inside a PreviewSurface. */
|
|
23
|
+
export declare function PreviewExample({ entry, example }: {
|
|
24
|
+
entry: ComponentEntry;
|
|
25
|
+
example: string;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
/** One pattern composition on its own. */
|
|
28
|
+
export declare function PreviewComposition({ pattern, composition }: {
|
|
29
|
+
pattern: PatternEntry;
|
|
30
|
+
composition: string;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ExternalLink, Monitor, Moon, Smartphone, Sun, Tablet } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { Boundary } from "./lib/boundary.js";
|
|
6
|
+
import { cx } from "./lib/cx.js";
|
|
7
|
+
import { Missing, renderExample, useLookup } from "./lib/lookup.js";
|
|
8
|
+
import { Frame, IconButton, IconLink, Toolbar } from "./lib/ui.js";
|
|
9
|
+
/**
|
|
10
|
+
* A preview route in an iframe, at set widths and in either mode, so a
|
|
11
|
+
* component is seen at a phone's width and its media queries actually fire.
|
|
12
|
+
*/
|
|
13
|
+
export function PreviewFrame({ src, title, widths = [375, 768, 1280], height = 360, }) {
|
|
14
|
+
const [width, setWidth] = useState(widths[0]);
|
|
15
|
+
const [theme, setTheme] = useState("light");
|
|
16
|
+
const url = `${src}${src.includes("?") ? "&" : "?"}theme=${theme}`;
|
|
17
|
+
const device = (w) => (w < 600 ? Smartphone : w < 1024 ? Tablet : Monitor);
|
|
18
|
+
return (_jsxs(Frame, { "data-slot": "preview-frame", children: [_jsxs(Toolbar, { children: [_jsx("div", { role: "group", "aria-label": "Preview width", className: "flex items-center gap-0.5 rounded-lg border border-border p-0.5", children: widths.map((w) => {
|
|
19
|
+
const Icon = device(w);
|
|
20
|
+
return (_jsxs("button", { type: "button", "aria-pressed": width === w, onClick: () => setWidth(w), className: cx("relative inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md px-2.5 font-mono text-xs text-muted-foreground transition-colors duration-(--dt-duration-fast) hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring aria-pressed:bg-muted aria-pressed:text-canvas-foreground", "after:absolute after:inset-x-0 after:-inset-y-[calc((var(--docs-target,2.75rem)-1.75rem)/2)] after:content-['']"), children: [_jsx(Icon, { "aria-hidden": true, className: "size-(--dt-size-xs)" }), w] }, w));
|
|
21
|
+
}) }), _jsxs("span", { className: "flex items-center gap-0.5", children: [_jsx(IconButton, { icon: theme === "dark" ? Sun : Moon, label: theme === "dark" ? "Show in light mode" : "Show in dark mode", pressed: theme === "dark", onClick: () => setTheme(theme === "dark" ? "light" : "dark") }), _jsx(IconLink, { icon: ExternalLink, label: "Open in a new tab", href: url, target: "_blank", rel: "noreferrer" })] })] }), _jsx("div", { className: "overflow-x-auto bg-muted/50 bg-[radial-gradient(color-mix(in_oklch,var(--border)_85%,transparent)_1px,transparent_1px)] [background-size:14px_14px] p-6", tabIndex: 0, role: "region", "aria-label": "Preview", children: _jsxs("div", { className: "mx-auto flex flex-col overflow-hidden rounded-lg border border-border bg-canvas shadow-md", style: { width, maxWidth: "none" }, children: [_jsxs("div", { "aria-hidden": true, className: "flex items-center gap-3 border-b border-border bg-surface px-3 py-2", children: [_jsxs("span", { className: "flex gap-1.5", children: [_jsx("span", { className: "size-2.5 rounded-full bg-border" }), _jsx("span", { className: "size-2.5 rounded-full bg-border" }), _jsx("span", { className: "size-2.5 rounded-full bg-border" })] }), _jsxs("span", { className: "min-w-0 flex-1 truncate rounded-md bg-muted px-2 py-0.5 text-center font-mono text-xs text-muted-foreground", children: [title, " \u00B7 ", width, "px"] })] }), _jsx("iframe", { title: `${title}, ${width}px wide, ${theme} mode`, src: url, "data-slot": "preview-iframe", className: "block w-full resize-y bg-canvas", style: { height } })] }) })] }));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What the preview route renders around a state: the page's canvas in the
|
|
25
|
+
* requested mode, set on the document too, so popups in portals match.
|
|
26
|
+
*/
|
|
27
|
+
export function PreviewSurface({ theme, children }) {
|
|
28
|
+
const mode = theme === "dark" || theme === "light" ? theme : undefined;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!mode)
|
|
31
|
+
return;
|
|
32
|
+
const root = document.documentElement;
|
|
33
|
+
const previous = root.dataset.theme;
|
|
34
|
+
root.dataset.theme = mode;
|
|
35
|
+
return () => {
|
|
36
|
+
if (previous === undefined)
|
|
37
|
+
delete root.dataset.theme;
|
|
38
|
+
else
|
|
39
|
+
root.dataset.theme = previous;
|
|
40
|
+
};
|
|
41
|
+
}, [mode]);
|
|
42
|
+
return (_jsx("div", { "data-designtools": "", "data-docs-stage": "", "data-slot": "preview-surface", "data-theme": mode, className: "min-h-dvh bg-canvas p-6 text-canvas-foreground", children: children }));
|
|
43
|
+
}
|
|
44
|
+
/** One example on its own, for the preview route to render inside a PreviewSurface. */
|
|
45
|
+
export function PreviewExample({ entry, example }) {
|
|
46
|
+
const lookup = useLookup();
|
|
47
|
+
const module = lookup.examplesFor(entry);
|
|
48
|
+
if (!module)
|
|
49
|
+
return _jsx(Missing, { name: entry.name, what: "examples" });
|
|
50
|
+
if (!(example in module))
|
|
51
|
+
return _jsxs("p", { className: "text-sm", children: [entry.name, " has no example called ", example, "."] });
|
|
52
|
+
return _jsx(Boundary, { label: example, children: renderExample(module[example], example) });
|
|
53
|
+
}
|
|
54
|
+
/** One pattern composition on its own. */
|
|
55
|
+
export function PreviewComposition({ pattern, composition }) {
|
|
56
|
+
const lookup = useLookup();
|
|
57
|
+
const module = lookup.compositionsFor(pattern);
|
|
58
|
+
if (!module)
|
|
59
|
+
return _jsx(Missing, { name: pattern.name, what: "compositions" });
|
|
60
|
+
if (!(composition in module))
|
|
61
|
+
return _jsxs("p", { className: "text-sm", children: [pattern.name, " has no composition called ", composition, "."] });
|
|
62
|
+
return _jsx(Boundary, { label: composition, children: renderExample(module[composition], composition) });
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentEntry } from "./lib/manifest-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A component's props, as an API reference: name, type and default on one line,
|
|
4
|
+
* each row opening to its description. Its own props first, then those of the
|
|
5
|
+
* primitive it wraps, folded away. Deprecated props are struck through with the
|
|
6
|
+
* reason; what it inherits from an element or primitive is named underneath.
|
|
7
|
+
*/
|
|
8
|
+
export declare function PropsTable({ entry }: {
|
|
9
|
+
entry: ComponentEntry;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
import { cx } from "./lib/cx.js";
|
|
4
|
+
import { inline } from "./lib/text.js";
|
|
5
|
+
/**
|
|
6
|
+
* A component's props, as an API reference: name, type and default on one line,
|
|
7
|
+
* each row opening to its description. Its own props first, then those of the
|
|
8
|
+
* primitive it wraps, folded away. Deprecated props are struck through with the
|
|
9
|
+
* reason; what it inherits from an element or primitive is named underneath.
|
|
10
|
+
*/
|
|
11
|
+
export function PropsTable({ entry }) {
|
|
12
|
+
const rows = Object.entries(entry.props);
|
|
13
|
+
const own = rows.filter(([, p]) => !p.from);
|
|
14
|
+
const byPackage = new Map();
|
|
15
|
+
for (const row of rows.filter(([, p]) => p.from)) {
|
|
16
|
+
byPackage.set(row[1].from, [...(byPackage.get(row[1].from) ?? []), row]);
|
|
17
|
+
}
|
|
18
|
+
return (_jsxs("div", { "data-slot": "props-table", className: "flex flex-col gap-4", children: [own.length > 0 ? _jsx(Table, { rows: own }) : _jsx("p", { className: "text-sm text-muted-foreground", children: "No props of its own." }), [...byPackage.entries()].map(([pkg, inherited]) => (_jsxs("details", { "data-slot": "props-inherited", className: "group/inherited overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground", children: [_jsxs("summary", { className: "flex min-h-(--docs-target,2.75rem) cursor-pointer list-none items-center justify-between gap-3 px-4 text-sm hover:bg-muted [&::-webkit-details-marker]:hidden", children: [_jsxs("span", { children: [inherited.length, " more from ", _jsx("code", { className: "font-mono text-xs", children: pkg })] }), _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--dt-size-xs) text-muted-foreground transition-transform group-open/inherited:rotate-180" })] }), _jsx("div", { className: "border-t border-border", children: _jsx(Table, { rows: inherited, bare: true }) })] }, pkg))), entry.inherits && entry.inherits.length > 0 && (_jsxs("p", { "data-slot": "props-inherits", className: "text-sm text-muted-foreground", children: ["Also accepts every prop of", " ", entry.inherits.map((x, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx("code", { className: "rounded-sm bg-muted px-1 py-0.5 font-mono text-xs text-canvas-foreground", children: /^[a-z][\w-]*$/.test(x) ? `<${x}>` : x })] }, x))), "."] }))] }));
|
|
19
|
+
}
|
|
20
|
+
const COLS = "grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 md:grid-cols-[minmax(9rem,0.9fr)_minmax(0,1.6fr)_minmax(5rem,0.6fr)_1.25rem] md:items-center";
|
|
21
|
+
function Table({ rows, bare }) {
|
|
22
|
+
return (_jsxs("div", { className: cx("min-w-0 text-sm", !bare && "overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground"), children: [_jsxs("div", { "aria-hidden": true, className: cx(COLS, "hidden border-b border-border bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground md:grid"), children: [_jsx("span", { children: "Prop" }), _jsx("span", { children: "Type" }), _jsx("span", { children: "Default" }), _jsx("span", {})] }), _jsx("ul", { "aria-label": "Props", className: "divide-y divide-border", children: rows.map(([name, prop]) => (_jsx("li", { children: _jsx(Row, { name: name, prop: prop }) }, name))) })] }));
|
|
23
|
+
}
|
|
24
|
+
function Row({ name, prop }) {
|
|
25
|
+
const deprecated = prop.deprecated !== undefined;
|
|
26
|
+
const line = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [_jsx("code", { className: cx("truncate font-mono text-xs font-medium text-primary-subdued-foreground", deprecated && "line-through opacity-70"), children: name }), prop.required && (_jsxs("span", { "data-slot": "props-required", className: "font-mono text-xs text-destructive-subdued-foreground", title: "Required", children: ["*", _jsx("span", { className: "sr-only", children: " required" })] }))] }), _jsxs("span", { className: "col-span-2 min-w-0 md:col-span-1", children: [_jsx("span", { className: "sr-only", children: ", type " }), _jsx(Type, { prop: prop })] }), _jsx("span", { className: "hidden font-mono text-xs text-muted-foreground md:block", children: prop.default !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: ", default " }), prop.default] })) : (_jsx("span", { "aria-hidden": true, children: "\u2014" })) })] }));
|
|
27
|
+
const described = deprecated || !!prop.description;
|
|
28
|
+
if (!described)
|
|
29
|
+
return (_jsxs("div", { "data-slot": "props-row", className: cx(COLS, "px-4 py-3"), children: [line, _jsx("span", {})] }));
|
|
30
|
+
return (_jsxs("details", { "data-slot": "props-row", className: "group/row", children: [_jsxs("summary", { className: cx(COLS, "min-h-(--docs-target,2.75rem) cursor-pointer list-none px-4 py-3 hover:bg-muted/60 [&::-webkit-details-marker]:hidden"), children: [line, _jsx(ChevronDown, { "aria-hidden": true, className: "col-start-2 row-start-1 size-(--dt-size-xs) justify-self-end text-muted-foreground transition-transform group-open/row:rotate-180 md:col-start-4" })] }), _jsxs("div", { className: "flex flex-col gap-2 px-4 pb-4 text-sm text-muted-foreground", children: [prop.default !== undefined && (_jsxs("p", { className: "md:hidden", children: ["Default ", _jsx("code", { className: "font-mono text-xs text-canvas-foreground", children: prop.default })] })), deprecated && _jsxs("p", { className: "font-medium text-canvas-foreground", children: ["Deprecated", prop.deprecated ? _jsxs(_Fragment, { children: [": ", inline(prop.deprecated)] }) : "."] }), prop.description?.split("\n\n").map((p) => (_jsx("p", { className: "max-w-prose whitespace-pre-line", children: inline(p) }, p)))] })] }));
|
|
31
|
+
}
|
|
32
|
+
/** A type, coloured by kind: literals, primitives and functions read apart at a glance. */
|
|
33
|
+
function Type({ prop }) {
|
|
34
|
+
const parts = prop.values ?? prop.type.split(/\s*\|\s*/);
|
|
35
|
+
return (_jsx("span", { className: "flex flex-wrap items-center gap-x-1 gap-y-1 font-mono text-xs", children: parts.map((t, i) => (_jsxs("span", { className: "inline-flex items-center gap-1", children: [i > 0 && _jsx("span", { className: "text-muted-foreground", children: "|" }), _jsx("span", { className: cx("break-all", tone(t, !!prop.values)), children: prop.values ? `"${t}"` : t })] }, `${t}-${i}`))) }));
|
|
36
|
+
}
|
|
37
|
+
function tone(t, literal) {
|
|
38
|
+
if (literal || /^["'].*["']$/.test(t))
|
|
39
|
+
return "text-success-subdued-foreground";
|
|
40
|
+
if (/^(boolean|number|string|true|false|null|undefined)$/.test(t))
|
|
41
|
+
return "text-primary-subdued-foreground";
|
|
42
|
+
if (/=>|^\(|Function|Handler|EventHandler/.test(t))
|
|
43
|
+
return "text-destructive-subdued-foreground";
|
|
44
|
+
return "text-canvas-foreground";
|
|
45
|
+
}
|
package/dist/rule.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { RuleEntry } from "./lib/manifest-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* One rule, brand or interface, written for people. On a wide screen, its name, status and the check
|
|
5
|
+
* that proves it hold the left column while the right reads: the statement set large, the
|
|
6
|
+
* demonstration (pass it as children, for a rule that needs persuading), the facts, then why. A small
|
|
7
|
+
* switch shows the same rule as an agent reads it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Rule({ rule, children }: {
|
|
10
|
+
rule: RuleEntry;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/** Every rule on one screen, a line each, linking to the rule: what it is, how settled, how it is checked. */
|
|
14
|
+
export declare function RuleIndex({ rules }: {
|
|
15
|
+
rules: RuleEntry[];
|
|
16
|
+
}): import("react").JSX.Element;
|