@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/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): { dark: Pick<ColorSet, "accentLow" | "accent" | "accentHigh">; light: Pick<ColorSet, "accentLow" | "accent" | "accentHigh"> } {
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 || "violet";
287
- const preset = THEMES[themeName] || THEMES["violet"];
116
+ const themeName = resolveThemeName(config.theme);
288
117
 
289
- const darkColors: ColorSet = { ...preset.dark };
290
- const lightColors: ColorSet = { ...preset.light };
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 color overrides
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
- lightColors.accentLow = palette.light.accentLow;
302
- lightColors.accent = palette.light.accent;
303
- lightColors.accentHigh = palette.light.accentHigh;
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
- darkColors.accentLow = palette.dark.accentLow;
309
- darkColors.accent = palette.dark.accent;
310
- darkColors.accentHigh = palette.dark.accentHigh;
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 Object.keys(THEMES);
167
+ return [...FUMADOCS_THEMES];
361
168
  }
362
169
 
363
- export { generateThemeCss, getThemeNames, THEMES, ThemeConfig, VeluColors, VeluStyling };
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: string;
37
+ slug?: string;
9
38
  icon?: string;
10
39
  tag?: string;
11
40
  expanded?: boolean;
12
- pages: (string | VeluGroup)[];
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: string;
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: VeluTab[];
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 collectPages(config: VeluConfig): string[] {
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 (typeof item === "string") {
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 config.navigation.tabs) {
125
+ for (const tab of tabs) {
54
126
  if (tab.pages) {
55
- pages.push(...tab.pages);
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 config = loadJson(configPath) as VeluConfig;
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(config);
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
- const seen = new Set<string>();
105
- for (const page of pages) {
106
- if (seen.has(page)) {
107
- errors.push(`Duplicate page reference: ${page}`);
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 };