@aravindc26/velu 0.10.0 → 0.11.0
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/schema/velu.schema.json +714 -16
- package/src/build.ts +207 -43
- package/src/cli.ts +65 -2
- package/src/engine/_server.mjs +127 -18
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +87 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +83 -6
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/global.css +327 -0
- package/src/engine/app/layout.tsx +3 -7
- package/src/engine/app/search.css +20 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/product-switcher.tsx +78 -0
- package/src/engine/components/providers.tsx +26 -0
- package/src/engine/components/search.tsx +66 -3
- package/src/engine/components/sidebar-links.tsx +51 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/lib/layout.shared.ts +28 -6
- package/src/engine/lib/navigation-normalize.mjs +456 -0
- package/src/engine/lib/navigation-normalize.ts +488 -0
- package/src/engine/lib/source.ts +14 -0
- package/src/engine/lib/velu.ts +267 -3
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/src/lib/velu.ts +86 -13
- package/src/navigation-normalize.ts +488 -0
- package/src/validate.ts +116 -18
package/src/engine/lib/velu.ts
CHANGED
|
@@ -1,15 +1,98 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { normalizeConfigNavigation } from './navigation-normalize';
|
|
3
4
|
|
|
4
5
|
interface VeluTab {
|
|
5
6
|
tab: string;
|
|
7
|
+
slug?: string;
|
|
6
8
|
href?: string;
|
|
9
|
+
pages?: Array<string | VeluSeparator | VeluLink>;
|
|
10
|
+
groups?: VeluGroup[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface VeluSeparator {
|
|
14
|
+
separator: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface VeluLink {
|
|
18
|
+
href: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface VeluGroup {
|
|
23
|
+
group: string;
|
|
24
|
+
slug?: string;
|
|
25
|
+
pages: Array<string | VeluGroup | VeluSeparator | VeluLink>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface VeluAnchor {
|
|
29
|
+
anchor: string;
|
|
30
|
+
href?: string;
|
|
31
|
+
icon?: string;
|
|
32
|
+
color?: {
|
|
33
|
+
light: string;
|
|
34
|
+
dark: string;
|
|
35
|
+
};
|
|
36
|
+
tabs?: VeluTab[];
|
|
37
|
+
hidden?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface VeluGlobalTab {
|
|
41
|
+
tab: string;
|
|
42
|
+
href: string;
|
|
43
|
+
icon?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface VeluLanguageNav {
|
|
47
|
+
language: string;
|
|
48
|
+
tabs: VeluTab[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface VeluProductNav {
|
|
52
|
+
product: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
icon?: string;
|
|
55
|
+
hidden?: boolean;
|
|
56
|
+
href?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface VeluVersionNav {
|
|
60
|
+
version: string;
|
|
61
|
+
default?: boolean;
|
|
62
|
+
hidden?: boolean;
|
|
63
|
+
href?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface VeluProductOption {
|
|
67
|
+
product: string;
|
|
68
|
+
slug: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
icon?: string;
|
|
71
|
+
tabSlugs: string[];
|
|
72
|
+
defaultPath: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface VeluVersionOption {
|
|
76
|
+
version: string;
|
|
77
|
+
slug: string;
|
|
78
|
+
isDefault: boolean;
|
|
79
|
+
tabSlugs: string[];
|
|
80
|
+
defaultPath: string;
|
|
7
81
|
}
|
|
8
82
|
|
|
9
83
|
interface VeluConfig {
|
|
10
84
|
appearance?: 'system' | 'light' | 'dark';
|
|
85
|
+
languages?: string[];
|
|
11
86
|
navigation: {
|
|
12
|
-
tabs
|
|
87
|
+
tabs?: VeluTab[];
|
|
88
|
+
languages?: VeluLanguageNav[];
|
|
89
|
+
products?: VeluProductNav[];
|
|
90
|
+
versions?: VeluVersionNav[];
|
|
91
|
+
anchors?: VeluAnchor[];
|
|
92
|
+
global?: {
|
|
93
|
+
anchors?: VeluAnchor[];
|
|
94
|
+
tabs?: VeluGlobalTab[];
|
|
95
|
+
};
|
|
13
96
|
};
|
|
14
97
|
}
|
|
15
98
|
|
|
@@ -19,20 +102,201 @@ function loadVeluConfig(): VeluConfig {
|
|
|
19
102
|
if (cachedConfig) return cachedConfig;
|
|
20
103
|
const configPath = resolve(process.cwd(), 'velu.json');
|
|
21
104
|
const raw = readFileSync(configPath, 'utf-8');
|
|
22
|
-
cachedConfig = JSON.parse(raw) as VeluConfig;
|
|
105
|
+
cachedConfig = normalizeConfigNavigation(JSON.parse(raw)) as VeluConfig;
|
|
23
106
|
return cachedConfig;
|
|
24
107
|
}
|
|
25
108
|
|
|
109
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
110
|
+
return typeof item === 'object' && item !== null && 'group' in item;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function slugify(input: string, fallback: string): string {
|
|
114
|
+
const slug = input
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.trim()
|
|
117
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
118
|
+
.replace(/^-+|-+$/g, '');
|
|
119
|
+
return slug || fallback;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pageBasename(page: string): string {
|
|
123
|
+
const parts = page.split('/').filter(Boolean);
|
|
124
|
+
return parts[parts.length - 1] ?? page;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findFirstPageInGroup(group: VeluGroup): string | undefined {
|
|
128
|
+
for (const item of group.pages) {
|
|
129
|
+
if (typeof item === 'string') return item;
|
|
130
|
+
if (isGroup(item)) {
|
|
131
|
+
const nested = findFirstPageInGroup(item);
|
|
132
|
+
if (nested) return nested;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function findFirstPageInTab(tab: VeluTab): string | undefined {
|
|
139
|
+
if (tab.pages) {
|
|
140
|
+
for (const item of tab.pages) {
|
|
141
|
+
if (typeof item === 'string') return item;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (tab.groups) {
|
|
145
|
+
for (const group of tab.groups) {
|
|
146
|
+
const nested = findFirstPageInGroup(group);
|
|
147
|
+
if (nested) return nested;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseVersionParts(version: string): number[] {
|
|
154
|
+
const parts = version.match(/\d+/g);
|
|
155
|
+
return parts ? parts.map((n) => Number(n)) : [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function compareVersionParts(a: number[], b: number[]): number {
|
|
159
|
+
const len = Math.max(a.length, b.length);
|
|
160
|
+
for (let i = 0; i < len; i += 1) {
|
|
161
|
+
const av = a[i] ?? 0;
|
|
162
|
+
const bv = b[i] ?? 0;
|
|
163
|
+
if (av !== bv) return av - bv;
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
26
168
|
export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
27
169
|
const config = loadVeluConfig();
|
|
28
170
|
const tabs = config.navigation?.tabs ?? [];
|
|
171
|
+
const globalTabs = config.navigation?.global?.tabs ?? [];
|
|
29
172
|
|
|
30
|
-
|
|
173
|
+
const tabLinks = tabs
|
|
31
174
|
.filter((tab): tab is VeluTab & { href: string } => typeof tab.href === 'string' && tab.href.length > 0)
|
|
32
175
|
.map((tab) => ({
|
|
33
176
|
label: tab.tab,
|
|
34
177
|
href: tab.href,
|
|
35
178
|
}));
|
|
179
|
+
|
|
180
|
+
const globalLinks = globalTabs
|
|
181
|
+
.filter((tab): tab is VeluGlobalTab => typeof tab.href === 'string' && tab.href.length > 0)
|
|
182
|
+
.map((tab) => ({
|
|
183
|
+
label: tab.tab,
|
|
184
|
+
href: tab.href,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
return [...tabLinks, ...globalLinks];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getNavbarAnchors(): VeluAnchor[] {
|
|
191
|
+
const config = loadVeluConfig();
|
|
192
|
+
return (config.navigation.anchors ?? []).filter(
|
|
193
|
+
(a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getGlobalAnchors(): VeluAnchor[] {
|
|
198
|
+
const config = loadVeluConfig();
|
|
199
|
+
return (config.navigation.global?.anchors ?? []).filter(
|
|
200
|
+
(a): a is VeluAnchor & { href: string } => typeof a.href === 'string' && a.href.length > 0 && !a.hidden
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getLanguages(): string[] {
|
|
205
|
+
const config = loadVeluConfig();
|
|
206
|
+
// Prefer navigation.languages codes, fall back to top-level languages
|
|
207
|
+
if (config.navigation.languages && config.navigation.languages.length > 0) {
|
|
208
|
+
return config.navigation.languages.map((l) => l.language);
|
|
209
|
+
}
|
|
210
|
+
return config.languages ?? [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function getProductOptions(): VeluProductOption[] {
|
|
214
|
+
const config = loadVeluConfig();
|
|
215
|
+
const products = (config.navigation.products ?? []).filter((p) => !p.hidden);
|
|
216
|
+
if (products.length === 0) return [];
|
|
217
|
+
|
|
218
|
+
const allTabs = config.navigation.tabs ?? [];
|
|
219
|
+
|
|
220
|
+
return products.map((product, index) => {
|
|
221
|
+
const prefix = slugify(product.product, `product-${index + 1}`);
|
|
222
|
+
const productTabs = allTabs.filter((tab) => {
|
|
223
|
+
const slug = tab.slug ?? '';
|
|
224
|
+
return slug === prefix || slug.startsWith(`${prefix}/`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const tabSlugs = productTabs
|
|
228
|
+
.map((tab) => tab.slug)
|
|
229
|
+
.filter((slug): slug is string => typeof slug === 'string' && slug.length > 0);
|
|
230
|
+
|
|
231
|
+
const firstTab = productTabs[0];
|
|
232
|
+
const firstPage = firstTab ? findFirstPageInTab(firstTab) : undefined;
|
|
233
|
+
const defaultPath = firstTab
|
|
234
|
+
? (firstPage ? `/${firstTab.slug}/${pageBasename(firstPage)}` : `/${firstTab.slug}`)
|
|
235
|
+
: (product.href ?? '/');
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
product: product.product,
|
|
239
|
+
slug: prefix,
|
|
240
|
+
description: product.description,
|
|
241
|
+
icon: product.icon,
|
|
242
|
+
tabSlugs,
|
|
243
|
+
defaultPath,
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function getVersionOptions(): VeluVersionOption[] {
|
|
249
|
+
const config = loadVeluConfig();
|
|
250
|
+
const versions = (config.navigation.versions ?? []).filter((v) => !v.hidden);
|
|
251
|
+
if (versions.length === 0) return [];
|
|
252
|
+
|
|
253
|
+
const allTabs = config.navigation.tabs ?? [];
|
|
254
|
+
|
|
255
|
+
const baseEntries = versions.map((version, index) => {
|
|
256
|
+
const prefix = slugify(version.version, `version-${index + 1}`);
|
|
257
|
+
const versionTabs = allTabs.filter((tab) => {
|
|
258
|
+
const slug = tab.slug ?? '';
|
|
259
|
+
return slug === prefix || slug.startsWith(`${prefix}/`);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const tabSlugs = versionTabs
|
|
263
|
+
.map((tab) => tab.slug)
|
|
264
|
+
.filter((slug): slug is string => typeof slug === 'string' && slug.length > 0);
|
|
265
|
+
|
|
266
|
+
const firstTab = versionTabs[0];
|
|
267
|
+
const firstPage = firstTab ? findFirstPageInTab(firstTab) : undefined;
|
|
268
|
+
const defaultPath = firstTab
|
|
269
|
+
? (firstPage ? `/${firstTab.slug}/${pageBasename(firstPage)}` : `/${firstTab.slug}`)
|
|
270
|
+
: (version.href ?? '/');
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
version: version.version,
|
|
274
|
+
slug: prefix,
|
|
275
|
+
explicitDefault: version.default === true,
|
|
276
|
+
versionParts: parseVersionParts(version.version),
|
|
277
|
+
tabSlugs,
|
|
278
|
+
defaultPath,
|
|
279
|
+
order: index,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const explicitDefault = baseEntries.find((entry) => entry.explicitDefault);
|
|
284
|
+
const latest = explicitDefault
|
|
285
|
+
?? baseEntries
|
|
286
|
+
.slice()
|
|
287
|
+
.sort((a, b) => {
|
|
288
|
+
const cmp = compareVersionParts(b.versionParts, a.versionParts);
|
|
289
|
+
if (cmp !== 0) return cmp;
|
|
290
|
+
return a.order - b.order;
|
|
291
|
+
})[0];
|
|
292
|
+
|
|
293
|
+
return baseEntries.map((entry) => ({
|
|
294
|
+
version: entry.version,
|
|
295
|
+
slug: entry.slug,
|
|
296
|
+
isDefault: entry.slug === latest?.slug,
|
|
297
|
+
tabSlugs: entry.tabSlugs,
|
|
298
|
+
defaultPath: entry.defaultPath,
|
|
299
|
+
}));
|
|
36
300
|
}
|
|
37
301
|
|
|
38
302
|
export function getAppearance(): 'system' | 'light' | 'dark' {
|
|
@@ -7,8 +7,8 @@ const withMDX = createMDX({
|
|
|
7
7
|
|
|
8
8
|
/** @type {import('next').NextConfig} */
|
|
9
9
|
const config = {
|
|
10
|
-
reactStrictMode:
|
|
11
|
-
output: 'export',
|
|
10
|
+
reactStrictMode: false,
|
|
11
|
+
output: process.env.NODE_ENV === 'production' ? 'export' : undefined,
|
|
12
12
|
distDir: 'dist',
|
|
13
13
|
devIndicators: false,
|
|
14
14
|
turbopack: {
|
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { normalizeConfigNavigation } from '../../lib/navigation-normalize';
|
|
3
4
|
|
|
4
5
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
5
6
|
|
|
7
|
+
export interface VeluSeparator {
|
|
8
|
+
separator: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VeluLink {
|
|
12
|
+
href: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface VeluAnchor {
|
|
18
|
+
anchor: string;
|
|
19
|
+
href?: string;
|
|
20
|
+
icon?: string;
|
|
21
|
+
color?: {
|
|
22
|
+
light: string;
|
|
23
|
+
dark: string;
|
|
24
|
+
};
|
|
25
|
+
tabs?: VeluTab[];
|
|
26
|
+
hidden?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VeluGlobalTab {
|
|
30
|
+
tab: string;
|
|
31
|
+
href: string;
|
|
32
|
+
icon?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
6
35
|
export interface VeluGroup {
|
|
7
36
|
group: string;
|
|
8
37
|
slug: string;
|
|
9
38
|
icon?: string;
|
|
10
39
|
tag?: string;
|
|
11
40
|
expanded?: boolean;
|
|
12
|
-
|
|
41
|
+
description?: string;
|
|
42
|
+
hidden?: boolean;
|
|
43
|
+
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
13
44
|
}
|
|
14
45
|
|
|
15
46
|
export interface VeluTab {
|
|
@@ -17,7 +48,7 @@ export interface VeluTab {
|
|
|
17
48
|
slug: string;
|
|
18
49
|
icon?: string;
|
|
19
50
|
href?: string;
|
|
20
|
-
pages?: string[];
|
|
51
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
21
52
|
groups?: VeluGroup[];
|
|
22
53
|
}
|
|
23
54
|
|
|
@@ -28,7 +59,12 @@ export interface VeluConfig {
|
|
|
28
59
|
appearance?: 'system' | 'light' | 'dark';
|
|
29
60
|
styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
|
|
30
61
|
navigation: {
|
|
31
|
-
tabs
|
|
62
|
+
tabs?: VeluTab[];
|
|
63
|
+
anchors?: VeluAnchor[];
|
|
64
|
+
global?: {
|
|
65
|
+
anchors?: VeluAnchor[];
|
|
66
|
+
tabs?: VeluGlobalTab[];
|
|
67
|
+
};
|
|
32
68
|
};
|
|
33
69
|
}
|
|
34
70
|
|
|
@@ -48,7 +84,7 @@ export function loadVeluConfig(): VeluConfig {
|
|
|
48
84
|
if (_cachedConfig) return _cachedConfig;
|
|
49
85
|
const configPath = resolve(process.cwd(), 'velu.json');
|
|
50
86
|
const raw = readFileSync(configPath, 'utf-8');
|
|
51
|
-
_cachedConfig = JSON.parse(raw);
|
|
87
|
+
_cachedConfig = normalizeConfigNavigation(JSON.parse(raw));
|
|
52
88
|
return _cachedConfig!;
|
|
53
89
|
}
|
|
54
90
|
|
|
@@ -64,7 +100,7 @@ function veluGroupToSidebar(group: VeluGroup, tabSlug: string): any {
|
|
|
64
100
|
for (const item of group.pages) {
|
|
65
101
|
if (typeof item === 'string') {
|
|
66
102
|
items.push(tabSlug + '/' + group.slug + '/' + pageBasename(item));
|
|
67
|
-
} else {
|
|
103
|
+
} else if (isGroup(item)) {
|
|
68
104
|
items.push(veluGroupToSidebar(item, tabSlug));
|
|
69
105
|
}
|
|
70
106
|
}
|
|
@@ -74,10 +110,18 @@ function veluGroupToSidebar(group: VeluGroup, tabSlug: string): any {
|
|
|
74
110
|
return result;
|
|
75
111
|
}
|
|
76
112
|
|
|
113
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
114
|
+
return typeof item === 'object' && item !== null && 'group' in item;
|
|
115
|
+
}
|
|
116
|
+
|
|
77
117
|
/** Get the first page dest path for a tab */
|
|
78
118
|
function firstTabPage(tab: VeluTab): string | undefined {
|
|
79
|
-
if (tab.pages
|
|
80
|
-
|
|
119
|
+
if (tab.pages) {
|
|
120
|
+
for (const item of tab.pages) {
|
|
121
|
+
if (typeof item === 'string') {
|
|
122
|
+
return tab.slug + '/' + pageBasename(item);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
81
125
|
}
|
|
82
126
|
if (tab.groups) {
|
|
83
127
|
for (const g of tab.groups) {
|
|
@@ -91,8 +135,10 @@ function firstTabPage(tab: VeluTab): string | undefined {
|
|
|
91
135
|
function firstGroupPage(group: VeluGroup, tabSlug: string): string | undefined {
|
|
92
136
|
for (const item of group.pages) {
|
|
93
137
|
if (typeof item === 'string') return tabSlug + '/' + group.slug + '/' + pageBasename(item);
|
|
94
|
-
|
|
95
|
-
|
|
138
|
+
if (isGroup(item)) {
|
|
139
|
+
const nested = firstGroupPage(item, tabSlug);
|
|
140
|
+
if (nested) return nested;
|
|
141
|
+
}
|
|
96
142
|
}
|
|
97
143
|
return undefined;
|
|
98
144
|
}
|
|
@@ -104,12 +150,14 @@ export function getSidebar(): any[] {
|
|
|
104
150
|
const config = loadVeluConfig();
|
|
105
151
|
const sidebar: any[] = [];
|
|
106
152
|
|
|
107
|
-
for (const tab of config.navigation.tabs) {
|
|
153
|
+
for (const tab of config.navigation.tabs ?? []) {
|
|
108
154
|
if (tab.href) continue;
|
|
109
155
|
const items: any[] = [];
|
|
110
156
|
if (tab.groups) for (const g of tab.groups) items.push(veluGroupToSidebar(g, tab.slug));
|
|
111
157
|
if (tab.pages) {
|
|
112
|
-
for (const p of tab.pages)
|
|
158
|
+
for (const p of tab.pages) {
|
|
159
|
+
if (typeof p === 'string') items.push(tab.slug + '/' + pageBasename(p));
|
|
160
|
+
}
|
|
113
161
|
}
|
|
114
162
|
sidebar.push({ label: tab.tab, items });
|
|
115
163
|
}
|
|
@@ -122,7 +170,7 @@ export function getTabs(): TabMeta[] {
|
|
|
122
170
|
const config = loadVeluConfig();
|
|
123
171
|
const tabs: TabMeta[] = [];
|
|
124
172
|
|
|
125
|
-
for (const tab of config.navigation.tabs) {
|
|
173
|
+
for (const tab of config.navigation.tabs ?? []) {
|
|
126
174
|
if (tab.href) {
|
|
127
175
|
tabs.push({ label: tab.tab, icon: tab.icon, href: tab.href, slugs: [] });
|
|
128
176
|
} else {
|
|
@@ -144,10 +192,35 @@ export function getTabSidebarMap(): Record<string, string[]> {
|
|
|
144
192
|
const config = loadVeluConfig();
|
|
145
193
|
const map: Record<string, string[]> = {};
|
|
146
194
|
|
|
147
|
-
for (const tab of config.navigation.tabs) {
|
|
195
|
+
for (const tab of config.navigation.tabs ?? []) {
|
|
148
196
|
if (tab.href) continue;
|
|
149
197
|
map[tab.slug] = [tab.tab];
|
|
150
198
|
}
|
|
151
199
|
|
|
152
200
|
return map;
|
|
153
201
|
}
|
|
202
|
+
|
|
203
|
+
/** Get all anchors (navigation.anchors + navigation.global.anchors), excluding hidden ones */
|
|
204
|
+
export function getAnchors(): VeluAnchor[] {
|
|
205
|
+
const config = loadVeluConfig();
|
|
206
|
+
const anchors: VeluAnchor[] = [];
|
|
207
|
+
if (config.navigation.anchors) {
|
|
208
|
+
anchors.push(...config.navigation.anchors.filter((a) => typeof a.href === 'string' && a.href.length > 0 && !a.hidden));
|
|
209
|
+
}
|
|
210
|
+
if (config.navigation.global?.anchors) {
|
|
211
|
+
anchors.push(...config.navigation.global.anchors.filter((a) => typeof a.href === 'string' && a.href.length > 0 && !a.hidden));
|
|
212
|
+
}
|
|
213
|
+
return anchors;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Get external tab links for the navbar */
|
|
217
|
+
export function getExternalTabs(): { label: string; href: string; icon?: string }[] {
|
|
218
|
+
const config = loadVeluConfig();
|
|
219
|
+
const tabLinks = (config.navigation.tabs ?? [])
|
|
220
|
+
.filter((tab) => !!tab.href)
|
|
221
|
+
.map((tab) => ({ label: tab.tab, href: tab.href!, icon: tab.icon }));
|
|
222
|
+
const globalLinks = (config.navigation.global?.tabs ?? [])
|
|
223
|
+
.filter((tab) => !!tab.href)
|
|
224
|
+
.map((tab) => ({ label: tab.tab, href: tab.href, icon: tab.icon }));
|
|
225
|
+
return [...tabLinks, ...globalLinks];
|
|
226
|
+
}
|