@veluai/velu 0.1.15 → 0.1.16
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/dist/cli.js +44 -23
- package/package.json +2 -1
- package/runtime/velu-ui/components/ApiClient.jsx +84 -8
- package/runtime/velu-ui/components/ApiReferencePage.jsx +388 -0
- package/runtime/velu-ui/components/ApiSamples.jsx +36 -0
- package/runtime/velu-ui/components/Sidebar.jsx +10 -2
- package/runtime/velu-ui/components/TryItBar.jsx +15 -3
- package/runtime/velu-ui/components/api-page.css +215 -0
- package/runtime/velu-ui/components/api.css +117 -1
- package/runtime/velu-ui/index.js +2 -0
- package/runtime/velu-ui/lib/api-send.js +92 -0
- package/runtime/velu-ui/styles.css +1 -0
- package/schema/velu.schema.json +46 -0
- package/src/navigation.js +11 -2
- package/src/runtime/App.jsx +33 -6
- package/templates/starter/api-reference/introduction.mdx +29 -14
- package/templates/starter/openapi.json +160 -0
- package/templates/starter/velu.json +6 -2
- package/templates/starter/api-reference/endpoint/create.mdx +0 -24
- package/templates/starter/api-reference/endpoint/get.mdx +0 -27
|
@@ -175,9 +175,11 @@
|
|
|
175
175
|
color: var(--text-color);
|
|
176
176
|
}
|
|
177
177
|
.velu-try-it__cta {
|
|
178
|
+
position: relative;
|
|
179
|
+
overflow: hidden;
|
|
178
180
|
display: inline-flex;
|
|
179
181
|
align-items: center;
|
|
180
|
-
|
|
182
|
+
justify-content: center;
|
|
181
183
|
padding-block: var(--s-3);
|
|
182
184
|
padding-inline: var(--s0);
|
|
183
185
|
background: var(--method-cta, var(--accent-color));
|
|
@@ -194,6 +196,56 @@
|
|
|
194
196
|
.velu-try-it__cta:hover {
|
|
195
197
|
background: color-mix(in srgb, var(--method-cta, var(--accent-color)) 88%, #000);
|
|
196
198
|
}
|
|
199
|
+
.velu-try-it__cta:disabled {
|
|
200
|
+
cursor: default;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Label (text + chevron) and the loading spinner are two stacked layers
|
|
204
|
+
that swap with a vertical slide: when loading, the label slides DOWN and
|
|
205
|
+
out the bottom while the spinner slides IN from the top. overflow:hidden
|
|
206
|
+
on the button clips the off-screen states. The label stays in normal flow
|
|
207
|
+
so it fixes the button's width; the spinner overlays it. */
|
|
208
|
+
.velu-try-it__cta-label {
|
|
209
|
+
display: inline-flex;
|
|
210
|
+
align-items: center;
|
|
211
|
+
gap: var(--s-2);
|
|
212
|
+
transition: transform 0.22s ease, opacity 0.18s ease;
|
|
213
|
+
}
|
|
214
|
+
.velu-try-it__cta-spinner {
|
|
215
|
+
position: absolute;
|
|
216
|
+
inset: 0;
|
|
217
|
+
display: inline-flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
transform: translateY(-110%);
|
|
221
|
+
opacity: 0;
|
|
222
|
+
transition: transform 0.22s ease, opacity 0.18s ease;
|
|
223
|
+
}
|
|
224
|
+
.velu-try-it__cta[data-loading='true'] .velu-try-it__cta-label {
|
|
225
|
+
transform: translateY(120%);
|
|
226
|
+
opacity: 0;
|
|
227
|
+
}
|
|
228
|
+
.velu-try-it__cta[data-loading='true'] .velu-try-it__cta-spinner {
|
|
229
|
+
transform: translateY(0);
|
|
230
|
+
opacity: 1;
|
|
231
|
+
}
|
|
232
|
+
.velu-try-it__cta-spinner svg {
|
|
233
|
+
animation: velu-cta-spin 0.7s linear infinite;
|
|
234
|
+
}
|
|
235
|
+
@keyframes velu-cta-spin {
|
|
236
|
+
to {
|
|
237
|
+
transform: rotate(360deg);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
@media (prefers-reduced-motion: reduce) {
|
|
241
|
+
.velu-try-it__cta-label,
|
|
242
|
+
.velu-try-it__cta-spinner {
|
|
243
|
+
transition-duration: 0.01ms;
|
|
244
|
+
}
|
|
245
|
+
.velu-try-it__cta-spinner svg {
|
|
246
|
+
animation-duration: 1.4s;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
197
249
|
.velu-try-it--get { --method-cta: var(--get-cta-color); }
|
|
198
250
|
.velu-try-it--post { --method-cta: var(--post-cta-color); }
|
|
199
251
|
.velu-try-it--put { --method-cta: var(--put-cta-color); }
|
|
@@ -262,6 +314,7 @@
|
|
|
262
314
|
AND natural height; the Cluster's align: flex-start keeps it from
|
|
263
315
|
stretching to the taller TryItBar's height). */
|
|
264
316
|
.velu-api-client__op {
|
|
317
|
+
position: relative;
|
|
265
318
|
display: flex;
|
|
266
319
|
align-items: center;
|
|
267
320
|
gap: var(--s-2);
|
|
@@ -274,6 +327,20 @@
|
|
|
274
327
|
line-height: var(--lh-h6);
|
|
275
328
|
flex: none;
|
|
276
329
|
}
|
|
330
|
+
/* The clickable trigger when the operation is switchable — a bare button
|
|
331
|
+
that keeps the same layout as the static row. */
|
|
332
|
+
.velu-api-client__op-btn {
|
|
333
|
+
display: inline-flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
gap: var(--s-2);
|
|
336
|
+
margin: 0;
|
|
337
|
+
padding: 0;
|
|
338
|
+
background: transparent;
|
|
339
|
+
border: 0;
|
|
340
|
+
color: inherit;
|
|
341
|
+
font: inherit;
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
}
|
|
277
344
|
.velu-api-client__op-label {
|
|
278
345
|
font-weight: var(--weight-medium);
|
|
279
346
|
}
|
|
@@ -281,6 +348,55 @@
|
|
|
281
348
|
display: inline-flex;
|
|
282
349
|
align-items: center;
|
|
283
350
|
color: var(--muted-color);
|
|
351
|
+
transition: transform 0.15s ease;
|
|
352
|
+
}
|
|
353
|
+
.velu-api-client__op-btn[aria-expanded='true'] .velu-api-client__op-chevron {
|
|
354
|
+
transform: rotate(180deg);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Operation switcher menu — lists every operation in the tab. */
|
|
358
|
+
.velu-api-client__op-menu {
|
|
359
|
+
position: absolute;
|
|
360
|
+
inset-block-start: calc(100% + var(--s-3));
|
|
361
|
+
inset-inline-start: 0;
|
|
362
|
+
z-index: 5;
|
|
363
|
+
display: flex;
|
|
364
|
+
flex-direction: column;
|
|
365
|
+
gap: 2px;
|
|
366
|
+
margin: 0;
|
|
367
|
+
padding: var(--s-3);
|
|
368
|
+
list-style: none;
|
|
369
|
+
min-inline-size: 16rem;
|
|
370
|
+
max-block-size: 18rem;
|
|
371
|
+
overflow-y: auto;
|
|
372
|
+
background: var(--page-bg);
|
|
373
|
+
border: var(--border-width) solid var(--border-color);
|
|
374
|
+
border-radius: var(--radius-md);
|
|
375
|
+
box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.4);
|
|
376
|
+
}
|
|
377
|
+
.velu-api-client__op-item {
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: var(--s-2);
|
|
381
|
+
inline-size: 100%;
|
|
382
|
+
padding: var(--s-3) var(--s-2);
|
|
383
|
+
background: transparent;
|
|
384
|
+
border: 0;
|
|
385
|
+
border-radius: var(--radius-sm);
|
|
386
|
+
color: var(--text-color);
|
|
387
|
+
font: inherit;
|
|
388
|
+
font-size: var(--f-h6);
|
|
389
|
+
text-align: start;
|
|
390
|
+
cursor: pointer;
|
|
391
|
+
}
|
|
392
|
+
.velu-api-client__op-item:hover,
|
|
393
|
+
.velu-api-client__op-item--active {
|
|
394
|
+
background: var(--surface-color);
|
|
395
|
+
}
|
|
396
|
+
.velu-api-client__op-item-label {
|
|
397
|
+
white-space: nowrap;
|
|
398
|
+
overflow: hidden;
|
|
399
|
+
text-overflow: ellipsis;
|
|
284
400
|
}
|
|
285
401
|
|
|
286
402
|
/* Inside ApiClient the outer frame already supplies the box — strip
|
package/runtime/velu-ui/index.js
CHANGED
|
@@ -26,6 +26,8 @@ export { default as TryItBar } from './components/TryItBar.jsx';
|
|
|
26
26
|
export { default as ApiField } from './components/ApiField.jsx';
|
|
27
27
|
export { default as ApiClient } from './components/ApiClient.jsx';
|
|
28
28
|
export { default as ApiSidebar } from './components/ApiSidebar.jsx';
|
|
29
|
+
export { default as ApiReferencePage } from './components/ApiReferencePage.jsx';
|
|
30
|
+
export { default as ApiSamples } from './components/ApiSamples.jsx';
|
|
29
31
|
export { default as AskBar } from './components/AskBar.jsx';
|
|
30
32
|
export { default as Chatbot } from './components/Chatbot.jsx';
|
|
31
33
|
export { default as PageFeedback } from './components/PageFeedback.jsx';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Build and send a "Try It" request from the playground field values.
|
|
2
|
+
//
|
|
3
|
+
// By default the request goes through the dev server's proxy
|
|
4
|
+
// (/@velu-api-proxy) so it isn't blocked by CORS — the proxy forwards it
|
|
5
|
+
// server-side and returns { status, statusText, headers, body }. Set the
|
|
6
|
+
// project's api.playground.proxy to false to send straight from the browser.
|
|
7
|
+
|
|
8
|
+
const PROXY_URL = '/@velu-api-proxy';
|
|
9
|
+
const BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
10
|
+
|
|
11
|
+
// Compose the concrete request (url + headers + body) from the operation and
|
|
12
|
+
// the user-entered values: { auth, path, query, header } maps + bodyText.
|
|
13
|
+
function buildRequest({ operation, server, values, bodyText }) {
|
|
14
|
+
const v = values || {};
|
|
15
|
+
let path = operation.path.replace(/\{([^}]+)\}/g, (m, name) => {
|
|
16
|
+
const val = v.path?.[name];
|
|
17
|
+
return val ? encodeURIComponent(val) : m; // leave {name} if unfilled
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const query = Object.entries(v.query || {}).filter(([, val]) => val !== '' && val != null);
|
|
21
|
+
const qs = query.map(([k, val]) => `${encodeURIComponent(k)}=${encodeURIComponent(val)}`).join('&');
|
|
22
|
+
const base = (server || '').replace(/\/+$/, '');
|
|
23
|
+
const url = `${base}${path}${qs ? `?${qs}` : ''}`;
|
|
24
|
+
|
|
25
|
+
const headers = {};
|
|
26
|
+
for (const a of operation.auth || []) {
|
|
27
|
+
const val = v.auth?.[a.name];
|
|
28
|
+
if (val) headers[a.name] = a.prefix ? `${a.prefix} ${val}` : val;
|
|
29
|
+
}
|
|
30
|
+
for (const [k, val] of Object.entries(v.header || {})) {
|
|
31
|
+
if (val !== '' && val != null) headers[k] = val;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let body = null;
|
|
35
|
+
if (operation.body && BODY_METHODS.has(operation.method)) {
|
|
36
|
+
body = bodyText || '';
|
|
37
|
+
if (body && !headers['Content-Type']) headers['Content-Type'] = operation.body.contentType || 'application/json';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { method: operation.method, url, headers, body };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readBody(res) {
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
const ct = res.headers.get('content-type') || '';
|
|
46
|
+
if (/json/i.test(ct)) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(text);
|
|
49
|
+
} catch {
|
|
50
|
+
/* fall through to raw text */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function sendApiRequest({ operation, server, proxy = true, values, bodyText }) {
|
|
57
|
+
const req = buildRequest({ operation, server, values, bodyText });
|
|
58
|
+
|
|
59
|
+
if (proxy) {
|
|
60
|
+
const res = await fetch(PROXY_URL, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(req),
|
|
64
|
+
});
|
|
65
|
+
// The proxy returns the upstream result as JSON.
|
|
66
|
+
const payload = await res.json().catch(() => ({}));
|
|
67
|
+
if (payload && payload.error) return { error: payload.error };
|
|
68
|
+
return {
|
|
69
|
+
status: payload.status,
|
|
70
|
+
statusText: payload.statusText || '',
|
|
71
|
+
headers: payload.headers || {},
|
|
72
|
+
body: payload.body,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Direct browser fetch (subject to CORS).
|
|
77
|
+
const res = await fetch(req.url, {
|
|
78
|
+
method: req.method,
|
|
79
|
+
headers: req.headers,
|
|
80
|
+
body: req.body || undefined,
|
|
81
|
+
});
|
|
82
|
+
const headers = {};
|
|
83
|
+
res.headers.forEach((val, key) => {
|
|
84
|
+
headers[key] = val;
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
status: res.status,
|
|
88
|
+
statusText: res.statusText,
|
|
89
|
+
headers,
|
|
90
|
+
body: await readBody(res),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
@import './components/steps.css';
|
|
31
31
|
@import './components/tree.css';
|
|
32
32
|
@import './components/api.css';
|
|
33
|
+
@import './components/api-page.css';
|
|
33
34
|
@import './components/ask-bar.css';
|
|
34
35
|
@import './components/chatbot.css';
|
|
35
36
|
@import './components/page-feedback.css';
|
package/schema/velu.schema.json
CHANGED
|
@@ -40,6 +40,43 @@
|
|
|
40
40
|
"type": "string",
|
|
41
41
|
"description": "Path to the favicon, relative to the project root."
|
|
42
42
|
},
|
|
43
|
+
"api": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"description": "API reference behaviour for OpenAPI-generated pages.",
|
|
47
|
+
"properties": {
|
|
48
|
+
"server": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Base URL override for the playground + code samples (otherwise the spec's first server is used)."
|
|
51
|
+
},
|
|
52
|
+
"playground": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"properties": {
|
|
56
|
+
"display": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["interactive", "simple", "none"],
|
|
59
|
+
"description": "Playground mode. \"none\" disables the proxy / live send."
|
|
60
|
+
},
|
|
61
|
+
"proxy": {
|
|
62
|
+
"type": "boolean",
|
|
63
|
+
"description": "Send Try-It requests through the dev proxy to avoid CORS (default true)."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"examples": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"properties": {
|
|
71
|
+
"languages": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": { "type": "string", "enum": ["curl", "javascript", "python", "ruby"] },
|
|
74
|
+
"description": "Which request-snippet languages to generate."
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
43
80
|
"logo": {
|
|
44
81
|
"description": "Site logo shown in the header, replacing the name wordmark. A single path used in both themes, or per-theme light/dark images with an optional click-through href. Paths are relative to the project root.",
|
|
45
82
|
"oneOf": [
|
|
@@ -205,6 +242,7 @@
|
|
|
205
242
|
"icon": { "type": "string", "description": "lucide icon id." },
|
|
206
243
|
"expanded": { "type": "boolean", "description": "Start expanded." },
|
|
207
244
|
"root": { "type": "string", "description": "Landing page path for the group." },
|
|
245
|
+
"openapi": { "$ref": "#/$defs/openapiRef" },
|
|
208
246
|
"pages": { "$ref": "#/$defs/pages" }
|
|
209
247
|
}
|
|
210
248
|
},
|
|
@@ -216,11 +254,19 @@
|
|
|
216
254
|
"tab": { "type": "string", "description": "Tab label (top nav)." },
|
|
217
255
|
"icon": { "type": "string" },
|
|
218
256
|
"href": { "type": "string", "description": "External/override link instead of in-site pages." },
|
|
257
|
+
"openapi": { "$ref": "#/$defs/openapiRef" },
|
|
219
258
|
"anchors": { "type": "array", "items": { "$ref": "#/$defs/anchor" } },
|
|
220
259
|
"groups": { "type": "array", "items": { "$ref": "#/$defs/group" } },
|
|
221
260
|
"pages": { "$ref": "#/$defs/pages" }
|
|
222
261
|
}
|
|
223
262
|
},
|
|
263
|
+
"openapiRef": {
|
|
264
|
+
"description": "Path or URL to an OpenAPI spec (JSON/YAML), or an array of them. Auto-generates a page per operation, grouped by tag.",
|
|
265
|
+
"oneOf": [
|
|
266
|
+
{ "type": "string" },
|
|
267
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
268
|
+
]
|
|
269
|
+
},
|
|
224
270
|
"anchor": {
|
|
225
271
|
"type": "object",
|
|
226
272
|
"additionalProperties": false,
|
package/src/navigation.js
CHANGED
|
@@ -186,9 +186,13 @@ function normalizeGroup(g) {
|
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
/** A `pages[]` entry is
|
|
189
|
+
/** A `pages[]` entry is a string (page path), a generated OpenAPI page
|
|
190
|
+
* ({ apiPage, label, method }), or a nested group. */
|
|
190
191
|
function normalizePageEntry(entry) {
|
|
191
192
|
if (typeof entry === 'string') return { kind: 'page', pagePath: entry };
|
|
193
|
+
if (entry && entry.apiPage) {
|
|
194
|
+
return { kind: 'page', pagePath: entry.apiPage, label: entry.label, method: entry.method, api: true };
|
|
195
|
+
}
|
|
192
196
|
return normalizeGroup(entry);
|
|
193
197
|
}
|
|
194
198
|
|
|
@@ -430,5 +434,10 @@ function itemsFor(nodes, pagesMap, ctx) {
|
|
|
430
434
|
function itemFor(pageNode, pagesMap, ctx) {
|
|
431
435
|
const href = urlInCtx(pageNode.pagePath, ctx);
|
|
432
436
|
const fm = pagesMap[href]?.frontmatter;
|
|
433
|
-
|
|
437
|
+
// Generated API pages carry their own label + HTTP method (for the sidebar
|
|
438
|
+
// method badge); normal pages source their label from frontmatter.
|
|
439
|
+
const label = pageNode.label ?? fm?.title ?? titleCase(lastSeg(href));
|
|
440
|
+
const item = { label, href };
|
|
441
|
+
if (pageNode.method) item.method = pageNode.method;
|
|
442
|
+
return item;
|
|
434
443
|
}
|
package/src/runtime/App.jsx
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
ApiClient,
|
|
48
48
|
ApiField,
|
|
49
49
|
ApiSidebar,
|
|
50
|
+
ApiSamples,
|
|
50
51
|
VeluMark,
|
|
51
52
|
} from 'velu-ui';
|
|
52
53
|
import { X, ChevronDown, ChevronUp } from 'lucide-react';
|
|
@@ -1057,6 +1058,7 @@ function DocsPage() {
|
|
|
1057
1058
|
data-chat-open={chatOpen ? 'true' : 'false'}
|
|
1058
1059
|
data-sidebar-open={sidebarOpen ? 'true' : 'false'}
|
|
1059
1060
|
data-drawer-open={drawerOpen ? 'true' : 'false'}
|
|
1061
|
+
data-api={entry?.api ? 'true' : 'false'}
|
|
1060
1062
|
>
|
|
1061
1063
|
{/* Scrim — visible at mobile while the drawer OR the chatbot
|
|
1062
1064
|
sheet is open. Sits between the article (z-0) and the
|
|
@@ -1263,11 +1265,27 @@ function DocsPage() {
|
|
|
1263
1265
|
scrollPaddingBlockEnd: 'var(--s1)',
|
|
1264
1266
|
}}
|
|
1265
1267
|
>
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1268
|
+
{entry?.api ? (
|
|
1269
|
+
<ApiSidebar
|
|
1270
|
+
sections={(nav?.sidebarSections ?? []).map((s) => ({
|
|
1271
|
+
title: s.title,
|
|
1272
|
+
icon: s.icon,
|
|
1273
|
+
endpoints: (s.items ?? []).map((it) => ({
|
|
1274
|
+
method: it.method,
|
|
1275
|
+
label: it.label,
|
|
1276
|
+
href: it.href,
|
|
1277
|
+
})),
|
|
1278
|
+
}))}
|
|
1279
|
+
activeHref={pathname}
|
|
1280
|
+
linkComponent={RouterLink}
|
|
1281
|
+
/>
|
|
1282
|
+
) : (
|
|
1283
|
+
<Sidebar
|
|
1284
|
+
sections={nav?.sidebarSections ?? []}
|
|
1285
|
+
activeHref={pathname}
|
|
1286
|
+
linkComponent={RouterLink}
|
|
1287
|
+
/>
|
|
1288
|
+
)}
|
|
1271
1289
|
</div>
|
|
1272
1290
|
<button
|
|
1273
1291
|
type="button"
|
|
@@ -1317,7 +1335,16 @@ function DocsPage() {
|
|
|
1317
1335
|
scrollPaddingBlockEnd: `calc(${footerOverlap}px + 2rem)`,
|
|
1318
1336
|
}}
|
|
1319
1337
|
>
|
|
1320
|
-
|
|
1338
|
+
{/* API reference pages put their code samples in the right rail
|
|
1339
|
+
(where the TOC sits for normal pages). */}
|
|
1340
|
+
{entry?.api && entry.operation ? (
|
|
1341
|
+
<ApiSamples
|
|
1342
|
+
samples={entry.samples}
|
|
1343
|
+
responses={entry.operation.responses}
|
|
1344
|
+
/>
|
|
1345
|
+
) : (
|
|
1346
|
+
<Toc items={toc} activeId={activeId} onSelect={scrollTo} />
|
|
1347
|
+
)}
|
|
1321
1348
|
</aside>
|
|
1322
1349
|
)}
|
|
1323
1350
|
|
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Introduction
|
|
3
|
-
description: How
|
|
3
|
+
description: How this API reference is generated.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
The endpoints in this tab are generated automatically from an OpenAPI
|
|
7
|
+
spec (`openapi.json`). Each operation becomes its own page — with
|
|
8
|
+
parameters, request body, and responses — and an interactive **Try It**
|
|
9
|
+
playground you can use to send real requests.
|
|
9
10
|
|
|
10
11
|
<Callout type="note">
|
|
11
12
|
This is a second tab ("API Reference"). Tabs let you keep guides and
|
|
12
|
-
reference docs in separate top-level sections of the same site.
|
|
13
|
+
reference docs in separate top-level sections of the same site. Point
|
|
14
|
+
the tab's `openapi` field at your own spec to replace this example.
|
|
13
15
|
</Callout>
|
|
14
16
|
|
|
15
|
-
##
|
|
17
|
+
## How it works
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
Add an `openapi` field to a tab (or group) in `velu.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"tab": "API Reference",
|
|
24
|
+
"openapi": "/openapi.json"
|
|
25
|
+
}
|
|
19
26
|
```
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
Velu reads the spec, groups the operations by their tag, and renders a
|
|
29
|
+
page per endpoint. No hand-written endpoint pages required.
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
## The example API
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
This starter points at a live demo API — a small **product catalog**
|
|
34
|
+
backed by [dummyjson.com](https://dummyjson.com). Open any endpoint in
|
|
35
|
+
the sidebar, hit **Try It**, and send a real request:
|
|
36
|
+
|
|
37
|
+
- **Get a product** — try `id` = `1`
|
|
38
|
+
- **Search products** — try `q` = `phone`
|
|
39
|
+
- **Add a product** — the body is pre-filled; Send returns a created product
|
|
40
|
+
|
|
41
|
+
Requests are routed through the dev server's proxy so they aren't blocked
|
|
42
|
+
by CORS. Set `api.playground.proxy` to `false` to send straight from the
|
|
43
|
+
browser.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Product Catalog API",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "A friendly, fully-hosted example API — a product catalog backed by the live https://dummyjson.com service. Every operation hits the real API: list, search, fetch, create, update and delete products and see real responses."
|
|
7
|
+
},
|
|
8
|
+
"servers": [{ "url": "https://dummyjson.com" }],
|
|
9
|
+
"tags": [{ "name": "Products", "description": "Browse and manage the product catalog." }],
|
|
10
|
+
"paths": {
|
|
11
|
+
"/products": {
|
|
12
|
+
"get": {
|
|
13
|
+
"operationId": "listProducts",
|
|
14
|
+
"summary": "List products",
|
|
15
|
+
"description": "List products, with optional pagination.",
|
|
16
|
+
"tags": ["Products"],
|
|
17
|
+
"parameters": [
|
|
18
|
+
{ "name": "limit", "in": "query", "schema": { "type": "integer" }, "description": "How many products to return (0 returns all)." },
|
|
19
|
+
{ "name": "skip", "in": "query", "schema": { "type": "integer" }, "description": "How many products to skip (for pagination)." }
|
|
20
|
+
],
|
|
21
|
+
"responses": {
|
|
22
|
+
"200": {
|
|
23
|
+
"description": "A page of products.",
|
|
24
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProductList" } } }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"/products/search": {
|
|
30
|
+
"get": {
|
|
31
|
+
"operationId": "searchProducts",
|
|
32
|
+
"summary": "Search products",
|
|
33
|
+
"description": "Search the catalog by a free-text query.",
|
|
34
|
+
"tags": ["Products"],
|
|
35
|
+
"parameters": [
|
|
36
|
+
{ "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The search query (try `phone`)." }
|
|
37
|
+
],
|
|
38
|
+
"responses": {
|
|
39
|
+
"200": {
|
|
40
|
+
"description": "Matching products.",
|
|
41
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProductList" } } }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"/products/{id}": {
|
|
47
|
+
"get": {
|
|
48
|
+
"operationId": "getProduct",
|
|
49
|
+
"summary": "Get a product",
|
|
50
|
+
"description": "Retrieve a single product by its id.",
|
|
51
|
+
"tags": ["Products"],
|
|
52
|
+
"parameters": [
|
|
53
|
+
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The product id (try `1`)." }
|
|
54
|
+
],
|
|
55
|
+
"responses": {
|
|
56
|
+
"200": {
|
|
57
|
+
"description": "The product.",
|
|
58
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Product" } } }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"put": {
|
|
63
|
+
"operationId": "updateProduct",
|
|
64
|
+
"summary": "Update a product",
|
|
65
|
+
"description": "Update fields on an existing product. (Simulated by the upstream API — the response reflects the change but nothing is persisted.)",
|
|
66
|
+
"tags": ["Products"],
|
|
67
|
+
"parameters": [
|
|
68
|
+
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The product id." }
|
|
69
|
+
],
|
|
70
|
+
"requestBody": {
|
|
71
|
+
"required": true,
|
|
72
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProductInput" } } }
|
|
73
|
+
},
|
|
74
|
+
"responses": {
|
|
75
|
+
"200": {
|
|
76
|
+
"description": "The updated product.",
|
|
77
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Product" } } }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"delete": {
|
|
82
|
+
"operationId": "deleteProduct",
|
|
83
|
+
"summary": "Delete a product",
|
|
84
|
+
"description": "Delete a product by its id. (Simulated — the response includes `isDeleted` and `deletedOn`.)",
|
|
85
|
+
"tags": ["Products"],
|
|
86
|
+
"parameters": [
|
|
87
|
+
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The product id." }
|
|
88
|
+
],
|
|
89
|
+
"responses": {
|
|
90
|
+
"200": {
|
|
91
|
+
"description": "The deleted product.",
|
|
92
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Product" } } }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"/products/add": {
|
|
98
|
+
"post": {
|
|
99
|
+
"operationId": "addProduct",
|
|
100
|
+
"summary": "Add a product",
|
|
101
|
+
"description": "Create a new product. (Simulated — the response echoes the body with a fresh `id`.)",
|
|
102
|
+
"tags": ["Products"],
|
|
103
|
+
"requestBody": {
|
|
104
|
+
"required": true,
|
|
105
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProductInput" } } }
|
|
106
|
+
},
|
|
107
|
+
"responses": {
|
|
108
|
+
"201": {
|
|
109
|
+
"description": "The created product.",
|
|
110
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Product" } } }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"components": {
|
|
117
|
+
"schemas": {
|
|
118
|
+
"Product": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"id": { "type": "integer", "description": "Unique identifier." },
|
|
122
|
+
"title": { "type": "string", "description": "Product name." },
|
|
123
|
+
"description": { "type": "string", "description": "Product description." },
|
|
124
|
+
"category": { "type": "string", "description": "Category slug." },
|
|
125
|
+
"price": { "type": "number", "description": "Price in USD." },
|
|
126
|
+
"discountPercentage": { "type": "number", "description": "Discount, as a percentage." },
|
|
127
|
+
"rating": { "type": "number", "description": "Average customer rating (0–5)." },
|
|
128
|
+
"stock": { "type": "integer", "description": "Units in stock." },
|
|
129
|
+
"brand": { "type": "string", "description": "Brand name." },
|
|
130
|
+
"thumbnail": { "type": "string", "description": "Thumbnail image URL." }
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"ProductList": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"products": { "type": "array", "description": "The products on this page.", "items": { "$ref": "#/components/schemas/Product" } },
|
|
137
|
+
"total": { "type": "integer", "description": "Total number of matching products." },
|
|
138
|
+
"skip": { "type": "integer", "description": "How many were skipped." },
|
|
139
|
+
"limit": { "type": "integer", "description": "How many were returned." }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"ProductInput": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"required": ["title"],
|
|
145
|
+
"properties": {
|
|
146
|
+
"title": { "type": "string", "description": "Product name." },
|
|
147
|
+
"description": { "type": "string", "description": "Product description." },
|
|
148
|
+
"category": { "type": "string", "description": "Category slug." },
|
|
149
|
+
"price": { "type": "number", "description": "Price in USD." }
|
|
150
|
+
},
|
|
151
|
+
"example": {
|
|
152
|
+
"title": "Velu Notebook",
|
|
153
|
+
"description": "A dotted-grid notebook for docs notes.",
|
|
154
|
+
"category": "stationery",
|
|
155
|
+
"price": 19.99
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -23,11 +23,15 @@
|
|
|
23
23
|
{
|
|
24
24
|
"tab": "API Reference",
|
|
25
25
|
"icon": "terminal",
|
|
26
|
+
"openapi": "/openapi.json",
|
|
26
27
|
"groups": [
|
|
27
|
-
{ "group": "API Documentation", "pages": ["api-reference/introduction"] }
|
|
28
|
-
{ "group": "Endpoint Examples", "pages": ["api-reference/endpoint/get", "api-reference/endpoint/create"] }
|
|
28
|
+
{ "group": "API Documentation", "pages": ["api-reference/introduction"] }
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
]
|
|
32
|
+
},
|
|
33
|
+
"api": {
|
|
34
|
+
"playground": { "proxy": true },
|
|
35
|
+
"examples": { "languages": ["curl", "javascript", "python", "ruby"] }
|
|
32
36
|
}
|
|
33
37
|
}
|