@aravindc26/velu 0.2.0 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aravindc26/velu",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A modern documentation site generator powered by Markdown and JSON configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,6 +10,69 @@
10
10
  "type": "string",
11
11
  "description": "Path or URL to the JSON schema for editor validation."
12
12
  },
13
+ "theme": {
14
+ "type": "string",
15
+ "description": "Named theme preset that controls the overall look and feel.",
16
+ "enum": ["violet", "maple", "palm", "willow", "linden", "almond", "aspen"],
17
+ "default": "violet"
18
+ },
19
+ "colors": {
20
+ "type": "object",
21
+ "description": "Override the theme's accent colors.",
22
+ "properties": {
23
+ "primary": {
24
+ "type": "string",
25
+ "description": "Primary brand color (hex). Used as accent in both light and dark modes unless overridden.",
26
+ "pattern": "^#[0-9a-fA-F]{6}$"
27
+ },
28
+ "light": {
29
+ "type": "string",
30
+ "description": "Accent color for light mode (hex). Overrides primary in light mode.",
31
+ "pattern": "^#[0-9a-fA-F]{6}$"
32
+ },
33
+ "dark": {
34
+ "type": "string",
35
+ "description": "Accent color for dark mode (hex). Overrides primary in dark mode.",
36
+ "pattern": "^#[0-9a-fA-F]{6}$"
37
+ }
38
+ },
39
+ "additionalProperties": false
40
+ },
41
+ "appearance": {
42
+ "type": "string",
43
+ "description": "Controls light/dark mode behavior.",
44
+ "enum": ["system", "light", "dark"],
45
+ "default": "system"
46
+ },
47
+ "styling": {
48
+ "type": "object",
49
+ "description": "Fine-grained styling options.",
50
+ "properties": {
51
+ "codeblocks": {
52
+ "type": "object",
53
+ "description": "Code block styling options.",
54
+ "properties": {
55
+ "theme": {
56
+ "description": "Shiki theme for syntax highlighting. Use a string for a single theme, or an object with light/dark keys.",
57
+ "oneOf": [
58
+ { "type": "string" },
59
+ {
60
+ "type": "object",
61
+ "properties": {
62
+ "light": { "type": "string" },
63
+ "dark": { "type": "string" }
64
+ },
65
+ "required": ["light", "dark"],
66
+ "additionalProperties": false
67
+ }
68
+ ]
69
+ }
70
+ },
71
+ "additionalProperties": false
72
+ }
73
+ },
74
+ "additionalProperties": false
75
+ },
13
76
  "navigation": {
14
77
  "type": "object",
15
78
  "description": "Defines the site navigation hierarchy: tabs → groups → pages.",
package/src/build.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, rmSync } from "node:fs";
2
2
  import { resolve, join, dirname } from "node:path";
3
+ import { generateThemeCss, type ThemeConfig, type VeluColors, type VeluStyling } from "./themes.js";
3
4
 
4
5
  // ── Types (used only by build.ts for page copying) ─────────────────────────────
5
6
 
@@ -17,6 +18,10 @@ interface VeluTab {
17
18
 
18
19
  interface VeluConfig {
19
20
  $schema?: string;
21
+ theme?: string;
22
+ colors?: VeluColors;
23
+ appearance?: "system" | "light" | "dark";
24
+ styling?: VeluStyling;
20
25
  navigation: {
21
26
  tabs?: VeluTab[];
22
27
  groups?: VeluGroup[];
@@ -132,6 +137,10 @@ export interface VeluTab {
132
137
 
133
138
  export interface VeluConfig {
134
139
  $schema?: string;
140
+ theme?: string;
141
+ colors?: { primary?: string; light?: string; dark?: string };
142
+ appearance?: 'system' | 'light' | 'dark';
143
+ styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
135
144
  navigation: {
136
145
  tabs?: VeluTab[];
137
146
  groups?: VeluGroup[];
@@ -296,9 +305,31 @@ export function getTabSidebarMap(): Record<string, string[]> {
296
305
  writeFileSync(join(outDir, "src", "lib", "velu.ts"), veluLib, "utf-8");
297
306
  console.log("📚 Generated config module");
298
307
 
299
- // ── 4. Generate site config ────────────────────────────────────────────────
308
+ // ── 4. Generate theme CSS ─────────────────────────────────────────────────
309
+ const themeCss = generateThemeCss({
310
+ theme: config.theme,
311
+ colors: config.colors,
312
+ appearance: config.appearance,
313
+ styling: config.styling,
314
+ });
315
+ writeFileSync(join(outDir, "src", "styles", "velu-theme.css"), themeCss, "utf-8");
316
+ console.log(`🎨 Generated theme: ${config.theme || "mint"}`);
317
+
318
+ // ── 5. Generate site config ────────────────────────────────────────────────
319
+ // Build expressiveCode config for code block themes
320
+ let expressiveCodeConfig = "";
321
+ if (config.styling?.codeblocks?.theme) {
322
+ const cbt = config.styling.codeblocks.theme;
323
+ if (typeof cbt === "string") {
324
+ expressiveCodeConfig = `\n expressiveCode: { themes: ['${cbt}'] },`;
325
+ } else {
326
+ expressiveCodeConfig = `\n expressiveCode: { themes: ['${cbt.dark}', '${cbt.light}'] },`;
327
+ }
328
+ }
329
+
300
330
  const astroConfig = `import { defineConfig } from 'astro/config';
301
331
  import starlight from '@astrojs/starlight';
332
+ import { ion } from 'starlight-ion-theme';
302
333
  import { getSidebar } from './src/lib/velu.ts';
303
334
 
304
335
  export default defineConfig({
@@ -306,11 +337,11 @@ export default defineConfig({
306
337
  integrations: [
307
338
  starlight({
308
339
  title: 'Velu Docs',
340
+ plugins: [ion()],
309
341
  components: {
310
- Header: './src/components/Header.astro',
311
342
  Sidebar: './src/components/Sidebar.astro',
312
343
  },
313
- customCss: ['./src/styles/tabs.css'],
344
+ customCss: ['./src/styles/velu-theme.css', './src/styles/tabs.css'],${expressiveCodeConfig}
314
345
  sidebar: getSidebar(),
315
346
  }),
316
347
  ],
@@ -319,64 +350,26 @@ export default defineConfig({
319
350
  writeFileSync(join(outDir, "_config.mjs"), astroConfig, "utf-8");
320
351
  console.log("⚙️ Generated site config");
321
352
 
322
- // ── 5. Generate Header.astro — reads tabs from velu.ts ────────────────────
323
- const headerComponent = `---
324
- import Default from '@astrojs/starlight/components/Header.astro';
325
- import { getTabs } from '../lib/velu.ts';
353
+ // ── 6. Generate Sidebar.astro — tabs at top + filtered content ─────────────
354
+ const sidebarComponent = `---
355
+ import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
356
+ import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';
357
+ import { getTabs, getTabSidebarMap } from '../lib/velu.ts';
326
358
 
327
359
  const tabs = getTabs();
360
+ const tabSidebarMap = getTabSidebarMap();
328
361
  const currentPath = Astro.url.pathname;
329
362
 
330
363
  function isTabActive(tab: any, path: string): boolean {
331
364
  if (tab.href) return false;
332
365
  if (tab.pathPrefix === '__default__') {
333
366
  const otherPrefixes = tabs
334
- .filter((t) => t.pathPrefix && t.pathPrefix !== '__default__' && !t.href)
335
- .map((t) => t.pathPrefix);
336
- return !otherPrefixes.some((p) => path.startsWith('/' + p + '/'));
367
+ .filter((t: any) => t.pathPrefix && t.pathPrefix !== '__default__' && !t.href)
368
+ .map((t: any) => t.pathPrefix);
369
+ return !otherPrefixes.some((p: string) => path.startsWith('/' + p + '/'));
337
370
  }
338
371
  return path.startsWith('/' + tab.pathPrefix + '/');
339
372
  }
340
- ---
341
-
342
- <Default {...Astro.props}>
343
- <slot />
344
- </Default>
345
-
346
- <nav class="velu-tabs">
347
- <div class="velu-tabs-inner">
348
- {tabs.map((tab) => {
349
- if (tab.href) {
350
- return (
351
- <a href={tab.href} class="velu-tab" target="_blank" rel="noopener noreferrer">
352
- {tab.label}
353
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
354
- </a>
355
- );
356
- }
357
- const active = isTabActive(tab, currentPath);
358
- const href = tab.firstPage ? '/' + tab.firstPage + '/' : '/';
359
- return (
360
- <a href={href} class:list={['velu-tab', { active }]}>
361
- {tab.label}
362
- </a>
363
- );
364
- })}
365
- </div>
366
- </nav>
367
- `;
368
- writeFileSync(join(outDir, "src", "components", "Header.astro"), headerComponent, "utf-8");
369
- console.log("🧩 Generated header component");
370
-
371
- // ── 6. Generate Sidebar.astro — reads filter map from velu.ts ─────────────
372
- const sidebarComponent = `---
373
- import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
374
- import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro';
375
- import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';
376
- import { getTabSidebarMap } from '../lib/velu.ts';
377
-
378
- const tabSidebarMap = getTabSidebarMap();
379
- const currentPath = Astro.url.pathname;
380
373
 
381
374
  function getActivePrefix(path: string): string {
382
375
  const prefixes = Object.keys(tabSidebarMap).filter(p => p !== '__default__');
@@ -396,9 +389,29 @@ const filteredSidebar = sidebar.filter((entry: any) => {
396
389
  });
397
390
  ---
398
391
 
399
- <SidebarPersister>
400
- <SidebarSublist sublist={filteredSidebar} />
401
- </SidebarPersister>
392
+ <div class="velu-sidebar-tabs">
393
+ {tabs.map((tab) => {
394
+ if (tab.href) {
395
+ return (
396
+ <a href={tab.href} class="velu-sidebar-tab" target="_blank" rel="noopener noreferrer">
397
+ {tab.icon && <span class="velu-sidebar-tab-icon" data-icon={tab.icon} />}
398
+ <span>{tab.label}</span>
399
+ <svg class="velu-external-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
400
+ </a>
401
+ );
402
+ }
403
+ const active = isTabActive(tab, currentPath);
404
+ const href = tab.firstPage ? '/' + tab.firstPage + '/' : '/';
405
+ return (
406
+ <a href={href} class:list={['velu-sidebar-tab', { active }]}>
407
+ {tab.icon && <span class="velu-sidebar-tab-icon" data-icon={tab.icon} />}
408
+ <span>{tab.label}</span>
409
+ </a>
410
+ );
411
+ })}
412
+ </div>
413
+
414
+ <SidebarSublist sublist={filteredSidebar} />
402
415
 
403
416
  <div class="md:sl-hidden">
404
417
  <MobileMenuFooter />
@@ -408,74 +421,61 @@ const filteredSidebar = sidebar.filter((entry: any) => {
408
421
  console.log("📋 Generated sidebar component");
409
422
 
410
423
  // ── 7. Generate tabs.css ──────────────────────────────────────────────────
411
- const tabsCss = `/* ── Velu layout overrides ──────────────────────────────────────────────── */
424
+ const tabsCss = `/* ── Velu sidebar tabs ─────────────────────────────────────────────────── */
412
425
 
413
426
  :root {
414
- --sl-nav-height: 6rem;
427
+ --sl-sidebar-width: 16rem;
415
428
  }
416
429
 
417
- /* Fixed header: flex column, no bottom padding — tab bar sits at the bottom */
418
- .page > header.header {
430
+ .velu-sidebar-tabs {
419
431
  display: flex;
420
432
  flex-direction: column;
421
- padding-bottom: 0;
433
+ gap: 0.15rem;
434
+ padding: 0.35rem;
435
+ margin-bottom: 1rem;
436
+ background-color: var(--sl-color-bg);
437
+ border: 1px solid var(--sl-color-gray-5);
438
+ border-radius: 0.5rem;
422
439
  }
423
440
 
424
- /* Standard nav content fills the top */
425
- .page > header.header > .header.sl-flex {
426
- height: auto;
427
- flex: 1;
428
- }
429
-
430
- /* ── Tab bar ───────────────────────────────────────────────────────────── */
431
-
432
- .velu-tabs {
433
- flex-shrink: 0;
434
- /* Stretch to full header width past its padding */
435
- margin-inline: calc(-1 * var(--sl-nav-pad-x));
436
- padding-inline: var(--sl-nav-pad-x);
437
- background: var(--sl-color-bg-nav);
438
- }
439
-
440
- .velu-tabs-inner {
441
+ .velu-sidebar-tab {
441
442
  display: flex;
442
- gap: 0.25rem;
443
- overflow-x: auto;
444
- }
445
-
446
- .velu-tab {
447
- display: inline-flex;
448
443
  align-items: center;
449
- gap: 0.35rem;
450
- padding: 0.55rem 0.85rem;
444
+ gap: 0.5rem;
445
+ padding: 0.45rem 0.65rem;
451
446
  font-size: var(--sl-text-sm);
452
- font-weight: 500;
447
+ font-weight: 600;
453
448
  color: var(--sl-color-gray-3);
454
449
  text-decoration: none;
455
450
  border-radius: 0.375rem;
456
451
  transition: color 0.15s, background-color 0.15s;
457
- white-space: nowrap;
458
452
  }
459
453
 
460
- .velu-tab:hover {
461
- color: var(--sl-color-gray-1);
454
+ .velu-sidebar-tab:hover {
455
+ color: var(--sl-color-white);
456
+ background-color: var(--sl-color-gray-6);
457
+ }
458
+
459
+ .velu-sidebar-tab.active {
460
+ color: var(--sl-color-white);
462
461
  background-color: var(--sl-color-gray-6);
463
462
  }
464
463
 
465
- .velu-tab.active {
464
+ :root[data-theme='light'] .velu-sidebar-tab.active {
466
465
  color: var(--sl-color-white);
467
- background-color: var(--sl-color-gray-5);
466
+ background-color: var(--sl-color-gray-7);
468
467
  }
469
468
 
470
- .velu-tab svg {
471
- opacity: 0.5;
469
+ .velu-external-icon {
470
+ opacity: 0.4;
472
471
  flex-shrink: 0;
472
+ margin-inline-start: auto;
473
473
  }
474
474
  `;
475
475
  writeFileSync(join(outDir, "src", "styles", "tabs.css"), tabsCss, "utf-8");
476
476
  console.log("🎨 Generated tabs.css");
477
477
 
478
- // ── 8. Static boilerplate ─────────────────────────────────────────────────
478
+ // ── 9. Static boilerplate ─────────────────────────────────────────────────
479
479
  const astroPkg = {
480
480
  name: "velu-docs-site",
481
481
  version: "0.0.1",
@@ -487,9 +487,10 @@ const filteredSidebar = sidebar.filter((entry: any) => {
487
487
  preview: "astro preview",
488
488
  },
489
489
  dependencies: {
490
- astro: "^5.1.0",
491
- "@astrojs/starlight": "^0.32.0",
490
+ astro: "^5.12.0",
491
+ "@astrojs/starlight": "^0.35.0",
492
492
  sharp: "^0.33.0",
493
+ "starlight-ion-theme": "^2.3.0",
493
494
  },
494
495
  };
495
496
  writeFileSync(join(outDir, "package.json"), JSON.stringify(astroPkg, null, 2) + "\n", "utf-8");
@@ -513,7 +514,7 @@ const filteredSidebar = sidebar.filter((entry: any) => {
513
514
  "utf-8"
514
515
  );
515
516
 
516
- // ── 9. Generate _server.mjs — programmatic dev/build/preview ────────────────
517
+ // ── 10. Generate _server.mjs — programmatic dev/build/preview ───────────────
517
518
  const serverScript = `import { dev, build, preview } from 'astro';
518
519
  import { watch } from 'node:fs';
519
520
  import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'node:fs';
@@ -623,7 +624,7 @@ if (command === 'dev') {
623
624
  `;
624
625
  writeFileSync(join(outDir, "_server.mjs"), serverScript, "utf-8");
625
626
 
626
- // ── 10. Generate .gitignore ──────────────────────────────────────────────
627
+ // ── 11. Generate .gitignore ──────────────────────────────────────────────
627
628
  writeFileSync(
628
629
  join(outDir, ".gitignore"),
629
630
  `.astro/\nnode_modules/\ndist/\n`,
package/src/cli.ts CHANGED
@@ -17,7 +17,7 @@ function printHelp() {
17
17
  velu init Scaffold a new docs project with example files
18
18
  velu lint Validate velu.json and check referenced pages
19
19
  velu run [--port N] Build site and start dev server (default: 4321)
20
- velu build Build site without starting the dev server
20
+ velu build Build a deployable static site (SSG)
21
21
 
22
22
  Options:
23
23
  --port <number> Port for the dev server (default: 4321)
@@ -38,6 +38,7 @@ function init(targetDir: string) {
38
38
  // velu.json
39
39
  const config = {
40
40
  $schema: "https://raw.githubusercontent.com/aravindc26/velu/main/schema/velu.schema.json",
41
+ theme: "violet" as const,
41
42
  navigation: {
42
43
  tabs: [
43
44
  {
@@ -109,13 +110,31 @@ async function lint(docsDir: string) {
109
110
 
110
111
  // ── build ────────────────────────────────────────────────────────────────────────
111
112
 
112
- async function buildSite(docsDir: string): Promise<string> {
113
+ async function generateProject(docsDir: string): Promise<string> {
113
114
  const { build } = await import("./build.js");
114
115
  const outDir = join(docsDir, ".velu-out");
115
116
  build(docsDir, outDir);
116
117
  return outDir;
117
118
  }
118
119
 
120
+ async function buildStatic(outDir: string) {
121
+ await new Promise<void>((res, rej) => {
122
+ const child = spawn("node", ["_server.mjs", "build"], {
123
+ cwd: outDir,
124
+ stdio: "inherit",
125
+ });
126
+ child.on("exit", (code) => (code === 0 ? res() : rej(new Error(`Build exited with ${code}`))));
127
+ });
128
+ }
129
+
130
+ async function buildSite(docsDir: string) {
131
+ const outDir = await generateProject(docsDir);
132
+ await installDeps(outDir);
133
+ await buildStatic(outDir);
134
+ const distDir = join(outDir, "dist");
135
+ console.log(`\n📁 Static site output: ${distDir}`);
136
+ }
137
+
119
138
  // ── run ──────────────────────────────────────────────────────────────────────────
120
139
 
121
140
  async function installDeps(outDir: string) {
@@ -146,7 +165,7 @@ function spawnServer(outDir: string, command: string, port: number) {
146
165
  }
147
166
 
148
167
  async function run(docsDir: string, port: number) {
149
- const outDir = await buildSite(docsDir);
168
+ const outDir = await generateProject(docsDir);
150
169
  await installDeps(outDir);
151
170
  spawnServer(outDir, "dev", port);
152
171
  }
package/src/themes.ts ADDED
@@ -0,0 +1,358 @@
1
+ // ── Types ────────────────────────────────────────────────────────────────────
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
+ interface VeluColors {
26
+ primary?: string;
27
+ light?: string;
28
+ dark?: string;
29
+ }
30
+
31
+ interface VeluStyling {
32
+ codeblocks?: {
33
+ theme?: string | { light: string; dark: string };
34
+ };
35
+ }
36
+
37
+ interface ThemeConfig {
38
+ theme?: string;
39
+ colors?: VeluColors;
40
+ appearance?: "system" | "light" | "dark";
41
+ styling?: VeluStyling;
42
+ }
43
+
44
+ // ── Color utilities ──────────────────────────────────────────────────────────
45
+
46
+ function hexToRgb(hex: string): [number, number, number] {
47
+ const h = hex.replace("#", "");
48
+ return [
49
+ parseInt(h.substring(0, 2), 16),
50
+ parseInt(h.substring(2, 4), 16),
51
+ parseInt(h.substring(4, 6), 16),
52
+ ];
53
+ }
54
+
55
+ function rgbToHex(r: number, g: number, b: number): string {
56
+ const clamp = (v: number) => Math.round(Math.max(0, Math.min(255, v)));
57
+ return (
58
+ "#" +
59
+ [clamp(r), clamp(g), clamp(b)]
60
+ .map((c) => c.toString(16).padStart(2, "0"))
61
+ .join("")
62
+ );
63
+ }
64
+
65
+ function mixColors(hex1: string, hex2: string, weight: number): string {
66
+ const [r1, g1, b1] = hexToRgb(hex1);
67
+ const [r2, g2, b2] = hexToRgb(hex2);
68
+ return rgbToHex(
69
+ r1 * weight + r2 * (1 - weight),
70
+ g1 * weight + g2 * (1 - weight),
71
+ b1 * weight + b2 * (1 - weight)
72
+ );
73
+ }
74
+
75
+ function deriveAccentPalette(primary: string): { dark: Pick<ColorSet, "accentLow" | "accent" | "accentHigh">; light: Pick<ColorSet, "accentLow" | "accent" | "accentHigh"> } {
76
+ return {
77
+ dark: {
78
+ accentLow: mixColors(primary, "#000000", 0.3),
79
+ accent: mixColors(primary, "#ffffff", 0.2),
80
+ accentHigh: mixColors(primary, "#ffffff", 0.8),
81
+ },
82
+ light: {
83
+ accentLow: mixColors(primary, "#ffffff", 0.15),
84
+ accent: primary,
85
+ accentHigh: mixColors(primary, "#000000", 0.55),
86
+ },
87
+ };
88
+ }
89
+
90
+ // ── Gray palettes ────────────────────────────────────────────────────────────
91
+ // Starlight convention: gray-1 = strongest foreground, gray-7 = subtlest
92
+ // In dark mode: gray-1 is light, gray-7 is dark
93
+ // In light mode: gray-1 is dark, gray-7 is light
94
+ // --sl-color-white = foreground extreme, --sl-color-black = background extreme
95
+
96
+ const GRAY_SLATE = {
97
+ dark: {
98
+ white: "#ffffff",
99
+ gray1: "#eceef2",
100
+ gray2: "#c0c2c7",
101
+ gray3: "#888b96",
102
+ gray4: "#545861",
103
+ gray5: "#353841",
104
+ gray6: "#24272f",
105
+ gray7: "#17181c",
106
+ black: "#13141a",
107
+ },
108
+ light: {
109
+ white: "#13141a",
110
+ gray1: "#17181c",
111
+ gray2: "#24272f",
112
+ gray3: "#545861",
113
+ gray4: "#888b96",
114
+ gray5: "#c0c2c7",
115
+ gray6: "#eceef2",
116
+ gray7: "#f5f6f8",
117
+ black: "#ffffff",
118
+ },
119
+ };
120
+
121
+ const GRAY_ZINC = {
122
+ dark: {
123
+ white: "#ffffff",
124
+ gray1: "#ececef",
125
+ gray2: "#bfc0c4",
126
+ gray3: "#878890",
127
+ gray4: "#53545c",
128
+ gray5: "#34353b",
129
+ gray6: "#23242a",
130
+ gray7: "#17171a",
131
+ black: "#121214",
132
+ },
133
+ light: {
134
+ white: "#121214",
135
+ gray1: "#17171a",
136
+ gray2: "#23242a",
137
+ gray3: "#53545c",
138
+ gray4: "#878890",
139
+ gray5: "#bfc0c4",
140
+ gray6: "#ececef",
141
+ gray7: "#f5f5f7",
142
+ black: "#ffffff",
143
+ },
144
+ };
145
+
146
+ const GRAY_STONE = {
147
+ dark: {
148
+ white: "#ffffff",
149
+ gray1: "#eeeceb",
150
+ gray2: "#c3bfbb",
151
+ gray3: "#8c8680",
152
+ gray4: "#585550",
153
+ gray5: "#383532",
154
+ gray6: "#272421",
155
+ gray7: "#1a1816",
156
+ black: "#141210",
157
+ },
158
+ light: {
159
+ white: "#141210",
160
+ gray1: "#1a1816",
161
+ gray2: "#272421",
162
+ gray3: "#585550",
163
+ gray4: "#8c8680",
164
+ gray5: "#c3bfbb",
165
+ gray6: "#eeeceb",
166
+ gray7: "#f7f6f5",
167
+ black: "#ffffff",
168
+ },
169
+ };
170
+
171
+ // ── Theme presets ────────────────────────────────────────────────────────────
172
+
173
+ const THEMES: Record<string, ThemePreset> = {
174
+ violet: {
175
+ dark: {
176
+ accentLow: "#1e1b4b",
177
+ accent: "#818cf8",
178
+ accentHigh: "#e0e7ff",
179
+ ...GRAY_SLATE.dark,
180
+ },
181
+ light: {
182
+ accentLow: "#e0e7ff",
183
+ accent: "#4f46e5",
184
+ accentHigh: "#1e1b4b",
185
+ ...GRAY_SLATE.light,
186
+ },
187
+ },
188
+
189
+ maple: {
190
+ dark: {
191
+ accentLow: "#2e1065",
192
+ accent: "#a78bfa",
193
+ accentHigh: "#ede9fe",
194
+ ...GRAY_ZINC.dark,
195
+ },
196
+ light: {
197
+ accentLow: "#ede9fe",
198
+ accent: "#7c3aed",
199
+ accentHigh: "#2e1065",
200
+ ...GRAY_ZINC.light,
201
+ },
202
+ },
203
+
204
+ palm: {
205
+ dark: {
206
+ accentLow: "#0c2d44",
207
+ accent: "#38bdf8",
208
+ accentHigh: "#e0f2fe",
209
+ ...GRAY_SLATE.dark,
210
+ },
211
+ light: {
212
+ accentLow: "#e0f2fe",
213
+ accent: "#0369a1",
214
+ accentHigh: "#0c2d44",
215
+ ...GRAY_SLATE.light,
216
+ },
217
+ },
218
+
219
+ willow: {
220
+ dark: {
221
+ accentLow: "#292524",
222
+ accent: "#a8a29e",
223
+ accentHigh: "#fafaf9",
224
+ ...GRAY_STONE.dark,
225
+ },
226
+ light: {
227
+ accentLow: "#f5f5f4",
228
+ accent: "#57534e",
229
+ accentHigh: "#1c1917",
230
+ ...GRAY_STONE.light,
231
+ },
232
+ },
233
+
234
+ linden: {
235
+ dark: {
236
+ accentLow: "#052e16",
237
+ accent: "#4ade80",
238
+ accentHigh: "#dcfce7",
239
+ ...GRAY_ZINC.dark,
240
+ },
241
+ light: {
242
+ accentLow: "#dcfce7",
243
+ accent: "#16a34a",
244
+ accentHigh: "#052e16",
245
+ ...GRAY_ZINC.light,
246
+ },
247
+ font: "'JetBrains Mono', 'Fira Code', 'Courier New', monospace",
248
+ },
249
+
250
+ almond: {
251
+ dark: {
252
+ accentLow: "#451a03",
253
+ accent: "#fbbf24",
254
+ accentHigh: "#fef3c7",
255
+ ...GRAY_STONE.dark,
256
+ },
257
+ light: {
258
+ accentLow: "#fef3c7",
259
+ accent: "#b45309",
260
+ accentHigh: "#451a03",
261
+ ...GRAY_STONE.light,
262
+ },
263
+ },
264
+
265
+ aspen: {
266
+ dark: {
267
+ accentLow: "#1e1b4b",
268
+ accent: "#818cf8",
269
+ accentHigh: "#e0e7ff",
270
+ ...GRAY_SLATE.dark,
271
+ },
272
+ light: {
273
+ accentLow: "#e0e7ff",
274
+ accent: "#4f46e5",
275
+ accentHigh: "#1e1b4b",
276
+ ...GRAY_SLATE.light,
277
+ },
278
+ },
279
+ };
280
+
281
+ // ── CSS generator ────────────────────────────────────────────────────────────
282
+
283
+ function colorSetToCss(colors: ColorSet): string {
284
+ return [
285
+ ` --sl-color-accent-low: ${colors.accentLow};`,
286
+ ` --sl-color-accent: ${colors.accent};`,
287
+ ` --sl-color-accent-high: ${colors.accentHigh};`,
288
+ ` --sl-color-white: ${colors.white};`,
289
+ ` --sl-color-gray-1: ${colors.gray1};`,
290
+ ` --sl-color-gray-2: ${colors.gray2};`,
291
+ ` --sl-color-gray-3: ${colors.gray3};`,
292
+ ` --sl-color-gray-4: ${colors.gray4};`,
293
+ ` --sl-color-gray-5: ${colors.gray5};`,
294
+ ` --sl-color-gray-6: ${colors.gray6};`,
295
+ ` --sl-color-gray-7: ${colors.gray7};`,
296
+ ` --sl-color-black: ${colors.black};`,
297
+ ].join("\n");
298
+ }
299
+
300
+ function generateThemeCss(config: ThemeConfig): string {
301
+ const themeName = config.theme || "violet";
302
+ const preset = THEMES[themeName] || THEMES["violet"];
303
+
304
+ const darkColors: ColorSet = { ...preset.dark };
305
+ const lightColors: ColorSet = { ...preset.light };
306
+
307
+ // Apply color overrides
308
+ if (config.colors) {
309
+ const { primary, light, dark } = config.colors;
310
+
311
+ const lightAccent = light || primary;
312
+ const darkAccent = dark || primary;
313
+
314
+ if (lightAccent) {
315
+ const palette = deriveAccentPalette(lightAccent);
316
+ lightColors.accentLow = palette.light.accentLow;
317
+ lightColors.accent = palette.light.accent;
318
+ lightColors.accentHigh = palette.light.accentHigh;
319
+ }
320
+
321
+ if (darkAccent) {
322
+ const palette = deriveAccentPalette(darkAccent);
323
+ darkColors.accentLow = palette.dark.accentLow;
324
+ darkColors.accent = palette.dark.accent;
325
+ darkColors.accentHigh = palette.dark.accentHigh;
326
+ }
327
+ }
328
+
329
+ const lines: string[] = [];
330
+ lines.push(`/* Velu Theme: ${themeName} */`);
331
+ lines.push("");
332
+
333
+ // Dark mode (Starlight default)
334
+ lines.push(":root {");
335
+ lines.push(colorSetToCss(darkColors));
336
+ if (preset.font) {
337
+ lines.push(` --sl-font: ${preset.font};`);
338
+ }
339
+ if (preset.fontMono) {
340
+ lines.push(` --sl-font-mono: ${preset.fontMono};`);
341
+ }
342
+ lines.push("}");
343
+ lines.push("");
344
+
345
+ // Light mode
346
+ lines.push(":root[data-theme='light'] {");
347
+ lines.push(colorSetToCss(lightColors));
348
+ lines.push("}");
349
+ lines.push("");
350
+
351
+ return lines.join("\n");
352
+ }
353
+
354
+ function getThemeNames(): string[] {
355
+ return Object.keys(THEMES);
356
+ }
357
+
358
+ export { generateThemeCss, getThemeNames, THEMES, ThemeConfig, VeluColors, VeluStyling };
package/src/validate.ts CHANGED
@@ -21,6 +21,10 @@ interface VeluTab {
21
21
 
22
22
  interface VeluConfig {
23
23
  $schema?: string;
24
+ theme?: string;
25
+ colors?: { primary?: string; light?: string; dark?: string };
26
+ appearance?: "system" | "light" | "dark";
27
+ styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
24
28
  navigation: {
25
29
  tabs?: VeluTab[];
26
30
  groups?: VeluGroup[];