@aravindc26/velu 0.11.16 → 0.11.19

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.11.16",
3
+ "version": "0.11.19",
4
4
  "description": "A modern documentation site generator powered by Markdown and JSON configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,6 +92,26 @@
92
92
  ],
93
93
  "default": "system"
94
94
  },
95
+ "fonts": {
96
+ "description": "Custom font configuration. Can be a single font definition (applied to both headings and body) or an object with separate heading and body font definitions.",
97
+ "oneOf": [
98
+ { "$ref": "#/definitions/fontDefinition" },
99
+ {
100
+ "type": "object",
101
+ "properties": {
102
+ "heading": {
103
+ "$ref": "#/definitions/fontDefinition",
104
+ "description": "Font used for headings (h1–h6)."
105
+ },
106
+ "body": {
107
+ "$ref": "#/definitions/fontDefinition",
108
+ "description": "Font used for body text."
109
+ }
110
+ },
111
+ "additionalProperties": false
112
+ }
113
+ ]
114
+ },
95
115
  "icons": {
96
116
  "type": "object",
97
117
  "description": "Icon settings compatible with Mint-style configuration.",
@@ -112,44 +132,6 @@
112
132
  ],
113
133
  "additionalProperties": false
114
134
  },
115
- "styling": {
116
- "type": "object",
117
- "description": "Fine-grained styling options.",
118
- "properties": {
119
- "codeblocks": {
120
- "type": "object",
121
- "description": "Code block styling options.",
122
- "properties": {
123
- "theme": {
124
- "description": "Shiki theme for syntax highlighting. Use a string for a single theme, or an object with light/dark keys.",
125
- "oneOf": [
126
- {
127
- "type": "string"
128
- },
129
- {
130
- "type": "object",
131
- "properties": {
132
- "light": {
133
- "type": "string"
134
- },
135
- "dark": {
136
- "type": "string"
137
- }
138
- },
139
- "required": [
140
- "light",
141
- "dark"
142
- ],
143
- "additionalProperties": false
144
- }
145
- ]
146
- }
147
- },
148
- "additionalProperties": false
149
- }
150
- },
151
- "additionalProperties": false
152
- },
153
135
  "languages": {
154
136
  "type": "array",
155
137
  "description": "Supported language codes (e.g. [\"en\", \"es\", \"ja\"]). The first is the default. Display labels are auto-derived.",
@@ -658,6 +640,32 @@
658
640
  },
659
641
  "additionalProperties": false,
660
642
  "definitions": {
643
+ "fontDefinition": {
644
+ "type": "object",
645
+ "description": "A font definition with family name and optional weight, source URL, and format.",
646
+ "properties": {
647
+ "family": {
648
+ "type": "string",
649
+ "description": "The font family name, e.g. \"Inter\", \"Playfair Display\"."
650
+ },
651
+ "weight": {
652
+ "type": "number",
653
+ "description": "The font weight, e.g. 400, 700. Variable font weights like 550 are supported."
654
+ },
655
+ "source": {
656
+ "type": "string",
657
+ "minLength": 1,
658
+ "description": "URL or path to a custom font file, e.g. /fonts/custom.woff2 or https://example.com/font.woff2."
659
+ },
660
+ "format": {
661
+ "type": "string",
662
+ "enum": ["woff", "woff2"],
663
+ "description": "The font file format when using a custom source."
664
+ }
665
+ },
666
+ "required": ["family"],
667
+ "additionalProperties": false
668
+ },
661
669
  "footerSocials": {
662
670
  "type": "object",
663
671
  "description": "Map of social network keys to URLs (for example {\"x\":\"https://x.com/org\",\"github\":\"https://github.com/org\"}).",
package/src/build.ts CHANGED
@@ -2,19 +2,19 @@ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSyn
2
2
  import { join, dirname, relative, extname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { parse as parseYaml } from "yaml";
5
- import { generateThemeCss, resolveThemeName, type VeluColors, type VeluStyling } from "./themes.js";
5
+ import { generateThemeCss, resolveThemeName, type VeluColors } from "./themes.js";
6
6
  import { normalizeConfigNavigation } from "./navigation-normalize.js";
7
7
 
8
8
  // ── Engine directory (shipped with the CLI package) ──────────────────────────
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
11
  const PACKAGED_ENGINE_DIR = join(__dirname, "engine");
12
- const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
13
- const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
14
- const CLI_PACKAGE_JSON_PATH = join(__dirname, "..", "package.json");
15
- const PRIMARY_CONFIG_NAME = "docs.json";
16
- const LEGACY_CONFIG_NAME = "velu.json";
17
- const SOURCE_MIRROR_DIR = "velu-imports";
12
+ const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
13
+ const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
14
+ const CLI_PACKAGE_JSON_PATH = join(__dirname, "..", "package.json");
15
+ const PRIMARY_CONFIG_NAME = "docs.json";
16
+ const LEGACY_CONFIG_NAME = "velu.json";
17
+ const SOURCE_MIRROR_DIR = "velu-imports";
18
18
 
19
19
  const SOURCE_MIRROR_EXTENSIONS = new Set([
20
20
  ".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
@@ -25,11 +25,11 @@ const SOURCE_MIRROR_EXTENSIONS = new Set([
25
25
  ".pdf", ".txt", ".xml", ".csv", ".zip",
26
26
  ]);
27
27
 
28
- const IMPORT_REWRITE_EXTENSIONS = new Set([".md", ".mdx", ".jsx", ".js", ".tsx", ".ts"]);
29
- const VARIABLE_SUBSTITUTION_EXTENSIONS = new Set([
30
- ".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
31
- ".json", ".yaml", ".yml", ".css", ".txt", ".xml", ".csv",
32
- ]);
28
+ const IMPORT_REWRITE_EXTENSIONS = new Set([".md", ".mdx", ".jsx", ".js", ".tsx", ".ts"]);
29
+ const VARIABLE_SUBSTITUTION_EXTENSIONS = new Set([
30
+ ".md", ".mdx", ".jsx", ".js", ".tsx", ".ts",
31
+ ".json", ".yaml", ".yml", ".css", ".txt", ".xml", ".csv",
32
+ ]);
33
33
 
34
34
  function resolveConfigPath(docsDir: string): string {
35
35
  const primary = join(docsDir, PRIMARY_CONFIG_NAME);
@@ -136,22 +136,22 @@ interface VeluRedirect {
136
136
  permanent?: boolean;
137
137
  }
138
138
 
139
- interface VeluConfig {
140
- $schema?: string;
141
- name?: string;
142
- title?: string;
143
- description?: string;
144
- theme?: string;
145
- variables?: Record<string, string>;
146
- colors?: VeluColors;
147
- appearance?: "system" | "light" | "dark";
148
- styling?: VeluStyling;
149
- metadata?: {
150
- timestamp?: boolean;
151
- };
152
- openapi?: VeluOpenApiSource;
153
- languages?: string[];
154
- redirects?: VeluRedirect[];
139
+ interface VeluConfig {
140
+ $schema?: string;
141
+ name?: string;
142
+ title?: string;
143
+ description?: string;
144
+ theme?: string;
145
+ variables?: Record<string, string>;
146
+ colors?: VeluColors;
147
+ appearance?: "system" | "light" | "dark";
148
+ fonts?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } | { heading?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" }; body?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } };
149
+ metadata?: {
150
+ timestamp?: boolean;
151
+ };
152
+ openapi?: VeluOpenApiSource;
153
+ languages?: string[];
154
+ redirects?: VeluRedirect[];
155
155
  navigation: {
156
156
  openapi?: VeluOpenApiSource;
157
157
  tabs?: VeluTab[];
@@ -405,115 +405,115 @@ function normalizeOpenApiSpecForFrontmatter(spec: string | undefined): string |
405
405
  return `/${trimmed.replace(/^\.?\/*/, "")}`;
406
406
  }
407
407
 
408
- // ── Helpers ────────────────────────────────────────────────────────────────────
409
-
410
- const VARIABLE_TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9.-]+)\s*\}\}/g;
411
- const VARIABLE_NAME_PATTERN = /^[A-Za-z0-9.-]+$/;
412
-
413
- function sanitizeVariableValue(value: string): string {
414
- return value.replace(/</g, "&lt;").replace(/>/g, "&gt;");
415
- }
416
-
417
- function extractVariables(input: unknown): Record<string, string> {
418
- if (!input || typeof input !== "object" || Array.isArray(input)) return {};
419
-
420
- const output: Record<string, string> = {};
421
- for (const [rawKey, rawValue] of Object.entries(input as Record<string, unknown>)) {
422
- const key = rawKey.trim();
423
- if (!key) continue;
424
- if (!VARIABLE_NAME_PATTERN.test(key)) {
425
- throw new Error(`Invalid variable name '${rawKey}'. Variable names can only contain letters, numbers, periods, and hyphens.`);
426
- }
427
- if (typeof rawValue !== "string") {
428
- throw new Error(`Invalid value for variable '${rawKey}'. Variables must be strings.`);
429
- }
430
- output[key] = rawValue;
431
- }
432
- return output;
433
- }
434
-
435
- function resolveVariableMap(rawVariables: Record<string, string>): Record<string, string> {
436
- const cache = new Map<string, string>();
437
- const activeStack = new Set<string>();
438
-
439
- function resolveOne(name: string): string {
440
- const cached = cache.get(name);
441
- if (cached !== undefined) return cached;
442
-
443
- if (activeStack.has(name)) {
444
- throw new Error(`Circular variable reference detected for '{{${name}}}'.`);
445
- }
446
-
447
- const raw = rawVariables[name];
448
- if (raw === undefined) {
449
- throw new Error(`Undefined variable '{{${name}}}' referenced in variable definitions.`);
450
- }
451
-
452
- activeStack.add(name);
453
- const resolved = raw.replace(VARIABLE_TOKEN_PATTERN, (_match, token: string) => resolveOne(token));
454
- activeStack.delete(name);
455
- cache.set(name, resolved);
456
- return resolved;
457
- }
458
-
459
- const output: Record<string, string> = {};
460
- for (const name of Object.keys(rawVariables)) {
461
- output[name] = resolveOne(name);
462
- }
463
- return output;
464
- }
465
-
466
- function replaceVariablesInString(
467
- value: string,
468
- variables: Record<string, string>,
469
- context: string,
470
- sanitizeValues: boolean,
471
- ): string {
472
- const undefinedVariables = new Set<string>();
473
- const replaced = value.replace(VARIABLE_TOKEN_PATTERN, (match, rawName: string) => {
474
- const name = rawName.trim();
475
- const resolved = variables[name];
476
- if (resolved === undefined) {
477
- undefinedVariables.add(name);
478
- return match;
479
- }
480
- return sanitizeValues ? sanitizeVariableValue(resolved) : resolved;
481
- });
482
-
483
- if (undefinedVariables.size > 0) {
484
- throw new Error(
485
- `Undefined variable(s) ${Array.from(undefinedVariables).map((name) => `'{{${name}}}'`).join(", ")} in ${context}.`
486
- );
487
- }
488
-
489
- return replaced;
490
- }
491
-
492
- function applyVariablesToConfig(value: unknown, variables: Record<string, string>, path = "docs.json"): unknown {
493
- if (typeof value === "string") return replaceVariablesInString(value, variables, path, false);
494
- if (Array.isArray(value)) return value.map((entry, index) => applyVariablesToConfig(entry, variables, `${path}[${index}]`));
495
- if (!value || typeof value !== "object") return value;
496
-
497
- const output: Record<string, unknown> = {};
498
- for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
499
- output[key] = applyVariablesToConfig(entry, variables, `${path}.${key}`);
500
- }
501
- return output;
502
- }
503
-
504
- function loadConfig(docsDir: string): { config: VeluConfig; rawConfig: VeluConfig; variables: Record<string, string> } {
505
- const raw = readFileSync(resolveConfigPath(docsDir), "utf-8");
506
- const parsed = JSON.parse(raw) as VeluConfig;
507
- const rawVariables = extractVariables(parsed.variables);
508
- const resolvedVariables = resolveVariableMap(rawVariables);
509
- const withVariables = applyVariablesToConfig(parsed, resolvedVariables) as VeluConfig;
510
- withVariables.variables = resolvedVariables;
511
- return {
512
- config: normalizeConfigNavigation(withVariables),
513
- rawConfig: withVariables,
514
- variables: resolvedVariables,
515
- };
516
- }
408
+ // ── Helpers ────────────────────────────────────────────────────────────────────
409
+
410
+ const VARIABLE_TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9.-]+)\s*\}\}/g;
411
+ const VARIABLE_NAME_PATTERN = /^[A-Za-z0-9.-]+$/;
412
+
413
+ function sanitizeVariableValue(value: string): string {
414
+ return value.replace(/</g, "&lt;").replace(/>/g, "&gt;");
415
+ }
416
+
417
+ function extractVariables(input: unknown): Record<string, string> {
418
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
419
+
420
+ const output: Record<string, string> = {};
421
+ for (const [rawKey, rawValue] of Object.entries(input as Record<string, unknown>)) {
422
+ const key = rawKey.trim();
423
+ if (!key) continue;
424
+ if (!VARIABLE_NAME_PATTERN.test(key)) {
425
+ throw new Error(`Invalid variable name '${rawKey}'. Variable names can only contain letters, numbers, periods, and hyphens.`);
426
+ }
427
+ if (typeof rawValue !== "string") {
428
+ throw new Error(`Invalid value for variable '${rawKey}'. Variables must be strings.`);
429
+ }
430
+ output[key] = rawValue;
431
+ }
432
+ return output;
433
+ }
434
+
435
+ function resolveVariableMap(rawVariables: Record<string, string>): Record<string, string> {
436
+ const cache = new Map<string, string>();
437
+ const activeStack = new Set<string>();
438
+
439
+ function resolveOne(name: string): string {
440
+ const cached = cache.get(name);
441
+ if (cached !== undefined) return cached;
442
+
443
+ if (activeStack.has(name)) {
444
+ throw new Error(`Circular variable reference detected for '{{${name}}}'.`);
445
+ }
446
+
447
+ const raw = rawVariables[name];
448
+ if (raw === undefined) {
449
+ throw new Error(`Undefined variable '{{${name}}}' referenced in variable definitions.`);
450
+ }
451
+
452
+ activeStack.add(name);
453
+ const resolved = raw.replace(VARIABLE_TOKEN_PATTERN, (_match, token: string) => resolveOne(token));
454
+ activeStack.delete(name);
455
+ cache.set(name, resolved);
456
+ return resolved;
457
+ }
458
+
459
+ const output: Record<string, string> = {};
460
+ for (const name of Object.keys(rawVariables)) {
461
+ output[name] = resolveOne(name);
462
+ }
463
+ return output;
464
+ }
465
+
466
+ function replaceVariablesInString(
467
+ value: string,
468
+ variables: Record<string, string>,
469
+ context: string,
470
+ sanitizeValues: boolean,
471
+ ): string {
472
+ const undefinedVariables = new Set<string>();
473
+ const replaced = value.replace(VARIABLE_TOKEN_PATTERN, (match, rawName: string) => {
474
+ const name = rawName.trim();
475
+ const resolved = variables[name];
476
+ if (resolved === undefined) {
477
+ undefinedVariables.add(name);
478
+ return match;
479
+ }
480
+ return sanitizeValues ? sanitizeVariableValue(resolved) : resolved;
481
+ });
482
+
483
+ if (undefinedVariables.size > 0) {
484
+ throw new Error(
485
+ `Undefined variable(s) ${Array.from(undefinedVariables).map((name) => `'{{${name}}}'`).join(", ")} in ${context}.`
486
+ );
487
+ }
488
+
489
+ return replaced;
490
+ }
491
+
492
+ function applyVariablesToConfig(value: unknown, variables: Record<string, string>, path = "docs.json"): unknown {
493
+ if (typeof value === "string") return replaceVariablesInString(value, variables, path, false);
494
+ if (Array.isArray(value)) return value.map((entry, index) => applyVariablesToConfig(entry, variables, `${path}[${index}]`));
495
+ if (!value || typeof value !== "object") return value;
496
+
497
+ const output: Record<string, unknown> = {};
498
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
499
+ output[key] = applyVariablesToConfig(entry, variables, `${path}.${key}`);
500
+ }
501
+ return output;
502
+ }
503
+
504
+ function loadConfig(docsDir: string): { config: VeluConfig; rawConfig: VeluConfig; variables: Record<string, string> } {
505
+ const raw = readFileSync(resolveConfigPath(docsDir), "utf-8");
506
+ const parsed = JSON.parse(raw) as VeluConfig;
507
+ const rawVariables = extractVariables(parsed.variables);
508
+ const resolvedVariables = resolveVariableMap(rawVariables);
509
+ const withVariables = applyVariablesToConfig(parsed, resolvedVariables) as VeluConfig;
510
+ withVariables.variables = resolvedVariables;
511
+ return {
512
+ config: normalizeConfigNavigation(withVariables),
513
+ rawConfig: withVariables,
514
+ variables: resolvedVariables,
515
+ };
516
+ }
517
517
 
518
518
  function isExternalDestination(value: string): boolean {
519
519
  return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value);
@@ -591,7 +591,7 @@ const STATIC_EXTENSIONS = new Set([
591
591
  ".zip",
592
592
  ]);
593
593
 
594
- function copyStaticAssets(docsDir: string, publicDir: string) {
594
+ function copyStaticAssets(docsDir: string, publicDir: string) {
595
595
  function walk(dir: string) {
596
596
  const entries = readdirSync(dir, { withFileTypes: true });
597
597
  for (const entry of entries) {
@@ -615,49 +615,49 @@ function copyStaticAssets(docsDir: string, publicDir: string) {
615
615
  }
616
616
  }
617
617
 
618
- walk(docsDir);
619
- }
620
-
621
- function resolveProjectName(config: VeluConfig): string {
622
- const fromName = typeof config.name === "string" ? config.name.trim() : "";
623
- if (fromName) return fromName;
624
- const fromTitle = typeof config.title === "string" ? config.title.trim() : "";
625
- if (fromTitle) return fromTitle;
626
- return "Documentation";
627
- }
628
-
629
- function resolveProjectDescription(config: VeluConfig): string {
630
- if (typeof config.description === "string") return config.description.trim();
631
- return "";
632
- }
633
-
634
- function resolveCliVersion(): string {
635
- try {
636
- const raw = readFileSync(CLI_PACKAGE_JSON_PATH, "utf-8");
637
- const parsed = JSON.parse(raw) as { version?: unknown };
638
- if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
639
- return parsed.version.trim();
640
- }
641
- } catch {
642
- // ignore and fallback
643
- }
644
- return "unknown";
645
- }
646
-
647
- function writeProjectConstFile(config: VeluConfig, outDir: string) {
648
- const constPayload = {
649
- name: resolveProjectName(config),
650
- description: resolveProjectDescription(config),
651
- version: resolveCliVersion(),
652
- };
653
-
654
- const constPath = join(outDir, "public", "const.json");
655
- writeFileSync(constPath, `${JSON.stringify(constPayload, null, 2)}\n`, "utf-8");
656
- }
657
-
658
- function toPosixPath(value: string): string {
659
- return value.replace(/\\/g, "/");
660
- }
618
+ walk(docsDir);
619
+ }
620
+
621
+ function resolveProjectName(config: VeluConfig): string {
622
+ const fromName = typeof config.name === "string" ? config.name.trim() : "";
623
+ if (fromName) return fromName;
624
+ const fromTitle = typeof config.title === "string" ? config.title.trim() : "";
625
+ if (fromTitle) return fromTitle;
626
+ return "Documentation";
627
+ }
628
+
629
+ function resolveProjectDescription(config: VeluConfig): string {
630
+ if (typeof config.description === "string") return config.description.trim();
631
+ return "";
632
+ }
633
+
634
+ function resolveCliVersion(): string {
635
+ try {
636
+ const raw = readFileSync(CLI_PACKAGE_JSON_PATH, "utf-8");
637
+ const parsed = JSON.parse(raw) as { version?: unknown };
638
+ if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
639
+ return parsed.version.trim();
640
+ }
641
+ } catch {
642
+ // ignore and fallback
643
+ }
644
+ return "unknown";
645
+ }
646
+
647
+ function writeProjectConstFile(config: VeluConfig, outDir: string) {
648
+ const constPayload = {
649
+ name: resolveProjectName(config),
650
+ description: resolveProjectDescription(config),
651
+ version: resolveCliVersion(),
652
+ };
653
+
654
+ const constPath = join(outDir, "public", "const.json");
655
+ writeFileSync(constPath, `${JSON.stringify(constPayload, null, 2)}\n`, "utf-8");
656
+ }
657
+
658
+ function toPosixPath(value: string): string {
659
+ return value.replace(/\\/g, "/");
660
+ }
661
661
 
662
662
  function isInsideDocsRoot(docsDir: string, targetPath: string): boolean {
663
663
  const relPath = relative(docsDir, targetPath);
@@ -824,41 +824,41 @@ function rewriteImportsInContent(
824
824
  return out.join("\n");
825
825
  }
826
826
 
827
- function copyMirroredSourceFile(
828
- srcPath: string,
829
- docsDir: string,
830
- mirrorDir: string,
831
- variables: Record<string, string>,
832
- ) {
833
- if (!shouldMirrorSourceFile(srcPath)) return;
834
- if (!isInsideDocsRoot(docsDir, srcPath)) return;
835
-
836
- const relPath = relative(docsDir, srcPath);
837
- const destPath = join(mirrorDir, relPath);
838
- mkdirSync(dirname(destPath), { recursive: true });
839
-
840
- if (shouldRewriteImports(srcPath)) {
841
- let raw = readFileSync(srcPath, "utf-8");
842
- raw = replaceVariablesInString(raw, variables, relPath, true);
843
- const rewritten = rewriteImportsInContent(raw, srcPath, destPath, docsDir, mirrorDir);
844
- writeFileSync(destPath, rewritten, "utf-8");
845
- return;
846
- }
847
-
848
- const extension = extname(srcPath).toLowerCase();
849
- if (VARIABLE_SUBSTITUTION_EXTENSIONS.has(extension)) {
850
- const raw = readFileSync(srcPath, "utf-8");
851
- const substituted = replaceVariablesInString(raw, variables, relPath, true);
852
- writeFileSync(destPath, substituted, "utf-8");
853
- return;
854
- }
855
-
856
- copyFileSync(srcPath, destPath);
857
- }
858
-
859
- function rebuildSourceMirror(docsDir: string, mirrorDir: string, variables: Record<string, string>) {
860
- rmSync(mirrorDir, { recursive: true, force: true });
861
- mkdirSync(mirrorDir, { recursive: true });
827
+ function copyMirroredSourceFile(
828
+ srcPath: string,
829
+ docsDir: string,
830
+ mirrorDir: string,
831
+ variables: Record<string, string>,
832
+ ) {
833
+ if (!shouldMirrorSourceFile(srcPath)) return;
834
+ if (!isInsideDocsRoot(docsDir, srcPath)) return;
835
+
836
+ const relPath = relative(docsDir, srcPath);
837
+ const destPath = join(mirrorDir, relPath);
838
+ mkdirSync(dirname(destPath), { recursive: true });
839
+
840
+ if (shouldRewriteImports(srcPath)) {
841
+ let raw = readFileSync(srcPath, "utf-8");
842
+ raw = replaceVariablesInString(raw, variables, relPath, true);
843
+ const rewritten = rewriteImportsInContent(raw, srcPath, destPath, docsDir, mirrorDir);
844
+ writeFileSync(destPath, rewritten, "utf-8");
845
+ return;
846
+ }
847
+
848
+ const extension = extname(srcPath).toLowerCase();
849
+ if (VARIABLE_SUBSTITUTION_EXTENSIONS.has(extension)) {
850
+ const raw = readFileSync(srcPath, "utf-8");
851
+ const substituted = replaceVariablesInString(raw, variables, relPath, true);
852
+ writeFileSync(destPath, substituted, "utf-8");
853
+ return;
854
+ }
855
+
856
+ copyFileSync(srcPath, destPath);
857
+ }
858
+
859
+ function rebuildSourceMirror(docsDir: string, mirrorDir: string, variables: Record<string, string>) {
860
+ rmSync(mirrorDir, { recursive: true, force: true });
861
+ mkdirSync(mirrorDir, { recursive: true });
862
862
 
863
863
  function walk(dir: string) {
864
864
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -866,14 +866,14 @@ function rebuildSourceMirror(docsDir: string, mirrorDir: string, variables: Reco
866
866
  if (entry.name.startsWith(".")) continue;
867
867
  if (entry.name === "node_modules") continue;
868
868
  const srcPath = join(dir, entry.name);
869
- if (entry.isDirectory()) {
870
- walk(srcPath);
871
- continue;
872
- }
873
- if (!shouldMirrorSourceFile(srcPath)) continue;
874
- copyMirroredSourceFile(srcPath, docsDir, mirrorDir, variables);
875
- }
876
- }
869
+ if (entry.isDirectory()) {
870
+ walk(srcPath);
871
+ continue;
872
+ }
873
+ if (!shouldMirrorSourceFile(srcPath)) continue;
874
+ copyMirroredSourceFile(srcPath, docsDir, mirrorDir, variables);
875
+ }
876
+ }
877
877
 
878
878
  walk(docsDir);
879
879
  }
@@ -1179,11 +1179,11 @@ function buildArtifacts(config: VeluConfig, docsDirForOpenApi?: string): BuildAr
1179
1179
 
1180
1180
  // ── Build ──────────────────────────────────────────────────────────────────────
1181
1181
 
1182
- function build(docsDir: string, outDir: string) {
1183
- const configPath = resolveConfigPath(docsDir);
1184
- const configName = configPath.endsWith(PRIMARY_CONFIG_NAME) ? PRIMARY_CONFIG_NAME : LEGACY_CONFIG_NAME;
1185
- console.log(`📖 Loading ${configName} from: ${docsDir}`);
1186
- const { config, rawConfig, variables } = loadConfig(docsDir);
1182
+ function build(docsDir: string, outDir: string) {
1183
+ const configPath = resolveConfigPath(docsDir);
1184
+ const configName = configPath.endsWith(PRIMARY_CONFIG_NAME) ? PRIMARY_CONFIG_NAME : LEGACY_CONFIG_NAME;
1185
+ console.log(`📖 Loading ${configName} from: ${docsDir}`);
1186
+ const { config, rawConfig, variables } = loadConfig(docsDir);
1187
1187
 
1188
1188
  if (existsSync(outDir)) {
1189
1189
  rmSync(outDir, { recursive: true, force: true });
@@ -1196,39 +1196,39 @@ function build(docsDir: string, outDir: string) {
1196
1196
  console.log("📦 Copied engine files");
1197
1197
 
1198
1198
  // ── 2. Create additional directories ─────────────────────────────────────
1199
- mkdirSync(join(outDir, "content", "docs"), { recursive: true });
1200
- mkdirSync(join(outDir, "public"), { recursive: true });
1201
- const sourceMirrorDir = join(outDir, SOURCE_MIRROR_DIR);
1202
- rebuildSourceMirror(docsDir, sourceMirrorDir, variables);
1199
+ mkdirSync(join(outDir, "content", "docs"), { recursive: true });
1200
+ mkdirSync(join(outDir, "public"), { recursive: true });
1201
+ const sourceMirrorDir = join(outDir, SOURCE_MIRROR_DIR);
1202
+ rebuildSourceMirror(docsDir, sourceMirrorDir, variables);
1203
1203
 
1204
1204
  // ── 3. Copy config into the generated project ────────────────────────────
1205
- const serializedConfig = `${JSON.stringify(rawConfig, null, 2)}\n`;
1206
- writeFileSync(join(outDir, PRIMARY_CONFIG_NAME), serializedConfig, "utf-8");
1207
- writeFileSync(join(outDir, LEGACY_CONFIG_NAME), serializedConfig, "utf-8");
1208
- console.log(`📋 Copied ${configName} as ${PRIMARY_CONFIG_NAME} (and legacy ${LEGACY_CONFIG_NAME})`);
1205
+ const serializedConfig = `${JSON.stringify(rawConfig, null, 2)}\n`;
1206
+ writeFileSync(join(outDir, PRIMARY_CONFIG_NAME), serializedConfig, "utf-8");
1207
+ writeFileSync(join(outDir, LEGACY_CONFIG_NAME), serializedConfig, "utf-8");
1208
+ console.log(`📋 Copied ${configName} as ${PRIMARY_CONFIG_NAME} (and legacy ${LEGACY_CONFIG_NAME})`);
1209
1209
 
1210
1210
  // ── 3b. Copy static assets from docs project into public/ ─────────────────
1211
- copyStaticAssets(docsDir, join(outDir, "public"));
1212
- writeRedirectArtifacts(config, outDir);
1213
- writeProjectConstFile(rawConfig, outDir);
1214
- if ((config.redirects ?? []).length > 0) {
1215
- console.log("↪️ Generated redirect artifacts");
1216
- }
1217
- console.log("🧾 Generated const.json");
1218
- console.log("🖼️ Copied static assets");
1211
+ copyStaticAssets(docsDir, join(outDir, "public"));
1212
+ writeRedirectArtifacts(config, outDir);
1213
+ writeProjectConstFile(rawConfig, outDir);
1214
+ if ((config.redirects ?? []).length > 0) {
1215
+ console.log("↪️ Generated redirect artifacts");
1216
+ }
1217
+ console.log("🧾 Generated const.json");
1218
+ console.log("🖼️ Copied static assets");
1219
1219
 
1220
1220
  // ── 4. Build content + metadata artifacts ────────────────────────────────
1221
1221
  const contentDir = join(outDir, "content", "docs");
1222
1222
  const navLanguages = config.navigation.languages;
1223
1223
  const simpleLanguages = config.languages || [];
1224
1224
 
1225
- function processPage(srcPath: string, destPath: string, slug: string) {
1226
- mkdirSync(dirname(destPath), { recursive: true });
1227
- let content = readFileSync(srcPath, "utf-8");
1228
- content = replaceVariablesInString(content, variables, relative(docsDir, srcPath), true);
1229
- if (!content.startsWith("---")) {
1230
- const titleMatch = content.match(/^#\s+(.+)$/m);
1231
- const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
1225
+ function processPage(srcPath: string, destPath: string, slug: string) {
1226
+ mkdirSync(dirname(destPath), { recursive: true });
1227
+ let content = readFileSync(srcPath, "utf-8");
1228
+ content = replaceVariablesInString(content, variables, relative(docsDir, srcPath), true);
1229
+ if (!content.startsWith("---")) {
1230
+ const titleMatch = content.match(/^#\s+(.+)$/m);
1231
+ const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
1232
1232
  if (titleMatch) {
1233
1233
  content = content.replace(/^#\s+.+$/m, "").trimStart();
1234
1234
  }
@@ -1289,11 +1289,11 @@ function build(docsDir: string, outDir: string) {
1289
1289
  const descriptionLine = description ? `\ndescription: "${description}"` : "";
1290
1290
  const deprecatedLine = mapping.deprecated === true ? `\ndeprecated: true` : "";
1291
1291
  const statusLine = mapping.deprecated === true ? `\nstatus: "deprecated"` : "";
1292
- const versionLine = version ? `\nversion: "${version}"` : "";
1293
- const content = typeof mapping.content === "string"
1294
- ? `${replaceVariablesInString(mapping.content.trim(), variables, `openapi:${mapping.dest}`, true)}\n`
1295
- : "";
1296
- writeFileSync(
1292
+ const versionLine = version ? `\nversion: "${version}"` : "";
1293
+ const content = typeof mapping.content === "string"
1294
+ ? `${replaceVariablesInString(mapping.content.trim(), variables, `openapi:${mapping.dest}`, true)}\n`
1295
+ : "";
1296
+ writeFileSync(
1297
1297
  destPath,
1298
1298
  `---\ntitle: "${title}"${descriptionLine}${deprecatedLine}${statusLine}${versionLine}\nopenapi: "${openapi}"\n---\n${warning}${content}`,
1299
1299
  "utf-8",
@@ -1370,11 +1370,22 @@ function build(docsDir: string, outDir: string) {
1370
1370
  console.log(`📄 Generated ${totalPages} pages + ${totalMeta} navigation meta files`);
1371
1371
 
1372
1372
  // ── 5. Generate theme CSS (dynamic — depends on user config) ─────────────
1373
+ // Resolve fonts config into { heading?, body? } shape
1374
+ const resolvedFonts = (() => {
1375
+ const raw = config.fonts;
1376
+ if (!raw) return undefined;
1377
+ if ('family' in raw && typeof (raw as Record<string, unknown>).family === 'string') {
1378
+ return { heading: raw as { family: string; weight?: number; source?: string; format?: "woff" | "woff2" }, body: raw as { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } };
1379
+ }
1380
+ const obj = raw as { heading?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" }; body?: { family: string; weight?: number; source?: string; format?: "woff" | "woff2" } };
1381
+ return (obj.heading || obj.body) ? obj : undefined;
1382
+ })();
1383
+
1373
1384
  const themeCss = generateThemeCss({
1374
1385
  theme: config.theme,
1375
1386
  colors: config.colors,
1376
1387
  appearance: config.appearance,
1377
- styling: config.styling,
1388
+ fonts: resolvedFonts,
1378
1389
  });
1379
1390
  writeFileSync(join(outDir, "app", "velu-theme.css"), themeCss, "utf-8");
1380
1391
  console.log(`🎨 Generated theme: ${resolveThemeName(config.theme)}`);
package/src/cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolve, join, dirname, delimiter } from "node:path";
2
- import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, cpSync, rmSync, renameSync, readFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, rmSync, readFileSync, statSync } from "node:fs";
3
3
  import { spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
@@ -233,31 +233,6 @@ async function generateProject(docsDir: string): Promise<string> {
233
233
  return outDir;
234
234
  }
235
235
 
236
- function samePath(a: string, b: string): boolean {
237
- return resolve(a).replace(/\\/g, "/").toLowerCase() === resolve(b).replace(/\\/g, "/").toLowerCase();
238
- }
239
-
240
- function prepareRuntimeOutDir(docsOutDir: string): string {
241
- const runtimeOutDir = join(PACKAGE_ROOT, ".velu-out");
242
- if (samePath(docsOutDir, runtimeOutDir)) return runtimeOutDir;
243
-
244
- try {
245
- rmSync(runtimeOutDir, { recursive: true, force: true });
246
- } catch {
247
- // On Windows the directory may be locked by a previous dev-server process.
248
- // Rename it aside so we can proceed; the stale copy is cleaned up later.
249
- const stale = `${runtimeOutDir}-stale-${Date.now()}`;
250
- try {
251
- renameSync(runtimeOutDir, stale);
252
- // Best-effort async cleanup — ignore errors if still locked.
253
- try { rmSync(stale, { recursive: true, force: true }); } catch {}
254
- } catch {
255
- // If even rename fails, try to overwrite in place.
256
- }
257
- }
258
- cpSync(docsOutDir, runtimeOutDir, { recursive: true, force: true });
259
- return runtimeOutDir;
260
- }
261
236
 
262
237
  async function buildStatic(outDir: string, docsDir: string) {
263
238
  await new Promise<void>((res, rej) => {
@@ -467,17 +442,9 @@ function addStaticRouteCompatibility(outDir: string) {
467
442
 
468
443
  async function buildSite(docsDir: string) {
469
444
  const docsOutDir = await generateProject(docsDir);
470
- const runtimeOutDir = prepareRuntimeOutDir(docsOutDir);
471
- await buildStatic(runtimeOutDir, docsDir);
472
- exportMarkdownRoutes(runtimeOutDir);
473
- addStaticRouteCompatibility(runtimeOutDir);
474
-
475
- if (!samePath(docsOutDir, runtimeOutDir)) {
476
- const docsDistDir = join(docsOutDir, "dist");
477
- const runtimeDistDir = join(runtimeOutDir, "dist");
478
- rmSync(docsDistDir, { recursive: true, force: true });
479
- cpSync(runtimeDistDir, docsDistDir, { recursive: true, force: true });
480
- }
445
+ await buildStatic(docsOutDir, docsDir);
446
+ exportMarkdownRoutes(docsOutDir);
447
+ addStaticRouteCompatibility(docsOutDir);
481
448
 
482
449
  const staticOutDir = join(docsOutDir, "dist");
483
450
  console.log(`\n📁 Static site output: ${staticOutDir}`);
@@ -501,8 +468,7 @@ function spawnServer(outDir: string, command: string, port: number, docsDir: str
501
468
 
502
469
  async function run(docsDir: string, port: number) {
503
470
  const docsOutDir = await generateProject(docsDir);
504
- const runtimeOutDir = prepareRuntimeOutDir(docsOutDir);
505
- spawnServer(runtimeOutDir, "dev", port, docsDir);
471
+ spawnServer(docsOutDir, "dev", port, docsDir);
506
472
  }
507
473
 
508
474
  // ── Parse args ───────────────────────────────────────────────────────────────────
@@ -987,14 +987,14 @@ nextjs-portal {
987
987
  display: grid !important;
988
988
  grid-template-columns: 2.25rem minmax(0, 1fr);
989
989
  column-gap: 0.75rem;
990
- align-items: start !important;
990
+ align-items: center !important;
991
991
  }
992
992
 
993
993
  [data-card].velu-card-horizontal > div.not-prose {
994
994
  grid-row: 1 / span 3;
995
995
  margin-bottom: 0;
996
- margin-top: 0.02rem;
997
- align-self: start;
996
+ margin-top: 0;
997
+ align-self: center;
998
998
  justify-self: start;
999
999
  }
1000
1000
 
@@ -1,6 +1,6 @@
1
1
  import type { Metadata } from 'next';
2
2
  import type { ReactNode } from 'react';
3
- import { getAppearance, getBannerConfig, getSeoConfig, getSiteDescription, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
3
+ import { getAppearance, getBannerConfig, getFontsConfig, getSeoConfig, getSiteDescription, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
4
4
  import { Providers } from '@/components/providers';
5
5
  import { VeluAssistant } from '@/components/assistant';
6
6
  import { VeluBanner } from '@/components/banner';
@@ -24,6 +24,7 @@ const seo = getSeoConfig();
24
24
  const favicon = getSiteFavicon();
25
25
  const primaryColor = getSitePrimaryColor();
26
26
  const bannerConfig = getBannerConfig();
27
+ const fontsConfig = getFontsConfig();
27
28
  const generatedDefaultSocialImage = '/og/index.svg';
28
29
  const defaultSocialImage = seo.metatags['og:image'] ?? seo.metatags['twitter:image'] ?? generatedDefaultSocialImage;
29
30
  const absoluteDefaultSocialImage = defaultSocialImage ? toAbsoluteUrl(siteOrigin, defaultSocialImage) : undefined;
@@ -79,6 +80,19 @@ export default function RootLayout({ children }: { children: ReactNode }) {
79
80
  <html lang="en" suppressHydrationWarning>
80
81
  <head>
81
82
  <link rel="sitemap" type="application/xml" href={`${siteOrigin}/sitemap.xml`} />
83
+ {fontsConfig && (() => {
84
+ // Collect Google Fonts families that don't use a custom source
85
+ const families = new Set<string>();
86
+ for (const def of [fontsConfig.heading, fontsConfig.body]) {
87
+ if (def && !def.source) {
88
+ const weight = def.weight ? `:wght@${def.weight}` : ':wght@400;500;600;700';
89
+ families.add(`${def.family.replace(/ /g, '+')}${weight}`);
90
+ }
91
+ }
92
+ if (families.size === 0) return null;
93
+ const url = `https://fonts.googleapis.com/css2?${[...families].map(f => `family=${f}`).join('&')}&display=swap`;
94
+ return <link rel="stylesheet" href={url} />;
95
+ })()}
82
96
  </head>
83
97
  <body className="min-h-screen" suppressHydrationWarning>
84
98
  <Providers theme={theme}>
@@ -173,6 +173,18 @@ interface VeluContextualCustomOption {
173
173
  href: string;
174
174
  }
175
175
 
176
+ interface VeluFontDef {
177
+ family: string;
178
+ weight?: number;
179
+ source?: string;
180
+ format?: 'woff' | 'woff2';
181
+ }
182
+
183
+ export interface VeluResolvedFonts {
184
+ heading?: VeluFontDef;
185
+ body?: VeluFontDef;
186
+ }
187
+
176
188
  interface VeluConfig {
177
189
  name?: string;
178
190
  description?: string;
@@ -188,6 +200,7 @@ interface VeluConfig {
188
200
  library?: string;
189
201
  };
190
202
  appearance?: 'system' | 'light' | 'dark';
203
+ fonts?: VeluFontDef | { heading?: VeluFontDef; body?: VeluFontDef };
191
204
  languages?: string[];
192
205
  openapi?: string | string[] | Record<string, unknown>;
193
206
  asyncapi?: string | string[] | Record<string, unknown>;
@@ -691,6 +704,19 @@ export function getAppearance(): 'system' | 'light' | 'dark' {
691
704
 
692
705
  export type VeluIconLibrary = 'fontawesome' | 'lucide' | 'tabler';
693
706
 
707
+ export function getFontsConfig(): VeluResolvedFonts | null {
708
+ const raw = loadVeluConfig().fonts;
709
+ if (!raw) return null;
710
+ // Single font definition (has 'family') → apply to both heading and body
711
+ if ('family' in raw && typeof raw.family === 'string') {
712
+ return { heading: raw as VeluFontDef, body: raw as VeluFontDef };
713
+ }
714
+ // Separate heading/body
715
+ const obj = raw as { heading?: VeluFontDef; body?: VeluFontDef };
716
+ if (!obj.heading && !obj.body) return null;
717
+ return { heading: obj.heading, body: obj.body };
718
+ }
719
+
694
720
  export function getIconLibrary(): VeluIconLibrary {
695
721
  const raw = loadVeluConfig().icons?.library;
696
722
  if (raw === 'lucide' || raw === 'tabler' || raw === 'fontawesome') return raw;
@@ -152,9 +152,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
152
152
  ...props
153
153
  }: any) => {
154
154
  const hasImage = Boolean(img || image);
155
- const hasIcon = Boolean(icon);
156
- // Mintlify-like default: icon cards are horizontal unless explicitly disabled.
157
- const useHorizontal = typeof horizontal === 'boolean' ? horizontal : (hasIcon && !hasImage);
155
+ const useHorizontal = horizontal === true;
158
156
 
159
157
  return (
160
158
  <Card
@@ -65,7 +65,6 @@ export interface VeluConfig {
65
65
  theme?: string;
66
66
  colors?: { primary?: string; light?: string; dark?: string };
67
67
  appearance?: 'system' | 'light' | 'dark';
68
- styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
69
68
  navigation: {
70
69
  tabs?: VeluTab[];
71
70
  anchors?: VeluAnchor[];
package/src/themes.ts CHANGED
@@ -6,17 +6,23 @@ interface VeluColors {
6
6
  dark?: string;
7
7
  }
8
8
 
9
- interface VeluStyling {
10
- codeblocks?: {
11
- theme?: string | { light: string; dark: string };
12
- };
9
+ interface VeluFontDef {
10
+ family: string;
11
+ weight?: number;
12
+ source?: string;
13
+ format?: "woff" | "woff2";
14
+ }
15
+
16
+ interface VeluFontsConfig {
17
+ heading?: VeluFontDef;
18
+ body?: VeluFontDef;
13
19
  }
14
20
 
15
21
  interface ThemeConfig {
16
22
  theme?: string;
17
23
  colors?: VeluColors;
18
24
  appearance?: "system" | "light" | "dark";
19
- styling?: VeluStyling;
25
+ fonts?: VeluFontsConfig;
20
26
  }
21
27
 
22
28
  const FUMADOCS_THEMES = [
@@ -160,6 +166,46 @@ function generateThemeCss(config: ThemeConfig): string {
160
166
  }
161
167
  lines.push("");
162
168
 
169
+ // Font configuration
170
+ if (config.fonts) {
171
+ const { heading, body } = config.fonts;
172
+ // @font-face declarations for custom sources
173
+ for (const def of [heading, body].filter(Boolean) as VeluFontDef[]) {
174
+ if (def.source) {
175
+ const fmt = def.format || (def.source.endsWith(".woff2") ? "woff2" : "woff");
176
+ lines.push(`@font-face {`);
177
+ lines.push(` font-family: '${def.family}';`);
178
+ lines.push(` src: url('${def.source}') format('${fmt}');`);
179
+ if (def.weight) lines.push(` font-weight: ${def.weight};`);
180
+ lines.push(` font-display: swap;`);
181
+ lines.push(`}`);
182
+ lines.push("");
183
+ }
184
+ }
185
+ // CSS variable overrides
186
+ const vars: string[] = [];
187
+ if (body) {
188
+ vars.push(` --font-fd-sans: '${body.family}', ui-sans-serif, system-ui, sans-serif;`);
189
+ }
190
+ if (heading) {
191
+ vars.push(` --velu-font-heading: '${heading.family}', ui-sans-serif, system-ui, sans-serif;`);
192
+ }
193
+ if (vars.length) {
194
+ lines.push(":root {");
195
+ lines.push(...vars);
196
+ lines.push("}");
197
+ lines.push("");
198
+ }
199
+ // Heading font-family rule
200
+ if (heading) {
201
+ lines.push("h1, h2, h3, h4, h5, h6 {");
202
+ lines.push(` font-family: var(--velu-font-heading);`);
203
+ if (heading.weight) lines.push(` font-weight: ${heading.weight};`);
204
+ lines.push("}");
205
+ lines.push("");
206
+ }
207
+ }
208
+
163
209
  return lines.join("\n");
164
210
  }
165
211
 
@@ -169,4 +215,4 @@ function getThemeNames(): string[] {
169
215
 
170
216
  const THEMES = [...FUMADOCS_THEMES];
171
217
 
172
- export { generateThemeCss, getThemeNames, resolveThemeName, THEMES, ThemeConfig, VeluColors, VeluStyling };
218
+ export { generateThemeCss, getThemeNames, resolveThemeName, THEMES, ThemeConfig, VeluColors };
package/src/validate.ts CHANGED
@@ -113,6 +113,13 @@ interface VeluVersionNav {
113
113
 
114
114
  type VeluOpenApiSource = string | string[] | Record<string, unknown>;
115
115
 
116
+ interface VeluFontDefinition {
117
+ family: string;
118
+ weight?: number;
119
+ source?: string;
120
+ format?: "woff" | "woff2";
121
+ }
122
+
116
123
  interface VeluConfig {
117
124
  $schema?: string;
118
125
  variables?: Record<string, string>;
@@ -123,7 +130,8 @@ interface VeluConfig {
123
130
  theme?: string;
124
131
  colors?: { primary?: string; light?: string; dark?: string };
125
132
  appearance?: "system" | "light" | "dark";
126
- styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
133
+
134
+ fonts?: VeluFontDefinition | { heading?: VeluFontDefinition; body?: VeluFontDefinition };
127
135
  openapi?: VeluOpenApiSource;
128
136
  asyncapi?: VeluOpenApiSource;
129
137
  api?: {