@aravindc26/velu 0.9.1 → 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 +717 -19
- package/src/build.ts +210 -46
- package/src/cli.ts +66 -3
- package/src/engine/_server.mjs +129 -20
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +87 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +86 -6
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/global.css +346 -1
- 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/themes.ts +66 -257
- package/src/validate.ts +116 -18
package/src/themes.ts
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
interface ColorSet {
|
|
4
|
-
accentLow: string;
|
|
5
|
-
accent: string;
|
|
6
|
-
accentHigh: string;
|
|
7
|
-
white: string;
|
|
8
|
-
gray1: string;
|
|
9
|
-
gray2: string;
|
|
10
|
-
gray3: string;
|
|
11
|
-
gray4: string;
|
|
12
|
-
gray5: string;
|
|
13
|
-
gray6: string;
|
|
14
|
-
gray7: string;
|
|
15
|
-
black: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ThemePreset {
|
|
19
|
-
dark: ColorSet;
|
|
20
|
-
light: ColorSet;
|
|
21
|
-
font?: string;
|
|
22
|
-
fontMono?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
3
|
interface VeluColors {
|
|
26
4
|
primary?: string;
|
|
27
5
|
light?: string;
|
|
@@ -41,6 +19,42 @@ interface ThemeConfig {
|
|
|
41
19
|
styling?: VeluStyling;
|
|
42
20
|
}
|
|
43
21
|
|
|
22
|
+
const FUMADOCS_THEMES = [
|
|
23
|
+
"neutral",
|
|
24
|
+
"black",
|
|
25
|
+
"vitepress",
|
|
26
|
+
"dusk",
|
|
27
|
+
"catppuccin",
|
|
28
|
+
"ocean",
|
|
29
|
+
"emerald",
|
|
30
|
+
"ruby",
|
|
31
|
+
"purple",
|
|
32
|
+
"solar",
|
|
33
|
+
"aspen",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
type FumadocsTheme = (typeof FUMADOCS_THEMES)[number];
|
|
37
|
+
|
|
38
|
+
const LEGACY_THEME_ALIASES: Record<string, FumadocsTheme> = {
|
|
39
|
+
violet: "purple",
|
|
40
|
+
maple: "catppuccin",
|
|
41
|
+
palm: "ocean",
|
|
42
|
+
willow: "neutral",
|
|
43
|
+
linden: "emerald",
|
|
44
|
+
almond: "solar",
|
|
45
|
+
aspen: "aspen",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function resolveThemeName(theme?: string): FumadocsTheme {
|
|
49
|
+
if (!theme) return "neutral";
|
|
50
|
+
|
|
51
|
+
if (FUMADOCS_THEMES.includes(theme as FumadocsTheme)) {
|
|
52
|
+
return theme as FumadocsTheme;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return LEGACY_THEME_ALIASES[theme] || "neutral";
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
// ── Color utilities ──────────────────────────────────────────────────────────
|
|
45
59
|
|
|
46
60
|
function hexToRgb(hex: string): [number, number, number] {
|
|
@@ -72,7 +86,10 @@ function mixColors(hex1: string, hex2: string, weight: number): string {
|
|
|
72
86
|
);
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
function deriveAccentPalette(primary: string): {
|
|
89
|
+
function deriveAccentPalette(primary: string): {
|
|
90
|
+
dark: { accentLow: string; accent: string; accentHigh: string };
|
|
91
|
+
light: { accentLow: string; accent: string; accentHigh: string };
|
|
92
|
+
} {
|
|
76
93
|
return {
|
|
77
94
|
dark: {
|
|
78
95
|
accentLow: mixColors(primary, "#000000", 0.3),
|
|
@@ -87,193 +104,6 @@ function deriveAccentPalette(primary: string): { dark: Pick<ColorSet, "accentLow
|
|
|
87
104
|
};
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
// ── Gray palettes ────────────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
const GRAY_SLATE = {
|
|
93
|
-
dark: {
|
|
94
|
-
white: "#ffffff",
|
|
95
|
-
gray1: "#eceef2",
|
|
96
|
-
gray2: "#c0c2c7",
|
|
97
|
-
gray3: "#888b96",
|
|
98
|
-
gray4: "#545861",
|
|
99
|
-
gray5: "#353841",
|
|
100
|
-
gray6: "#24272f",
|
|
101
|
-
gray7: "#17181c",
|
|
102
|
-
black: "#13141a",
|
|
103
|
-
},
|
|
104
|
-
light: {
|
|
105
|
-
white: "#13141a",
|
|
106
|
-
gray1: "#17181c",
|
|
107
|
-
gray2: "#24272f",
|
|
108
|
-
gray3: "#545861",
|
|
109
|
-
gray4: "#888b96",
|
|
110
|
-
gray5: "#c0c2c7",
|
|
111
|
-
gray6: "#eceef2",
|
|
112
|
-
gray7: "#f5f6f8",
|
|
113
|
-
black: "#ffffff",
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const GRAY_ZINC = {
|
|
118
|
-
dark: {
|
|
119
|
-
white: "#ffffff",
|
|
120
|
-
gray1: "#ececef",
|
|
121
|
-
gray2: "#bfc0c4",
|
|
122
|
-
gray3: "#878890",
|
|
123
|
-
gray4: "#53545c",
|
|
124
|
-
gray5: "#34353b",
|
|
125
|
-
gray6: "#23242a",
|
|
126
|
-
gray7: "#17171a",
|
|
127
|
-
black: "#121214",
|
|
128
|
-
},
|
|
129
|
-
light: {
|
|
130
|
-
white: "#121214",
|
|
131
|
-
gray1: "#17171a",
|
|
132
|
-
gray2: "#23242a",
|
|
133
|
-
gray3: "#53545c",
|
|
134
|
-
gray4: "#878890",
|
|
135
|
-
gray5: "#bfc0c4",
|
|
136
|
-
gray6: "#ececef",
|
|
137
|
-
gray7: "#f5f5f7",
|
|
138
|
-
black: "#ffffff",
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const GRAY_STONE = {
|
|
143
|
-
dark: {
|
|
144
|
-
white: "#ffffff",
|
|
145
|
-
gray1: "#eeeceb",
|
|
146
|
-
gray2: "#c3bfbb",
|
|
147
|
-
gray3: "#8c8680",
|
|
148
|
-
gray4: "#585550",
|
|
149
|
-
gray5: "#383532",
|
|
150
|
-
gray6: "#272421",
|
|
151
|
-
gray7: "#1a1816",
|
|
152
|
-
black: "#141210",
|
|
153
|
-
},
|
|
154
|
-
light: {
|
|
155
|
-
white: "#141210",
|
|
156
|
-
gray1: "#1a1816",
|
|
157
|
-
gray2: "#272421",
|
|
158
|
-
gray3: "#585550",
|
|
159
|
-
gray4: "#8c8680",
|
|
160
|
-
gray5: "#c3bfbb",
|
|
161
|
-
gray6: "#eeeceb",
|
|
162
|
-
gray7: "#f7f6f5",
|
|
163
|
-
black: "#ffffff",
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// ── Theme presets ────────────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
const THEMES: Record<string, ThemePreset> = {
|
|
170
|
-
violet: {
|
|
171
|
-
dark: {
|
|
172
|
-
accentLow: "#1e1b4b",
|
|
173
|
-
accent: "#818cf8",
|
|
174
|
-
accentHigh: "#e0e7ff",
|
|
175
|
-
...GRAY_SLATE.dark,
|
|
176
|
-
},
|
|
177
|
-
light: {
|
|
178
|
-
accentLow: "#e0e7ff",
|
|
179
|
-
accent: "#4f46e5",
|
|
180
|
-
accentHigh: "#1e1b4b",
|
|
181
|
-
...GRAY_SLATE.light,
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
maple: {
|
|
186
|
-
dark: {
|
|
187
|
-
accentLow: "#2e1065",
|
|
188
|
-
accent: "#a78bfa",
|
|
189
|
-
accentHigh: "#ede9fe",
|
|
190
|
-
...GRAY_ZINC.dark,
|
|
191
|
-
},
|
|
192
|
-
light: {
|
|
193
|
-
accentLow: "#ede9fe",
|
|
194
|
-
accent: "#7c3aed",
|
|
195
|
-
accentHigh: "#2e1065",
|
|
196
|
-
...GRAY_ZINC.light,
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
palm: {
|
|
201
|
-
dark: {
|
|
202
|
-
accentLow: "#0c2d44",
|
|
203
|
-
accent: "#38bdf8",
|
|
204
|
-
accentHigh: "#e0f2fe",
|
|
205
|
-
...GRAY_SLATE.dark,
|
|
206
|
-
},
|
|
207
|
-
light: {
|
|
208
|
-
accentLow: "#e0f2fe",
|
|
209
|
-
accent: "#0369a1",
|
|
210
|
-
accentHigh: "#0c2d44",
|
|
211
|
-
...GRAY_SLATE.light,
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
willow: {
|
|
216
|
-
dark: {
|
|
217
|
-
accentLow: "#292524",
|
|
218
|
-
accent: "#a8a29e",
|
|
219
|
-
accentHigh: "#fafaf9",
|
|
220
|
-
...GRAY_STONE.dark,
|
|
221
|
-
},
|
|
222
|
-
light: {
|
|
223
|
-
accentLow: "#f5f5f4",
|
|
224
|
-
accent: "#57534e",
|
|
225
|
-
accentHigh: "#1c1917",
|
|
226
|
-
...GRAY_STONE.light,
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
linden: {
|
|
231
|
-
dark: {
|
|
232
|
-
accentLow: "#052e16",
|
|
233
|
-
accent: "#4ade80",
|
|
234
|
-
accentHigh: "#dcfce7",
|
|
235
|
-
...GRAY_ZINC.dark,
|
|
236
|
-
},
|
|
237
|
-
light: {
|
|
238
|
-
accentLow: "#dcfce7",
|
|
239
|
-
accent: "#16a34a",
|
|
240
|
-
accentHigh: "#052e16",
|
|
241
|
-
...GRAY_ZINC.light,
|
|
242
|
-
},
|
|
243
|
-
font: "'JetBrains Mono', 'Fira Code', 'Courier New', monospace",
|
|
244
|
-
},
|
|
245
|
-
|
|
246
|
-
almond: {
|
|
247
|
-
dark: {
|
|
248
|
-
accentLow: "#451a03",
|
|
249
|
-
accent: "#fbbf24",
|
|
250
|
-
accentHigh: "#fef3c7",
|
|
251
|
-
...GRAY_STONE.dark,
|
|
252
|
-
},
|
|
253
|
-
light: {
|
|
254
|
-
accentLow: "#fef3c7",
|
|
255
|
-
accent: "#b45309",
|
|
256
|
-
accentHigh: "#451a03",
|
|
257
|
-
...GRAY_STONE.light,
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
aspen: {
|
|
262
|
-
dark: {
|
|
263
|
-
accentLow: "#1e1b4b",
|
|
264
|
-
accent: "#818cf8",
|
|
265
|
-
accentHigh: "#e0e7ff",
|
|
266
|
-
...GRAY_SLATE.dark,
|
|
267
|
-
},
|
|
268
|
-
light: {
|
|
269
|
-
accentLow: "#e0e7ff",
|
|
270
|
-
accent: "#4f46e5",
|
|
271
|
-
accentHigh: "#1e1b4b",
|
|
272
|
-
...GRAY_SLATE.light,
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
|
|
277
107
|
// ── CSS generator ────────────────────────────────────────────────────────────
|
|
278
108
|
|
|
279
109
|
function textColorFor(hex: string): string {
|
|
@@ -283,67 +113,44 @@ function textColorFor(hex: string): string {
|
|
|
283
113
|
}
|
|
284
114
|
|
|
285
115
|
function generateThemeCss(config: ThemeConfig): string {
|
|
286
|
-
const themeName = config.theme
|
|
287
|
-
const preset = THEMES[themeName] || THEMES["violet"];
|
|
116
|
+
const themeName = resolveThemeName(config.theme);
|
|
288
117
|
|
|
289
|
-
const
|
|
290
|
-
|
|
118
|
+
const lines: string[] = [];
|
|
119
|
+
lines.push(`/* Velu Theme: ${themeName} */`);
|
|
120
|
+
lines.push(`@import 'fumadocs-ui/css/${themeName}.css';`);
|
|
121
|
+
lines.push("");
|
|
291
122
|
|
|
292
|
-
// Apply
|
|
123
|
+
// Apply accent overrides on top of selected Fumadocs theme.
|
|
293
124
|
if (config.colors) {
|
|
294
125
|
const { primary, light, dark } = config.colors;
|
|
295
|
-
|
|
296
126
|
const lightAccent = light || primary;
|
|
297
127
|
const darkAccent = dark || primary;
|
|
298
128
|
|
|
299
129
|
if (lightAccent) {
|
|
300
130
|
const palette = deriveAccentPalette(lightAccent);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
131
|
+
lines.push(":root {");
|
|
132
|
+
lines.push(` --color-fd-primary: ${palette.light.accent};`);
|
|
133
|
+
lines.push(` --color-fd-primary-foreground: ${textColorFor(palette.light.accent)};`);
|
|
134
|
+
lines.push(` --color-fd-accent: ${palette.light.accentLow};`);
|
|
135
|
+
lines.push(` --color-fd-accent-foreground: ${textColorFor(palette.light.accentLow)};`);
|
|
136
|
+
lines.push(` --color-fd-ring: ${palette.light.accent};`);
|
|
137
|
+
lines.push("}");
|
|
138
|
+
lines.push("");
|
|
304
139
|
}
|
|
305
140
|
|
|
306
141
|
if (darkAccent) {
|
|
307
142
|
const palette = deriveAccentPalette(darkAccent);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
143
|
+
lines.push(".dark {");
|
|
144
|
+
lines.push(` --color-fd-primary: ${palette.dark.accent};`);
|
|
145
|
+
lines.push(` --color-fd-primary-foreground: ${textColorFor(palette.dark.accent)};`);
|
|
146
|
+
lines.push(` --color-fd-accent: ${palette.dark.accentLow};`);
|
|
147
|
+
lines.push(` --color-fd-accent-foreground: ${textColorFor(palette.dark.accentLow)};`);
|
|
148
|
+
lines.push(` --color-fd-ring: ${palette.dark.accent};`);
|
|
149
|
+
lines.push("}");
|
|
150
|
+
lines.push("");
|
|
311
151
|
}
|
|
312
152
|
}
|
|
313
153
|
|
|
314
|
-
const lines: string[] = [];
|
|
315
|
-
lines.push(`/* Velu Theme: ${themeName} */`);
|
|
316
|
-
lines.push("");
|
|
317
|
-
|
|
318
|
-
// Light mode (default)
|
|
319
|
-
lines.push(":root {");
|
|
320
|
-
lines.push(` --color-fd-primary: ${lightColors.accent};`);
|
|
321
|
-
lines.push(` --color-fd-primary-foreground: ${textColorFor(lightColors.accent)};`);
|
|
322
|
-
lines.push(` --color-fd-accent: ${lightColors.accentLow};`);
|
|
323
|
-
lines.push(` --color-fd-accent-foreground: ${textColorFor(lightColors.accentLow)};`);
|
|
324
|
-
lines.push(` --color-fd-ring: ${lightColors.accent};`);
|
|
325
|
-
lines.push("}");
|
|
326
|
-
|
|
327
|
-
// Dark mode
|
|
328
|
-
lines.push("");
|
|
329
|
-
lines.push(".dark {");
|
|
330
|
-
lines.push(` --color-fd-primary: ${darkColors.accent};`);
|
|
331
|
-
lines.push(` --color-fd-primary-foreground: ${textColorFor(darkColors.accent)};`);
|
|
332
|
-
lines.push(` --color-fd-accent: ${darkColors.accentLow};`);
|
|
333
|
-
lines.push(` --color-fd-accent-foreground: ${textColorFor(darkColors.accentLow)};`);
|
|
334
|
-
lines.push(` --color-fd-ring: ${darkColors.accent};`);
|
|
335
|
-
lines.push("}");
|
|
336
|
-
|
|
337
|
-
if (preset.font || preset.fontMono) {
|
|
338
|
-
lines.push("");
|
|
339
|
-
}
|
|
340
|
-
if (preset.font) {
|
|
341
|
-
lines.push(`body { font-family: ${preset.font}; }`);
|
|
342
|
-
}
|
|
343
|
-
if (preset.fontMono) {
|
|
344
|
-
lines.push(`code, pre, kbd, samp { font-family: ${preset.fontMono}; }`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
154
|
if (config.appearance === "light") {
|
|
348
155
|
lines.push("");
|
|
349
156
|
lines.push("html { color-scheme: light; }");
|
|
@@ -357,7 +164,9 @@ function generateThemeCss(config: ThemeConfig): string {
|
|
|
357
164
|
}
|
|
358
165
|
|
|
359
166
|
function getThemeNames(): string[] {
|
|
360
|
-
return
|
|
167
|
+
return [...FUMADOCS_THEMES];
|
|
361
168
|
}
|
|
362
169
|
|
|
363
|
-
|
|
170
|
+
const THEMES = [...FUMADOCS_THEMES];
|
|
171
|
+
|
|
172
|
+
export { generateThemeCss, getThemeNames, resolveThemeName, THEMES, ThemeConfig, VeluColors, VeluStyling };
|
package/src/validate.ts
CHANGED
|
@@ -2,23 +2,79 @@ import Ajv, { type AnySchema } from "ajv";
|
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { resolve, join } from "node:path";
|
|
5
|
+
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
6
|
+
|
|
7
|
+
interface VeluSeparator {
|
|
8
|
+
separator: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface VeluLink {
|
|
12
|
+
href: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
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
|
+
interface VeluGlobalTab {
|
|
30
|
+
tab: string;
|
|
31
|
+
href: string;
|
|
32
|
+
icon?: string;
|
|
33
|
+
}
|
|
5
34
|
|
|
6
35
|
interface VeluGroup {
|
|
7
36
|
group: string;
|
|
8
|
-
slug
|
|
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)[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface VeluMenuItem {
|
|
47
|
+
item: string;
|
|
48
|
+
icon?: string;
|
|
49
|
+
groups?: VeluGroup[];
|
|
50
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
13
51
|
}
|
|
14
52
|
|
|
15
53
|
interface VeluTab {
|
|
16
54
|
tab: string;
|
|
17
|
-
slug
|
|
55
|
+
slug?: string;
|
|
18
56
|
icon?: string;
|
|
19
57
|
href?: string;
|
|
20
|
-
pages?: string[];
|
|
58
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
21
59
|
groups?: VeluGroup[];
|
|
60
|
+
menu?: VeluMenuItem[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface VeluLanguageNav {
|
|
64
|
+
language: string;
|
|
65
|
+
tabs: VeluTab[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface VeluProductNav {
|
|
69
|
+
product: string;
|
|
70
|
+
icon?: string;
|
|
71
|
+
tabs?: VeluTab[];
|
|
72
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface VeluVersionNav {
|
|
76
|
+
version: string;
|
|
77
|
+
tabs: VeluTab[];
|
|
22
78
|
}
|
|
23
79
|
|
|
24
80
|
interface VeluConfig {
|
|
@@ -28,7 +84,15 @@ interface VeluConfig {
|
|
|
28
84
|
appearance?: "system" | "light" | "dark";
|
|
29
85
|
styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
|
|
30
86
|
navigation: {
|
|
31
|
-
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
|
+
};
|
|
32
96
|
};
|
|
33
97
|
}
|
|
34
98
|
|
|
@@ -37,22 +101,34 @@ function loadJson(filePath: string): unknown {
|
|
|
37
101
|
return JSON.parse(raw);
|
|
38
102
|
}
|
|
39
103
|
|
|
40
|
-
function
|
|
104
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
105
|
+
return typeof item === "object" && item !== null && "group" in item;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isPageString(item: unknown): item is string {
|
|
109
|
+
return typeof item === "string";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectPagesFromTabs(tabs: VeluTab[]): string[] {
|
|
41
113
|
const pages: string[] = [];
|
|
42
114
|
|
|
43
115
|
function collectFromGroup(group: VeluGroup) {
|
|
44
116
|
for (const item of group.pages) {
|
|
45
|
-
if (
|
|
117
|
+
if (isPageString(item)) {
|
|
46
118
|
pages.push(item);
|
|
47
|
-
} else {
|
|
119
|
+
} else if (isGroup(item)) {
|
|
48
120
|
collectFromGroup(item);
|
|
49
121
|
}
|
|
50
122
|
}
|
|
51
123
|
}
|
|
52
124
|
|
|
53
|
-
for (const tab of
|
|
125
|
+
for (const tab of tabs) {
|
|
54
126
|
if (tab.pages) {
|
|
55
|
-
|
|
127
|
+
for (const item of tab.pages) {
|
|
128
|
+
if (isPageString(item)) {
|
|
129
|
+
pages.push(item);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
56
132
|
}
|
|
57
133
|
if (tab.groups) {
|
|
58
134
|
for (const group of tab.groups) {
|
|
@@ -64,6 +140,13 @@ function collectPages(config: VeluConfig): string[] {
|
|
|
64
140
|
return pages;
|
|
65
141
|
}
|
|
66
142
|
|
|
143
|
+
function collectPages(config: VeluConfig): string[] {
|
|
144
|
+
const tabs = config.navigation.languages && config.navigation.languages.length > 0
|
|
145
|
+
? config.navigation.languages.flatMap((lang) => lang.tabs)
|
|
146
|
+
: (config.navigation.tabs ?? []);
|
|
147
|
+
return collectPagesFromTabs(tabs);
|
|
148
|
+
}
|
|
149
|
+
|
|
67
150
|
function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boolean; errors: string[] } {
|
|
68
151
|
const errors: string[] = [];
|
|
69
152
|
|
|
@@ -77,13 +160,13 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
77
160
|
}
|
|
78
161
|
|
|
79
162
|
const schema = loadJson(schemaPath) as AnySchema;
|
|
80
|
-
const
|
|
163
|
+
const rawConfig = loadJson(configPath) as VeluConfig;
|
|
81
164
|
|
|
82
165
|
// Validate against JSON schema
|
|
83
166
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
84
167
|
addFormats(ajv);
|
|
85
168
|
const validate = ajv.compile(schema);
|
|
86
|
-
const schemaValid = validate(
|
|
169
|
+
const schemaValid = validate(rawConfig);
|
|
87
170
|
|
|
88
171
|
if (!schemaValid && validate.errors) {
|
|
89
172
|
for (const err of validate.errors) {
|
|
@@ -91,6 +174,8 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
91
174
|
}
|
|
92
175
|
}
|
|
93
176
|
|
|
177
|
+
const config = normalizeConfigNavigation(rawConfig);
|
|
178
|
+
|
|
94
179
|
// Validate that all referenced .md files exist
|
|
95
180
|
const pages = collectPages(config);
|
|
96
181
|
for (const page of pages) {
|
|
@@ -101,15 +186,28 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
101
186
|
}
|
|
102
187
|
|
|
103
188
|
// Check for duplicate page references
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
if (config.navigation.languages && config.navigation.languages.length > 0) {
|
|
190
|
+
for (const lang of config.navigation.languages) {
|
|
191
|
+
const seen = new Set<string>();
|
|
192
|
+
const langPages = collectPagesFromTabs(lang.tabs);
|
|
193
|
+
for (const page of langPages) {
|
|
194
|
+
if (seen.has(page)) {
|
|
195
|
+
errors.push(`Duplicate page reference in language '${lang.language}': ${page}`);
|
|
196
|
+
}
|
|
197
|
+
seen.add(page);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const seen = new Set<string>();
|
|
202
|
+
for (const page of pages) {
|
|
203
|
+
if (seen.has(page)) {
|
|
204
|
+
errors.push(`Duplicate page reference: ${page}`);
|
|
205
|
+
}
|
|
206
|
+
seen.add(page);
|
|
108
207
|
}
|
|
109
|
-
seen.add(page);
|
|
110
208
|
}
|
|
111
209
|
|
|
112
210
|
return { valid: errors.length === 0, errors };
|
|
113
211
|
}
|
|
114
212
|
|
|
115
|
-
export { validateVeluConfig, collectPages, VeluConfig, VeluGroup, VeluTab };
|
|
213
|
+
export { validateVeluConfig, collectPages, VeluConfig, VeluGroup, VeluTab, VeluSeparator, VeluLink, VeluAnchor };
|