@veluai/velu 0.2.25 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +44 -44
  3. package/package.json +64 -64
  4. package/runtime/velu-ui/base.css +320 -320
  5. package/runtime/velu-ui/components/Accordion.jsx +64 -64
  6. package/runtime/velu-ui/components/ApiClient.jsx +207 -207
  7. package/runtime/velu-ui/components/ApiField.jsx +87 -87
  8. package/runtime/velu-ui/components/ApiPath.jsx +63 -63
  9. package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
  10. package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
  11. package/runtime/velu-ui/components/ApiSidebar.jsx +125 -125
  12. package/runtime/velu-ui/components/AskBar.jsx +71 -71
  13. package/runtime/velu-ui/components/Callout.jsx +114 -114
  14. package/runtime/velu-ui/components/Card.jsx +131 -131
  15. package/runtime/velu-ui/components/ChangelogFilters.jsx +75 -75
  16. package/runtime/velu-ui/components/Chatbot.jsx +972 -972
  17. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  18. package/runtime/velu-ui/components/Columns.jsx +56 -56
  19. package/runtime/velu-ui/components/ContextMenu.jsx +327 -327
  20. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  21. package/runtime/velu-ui/components/Field.jsx +81 -81
  22. package/runtime/velu-ui/components/Image.jsx +163 -163
  23. package/runtime/velu-ui/components/Logo.jsx +31 -31
  24. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  25. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  26. package/runtime/velu-ui/components/NotFound.jsx +63 -63
  27. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  28. package/runtime/velu-ui/components/PageFooter.jsx +145 -145
  29. package/runtime/velu-ui/components/PageHeader.jsx +422 -422
  30. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  31. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  32. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  33. package/runtime/velu-ui/components/Search.jsx +460 -460
  34. package/runtime/velu-ui/components/Sidebar.jsx +246 -246
  35. package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
  36. package/runtime/velu-ui/components/Steps.jsx +65 -65
  37. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  38. package/runtime/velu-ui/components/Toc.jsx +537 -537
  39. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  40. package/runtime/velu-ui/components/Tree.jsx +87 -87
  41. package/runtime/velu-ui/components/TryItBar.jsx +102 -102
  42. package/runtime/velu-ui/components/Update.jsx +92 -92
  43. package/runtime/velu-ui/components/accordion.css +92 -92
  44. package/runtime/velu-ui/components/api-page.css +208 -208
  45. package/runtime/velu-ui/components/api.css +635 -635
  46. package/runtime/velu-ui/components/ask-bar.css +103 -103
  47. package/runtime/velu-ui/components/card.css +105 -105
  48. package/runtime/velu-ui/components/changelog-filters.css +43 -43
  49. package/runtime/velu-ui/components/chatbot.css +687 -687
  50. package/runtime/velu-ui/components/code-block.css +263 -263
  51. package/runtime/velu-ui/components/context-menu.css +199 -199
  52. package/runtime/velu-ui/components/docs-layout.css +822 -822
  53. package/runtime/velu-ui/components/field.css +82 -82
  54. package/runtime/velu-ui/components/image.css +237 -237
  55. package/runtime/velu-ui/components/nav-select.css +157 -157
  56. package/runtime/velu-ui/components/not-found.css +94 -94
  57. package/runtime/velu-ui/components/page-feedback.css +261 -261
  58. package/runtime/velu-ui/components/page-footer.css +130 -130
  59. package/runtime/velu-ui/components/page-header.css +558 -558
  60. package/runtime/velu-ui/components/page-nav.css +50 -50
  61. package/runtime/velu-ui/components/powered-by.css +92 -92
  62. package/runtime/velu-ui/components/prompt.css +99 -99
  63. package/runtime/velu-ui/components/search.css +307 -307
  64. package/runtime/velu-ui/components/sidebar.css +205 -205
  65. package/runtime/velu-ui/components/steps.css +77 -77
  66. package/runtime/velu-ui/components/theme-toggle.css +102 -102
  67. package/runtime/velu-ui/components/toc-bar.css +234 -234
  68. package/runtime/velu-ui/components/tree.css +49 -49
  69. package/runtime/velu-ui/components/update.css +111 -111
  70. package/runtime/velu-ui/index.js +58 -58
  71. package/runtime/velu-ui/lib/api-send.js +118 -118
  72. package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
  73. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  74. package/runtime/velu-ui/lib/copyText.js +64 -64
  75. package/runtime/velu-ui/lib/docs-assistant.js +273 -273
  76. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  77. package/runtime/velu-ui/lib/page-feedback.js +72 -72
  78. package/runtime/velu-ui/lib/pagefind.js +116 -116
  79. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  80. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  81. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  82. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  83. package/runtime/velu-ui/mdx-components.jsx +107 -107
  84. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  85. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  86. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  87. package/runtime/velu-ui/primitives/stack.css +3 -3
  88. package/runtime/velu-ui/primitives/switcher.css +25 -25
  89. package/runtime/velu-ui/styles.css +48 -48
  90. package/runtime/velu-ui/tokens.css +4 -4
  91. package/schema/velu.schema.json +419 -419
  92. package/src/lib/extract-mdx-error.js +170 -170
  93. package/src/lib/issues.js +159 -159
  94. package/src/lib/known-components.js +35 -35
  95. package/src/navigation.js +459 -459
  96. package/src/runtime/App.jsx +1754 -1754
  97. package/src/runtime/ErrorBoundary.jsx +54 -54
  98. package/src/runtime/client-entry.jsx +27 -27
  99. package/src/runtime/server-entry.jsx +24 -24
  100. package/src/template.html +48 -48
  101. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  102. package/templates/starter/ai-tools/cursor.mdx +17 -17
  103. package/templates/starter/api-reference/introduction.mdx +43 -43
  104. package/templates/starter/development.mdx +19 -19
  105. package/templates/starter/essentials/code.mdx +29 -29
  106. package/templates/starter/essentials/images.mdx +29 -29
  107. package/templates/starter/essentials/markdown.mdx +25 -25
  108. package/templates/starter/essentials/navigation.mdx +39 -39
  109. package/templates/starter/essentials/settings.mdx +30 -30
  110. package/templates/starter/favicon.svg +6 -6
  111. package/templates/starter/index.mdx +31 -31
  112. package/templates/starter/openapi.json +160 -160
  113. package/templates/starter/quickstart.mdx +31 -31
  114. package/templates/starter/velu.json +40 -40
@@ -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
+ }