kitfly 0.2.1 → 0.2.4

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +38 -21
  3. package/VERSION +1 -1
  4. package/dist/_raw/content/guide/branding.md +146 -0
  5. package/dist/_raw/content/guide/data-driven-content.md +204 -0
  6. package/dist/_raw/content/reference/configuration.md +145 -7
  7. package/dist/_raw/content/reference/environment-variables.md +26 -1
  8. package/dist/_raw/content/reference/gantt-widget.md +468 -0
  9. package/dist/_raw/content/reference/glossary.md +25 -1
  10. package/dist/_raw/content/reference/key-concepts.md +30 -2
  11. package/dist/_raw/content/reference/plugins.md +170 -1
  12. package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
  13. package/dist/content/deployment/preflight.html +11 -8
  14. package/dist/content/deployment/recipes/aws-s3.html +11 -8
  15. package/dist/content/deployment/recipes/cloudflare-pages.html +11 -8
  16. package/dist/content/deployment/recipes/cloudflare-r2.html +11 -8
  17. package/dist/content/deployment/recipes/fly-io.html +11 -8
  18. package/dist/content/deployment/recipes/github-pages.html +11 -8
  19. package/dist/content/deployment/recipes/netlify.html +11 -8
  20. package/dist/content/deployment/recipes/vercel.html +11 -8
  21. package/dist/content/deployment/secrets-and-env-vars.html +11 -8
  22. package/dist/content/deployment.html +11 -8
  23. package/dist/content/guide/approaches.html +11 -8
  24. package/dist/content/guide/branding.html +509 -0
  25. package/dist/content/guide/data-driven-content.html +542 -0
  26. package/dist/content/guide/features.html +11 -8
  27. package/dist/content/guide/getting-started.html +11 -8
  28. package/dist/content/guide/kitfly-overview.html +11 -8
  29. package/dist/content/reference/configuration.html +136 -11
  30. package/dist/content/reference/design-catalog.html +11 -8
  31. package/dist/content/reference/environment-variables.html +51 -10
  32. package/dist/content/reference/gantt-widget.html +899 -0
  33. package/dist/content/reference/glossary.html +25 -10
  34. package/dist/content/reference/key-concepts.html +34 -11
  35. package/dist/content/reference/plugins.html +261 -10
  36. package/dist/content/reference/slides-authoring-guidelines.html +11 -8
  37. package/dist/content/reference/structure.html +11 -8
  38. package/dist/content/reference.html +11 -8
  39. package/dist/content/templates/crucible.html +11 -8
  40. package/dist/content/templates/handbook.html +11 -8
  41. package/dist/content/templates/minimal.html +11 -8
  42. package/dist/content/templates/overview.html +11 -8
  43. package/dist/content/templates/pipeline.html +11 -8
  44. package/dist/content/templates/productbook.html +11 -8
  45. package/dist/content/templates/runbook.html +11 -8
  46. package/dist/content/templates/servicebook.html +11 -8
  47. package/dist/content-index.json +37 -2
  48. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +11 -8
  49. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +11 -8
  50. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +11 -8
  51. package/dist/docs/decisions/ADR-0004-bun-runtime.html +11 -8
  52. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +11 -8
  53. package/dist/docs/decisions/ADR-0006-data-driven-content.html +751 -0
  54. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +11 -8
  55. package/dist/docs/decisions/DDR-0002-theme-system.html +11 -8
  56. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +11 -8
  57. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +11 -8
  58. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +11 -8
  59. package/dist/docs/userguide/cli/build.html +11 -8
  60. package/dist/docs/userguide/cli/bundle.html +11 -8
  61. package/dist/docs/userguide/cli/dev.html +11 -8
  62. package/dist/docs/userguide/cli/init.html +11 -8
  63. package/dist/docs/userguide/cli/servers.html +11 -8
  64. package/dist/docs/userguide/cli/stop.html +11 -8
  65. package/dist/docs/userguide/cli/update.html +11 -8
  66. package/dist/docs/userguide/cli/version.html +11 -8
  67. package/dist/docs/userguide/cli.html +11 -8
  68. package/dist/docs/userguide/sharing.html +11 -8
  69. package/dist/index.html +11 -8
  70. package/dist/llms.txt +3 -3
  71. package/dist/provenance.json +4 -5
  72. package/dist/reports/license-inventory.csv +199 -0
  73. package/dist/schemas/plugin-registry.schema.html +11 -8
  74. package/dist/schemas/plugin-schemas-notes.html +11 -8
  75. package/dist/schemas/plugin.schema.html +11 -8
  76. package/dist/schemas/plugins.schema.html +11 -8
  77. package/dist/schemas/v0/common.schema.html +15 -12
  78. package/dist/schemas/v0/plugin-registry.schema.html +14 -11
  79. package/dist/schemas/v0/plugin.schema.html +14 -11
  80. package/dist/schemas/v0/plugins.schema.html +14 -11
  81. package/dist/schemas/v0/site.schema.html +68 -9
  82. package/dist/schemas/v0/theme.schema.html +22 -19
  83. package/dist/schemas.html +11 -8
  84. package/dist/styles.css +39 -4
  85. package/package.json +1 -1
  86. package/plugins-dist/latex-runtime.js +140 -0
  87. package/plugins-dist/latex.js +178 -0
  88. package/plugins-dist/planning-visuals.css +261 -0
  89. package/plugins-dist/planning-visuals.js +669 -0
  90. package/plugins-dist/slides-charts-lite-runtime.js +179 -0
  91. package/plugins-dist/slides-charts-lite.js +198 -0
  92. package/registry/plugins.yaml +40 -1
  93. package/schemas/v0/site.schema.json +56 -0
  94. package/scripts/build-all.ts +5 -0
  95. package/scripts/build.ts +264 -80
  96. package/scripts/bundle.ts +188 -17
  97. package/scripts/dev.ts +294 -171
  98. package/scripts/embed-docs.ts +119 -0
  99. package/src/__tests__/brief.test.ts +151 -0
  100. package/src/__tests__/build.test.ts +293 -1
  101. package/src/__tests__/bundle.test.ts +195 -0
  102. package/src/__tests__/docs.test.ts +117 -0
  103. package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
  104. package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
  105. package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
  106. package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
  107. package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
  108. package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
  109. package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
  110. package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
  111. package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
  112. package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
  113. package/src/__tests__/init.test.ts +51 -2
  114. package/src/__tests__/latex-runtime.bun.test.ts +35 -0
  115. package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
  116. package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
  117. package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
  118. package/src/__tests__/shared.test.ts +719 -1
  119. package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
  120. package/src/cli.ts +124 -22
  121. package/src/commands/docs.ts +71 -0
  122. package/src/commands/init.ts +1 -1
  123. package/src/generated/embedded-docs.ts +2384 -0
  124. package/src/server-registry.ts +50 -10
  125. package/src/shared.ts +1174 -43
  126. package/src/site/styles.css +39 -4
  127. package/src/site/template.html +5 -2
  128. package/src/templates/brief.ts +486 -0
  129. package/src/templates/deck.ts +59 -0
  130. package/src/templates/driver.ts +46 -13
  131. package/src/templates/handbook.ts +32 -0
  132. package/src/templates/runbook.ts +32 -0
package/scripts/bundle.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * Options:
7
7
  * -o, --out <dir> Output directory [env: KITFLY_BUNDLE_OUT] [default: bundles]
8
8
  * -n, --name <file> Bundle filename [env: KITFLY_BUNDLE_NAME] [default: bundle.html]
9
+ * --profile <name> Active content profile [env: KITFLY_PROFILE]
9
10
  * --raw Include raw markdown in bundle [env: KITFLY_BUNDLE_RAW] [default: true]
10
11
  * --no-raw Don't include raw markdown
11
12
  * --help Show help message
@@ -21,29 +22,39 @@ import { ENGINE_ASSETS_DIR } from "../src/engine.ts";
21
22
  import { loadPluginInjections } from "../src/plugin-loader.ts";
22
23
  import {
23
24
  buildBundleFooter,
25
+ buildLogoImgHtml,
24
26
  buildSectionNav,
25
27
  // Navigation/template building
26
- buildSlideNav,
27
- // Types
28
+ buildSlideNavHierarchical,
28
29
  type ContentFile,
29
30
  collectFiles,
31
+ // Types
32
+ collectPlanningVisualsContainmentWarnings,
30
33
  collectSlides,
31
34
  envBool,
32
35
  // Config helpers
33
36
  envString,
34
37
  // Formatting
35
38
  escapeHtml,
39
+ filterByProfile,
40
+ filterUnknownPlanningVisualsTypeDiagnostics,
36
41
  filterUnknownSlidesVisualsTypeDiagnostics,
37
42
  // YAML/Config parsing
43
+ loadDataBindings,
38
44
  loadSiteConfig,
45
+ mergeFrontmatterWithBody,
39
46
  // Markdown utilities
47
+ pagePathForData,
40
48
  parseFrontmatter,
41
49
  parseYaml,
50
+ resolveBindings,
42
51
  resolveSiteVersion,
43
52
  resolveStylesPath,
53
+ runPrebuildHooks,
44
54
  type SiteConfig,
45
55
  slugify,
46
56
  validatePath,
57
+ validatePlanningVisualsFences,
47
58
  validateSlidesVisualsFences,
48
59
  } from "../src/shared.ts";
49
60
  import { generateThemeCSS, getPrismUrls, loadTheme } from "../src/theme.ts";
@@ -55,6 +66,41 @@ const DEFAULT_NAME = "bundle.html";
55
66
  let ROOT = process.cwd();
56
67
  let OUT_DIR = DEFAULT_OUT;
57
68
  let BUNDLE_NAME = DEFAULT_NAME;
69
+ let ACTIVE_PROFILE: string | undefined;
70
+
71
+ async function applyDataBindingsToMarkdown(
72
+ rawMarkdown: string,
73
+ filePath: string,
74
+ config: SiteConfig,
75
+ ): Promise<{ frontmatter: Record<string, unknown>; body: string }> {
76
+ const parsed = parseFrontmatter(rawMarkdown);
77
+ const dataRef = typeof parsed.frontmatter.data === "string" ? parsed.frontmatter.data.trim() : "";
78
+ if (!dataRef) return parsed;
79
+
80
+ const pagePath = pagePathForData(ROOT, config.docroot, filePath);
81
+ const bindings = await loadDataBindings(dataRef, pagePath, ROOT, config.docroot, config.dataroot);
82
+ return {
83
+ frontmatter: parsed.frontmatter,
84
+ body: resolveBindings(parsed.body, bindings, pagePath),
85
+ };
86
+ }
87
+
88
+ async function applyDataBindingsForSlides(
89
+ rawMarkdown: string,
90
+ filePath: string,
91
+ config: SiteConfig,
92
+ ): Promise<string> {
93
+ const resolved = await applyDataBindingsToMarkdown(rawMarkdown, filePath, config);
94
+ return mergeFrontmatterWithBody(rawMarkdown, resolved.body);
95
+ }
96
+
97
+ function normalizeMsysPath(p: string): string {
98
+ // Git Bash / MSYS-style paths: /c/Users/... -> C:\Users\...
99
+ if (process.platform !== "win32") return p;
100
+ const m = p.match(/^\/([a-zA-Z])\/(.*)$/);
101
+ if (!m) return p;
102
+ return `${m[1].toUpperCase()}:\\${m[2].replaceAll("/", "\\")}`;
103
+ }
58
104
 
59
105
  // ---------------------------------------------------------------------------
60
106
  // CLI argument parsing
@@ -65,6 +111,7 @@ interface ParsedArgs {
65
111
  out?: string;
66
112
  name?: string;
67
113
  raw?: boolean;
114
+ profile?: string;
68
115
  }
69
116
 
70
117
  function parseArgs(argv: string[]): ParsedArgs {
@@ -79,6 +126,9 @@ function parseArgs(argv: string[]): ParsedArgs {
79
126
  } else if ((arg === "--name" || arg === "-n") && next && !next.startsWith("-")) {
80
127
  result.name = next;
81
128
  i++;
129
+ } else if (arg === "--profile" && next && !next.startsWith("-")) {
130
+ result.profile = next;
131
+ i++;
82
132
  } else if (arg === "--raw") {
83
133
  result.raw = true;
84
134
  } else if (arg === "--no-raw") {
@@ -95,6 +145,7 @@ function getConfig(): {
95
145
  out: string;
96
146
  name: string;
97
147
  raw: boolean;
148
+ profile?: string;
98
149
  } {
99
150
  const args = parseArgs(process.argv.slice(2));
100
151
  const legacyOut = envString("KITFLY_BUILD_OUT", DEFAULT_OUT);
@@ -106,6 +157,7 @@ function getConfig(): {
106
157
  out,
107
158
  name: args.name ?? envString("KITFLY_BUNDLE_NAME", DEFAULT_NAME),
108
159
  raw,
160
+ profile: args.profile ?? process.env.KITFLY_PROFILE,
109
161
  };
110
162
  }
111
163
 
@@ -305,22 +357,35 @@ function buildBundleNav(files: ContentFile[], config: SiteConfig): string {
305
357
  return html;
306
358
  }
307
359
 
308
- async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig): Promise<string> {
309
- const slides = await collectSlides(files);
310
- let validateFences = false;
360
+ async function getFenceValidationFlags(root: string): Promise<{
361
+ slidesVisuals: boolean;
362
+ planningVisuals: boolean;
363
+ }> {
311
364
  try {
312
- const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
365
+ const raw = await readFile(join(root, "kitfly.plugins.yaml"), "utf-8");
313
366
  const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
314
367
  const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
315
- validateFences = enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@"));
368
+ return {
369
+ slidesVisuals: enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
370
+ planningVisuals: enabled.some(
371
+ (p) => typeof p === "string" && p.startsWith("planning-visuals@"),
372
+ ),
373
+ };
316
374
  } catch {
317
- // no config, skip
375
+ return { slidesVisuals: false, planningVisuals: false };
318
376
  }
377
+ }
378
+
379
+ async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig): Promise<string> {
380
+ const slides = await collectSlides(files, {
381
+ markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
382
+ });
383
+ const fenceValidation = await getFenceValidationFlags(ROOT);
319
384
  const renderedSlides = await Promise.all(
320
385
  slides.map(async (slide, i) => {
321
386
  let inner = "";
322
387
  if (slide.kind === "markdown") {
323
- if (validateFences) {
388
+ if (fenceValidation.slidesVisuals) {
324
389
  const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
325
390
  validateSlidesVisualsFences(slide.body),
326
391
  );
@@ -332,6 +397,22 @@ async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig
332
397
  throw new Error(`slides-visuals fence contract violations:\n${msg}`);
333
398
  }
334
399
  }
400
+ if (fenceValidation.planningVisuals) {
401
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
402
+ validatePlanningVisualsFences(slide.body),
403
+ );
404
+ if (diagnostics.length) {
405
+ const msg = diagnostics
406
+ .slice(0, 12)
407
+ .map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
408
+ .join("\n");
409
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
410
+ }
411
+ const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
412
+ for (const warning of warnings.slice(0, 12)) {
413
+ console.warn(` ⚠ ${slide.sourcePath}:${warning.line} ${warning.message}`);
414
+ }
415
+ }
335
416
  inner = marked.parse(slide.body) as string;
336
417
  } else if (slide.kind === "yaml") {
337
418
  inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
@@ -376,18 +457,26 @@ function buildBundleSidebarHeader(
376
457
  config: SiteConfig,
377
458
  version: string | undefined,
378
459
  brandLogo: string,
460
+ brandLogoDark?: string,
379
461
  ): string {
380
462
  const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
381
463
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
382
464
  const productHref = config.home ? "#home" : "#";
383
465
  const versionLabel = version ? `v${version}` : "unversioned";
384
466
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
467
+ const brandLogoHtml = buildLogoImgHtml({
468
+ logo: brandLogo,
469
+ logoDark: brandLogoDark,
470
+ alt: config.brand.name,
471
+ className: "logo-img",
472
+ onerrorFallback: true,
473
+ });
385
474
 
386
475
  return `
387
476
  <div class="sidebar-header">
388
477
  <div class="logo ${logoClass}">
389
478
  <a href="${config.brand.url}" class="logo-icon" data-initial="${brandInitial}"${brandTarget}>
390
- <img src="${brandLogo}" alt="${config.brand.name}" class="logo-img" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')">
479
+ ${brandLogoHtml}
391
480
  </a>
392
481
  <span class="logo-text">
393
482
  <a href="${config.brand.url}" class="brand"${brandTarget}>${config.brand.name}</a>
@@ -427,6 +516,18 @@ async function inlineBrandAsset(assetPath: string): Promise<string> {
427
516
  /* continue */
428
517
  }
429
518
  }
519
+
520
+ // Support any safe site-root-relative path (e.g., logos/footer.png).
521
+ const siteRootPath = validatePath(ROOT, ".", clean, false);
522
+ if (siteRootPath) {
523
+ try {
524
+ await stat(siteRootPath);
525
+ const uri = await fileToDataUri(siteRootPath);
526
+ if (uri) return uri;
527
+ } catch {
528
+ /* continue */
529
+ }
530
+ }
430
531
  return assetPath;
431
532
  }
432
533
 
@@ -466,12 +567,26 @@ async function bundle() {
466
567
 
467
568
  const config = await loadSiteConfig(ROOT, "Documentation");
468
569
  console.log(` ✓ Loaded config: "${config.title}" (${config.sections.length} sections)`);
570
+ if (config.prebuild?.length) {
571
+ await runPrebuildHooks(
572
+ config.prebuild,
573
+ ROOT,
574
+ "bundle",
575
+ ACTIVE_PROFILE,
576
+ config.dataroot || "data",
577
+ );
578
+ console.log(` ✓ prebuild hooks (${config.prebuild.length})`);
579
+ }
469
580
 
470
581
  const theme = await loadTheme(ROOT);
471
582
  console.log(` ✓ Loaded theme: "${theme.name || "default"}"`);
472
583
  const prismUrls = getPrismUrls(theme);
473
584
 
474
- const files = await collectFiles(ROOT, config);
585
+ const files = await filterByProfile(
586
+ await collectFiles(ROOT, config),
587
+ ACTIVE_PROFILE,
588
+ config.profiles,
589
+ );
475
590
  if (files.length === 0) {
476
591
  console.error("No content files found. Cannot create bundle.");
477
592
  process.exit(1);
@@ -514,9 +629,10 @@ async function bundle() {
514
629
  section: slide.section,
515
630
  });
516
631
  }
517
- navHtml = buildSlideNav(slides, config, "slide-1");
632
+ navHtml = buildSlideNavHierarchical(slides, config, "slide-1");
518
633
  contentHtml = await buildSlidesBundleContent(files, config);
519
634
  } else {
635
+ const fenceValidation = await getFenceValidationFlags(ROOT);
520
636
  // Build navigation and content sections
521
637
  const sections: Map<string, { id: string; title: string; html: string }[]> = new Map();
522
638
 
@@ -527,7 +643,27 @@ async function bundle() {
527
643
  try {
528
644
  await stat(homePath);
529
645
  const content = await readFile(homePath, "utf-8");
530
- const { frontmatter, body } = parseFrontmatter(content);
646
+ const { frontmatter, body } = await applyDataBindingsToMarkdown(
647
+ content,
648
+ homePath,
649
+ config,
650
+ );
651
+ if (fenceValidation.planningVisuals) {
652
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
653
+ validatePlanningVisualsFences(body),
654
+ );
655
+ if (diagnostics.length) {
656
+ const msg = diagnostics
657
+ .slice(0, 12)
658
+ .map((d) => ` - ${homePath}:${d.line} ${d.message}`)
659
+ .join("\n");
660
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
661
+ }
662
+ const warnings = collectPlanningVisualsContainmentWarnings(body);
663
+ for (const warning of warnings.slice(0, 12)) {
664
+ console.warn(` ⚠ ${homePath}:${warning.line} ${warning.message}`);
665
+ }
666
+ }
531
667
  const title = (frontmatter.title as string) || "Home";
532
668
  let htmlContent = marked.parse(body) as string;
533
669
  htmlContent = await inlineLocalImages(htmlContent, config);
@@ -558,13 +694,29 @@ async function bundle() {
558
694
  }
559
695
  htmlContent = `<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
560
696
  } else {
561
- const { frontmatter, body } = parseFrontmatter(content);
697
+ const { frontmatter, body } = await applyDataBindingsToMarkdown(content, file.path, config);
562
698
  if (frontmatter.title) {
563
699
  title = frontmatter.title as string;
564
700
  }
565
701
  if (frontmatter.description) {
566
702
  description = frontmatter.description as string;
567
703
  }
704
+ if (fenceValidation.planningVisuals) {
705
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
706
+ validatePlanningVisualsFences(body),
707
+ );
708
+ if (diagnostics.length) {
709
+ const msg = diagnostics
710
+ .slice(0, 12)
711
+ .map((d) => ` - ${file.path}:${d.line} ${d.message}`)
712
+ .join("\n");
713
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
714
+ }
715
+ const warnings = collectPlanningVisualsContainmentWarnings(body);
716
+ for (const warning of warnings.slice(0, 12)) {
717
+ console.warn(` ⚠ ${file.path}:${warning.line} ${warning.message}`);
718
+ }
719
+ }
568
720
  htmlContent = marked.parse(body) as string;
569
721
 
570
722
  // Collect raw markdown for AI accessibility
@@ -617,7 +769,19 @@ async function bundle() {
617
769
 
618
770
  // Inline brand assets for self-contained bundle
619
771
  const brandLogo = await inlineBrandAsset(config.brand.logo || "assets/brand/logo.png");
772
+ const brandLogoDark =
773
+ typeof config.brand.logoDark === "string"
774
+ ? await inlineBrandAsset(config.brand.logoDark)
775
+ : undefined;
620
776
  const brandFavicon = await inlineBrandAsset(config.brand.favicon || "assets/brand/favicon.png");
777
+ const footerLogo =
778
+ typeof config.footer?.logo === "string"
779
+ ? await inlineBrandAsset(config.footer.logo)
780
+ : undefined;
781
+ const footerLogoDark =
782
+ typeof config.footer?.logoDark === "string"
783
+ ? await inlineBrandAsset(config.footer.logoDark)
784
+ : undefined;
621
785
 
622
786
  // Build the complete HTML document
623
787
  const html = `<!DOCTYPE html>
@@ -672,7 +836,7 @@ ${assets.prismCssDark}
672
836
  <body class="${config.mode === "slides" ? "mode-slides" : "mode-docs"}">
673
837
  <div class="layout">
674
838
  <nav class="sidebar">
675
- ${buildBundleSidebarHeader(config, version, brandLogo)}
839
+ ${buildBundleSidebarHeader(config, version, brandLogo, brandLogoDark)}
676
840
  <div class="sidebar-nav">
677
841
  ${navHtml}
678
842
  </div>
@@ -683,7 +847,7 @@ ${buildBundleSidebarHeader(config, version, brandLogo)}
683
847
  </article>
684
848
  </main>
685
849
  </div>
686
- ${buildBundleFooter(version, config)}
850
+ ${buildBundleFooter(version, config, footerLogo, footerLogoDark)}
687
851
  <script>
688
852
  ${assets.prismCore}
689
853
  </script>
@@ -749,6 +913,9 @@ ${assets.mermaid}
749
913
  if (window.reinitMermaid) {
750
914
  window.reinitMermaid();
751
915
  }
916
+ if (window.reinitCharts) {
917
+ window.reinitCharts();
918
+ }
752
919
  }
753
920
 
754
921
  // Slides mode hash routing
@@ -864,7 +1031,7 @@ ${JSON.stringify(
864
1031
  </html>`;
865
1032
 
866
1033
  // Write the bundle
867
- const outDir = join(ROOT, OUT_DIR);
1034
+ const outDir = resolve(ROOT, normalizeMsysPath(OUT_DIR));
868
1035
  await mkdir(outDir, { recursive: true });
869
1036
  const bundlePath = join(outDir, BUNDLE_NAME);
870
1037
  await writeFile(bundlePath, html);
@@ -881,6 +1048,7 @@ export interface BundleOptions {
881
1048
  out?: string;
882
1049
  name?: string;
883
1050
  raw?: boolean; // Include raw markdown in bundle (default: true)
1051
+ profile?: string;
884
1052
  }
885
1053
 
886
1054
  let INCLUDE_RAW = true;
@@ -910,6 +1078,7 @@ export async function bundleSite(options: BundleOptions = {}) {
910
1078
  if (options.raw === false) {
911
1079
  INCLUDE_RAW = false;
912
1080
  }
1081
+ ACTIVE_PROFILE = options.profile;
913
1082
  await bundle();
914
1083
  }
915
1084
 
@@ -922,6 +1091,7 @@ Usage: bun run bundle [folder] [options]
922
1091
  Options:
923
1092
  -o, --out <dir> Output directory [env: KITFLY_BUNDLE_OUT] [default: ${DEFAULT_OUT}]
924
1093
  -n, --name <file> Bundle filename [env: KITFLY_BUNDLE_NAME] [default: ${DEFAULT_NAME}]
1094
+ --profile <name> Active content profile [env: KITFLY_PROFILE]
925
1095
  --raw Include raw markdown in bundle [env: KITFLY_BUNDLE_RAW] [default: true]
926
1096
  --no-raw Don't include raw markdown
927
1097
  --help Show this help message
@@ -943,5 +1113,6 @@ Examples:
943
1113
  out: cfg.out,
944
1114
  name: cfg.name,
945
1115
  raw: cfg.raw,
1116
+ profile: cfg.profile,
946
1117
  }).catch(console.error);
947
1118
  }