@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,388 @@
|
|
|
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 }];
|
|
41
|
+
const current = ops.find((o) => o.id === activeId) || { operation, samples };
|
|
42
|
+
const server = api.server || current.operation.servers?.[0] || '';
|
|
43
|
+
|
|
44
|
+
// While the playground modal is open, close on Escape and lock the
|
|
45
|
+
// page scroll so the dimmed backdrop reads as a true overlay.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!open || typeof document === 'undefined') return undefined;
|
|
48
|
+
const onKey = (e) => {
|
|
49
|
+
if (e.key === 'Escape') setOpen(false);
|
|
50
|
+
};
|
|
51
|
+
document.addEventListener('keydown', onKey);
|
|
52
|
+
const prev = document.body.style.overflow;
|
|
53
|
+
document.body.style.overflow = 'hidden';
|
|
54
|
+
return () => {
|
|
55
|
+
document.removeEventListener('keydown', onKey);
|
|
56
|
+
document.body.style.overflow = prev;
|
|
57
|
+
};
|
|
58
|
+
}, [open]);
|
|
59
|
+
|
|
60
|
+
if (!operation) return null;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="velu-api-page">
|
|
64
|
+
{/* The page title + description come from the page frontmatter heading
|
|
65
|
+
(rendered by the runtime), so this component starts at the Try-It bar
|
|
66
|
+
to avoid a duplicate title. */}
|
|
67
|
+
<TryItBar
|
|
68
|
+
method={operation.method}
|
|
69
|
+
path={operation.path}
|
|
70
|
+
cta="Try It"
|
|
71
|
+
onTry={() => {
|
|
72
|
+
setActiveId(operation.id);
|
|
73
|
+
setOpen(true);
|
|
74
|
+
}}
|
|
75
|
+
className="velu-api-page__tryit"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
{/* Authorizations */}
|
|
79
|
+
{operation.auth?.length > 0 && (
|
|
80
|
+
<section className="velu-api-page__section">
|
|
81
|
+
<h2>Authorizations</h2>
|
|
82
|
+
{operation.auth.map((a, i) => (
|
|
83
|
+
<Field key={i} name={a.name} pre={a.prefix} type={a.type} required={a.required} post={a.in}>
|
|
84
|
+
{a.description}
|
|
85
|
+
</Field>
|
|
86
|
+
))}
|
|
87
|
+
</section>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Parameters, by location */}
|
|
91
|
+
{['path', 'query', 'header'].map((loc) => {
|
|
92
|
+
const rows = operation.parameters?.[loc] || [];
|
|
93
|
+
if (!rows.length) return null;
|
|
94
|
+
return (
|
|
95
|
+
<section key={loc} className="velu-api-page__section">
|
|
96
|
+
<h2>{LOCATION_LABEL[loc]}</h2>
|
|
97
|
+
{rows.map((p, i) => (
|
|
98
|
+
<Field key={i} name={p.name} type={p.type} required={p.required}>
|
|
99
|
+
{p.description}
|
|
100
|
+
{p.enum && <EnumHint values={p.enum} />}
|
|
101
|
+
</Field>
|
|
102
|
+
))}
|
|
103
|
+
</section>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
|
|
107
|
+
{/* Request body */}
|
|
108
|
+
{operation.body?.fields?.length > 0 && (
|
|
109
|
+
<section className="velu-api-page__section">
|
|
110
|
+
<h2>Body</h2>
|
|
111
|
+
{operation.body.fields.map((f, i) => (
|
|
112
|
+
<Field key={i} name={f.name} type={f.type} required={f.required}>
|
|
113
|
+
{f.description}
|
|
114
|
+
{f.enum && <EnumHint values={f.enum} />}
|
|
115
|
+
</Field>
|
|
116
|
+
))}
|
|
117
|
+
</section>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Responses */}
|
|
121
|
+
{operation.responses?.length > 0 && (
|
|
122
|
+
<section className="velu-api-page__section">
|
|
123
|
+
<h2>Response</h2>
|
|
124
|
+
<ResponseSections responses={operation.responses} />
|
|
125
|
+
</section>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* Try-It playground — rendered as an overlay above the page, with a
|
|
129
|
+
blurred backdrop, rather than inline. Portaled to <body> so the
|
|
130
|
+
docs layout's @container containment can't clip the fixed overlay. */}
|
|
131
|
+
{open && typeof document !== 'undefined' &&
|
|
132
|
+
createPortal(
|
|
133
|
+
<div
|
|
134
|
+
className="velu-api-modal"
|
|
135
|
+
role="presentation"
|
|
136
|
+
onClick={() => setOpen(false)}
|
|
137
|
+
>
|
|
138
|
+
<div
|
|
139
|
+
className="velu-api-modal__panel"
|
|
140
|
+
role="dialog"
|
|
141
|
+
aria-modal="true"
|
|
142
|
+
aria-label={`${operation.title} playground`}
|
|
143
|
+
onClick={(e) => e.stopPropagation()}
|
|
144
|
+
>
|
|
145
|
+
<Playground
|
|
146
|
+
key={current.operation.id}
|
|
147
|
+
operation={current.operation}
|
|
148
|
+
samples={current.samples}
|
|
149
|
+
operations={ops}
|
|
150
|
+
activeId={activeId}
|
|
151
|
+
onSelectOperation={setActiveId}
|
|
152
|
+
server={server}
|
|
153
|
+
proxy={api.proxy !== false}
|
|
154
|
+
onClose={() => setOpen(false)}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</div>,
|
|
158
|
+
document.body,
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function EnumHint({ values }) {
|
|
165
|
+
return (
|
|
166
|
+
<p className="velu-api-page__enum">
|
|
167
|
+
Allowed values:{' '}
|
|
168
|
+
{values.map((v, i) => (
|
|
169
|
+
<code key={i}>{String(v)}</code>
|
|
170
|
+
))}
|
|
171
|
+
</p>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ResponseSections({ responses }) {
|
|
176
|
+
return (
|
|
177
|
+
<AccordionGroup className="velu-api-resp">
|
|
178
|
+
{responses.map((r, i) => {
|
|
179
|
+
return (
|
|
180
|
+
<Accordion
|
|
181
|
+
key={i}
|
|
182
|
+
defaultOpen
|
|
183
|
+
title={
|
|
184
|
+
<span className="velu-api-resp__head">
|
|
185
|
+
<span className="velu-api-resp__status">{r.status}</span>
|
|
186
|
+
{r.contentType && (
|
|
187
|
+
<span className="velu-api-resp__ctype">{r.contentType}</span>
|
|
188
|
+
)}
|
|
189
|
+
</span>
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
{r.description && <p className="velu-api-resp__desc">{r.description}</p>}
|
|
193
|
+
{r.fields?.map((f, j) => (
|
|
194
|
+
<Field key={j} name={f.name} type={f.type} required={f.required}>
|
|
195
|
+
{f.description}
|
|
196
|
+
</Field>
|
|
197
|
+
))}
|
|
198
|
+
</Accordion>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</AccordionGroup>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── The interactive playground ──────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function Playground({
|
|
208
|
+
operation,
|
|
209
|
+
samples = [],
|
|
210
|
+
operations = [],
|
|
211
|
+
activeId,
|
|
212
|
+
onSelectOperation,
|
|
213
|
+
server,
|
|
214
|
+
proxy,
|
|
215
|
+
onClose,
|
|
216
|
+
}) {
|
|
217
|
+
const [values, setValues] = useState({});
|
|
218
|
+
const [bodyText, setBodyText] = useState(
|
|
219
|
+
operation.body ? JSON.stringify(operation.body.example ?? {}, null, 2) : '',
|
|
220
|
+
);
|
|
221
|
+
const [busy, setBusy] = useState(false);
|
|
222
|
+
const [resp, setResp] = useState(null);
|
|
223
|
+
|
|
224
|
+
const set = (loc, name, v) =>
|
|
225
|
+
setValues((prev) => ({ ...prev, [loc]: { ...(prev[loc] || {}), [name]: v } }));
|
|
226
|
+
|
|
227
|
+
const onSend = useCallback(async () => {
|
|
228
|
+
setBusy(true);
|
|
229
|
+
setResp(null);
|
|
230
|
+
try {
|
|
231
|
+
const result = await sendApiRequest({ operation, server, proxy, values, bodyText });
|
|
232
|
+
setResp(result);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
setResp({ error: String(err?.message || err) });
|
|
235
|
+
} finally {
|
|
236
|
+
setBusy(false);
|
|
237
|
+
}
|
|
238
|
+
}, [operation, server, proxy, values, bodyText]);
|
|
239
|
+
|
|
240
|
+
const sections = [];
|
|
241
|
+
if (operation.auth?.length) sections.push(['auth', 'Authorization', operation.auth]);
|
|
242
|
+
for (const loc of ['path', 'query', 'header']) {
|
|
243
|
+
const rows = operation.parameters?.[loc] || [];
|
|
244
|
+
if (rows.length) sections.push([loc, LOCATION_LABEL[loc].replace(' Parameters', ''), rows]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<ApiClient
|
|
249
|
+
method={operation.method}
|
|
250
|
+
label={operation.title}
|
|
251
|
+
path={operation.path}
|
|
252
|
+
description={operation.description}
|
|
253
|
+
operations={operations}
|
|
254
|
+
activeId={activeId}
|
|
255
|
+
onSelectOperation={onSelectOperation}
|
|
256
|
+
onSend={onSend}
|
|
257
|
+
sending={busy}
|
|
258
|
+
onClose={onClose}
|
|
259
|
+
aside={
|
|
260
|
+
<div className="velu-api-pg-aside">
|
|
261
|
+
{/* The live response is prepended above the static request /
|
|
262
|
+
response samples as it comes in. Progress is shown by the Send
|
|
263
|
+
button's spinner — no "Sending…" placeholder here. */}
|
|
264
|
+
{resp && <ResponsePanel resp={resp} />}
|
|
265
|
+
<ApiSamples samples={samples} responses={operation.responses} />
|
|
266
|
+
</div>
|
|
267
|
+
}
|
|
268
|
+
>
|
|
269
|
+
<AccordionGroup>
|
|
270
|
+
{sections.map(([loc, title, rows]) => (
|
|
271
|
+
<Accordion key={loc} title={title} defaultOpen>
|
|
272
|
+
{rows.map((f, i) => (
|
|
273
|
+
<ApiField
|
|
274
|
+
key={i}
|
|
275
|
+
name={f.name}
|
|
276
|
+
type={f.type}
|
|
277
|
+
required={f.required}
|
|
278
|
+
prefix={f.prefix}
|
|
279
|
+
value={values[loc]?.[f.name] ?? ''}
|
|
280
|
+
onChange={(e) => set(loc, f.name, e.target.value)}
|
|
281
|
+
>
|
|
282
|
+
{f.description}
|
|
283
|
+
</ApiField>
|
|
284
|
+
))}
|
|
285
|
+
</Accordion>
|
|
286
|
+
))}
|
|
287
|
+
{operation.body && (
|
|
288
|
+
<Accordion title="Body" defaultOpen>
|
|
289
|
+
<textarea
|
|
290
|
+
className="velu-api-body-input"
|
|
291
|
+
spellCheck={false}
|
|
292
|
+
rows={Math.min(14, bodyText.split('\n').length + 1)}
|
|
293
|
+
value={bodyText}
|
|
294
|
+
onChange={(e) => setBodyText(e.target.value)}
|
|
295
|
+
/>
|
|
296
|
+
</Accordion>
|
|
297
|
+
)}
|
|
298
|
+
</AccordionGroup>
|
|
299
|
+
</ApiClient>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* ResponsePanel — the live-response viewer: a Body / Header tabbed card.
|
|
305
|
+
* - Body: a status-colored Callout (green for 2xx, red otherwise) plus the
|
|
306
|
+
* response body.
|
|
307
|
+
* - Header: the response headers as a key/value table.
|
|
308
|
+
*/
|
|
309
|
+
function ResponsePanel({ resp }) {
|
|
310
|
+
const [tab, setTab] = useState('body');
|
|
311
|
+
if (!resp) return null;
|
|
312
|
+
|
|
313
|
+
const isError = resp.error != null;
|
|
314
|
+
const ok = !isError && resp.status >= 200 && resp.status < 300;
|
|
315
|
+
const statusLabel = isError
|
|
316
|
+
? 'Request failed'
|
|
317
|
+
: `${resp.status}${resp.statusText ? ` - ${resp.statusText}` : ''}`;
|
|
318
|
+
const bodyStr = isError
|
|
319
|
+
? resp.error
|
|
320
|
+
: typeof resp.body === 'string'
|
|
321
|
+
? resp.body
|
|
322
|
+
: JSON.stringify(resp.body, null, 2);
|
|
323
|
+
const headerRows = Object.entries(resp.headers || {});
|
|
324
|
+
|
|
325
|
+
// Reuse the CodeGroup's tab chrome (velu-code-block__*) so the Body/Header
|
|
326
|
+
// switcher looks identical to the code-sample tabs.
|
|
327
|
+
const tabCls = (name) =>
|
|
328
|
+
`velu-code-block__tab${tab === name ? ' velu-code-block__tab--active' : ''}`;
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div className="velu-code-block velu-api-resp-panel">
|
|
332
|
+
<div className="velu-code-block__header">
|
|
333
|
+
<div className="velu-code-block__tabs" role="tablist">
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
role="tab"
|
|
337
|
+
aria-selected={tab === 'body'}
|
|
338
|
+
className={tabCls('body')}
|
|
339
|
+
onClick={() => setTab('body')}
|
|
340
|
+
>
|
|
341
|
+
<span className="velu-code-block__tab-label">Body</span>
|
|
342
|
+
</button>
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
role="tab"
|
|
346
|
+
aria-selected={tab === 'header'}
|
|
347
|
+
className={tabCls('header')}
|
|
348
|
+
onClick={() => setTab('header')}
|
|
349
|
+
disabled={!headerRows.length || undefined}
|
|
350
|
+
>
|
|
351
|
+
<span className="velu-code-block__tab-label">Header</span>
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div className="velu-api-resp-panel__content">
|
|
357
|
+
{tab === 'body' ? (
|
|
358
|
+
<>
|
|
359
|
+
<Callout
|
|
360
|
+
type={ok ? 'check' : 'danger'}
|
|
361
|
+
className="velu-api-resp-panel__status"
|
|
362
|
+
>
|
|
363
|
+
{statusLabel}
|
|
364
|
+
</Callout>
|
|
365
|
+
{!isError && (
|
|
366
|
+
<div className="velu-api-resp-body__scroll velu-hide-scrollbar">
|
|
367
|
+
<CodeBlock language="json">{bodyStr || '(empty)'}</CodeBlock>
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
</>
|
|
371
|
+
) : (
|
|
372
|
+
<div className="velu-api-resp-headers__scroll velu-hide-scrollbar">
|
|
373
|
+
<table className="velu-api-resp-headers">
|
|
374
|
+
<tbody>
|
|
375
|
+
{headerRows.map(([k, val]) => (
|
|
376
|
+
<tr key={k}>
|
|
377
|
+
<td className="velu-api-resp-headers__key">{k}</td>
|
|
378
|
+
<td className="velu-api-resp-headers__val">{val}</td>
|
|
379
|
+
</tr>
|
|
380
|
+
))}
|
|
381
|
+
</tbody>
|
|
382
|
+
</table>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ApiSamples — the right-rail of an API reference page: the request code
|
|
6
|
+
* snippets (curl / JS / Python / Ruby tabs) and the example responses
|
|
7
|
+
* (one tab per status). The runtime renders this in the page's right aside
|
|
8
|
+
* (where the TOC sits for normal pages).
|
|
9
|
+
*
|
|
10
|
+
* @param {{ samples: {key,label,language,code}[], responses: object[] }} props
|
|
11
|
+
*/
|
|
12
|
+
export default function ApiSamples({ samples = [], responses = [] }) {
|
|
13
|
+
const withExample = responses.filter((r) => r.example != null);
|
|
14
|
+
return (
|
|
15
|
+
<div className="velu-api-samples">
|
|
16
|
+
{samples.length > 0 && (
|
|
17
|
+
<CodeGroup className="velu-api-samples__req">
|
|
18
|
+
{samples.map((s) => (
|
|
19
|
+
<CodeBlock key={s.key} title={s.label} language={s.language}>
|
|
20
|
+
{s.code}
|
|
21
|
+
</CodeBlock>
|
|
22
|
+
))}
|
|
23
|
+
</CodeGroup>
|
|
24
|
+
)}
|
|
25
|
+
{withExample.length > 0 && (
|
|
26
|
+
<CodeGroup className="velu-api-samples__res">
|
|
27
|
+
{withExample.map((r) => (
|
|
28
|
+
<CodeBlock key={r.status} title={r.status} language="json">
|
|
29
|
+
{JSON.stringify(r.example, null, 2)}
|
|
30
|
+
</CodeBlock>
|
|
31
|
+
))}
|
|
32
|
+
</CodeGroup>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logo — the configured site logo image(s), shown in the header (and mobile
|
|
5
|
+
* drawer) in place of the name wordmark.
|
|
6
|
+
*
|
|
7
|
+
* `logo` is `{ light, dark, mono }` where light/dark are URLs the dev server
|
|
8
|
+
* serves from the project's logo files. When `mono`, a single image covers
|
|
9
|
+
* both themes; otherwise both render and CSS shows the right one per
|
|
10
|
+
* `[data-theme]` (see page-header.css `.velu-logo--light/--dark`).
|
|
11
|
+
*
|
|
12
|
+
* SSR-safe: identical markup on server + client (the theme picks the visible
|
|
13
|
+
* image via CSS, not JS), so there's no hydration mismatch or flash.
|
|
14
|
+
*/
|
|
15
|
+
export default function Logo({ logo, alt = '' }) {
|
|
16
|
+
if (!logo) return null;
|
|
17
|
+
if (logo.mono) {
|
|
18
|
+
return <img className="velu-logo" src={logo.light} alt={alt} />;
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<img className="velu-logo velu-logo--light" src={logo.light} alt={alt} />
|
|
23
|
+
<img
|
|
24
|
+
className="velu-logo velu-logo--dark"
|
|
25
|
+
src={logo.dark}
|
|
26
|
+
alt={alt}
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
/>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Stack from '../primitives/Stack.jsx';
|
|
3
3
|
import Cluster from '../primitives/Cluster.jsx';
|
|
4
|
+
import SocialLinks from './SocialLinks.jsx';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* PageFooter — the docs site's footer.
|
|
@@ -65,54 +66,6 @@ function VeluMark() {
|
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/* ── Social glyphs — inline SVGs with fill: currentColor so they
|
|
69
|
-
inherit color (themed) and flip with light/dark. ─────────────────── */
|
|
70
|
-
function GithubIcon() {
|
|
71
|
-
return (
|
|
72
|
-
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
73
|
-
<path
|
|
74
|
-
fillRule="evenodd"
|
|
75
|
-
clipRule="evenodd"
|
|
76
|
-
d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
|
|
77
|
-
/>
|
|
78
|
-
</svg>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
function XIcon() {
|
|
82
|
-
return (
|
|
83
|
-
<svg viewBox="0 0 1200 1227" fill="currentColor" aria-hidden="true">
|
|
84
|
-
<path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
|
|
85
|
-
</svg>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
function YoutubeIcon() {
|
|
89
|
-
return (
|
|
90
|
-
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
91
|
-
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
|
92
|
-
</svg>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
function LinkedinIcon() {
|
|
96
|
-
return (
|
|
97
|
-
<svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
|
|
98
|
-
<path d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" />
|
|
99
|
-
</svg>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const SOCIAL_ICONS = {
|
|
104
|
-
github: GithubIcon,
|
|
105
|
-
x: XIcon,
|
|
106
|
-
youtube: YoutubeIcon,
|
|
107
|
-
linkedin: LinkedinIcon,
|
|
108
|
-
};
|
|
109
|
-
const SOCIAL_LABELS = {
|
|
110
|
-
github: 'GitHub',
|
|
111
|
-
x: 'X',
|
|
112
|
-
youtube: 'YouTube',
|
|
113
|
-
linkedin: 'LinkedIn',
|
|
114
|
-
};
|
|
115
|
-
|
|
116
69
|
export default function PageFooter({
|
|
117
70
|
brand,
|
|
118
71
|
columns = [],
|
|
@@ -182,32 +135,11 @@ export default function PageFooter({
|
|
|
182
135
|
</Stack>
|
|
183
136
|
|
|
184
137
|
{/* Socials sit OUTSIDE the container, at the bottom of the footer. */}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
className="velu-footer__socials"
|
|
191
|
-
>
|
|
192
|
-
{socials.map((s, i) => {
|
|
193
|
-
const Icon = SOCIAL_ICONS[s.kind];
|
|
194
|
-
if (!Icon) return null;
|
|
195
|
-
return (
|
|
196
|
-
<a
|
|
197
|
-
key={i}
|
|
198
|
-
href={s.href}
|
|
199
|
-
target="_blank"
|
|
200
|
-
rel="noreferrer"
|
|
201
|
-
aria-label={SOCIAL_LABELS[s.kind] ?? s.kind}
|
|
202
|
-
className="velu-footer__social"
|
|
203
|
-
data-kind={s.kind}
|
|
204
|
-
>
|
|
205
|
-
<Icon />
|
|
206
|
-
</a>
|
|
207
|
-
);
|
|
208
|
-
})}
|
|
209
|
-
</Cluster>
|
|
210
|
-
)}
|
|
138
|
+
<SocialLinks
|
|
139
|
+
socials={socials}
|
|
140
|
+
justify="center"
|
|
141
|
+
className="velu-footer__socials"
|
|
142
|
+
/>
|
|
211
143
|
</Stack>
|
|
212
144
|
);
|
|
213
145
|
}
|
|
@@ -3,6 +3,7 @@ import { MoreVertical, Menu, ChevronRight } from 'lucide-react';
|
|
|
3
3
|
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
4
4
|
import Stack from '../primitives/Stack.jsx';
|
|
5
5
|
import Cluster from '../primitives/Cluster.jsx';
|
|
6
|
+
import Logo from './Logo.jsx';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* PageHeader — top of the docs site.
|
|
@@ -258,10 +259,17 @@ export default function PageHeader({
|
|
|
258
259
|
return () => window.removeEventListener('scroll', onScroll);
|
|
259
260
|
}, []);
|
|
260
261
|
|
|
262
|
+
// A configured logo replaces the mark + wordmark entirely (Mintlify-style).
|
|
261
263
|
const brandBlock = (
|
|
262
264
|
<Cluster space="var(--s-3)" align="center" className="velu-header__brand">
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
{brand?.logo ? (
|
|
266
|
+
<Logo logo={brand.logo} alt={brandLabel} />
|
|
267
|
+
) : (
|
|
268
|
+
<>
|
|
269
|
+
<VeluMark />
|
|
270
|
+
<span className="velu-header__wordmark">{brandLabel}</span>
|
|
271
|
+
</>
|
|
272
|
+
)}
|
|
265
273
|
</Cluster>
|
|
266
274
|
);
|
|
267
275
|
|
|
@@ -87,7 +87,7 @@ function ExternalIcon() {
|
|
|
87
87
|
/** Recursive node. depth 0 = top-level; depth >= 1 = nested (subitem style). */
|
|
88
88
|
function Node({ item, depth }) {
|
|
89
89
|
const { activeHref, Link } = React.useContext(SidebarCtx);
|
|
90
|
-
const { label, href = '#', icon, external, items } = item;
|
|
90
|
+
const { label, href = '#', icon, external, items, method } = item;
|
|
91
91
|
const base = depth === 0 ? 'velu-sidebar__item' : 'velu-sidebar__subitem';
|
|
92
92
|
|
|
93
93
|
if (items && items.length) {
|
|
@@ -125,7 +125,15 @@ function Node({ item, depth }) {
|
|
|
125
125
|
aria-current={active ? 'page' : undefined}
|
|
126
126
|
{...linkProps}
|
|
127
127
|
>
|
|
128
|
-
|
|
128
|
+
{method ? (
|
|
129
|
+
<span
|
|
130
|
+
className={`velu-sidebar__method velu-method-badge--${String(method).toLowerCase()}`}
|
|
131
|
+
>
|
|
132
|
+
{method}
|
|
133
|
+
</span>
|
|
134
|
+
) : (
|
|
135
|
+
<Icon icon={icon} />
|
|
136
|
+
)}
|
|
129
137
|
<span className="velu-sidebar__label">{label}</span>
|
|
130
138
|
{external ? <ExternalIcon /> : null}
|
|
131
139
|
</LinkTag>
|