@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,587 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Live } from "./lib/live";
|
|
4
|
+
import { Tabs } from "@base-ui/react/tabs";
|
|
5
|
+
import { Toggle } from "@base-ui/react/toggle";
|
|
6
|
+
import { ToggleGroup } from "@base-ui/react/toggle-group";
|
|
7
|
+
import {
|
|
8
|
+
Bot,
|
|
9
|
+
Braces,
|
|
10
|
+
Check,
|
|
11
|
+
Code,
|
|
12
|
+
Eye,
|
|
13
|
+
ExternalLink,
|
|
14
|
+
Link2,
|
|
15
|
+
Maximize2,
|
|
16
|
+
Minimize2,
|
|
17
|
+
Minus,
|
|
18
|
+
Monitor,
|
|
19
|
+
Moon,
|
|
20
|
+
PaintBucket,
|
|
21
|
+
PersonStanding,
|
|
22
|
+
Plus,
|
|
23
|
+
RotateCcw,
|
|
24
|
+
Ruler,
|
|
25
|
+
SquareDashed,
|
|
26
|
+
Sun,
|
|
27
|
+
Trash2,
|
|
28
|
+
Zap,
|
|
29
|
+
} from "lucide-react";
|
|
30
|
+
import { cloneElement, isValidElement, type ReactElement, type ReactNode, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
31
|
+
import { AxeReport, useAxe } from "./a11y-panel";
|
|
32
|
+
import { CodeView } from "./code-view";
|
|
33
|
+
import { Boundary } from "./lib/boundary";
|
|
34
|
+
import { cx } from "./lib/cx";
|
|
35
|
+
import { toJSX } from "./lib/jsx";
|
|
36
|
+
import { coerce, Missing, useLookup } from "./lib/lookup";
|
|
37
|
+
import { importOf } from "./lib/manifest";
|
|
38
|
+
import type { ComponentEntry, PropEntry, VariantAxis } from "./lib/manifest-types";
|
|
39
|
+
import { componentMarkdown } from "./lib/markdown";
|
|
40
|
+
import { Measure } from "./lib/measure";
|
|
41
|
+
import { installPseudoStates, type Pseudo } from "./lib/pseudo";
|
|
42
|
+
import { IconButton, IconLink, Toolbar, ToolbarDivider, ToolbarMenu } from "./lib/ui";
|
|
43
|
+
import { filterFor, type Vision, VISIONS, VisionFilters } from "./lib/vision";
|
|
44
|
+
import { AxisControl, FlagControl, JsonControl, Label, NumberControl, StringControl, TextControl, visible } from "./playground";
|
|
45
|
+
import { PropsTable } from "./props-table";
|
|
46
|
+
|
|
47
|
+
type State = "rest" | Pseudo | "disabled";
|
|
48
|
+
type Viewport = "fill" | "375" | "768" | "1280";
|
|
49
|
+
type View = { theme?: "light" | "dark"; bg: string; viewport: Viewport; zoom: number; rtl: boolean; vision: Vision; outline: boolean; measure: boolean };
|
|
50
|
+
|
|
51
|
+
const VIEW: View = { bg: "canvas", viewport: "fill", zoom: 100, rtl: false, vision: "typical", outline: false, measure: false };
|
|
52
|
+
const ZOOMS = [50, 75, 100, 125, 150, 200];
|
|
53
|
+
const VIEWPORTS: { value: Viewport; label: string; note?: string }[] = [
|
|
54
|
+
{ value: "fill", label: "Fill", note: "The width of the stage" },
|
|
55
|
+
{ value: "375", label: "Phone", note: "375 px" },
|
|
56
|
+
{ value: "768", label: "Tablet", note: "768 px" },
|
|
57
|
+
{ value: "1280", label: "Desktop", note: "1280 px" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Props the panel never offers: they are the component's plumbing, not its settings. */
|
|
61
|
+
const PLUMBING = new Set(["children", "className", "style", "render", "ref", "key", "id", "asChild"]);
|
|
62
|
+
|
|
63
|
+
const isFunction = (name: string, p: PropEntry) => /=>|^\(|Function|Handler/.test(p.type) || /^on[A-Z]/.test(name);
|
|
64
|
+
const isJson = (p: PropEntry) => /\[\]|^\{|Array<|Record<|\bobject\b/.test(p.type);
|
|
65
|
+
|
|
66
|
+
interface LogEntry {
|
|
67
|
+
id: number;
|
|
68
|
+
name: string;
|
|
69
|
+
time: string;
|
|
70
|
+
detail: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An event or value, in a line someone can read: `click on <button>`, `"ines"`, `{ value: 3 }`. */
|
|
74
|
+
function describe(args: unknown[]): string {
|
|
75
|
+
return args
|
|
76
|
+
.map((a) => {
|
|
77
|
+
if (a && typeof a === "object" && "nativeEvent" in a && "type" in a) {
|
|
78
|
+
const t = (a as { target?: Element }).target;
|
|
79
|
+
return `${String((a as { type: unknown }).type)} on <${t?.tagName?.toLowerCase() ?? "element"}>`;
|
|
80
|
+
}
|
|
81
|
+
if (a instanceof Event) return `${a.type} event`;
|
|
82
|
+
try {
|
|
83
|
+
const json = JSON.stringify(a);
|
|
84
|
+
return json === undefined ? String(a) : json.length > 80 ? `${json.slice(0, 77)}…` : json;
|
|
85
|
+
} catch {
|
|
86
|
+
return String(a);
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.join(", ");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A component's one main area. On the stage: the component live, at any viewport
|
|
94
|
+
* and zoom, on any background, in either mode or direction, as people with
|
|
95
|
+
* different colour vision see it, with its boxes outlined or measured, and pinned
|
|
96
|
+
* at rest, hovered, focused, pressed or disabled. Beside it: a property for each
|
|
97
|
+
* variant and each prop, generated from their types. Under it: the code for
|
|
98
|
+
* exactly what is showing, the props, a log of every handler that fires, what an
|
|
99
|
+
* agent reads, and a full accessibility report that re-runs as anything changes.
|
|
100
|
+
* Every setting is in the URL, so a configuration can be shared as a link.
|
|
101
|
+
*
|
|
102
|
+
* Pass `props` for what it needs to render at all (its starting values), `content`
|
|
103
|
+
* for children richer than text (a Card's parts), and `wrap` for what it must sit
|
|
104
|
+
* inside to work (a provider, the root a part belongs to): an element, which the
|
|
105
|
+
* component is rendered into.
|
|
106
|
+
*/
|
|
107
|
+
export function Workbench({
|
|
108
|
+
entry,
|
|
109
|
+
props: base = {},
|
|
110
|
+
children: initialText = entry.name,
|
|
111
|
+
content,
|
|
112
|
+
wrap,
|
|
113
|
+
backgrounds = [],
|
|
114
|
+
previewHref,
|
|
115
|
+
}: {
|
|
116
|
+
entry: ComponentEntry;
|
|
117
|
+
props?: Record<string, unknown>;
|
|
118
|
+
/** Text content, editable here. Pass "" for a component without children. */
|
|
119
|
+
children?: string;
|
|
120
|
+
/** Children richer than text; replaces the text control. */
|
|
121
|
+
content?: ReactNode;
|
|
122
|
+
/** An element to render the component inside: a provider, or the root a part needs. */
|
|
123
|
+
wrap?: ReactElement<{ children?: ReactNode }>;
|
|
124
|
+
/** Named backgrounds the brand has (`surface-stone`), offered on the stage beside canvas, surface and muted. */
|
|
125
|
+
backgrounds?: string[];
|
|
126
|
+
/** The preview route for its canonical example, opened from the toolbar. */
|
|
127
|
+
previewHref?: string;
|
|
128
|
+
}) {
|
|
129
|
+
const lookup = useLookup();
|
|
130
|
+
const Component = lookup.componentFor(entry);
|
|
131
|
+
const axes = entry.variants?.axes ?? [];
|
|
132
|
+
const axisNames = new Set(axes.map((a) => a.name));
|
|
133
|
+
const own = Object.entries(entry.props).filter(([name, p]) => !axisNames.has(name) && !PLUMBING.has(name) && p.deprecated === undefined);
|
|
134
|
+
// disabled is a pinned state ("Off"), not a prop control
|
|
135
|
+
const settable = own.filter(([name, p]) => !isFunction(name, p) && !p.from && name !== "disabled");
|
|
136
|
+
const handlers = own.filter(([name, p]) => isFunction(name, p)).map(([name]) => name);
|
|
137
|
+
const canDisable = "disabled" in entry.props;
|
|
138
|
+
|
|
139
|
+
const [variants, setVariants] = useState<Record<string, string>>({});
|
|
140
|
+
const [values, setValues] = useState<Record<string, unknown>>({});
|
|
141
|
+
const [text, setText] = useState(initialText);
|
|
142
|
+
const [state, setState] = useState<State>("rest");
|
|
143
|
+
const [view, setView] = useState<View>(VIEW);
|
|
144
|
+
const [log, setLog] = useState<LogEntry[]>([]);
|
|
145
|
+
const [tab, setTab] = useState<string>("code");
|
|
146
|
+
const [full, setFull] = useState(false);
|
|
147
|
+
const [shared, setShared] = useState(false);
|
|
148
|
+
const set = <K extends keyof View>(key: K, value: View[K]) => setView((v) => ({ ...v, [key]: value }));
|
|
149
|
+
|
|
150
|
+
// read a shared configuration from the URL once, then keep the URL in step with every change;
|
|
151
|
+
// the ref keeps a second run (React's development double effect) from reading the URL back after it was rewritten
|
|
152
|
+
const read = useRef(false);
|
|
153
|
+
const [ready, setReady] = useState(false);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (read.current) return;
|
|
156
|
+
read.current = true;
|
|
157
|
+
const q = new URLSearchParams(window.location.search);
|
|
158
|
+
const v: Record<string, string> = {};
|
|
159
|
+
const p: Record<string, unknown> = {};
|
|
160
|
+
for (const [k, val] of q) {
|
|
161
|
+
if (k.startsWith("v.")) v[k.slice(2)] = val;
|
|
162
|
+
if (k.startsWith("p.")) {
|
|
163
|
+
try {
|
|
164
|
+
p[k.slice(2)] = JSON.parse(val);
|
|
165
|
+
} catch {
|
|
166
|
+
p[k.slice(2)] = val;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
setVariants(v);
|
|
171
|
+
setValues(p);
|
|
172
|
+
if (q.has("text")) setText(q.get("text") ?? "");
|
|
173
|
+
if (q.has("state")) setState(q.get("state") as State);
|
|
174
|
+
setView((cur) => ({
|
|
175
|
+
...cur,
|
|
176
|
+
theme: (q.get("theme") as View["theme"]) ?? undefined,
|
|
177
|
+
bg: q.get("bg") ?? cur.bg,
|
|
178
|
+
viewport: (q.get("vp") as Viewport) ?? cur.viewport,
|
|
179
|
+
zoom: Number(q.get("zoom")) || cur.zoom,
|
|
180
|
+
rtl: q.get("dir") === "rtl",
|
|
181
|
+
vision: (q.get("vision") as Vision) ?? cur.vision,
|
|
182
|
+
}));
|
|
183
|
+
setReady(true);
|
|
184
|
+
}, []);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!ready) return;
|
|
187
|
+
const q = new URLSearchParams();
|
|
188
|
+
for (const [k, v] of Object.entries(variants)) q.set(`v.${k}`, v);
|
|
189
|
+
for (const [k, v] of Object.entries(values)) q.set(`p.${k}`, typeof v === "string" ? v : JSON.stringify(v));
|
|
190
|
+
if (text !== initialText) q.set("text", text);
|
|
191
|
+
if (state !== "rest") q.set("state", state);
|
|
192
|
+
if (view.theme) q.set("theme", view.theme);
|
|
193
|
+
if (view.bg !== VIEW.bg) q.set("bg", view.bg);
|
|
194
|
+
if (view.viewport !== VIEW.viewport) q.set("vp", view.viewport);
|
|
195
|
+
if (view.zoom !== VIEW.zoom) q.set("zoom", String(view.zoom));
|
|
196
|
+
if (view.rtl) q.set("dir", "rtl");
|
|
197
|
+
if (view.vision !== VIEW.vision) q.set("vision", view.vision);
|
|
198
|
+
const search = q.toString();
|
|
199
|
+
window.history.replaceState(null, "", `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`);
|
|
200
|
+
}, [ready, variants, values, text, state, view, initialText]);
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (state === "hover" || state === "focus" || state === "active") installPseudoStates();
|
|
204
|
+
}, [state]);
|
|
205
|
+
|
|
206
|
+
const root = useRef<HTMLDivElement>(null);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const on = () => setFull(document.fullscreenElement === root.current);
|
|
209
|
+
document.addEventListener("fullscreenchange", on);
|
|
210
|
+
return () => document.removeEventListener("fullscreenchange", on);
|
|
211
|
+
}, []);
|
|
212
|
+
const toggleFull = () => (document.fullscreenElement ? document.exitFullscreen() : root.current?.requestFullscreen());
|
|
213
|
+
|
|
214
|
+
const record = useCallback((name: string, args: unknown[]) => {
|
|
215
|
+
const now = new Date();
|
|
216
|
+
setLog((l) => [{ id: now.getTime() + Math.random(), name, time: now.toLocaleTimeString(undefined, { hour12: false }), detail: describe(args) }, ...l].slice(0, 50));
|
|
217
|
+
}, []);
|
|
218
|
+
// every handler the component declares, and the DOM's own click and change, reported to the log
|
|
219
|
+
const logged = useMemo(
|
|
220
|
+
() => Object.fromEntries([...new Set([...handlers, "onClick", "onChange"])].map((name) => [name, (...args: unknown[]) => record(name, args)])),
|
|
221
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the handler names come from the manifest and do not change
|
|
222
|
+
[record],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const chosen = { ...coerce(variants), ...values, ...(state === "disabled" ? { disabled: true } : {}) };
|
|
226
|
+
const jsx = toJSX(entry.export, { ...visible(base), ...visible(values), ...coerce(variants), ...(state === "disabled" ? { disabled: true } : {}) }, content ? undefined : text || undefined);
|
|
227
|
+
const code = `import { ${entry.export} } from "${importOf(entry)}";\n\n${jsx}`;
|
|
228
|
+
const markdown = useMemo(() => componentMarkdown(entry), [entry]);
|
|
229
|
+
|
|
230
|
+
const area = useRef<HTMLDivElement>(null);
|
|
231
|
+
const target = useRef<HTMLDivElement>(null);
|
|
232
|
+
const axe = useAxe(target, { auto: true, watch: JSON.stringify([variants, values, text, state, view.theme, view.bg]) });
|
|
233
|
+
const issues = axe.result?.violations.length;
|
|
234
|
+
const filterId = useId().replace(/:/g, "");
|
|
235
|
+
|
|
236
|
+
if (!Component) return <Missing name={entry.name} />;
|
|
237
|
+
|
|
238
|
+
const changed = Object.keys(variants).length > 0 || Object.keys(values).length > 0 || text !== initialText || state !== "rest";
|
|
239
|
+
const reset = () => {
|
|
240
|
+
setVariants({});
|
|
241
|
+
setValues({});
|
|
242
|
+
setText(initialText);
|
|
243
|
+
setState("rest");
|
|
244
|
+
setView(VIEW);
|
|
245
|
+
};
|
|
246
|
+
const share = async () => {
|
|
247
|
+
try {
|
|
248
|
+
await navigator.clipboard.writeText(window.location.href);
|
|
249
|
+
setShared(true);
|
|
250
|
+
setTimeout(() => setShared(false), 1500);
|
|
251
|
+
} catch {
|
|
252
|
+
// clipboard refused: the URL bar still holds the link
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const bgs = [
|
|
257
|
+
{ value: "canvas", label: "Canvas", note: "The page" },
|
|
258
|
+
{ value: "surface", label: "Surface", note: "Cards and panels" },
|
|
259
|
+
{ value: "muted", label: "Muted", note: "Quiet panels" },
|
|
260
|
+
{ value: "primary", label: "Primary", note: "On the brand colour" },
|
|
261
|
+
...backgrounds.map((b) => ({ value: b, label: b.replace(/^surface-/, "").replace(/^./, (c) => c.toUpperCase()), note: `--${b}` })),
|
|
262
|
+
{ value: "checker", label: "Transparent", note: "To see what it paints" },
|
|
263
|
+
];
|
|
264
|
+
const paint =
|
|
265
|
+
view.bg === "checker"
|
|
266
|
+
? { background: "repeating-conic-gradient(var(--muted) 0 25%, var(--canvas) 0 50%) 0 0 / 16px 16px", color: "var(--canvas-foreground)" }
|
|
267
|
+
: { background: `var(--${view.bg})`, color: `var(--${view.bg}-foreground, var(--canvas-foreground))` };
|
|
268
|
+
const dotted = view.bg === "canvas";
|
|
269
|
+
|
|
270
|
+
const node = (
|
|
271
|
+
<Live>
|
|
272
|
+
<Component {...base} {...logged} {...chosen}>
|
|
273
|
+
{content ?? (text || undefined)}
|
|
274
|
+
</Component>
|
|
275
|
+
</Live>
|
|
276
|
+
);
|
|
277
|
+
const scope = [
|
|
278
|
+
entry.name,
|
|
279
|
+
...Object.entries(variants).map(([k, v]) => `${k} ${v}`),
|
|
280
|
+
state !== "rest" ? state : null,
|
|
281
|
+
view.theme ? `in ${view.theme} mode` : "in the page's mode",
|
|
282
|
+
view.bg !== "canvas" ? `on ${view.bg}` : null,
|
|
283
|
+
]
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join(", ");
|
|
286
|
+
|
|
287
|
+
const tabClass =
|
|
288
|
+
"relative inline-flex h-11 shrink-0 cursor-pointer items-center gap-2 px-1 text-sm text-muted-foreground transition-colors duration-(--dt-duration-fast) hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-active:text-canvas-foreground";
|
|
289
|
+
const toggle = (on: boolean) =>
|
|
290
|
+
cx(
|
|
291
|
+
"relative inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors duration-(--dt-duration-fast)",
|
|
292
|
+
"after:absolute after:inset-x-0 after:-inset-y-[calc((var(--docs-target,2.75rem)-2rem)/2)] after:content-['']",
|
|
293
|
+
"hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring",
|
|
294
|
+
on && "bg-muted text-canvas-foreground",
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div data-slot="workbench" className="overflow-hidden rounded-2xl border border-border bg-surface text-surface-foreground shadow-xs">
|
|
299
|
+
<style>{`[data-wb-outline] * { outline: 1px solid color-mix(in oklch, var(--ring) 55%, transparent); outline-offset: -1px; }`}</style>
|
|
300
|
+
<VisionFilters id={filterId} />
|
|
301
|
+
<div ref={root} className={cx("flex flex-col bg-surface", full && "h-dvh")}>
|
|
302
|
+
<Toolbar className="flex-nowrap overflow-x-auto">
|
|
303
|
+
<span className="flex items-center gap-0.5">
|
|
304
|
+
<ToolbarMenu icon={Monitor} label="Viewport" value={view.viewport} fallback="fill" options={VIEWPORTS} onChange={(v) => set("viewport", v)} />
|
|
305
|
+
<ToolbarMenu
|
|
306
|
+
icon={PaintBucket}
|
|
307
|
+
label="Background"
|
|
308
|
+
value={view.bg}
|
|
309
|
+
fallback="canvas"
|
|
310
|
+
options={bgs}
|
|
311
|
+
onChange={(v) => set("bg", v)}
|
|
312
|
+
swatch={(v) => (v === "checker" ? "repeating-conic-gradient(var(--muted) 0 25%, var(--canvas) 0 50%) 0 0 / 6px 6px" : `var(--${v})`)}
|
|
313
|
+
/>
|
|
314
|
+
<span className="flex items-center rounded-md" role="group" aria-label="Zoom">
|
|
315
|
+
<IconButton
|
|
316
|
+
icon={Minus}
|
|
317
|
+
label="Zoom out"
|
|
318
|
+
disabled={view.zoom <= ZOOMS[0]}
|
|
319
|
+
onClick={() => set("zoom", ZOOMS[Math.max(0, ZOOMS.indexOf(view.zoom) - 1)])}
|
|
320
|
+
className="disabled:opacity-40"
|
|
321
|
+
/>
|
|
322
|
+
<button type="button" onClick={() => set("zoom", 100)} title="Reset zoom" className="h-8 w-11 cursor-pointer rounded-md font-mono text-xs tabular-nums hover:bg-muted">
|
|
323
|
+
{view.zoom}%
|
|
324
|
+
</button>
|
|
325
|
+
<IconButton
|
|
326
|
+
icon={Plus}
|
|
327
|
+
label="Zoom in"
|
|
328
|
+
disabled={view.zoom >= ZOOMS[ZOOMS.length - 1]}
|
|
329
|
+
onClick={() => set("zoom", ZOOMS[Math.min(ZOOMS.length - 1, ZOOMS.indexOf(view.zoom) + 1)])}
|
|
330
|
+
className="disabled:opacity-40"
|
|
331
|
+
/>
|
|
332
|
+
</span>
|
|
333
|
+
<ToolbarDivider />
|
|
334
|
+
<button type="button" aria-pressed={view.outline} title="Outline every box" onClick={() => set("outline", !view.outline)} className={toggle(view.outline)}>
|
|
335
|
+
<SquareDashed aria-hidden className="size-(--dt-size-xs)" />
|
|
336
|
+
<span className="sr-only">Outline every box</span>
|
|
337
|
+
</button>
|
|
338
|
+
<button type="button" aria-pressed={view.measure} title="Measure: point at an element" onClick={() => set("measure", !view.measure)} className={toggle(view.measure)}>
|
|
339
|
+
<Ruler aria-hidden className="size-(--dt-size-xs)" />
|
|
340
|
+
<span className="sr-only">Measure</span>
|
|
341
|
+
</button>
|
|
342
|
+
<ToolbarMenu icon={Eye} label="Colour vision" value={view.vision} fallback="typical" options={VISIONS} onChange={(v) => set("vision", v)} />
|
|
343
|
+
<button type="button" aria-pressed={view.rtl} title="Right to left" onClick={() => set("rtl", !view.rtl)} className={toggle(view.rtl)}>
|
|
344
|
+
<span className="font-mono text-[0.6875rem] tracking-wide">RTL</span>
|
|
345
|
+
</button>
|
|
346
|
+
<IconButton
|
|
347
|
+
icon={view.theme === "dark" ? Sun : Moon}
|
|
348
|
+
label={view.theme === "dark" ? "Show in light mode" : "Show in dark mode"}
|
|
349
|
+
pressed={view.theme !== undefined}
|
|
350
|
+
onClick={() => set("theme", view.theme === "dark" ? "light" : "dark")}
|
|
351
|
+
/>
|
|
352
|
+
</span>
|
|
353
|
+
<span className="flex items-center gap-0.5">
|
|
354
|
+
<IconButton icon={shared ? Check : Link2} label={shared ? "Link copied" : "Copy a link to this configuration"} onClick={share} />
|
|
355
|
+
<IconButton icon={full ? Minimize2 : Maximize2} label={full ? "Leave full screen" : "Full screen"} onClick={toggleFull} />
|
|
356
|
+
{previewHref && <IconLink icon={ExternalLink} label="Open on its own" href={previewHref} target="_blank" rel="noreferrer" />}
|
|
357
|
+
</span>
|
|
358
|
+
</Toolbar>
|
|
359
|
+
|
|
360
|
+
<div className={cx("grid min-h-0 flex-1 lg:grid-cols-[minmax(0,1fr)_20rem]", full && "overflow-hidden")}>
|
|
361
|
+
<div ref={area} data-slot="workbench-stage" className={cx("relative min-w-0 overflow-auto", full ? "h-full" : "min-h-96")}>
|
|
362
|
+
<div
|
|
363
|
+
data-docs-stage=""
|
|
364
|
+
data-theme={view.theme}
|
|
365
|
+
className={cx(
|
|
366
|
+
"flex min-h-full min-w-fit items-center justify-center p-10",
|
|
367
|
+
dotted && "bg-[radial-gradient(color-mix(in_oklch,var(--border)_85%,transparent)_1px,transparent_1px)] [background-size:14px_14px]",
|
|
368
|
+
)}
|
|
369
|
+
style={paint}
|
|
370
|
+
>
|
|
371
|
+
<div
|
|
372
|
+
dir={view.rtl ? "rtl" : undefined}
|
|
373
|
+
data-pseudo={state === "hover" || state === "focus" || state === "active" ? state : undefined}
|
|
374
|
+
data-wb-outline={view.outline || undefined}
|
|
375
|
+
className={cx("flex items-center justify-center", view.viewport !== "fill" && "rounded-lg border border-dashed border-current/25 p-4")}
|
|
376
|
+
style={{
|
|
377
|
+
width: view.viewport === "fill" ? undefined : Number(view.viewport),
|
|
378
|
+
zoom: view.zoom === 100 ? undefined : view.zoom / 100,
|
|
379
|
+
filter: filterFor(view.vision, filterId),
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
<div ref={target} data-slot="workbench-target" className="flex max-w-full items-center justify-center">
|
|
383
|
+
<Boundary label={entry.name}>{wrap && isValidElement(wrap) ? cloneElement(wrap, undefined, node) : node}</Boundary>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
{view.measure && <Measure area={area} target={target} />}
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div data-slot="workbench-controls" className={cx("flex flex-col gap-7 border-t border-border p-5 lg:border-t-0 lg:border-l", full ? "overflow-y-auto" : "lg:max-h-[44rem] lg:overflow-y-auto")}>
|
|
391
|
+
<div className="-mt-1.5 -mb-2 flex min-h-9 items-center justify-between">
|
|
392
|
+
<p className="text-sm font-medium">Properties</p>
|
|
393
|
+
<IconButton icon={RotateCcw} label="Reset everything" onClick={reset} disabled={!changed} className="disabled:pointer-events-none disabled:opacity-40" />
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<StateControl value={state} onChange={setState} canDisable={canDisable} />
|
|
397
|
+
|
|
398
|
+
{axes.length > 0 && (
|
|
399
|
+
<Section title="Variants">
|
|
400
|
+
{axes.map((axis) => (
|
|
401
|
+
<AxisControl key={axis.name} axis={axis} value={variants[axis.name] ?? axis.default ?? ""} onChange={(v) => setVariants((s) => ({ ...s, [axis.name]: v }))} />
|
|
402
|
+
))}
|
|
403
|
+
</Section>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{settable.length > 0 && (
|
|
407
|
+
<Section title="Props">
|
|
408
|
+
{settable.map(([name, p]) => {
|
|
409
|
+
const current = name in values ? values[name] : base[name];
|
|
410
|
+
const put = (v: unknown) => setValues((s) => ({ ...s, [name]: v }));
|
|
411
|
+
const info = <PropInfo prop={p} />;
|
|
412
|
+
if (p.type === "boolean") return <FlagControl key={name} name={name} checked={!!current} onChange={put} />;
|
|
413
|
+
if (p.values?.length) {
|
|
414
|
+
const axis: VariantAxis = { name, options: p.values.map((v) => ({ name: v })), default: p.default?.replace(/^["']|["']$/g, "") };
|
|
415
|
+
return <AxisControl key={name} axis={axis} value={String(current ?? axis.default ?? "")} onChange={put} />;
|
|
416
|
+
}
|
|
417
|
+
if (p.type === "number") return <NumberControl key={name} name={name} value={current as number | undefined} onChange={put} info={info} />;
|
|
418
|
+
if (isJson(p)) return <JsonControl key={name} name={name} value={current} onChange={put} info={info} />;
|
|
419
|
+
return <StringControl key={name} name={name} value={String(current ?? "")} onChange={put} info={info} />;
|
|
420
|
+
})}
|
|
421
|
+
</Section>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{!content && initialText !== "" && (
|
|
425
|
+
<Section title="Content">
|
|
426
|
+
<TextControl value={text} onChange={setText} />
|
|
427
|
+
</Section>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{handlers.length > 0 && (
|
|
431
|
+
<p className="text-xs text-muted-foreground">
|
|
432
|
+
{handlers.join(", ")} {handlers.length === 1 ? "is" : "are"} logged in{" "}
|
|
433
|
+
<button type="button" onClick={() => setTab("actions")} className="cursor-pointer underline underline-offset-2">
|
|
434
|
+
Actions
|
|
435
|
+
</button>
|
|
436
|
+
.
|
|
437
|
+
</p>
|
|
438
|
+
)}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<Tabs.Root value={tab} onValueChange={(v) => setTab(String(v))} className="border-t border-border">
|
|
444
|
+
<Tabs.List aria-label={`About this ${entry.name}`} className="relative flex gap-6 overflow-x-auto border-b border-border px-5">
|
|
445
|
+
<Tabs.Tab value="code" className={tabClass}>
|
|
446
|
+
<Code aria-hidden className="size-4" /> Code
|
|
447
|
+
</Tabs.Tab>
|
|
448
|
+
<Tabs.Tab value="props" className={tabClass}>
|
|
449
|
+
<Braces aria-hidden className="size-4" /> Props
|
|
450
|
+
<span className="font-mono text-xs text-muted-foreground">{Object.keys(entry.props).length}</span>
|
|
451
|
+
</Tabs.Tab>
|
|
452
|
+
<Tabs.Tab value="actions" className={tabClass}>
|
|
453
|
+
<Zap aria-hidden className="size-4" /> Actions
|
|
454
|
+
{log.length > 0 && <span className="rounded-full bg-primary-subdued px-1.5 font-mono text-xs leading-5 text-primary-subdued-foreground">{log.length}</span>}
|
|
455
|
+
</Tabs.Tab>
|
|
456
|
+
<Tabs.Tab value="agent" className={tabClass}>
|
|
457
|
+
<Bot aria-hidden className="size-4" /> For agents
|
|
458
|
+
</Tabs.Tab>
|
|
459
|
+
<Tabs.Tab value="a11y" className={tabClass}>
|
|
460
|
+
<PersonStanding aria-hidden className="size-4" /> Accessibility
|
|
461
|
+
{issues !== undefined && (
|
|
462
|
+
<span
|
|
463
|
+
className={cx(
|
|
464
|
+
"rounded-full px-1.5 font-mono text-xs leading-5",
|
|
465
|
+
issues ? "bg-destructive-subdued text-destructive-subdued-foreground" : "bg-success-subdued text-success-subdued-foreground",
|
|
466
|
+
)}
|
|
467
|
+
>
|
|
468
|
+
{issues ? issues : "ok"}
|
|
469
|
+
<span className="sr-only">{issues ? ` ${issues === 1 ? "violation" : "violations"}` : ", no violations"}</span>
|
|
470
|
+
</span>
|
|
471
|
+
)}
|
|
472
|
+
</Tabs.Tab>
|
|
473
|
+
<Tabs.Indicator className="absolute bottom-0 left-(--active-tab-left) h-0.5 w-(--active-tab-width) bg-[var(--docs-accent,var(--primary))] transition-[left,width] duration-(--dt-duration-base) ease-(--dt-ease-out)" />
|
|
474
|
+
</Tabs.List>
|
|
475
|
+
<Tabs.Panel value="code" className="outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring">
|
|
476
|
+
<CodeView code={code} label={`${entry.name}.tsx`} bare />
|
|
477
|
+
</Tabs.Panel>
|
|
478
|
+
<Tabs.Panel value="props" className="p-5 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring">
|
|
479
|
+
<PropsTable entry={entry} />
|
|
480
|
+
</Tabs.Panel>
|
|
481
|
+
<Tabs.Panel value="actions" className="outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring">
|
|
482
|
+
<ActionLog log={log} onClear={() => setLog([])} />
|
|
483
|
+
</Tabs.Panel>
|
|
484
|
+
<Tabs.Panel value="agent" className="outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring">
|
|
485
|
+
<p className="border-b border-border px-5 py-3 text-sm text-muted-foreground">
|
|
486
|
+
What an agent reads at this page's address with <code className="font-mono text-xs">.md</code>: generated from the manifest, never written twice.
|
|
487
|
+
</p>
|
|
488
|
+
<CodeView code={markdown} lang="markdown" label={`${entry.name}.md`} bare wrap />
|
|
489
|
+
</Tabs.Panel>
|
|
490
|
+
<Tabs.Panel value="a11y" className="p-5 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring">
|
|
491
|
+
<AxeReport {...axe} entry={entry} scope={scope} />
|
|
492
|
+
</Tabs.Panel>
|
|
493
|
+
</Tabs.Root>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function Section({ title, children }: { title: string; children: ReactNode }) {
|
|
499
|
+
return (
|
|
500
|
+
<section className="flex flex-col gap-5">
|
|
501
|
+
<h4 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">{title}</h4>
|
|
502
|
+
{children}
|
|
503
|
+
</section>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function PropInfo({ prop }: { prop: PropEntry }) {
|
|
508
|
+
return (
|
|
509
|
+
<>
|
|
510
|
+
<span className="font-mono text-muted-foreground">{prop.type}</span>
|
|
511
|
+
{prop.description && <span className="text-muted-foreground">{prop.description}</span>}
|
|
512
|
+
{prop.default !== undefined && <span className="text-muted-foreground">Default: {prop.default}</span>}
|
|
513
|
+
</>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Rest, hover, focus, pressed and disabled, pinned: hover and focus without a pointer or a keyboard. */
|
|
518
|
+
function StateControl({ value, onChange, canDisable }: { value: State; onChange: (s: State) => void; canDisable: boolean }) {
|
|
519
|
+
const states: { value: State; label: string }[] = [
|
|
520
|
+
{ value: "rest", label: "Rest" },
|
|
521
|
+
{ value: "hover", label: "Hover" },
|
|
522
|
+
{ value: "focus", label: "Focus" },
|
|
523
|
+
{ value: "active", label: "Press" },
|
|
524
|
+
...(canDisable ? [{ value: "disabled" as State, label: "Off" }] : []),
|
|
525
|
+
];
|
|
526
|
+
const id = useId();
|
|
527
|
+
return (
|
|
528
|
+
<div className="flex flex-col gap-2">
|
|
529
|
+
<Label
|
|
530
|
+
id={id}
|
|
531
|
+
name="state"
|
|
532
|
+
info={<span className="text-muted-foreground">Pins a state on the preview without a pointer or keyboard. Off is the disabled prop.</span>}
|
|
533
|
+
/>
|
|
534
|
+
<ToggleGroup
|
|
535
|
+
aria-labelledby={id}
|
|
536
|
+
value={[value]}
|
|
537
|
+
onValueChange={(next) => next[0] && onChange(next[0] as State)}
|
|
538
|
+
className="grid grid-flow-col auto-cols-fr gap-0.5 rounded-lg bg-muted p-1"
|
|
539
|
+
>
|
|
540
|
+
{states.map((s) => (
|
|
541
|
+
<Toggle
|
|
542
|
+
key={s.value}
|
|
543
|
+
value={s.value}
|
|
544
|
+
className="relative h-8 min-w-0 cursor-pointer truncate rounded-md px-1 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"
|
|
545
|
+
>
|
|
546
|
+
{s.label}
|
|
547
|
+
</Toggle>
|
|
548
|
+
))}
|
|
549
|
+
</ToggleGroup>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Every handler call, newest first: which handler, when, and with what. */
|
|
555
|
+
function ActionLog({ log, onClear }: { log: LogEntry[]; onClear: () => void }) {
|
|
556
|
+
if (log.length === 0) {
|
|
557
|
+
return (
|
|
558
|
+
<p className="flex items-center gap-3 px-5 py-8 text-sm text-muted-foreground">
|
|
559
|
+
<Zap aria-hidden className="size-4" />
|
|
560
|
+
Use the component on the stage: every handler it calls appears here, with what it was called with.
|
|
561
|
+
</p>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return (
|
|
565
|
+
<div className="flex flex-col">
|
|
566
|
+
<div className="flex items-center justify-between border-b border-border px-5 py-2">
|
|
567
|
+
<span className="text-xs text-muted-foreground" aria-live="polite">
|
|
568
|
+
{log.length} {log.length === 1 ? "call" : "calls"}
|
|
569
|
+
</span>
|
|
570
|
+
<button type="button" onClick={onClear} className="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-canvas-foreground">
|
|
571
|
+
<Trash2 aria-hidden className="size-3.5" /> Clear
|
|
572
|
+
</button>
|
|
573
|
+
</div>
|
|
574
|
+
<ol className="max-h-80 divide-y divide-border overflow-y-auto font-mono text-xs">
|
|
575
|
+
{log.map((e) => (
|
|
576
|
+
<li key={e.id} className="grid grid-cols-[5rem_8rem_minmax(0,1fr)] items-baseline gap-4 px-5 py-2.5">
|
|
577
|
+
<span className="text-muted-foreground tabular-nums">{e.time}</span>
|
|
578
|
+
<span className="font-medium text-primary-subdued-foreground">{e.name}</span>
|
|
579
|
+
<span className="truncate text-muted-foreground" title={e.detail}>
|
|
580
|
+
{e.detail}
|
|
581
|
+
</span>
|
|
582
|
+
</li>
|
|
583
|
+
))}
|
|
584
|
+
</ol>
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|