@veluai/velu 0.1.14 → 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/Logo.jsx +31 -0
- package/runtime/velu-ui/components/PageFooter.jsx +6 -74
- package/runtime/velu-ui/components/PageHeader.jsx +10 -2
- package/runtime/velu-ui/components/Sidebar.jsx +10 -2
- package/runtime/velu-ui/components/SocialLinks.jsx +90 -0
- 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/components/page-footer.css +5 -5
- package/runtime/velu-ui/components/page-header.css +37 -0
- package/runtime/velu-ui/components/powered-by.css +20 -0
- package/runtime/velu-ui/index.js +4 -0
- package/runtime/velu-ui/lib/api-send.js +92 -0
- package/runtime/velu-ui/styles.css +1 -0
- package/schema/velu.schema.json +160 -0
- package/src/navigation.js +11 -2
- package/src/runtime/App.jsx +109 -66
- 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
|
@@ -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,157 @@
|
|
|
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
|
+
},
|
|
80
|
+
"logo": {
|
|
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.",
|
|
82
|
+
"oneOf": [
|
|
83
|
+
{
|
|
84
|
+
"type": "string",
|
|
85
|
+
"minLength": 1,
|
|
86
|
+
"description": "Logo path used in both light and dark mode."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"type": "object",
|
|
90
|
+
"additionalProperties": false,
|
|
91
|
+
"required": ["light", "dark"],
|
|
92
|
+
"properties": {
|
|
93
|
+
"light": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"minLength": 1,
|
|
96
|
+
"description": "Logo shown in light mode."
|
|
97
|
+
},
|
|
98
|
+
"dark": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"minLength": 1,
|
|
101
|
+
"description": "Logo shown in dark mode."
|
|
102
|
+
},
|
|
103
|
+
"href": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Where clicking the logo links. Defaults to the site root."
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"navbar": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"additionalProperties": false,
|
|
114
|
+
"description": "The header's top-right area: secondary links plus a primary CTA button.",
|
|
115
|
+
"properties": {
|
|
116
|
+
"links": {
|
|
117
|
+
"type": "array",
|
|
118
|
+
"description": "Secondary header links (to the left of the primary CTA).",
|
|
119
|
+
"items": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"required": ["href"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"label": { "type": "string", "description": "Link text." },
|
|
125
|
+
"href": { "type": "string", "description": "Link URL." },
|
|
126
|
+
"icon": { "type": "string", "description": "lucide icon id." }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"primary": {
|
|
131
|
+
"description": "The prominent CTA button at the far right.",
|
|
132
|
+
"oneOf": [
|
|
133
|
+
{
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": false,
|
|
136
|
+
"required": ["type", "label", "href"],
|
|
137
|
+
"properties": {
|
|
138
|
+
"type": { "const": "button" },
|
|
139
|
+
"label": { "type": "string", "description": "Button text." },
|
|
140
|
+
"href": { "type": "string", "description": "Button URL." }
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"required": ["type", "href"],
|
|
147
|
+
"properties": {
|
|
148
|
+
"type": { "const": "github" },
|
|
149
|
+
"href": { "type": "string", "description": "GitHub repository URL." },
|
|
150
|
+
"label": { "type": "string", "description": "Button text (default \"GitHub\")." }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"footer": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"description": "Site footer. With `links`, a full footer section renders; with only `socials`, the icons sit next to the \"Powered by\" line instead.",
|
|
161
|
+
"properties": {
|
|
162
|
+
"socials": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"description": "Social links as platform → URL. Recognized icons: github, x (or twitter), youtube, linkedin.",
|
|
165
|
+
"additionalProperties": { "type": "string" }
|
|
166
|
+
},
|
|
167
|
+
"links": {
|
|
168
|
+
"type": "array",
|
|
169
|
+
"description": "Footer link columns (max 4).",
|
|
170
|
+
"maxItems": 4,
|
|
171
|
+
"items": {
|
|
172
|
+
"type": "object",
|
|
173
|
+
"additionalProperties": false,
|
|
174
|
+
"required": ["header", "items"],
|
|
175
|
+
"properties": {
|
|
176
|
+
"header": { "type": "string", "description": "Column heading." },
|
|
177
|
+
"items": {
|
|
178
|
+
"type": "array",
|
|
179
|
+
"items": {
|
|
180
|
+
"type": "object",
|
|
181
|
+
"additionalProperties": false,
|
|
182
|
+
"required": ["label", "href"],
|
|
183
|
+
"properties": {
|
|
184
|
+
"label": { "type": "string", "description": "Link text." },
|
|
185
|
+
"href": { "type": "string", "description": "Link URL." }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
43
194
|
"font": {
|
|
44
195
|
"type": "object",
|
|
45
196
|
"description": "Typography. Maps onto the --font-sans token in base.css.",
|
|
@@ -91,6 +242,7 @@
|
|
|
91
242
|
"icon": { "type": "string", "description": "lucide icon id." },
|
|
92
243
|
"expanded": { "type": "boolean", "description": "Start expanded." },
|
|
93
244
|
"root": { "type": "string", "description": "Landing page path for the group." },
|
|
245
|
+
"openapi": { "$ref": "#/$defs/openapiRef" },
|
|
94
246
|
"pages": { "$ref": "#/$defs/pages" }
|
|
95
247
|
}
|
|
96
248
|
},
|
|
@@ -102,11 +254,19 @@
|
|
|
102
254
|
"tab": { "type": "string", "description": "Tab label (top nav)." },
|
|
103
255
|
"icon": { "type": "string" },
|
|
104
256
|
"href": { "type": "string", "description": "External/override link instead of in-site pages." },
|
|
257
|
+
"openapi": { "$ref": "#/$defs/openapiRef" },
|
|
105
258
|
"anchors": { "type": "array", "items": { "$ref": "#/$defs/anchor" } },
|
|
106
259
|
"groups": { "type": "array", "items": { "$ref": "#/$defs/group" } },
|
|
107
260
|
"pages": { "$ref": "#/$defs/pages" }
|
|
108
261
|
}
|
|
109
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
|
+
},
|
|
110
270
|
"anchor": {
|
|
111
271
|
"type": "object",
|
|
112
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
|
@@ -4,7 +4,7 @@ import { MDXProvider } from '@mdx-js/react';
|
|
|
4
4
|
// The project's pages + navigation, generated from velu.json by
|
|
5
5
|
// vite-plugin-velu-site (see src/vite-plugin-velu-site.js). `pages` is
|
|
6
6
|
// url → { Component, frontmatter, toc } (or { missing:true }).
|
|
7
|
-
import { pages, navigation } from 'virtual:velu-site';
|
|
7
|
+
import { pages, navigation, site } from 'virtual:velu-site';
|
|
8
8
|
import { resolve, normalizeUrl } from '../navigation.js';
|
|
9
9
|
import {
|
|
10
10
|
Stack,
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
defaultMdxComponents,
|
|
39
39
|
resolveIcon,
|
|
40
40
|
Search,
|
|
41
|
+
Logo,
|
|
42
|
+
SocialLinks,
|
|
41
43
|
Tree,
|
|
42
44
|
Folder,
|
|
43
45
|
File,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
ApiClient,
|
|
46
48
|
ApiField,
|
|
47
49
|
ApiSidebar,
|
|
50
|
+
ApiSamples,
|
|
48
51
|
VeluMark,
|
|
49
52
|
} from 'velu-ui';
|
|
50
53
|
import { X, ChevronDown, ChevronUp } from 'lucide-react';
|
|
@@ -944,6 +947,35 @@ function DocsPage() {
|
|
|
944
947
|
return () => scroller.removeEventListener('scroll', update);
|
|
945
948
|
}, [pathname, chatOpen]);
|
|
946
949
|
|
|
950
|
+
// Footer config (from velu.json via virtual:velu-site). Link columns →
|
|
951
|
+
// full footer section; socials with no columns → inline next to PoweredBy.
|
|
952
|
+
const footerSocials = site.footer?.socials ?? [];
|
|
953
|
+
const footerHasLinks = (site.footer?.links?.length ?? 0) > 0;
|
|
954
|
+
|
|
955
|
+
// Header top-right actions, built from velu.json's `navbar`: secondary
|
|
956
|
+
// `links` (plain text links) then the `primary` CTA (a filled button, or an
|
|
957
|
+
// outlined GitHub button). Empty when unconfigured — no placeholder buttons.
|
|
958
|
+
const navActions = [];
|
|
959
|
+
for (const l of site.navbar?.links ?? []) {
|
|
960
|
+
navActions.push({
|
|
961
|
+
label: l.label,
|
|
962
|
+
href: l.href,
|
|
963
|
+
icon: l.icon || undefined,
|
|
964
|
+
kind: 'link',
|
|
965
|
+
external: l.external,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
if (site.navbar?.primary) {
|
|
969
|
+
const p = site.navbar.primary;
|
|
970
|
+
navActions.push({
|
|
971
|
+
label: p.label,
|
|
972
|
+
href: p.href,
|
|
973
|
+
kind: p.kind === 'github' ? 'outlined' : 'primary',
|
|
974
|
+
icon: p.kind === 'github' ? 'github' : undefined,
|
|
975
|
+
external: p.external,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
947
979
|
const footerRef = React.useRef(null);
|
|
948
980
|
const [footerOverlap, setFooterOverlap] = React.useState(0);
|
|
949
981
|
React.useEffect(() => {
|
|
@@ -1026,6 +1058,7 @@ function DocsPage() {
|
|
|
1026
1058
|
data-chat-open={chatOpen ? 'true' : 'false'}
|
|
1027
1059
|
data-sidebar-open={sidebarOpen ? 'true' : 'false'}
|
|
1028
1060
|
data-drawer-open={drawerOpen ? 'true' : 'false'}
|
|
1061
|
+
data-api={entry?.api ? 'true' : 'false'}
|
|
1029
1062
|
>
|
|
1030
1063
|
{/* Scrim — visible at mobile while the drawer OR the chatbot
|
|
1031
1064
|
sheet is open. Sits between the article (z-0) and the
|
|
@@ -1043,7 +1076,11 @@ function DocsPage() {
|
|
|
1043
1076
|
tabs row. Configurable: pass any number of actions / tabs. */}
|
|
1044
1077
|
<PageHeader
|
|
1045
1078
|
linkComponent={RouterLink}
|
|
1046
|
-
brand={{
|
|
1079
|
+
brand={{
|
|
1080
|
+
label: site.name,
|
|
1081
|
+
href: site.logo?.href || '/',
|
|
1082
|
+
logo: site.logo,
|
|
1083
|
+
}}
|
|
1047
1084
|
brandTrailing={
|
|
1048
1085
|
versionSwitcher && (
|
|
1049
1086
|
<span className="velu-hide-on-mobile">{versionSwitcher}</span>
|
|
@@ -1069,13 +1106,7 @@ function DocsPage() {
|
|
|
1069
1106
|
)}
|
|
1070
1107
|
</Cluster>
|
|
1071
1108
|
}
|
|
1072
|
-
actions={
|
|
1073
|
-
{
|
|
1074
|
-
label: 'Book Demo',
|
|
1075
|
-
kind: 'primary',
|
|
1076
|
-
href: '#',
|
|
1077
|
-
},
|
|
1078
|
-
]}
|
|
1109
|
+
actions={navActions}
|
|
1079
1110
|
trailing={<ThemeToggle />}
|
|
1080
1111
|
onMenuClick={() => setDrawerOpen((v) => !v)}
|
|
1081
1112
|
breadcrumb={nav?.breadcrumb ?? []}
|
|
@@ -1111,8 +1142,14 @@ function DocsPage() {
|
|
|
1111
1142
|
href="/"
|
|
1112
1143
|
className="velu-docs-layout__drawer-brand"
|
|
1113
1144
|
>
|
|
1114
|
-
|
|
1115
|
-
|
|
1145
|
+
{site.logo ? (
|
|
1146
|
+
<Logo logo={site.logo} alt={site.name} />
|
|
1147
|
+
) : (
|
|
1148
|
+
<>
|
|
1149
|
+
<VeluMark />
|
|
1150
|
+
<span className="velu-header__wordmark">{site.name}</span>
|
|
1151
|
+
</>
|
|
1152
|
+
)}
|
|
1116
1153
|
</RouterLink>
|
|
1117
1154
|
<ThemeToggle />
|
|
1118
1155
|
<button
|
|
@@ -1228,11 +1265,27 @@ function DocsPage() {
|
|
|
1228
1265
|
scrollPaddingBlockEnd: 'var(--s1)',
|
|
1229
1266
|
}}
|
|
1230
1267
|
>
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
+
)}
|
|
1236
1289
|
</div>
|
|
1237
1290
|
<button
|
|
1238
1291
|
type="button"
|
|
@@ -1282,7 +1335,16 @@ function DocsPage() {
|
|
|
1282
1335
|
scrollPaddingBlockEnd: `calc(${footerOverlap}px + 2rem)`,
|
|
1283
1336
|
}}
|
|
1284
1337
|
>
|
|
1285
|
-
|
|
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
|
+
)}
|
|
1286
1348
|
</aside>
|
|
1287
1349
|
)}
|
|
1288
1350
|
|
|
@@ -1417,59 +1479,40 @@ function DocsPage() {
|
|
|
1417
1479
|
}}
|
|
1418
1480
|
/>
|
|
1419
1481
|
)}
|
|
1420
|
-
{/* "Powered by Velu"
|
|
1421
|
-
article column
|
|
1422
|
-
|
|
1423
|
-
|
|
1482
|
+
{/* Content foot — "Powered by Velu" at the bottom-right of the
|
|
1483
|
+
article column. When the footer has no link columns, the
|
|
1484
|
+
configured social icons sit on the LEFT of this same row (no
|
|
1485
|
+
full footer section); with link columns they live in the full
|
|
1486
|
+
footer below instead. */}
|
|
1487
|
+
{footerHasLinks ? (
|
|
1488
|
+
<PoweredBy />
|
|
1489
|
+
) : (
|
|
1490
|
+
<div className="velu-content-foot">
|
|
1491
|
+
<SocialLinks socials={footerSocials} />
|
|
1492
|
+
<PoweredBy />
|
|
1493
|
+
</div>
|
|
1494
|
+
)}
|
|
1424
1495
|
</div>
|
|
1425
1496
|
</main>
|
|
1426
1497
|
</div>
|
|
1427
1498
|
|
|
1428
|
-
{/*
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
{ label: 'Enterprise', href: '#' },
|
|
1446
|
-
{ label: 'Status', href: '#' },
|
|
1447
|
-
],
|
|
1448
|
-
},
|
|
1449
|
-
{
|
|
1450
|
-
title: 'Company',
|
|
1451
|
-
items: [
|
|
1452
|
-
{ label: 'Careers', href: '#' },
|
|
1453
|
-
{ label: 'Blog', href: '#' },
|
|
1454
|
-
{ label: 'Community', href: '#' },
|
|
1455
|
-
],
|
|
1456
|
-
},
|
|
1457
|
-
{
|
|
1458
|
-
title: 'Policies',
|
|
1459
|
-
items: [
|
|
1460
|
-
{ label: 'Subprocessors', href: '#' },
|
|
1461
|
-
{ label: 'Terms of Service', href: '#' },
|
|
1462
|
-
],
|
|
1463
|
-
},
|
|
1464
|
-
]}
|
|
1465
|
-
socials={[
|
|
1466
|
-
{ kind: 'github', href: 'https://github.com/aravindc26/velu-cli' },
|
|
1467
|
-
{ kind: 'x', href: 'https://x.com/' },
|
|
1468
|
-
{ kind: 'youtube', href: 'https://youtube.com/' },
|
|
1469
|
-
{ kind: 'linkedin', href: 'https://linkedin.com/' },
|
|
1470
|
-
]}
|
|
1471
|
-
/>
|
|
1472
|
-
</div>
|
|
1499
|
+
{/* Full site footer — only when the config provides link columns. Spans
|
|
1500
|
+
full width below the article; its raised z-index eclipses the bottoms
|
|
1501
|
+
of the fixed sidebar and TOC as the page scrolls into it, and the ref
|
|
1502
|
+
is watched so the asides can pad their bottom by the overlap. */}
|
|
1503
|
+
{footerHasLinks && (
|
|
1504
|
+
<div
|
|
1505
|
+
ref={footerRef}
|
|
1506
|
+
data-velu-footer
|
|
1507
|
+
style={{ position: 'relative', zIndex: 20 }}
|
|
1508
|
+
>
|
|
1509
|
+
<PageFooter
|
|
1510
|
+
brand={{ href: site.logo?.href || '/' }}
|
|
1511
|
+
columns={site.footer.links}
|
|
1512
|
+
socials={footerSocials}
|
|
1513
|
+
/>
|
|
1514
|
+
</div>
|
|
1515
|
+
)}
|
|
1473
1516
|
|
|
1474
1517
|
{/* Ask-AI chatbot — slides in from the inline-end edge. */}
|
|
1475
1518
|
<Chatbot
|
|
@@ -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.
|