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
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Tests for the Brief template definition
3
+ *
4
+ * Covers: src/templates/brief.ts
5
+ * Strategy: import the template directly, verify structure and generated content
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+ import { brief } from "../templates/brief.ts";
10
+ import type { BrandingConfig, TemplateContext, TemplateFile } from "../templates/schema.ts";
11
+
12
+ function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
13
+ const branding: BrandingConfig = {
14
+ siteName: "Test Brief",
15
+ brandName: "Acme Corp",
16
+ brandUrl: "https://acme.example.com",
17
+ primaryColor: "#2563eb",
18
+ footerText: "Footer text",
19
+ };
20
+ return {
21
+ name: "test-brief",
22
+ branding,
23
+ template: brief,
24
+ year: 2026,
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ function resolveContent(file: TemplateFile, ctx: TemplateContext): string {
30
+ return typeof file.content === "function" ? file.content(ctx) : file.content;
31
+ }
32
+
33
+ function findFile(path: string): TemplateFile {
34
+ const f = brief.files.find((entry) => entry.path === path);
35
+ if (!f) throw new Error(`Template file not found: ${path}`);
36
+ return f;
37
+ }
38
+
39
+ describe("brief template definition", () => {
40
+ it("has correct identity metadata", () => {
41
+ expect(brief.id).toBe("brief");
42
+ expect(brief.name).toBe("Brief");
43
+ expect(brief.version).toBe(1);
44
+ expect(brief.extends).toBe("minimal");
45
+ });
46
+
47
+ it("defines exactly four sections", () => {
48
+ expect(brief.sections).toHaveLength(4);
49
+ expect(brief.sections.map((s) => s.name)).toEqual([
50
+ "Product",
51
+ "Use Cases",
52
+ "Getting Started",
53
+ "Reference",
54
+ ]);
55
+ expect(brief.sections.map((s) => s.path)).toEqual([
56
+ "content/product",
57
+ "content/use-cases",
58
+ "content/getting-started",
59
+ "content/reference",
60
+ ]);
61
+ });
62
+
63
+ it("defines unique file paths", () => {
64
+ const paths = brief.files.map((f) => f.path);
65
+ expect(new Set(paths).size).toBe(paths.length);
66
+ });
67
+ });
68
+
69
+ describe("brief site.yaml", () => {
70
+ const ctx = makeCtx();
71
+ const file = findFile("site.yaml");
72
+
73
+ it("includes branding and all four section paths", () => {
74
+ const content = resolveContent(file, ctx);
75
+ expect(content).toContain('title: "Test Brief"');
76
+ expect(content).toContain('name: "Acme Corp"');
77
+ expect(content).toContain('url: "https://acme.example.com"');
78
+ expect(content).toContain('"content/product"');
79
+ expect(content).toContain('"content/use-cases"');
80
+ expect(content).toContain('"content/getting-started"');
81
+ expect(content).toContain('"content/reference"');
82
+ expect(content).toContain('home: "index.md"');
83
+ });
84
+ });
85
+
86
+ describe("brief starter files", () => {
87
+ const ctx = makeCtx();
88
+ const starterFiles = [
89
+ "index.md",
90
+ "content/product/overview.md",
91
+ "content/product/capabilities.md",
92
+ "content/use-cases/overview.md",
93
+ "content/use-cases/example-use-case.md",
94
+ "content/getting-started/overview.md",
95
+ "content/getting-started/requirements.md",
96
+ "content/reference/architecture.md",
97
+ "content/reference/faq.md",
98
+ "content/reference/contacts.md",
99
+ ];
100
+
101
+ it("ships all 10 starter files", () => {
102
+ expect(starterFiles).toHaveLength(10);
103
+ for (const path of starterFiles) {
104
+ expect(brief.files.some((f) => f.path === path)).toBe(true);
105
+ }
106
+ });
107
+
108
+ it("includes frontmatter in markdown starter files", () => {
109
+ for (const path of starterFiles) {
110
+ const content = resolveContent(findFile(path), ctx);
111
+ expect(content).toMatch(/^---\n/);
112
+ }
113
+ });
114
+
115
+ it("includes customize markers in starter files", () => {
116
+ for (const path of starterFiles) {
117
+ const content = resolveContent(findFile(path), ctx);
118
+ expect(content).toContain("<!-- ← CUSTOMIZE");
119
+ }
120
+ });
121
+
122
+ it("uses professional framing in index and use cases", () => {
123
+ const index = resolveContent(findFile("index.md"), ctx);
124
+ expect(index).toContain("At a Glance");
125
+ expect(index).toContain("[Use Cases](/content/use-cases/overview)");
126
+
127
+ const useCases = resolveContent(findFile("content/use-cases/example-use-case.md"), ctx);
128
+ expect(useCases).toContain("## Scenario");
129
+ expect(useCases).toContain("## Outcome");
130
+ expect(useCases).toContain("| Metric | Before | After |");
131
+ });
132
+ });
133
+
134
+ describe("brief CUSTOMIZING.md", () => {
135
+ const ctx = makeCtx();
136
+ const file = findFile("CUSTOMIZING.md");
137
+
138
+ it("documents template identity and external-audience tone", () => {
139
+ const content = resolveContent(file, ctx);
140
+ expect(content).toContain("template: brief");
141
+ expect(content).toContain("clients, prospects, and partners");
142
+ expect(content).toContain("Informational and professional");
143
+ expect(content).toContain("Not sales copy");
144
+ });
145
+
146
+ it("guides use-case duplication workflow", () => {
147
+ const content = resolveContent(file, ctx);
148
+ expect(content).toContain("Duplicate `content/use-cases/example-use-case.md`");
149
+ expect(content).toContain("problem -> solution -> outcome");
150
+ });
151
+ });
@@ -35,7 +35,8 @@ async function writeSiteYaml(dir: string, extra: Record<string, unknown> = {}):
35
35
  const mode = extra.mode ? `mode: ${extra.mode}\n` : "";
36
36
  const aspect = extra.aspect ? `aspect: ${extra.aspect}\n` : "";
37
37
  const home = extra.home ? `home: ${extra.home}\n` : "";
38
- const yaml = `title: ${title}\n${version}${mode}${aspect}brand:\n${brand}\n${home}sections:\n${sections}\n`;
38
+ const footer = extra.footer ? `footer:\n${extra.footer}\n` : "";
39
+ const yaml = `title: ${title}\n${version}${mode}${aspect}brand:\n${brand}\n${home}${footer}sections:\n${sections}\n`;
39
40
  await writeFile(join(dir, "site.yaml"), yaml);
40
41
  }
41
42
 
@@ -216,6 +217,45 @@ describe("build", () => {
216
217
  expect(html).toContain("unversioned");
217
218
  });
218
219
 
220
+ it("renders footer logo with static path prefix in built output", async () => {
221
+ const siteDir = await makeTempDir();
222
+ const outDir = "out";
223
+ await writeSiteYaml(siteDir, {
224
+ footer: ' logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24',
225
+ });
226
+ await writeMd(siteDir, "docs/page.md", "# Page");
227
+
228
+ await build({ folder: siteDir, out: outDir });
229
+
230
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
231
+ expect(html).toContain('class="footer-logo-img"');
232
+ expect(html).toContain('src="./assets/brand/footer-logo.png"');
233
+ expect(html).toContain('alt="Footer Brand"');
234
+ expect(html).toContain("max-height: 24px");
235
+ });
236
+
237
+ it("renders light/dark variants for header and footer logos when configured", async () => {
238
+ const siteDir = await makeTempDir();
239
+ const outDir = "out";
240
+ await writeSiteYaml(siteDir, {
241
+ brand:
242
+ ' name: Test\n url: /\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"',
243
+ footer:
244
+ ' logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"',
245
+ });
246
+ await writeMd(siteDir, "docs/page.md", "# Page");
247
+
248
+ await build({ folder: siteDir, out: outDir });
249
+
250
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
251
+ expect(html).toContain("logo-img logo-light");
252
+ expect(html).toContain("logo-img logo-dark");
253
+ expect(html).toContain("./assets/brand/logo-dark.png");
254
+ expect(html).toContain("footer-logo-img logo-light");
255
+ expect(html).toContain("footer-logo-img logo-dark");
256
+ expect(html).toContain("./assets/brand/footer-logo-dark.png");
257
+ });
258
+
219
259
  it("builds a single-page hash-routed deck when mode is slides", async () => {
220
260
  const siteDir = await makeTempDir();
221
261
  const outDir = "out";
@@ -332,6 +372,87 @@ plugins:
332
372
  expect(html).toContain(js);
333
373
  });
334
374
 
375
+ it("preserves dollar-sign replacement patterns in injected plugin JS", async () => {
376
+ const siteDir = await makeTempDir();
377
+ const outDir = "out";
378
+ await writeSiteYaml(siteDir);
379
+ await writeMd(siteDir, "docs/page.md", "# Test");
380
+
381
+ const js = "const expr='x'; const sample = `$${expr}$`; const marker = '$`';";
382
+ await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
383
+ await writeFile(join(siteDir, "plugins-dist", "literal.js"), js, "utf-8");
384
+
385
+ await mkdir(join(siteDir, "registry"), { recursive: true });
386
+ await writeFile(
387
+ join(siteDir, "registry", "plugins.yaml"),
388
+ `version: 1
389
+ updated: "2026-02-16"
390
+ baseUrl: ""
391
+ plugins:
392
+ literal:
393
+ name: "Literal"
394
+ description: "Replacement pattern regression"
395
+ version: "1.0.0"
396
+ contract: "1"
397
+ kitfly: ">=0.2.0 <1.0.0"
398
+ license: MIT
399
+ verified: true
400
+ assets:
401
+ js: "plugins-dist/literal.js"
402
+ assetSha256:
403
+ js: "sha256:${sha256Hex(js)}"
404
+ `,
405
+ "utf-8",
406
+ );
407
+
408
+ await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - literal@1.0.0\n", "utf-8");
409
+
410
+ await build({ folder: siteDir, out: outDir });
411
+
412
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
413
+ expect(html).toContain("const sample = `$${expr}$`;");
414
+ expect(html).toContain("const marker = '$`';");
415
+ expect(html).not.toContain("<!DOCTYPE html><html");
416
+ });
417
+
418
+ it("preserves dollar-sign replacement patterns in markdown content", async () => {
419
+ const siteDir = await makeTempDir();
420
+ const outDir = "out";
421
+ await writeSiteYaml(siteDir);
422
+ await writeMd(
423
+ siteDir,
424
+ "docs/dollars.md",
425
+ "# Dollars\n\nLiteral dollars: $$ and $` and $' and $&\n\nMath style: $$x^2$$",
426
+ );
427
+
428
+ await build({ folder: siteDir, out: outDir });
429
+
430
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
431
+ expect(html).toContain("Literal dollars: $$ and $` and $&#39; and $&amp;");
432
+ expect(html).toContain("Math style: $$x^2$$");
433
+ expect(html.match(/<!DOCTYPE html>/g)?.length ?? 0).toBe(1);
434
+ });
435
+
436
+ it("injects latex plugin in docs mode", async () => {
437
+ const siteDir = await makeTempDir();
438
+ const outDir = "out";
439
+ await writeSiteYaml(siteDir);
440
+ await writeMd(
441
+ siteDir,
442
+ "docs/math.md",
443
+ "# Math\n\nInline: $x^2$.\n\n$$\n\\\\int_0^1 x^2 dx\n$$\n\n```math\n\\\\sum_{i=1}^{n} i\n```",
444
+ );
445
+ await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
446
+
447
+ await build({ folder: siteDir, out: outDir });
448
+
449
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
450
+ expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
451
+ expect(html).toContain("const KATEX_JS_URL =");
452
+ expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
453
+ expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
454
+ });
455
+
335
456
  it("injects slides-only plugins when mode=slides", async () => {
336
457
  const siteDir = await makeTempDir();
337
458
  const outDir = "out";
@@ -365,7 +486,7 @@ plugins:
365
486
  slides-visuals:
366
487
  name: "Slides Visuals"
367
488
  description: "Test visuals"
368
- version: "0.2.0"
489
+ version: "0.2.1"
369
490
  contract: "1"
370
491
  kitfly: ">=0.2.0 <1.0.0"
371
492
  license: MIT
@@ -383,15 +504,124 @@ plugins:
383
504
 
384
505
  await writeFile(
385
506
  join(siteDir, "kitfly.plugins.yaml"),
386
- "plugins:\n - slides-visuals@0.2.0\n",
507
+ "plugins:\n - slides-visuals@0.2.1\n",
387
508
  "utf-8",
388
509
  );
389
510
 
390
511
  await build({ folder: siteDir, out: outDir });
391
512
 
392
513
  const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
393
- expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.0"');
514
+ expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
394
515
  expect(html).toContain(css);
395
516
  expect(html).toContain(js);
396
517
  });
518
+
519
+ it("injects slides-charts-lite plugin in slides mode", async () => {
520
+ const siteDir = await makeTempDir();
521
+ const outDir = "out";
522
+ await writeSiteYaml(siteDir, { mode: "slides" });
523
+ await writeMd(
524
+ siteDir,
525
+ "docs/deck.md",
526
+ `# Chart
527
+
528
+ \`\`\`chart
529
+ kind: bar
530
+ title: Revenue
531
+ labels: ["Q1", "Q2"]
532
+ data: [10, 12]
533
+ \`\`\`
534
+ `,
535
+ );
536
+ await writeFile(
537
+ join(siteDir, "kitfly.plugins.yaml"),
538
+ "plugins:\n - slides-charts-lite@0.2.2\n",
539
+ "utf-8",
540
+ );
541
+
542
+ await build({ folder: siteDir, out: outDir });
543
+
544
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
545
+ expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
546
+ expect(html).toContain("kitfly-chart-wrapper");
547
+ });
548
+
549
+ it("does not inject slides-charts-lite in docs mode", async () => {
550
+ const siteDir = await makeTempDir();
551
+ const outDir = "out";
552
+ await writeSiteYaml(siteDir, { mode: "docs" });
553
+ await writeMd(siteDir, "docs/page.md", "# Page");
554
+ await writeFile(
555
+ join(siteDir, "kitfly.plugins.yaml"),
556
+ "plugins:\n - slides-charts-lite@0.2.2\n",
557
+ "utf-8",
558
+ );
559
+
560
+ await build({ folder: siteDir, out: outDir });
561
+
562
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
563
+ expect(html).not.toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
564
+ });
565
+
566
+ it("ignores unknown slides-visuals block types while enforcing known contracts", async () => {
567
+ const siteDir = await makeTempDir();
568
+ const outDir = "out";
569
+ await writeSiteYaml(siteDir, { mode: "slides" });
570
+ await writeMd(
571
+ siteDir,
572
+ "docs/deck.md",
573
+ `# Title
574
+
575
+ :::future-thing
576
+ note: this should pass through
577
+ :::
578
+
579
+ :::kpi
580
+ label: Uptime
581
+ value: 99.95%
582
+ :::
583
+ `,
584
+ );
585
+
586
+ const js = "console.log('slides visuals');";
587
+ const css = ".kitfly-visual{border:1px solid red;}";
588
+ await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
589
+ await writeFile(join(siteDir, "plugins-dist", "slides-visuals.js"), js, "utf-8");
590
+ await writeFile(join(siteDir, "plugins-dist", "slides-visuals.css"), css, "utf-8");
591
+ await mkdir(join(siteDir, "registry"), { recursive: true });
592
+ await writeFile(
593
+ join(siteDir, "registry", "plugins.yaml"),
594
+ `version: 1
595
+ updated: "2026-02-15"
596
+ baseUrl: ""
597
+ plugins:
598
+ slides-visuals:
599
+ name: "Slides Visuals"
600
+ description: "Test visuals"
601
+ version: "0.2.1"
602
+ contract: "1"
603
+ kitfly: ">=0.2.0 <1.0.0"
604
+ license: MIT
605
+ verified: true
606
+ modes: ["slides"]
607
+ assets:
608
+ js: "plugins-dist/slides-visuals.js"
609
+ css: "plugins-dist/slides-visuals.css"
610
+ assetSha256:
611
+ js: "sha256:${sha256Hex(js)}"
612
+ css: "sha256:${sha256Hex(css)}"
613
+ `,
614
+ "utf-8",
615
+ );
616
+ await writeFile(
617
+ join(siteDir, "kitfly.plugins.yaml"),
618
+ "plugins:\n - slides-visuals@0.2.1\n",
619
+ "utf-8",
620
+ );
621
+
622
+ await expect(build({ folder: siteDir, out: outDir })).resolves.toBeUndefined();
623
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
624
+ expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
625
+ expect(html).toContain("future-thing");
626
+ });
397
627
  });
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
5
5
  import {
6
6
  buildBundleNav,
7
7
  buildBundleSidebarHeader,
8
+ bundleSite,
8
9
  fileToDataUri,
9
10
  imageMime,
10
11
  inlineLocalImages,
@@ -815,3 +816,136 @@ describe("buildBundleSidebarHeader", () => {
815
816
  expect(html).toContain(`src="${logo}"`);
816
817
  });
817
818
  });
819
+
820
+ describe("bundleSite plugin integration", () => {
821
+ it("inlines latex plugin script when enabled", async () => {
822
+ const siteDir = await makeTempDir();
823
+ await mkdir(join(siteDir, "docs"), { recursive: true });
824
+ await writeFile(
825
+ join(siteDir, "site.yaml"),
826
+ 'title: "Bundle Test"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Docs\n path: docs\n',
827
+ "utf-8",
828
+ );
829
+ await writeFile(join(siteDir, "docs", "index.md"), "# Bundle Math\n\n$E=mc^2$", "utf-8");
830
+ await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
831
+
832
+ await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
833
+
834
+ const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
835
+ expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
836
+ expect(html).toContain("const KATEX_JS_URL =");
837
+ expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
838
+ expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
839
+ });
840
+
841
+ it("inlines slides-charts-lite plugin in slides mode", async () => {
842
+ const siteDir = await makeTempDir();
843
+ await mkdir(join(siteDir, "slides"), { recursive: true });
844
+ await writeFile(
845
+ join(siteDir, "site.yaml"),
846
+ 'title: "Bundle Charts Test"\nmode: "slides"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Slides\n path: slides\n',
847
+ "utf-8",
848
+ );
849
+ await writeFile(
850
+ join(siteDir, "slides", "deck.md"),
851
+ '# Deck\n\n```chart\nkind: line\nlabels: ["W1", "W2"]\ndata: [2, 3]\n```',
852
+ "utf-8",
853
+ );
854
+ await writeFile(
855
+ join(siteDir, "kitfly.plugins.yaml"),
856
+ "plugins:\n - slides-charts-lite@0.2.2\n",
857
+ "utf-8",
858
+ );
859
+
860
+ await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
861
+
862
+ const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
863
+ expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
864
+ expect(html).toContain("kitfly-chart-wrapper");
865
+ });
866
+
867
+ it("inlines footer logo image when configured", async () => {
868
+ const siteDir = await makeTempDir();
869
+ await mkdir(join(siteDir, "docs"), { recursive: true });
870
+ await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
871
+ await writeFile(
872
+ join(siteDir, "site.yaml"),
873
+ 'title: "Bundle Footer Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24\nsections:\n - name: Docs\n path: docs\n',
874
+ "utf-8",
875
+ );
876
+ await writeFile(join(siteDir, "docs", "index.md"), "# Footer Logo");
877
+ await writeFile(
878
+ join(siteDir, "assets", "brand", "footer-logo.png"),
879
+ Buffer.from(
880
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
881
+ "base64",
882
+ ),
883
+ );
884
+
885
+ await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
886
+
887
+ const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
888
+ expect(html).toContain('class="footer-logo-img"');
889
+ expect(html).toContain('src="data:image/png;base64,');
890
+ expect(html).not.toContain('src="assets/brand/footer-logo.png"');
891
+ expect(html).toContain('alt="Footer Brand"');
892
+ expect(html).toContain("max-height: 24px");
893
+ });
894
+
895
+ it("inlines footer logo from site-root-relative path outside assets", async () => {
896
+ const siteDir = await makeTempDir();
897
+ await mkdir(join(siteDir, "docs"), { recursive: true });
898
+ await mkdir(join(siteDir, "logos"), { recursive: true });
899
+ await writeFile(
900
+ join(siteDir, "site.yaml"),
901
+ 'title: "Bundle Footer Root Path Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "logos/footer.png"\n logoAlt: "Footer Root Logo"\nsections:\n - name: Docs\n path: docs\n',
902
+ "utf-8",
903
+ );
904
+ await writeFile(join(siteDir, "docs", "index.md"), "# Footer Root Path");
905
+ await writeFile(
906
+ join(siteDir, "logos", "footer.png"),
907
+ Buffer.from(
908
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
909
+ "base64",
910
+ ),
911
+ );
912
+
913
+ await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
914
+
915
+ const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
916
+ expect(html).toContain('class="footer-logo-img"');
917
+ expect(html).toContain('src="data:image/png;base64,');
918
+ expect(html).not.toContain('src="logos/footer.png"');
919
+ expect(html).toContain('alt="Footer Root Logo"');
920
+ });
921
+
922
+ it("inlines light/dark variants for header and footer logos", async () => {
923
+ const siteDir = await makeTempDir();
924
+ await mkdir(join(siteDir, "docs"), { recursive: true });
925
+ await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
926
+ await writeFile(
927
+ join(siteDir, "site.yaml"),
928
+ 'title: "Bundle Dark Logos Test"\nbrand:\n name: "Test"\n url: "/"\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"\nsections:\n - name: Docs\n path: docs\n',
929
+ "utf-8",
930
+ );
931
+ await writeFile(join(siteDir, "docs", "index.md"), "# Dark Logos");
932
+ const pixelPng = Buffer.from(
933
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
934
+ "base64",
935
+ );
936
+ await writeFile(join(siteDir, "assets", "brand", "logo.png"), pixelPng);
937
+ await writeFile(join(siteDir, "assets", "brand", "logo-dark.png"), pixelPng);
938
+ await writeFile(join(siteDir, "assets", "brand", "footer-logo.png"), pixelPng);
939
+ await writeFile(join(siteDir, "assets", "brand", "footer-logo-dark.png"), pixelPng);
940
+
941
+ await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
942
+
943
+ const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
944
+ expect(html).toContain('class="logo-img logo-light"');
945
+ expect(html).toContain('class="logo-img logo-dark"');
946
+ expect(html).toContain('class="footer-logo-img logo-light"');
947
+ expect(html).toContain('class="footer-logo-img logo-dark"');
948
+ expect(html).not.toContain('src="assets/brand/logo-dark.png"');
949
+ expect(html).not.toContain('src="assets/brand/footer-logo-dark.png"');
950
+ });
951
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildDevPluginErrorHtml } from "../../scripts/dev.ts";
3
+
4
+ describe("dev plugin error page", () => {
5
+ it("shows actionable update hint for plugin version mismatch", () => {
6
+ const html = buildDevPluginErrorHtml("Plugin slides-visuals version mismatch: 0.2.0 != 0.2.1");
7
+ expect(html).toContain("Plugin setup error");
8
+ expect(html).toContain("kitfly.plugins.yaml");
9
+ expect(html).toContain("slides-visuals@0.2.1");
10
+ });
11
+
12
+ it("escapes error text and falls back to generic guidance", () => {
13
+ const html = buildDevPluginErrorHtml('Invalid plugin ref: bad"@1.0.0 <x>');
14
+ expect(html).toContain(
15
+ "Check <code>kitfly.plugins.yaml</code> and <code>registry/plugins.yaml</code>",
16
+ );
17
+ expect(html).toContain("bad&quot;@1.0.0 &lt;x&gt;");
18
+ expect(html).not.toContain('bad"@1.0.0 <x>');
19
+ });
20
+ });
@@ -0,0 +1,5 @@
1
+ :::flow-branching
2
+ branches:
3
+ - "API Handler"
4
+ - "Static Files"
5
+ :::
@@ -0,0 +1,6 @@
1
+ :::flow-converging
2
+ sources:
3
+ - "Frontend Logs"
4
+ - "API Logs"
5
+ merge: "Aggregator"
6
+ :::
@@ -0,0 +1,7 @@
1
+ :::flow-branching
2
+ source: "Incoming Request"
3
+ branches:
4
+ - "API Handler"
5
+ - "Static Files"
6
+ - "WebSocket"
7
+ :::
@@ -0,0 +1,8 @@
1
+ :::flow-branching
2
+ source: "Incoming Request"
3
+ split: "Route"
4
+ branches:
5
+ - "API Handler"
6
+ - "Static Files"
7
+ - "WebSocket"
8
+ :::
@@ -0,0 +1,7 @@
1
+ :::flow-converging
2
+ sources:
3
+ - "Frontend Logs"
4
+ - "API Logs"
5
+ - "DB Logs"
6
+ target: "Dashboard"
7
+ :::
@@ -0,0 +1,8 @@
1
+ :::flow-converging
2
+ sources:
3
+ - "Frontend Logs"
4
+ - "API Logs"
5
+ - "DB Logs"
6
+ merge: "Aggregator"
7
+ target: "Dashboard"
8
+ :::
@@ -0,0 +1,7 @@
1
+ :::staircase
2
+ direction: down
3
+ steps:
4
+ - "Optimized"
5
+ - "Managed"
6
+ - "Defined"
7
+ :::
@@ -0,0 +1,8 @@
1
+ :::staircase
2
+ steps:
3
+ - "Ad Hoc"
4
+ - "Repeatable"
5
+ - "Defined"
6
+ - "Managed"
7
+ - "Optimized"
8
+ :::