@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.
Files changed (76) hide show
  1. package/package.json +1 -1
  2. package/src/build.ts +13 -0
  3. package/src/cli.ts +60 -11
  4. package/src/engine/app/(docs)/[...slug]/layout.tsx +21 -537
  5. package/src/engine/app/_preview/[sessionId]/[...slug]/layout.tsx +96 -0
  6. package/src/engine/app/_preview/[sessionId]/[...slug]/page.tsx +298 -0
  7. package/src/engine/app/_preview/[sessionId]/layout.tsx +56 -0
  8. package/src/{preview-engine/app → engine/app/_preview}/[sessionId]/page.tsx +7 -3
  9. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/assets/[...path]/route.ts +1 -1
  10. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/init/route.ts +2 -2
  11. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/route.ts +3 -3
  12. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/sync/route.ts +2 -2
  13. package/src/{preview-engine/app → engine/app/_preview}/layout.tsx +4 -1
  14. package/src/engine/app/global.css +0 -3623
  15. package/src/engine/app/layout.tsx +4 -3
  16. package/src/engine/components/sidebar-links.tsx +11 -5
  17. package/src/engine/lib/docs-layout.tsx +605 -0
  18. package/src/engine/lib/layout.shared.ts +7 -7
  19. package/src/engine/lib/preview-config.ts +129 -0
  20. package/src/{preview-engine/lib/content-generator.ts → engine/lib/preview-content.ts} +238 -42
  21. package/src/engine/lib/source.ts +80 -97
  22. package/src/engine/lib/velu.ts +79 -55
  23. package/src/engine/mdx-components.tsx +14 -650
  24. package/src/engine/source.config.ts +11 -89
  25. package/src/engine/tsconfig.json +1 -0
  26. package/src/engine-core/components/assistant.tsx +361 -0
  27. package/src/engine-core/components/banner.tsx +80 -0
  28. package/src/engine-core/components/changelog-filters.tsx +114 -0
  29. package/src/engine-core/components/code-group.tsx +383 -0
  30. package/src/engine-core/components/color.tsx +118 -0
  31. package/src/engine-core/components/copy-page.tsx +223 -0
  32. package/src/engine-core/components/dropdown-switcher.tsx +142 -0
  33. package/src/engine-core/components/expandable.tsx +77 -0
  34. package/src/engine-core/components/header-tab-link.tsx +43 -0
  35. package/src/engine-core/components/icon.tsx +136 -0
  36. package/src/engine-core/components/image-zoom-fallback.tsx +147 -0
  37. package/src/engine-core/components/image.tsx +111 -0
  38. package/src/engine-core/components/lang-switcher.tsx +101 -0
  39. package/src/engine-core/components/manual-api-playground.tsx +154 -0
  40. package/src/engine-core/components/mermaid.tsx +142 -0
  41. package/src/engine-core/components/openapi-toc-sync.tsx +59 -0
  42. package/src/engine-core/components/openapi.tsx +1682 -0
  43. package/src/engine-core/components/page-feedback-api.test.ts +83 -0
  44. package/src/engine-core/components/page-feedback-api.ts +89 -0
  45. package/src/engine-core/components/page-feedback.tsx +200 -0
  46. package/src/engine-core/components/product-switcher.tsx +107 -0
  47. package/src/engine-core/components/prompt.tsx +90 -0
  48. package/src/engine-core/components/providers.tsx +21 -0
  49. package/src/engine-core/components/search.tsx +318 -0
  50. package/src/engine-core/components/sidebar-links.tsx +54 -0
  51. package/src/engine-core/components/synced-tabs.tsx +57 -0
  52. package/src/engine-core/components/theme-toggle.tsx +39 -0
  53. package/src/engine-core/components/toc-examples.tsx +110 -0
  54. package/src/engine-core/components/version-switcher.tsx +95 -0
  55. package/src/engine-core/components/view.tsx +344 -0
  56. package/src/engine-core/css/assistant.css +326 -0
  57. package/src/engine-core/css/copy-page.css +206 -0
  58. package/src/engine-core/css/search.css +142 -0
  59. package/src/engine-core/css/shared.css +3628 -0
  60. package/src/engine-core/lib/remark-plugins.ts +102 -0
  61. package/src/engine-core/lib/source-plugins.ts +105 -0
  62. package/src/engine-core/mdx-components.tsx +654 -0
  63. package/src/engine-core/types.ts +49 -0
  64. package/src/preview-engine/app/[sessionId]/[...slug]/page.tsx +0 -41
  65. package/src/preview-engine/app/[sessionId]/layout.tsx +0 -26
  66. package/src/preview-engine/app/global.css +0 -29
  67. package/src/preview-engine/lib/session-config.ts +0 -86
  68. package/src/preview-engine/lib/session-layout.ts +0 -190
  69. package/src/preview-engine/lib/source.ts +0 -60
  70. package/src/preview-engine/next.config.mjs +0 -20
  71. package/src/preview-engine/postcss.config.mjs +0 -8
  72. package/src/preview-engine/source.config.ts +0 -26
  73. package/src/preview-engine/tsconfig.json +0 -32
  74. package/src/preview-engine/tsconfig.tsbuildinfo +0 -1
  75. /package/src/{preview-engine/app → engine/app/_preview}/page.tsx +0 -0
  76. /package/src/{preview-engine/lib/auth.ts → engine/lib/preview-auth.ts} +0 -0
@@ -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
- statusBadgesPlugin({
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
+ }
@@ -185,7 +185,22 @@ export interface VeluResolvedFonts {
185
185
  body?: VeluFontDef;
186
186
  }
187
187
 
188
- interface VeluConfig {
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.anchors ?? []).filter(
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.global?.anchors ?? []).filter(
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.languages && config.navigation.languages.length > 0) {
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.products ?? []).filter((p) => !p.hidden);
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.tabs ?? [];
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.versions ?? []).filter((v) => !v.hidden);
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.tabs ?? [];
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,