kitfly 0.1.2 → 0.2.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +63 -16
  3. package/VERSION +1 -1
  4. package/dist/_raw/content/deployment/preflight.md +134 -0
  5. package/dist/_raw/content/deployment/recipes/aws-s3.md +128 -0
  6. package/dist/_raw/content/deployment/recipes/cloudflare-pages.md +73 -0
  7. package/dist/_raw/content/deployment/recipes/cloudflare-r2.md +156 -0
  8. package/dist/_raw/content/deployment/recipes/fly-io.md +57 -0
  9. package/dist/_raw/content/deployment/recipes/github-pages.md +112 -0
  10. package/dist/_raw/content/deployment/recipes/netlify.md +99 -0
  11. package/dist/_raw/content/deployment/recipes/vercel.md +88 -0
  12. package/dist/_raw/content/deployment/secrets-and-env-vars.md +75 -0
  13. package/dist/_raw/content/deployment.md +128 -0
  14. package/dist/_raw/content/guide/approaches.md +182 -0
  15. package/dist/_raw/content/guide/features.md +121 -0
  16. package/dist/_raw/content/guide/getting-started.md +112 -0
  17. package/dist/_raw/content/guide/kitfly-overview.md +209 -0
  18. package/dist/_raw/content/reference/configuration.md +259 -0
  19. package/dist/_raw/content/reference/design-catalog.md +167 -0
  20. package/dist/_raw/content/reference/environment-variables.md +66 -0
  21. package/dist/_raw/content/reference/glossary.md +92 -0
  22. package/dist/_raw/content/reference/key-concepts.md +118 -0
  23. package/dist/_raw/content/reference/plugins.md +220 -0
  24. package/dist/_raw/content/reference/structure.md +166 -0
  25. package/dist/_raw/content/reference.md +19 -0
  26. package/dist/_raw/content/templates/crucible.md +192 -0
  27. package/dist/_raw/content/templates/handbook.md +83 -0
  28. package/dist/_raw/content/templates/minimal.md +138 -0
  29. package/dist/_raw/content/templates/overview.md +187 -0
  30. package/dist/_raw/content/templates/pipeline.md +151 -0
  31. package/dist/_raw/content/templates/productbook.md +187 -0
  32. package/dist/_raw/content/templates/runbook.md +193 -0
  33. package/dist/_raw/content/templates/servicebook.md +163 -0
  34. package/dist/_raw/docs/decisions/ADR-0001-minimalist-site-code.md +118 -0
  35. package/dist/_raw/docs/decisions/ADR-0002-ai-accessibility.md +153 -0
  36. package/dist/_raw/docs/decisions/ADR-0003-single-file-bundle.md +93 -0
  37. package/dist/_raw/docs/decisions/ADR-0004-bun-runtime.md +98 -0
  38. package/dist/_raw/docs/decisions/ADR-0005-plugin-contract-and-distribution.md +110 -0
  39. package/dist/_raw/docs/decisions/DDR-0001-viewport-locked-layout.md +111 -0
  40. package/dist/_raw/docs/decisions/DDR-0002-theme-system.md +131 -0
  41. package/dist/_raw/docs/decisions/DDR-0003-bounded-logo-slot.md +106 -0
  42. package/dist/_raw/docs/decisions/DDR-0004-slides-rendering-model.md +113 -0
  43. package/dist/_raw/docs/decisions/DDR-0005-deterministic-layout-boundary.md +107 -0
  44. package/dist/_raw/docs/userguide/cli/build.md +85 -0
  45. package/dist/_raw/docs/userguide/cli/bundle.md +81 -0
  46. package/dist/_raw/docs/userguide/cli/dev.md +92 -0
  47. package/dist/_raw/docs/userguide/cli/init.md +116 -0
  48. package/dist/_raw/docs/userguide/cli/servers.md +69 -0
  49. package/dist/_raw/docs/userguide/cli/stop.md +76 -0
  50. package/dist/_raw/docs/userguide/cli/update.md +78 -0
  51. package/dist/_raw/docs/userguide/cli/version.md +65 -0
  52. package/dist/_raw/docs/userguide/cli.md +34 -0
  53. package/dist/_raw/docs/userguide/sharing.md +94 -0
  54. package/dist/_raw/schemas/plugin-schemas-notes.md +71 -0
  55. package/dist/_raw/schemas.md +42 -0
  56. package/dist/assets/brand/kitfly-favicon-32.png +0 -0
  57. package/dist/assets/brand/kitfly-icon-64.png +0 -0
  58. package/dist/assets/brand/kitfly-logo-128.png +0 -0
  59. package/dist/assets/brand/kitfly-logo-512.png +0 -0
  60. package/dist/assets/brand/kitfly-logo.svg +12132 -0
  61. package/dist/assets/brand/kitfly-neon-128.png +0 -0
  62. package/dist/assets/brand/kitfly-neon-192.png +0 -0
  63. package/dist/assets/brand/kitfly-neon-256.png +0 -0
  64. package/dist/assets/brand/kitfly-neon.png +0 -0
  65. package/dist/assets/brand/palette.md +75 -0
  66. package/dist/content/deployment/index.html +11 -0
  67. package/dist/content/deployment/preflight.html +418 -0
  68. package/dist/content/deployment/recipes/aws-s3.html +421 -0
  69. package/dist/content/deployment/recipes/cloudflare-pages.html +372 -0
  70. package/dist/content/deployment/recipes/cloudflare-r2.html +443 -0
  71. package/dist/content/deployment/recipes/fly-io.html +356 -0
  72. package/dist/content/deployment/recipes/github-pages.html +414 -0
  73. package/dist/content/deployment/recipes/index.html +11 -0
  74. package/dist/content/deployment/recipes/netlify.html +394 -0
  75. package/dist/content/deployment/recipes/vercel.html +382 -0
  76. package/dist/content/deployment/secrets-and-env-vars.html +380 -0
  77. package/dist/content/deployment.html +426 -0
  78. package/dist/content/guide/approaches.html +501 -0
  79. package/dist/content/guide/features.html +436 -0
  80. package/dist/content/guide/getting-started.html +403 -0
  81. package/dist/content/guide/index.html +11 -0
  82. package/dist/content/guide/kitfly-overview.html +544 -0
  83. package/dist/content/index.html +11 -0
  84. package/dist/content/reference/configuration.html +580 -0
  85. package/dist/content/reference/design-catalog.html +449 -0
  86. package/dist/content/reference/environment-variables.html +367 -0
  87. package/dist/content/reference/glossary.html +368 -0
  88. package/dist/content/reference/index.html +11 -0
  89. package/dist/content/reference/key-concepts.html +399 -0
  90. package/dist/content/reference/plugins.html +491 -0
  91. package/dist/content/reference/structure.html +463 -0
  92. package/dist/content/reference.html +334 -0
  93. package/dist/content/templates/crucible.html +546 -0
  94. package/dist/content/templates/handbook.html +405 -0
  95. package/dist/content/templates/index.html +11 -0
  96. package/dist/content/templates/minimal.html +447 -0
  97. package/dist/content/templates/overview.html +558 -0
  98. package/dist/content/templates/pipeline.html +494 -0
  99. package/dist/content/templates/productbook.html +540 -0
  100. package/dist/content/templates/runbook.html +543 -0
  101. package/dist/content/templates/servicebook.html +523 -0
  102. package/dist/content-index.json +540 -0
  103. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +491 -0
  104. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +434 -0
  105. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +412 -0
  106. package/dist/docs/decisions/ADR-0004-bun-runtime.html +409 -0
  107. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +402 -0
  108. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +459 -0
  109. package/dist/docs/decisions/DDR-0002-theme-system.html +452 -0
  110. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +423 -0
  111. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +399 -0
  112. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +422 -0
  113. package/dist/docs/decisions/index.html +11 -0
  114. package/dist/docs/userguide/cli/build.html +408 -0
  115. package/dist/docs/userguide/cli/bundle.html +419 -0
  116. package/dist/docs/userguide/cli/dev.html +428 -0
  117. package/dist/docs/userguide/cli/index.html +11 -0
  118. package/dist/docs/userguide/cli/init.html +436 -0
  119. package/dist/docs/userguide/cli/servers.html +393 -0
  120. package/dist/docs/userguide/cli/stop.html +408 -0
  121. package/dist/docs/userguide/cli/update.html +406 -0
  122. package/dist/docs/userguide/cli/version.html +406 -0
  123. package/dist/docs/userguide/cli.html +386 -0
  124. package/dist/docs/userguide/index.html +11 -0
  125. package/dist/docs/userguide/sharing.html +465 -0
  126. package/dist/index.html +387 -0
  127. package/dist/llms.txt +18 -0
  128. package/dist/provenance.json +7 -0
  129. package/dist/schemas/index.html +11 -0
  130. package/dist/schemas/plugin-registry.schema.html +327 -0
  131. package/dist/schemas/plugin-schemas-notes.html +364 -0
  132. package/dist/schemas/plugin.schema.html +327 -0
  133. package/dist/schemas/plugins.schema.html +327 -0
  134. package/dist/schemas/v0/common.schema.html +386 -0
  135. package/dist/schemas/v0/index.html +11 -0
  136. package/dist/schemas/v0/plugin-registry.schema.html +547 -0
  137. package/dist/schemas/v0/plugin.schema.html +497 -0
  138. package/dist/schemas/v0/plugins.schema.html +406 -0
  139. package/dist/schemas/v0/site.schema.html +541 -0
  140. package/dist/schemas/v0/theme.schema.html +615 -0
  141. package/dist/schemas.html +351 -0
  142. package/dist/styles.css +1262 -0
  143. package/package.json +4 -2
  144. package/plugins-dist/callouts.css +32 -0
  145. package/plugins-dist/callouts.js +46 -0
  146. package/plugins-dist/slides-visuals.css +224 -0
  147. package/plugins-dist/slides-visuals.js +598 -0
  148. package/registry/plugins.yaml +35 -0
  149. package/schemas/README.md +10 -0
  150. package/schemas/plugin-registry.schema.json +5 -0
  151. package/schemas/plugin-schemas-notes.md +71 -0
  152. package/schemas/plugin.schema.json +5 -0
  153. package/schemas/plugins.schema.json +5 -0
  154. package/schemas/v0/common.schema.json +64 -0
  155. package/schemas/v0/plugin-registry.schema.json +225 -0
  156. package/schemas/v0/plugin.schema.json +175 -0
  157. package/schemas/v0/plugins.schema.json +84 -0
  158. package/schemas/v0/site.schema.json +56 -9
  159. package/schemas/v0/theme.schema.json +105 -22
  160. package/scripts/build.ts +155 -3
  161. package/scripts/bundle.ts +258 -95
  162. package/scripts/dev.ts +203 -1
  163. package/src/__tests__/build.test.ts +158 -1
  164. package/src/__tests__/bundle.test.ts +31 -0
  165. package/src/__tests__/cli.test.ts +14 -3
  166. package/src/__tests__/fixtures/fences/slides-visuals/invalid/bad-list-indent.md +5 -0
  167. package/src/__tests__/fixtures/fences/slides-visuals/invalid/blank-line.md +5 -0
  168. package/src/__tests__/fixtures/fences/slides-visuals/invalid/compare-object-items.md +9 -0
  169. package/src/__tests__/fixtures/fences/slides-visuals/invalid/indented-fence.md +4 -0
  170. package/src/__tests__/fixtures/fences/slides-visuals/invalid/stat-grid-missing-fields.md +5 -0
  171. package/src/__tests__/fixtures/fences/slides-visuals/invalid/unknown-type.md +3 -0
  172. package/src/__tests__/fixtures/fences/slides-visuals/valid/compare.md +10 -0
  173. package/src/__tests__/fixtures/fences/slides-visuals/valid/comparison-table.md +14 -0
  174. package/src/__tests__/fixtures/fences/slides-visuals/valid/funnel.md +7 -0
  175. package/src/__tests__/fixtures/fences/slides-visuals/valid/kpi.md +5 -0
  176. package/src/__tests__/fixtures/fences/slides-visuals/valid/layer-cake.md +6 -0
  177. package/src/__tests__/fixtures/fences/slides-visuals/valid/pyramid.md +6 -0
  178. package/src/__tests__/fixtures/fences/slides-visuals/valid/quadrant-grid.md +8 -0
  179. package/src/__tests__/fixtures/fences/slides-visuals/valid/scorecard.md +13 -0
  180. package/src/__tests__/fixtures/fences/slides-visuals/valid/stat-grid.md +8 -0
  181. package/src/__tests__/init.test.ts +35 -0
  182. package/src/__tests__/plugin-loader.test.ts +221 -0
  183. package/src/__tests__/shared.test.ts +428 -0
  184. package/src/__tests__/slides-visuals-fence-contract.test.ts +28 -0
  185. package/src/__tests__/slides-visuals-runtime-regressions.bun.test.ts +114 -0
  186. package/src/__tests__/styles.test.ts +35 -0
  187. package/src/cli.ts +9 -4
  188. package/src/plugin-loader.ts +245 -0
  189. package/src/shared.ts +614 -7
  190. package/src/site/styles.css +331 -0
  191. package/src/site/template.html +66 -5
  192. package/src/templates/deck.ts +186 -0
  193. package/src/templates/driver.ts +11 -1
  194. package/src/templates/minimal.ts +1 -0
package/scripts/bundle.ts CHANGED
@@ -4,13 +4,13 @@
4
4
  * Usage: bun run bundle [folder] [options]
5
5
  *
6
6
  * Options:
7
- * -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: dist]
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
- * --raw Include raw markdown in bundle [env: KITFLY_BUILD_RAW] [default: true]
9
+ * --raw Include raw markdown in bundle [env: KITFLY_BUNDLE_RAW] [default: true]
10
10
  * --no-raw Don't include raw markdown
11
11
  * --help Show help message
12
12
  *
13
- * Creates dist/bundle.html - a single file containing all content,
13
+ * Creates bundles/bundle.html - a single file containing all content,
14
14
  * styles, and scripts for offline viewing.
15
15
  */
16
16
 
@@ -18,13 +18,16 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
18
18
  import { basename, extname, join, resolve } from "node:path";
19
19
  import { marked, Renderer } from "marked";
20
20
  import { ENGINE_ASSETS_DIR } from "../src/engine.ts";
21
+ import { loadPluginInjections } from "../src/plugin-loader.ts";
21
22
  import {
22
23
  buildBundleFooter,
23
- // Navigation/template building
24
24
  buildSectionNav,
25
+ // Navigation/template building
26
+ buildSlideNav,
25
27
  // Types
26
28
  type ContentFile,
27
29
  collectFiles,
30
+ collectSlides,
28
31
  envBool,
29
32
  // Config helpers
30
33
  envString,
@@ -34,16 +37,18 @@ import {
34
37
  loadSiteConfig,
35
38
  // Markdown utilities
36
39
  parseFrontmatter,
40
+ parseYaml,
37
41
  resolveSiteVersion,
38
42
  resolveStylesPath,
39
43
  type SiteConfig,
40
44
  slugify,
41
45
  validatePath,
46
+ validateSlidesVisualsFences,
42
47
  } from "../src/shared.ts";
43
48
  import { generateThemeCSS, getPrismUrls, loadTheme } from "../src/theme.ts";
44
49
 
45
50
  // Defaults
46
- const DEFAULT_OUT = "dist";
51
+ const DEFAULT_OUT = "bundles";
47
52
  const DEFAULT_NAME = "bundle.html";
48
53
 
49
54
  let ROOT = process.cwd();
@@ -91,11 +96,15 @@ function getConfig(): {
91
96
  raw: boolean;
92
97
  } {
93
98
  const args = parseArgs(process.argv.slice(2));
99
+ const legacyOut = envString("KITFLY_BUILD_OUT", DEFAULT_OUT);
100
+ const out = args.out ?? envString("KITFLY_BUNDLE_OUT", legacyOut);
101
+ const legacyRaw = envBool("KITFLY_BUILD_RAW", true);
102
+ const raw = args.raw ?? envBool("KITFLY_BUNDLE_RAW", legacyRaw);
94
103
  return {
95
104
  folder: args.folder,
96
- out: args.out ?? envString("KITFLY_BUILD_OUT", DEFAULT_OUT),
105
+ out,
97
106
  name: args.name ?? envString("KITFLY_BUNDLE_NAME", DEFAULT_NAME),
98
- raw: args.raw ?? envBool("KITFLY_BUILD_RAW", true),
107
+ raw,
99
108
  };
100
109
  }
101
110
 
@@ -295,6 +304,71 @@ function buildBundleNav(files: ContentFile[], config: SiteConfig): string {
295
304
  return html;
296
305
  }
297
306
 
307
+ async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig): Promise<string> {
308
+ const slides = await collectSlides(files);
309
+ let validateFences = false;
310
+ try {
311
+ const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
312
+ const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
313
+ const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
314
+ validateFences = enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@"));
315
+ } catch {
316
+ // no config, skip
317
+ }
318
+ const renderedSlides = await Promise.all(
319
+ slides.map(async (slide, i) => {
320
+ let inner = "";
321
+ if (slide.kind === "markdown") {
322
+ if (validateFences) {
323
+ const diagnostics = validateSlidesVisualsFences(slide.body);
324
+ if (diagnostics.length) {
325
+ const msg = diagnostics
326
+ .slice(0, 12)
327
+ .map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
328
+ .join("\n");
329
+ throw new Error(`slides-visuals fence contract violations:\n${msg}`);
330
+ }
331
+ }
332
+ inner = marked.parse(slide.body) as string;
333
+ } else if (slide.kind === "yaml") {
334
+ inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
335
+ } else {
336
+ let prettyJson = slide.body;
337
+ try {
338
+ prettyJson = JSON.stringify(JSON.parse(slide.body), null, 2);
339
+ } catch {
340
+ // Keep original text
341
+ }
342
+ inner = `<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
343
+ }
344
+
345
+ inner = await inlineLocalImages(inner, config);
346
+ inner = rewriteContentLinks(inner, files, slide.sourceUrlPath, config.docroot);
347
+
348
+ const activeClass = i === 0 ? " active" : "";
349
+ const classToken = slide.className ? ` ${slide.className}` : "";
350
+ return `<section id="${slide.id}" class="slide${classToken}${activeClass}" data-slide-index="${i}">${inner}</section>`;
351
+ }),
352
+ );
353
+
354
+ return `
355
+ <div class="slides-shell" style="--slide-aspect: ${config.aspect || "16/9"}">
356
+ <div class="slide-viewport">
357
+ <div class="slide-frame">
358
+ ${renderedSlides.join("\n")}
359
+ </div>
360
+ </div>
361
+ <div class="slide-nav" aria-label="Slide navigation">
362
+ <button class="slide-prev" type="button" aria-label="Previous slide">Prev</button>
363
+ <span class="slide-counter">1 / ${slides.length}</span>
364
+ <button class="slide-next" type="button" aria-label="Next slide">Next</button>
365
+ <div class="slide-progress" role="presentation">
366
+ <span class="slide-progress-bar" style="width: ${(1 / slides.length) * 100}%"></span>
367
+ </div>
368
+ </div>
369
+ </div>`;
370
+ }
371
+
298
372
  function buildBundleSidebarHeader(
299
373
  config: SiteConfig,
300
374
  version: string | undefined,
@@ -304,12 +378,13 @@ function buildBundleSidebarHeader(
304
378
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
305
379
  const productHref = config.home ? "#home" : "#";
306
380
  const versionLabel = version ? `v${version}` : "unversioned";
381
+ const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
307
382
 
308
383
  return `
309
384
  <div class="sidebar-header">
310
385
  <div class="logo ${logoClass}">
311
- <a href="${config.brand.url}" class="logo-icon"${brandTarget}>
312
- <img src="${brandLogo}" alt="${config.brand.name}" class="logo-img">
386
+ <a href="${config.brand.url}" class="logo-icon" data-initial="${brandInitial}"${brandTarget}>
387
+ <img src="${brandLogo}" alt="${config.brand.name}" class="logo-img" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')">
313
388
  </a>
314
389
  <span class="logo-text">
315
390
  <a href="${config.brand.url}" class="brand"${brandTarget}>${config.brand.name}</a>
@@ -410,29 +485,6 @@ async function bundle() {
410
485
  // Resolve site version (site.yaml version, then git tag)
411
486
  const version = await resolveSiteVersion(ROOT, config.version);
412
487
 
413
- // Build navigation and content sections
414
- const sections: Map<string, { id: string; title: string; html: string }[]> = new Map();
415
-
416
- // Add home page as first item if specified
417
- if (config.home) {
418
- const homePath = validatePath(ROOT, config.docroot, config.home);
419
- if (homePath) {
420
- try {
421
- await stat(homePath);
422
- const content = await readFile(homePath, "utf-8");
423
- const { frontmatter, body } = parseFrontmatter(content);
424
- const title = (frontmatter.title as string) || "Home";
425
- let htmlContent = marked.parse(body) as string;
426
- htmlContent = await inlineLocalImages(htmlContent, config);
427
- htmlContent = rewriteContentLinks(htmlContent, files, undefined, config.docroot);
428
- sections.set("Home", [{ id: "home", title, html: htmlContent }]);
429
- console.log(` ✓ Added home page: ${config.home}`);
430
- } catch {
431
- console.warn(` ⚠ Home page ${config.home} not found`);
432
- }
433
- }
434
- }
435
-
436
488
  // Collect page metadata and raw content for AI accessibility
437
489
  const pageIndex: {
438
490
  path: string;
@@ -441,77 +493,124 @@ async function bundle() {
441
493
  description?: string;
442
494
  }[] = [];
443
495
  const rawMarkdown: { path: string; content: string }[] = [];
496
+ let navHtml = "";
497
+ let contentHtml = "";
444
498
 
445
- for (const file of files) {
446
- const content = await readFile(file.path, "utf-8");
447
- let title = basename(file.path).replace(/\.(md|yaml|json)$/, "");
448
- let description: string | undefined;
449
- let htmlContent: string;
450
-
451
- if (file.path.endsWith(".yaml")) {
452
- htmlContent = `<pre><code class="language-yaml">${escapeHtml(content)}</code></pre>`;
453
- } else if (file.path.endsWith(".json")) {
454
- // Render JSON as code block (pretty-printed)
455
- let prettyJson = content;
456
- try {
457
- prettyJson = JSON.stringify(JSON.parse(content), null, 2);
458
- } catch {
459
- // Use original if not valid JSON
460
- }
461
- htmlContent = `<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
462
- } else {
463
- const { frontmatter, body } = parseFrontmatter(content);
464
- if (frontmatter.title) {
465
- title = frontmatter.title as string;
499
+ if (config.mode === "slides") {
500
+ const slides = await collectSlides(files);
501
+ for (const file of files) {
502
+ const content = await readFile(file.path, "utf-8");
503
+ if (INCLUDE_RAW && file.path.endsWith(".md")) {
504
+ rawMarkdown.push({ path: file.urlPath, content });
466
505
  }
467
- if (frontmatter.description) {
468
- description = frontmatter.description as string;
506
+ }
507
+ for (const slide of slides) {
508
+ pageIndex.push({
509
+ path: slide.id,
510
+ title: slide.title,
511
+ section: slide.section,
512
+ });
513
+ }
514
+ navHtml = buildSlideNav(slides, config, "slide-1");
515
+ contentHtml = await buildSlidesBundleContent(files, config);
516
+ } else {
517
+ // Build navigation and content sections
518
+ const sections: Map<string, { id: string; title: string; html: string }[]> = new Map();
519
+
520
+ // Add home page as first item if specified
521
+ if (config.home) {
522
+ const homePath = validatePath(ROOT, config.docroot, config.home);
523
+ if (homePath) {
524
+ try {
525
+ await stat(homePath);
526
+ const content = await readFile(homePath, "utf-8");
527
+ const { frontmatter, body } = parseFrontmatter(content);
528
+ const title = (frontmatter.title as string) || "Home";
529
+ let htmlContent = marked.parse(body) as string;
530
+ htmlContent = await inlineLocalImages(htmlContent, config);
531
+ htmlContent = rewriteContentLinks(htmlContent, files, undefined, config.docroot);
532
+ sections.set("Home", [{ id: "home", title, html: htmlContent }]);
533
+ console.log(` ✓ Added home page: ${config.home}`);
534
+ } catch {
535
+ console.warn(` ⚠ Home page ${config.home} not found`);
536
+ }
469
537
  }
470
- htmlContent = marked.parse(body) as string;
538
+ }
471
539
 
472
- // Collect raw markdown for AI accessibility
473
- if (INCLUDE_RAW) {
474
- rawMarkdown.push({ path: file.urlPath, content });
540
+ for (const file of files) {
541
+ const content = await readFile(file.path, "utf-8");
542
+ let title = basename(file.path).replace(/\.(md|yaml|json)$/, "");
543
+ let description: string | undefined;
544
+ let htmlContent: string;
545
+
546
+ if (file.path.endsWith(".yaml")) {
547
+ htmlContent = `<pre><code class="language-yaml">${escapeHtml(content)}</code></pre>`;
548
+ } else if (file.path.endsWith(".json")) {
549
+ // Render JSON as code block (pretty-printed)
550
+ let prettyJson = content;
551
+ try {
552
+ prettyJson = JSON.stringify(JSON.parse(content), null, 2);
553
+ } catch {
554
+ // Use original if not valid JSON
555
+ }
556
+ htmlContent = `<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
557
+ } else {
558
+ const { frontmatter, body } = parseFrontmatter(content);
559
+ if (frontmatter.title) {
560
+ title = frontmatter.title as string;
561
+ }
562
+ if (frontmatter.description) {
563
+ description = frontmatter.description as string;
564
+ }
565
+ htmlContent = marked.parse(body) as string;
566
+
567
+ // Collect raw markdown for AI accessibility
568
+ if (INCLUDE_RAW) {
569
+ rawMarkdown.push({ path: file.urlPath, content });
570
+ }
475
571
  }
476
- }
477
572
 
478
- // Collect page metadata for content index
479
- pageIndex.push({
480
- path: file.urlPath,
481
- title,
482
- section: file.section,
483
- description,
484
- });
573
+ // Collect page metadata for content index
574
+ pageIndex.push({
575
+ path: file.urlPath,
576
+ title,
577
+ section: file.section,
578
+ description,
579
+ });
485
580
 
486
- // Inline any SVG references
487
- htmlContent = await inlineLocalImages(htmlContent, config);
488
- htmlContent = rewriteContentLinks(htmlContent, files, file.urlPath, config.docroot);
581
+ // Inline any SVG references
582
+ htmlContent = await inlineLocalImages(htmlContent, config);
583
+ htmlContent = rewriteContentLinks(htmlContent, files, file.urlPath, config.docroot);
489
584
 
490
- const sectionId = slugify(file.urlPath);
585
+ const sectionId = slugify(file.urlPath);
491
586
 
492
- if (!sections.has(file.section)) {
493
- sections.set(file.section, []);
587
+ if (!sections.has(file.section)) {
588
+ sections.set(file.section, []);
589
+ }
590
+ sections.get(file.section)?.push({ id: sectionId, title, html: htmlContent });
494
591
  }
495
- sections.get(file.section)?.push({ id: sectionId, title, html: htmlContent });
496
- }
497
592
 
498
- // Build navigation HTML from shared hierarchical nav tree
499
- const navHtml = buildBundleNav(files, config);
593
+ // Build navigation HTML from shared hierarchical nav tree
594
+ navHtml = buildBundleNav(files, config);
500
595
 
501
- // Build content HTML
502
- let contentHtml = "";
503
- for (const [, items] of sections) {
504
- for (const item of items) {
505
- contentHtml += `
596
+ // Build content HTML
597
+ for (const [, items] of sections) {
598
+ for (const item of items) {
599
+ contentHtml += `
506
600
  <section id="${item.id}" class="bundle-section">
507
601
  <h1 class="section-title">${item.title}</h1>
508
602
  ${item.html}
509
603
  </section>
510
604
  `;
605
+ }
511
606
  }
512
607
  }
513
608
 
514
609
  const themeCSS = generateThemeCSS(theme);
610
+ const plugins = await loadPluginInjections({
611
+ root: ROOT,
612
+ mode: config.mode === "slides" ? "slides" : "docs",
613
+ });
515
614
 
516
615
  // Inline brand assets for self-contained bundle
517
616
  const brandLogo = await inlineBrandAsset(config.brand.logo || "assets/brand/logo.png");
@@ -551,6 +650,7 @@ ${assets.prismCss}
551
650
  <style id="prism-dark" disabled>
552
651
  ${assets.prismCssDark}
553
652
  </style>
653
+ ${plugins.head}
554
654
  <script>
555
655
  (function() {
556
656
  const saved = localStorage.getItem('theme');
@@ -566,7 +666,7 @@ ${assets.prismCssDark}
566
666
  })();
567
667
  </script>
568
668
  </head>
569
- <body>
669
+ <body class="${config.mode === "slides" ? "mode-slides" : "mode-docs"}">
570
670
  <div class="layout">
571
671
  <nav class="sidebar">
572
672
  ${buildBundleSidebarHeader(config, version, brandLogo)}
@@ -590,6 +690,7 @@ ${assets.prismAutoloader}
590
690
  <script>
591
691
  ${assets.mermaid}
592
692
  </script>
693
+ ${plugins.bodyEnd}
593
694
  <script>
594
695
  // Initialize Mermaid
595
696
  function getMermaidTheme() {
@@ -647,17 +748,78 @@ ${assets.mermaid}
647
748
  }
648
749
  }
649
750
 
650
- // Smooth scroll for anchor links
651
- document.querySelectorAll('a[href^="#"]').forEach(anchor => {
652
- anchor.addEventListener('click', function (e) {
653
- e.preventDefault();
654
- const target = document.querySelector(this.getAttribute('href'));
655
- if (target) {
656
- target.scrollIntoView({ behavior: 'smooth' });
657
- history.pushState(null, '', this.getAttribute('href'));
751
+ // Slides mode hash routing
752
+ (function initSlidesMode() {
753
+ const shell = document.querySelector('.slides-shell');
754
+ if (!shell) {
755
+ // Docs mode: retain smooth in-page anchor scrolling.
756
+ document.querySelectorAll('a[href^="#"]').forEach((link) => {
757
+ link.addEventListener('click', (e) => {
758
+ const href = link.getAttribute('href') || '';
759
+ if (href.length <= 1) return;
760
+ const target = document.querySelector(href);
761
+ if (!target) return;
762
+ e.preventDefault();
763
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
764
+ history.replaceState(null, '', href);
765
+ });
766
+ });
767
+ return;
768
+ }
769
+
770
+ const slides = Array.from(document.querySelectorAll('.slide'));
771
+ if (!slides.length) return;
772
+
773
+ const prevBtn = document.querySelector('.slide-prev');
774
+ const nextBtn = document.querySelector('.slide-next');
775
+ const counter = document.querySelector('.slide-counter');
776
+ const progressBar = document.querySelector('.slide-progress-bar');
777
+ const navLinks = Array.from(document.querySelectorAll('.sidebar-nav a[href^="#slide-"]'));
778
+ let current = 0;
779
+
780
+ function setActive(n) {
781
+ current = Math.max(0, Math.min(n, slides.length - 1));
782
+ slides.forEach((slide, idx) => slide.classList.toggle('active', idx === current));
783
+ navLinks.forEach((link) => {
784
+ const active = link.getAttribute('href') === '#' + slides[current].id;
785
+ link.classList.toggle('active', active);
786
+ });
787
+ if (counter) counter.textContent = (current + 1) + ' / ' + slides.length;
788
+ if (progressBar) progressBar.style.width = (((current + 1) / slides.length) * 100) + '%';
789
+ if (prevBtn) prevBtn.disabled = current === 0;
790
+ if (nextBtn) nextBtn.disabled = current === slides.length - 1;
791
+ history.replaceState(null, '', '#'+slides[current].id);
792
+ }
793
+
794
+ function setFromHash() {
795
+ const hash = window.location.hash || '';
796
+ const idx = slides.findIndex((s) => '#'+s.id === hash);
797
+ if (idx >= 0) setActive(idx);
798
+ else setActive(0);
799
+ }
800
+
801
+ prevBtn?.addEventListener('click', () => setActive(current - 1));
802
+ nextBtn?.addEventListener('click', () => setActive(current + 1));
803
+
804
+ document.addEventListener('keydown', (e) => {
805
+ if (e.key === 'ArrowRight' || e.key === ' ') {
806
+ e.preventDefault();
807
+ setActive(current + 1);
808
+ } else if (e.key === 'ArrowLeft') {
809
+ e.preventDefault();
810
+ setActive(current - 1);
811
+ } else if (e.key === 'Home') {
812
+ e.preventDefault();
813
+ setActive(0);
814
+ } else if (e.key === 'End') {
815
+ e.preventDefault();
816
+ setActive(slides.length - 1);
658
817
  }
659
818
  });
660
- });
819
+
820
+ window.addEventListener('hashchange', setFromHash);
821
+ setFromHash();
822
+ })();
661
823
  </script>
662
824
  <!-- AI Accessibility: Content Index -->
663
825
  <script type="application/json" id="kitfly-content-index">
@@ -755,9 +917,9 @@ if (import.meta.main) {
755
917
  Usage: bun run bundle [folder] [options]
756
918
 
757
919
  Options:
758
- -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: ${DEFAULT_OUT}]
920
+ -o, --out <dir> Output directory [env: KITFLY_BUNDLE_OUT] [default: ${DEFAULT_OUT}]
759
921
  -n, --name <file> Bundle filename [env: KITFLY_BUNDLE_NAME] [default: ${DEFAULT_NAME}]
760
- --raw Include raw markdown in bundle [env: KITFLY_BUILD_RAW] [default: true]
922
+ --raw Include raw markdown in bundle [env: KITFLY_BUNDLE_RAW] [default: true]
761
923
  --no-raw Don't include raw markdown
762
924
  --help Show this help message
763
925
 
@@ -765,8 +927,9 @@ Examples:
765
927
  bun run bundle
766
928
  bun run bundle ./docs
767
929
  bun run bundle --name docs.html
768
- bun run bundle ./docs --out ./public --name handbook.html
930
+ bun run bundle ./docs --out ./bundles --name handbook.html
769
931
  KITFLY_BUNDLE_NAME=docs.html bun run bundle
932
+ KITFLY_BUNDLE_OUT=release bun run bundle
770
933
  `);
771
934
  process.exit(0);
772
935
  }