@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.
- package/package.json +1 -1
- package/src/build.ts +13 -0
- package/src/cli.ts +51 -9
- 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/engine/app/_preview/api/sessions/[sessionId]/assets/[...path]/route.ts +51 -0
- 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} +242 -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 -23
- package/src/preview-engine/app/global.css +0 -3
- package/src/preview-engine/lib/session-config.ts +0 -86
- 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/app → engine/app/_preview}/page.tsx +0 -0
- /package/src/{preview-engine/lib/auth.ts → engine/lib/preview-auth.ts} +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
11
|
+
getGlobalAnchors,
|
|
12
|
+
getAppearance,
|
|
15
13
|
} from '@/lib/velu';
|
|
16
|
-
import {
|
|
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
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
}
|