@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 +1 -1
- package/docs/data-sources.md +1 -1
- package/docs/deploy.md +1 -1
- package/docs/features.md +1 -1
- package/docs/markdown.md +1 -1
- package/docs/theming.md +1 -1
- package/docs/translate.md +46 -0
- package/package.json +7 -5
- package/scripts/generate-docs-index.ts +3 -3
- package/scripts/translate.ts +127 -0
- package/src/adapters/github/github-pull.ts +23 -0
- package/src/runtime/components/ApiReference.tsx +332 -0
- package/src/runtime/components/ContextMenu.tsx +88 -0
- package/src/runtime/components/MobileBottomNav.tsx +3 -3
- package/src/runtime/components/SidebarNav.tsx +190 -3
- package/src/runtime/components/TopNav.tsx +163 -49
- package/src/runtime/config.ts +24 -1
- package/src/runtime/content.ts +17 -15
- package/src/runtime/index.ts +6 -0
- package/src/runtime/pages/ApiEndpointPage.tsx +54 -141
- package/src/runtime/pages/DocPage.tsx +10 -1
- package/src/runtime/pages/PluginsPage.tsx +38 -13
- package/src/runtime/pages/SearchPage.tsx +374 -0
- package/src/runtime/pages/ShowcasePage.tsx +143 -15
- package/src/runtime/pages/TranslatePage.tsx +338 -0
- package/src/runtime/plugins-catalog.ts +170 -0
- package/src/runtime/router.tsx +23 -0
package/docs/config.md
CHANGED
package/docs/data-sources.md
CHANGED
package/docs/deploy.md
CHANGED
package/docs/features.md
CHANGED
package/docs/markdown.md
CHANGED
package/docs/theming.md
CHANGED
|
@@ -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
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"create-docs": "
|
|
8
|
-
"create-docs-pull": "
|
|
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("--
|
|
103
|
-
? join(root, "
|
|
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
|
+
}
|