kitfly 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +38 -21
- package/VERSION +1 -1
- package/dist/_raw/content/guide/branding.md +146 -0
- package/dist/_raw/content/guide/data-driven-content.md +204 -0
- package/dist/_raw/content/reference/configuration.md +145 -7
- package/dist/_raw/content/reference/environment-variables.md +26 -1
- package/dist/_raw/content/reference/gantt-widget.md +468 -0
- package/dist/_raw/content/reference/glossary.md +25 -1
- package/dist/_raw/content/reference/key-concepts.md +30 -2
- package/dist/_raw/content/reference/plugins.md +170 -1
- package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
- package/dist/content/deployment/preflight.html +11 -8
- package/dist/content/deployment/recipes/aws-s3.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-pages.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-r2.html +11 -8
- package/dist/content/deployment/recipes/fly-io.html +11 -8
- package/dist/content/deployment/recipes/github-pages.html +11 -8
- package/dist/content/deployment/recipes/netlify.html +11 -8
- package/dist/content/deployment/recipes/vercel.html +11 -8
- package/dist/content/deployment/secrets-and-env-vars.html +11 -8
- package/dist/content/deployment.html +11 -8
- package/dist/content/guide/approaches.html +11 -8
- package/dist/content/guide/branding.html +509 -0
- package/dist/content/guide/data-driven-content.html +542 -0
- package/dist/content/guide/features.html +11 -8
- package/dist/content/guide/getting-started.html +11 -8
- package/dist/content/guide/kitfly-overview.html +11 -8
- package/dist/content/reference/configuration.html +136 -11
- package/dist/content/reference/design-catalog.html +11 -8
- package/dist/content/reference/environment-variables.html +51 -10
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +25 -10
- package/dist/content/reference/key-concepts.html +34 -11
- package/dist/content/reference/plugins.html +261 -10
- package/dist/content/reference/slides-authoring-guidelines.html +11 -8
- package/dist/content/reference/structure.html +11 -8
- package/dist/content/reference.html +11 -8
- package/dist/content/templates/crucible.html +11 -8
- package/dist/content/templates/handbook.html +11 -8
- package/dist/content/templates/minimal.html +11 -8
- package/dist/content/templates/overview.html +11 -8
- package/dist/content/templates/pipeline.html +11 -8
- package/dist/content/templates/productbook.html +11 -8
- package/dist/content/templates/runbook.html +11 -8
- package/dist/content/templates/servicebook.html +11 -8
- package/dist/content-index.json +37 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +11 -8
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +11 -8
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +11 -8
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +11 -8
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +11 -8
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +751 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +11 -8
- package/dist/docs/decisions/DDR-0002-theme-system.html +11 -8
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +11 -8
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +11 -8
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +11 -8
- package/dist/docs/userguide/cli/build.html +11 -8
- package/dist/docs/userguide/cli/bundle.html +11 -8
- package/dist/docs/userguide/cli/dev.html +11 -8
- package/dist/docs/userguide/cli/init.html +11 -8
- package/dist/docs/userguide/cli/servers.html +11 -8
- package/dist/docs/userguide/cli/stop.html +11 -8
- package/dist/docs/userguide/cli/update.html +11 -8
- package/dist/docs/userguide/cli/version.html +11 -8
- package/dist/docs/userguide/cli.html +11 -8
- package/dist/docs/userguide/sharing.html +11 -8
- package/dist/index.html +11 -8
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -5
- package/dist/reports/license-inventory.csv +199 -0
- package/dist/schemas/plugin-registry.schema.html +11 -8
- package/dist/schemas/plugin-schemas-notes.html +11 -8
- package/dist/schemas/plugin.schema.html +11 -8
- package/dist/schemas/plugins.schema.html +11 -8
- package/dist/schemas/v0/common.schema.html +15 -12
- package/dist/schemas/v0/plugin-registry.schema.html +14 -11
- package/dist/schemas/v0/plugin.schema.html +14 -11
- package/dist/schemas/v0/plugins.schema.html +14 -11
- package/dist/schemas/v0/site.schema.html +68 -9
- package/dist/schemas/v0/theme.schema.html +22 -19
- package/dist/schemas.html +11 -8
- package/dist/styles.css +39 -4
- package/package.json +1 -1
- package/plugins-dist/latex-runtime.js +140 -0
- package/plugins-dist/latex.js +178 -0
- package/plugins-dist/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/plugins-dist/slides-charts-lite-runtime.js +179 -0
- package/plugins-dist/slides-charts-lite.js +198 -0
- package/registry/plugins.yaml +40 -1
- package/schemas/v0/site.schema.json +56 -0
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +264 -80
- package/scripts/bundle.ts +188 -17
- package/scripts/dev.ts +294 -171
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/brief.test.ts +151 -0
- package/src/__tests__/build.test.ts +293 -1
- package/src/__tests__/bundle.test.ts +195 -0
- package/src/__tests__/docs.test.ts +117 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
- package/src/__tests__/init.test.ts +51 -2
- package/src/__tests__/latex-runtime.bun.test.ts +35 -0
- package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
- package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
- package/src/__tests__/shared.test.ts +719 -1
- package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
- package/src/cli.ts +124 -22
- package/src/commands/docs.ts +71 -0
- package/src/commands/init.ts +1 -1
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- package/src/shared.ts +1174 -43
- package/src/site/styles.css +39 -4
- package/src/site/template.html +5 -2
- package/src/templates/brief.ts +486 -0
- package/src/templates/deck.ts +59 -0
- package/src/templates/driver.ts +46 -13
- package/src/templates/handbook.ts +32 -0
- package/src/templates/runbook.ts +32 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Data-Driven Content - Kitfly Docs</title>
|
|
7
|
+
<link rel="icon" type="image/png" sizes="32x32" href="../../assets/brand/kitfly-favicon-32.png">
|
|
8
|
+
<link rel="icon" type="image/png" sizes="64x64" href="../../assets/brand/kitfly-neon-256.png">
|
|
9
|
+
<link rel="stylesheet" href="../../styles.css">
|
|
10
|
+
<style id="kitfly-theme">
|
|
11
|
+
:root { --color-bg: #ffffff;
|
|
12
|
+
--color-bg-sidebar: #f5f7f8;
|
|
13
|
+
--color-text: #374151;
|
|
14
|
+
--color-text-muted: #6b7280;
|
|
15
|
+
--color-border: #e5e7eb;
|
|
16
|
+
--color-link: #007182;
|
|
17
|
+
--color-link-hover: #0a6172;
|
|
18
|
+
--color-accent: #152F46;
|
|
19
|
+
--color-code-bg: #f5f7f8;
|
|
20
|
+
--color-logo: #152F46;
|
|
21
|
+
--sidebar-width: 280px;
|
|
22
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
23
|
+
--font-headings: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
24
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
|
|
25
|
+
html { font-size: 16px; }
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
:root:not([data-theme="light"]) { --color-bg: #0d1117;
|
|
28
|
+
--color-bg-sidebar: #152F46;
|
|
29
|
+
--color-text: #e5e7eb;
|
|
30
|
+
--color-text-muted: #9ca3af;
|
|
31
|
+
--color-border: #374151;
|
|
32
|
+
--color-link: #709EA6;
|
|
33
|
+
--color-link-hover: #8fb5bc;
|
|
34
|
+
--color-accent: #f9fafb;
|
|
35
|
+
--color-code-bg: #152F46;
|
|
36
|
+
--color-logo: #f9fafb; }
|
|
37
|
+
}
|
|
38
|
+
[data-theme="dark"] { --color-bg: #0d1117;
|
|
39
|
+
--color-bg-sidebar: #152F46;
|
|
40
|
+
--color-text: #e5e7eb;
|
|
41
|
+
--color-text-muted: #9ca3af;
|
|
42
|
+
--color-border: #374151;
|
|
43
|
+
--color-link: #709EA6;
|
|
44
|
+
--color-link-hover: #8fb5bc;
|
|
45
|
+
--color-accent: #f9fafb;
|
|
46
|
+
--color-code-bg: #152F46;
|
|
47
|
+
--color-logo: #f9fafb; }
|
|
48
|
+
[data-theme="light"] { --color-bg: #ffffff;
|
|
49
|
+
--color-bg-sidebar: #f5f7f8;
|
|
50
|
+
--color-text: #374151;
|
|
51
|
+
--color-text-muted: #6b7280;
|
|
52
|
+
--color-border: #e5e7eb;
|
|
53
|
+
--color-link: #007182;
|
|
54
|
+
--color-link-hover: #0a6172;
|
|
55
|
+
--color-accent: #152F46;
|
|
56
|
+
--color-code-bg: #f5f7f8;
|
|
57
|
+
--color-logo: #152F46;
|
|
58
|
+
--sidebar-width: 280px;
|
|
59
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
60
|
+
--font-headings: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
61
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
|
|
62
|
+
</style>
|
|
63
|
+
<!-- Syntax highlighting - Prism.js -->
|
|
64
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" id="prism-light">
|
|
65
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css" id="prism-dark" disabled>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
// Apply saved theme immediately to prevent flash
|
|
69
|
+
(function() {
|
|
70
|
+
const saved = localStorage.getItem('theme');
|
|
71
|
+
if (saved) {
|
|
72
|
+
document.documentElement.setAttribute('data-theme', saved);
|
|
73
|
+
}
|
|
74
|
+
// Set Prism theme based on saved or system preference
|
|
75
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
76
|
+
const isDark = saved === 'dark' || (!saved && prefersDark);
|
|
77
|
+
if (isDark) {
|
|
78
|
+
document.getElementById('prism-light')?.setAttribute('disabled', '');
|
|
79
|
+
document.getElementById('prism-dark')?.removeAttribute('disabled');
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
</script>
|
|
83
|
+
</head>
|
|
84
|
+
<body class="mode-docs">
|
|
85
|
+
<div class="mobile-header">
|
|
86
|
+
<button class="nav-toggle" onclick="toggleNav()" aria-label="Toggle navigation">
|
|
87
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
88
|
+
<path d="M3 12h18M3 6h18M3 18h18"/>
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
<a href="../../" class="mobile-logo" title="Home" data-initial="K">
|
|
92
|
+
<img src="../../assets/brand/kitfly-neon-256.png" alt="Kitfly" class="logo-img logo-icon" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')">
|
|
93
|
+
</a>
|
|
94
|
+
<button class="mobile-theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
|
|
95
|
+
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
96
|
+
<circle cx="12" cy="12" r="5"/>
|
|
97
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
|
98
|
+
</svg>
|
|
99
|
+
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
100
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
101
|
+
</svg>
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="layout">
|
|
105
|
+
<nav class="sidebar">
|
|
106
|
+
<div class="sidebar-header">
|
|
107
|
+
<div class="logo logo-icon">
|
|
108
|
+
<a href="/" class="logo-icon" data-initial="K">
|
|
109
|
+
<img src="../../assets/brand/kitfly-neon-256.png" alt="Kitfly" class="logo-img" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')">
|
|
110
|
+
</a>
|
|
111
|
+
<span class="logo-text">
|
|
112
|
+
<a href="/" class="brand">Kitfly</a>
|
|
113
|
+
<a href="../../" class="product">Kitfly Docs</a>
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="header-tools">
|
|
117
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
|
|
118
|
+
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
119
|
+
<circle cx="12" cy="12" r="5"/>
|
|
120
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
|
121
|
+
</svg>
|
|
122
|
+
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
123
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
124
|
+
</svg>
|
|
125
|
+
</button>
|
|
126
|
+
<div class="sidebar-meta">
|
|
127
|
+
<span class="meta-version">unversioned</span>
|
|
128
|
+
<span class="meta-branch">main</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="sidebar-nav">
|
|
133
|
+
<ul><li><a href="../../index.html" class="nav-home">Home</a></li><li><span class="nav-section">Guide</span><ul><li><a href="../../content/guide/approaches.html">approaches</a></li><li><a href="../../content/guide/branding.html">branding</a></li><li><a href="../../content/guide/data-driven-content.html" class="active">data-driven-content</a></li><li><a href="../../content/guide/features.html">features</a></li><li><a href="../../content/guide/getting-started.html">getting-started</a></li><li><a href="../../content/guide/kitfly-overview.html">kitfly-overview</a></li></ul></li><li><span class="nav-section">Templates</span><ul><li><a href="../../content/templates/crucible.html">crucible</a></li><li><a href="../../content/templates/handbook.html">handbook</a></li><li><a href="../../content/templates/minimal.html">minimal</a></li><li><a href="../../content/templates/overview.html">overview</a></li><li><a href="../../content/templates/pipeline.html">pipeline</a></li><li><a href="../../content/templates/productbook.html">productbook</a></li><li><a href="../../content/templates/runbook.html">runbook</a></li><li><a href="../../content/templates/servicebook.html">servicebook</a></li></ul></li><li><a href="../../content/reference.html" class="nav-section">Reference</a><ul><li><a href="../../content/reference/configuration.html">configuration</a></li><li><a href="../../content/reference/design-catalog.html">design-catalog</a></li><li><a href="../../content/reference/environment-variables.html">environment-variables</a></li><li><a href="../../content/reference/gantt-widget.html">gantt-widget</a></li><li><a href="../../content/reference/glossary.html">glossary</a></li><li><a href="../../content/reference/key-concepts.html">key-concepts</a></li><li><a href="../../content/reference/plugins.html">plugins</a></li><li><a href="../../content/reference/slides-authoring-guidelines.html">slides-authoring-guidelines</a></li><li><a href="../../content/reference/structure.html">structure</a></li></ul></li><li><a href="../../content/deployment.html" class="nav-section">Deployment</a><ul><li><a href="../../content/deployment/preflight.html">preflight</a></li><li><details><summary class="nav-group">recipes</summary><ul><li><a href="../../content/deployment/recipes/aws-s3.html">aws-s3</a></li><li><a href="../../content/deployment/recipes/cloudflare-pages.html">cloudflare-pages</a></li><li><a href="../../content/deployment/recipes/cloudflare-r2.html">cloudflare-r2</a></li><li><a href="../../content/deployment/recipes/fly-io.html">fly-io</a></li><li><a href="../../content/deployment/recipes/github-pages.html">github-pages</a></li><li><a href="../../content/deployment/recipes/netlify.html">netlify</a></li><li><a href="../../content/deployment/recipes/vercel.html">vercel</a></li></ul></details></li><li><a href="../../content/deployment/secrets-and-env-vars.html">secrets-and-env-vars</a></li></ul></li><li><span class="nav-section">User Guide</span><ul><li><details><summary class="nav-group"><a href="../../docs/userguide/cli.html">cli</a></summary><ul><li><a href="../../docs/userguide/cli/build.html">build</a></li><li><a href="../../docs/userguide/cli/bundle.html">bundle</a></li><li><a href="../../docs/userguide/cli/dev.html">dev</a></li><li><a href="../../docs/userguide/cli/init.html">init</a></li><li><a href="../../docs/userguide/cli/servers.html">servers</a></li><li><a href="../../docs/userguide/cli/stop.html">stop</a></li><li><a href="../../docs/userguide/cli/update.html">update</a></li><li><a href="../../docs/userguide/cli/version.html">version</a></li></ul></details></li><li><a href="../../docs/userguide/sharing.html">sharing</a></li></ul></li><li><span class="nav-section">Decisions</span><ul><li><a href="../../docs/decisions/ADR-0001-minimalist-site-code.html">ADR-0001-minimalist-site-code</a></li><li><a href="../../docs/decisions/ADR-0002-ai-accessibility.html">ADR-0002-ai-accessibility</a></li><li><a href="../../docs/decisions/ADR-0003-single-file-bundle.html">ADR-0003-single-file-bundle</a></li><li><a href="../../docs/decisions/ADR-0004-bun-runtime.html">ADR-0004-bun-runtime</a></li><li><a href="../../docs/decisions/ADR-0005-plugin-contract-and-distribution.html">ADR-0005-plugin-contract-and-distribution</a></li><li><a href="../../docs/decisions/ADR-0006-data-driven-content.html">ADR-0006-data-driven-content</a></li><li><a href="../../docs/decisions/DDR-0001-viewport-locked-layout.html">DDR-0001-viewport-locked-layout</a></li><li><a href="../../docs/decisions/DDR-0002-theme-system.html">DDR-0002-theme-system</a></li><li><a href="../../docs/decisions/DDR-0003-bounded-logo-slot.html">DDR-0003-bounded-logo-slot</a></li><li><a href="../../docs/decisions/DDR-0004-slides-rendering-model.html">DDR-0004-slides-rendering-model</a></li><li><a href="../../docs/decisions/DDR-0005-deterministic-layout-boundary.html">DDR-0005-deterministic-layout-boundary</a></li></ul></li><li><a href="../../schemas.html" class="nav-section">Schemas</a><ul><li><a href="../../schemas/plugin-registry.schema.html">plugin-registry.schema</a></li><li><a href="../../schemas/plugin-schemas-notes.html">plugin-schemas-notes</a></li><li><a href="../../schemas/plugin.schema.html">plugin.schema</a></li><li><a href="../../schemas/plugins.schema.html">plugins.schema</a></li><li><details><summary class="nav-group">v0</summary><ul><li><a href="../../schemas/v0/common.schema.html">common.schema</a></li><li><a href="../../schemas/v0/plugin-registry.schema.html">plugin-registry.schema</a></li><li><a href="../../schemas/v0/plugin.schema.html">plugin.schema</a></li><li><a href="../../schemas/v0/plugins.schema.html">plugins.schema</a></li><li><a href="../../schemas/v0/site.schema.html">site.schema</a></li><li><a href="../../schemas/v0/theme.schema.html">theme.schema</a></li></ul></details></li></ul></li></ul>
|
|
134
|
+
</div>
|
|
135
|
+
</nav>
|
|
136
|
+
<main class="content">
|
|
137
|
+
<article class="prose">
|
|
138
|
+
<nav class="breadcrumbs"><a href="../../content/guide/approaches.html">Content</a><span class="separator">›</span><a href="../../content/guide/approaches.html">Guide</a><span class="separator">›</span><span>data-driven-content</span></nav>
|
|
139
|
+
<div class="page-meta">Last updated: 2026-02-17</div>
|
|
140
|
+
<h1 id="data-driven-content">Data-Driven Content</h1>
|
|
141
|
+
<p>Some pages are driven by structured data — pricing tables, team rosters, metrics dashboards, product catalogs. Data-driven content lets you separate prose (markdown), computation (generators), and data (YAML/JSON files) so each layer does what it's good at.</p>
|
|
142
|
+
<h2 id="when-to-use-this">When to use this</h2>
|
|
143
|
+
<p>Use data bindings when:</p>
|
|
144
|
+
<ul>
|
|
145
|
+
<li>The same values appear in multiple places on a page (DRY)</li>
|
|
146
|
+
<li>A generator computes values that appear in prose (pricing math, discount brackets)</li>
|
|
147
|
+
<li>Tables or blocks are produced by scripts and injected into narrative content</li>
|
|
148
|
+
</ul>
|
|
149
|
+
<p>Don't use data bindings when:</p>
|
|
150
|
+
<ul>
|
|
151
|
+
<li>The page is pure prose with no external data</li>
|
|
152
|
+
<li>You need loops, conditionals, or expressions — use a generator to produce the final markdown instead</li>
|
|
153
|
+
</ul>
|
|
154
|
+
<h2 id="how-it-works">How it works</h2>
|
|
155
|
+
<pre><code>raw input → generator → data file → kitfly bindings → markdown → HTML
|
|
156
|
+
</code></pre>
|
|
157
|
+
<ol>
|
|
158
|
+
<li>A <strong>generator</strong> (any script) reads raw input and writes a data file</li>
|
|
159
|
+
<li>A <strong>data file</strong> (YAML or JSON) declares globals, per-page values, and snippets</li>
|
|
160
|
+
<li><strong>Markdown templates</strong> use <code>{{ key }}</code> and <code>{{ snippet:name }}</code> placeholders</li>
|
|
161
|
+
<li>Kitfly resolves bindings at build time, before markdown rendering</li>
|
|
162
|
+
</ol>
|
|
163
|
+
<h2 id="data-file-structure">Data file structure</h2>
|
|
164
|
+
<pre><code class="language-yaml"># data/pricing.yaml
|
|
165
|
+
globals:
|
|
166
|
+
company: "Acme Corp"
|
|
167
|
+
baseline_rate: "200"
|
|
168
|
+
credit_validity: "12"
|
|
169
|
+
|
|
170
|
+
pages:
|
|
171
|
+
- path: content/product/pricing.md
|
|
172
|
+
inject:
|
|
173
|
+
hero: "Implementation and operating costs"
|
|
174
|
+
discount_range: "5–20%"
|
|
175
|
+
snippets:
|
|
176
|
+
- slot: pricing-table
|
|
177
|
+
content: |
|
|
178
|
+
| Tier | Price | Features |
|
|
179
|
+
|------|-------|----------|
|
|
180
|
+
| Basic | $10/mo | Essentials |
|
|
181
|
+
| Pro | $50/mo | Full access |
|
|
182
|
+
</code></pre>
|
|
183
|
+
<p><strong>Path convention:</strong> <code>pages[].path</code> must be relative to site root including the <code>content/</code> prefix. Use <code>content/product/pricing.md</code>, not <code>product/pricing.md</code>. Kitfly's error messages include the expected path if there's a mismatch.</p>
|
|
184
|
+
<h3 id="resolution-order">Resolution order</h3>
|
|
185
|
+
<p>For <code>{{ key }}</code>: page <code>inject</code> first, then <code>globals</code>. Page-level values shadow globals on key collision.</p>
|
|
186
|
+
<p>For <code>{{ snippet:name }}</code>: matched by <code>slot</code> name from the page's <code>snippets</code> array.</p>
|
|
187
|
+
<h2 id="binding-syntax">Binding syntax</h2>
|
|
188
|
+
<h3 id="value-substitution">Value substitution</h3>
|
|
189
|
+
<pre><code class="language-markdown">Implementation rate: **{{ baseline_rate | dollar }}/hour**
|
|
190
|
+
|
|
191
|
+
Credits valid for {{ credit_validity }} months.
|
|
192
|
+
</code></pre>
|
|
193
|
+
<h3 id="snippet-injection">Snippet injection</h3>
|
|
194
|
+
<pre><code class="language-markdown">## Pricing Tiers
|
|
195
|
+
|
|
196
|
+
{{ snippet:pricing-table }}
|
|
197
|
+
</code></pre>
|
|
198
|
+
<p>Snippets are injected verbatim as markdown. After all bindings resolve, the full document passes through the markdown renderer as usual.</p>
|
|
199
|
+
<h2 id="formatters">Formatters</h2>
|
|
200
|
+
<p>Apply with pipe syntax. Chaining composes left to right: <code>{{ key | round(0) | dollar }}</code>.</p>
|
|
201
|
+
<table>
|
|
202
|
+
<thead>
|
|
203
|
+
<tr>
|
|
204
|
+
<th>Formatter</th>
|
|
205
|
+
<th>Input</th>
|
|
206
|
+
<th>Output</th>
|
|
207
|
+
<th>Notes</th>
|
|
208
|
+
</tr>
|
|
209
|
+
</thead>
|
|
210
|
+
<tbody><tr>
|
|
211
|
+
<td><code>dollar</code></td>
|
|
212
|
+
<td><code>"1500"</code></td>
|
|
213
|
+
<td><code>$1,500</code></td>
|
|
214
|
+
<td>USD format; cents only if fractional</td>
|
|
215
|
+
</tr>
|
|
216
|
+
<tr>
|
|
217
|
+
<td><code>number</code></td>
|
|
218
|
+
<td><code>"2500"</code></td>
|
|
219
|
+
<td><code>2,500</code></td>
|
|
220
|
+
<td>Comma-separated</td>
|
|
221
|
+
</tr>
|
|
222
|
+
<tr>
|
|
223
|
+
<td><code>percent</code></td>
|
|
224
|
+
<td><code>"0.15"</code></td>
|
|
225
|
+
<td><code>15%</code></td>
|
|
226
|
+
<td><strong>Input must be a decimal ratio (0.0–1.0)</strong></td>
|
|
227
|
+
</tr>
|
|
228
|
+
<tr>
|
|
229
|
+
<td><code>round(n)</code></td>
|
|
230
|
+
<td><code>"3.14159"</code></td>
|
|
231
|
+
<td><code>3.14</code></td>
|
|
232
|
+
<td>Round to n decimal places</td>
|
|
233
|
+
</tr>
|
|
234
|
+
<tr>
|
|
235
|
+
<td><code>upper</code></td>
|
|
236
|
+
<td><code>"hello"</code></td>
|
|
237
|
+
<td><code>HELLO</code></td>
|
|
238
|
+
<td>Uppercase</td>
|
|
239
|
+
</tr>
|
|
240
|
+
<tr>
|
|
241
|
+
<td><code>lower</code></td>
|
|
242
|
+
<td><code>"HELLO"</code></td>
|
|
243
|
+
<td><code>hello</code></td>
|
|
244
|
+
<td>Lowercase</td>
|
|
245
|
+
</tr>
|
|
246
|
+
</tbody></table>
|
|
247
|
+
<p><strong><code>percent</code> expects a decimal ratio.</strong> <code>"0.15"</code> becomes <code>15%</code>. If your generator already computes integer percentages (like <code>"5"</code> for 5%), store them as pre-formatted strings (<code>"5%"</code>) and don't pipe through <code>percent</code> — that would yield <code>500%</code>.</p>
|
|
248
|
+
<p>All formatters are pure functions: string in, string out. The set is closed — adding a formatter requires a kitfly code change.</p>
|
|
249
|
+
<h2 id="error-handling">Error handling</h2>
|
|
250
|
+
<p>Kitfly fails loud on binding errors. These are build errors, not warnings:</p>
|
|
251
|
+
<table>
|
|
252
|
+
<thead>
|
|
253
|
+
<tr>
|
|
254
|
+
<th>Condition</th>
|
|
255
|
+
<th>Error message</th>
|
|
256
|
+
</tr>
|
|
257
|
+
</thead>
|
|
258
|
+
<tbody><tr>
|
|
259
|
+
<td>Data file not found</td>
|
|
260
|
+
<td><code>data file not found: data/pricing.yaml</code></td>
|
|
261
|
+
</tr>
|
|
262
|
+
<tr>
|
|
263
|
+
<td>Unresolved <code>{{ key }}</code></td>
|
|
264
|
+
<td><code>unresolved binding "key" in content/product/pricing.md</code></td>
|
|
265
|
+
</tr>
|
|
266
|
+
<tr>
|
|
267
|
+
<td>Unknown snippet slot</td>
|
|
268
|
+
<td><code>unknown snippet "name" in content/product/pricing.md</code></td>
|
|
269
|
+
</tr>
|
|
270
|
+
<tr>
|
|
271
|
+
<td>Formatter parse failure</td>
|
|
272
|
+
<td><code>dollar formatter: "hello" is not a number ...</code></td>
|
|
273
|
+
</tr>
|
|
274
|
+
<tr>
|
|
275
|
+
<td>Unknown formatter</td>
|
|
276
|
+
<td><code>unknown formatter "custom_fn" ...</code></td>
|
|
277
|
+
</tr>
|
|
278
|
+
<tr>
|
|
279
|
+
<td>Path escapes kitsite</td>
|
|
280
|
+
<td><code>data path escapes kitsite: ../secrets/data.yaml</code></td>
|
|
281
|
+
</tr>
|
|
282
|
+
</tbody></table>
|
|
283
|
+
<h2 id="pre-build-hooks">Pre-build hooks</h2>
|
|
284
|
+
<p>Declare generators in <code>site.yaml</code>:</p>
|
|
285
|
+
<pre><code class="language-yaml">prebuild:
|
|
286
|
+
- command: "bun run scripts/generate-pricing-data.ts"
|
|
287
|
+
watch: ["data/raw/pricing-input.json"]
|
|
288
|
+
- command: "bun run scripts/generate-team-data.ts"
|
|
289
|
+
</code></pre>
|
|
290
|
+
<table>
|
|
291
|
+
<thead>
|
|
292
|
+
<tr>
|
|
293
|
+
<th>Context</th>
|
|
294
|
+
<th>When hooks run</th>
|
|
295
|
+
</tr>
|
|
296
|
+
</thead>
|
|
297
|
+
<tbody><tr>
|
|
298
|
+
<td><code>bun run dev</code></td>
|
|
299
|
+
<td>Once at startup, then again when watched files change</td>
|
|
300
|
+
</tr>
|
|
301
|
+
<tr>
|
|
302
|
+
<td><code>bun run build</code></td>
|
|
303
|
+
<td>Once before build starts</td>
|
|
304
|
+
</tr>
|
|
305
|
+
<tr>
|
|
306
|
+
<td><code>bun run bundle</code></td>
|
|
307
|
+
<td>Once before bundle starts</td>
|
|
308
|
+
</tr>
|
|
309
|
+
</tbody></table>
|
|
310
|
+
<p>Hooks run sequentially in declared order. A non-zero exit code halts the build with the hook's stderr as error context.</p>
|
|
311
|
+
<h3 id="watch-flow-in-dev-mode">Watch flow in dev mode</h3>
|
|
312
|
+
<pre><code>watched file changes → re-run matching hook → hook writes to data/ → data/ change triggers rebuild
|
|
313
|
+
</code></pre>
|
|
314
|
+
<p>Content changes (edits to markdown) do NOT re-run hooks. Only watched file changes trigger hooks.</p>
|
|
315
|
+
<h2 id="schema-validation-optional">Schema validation (optional)</h2>
|
|
316
|
+
<p>If <code>data/pricing.schema.json</code> exists alongside <code>data/pricing.yaml</code>, kitfly validates the data at build time. JSON Schema structural checks cover required fields, value types, and pattern constraints.</p>
|
|
317
|
+
<p>Missing schema files are not an error — validation is opt-in.</p>
|
|
318
|
+
<h2 id="generator-best-practices">Generator best practices</h2>
|
|
319
|
+
<p>These patterns emerged from real-world usage and apply to any non-trivial generator.</p>
|
|
320
|
+
<h3 id="the-template-is-the-contract">The template is the contract</h3>
|
|
321
|
+
<p>Every <code>{{ snippet:X }}</code> in a markdown template means the generator must always emit a snippet named <code>X</code> in the data file, even if the content is empty. Kitfly treats missing snippets as build errors — by design.</p>
|
|
322
|
+
<p>If a section is conditionally relevant (e.g. an implementation-tiers table that only applies to multi-tier configurations), the generator should emit the snippet with empty string content when the section doesn't apply. The blank line in the rendered output is acceptable and avoids conditional logic in the template.</p>
|
|
323
|
+
<pre><code class="language-yaml"># Generator always emits this, even when single-tier
|
|
324
|
+
snippets:
|
|
325
|
+
- slot: implementation-tiers
|
|
326
|
+
content: ""
|
|
327
|
+
</code></pre>
|
|
328
|
+
<h3 id="use-json-for-generator-output">Use JSON for generator output</h3>
|
|
329
|
+
<p>Generators should write JSON data files. <code>JSON.stringify</code> is deterministic in every language, there's no quoting ambiguity, and multiline strings use unambiguous <code>\n</code> escapes.</p>
|
|
330
|
+
<p>Reserve YAML for hand-authored data files (team rosters, simple config) where human readability matters.</p>
|
|
331
|
+
<h3 id="separate-raw-input-from-kitfly-data">Separate raw input from kitfly data</h3>
|
|
332
|
+
<p>Keep generator source files in <code>data/raw/</code> and kitfly data files in <code>data/</code>:</p>
|
|
333
|
+
<pre><code>data/
|
|
334
|
+
raw/
|
|
335
|
+
pricing-input.json ← generator reads this
|
|
336
|
+
team.csv ← generator reads this
|
|
337
|
+
pricing.json ← generator writes this (kitfly reads it)
|
|
338
|
+
team.json ← generator writes this (kitfly reads it)
|
|
339
|
+
</code></pre>
|
|
340
|
+
<p>This prevents naming collisions and makes the data flow visible. The <code>prebuild:</code> hook <code>watch:</code> patterns point at <code>data/raw/</code> sources, and kitfly reads the generated files from <code>data/</code>.</p>
|
|
341
|
+
<h3 id="generator-language">Generator language</h3>
|
|
342
|
+
<p>Generators can be written in any language. Kitfly runs them as shell commands. The motivating use case was a Python generator in an otherwise TypeScript/Bun ecosystem — pre-build hooks eliminate the manual step and integrate it into the dev/build pipeline regardless of language.</p>
|
|
343
|
+
<h2 id="interaction-with-content-profiles">Interaction with content profiles</h2>
|
|
344
|
+
<p>Data bindings and content profiles are independent features that compose naturally:</p>
|
|
345
|
+
<ul>
|
|
346
|
+
<li>Profile filtering runs first (after file collection, before rendering)</li>
|
|
347
|
+
<li>Data binding resolution runs second (after reading markdown, before markdown rendering)</li>
|
|
348
|
+
<li>A page excluded by profile filtering is never read, so its bindings are never resolved</li>
|
|
349
|
+
<li><code>KITFLY_PROFILE</code> is passed to pre-build hooks so generators can adapt output per profile</li>
|
|
350
|
+
</ul>
|
|
351
|
+
<h2 id="backwards-compatibility">Backwards compatibility</h2>
|
|
352
|
+
<ul>
|
|
353
|
+
<li>Pages without <code>data:</code> frontmatter: no binding resolution, literal <code>{{ }}</code> passes through unchanged</li>
|
|
354
|
+
<li>Sites without <code>prebuild:</code> in site.yaml: no hooks run, zero overhead</li>
|
|
355
|
+
<li>Sites without a <code>data/</code> directory: no binding resolution, zero overhead</li>
|
|
356
|
+
</ul>
|
|
357
|
+
|
|
358
|
+
</article>
|
|
359
|
+
<aside class="toc"><span class="toc-title">On this page</span><ul><li><a href="#when-to-use-this">When to use this</a></li><li><a href="#how-it-works">How it works</a></li><li><a href="#data-file-structure">Data file structure</a></li><li class="toc-h3"><a href="#resolution-order">Resolution order</a></li><li><a href="#binding-syntax">Binding syntax</a></li><li class="toc-h3"><a href="#value-substitution">Value substitution</a></li><li class="toc-h3"><a href="#snippet-injection">Snippet injection</a></li><li><a href="#formatters">Formatters</a></li><li><a href="#error-handling">Error handling</a></li><li><a href="#pre-build-hooks">Pre-build hooks</a></li><li class="toc-h3"><a href="#watch-flow-in-dev-mode">Watch flow in dev mode</a></li><li><a href="#schema-validation-optional">Schema validation (optional)</a></li><li><a href="#generator-best-practices">Generator best practices</a></li><li class="toc-h3"><a href="#the-template-is-the-contract">The template is the contract</a></li><li class="toc-h3"><a href="#use-json-for-generator-output">Use JSON for generator output</a></li><li class="toc-h3"><a href="#separate-raw-input-from-kitfly-data">Separate raw input from kitfly data</a></li><li class="toc-h3"><a href="#generator-language">Generator language</a></li><li><a href="#interaction-with-content-profiles">Interaction with content profiles</a></li><li><a href="#backwards-compatibility">Backwards compatibility</a></li></ul></aside>
|
|
360
|
+
</main>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<footer class="site-footer">
|
|
364
|
+
<div class="footer-content">
|
|
365
|
+
<div class="footer-left">
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
<span class="footer-commit" title="Commit: 4062660">Published 2026-03-09</span>
|
|
369
|
+
</div>
|
|
370
|
+
<div class="footer-center">
|
|
371
|
+
<span class="footer-copyright"><a href="https://3leaps.net" class="footer-link">© 2026 3 Leaps, LLC</a></span>
|
|
372
|
+
<span class="footer-separator">·</span><a href="/" class="footer-link">Kitfly</a>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="footer-right">
|
|
375
|
+
<a href="https://kitfly.dev" class="footer-link">Built with Kitfly</a>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</footer>
|
|
379
|
+
<!-- Syntax highlighting - Prism.js -->
|
|
380
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js"></script>
|
|
381
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"></script>
|
|
382
|
+
<!-- Mermaid diagram support -->
|
|
383
|
+
<script type="module">
|
|
384
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
385
|
+
|
|
386
|
+
function getMermaidTheme() {
|
|
387
|
+
const theme = document.documentElement.getAttribute('data-theme');
|
|
388
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
389
|
+
const isDark = theme === 'dark' || (!theme && prefersDark);
|
|
390
|
+
return isDark ? 'dark' : 'neutral';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
mermaid.initialize({
|
|
394
|
+
startOnLoad: true,
|
|
395
|
+
theme: getMermaidTheme()
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Re-render mermaid diagrams when theme changes
|
|
399
|
+
window.reinitMermaid = async function() {
|
|
400
|
+
mermaid.initialize({ startOnLoad: false, theme: getMermaidTheme() });
|
|
401
|
+
const diagrams = document.querySelectorAll('.mermaid');
|
|
402
|
+
for (const el of diagrams) {
|
|
403
|
+
const code = el.getAttribute('data-mermaid-source');
|
|
404
|
+
if (code) {
|
|
405
|
+
el.innerHTML = code;
|
|
406
|
+
el.removeAttribute('data-processed');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
await mermaid.run({ nodes: diagrams });
|
|
410
|
+
};
|
|
411
|
+
</script>
|
|
412
|
+
|
|
413
|
+
<script>
|
|
414
|
+
function toggleTheme() {
|
|
415
|
+
const html = document.documentElement;
|
|
416
|
+
const current = html.getAttribute('data-theme');
|
|
417
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
418
|
+
|
|
419
|
+
let next;
|
|
420
|
+
if (current === 'dark') {
|
|
421
|
+
next = 'light';
|
|
422
|
+
} else if (current === 'light') {
|
|
423
|
+
next = 'dark';
|
|
424
|
+
} else {
|
|
425
|
+
// No explicit theme set, toggle from system preference
|
|
426
|
+
next = prefersDark ? 'light' : 'dark';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
html.setAttribute('data-theme', next);
|
|
430
|
+
localStorage.setItem('theme', next);
|
|
431
|
+
|
|
432
|
+
// Switch Prism theme
|
|
433
|
+
const prismLight = document.getElementById('prism-light');
|
|
434
|
+
const prismDark = document.getElementById('prism-dark');
|
|
435
|
+
if (next === 'dark') {
|
|
436
|
+
prismLight?.setAttribute('disabled', '');
|
|
437
|
+
prismDark?.removeAttribute('disabled');
|
|
438
|
+
} else {
|
|
439
|
+
prismLight?.removeAttribute('disabled');
|
|
440
|
+
prismDark?.setAttribute('disabled', '');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Re-render mermaid diagrams with new theme
|
|
444
|
+
if (window.reinitMermaid) {
|
|
445
|
+
window.reinitMermaid();
|
|
446
|
+
}
|
|
447
|
+
if (window.reinitCharts) {
|
|
448
|
+
window.reinitCharts();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Slides mode hash routing
|
|
453
|
+
(function initSlidesMode() {
|
|
454
|
+
const shell = document.querySelector('.slides-shell');
|
|
455
|
+
if (!shell) return;
|
|
456
|
+
|
|
457
|
+
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
458
|
+
if (!slides.length) return;
|
|
459
|
+
|
|
460
|
+
const prevBtn = document.querySelector('.slide-prev');
|
|
461
|
+
const nextBtn = document.querySelector('.slide-next');
|
|
462
|
+
const counter = document.querySelector('.slide-counter');
|
|
463
|
+
const progressBar = document.querySelector('.slide-progress-bar');
|
|
464
|
+
const navLinks = Array.from(document.querySelectorAll('.sidebar-nav a[href^="#slide-"]'));
|
|
465
|
+
let current = 0;
|
|
466
|
+
|
|
467
|
+
function setActive(n) {
|
|
468
|
+
current = Math.max(0, Math.min(n, slides.length - 1));
|
|
469
|
+
slides.forEach((slide, idx) => slide.classList.toggle('active', idx === current));
|
|
470
|
+
navLinks.forEach((link) => {
|
|
471
|
+
const active = link.getAttribute('href') === '#' + slides[current].id;
|
|
472
|
+
link.classList.toggle('active', active);
|
|
473
|
+
});
|
|
474
|
+
if (counter) counter.textContent = (current + 1) + ' / ' + slides.length;
|
|
475
|
+
if (progressBar) progressBar.style.width = (((current + 1) / slides.length) * 100) + '%';
|
|
476
|
+
if (prevBtn) prevBtn.disabled = current === 0;
|
|
477
|
+
if (nextBtn) nextBtn.disabled = current === slides.length - 1;
|
|
478
|
+
history.replaceState(null, '', '#' + slides[current].id);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function setFromHash() {
|
|
482
|
+
const hash = window.location.hash || '';
|
|
483
|
+
const idx = slides.findIndex((s) => '#' + s.id === hash);
|
|
484
|
+
if (idx >= 0) setActive(idx);
|
|
485
|
+
else setActive(0);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
prevBtn?.addEventListener('click', () => setActive(current - 1));
|
|
489
|
+
nextBtn?.addEventListener('click', () => setActive(current + 1));
|
|
490
|
+
|
|
491
|
+
document.addEventListener('keydown', (e) => {
|
|
492
|
+
if (e.key === 'ArrowRight' || e.key === ' ') {
|
|
493
|
+
e.preventDefault();
|
|
494
|
+
setActive(current + 1);
|
|
495
|
+
} else if (e.key === 'ArrowLeft') {
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
setActive(current - 1);
|
|
498
|
+
} else if (e.key === 'Home') {
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
setActive(0);
|
|
501
|
+
} else if (e.key === 'End') {
|
|
502
|
+
e.preventDefault();
|
|
503
|
+
setActive(slides.length - 1);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
window.addEventListener('hashchange', setFromHash);
|
|
508
|
+
setFromHash();
|
|
509
|
+
})();
|
|
510
|
+
|
|
511
|
+
// Copy code button
|
|
512
|
+
document.querySelectorAll('.prose pre code').forEach(block => {
|
|
513
|
+
const button = document.createElement('button');
|
|
514
|
+
button.className = 'copy-button';
|
|
515
|
+
button.textContent = 'Copy';
|
|
516
|
+
button.onclick = async () => {
|
|
517
|
+
await navigator.clipboard.writeText(block.textContent);
|
|
518
|
+
button.textContent = 'Copied!';
|
|
519
|
+
setTimeout(() => button.textContent = 'Copy', 2000);
|
|
520
|
+
};
|
|
521
|
+
block.parentElement.appendChild(button);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Mobile nav toggle
|
|
525
|
+
function toggleNav() {
|
|
526
|
+
document.querySelector('.sidebar').classList.toggle('open');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Close nav when clicking outside on mobile
|
|
530
|
+
document.addEventListener('click', (e) => {
|
|
531
|
+
const sidebar = document.querySelector('.sidebar');
|
|
532
|
+
const toggle = document.querySelector('.nav-toggle');
|
|
533
|
+
if (sidebar.classList.contains('open') &&
|
|
534
|
+
!sidebar.contains(e.target) &&
|
|
535
|
+
!toggle.contains(e.target)) {
|
|
536
|
+
sidebar.classList.remove('open');
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
</script>
|
|
540
|
+
|
|
541
|
+
</body>
|
|
542
|
+
</html>
|