@veluai/velu 0.2.13 → 0.2.15
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 +80 -80
- package/dist/cli.js +37 -37
- package/package.json +64 -64
- package/runtime/velu-ui/base.css +320 -320
- package/runtime/velu-ui/components/Accordion.jsx +64 -64
- package/runtime/velu-ui/components/ApiClient.jsx +207 -207
- package/runtime/velu-ui/components/ApiField.jsx +87 -87
- package/runtime/velu-ui/components/ApiPath.jsx +63 -63
- package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
- package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
- package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
- package/runtime/velu-ui/components/AskBar.jsx +71 -71
- package/runtime/velu-ui/components/Callout.jsx +114 -114
- package/runtime/velu-ui/components/Card.jsx +131 -131
- package/runtime/velu-ui/components/Chatbot.jsx +885 -885
- package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
- package/runtime/velu-ui/components/Columns.jsx +56 -56
- package/runtime/velu-ui/components/ContextMenu.jsx +298 -273
- package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
- package/runtime/velu-ui/components/Field.jsx +81 -81
- package/runtime/velu-ui/components/Image.jsx +163 -163
- package/runtime/velu-ui/components/Logo.jsx +31 -31
- package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
- package/runtime/velu-ui/components/NavSelect.jsx +108 -108
- package/runtime/velu-ui/components/NotFound.jsx +63 -63
- package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
- package/runtime/velu-ui/components/PageFooter.jsx +145 -145
- package/runtime/velu-ui/components/PageHeader.jsx +422 -422
- package/runtime/velu-ui/components/PageNav.jsx +77 -77
- package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
- package/runtime/velu-ui/components/Prompt.jsx +115 -115
- package/runtime/velu-ui/components/Search.jsx +460 -460
- package/runtime/velu-ui/components/Sidebar.jsx +254 -254
- package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
- package/runtime/velu-ui/components/Steps.jsx +65 -65
- package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
- package/runtime/velu-ui/components/Toc.jsx +537 -537
- package/runtime/velu-ui/components/TocBar.jsx +195 -195
- package/runtime/velu-ui/components/Tree.jsx +87 -87
- package/runtime/velu-ui/components/TryItBar.jsx +102 -102
- package/runtime/velu-ui/components/accordion.css +92 -92
- package/runtime/velu-ui/components/api-page.css +208 -208
- package/runtime/velu-ui/components/api.css +635 -635
- package/runtime/velu-ui/components/ask-bar.css +94 -94
- package/runtime/velu-ui/components/card.css +105 -105
- package/runtime/velu-ui/components/chatbot.css +622 -622
- package/runtime/velu-ui/components/code-block.css +263 -263
- package/runtime/velu-ui/components/context-menu.css +173 -173
- package/runtime/velu-ui/components/docs-layout.css +822 -822
- package/runtime/velu-ui/components/field.css +82 -82
- package/runtime/velu-ui/components/image.css +237 -237
- package/runtime/velu-ui/components/nav-select.css +157 -157
- package/runtime/velu-ui/components/not-found.css +94 -94
- package/runtime/velu-ui/components/page-feedback.css +241 -241
- package/runtime/velu-ui/components/page-footer.css +130 -130
- package/runtime/velu-ui/components/page-header.css +558 -558
- package/runtime/velu-ui/components/page-nav.css +50 -50
- package/runtime/velu-ui/components/powered-by.css +92 -92
- package/runtime/velu-ui/components/prompt.css +99 -99
- package/runtime/velu-ui/components/search.css +307 -307
- package/runtime/velu-ui/components/sidebar.css +205 -205
- package/runtime/velu-ui/components/steps.css +77 -77
- package/runtime/velu-ui/components/theme-toggle.css +102 -102
- package/runtime/velu-ui/components/toc-bar.css +234 -234
- package/runtime/velu-ui/components/tree.css +49 -49
- package/runtime/velu-ui/index.js +54 -54
- package/runtime/velu-ui/lib/api-send.js +92 -92
- package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
- package/runtime/velu-ui/lib/component-schemas.js +100 -100
- package/runtime/velu-ui/lib/copyText.js +64 -64
- package/runtime/velu-ui/lib/docs-assistant.js +250 -250
- package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
- package/runtime/velu-ui/lib/pagefind.js +113 -113
- package/runtime/velu-ui/lib/prism-langs.js +957 -957
- package/runtime/velu-ui/lib/prism-loader.js +74 -74
- package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
- package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
- package/runtime/velu-ui/mdx-components.jsx +105 -105
- package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
- package/runtime/velu-ui/primitives/Stack.jsx +63 -63
- package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
- package/runtime/velu-ui/primitives/stack.css +3 -3
- package/runtime/velu-ui/primitives/switcher.css +25 -25
- package/runtime/velu-ui/styles.css +46 -46
- package/runtime/velu-ui/tokens.css +4 -4
- package/schema/velu.schema.json +423 -423
- package/src/lib/extract-mdx-error.js +170 -170
- package/src/lib/issues.js +159 -159
- package/src/lib/known-components.js +34 -34
- package/src/navigation.js +443 -443
- package/src/runtime/App.jsx +1669 -1668
- package/src/runtime/ErrorBoundary.jsx +54 -54
- package/src/runtime/client-entry.jsx +22 -22
- package/src/runtime/server-entry.jsx +16 -16
- package/src/template.html +48 -48
- package/templates/starter/ai-tools/claude-code.mdx +26 -26
- package/templates/starter/ai-tools/cursor.mdx +17 -17
- package/templates/starter/api-reference/introduction.mdx +43 -43
- package/templates/starter/development.mdx +19 -19
- package/templates/starter/essentials/code.mdx +29 -29
- package/templates/starter/essentials/images.mdx +29 -29
- package/templates/starter/essentials/markdown.mdx +25 -25
- package/templates/starter/essentials/navigation.mdx +39 -39
- package/templates/starter/essentials/settings.mdx +30 -30
- package/templates/starter/favicon.svg +6 -6
- package/templates/starter/index.mdx +31 -31
- package/templates/starter/openapi.json +160 -160
- package/templates/starter/quickstart.mdx +31 -31
- package/templates/starter/velu.json +41 -41
|
@@ -1,384 +1,384 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import MethodBadge from './MethodBadge.jsx';
|
|
4
|
-
import ApiPath from './ApiPath.jsx';
|
|
5
|
-
import TryItBar from './TryItBar.jsx';
|
|
6
|
-
import ApiClient from './ApiClient.jsx';
|
|
7
|
-
import ApiField from './ApiField.jsx';
|
|
8
|
-
import Field from './Field.jsx';
|
|
9
|
-
import Accordion, { AccordionGroup } from './Accordion.jsx';
|
|
10
|
-
import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
|
|
11
|
-
import ApiSamples from './ApiSamples.jsx';
|
|
12
|
-
import Callout from './Callout.jsx';
|
|
13
|
-
import { sendApiRequest } from '../lib/api-send.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* ApiReferencePage — the full, auto-generated reference page for one OpenAPI
|
|
17
|
-
* operation. Driven entirely by the normalized `operation` model (see the
|
|
18
|
-
* CLI's src/openapi/parse.js). Renders the doc body (auth, params, body,
|
|
19
|
-
* responses) and an interactive "Try It" playground that builds + sends the
|
|
20
|
-
* request (through the dev proxy by default — see lib/api-send.js).
|
|
21
|
-
*
|
|
22
|
-
* The right-rail code samples live in the page's right aside (the runtime
|
|
23
|
-
* renders <ApiSamples> there for API pages) — this component owns the main
|
|
24
|
-
* column + the playground only.
|
|
25
|
-
*
|
|
26
|
-
* @param {{ operation: object, api?: { proxy?: boolean, server?: string } }} props
|
|
27
|
-
*/
|
|
28
|
-
const LOCATION_LABEL = { path: 'Path Parameters', query: 'Query Parameters', header: 'Header Parameters' };
|
|
29
|
-
|
|
30
|
-
export default function ApiReferencePage({ operation, samples = [], apiOperations = [], api = {} }) {
|
|
31
|
-
const [open, setOpen] = useState(false);
|
|
32
|
-
// Which operation the playground is currently showing. Defaults to this
|
|
33
|
-
// page's own operation; the dropdown can switch it to any sibling.
|
|
34
|
-
const [activeId, setActiveId] = useState(operation.id);
|
|
35
|
-
|
|
36
|
-
// The switchable list — fall back to just this page's operation.
|
|
37
|
-
const ops =
|
|
38
|
-
apiOperations.length > 0
|
|
39
|
-
? apiOperations
|
|
40
|
-
: [{ id: operation.id, method: operation.method, title: operation.title, operation, samples, api }];
|
|
41
|
-
const current = ops.find((o) => o.id === activeId) || { operation, samples, api };
|
|
42
|
-
// Each operation carries its own (section-level) api config; fall back to
|
|
43
|
-
// this page's config.
|
|
44
|
-
const currentApi = current.api ?? api;
|
|
45
|
-
const server = currentApi.server || current.operation.servers?.[0] || '';
|
|
46
|
-
|
|
47
|
-
// While the playground modal is open, close on Escape and lock the
|
|
48
|
-
// page scroll so the dimmed backdrop reads as a true overlay.
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (!open || typeof document === 'undefined') return undefined;
|
|
51
|
-
const onKey = (e) => {
|
|
52
|
-
if (e.key === 'Escape') setOpen(false);
|
|
53
|
-
};
|
|
54
|
-
document.addEventListener('keydown', onKey);
|
|
55
|
-
const prev = document.body.style.overflow;
|
|
56
|
-
document.body.style.overflow = 'hidden';
|
|
57
|
-
return () => {
|
|
58
|
-
document.removeEventListener('keydown', onKey);
|
|
59
|
-
document.body.style.overflow = prev;
|
|
60
|
-
};
|
|
61
|
-
}, [open]);
|
|
62
|
-
|
|
63
|
-
if (!operation) return null;
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<div className="velu-api-page">
|
|
67
|
-
{/* The page title + description come from the page frontmatter heading
|
|
68
|
-
(rendered by the runtime), so this component starts at the Try-It bar
|
|
69
|
-
to avoid a duplicate title. */}
|
|
70
|
-
<TryItBar
|
|
71
|
-
method={operation.method}
|
|
72
|
-
path={operation.path}
|
|
73
|
-
cta="Try It"
|
|
74
|
-
onTry={() => {
|
|
75
|
-
setActiveId(operation.id);
|
|
76
|
-
setOpen(true);
|
|
77
|
-
}}
|
|
78
|
-
className="velu-api-page__tryit"
|
|
79
|
-
/>
|
|
80
|
-
|
|
81
|
-
{/* Authorizations */}
|
|
82
|
-
{operation.auth?.length > 0 && (
|
|
83
|
-
<section className="velu-api-page__section">
|
|
84
|
-
<h2>Authorizations</h2>
|
|
85
|
-
{operation.auth.map((a, i) => (
|
|
86
|
-
<Field key={i} name={a.name} pre={a.prefix} type={a.type} required={a.required} post={a.in}>
|
|
87
|
-
{a.description}
|
|
88
|
-
</Field>
|
|
89
|
-
))}
|
|
90
|
-
</section>
|
|
91
|
-
)}
|
|
92
|
-
|
|
93
|
-
{/* Parameters, by location */}
|
|
94
|
-
{['path', 'query', 'header'].map((loc) => {
|
|
95
|
-
const rows = operation.parameters?.[loc] || [];
|
|
96
|
-
if (!rows.length) return null;
|
|
97
|
-
return (
|
|
98
|
-
<section key={loc} className="velu-api-page__section">
|
|
99
|
-
<h2>{LOCATION_LABEL[loc]}</h2>
|
|
100
|
-
{rows.map((p, i) => (
|
|
101
|
-
<Field key={i} name={p.name} type={p.type} required={p.required}>
|
|
102
|
-
{p.description}
|
|
103
|
-
{p.enum && <EnumHint values={p.enum} />}
|
|
104
|
-
</Field>
|
|
105
|
-
))}
|
|
106
|
-
</section>
|
|
107
|
-
);
|
|
108
|
-
})}
|
|
109
|
-
|
|
110
|
-
{/* Request body */}
|
|
111
|
-
{operation.body?.fields?.length > 0 && (
|
|
112
|
-
<section className="velu-api-page__section">
|
|
113
|
-
<h2>Body</h2>
|
|
114
|
-
{operation.body.fields.map((f, i) => (
|
|
115
|
-
<Field key={i} name={f.name} type={f.type} required={f.required}>
|
|
116
|
-
{f.description}
|
|
117
|
-
{f.enum && <EnumHint values={f.enum} />}
|
|
118
|
-
</Field>
|
|
119
|
-
))}
|
|
120
|
-
</section>
|
|
121
|
-
)}
|
|
122
|
-
|
|
123
|
-
{/* Responses */}
|
|
124
|
-
{operation.responses?.length > 0 && (
|
|
125
|
-
<section className="velu-api-page__section">
|
|
126
|
-
<h2>Response</h2>
|
|
127
|
-
<ResponseSections responses={operation.responses} />
|
|
128
|
-
</section>
|
|
129
|
-
)}
|
|
130
|
-
|
|
131
|
-
{/* Try-It playground — rendered as an overlay above the page, with a
|
|
132
|
-
blurred backdrop, rather than inline. Portaled to <body> so the
|
|
133
|
-
docs layout's @container containment can't clip the fixed overlay. */}
|
|
134
|
-
{open && typeof document !== 'undefined' &&
|
|
135
|
-
createPortal(
|
|
136
|
-
<div
|
|
137
|
-
className="velu-api-modal"
|
|
138
|
-
role="presentation"
|
|
139
|
-
onClick={() => setOpen(false)}
|
|
140
|
-
>
|
|
141
|
-
<div
|
|
142
|
-
className="velu-api-modal__panel"
|
|
143
|
-
role="dialog"
|
|
144
|
-
aria-modal="true"
|
|
145
|
-
aria-label={`${operation.title} playground`}
|
|
146
|
-
onClick={(e) => e.stopPropagation()}
|
|
147
|
-
>
|
|
148
|
-
<Playground
|
|
149
|
-
key={current.operation.id}
|
|
150
|
-
operation={current.operation}
|
|
151
|
-
samples={current.samples}
|
|
152
|
-
operations={ops}
|
|
153
|
-
activeId={activeId}
|
|
154
|
-
onSelectOperation={setActiveId}
|
|
155
|
-
server={server}
|
|
156
|
-
proxy={currentApi.proxy !== false}
|
|
157
|
-
onClose={() => setOpen(false)}
|
|
158
|
-
/>
|
|
159
|
-
</div>
|
|
160
|
-
</div>,
|
|
161
|
-
document.body,
|
|
162
|
-
)}
|
|
163
|
-
</div>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function EnumHint({ values }) {
|
|
168
|
-
return (
|
|
169
|
-
<p className="velu-api-page__enum">
|
|
170
|
-
Allowed values:{' '}
|
|
171
|
-
{values.map((v, i) => (
|
|
172
|
-
<code key={i}>{String(v)}</code>
|
|
173
|
-
))}
|
|
174
|
-
</p>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function ResponseSections({ responses }) {
|
|
179
|
-
return (
|
|
180
|
-
<AccordionGroup className="velu-api-resp">
|
|
181
|
-
{responses.map((r, i) => {
|
|
182
|
-
return (
|
|
183
|
-
<Accordion
|
|
184
|
-
key={i}
|
|
185
|
-
defaultOpen
|
|
186
|
-
title={
|
|
187
|
-
<span className="velu-api-resp__head">
|
|
188
|
-
<span className="velu-api-resp__status">{r.status}</span>
|
|
189
|
-
{r.contentType && (
|
|
190
|
-
<span className="velu-api-resp__ctype">{r.contentType}</span>
|
|
191
|
-
)}
|
|
192
|
-
</span>
|
|
193
|
-
}
|
|
194
|
-
>
|
|
195
|
-
{r.description && <p className="velu-api-resp__desc">{r.description}</p>}
|
|
196
|
-
{r.fields?.map((f, j) => (
|
|
197
|
-
<Field key={j} name={f.name} type={f.type} required={f.required}>
|
|
198
|
-
{f.description}
|
|
199
|
-
</Field>
|
|
200
|
-
))}
|
|
201
|
-
</Accordion>
|
|
202
|
-
);
|
|
203
|
-
})}
|
|
204
|
-
</AccordionGroup>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ── The interactive playground ──────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
function Playground({
|
|
211
|
-
operation,
|
|
212
|
-
samples = [],
|
|
213
|
-
operations = [],
|
|
214
|
-
activeId,
|
|
215
|
-
onSelectOperation,
|
|
216
|
-
server,
|
|
217
|
-
proxy,
|
|
218
|
-
onClose,
|
|
219
|
-
}) {
|
|
220
|
-
const [values, setValues] = useState({});
|
|
221
|
-
const [bodyText, setBodyText] = useState(
|
|
222
|
-
operation.body ? JSON.stringify(operation.body.example ?? {}, null, 2) : '',
|
|
223
|
-
);
|
|
224
|
-
const [busy, setBusy] = useState(false);
|
|
225
|
-
const [resp, setResp] = useState(null);
|
|
226
|
-
|
|
227
|
-
const set = (loc, name, v) =>
|
|
228
|
-
setValues((prev) => ({ ...prev, [loc]: { ...(prev[loc] || {}), [name]: v } }));
|
|
229
|
-
|
|
230
|
-
const onSend = useCallback(async () => {
|
|
231
|
-
setBusy(true);
|
|
232
|
-
setResp(null);
|
|
233
|
-
try {
|
|
234
|
-
const result = await sendApiRequest({ operation, server, proxy, values, bodyText });
|
|
235
|
-
setResp(result);
|
|
236
|
-
} catch (err) {
|
|
237
|
-
setResp({ error: String(err?.message || err) });
|
|
238
|
-
} finally {
|
|
239
|
-
setBusy(false);
|
|
240
|
-
}
|
|
241
|
-
}, [operation, server, proxy, values, bodyText]);
|
|
242
|
-
|
|
243
|
-
const sections = [];
|
|
244
|
-
if (operation.auth?.length) sections.push(['auth', 'Authorization', operation.auth]);
|
|
245
|
-
for (const loc of ['path', 'query', 'header']) {
|
|
246
|
-
const rows = operation.parameters?.[loc] || [];
|
|
247
|
-
if (rows.length) sections.push([loc, LOCATION_LABEL[loc].replace(' Parameters', ''), rows]);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return (
|
|
251
|
-
<ApiClient
|
|
252
|
-
method={operation.method}
|
|
253
|
-
label={operation.title}
|
|
254
|
-
path={operation.path}
|
|
255
|
-
description={operation.description}
|
|
256
|
-
operations={operations}
|
|
257
|
-
activeId={activeId}
|
|
258
|
-
onSelectOperation={onSelectOperation}
|
|
259
|
-
onSend={onSend}
|
|
260
|
-
sending={busy}
|
|
261
|
-
onClose={onClose}
|
|
262
|
-
response={resp ? <ResponsePanel resp={resp} /> : null}
|
|
263
|
-
aside={<ApiSamples samples={samples} responses={operation.responses} />}
|
|
264
|
-
>
|
|
265
|
-
<AccordionGroup>
|
|
266
|
-
{sections.map(([loc, title, rows]) => (
|
|
267
|
-
<Accordion key={loc} title={title} defaultOpen>
|
|
268
|
-
{rows.map((f, i) => (
|
|
269
|
-
<ApiField
|
|
270
|
-
key={i}
|
|
271
|
-
name={f.name}
|
|
272
|
-
type={f.type}
|
|
273
|
-
required={f.required}
|
|
274
|
-
prefix={f.prefix}
|
|
275
|
-
value={values[loc]?.[f.name] ?? ''}
|
|
276
|
-
onChange={(e) => set(loc, f.name, e.target.value)}
|
|
277
|
-
>
|
|
278
|
-
{f.description}
|
|
279
|
-
</ApiField>
|
|
280
|
-
))}
|
|
281
|
-
</Accordion>
|
|
282
|
-
))}
|
|
283
|
-
{operation.body && (
|
|
284
|
-
<Accordion title="Body" defaultOpen>
|
|
285
|
-
<textarea
|
|
286
|
-
className="velu-api-body-input"
|
|
287
|
-
spellCheck={false}
|
|
288
|
-
rows={Math.min(14, bodyText.split('\n').length + 1)}
|
|
289
|
-
value={bodyText}
|
|
290
|
-
onChange={(e) => setBodyText(e.target.value)}
|
|
291
|
-
/>
|
|
292
|
-
</Accordion>
|
|
293
|
-
)}
|
|
294
|
-
</AccordionGroup>
|
|
295
|
-
</ApiClient>
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* ResponsePanel — the live-response viewer: a Body / Header tabbed card.
|
|
301
|
-
* - Body: a status-colored Callout (green for 2xx, red otherwise) plus the
|
|
302
|
-
* response body.
|
|
303
|
-
* - Header: the response headers as a key/value table.
|
|
304
|
-
*/
|
|
305
|
-
function ResponsePanel({ resp }) {
|
|
306
|
-
const [tab, setTab] = useState('body');
|
|
307
|
-
if (!resp) return null;
|
|
308
|
-
|
|
309
|
-
const isError = resp.error != null;
|
|
310
|
-
const ok = !isError && resp.status >= 200 && resp.status < 300;
|
|
311
|
-
const statusLabel = isError
|
|
312
|
-
? 'Request failed'
|
|
313
|
-
: `${resp.status}${resp.statusText ? ` - ${resp.statusText}` : ''}`;
|
|
314
|
-
const bodyStr = isError
|
|
315
|
-
? resp.error
|
|
316
|
-
: typeof resp.body === 'string'
|
|
317
|
-
? resp.body
|
|
318
|
-
: JSON.stringify(resp.body, null, 2);
|
|
319
|
-
const headerRows = Object.entries(resp.headers || {});
|
|
320
|
-
|
|
321
|
-
// Reuse the CodeGroup's tab chrome (velu-code-block__*) so the Body/Header
|
|
322
|
-
// switcher looks identical to the code-sample tabs.
|
|
323
|
-
const tabCls = (name) =>
|
|
324
|
-
`velu-code-block__tab${tab === name ? ' velu-code-block__tab--active' : ''}`;
|
|
325
|
-
|
|
326
|
-
return (
|
|
327
|
-
<div className="velu-code-block velu-api-resp-panel">
|
|
328
|
-
<div className="velu-code-block__header">
|
|
329
|
-
<div className="velu-code-block__tabs" role="tablist">
|
|
330
|
-
<button
|
|
331
|
-
type="button"
|
|
332
|
-
role="tab"
|
|
333
|
-
aria-selected={tab === 'body'}
|
|
334
|
-
className={tabCls('body')}
|
|
335
|
-
onClick={() => setTab('body')}
|
|
336
|
-
>
|
|
337
|
-
<span className="velu-code-block__tab-label">Body</span>
|
|
338
|
-
</button>
|
|
339
|
-
<button
|
|
340
|
-
type="button"
|
|
341
|
-
role="tab"
|
|
342
|
-
aria-selected={tab === 'header'}
|
|
343
|
-
className={tabCls('header')}
|
|
344
|
-
onClick={() => setTab('header')}
|
|
345
|
-
disabled={!headerRows.length || undefined}
|
|
346
|
-
>
|
|
347
|
-
<span className="velu-code-block__tab-label">Header</span>
|
|
348
|
-
</button>
|
|
349
|
-
</div>
|
|
350
|
-
</div>
|
|
351
|
-
|
|
352
|
-
<div className="velu-api-resp-panel__content">
|
|
353
|
-
{tab === 'body' ? (
|
|
354
|
-
<>
|
|
355
|
-
<Callout
|
|
356
|
-
type={ok ? 'check' : 'danger'}
|
|
357
|
-
className="velu-api-resp-panel__status"
|
|
358
|
-
>
|
|
359
|
-
{statusLabel}
|
|
360
|
-
</Callout>
|
|
361
|
-
{!isError && (
|
|
362
|
-
<div className="velu-api-resp-body__scroll velu-hide-scrollbar">
|
|
363
|
-
<CodeBlock language="json">{bodyStr || '(empty)'}</CodeBlock>
|
|
364
|
-
</div>
|
|
365
|
-
)}
|
|
366
|
-
</>
|
|
367
|
-
) : (
|
|
368
|
-
<div className="velu-api-resp-headers__scroll velu-hide-scrollbar">
|
|
369
|
-
<table className="velu-api-resp-headers">
|
|
370
|
-
<tbody>
|
|
371
|
-
{headerRows.map(([k, val]) => (
|
|
372
|
-
<tr key={k}>
|
|
373
|
-
<td className="velu-api-resp-headers__key">{k}</td>
|
|
374
|
-
<td className="velu-api-resp-headers__val">{val}</td>
|
|
375
|
-
</tr>
|
|
376
|
-
))}
|
|
377
|
-
</tbody>
|
|
378
|
-
</table>
|
|
379
|
-
</div>
|
|
380
|
-
)}
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
);
|
|
384
|
-
}
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import MethodBadge from './MethodBadge.jsx';
|
|
4
|
+
import ApiPath from './ApiPath.jsx';
|
|
5
|
+
import TryItBar from './TryItBar.jsx';
|
|
6
|
+
import ApiClient from './ApiClient.jsx';
|
|
7
|
+
import ApiField from './ApiField.jsx';
|
|
8
|
+
import Field from './Field.jsx';
|
|
9
|
+
import Accordion, { AccordionGroup } from './Accordion.jsx';
|
|
10
|
+
import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
|
|
11
|
+
import ApiSamples from './ApiSamples.jsx';
|
|
12
|
+
import Callout from './Callout.jsx';
|
|
13
|
+
import { sendApiRequest } from '../lib/api-send.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ApiReferencePage — the full, auto-generated reference page for one OpenAPI
|
|
17
|
+
* operation. Driven entirely by the normalized `operation` model (see the
|
|
18
|
+
* CLI's src/openapi/parse.js). Renders the doc body (auth, params, body,
|
|
19
|
+
* responses) and an interactive "Try It" playground that builds + sends the
|
|
20
|
+
* request (through the dev proxy by default — see lib/api-send.js).
|
|
21
|
+
*
|
|
22
|
+
* The right-rail code samples live in the page's right aside (the runtime
|
|
23
|
+
* renders <ApiSamples> there for API pages) — this component owns the main
|
|
24
|
+
* column + the playground only.
|
|
25
|
+
*
|
|
26
|
+
* @param {{ operation: object, api?: { proxy?: boolean, server?: string } }} props
|
|
27
|
+
*/
|
|
28
|
+
const LOCATION_LABEL = { path: 'Path Parameters', query: 'Query Parameters', header: 'Header Parameters' };
|
|
29
|
+
|
|
30
|
+
export default function ApiReferencePage({ operation, samples = [], apiOperations = [], api = {} }) {
|
|
31
|
+
const [open, setOpen] = useState(false);
|
|
32
|
+
// Which operation the playground is currently showing. Defaults to this
|
|
33
|
+
// page's own operation; the dropdown can switch it to any sibling.
|
|
34
|
+
const [activeId, setActiveId] = useState(operation.id);
|
|
35
|
+
|
|
36
|
+
// The switchable list — fall back to just this page's operation.
|
|
37
|
+
const ops =
|
|
38
|
+
apiOperations.length > 0
|
|
39
|
+
? apiOperations
|
|
40
|
+
: [{ id: operation.id, method: operation.method, title: operation.title, operation, samples, api }];
|
|
41
|
+
const current = ops.find((o) => o.id === activeId) || { operation, samples, api };
|
|
42
|
+
// Each operation carries its own (section-level) api config; fall back to
|
|
43
|
+
// this page's config.
|
|
44
|
+
const currentApi = current.api ?? api;
|
|
45
|
+
const server = currentApi.server || current.operation.servers?.[0] || '';
|
|
46
|
+
|
|
47
|
+
// While the playground modal is open, close on Escape and lock the
|
|
48
|
+
// page scroll so the dimmed backdrop reads as a true overlay.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!open || typeof document === 'undefined') return undefined;
|
|
51
|
+
const onKey = (e) => {
|
|
52
|
+
if (e.key === 'Escape') setOpen(false);
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener('keydown', onKey);
|
|
55
|
+
const prev = document.body.style.overflow;
|
|
56
|
+
document.body.style.overflow = 'hidden';
|
|
57
|
+
return () => {
|
|
58
|
+
document.removeEventListener('keydown', onKey);
|
|
59
|
+
document.body.style.overflow = prev;
|
|
60
|
+
};
|
|
61
|
+
}, [open]);
|
|
62
|
+
|
|
63
|
+
if (!operation) return null;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="velu-api-page">
|
|
67
|
+
{/* The page title + description come from the page frontmatter heading
|
|
68
|
+
(rendered by the runtime), so this component starts at the Try-It bar
|
|
69
|
+
to avoid a duplicate title. */}
|
|
70
|
+
<TryItBar
|
|
71
|
+
method={operation.method}
|
|
72
|
+
path={operation.path}
|
|
73
|
+
cta="Try It"
|
|
74
|
+
onTry={() => {
|
|
75
|
+
setActiveId(operation.id);
|
|
76
|
+
setOpen(true);
|
|
77
|
+
}}
|
|
78
|
+
className="velu-api-page__tryit"
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
{/* Authorizations */}
|
|
82
|
+
{operation.auth?.length > 0 && (
|
|
83
|
+
<section className="velu-api-page__section">
|
|
84
|
+
<h2>Authorizations</h2>
|
|
85
|
+
{operation.auth.map((a, i) => (
|
|
86
|
+
<Field key={i} name={a.name} pre={a.prefix} type={a.type} required={a.required} post={a.in}>
|
|
87
|
+
{a.description}
|
|
88
|
+
</Field>
|
|
89
|
+
))}
|
|
90
|
+
</section>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Parameters, by location */}
|
|
94
|
+
{['path', 'query', 'header'].map((loc) => {
|
|
95
|
+
const rows = operation.parameters?.[loc] || [];
|
|
96
|
+
if (!rows.length) return null;
|
|
97
|
+
return (
|
|
98
|
+
<section key={loc} className="velu-api-page__section">
|
|
99
|
+
<h2>{LOCATION_LABEL[loc]}</h2>
|
|
100
|
+
{rows.map((p, i) => (
|
|
101
|
+
<Field key={i} name={p.name} type={p.type} required={p.required}>
|
|
102
|
+
{p.description}
|
|
103
|
+
{p.enum && <EnumHint values={p.enum} />}
|
|
104
|
+
</Field>
|
|
105
|
+
))}
|
|
106
|
+
</section>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
|
|
110
|
+
{/* Request body */}
|
|
111
|
+
{operation.body?.fields?.length > 0 && (
|
|
112
|
+
<section className="velu-api-page__section">
|
|
113
|
+
<h2>Body</h2>
|
|
114
|
+
{operation.body.fields.map((f, i) => (
|
|
115
|
+
<Field key={i} name={f.name} type={f.type} required={f.required}>
|
|
116
|
+
{f.description}
|
|
117
|
+
{f.enum && <EnumHint values={f.enum} />}
|
|
118
|
+
</Field>
|
|
119
|
+
))}
|
|
120
|
+
</section>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{/* Responses */}
|
|
124
|
+
{operation.responses?.length > 0 && (
|
|
125
|
+
<section className="velu-api-page__section">
|
|
126
|
+
<h2>Response</h2>
|
|
127
|
+
<ResponseSections responses={operation.responses} />
|
|
128
|
+
</section>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Try-It playground — rendered as an overlay above the page, with a
|
|
132
|
+
blurred backdrop, rather than inline. Portaled to <body> so the
|
|
133
|
+
docs layout's @container containment can't clip the fixed overlay. */}
|
|
134
|
+
{open && typeof document !== 'undefined' &&
|
|
135
|
+
createPortal(
|
|
136
|
+
<div
|
|
137
|
+
className="velu-api-modal"
|
|
138
|
+
role="presentation"
|
|
139
|
+
onClick={() => setOpen(false)}
|
|
140
|
+
>
|
|
141
|
+
<div
|
|
142
|
+
className="velu-api-modal__panel"
|
|
143
|
+
role="dialog"
|
|
144
|
+
aria-modal="true"
|
|
145
|
+
aria-label={`${operation.title} playground`}
|
|
146
|
+
onClick={(e) => e.stopPropagation()}
|
|
147
|
+
>
|
|
148
|
+
<Playground
|
|
149
|
+
key={current.operation.id}
|
|
150
|
+
operation={current.operation}
|
|
151
|
+
samples={current.samples}
|
|
152
|
+
operations={ops}
|
|
153
|
+
activeId={activeId}
|
|
154
|
+
onSelectOperation={setActiveId}
|
|
155
|
+
server={server}
|
|
156
|
+
proxy={currentApi.proxy !== false}
|
|
157
|
+
onClose={() => setOpen(false)}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
</div>,
|
|
161
|
+
document.body,
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function EnumHint({ values }) {
|
|
168
|
+
return (
|
|
169
|
+
<p className="velu-api-page__enum">
|
|
170
|
+
Allowed values:{' '}
|
|
171
|
+
{values.map((v, i) => (
|
|
172
|
+
<code key={i}>{String(v)}</code>
|
|
173
|
+
))}
|
|
174
|
+
</p>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ResponseSections({ responses }) {
|
|
179
|
+
return (
|
|
180
|
+
<AccordionGroup className="velu-api-resp">
|
|
181
|
+
{responses.map((r, i) => {
|
|
182
|
+
return (
|
|
183
|
+
<Accordion
|
|
184
|
+
key={i}
|
|
185
|
+
defaultOpen
|
|
186
|
+
title={
|
|
187
|
+
<span className="velu-api-resp__head">
|
|
188
|
+
<span className="velu-api-resp__status">{r.status}</span>
|
|
189
|
+
{r.contentType && (
|
|
190
|
+
<span className="velu-api-resp__ctype">{r.contentType}</span>
|
|
191
|
+
)}
|
|
192
|
+
</span>
|
|
193
|
+
}
|
|
194
|
+
>
|
|
195
|
+
{r.description && <p className="velu-api-resp__desc">{r.description}</p>}
|
|
196
|
+
{r.fields?.map((f, j) => (
|
|
197
|
+
<Field key={j} name={f.name} type={f.type} required={f.required}>
|
|
198
|
+
{f.description}
|
|
199
|
+
</Field>
|
|
200
|
+
))}
|
|
201
|
+
</Accordion>
|
|
202
|
+
);
|
|
203
|
+
})}
|
|
204
|
+
</AccordionGroup>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── The interactive playground ──────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
function Playground({
|
|
211
|
+
operation,
|
|
212
|
+
samples = [],
|
|
213
|
+
operations = [],
|
|
214
|
+
activeId,
|
|
215
|
+
onSelectOperation,
|
|
216
|
+
server,
|
|
217
|
+
proxy,
|
|
218
|
+
onClose,
|
|
219
|
+
}) {
|
|
220
|
+
const [values, setValues] = useState({});
|
|
221
|
+
const [bodyText, setBodyText] = useState(
|
|
222
|
+
operation.body ? JSON.stringify(operation.body.example ?? {}, null, 2) : '',
|
|
223
|
+
);
|
|
224
|
+
const [busy, setBusy] = useState(false);
|
|
225
|
+
const [resp, setResp] = useState(null);
|
|
226
|
+
|
|
227
|
+
const set = (loc, name, v) =>
|
|
228
|
+
setValues((prev) => ({ ...prev, [loc]: { ...(prev[loc] || {}), [name]: v } }));
|
|
229
|
+
|
|
230
|
+
const onSend = useCallback(async () => {
|
|
231
|
+
setBusy(true);
|
|
232
|
+
setResp(null);
|
|
233
|
+
try {
|
|
234
|
+
const result = await sendApiRequest({ operation, server, proxy, values, bodyText });
|
|
235
|
+
setResp(result);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
setResp({ error: String(err?.message || err) });
|
|
238
|
+
} finally {
|
|
239
|
+
setBusy(false);
|
|
240
|
+
}
|
|
241
|
+
}, [operation, server, proxy, values, bodyText]);
|
|
242
|
+
|
|
243
|
+
const sections = [];
|
|
244
|
+
if (operation.auth?.length) sections.push(['auth', 'Authorization', operation.auth]);
|
|
245
|
+
for (const loc of ['path', 'query', 'header']) {
|
|
246
|
+
const rows = operation.parameters?.[loc] || [];
|
|
247
|
+
if (rows.length) sections.push([loc, LOCATION_LABEL[loc].replace(' Parameters', ''), rows]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<ApiClient
|
|
252
|
+
method={operation.method}
|
|
253
|
+
label={operation.title}
|
|
254
|
+
path={operation.path}
|
|
255
|
+
description={operation.description}
|
|
256
|
+
operations={operations}
|
|
257
|
+
activeId={activeId}
|
|
258
|
+
onSelectOperation={onSelectOperation}
|
|
259
|
+
onSend={onSend}
|
|
260
|
+
sending={busy}
|
|
261
|
+
onClose={onClose}
|
|
262
|
+
response={resp ? <ResponsePanel resp={resp} /> : null}
|
|
263
|
+
aside={<ApiSamples samples={samples} responses={operation.responses} />}
|
|
264
|
+
>
|
|
265
|
+
<AccordionGroup>
|
|
266
|
+
{sections.map(([loc, title, rows]) => (
|
|
267
|
+
<Accordion key={loc} title={title} defaultOpen>
|
|
268
|
+
{rows.map((f, i) => (
|
|
269
|
+
<ApiField
|
|
270
|
+
key={i}
|
|
271
|
+
name={f.name}
|
|
272
|
+
type={f.type}
|
|
273
|
+
required={f.required}
|
|
274
|
+
prefix={f.prefix}
|
|
275
|
+
value={values[loc]?.[f.name] ?? ''}
|
|
276
|
+
onChange={(e) => set(loc, f.name, e.target.value)}
|
|
277
|
+
>
|
|
278
|
+
{f.description}
|
|
279
|
+
</ApiField>
|
|
280
|
+
))}
|
|
281
|
+
</Accordion>
|
|
282
|
+
))}
|
|
283
|
+
{operation.body && (
|
|
284
|
+
<Accordion title="Body" defaultOpen>
|
|
285
|
+
<textarea
|
|
286
|
+
className="velu-api-body-input"
|
|
287
|
+
spellCheck={false}
|
|
288
|
+
rows={Math.min(14, bodyText.split('\n').length + 1)}
|
|
289
|
+
value={bodyText}
|
|
290
|
+
onChange={(e) => setBodyText(e.target.value)}
|
|
291
|
+
/>
|
|
292
|
+
</Accordion>
|
|
293
|
+
)}
|
|
294
|
+
</AccordionGroup>
|
|
295
|
+
</ApiClient>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* ResponsePanel — the live-response viewer: a Body / Header tabbed card.
|
|
301
|
+
* - Body: a status-colored Callout (green for 2xx, red otherwise) plus the
|
|
302
|
+
* response body.
|
|
303
|
+
* - Header: the response headers as a key/value table.
|
|
304
|
+
*/
|
|
305
|
+
function ResponsePanel({ resp }) {
|
|
306
|
+
const [tab, setTab] = useState('body');
|
|
307
|
+
if (!resp) return null;
|
|
308
|
+
|
|
309
|
+
const isError = resp.error != null;
|
|
310
|
+
const ok = !isError && resp.status >= 200 && resp.status < 300;
|
|
311
|
+
const statusLabel = isError
|
|
312
|
+
? 'Request failed'
|
|
313
|
+
: `${resp.status}${resp.statusText ? ` - ${resp.statusText}` : ''}`;
|
|
314
|
+
const bodyStr = isError
|
|
315
|
+
? resp.error
|
|
316
|
+
: typeof resp.body === 'string'
|
|
317
|
+
? resp.body
|
|
318
|
+
: JSON.stringify(resp.body, null, 2);
|
|
319
|
+
const headerRows = Object.entries(resp.headers || {});
|
|
320
|
+
|
|
321
|
+
// Reuse the CodeGroup's tab chrome (velu-code-block__*) so the Body/Header
|
|
322
|
+
// switcher looks identical to the code-sample tabs.
|
|
323
|
+
const tabCls = (name) =>
|
|
324
|
+
`velu-code-block__tab${tab === name ? ' velu-code-block__tab--active' : ''}`;
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div className="velu-code-block velu-api-resp-panel">
|
|
328
|
+
<div className="velu-code-block__header">
|
|
329
|
+
<div className="velu-code-block__tabs" role="tablist">
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
role="tab"
|
|
333
|
+
aria-selected={tab === 'body'}
|
|
334
|
+
className={tabCls('body')}
|
|
335
|
+
onClick={() => setTab('body')}
|
|
336
|
+
>
|
|
337
|
+
<span className="velu-code-block__tab-label">Body</span>
|
|
338
|
+
</button>
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
role="tab"
|
|
342
|
+
aria-selected={tab === 'header'}
|
|
343
|
+
className={tabCls('header')}
|
|
344
|
+
onClick={() => setTab('header')}
|
|
345
|
+
disabled={!headerRows.length || undefined}
|
|
346
|
+
>
|
|
347
|
+
<span className="velu-code-block__tab-label">Header</span>
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div className="velu-api-resp-panel__content">
|
|
353
|
+
{tab === 'body' ? (
|
|
354
|
+
<>
|
|
355
|
+
<Callout
|
|
356
|
+
type={ok ? 'check' : 'danger'}
|
|
357
|
+
className="velu-api-resp-panel__status"
|
|
358
|
+
>
|
|
359
|
+
{statusLabel}
|
|
360
|
+
</Callout>
|
|
361
|
+
{!isError && (
|
|
362
|
+
<div className="velu-api-resp-body__scroll velu-hide-scrollbar">
|
|
363
|
+
<CodeBlock language="json">{bodyStr || '(empty)'}</CodeBlock>
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
</>
|
|
367
|
+
) : (
|
|
368
|
+
<div className="velu-api-resp-headers__scroll velu-hide-scrollbar">
|
|
369
|
+
<table className="velu-api-resp-headers">
|
|
370
|
+
<tbody>
|
|
371
|
+
{headerRows.map(([k, val]) => (
|
|
372
|
+
<tr key={k}>
|
|
373
|
+
<td className="velu-api-resp-headers__key">{k}</td>
|
|
374
|
+
<td className="velu-api-resp-headers__val">{val}</td>
|
|
375
|
+
</tr>
|
|
376
|
+
))}
|
|
377
|
+
</tbody>
|
|
378
|
+
</table>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|