@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 +1 -1
- package/schema/velu.schema.json +63 -0
- package/src/build.ts +97 -96
- package/src/cli.ts +22 -3
- package/src/themes.ts +358 -0
- package/src/validate.ts +4 -0
package/package.json
CHANGED
package/schema/velu.schema.json
CHANGED
|
@@ -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
|
|
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
|
-
// ──
|
|
323
|
-
const
|
|
324
|
-
import
|
|
325
|
-
import
|
|
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
|
-
<
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
424
|
+
const tabsCss = `/* ── Velu sidebar tabs ─────────────────────────────────────────────────── */
|
|
412
425
|
|
|
413
426
|
:root {
|
|
414
|
-
--sl-
|
|
427
|
+
--sl-sidebar-width: 16rem;
|
|
415
428
|
}
|
|
416
429
|
|
|
417
|
-
|
|
418
|
-
.page > header.header {
|
|
430
|
+
.velu-sidebar-tabs {
|
|
419
431
|
display: flex;
|
|
420
432
|
flex-direction: column;
|
|
421
|
-
|
|
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
|
-
|
|
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.
|
|
450
|
-
padding: 0.
|
|
444
|
+
gap: 0.5rem;
|
|
445
|
+
padding: 0.45rem 0.65rem;
|
|
451
446
|
font-size: var(--sl-text-sm);
|
|
452
|
-
font-weight:
|
|
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-
|
|
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-
|
|
466
|
+
background-color: var(--sl-color-gray-7);
|
|
468
467
|
}
|
|
469
468
|
|
|
470
|
-
.velu-
|
|
471
|
-
opacity: 0.
|
|
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
|
-
// ──
|
|
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.
|
|
491
|
-
"@astrojs/starlight": "^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
|
-
// ──
|
|
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
|
-
// ──
|
|
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
|
|
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
|
|
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
|
|
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[];
|