@wrikka/create-docs 0.1.0 → 0.2.1

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/docs/config.md CHANGED
@@ -2,7 +2,7 @@
2
2
  title: Configuration
3
3
  description: Site, features, i18n, versioning, and navigation config.
4
4
  order: 4
5
- category: Guide
5
+ category: Reference
6
6
  tags: [config]
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  title: Data Sources
3
3
  description: Filesystem, remote, composite, and custom data sources.
4
4
  order: 6
5
- category: Guide
5
+ category: Reference
6
6
  tags: [data]
7
7
  ---
8
8
 
package/docs/deploy.md CHANGED
@@ -2,7 +2,7 @@
2
2
  title: Deployment
3
3
  description: Build, output, and deploy the docs site.
4
4
  order: 7
5
- category: Guide
5
+ category: Reference
6
6
  tags: [deploy]
7
7
  ---
8
8
 
package/docs/features.md CHANGED
@@ -2,7 +2,7 @@
2
2
  title: Features
3
3
  description: Search, SEO, API references, analytics, PWA, and more.
4
4
  order: 5
5
- category: Guide
5
+ category: Features
6
6
  tags: [features]
7
7
  ---
8
8
 
package/docs/markdown.md CHANGED
@@ -2,7 +2,7 @@
2
2
  title: Markdown Syntax
3
3
  description: Callouts, code groups, steps, playgrounds, UI components, and more.
4
4
  order: 3
5
- category: Guide
5
+ category: Features
6
6
  tags: [markdown]
7
7
  ---
8
8
 
package/docs/theming.md CHANGED
@@ -2,7 +2,7 @@
2
2
  title: Theming
3
3
  description: Customize colors, fonts, and UnoCSS shortcuts.
4
4
  order: 8
5
- category: Guide
5
+ category: Features
6
6
  tags: [theme]
7
7
  ---
8
8
 
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: AI Translation
3
+ description: Translate docs with AI from CI — plan, review, and ship locales.
4
+ order: 9
5
+ category: Features
6
+ tags: [translate, i18n, ai]
7
+ ---
8
+
9
+ create-docs ships a translation pipeline that runs in CI, not in the browser.
10
+ The runtime shows the UX — locale cards, per-doc status, and copyable
11
+ commands — while the actual translation is produced by an AI provider in a
12
+ workflow.
13
+
14
+ ## Enable
15
+
16
+ ```ts
17
+ mountDocsApp({
18
+ features: { translate: true },
19
+ translate: {
20
+ provider: "ai", // provider hint shown in the UI
21
+ workflow: "translate.yml",
22
+ locales: [
23
+ { id: "th", label: "ไทย" },
24
+ { id: "ja", label: "日本語" },
25
+ ],
26
+ },
27
+ });
28
+ ```
29
+
30
+ This adds a **Translate** item to the top nav and sidebar, a `/translate`
31
+ page, and a per-page Translate action on doc pages.
32
+
33
+ ## How it works
34
+
35
+ 1. `bunx create-docs-translate --docs docs --locales th,ja` scans your docs
36
+ and writes `translation-plan.json` with one task per
37
+ `(document, locale)` pair.
38
+ 2. A CI workflow (e.g. `.github/workflows/translate.yml`) runs the planner
39
+ and — once a `TRANSLATE_PROVIDER` and API key are configured — feeds the
40
+ plan to the provider.
41
+ 3. Translated markdown lands under `docs/<locale>/…`, gets committed, and the
42
+ locale switcher picks it up automatically.
43
+
44
+ The `/translate` page derives per-doc status from doc ids and tags: a doc
45
+ counts as translated for a locale when its id contains `.{locale}` or
46
+ `--{locale}`, or when it carries a `lang:{locale}` / `locale:{locale}` tag.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@wrikka/create-docs",
3
3
  "description": "Vite plugin for documentation sites built on Functional Clean Architecture with SolidJS support",
4
- "version": "0.1.0",
4
+ "version": "0.2.1",
5
5
  "type": "module",
6
6
  "bin": {
7
- "create-docs": "./bin/cli.js",
8
- "create-docs-pull": "./scripts/github-pull.ts"
7
+ "create-docs": "bin/cli.js",
8
+ "create-docs-pull": "scripts/github-pull.ts",
9
+ "create-docs-translate": "scripts/translate.ts"
9
10
  },
10
11
  "files": [
11
12
  "bin",
@@ -57,7 +58,7 @@
57
58
  "license": "MIT",
58
59
  "repository": {
59
60
  "type": "git",
60
- "url": "https://github.com/wrikka/create-docs"
61
+ "url": "git+https://github.com/wrikka/create-docs.git"
61
62
  },
62
63
  "bugs": {
63
64
  "url": "https://github.com/wrikka/create-docs/issues"
@@ -86,7 +87,8 @@
86
87
  "deps:analyze": "bunx depcheck",
87
88
  "clean": "bunx rimraf dist node_modules",
88
89
  "security": "bunx audit",
89
- "pull": "bun scripts/github-pull.ts"
90
+ "pull": "bun scripts/github-pull.ts",
91
+ "translate": "bun scripts/translate.ts"
90
92
  },
91
93
  "dependencies": {
92
94
  "@codemirror/basic-setup": "^0.20.0",
@@ -99,9 +99,9 @@ for (const collection of collections) {
99
99
  }
100
100
  }
101
101
 
102
- const outDir = process.argv.includes("--dev")
103
- ? join(root, "dev", "dist")
104
- : join(root, "dist");
102
+ const outDir = process.argv.includes("--lib")
103
+ ? join(root, "dist")
104
+ : join(root, "dev", "dist");
105
105
  mkdirSync(outDir, { recursive: true });
106
106
  writeFileSync(
107
107
  join(outDir, "search-index.json"),
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * create-docs translate — CI-friendly AI translation planner.
4
+ *
5
+ * Scans a docs directory, computes which (document, locale) pairs are missing
6
+ * translations, and writes a `translation-plan.json` that a CI workflow (or an
7
+ * AI provider integration) can consume.
8
+ *
9
+ * Usage:
10
+ * bun scripts/translate.ts --docs docs --locales th,ja [--out translation-plan.json]
11
+ *
12
+ * The script is intentionally a planner: it does not call an AI provider yet.
13
+ * To wire a real provider, set TRANSLATE_PROVIDER plus the provider's API key
14
+ * (e.g. OPENAI_API_KEY) and pipe the emitted plan into your translation step.
15
+ */
16
+
17
+ import {
18
+ mkdirSync,
19
+ readdirSync,
20
+ statSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import * as path from "node:path";
24
+
25
+ interface CliArgs {
26
+ docs: string;
27
+ locales: string[];
28
+ out: string;
29
+ }
30
+
31
+ function parseArgs(argv: string[]): CliArgs {
32
+ const args: CliArgs = {
33
+ docs: "docs",
34
+ locales: [],
35
+ out: "translation-plan.json",
36
+ };
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const a = argv[i];
39
+ if (a === "--docs") args.docs = argv[++i] ?? args.docs;
40
+ else if (a === "--locales")
41
+ args.locales = (argv[++i] ?? "")
42
+ .split(",")
43
+ .map((s) => s.trim())
44
+ .filter(Boolean);
45
+ else if (a === "--out") args.out = argv[++i] ?? args.out;
46
+ }
47
+ if (args.locales.length === 0) {
48
+ console.error("Missing --locales (comma separated, e.g. --locales th,ja)");
49
+ process.exit(1);
50
+ }
51
+ return args;
52
+ }
53
+
54
+ function walk(dir: string, prefix = ""): string[] {
55
+ const out: string[] = [];
56
+ for (const name of readdirSync(dir)) {
57
+ if (name.startsWith(".") || name === "node_modules") continue;
58
+ const abs = path.join(dir, name);
59
+ const rel = prefix ? `${prefix}/${name}` : name;
60
+ if (statSync(abs).isDirectory()) out.push(...walk(abs, rel));
61
+ else if (/\.(md|mdx)$/i.test(name)) out.push(rel);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function main() {
67
+ const args = parseArgs(process.argv.slice(2));
68
+ const docsDir = path.resolve(args.docs);
69
+ if (!statSync(docsDir, { throwIfNoEntry: false })?.isDirectory()) {
70
+ console.error(`Docs directory not found: ${docsDir}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ const files = walk(docsDir);
75
+ const provider = process.env.TRANSLATE_PROVIDER ?? "ai";
76
+
77
+ const plan = {
78
+ generatedAt: new Date().toISOString(),
79
+ provider,
80
+ sourceLocale: process.env.SOURCE_LOCALE ?? "en",
81
+ targetLocales: args.locales,
82
+ docsDir: args.docs,
83
+ tasks: [] as Array<{
84
+ source: string;
85
+ target: string;
86
+ locale: string;
87
+ sourcePath: string;
88
+ targetPath: string;
89
+ status: "pending" | "translated";
90
+ }>,
91
+ };
92
+
93
+ let pending = 0;
94
+ let done = 0;
95
+ for (const file of files) {
96
+ for (const locale of args.locales) {
97
+ const targetPath = path.join(docsDir, locale, file);
98
+ const exists =
99
+ statSync(targetPath, { throwIfNoEntry: false })?.isFile() ?? false;
100
+ plan.tasks.push({
101
+ source: file,
102
+ target: `${locale}/${file}`,
103
+ locale,
104
+ sourcePath: path.join(docsDir, file),
105
+ targetPath,
106
+ status: exists ? "translated" : "pending",
107
+ });
108
+ if (exists) done++;
109
+ else pending++;
110
+ }
111
+ }
112
+
113
+ mkdirSync(path.dirname(path.resolve(args.out)), { recursive: true });
114
+ writeFileSync(args.out, JSON.stringify(plan, null, 2), "utf8");
115
+
116
+ console.log(
117
+ `[create-docs translate] ${files.length} docs x ${args.locales.length} locales -> ${done} translated, ${pending} pending`,
118
+ );
119
+ console.log(`Plan written to ${path.resolve(args.out)}`);
120
+ if (pending > 0 && !process.env.TRANSLATE_PROVIDER) {
121
+ console.log(
122
+ "No TRANSLATE_PROVIDER configured — plan emitted for review; set a provider + API key to run real translations.",
123
+ );
124
+ }
125
+ }
126
+
127
+ main();
@@ -12,6 +12,7 @@ import {
12
12
  mkdirSync,
13
13
  readdirSync,
14
14
  readFileSync,
15
+ rmSync,
15
16
  statSync,
16
17
  writeFileSync,
17
18
  } from "node:fs";
@@ -179,12 +180,31 @@ interface LocalFile {
179
180
  absolute: string;
180
181
  }
181
182
 
183
+ const EXCLUDED_DIRS = new Set([
184
+ "node_modules",
185
+ "dist",
186
+ "build",
187
+ "coverage",
188
+ ".git",
189
+ ".vitepress",
190
+ ".next",
191
+ ".turbo",
192
+ ".cache",
193
+ ]);
194
+
195
+ function isExcludedPath(relative: string): boolean {
196
+ return relative
197
+ .split("/")
198
+ .some((seg) => EXCLUDED_DIRS.has(seg) || seg.startsWith("."));
199
+ }
200
+
182
201
  function scanLocalDir(dir: string, prefix = ""): LocalFile[] {
183
202
  const items: LocalFile[] = [];
184
203
  for (const name of readdirSync(dir)) {
185
204
  const absolute = path.join(dir, name);
186
205
  const relative = prefix ? `${prefix}/${name}` : name;
187
206
  if (statSync(absolute).isDirectory()) {
207
+ if (EXCLUDED_DIRS.has(name) || name.startsWith(".")) continue;
188
208
  items.push(...scanLocalDir(absolute, relative));
189
209
  } else if (/\.(md|mdx)$/i.test(name) || /^_dir\.ya?ml$/i.test(name)) {
190
210
  items.push({ relative, absolute });
@@ -252,6 +272,8 @@ export async function pullFromGitHub(
252
272
  const collectionId = source.id;
253
273
 
254
274
  const collectionOut = path.join(docsOut, collectionId);
275
+ // Clean previous output so deleted/excluded upstream files don't linger.
276
+ rmSync(collectionOut, { recursive: true, force: true });
255
277
  mkdirSync(collectionOut, { recursive: true });
256
278
 
257
279
  // Local mode: copy files from a local directory.
@@ -277,6 +299,7 @@ export async function pullFromGitHub(
277
299
  if (item.type !== "blob") continue;
278
300
  const relative = relativeWithinDocs(item.path, docsDir);
279
301
  if (relative === item.path && docsDir !== "") continue;
302
+ if (isExcludedPath(relative)) continue;
280
303
  if (
281
304
  /\.(md|mdx)$/i.test(relative) ||
282
305
  /^_dir\.ya?ml$/i.test(path.basename(relative))
@@ -0,0 +1,332 @@
1
+ import { createSignal, For, Show } from "solid-js";
2
+ import type { ApiEndpoint, ApiParameter } from "../types";
3
+ import { DocMarkdown } from "./DocMarkdown";
4
+
5
+ function CopyButton(props: { text: () => string; label?: string }) {
6
+ const [copied, setCopied] = createSignal(false);
7
+ return (
8
+ <button
9
+ type="button"
10
+ onClick={() => {
11
+ navigator.clipboard.writeText(props.text()).catch(() => {});
12
+ setCopied(true);
13
+ setTimeout(() => setCopied(false), 1500);
14
+ }}
15
+ class="px-2 h-6 inline-flex items-center gap-1 rounded text-[10px] text-muted hover:text-foreground hover:bg-surface transition-colors cursor-pointer border-none bg-transparent"
16
+ >
17
+ <span
18
+ class={copied() ? "i-mdi:check text-success" : "i-mdi:content-copy"}
19
+ aria-hidden="true"
20
+ />
21
+ {copied() ? "Copied" : (props.label ?? "Copy")}
22
+ </button>
23
+ );
24
+ }
25
+
26
+ function SectionCard(props: {
27
+ title: string;
28
+ icon: string;
29
+ children: import("solid-js").JSX.Element;
30
+ actions?: import("solid-js").JSX.Element;
31
+ }) {
32
+ return (
33
+ <section class="border border-border rounded-xl bg-surface/30 overflow-hidden mb-6">
34
+ <header class="flex items-center gap-2 px-4 py-2.5 border-b border-border bg-background/50">
35
+ <span class={`${props.icon} text-primary`} aria-hidden="true" />
36
+ <h2 class="text-sm font-semibold text-foreground m-0">{props.title}</h2>
37
+ <div class="ml-auto">{props.actions}</div>
38
+ </header>
39
+ <div class="p-4">{props.children}</div>
40
+ </section>
41
+ );
42
+ }
43
+
44
+ const IN_BADGE: Record<string, string> = {
45
+ path: "bg-primary/10 text-primary border-primary/30",
46
+ query: "bg-accent/10 text-accent border-accent/30",
47
+ header: "bg-warning/10 text-warning border-warning/30",
48
+ cookie: "bg-muted/10 text-muted border-border",
49
+ };
50
+
51
+ function statusClass(code: string): string {
52
+ const n = Number.parseInt(code, 10);
53
+ if (n >= 200 && n < 300)
54
+ return "bg-success/15 text-success border-success/30";
55
+ if (n >= 300 && n < 400) return "bg-accent/15 text-accent border-accent/30";
56
+ if (n >= 400 && n < 500)
57
+ return "bg-warning/15 text-warning border-warning/30";
58
+ if (n >= 500)
59
+ return "bg-destructive/15 text-destructive border-destructive/30";
60
+ return "bg-surface text-muted border-border";
61
+ }
62
+
63
+ function ParamRow(props: { p: ApiParameter }) {
64
+ return (
65
+ <div class="flex items-start gap-3 py-2.5 border-b border-border/50 last:border-0">
66
+ <div class="min-w-0 flex-1">
67
+ <div class="flex items-center gap-2 flex-wrap">
68
+ <code class="text-sm font-mono font-semibold text-foreground">
69
+ {props.p.name}
70
+ </code>
71
+ <span
72
+ class={`text-[10px] px-1.5 py-0.5 rounded border ${IN_BADGE[props.p.in] ?? IN_BADGE.query}`}
73
+ >
74
+ {props.p.in}
75
+ </span>
76
+ <Show
77
+ when={props.p.required}
78
+ fallback={<span class="text-[10px] text-muted">optional</span>}
79
+ >
80
+ <span class="text-[10px] px-1.5 py-0.5 rounded bg-destructive/10 text-destructive font-medium">
81
+ required
82
+ </span>
83
+ </Show>
84
+ </div>
85
+ <Show when={props.p.description}>
86
+ <p class="text-xs text-muted m-0 mt-1 leading-relaxed">
87
+ {props.p.description}
88
+ </p>
89
+ </Show>
90
+ </div>
91
+ <Show when={props.p.example != null}>
92
+ <code class="text-[11px] font-mono px-2 py-1 rounded bg-background border border-border text-muted shrink-0 max-w-40 truncate">
93
+ {typeof props.p.example === "string"
94
+ ? props.p.example
95
+ : JSON.stringify(props.p.example)}
96
+ </code>
97
+ </Show>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ function JsonBlock(props: { value: unknown }) {
103
+ const text = () => JSON.stringify(props.value, null, 2);
104
+ return (
105
+ <div class="relative rounded-lg bg-background border border-border">
106
+ <div class="absolute top-1.5 right-1.5">
107
+ <CopyButton text={text} />
108
+ </div>
109
+ <pre class="text-xs font-mono text-foreground p-3 m-0 overflow-x-auto whitespace-pre-wrap break-all">
110
+ {text()}
111
+ </pre>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function ResponseRow(props: {
117
+ code: string;
118
+ description: string;
119
+ example?: unknown;
120
+ }) {
121
+ const [open, setOpen] = createSignal(false);
122
+ return (
123
+ <div class="border-b border-border/50 last:border-0">
124
+ <button
125
+ type="button"
126
+ onClick={() => props.example != null && setOpen(!open())}
127
+ class="w-full flex items-center gap-3 px-1 py-2.5 text-left cursor-pointer border-none bg-transparent"
128
+ >
129
+ <span
130
+ class={`inline-flex items-center justify-center min-w-12 px-2 py-0.5 rounded-md border text-xs font-bold font-mono ${statusClass(props.code)}`}
131
+ >
132
+ {props.code}
133
+ </span>
134
+ <span class="text-sm text-muted flex-1">{props.description}</span>
135
+ <Show when={props.example != null}>
136
+ <span
137
+ class={`i-mdi:chevron-down text-muted transition-transform ${open() ? "rotate-180" : ""}`}
138
+ aria-hidden="true"
139
+ />
140
+ </Show>
141
+ </button>
142
+ <Show when={open() && props.example != null}>
143
+ <div class="pb-3">
144
+ <JsonBlock value={props.example} />
145
+ </div>
146
+ </Show>
147
+ </div>
148
+ );
149
+ }
150
+
151
+ function curlCommand(ep: ApiEndpoint, base?: string): string {
152
+ const url = `${base ?? ep.server ?? "https://api.example.com"}${ep.path}`;
153
+ const lines = [`curl -X ${ep.method} "${url}"`];
154
+ for (const p of ep.parameters.filter((x) => x.in === "header")) {
155
+ lines.push(` -H "${p.name}: <value>"`);
156
+ }
157
+ if (ep.requestBody?.example != null) {
158
+ lines.push(` -H "Content-Type: application/json"`);
159
+ lines.push(` -d '${JSON.stringify(ep.requestBody.example)}'`);
160
+ }
161
+ return lines.join(" \\\n");
162
+ }
163
+
164
+ export function ApiReference(props: {
165
+ endpoint: ApiEndpoint;
166
+ style: "rest" | "graphql" | "cli" | "rpc";
167
+ }) {
168
+ const ep = () => props.endpoint;
169
+ const codes = () => Object.keys(ep().responses);
170
+
171
+ return (
172
+ <div>
173
+ <Show when={ep().description}>
174
+ <div class="markdown-body mb-6">
175
+ <DocMarkdown source={ep().description ?? ""} />
176
+ </div>
177
+ </Show>
178
+
179
+ <Show when={props.style === "cli"}>
180
+ <SectionCard
181
+ title="Usage"
182
+ icon="i-mdi:console-line"
183
+ actions={
184
+ <CopyButton
185
+ text={() =>
186
+ ep().cli
187
+ ? [
188
+ ep().cli!.command,
189
+ ep().cli!.subcommand,
190
+ ...(ep().cli!.args?.map((a) =>
191
+ a.required ? `<${a.name}>` : `[${a.name}]`,
192
+ ) ?? []),
193
+ ].join(" ")
194
+ : `${ep().method} ${ep().path}`
195
+ }
196
+ />
197
+ }
198
+ >
199
+ <pre class="text-sm font-mono text-foreground bg-background border border-border rounded-lg p-3 m-0 overflow-x-auto">
200
+ {ep().cli
201
+ ? [
202
+ ep().cli!.command,
203
+ ep().cli!.subcommand,
204
+ ...(ep().cli!.args?.map((a) =>
205
+ a.required ? `<${a.name}>` : `[${a.name}]`,
206
+ ) ?? []),
207
+ ].join(" ")
208
+ : `${ep().method} ${ep().path}`}
209
+ </pre>
210
+ <Show when={ep().cli?.args?.length}>
211
+ <div class="mt-4">
212
+ <For each={ep().cli?.args ?? []}>
213
+ {(a) => (
214
+ <div class="flex items-start gap-3 py-2 border-b border-border/50 last:border-0">
215
+ <code class="text-sm font-mono font-semibold text-foreground">
216
+ {a.name}
217
+ </code>
218
+ <Show when={a.required}>
219
+ <span class="text-[10px] px-1.5 py-0.5 rounded bg-destructive/10 text-destructive font-medium">
220
+ required
221
+ </span>
222
+ </Show>
223
+ <span class="text-xs text-muted flex-1">
224
+ {a.description}
225
+ </span>
226
+ </div>
227
+ )}
228
+ </For>
229
+ </div>
230
+ </Show>
231
+ </SectionCard>
232
+ </Show>
233
+
234
+ <Show when={props.style === "graphql"}>
235
+ <Show when={ep().graphql?.query}>
236
+ <SectionCard
237
+ title="Query"
238
+ icon="i-mdi:graphql"
239
+ actions={<CopyButton text={() => ep().graphql?.query ?? ""} />}
240
+ >
241
+ <pre class="text-xs font-mono text-foreground bg-background border border-border rounded-lg p-3 m-0 overflow-x-auto whitespace-pre-wrap">
242
+ {ep().graphql?.query}
243
+ </pre>
244
+ </SectionCard>
245
+ </Show>
246
+ <Show when={ep().graphql?.variables != null}>
247
+ <SectionCard title="Variables" icon="i-mdi:code-json">
248
+ <JsonBlock value={ep().graphql?.variables} />
249
+ </SectionCard>
250
+ </Show>
251
+ </Show>
252
+
253
+ <Show when={props.style === "rest" || props.style === "rpc"}>
254
+ <Show when={ep().parameters.length > 0}>
255
+ <SectionCard
256
+ title="Parameters"
257
+ icon="i-mdi:tune-variant"
258
+ actions={
259
+ <span class="text-[10px] text-muted">
260
+ {ep().parameters.length} field
261
+ {ep().parameters.length === 1 ? "" : "s"}
262
+ </span>
263
+ }
264
+ >
265
+ <For each={ep().parameters}>{(p) => <ParamRow p={p} />}</For>
266
+ </SectionCard>
267
+ </Show>
268
+
269
+ <Show when={ep().requestBody}>
270
+ <SectionCard
271
+ title="Request Body"
272
+ icon="i-mdi:code-braces"
273
+ actions={
274
+ <Show when={ep().requestBody?.required}>
275
+ <span class="text-[10px] px-1.5 py-0.5 rounded bg-destructive/10 text-destructive font-medium">
276
+ required
277
+ </span>
278
+ </Show>
279
+ }
280
+ >
281
+ <Show when={ep().requestBody?.description}>
282
+ <p class="text-sm text-muted m-0 mb-3 leading-relaxed">
283
+ {ep().requestBody?.description}
284
+ </p>
285
+ </Show>
286
+ <Show when={ep().requestBody?.example != null}>
287
+ <JsonBlock value={ep().requestBody?.example} />
288
+ </Show>
289
+ </SectionCard>
290
+ </Show>
291
+
292
+ <SectionCard
293
+ title="cURL"
294
+ icon="i-mdi:console"
295
+ actions={<CopyButton text={() => curlCommand(ep())} />}
296
+ >
297
+ <pre class="text-xs font-mono text-foreground bg-background border border-border rounded-lg p-3 m-0 overflow-x-auto whitespace-pre-wrap">
298
+ {curlCommand(ep())}
299
+ </pre>
300
+ </SectionCard>
301
+ </Show>
302
+
303
+ <Show when={ep().requestBody?.example != null && props.style === "cli"}>
304
+ <SectionCard title="Options" icon="i-mdi:code-json">
305
+ <JsonBlock value={ep().requestBody?.example} />
306
+ </SectionCard>
307
+ </Show>
308
+
309
+ <Show when={codes().length > 0}>
310
+ <SectionCard
311
+ title={props.style === "cli" ? "Exit codes" : "Responses"}
312
+ icon="i-mdi:reply-outline"
313
+ actions={
314
+ <span class="text-[10px] text-muted">
315
+ {codes().length} code{codes().length === 1 ? "" : "s"}
316
+ </span>
317
+ }
318
+ >
319
+ <For each={codes()}>
320
+ {(code) => (
321
+ <ResponseRow
322
+ code={code}
323
+ description={ep().responses[code]?.description ?? ""}
324
+ example={ep().responses[code]?.example}
325
+ />
326
+ )}
327
+ </For>
328
+ </SectionCard>
329
+ </Show>
330
+ </div>
331
+ );
332
+ }