@designtools/blocks 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 +9 -0
- package/LICENSE +21 -0
- package/README.md +117 -2
- package/dist/cli.js +381 -0
- package/package.json +59 -3
- package/registry/a11y-panel.tsx +99 -0
- package/registry/adherence-summary.tsx +110 -0
- package/registry/agent-view.tsx +57 -0
- package/registry/anatomy.tsx +88 -0
- package/registry/ask-claude.tsx +71 -0
- package/registry/code-view.tsx +74 -0
- package/registry/examples.tsx +96 -0
- package/registry/glossary.tsx +58 -0
- package/registry/imagery.tsx +39 -0
- package/registry/lib/adherence-types.ts +179 -0
- package/registry/lib/boundary.tsx +21 -0
- package/registry/lib/contrast.ts +44 -0
- package/registry/lib/cx.ts +4 -0
- package/registry/lib/jsx.ts +29 -0
- package/registry/lib/lookup.tsx +46 -0
- package/registry/lib/manifest-types.ts +273 -0
- package/registry/lib/manifest.ts +182 -0
- package/registry/lib/markdown.ts +283 -0
- package/registry/lib/status.tsx +30 -0
- package/registry/lib/text.tsx +27 -0
- package/registry/logo-usage.tsx +87 -0
- package/registry/pattern.tsx +68 -0
- package/registry/playground.tsx +180 -0
- package/registry/preview-frame.tsx +106 -0
- package/registry/props-table.tsx +110 -0
- package/registry/registry.generated.tsx +9 -0
- package/registry/rule.tsx +98 -0
- package/registry/scale.tsx +115 -0
- package/registry/shell.tsx +184 -0
- package/registry/standards.tsx +45 -0
- package/registry/swatches.tsx +146 -0
- package/registry/tsconfig.json +13 -0
- package/registry/type-ramp.tsx +105 -0
- package/registry/usage.tsx +72 -0
- package/registry/variant-matrix.tsx +105 -0
- package/registry/voice-terms.tsx +45 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { CodeView } from "./code-view";
|
|
5
|
+
import { Boundary } from "./lib/boundary";
|
|
6
|
+
import { compositionsFor, Missing, renderExample } from "./lib/lookup";
|
|
7
|
+
import { slugOf } from "./lib/manifest";
|
|
8
|
+
import type { ComponentsManifest, PatternEntry } from "./lib/manifest-types";
|
|
9
|
+
import { StatusBadge } from "./lib/status";
|
|
10
|
+
import { inline } from "./lib/text";
|
|
11
|
+
|
|
12
|
+
/** A recurring screen composition: each composition live, with its code and the components it uses. */
|
|
13
|
+
export function Pattern({
|
|
14
|
+
pattern,
|
|
15
|
+
manifest,
|
|
16
|
+
basePath = "/design/system",
|
|
17
|
+
}: {
|
|
18
|
+
pattern: PatternEntry;
|
|
19
|
+
/** To link the components it uses to their pages. */
|
|
20
|
+
manifest?: ComponentsManifest;
|
|
21
|
+
basePath?: string;
|
|
22
|
+
}) {
|
|
23
|
+
const module = compositionsFor(pattern);
|
|
24
|
+
const known = new Map((manifest?.components ?? []).map((c) => [c.name, c]));
|
|
25
|
+
return (
|
|
26
|
+
<div data-slot="pattern" className="flex flex-col gap-6">
|
|
27
|
+
<div className="flex flex-wrap items-center gap-2 text-sm">
|
|
28
|
+
<StatusBadge status={pattern.status} />
|
|
29
|
+
{pattern.components.length > 0 && <span className="text-muted-foreground">Composes</span>}
|
|
30
|
+
{pattern.components.map((name) => {
|
|
31
|
+
const c = known.get(name);
|
|
32
|
+
return c ? (
|
|
33
|
+
<Link key={name} href={`${basePath}/components/${slugOf(c)}`} className="rounded-sm bg-muted px-1.5 py-0.5 font-mono text-xs hover:underline">
|
|
34
|
+
{name}
|
|
35
|
+
</Link>
|
|
36
|
+
) : (
|
|
37
|
+
<code key={name} className="rounded-sm bg-muted px-1.5 py-0.5 font-mono text-xs">
|
|
38
|
+
{name}
|
|
39
|
+
</code>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</div>
|
|
43
|
+
{!module ? (
|
|
44
|
+
<Missing name={pattern.name} what="compositions" />
|
|
45
|
+
) : (
|
|
46
|
+
pattern.examples.map((ex) => (
|
|
47
|
+
<figure key={ex.name} data-slot="pattern-composition" className="flex min-w-0 flex-col overflow-hidden rounded-lg border border-border">
|
|
48
|
+
<div className="flex min-h-48 items-center justify-center overflow-auto bg-surface p-6 text-surface-foreground">
|
|
49
|
+
<Boundary label={ex.name}>{renderExample(module[ex.name], ex.name)}</Boundary>
|
|
50
|
+
</div>
|
|
51
|
+
<figcaption className="flex flex-col gap-2 border-t border-border px-4 py-3 text-sm">
|
|
52
|
+
<code className="font-mono text-xs text-muted-foreground">{ex.name}</code>
|
|
53
|
+
{ex.description && <span className="text-muted-foreground">{inline(ex.description)}</span>}
|
|
54
|
+
{ex.code && (
|
|
55
|
+
<details>
|
|
56
|
+
<summary className="cursor-pointer text-xs text-muted-foreground">Code</summary>
|
|
57
|
+
<div className="mt-2">
|
|
58
|
+
<CodeView code={ex.code} label={ex.name} />
|
|
59
|
+
</div>
|
|
60
|
+
</details>
|
|
61
|
+
)}
|
|
62
|
+
</figcaption>
|
|
63
|
+
</figure>
|
|
64
|
+
))
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
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 { useId, useState } from "react";
|
|
8
|
+
import { CodeView } from "./code-view";
|
|
9
|
+
import { Boundary } from "./lib/boundary";
|
|
10
|
+
import { cx } from "./lib/cx";
|
|
11
|
+
import { toJSX } from "./lib/jsx";
|
|
12
|
+
import { coerce, componentFor, Missing } from "./lib/lookup";
|
|
13
|
+
import type { ComponentEntry, VariantAxis } from "./lib/manifest-types";
|
|
14
|
+
|
|
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({
|
|
21
|
+
entry,
|
|
22
|
+
props: base = {},
|
|
23
|
+
children: initialText = entry.name,
|
|
24
|
+
}: {
|
|
25
|
+
entry: ComponentEntry;
|
|
26
|
+
props?: Record<string, unknown>;
|
|
27
|
+
/** Text content, editable in the playground. Pass "" for a component without children. */
|
|
28
|
+
children?: string;
|
|
29
|
+
}) {
|
|
30
|
+
const Component = componentFor(entry);
|
|
31
|
+
const axes = entry.variants?.axes ?? [];
|
|
32
|
+
const axisNames = new Set(axes.map((a) => a.name));
|
|
33
|
+
const flags = Object.entries(entry.props)
|
|
34
|
+
.filter(([name, p]) => !p.from && !axisNames.has(name) && p.type === "boolean" && p.deprecated === undefined)
|
|
35
|
+
.map(([name]) => name);
|
|
36
|
+
|
|
37
|
+
const [variants, setVariants] = useState<Record<string, string>>({});
|
|
38
|
+
const [on, setOn] = useState<Record<string, boolean>>({});
|
|
39
|
+
const [text, setText] = useState(initialText);
|
|
40
|
+
|
|
41
|
+
if (!Component) return <Missing name={entry.name} />;
|
|
42
|
+
|
|
43
|
+
const chosen = { ...coerce(variants), ...Object.fromEntries(Object.entries(on).filter(([, v]) => v)) };
|
|
44
|
+
const code = toJSX(entry.export, { ...visible(base), ...chosen }, text || undefined);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div data-slot="playground" className="flex flex-col gap-4">
|
|
48
|
+
<div className="grid overflow-hidden rounded-lg border border-border md:grid-cols-[minmax(0,1fr)_16rem]">
|
|
49
|
+
<div data-slot="playground-preview" className="flex min-h-48 min-w-0 items-center justify-center overflow-auto bg-surface p-8 text-surface-foreground">
|
|
50
|
+
<Boundary label={entry.name}>
|
|
51
|
+
<Component {...base} {...chosen}>
|
|
52
|
+
{text || undefined}
|
|
53
|
+
</Component>
|
|
54
|
+
</Boundary>
|
|
55
|
+
</div>
|
|
56
|
+
<div data-slot="playground-controls" className="flex flex-col gap-4 border-t border-border p-4 md:border-t-0 md:border-l">
|
|
57
|
+
{axes.map((axis) => (
|
|
58
|
+
<AxisControl
|
|
59
|
+
key={axis.name}
|
|
60
|
+
axis={axis}
|
|
61
|
+
value={variants[axis.name] ?? axis.default ?? ""}
|
|
62
|
+
onChange={(v) => setVariants((s) => ({ ...s, [axis.name]: v }))}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
{flags.map((name) => (
|
|
66
|
+
<FlagControl key={name} name={name} checked={!!on[name]} onChange={(v) => setOn((s) => ({ ...s, [name]: v }))} />
|
|
67
|
+
))}
|
|
68
|
+
{initialText !== "" && <TextControl value={text} onChange={setText} />}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<CodeView code={code} label="JSX" />
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Props passed only so the component renders (handlers, data) are left out of the snippet unless they are short. */
|
|
77
|
+
function visible(props: Record<string, unknown>): Record<string, unknown> {
|
|
78
|
+
return Object.fromEntries(Object.entries(props).filter(([, v]) => typeof v !== "function" && JSON.stringify(v ?? "").length <= 40));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const label = "text-xs font-medium text-muted-foreground";
|
|
82
|
+
|
|
83
|
+
function AxisControl({ axis, value, onChange }: { axis: VariantAxis; value: string; onChange: (v: string) => void }) {
|
|
84
|
+
const id = useId();
|
|
85
|
+
if (axis.options.length <= 5) {
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex flex-col gap-1.5">
|
|
88
|
+
<span id={id} className={cx(label, "font-mono")}>
|
|
89
|
+
{axis.name}
|
|
90
|
+
</span>
|
|
91
|
+
<ToggleGroup
|
|
92
|
+
aria-labelledby={id}
|
|
93
|
+
value={value ? [value] : []}
|
|
94
|
+
onValueChange={(next) => next[0] && onChange(String(next[0]))}
|
|
95
|
+
className="flex flex-wrap gap-1"
|
|
96
|
+
>
|
|
97
|
+
{axis.options.map((o) => (
|
|
98
|
+
<Toggle
|
|
99
|
+
key={o.name}
|
|
100
|
+
value={o.name}
|
|
101
|
+
title={o.description}
|
|
102
|
+
className="rounded-md border border-border px-2 py-1 font-mono text-xs hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring data-[pressed]:bg-primary data-[pressed]:text-primary-foreground"
|
|
103
|
+
>
|
|
104
|
+
{o.name}
|
|
105
|
+
</Toggle>
|
|
106
|
+
))}
|
|
107
|
+
</ToggleGroup>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex flex-col gap-1.5">
|
|
113
|
+
<span id={id} className={cx(label, "font-mono")}>
|
|
114
|
+
{axis.name}
|
|
115
|
+
</span>
|
|
116
|
+
<Select.Root value={value} onValueChange={(v) => typeof v === "string" && onChange(v)}>
|
|
117
|
+
<Select.Trigger
|
|
118
|
+
aria-labelledby={id}
|
|
119
|
+
className="flex items-center justify-between rounded-md border border-border px-2 py-1 font-mono text-xs focus-visible:outline-2 focus-visible:outline-ring"
|
|
120
|
+
>
|
|
121
|
+
<Select.Value />
|
|
122
|
+
<Select.Icon aria-hidden>▾</Select.Icon>
|
|
123
|
+
</Select.Trigger>
|
|
124
|
+
<Select.Portal>
|
|
125
|
+
<Select.Positioner sideOffset={4} className="z-50">
|
|
126
|
+
<Select.Popup className="rounded-md border border-border bg-overlay p-1 text-overlay-foreground shadow-md">
|
|
127
|
+
<Select.List>
|
|
128
|
+
{axis.options.map((o) => (
|
|
129
|
+
<Select.Item
|
|
130
|
+
key={o.name}
|
|
131
|
+
value={o.name}
|
|
132
|
+
className="cursor-default rounded-sm px-2 py-1 font-mono text-xs outline-none data-[highlighted]:bg-muted"
|
|
133
|
+
>
|
|
134
|
+
<Select.ItemText>{o.name}</Select.ItemText>
|
|
135
|
+
</Select.Item>
|
|
136
|
+
))}
|
|
137
|
+
</Select.List>
|
|
138
|
+
</Select.Popup>
|
|
139
|
+
</Select.Positioner>
|
|
140
|
+
</Select.Portal>
|
|
141
|
+
</Select.Root>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function FlagControl({ name, checked, onChange }: { name: string; checked: boolean; onChange: (v: boolean) => void }) {
|
|
147
|
+
const id = useId();
|
|
148
|
+
return (
|
|
149
|
+
<div className="flex items-center justify-between gap-3">
|
|
150
|
+
<label htmlFor={id} className={cx(label, "font-mono")}>
|
|
151
|
+
{name}
|
|
152
|
+
</label>
|
|
153
|
+
<Switch.Root
|
|
154
|
+
id={id}
|
|
155
|
+
checked={checked}
|
|
156
|
+
onCheckedChange={onChange}
|
|
157
|
+
className="relative h-5 w-9 shrink-0 rounded-full border border-border bg-canvas outline-offset-2 focus-visible:outline-2 focus-visible:outline-ring data-[checked]:border-primary data-[checked]:bg-primary"
|
|
158
|
+
>
|
|
159
|
+
<Switch.Thumb className="block size-3.5 translate-x-0.5 rounded-full bg-muted-foreground transition-transform data-[checked]:translate-x-[1.0625rem] data-[checked]:bg-primary-foreground motion-reduce:transition-none" />
|
|
160
|
+
</Switch.Root>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function TextControl({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
166
|
+
const id = useId();
|
|
167
|
+
return (
|
|
168
|
+
<div className="flex flex-col gap-1.5">
|
|
169
|
+
<label htmlFor={id} className={label}>
|
|
170
|
+
Content
|
|
171
|
+
</label>
|
|
172
|
+
<input
|
|
173
|
+
id={id}
|
|
174
|
+
value={value}
|
|
175
|
+
onChange={(e) => onChange(e.target.value)}
|
|
176
|
+
className="rounded-md border border-border bg-canvas px-2 py-1 text-sm focus-visible:outline-2 focus-visible:outline-ring"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
4
|
+
import { Boundary } from "./lib/boundary";
|
|
5
|
+
import { cx } from "./lib/cx";
|
|
6
|
+
import { compositionsFor, examplesFor, Missing, renderExample } from "./lib/lookup";
|
|
7
|
+
import type { ComponentEntry, PatternEntry } from "./lib/manifest-types";
|
|
8
|
+
|
|
9
|
+
type Theme = "light" | "dark";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A preview route in an iframe, at set widths and in either mode, so a
|
|
13
|
+
* component is seen at a phone's width and its media queries actually fire.
|
|
14
|
+
*/
|
|
15
|
+
export function PreviewFrame({
|
|
16
|
+
src,
|
|
17
|
+
title,
|
|
18
|
+
widths = [375, 768, 1280],
|
|
19
|
+
height = 360,
|
|
20
|
+
}: {
|
|
21
|
+
/** The preview route, e.g. `/design/system/preview/badge/canonical`. */
|
|
22
|
+
src: string;
|
|
23
|
+
title: string;
|
|
24
|
+
widths?: number[];
|
|
25
|
+
height?: number;
|
|
26
|
+
}) {
|
|
27
|
+
const [width, setWidth] = useState(widths[0]);
|
|
28
|
+
const [theme, setTheme] = useState<Theme>("light");
|
|
29
|
+
const url = `${src}${src.includes("?") ? "&" : "?"}theme=${theme}`;
|
|
30
|
+
const button = "rounded-md px-2 py-1 font-mono text-xs hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring";
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<figure data-slot="preview-frame" className="flex flex-col gap-2">
|
|
34
|
+
<figcaption className="flex flex-wrap items-center justify-between gap-2">
|
|
35
|
+
<span className="text-sm font-medium">{title}</span>
|
|
36
|
+
<span className="flex flex-wrap items-center gap-1" role="group" aria-label="Preview width and mode">
|
|
37
|
+
{widths.map((w) => (
|
|
38
|
+
<button key={w} type="button" aria-pressed={width === w} onClick={() => setWidth(w)} className={cx(button, width === w && "bg-muted")}>
|
|
39
|
+
{w}
|
|
40
|
+
</button>
|
|
41
|
+
))}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
aria-pressed={theme === "dark"}
|
|
45
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
46
|
+
className={cx(button, theme === "dark" && "bg-muted")}
|
|
47
|
+
>
|
|
48
|
+
dark
|
|
49
|
+
</button>
|
|
50
|
+
<a href={url} target="_blank" rel="noreferrer" className={button}>
|
|
51
|
+
open ↗
|
|
52
|
+
</a>
|
|
53
|
+
</span>
|
|
54
|
+
</figcaption>
|
|
55
|
+
<div className="overflow-x-auto rounded-lg border border-border bg-muted p-2">
|
|
56
|
+
<iframe
|
|
57
|
+
title={`${title}, ${width}px wide, ${theme} mode`}
|
|
58
|
+
src={url}
|
|
59
|
+
data-slot="preview-iframe"
|
|
60
|
+
className="block resize-y rounded-md border border-border bg-canvas"
|
|
61
|
+
style={{ width, height, maxWidth: "none" }}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</figure>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What the preview route renders around a state: the page's canvas in the
|
|
70
|
+
* requested mode, set on the document too, so popups in portals match.
|
|
71
|
+
*/
|
|
72
|
+
export function PreviewSurface({ theme, children }: { theme?: string; children: ReactNode }) {
|
|
73
|
+
const mode: Theme | undefined = theme === "dark" || theme === "light" ? theme : undefined;
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!mode) return;
|
|
76
|
+
const root = document.documentElement;
|
|
77
|
+
const previous = root.dataset.theme;
|
|
78
|
+
root.dataset.theme = mode;
|
|
79
|
+
return () => {
|
|
80
|
+
if (previous === undefined) delete root.dataset.theme;
|
|
81
|
+
else root.dataset.theme = previous;
|
|
82
|
+
};
|
|
83
|
+
}, [mode]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div data-slot="preview-surface" data-theme={mode} className="min-h-dvh bg-canvas p-6 text-canvas-foreground">
|
|
87
|
+
{children}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One example on its own, for the preview route to render inside a PreviewSurface. */
|
|
93
|
+
export function PreviewExample({ entry, example }: { entry: ComponentEntry; example: string }) {
|
|
94
|
+
const module = examplesFor(entry);
|
|
95
|
+
if (!module) return <Missing name={entry.name} what="examples" />;
|
|
96
|
+
if (!(example in module)) return <p className="text-sm">{entry.name} has no example called {example}.</p>;
|
|
97
|
+
return <Boundary label={example}>{renderExample(module[example], example)}</Boundary>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One pattern composition on its own. */
|
|
101
|
+
export function PreviewComposition({ pattern, composition }: { pattern: PatternEntry; composition: string }) {
|
|
102
|
+
const module = compositionsFor(pattern);
|
|
103
|
+
if (!module) return <Missing name={pattern.name} what="compositions" />;
|
|
104
|
+
if (!(composition in module)) return <p className="text-sm">{pattern.name} has no composition called {composition}.</p>;
|
|
105
|
+
return <Boundary label={composition}>{renderExample(module[composition], composition)}</Boundary>;
|
|
106
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { cx } from "./lib/cx";
|
|
2
|
+
import { inline } from "./lib/text";
|
|
3
|
+
import type { ComponentEntry, PropEntry } from "./lib/manifest-types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A component's props: its own first, then those of the primitive it wraps,
|
|
7
|
+
* folded away. Deprecated props are struck through with the reason; what it
|
|
8
|
+
* inherits from an element or primitive is named underneath rather than listed.
|
|
9
|
+
*/
|
|
10
|
+
export function PropsTable({ entry }: { entry: ComponentEntry }) {
|
|
11
|
+
const rows = Object.entries(entry.props);
|
|
12
|
+
const own = rows.filter(([, p]) => !p.from);
|
|
13
|
+
const byPackage = new Map<string, [string, PropEntry][]>();
|
|
14
|
+
for (const row of rows.filter(([, p]) => p.from)) {
|
|
15
|
+
byPackage.set(row[1].from!, [...(byPackage.get(row[1].from!) ?? []), row]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div data-slot="props-table" className="flex flex-col gap-4">
|
|
20
|
+
{own.length > 0 ? <Table rows={own} /> : <p className="text-sm text-muted-foreground">No props of its own.</p>}
|
|
21
|
+
{[...byPackage.entries()].map(([pkg, inherited]) => (
|
|
22
|
+
<details key={pkg} data-slot="props-inherited" className="rounded-md border border-border">
|
|
23
|
+
<summary className="cursor-pointer px-3 py-2 text-sm">
|
|
24
|
+
{inherited.length} props from <span className="font-mono">{pkg}</span>
|
|
25
|
+
</summary>
|
|
26
|
+
<div className="border-t border-border p-3">
|
|
27
|
+
<Table rows={inherited} />
|
|
28
|
+
</div>
|
|
29
|
+
</details>
|
|
30
|
+
))}
|
|
31
|
+
{entry.inherits && entry.inherits.length > 0 && (
|
|
32
|
+
<p data-slot="props-inherits" className="text-sm text-muted-foreground">
|
|
33
|
+
Also accepts every prop of{" "}
|
|
34
|
+
{entry.inherits.map((x, i) => (
|
|
35
|
+
<span key={x}>
|
|
36
|
+
{i > 0 && ", "}
|
|
37
|
+
<code className="font-mono text-xs">{/^[a-z][\w-]*$/.test(x) ? `<${x}>` : x}</code>
|
|
38
|
+
</span>
|
|
39
|
+
))}
|
|
40
|
+
.
|
|
41
|
+
</p>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Table({ rows }: { rows: [string, PropEntry][] }) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="overflow-x-auto">
|
|
50
|
+
<table className="w-full min-w-[36rem] border-collapse text-left text-sm">
|
|
51
|
+
<thead>
|
|
52
|
+
<tr className="border-b border-border text-xs text-muted-foreground">
|
|
53
|
+
<th scope="col" className="py-2 pr-4 font-medium">
|
|
54
|
+
Prop
|
|
55
|
+
</th>
|
|
56
|
+
<th scope="col" className="py-2 pr-4 font-medium">
|
|
57
|
+
Type
|
|
58
|
+
</th>
|
|
59
|
+
<th scope="col" className="py-2 pr-4 font-medium">
|
|
60
|
+
Default
|
|
61
|
+
</th>
|
|
62
|
+
<th scope="col" className="py-2 font-medium">
|
|
63
|
+
Description
|
|
64
|
+
</th>
|
|
65
|
+
</tr>
|
|
66
|
+
</thead>
|
|
67
|
+
<tbody>
|
|
68
|
+
{rows.map(([name, prop]) => (
|
|
69
|
+
<tr key={name} data-slot="props-row" className="border-b border-border align-top">
|
|
70
|
+
<th scope="row" className="py-2 pr-4 font-normal">
|
|
71
|
+
<span className="flex flex-col items-start gap-1">
|
|
72
|
+
<code className={cx("font-mono text-xs font-medium", prop.deprecated !== undefined && "line-through")}>{name}</code>
|
|
73
|
+
{prop.required && (
|
|
74
|
+
<span data-slot="props-required" className="rounded-sm bg-muted px-1 text-[0.625rem] text-muted-foreground">
|
|
75
|
+
required
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
</span>
|
|
79
|
+
</th>
|
|
80
|
+
<td className="py-2 pr-4">
|
|
81
|
+
{prop.values ? (
|
|
82
|
+
<span className="flex flex-wrap gap-1">
|
|
83
|
+
{prop.values.map((v) => (
|
|
84
|
+
<code key={v} className="rounded-sm bg-muted px-1 font-mono text-xs">
|
|
85
|
+
{v}
|
|
86
|
+
</code>
|
|
87
|
+
))}
|
|
88
|
+
</span>
|
|
89
|
+
) : (
|
|
90
|
+
<code className="font-mono text-xs break-words">{prop.type}</code>
|
|
91
|
+
)}
|
|
92
|
+
</td>
|
|
93
|
+
<td className="py-2 pr-4">{prop.default !== undefined && <code className="font-mono text-xs">{prop.default}</code>}</td>
|
|
94
|
+
<td className="py-2 text-muted-foreground">
|
|
95
|
+
{prop.deprecated !== undefined && (
|
|
96
|
+
<p className="font-medium text-canvas-foreground">Deprecated{prop.deprecated ? <>: {inline(prop.deprecated)}</> : "."}</p>
|
|
97
|
+
)}
|
|
98
|
+
{prop.description?.split("\n\n").map((p) => (
|
|
99
|
+
<p key={p} className="whitespace-pre-line">
|
|
100
|
+
{inline(p)}
|
|
101
|
+
</p>
|
|
102
|
+
))}
|
|
103
|
+
</td>
|
|
104
|
+
</tr>
|
|
105
|
+
))}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// A placeholder so the blocks typecheck in the registry. `designtools-blocks map` writes
|
|
3
|
+
// the real one into a project, from its manifest.
|
|
4
|
+
import type { ComponentType } from "react";
|
|
5
|
+
|
|
6
|
+
export const components: Record<string, ComponentType<any>> = {};
|
|
7
|
+
export const examples: Record<string, Record<string, unknown>> = {};
|
|
8
|
+
export const patterns: Record<string, Record<string, unknown>> = {};
|
|
9
|
+
export const assets: Record<string, string | { src: string }> = {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tabs } from "@base-ui/react/tabs";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { CodeView } from "./code-view";
|
|
6
|
+
import { ruleMarkdown } from "./lib/markdown";
|
|
7
|
+
import type { RuleEntry } from "./lib/manifest-types";
|
|
8
|
+
import { StatusBadge } from "./lib/status";
|
|
9
|
+
import { inline, Prose } from "./lib/text";
|
|
10
|
+
|
|
11
|
+
const tab =
|
|
12
|
+
"-mb-px border-b-2 border-transparent px-1 pb-2 text-sm text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-[active]:border-primary data-[active]:font-medium data-[active]:text-canvas-foreground";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One rule, brand or interface, written for people: the statement, the line it
|
|
16
|
+
* draws, what it applies to, the check that proves it. The second tab is the
|
|
17
|
+
* same rule as an agent reads it. Pass a demonstration as children where the
|
|
18
|
+
* rule needs persuading.
|
|
19
|
+
*/
|
|
20
|
+
export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode }) {
|
|
21
|
+
const facts: [string, ReactNode][] = [];
|
|
22
|
+
if (rule.threshold) facts.push(["Threshold", rule.threshold]);
|
|
23
|
+
if (rule.appliesTo?.length) facts.push(["Applies to", rule.appliesTo.join(", ")]);
|
|
24
|
+
if (rule.exceptions?.length) facts.push(["Exceptions", rule.exceptions.join("; ")]);
|
|
25
|
+
if (rule.tokens?.length)
|
|
26
|
+
facts.push([
|
|
27
|
+
"Tokens",
|
|
28
|
+
rule.tokens.map((t) => (
|
|
29
|
+
<code key={t} className="mr-1 font-mono text-xs">
|
|
30
|
+
{t}
|
|
31
|
+
</code>
|
|
32
|
+
)),
|
|
33
|
+
]);
|
|
34
|
+
if (rule.check)
|
|
35
|
+
facts.push([
|
|
36
|
+
"Checked by",
|
|
37
|
+
<>
|
|
38
|
+
{rule.check.kind}
|
|
39
|
+
{rule.check.ref && (
|
|
40
|
+
<>
|
|
41
|
+
{" "}
|
|
42
|
+
<code className="font-mono text-xs">{rule.check.ref}</code>
|
|
43
|
+
</>
|
|
44
|
+
)}
|
|
45
|
+
{rule.check.description && <>. {inline(rule.check.description)}</>}
|
|
46
|
+
</>,
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<article id={rule.id} data-slot="rule" className="flex scroll-mt-8 flex-col gap-4 rounded-lg border border-border p-5">
|
|
51
|
+
<header className="flex flex-wrap items-center justify-between gap-3">
|
|
52
|
+
<h3 className="text-lg font-semibold">
|
|
53
|
+
<a href={`#${rule.id}`} className="hover:underline">
|
|
54
|
+
{rule.title}
|
|
55
|
+
</a>
|
|
56
|
+
</h3>
|
|
57
|
+
<span className="flex items-center gap-2">
|
|
58
|
+
<span className="text-xs text-muted-foreground">{rule.kind}</span>
|
|
59
|
+
<StatusBadge status={rule.status} />
|
|
60
|
+
</span>
|
|
61
|
+
</header>
|
|
62
|
+
<Tabs.Root defaultValue="rendered" className="flex flex-col gap-4">
|
|
63
|
+
<Tabs.List aria-label={`${rule.title}, two ways`} className="flex gap-6 border-b border-border">
|
|
64
|
+
<Tabs.Tab value="rendered" className={tab}>
|
|
65
|
+
Rendered
|
|
66
|
+
</Tabs.Tab>
|
|
67
|
+
<Tabs.Tab value="agent" className={tab}>
|
|
68
|
+
As an agent reads it
|
|
69
|
+
</Tabs.Tab>
|
|
70
|
+
</Tabs.List>
|
|
71
|
+
<Tabs.Panel value="rendered" className="flex flex-col gap-4">
|
|
72
|
+
<p data-slot="rule-statement" className="text-base">
|
|
73
|
+
{inline(rule.statement)}
|
|
74
|
+
</p>
|
|
75
|
+
{facts.length > 0 && (
|
|
76
|
+
<dl className="grid gap-x-6 gap-y-2 text-sm sm:grid-cols-[8rem_1fr]">
|
|
77
|
+
{facts.map(([term, detail]) => (
|
|
78
|
+
<div key={term} className="contents">
|
|
79
|
+
<dt className="font-medium">{term}</dt>
|
|
80
|
+
<dd className="text-muted-foreground">{detail}</dd>
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</dl>
|
|
84
|
+
)}
|
|
85
|
+
{rule.rationale && (
|
|
86
|
+
<div className="flex max-w-prose flex-col gap-2 text-sm text-muted-foreground">
|
|
87
|
+
<Prose text={rule.rationale} />
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
{children && <div data-slot="rule-demonstration">{children}</div>}
|
|
91
|
+
</Tabs.Panel>
|
|
92
|
+
<Tabs.Panel value="agent">
|
|
93
|
+
<CodeView code={ruleMarkdown(rule)} lang="markdown" label="As an agent reads it" wrap />
|
|
94
|
+
</Tabs.Panel>
|
|
95
|
+
</Tabs.Root>
|
|
96
|
+
</article>
|
|
97
|
+
);
|
|
98
|
+
}
|