@aravindc26/velu 0.12.7 → 0.12.9

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 (74) hide show
  1. package/package.json +1 -1
  2. package/src/build.ts +13 -0
  3. package/src/cli.ts +51 -9
  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/engine/app/_preview/api/sessions/[sessionId]/assets/[...path]/route.ts +51 -0
  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} +242 -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 -23
  66. package/src/preview-engine/app/global.css +0 -3
  67. package/src/preview-engine/lib/session-config.ts +0 -86
  68. package/src/preview-engine/lib/source.ts +0 -60
  69. package/src/preview-engine/next.config.mjs +0 -20
  70. package/src/preview-engine/postcss.config.mjs +0 -8
  71. package/src/preview-engine/source.config.ts +0 -26
  72. package/src/preview-engine/tsconfig.json +0 -32
  73. /package/src/{preview-engine/app → engine/app/_preview}/page.tsx +0 -0
  74. /package/src/{preview-engine/lib/auth.ts → engine/lib/preview-auth.ts} +0 -0
@@ -1,6 +1,4 @@
1
- import { isValidElement, type ReactNode } from 'react';
2
- import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
3
- import type { LinkItemType } from 'fumadocs-ui/layouts/shared';
1
+ import type { ReactNode } from 'react';
4
2
  import { baseOptions } from '@/lib/layout.shared';
5
3
  import { source } from '@/lib/source';
6
4
  import {
@@ -10,13 +8,10 @@ import {
10
8
  getVersionOptions,
11
9
  getProductOptions,
12
10
  getTabMenuDefinitions,
13
- type VeluVersionOption,
14
- type VeluProductOption,
11
+ getGlobalAnchors,
12
+ getAppearance,
15
13
  } from '@/lib/velu';
16
- import { SidebarLinks } from '@/components/sidebar-links';
17
- import { ProductSwitcher } from '@/components/product-switcher';
18
- import { VeluIcon } from '@/components/icon';
19
- import { HeaderTabLink } from '@/components/header-tab-link';
14
+ import { renderDocsLayout, resolveLocale } from '@/lib/docs-layout';
20
15
 
21
16
  interface LayoutParams {
22
17
  slug?: string[];
@@ -27,538 +22,27 @@ interface SlugLayoutProps {
27
22
  params: Promise<LayoutParams>;
28
23
  }
29
24
 
30
- interface PageTreePageNode {
31
- type?: string;
32
- url?: string;
33
- external?: boolean;
34
- }
35
-
36
- interface PageTreeFolderNode {
37
- type?: string;
38
- name?: ReactNode;
39
- icon?: ReactNode;
40
- description?: ReactNode;
41
- root?: boolean;
42
- index?: { url?: string };
43
- children?: unknown[];
44
- }
45
-
46
- function withTrailingSlashUrl(url: string): string {
47
- const trimmed = url.trim();
48
- if (trimmed.length === 0) return trimmed;
49
- if (/^(https?:|mailto:|tel:|#)/i.test(trimmed)) return trimmed;
50
-
51
- const hashIndex = trimmed.indexOf('#');
52
- const queryIndex = trimmed.indexOf('?');
53
- const endIndex = [hashIndex, queryIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0] ?? trimmed.length;
54
- const path = trimmed.slice(0, endIndex);
55
- const suffix = trimmed.slice(endIndex);
56
-
57
- if (!path.startsWith('/')) return trimmed;
58
- if (path === '/' || path.endsWith('/')) return `${path}${suffix}`;
59
-
60
- const lastSegment = path.split('/').filter(Boolean).pop() ?? '';
61
- if (lastSegment.includes('.')) return trimmed;
62
-
63
- return `${path}/${suffix}`;
64
- }
65
-
66
- function resolveLocale(slugInput: string[] | undefined): string {
67
- const languages = getLanguages();
68
- const defaultLanguage = languages[0] ?? 'en';
69
- const slug = slugInput ?? [];
70
- const firstSeg = slug[0];
71
-
72
- return languages.includes(firstSeg ?? '') ? firstSeg! : defaultLanguage;
73
- }
74
-
75
- function resolveCurrentVersion(slugInput: string[] | undefined, versions: VeluVersionOption[]): VeluVersionOption | undefined {
76
- if (versions.length === 0) return undefined;
77
- const firstSeg = (slugInput ?? [])[0] ?? '';
78
- return versions.find((v) => v.slug === firstSeg) ?? versions.find((v) => v.isDefault) ?? versions[0];
79
- }
80
-
81
- function filterTreeBySlugPrefix<T extends { children?: unknown[] }>(tree: T, prefix?: string): T {
82
- if (!prefix) return tree;
83
-
84
- const normPrefix = prefix.replace(/^\/+|\/+$/g, '').toLowerCase();
85
- if (!normPrefix) return tree;
86
-
87
- const matchesPrefix = (value: string): boolean => {
88
- const norm = value.replace(/^\/+|\/+$/g, '').toLowerCase();
89
- return norm === normPrefix || norm.startsWith(`${normPrefix}/`) || norm.includes(`/${normPrefix}/`) || norm.endsWith(`/${normPrefix}`);
90
- };
91
-
92
- const filterNodes = (nodes: unknown[]): unknown[] => {
93
- const kept: unknown[] = [];
94
-
95
- for (const node of nodes) {
96
- if (typeof node !== 'object' || node === null) continue;
97
- const entry = node as {
98
- url?: unknown;
99
- path?: unknown;
100
- $ref?: { metaFile?: unknown; file?: unknown };
101
- children?: unknown[];
102
- };
103
-
104
- const candidates = [entry.url, entry.path, entry.$ref?.metaFile, entry.$ref?.file]
105
- .filter((value): value is string => typeof value === 'string');
106
- const selfMatch = candidates.some(matchesPrefix);
107
-
108
- const childNodes = Array.isArray(entry.children) ? entry.children : [];
109
- const filteredChildren = childNodes.length > 0 ? filterNodes(childNodes) : [];
110
- const childMatch = filteredChildren.length > 0;
111
-
112
- if (selfMatch || childMatch) {
113
- kept.push(childMatch ? { ...entry, children: filteredChildren } : entry);
114
- }
115
- }
116
-
117
- return kept;
118
- };
119
-
120
- const children = Array.isArray(tree.children) ? tree.children : [];
121
- const filtered = filterNodes(children);
122
- if (filtered.length === 0) return tree;
123
- return { ...tree, children: filtered } as T;
124
- }
125
-
126
- function resolveCurrentProduct(slugInput: string[] | undefined, products: VeluProductOption[]): VeluProductOption | undefined {
127
- if (products.length === 0) return undefined;
128
- const firstSeg = (slugInput ?? [])[0] ?? '';
129
- return products.find((p) => p.slug === firstSeg) ?? products[0];
130
- }
131
-
132
- function renderIconsInTree<T>(node: T, iconLibrary: 'fontawesome' | 'lucide' | 'tabler'): T {
133
- if (Array.isArray(node)) return node.map((item) => renderIconsInTree(item, iconLibrary)) as T;
134
- if (isValidElement(node)) return node;
135
- if (typeof node !== 'object' || node === null) return node;
136
-
137
- const out: Record<string, unknown> = {};
138
- const nodeWithIconType = node as { iconType?: unknown };
139
- for (const [key, value] of Object.entries(node)) {
140
- if (key === 'icon' && typeof value === 'string') {
141
- const iconType = typeof nodeWithIconType.iconType === 'string'
142
- ? nodeWithIconType.iconType
143
- : undefined;
144
- out[key] = <VeluIcon name={value} iconType={iconType} library={iconLibrary} fallback={false} />;
145
- continue;
146
- }
147
- if (key === 'url' && typeof value === 'string') {
148
- out[key] = withTrailingSlashUrl(value);
149
- continue;
150
- }
151
- out[key] = renderIconsInTree(value, iconLibrary);
152
- }
153
- return out as T;
154
- }
155
-
156
- function collectFolderUrls(folder: PageTreeFolderNode, out: Set<string> = new Set<string>()): Set<string> {
157
- if (typeof folder.index?.url === 'string' && folder.index.url.length > 0) out.add(folder.index.url);
158
- for (const child of Array.isArray(folder.children) ? folder.children : []) {
159
- const node = child as PageTreePageNode & PageTreeFolderNode;
160
- if (node?.type === 'page' && !node.external && typeof node.url === 'string' && node.url.length > 0) {
161
- out.add(node.url);
162
- continue;
163
- }
164
- if (node?.type === 'folder') collectFolderUrls(node, out);
165
- }
166
- return out;
167
- }
168
-
169
- function buildNavbarTabs(tree: unknown): Array<{
170
- url: string;
171
- title: ReactNode;
172
- icon?: ReactNode;
173
- description?: ReactNode;
174
- urls: Set<string>;
175
- }> | undefined {
176
- const rootChildren = Array.isArray((tree as { children?: unknown[] })?.children)
177
- ? (tree as { children: unknown[] }).children
178
- : [];
179
-
180
- const rootFolders = rootChildren.filter((child) => {
181
- const node = child as PageTreeFolderNode;
182
- return node?.type === 'folder' && node.root === true;
183
- }) as PageTreeFolderNode[];
184
-
185
- // Two shapes are supported:
186
- // 1) Multiple root folders => each root folder is a top-level tab.
187
- // 2) Single root folder containing tab folders as children.
188
- const tabFolders: PageTreeFolderNode[] = rootFolders.length > 1
189
- ? rootFolders
190
- : (rootFolders.length === 1 && Array.isArray(rootFolders[0]?.children)
191
- ? rootFolders[0].children.filter((child) => (child as PageTreeFolderNode)?.type === 'folder') as PageTreeFolderNode[]
192
- : rootChildren.filter((child) => (child as PageTreeFolderNode)?.type === 'folder') as PageTreeFolderNode[]);
193
-
194
- const tabs = tabFolders
195
- .map((folder) => {
196
- const urls = collectFolderUrls(folder);
197
- const firstUrl = urls.values().next().value as string | undefined;
198
- if (!firstUrl) return null;
199
- return {
200
- url: firstUrl,
201
- title: folder.name ?? '',
202
- icon: folder.icon,
203
- description: folder.description,
204
- urls,
205
- };
206
- })
207
- .filter((tab): tab is NonNullable<typeof tab> => tab !== null);
208
-
209
- return tabs.length > 0 ? tabs : undefined;
210
- }
211
-
212
- function resolveTabContext(slugInput: string[] | undefined): { containerSlug?: string; tabSlug?: string } {
213
- const languages = getLanguages();
214
- const slug = slugInput ?? [];
215
- const contentSlug = languages.includes(slug[0] ?? '') ? slug.slice(1) : slug;
216
- if (contentSlug.length === 0) return {};
217
- if (contentSlug.length > 1) {
218
- return { containerSlug: contentSlug[0], tabSlug: contentSlug[1] };
219
- }
220
- return { tabSlug: contentSlug[0] };
221
- }
222
-
223
- function normalizePath(value: string): string {
224
- return value.replace(/^\/+|\/+$/g, '').toLowerCase();
225
- }
226
-
227
- function normalizeSidebarTabUrl(url: string): string {
228
- const trimmed = url.trim();
229
- if (trimmed.length <= 1) return trimmed;
230
- return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
231
- }
232
-
233
- function basename(value: string): string {
234
- const normalized = normalizePath(value);
235
- const parts = normalized.split('/').filter(Boolean);
236
- return parts[parts.length - 1] ?? normalized;
237
- }
238
-
239
- function collectPageUrls(tree: unknown, out: Set<string> = new Set<string>()): Set<string> {
240
- if (!tree || typeof tree !== 'object') return out;
241
-
242
- const node = tree as {
243
- type?: string;
244
- url?: unknown;
245
- external?: unknown;
246
- index?: { url?: unknown };
247
- children?: unknown[];
248
- };
249
-
250
- if (node.type === 'page' && node.external !== true && typeof node.url === 'string' && node.url.length > 0) {
251
- out.add(normalizeSidebarTabUrl(node.url));
252
- }
253
-
254
- if (node.type === 'folder' && typeof node.index?.url === 'string' && node.index.url.length > 0) {
255
- out.add(normalizeSidebarTabUrl(node.index.url));
256
- }
257
-
258
- if (Array.isArray(node.children)) {
259
- for (const child of node.children) collectPageUrls(child, out);
260
- }
261
-
262
- return out;
263
- }
264
-
265
- function doesUrlBelongToTab(url: string, tabSlug: string): boolean {
266
- const normalizedUrl = normalizePath(url);
267
- const normalizedTab = normalizePath(tabSlug);
268
- if (!normalizedUrl || !normalizedTab) return false;
269
- return normalizedUrl === normalizedTab
270
- || normalizedUrl.startsWith(`${normalizedTab}/`)
271
- || normalizedUrl.includes(`/${normalizedTab}/`)
272
- || normalizedUrl.endsWith(`/${normalizedTab}`);
273
- }
274
-
275
- function resolveMenuTargetUrl(menuPages: string[], tabUrls: Set<string>): string | undefined {
276
- const urls = Array.from(tabUrls);
277
- if (urls.length === 0) return undefined;
278
-
279
- for (const page of menuPages) {
280
- const normalizedPage = normalizePath(page);
281
- if (!normalizedPage) continue;
282
-
283
- const direct = urls.find((url) => {
284
- const normalizedUrl = normalizePath(url);
285
- return normalizedUrl === normalizedPage || normalizedUrl.endsWith(`/${normalizedPage}`);
286
- });
287
- if (direct) return direct;
288
-
289
- const pageBase = basename(normalizedPage);
290
- const basenameMatches = urls.filter((url) => basename(url) === pageBase);
291
- if (basenameMatches.length === 1) return basenameMatches[0];
292
- }
293
-
294
- return undefined;
295
- }
296
-
297
- function resolveMenuLinksForTab(
298
- tabUrls: Set<string>,
299
- candidates: ReturnType<typeof getTabMenuDefinitions>,
300
- ): Array<{ text: string; url: string }> {
301
- let best: Array<{ text: string; url: string }> = [];
302
-
303
- for (const candidate of candidates) {
304
- const resolved = candidate.items
305
- .map((item) => {
306
- const target = resolveMenuTargetUrl(item.pages, tabUrls);
307
- if (!target) return null;
308
- return { text: item.item, url: target };
309
- })
310
- .filter((entry): entry is { text: string; url: string } => entry !== null);
311
-
312
- if (resolved.length > best.length) best = resolved;
313
- }
314
-
315
- return best;
316
- }
317
-
318
- function withPrefixedPath(url: string, prefix?: string): string {
319
- const normalizedPrefix = (prefix ?? '').trim().replace(/^\/+|\/+$/g, '');
320
- if (!normalizedPrefix) return url;
321
- if (/^(https?:|mailto:|tel:|#)/i.test(url)) return url;
322
-
323
- const hashIndex = url.indexOf('#');
324
- const queryIndex = url.indexOf('?');
325
- const endIndex = [hashIndex, queryIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0] ?? url.length;
326
- const path = url.slice(0, endIndex);
327
- const suffix = url.slice(endIndex);
328
- if (!path.startsWith('/')) return url;
329
-
330
- const prefixed = path === '/'
331
- ? `/${normalizedPrefix}`
332
- : path.startsWith(`/${normalizedPrefix}/`) || path === `/${normalizedPrefix}`
333
- ? path
334
- : `/${normalizedPrefix}${path}`;
335
-
336
- return `${prefixed}${suffix}`;
337
- }
338
-
339
- function resolveRequestPathPrefix(
340
- slugInput: string[] | undefined,
341
- tabs: Array<{ url: string }>,
342
- ): string | undefined {
343
- const slug = (slugInput ?? []).map((segment) => segment.trim().toLowerCase()).filter(Boolean);
344
- if (slug.length < 2) return undefined;
345
-
346
- const tabRoots = new Set(
347
- tabs
348
- .map((tab) => normalizePath(tab.url).split('/').filter(Boolean)[0] ?? '')
349
- .map((segment) => segment.toLowerCase())
350
- .filter((segment) => segment.length > 0),
351
- );
352
-
353
- const first = slug[0] ?? '';
354
- const second = slug[1] ?? '';
355
- if (!first || !second) return undefined;
356
- if (tabRoots.has(first)) return undefined;
357
- if (tabRoots.has(second)) return first;
358
- return undefined;
359
- }
360
-
361
- function scopeTreeToTab<T extends { children?: unknown[] }>(
362
- tree: T,
363
- tabSlug?: string,
364
- containerSlug?: string,
365
- ): T {
366
- const normalizedTab = (tabSlug ?? '').trim().toLowerCase();
367
- if (!normalizedTab) return tree;
368
-
369
- const topChildren = Array.isArray(tree.children) ? tree.children : [];
370
- const rootFolders = topChildren.filter((child) => {
371
- const node = child as PageTreeFolderNode;
372
- return node?.type === 'folder' && node.root === true;
373
- }) as PageTreeFolderNode[];
374
-
375
- // When docs have multiple top-level root folders (tabs), avoid rendering
376
- // the sidebar root switcher. Show only the active tab's children.
377
- if (rootFolders.length > 1) {
378
- const activeTopTab = (containerSlug ?? tabSlug ?? '').trim().toLowerCase();
379
- if (!activeTopTab) return tree;
380
-
381
- const matchedRoot = rootFolders.find((folder) => {
382
- const urls = collectFolderUrls(folder);
383
- for (const url of urls) {
384
- const segments = url.split('/').filter(Boolean).map((segment) => segment.toLowerCase());
385
- if ((segments[0] ?? '') === activeTopTab) return true;
386
- }
387
- return false;
388
- });
389
-
390
- if (!matchedRoot || !Array.isArray(matchedRoot.children)) return tree;
391
- return { ...tree, children: matchedRoot.children } as T;
392
- }
393
-
394
- const rootFolder = topChildren.find((child) => {
395
- const node = child as PageTreeFolderNode;
396
- return node?.type === 'folder' && node.root === true;
397
- }) as PageTreeFolderNode | undefined;
398
-
399
- if (!rootFolder || !Array.isArray(rootFolder.children)) return tree;
400
-
401
- const normalizedContainer = (containerSlug ?? '').trim().toLowerCase();
402
- const matchingChildren = rootFolder.children.filter((child): child is PageTreeFolderNode => {
403
- const folder = child as PageTreeFolderNode;
404
- if (folder?.type !== 'folder') return false;
405
-
406
- const urls = collectFolderUrls(folder);
407
- for (const url of urls) {
408
- const segments = url.split('/').filter(Boolean).map((segment) => segment.toLowerCase());
409
- if (segments.length === 0) continue;
410
-
411
- const tabCandidate = normalizedContainer && segments[0] === normalizedContainer
412
- ? segments[1]
413
- : segments[0];
414
- if (tabCandidate === normalizedTab) return true;
415
- }
416
- return false;
417
- });
418
-
419
- if (matchingChildren.length === 0) return tree;
420
-
421
- const firstMatch = matchingChildren[0];
422
- const flattenedChildren = matchingChildren.length === 1 && Array.isArray(firstMatch?.children) && firstMatch.children.length > 0
423
- ? firstMatch.children
424
- : matchingChildren;
425
-
426
- const scopedRoot = { ...rootFolder, children: flattenedChildren };
427
- const scopedChildren = topChildren.map((child) => (child === rootFolder ? scopedRoot : child));
428
- return { ...tree, children: scopedChildren } as T;
429
- }
430
-
431
- function flattenSingleRootFolder<T extends { children?: unknown[] }>(tree: T): T {
432
- const topChildren = Array.isArray(tree.children) ? tree.children : [];
433
- if (topChildren.length === 0) return tree;
434
-
435
- const rootFolders = topChildren.filter((child) => {
436
- const node = child as PageTreeFolderNode;
437
- return node?.type === 'folder' && node.root === true;
438
- }) as PageTreeFolderNode[];
439
-
440
- if (rootFolders.length !== 1) return tree;
441
- const rootFolder = rootFolders[0];
442
- const rootChildren = Array.isArray(rootFolder.children) ? rootFolder.children : [];
443
- if (rootChildren.length === 0) return tree;
444
-
445
- const nonRootChildren = topChildren.filter((child) => child !== rootFolder);
446
- return { ...tree, children: [...rootChildren, ...nonRootChildren] } as T;
447
- }
448
-
449
25
  export default async function SlugLayout({ children, params }: SlugLayoutProps) {
450
26
  const resolvedParams = await params;
451
- const locale = resolveLocale(resolvedParams.slug);
27
+ const languages = getLanguages();
28
+ const locale = resolveLocale(resolvedParams.slug, languages);
452
29
  const localePageTree = source.getPageTree(locale);
453
- const versions = getVersionOptions();
454
- const products = getProductOptions();
455
- const dropdowns = getDropdownOptions();
456
- const iconLibrary = getIconLibrary();
457
- const currentVersion = resolveCurrentVersion(resolvedParams.slug, versions);
458
- const currentProduct = resolveCurrentProduct(resolvedParams.slug, products);
459
- const { containerSlug, tabSlug: currentTabSlug } = resolveTabContext(resolvedParams.slug);
460
- const activePrefix = currentVersion?.slug ?? currentProduct?.slug;
461
- const containerScopedTree = filterTreeBySlugPrefix(localePageTree, activePrefix);
462
- const rawTree = scopeTreeToTab(containerScopedTree, currentTabSlug, containerSlug);
463
- const activeTree = dropdowns.length > 0 ? flattenSingleRootFolder(rawTree) : rawTree;
464
- const navbarTabs = buildNavbarTabs(localePageTree) ?? [];
465
- const allPageUrls = collectPageUrls(localePageTree);
466
- const requestPathPrefix = resolveRequestPathPrefix(resolvedParams.slug, navbarTabs);
467
- const tabMenuDefinitions = getTabMenuDefinitions();
468
- const tree = renderIconsInTree(activeTree, iconLibrary);
469
- const base = baseOptions();
470
- const dropdownTabs = dropdowns.map((dropdown) => {
471
- const defaultUrl = withTrailingSlashUrl(dropdown.defaultPath);
472
- const matchingUrls = Array.from(allPageUrls).filter((url) => (
473
- doesUrlBelongToTab(url, dropdown.slug)
474
- || dropdown.tabSlugs.some((tabSlug) => doesUrlBelongToTab(url, tabSlug))
475
- ));
476
- const urls = new Set<string>(matchingUrls);
477
- urls.add(normalizeSidebarTabUrl(defaultUrl));
478
-
479
- return {
480
- url: defaultUrl,
481
- urls,
482
- title: dropdown.dropdown,
483
- description: dropdown.description,
484
- icon: dropdown.icon ? (
485
- <VeluIcon
486
- name={dropdown.icon}
487
- iconType={dropdown.iconType}
488
- library={iconLibrary}
489
- fallback={false}
490
- />
491
- ) : undefined,
492
- };
493
- });
494
- const headerTabLinks: LinkItemType[] = navbarTabs
495
- .map((tab): LinkItemType | null => {
496
- const tabText = typeof tab.title === 'string' ? tab.title : '';
497
- if (tabText.length === 0) return null;
498
-
499
- const menuCandidates = tabMenuDefinitions.filter(
500
- (definition) => definition.tab.trim().toLowerCase() === tabText.trim().toLowerCase(),
501
- );
502
- const menuLinks = resolveMenuLinksForTab(tab.urls, menuCandidates);
503
-
504
- if (menuLinks.length > 0) {
505
- return {
506
- type: 'menu',
507
- text: tabText,
508
- url: withTrailingSlashUrl(withPrefixedPath(tab.url, requestPathPrefix)),
509
- active: 'nested-url',
510
- secondary: false,
511
- items: menuLinks.map((item) => ({
512
- text: item.text,
513
- url: withTrailingSlashUrl(withPrefixedPath(item.url, requestPathPrefix)),
514
- active: 'nested-url',
515
- })),
516
- };
517
- }
518
-
519
- return {
520
- type: 'custom',
521
- secondary: false,
522
- children: (
523
- <HeaderTabLink
524
- text={tabText}
525
- href={withTrailingSlashUrl(withPrefixedPath(tab.url, requestPathPrefix))}
526
- urls={Array.from(tab.urls).map((url) => withTrailingSlashUrl(withPrefixedPath(url, requestPathPrefix)))}
527
- />
528
- ),
529
- };
530
- })
531
- .filter((link): link is LinkItemType => link !== null);
532
30
 
533
- return (
534
- <DocsLayout
535
- tree={tree}
536
- sidebar={{
537
- tabs: dropdownTabs.length > 0 ? dropdownTabs : undefined,
538
- collapsible: true,
539
- banner: products.length > 1
540
- ? (
541
- <div className="velu-sidebar-banner">
542
- <ProductSwitcher products={products} iconLibrary={iconLibrary} />
543
- </div>
544
- )
545
- : undefined,
546
- footer: ({ className, children, ...props }: any) => (
547
- <div
548
- className={['velu-sidebar-footer-shell', className].filter(Boolean).join(' ')}
549
- {...props}
550
- >
551
- {children ? <div className="velu-sidebar-footer-icons">{children}</div> : null}
552
- <SidebarLinks />
553
- </div>
554
- ),
555
- }}
556
- {...base}
557
- links={headerTabLinks.length > 0 ? headerTabLinks : base.links}
558
- themeSwitch={{ enabled: false }}
559
- >
560
- {children}
561
- </DocsLayout>
31
+ return renderDocsLayout(
32
+ {
33
+ slug: resolvedParams.slug,
34
+ tree: localePageTree,
35
+ languages,
36
+ versions: getVersionOptions(),
37
+ products: getProductOptions(),
38
+ dropdowns: getDropdownOptions(),
39
+ iconLibrary: getIconLibrary(),
40
+ tabMenuDefinitions: getTabMenuDefinitions(),
41
+ base: baseOptions(),
42
+ globalAnchors: getGlobalAnchors(),
43
+ appearance: getAppearance(),
44
+ },
45
+ children,
562
46
  );
563
47
  }
564
48
 
@@ -0,0 +1,96 @@
1
+ import { createElement, type ReactNode } from 'react';
2
+ import { loadSessionConfigSource } from '@/lib/preview-config';
3
+ import {
4
+ getAppearance,
5
+ getLanguages,
6
+ getVersionOptions,
7
+ getProductOptions,
8
+ getDropdownOptions,
9
+ getIconLibrary,
10
+ getTabMenuDefinitions,
11
+ getGlobalAnchors,
12
+ getSiteName,
13
+ getSiteLogoAsset,
14
+ } from '@/lib/velu';
15
+ import { baseOptions } from '@/lib/layout.shared';
16
+ import { getSessionPageTree } from '@/lib/source';
17
+ import { renderDocsLayout } from '@/lib/docs-layout';
18
+
19
+ interface LayoutProps {
20
+ children: ReactNode;
21
+ params: Promise<{ sessionId: string; slug: string[] }>;
22
+ }
23
+
24
+ function resolveAssetUrl(sessionId: string, path: string | undefined): string | undefined {
25
+ if (!path) return undefined;
26
+ if (path.startsWith('http://') || path.startsWith('https://')) return path;
27
+ const clean = path.startsWith('/') ? path.slice(1) : path;
28
+ return `/api/sessions/${sessionId}/assets/${clean}`;
29
+ }
30
+
31
+ export default async function SessionDocsLayout({ children, params }: LayoutProps) {
32
+ const { sessionId, slug } = await params;
33
+ const configSource = loadSessionConfigSource(sessionId);
34
+
35
+ if (!configSource) return <>{children}</>;
36
+
37
+ const src = configSource;
38
+ const logo = getSiteLogoAsset(src);
39
+ const siteName = getSiteName(src);
40
+ const resolvedLogoLight = resolveAssetUrl(sessionId, logo.light ?? logo.dark);
41
+ const resolvedLogoDark = resolveAssetUrl(sessionId, logo.dark ?? logo.light);
42
+ const logoHref = typeof logo.href === 'string' && logo.href.trim().length > 0 ? logo.href.trim() : `/${sessionId}`;
43
+
44
+ const navTitle =
45
+ resolvedLogoLight || resolvedLogoDark
46
+ ? createElement(
47
+ 'span',
48
+ { className: 'velu-nav-brand' },
49
+ resolvedLogoLight
50
+ ? createElement('img', {
51
+ src: resolvedLogoLight,
52
+ alt: siteName,
53
+ className: 'velu-nav-logo velu-nav-logo-light',
54
+ })
55
+ : null,
56
+ resolvedLogoDark
57
+ ? createElement('img', {
58
+ src: resolvedLogoDark,
59
+ alt: siteName,
60
+ className: 'velu-nav-logo velu-nav-logo-dark',
61
+ })
62
+ : null,
63
+ )
64
+ : siteName;
65
+
66
+ const base = baseOptions(src);
67
+ const sessionBase = {
68
+ ...base,
69
+ nav: {
70
+ ...base.nav,
71
+ title: navTitle,
72
+ url: logoHref,
73
+ },
74
+ };
75
+
76
+ const languages = getLanguages(src);
77
+ const tree = getSessionPageTree(sessionId);
78
+
79
+ return renderDocsLayout(
80
+ {
81
+ slug,
82
+ tree,
83
+ languages,
84
+ versions: getVersionOptions(src),
85
+ products: getProductOptions(src),
86
+ dropdowns: getDropdownOptions(src),
87
+ iconLibrary: getIconLibrary(src),
88
+ tabMenuDefinitions: getTabMenuDefinitions(src),
89
+ base: sessionBase,
90
+ globalAnchors: getGlobalAnchors(src),
91
+ appearance: getAppearance(src),
92
+ urlPrefix: `/${sessionId}`,
93
+ },
94
+ children,
95
+ );
96
+ }