@aravindc26/velu 0.12.8 → 0.12.10
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/package.json +1 -1
- package/src/build.ts +13 -0
- package/src/cli.ts +60 -11
- package/src/engine/app/(docs)/[...slug]/layout.tsx +21 -537
- package/src/engine/app/_preview/[sessionId]/[...slug]/layout.tsx +96 -0
- package/src/engine/app/_preview/[sessionId]/[...slug]/page.tsx +298 -0
- package/src/engine/app/_preview/[sessionId]/layout.tsx +56 -0
- package/src/{preview-engine/app → engine/app/_preview}/[sessionId]/page.tsx +7 -3
- package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/assets/[...path]/route.ts +1 -1
- package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/init/route.ts +2 -2
- package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/route.ts +3 -3
- package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/sync/route.ts +2 -2
- package/src/{preview-engine/app → engine/app/_preview}/layout.tsx +4 -1
- package/src/engine/app/global.css +0 -3623
- package/src/engine/app/layout.tsx +4 -3
- package/src/engine/components/sidebar-links.tsx +11 -5
- package/src/engine/lib/docs-layout.tsx +605 -0
- package/src/engine/lib/layout.shared.ts +7 -7
- package/src/engine/lib/preview-config.ts +129 -0
- package/src/{preview-engine/lib/content-generator.ts → engine/lib/preview-content.ts} +238 -42
- package/src/engine/lib/source.ts +80 -97
- package/src/engine/lib/velu.ts +79 -55
- package/src/engine/mdx-components.tsx +14 -650
- package/src/engine/source.config.ts +11 -89
- package/src/engine/tsconfig.json +1 -0
- package/src/engine-core/components/assistant.tsx +361 -0
- package/src/engine-core/components/banner.tsx +80 -0
- package/src/engine-core/components/changelog-filters.tsx +114 -0
- package/src/engine-core/components/code-group.tsx +383 -0
- package/src/engine-core/components/color.tsx +118 -0
- package/src/engine-core/components/copy-page.tsx +223 -0
- package/src/engine-core/components/dropdown-switcher.tsx +142 -0
- package/src/engine-core/components/expandable.tsx +77 -0
- package/src/engine-core/components/header-tab-link.tsx +43 -0
- package/src/engine-core/components/icon.tsx +136 -0
- package/src/engine-core/components/image-zoom-fallback.tsx +147 -0
- package/src/engine-core/components/image.tsx +111 -0
- package/src/engine-core/components/lang-switcher.tsx +101 -0
- package/src/engine-core/components/manual-api-playground.tsx +154 -0
- package/src/engine-core/components/mermaid.tsx +142 -0
- package/src/engine-core/components/openapi-toc-sync.tsx +59 -0
- package/src/engine-core/components/openapi.tsx +1682 -0
- package/src/engine-core/components/page-feedback-api.test.ts +83 -0
- package/src/engine-core/components/page-feedback-api.ts +89 -0
- package/src/engine-core/components/page-feedback.tsx +200 -0
- package/src/engine-core/components/product-switcher.tsx +107 -0
- package/src/engine-core/components/prompt.tsx +90 -0
- package/src/engine-core/components/providers.tsx +21 -0
- package/src/engine-core/components/search.tsx +318 -0
- package/src/engine-core/components/sidebar-links.tsx +54 -0
- package/src/engine-core/components/synced-tabs.tsx +57 -0
- package/src/engine-core/components/theme-toggle.tsx +39 -0
- package/src/engine-core/components/toc-examples.tsx +110 -0
- package/src/engine-core/components/version-switcher.tsx +95 -0
- package/src/engine-core/components/view.tsx +344 -0
- package/src/engine-core/css/assistant.css +326 -0
- package/src/engine-core/css/copy-page.css +206 -0
- package/src/engine-core/css/search.css +142 -0
- package/src/engine-core/css/shared.css +3628 -0
- package/src/engine-core/lib/remark-plugins.ts +102 -0
- package/src/engine-core/lib/source-plugins.ts +105 -0
- package/src/engine-core/mdx-components.tsx +654 -0
- package/src/engine-core/types.ts +49 -0
- package/src/preview-engine/app/[sessionId]/[...slug]/page.tsx +0 -41
- package/src/preview-engine/app/[sessionId]/layout.tsx +0 -26
- package/src/preview-engine/app/global.css +0 -29
- package/src/preview-engine/lib/session-config.ts +0 -86
- package/src/preview-engine/lib/session-layout.ts +0 -190
- package/src/preview-engine/lib/source.ts +0 -60
- package/src/preview-engine/next.config.mjs +0 -20
- package/src/preview-engine/postcss.config.mjs +0 -8
- package/src/preview-engine/source.config.ts +0 -26
- package/src/preview-engine/tsconfig.json +0 -32
- package/src/preview-engine/tsconfig.tsbuildinfo +0 -1
- /package/src/{preview-engine/app → engine/app/_preview}/page.tsx +0 -0
- /package/src/{preview-engine/lib/auth.ts → engine/lib/preview-auth.ts} +0 -0
package/src/engine/lib/source.ts
CHANGED
|
@@ -1,117 +1,22 @@
|
|
|
1
1
|
import { loader } from 'fumadocs-core/source';
|
|
2
|
-
import { statusBadgesPlugin } from 'fumadocs-core/source/status-badges';
|
|
3
2
|
import * as mdxCollections from 'fumadocs-mdx:collections/server';
|
|
4
|
-
import { createElement } from 'react';
|
|
5
3
|
import { getLanguages } from '@/lib/velu';
|
|
4
|
+
import { openApiSidebarMethodBadgePlugin, createStatusBadgesPlugin } from '@core/lib/source-plugins';
|
|
6
5
|
|
|
7
6
|
const languages = getLanguages();
|
|
8
7
|
const defaultLanguage = languages[0] ?? 'en';
|
|
9
|
-
const OPENAPI_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE', 'WEBHOOK']);
|
|
10
8
|
const docsCollection = (mdxCollections as { docs?: { toFumadocsSource?: () => unknown } }).docs;
|
|
11
9
|
|
|
12
10
|
if (!docsCollection?.toFumadocsSource) {
|
|
13
11
|
throw new Error('MDX collections are not ready yet. Please retry in a moment.');
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
function methodBadgeClass(method: string): string {
|
|
17
|
-
const upper = method.toUpperCase();
|
|
18
|
-
if (upper === 'POST') return 'velu-openapi-method-badge velu-openapi-method-post';
|
|
19
|
-
if (upper === 'PUT') return 'velu-openapi-method-badge velu-openapi-method-put';
|
|
20
|
-
if (upper === 'PATCH') return 'velu-openapi-method-badge velu-openapi-method-patch';
|
|
21
|
-
if (upper === 'DELETE') return 'velu-openapi-method-badge velu-openapi-method-delete';
|
|
22
|
-
if (upper === 'WEBHOOK') return 'velu-openapi-method-badge velu-openapi-method-webhook';
|
|
23
|
-
return 'velu-openapi-method-badge velu-openapi-method-get';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parseOperationReference(value: string, requireUppercaseMethod = false): { method: string; target: string } | null {
|
|
27
|
-
const trimmed = value.trim();
|
|
28
|
-
if (!trimmed) return null;
|
|
29
|
-
const withSpec = trimmed.match(/^(\S+)\s+([A-Za-z]+)\s+(.+)$/);
|
|
30
|
-
if (withSpec) {
|
|
31
|
-
const rawMethod = withSpec[2];
|
|
32
|
-
const method = withSpec[2].toUpperCase();
|
|
33
|
-
if (requireUppercaseMethod && rawMethod !== method) return null;
|
|
34
|
-
if (!OPENAPI_METHODS.has(method)) return null;
|
|
35
|
-
return { method, target: withSpec[3].trim() };
|
|
36
|
-
}
|
|
37
|
-
const noSpec = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
|
|
38
|
-
if (noSpec) {
|
|
39
|
-
const rawMethod = noSpec[1];
|
|
40
|
-
const method = noSpec[1].toUpperCase();
|
|
41
|
-
if (requireUppercaseMethod && rawMethod !== method) return null;
|
|
42
|
-
if (!OPENAPI_METHODS.has(method)) return null;
|
|
43
|
-
return { method, target: noSpec[2].trim() };
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function stripMethodPrefix(name: string, method: string): string {
|
|
49
|
-
const regex = new RegExp(`^${method}\\s+`, 'i');
|
|
50
|
-
return name.replace(regex, '').trim();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function openApiSidebarMethodBadgePlugin() {
|
|
54
|
-
return {
|
|
55
|
-
name: 'velu:openapi-sidebar-method-badge',
|
|
56
|
-
transformPageTree: {
|
|
57
|
-
file(node: Record<string, unknown>, filePath?: string) {
|
|
58
|
-
let data: Record<string, unknown> = {};
|
|
59
|
-
if (filePath) {
|
|
60
|
-
const file = (this as { storage?: { read?: (path: string) => unknown } }).storage?.read?.(filePath) as
|
|
61
|
-
| { format?: string; data?: Record<string, unknown> }
|
|
62
|
-
| undefined;
|
|
63
|
-
if (file?.format === 'page') data = file.data ?? {};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const nameCandidate = typeof node.name === 'string' ? node.name.trim() : '';
|
|
67
|
-
const titleCandidate = typeof data.title === 'string' ? data.title.trim() : '';
|
|
68
|
-
const openApiCandidate = typeof data.openapi === 'string' ? data.openapi.trim() : '';
|
|
69
|
-
const parsed = openApiCandidate
|
|
70
|
-
? parseOperationReference(openApiCandidate)
|
|
71
|
-
: parseOperationReference(nameCandidate, true) ?? parseOperationReference(titleCandidate, true);
|
|
72
|
-
if (!parsed) return node;
|
|
73
|
-
|
|
74
|
-
const method = parsed.method;
|
|
75
|
-
const rawName = nameCandidate || titleCandidate || parsed.target;
|
|
76
|
-
const text = stripMethodPrefix(rawName, method) || parsed.target || rawName || method;
|
|
77
|
-
const stableIdRaw = filePath || openApiCandidate || rawName || `${method}-${parsed.target}`;
|
|
78
|
-
const stableId = stableIdRaw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
79
|
-
|
|
80
|
-
node.name = createElement(
|
|
81
|
-
'span',
|
|
82
|
-
{ className: 'velu-openapi-sidebar-item', key: `openapi-item-${stableId || 'unknown'}` },
|
|
83
|
-
createElement(
|
|
84
|
-
'span',
|
|
85
|
-
{ className: methodBadgeClass(method), key: `openapi-item-${stableId || 'unknown'}-method` },
|
|
86
|
-
method,
|
|
87
|
-
),
|
|
88
|
-
createElement(
|
|
89
|
-
'span',
|
|
90
|
-
{ className: 'velu-openapi-sidebar-label', key: `openapi-item-${stableId || 'unknown'}-label` },
|
|
91
|
-
text,
|
|
92
|
-
),
|
|
93
|
-
);
|
|
94
|
-
return node;
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
14
|
export const source = loader({
|
|
101
15
|
baseUrl: '/',
|
|
102
16
|
source: docsCollection.toFumadocsSource() as any,
|
|
103
17
|
plugins: [
|
|
104
18
|
openApiSidebarMethodBadgePlugin() as any,
|
|
105
|
-
|
|
106
|
-
renderBadge: (status: string) => {
|
|
107
|
-
const normalized = status.trim().toLowerCase();
|
|
108
|
-
const label = normalized === 'deprecated' ? 'Deprecated' : status;
|
|
109
|
-
const className = normalized === 'deprecated'
|
|
110
|
-
? 'velu-status-badge velu-status-badge-deprecated'
|
|
111
|
-
: 'velu-status-badge';
|
|
112
|
-
return createElement('span', { className, 'data-status': normalized }, label);
|
|
113
|
-
},
|
|
114
|
-
}),
|
|
19
|
+
createStatusBadgesPlugin(),
|
|
115
20
|
],
|
|
116
21
|
i18n:
|
|
117
22
|
languages.length > 1
|
|
@@ -124,3 +29,81 @@ export const source = loader({
|
|
|
124
29
|
}
|
|
125
30
|
: undefined,
|
|
126
31
|
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the page tree filtered to a specific session's content.
|
|
35
|
+
* The content directory has files at {sessionId}/{slug}.mdx,
|
|
36
|
+
* so the page tree has top-level folders per session.
|
|
37
|
+
*/
|
|
38
|
+
export function getSessionPageTree(sessionId: string) {
|
|
39
|
+
const fullTree = source.getPageTree();
|
|
40
|
+
const children = Array.isArray(fullTree.children) ? fullTree.children : [];
|
|
41
|
+
|
|
42
|
+
// Find the root folder matching this session ID
|
|
43
|
+
const sessionFolder = children.find((child: any) => {
|
|
44
|
+
if (child?.type !== 'folder') return false;
|
|
45
|
+
const urls = collectUrls(child);
|
|
46
|
+
for (const url of urls) {
|
|
47
|
+
const firstSegment = url.replace(/^\/+/, '').split('/')[0];
|
|
48
|
+
if (firstSegment === sessionId) return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}) as any;
|
|
52
|
+
|
|
53
|
+
if (sessionFolder && Array.isArray(sessionFolder.children)) {
|
|
54
|
+
// Mark first-level folders as root (fumadocs sets root:true on top-level folders,
|
|
55
|
+
// but after extracting session children they lose that flag)
|
|
56
|
+
const children = sessionFolder.children.map((child: any) => {
|
|
57
|
+
if (child?.type === 'folder') return { ...child, root: true };
|
|
58
|
+
return child;
|
|
59
|
+
});
|
|
60
|
+
return stripUrlPrefix({ ...fullTree, children }, sessionId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fallback: filter children by URL prefix
|
|
64
|
+
const filtered = children.filter((child: any) => {
|
|
65
|
+
const urls = collectUrls(child);
|
|
66
|
+
return urls.some((url: string) => {
|
|
67
|
+
const segments = url.replace(/^\/+/, '').split('/');
|
|
68
|
+
return segments[0] === sessionId;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return stripUrlPrefix({ ...fullTree, children: filtered }, sessionId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Recursively strip the session prefix from all URLs in the tree
|
|
77
|
+
* so that /mint-test/platform/... becomes /platform/...
|
|
78
|
+
*/
|
|
79
|
+
function stripUrlPrefix(tree: any, sessionId: string): any {
|
|
80
|
+
const prefix = `/${sessionId}`;
|
|
81
|
+
function stripUrl(url: string): string {
|
|
82
|
+
if (url === prefix || url === `${prefix}/`) return '/';
|
|
83
|
+
if (url.startsWith(`${prefix}/`)) return url.slice(prefix.length);
|
|
84
|
+
return url;
|
|
85
|
+
}
|
|
86
|
+
function walk(node: any): any {
|
|
87
|
+
if (!node || typeof node !== 'object') return node;
|
|
88
|
+
const copy = { ...node };
|
|
89
|
+
if (typeof copy.url === 'string') copy.url = stripUrl(copy.url);
|
|
90
|
+
if (copy.index && typeof copy.index.url === 'string') {
|
|
91
|
+
copy.index = { ...copy.index, url: stripUrl(copy.index.url) };
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(copy.children)) {
|
|
94
|
+
copy.children = copy.children.map(walk);
|
|
95
|
+
}
|
|
96
|
+
return copy;
|
|
97
|
+
}
|
|
98
|
+
return walk(tree);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function collectUrls(node: any, out: string[] = []): string[] {
|
|
102
|
+
if (!node || typeof node !== 'object') return out;
|
|
103
|
+
if (typeof node.url === 'string') out.push(node.url);
|
|
104
|
+
if (node.index && typeof node.index.url === 'string') out.push(node.index.url);
|
|
105
|
+
if (Array.isArray(node.children)) {
|
|
106
|
+
for (const child of node.children) collectUrls(child, out);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
package/src/engine/lib/velu.ts
CHANGED
|
@@ -185,7 +185,22 @@ export interface VeluResolvedFonts {
|
|
|
185
185
|
body?: VeluFontDef;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
interface
|
|
188
|
+
export interface VeluConfigSource {
|
|
189
|
+
config: VeluConfig;
|
|
190
|
+
rawConfig: Record<string, unknown>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function loadConfigFromPath(configPath: string): VeluConfigSource {
|
|
194
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
const config = normalizeConfigNavigation(parsed) as VeluConfig;
|
|
197
|
+
const rawConfig = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
198
|
+
? parsed as Record<string, unknown>
|
|
199
|
+
: {};
|
|
200
|
+
return { config, rawConfig };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface VeluConfig {
|
|
189
204
|
name?: string;
|
|
190
205
|
description?: string;
|
|
191
206
|
title?: string;
|
|
@@ -232,6 +247,11 @@ let cachedRawConfig: Record<string, unknown> | null = null;
|
|
|
232
247
|
function loadVeluConfig(): VeluConfig {
|
|
233
248
|
if (cachedConfig) return cachedConfig;
|
|
234
249
|
const configPath = resolveConfigPath(process.cwd());
|
|
250
|
+
if (!existsSync(configPath)) {
|
|
251
|
+
// Preview mode: no singleton config, return empty defaults
|
|
252
|
+
cachedConfig = {} as VeluConfig;
|
|
253
|
+
return cachedConfig;
|
|
254
|
+
}
|
|
235
255
|
const raw = readFileSync(configPath, 'utf-8');
|
|
236
256
|
cachedConfig = normalizeConfigNavigation(JSON.parse(raw)) as VeluConfig;
|
|
237
257
|
return cachedConfig;
|
|
@@ -240,6 +260,10 @@ function loadVeluConfig(): VeluConfig {
|
|
|
240
260
|
function loadRawConfig(): Record<string, unknown> {
|
|
241
261
|
if (cachedRawConfig) return cachedRawConfig;
|
|
242
262
|
const configPath = resolveConfigPath(process.cwd());
|
|
263
|
+
if (!existsSync(configPath)) {
|
|
264
|
+
cachedRawConfig = {};
|
|
265
|
+
return cachedRawConfig;
|
|
266
|
+
}
|
|
243
267
|
const raw = readFileSync(configPath, 'utf-8');
|
|
244
268
|
const parsed = JSON.parse(raw);
|
|
245
269
|
cachedRawConfig = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
@@ -450,8 +474,8 @@ export interface VeluBannerConfig {
|
|
|
450
474
|
dismissible: boolean;
|
|
451
475
|
}
|
|
452
476
|
|
|
453
|
-
export function getBannerConfig(): VeluBannerConfig | null {
|
|
454
|
-
const config = loadVeluConfig();
|
|
477
|
+
export function getBannerConfig(src?: VeluConfigSource): VeluBannerConfig | null {
|
|
478
|
+
const config = src?.config ?? loadVeluConfig();
|
|
455
479
|
const banner = config.banner;
|
|
456
480
|
if (!banner) return null;
|
|
457
481
|
const content = typeof banner.content === 'string' ? banner.content.trim() : '';
|
|
@@ -459,8 +483,8 @@ export function getBannerConfig(): VeluBannerConfig | null {
|
|
|
459
483
|
return { content, dismissible: banner.dismissible === true };
|
|
460
484
|
}
|
|
461
485
|
|
|
462
|
-
export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
463
|
-
const config = loadVeluConfig();
|
|
486
|
+
export function getExternalTabs(src?: VeluConfigSource): Array<{ label: string; href: string }> {
|
|
487
|
+
const config = src?.config ?? loadVeluConfig();
|
|
464
488
|
const tabs = config.navigation?.tabs ?? [];
|
|
465
489
|
const globalTabs = config.navigation?.global?.tabs ?? [];
|
|
466
490
|
|
|
@@ -481,22 +505,22 @@ export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
|
481
505
|
return [...tabLinks, ...globalLinks];
|
|
482
506
|
}
|
|
483
507
|
|
|
484
|
-
export function getNavbarAnchors(): VeluAnchor[] {
|
|
485
|
-
const config = loadVeluConfig();
|
|
486
|
-
return (config.navigation
|
|
508
|
+
export function getNavbarAnchors(src?: VeluConfigSource): VeluAnchor[] {
|
|
509
|
+
const config = src?.config ?? loadVeluConfig();
|
|
510
|
+
return (config.navigation?.anchors ?? []).filter(
|
|
487
511
|
(a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
|
|
488
512
|
);
|
|
489
513
|
}
|
|
490
514
|
|
|
491
|
-
export function getGlobalAnchors(): VeluAnchor[] {
|
|
492
|
-
const config = loadVeluConfig();
|
|
493
|
-
return (config.navigation
|
|
515
|
+
export function getGlobalAnchors(src?: VeluConfigSource): VeluAnchor[] {
|
|
516
|
+
const config = src?.config ?? loadVeluConfig();
|
|
517
|
+
return (config.navigation?.global?.anchors ?? []).filter(
|
|
494
518
|
(a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
|
|
495
519
|
);
|
|
496
520
|
}
|
|
497
521
|
|
|
498
|
-
export function getFooterSocials(): VeluFooterSocialLink[] {
|
|
499
|
-
const config = loadVeluConfig();
|
|
522
|
+
export function getFooterSocials(src?: VeluConfigSource): VeluFooterSocialLink[] {
|
|
523
|
+
const config = src?.config ?? loadVeluConfig();
|
|
500
524
|
const source = config.footer?.socials ?? config.footerSocials;
|
|
501
525
|
const fromMap = normalizeFooterSocialMap(source);
|
|
502
526
|
const fromList = normalizeFooterSocialList(source);
|
|
@@ -549,30 +573,30 @@ function collectTabMenus(section: unknown, out: VeluTabMenuDefinition[]): void {
|
|
|
549
573
|
}
|
|
550
574
|
}
|
|
551
575
|
|
|
552
|
-
export function getTabMenuDefinitions(): VeluTabMenuDefinition[] {
|
|
553
|
-
const raw = loadRawConfig();
|
|
576
|
+
export function getTabMenuDefinitions(src?: VeluConfigSource): VeluTabMenuDefinition[] {
|
|
577
|
+
const raw = src?.rawConfig ?? loadRawConfig();
|
|
554
578
|
const navigation = isRecord(raw.navigation) ? raw.navigation : {};
|
|
555
579
|
const out: VeluTabMenuDefinition[] = [];
|
|
556
580
|
collectTabMenus(navigation, out);
|
|
557
581
|
return out;
|
|
558
582
|
}
|
|
559
583
|
|
|
560
|
-
export function getLanguages(): string[] {
|
|
561
|
-
const config = loadVeluConfig();
|
|
584
|
+
export function getLanguages(src?: VeluConfigSource): string[] {
|
|
585
|
+
const config = src?.config ?? loadVeluConfig();
|
|
562
586
|
// Prefer navigation.languages codes, fall back to top-level languages
|
|
563
|
-
if (config.navigation
|
|
587
|
+
if (config.navigation?.languages && config.navigation.languages.length > 0) {
|
|
564
588
|
return config.navigation.languages.map((l) => l.language);
|
|
565
589
|
}
|
|
566
590
|
return config.languages ?? [];
|
|
567
591
|
}
|
|
568
592
|
|
|
569
|
-
export function getDropdownOptions(): VeluDropdownOption[] {
|
|
570
|
-
const raw = loadRawConfig();
|
|
593
|
+
export function getDropdownOptions(src?: VeluConfigSource): VeluDropdownOption[] {
|
|
594
|
+
const raw = src?.rawConfig ?? loadRawConfig();
|
|
571
595
|
const navigation = isRecord(raw.navigation) ? raw.navigation : {};
|
|
572
596
|
const dropdowns = Array.isArray(navigation.dropdowns) ? navigation.dropdowns : [];
|
|
573
597
|
if (dropdowns.length === 0) return [];
|
|
574
598
|
|
|
575
|
-
const allTabs = loadVeluConfig().navigation.tabs ?? [];
|
|
599
|
+
const allTabs = (src?.config ?? loadVeluConfig()).navigation.tabs ?? [];
|
|
576
600
|
const out: VeluDropdownOption[] = [];
|
|
577
601
|
|
|
578
602
|
dropdowns.forEach((entry, index) => {
|
|
@@ -606,12 +630,12 @@ export function getDropdownOptions(): VeluDropdownOption[] {
|
|
|
606
630
|
return out;
|
|
607
631
|
}
|
|
608
632
|
|
|
609
|
-
export function getProductOptions(): VeluProductOption[] {
|
|
610
|
-
const config = loadVeluConfig();
|
|
611
|
-
const products = (config.navigation
|
|
633
|
+
export function getProductOptions(src?: VeluConfigSource): VeluProductOption[] {
|
|
634
|
+
const config = src?.config ?? loadVeluConfig();
|
|
635
|
+
const products = (config.navigation?.products ?? []).filter((p) => !p.hidden);
|
|
612
636
|
if (products.length === 0) return [];
|
|
613
637
|
|
|
614
|
-
const allTabs = config.navigation
|
|
638
|
+
const allTabs = config.navigation?.tabs ?? [];
|
|
615
639
|
|
|
616
640
|
return products.map((product, index) => {
|
|
617
641
|
const prefix = slugify(product.product, `product-${index + 1}`);
|
|
@@ -642,12 +666,12 @@ export function getProductOptions(): VeluProductOption[] {
|
|
|
642
666
|
});
|
|
643
667
|
}
|
|
644
668
|
|
|
645
|
-
export function getVersionOptions(): VeluVersionOption[] {
|
|
646
|
-
const config = loadVeluConfig();
|
|
647
|
-
const versions = (config.navigation
|
|
669
|
+
export function getVersionOptions(src?: VeluConfigSource): VeluVersionOption[] {
|
|
670
|
+
const config = src?.config ?? loadVeluConfig();
|
|
671
|
+
const versions = (config.navigation?.versions ?? []).filter((v) => !v.hidden);
|
|
648
672
|
if (versions.length === 0) return [];
|
|
649
673
|
|
|
650
|
-
const allTabs = config.navigation
|
|
674
|
+
const allTabs = config.navigation?.tabs ?? [];
|
|
651
675
|
|
|
652
676
|
const baseEntries = versions.map((version, index) => {
|
|
653
677
|
const prefix = slugify(version.version, `version-${index + 1}`);
|
|
@@ -696,16 +720,16 @@ export function getVersionOptions(): VeluVersionOption[] {
|
|
|
696
720
|
}));
|
|
697
721
|
}
|
|
698
722
|
|
|
699
|
-
export function getAppearance(): 'system' | 'light' | 'dark' {
|
|
700
|
-
const appearance = loadVeluConfig().appearance;
|
|
723
|
+
export function getAppearance(src?: VeluConfigSource): 'system' | 'light' | 'dark' {
|
|
724
|
+
const appearance = (src?.config ?? loadVeluConfig()).appearance;
|
|
701
725
|
if (appearance === 'light' || appearance === 'dark') return appearance;
|
|
702
726
|
return 'system';
|
|
703
727
|
}
|
|
704
728
|
|
|
705
729
|
export type VeluIconLibrary = 'fontawesome' | 'lucide' | 'tabler';
|
|
706
730
|
|
|
707
|
-
export function getFontsConfig(): VeluResolvedFonts | null {
|
|
708
|
-
const raw = loadVeluConfig().fonts;
|
|
731
|
+
export function getFontsConfig(src?: VeluConfigSource): VeluResolvedFonts | null {
|
|
732
|
+
const raw = (src?.config ?? loadVeluConfig()).fonts;
|
|
709
733
|
if (!raw) return null;
|
|
710
734
|
// Single font definition (has 'family') → apply to both heading and body
|
|
711
735
|
if ('family' in raw && typeof raw.family === 'string') {
|
|
@@ -717,8 +741,8 @@ export function getFontsConfig(): VeluResolvedFonts | null {
|
|
|
717
741
|
return { heading: obj.heading, body: obj.body };
|
|
718
742
|
}
|
|
719
743
|
|
|
720
|
-
export function getIconLibrary(): VeluIconLibrary {
|
|
721
|
-
const raw = loadVeluConfig().icons?.library;
|
|
744
|
+
export function getIconLibrary(src?: VeluConfigSource): VeluIconLibrary {
|
|
745
|
+
const raw = (src?.config ?? loadVeluConfig()).icons?.library;
|
|
722
746
|
if (raw === 'lucide' || raw === 'tabler' || raw === 'fontawesome') return raw;
|
|
723
747
|
return 'fontawesome';
|
|
724
748
|
}
|
|
@@ -857,8 +881,8 @@ function extractOrigin(value: string | undefined): string | undefined {
|
|
|
857
881
|
}
|
|
858
882
|
}
|
|
859
883
|
|
|
860
|
-
export function getApiConfig(): VeluResolvedApiConfig {
|
|
861
|
-
const config = loadVeluConfig();
|
|
884
|
+
export function getApiConfig(src?: VeluConfigSource): VeluResolvedApiConfig {
|
|
885
|
+
const config = src?.config ?? loadVeluConfig();
|
|
862
886
|
const api = config.api;
|
|
863
887
|
const auth = api?.mdx?.auth;
|
|
864
888
|
const examples = api?.examples;
|
|
@@ -885,8 +909,8 @@ export function getApiConfig(): VeluResolvedApiConfig {
|
|
|
885
909
|
};
|
|
886
910
|
}
|
|
887
911
|
|
|
888
|
-
export function getSeoConfig(): VeluResolvedSeoConfig {
|
|
889
|
-
const config = loadVeluConfig();
|
|
912
|
+
export function getSeoConfig(src?: VeluConfigSource): VeluResolvedSeoConfig {
|
|
913
|
+
const config = src?.config ?? loadVeluConfig();
|
|
890
914
|
const seo = config.seo;
|
|
891
915
|
const indexing: 'navigable' | 'all' = seo?.indexing === 'all' ? 'all' : 'navigable';
|
|
892
916
|
return {
|
|
@@ -895,15 +919,15 @@ export function getSeoConfig(): VeluResolvedSeoConfig {
|
|
|
895
919
|
};
|
|
896
920
|
}
|
|
897
921
|
|
|
898
|
-
export function getMetadataConfig(): VeluResolvedMetadataConfig {
|
|
899
|
-
const config = loadVeluConfig();
|
|
922
|
+
export function getMetadataConfig(src?: VeluConfigSource): VeluResolvedMetadataConfig {
|
|
923
|
+
const config = src?.config ?? loadVeluConfig();
|
|
900
924
|
return {
|
|
901
925
|
timestamp: config.metadata?.timestamp === true,
|
|
902
926
|
};
|
|
903
927
|
}
|
|
904
928
|
|
|
905
|
-
export function getSiteName(): string {
|
|
906
|
-
const config = loadVeluConfig();
|
|
929
|
+
export function getSiteName(src?: VeluConfigSource): string {
|
|
930
|
+
const config = src?.config ?? loadVeluConfig();
|
|
907
931
|
const fromName = normalizeAssetPath(config.name);
|
|
908
932
|
if (fromName) return fromName;
|
|
909
933
|
const fromTitle = normalizeAssetPath(config.title);
|
|
@@ -911,24 +935,24 @@ export function getSiteName(): string {
|
|
|
911
935
|
return 'Velu Docs';
|
|
912
936
|
}
|
|
913
937
|
|
|
914
|
-
export function getSiteDescription(): string | undefined {
|
|
915
|
-
const config = loadVeluConfig();
|
|
938
|
+
export function getSiteDescription(src?: VeluConfigSource): string | undefined {
|
|
939
|
+
const config = src?.config ?? loadVeluConfig();
|
|
916
940
|
return normalizeAssetPath(config.description);
|
|
917
941
|
}
|
|
918
942
|
|
|
919
|
-
export function getSiteFavicon(): string | undefined {
|
|
920
|
-
const config = loadVeluConfig();
|
|
943
|
+
export function getSiteFavicon(src?: VeluConfigSource): string | undefined {
|
|
944
|
+
const config = src?.config ?? loadVeluConfig();
|
|
921
945
|
const asset = normalizeThemeAsset(config.favicon);
|
|
922
946
|
return asset.light ?? asset.dark;
|
|
923
947
|
}
|
|
924
948
|
|
|
925
|
-
export function getSiteLogoAsset(): VeluThemeAsset {
|
|
926
|
-
const config = loadVeluConfig();
|
|
949
|
+
export function getSiteLogoAsset(src?: VeluConfigSource): VeluThemeAsset {
|
|
950
|
+
const config = src?.config ?? loadVeluConfig();
|
|
927
951
|
return normalizeThemeAsset(config.logo);
|
|
928
952
|
}
|
|
929
953
|
|
|
930
|
-
export function getSitePrimaryColor(): string | undefined {
|
|
931
|
-
const config = loadVeluConfig();
|
|
954
|
+
export function getSitePrimaryColor(src?: VeluConfigSource): string | undefined {
|
|
955
|
+
const config = src?.config ?? loadVeluConfig();
|
|
932
956
|
const colors = config.colors;
|
|
933
957
|
if (!colors) return undefined;
|
|
934
958
|
return normalizeAssetPath(colors.primary) ?? normalizeAssetPath(colors.light) ?? normalizeAssetPath(colors.dark);
|
|
@@ -957,8 +981,8 @@ const CONTEXTUAL_PRESETS: Record<string, { title: string; description: string }>
|
|
|
957
981
|
|
|
958
982
|
const DEFAULT_CONTEXTUAL_OPTIONS = ['copy', 'chatgpt', 'claude', 'add-mcp', 'cursor', 'vscode'];
|
|
959
983
|
|
|
960
|
-
export function getContextualOptions(): VeluContextualOption[] {
|
|
961
|
-
const config = loadVeluConfig();
|
|
984
|
+
export function getContextualOptions(src?: VeluConfigSource): VeluContextualOption[] {
|
|
985
|
+
const config = src?.config ?? loadVeluConfig();
|
|
962
986
|
const raw = config.contextual?.options;
|
|
963
987
|
|
|
964
988
|
const ids = raw ?? DEFAULT_CONTEXTUAL_OPTIONS;
|
|
@@ -988,8 +1012,8 @@ export function getContextualOptions(): VeluContextualOption[] {
|
|
|
988
1012
|
return options;
|
|
989
1013
|
}
|
|
990
1014
|
|
|
991
|
-
export function getSiteOrigin(): string {
|
|
992
|
-
const seo = getSeoConfig();
|
|
1015
|
+
export function getSiteOrigin(src?: VeluConfigSource): string {
|
|
1016
|
+
const seo = getSeoConfig(src);
|
|
993
1017
|
const envCandidates = [
|
|
994
1018
|
process.env.VELU_SITE_URL,
|
|
995
1019
|
process.env.NEXT_PUBLIC_SITE_URL,
|