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.
- package/README.md +11 -47
- package/dev-server/app/layout.tsx +1 -1
- package/dev-server/next.config.js +19 -9
- package/dev-server/tsconfig.json +0 -3
- package/dist/cli.mjs +732 -123
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +15 -1
- package/docs/components/api/api-playground.tsx +2 -2
- package/docs/components/docs/copy-page-menu.tsx +55 -27
- package/docs/components/docs/doc-header.tsx +1 -1
- package/docs/components/docs/doc-shell.tsx +89 -88
- package/docs/components/docs/doc-sidebar.tsx +6 -3
- package/docs/components/docs/doc-toc.tsx +1 -1
- package/docs/components/docs/mobile-nav.tsx +8 -16
- package/docs/components/docs/sidebar-scroll-area.tsx +58 -0
- package/docs/components/git/repo-picker.tsx +526 -0
- package/docs/components/mdx/agent-instructions.tsx +17 -0
- package/docs/components/mdx/code-block.tsx +6 -1
- package/docs/components/mdx/code-group.tsx +1 -1
- package/docs/components/mdx/iframe.tsx +62 -0
- package/docs/components/mdx/index.tsx +4 -0
- package/docs/components/mdx/tabs.tsx +5 -5
- package/docs/components/mdx/video.tsx +45 -12
- package/docs/components/third-parties.tsx +29 -0
- package/docs/components/ui/badge.tsx +61 -0
- package/docs/components/ui/breadcrumb.tsx +61 -41
- package/docs/components/ui/button-group.tsx +83 -0
- package/docs/components/ui/button.tsx +30 -55
- package/docs/components/ui/command.tsx +32 -4
- package/docs/components/ui/copy-button.tsx +12 -19
- package/docs/components/ui/dialog.tsx +50 -1
- package/docs/components/ui/input.tsx +16 -97
- package/docs/components/ui/kbd.tsx +98 -0
- package/docs/components/ui/morph-icon.tsx +79 -0
- package/docs/components/ui/popover.tsx +225 -30
- package/docs/components/ui/search.tsx +0 -9
- package/docs/components/ui/sheet.tsx +30 -1
- package/docs/components/ui/sidebar.tsx +332 -7
- package/docs/components/ui/site-footer.tsx +6 -4
- package/docs/components/ui/skeleton.tsx +11 -0
- package/docs/components/ui/switch.tsx +32 -0
- package/docs/components/ui/tabs.tsx +138 -0
- package/docs/lib/api-client.ts +72 -0
- package/docs/lib/contextual-options.ts +9 -0
- package/docs/lib/dashboard-session.ts +167 -0
- package/docs/lib/db.ts +13 -0
- package/docs/lib/env.ts +4 -3
- package/docs/lib/etag.ts +22 -0
- package/docs/lib/github-install.ts +33 -0
- package/docs/lib/project-authz.ts +46 -0
- package/docs/lib/routes.ts +5 -1
- package/docs/lib/supabase.ts +30 -6
- package/docs/lib/tenancy.ts +1 -0
- package/docs/lib/tenant-static.ts +206 -4
- package/docs/lib/tenants.ts +5 -1
- package/docs/lib/time-ago.ts +24 -0
- package/docs/lib/use-tab-observer.ts +71 -0
- package/package.json +3 -1
- package/packages/@repo/common/package.json +2 -2
- package/packages/@repo/contracts/dist/git.d.ts +28 -0
- package/packages/@repo/contracts/dist/git.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/git.js +24 -0
- package/packages/@repo/contracts/dist/index.d.ts +1 -1
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/index.js +1 -1
- package/packages/@repo/contracts/package.json +2 -2
- package/packages/@repo/contracts/src/git.ts +31 -0
- package/packages/@repo/contracts/src/index.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +6 -0
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +1 -0
- package/packages/@repo/models/package.json +2 -2
- package/packages/@repo/models/src/docs-config.ts +1 -0
- package/packages/@repo/prebuild/package.json +2 -2
- package/packages/@repo/previewing/dist/index.d.ts +3 -0
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +48 -0
- package/packages/@repo/previewing/package.json +2 -2
- package/packages/@repo/previewing/src/index.ts +56 -0
- package/packages/@repo/validation/package.json +2 -2
- package/packages/@repo/validation/src/blodemd-docs-schema.json +1 -0
- package/scripts/prepare-package.mjs +14 -0
- package/packages/@repo/contracts/dist/api-key.d.ts +0 -30
- package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
- package/packages/@repo/contracts/dist/api-key.js +0 -20
- 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'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'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 "{search}".
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
? "
|
|
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)}
|