@veluai/velu 0.1.16 → 0.2.1
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 +45 -39
- package/package.json +9 -3
- package/runtime/velu-ui/components/ApiClient.jsx +13 -3
- package/runtime/velu-ui/components/ApiReferencePage.jsx +9 -13
- package/runtime/velu-ui/components/ContextMenu.jsx +273 -0
- package/runtime/velu-ui/components/NotFound.jsx +63 -0
- package/runtime/velu-ui/components/Search.jsx +66 -17
- package/runtime/velu-ui/components/Sidebar.jsx +55 -0
- package/runtime/velu-ui/components/api-page.css +0 -7
- package/runtime/velu-ui/components/api.css +48 -8
- package/runtime/velu-ui/components/context-menu.css +173 -0
- package/runtime/velu-ui/components/docs-layout.css +35 -4
- package/runtime/velu-ui/components/not-found.css +94 -0
- package/runtime/velu-ui/components/page-header.css +1 -0
- package/runtime/velu-ui/components/powered-by.css +6 -0
- package/runtime/velu-ui/components/sidebar.css +64 -3
- package/runtime/velu-ui/components/theme-toggle.css +1 -0
- package/runtime/velu-ui/index.js +3 -0
- package/runtime/velu-ui/lib/brand-icons.jsx +103 -0
- package/runtime/velu-ui/lib/pagefind.js +113 -0
- package/runtime/velu-ui/styles.css +2 -0
- package/schema/velu.schema.json +112 -37
- package/src/runtime/App.jsx +93 -3
- package/templates/starter/velu.json +4 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Pagefind client — queries the static search index that `velu build` writes to
|
|
2
|
+
// /pagefind/. The index is a chunked, lazy-loaded WASM bundle: the browser only
|
|
3
|
+
// downloads the fragments it needs per query, so search scales to huge sites
|
|
4
|
+
// without shipping one giant index. Generated at build time; absent in dev
|
|
5
|
+
// (the dev preview shows search as "unavailable").
|
|
6
|
+
//
|
|
7
|
+
// The runtime is loaded via a NON-analyzable dynamic import (a Function-wrapped
|
|
8
|
+
// import) so neither Vite nor the bundler tries to resolve /pagefind/pagefind.js
|
|
9
|
+
// at build time — it only exists in the deployed output.
|
|
10
|
+
|
|
11
|
+
let _pf = null;
|
|
12
|
+
let _loading = null;
|
|
13
|
+
|
|
14
|
+
async function loadRuntime() {
|
|
15
|
+
if (_pf) return _pf;
|
|
16
|
+
if (_loading) return _loading;
|
|
17
|
+
_loading = (async () => {
|
|
18
|
+
const importer = new Function('p', 'return import(p)');
|
|
19
|
+
const mod = await importer('/pagefind/pagefind.js');
|
|
20
|
+
if (typeof mod.init === 'function') await mod.init();
|
|
21
|
+
_pf = mod;
|
|
22
|
+
return mod;
|
|
23
|
+
})();
|
|
24
|
+
try {
|
|
25
|
+
return await _loading;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
_loading = null;
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stripHtml = (s) => String(s || '').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
33
|
+
|
|
34
|
+
// Pagefind returns directory URLs (/quickstart/, /a/b/, /); normalize to the
|
|
35
|
+
// router's path form (/quickstart, /a/b, /). Preserve any #anchor.
|
|
36
|
+
function normalizeUrl(url) {
|
|
37
|
+
if (!url) return '/';
|
|
38
|
+
let [path, hash] = String(url).split('#');
|
|
39
|
+
path = path.replace(/index\.html?$/i, '').replace(/\.html?$/i, '');
|
|
40
|
+
if (path.length > 1) path = path.replace(/\/+$/, '');
|
|
41
|
+
if (!path) path = '/';
|
|
42
|
+
return hash ? `${path}#${hash}` : path;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Breadcrumb from the URL path (everything but the leaf), title-cased.
|
|
46
|
+
function crumbsFromUrl(url) {
|
|
47
|
+
const segs = String(url).split('#')[0].split('/').filter(Boolean);
|
|
48
|
+
segs.pop();
|
|
49
|
+
return segs.map((s) => s.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run a query against the static Pagefind index.
|
|
54
|
+
* @returns {Promise<Array<SearchResult>|null>} palette-shaped results, or null
|
|
55
|
+
* when the index can't be loaded (→ caller can show "unavailable").
|
|
56
|
+
*/
|
|
57
|
+
export default async function pagefindSearch(query) {
|
|
58
|
+
const q = String(query || '').trim();
|
|
59
|
+
if (!q) return [];
|
|
60
|
+
|
|
61
|
+
let pf;
|
|
62
|
+
try {
|
|
63
|
+
pf = await loadRuntime();
|
|
64
|
+
} catch {
|
|
65
|
+
return null; // index not present (e.g. not built) → unavailable
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await pf.search(q);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hits = await Promise.all(response.results.slice(0, 8).map((r) => r.data()));
|
|
76
|
+
const out = [];
|
|
77
|
+
const seen = new Set(); // dedupe by href
|
|
78
|
+
for (const hit of hits) {
|
|
79
|
+
const href = normalizeUrl(hit.url);
|
|
80
|
+
const title = hit.meta?.title || crumbsFromUrl(hit.url).pop() || href;
|
|
81
|
+
if (!seen.has(href)) {
|
|
82
|
+
seen.add(href);
|
|
83
|
+
out.push({
|
|
84
|
+
id: href,
|
|
85
|
+
group: 'Pages',
|
|
86
|
+
breadcrumb: crumbsFromUrl(hit.url),
|
|
87
|
+
title,
|
|
88
|
+
kind: 'page',
|
|
89
|
+
desc: stripHtml(hit.excerpt),
|
|
90
|
+
href,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Heading-level matches → deep-linkable anchor rows. Skip the page's own
|
|
94
|
+
// top section (pagefind repeats it as a sub-result with the page title).
|
|
95
|
+
for (const sub of hit.sub_results || []) {
|
|
96
|
+
const subHref = normalizeUrl(sub.url);
|
|
97
|
+
if (seen.has(subHref) || !subHref.includes('#') || sub.title === title) continue;
|
|
98
|
+
seen.add(subHref);
|
|
99
|
+
out.push({
|
|
100
|
+
id: subHref,
|
|
101
|
+
group: 'Pages',
|
|
102
|
+
breadcrumb: [title],
|
|
103
|
+
title: sub.title || title,
|
|
104
|
+
kind: 'anchor',
|
|
105
|
+
desc: stripHtml(sub.excerpt),
|
|
106
|
+
href: subHref,
|
|
107
|
+
});
|
|
108
|
+
if (out.length >= 12) break;
|
|
109
|
+
}
|
|
110
|
+
if (out.length >= 12) break;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
@import './primitives/switcher.css';
|
|
22
22
|
@import './components/sidebar.css';
|
|
23
23
|
@import './components/nav-select.css';
|
|
24
|
+
@import './components/context-menu.css';
|
|
24
25
|
@import './components/accordion.css';
|
|
25
26
|
@import './components/card.css';
|
|
26
27
|
@import './components/image.css';
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
@import './components/tree.css';
|
|
32
33
|
@import './components/api.css';
|
|
33
34
|
@import './components/api-page.css';
|
|
35
|
+
@import './components/not-found.css';
|
|
34
36
|
@import './components/ask-bar.css';
|
|
35
37
|
@import './components/chatbot.css';
|
|
36
38
|
@import './components/page-feedback.css';
|
package/schema/velu.schema.json
CHANGED
|
@@ -40,43 +40,7 @@
|
|
|
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
|
+
"api": { "$ref": "#/$defs/api" },
|
|
80
44
|
"logo": {
|
|
81
45
|
"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
46
|
"oneOf": [
|
|
@@ -206,6 +170,78 @@
|
|
|
206
170
|
"navigation": {
|
|
207
171
|
"$ref": "#/$defs/navContainer",
|
|
208
172
|
"description": "Site navigation. Mintlify-compatible: products > versions > languages > tabs > groups > pages. Switchable axes (product/version/language) become URL path prefixes; the default value of each is unprefixed. Dropdowns are intentionally unsupported."
|
|
173
|
+
},
|
|
174
|
+
"url": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"description": "Production site origin (e.g. \"https://docs.example.com\"), no trailing slash. Used by the static build for canonical URLs, og:url, and sitemap.xml, and by llms.txt for absolute links."
|
|
177
|
+
},
|
|
178
|
+
"thumbnails": {
|
|
179
|
+
"type": "object",
|
|
180
|
+
"additionalProperties": false,
|
|
181
|
+
"description": "Auto-generated OG (social share) image options. Build-only.",
|
|
182
|
+
"properties": {
|
|
183
|
+
"background": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Project-relative path to a background image for the OG cards (e.g. \"/images/og-background.png\"). Defaults to a brand-color glow."
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"seo": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"additionalProperties": false,
|
|
192
|
+
"description": "Search-engine optimization. Build-only: meta tags and sitemap behaviour applied when the site is built (not in the dev preview).",
|
|
193
|
+
"properties": {
|
|
194
|
+
"metatags": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"description": "Extra meta tags injected on every page, as a flat key→value map (e.g. \"google-site-verification\", \"og:image\").",
|
|
197
|
+
"additionalProperties": { "type": "string" }
|
|
198
|
+
},
|
|
199
|
+
"indexing": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"enum": ["navigable", "all"],
|
|
202
|
+
"description": "\"navigable\" (default) indexes only pages in navigation; \"all\" also includes hidden pages."
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"contextual": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"additionalProperties": false,
|
|
209
|
+
"description": "Per-page context menu — the \"Copy Page\" split-button + dropdown of agent/IDE actions shown at the top of each page. Mintlify-compatible. Unset → copy, view, chatgpt, claude.",
|
|
210
|
+
"required": ["options"],
|
|
211
|
+
"properties": {
|
|
212
|
+
"options": {
|
|
213
|
+
"type": "array",
|
|
214
|
+
"description": "Menu items, in order. Known string options, or custom { title, description, href } entries.",
|
|
215
|
+
"items": {
|
|
216
|
+
"oneOf": [
|
|
217
|
+
{
|
|
218
|
+
"type": "string",
|
|
219
|
+
"enum": [
|
|
220
|
+
"copy", "assistant", "view", "download-pdf", "download-spec",
|
|
221
|
+
"chatgpt", "claude", "perplexity", "grok", "aistudio", "devin", "windsurf",
|
|
222
|
+
"mcp", "add-mcp", "cursor", "vscode", "devin-mcp"
|
|
223
|
+
]
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"type": "object",
|
|
227
|
+
"additionalProperties": false,
|
|
228
|
+
"required": ["title", "href"],
|
|
229
|
+
"properties": {
|
|
230
|
+
"title": { "type": "string", "description": "Menu item label." },
|
|
231
|
+
"description": { "type": "string", "description": "Secondary line under the label." },
|
|
232
|
+
"href": { "type": "string", "description": "URL to open." },
|
|
233
|
+
"icon": { "type": "string", "description": "Lucide icon id." }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"display": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"enum": ["header", "toc"],
|
|
242
|
+
"description": "Where the menu renders. Default \"header\"."
|
|
243
|
+
}
|
|
244
|
+
}
|
|
209
245
|
}
|
|
210
246
|
},
|
|
211
247
|
"$defs": {
|
|
@@ -243,6 +279,7 @@
|
|
|
243
279
|
"expanded": { "type": "boolean", "description": "Start expanded." },
|
|
244
280
|
"root": { "type": "string", "description": "Landing page path for the group." },
|
|
245
281
|
"openapi": { "$ref": "#/$defs/openapiRef" },
|
|
282
|
+
"api": { "$ref": "#/$defs/api" },
|
|
246
283
|
"pages": { "$ref": "#/$defs/pages" }
|
|
247
284
|
}
|
|
248
285
|
},
|
|
@@ -255,11 +292,49 @@
|
|
|
255
292
|
"icon": { "type": "string" },
|
|
256
293
|
"href": { "type": "string", "description": "External/override link instead of in-site pages." },
|
|
257
294
|
"openapi": { "$ref": "#/$defs/openapiRef" },
|
|
295
|
+
"api": { "$ref": "#/$defs/api" },
|
|
258
296
|
"anchors": { "type": "array", "items": { "$ref": "#/$defs/anchor" } },
|
|
259
297
|
"groups": { "type": "array", "items": { "$ref": "#/$defs/group" } },
|
|
260
298
|
"pages": { "$ref": "#/$defs/pages" }
|
|
261
299
|
}
|
|
262
300
|
},
|
|
301
|
+
"api": {
|
|
302
|
+
"type": "object",
|
|
303
|
+
"additionalProperties": false,
|
|
304
|
+
"description": "API reference behaviour for OpenAPI-generated pages. Set at the root, or on a tab/group with `openapi` to override it for just that section.",
|
|
305
|
+
"properties": {
|
|
306
|
+
"server": {
|
|
307
|
+
"type": "string",
|
|
308
|
+
"description": "Base URL override for the playground + code samples (otherwise the spec's first server is used)."
|
|
309
|
+
},
|
|
310
|
+
"playground": {
|
|
311
|
+
"type": "object",
|
|
312
|
+
"additionalProperties": false,
|
|
313
|
+
"properties": {
|
|
314
|
+
"display": {
|
|
315
|
+
"type": "string",
|
|
316
|
+
"enum": ["interactive", "simple", "none"],
|
|
317
|
+
"description": "Playground mode. \"none\" disables the proxy / live send."
|
|
318
|
+
},
|
|
319
|
+
"proxy": {
|
|
320
|
+
"type": "boolean",
|
|
321
|
+
"description": "Send Try-It requests through the dev proxy to avoid CORS (default true)."
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
"examples": {
|
|
326
|
+
"type": "object",
|
|
327
|
+
"additionalProperties": false,
|
|
328
|
+
"properties": {
|
|
329
|
+
"languages": {
|
|
330
|
+
"type": "array",
|
|
331
|
+
"items": { "type": "string", "enum": ["curl", "javascript", "python", "ruby"] },
|
|
332
|
+
"description": "Which request-snippet languages to generate."
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
},
|
|
263
338
|
"openapiRef": {
|
|
264
339
|
"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
340
|
"oneOf": [
|
package/src/runtime/App.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Routes, Route, useLocation, Link } from 'react-router-dom';
|
|
2
|
+
import { Routes, Route, useLocation, useNavigate, Link } from 'react-router-dom';
|
|
3
3
|
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
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
NavSelect,
|
|
15
15
|
Toc,
|
|
16
16
|
TocBar,
|
|
17
|
+
ContextMenu,
|
|
17
18
|
Callout,
|
|
18
19
|
Accordion,
|
|
19
20
|
AccordionGroup,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
defaultMdxComponents,
|
|
39
40
|
resolveIcon,
|
|
40
41
|
Search,
|
|
42
|
+
pagefindSearch,
|
|
41
43
|
Logo,
|
|
42
44
|
SocialLinks,
|
|
43
45
|
Tree,
|
|
@@ -48,6 +50,7 @@ import {
|
|
|
48
50
|
ApiField,
|
|
49
51
|
ApiSidebar,
|
|
50
52
|
ApiSamples,
|
|
53
|
+
NotFound,
|
|
51
54
|
VeluMark,
|
|
52
55
|
} from 'velu-ui';
|
|
53
56
|
import { X, ChevronDown, ChevronUp } from 'lucide-react';
|
|
@@ -717,6 +720,7 @@ function DocsPage() {
|
|
|
717
720
|
// SSR and client compute these from the same pathname + the same
|
|
718
721
|
// shared `resolve()`, so the render is identical (hydration-safe).
|
|
719
722
|
const location = useLocation();
|
|
723
|
+
const navigate = useNavigate();
|
|
720
724
|
const pathname = normalizeUrl(location.pathname);
|
|
721
725
|
const entry = pages[pathname];
|
|
722
726
|
const nav = React.useMemo(
|
|
@@ -724,9 +728,28 @@ function DocsPage() {
|
|
|
724
728
|
[pathname],
|
|
725
729
|
);
|
|
726
730
|
|
|
731
|
+
// Search source: Pagefind for content/excerpts, but resolve each result's
|
|
732
|
+
// breadcrumb from the real NAVIGATION (a page's nav group can differ from its
|
|
733
|
+
// URL path — e.g. essentials/markdown.mdx lives under the "Writing Content"
|
|
734
|
+
// group). Page rows show the path to the page; anchor rows include the page.
|
|
735
|
+
const searchDocs = React.useCallback(async (q) => {
|
|
736
|
+
const results = await pagefindSearch(q);
|
|
737
|
+
if (!Array.isArray(results)) return results;
|
|
738
|
+
return results.map((r) => {
|
|
739
|
+
const res = resolve(r.href.split('#')[0], navigation, pages);
|
|
740
|
+
if (!res?.breadcrumb?.length) return r;
|
|
741
|
+
const labels = res.breadcrumb.map((c) => c.label);
|
|
742
|
+
const crumbs = r.kind === 'anchor' ? labels : labels.slice(0, -1);
|
|
743
|
+
return crumbs.length ? { ...r, breadcrumb: crumbs } : r;
|
|
744
|
+
});
|
|
745
|
+
}, []);
|
|
746
|
+
|
|
727
747
|
const frontmatter = entry?.frontmatter ?? {};
|
|
728
748
|
const PageComponent = entry?.Component ?? null;
|
|
729
749
|
const pageToc = entry?.toc ?? [];
|
|
750
|
+
// No renderable page for this route → show the 404 page (a clean centered
|
|
751
|
+
// takeover: header + footer stay, the docs sidebar/TOC are hidden).
|
|
752
|
+
const isNotFound = !PageComponent;
|
|
730
753
|
|
|
731
754
|
// Switcher option sets (only render a switcher when an axis has >1
|
|
732
755
|
// option). Anchors are pinned sidebar links shown in the context zone.
|
|
@@ -763,6 +786,27 @@ function DocsPage() {
|
|
|
763
786
|
/>
|
|
764
787
|
);
|
|
765
788
|
|
|
789
|
+
// Keep document.title / meta description / <html lang> in sync on client-side
|
|
790
|
+
// navigation. The HTML is SSR'd with a per-page (build) or generic (dev) title;
|
|
791
|
+
// without this the tab title would stay frozen on the first page as you
|
|
792
|
+
// navigate the SPA, and JS-executing crawlers would read the stale value.
|
|
793
|
+
React.useEffect(() => {
|
|
794
|
+
const t = frontmatter.title;
|
|
795
|
+
document.title = isNotFound
|
|
796
|
+
? `Page not found - ${site.name}`
|
|
797
|
+
: t && t !== site.name
|
|
798
|
+
? `${t} - ${site.name}`
|
|
799
|
+
: site.name;
|
|
800
|
+
let descTag = document.querySelector('meta[name="description"]');
|
|
801
|
+
if (!descTag) {
|
|
802
|
+
descTag = document.createElement('meta');
|
|
803
|
+
descTag.setAttribute('name', 'description');
|
|
804
|
+
document.head.appendChild(descTag);
|
|
805
|
+
}
|
|
806
|
+
descTag.setAttribute('content', frontmatter.description || '');
|
|
807
|
+
document.documentElement.lang = nav?.activeLanguageCode || 'en';
|
|
808
|
+
}, [pathname, isNotFound, frontmatter.title, frontmatter.description, nav?.activeLanguageCode]);
|
|
809
|
+
|
|
766
810
|
// Frontmatter title needs an id so scroll-spy + click-to-scroll work
|
|
767
811
|
// against it like any other heading.
|
|
768
812
|
const pageId = React.useMemo(() => {
|
|
@@ -1059,6 +1103,7 @@ function DocsPage() {
|
|
|
1059
1103
|
data-sidebar-open={sidebarOpen ? 'true' : 'false'}
|
|
1060
1104
|
data-drawer-open={drawerOpen ? 'true' : 'false'}
|
|
1061
1105
|
data-api={entry?.api ? 'true' : 'false'}
|
|
1106
|
+
data-not-found={isNotFound ? 'true' : undefined}
|
|
1062
1107
|
>
|
|
1063
1108
|
{/* Scrim — visible at mobile while the drawer OR the chatbot
|
|
1064
1109
|
sheet is open. Sits between the article (z-0) and the
|
|
@@ -1089,7 +1134,12 @@ function DocsPage() {
|
|
|
1089
1134
|
tabsTrailing={languageSwitcher || undefined}
|
|
1090
1135
|
center={
|
|
1091
1136
|
<Cluster space="var(--s-6)" align="center">
|
|
1092
|
-
<Search
|
|
1137
|
+
<Search
|
|
1138
|
+
style={{ inlineSize: '30ch' }}
|
|
1139
|
+
unavailable={IS_DEV_PREVIEW}
|
|
1140
|
+
search={IS_DEV_PREVIEW ? undefined : searchDocs}
|
|
1141
|
+
onSelect={(item) => item.href && navigate(item.href)}
|
|
1142
|
+
/>
|
|
1093
1143
|
{/* Ask AI talks to the deployed site's AI backend — hidden in
|
|
1094
1144
|
the local dev preview where there's nothing to talk to. */}
|
|
1095
1145
|
{!IS_DEV_PREVIEW && (
|
|
@@ -1249,6 +1299,11 @@ function DocsPage() {
|
|
|
1249
1299
|
)}
|
|
1250
1300
|
</Stack>
|
|
1251
1301
|
)}
|
|
1302
|
+
{/* Hairline separating the top anchor links from the nav sections
|
|
1303
|
+
(faithful to the sidebar design's anchor↔sidebar divider). */}
|
|
1304
|
+
{anchors.length > 0 && (
|
|
1305
|
+
<div className="velu-docs-context-divider" aria-hidden="true" />
|
|
1306
|
+
)}
|
|
1252
1307
|
{/* Only this region scrolls — the context zone above stays
|
|
1253
1308
|
pinned. The up/down arrows overlay its top/bottom edges and
|
|
1254
1309
|
appear (via the data-fade-* attrs the scroll handler sets)
|
|
@@ -1353,6 +1408,21 @@ function DocsPage() {
|
|
|
1353
1408
|
and the right margin collapses to 0 while the chatbot is
|
|
1354
1409
|
open via [data-chat-open="true"]). */}
|
|
1355
1410
|
<div className="velu-docs-layout__center">
|
|
1411
|
+
{isNotFound ? (
|
|
1412
|
+
<main className="velu-404-main">
|
|
1413
|
+
<NotFound
|
|
1414
|
+
homeHref="/"
|
|
1415
|
+
linkComponent={RouterLink}
|
|
1416
|
+
onSearch={() =>
|
|
1417
|
+
window.dispatchEvent(
|
|
1418
|
+
new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, metaKey: true }),
|
|
1419
|
+
)
|
|
1420
|
+
}
|
|
1421
|
+
onAskAI={IS_DEV_PREVIEW ? undefined : () => askAI('')}
|
|
1422
|
+
/>
|
|
1423
|
+
</main>
|
|
1424
|
+
) : (
|
|
1425
|
+
<>
|
|
1356
1426
|
{/* Narrow-layout TOC bar — always in DOM, only visible at
|
|
1357
1427
|
< 1024px (toggled by @container in toc-bar.css). Shares
|
|
1358
1428
|
its `items` + `activeId` + `onSelect` API with the
|
|
@@ -1395,7 +1465,25 @@ function DocsPage() {
|
|
|
1395
1465
|
})}
|
|
1396
1466
|
</span>
|
|
1397
1467
|
</button>
|
|
1398
|
-
<div className="velu-docs-layout__article">
|
|
1468
|
+
<div className="velu-docs-layout__article" data-pagefind-body="">
|
|
1469
|
+
{/* Per-page agent/IDE action bar: the section eyebrow + a
|
|
1470
|
+
"Copy Page" split-button whose dropdown is driven by the
|
|
1471
|
+
Mintlify-compatible `contextual` config. Renders nothing
|
|
1472
|
+
when there's neither an eyebrow nor any enabled options. */}
|
|
1473
|
+
<ContextMenu
|
|
1474
|
+
eyebrow={
|
|
1475
|
+
nav?.breadcrumb && nav.breadcrumb.length > 1
|
|
1476
|
+
? nav.breadcrumb[nav.breadcrumb.length - 2].label
|
|
1477
|
+
: undefined
|
|
1478
|
+
}
|
|
1479
|
+
pageUrl={pathname}
|
|
1480
|
+
title={frontmatter.title}
|
|
1481
|
+
isApi={entry?.api === true}
|
|
1482
|
+
siteName={site.name}
|
|
1483
|
+
options={site.contextual?.options ?? []}
|
|
1484
|
+
onAssistant={IS_DEV_PREVIEW ? undefined : () => askAI('')}
|
|
1485
|
+
/>
|
|
1486
|
+
|
|
1399
1487
|
{/* Page hero from MDX frontmatter. `.velu-hero` rules
|
|
1400
1488
|
(in base.css) keep the title and description tightly
|
|
1401
1489
|
grouped and create a clear break before the prose body. */}
|
|
@@ -1494,6 +1582,8 @@ function DocsPage() {
|
|
|
1494
1582
|
)}
|
|
1495
1583
|
</div>
|
|
1496
1584
|
</main>
|
|
1585
|
+
</>
|
|
1586
|
+
)}
|
|
1497
1587
|
</div>
|
|
1498
1588
|
|
|
1499
1589
|
{/* Full site footer — only when the config provides link columns. Spans
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://veludocs.com/velu.schema.json",
|
|
3
3
|
"name": "Starter",
|
|
4
|
+
"url": "https://veludocs.com",
|
|
4
5
|
"colors": {
|
|
5
6
|
"primary": "#dc143c"
|
|
6
7
|
},
|
|
7
8
|
"favicon": "/favicon.svg",
|
|
9
|
+
"contextual": {
|
|
10
|
+
"options": ["copy", "view", "chatgpt", "claude", "mcp", "cursor", "vscode"]
|
|
11
|
+
},
|
|
8
12
|
"navigation": {
|
|
9
13
|
"anchors": [
|
|
10
14
|
{ "anchor": "Documentation", "icon": "book-open", "href": "https://veludocs.com" },
|