blodemd 0.0.11 → 0.0.13

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.
Files changed (86) hide show
  1. package/README.md +11 -47
  2. package/dev-server/app/layout.tsx +1 -1
  3. package/dev-server/next.config.js +19 -9
  4. package/dev-server/tsconfig.json +0 -3
  5. package/dist/cli.mjs +732 -123
  6. package/dist/cli.mjs.map +1 -1
  7. package/docs/app/globals.css +15 -1
  8. package/docs/components/api/api-playground.tsx +2 -2
  9. package/docs/components/docs/copy-page-menu.tsx +55 -27
  10. package/docs/components/docs/doc-header.tsx +1 -1
  11. package/docs/components/docs/doc-shell.tsx +89 -88
  12. package/docs/components/docs/doc-sidebar.tsx +6 -3
  13. package/docs/components/docs/doc-toc.tsx +1 -1
  14. package/docs/components/docs/mobile-nav.tsx +8 -16
  15. package/docs/components/docs/sidebar-scroll-area.tsx +58 -0
  16. package/docs/components/git/repo-picker.tsx +526 -0
  17. package/docs/components/mdx/agent-instructions.tsx +17 -0
  18. package/docs/components/mdx/code-block.tsx +6 -1
  19. package/docs/components/mdx/code-group.tsx +1 -1
  20. package/docs/components/mdx/iframe.tsx +62 -0
  21. package/docs/components/mdx/index.tsx +4 -0
  22. package/docs/components/mdx/tabs.tsx +5 -5
  23. package/docs/components/mdx/video.tsx +45 -12
  24. package/docs/components/third-parties.tsx +29 -0
  25. package/docs/components/ui/badge.tsx +61 -0
  26. package/docs/components/ui/breadcrumb.tsx +61 -41
  27. package/docs/components/ui/button-group.tsx +83 -0
  28. package/docs/components/ui/button.tsx +30 -55
  29. package/docs/components/ui/command.tsx +32 -4
  30. package/docs/components/ui/copy-button.tsx +12 -19
  31. package/docs/components/ui/dialog.tsx +50 -1
  32. package/docs/components/ui/input.tsx +16 -97
  33. package/docs/components/ui/kbd.tsx +98 -0
  34. package/docs/components/ui/morph-icon.tsx +79 -0
  35. package/docs/components/ui/popover.tsx +225 -30
  36. package/docs/components/ui/search.tsx +0 -9
  37. package/docs/components/ui/sheet.tsx +30 -1
  38. package/docs/components/ui/sidebar.tsx +332 -7
  39. package/docs/components/ui/site-footer.tsx +6 -4
  40. package/docs/components/ui/skeleton.tsx +11 -0
  41. package/docs/components/ui/switch.tsx +32 -0
  42. package/docs/components/ui/tabs.tsx +138 -0
  43. package/docs/lib/api-client.ts +72 -0
  44. package/docs/lib/contextual-options.ts +9 -0
  45. package/docs/lib/dashboard-session.ts +167 -0
  46. package/docs/lib/db.ts +13 -0
  47. package/docs/lib/env.ts +4 -3
  48. package/docs/lib/etag.ts +22 -0
  49. package/docs/lib/github-install.ts +33 -0
  50. package/docs/lib/project-authz.ts +46 -0
  51. package/docs/lib/routes.ts +5 -1
  52. package/docs/lib/supabase.ts +30 -6
  53. package/docs/lib/tenancy.ts +1 -0
  54. package/docs/lib/tenant-static.ts +206 -4
  55. package/docs/lib/tenants.ts +5 -1
  56. package/docs/lib/time-ago.ts +24 -0
  57. package/docs/lib/use-tab-observer.ts +71 -0
  58. package/package.json +3 -1
  59. package/packages/@repo/common/package.json +2 -2
  60. package/packages/@repo/contracts/dist/git.d.ts +28 -0
  61. package/packages/@repo/contracts/dist/git.d.ts.map +1 -0
  62. package/packages/@repo/contracts/dist/git.js +24 -0
  63. package/packages/@repo/contracts/dist/index.d.ts +1 -1
  64. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  65. package/packages/@repo/contracts/dist/index.js +1 -1
  66. package/packages/@repo/contracts/package.json +2 -2
  67. package/packages/@repo/contracts/src/git.ts +31 -0
  68. package/packages/@repo/contracts/src/index.ts +1 -1
  69. package/packages/@repo/models/dist/docs-config.d.ts +6 -0
  70. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  71. package/packages/@repo/models/dist/docs-config.js +1 -0
  72. package/packages/@repo/models/package.json +2 -2
  73. package/packages/@repo/models/src/docs-config.ts +1 -0
  74. package/packages/@repo/prebuild/package.json +2 -2
  75. package/packages/@repo/previewing/dist/index.d.ts +3 -0
  76. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  77. package/packages/@repo/previewing/dist/index.js +48 -0
  78. package/packages/@repo/previewing/package.json +2 -2
  79. package/packages/@repo/previewing/src/index.ts +56 -0
  80. package/packages/@repo/validation/package.json +2 -2
  81. package/packages/@repo/validation/src/blodemd-docs-schema.json +1 -0
  82. package/scripts/prepare-package.mjs +14 -0
  83. package/packages/@repo/contracts/dist/api-key.d.ts +0 -30
  84. package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
  85. package/packages/@repo/contracts/dist/api-key.js +0 -20
  86. package/packages/@repo/contracts/src/api-key.ts +0 -27
@@ -0,0 +1,526 @@
1
+ "use client";
2
+
3
+ // oxlint-disable eslint-plugin-react-perf/jsx-no-new-function-as-prop -- deferred useCallback refactor
4
+ import type { GitConnection } from "@repo/contracts";
5
+ import {
6
+ CheckIcon,
7
+ ChevronDownIcon,
8
+ GithubIcon,
9
+ LockIcon,
10
+ PlusIcon,
11
+ SearchIcon,
12
+ } from "blode-icons-react";
13
+ import Image from "next/image";
14
+ import { useRouter } from "next/navigation";
15
+ import { useEffect, useState } from "react";
16
+
17
+ import { Button } from "@/components/ui/button";
18
+ import {
19
+ Card,
20
+ CardContent,
21
+ CardDescription,
22
+ CardHeader,
23
+ CardTitle,
24
+ } from "@/components/ui/card";
25
+ import {
26
+ Command,
27
+ CommandEmpty,
28
+ CommandGroup,
29
+ CommandInput,
30
+ CommandItem,
31
+ CommandList,
32
+ CommandSeparator,
33
+ } from "@/components/ui/command";
34
+ import {
35
+ Field,
36
+ FieldDescription,
37
+ FieldError,
38
+ FieldGroup,
39
+ FieldLabel,
40
+ } from "@/components/ui/field";
41
+ import { Input } from "@/components/ui/input";
42
+ import {
43
+ Popover,
44
+ PopoverContent,
45
+ PopoverTrigger,
46
+ } from "@/components/ui/popover";
47
+ import { ApiError, apiFetch } from "@/lib/api-client";
48
+ import { GITHUB_INSTALL_STATE_KEY } from "@/lib/github-install";
49
+ import { timeAgo } from "@/lib/time-ago";
50
+ import { cn } from "@/lib/utils";
51
+
52
+ export interface RepoPickerInstallation {
53
+ id: number;
54
+ accountLogin: string;
55
+ accountType: string;
56
+ }
57
+
58
+ export interface RepoPickerProps {
59
+ accessToken: string;
60
+ installations: RepoPickerInstallation[];
61
+ onAddAccount: () => void;
62
+ projectId: string;
63
+ projectSlug: string;
64
+ addAccountPending?: boolean;
65
+ initialInstallationId?: number | null;
66
+ onConnected?: (connection: GitConnection) => void;
67
+ }
68
+
69
+ interface RepoSummary {
70
+ defaultBranch: string;
71
+ fullName: string;
72
+ private: boolean;
73
+ pushedAt: string | null;
74
+ }
75
+
76
+ const GithubAvatar = ({
77
+ className,
78
+ login,
79
+ size = 24,
80
+ }: {
81
+ className?: string;
82
+ login: string;
83
+ size?: number;
84
+ }) => (
85
+ <Image
86
+ alt=""
87
+ className={cn(
88
+ "shrink-0 rounded-full bg-muted ring-1 ring-black/5",
89
+ className
90
+ )}
91
+ height={size}
92
+ src={`https://github.com/${login}.png?size=${size * 2}`}
93
+ unoptimized
94
+ width={size}
95
+ />
96
+ );
97
+
98
+ const ConfigureRepoCard = ({
99
+ branch,
100
+ docsPath,
101
+ formError,
102
+ onBranchChange,
103
+ onChangeRepo,
104
+ onConnect,
105
+ onDocsPathChange,
106
+ selected,
107
+ submitting,
108
+ }: {
109
+ branch: string;
110
+ docsPath: string;
111
+ formError: string | null;
112
+ onBranchChange: (value: string) => void;
113
+ onChangeRepo: () => void;
114
+ onConnect: () => void;
115
+ onDocsPathChange: (value: string) => void;
116
+ selected: RepoSummary;
117
+ submitting: boolean;
118
+ }) => (
119
+ <Card>
120
+ <CardHeader>
121
+ <CardTitle>Configure repository</CardTitle>
122
+ <CardDescription>
123
+ Set the branch and docs folder for this project.
124
+ </CardDescription>
125
+ </CardHeader>
126
+ <CardContent className="flex flex-col gap-4">
127
+ <div className="flex items-center gap-4 rounded-md border border-border bg-card px-4 py-3">
128
+ <GithubAvatar
129
+ className="size-6"
130
+ login={selected.fullName.split("/")[0] ?? ""}
131
+ />
132
+ <div className="flex min-w-0 flex-1 items-center gap-1.5">
133
+ <span className="truncate text-sm font-medium">
134
+ {selected.fullName}
135
+ </span>
136
+ {selected.private && (
137
+ <LockIcon className="size-3.5 shrink-0 text-muted-foreground" />
138
+ )}
139
+ </div>
140
+ <Button onClick={onChangeRepo} size="sm" type="button" variant="ghost">
141
+ Change
142
+ </Button>
143
+ </div>
144
+ <FieldGroup>
145
+ <Field>
146
+ <FieldLabel htmlFor="branch">Branch</FieldLabel>
147
+ <Input
148
+ id="branch"
149
+ onChange={(event) => onBranchChange(event.target.value)}
150
+ value={branch}
151
+ />
152
+ </Field>
153
+ <Field>
154
+ <FieldLabel htmlFor="docs-path">Docs path</FieldLabel>
155
+ <Input
156
+ id="docs-path"
157
+ onChange={(event) => onDocsPathChange(event.target.value)}
158
+ value={docsPath}
159
+ />
160
+ <FieldDescription>
161
+ Folder inside the repo with your <code>docs.json</code>.
162
+ </FieldDescription>
163
+ </Field>
164
+ </FieldGroup>
165
+ {formError && <FieldError>{formError}</FieldError>}
166
+ <div>
167
+ <Button disabled={submitting} onClick={onConnect} type="button">
168
+ {submitting ? "Connecting..." : "Connect repository"}
169
+ </Button>
170
+ </div>
171
+ </CardContent>
172
+ </Card>
173
+ );
174
+
175
+ const RepoRow = ({
176
+ onImport,
177
+ repo,
178
+ }: {
179
+ onImport: () => void;
180
+ repo: RepoSummary;
181
+ }) => {
182
+ const name = repo.fullName.split("/")[1] ?? repo.fullName;
183
+ const ago = timeAgo(repo.pushedAt);
184
+ return (
185
+ <div className="flex items-center gap-4 bg-card px-4 py-3">
186
+ <GithubAvatar
187
+ className="size-6"
188
+ login={repo.fullName.split("/")[0] ?? ""}
189
+ />
190
+ <div className="flex min-w-0 flex-1 items-center gap-1.5">
191
+ <span className="truncate text-sm font-medium" title={repo.fullName}>
192
+ {name}
193
+ </span>
194
+ {repo.private && (
195
+ <LockIcon className="size-3.5 shrink-0 text-muted-foreground" />
196
+ )}
197
+ {ago && (
198
+ <>
199
+ <span
200
+ aria-hidden="true"
201
+ className="hidden text-muted-foreground sm:inline"
202
+ >
203
+ ·
204
+ </span>
205
+ <span className="hidden text-xs text-muted-foreground sm:inline">
206
+ {ago}
207
+ </span>
208
+ </>
209
+ )}
210
+ </div>
211
+ <Button
212
+ aria-label={`Import ${repo.fullName}`}
213
+ onClick={onImport}
214
+ size="sm"
215
+ type="button"
216
+ >
217
+ Import
218
+ </Button>
219
+ </div>
220
+ );
221
+ };
222
+
223
+ const AccountPicker = ({
224
+ addAccountPending,
225
+ installations,
226
+ onAddAccount,
227
+ onSelect,
228
+ selectedId,
229
+ }: {
230
+ addAccountPending?: boolean;
231
+ installations: RepoPickerInstallation[];
232
+ onAddAccount: () => void;
233
+ onSelect: (id: number) => void;
234
+ selectedId: number | null;
235
+ }) => {
236
+ const [open, setOpen] = useState(false);
237
+ const selected =
238
+ installations.find((installation) => installation.id === selectedId) ??
239
+ null;
240
+
241
+ return (
242
+ <Popover onOpenChange={setOpen} open={open}>
243
+ <PopoverTrigger
244
+ aria-label="Select GitHub account"
245
+ className="flex h-9 w-full items-center gap-2 rounded-md border border-border bg-background px-3 text-sm outline-none transition hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
246
+ type="button"
247
+ >
248
+ {selected ? (
249
+ <GithubAvatar className="size-5" login={selected.accountLogin} />
250
+ ) : (
251
+ <GithubIcon className="size-4 shrink-0 text-muted-foreground" />
252
+ )}
253
+ <span className="truncate font-medium">
254
+ {selected?.accountLogin ?? "Select account"}
255
+ </span>
256
+ <ChevronDownIcon className="ml-auto size-4 shrink-0 text-muted-foreground" />
257
+ </PopoverTrigger>
258
+ <PopoverContent
259
+ align="start"
260
+ className="w-[min(20rem,var(--available-width,20rem))] p-0"
261
+ >
262
+ <Command>
263
+ <CommandInput placeholder="Search accounts…" />
264
+ <CommandList>
265
+ <CommandEmpty>No accounts match.</CommandEmpty>
266
+ {installations.length > 0 && (
267
+ <CommandGroup>
268
+ {installations.map((installation) => (
269
+ <CommandItem
270
+ key={installation.id}
271
+ onSelect={() => {
272
+ onSelect(installation.id);
273
+ setOpen(false);
274
+ }}
275
+ value={installation.accountLogin}
276
+ >
277
+ <GithubAvatar
278
+ className="size-5"
279
+ login={installation.accountLogin}
280
+ />
281
+ <span className="truncate">
282
+ {installation.accountLogin}
283
+ </span>
284
+ <CheckIcon
285
+ className={cn(
286
+ "ml-auto size-4",
287
+ installation.id === selectedId
288
+ ? "opacity-100"
289
+ : "opacity-0"
290
+ )}
291
+ />
292
+ </CommandItem>
293
+ ))}
294
+ </CommandGroup>
295
+ )}
296
+ {installations.length > 0 && <CommandSeparator />}
297
+ <CommandGroup forceMount>
298
+ <CommandItem
299
+ disabled={addAccountPending}
300
+ forceMount
301
+ onSelect={() => {
302
+ setOpen(false);
303
+ onAddAccount();
304
+ }}
305
+ value="Add GitHub Account"
306
+ >
307
+ <PlusIcon className="size-4" />
308
+ <span>
309
+ {addAccountPending ? "Redirecting…" : "Add GitHub Account"}
310
+ </span>
311
+ </CommandItem>
312
+ </CommandGroup>
313
+ </CommandList>
314
+ </Command>
315
+ </PopoverContent>
316
+ </Popover>
317
+ );
318
+ };
319
+
320
+ export const RepoPicker = ({
321
+ accessToken,
322
+ addAccountPending,
323
+ initialInstallationId = null,
324
+ installations,
325
+ onAddAccount,
326
+ onConnected,
327
+ projectId,
328
+ projectSlug,
329
+ }: RepoPickerProps) => {
330
+ const router = useRouter();
331
+ const preselected =
332
+ initialInstallationId !== null &&
333
+ installations.some((i) => i.id === initialInstallationId)
334
+ ? initialInstallationId
335
+ : (installations[0]?.id ?? null);
336
+ const [selectedInstallationId, setSelectedInstallationId] = useState<
337
+ number | null
338
+ >(preselected);
339
+ const [repos, setRepos] = useState<RepoSummary[] | null>(null);
340
+ const [formError, setError] = useState<string | null>(null);
341
+ const [selected, setSelected] = useState<RepoSummary | null>(null);
342
+ const [branch, setBranch] = useState("main");
343
+ const [docsPath, setDocsPath] = useState("docs");
344
+ const [submitting, setSubmitting] = useState(false);
345
+ const [search, setSearch] = useState("");
346
+
347
+ useEffect(() => {
348
+ if (!installations.some((i) => i.id === selectedInstallationId)) {
349
+ setSelectedInstallationId(installations[0]?.id ?? null);
350
+ }
351
+ }, [installations, selectedInstallationId]);
352
+
353
+ useEffect(() => {
354
+ if (selectedInstallationId === null) {
355
+ setRepos(null);
356
+ return;
357
+ }
358
+ let cancelled = false;
359
+ setRepos(null);
360
+ setSelected(null);
361
+ setError(null);
362
+ const run = async () => {
363
+ try {
364
+ const reposResult = await apiFetch<{ repos: RepoSummary[] }>(
365
+ `/git/installations/${selectedInstallationId}/repos`,
366
+ { accessToken }
367
+ );
368
+ if (!cancelled) {
369
+ setRepos(reposResult.repos);
370
+ }
371
+ } catch (error) {
372
+ const message =
373
+ error instanceof ApiError
374
+ ? error.message
375
+ : "Failed to load repositories.";
376
+ if (!cancelled) {
377
+ setError(message);
378
+ }
379
+ }
380
+ };
381
+ run();
382
+ return () => {
383
+ cancelled = true;
384
+ };
385
+ }, [accessToken, selectedInstallationId]);
386
+
387
+ const handlePick = (repo: RepoSummary) => {
388
+ setSelected(repo);
389
+ setBranch(repo.defaultBranch || "main");
390
+ setError(null);
391
+ };
392
+
393
+ const handleConnect = async () => {
394
+ if (!(selected && selectedInstallationId !== null)) {
395
+ return;
396
+ }
397
+ setSubmitting(true);
398
+ setError(null);
399
+ try {
400
+ const connection = await apiFetch<GitConnection>(
401
+ `/projects/${projectId}/git`,
402
+ {
403
+ accessToken,
404
+ body: {
405
+ branch: branch.trim() || "main",
406
+ docsPath: docsPath.trim() || "docs",
407
+ installationId: selectedInstallationId,
408
+ repository: selected.fullName,
409
+ },
410
+ method: "POST",
411
+ }
412
+ );
413
+ sessionStorage.removeItem(GITHUB_INSTALL_STATE_KEY);
414
+ if (onConnected) {
415
+ onConnected(connection);
416
+ } else {
417
+ router.push(`/app/${projectSlug}/git`);
418
+ router.refresh();
419
+ }
420
+ } catch (error) {
421
+ const message =
422
+ error instanceof ApiError
423
+ ? error.message
424
+ : "Could not save connection.";
425
+ setError(message);
426
+ setSubmitting(false);
427
+ }
428
+ };
429
+
430
+ const filtered =
431
+ repos?.filter((repo) =>
432
+ repo.fullName.toLowerCase().includes(search.trim().toLowerCase())
433
+ ) ?? [];
434
+
435
+ if (selected) {
436
+ return (
437
+ <ConfigureRepoCard
438
+ branch={branch}
439
+ docsPath={docsPath}
440
+ formError={formError}
441
+ onBranchChange={setBranch}
442
+ onChangeRepo={() => setSelected(null)}
443
+ onConnect={handleConnect}
444
+ onDocsPathChange={setDocsPath}
445
+ selected={selected}
446
+ submitting={submitting}
447
+ />
448
+ );
449
+ }
450
+
451
+ return (
452
+ <Card>
453
+ <CardHeader>
454
+ <CardTitle>Import Git repository</CardTitle>
455
+ <CardDescription>
456
+ Choose the repo with your docs. We&apos;ll deploy on every push to the
457
+ selected branch.
458
+ </CardDescription>
459
+ </CardHeader>
460
+ <CardContent className="flex flex-col gap-4">
461
+ <div className="flex gap-2">
462
+ <div className="flex-1">
463
+ <AccountPicker
464
+ addAccountPending={addAccountPending}
465
+ installations={installations}
466
+ onAddAccount={onAddAccount}
467
+ onSelect={setSelectedInstallationId}
468
+ selectedId={selectedInstallationId}
469
+ />
470
+ </div>
471
+ <div className="relative flex-1">
472
+ <SearchIcon
473
+ aria-hidden="true"
474
+ className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 size-4 text-muted-foreground"
475
+ />
476
+ <Input
477
+ aria-label="Search repositories"
478
+ className="pl-9"
479
+ onChange={(event) => setSearch(event.target.value)}
480
+ placeholder="Search…"
481
+ type="search"
482
+ value={search}
483
+ />
484
+ </div>
485
+ </div>
486
+
487
+ {formError && <FieldError>{formError}</FieldError>}
488
+
489
+ {selectedInstallationId === null && (
490
+ <p className="text-sm text-muted-foreground">
491
+ Select a GitHub account to see repositories.
492
+ </p>
493
+ )}
494
+
495
+ {selectedInstallationId !== null && repos === null && !formError && (
496
+ <p className="text-sm text-muted-foreground">Loading…</p>
497
+ )}
498
+
499
+ {repos?.length === 0 && (
500
+ <p className="text-sm text-muted-foreground">
501
+ The Blode.md app isn&apos;t installed on any repos in this account
502
+ yet. Add at least one in GitHub and refresh.
503
+ </p>
504
+ )}
505
+
506
+ {repos && repos.length > 0 && filtered.length === 0 && (
507
+ <p className="text-sm text-muted-foreground">
508
+ No repositories match &quot;{search}&quot;.
509
+ </p>
510
+ )}
511
+
512
+ {filtered.length > 0 && (
513
+ <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
514
+ {filtered.map((repo) => (
515
+ <RepoRow
516
+ key={repo.fullName}
517
+ onImport={() => handlePick(repo)}
518
+ repo={repo}
519
+ />
520
+ ))}
521
+ </div>
522
+ )}
523
+ </CardContent>
524
+ </Card>
525
+ );
526
+ };
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ interface AgentInstructionsProps {
4
+ children: ReactNode;
5
+ }
6
+
7
+ /**
8
+ * Content inside <AgentInstructions> is hidden from human readers in the
9
+ * rendered HTML but preserved in the raw MDX source. When pages are exported
10
+ * as Markdown (via /{page}.md or llms-full.txt), the content is included so
11
+ * AI agents receive it as context.
12
+ */
13
+ export const AgentInstructions = ({ children }: AgentInstructionsProps) => (
14
+ <div hidden data-agent-instructions="">
15
+ {children}
16
+ </div>
17
+ );
@@ -45,13 +45,18 @@ export const CodeBlock = ({
45
45
  <figure data-rehype-pretty-code-figure="">
46
46
  <pre
47
47
  className={cn(
48
- "no-scrollbar min-w-0 overflow-x-auto overflow-y-auto overscroll-y-auto overscroll-x-contain px-4 py-3.5 outline-none has-[[data-slot=tabs]]:p-0 has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0",
48
+ "no-scrollbar min-w-0 overflow-x-auto overflow-y-auto overscroll-y-auto overscroll-x-contain pl-4 pr-14 py-3.5 outline-none has-[[data-slot=tabs]]:p-0 has-[[data-highlighted-line]]:pl-0 has-[[data-line-numbers]]:pl-0",
49
49
  className
50
50
  )}
51
51
  style={preStyle}
52
52
  tabIndex={tabIndex ?? 0}
53
53
  {...props}
54
54
  >
55
+ <div
56
+ aria-hidden="true"
57
+ className="pointer-events-none absolute top-0 right-0 bottom-0 z-[9] w-16 bg-gradient-to-r from-transparent to-code print:hidden"
58
+ data-slot="fade-overlay"
59
+ />
55
60
  <Button
56
61
  className="absolute top-3 right-2 z-10 size-7 bg-code hover:opacity-100 focus-visible:opacity-100"
57
62
  data-copied={copied}
@@ -157,7 +157,7 @@ export const CodeGroup = ({ children }: CodeGroupProps) => {
157
157
  }
158
158
 
159
159
  return (
160
- <div className="my-4 overflow-hidden rounded-xl border border-border bg-code">
160
+ <div className="my-4 overflow-hidden rounded-xl bg-code">
161
161
  <div
162
162
  aria-orientation="horizontal"
163
163
  className="flex gap-1 border-b border-border bg-muted/50 px-2 pt-2"
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ interface IframeProps {
8
+ src: string;
9
+ height: number;
10
+ title?: string;
11
+ allowResize?: boolean;
12
+ className?: string;
13
+ }
14
+
15
+ export const Iframe = ({
16
+ src,
17
+ height,
18
+ title = "Embedded content",
19
+ allowResize = false,
20
+ className,
21
+ }: IframeProps) => {
22
+ const iframeRef = useRef<HTMLIFrameElement>(null);
23
+ const [dynamicHeight, setDynamicHeight] = useState<number | null>(null);
24
+
25
+ useEffect(() => {
26
+ if (!allowResize) {
27
+ return;
28
+ }
29
+
30
+ const handleMessage = (event: MessageEvent) => {
31
+ if (
32
+ event.data?.type === "resize" &&
33
+ typeof event.data?.height === "number"
34
+ ) {
35
+ setDynamicHeight(event.data.height);
36
+ }
37
+ };
38
+
39
+ window.addEventListener("message", handleMessage);
40
+ return () => window.removeEventListener("message", handleMessage);
41
+ }, [allowResize]);
42
+
43
+ const resolvedHeight = dynamicHeight ?? height;
44
+
45
+ return (
46
+ <div
47
+ className={cn(
48
+ "relative overflow-hidden rounded-xl border border-border bg-card",
49
+ className
50
+ )}
51
+ style={{ height: `${resolvedHeight}px` }}
52
+ >
53
+ <iframe
54
+ ref={iframeRef}
55
+ className="h-full w-full border-0"
56
+ sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
57
+ src={src}
58
+ title={title}
59
+ />
60
+ </div>
61
+ );
62
+ };
@@ -3,6 +3,7 @@ import Link from "next/link";
3
3
  import type { ComponentProps } from "react";
4
4
 
5
5
  import { Accordion, AccordionGroup } from "./accordion";
6
+ import { AgentInstructions } from "./agent-instructions";
6
7
  import { Badge } from "./badge";
7
8
  import { Callout, Check, Danger, Info, Note, Tip, Warning } from "./callout";
8
9
  import { Card } from "./card";
@@ -13,6 +14,7 @@ import { Column, Columns } from "./columns";
13
14
  import { Expandable } from "./expandable";
14
15
  import { Frame } from "./frame";
15
16
  import { Icon } from "./icon";
17
+ import { Iframe } from "./iframe";
16
18
  import { Installer } from "./installer";
17
19
  import { Panel } from "./panel";
18
20
  import { ParamField } from "./param-field";
@@ -66,6 +68,7 @@ const MdxLink = ({
66
68
  export const mdxComponents: MDXComponents = {
67
69
  Accordion,
68
70
  AccordionGroup,
71
+ AgentInstructions,
69
72
  Badge,
70
73
  Callout,
71
74
  Card,
@@ -78,6 +81,7 @@ export const mdxComponents: MDXComponents = {
78
81
  Expandable,
79
82
  Frame,
80
83
  Icon,
84
+ Iframe,
81
85
  Info,
82
86
  Installer,
83
87
  Note,
@@ -141,13 +141,13 @@ export const Tabs = ({
141
141
  return (
142
142
  <div
143
143
  className={cn(
144
- "overflow-hidden rounded-xl border border-border bg-surface",
144
+ "overflow-hidden rounded-xl bg-code",
145
145
  borderBottom && "border-b-2"
146
146
  )}
147
147
  >
148
148
  <div
149
149
  aria-orientation="horizontal"
150
- className="flex gap-2 bg-muted p-2"
150
+ className="flex gap-1 border-b border-border bg-muted/50 px-2 pt-2"
151
151
  role="tablist"
152
152
  >
153
153
  {items.map((item, index) => {
@@ -158,10 +158,10 @@ export const Tabs = ({
158
158
  aria-controls={getPanelId(index)}
159
159
  aria-selected={isSelected}
160
160
  className={cn(
161
- "inline-flex cursor-pointer items-center gap-1.5 rounded-full border-none bg-transparent px-3 py-2 text-sm transition-colors",
161
+ "inline-flex cursor-pointer items-center gap-1.5 rounded-t-md border-b-2 bg-transparent px-3 py-2 text-sm transition-colors",
162
162
  isSelected
163
- ? "bg-primary text-primary-foreground"
164
- : "text-muted-foreground hover:text-foreground"
163
+ ? "border-primary text-foreground"
164
+ : "border-transparent text-muted-foreground hover:text-foreground"
165
165
  )}
166
166
  data-index={index}
167
167
  id={getTabId(index)}