@veluai/velu 0.2.48 → 0.2.50
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 +51 -46
- package/docs/mintlify-migration.md +4 -2
- package/package.json +1 -1
- package/runtime/velu-ui/components/ApiReferencePage.jsx +46 -18
- package/runtime/velu-ui/components/ApiSidebar.jsx +5 -3
- package/runtime/velu-ui/components/api.css +9 -1
- package/runtime/velu-ui/components/docs-layout.css +3 -0
- package/runtime/velu-ui/index.js +11 -0
- package/runtime/velu-ui/lib/reader-user.js +135 -0
- package/runtime/velu-ui/themes/nila.css +1 -1
- package/runtime/velu-ui/themes/thulir.css +2 -2
- package/src/navigation.js +4 -0
- package/src/runtime/App.jsx +93 -6
|
@@ -121,9 +121,11 @@ The presets are not a certification of pixel equality for every component or cus
|
|
|
121
121
|
|
|
122
122
|
Features without full Velu equivalents require review: remaining Mintlify themes, advanced
|
|
123
123
|
fonts and styling, redirects, analytics/support integrations, hosting settings,
|
|
124
|
-
authentication, personalization, hidden-page routing, legacy version filtering,
|
|
124
|
+
authentication, standalone public personalization, hidden-page routing, legacy version filtering,
|
|
125
125
|
complex dropdown/global navigation, AsyncAPI, manually authored `api:` pages,
|
|
126
|
-
multi-file/remote endpoint snapshots, OpenAPI overlays
|
|
126
|
+
multi-file/remote endpoint snapshots, and OpenAPI overlays.
|
|
127
|
+
OpenAPI `x-mint` / `x-mint-enum` personalization keys are rewritten to `x-velu` /
|
|
128
|
+
`x-velu-enum`. The generator still reads leftover `x-mint` keys in unmigrated specs.
|
|
127
129
|
Access/visibility settings are reported as errors; they must be migrated before
|
|
128
130
|
publishing. Unlisted content is copied but Velu currently renders navigation
|
|
129
131
|
pages only. Snapshot API pages must be regenerated or edited when their specs
|
package/package.json
CHANGED
|
@@ -11,6 +11,24 @@ import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
|
|
|
11
11
|
import ApiSamples from './ApiSamples.jsx';
|
|
12
12
|
import Callout from './Callout.jsx';
|
|
13
13
|
import { sendApiRequest } from '../lib/api-send.js';
|
|
14
|
+
import {
|
|
15
|
+
getReaderGroups,
|
|
16
|
+
visibleToGroups,
|
|
17
|
+
initialPlaygroundValues,
|
|
18
|
+
persistPlaygroundValues,
|
|
19
|
+
personalizedServer,
|
|
20
|
+
} from '../lib/reader-user.js';
|
|
21
|
+
|
|
22
|
+
function visibleFields(rows = [], groups) {
|
|
23
|
+
return rows.filter((row) => visibleToGroups(row.groups, groups));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function visibleEnum(row, groups) {
|
|
27
|
+
if (!Array.isArray(row?.enum)) return row?.enum;
|
|
28
|
+
const mapping = row.veluEnum || row.mintEnum;
|
|
29
|
+
if (!mapping) return row.enum;
|
|
30
|
+
return row.enum.filter((value) => visibleToGroups(mapping[String(value)], groups));
|
|
31
|
+
}
|
|
14
32
|
|
|
15
33
|
/**
|
|
16
34
|
* ApiReferencePage — the full, auto-generated reference page for one OpenAPI
|
|
@@ -25,24 +43,29 @@ import { sendApiRequest } from '../lib/api-send.js';
|
|
|
25
43
|
*
|
|
26
44
|
* @param {{ operation: object, api?: { proxy?: boolean, server?: string } }} props
|
|
27
45
|
*/
|
|
28
|
-
const LOCATION_LABEL = { path: 'Path Parameters', query: 'Query Parameters', header: 'Header Parameters' };
|
|
46
|
+
const LOCATION_LABEL = { path: 'Path Parameters', query: 'Query Parameters', header: 'Header Parameters', cookie: 'Cookie Parameters' };
|
|
29
47
|
|
|
30
48
|
export default function ApiReferencePage({ operation, samples = [], apiOperations = [], api = {}, children, preset }) {
|
|
31
49
|
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
50
|
const [activeId, setActiveId] = useState(operation.id);
|
|
51
|
+
const [groups, setGroups] = useState(() => getReaderGroups());
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const sync = () => setGroups(getReaderGroups());
|
|
54
|
+
sync();
|
|
55
|
+
if (typeof window === 'undefined') return undefined;
|
|
56
|
+
window.addEventListener('velu:user', sync);
|
|
57
|
+
return () => window.removeEventListener('velu:user', sync);
|
|
58
|
+
}, []);
|
|
35
59
|
|
|
36
|
-
// The switchable list — fall back to just this page's operation.
|
|
37
60
|
const ops =
|
|
38
61
|
apiOperations.length > 0
|
|
39
|
-
? apiOperations
|
|
62
|
+
? apiOperations.filter((o) => visibleToGroups((o.operation || o).groups, groups))
|
|
40
63
|
: [{ id: operation.id, method: operation.method, title: operation.title, operation, samples, api }];
|
|
41
|
-
const current = ops.find((o) => o.id === activeId) || { operation, samples, api };
|
|
64
|
+
const current = ops.find((o) => o.id === activeId) || ops[0] || { operation, samples, api };
|
|
42
65
|
// Each operation carries its own (section-level) api config; fall back to
|
|
43
66
|
// this page's config.
|
|
44
67
|
const currentApi = current.api ?? api;
|
|
45
|
-
const server = currentApi.server || current.operation.servers?.[0] || '';
|
|
68
|
+
const server = personalizedServer(currentApi.server || current.operation.servers?.[0] || '');
|
|
46
69
|
|
|
47
70
|
// While the playground modal is open, close on Escape and lock the
|
|
48
71
|
// page scroll so the dimmed backdrop reads as a true overlay.
|
|
@@ -94,8 +117,8 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
|
|
|
94
117
|
)}
|
|
95
118
|
|
|
96
119
|
{/* Parameters, by location */}
|
|
97
|
-
{['path', 'query', 'header'].map((loc) => {
|
|
98
|
-
const rows = operation.parameters?.[loc] || [];
|
|
120
|
+
{['path', 'query', 'header', 'cookie'].map((loc) => {
|
|
121
|
+
const rows = visibleFields(operation.parameters?.[loc] || [], groups);
|
|
99
122
|
if (!rows.length) return null;
|
|
100
123
|
return (
|
|
101
124
|
<api-section key={loc} class="velu-api-page__section">
|
|
@@ -103,7 +126,7 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
|
|
|
103
126
|
{rows.map((p, i) => (
|
|
104
127
|
<Field key={i} name={p.name} type={p.type} required={p.required}>
|
|
105
128
|
{p.description}
|
|
106
|
-
{p
|
|
129
|
+
{visibleEnum(p, groups)?.length ? <EnumHint values={visibleEnum(p, groups)} /> : null}
|
|
107
130
|
</Field>
|
|
108
131
|
))}
|
|
109
132
|
</api-section>
|
|
@@ -115,10 +138,10 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
|
|
|
115
138
|
<api-section class="velu-api-page__section">
|
|
116
139
|
<h2 data-component="api-section-heading">Body</h2>
|
|
117
140
|
{['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) && operation.body.contentType && <span className="velu-api-body-type">{operation.body.contentType}</span>}
|
|
118
|
-
{operation.body.fields.map((f, i) => (
|
|
141
|
+
{visibleFields(operation.body.fields, groups).map((f, i) => (
|
|
119
142
|
<Field key={i} name={f.name} type={f.type} required={f.required}>
|
|
120
143
|
{f.description}
|
|
121
|
-
{f
|
|
144
|
+
{visibleEnum(f, groups)?.length ? <EnumHint values={visibleEnum(f, groups)} /> : null}
|
|
122
145
|
</Field>
|
|
123
146
|
))}
|
|
124
147
|
</api-section>
|
|
@@ -183,7 +206,7 @@ function ResponseSections({ responses, preset }) {
|
|
|
183
206
|
if (preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset)) return <div className="velu-vepa-responses">{responses.map((r, i) => <section key={i}>
|
|
184
207
|
<div className="velu-vepa-responses__status">{r.status}{r.contentType && ` · ${r.contentType}`}</div>
|
|
185
208
|
{r.description && <p>{r.description}</p>}
|
|
186
|
-
{(['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? [...(r.fields
|
|
209
|
+
{(['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? [...visibleFields(r.fields, getReaderGroups())].sort((a, b) => Number(Boolean(b.required)) - Number(Boolean(a.required))) : visibleFields(r.fields, getReaderGroups()))?.map((f, j) => <Field key={f.name || j} name={f.name} type={f.type} required={f.required}>{f.description}{visibleEnum(f, getReaderGroups())?.length ? <EnumHint values={visibleEnum(f, getReaderGroups())} /> : null}</Field>)}
|
|
187
210
|
</section>)}</div>;
|
|
188
211
|
return (
|
|
189
212
|
<AccordionGroup className="velu-api-resp">
|
|
@@ -202,9 +225,10 @@ function ResponseSections({ responses, preset }) {
|
|
|
202
225
|
}
|
|
203
226
|
>
|
|
204
227
|
{r.description && <p className="velu-api-resp__desc">{r.description}</p>}
|
|
205
|
-
{r.fields
|
|
228
|
+
{visibleFields(r.fields, getReaderGroups()).map((f, j) => (
|
|
206
229
|
<Field key={j} name={f.name} type={f.type} required={f.required}>
|
|
207
230
|
{f.description}
|
|
231
|
+
{visibleEnum(f, getReaderGroups())?.length ? <EnumHint values={visibleEnum(f, getReaderGroups())} /> : null}
|
|
208
232
|
</Field>
|
|
209
233
|
))}
|
|
210
234
|
</Accordion>
|
|
@@ -226,7 +250,7 @@ function Playground({
|
|
|
226
250
|
proxy,
|
|
227
251
|
onClose,
|
|
228
252
|
}) {
|
|
229
|
-
const [values, setValues] = useState(
|
|
253
|
+
const [values, setValues] = useState(() => initialPlaygroundValues(operation));
|
|
230
254
|
const [bodyText, setBodyText] = useState(
|
|
231
255
|
operation.body ? JSON.stringify(operation.body.example ?? {}, null, 2) : '',
|
|
232
256
|
);
|
|
@@ -234,7 +258,11 @@ function Playground({
|
|
|
234
258
|
const [resp, setResp] = useState(null);
|
|
235
259
|
|
|
236
260
|
const set = (loc, name, v) =>
|
|
237
|
-
setValues((prev) =>
|
|
261
|
+
setValues((prev) => {
|
|
262
|
+
const next = { ...prev, [loc]: { ...(prev[loc] || {}), [name]: v } };
|
|
263
|
+
persistPlaygroundValues(operation.id, next);
|
|
264
|
+
return next;
|
|
265
|
+
});
|
|
238
266
|
|
|
239
267
|
const onSend = useCallback(async () => {
|
|
240
268
|
setBusy(true);
|
|
@@ -251,8 +279,8 @@ function Playground({
|
|
|
251
279
|
|
|
252
280
|
const sections = [];
|
|
253
281
|
if (operation.auth?.length) sections.push(['auth', 'Authorization', operation.auth]);
|
|
254
|
-
for (const loc of ['path', 'query', 'header']) {
|
|
255
|
-
const rows = operation.parameters?.[loc] || [];
|
|
282
|
+
for (const loc of ['path', 'query', 'header', 'cookie']) {
|
|
283
|
+
const rows = visibleFields(operation.parameters?.[loc] || [], getReaderGroups());
|
|
256
284
|
if (rows.length) sections.push([loc, LOCATION_LABEL[loc].replace(' Parameters', ''), rows]);
|
|
257
285
|
}
|
|
258
286
|
|
|
@@ -34,7 +34,8 @@ import MethodBadge from './MethodBadge.jsx';
|
|
|
34
34
|
*
|
|
35
35
|
* @typedef {Object} ApiEndpoint
|
|
36
36
|
* @property {string} method HTTP method — drives the MethodBadge color
|
|
37
|
-
* @property {string} label
|
|
37
|
+
* @property {string} [label] endpoint display name
|
|
38
|
+
* @property {string} [name] alias for label (accepted for back-compat)
|
|
38
39
|
* @property {string} [href]
|
|
39
40
|
* @property {boolean} [active]
|
|
40
41
|
*
|
|
@@ -56,7 +57,8 @@ function Icon({ icon }) {
|
|
|
56
57
|
|
|
57
58
|
function Endpoint({ endpoint }) {
|
|
58
59
|
const { activeHref, Link } = React.useContext(Ctx);
|
|
59
|
-
const { method, label, href = '#', active, icon } = endpoint;
|
|
60
|
+
const { method, label, name, href = '#', active, icon } = endpoint;
|
|
61
|
+
const title = label || name;
|
|
60
62
|
const isActive = Boolean(
|
|
61
63
|
active || (href != null && href === activeHref),
|
|
62
64
|
);
|
|
@@ -74,7 +76,7 @@ function Endpoint({ endpoint }) {
|
|
|
74
76
|
aria-current={isActive ? 'page' : undefined}
|
|
75
77
|
>
|
|
76
78
|
{method ? <MethodBadge method={method} /> : <Icon icon={icon} />}
|
|
77
|
-
<span className="velu-api-sidebar__label">{
|
|
79
|
+
<span className="velu-api-sidebar__label">{title}</span>
|
|
78
80
|
</Link>
|
|
79
81
|
</li>
|
|
80
82
|
);
|
|
@@ -611,12 +611,15 @@
|
|
|
611
611
|
margin-block-start: var(--s2);
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
/* Endpoint row — method pill + name.
|
|
614
|
+
/* Endpoint row — method pill + name. Keep rows inside the scroll
|
|
615
|
+
content box so the active/hover tint never paints over the gutter. */
|
|
615
616
|
.velu-api-sidebar__item {
|
|
616
617
|
display: flex;
|
|
617
618
|
align-items: center;
|
|
618
619
|
gap: 10px;
|
|
619
620
|
min-height: 36px;
|
|
621
|
+
max-inline-size: 100%;
|
|
622
|
+
box-sizing: border-box;
|
|
620
623
|
padding: 6px var(--s0);
|
|
621
624
|
border-radius: 12px;
|
|
622
625
|
color: var(--text-color);
|
|
@@ -637,7 +640,12 @@
|
|
|
637
640
|
background: var(--method-cta);
|
|
638
641
|
}
|
|
639
642
|
.velu-api-sidebar__label {
|
|
643
|
+
flex: 1;
|
|
644
|
+
min-inline-size: 0;
|
|
640
645
|
font: inherit;
|
|
646
|
+
overflow: hidden;
|
|
647
|
+
text-overflow: ellipsis;
|
|
648
|
+
white-space: nowrap;
|
|
641
649
|
}
|
|
642
650
|
|
|
643
651
|
/* Compact method pill — colored label on the method tint, not a filled
|
|
@@ -130,9 +130,12 @@
|
|
|
130
130
|
.velu-docs-nav-scroll {
|
|
131
131
|
flex: 1 1 auto;
|
|
132
132
|
min-block-size: 0;
|
|
133
|
+
min-inline-size: 0;
|
|
133
134
|
/* Always reserve the scrollbar gutter (see always-on track styles
|
|
134
135
|
further below) instead of only appearing when content overflows. */
|
|
135
136
|
overflow-y: scroll;
|
|
137
|
+
/* Keep active/hover row tints out of the scrollbar track. */
|
|
138
|
+
scrollbar-gutter: stable;
|
|
136
139
|
/* Clear sticky section headings when scrolling the active item into view. */
|
|
137
140
|
scroll-padding-block-start: 2.75rem;
|
|
138
141
|
}
|
package/runtime/velu-ui/index.js
CHANGED
|
@@ -49,6 +49,17 @@ export { default as Search } from './components/Search.jsx';
|
|
|
49
49
|
export { default as pagefindSearch } from './lib/pagefind.js';
|
|
50
50
|
export { createDocsAssistant } from './lib/docs-assistant.js';
|
|
51
51
|
export { createPageFeedback } from './lib/page-feedback.js';
|
|
52
|
+
export {
|
|
53
|
+
setReaderIdentity,
|
|
54
|
+
emptyReaderIdentity,
|
|
55
|
+
identityFromUserData,
|
|
56
|
+
getReaderUser,
|
|
57
|
+
getReaderGroups,
|
|
58
|
+
getPlaygroundInputs,
|
|
59
|
+
visibleToGroups,
|
|
60
|
+
filterSidebarSections,
|
|
61
|
+
pageRequiredGroups,
|
|
62
|
+
} from './lib/reader-user.js';
|
|
52
63
|
export { default as Image } from './components/Image.jsx';
|
|
53
64
|
export {
|
|
54
65
|
default as CodeBlock,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Identified-reader personalization (Mintlify-compatible `user` + playground). */
|
|
2
|
+
|
|
3
|
+
export function emptyReaderIdentity() {
|
|
4
|
+
return { user: {}, groups: [], apiPlaygroundInputs: {} };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function identityFromUserData(data) {
|
|
8
|
+
const empty = emptyReaderIdentity();
|
|
9
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return empty;
|
|
10
|
+
if (typeof data.expiresAt === 'number' && data.expiresAt * 1000 <= Date.now()) return empty;
|
|
11
|
+
const groups = Array.isArray(data.groups)
|
|
12
|
+
? [...new Set(data.groups.filter((g) => typeof g === 'string' && g.trim()).map((g) => g.trim()))].slice(0, 200)
|
|
13
|
+
: [];
|
|
14
|
+
const user = data.content && typeof data.content === 'object' && !Array.isArray(data.content) ? data.content : {};
|
|
15
|
+
const apiPlaygroundInputs =
|
|
16
|
+
data.apiPlaygroundInputs && typeof data.apiPlaygroundInputs === 'object' && !Array.isArray(data.apiPlaygroundInputs)
|
|
17
|
+
? data.apiPlaygroundInputs
|
|
18
|
+
: {};
|
|
19
|
+
return { user, groups, apiPlaygroundInputs };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pageRequiredGroups(page) {
|
|
23
|
+
const fromFrontmatter = page?.frontmatter?.groups;
|
|
24
|
+
if (Array.isArray(fromFrontmatter) && fromFrontmatter.length) return fromFrontmatter;
|
|
25
|
+
const fromOperation = page?.operation?.groups;
|
|
26
|
+
if (Array.isArray(fromOperation) && fromOperation.length) return fromOperation;
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function filterSidebarSections(sections = [], groups = getReaderGroups(), pagesMap = {}) {
|
|
31
|
+
const keepItems = (items) =>
|
|
32
|
+
(items || []).flatMap((item) => {
|
|
33
|
+
if (item?.items) {
|
|
34
|
+
const nested = keepItems(item.items);
|
|
35
|
+
return nested.length ? [{ ...item, items: nested }] : [];
|
|
36
|
+
}
|
|
37
|
+
const required = Array.isArray(item?.groups) && item.groups.length
|
|
38
|
+
? item.groups
|
|
39
|
+
: pageRequiredGroups(pagesMap[item?.href]);
|
|
40
|
+
return visibleToGroups(required, groups) ? [item] : [];
|
|
41
|
+
});
|
|
42
|
+
return (sections || [])
|
|
43
|
+
.map((section) => {
|
|
44
|
+
const items = keepItems(section.items);
|
|
45
|
+
return items.length ? { ...section, items } : null;
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function setReaderIdentity(identity = {}) {
|
|
51
|
+
const next = {
|
|
52
|
+
user: identity.user && typeof identity.user === 'object' && !Array.isArray(identity.user) ? identity.user : {},
|
|
53
|
+
groups: Array.isArray(identity.groups) ? identity.groups.filter((g) => typeof g === 'string') : [],
|
|
54
|
+
apiPlaygroundInputs:
|
|
55
|
+
identity.apiPlaygroundInputs && typeof identity.apiPlaygroundInputs === 'object' && !Array.isArray(identity.apiPlaygroundInputs)
|
|
56
|
+
? identity.apiPlaygroundInputs
|
|
57
|
+
: {},
|
|
58
|
+
};
|
|
59
|
+
if (typeof globalThis === 'undefined') return next;
|
|
60
|
+
globalThis.velu = { ...(globalThis.velu || {}), ...next };
|
|
61
|
+
globalThis.mintlify = { ...(globalThis.mintlify || {}), user: next.user };
|
|
62
|
+
if (typeof globalThis.dispatchEvent === 'function' && typeof globalThis.Event === 'function') {
|
|
63
|
+
globalThis.dispatchEvent(new Event('velu:user'));
|
|
64
|
+
globalThis.dispatchEvent(new Event('mintlify:user'));
|
|
65
|
+
}
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getReaderUser() {
|
|
70
|
+
return globalThis.velu?.user && typeof globalThis.velu.user === 'object' ? globalThis.velu.user : {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getReaderGroups() {
|
|
74
|
+
return Array.isArray(globalThis.velu?.groups) ? globalThis.velu.groups : [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getPlaygroundInputs() {
|
|
78
|
+
const inputs = globalThis.velu?.apiPlaygroundInputs;
|
|
79
|
+
return inputs && typeof inputs === 'object' ? inputs : {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function visibleToGroups(required, groups = getReaderGroups()) {
|
|
83
|
+
if (!Array.isArray(required) || required.length === 0) return true;
|
|
84
|
+
return groups.some((g) => required.includes(g));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function initialPlaygroundValues(operation) {
|
|
88
|
+
const inputs = getPlaygroundInputs();
|
|
89
|
+
const values = { auth: {}, path: {}, query: {}, header: {}, cookie: {} };
|
|
90
|
+
for (const a of operation?.auth || []) {
|
|
91
|
+
const raw = inputs.header?.[a.name];
|
|
92
|
+
if (typeof raw !== 'string' || !raw) continue;
|
|
93
|
+
values.auth[a.name] = a.prefix && raw.startsWith(`${a.prefix} `) ? raw.slice(a.prefix.length + 1) : raw;
|
|
94
|
+
}
|
|
95
|
+
for (const loc of ['path', 'query', 'header', 'cookie']) {
|
|
96
|
+
for (const p of operation?.parameters?.[loc] || []) {
|
|
97
|
+
const raw = inputs[loc]?.[p.name];
|
|
98
|
+
if (typeof raw === 'string') values[loc][p.name] = raw;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (typeof globalThis.localStorage === 'undefined') return values;
|
|
102
|
+
try {
|
|
103
|
+
const stored = JSON.parse(globalThis.localStorage.getItem(`velu-playground:${operation?.id}`) || 'null');
|
|
104
|
+
if (stored && typeof stored === 'object') {
|
|
105
|
+
for (const loc of Object.keys(values)) {
|
|
106
|
+
values[loc] = { ...values[loc], ...(stored[loc] || {}) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
/* ignore malformed local overrides */
|
|
111
|
+
}
|
|
112
|
+
return values;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function persistPlaygroundValues(operationId, values) {
|
|
116
|
+
if (typeof globalThis.localStorage === 'undefined' || !operationId) return;
|
|
117
|
+
try {
|
|
118
|
+
globalThis.localStorage.setItem(`velu-playground:${operationId}`, JSON.stringify(values || {}));
|
|
119
|
+
} catch {
|
|
120
|
+
/* quota / private mode */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function personalizedServer(fallback) {
|
|
125
|
+
const vars = getPlaygroundInputs().server || {};
|
|
126
|
+
let url = (typeof vars.baseUrl === 'string' && vars.baseUrl)
|
|
127
|
+
|| (typeof vars.url === 'string' && vars.url)
|
|
128
|
+
|| fallback
|
|
129
|
+
|| '';
|
|
130
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
131
|
+
if (key === 'baseUrl' || key === 'url' || typeof value !== 'string') continue;
|
|
132
|
+
url = url.replaceAll(`{${key}}`, value);
|
|
133
|
+
}
|
|
134
|
+
return url;
|
|
135
|
+
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
[data-site-theme='nila'] .velu-docs-context:not(:has(.velu-docs-anchors)) { display: none !important; }
|
|
38
38
|
[data-site-theme='nila'] .velu-header { height: 96px; }
|
|
39
39
|
[data-site-theme='nila'] .velu-docs-layout__aside--left { left: 0; top: 96px; bottom: 0; width: 308px; padding: 16px 20px; border-right: 1px solid var(--border-color); }
|
|
40
|
-
[data-site-theme='nila'] .velu-sidebar { width:
|
|
40
|
+
[data-site-theme='nila'] .velu-sidebar { width: 100%; max-inline-size: 100%; box-sizing: border-box; }
|
|
41
41
|
[data-site-theme='nila'] .velu-docs-layout__center { margin-left: 308px; margin-right: 324px; }
|
|
42
42
|
[data-site-theme='nila'] .velu-docs-layout__main { padding: 44px 32px 0; }
|
|
43
43
|
[data-site-theme='nila'] .velu-docs-layout__article { max-inline-size: 632px; margin-inline: auto; }
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
[data-site-theme='thulir'] .velu-docs-nav-scroll::-webkit-scrollbar { display: none; }
|
|
155
155
|
|
|
156
156
|
/* Navigation: 14/24 type, plain group labels, and a rounded active tint. */
|
|
157
|
-
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-sidebar { margin: 0; width:
|
|
157
|
+
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-sidebar { margin: 0; width: 100%; max-inline-size: 100%; box-sizing: border-box; }
|
|
158
158
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-sidebar__indicator { inset-inline-end: 0; border-radius: 12px; background: rgb(from var(--accent-color) r g b / 10%); }
|
|
159
159
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-sidebar__indicator::before { display: none; }
|
|
160
160
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) :is(.velu-sidebar__section,.velu-api-sidebar__section) {
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) :is(.velu-sidebar__item--active,.velu-api-sidebar__item--active) { color: var(--accent-color); font-weight: 600; border: 0; }
|
|
178
178
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) :is(.velu-sidebar__list,.velu-api-sidebar__list) { gap: 0 !important; }
|
|
179
179
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-sidebar > sidebar-group { gap: 0 !important; margin-bottom: 28px; }
|
|
180
|
-
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-api-sidebar { margin: 0; border: 0; width:
|
|
180
|
+
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-api-sidebar { margin: 0; border: 0; width: 100%; max-inline-size: 100%; box-sizing: border-box; gap: 0 !important; }
|
|
181
181
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-api-sidebar__list + .velu-api-sidebar__section { margin-top: 28px; }
|
|
182
182
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-api-sidebar__item--active { background: rgb(from var(--accent-color) r g b / 10%); }
|
|
183
183
|
:is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-api-sidebar__item--active .velu-method-badge { color: #fff; background: var(--method-cta); }
|
package/src/navigation.js
CHANGED
|
@@ -478,5 +478,9 @@ function itemFor(pageNode, pagesMap, ctx) {
|
|
|
478
478
|
}
|
|
479
479
|
const method = pageNode.method ?? pagesMap[href]?.operation?.method;
|
|
480
480
|
if (method) item.method = method;
|
|
481
|
+
const groups = pageNode.groups
|
|
482
|
+
?? (Array.isArray(fm?.groups) && fm.groups.length ? fm.groups : undefined)
|
|
483
|
+
?? pagesMap[href]?.operation?.groups;
|
|
484
|
+
if (Array.isArray(groups) && groups.length) item.groups = groups;
|
|
481
485
|
return item;
|
|
482
486
|
}
|
package/src/runtime/App.jsx
CHANGED
|
@@ -58,6 +58,10 @@ import {
|
|
|
58
58
|
ApiReferencePage,
|
|
59
59
|
NotFound,
|
|
60
60
|
VeluMark,
|
|
61
|
+
setReaderIdentity,
|
|
62
|
+
emptyReaderIdentity,
|
|
63
|
+
identityFromUserData,
|
|
64
|
+
filterSidebarSections,
|
|
61
65
|
} from 'velu-ui';
|
|
62
66
|
import { X, ChevronDown } from 'lucide-react';
|
|
63
67
|
import ErrorBoundary from './ErrorBoundary.jsx';
|
|
@@ -115,6 +119,22 @@ function sectionToEndpoints(items) {
|
|
|
115
119
|
return out;
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
function flattenSidebarPages(sections) {
|
|
123
|
+
const out = [];
|
|
124
|
+
const walk = (items) => {
|
|
125
|
+
for (const it of items ?? []) {
|
|
126
|
+
if (it.items) walk(it.items);
|
|
127
|
+
else {
|
|
128
|
+
const page = { label: it.label, href: it.href };
|
|
129
|
+
if (it.description) page.description = it.description;
|
|
130
|
+
out.push(page);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
for (const section of sections ?? []) walk(section.items);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
118
138
|
const CALLOUT_TYPES = [
|
|
119
139
|
'note',
|
|
120
140
|
'warning',
|
|
@@ -788,6 +808,53 @@ function DocsPage() {
|
|
|
788
808
|
const location = useLocation();
|
|
789
809
|
const navigate = useNavigate();
|
|
790
810
|
const pathname = normalizeUrl(location.pathname);
|
|
811
|
+
const [readerUser, setReaderUser] = React.useState(() =>
|
|
812
|
+
typeof window === 'undefined' ? {} : window.velu?.user || {},
|
|
813
|
+
);
|
|
814
|
+
const [readerGroups, setReaderGroups] = React.useState(() =>
|
|
815
|
+
typeof window === 'undefined' ? [] : window.velu?.groups || [],
|
|
816
|
+
);
|
|
817
|
+
const [sharedSession, setSharedSession] = React.useState(null);
|
|
818
|
+
React.useEffect(() => {
|
|
819
|
+
const sync = () => {
|
|
820
|
+
setReaderUser(window.velu?.user || {});
|
|
821
|
+
setReaderGroups(Array.isArray(window.velu?.groups) ? window.velu.groups : []);
|
|
822
|
+
};
|
|
823
|
+
sync();
|
|
824
|
+
window.addEventListener('velu:user', sync);
|
|
825
|
+
return () => window.removeEventListener('velu:user', sync);
|
|
826
|
+
}, []);
|
|
827
|
+
React.useEffect(() => {
|
|
828
|
+
if (IS_DEV_PREVIEW || site.readerPrivate || site.guarded) return undefined;
|
|
829
|
+
let alive = true;
|
|
830
|
+
(async () => {
|
|
831
|
+
try {
|
|
832
|
+
const config = await fetch(`${BASE_PATH}/@velu-reader/personalization`, {
|
|
833
|
+
credentials: 'same-origin',
|
|
834
|
+
cache: 'no-store',
|
|
835
|
+
}).then((response) => (response.ok ? response.json() : null));
|
|
836
|
+
if (!alive || config?.method !== 'shared_session' || !config.infoUrl) return;
|
|
837
|
+
setSharedSession({ loginUrl: config.loginUrl || null, identified: false });
|
|
838
|
+
const response = await fetch(config.infoUrl, { credentials: 'include', cache: 'no-store' });
|
|
839
|
+
if (!alive) return;
|
|
840
|
+
if (!response.ok) {
|
|
841
|
+
setReaderIdentity(emptyReaderIdentity());
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const identity = identityFromUserData(await response.json());
|
|
845
|
+
setReaderIdentity(identity);
|
|
846
|
+
setSharedSession({
|
|
847
|
+
loginUrl: config.loginUrl || null,
|
|
848
|
+
identified: identity.groups.length > 0 || Object.keys(identity.user).length > 0,
|
|
849
|
+
});
|
|
850
|
+
} catch {
|
|
851
|
+
if (alive) setReaderIdentity(emptyReaderIdentity());
|
|
852
|
+
}
|
|
853
|
+
})();
|
|
854
|
+
return () => {
|
|
855
|
+
alive = false;
|
|
856
|
+
};
|
|
857
|
+
}, []);
|
|
791
858
|
const entry = pages[pathname];
|
|
792
859
|
// `navigation`/`pages` are module constants in dev/build (the deps never
|
|
793
860
|
// change there); the instant preview's virtual-site substitute reassigns
|
|
@@ -797,6 +864,18 @@ function DocsPage() {
|
|
|
797
864
|
() => resolve(pathname, navigation, pages),
|
|
798
865
|
[pathname, navigation, pages],
|
|
799
866
|
);
|
|
867
|
+
const visibleNav = React.useMemo(() => {
|
|
868
|
+
if (!nav || site.readerPrivate || site.guarded) return nav;
|
|
869
|
+
const sidebarSections = filterSidebarSections(nav.sidebarSections, readerGroups, pages);
|
|
870
|
+
const ordered = flattenSidebarPages(sidebarSections);
|
|
871
|
+
const idx = ordered.findIndex((page) => page.href === pathname);
|
|
872
|
+
return {
|
|
873
|
+
...nav,
|
|
874
|
+
sidebarSections,
|
|
875
|
+
prev: idx > 0 ? ordered[idx - 1] : undefined,
|
|
876
|
+
next: idx >= 0 && idx < ordered.length - 1 ? ordered[idx + 1] : undefined,
|
|
877
|
+
};
|
|
878
|
+
}, [nav, pathname, readerGroups]);
|
|
800
879
|
|
|
801
880
|
// Search source: Pagefind for content/excerpts, but resolve each result's
|
|
802
881
|
// breadcrumb from the real NAVIGATION (a page's nav group can differ from its
|
|
@@ -1257,6 +1336,14 @@ function DocsPage() {
|
|
|
1257
1336
|
kind: 'outlined',
|
|
1258
1337
|
component: 'logout-link',
|
|
1259
1338
|
});
|
|
1339
|
+
} else if (sharedSession?.loginUrl && !sharedSession.identified) {
|
|
1340
|
+
navActions.push({
|
|
1341
|
+
label: 'Log in',
|
|
1342
|
+
href: sharedSession.loginUrl,
|
|
1343
|
+
kind: 'outlined',
|
|
1344
|
+
external: true,
|
|
1345
|
+
component: 'login-link',
|
|
1346
|
+
});
|
|
1260
1347
|
}
|
|
1261
1348
|
|
|
1262
1349
|
// Callback ref so this rebinds when the footer mounts/unmounts.
|
|
@@ -1676,7 +1763,7 @@ function DocsPage() {
|
|
|
1676
1763
|
the sidebar shape). */}
|
|
1677
1764
|
{nav?.sidebarHasApi ? (
|
|
1678
1765
|
<ApiSidebar
|
|
1679
|
-
sections={(
|
|
1766
|
+
sections={(visibleNav?.sidebarSections ?? []).map((s) => ({
|
|
1680
1767
|
title: s.title,
|
|
1681
1768
|
icon: s.icon,
|
|
1682
1769
|
endpoints: sectionToEndpoints(s.items),
|
|
@@ -1686,7 +1773,7 @@ function DocsPage() {
|
|
|
1686
1773
|
/>
|
|
1687
1774
|
) : (
|
|
1688
1775
|
<Sidebar
|
|
1689
|
-
sections={
|
|
1776
|
+
sections={visibleNav?.sidebarSections ?? []}
|
|
1690
1777
|
activeHref={pathname}
|
|
1691
1778
|
linkComponent={RouterLink}
|
|
1692
1779
|
/>
|
|
@@ -1877,7 +1964,7 @@ function DocsPage() {
|
|
|
1877
1964
|
<mdx-content key={site.theme === 'vepa' ? pathname : undefined} id={(site.theme === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(site.theme)) && !entry?.api ? 'content' : undefined} class="velu-prose">
|
|
1878
1965
|
{PageComponent ? (
|
|
1879
1966
|
<ErrorBoundary key={pathname} file={entry?.relPath}>
|
|
1880
|
-
{entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent /></ApiReferencePage> : <PageComponent />}
|
|
1967
|
+
{entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent user={readerUser} /></ApiReferencePage> : <PageComponent user={readerUser} />}
|
|
1881
1968
|
</ErrorBoundary>
|
|
1882
1969
|
) : (
|
|
1883
1970
|
<p style={{ color: 'var(--muted-color)' }}>
|
|
@@ -1910,14 +1997,14 @@ function DocsPage() {
|
|
|
1910
1997
|
sidebar reading order of the active section. The wrapper
|
|
1911
1998
|
doubles as the AskBar fade anchor on themes without a
|
|
1912
1999
|
PageFeedback widget (see the observer above). */}
|
|
1913
|
-
{chrome.articleChrome && !frontmatter.hidePagination && (
|
|
2000
|
+
{chrome.articleChrome && !frontmatter.hidePagination && (visibleNav?.prev || visibleNav?.next) && (
|
|
1914
2001
|
<div ref={setNavAnchorEl}>
|
|
1915
2002
|
<PageNav
|
|
1916
2003
|
id="pagination"
|
|
1917
2004
|
style={['thulir', 'aalam', 'nila', 'kadhir'].includes(site.theme) ? undefined : { marginTop: 'var(--s2)' }}
|
|
1918
2005
|
variant={site.theme === 'aalam' ? 'aalam' : 'default'}
|
|
1919
|
-
prev={
|
|
1920
|
-
next={
|
|
2006
|
+
prev={visibleNav?.prev}
|
|
2007
|
+
next={visibleNav?.next}
|
|
1921
2008
|
linkComponent={RouterLink}
|
|
1922
2009
|
/>
|
|
1923
2010
|
</div>
|