kitfly 0.2.0 → 0.2.3

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 (126) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +25 -10
  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/glossary.md +25 -1
  9. package/dist/_raw/content/reference/key-concepts.md +30 -2
  10. package/dist/_raw/content/reference/plugins.md +14 -0
  11. package/dist/_raw/content/reference/slides-authoring-guidelines.md +129 -0
  12. package/dist/_raw/content/reference.md +1 -0
  13. package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
  14. package/dist/content/deployment/preflight.html +10 -6
  15. package/dist/content/deployment/recipes/aws-s3.html +10 -6
  16. package/dist/content/deployment/recipes/cloudflare-pages.html +10 -6
  17. package/dist/content/deployment/recipes/cloudflare-r2.html +10 -6
  18. package/dist/content/deployment/recipes/fly-io.html +10 -6
  19. package/dist/content/deployment/recipes/github-pages.html +10 -6
  20. package/dist/content/deployment/recipes/netlify.html +10 -6
  21. package/dist/content/deployment/recipes/vercel.html +10 -6
  22. package/dist/content/deployment/secrets-and-env-vars.html +10 -6
  23. package/dist/content/deployment.html +10 -6
  24. package/dist/content/guide/approaches.html +10 -6
  25. package/dist/content/guide/branding.html +510 -0
  26. package/dist/content/guide/data-driven-content.html +543 -0
  27. package/dist/content/guide/features.html +10 -6
  28. package/dist/content/guide/getting-started.html +10 -6
  29. package/dist/content/guide/kitfly-overview.html +10 -6
  30. package/dist/content/reference/configuration.html +135 -9
  31. package/dist/content/reference/design-catalog.html +10 -6
  32. package/dist/content/reference/environment-variables.html +50 -8
  33. package/dist/content/reference/glossary.html +24 -8
  34. package/dist/content/reference/key-concepts.html +33 -9
  35. package/dist/content/reference/plugins.html +22 -7
  36. package/dist/content/reference/slides-authoring-guidelines.html +422 -0
  37. package/dist/content/reference/structure.html +10 -6
  38. package/dist/content/reference.html +11 -6
  39. package/dist/content/templates/crucible.html +10 -6
  40. package/dist/content/templates/handbook.html +10 -6
  41. package/dist/content/templates/minimal.html +10 -6
  42. package/dist/content/templates/overview.html +10 -6
  43. package/dist/content/templates/pipeline.html +10 -6
  44. package/dist/content/templates/productbook.html +10 -6
  45. package/dist/content/templates/runbook.html +10 -6
  46. package/dist/content/templates/servicebook.html +10 -6
  47. package/dist/content-index.json +38 -2
  48. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +10 -6
  49. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +10 -6
  50. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +10 -6
  51. package/dist/docs/decisions/ADR-0004-bun-runtime.html +10 -6
  52. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +10 -6
  53. package/dist/docs/decisions/ADR-0006-data-driven-content.html +752 -0
  54. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +10 -6
  55. package/dist/docs/decisions/DDR-0002-theme-system.html +10 -6
  56. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +10 -6
  57. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +10 -6
  58. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +10 -6
  59. package/dist/docs/userguide/cli/build.html +10 -6
  60. package/dist/docs/userguide/cli/bundle.html +10 -6
  61. package/dist/docs/userguide/cli/dev.html +10 -6
  62. package/dist/docs/userguide/cli/init.html +10 -6
  63. package/dist/docs/userguide/cli/servers.html +10 -6
  64. package/dist/docs/userguide/cli/stop.html +10 -6
  65. package/dist/docs/userguide/cli/update.html +10 -6
  66. package/dist/docs/userguide/cli/version.html +10 -6
  67. package/dist/docs/userguide/cli.html +10 -6
  68. package/dist/docs/userguide/sharing.html +10 -6
  69. package/dist/index.html +10 -6
  70. package/dist/llms.txt +3 -3
  71. package/dist/provenance.json +4 -4
  72. package/dist/schemas/plugin-registry.schema.html +10 -6
  73. package/dist/schemas/plugin-schemas-notes.html +10 -6
  74. package/dist/schemas/plugin.schema.html +10 -6
  75. package/dist/schemas/plugins.schema.html +10 -6
  76. package/dist/schemas/v0/common.schema.html +14 -10
  77. package/dist/schemas/v0/plugin-registry.schema.html +13 -9
  78. package/dist/schemas/v0/plugin.schema.html +13 -9
  79. package/dist/schemas/v0/plugins.schema.html +13 -9
  80. package/dist/schemas/v0/site.schema.html +67 -7
  81. package/dist/schemas/v0/theme.schema.html +21 -17
  82. package/dist/schemas.html +10 -6
  83. package/dist/styles.css +39 -4
  84. package/package.json +1 -1
  85. package/plugins-dist/latex-runtime.js +140 -0
  86. package/plugins-dist/latex.js +178 -0
  87. package/plugins-dist/slides-charts-lite-runtime.js +179 -0
  88. package/plugins-dist/slides-charts-lite.js +198 -0
  89. package/plugins-dist/slides-visuals.css +166 -0
  90. package/plugins-dist/slides-visuals.js +124 -33
  91. package/registry/plugins.yaml +30 -5
  92. package/schemas/v0/site.schema.json +56 -0
  93. package/scripts/build.ts +195 -70
  94. package/scripts/bundle.ts +122 -11
  95. package/scripts/dev.ts +345 -178
  96. package/src/__tests__/brief.test.ts +151 -0
  97. package/src/__tests__/build.test.ts +234 -4
  98. package/src/__tests__/bundle.test.ts +134 -0
  99. package/src/__tests__/dev-plugin-errors.test.ts +20 -0
  100. package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-branching-no-source.md +5 -0
  101. package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-converging-no-target.md +6 -0
  102. package/src/__tests__/fixtures/fences/slides-visuals/invalid/staircase-empty-steps.md +3 -0
  103. package/src/__tests__/fixtures/fences/slides-visuals/invalid/timeline-horizontal-no-events.md +2 -0
  104. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching-no-split.md +7 -0
  105. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching.md +8 -0
  106. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging-no-merge.md +7 -0
  107. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging.md +8 -0
  108. package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase-down.md +7 -0
  109. package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase.md +8 -0
  110. package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-horizontal.md +9 -0
  111. package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-vertical.md +10 -0
  112. package/src/__tests__/init.test.ts +51 -2
  113. package/src/__tests__/latex-runtime.bun.test.ts +35 -0
  114. package/src/__tests__/shared.test.ts +621 -1
  115. package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
  116. package/src/__tests__/slides-visuals-runtime-regressions.bun.test.ts +33 -0
  117. package/src/cli.ts +11 -4
  118. package/src/commands/init.ts +1 -1
  119. package/src/shared.ts +761 -18
  120. package/src/site/styles.css +39 -4
  121. package/src/site/template.html +5 -2
  122. package/src/templates/brief.ts +486 -0
  123. package/src/templates/deck.ts +59 -0
  124. package/src/templates/driver.ts +46 -13
  125. package/src/templates/handbook.ts +32 -0
  126. package/src/templates/runbook.ts +32 -0
package/scripts/build.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Options:
7
7
  * -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: dist]
8
+ * --profile <name> Active content profile [env: KITFLY_PROFILE]
8
9
  * --raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
9
10
  * --no-raw Don't include raw markdown files
10
11
  * --help Show help message
@@ -20,9 +21,10 @@ import { loadPluginInjections, type PluginInjections } from "../src/plugin-loade
20
21
  import {
21
22
  buildBreadcrumbsStatic,
22
23
  buildFooter,
24
+ buildLogoImgHtml,
23
25
  buildNavStatic,
24
26
  buildPageMeta,
25
- buildSlideNav,
27
+ buildSlideNavHierarchical,
26
28
  buildToc,
27
29
  type ContentFile,
28
30
  collectFiles,
@@ -35,17 +37,24 @@ import {
35
37
  escapeHtml,
36
38
  // File utilities
37
39
  exists,
40
+ filterByProfile,
41
+ filterUnknownSlidesVisualsTypeDiagnostics,
38
42
  // Provenance
39
43
  generateProvenance,
44
+ loadDataBindings,
40
45
  // YAML/Config parsing
41
46
  loadSiteConfig,
47
+ mergeFrontmatterWithBody,
42
48
  type Provenance,
49
+ pagePathForData,
43
50
  // Markdown utilities
44
51
  parseFrontmatter,
45
52
  parseYaml,
53
+ resolveBindings,
46
54
  resolveStylesPath,
47
55
  resolveTemplatePath,
48
56
  rewriteRelativeAssetUrls,
57
+ runPrebuildHooks,
49
58
  // Types
50
59
  type SiteConfig,
51
60
  slugify,
@@ -59,6 +68,41 @@ const DEFAULT_OUT = "dist";
59
68
 
60
69
  let ROOT = process.cwd();
61
70
  let OUT_DIR = DEFAULT_OUT;
71
+ let ACTIVE_PROFILE: string | undefined;
72
+
73
+ async function applyDataBindingsToMarkdown(
74
+ rawMarkdown: string,
75
+ filePath: string,
76
+ config: SiteConfig,
77
+ ): Promise<{ frontmatter: Record<string, unknown>; body: string }> {
78
+ const parsed = parseFrontmatter(rawMarkdown);
79
+ const dataRef = typeof parsed.frontmatter.data === "string" ? parsed.frontmatter.data.trim() : "";
80
+ if (!dataRef) return parsed;
81
+
82
+ const pagePath = pagePathForData(ROOT, config.docroot, filePath);
83
+ const bindings = await loadDataBindings(dataRef, pagePath, ROOT, config.docroot, config.dataroot);
84
+ return {
85
+ frontmatter: parsed.frontmatter,
86
+ body: resolveBindings(parsed.body, bindings, pagePath),
87
+ };
88
+ }
89
+
90
+ async function applyDataBindingsForSlides(
91
+ rawMarkdown: string,
92
+ filePath: string,
93
+ config: SiteConfig,
94
+ ): Promise<string> {
95
+ const resolved = await applyDataBindingsToMarkdown(rawMarkdown, filePath, config);
96
+ return mergeFrontmatterWithBody(rawMarkdown, resolved.body);
97
+ }
98
+
99
+ function normalizeMsysPath(p: string): string {
100
+ // Git Bash / MSYS-style paths: /c/Users/... -> C:\Users\...
101
+ if (process.platform !== "win32") return p;
102
+ const m = p.match(/^\/([a-zA-Z])\/(.*)$/);
103
+ if (!m) return p;
104
+ return `${m[1].toUpperCase()}:\\${m[2].replaceAll("/", "\\")}`;
105
+ }
62
106
 
63
107
  // ---------------------------------------------------------------------------
64
108
  // CLI argument parsing
@@ -68,6 +112,7 @@ interface ParsedArgs {
68
112
  folder?: string;
69
113
  out?: string;
70
114
  raw?: boolean;
115
+ profile?: string;
71
116
  }
72
117
 
73
118
  function parseArgs(argv: string[]): ParsedArgs {
@@ -79,6 +124,9 @@ function parseArgs(argv: string[]): ParsedArgs {
79
124
  if ((arg === "--out" || arg === "-o") && next && !next.startsWith("-")) {
80
125
  result.out = next;
81
126
  i++;
127
+ } else if (arg === "--profile" && next && !next.startsWith("-")) {
128
+ result.profile = next;
129
+ i++;
82
130
  } else if (arg === "--raw") {
83
131
  result.raw = true;
84
132
  } else if (arg === "--no-raw") {
@@ -90,12 +138,13 @@ function parseArgs(argv: string[]): ParsedArgs {
90
138
  return result;
91
139
  }
92
140
 
93
- function getConfig(): { folder?: string; out: string; raw: boolean } {
141
+ function getConfig(): { folder?: string; out: string; raw: boolean; profile?: string } {
94
142
  const args = parseArgs(process.argv.slice(2));
95
143
  return {
96
144
  folder: args.folder,
97
145
  out: args.out ?? envString("KITFLY_BUILD_OUT", DEFAULT_OUT),
98
146
  raw: args.raw ?? envBool("KITFLY_BUILD_RAW", true),
147
+ profile: args.profile ?? process.env.KITFLY_PROFILE,
99
148
  };
100
149
  }
101
150
 
@@ -188,7 +237,7 @@ async function renderFile(
188
237
  }
189
238
  htmlContent = `<h1>${title}</h1>\n<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
190
239
  } else {
191
- const { frontmatter, body } = parseFrontmatter(content);
240
+ const { frontmatter, body } = await applyDataBindingsToMarkdown(content, filePath, config);
192
241
  if (frontmatter.title) {
193
242
  title = frontmatter.title as string;
194
243
  }
@@ -198,7 +247,7 @@ async function renderFile(
198
247
 
199
248
  const pathPrefix = computePathPrefix(urlKey);
200
249
  const nav = buildNavStatic(files, urlKey, config, pathPrefix);
201
- const footer = buildFooter(provenance, config);
250
+ const footer = buildFooter(provenance, config, pathPrefix);
202
251
  const breadcrumbs = buildBreadcrumbsStatic(urlKey, pathPrefix, files, config);
203
252
  const toc = buildToc(htmlContent);
204
253
  const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
@@ -206,32 +255,50 @@ async function renderFile(
206
255
  const prismUrls = getPrismUrls(theme);
207
256
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
208
257
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
258
+ const mobileLogoHtml = buildLogoImgHtml({
259
+ logo: config.brand.logo || "assets/brand/logo.png",
260
+ logoDark: config.brand.logoDark,
261
+ alt: config.brand.name,
262
+ className: `logo-img ${logoClass}`,
263
+ pathPrefix,
264
+ onerrorFallback: true,
265
+ });
266
+ const sidebarLogoHtml = buildLogoImgHtml({
267
+ logo: config.brand.logo || "assets/brand/logo.png",
268
+ logoDark: config.brand.logoDark,
269
+ alt: config.brand.name,
270
+ className: "logo-img",
271
+ pathPrefix,
272
+ onerrorFallback: true,
273
+ });
209
274
 
210
275
  return template
211
276
  .replace("{{BODY_CLASS}}", "mode-docs")
212
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
213
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
214
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
215
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
216
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
217
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
218
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
219
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
220
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
221
- .replace("{{TITLE}}", title)
222
- .replace("{{VERSION}}", uiVersion)
223
- .replace("{{BRANCH}}", provenance.gitBranch)
224
- .replace("{{BREADCRUMBS}}", breadcrumbs)
225
- .replace("{{PAGE_META}}", pageMeta)
226
- .replace("{{NAV}}", nav)
227
- .replace("{{CONTENT}}", htmlContent)
228
- .replace("{{TOC}}", toc)
229
- .replace("{{FOOTER}}", footer)
230
- .replace("{{THEME_CSS}}", themeCSS)
231
- .replace("{{PLUGIN_HEAD}}", plugins.head)
232
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
233
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
234
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
277
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
278
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
279
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
280
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
281
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
282
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
283
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
284
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
285
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
286
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
287
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
288
+ .replace("{{TITLE}}", () => title)
289
+ .replace("{{VERSION}}", () => uiVersion)
290
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
291
+ .replace("{{BREADCRUMBS}}", () => breadcrumbs)
292
+ .replace("{{PAGE_META}}", () => pageMeta)
293
+ .replace("{{NAV}}", () => nav)
294
+ .replace("{{CONTENT}}", () => htmlContent)
295
+ .replace("{{TOC}}", () => toc)
296
+ .replace("{{FOOTER}}", () => footer)
297
+ .replace("{{THEME_CSS}}", () => themeCSS)
298
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
299
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
300
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
301
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
235
302
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
236
303
  }
237
304
 
@@ -273,32 +340,50 @@ sections:
273
340
  const pathPrefix = "./";
274
341
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
275
342
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
343
+ const mobileLogoHtml = buildLogoImgHtml({
344
+ logo: config.brand.logo || "assets/brand/logo.png",
345
+ logoDark: config.brand.logoDark,
346
+ alt: config.brand.name,
347
+ className: `logo-img ${logoClass}`,
348
+ pathPrefix,
349
+ onerrorFallback: true,
350
+ });
351
+ const sidebarLogoHtml = buildLogoImgHtml({
352
+ logo: config.brand.logo || "assets/brand/logo.png",
353
+ logoDark: config.brand.logoDark,
354
+ alt: config.brand.name,
355
+ className: "logo-img",
356
+ pathPrefix,
357
+ onerrorFallback: true,
358
+ });
276
359
 
277
360
  return template
278
361
  .replace("{{BODY_CLASS}}", "mode-docs")
279
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
280
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
281
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
282
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
283
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
284
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
285
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
286
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
287
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
362
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
363
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
364
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
365
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
366
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
367
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
368
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
369
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
370
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
371
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
372
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
288
373
  .replace("{{TITLE}}", "Getting Started")
289
- .replace("{{VERSION}}", uiVersion)
290
- .replace("{{BRANCH}}", provenance.gitBranch)
374
+ .replace("{{VERSION}}", () => uiVersion)
375
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
291
376
  .replace("{{BREADCRUMBS}}", "")
292
377
  .replace("{{PAGE_META}}", "")
293
378
  .replace("{{NAV}}", "<ul></ul>")
294
- .replace("{{CONTENT}}", htmlContent)
379
+ .replace("{{CONTENT}}", () => htmlContent)
295
380
  .replace("{{TOC}}", "")
296
- .replace("{{FOOTER}}", buildFooter(provenance, config))
297
- .replace("{{THEME_CSS}}", themeCSS)
298
- .replace("{{PLUGIN_HEAD}}", plugins.head)
299
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
300
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
301
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
381
+ .replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
382
+ .replace("{{THEME_CSS}}", () => themeCSS)
383
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
384
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
385
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
386
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
302
387
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
303
388
  }
304
389
 
@@ -312,7 +397,9 @@ async function renderSlidesIndex(
312
397
  ): Promise<string> {
313
398
  const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
314
399
  const pathPrefix = "./";
315
- const slides = await collectSlides(files);
400
+ const slides = await collectSlides(files, {
401
+ markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
402
+ });
316
403
  let validateFences = false;
317
404
  try {
318
405
  const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
@@ -327,7 +414,9 @@ async function renderSlidesIndex(
327
414
  let inner = "";
328
415
  if (slide.kind === "markdown") {
329
416
  if (validateFences) {
330
- const diagnostics = validateSlidesVisualsFences(slide.body);
417
+ const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
418
+ validateSlidesVisualsFences(slide.body),
419
+ );
331
420
  if (diagnostics.length) {
332
421
  const msg = diagnostics
333
422
  .slice(0, 12)
@@ -373,38 +462,56 @@ async function renderSlidesIndex(
373
462
  </div>
374
463
  </div>`;
375
464
 
376
- const nav = buildSlideNav(slides, config, "slide-1");
465
+ const nav = buildSlideNavHierarchical(slides, config, "slide-1");
377
466
  const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
378
467
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
379
468
  const themeCSS = generateThemeCSS(theme);
380
469
  const prismUrls = getPrismUrls(theme);
381
470
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
471
+ const mobileLogoHtml = buildLogoImgHtml({
472
+ logo: config.brand.logo || "assets/brand/logo.png",
473
+ logoDark: config.brand.logoDark,
474
+ alt: config.brand.name,
475
+ className: `logo-img ${logoClass}`,
476
+ pathPrefix,
477
+ onerrorFallback: true,
478
+ });
479
+ const sidebarLogoHtml = buildLogoImgHtml({
480
+ logo: config.brand.logo || "assets/brand/logo.png",
481
+ logoDark: config.brand.logoDark,
482
+ alt: config.brand.name,
483
+ className: "logo-img",
484
+ pathPrefix,
485
+ onerrorFallback: true,
486
+ });
382
487
 
383
488
  return template
384
489
  .replace("{{BODY_CLASS}}", "mode-slides")
385
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
386
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
387
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
388
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
389
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
390
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
391
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
392
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
393
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
394
- .replace("{{TITLE}}", config.title)
395
- .replace("{{VERSION}}", uiVersion)
396
- .replace("{{BRANCH}}", provenance.gitBranch)
490
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
491
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
492
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
493
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
494
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
495
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
496
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
497
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
498
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
499
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
500
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
501
+ .replace("{{TITLE}}", () => config.title)
502
+ .replace("{{VERSION}}", () => uiVersion)
503
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
397
504
  .replace("{{BREADCRUMBS}}", "")
398
505
  .replace("{{PAGE_META}}", "")
399
- .replace("{{NAV}}", nav)
400
- .replace("{{CONTENT}}", htmlContent)
506
+ .replace("{{NAV}}", () => nav)
507
+ .replace("{{CONTENT}}", () => htmlContent)
401
508
  .replace("{{TOC}}", "")
402
- .replace("{{FOOTER}}", buildFooter(provenance, config))
403
- .replace("{{THEME_CSS}}", themeCSS)
404
- .replace("{{PLUGIN_HEAD}}", plugins.head)
405
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
406
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
407
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
509
+ .replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
510
+ .replace("{{THEME_CSS}}", () => themeCSS)
511
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
512
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
513
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
514
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
408
515
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
409
516
  }
410
517
 
@@ -413,6 +520,7 @@ export interface BuildOptions {
413
520
  folder?: string;
414
521
  out?: string;
415
522
  raw?: boolean; // Include raw markdown files (default: true)
523
+ profile?: string;
416
524
  }
417
525
 
418
526
  let INCLUDE_RAW = true;
@@ -427,18 +535,29 @@ export async function build(options: BuildOptions = {}) {
427
535
  if (options.raw === false) {
428
536
  INCLUDE_RAW = false;
429
537
  }
538
+ ACTIVE_PROFILE = options.profile;
430
539
  await buildSite();
431
540
  }
432
541
 
433
542
  // Rename internal function
434
543
  async function buildSite() {
435
- const DIST = join(ROOT, OUT_DIR);
544
+ const DIST = resolve(ROOT, normalizeMsysPath(OUT_DIR));
436
545
 
437
546
  console.log("Building site...\n");
438
547
 
439
548
  // Load configuration
440
549
  const config = await loadSiteConfig(ROOT);
441
550
  console.log(` ✓ Loaded config: "${config.title}" (${config.sections.length} sections)`);
551
+ if (config.prebuild?.length) {
552
+ await runPrebuildHooks(
553
+ config.prebuild,
554
+ ROOT,
555
+ "build",
556
+ ACTIVE_PROFILE,
557
+ config.dataroot || "data",
558
+ );
559
+ console.log(` ✓ prebuild hooks (${config.prebuild.length})`);
560
+ }
442
561
 
443
562
  // Load theme
444
563
  const theme = await loadTheme(ROOT);
@@ -496,7 +615,11 @@ async function buildSite() {
496
615
  }
497
616
 
498
617
  // Collect and render all files
499
- const files = await collectFiles(ROOT, config);
618
+ const files = await filterByProfile(
619
+ await collectFiles(ROOT, config),
620
+ ACTIVE_PROFILE,
621
+ config.profiles,
622
+ );
500
623
 
501
624
  if (files.length === 0) {
502
625
  // No content - render Getting Started page
@@ -730,6 +853,7 @@ Usage: bun run build [folder] [options]
730
853
 
731
854
  Options:
732
855
  -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: ${DEFAULT_OUT}]
856
+ --profile <name> Active content profile [env: KITFLY_PROFILE]
733
857
  --raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
734
858
  --no-raw Don't include raw markdown files
735
859
  --help Show this help message
@@ -749,5 +873,6 @@ Examples:
749
873
  folder: cfg.folder,
750
874
  out: cfg.out,
751
875
  raw: cfg.raw,
876
+ profile: cfg.profile,
752
877
  }).catch(console.error);
753
878
  }