@veluai/velu 0.2.19 → 0.2.23
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 +52 -35
- package/package.json +64 -64
- package/runtime/velu-ui/components/ChangelogFilters.jsx +75 -0
- package/runtime/velu-ui/components/Chatbot.jsx +125 -38
- package/runtime/velu-ui/components/ContextMenu.jsx +45 -16
- package/runtime/velu-ui/components/Update.jsx +92 -0
- package/runtime/velu-ui/components/changelog-filters.css +43 -0
- package/runtime/velu-ui/components/chatbot.css +66 -1
- package/runtime/velu-ui/components/context-menu.css +26 -0
- package/runtime/velu-ui/components/page-feedback.css +20 -0
- package/runtime/velu-ui/components/update.css +110 -0
- package/runtime/velu-ui/index.js +2 -0
- package/runtime/velu-ui/lib/docs-assistant.js +26 -3
- package/runtime/velu-ui/lib/pagefind.js +4 -1
- package/runtime/velu-ui/mdx-components.jsx +2 -0
- package/runtime/velu-ui/styles.css +2 -0
- package/schema/velu.schema.json +0 -4
- package/src/lib/known-components.js +1 -0
- package/src/runtime/App.jsx +49 -6
- package/src/runtime/client-entry.jsx +6 -1
- package/src/runtime/server-entry.jsx +9 -1
- package/templates/starter/index.mdx +4 -4
- package/templates/starter/velu.json +0 -1
|
@@ -385,6 +385,63 @@
|
|
|
385
385
|
border-radius: var(--radius-sm);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/* Rendered markdown in answers */
|
|
389
|
+
.velu-chatbot__answer strong {
|
|
390
|
+
font-weight: 600;
|
|
391
|
+
}
|
|
392
|
+
.velu-chatbot__answer em {
|
|
393
|
+
font-style: italic;
|
|
394
|
+
}
|
|
395
|
+
.velu-chatbot__answer a:not(.velu-chatbot__cite) {
|
|
396
|
+
color: var(--accent-color);
|
|
397
|
+
text-decoration: underline;
|
|
398
|
+
text-underline-offset: 2px;
|
|
399
|
+
}
|
|
400
|
+
.velu-chatbot__answer h3,
|
|
401
|
+
.velu-chatbot__answer h4,
|
|
402
|
+
.velu-chatbot__answer h5,
|
|
403
|
+
.velu-chatbot__answer h6 {
|
|
404
|
+
margin: var(--s-2) 0 var(--s-4);
|
|
405
|
+
font-weight: 600;
|
|
406
|
+
line-height: var(--lh-heading, 1.3);
|
|
407
|
+
}
|
|
408
|
+
.velu-chatbot__answer h3 { font-size: var(--f-h5); }
|
|
409
|
+
.velu-chatbot__answer h4 { font-size: var(--f-h6); }
|
|
410
|
+
.velu-chatbot__answer h5,
|
|
411
|
+
.velu-chatbot__answer h6 { font-size: var(--f-h7); }
|
|
412
|
+
.velu-chatbot__answer ul,
|
|
413
|
+
.velu-chatbot__answer ol {
|
|
414
|
+
margin: 0 0 var(--s-3);
|
|
415
|
+
padding-inline-start: var(--s-1);
|
|
416
|
+
}
|
|
417
|
+
.velu-chatbot__answer li {
|
|
418
|
+
margin: 0 0 var(--s-5);
|
|
419
|
+
}
|
|
420
|
+
.velu-chatbot__answer li::marker {
|
|
421
|
+
color: var(--muted-color);
|
|
422
|
+
}
|
|
423
|
+
.velu-chatbot__pre {
|
|
424
|
+
margin: 0 0 var(--s-3);
|
|
425
|
+
padding: var(--s-3);
|
|
426
|
+
overflow-x: auto;
|
|
427
|
+
background: var(--surface-color);
|
|
428
|
+
border: var(--border-width) solid var(--border-color);
|
|
429
|
+
border-radius: var(--radius-sm);
|
|
430
|
+
}
|
|
431
|
+
.velu-chatbot__pre code {
|
|
432
|
+
padding: 0;
|
|
433
|
+
background: none;
|
|
434
|
+
border: none;
|
|
435
|
+
font-size: var(--f-h7);
|
|
436
|
+
white-space: pre;
|
|
437
|
+
}
|
|
438
|
+
.velu-chatbot__answer blockquote {
|
|
439
|
+
margin: 0 0 var(--s-3);
|
|
440
|
+
padding-inline-start: var(--s-3);
|
|
441
|
+
border-inline-start: 2px solid var(--border-color);
|
|
442
|
+
color: var(--muted-color);
|
|
443
|
+
}
|
|
444
|
+
|
|
388
445
|
/* Inline citation chip */
|
|
389
446
|
.velu-chatbot__cite {
|
|
390
447
|
display: inline-flex;
|
|
@@ -510,7 +567,10 @@
|
|
|
510
567
|
opacity: 0;
|
|
511
568
|
transition: opacity 0.12s ease;
|
|
512
569
|
}
|
|
513
|
-
.velu-chatbot__msg--ai:hover .velu-chatbot__msg-actions
|
|
570
|
+
.velu-chatbot__msg--ai:hover .velu-chatbot__msg-actions,
|
|
571
|
+
/* Keep the row visible once a vote is cast, so the selection stays
|
|
572
|
+
confirmed after the pointer leaves the message. */
|
|
573
|
+
.velu-chatbot__msg-actions:has(.is-active) {
|
|
514
574
|
opacity: 1;
|
|
515
575
|
}
|
|
516
576
|
.velu-chatbot__msg-action {
|
|
@@ -530,6 +590,11 @@
|
|
|
530
590
|
background: var(--surface-color);
|
|
531
591
|
color: var(--text-color);
|
|
532
592
|
}
|
|
593
|
+
/* Chosen thumb (up/down) — accent tint so the click is clearly registered. */
|
|
594
|
+
.velu-chatbot__msg-action.is-active {
|
|
595
|
+
background: color-mix(in srgb, var(--accent-color) 12%, transparent);
|
|
596
|
+
color: var(--accent-color);
|
|
597
|
+
}
|
|
533
598
|
|
|
534
599
|
/* ── Composer ───────────────────────────────────────────────────────── */
|
|
535
600
|
.velu-chatbot__composer {
|
|
@@ -23,11 +23,37 @@
|
|
|
23
23
|
color: var(--accent-color);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
.velu-context-bar__actions {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--s-2);
|
|
30
|
+
flex: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
.velu-context-menu {
|
|
27
34
|
position: relative;
|
|
28
35
|
flex: none;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/* RSS feed link — a round icon button next to "Copy Page" (changelog pages). */
|
|
39
|
+
.velu-context-menu__rss {
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
inline-size: 2.1rem;
|
|
44
|
+
block-size: 2.1rem;
|
|
45
|
+
border: var(--border-width) solid var(--border-color);
|
|
46
|
+
border-radius: 999px;
|
|
47
|
+
color: var(--muted-color);
|
|
48
|
+
background: var(--page-bg);
|
|
49
|
+
transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
|
|
50
|
+
}
|
|
51
|
+
.velu-context-menu__rss:hover {
|
|
52
|
+
color: var(--accent-color);
|
|
53
|
+
border-color: var(--accent-color);
|
|
54
|
+
background: color-mix(in srgb, var(--accent-color) 8%, transparent);
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
/* The split button: "Copy Page" + a divider + a chevron toggle, joined as one
|
|
32
58
|
segmented control. */
|
|
33
59
|
.velu-context-menu__split {
|
|
@@ -222,6 +222,26 @@
|
|
|
222
222
|
.velu-feedback__heart--3 {
|
|
223
223
|
color: color-mix(in srgb, var(--accent-color) 82%, #fff);
|
|
224
224
|
}
|
|
225
|
+
/* Register the per-heart custom props so they're typed + animatable inside the
|
|
226
|
+
keyframes. WebKit (newer iOS Safari) won't interpolate UNregistered custom
|
|
227
|
+
properties used in @keyframes transforms — without this the hearts never move
|
|
228
|
+
and the burst is invisible. Browsers without @property ignore these rules and
|
|
229
|
+
fall back to the inline values (the prior behaviour). */
|
|
230
|
+
@property --hb-tx {
|
|
231
|
+
syntax: "<length>";
|
|
232
|
+
inherits: false;
|
|
233
|
+
initial-value: 0px;
|
|
234
|
+
}
|
|
235
|
+
@property --hb-ty {
|
|
236
|
+
syntax: "<length>";
|
|
237
|
+
inherits: false;
|
|
238
|
+
initial-value: 0px;
|
|
239
|
+
}
|
|
240
|
+
@property --hb-rot {
|
|
241
|
+
syntax: "<angle>";
|
|
242
|
+
inherits: false;
|
|
243
|
+
initial-value: 0deg;
|
|
244
|
+
}
|
|
225
245
|
@keyframes velu-feedback-hearts {
|
|
226
246
|
0% {
|
|
227
247
|
transform: translate(-50%, 0) rotate(0deg) scale(0.6);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* Update — changelog entries. Two-column rows: a sticky left rail with the
|
|
2
|
+
date/label pill, and the entry body on the right. All values are tokens;
|
|
3
|
+
light/dark via [data-theme]. */
|
|
4
|
+
|
|
5
|
+
.velu-update {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: minmax(8rem, 11rem) minmax(0, 1fr);
|
|
8
|
+
column-gap: var(--s1);
|
|
9
|
+
row-gap: var(--s-1);
|
|
10
|
+
align-items: start;
|
|
11
|
+
margin-block: var(--s1) var(--s3);
|
|
12
|
+
/* Clear the fixed header when an entry is jumped to via its anchor. */
|
|
13
|
+
scroll-margin-top: calc(var(--velu-header-height) + var(--s0));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.velu-update__meta {
|
|
17
|
+
position: sticky;
|
|
18
|
+
/* Stick just below the fixed header instead of sliding behind it. */
|
|
19
|
+
top: calc(var(--velu-header-height) + var(--s0));
|
|
20
|
+
align-self: start;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--s-2);
|
|
24
|
+
padding-left: var(--s0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.velu-update__anchor {
|
|
28
|
+
position: absolute;
|
|
29
|
+
left: 0;
|
|
30
|
+
top: 0.3rem;
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
/* Hidden until the entry is hovered/focused (like heading anchors). */
|
|
33
|
+
color: var(--muted-color);
|
|
34
|
+
opacity: 0;
|
|
35
|
+
transition: opacity 0.15s ease, color 0.15s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.velu-update:hover .velu-update__anchor,
|
|
39
|
+
.velu-update:focus-within .velu-update__anchor {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
color: var(--accent-color);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.velu-update__label {
|
|
45
|
+
width: fit-content;
|
|
46
|
+
border-radius: 999px;
|
|
47
|
+
padding: 0.2rem 0.6rem;
|
|
48
|
+
font-size: var(--f-h6);
|
|
49
|
+
font-weight: var(--weight-semibold);
|
|
50
|
+
line-height: 1.2;
|
|
51
|
+
color: var(--page-bg);
|
|
52
|
+
background: var(--accent-color);
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.velu-update__label:hover {
|
|
57
|
+
background: color-mix(in srgb, var(--accent-color) 85%, black);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.velu-update__description {
|
|
61
|
+
color: var(--muted-color);
|
|
62
|
+
font-size: var(--f-h6);
|
|
63
|
+
line-height: var(--lh-h6);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.velu-update__tags {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
gap: var(--s-2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.velu-update__tag {
|
|
73
|
+
font-size: var(--f-small, 0.72rem);
|
|
74
|
+
color: var(--muted-color);
|
|
75
|
+
border: var(--border-width) solid var(--border-color);
|
|
76
|
+
border-radius: 999px;
|
|
77
|
+
padding: 0.1rem 0.45rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.velu-update__content {
|
|
81
|
+
min-width: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.velu-update__content > :first-child {
|
|
85
|
+
margin-top: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.velu-update__content > :last-child {
|
|
89
|
+
margin-bottom: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Hidden by a tag filter (DOM-driven). */
|
|
93
|
+
.velu-update[hidden] {
|
|
94
|
+
display: none !important;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (max-width: 768px) {
|
|
98
|
+
.velu-update {
|
|
99
|
+
grid-template-columns: minmax(0, 1fr);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.velu-update__meta {
|
|
103
|
+
position: static;
|
|
104
|
+
padding-left: var(--s2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.velu-update__anchor {
|
|
108
|
+
left: 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/runtime/velu-ui/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { default as Field } from './components/Field.jsx';
|
|
|
21
21
|
export { default as Prompt } from './components/Prompt.jsx';
|
|
22
22
|
export { default as Steps, Step } from './components/Steps.jsx';
|
|
23
23
|
export { default as Tree, Folder, File } from './components/Tree.jsx';
|
|
24
|
+
export { default as Update } from './components/Update.jsx';
|
|
25
|
+
export { default as ChangelogFilters } from './components/ChangelogFilters.jsx';
|
|
24
26
|
export { default as MethodBadge } from './components/MethodBadge.jsx';
|
|
25
27
|
export { default as ApiPath } from './components/ApiPath.jsx';
|
|
26
28
|
export { default as TryItBar } from './components/TryItBar.jsx';
|
|
@@ -91,6 +91,11 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
91
91
|
if (!base) return null;
|
|
92
92
|
const root = `${base}/api/v1/public/ai-assistant`;
|
|
93
93
|
let bootstrapped = false;
|
|
94
|
+
// Whether the docs owner's plan + credit balance allow Ask-AI right now.
|
|
95
|
+
// Set from /bootstrap; the host UI hides the widget when false. Fail-open
|
|
96
|
+
// (stays true) if bootstrap can't be reached so a transient error doesn't
|
|
97
|
+
// wrongly hide a working assistant.
|
|
98
|
+
let askEnabled = true;
|
|
94
99
|
// Highest event seq seen per conversation. The events endpoint replays history
|
|
95
100
|
// after `after_seq`; without this a reused (multi-turn) conversation replays
|
|
96
101
|
// from 0 and the first (old) assistant.completed is returned for every
|
|
@@ -104,15 +109,33 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
104
109
|
|
|
105
110
|
async function bootstrap() {
|
|
106
111
|
if (bootstrapped) return;
|
|
107
|
-
// Sets the httponly visitor cookie used by every subsequent call
|
|
108
|
-
|
|
112
|
+
// Sets the httponly visitor cookie used by every subsequent call, and tells
|
|
113
|
+
// us whether Ask-AI is currently enabled for this site (plan + credits).
|
|
114
|
+
const res = await fetch(`${root}/bootstrap`, {
|
|
109
115
|
method: 'GET',
|
|
110
116
|
credentials: 'include',
|
|
111
117
|
headers: headers(),
|
|
112
118
|
});
|
|
119
|
+
try {
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
if (typeof data?.ask_ai_enabled === 'boolean') askEnabled = data.ask_ai_enabled;
|
|
122
|
+
} catch {
|
|
123
|
+
/* keep fail-open default */
|
|
124
|
+
}
|
|
113
125
|
bootstrapped = true;
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
// Resolve whether the widget should be shown (runs bootstrap once). Returns
|
|
129
|
+
// true on any error so a transient failure never hides a working assistant.
|
|
130
|
+
async function isEnabled() {
|
|
131
|
+
try {
|
|
132
|
+
await bootstrap();
|
|
133
|
+
} catch {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return askEnabled;
|
|
137
|
+
}
|
|
138
|
+
|
|
116
139
|
// Async generator: yields { conversationId } early, then one
|
|
117
140
|
// { delta, citations, messageId, conversationId } when the answer is ready.
|
|
118
141
|
async function* ask(prompt, { conversationId, signal } = {}) {
|
|
@@ -244,7 +267,7 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
244
267
|
return { conversationId, messages };
|
|
245
268
|
}
|
|
246
269
|
|
|
247
|
-
return { ask, sendFeedback, listConversations, loadConversation };
|
|
270
|
+
return { ask, sendFeedback, listConversations, loadConversation, isEnabled };
|
|
248
271
|
}
|
|
249
272
|
|
|
250
273
|
export default createDocsAssistant;
|
|
@@ -16,7 +16,10 @@ async function loadRuntime() {
|
|
|
16
16
|
if (_loading) return _loading;
|
|
17
17
|
_loading = (async () => {
|
|
18
18
|
const importer = new Function('p', 'return import(p)');
|
|
19
|
-
|
|
19
|
+
// BASE_URL is '/' normally, '/docs/' under subpath hosting — the index is
|
|
20
|
+
// emitted relative to the served root, so prefix the runtime URL to match.
|
|
21
|
+
const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
|
22
|
+
const mod = await importer(`${base}/pagefind/pagefind.js`);
|
|
20
23
|
if (typeof mod.init === 'function') await mod.init();
|
|
21
24
|
_pf = mod;
|
|
22
25
|
return mod;
|
|
@@ -7,6 +7,7 @@ import Field from './components/Field.jsx';
|
|
|
7
7
|
import Prompt from './components/Prompt.jsx';
|
|
8
8
|
import Steps, { Step } from './components/Steps.jsx';
|
|
9
9
|
import Tree, { Folder, File } from './components/Tree.jsx';
|
|
10
|
+
import Update from './components/Update.jsx';
|
|
10
11
|
import Image from './components/Image.jsx';
|
|
11
12
|
import CodeBlock, { CodeGroup } from './components/CodeBlock.jsx';
|
|
12
13
|
import MethodBadge from './components/MethodBadge.jsx';
|
|
@@ -91,6 +92,7 @@ export const defaultMdxComponents = {
|
|
|
91
92
|
Tree,
|
|
92
93
|
Folder,
|
|
93
94
|
File,
|
|
95
|
+
Update,
|
|
94
96
|
Image: checked('Image', Image),
|
|
95
97
|
CodeBlock,
|
|
96
98
|
CodeGroup,
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
@import './components/prompt.css';
|
|
31
31
|
@import './components/steps.css';
|
|
32
32
|
@import './components/tree.css';
|
|
33
|
+
@import './components/update.css';
|
|
34
|
+
@import './components/changelog-filters.css';
|
|
33
35
|
@import './components/api.css';
|
|
34
36
|
@import './components/api-page.css';
|
|
35
37
|
@import './components/not-found.css';
|
package/schema/velu.schema.json
CHANGED
|
@@ -171,10 +171,6 @@
|
|
|
171
171
|
"$ref": "#/$defs/navContainer",
|
|
172
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
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
174
|
"thumbnails": {
|
|
179
175
|
"type": "object",
|
|
180
176
|
"additionalProperties": false,
|
package/src/runtime/App.jsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
NavSelect,
|
|
15
15
|
Toc,
|
|
16
16
|
TocBar,
|
|
17
|
+
ChangelogFilters,
|
|
17
18
|
ContextMenu,
|
|
18
19
|
Callout,
|
|
19
20
|
Accordion,
|
|
@@ -64,6 +65,12 @@ import ErrorBoundary from './ErrorBoundary.jsx';
|
|
|
64
65
|
// preview. Identical on SSR + client in each mode, so no hydration mismatch.
|
|
65
66
|
const IS_DEV_PREVIEW = import.meta.env.DEV;
|
|
66
67
|
|
|
68
|
+
// Subpath-hosting prefix (e.g. "/docs"), from Vite's BASE_URL. Empty at the
|
|
69
|
+
// origin root. The router handles internal <Link>s on its own, but server-served
|
|
70
|
+
// resources the router doesn't own — the .md twins, the /mcp endpoint — are
|
|
71
|
+
// root-relative and need this prefix to resolve under a subpath.
|
|
72
|
+
const BASE_PATH = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
|
73
|
+
|
|
67
74
|
// Flatten a sidebar section's items (which may nest into sub-groups) into the
|
|
68
75
|
// flat endpoint list <ApiSidebar> wants. API tabs are flat in practice; this
|
|
69
76
|
// only guards the rare case of a normal nested group living beside generated
|
|
@@ -780,6 +787,19 @@ function DocsPage() {
|
|
|
780
787
|
return createDocsAssistant({ apiBase, host: site.assistant?.host });
|
|
781
788
|
}, []);
|
|
782
789
|
|
|
790
|
+
// Hide the Ask-AI widget when the docs owner's plan/credits disable it (the
|
|
791
|
+
// backend bootstrap returns ask_ai_enabled:false). Only applies to a real
|
|
792
|
+
// configured assistant — the dev-preview/demo widget stays visible.
|
|
793
|
+
const [askAiHidden, setAskAiHidden] = React.useState(false);
|
|
794
|
+
React.useEffect(() => {
|
|
795
|
+
if (!assistant) return undefined;
|
|
796
|
+
let alive = true;
|
|
797
|
+
assistant.isEnabled().then((ok) => {
|
|
798
|
+
if (alive && !ok) setAskAiHidden(true);
|
|
799
|
+
}).catch(() => {});
|
|
800
|
+
return () => { alive = false; };
|
|
801
|
+
}, [assistant]);
|
|
802
|
+
|
|
783
803
|
// Page-feedback backend: reuses the same injected apiBase as the assistant
|
|
784
804
|
// (baked into every published velu.json), so the "Was this page helpful?"
|
|
785
805
|
// widget can POST to /api/v1/public/feedback. Null in the dev preview, where
|
|
@@ -794,6 +814,11 @@ function DocsPage() {
|
|
|
794
814
|
const frontmatter = entry?.frontmatter ?? {};
|
|
795
815
|
const PageComponent = entry?.Component ?? null;
|
|
796
816
|
const pageToc = entry?.toc ?? [];
|
|
817
|
+
// Changelog pages (any page with <Update> entries) carry a `changelog`
|
|
818
|
+
// export. We swap the right-rail TOC for tag filters and expose an RSS link.
|
|
819
|
+
const changelog = entry?.changelog ?? null;
|
|
820
|
+
const isChangelog = !!changelog;
|
|
821
|
+
const changelogTags = changelog?.tags ?? [];
|
|
797
822
|
// No renderable page for this route → show the 404 page (a clean centered
|
|
798
823
|
// takeover: header + footer stay, the docs sidebar/TOC are hidden).
|
|
799
824
|
const isNotFound = !PageComponent;
|
|
@@ -911,6 +936,14 @@ function DocsPage() {
|
|
|
911
936
|
document.removeEventListener('keydown', onKey);
|
|
912
937
|
};
|
|
913
938
|
}, [navOpen]);
|
|
939
|
+
|
|
940
|
+
// Close the mobile drawer (and its nav dropdown) whenever the route changes,
|
|
941
|
+
// so picking a page from the sidebar collapses the drawer instead of leaving
|
|
942
|
+
// it open over the article.
|
|
943
|
+
React.useEffect(() => {
|
|
944
|
+
setDrawerOpen(false);
|
|
945
|
+
setNavOpen(false);
|
|
946
|
+
}, [pathname]);
|
|
914
947
|
// Mobile drawer's section picker = the resolved tabs (top-level
|
|
915
948
|
// navigation), so it mirrors the desktop tabs row instead of a
|
|
916
949
|
// hardcoded list. Its button shows the active tab's label.
|
|
@@ -1197,8 +1230,9 @@ function DocsPage() {
|
|
|
1197
1230
|
onSelect={(item) => item.href && navigate(item.href)}
|
|
1198
1231
|
/>
|
|
1199
1232
|
{/* Ask AI talks to the deployed site's AI backend — hidden in
|
|
1200
|
-
the local dev preview where there's nothing to talk to
|
|
1201
|
-
|
|
1233
|
+
the local dev preview where there's nothing to talk to, and
|
|
1234
|
+
when the owner's plan/credits disable the assistant. */}
|
|
1235
|
+
{!IS_DEV_PREVIEW && !askAiHidden && (
|
|
1202
1236
|
<button
|
|
1203
1237
|
type="button"
|
|
1204
1238
|
className="velu-header__action velu-header__action--outlined"
|
|
@@ -1453,6 +1487,8 @@ function DocsPage() {
|
|
|
1453
1487
|
samples={entry.samples}
|
|
1454
1488
|
responses={entry.operation.responses}
|
|
1455
1489
|
/>
|
|
1490
|
+
) : isChangelog && changelogTags.length ? (
|
|
1491
|
+
<ChangelogFilters tags={changelogTags} />
|
|
1456
1492
|
) : (
|
|
1457
1493
|
<Toc items={toc} activeId={activeId} onSelect={scrollTo} />
|
|
1458
1494
|
)}
|
|
@@ -1474,7 +1510,7 @@ function DocsPage() {
|
|
|
1474
1510
|
new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, metaKey: true }),
|
|
1475
1511
|
)
|
|
1476
1512
|
}
|
|
1477
|
-
onAskAI={IS_DEV_PREVIEW ? undefined : () => askAI('')}
|
|
1513
|
+
onAskAI={IS_DEV_PREVIEW || askAiHidden ? undefined : () => askAI('')}
|
|
1478
1514
|
/>
|
|
1479
1515
|
</main>
|
|
1480
1516
|
) : (
|
|
@@ -1485,6 +1521,8 @@ function DocsPage() {
|
|
|
1485
1521
|
right-rail <Toc> so both views are driven by the same
|
|
1486
1522
|
data; whichever one is visible at a given width responds
|
|
1487
1523
|
to the same scroll-spy state. */}
|
|
1524
|
+
{/* Changelog tag filters are a wide-layout (right-rail) affordance only;
|
|
1525
|
+
on tab/mobile we keep the normal TOC bar (no filters). */}
|
|
1488
1526
|
<TocBar items={toc} activeId={activeId} onSelect={scrollTo} />
|
|
1489
1527
|
<main
|
|
1490
1528
|
className="velu-docs-layout__main"
|
|
@@ -1533,12 +1571,14 @@ function DocsPage() {
|
|
|
1533
1571
|
: undefined
|
|
1534
1572
|
}
|
|
1535
1573
|
pageUrl={pathname}
|
|
1574
|
+
basePath={BASE_PATH}
|
|
1536
1575
|
title={frontmatter.title}
|
|
1537
1576
|
isApi={entry?.api === true}
|
|
1538
1577
|
siteName={site.name}
|
|
1539
1578
|
options={site.contextual?.options ?? []}
|
|
1540
|
-
onAssistant={IS_DEV_PREVIEW ? undefined : () => askAI('')}
|
|
1579
|
+
onAssistant={IS_DEV_PREVIEW || askAiHidden ? undefined : () => askAI('')}
|
|
1541
1580
|
getMarkdown={entry?.getSource}
|
|
1581
|
+
rssHref={isChangelog ? `${BASE_PATH}/rss.xml` : undefined}
|
|
1542
1582
|
/>
|
|
1543
1583
|
|
|
1544
1584
|
{/* Page hero from MDX frontmatter. `.velu-hero` rules
|
|
@@ -1614,7 +1654,7 @@ function DocsPage() {
|
|
|
1614
1654
|
approaches (see IntersectionObserver above). */}
|
|
1615
1655
|
{/* Ask-a-question bar — opens the AI chatbot, which runs on the
|
|
1616
1656
|
deployed site. Hidden in the local dev preview. */}
|
|
1617
|
-
{!IS_DEV_PREVIEW && !chatOpen && (
|
|
1657
|
+
{!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && (
|
|
1618
1658
|
<AskBar
|
|
1619
1659
|
onSubmit={askAI}
|
|
1620
1660
|
style={{
|
|
@@ -1672,7 +1712,9 @@ function DocsPage() {
|
|
|
1672
1712
|
</div>
|
|
1673
1713
|
)}
|
|
1674
1714
|
|
|
1675
|
-
{/* Ask-AI chatbot — slides in from the inline-end edge.
|
|
1715
|
+
{/* Ask-AI chatbot — slides in from the inline-end edge. Hidden when the
|
|
1716
|
+
docs owner's plan/credits disable Ask-AI. */}
|
|
1717
|
+
{!askAiHidden && (
|
|
1676
1718
|
<Chatbot
|
|
1677
1719
|
open={chatOpen}
|
|
1678
1720
|
seedQuestion={chatQuestion}
|
|
@@ -1698,6 +1740,7 @@ function DocsPage() {
|
|
|
1698
1740
|
setTimeout(go, 50);
|
|
1699
1741
|
}}
|
|
1700
1742
|
/>
|
|
1743
|
+
)}
|
|
1701
1744
|
</div>
|
|
1702
1745
|
);
|
|
1703
1746
|
}
|
|
@@ -4,9 +4,14 @@ import { BrowserRouter } from 'react-router-dom';
|
|
|
4
4
|
import 'velu-ui/styles.css';
|
|
5
5
|
import App from './App.jsx';
|
|
6
6
|
|
|
7
|
+
// Subpath hosting: Vite sets BASE_URL from the build's `base` (e.g. "/docs/").
|
|
8
|
+
// React Router wants a basename WITHOUT the trailing slash ("/docs"); the root
|
|
9
|
+
// case ("/") becomes undefined so the router behaves as if mounted at origin.
|
|
10
|
+
const basename = (import.meta.env.BASE_URL || '/').replace(/\/$/, '') || undefined;
|
|
11
|
+
|
|
7
12
|
hydrateRoot(
|
|
8
13
|
document.getElementById('root'),
|
|
9
|
-
<BrowserRouter>
|
|
14
|
+
<BrowserRouter basename={basename}>
|
|
10
15
|
<App />
|
|
11
16
|
</BrowserRouter>
|
|
12
17
|
);
|
|
@@ -7,9 +7,17 @@ import { StaticRouter } from 'react-router-dom/server';
|
|
|
7
7
|
import 'velu-ui/styles.css';
|
|
8
8
|
import App from './App.jsx';
|
|
9
9
|
|
|
10
|
+
// Subpath hosting: mirror the client's basename (from Vite's BASE_URL). The
|
|
11
|
+
// prerender is driven by root-relative route paths ("/foo"), so the location
|
|
12
|
+
// handed to StaticRouter must carry the basename prefix ("/docs/foo") for it to
|
|
13
|
+
// match — and so the rendered <Link> hrefs come out prefixed too, matching what
|
|
14
|
+
// the hydrated BrowserRouter produces.
|
|
15
|
+
const basename = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
|
16
|
+
|
|
10
17
|
export async function render(url) {
|
|
18
|
+
const location = basename ? `${basename}${url}` : url;
|
|
11
19
|
return renderToString(
|
|
12
|
-
<StaticRouter location={
|
|
20
|
+
<StaticRouter basename={basename || undefined} location={location}>
|
|
13
21
|
<App />
|
|
14
22
|
</StaticRouter>
|
|
15
23
|
);
|
|
@@ -16,16 +16,16 @@ save.
|
|
|
16
16
|
## Get going
|
|
17
17
|
|
|
18
18
|
<CardGroup>
|
|
19
|
-
<Card title="Quickstart" icon="rocket"
|
|
19
|
+
<Card title="Quickstart" icon="rocket" cta={{ label: "Get started", href: "/quickstart" }}>
|
|
20
20
|
Run the dev server and make your first edit.
|
|
21
21
|
</Card>
|
|
22
|
-
<Card title="Customize" icon="sliders-horizontal"
|
|
22
|
+
<Card title="Customize" icon="sliders-horizontal" cta={{ label: "Customize", href: "/essentials/settings" }}>
|
|
23
23
|
Set your colors, font, and favicon.
|
|
24
24
|
</Card>
|
|
25
|
-
<Card title="Write content" icon="pen-line"
|
|
25
|
+
<Card title="Write content" icon="pen-line" cta={{ label: "Write content", href: "/essentials/markdown" }}>
|
|
26
26
|
Markdown plus a library of components.
|
|
27
27
|
</Card>
|
|
28
|
-
<Card title="API reference" icon="terminal"
|
|
28
|
+
<Card title="API reference" icon="terminal" cta={{ label: "View reference", href: "/api-reference/introduction" }}>
|
|
29
29
|
Document your endpoints.
|
|
30
30
|
</Card>
|
|
31
31
|
</CardGroup>
|