@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.
@@ -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
- gap: var(--s-2);
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
@@ -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';
@@ -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 either a string (page path) or a nested group. */
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
- return { label: fm?.title ?? titleCase(lastSeg(href)), href };
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
  }
@@ -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
- <Sidebar
1267
- sections={nav?.sidebarSections ?? []}
1268
- activeHref={pathname}
1269
- linkComponent={RouterLink}
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
- <Toc items={toc} activeId={activeId} onSelect={scrollTo} />
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 to read this API reference.
3
+ description: How this API reference is generated.
4
4
  ---
5
5
 
6
- This section documents the example API. Endpoints are grouped in the
7
- sidebar; each page shows the request, parameters, and a sample
8
- response.
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
- ## Base URL
17
+ ## How it works
16
18
 
17
- ```
18
- https://api.example.com/v1
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
- ## Authentication
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
- Send your key as a bearer token:
31
+ ## The example API
24
32
 
25
- ```bash
26
- curl https://api.example.com/v1/items \
27
- -H "Authorization: Bearer YOUR_TOKEN"
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
  }