@tonycodes-ui/mortar 0.0.0-stage → 0.12.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/AGENTS.md +10 -0
- package/CHANGELOG.md +268 -0
- package/CONTRIBUTING.md +60 -0
- package/DESIGN.md +51 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/TESTING.md +83 -0
- package/baseline-policy.json +16 -0
- package/bin/mortar.mjs +258 -0
- package/catalog.json +9900 -0
- package/cli/audit.mjs +943 -0
- package/cli/data.mjs +176 -0
- package/cli/mcp.mjs +216 -0
- package/cli/migrations.mjs +128 -0
- package/cli/packet.mjs +267 -0
- package/cli/search.mjs +246 -0
- package/cli/tools.mjs +229 -0
- package/design-principles.json +30 -0
- package/dist/agent-data.json +1 -0
- package/dist/application.d.ts +340 -0
- package/dist/application.js +240 -0
- package/dist/auth.d.ts +121 -0
- package/dist/auth.js +97 -0
- package/dist/brand.d.ts +69 -0
- package/dist/brand.js +266 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.js +310 -0
- package/dist/choice.d.ts +36 -0
- package/dist/choice.js +36 -0
- package/dist/composer.d.ts +28 -0
- package/dist/composer.js +47 -0
- package/dist/display.d.ts +136 -0
- package/dist/display.js +75 -0
- package/dist/docs.d.ts +145 -0
- package/dist/docs.js +141 -0
- package/dist/ecommerce.d.ts +261 -0
- package/dist/ecommerce.js +177 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.js +98 -0
- package/dist/foundation.d.ts +42 -0
- package/dist/foundation.js +121 -0
- package/dist/heading.d.ts +5 -0
- package/dist/heading.js +9 -0
- package/dist/icons.d.ts +50 -0
- package/dist/icons.js +70 -0
- package/dist/illustration.d.ts +47 -0
- package/dist/illustration.js +47 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +18 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +14 -0
- package/dist/internal.d.ts +514 -0
- package/dist/internal.js +332 -0
- package/dist/layout.d.ts +107 -0
- package/dist/layout.js +66 -0
- package/dist/marketing-sections.d.ts +161 -0
- package/dist/marketing-sections.js +58 -0
- package/dist/marketing.d.ts +277 -0
- package/dist/marketing.js +163 -0
- package/dist/navigation.d.ts +35 -0
- package/dist/navigation.js +44 -0
- package/dist/primitives.d.ts +258 -0
- package/dist/primitives.js +244 -0
- package/dist/showcase.d.ts +29 -0
- package/dist/showcase.js +10 -0
- package/dist/workspace.d.ts +80 -0
- package/dist/workspace.js +48 -0
- package/docs/agents.md +127 -0
- package/docs/auth-flows.md +71 -0
- package/docs/migrating-to-0.6.md +94 -0
- package/docs/theming.md +170 -0
- package/package.json +94 -4
- package/skills/mortar/SKILL.md +36 -0
- package/src/application.css +1487 -0
- package/src/application.tsx +1413 -0
- package/src/auth.tsx +452 -0
- package/src/brand.ts +335 -0
- package/src/calendar.css +468 -0
- package/src/calendar.tsx +868 -0
- package/src/choice.tsx +240 -0
- package/src/composer.tsx +158 -0
- package/src/data.css +690 -0
- package/src/display.tsx +452 -0
- package/src/docs.css +704 -0
- package/src/docs.tsx +737 -0
- package/src/ecommerce.css +810 -0
- package/src/ecommerce.tsx +1033 -0
- package/src/feedback.tsx +388 -0
- package/src/foundation.css +1867 -0
- package/src/foundation.tsx +227 -0
- package/src/heading.ts +13 -0
- package/src/icons.tsx +237 -0
- package/src/illustration.tsx +169 -0
- package/src/index.ts +233 -0
- package/src/inspection.tsx +91 -0
- package/src/internal.tsx +700 -0
- package/src/layout.tsx +342 -0
- package/src/marketing-sections.tsx +621 -0
- package/src/marketing.css +2579 -0
- package/src/marketing.tsx +1235 -0
- package/src/navigation.tsx +118 -0
- package/src/overlays.css +350 -0
- package/src/primitives.tsx +1135 -0
- package/src/showcase.tsx +120 -0
- package/src/tokens.css +449 -0
- package/src/workspace.tsx +333 -0
- package/styles.css +56 -0
package/src/docs.tsx
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { KitLink } from "./navigation.js";
|
|
2
|
+
import { useEffect, useId, useState, type ReactNode } from "react";
|
|
3
|
+
import { Button, type LinkAction, type Tone } from "./primitives.js";
|
|
4
|
+
import type { NavigationItem } from "./application.js";
|
|
5
|
+
import { CommandPalette, type CommandItem } from "./workspace.js";
|
|
6
|
+
import { Kbd } from "./display.js";
|
|
7
|
+
import { BrandMark } from "./layout.js";
|
|
8
|
+
import type { ItemGroup, LinkItem } from "./navigation.js";
|
|
9
|
+
import { childLevel, headingTag, type HeadingLevel } from "./heading.js";
|
|
10
|
+
import {
|
|
11
|
+
AsComponentRoot,
|
|
12
|
+
MobileNavDialog,
|
|
13
|
+
NavGroups,
|
|
14
|
+
TermList,
|
|
15
|
+
cx,
|
|
16
|
+
componentRoot,
|
|
17
|
+
rovingIndex,
|
|
18
|
+
} from "./internal.js";
|
|
19
|
+
|
|
20
|
+
export interface DocsShellProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
brand: string;
|
|
23
|
+
/** Logo node shown in place of the placeholder brand mark. */
|
|
24
|
+
logo?: ReactNode;
|
|
25
|
+
/** Use the available content width for explorers and workspaces. */
|
|
26
|
+
layout?: "article" | "wide";
|
|
27
|
+
navigationLabel?: string;
|
|
28
|
+
homeHref?: string;
|
|
29
|
+
version?: ReactNode;
|
|
30
|
+
sections?: Pick<LinkItem, "label" | "href" | "current">[];
|
|
31
|
+
/** Groups of pages; items show their icon and count, and children become disclosures. */
|
|
32
|
+
navigation: ItemGroup<NavigationItem>[];
|
|
33
|
+
toc?: { id: string; label: string; current?: boolean }[];
|
|
34
|
+
tocLabel?: string;
|
|
35
|
+
tocFooter?: ReactNode;
|
|
36
|
+
search?: ReactNode;
|
|
37
|
+
actions?: ReactNode;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function DocsShell({
|
|
42
|
+
className,
|
|
43
|
+
brand,
|
|
44
|
+
logo,
|
|
45
|
+
layout = "article",
|
|
46
|
+
navigationLabel = "Documentation",
|
|
47
|
+
homeHref = "/",
|
|
48
|
+
version,
|
|
49
|
+
sections = [],
|
|
50
|
+
navigation,
|
|
51
|
+
toc = [],
|
|
52
|
+
tocLabel = "On this page",
|
|
53
|
+
tocFooter,
|
|
54
|
+
search,
|
|
55
|
+
actions,
|
|
56
|
+
children,
|
|
57
|
+
}: DocsShellProps) {
|
|
58
|
+
const [open, setOpen] = useState(false);
|
|
59
|
+
const contentId = useId();
|
|
60
|
+
const tree = (
|
|
61
|
+
<nav aria-label={navigationLabel}>
|
|
62
|
+
<NavGroups groups={navigation} onNavigate={() => setOpen(false)} />
|
|
63
|
+
</nav>
|
|
64
|
+
);
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
{...componentRoot("DocsShell")}
|
|
68
|
+
className={cx("m-docs-shell", className)}
|
|
69
|
+
data-layout={layout === "article" ? undefined : layout}
|
|
70
|
+
>
|
|
71
|
+
<KitLink className="m-skip" href={`#${contentId}`}>
|
|
72
|
+
Skip to content
|
|
73
|
+
</KitLink>
|
|
74
|
+
<header className="m-docs-header">
|
|
75
|
+
<Button
|
|
76
|
+
variant="ghost"
|
|
77
|
+
className="m-menu-trigger"
|
|
78
|
+
onClick={() => setOpen(true)}
|
|
79
|
+
aria-haspopup="dialog"
|
|
80
|
+
aria-expanded={open}
|
|
81
|
+
>
|
|
82
|
+
Menu
|
|
83
|
+
</Button>
|
|
84
|
+
<KitLink className="m-brand" href={homeHref}>
|
|
85
|
+
<BrandMark logo={logo} />
|
|
86
|
+
{brand}
|
|
87
|
+
</KitLink>
|
|
88
|
+
{version && <div className="m-docs-version">{version}</div>}
|
|
89
|
+
{sections.length > 0 && (
|
|
90
|
+
<nav aria-label="Sections" className="m-docs-sections">
|
|
91
|
+
{sections.map((s, index) => (
|
|
92
|
+
<KitLink
|
|
93
|
+
key={`${s.label}:${s.href}:${index}`}
|
|
94
|
+
href={s.href}
|
|
95
|
+
aria-current={s.current ? "page" : undefined}
|
|
96
|
+
>
|
|
97
|
+
{s.label}
|
|
98
|
+
</KitLink>
|
|
99
|
+
))}
|
|
100
|
+
</nav>
|
|
101
|
+
)}
|
|
102
|
+
<div className="m-actions">
|
|
103
|
+
{search}
|
|
104
|
+
{actions}
|
|
105
|
+
</div>
|
|
106
|
+
</header>
|
|
107
|
+
<div className="m-docs-body">
|
|
108
|
+
<aside className="m-docs-nav">{tree}</aside>
|
|
109
|
+
<main id={contentId} tabIndex={-1}>
|
|
110
|
+
{children}
|
|
111
|
+
</main>
|
|
112
|
+
{toc.length > 0 && (
|
|
113
|
+
<aside className="m-docs-toc">
|
|
114
|
+
<nav aria-label={tocLabel}>
|
|
115
|
+
<h2>{tocLabel}</h2>
|
|
116
|
+
{toc.map((t) => (
|
|
117
|
+
<KitLink
|
|
118
|
+
key={t.id}
|
|
119
|
+
href={`#${t.id}`}
|
|
120
|
+
aria-current={t.current ? "location" : undefined}
|
|
121
|
+
>
|
|
122
|
+
{t.label}
|
|
123
|
+
</KitLink>
|
|
124
|
+
))}
|
|
125
|
+
</nav>
|
|
126
|
+
{tocFooter && <div className="m-docs-toc-footer">{tocFooter}</div>}
|
|
127
|
+
</aside>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
<MobileNavDialog open={open} onClose={() => setOpen(false)} title={brand}>
|
|
131
|
+
{tree}
|
|
132
|
+
</MobileNavDialog>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True when a key press belongs to a text field or an open dialog, not the page. */
|
|
138
|
+
function ownedKeyPress(target: EventTarget | null) {
|
|
139
|
+
if (!(target instanceof Element)) return false;
|
|
140
|
+
return Boolean(
|
|
141
|
+
target.closest(
|
|
142
|
+
'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="dialog"], [role="alertdialog"], dialog[open]',
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function SearchTrigger({
|
|
148
|
+
className,
|
|
149
|
+
onOpen,
|
|
150
|
+
label = "Search docs",
|
|
151
|
+
}: {
|
|
152
|
+
className?: string;
|
|
153
|
+
onOpen: () => void;
|
|
154
|
+
label?: string;
|
|
155
|
+
}) {
|
|
156
|
+
// The shortcut hint is platform-specific; it renders after mount so server
|
|
157
|
+
// and client markup agree.
|
|
158
|
+
const [apple, setApple] = useState<boolean>();
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
setApple(
|
|
161
|
+
/Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent),
|
|
162
|
+
);
|
|
163
|
+
}, []);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const onKey = (e: KeyboardEvent) => {
|
|
166
|
+
if (
|
|
167
|
+
!(e.metaKey || e.ctrlKey) ||
|
|
168
|
+
e.altKey ||
|
|
169
|
+
e.shiftKey ||
|
|
170
|
+
e.key.toLowerCase() !== "k" ||
|
|
171
|
+
e.repeat ||
|
|
172
|
+
// Another trigger on the page already handled this press.
|
|
173
|
+
e.defaultPrevented ||
|
|
174
|
+
ownedKeyPress(e.target)
|
|
175
|
+
)
|
|
176
|
+
return;
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
onOpen();
|
|
179
|
+
};
|
|
180
|
+
window.addEventListener("keydown", onKey);
|
|
181
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
182
|
+
}, [onOpen]);
|
|
183
|
+
return (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
{...componentRoot("SearchTrigger")}
|
|
187
|
+
className={cx("m-search-trigger", className)}
|
|
188
|
+
onClick={onOpen}
|
|
189
|
+
aria-keyshortcuts="Meta+K Control+K"
|
|
190
|
+
>
|
|
191
|
+
<span>{label}</span>
|
|
192
|
+
{apple !== undefined && <Kbd keys={[apple ? "⌘K" : "Ctrl K"]} />}
|
|
193
|
+
</button>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function SearchDialog(props: {
|
|
198
|
+
open: boolean;
|
|
199
|
+
onClose: () => void;
|
|
200
|
+
query: string;
|
|
201
|
+
onQueryChange: (value: string) => void;
|
|
202
|
+
groups: ItemGroup<CommandItem>[];
|
|
203
|
+
attribution?: ReactNode;
|
|
204
|
+
}) {
|
|
205
|
+
return (
|
|
206
|
+
<AsComponentRoot name="SearchDialog" of="CommandPalette">
|
|
207
|
+
<CommandPalette
|
|
208
|
+
{...props}
|
|
209
|
+
label="Search documentation"
|
|
210
|
+
placeholder="Search docs"
|
|
211
|
+
footer={
|
|
212
|
+
<>
|
|
213
|
+
<span>
|
|
214
|
+
<Kbd keys={["↑", "↓"]} /> navigate
|
|
215
|
+
</span>
|
|
216
|
+
<span>
|
|
217
|
+
<Kbd keys={["↵"]} /> open
|
|
218
|
+
</span>
|
|
219
|
+
{props.attribution && (
|
|
220
|
+
<span className="m-palette-attribution">{props.attribution}</span>
|
|
221
|
+
)}
|
|
222
|
+
</>
|
|
223
|
+
}
|
|
224
|
+
/>
|
|
225
|
+
</AsComponentRoot>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function Steps({
|
|
230
|
+
id,
|
|
231
|
+
className,
|
|
232
|
+
items,
|
|
233
|
+
itemHeadingLevel = 3,
|
|
234
|
+
}: {
|
|
235
|
+
id?: string;
|
|
236
|
+
className?: string;
|
|
237
|
+
items: {
|
|
238
|
+
title: string;
|
|
239
|
+
id?: string;
|
|
240
|
+
description?: ReactNode;
|
|
241
|
+
content?: ReactNode;
|
|
242
|
+
}[];
|
|
243
|
+
/** Level of each step's title. */
|
|
244
|
+
itemHeadingLevel?: HeadingLevel;
|
|
245
|
+
}) {
|
|
246
|
+
const Heading = headingTag(itemHeadingLevel);
|
|
247
|
+
return (
|
|
248
|
+
<ol
|
|
249
|
+
id={id}
|
|
250
|
+
{...componentRoot("Steps")}
|
|
251
|
+
className={cx("m-steps", className)}
|
|
252
|
+
>
|
|
253
|
+
{items.map((item, i) => (
|
|
254
|
+
<li key={`${item.title}:${i}`}>
|
|
255
|
+
<span className="m-step-number" aria-hidden="true">
|
|
256
|
+
{i + 1}
|
|
257
|
+
</span>
|
|
258
|
+
<div>
|
|
259
|
+
<Heading id={item.id}>{item.title}</Heading>
|
|
260
|
+
{item.description && (
|
|
261
|
+
<div className="m-step-description">{item.description}</div>
|
|
262
|
+
)}
|
|
263
|
+
{item.content}
|
|
264
|
+
</div>
|
|
265
|
+
</li>
|
|
266
|
+
))}
|
|
267
|
+
</ol>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const calloutLabel: Record<Tone, string> = {
|
|
272
|
+
neutral: "Note",
|
|
273
|
+
info: "Note",
|
|
274
|
+
success: "Tip",
|
|
275
|
+
warning: "Warning",
|
|
276
|
+
danger: "Caution",
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export function Callout({
|
|
280
|
+
id,
|
|
281
|
+
className,
|
|
282
|
+
tone = "neutral",
|
|
283
|
+
title,
|
|
284
|
+
children,
|
|
285
|
+
}: {
|
|
286
|
+
id?: string;
|
|
287
|
+
className?: string;
|
|
288
|
+
/** neutral: a note. info: a highlighted note. success: a tip. warning and danger: a caution, e.g. a breaking change. */
|
|
289
|
+
tone?: Tone;
|
|
290
|
+
/** Visible lead-in that also names the note; defaults to the tone's word, e.g. "Warning". */
|
|
291
|
+
title?: string;
|
|
292
|
+
children: ReactNode;
|
|
293
|
+
}) {
|
|
294
|
+
const labelId = useId();
|
|
295
|
+
return (
|
|
296
|
+
<div
|
|
297
|
+
id={id}
|
|
298
|
+
{...componentRoot("Callout")}
|
|
299
|
+
className={cx("m-callout", className)}
|
|
300
|
+
role="note"
|
|
301
|
+
aria-labelledby={labelId}
|
|
302
|
+
data-tone={tone === "neutral" ? undefined : tone}
|
|
303
|
+
>
|
|
304
|
+
<strong id={labelId}>{title || calloutLabel[tone]}</strong>
|
|
305
|
+
<span aria-hidden="true"> · </span>
|
|
306
|
+
<div>{children}</div>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface FileNode {
|
|
312
|
+
name: string;
|
|
313
|
+
note?: string;
|
|
314
|
+
highlighted?: boolean;
|
|
315
|
+
children?: FileNode[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function FileBranch({ nodes }: { nodes: FileNode[] }) {
|
|
319
|
+
return (
|
|
320
|
+
<ul>
|
|
321
|
+
{nodes.map((node) => (
|
|
322
|
+
<li
|
|
323
|
+
key={node.name}
|
|
324
|
+
data-folder={!!node.children || undefined}
|
|
325
|
+
data-highlighted={node.highlighted || undefined}
|
|
326
|
+
>
|
|
327
|
+
<span className="m-file-row">
|
|
328
|
+
<span aria-hidden="true" className="m-file-caret">
|
|
329
|
+
{node.children ? "▾" : ""}
|
|
330
|
+
</span>
|
|
331
|
+
<span>{node.name}</span>
|
|
332
|
+
{node.note && <small>{node.note}</small>}
|
|
333
|
+
</span>
|
|
334
|
+
{node.children && <FileBranch nodes={node.children} />}
|
|
335
|
+
</li>
|
|
336
|
+
))}
|
|
337
|
+
</ul>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function FileTree({
|
|
342
|
+
id,
|
|
343
|
+
className,
|
|
344
|
+
label,
|
|
345
|
+
items,
|
|
346
|
+
}: {
|
|
347
|
+
id?: string;
|
|
348
|
+
className?: string;
|
|
349
|
+
label: string;
|
|
350
|
+
items: FileNode[];
|
|
351
|
+
}) {
|
|
352
|
+
return (
|
|
353
|
+
<figure
|
|
354
|
+
id={id}
|
|
355
|
+
{...componentRoot("FileTree")}
|
|
356
|
+
className={cx("m-file-tree", className)}
|
|
357
|
+
aria-label={label}
|
|
358
|
+
>
|
|
359
|
+
<FileBranch nodes={items} />
|
|
360
|
+
</figure>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function CopyButton({ text }: { text: string }) {
|
|
365
|
+
const [result, setResult] = useState<"copied" | "failed">();
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (!result) return;
|
|
368
|
+
const t = setTimeout(() => setResult(undefined), 1600);
|
|
369
|
+
return () => clearTimeout(t);
|
|
370
|
+
}, [result]);
|
|
371
|
+
const copy = async () => {
|
|
372
|
+
try {
|
|
373
|
+
// A missing API (insecure context) and a denied permission both fail.
|
|
374
|
+
if (!navigator.clipboard) throw Error("Clipboard unavailable");
|
|
375
|
+
await navigator.clipboard.writeText(text);
|
|
376
|
+
setResult("copied");
|
|
377
|
+
} catch {
|
|
378
|
+
setResult("failed");
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
return (
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
className="m-code-copy"
|
|
385
|
+
data-result={result}
|
|
386
|
+
onClick={copy}
|
|
387
|
+
>
|
|
388
|
+
<span aria-live="polite">
|
|
389
|
+
{result === "copied"
|
|
390
|
+
? "Copied"
|
|
391
|
+
: result === "failed"
|
|
392
|
+
? "Copy failed"
|
|
393
|
+
: "Copy"}
|
|
394
|
+
</span>
|
|
395
|
+
</button>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Dark code surface with optional language tabs, filename, line numbers and highlighted lines. */
|
|
400
|
+
export function CodeTabs({
|
|
401
|
+
id,
|
|
402
|
+
className,
|
|
403
|
+
tabs,
|
|
404
|
+
filename,
|
|
405
|
+
language,
|
|
406
|
+
lineNumbers = false,
|
|
407
|
+
highlightLines = [],
|
|
408
|
+
prompt,
|
|
409
|
+
}: {
|
|
410
|
+
id?: string;
|
|
411
|
+
className?: string;
|
|
412
|
+
tabs: { id: string; label: string; code: string }[];
|
|
413
|
+
filename?: string;
|
|
414
|
+
language?: string;
|
|
415
|
+
lineNumbers?: boolean;
|
|
416
|
+
highlightLines?: number[];
|
|
417
|
+
prompt?: string;
|
|
418
|
+
}) {
|
|
419
|
+
const [active, setActive] = useState(tabs[0]?.id);
|
|
420
|
+
const base = useId();
|
|
421
|
+
const current = tabs.find((t) => t.id === active) || tabs[0];
|
|
422
|
+
if (!current) return null;
|
|
423
|
+
const lines = current.code.split("\n");
|
|
424
|
+
const tabId = (tab: { id: string }) => `${base}-tab-${tab.id}`;
|
|
425
|
+
const panelId = `${base}-panel`;
|
|
426
|
+
return (
|
|
427
|
+
<figure
|
|
428
|
+
id={id}
|
|
429
|
+
{...componentRoot("CodeTabs")}
|
|
430
|
+
className={cx("m-code-tabs", className)}
|
|
431
|
+
>
|
|
432
|
+
<div className="m-code-bar">
|
|
433
|
+
{tabs.length > 1 ? (
|
|
434
|
+
<div role="tablist" aria-label={filename || "Code examples"}>
|
|
435
|
+
{tabs.map((t) => (
|
|
436
|
+
<button
|
|
437
|
+
key={t.id}
|
|
438
|
+
type="button"
|
|
439
|
+
role="tab"
|
|
440
|
+
id={tabId(t)}
|
|
441
|
+
aria-selected={t.id === current.id}
|
|
442
|
+
aria-controls={panelId}
|
|
443
|
+
tabIndex={t.id === current.id ? 0 : -1}
|
|
444
|
+
onClick={() => setActive(t.id)}
|
|
445
|
+
onKeyDown={(e) => {
|
|
446
|
+
const n = rovingIndex(
|
|
447
|
+
e.key,
|
|
448
|
+
tabs.findIndex((x) => x.id === current.id),
|
|
449
|
+
tabs.length,
|
|
450
|
+
);
|
|
451
|
+
if (n === null) return;
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
const next = tabs[n]!;
|
|
454
|
+
setActive(next.id);
|
|
455
|
+
document.getElementById(tabId(next))?.focus();
|
|
456
|
+
}}
|
|
457
|
+
>
|
|
458
|
+
{t.label}
|
|
459
|
+
</button>
|
|
460
|
+
))}
|
|
461
|
+
</div>
|
|
462
|
+
) : (
|
|
463
|
+
<figcaption>{filename || current.label}</figcaption>
|
|
464
|
+
)}
|
|
465
|
+
{language && <span className="m-code-language">{language}</span>}
|
|
466
|
+
<CopyButton text={current.code} />
|
|
467
|
+
</div>
|
|
468
|
+
<pre
|
|
469
|
+
tabIndex={0}
|
|
470
|
+
id={tabs.length > 1 ? panelId : undefined}
|
|
471
|
+
role={tabs.length > 1 ? "tabpanel" : undefined}
|
|
472
|
+
aria-labelledby={tabs.length > 1 ? tabId(current) : undefined}
|
|
473
|
+
>
|
|
474
|
+
<code>
|
|
475
|
+
{lines.map((line, i) => (
|
|
476
|
+
<span
|
|
477
|
+
key={i}
|
|
478
|
+
className="m-code-line"
|
|
479
|
+
data-highlighted={highlightLines.includes(i + 1) || undefined}
|
|
480
|
+
>
|
|
481
|
+
{lineNumbers && (
|
|
482
|
+
<span className="m-code-number" aria-hidden="true">
|
|
483
|
+
{i + 1}
|
|
484
|
+
</span>
|
|
485
|
+
)}
|
|
486
|
+
{prompt && (
|
|
487
|
+
<span className="m-code-prompt" aria-hidden="true">
|
|
488
|
+
{prompt}{" "}
|
|
489
|
+
</span>
|
|
490
|
+
)}
|
|
491
|
+
{line || " "}
|
|
492
|
+
</span>
|
|
493
|
+
))}
|
|
494
|
+
</code>
|
|
495
|
+
</pre>
|
|
496
|
+
</figure>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function Release({
|
|
501
|
+
id,
|
|
502
|
+
className,
|
|
503
|
+
version,
|
|
504
|
+
date,
|
|
505
|
+
dateLabel,
|
|
506
|
+
changes,
|
|
507
|
+
headingLevel = 3,
|
|
508
|
+
}: {
|
|
509
|
+
id?: string;
|
|
510
|
+
className?: string;
|
|
511
|
+
/** Level of the version heading; change groups sit one level below. */
|
|
512
|
+
headingLevel?: HeadingLevel;
|
|
513
|
+
version: string;
|
|
514
|
+
date: string;
|
|
515
|
+
dateLabel: string;
|
|
516
|
+
changes: {
|
|
517
|
+
type: "added" | "changed" | "fixed" | "deprecated" | "removed";
|
|
518
|
+
items: ReactNode[];
|
|
519
|
+
}[];
|
|
520
|
+
}) {
|
|
521
|
+
const Heading = headingTag(headingLevel);
|
|
522
|
+
const GroupHeading = headingTag(childLevel(headingLevel));
|
|
523
|
+
return (
|
|
524
|
+
<article
|
|
525
|
+
id={id}
|
|
526
|
+
{...componentRoot("Release")}
|
|
527
|
+
className={cx("m-release", className)}
|
|
528
|
+
>
|
|
529
|
+
<header>
|
|
530
|
+
<Heading className="m-release-version">{version}</Heading>
|
|
531
|
+
<time dateTime={date}>{dateLabel}</time>
|
|
532
|
+
</header>
|
|
533
|
+
<div>
|
|
534
|
+
{changes.map((c) => (
|
|
535
|
+
<section key={c.type} data-type={c.type}>
|
|
536
|
+
<GroupHeading className="m-release-type">
|
|
537
|
+
{c.type.charAt(0).toUpperCase() + c.type.slice(1)}
|
|
538
|
+
</GroupHeading>
|
|
539
|
+
<ul>
|
|
540
|
+
{c.items.map((item, i) => (
|
|
541
|
+
<li key={i}>{item}</li>
|
|
542
|
+
))}
|
|
543
|
+
</ul>
|
|
544
|
+
</section>
|
|
545
|
+
))}
|
|
546
|
+
</div>
|
|
547
|
+
</article>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function ApiEndpoint({
|
|
552
|
+
id,
|
|
553
|
+
className,
|
|
554
|
+
headingLevel = 2,
|
|
555
|
+
method,
|
|
556
|
+
path,
|
|
557
|
+
title,
|
|
558
|
+
description,
|
|
559
|
+
paramsLabel = "Body parameters",
|
|
560
|
+
params,
|
|
561
|
+
examples,
|
|
562
|
+
}: {
|
|
563
|
+
id?: string;
|
|
564
|
+
className?: string;
|
|
565
|
+
/** Level of the endpoint title; the parameters heading sits one level below. */
|
|
566
|
+
headingLevel?: HeadingLevel;
|
|
567
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
568
|
+
path: string;
|
|
569
|
+
title: string;
|
|
570
|
+
description?: ReactNode;
|
|
571
|
+
paramsLabel?: string;
|
|
572
|
+
params: {
|
|
573
|
+
name: string;
|
|
574
|
+
type: string;
|
|
575
|
+
required?: boolean;
|
|
576
|
+
description: ReactNode;
|
|
577
|
+
}[];
|
|
578
|
+
/** Request/response samples, typically CodeTabs. */
|
|
579
|
+
examples?: ReactNode;
|
|
580
|
+
}) {
|
|
581
|
+
const headingId = useId();
|
|
582
|
+
const Heading = headingTag(headingLevel);
|
|
583
|
+
const ParamsHeading = headingTag(childLevel(headingLevel));
|
|
584
|
+
return (
|
|
585
|
+
<section
|
|
586
|
+
id={id}
|
|
587
|
+
{...componentRoot("ApiEndpoint")}
|
|
588
|
+
className={cx("m-api-endpoint", className)}
|
|
589
|
+
aria-labelledby={headingId}
|
|
590
|
+
>
|
|
591
|
+
<div className="m-api-text">
|
|
592
|
+
<p className="m-api-route">
|
|
593
|
+
<span data-method={method}>{method}</span>
|
|
594
|
+
<code>{path}</code>
|
|
595
|
+
</p>
|
|
596
|
+
<Heading id={headingId} className="m-api-title">
|
|
597
|
+
{title}
|
|
598
|
+
</Heading>
|
|
599
|
+
{description && <div className="m-api-description">{description}</div>}
|
|
600
|
+
<ParamsHeading className="m-api-params-title">
|
|
601
|
+
{paramsLabel}
|
|
602
|
+
</ParamsHeading>
|
|
603
|
+
<TermList
|
|
604
|
+
className="m-param-list"
|
|
605
|
+
items={params.map((p) => ({
|
|
606
|
+
key: p.name,
|
|
607
|
+
term: (
|
|
608
|
+
<>
|
|
609
|
+
<code>{p.name}</code>
|
|
610
|
+
<span>{p.type}</span>
|
|
611
|
+
<span data-required={p.required || undefined}>
|
|
612
|
+
{p.required ? "Required" : "Optional"}
|
|
613
|
+
</span>
|
|
614
|
+
</>
|
|
615
|
+
),
|
|
616
|
+
details: p.description,
|
|
617
|
+
}))}
|
|
618
|
+
/>
|
|
619
|
+
</div>
|
|
620
|
+
{examples && <div className="m-api-examples">{examples}</div>}
|
|
621
|
+
</section>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function PageFeedback({
|
|
626
|
+
id,
|
|
627
|
+
className,
|
|
628
|
+
question = "Was this page helpful?",
|
|
629
|
+
value,
|
|
630
|
+
onChange,
|
|
631
|
+
updated,
|
|
632
|
+
}: {
|
|
633
|
+
id?: string;
|
|
634
|
+
className?: string;
|
|
635
|
+
question?: string;
|
|
636
|
+
value: "yes" | "no" | null;
|
|
637
|
+
onChange: (value: "yes" | "no") => void;
|
|
638
|
+
updated?: ReactNode;
|
|
639
|
+
}) {
|
|
640
|
+
return (
|
|
641
|
+
<div
|
|
642
|
+
id={id}
|
|
643
|
+
{...componentRoot("PageFeedback")}
|
|
644
|
+
className={cx("m-page-feedback", className)}
|
|
645
|
+
>
|
|
646
|
+
<div role="group" aria-label={question}>
|
|
647
|
+
<span>{question}</span>
|
|
648
|
+
{(["yes", "no"] as const).map((v) => (
|
|
649
|
+
<button
|
|
650
|
+
key={v}
|
|
651
|
+
type="button"
|
|
652
|
+
aria-pressed={value === v}
|
|
653
|
+
onClick={() => onChange(v)}
|
|
654
|
+
>
|
|
655
|
+
{v === "yes" ? "Yes" : "No"}
|
|
656
|
+
</button>
|
|
657
|
+
))}
|
|
658
|
+
</div>
|
|
659
|
+
{updated && <span>{updated}</span>}
|
|
660
|
+
</div>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function PagerLinks({
|
|
665
|
+
id,
|
|
666
|
+
className,
|
|
667
|
+
previous,
|
|
668
|
+
next,
|
|
669
|
+
label = "Previous and next page",
|
|
670
|
+
}: {
|
|
671
|
+
id?: string;
|
|
672
|
+
className?: string;
|
|
673
|
+
previous?: LinkAction;
|
|
674
|
+
next?: LinkAction;
|
|
675
|
+
/** Names the navigation landmark; keep it distinct from other navigation on the page. */
|
|
676
|
+
label?: string;
|
|
677
|
+
}) {
|
|
678
|
+
return (
|
|
679
|
+
<nav
|
|
680
|
+
id={id}
|
|
681
|
+
{...componentRoot("PagerLinks")}
|
|
682
|
+
className={cx("m-pager", className)}
|
|
683
|
+
aria-label={label}
|
|
684
|
+
>
|
|
685
|
+
{previous ? (
|
|
686
|
+
<KitLink href={previous.href} rel="prev">
|
|
687
|
+
<small>Previous</small>
|
|
688
|
+
<span>
|
|
689
|
+
<span aria-hidden="true">← </span>
|
|
690
|
+
{previous.label}
|
|
691
|
+
</span>
|
|
692
|
+
</KitLink>
|
|
693
|
+
) : (
|
|
694
|
+
<span />
|
|
695
|
+
)}
|
|
696
|
+
{next && (
|
|
697
|
+
<KitLink href={next.href} rel="next" data-next="true">
|
|
698
|
+
<small>Next</small>
|
|
699
|
+
<span>
|
|
700
|
+
{next.label}
|
|
701
|
+
<span aria-hidden="true"> →</span>
|
|
702
|
+
</span>
|
|
703
|
+
</KitLink>
|
|
704
|
+
)}
|
|
705
|
+
</nav>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function LinkCards({
|
|
710
|
+
id,
|
|
711
|
+
className,
|
|
712
|
+
items,
|
|
713
|
+
}: {
|
|
714
|
+
id?: string;
|
|
715
|
+
className?: string;
|
|
716
|
+
items: Pick<LinkItem, "label" | "href" | "description">[];
|
|
717
|
+
}) {
|
|
718
|
+
return (
|
|
719
|
+
<ul
|
|
720
|
+
id={id}
|
|
721
|
+
{...componentRoot("LinkCards")}
|
|
722
|
+
className={cx("m-link-cards", className)}
|
|
723
|
+
>
|
|
724
|
+
{items.map((item, index) => (
|
|
725
|
+
<li key={`${item.label}:${item.href}:${index}`}>
|
|
726
|
+
<KitLink href={item.href}>
|
|
727
|
+
<strong>
|
|
728
|
+
{item.label}
|
|
729
|
+
<span aria-hidden="true"> →</span>
|
|
730
|
+
</strong>
|
|
731
|
+
{item.description && <span>{item.description}</span>}
|
|
732
|
+
</KitLink>
|
|
733
|
+
</li>
|
|
734
|
+
))}
|
|
735
|
+
</ul>
|
|
736
|
+
);
|
|
737
|
+
}
|