jekyll-theme-zer0 1.28.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1143 -13
  3. data/_data/README.md +2 -0
  4. data/_data/ai.yml +5 -3
  5. data/_data/ai_pricing.yml +36 -0
  6. data/_data/backlog.yml +507 -2
  7. data/_data/consumers.yml +157 -9
  8. data/_data/features.yml +303 -20
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/i18n/fr.yml +12 -7
  11. data/_data/i18n/manifest.yml +39 -12
  12. data/_data/ingredient_densities.yml +122 -0
  13. data/_data/landing.yml +5 -2
  14. data/_data/navigation/main.yml +16 -0
  15. data/_data/navigation/quickstart.yml +4 -0
  16. data/_data/recipe_courses.yml +64 -0
  17. data/_data/site_builder.yml +874 -0
  18. data/_data/theme-manifest.yml +160 -124
  19. data/_data/ui-text.yml +26 -0
  20. data/_includes/README.md +26 -2
  21. data/_includes/analytics/posthog.html +2 -2
  22. data/_includes/components/admin-links.html +2 -2
  23. data/_includes/components/admin-tabs.html +2 -2
  24. data/_includes/components/ai-chat.html +14 -11
  25. data/_includes/components/analytics-dashboard.html +8 -8
  26. data/_includes/components/author-bio.html +1 -1
  27. data/_includes/components/author-card.html +10 -2
  28. data/_includes/components/author-eeat.html +4 -4
  29. data/_includes/components/background-customizer.html +10 -10
  30. data/_includes/components/background-image.html +114 -0
  31. data/_includes/components/background-settings.html +28 -15
  32. data/_includes/components/collection-manager.html +5 -5
  33. data/_includes/components/component-showcase.html +13 -13
  34. data/_includes/components/config-editor.html +12 -12
  35. data/_includes/components/config-viewer.html +8 -8
  36. data/_includes/components/cookie-consent.html +15 -15
  37. data/_includes/components/cta-button.html +7 -2
  38. data/_includes/components/dev-shortcuts.html +7 -7
  39. data/_includes/components/env-dashboard.html +8 -8
  40. data/_includes/components/env-switcher.html +9 -9
  41. data/_includes/components/feature-card.html +2 -2
  42. data/_includes/components/halfmoon.html +2 -2
  43. data/_includes/components/info-section.html +42 -37
  44. data/_includes/components/js-cdn.html +15 -15
  45. data/_includes/components/language-toggle.html +168 -21
  46. data/_includes/components/mermaid.html +72 -435
  47. data/_includes/components/nanobar.html +5 -5
  48. data/_includes/components/nav-editor.html +2 -2
  49. data/_includes/components/nav-export.html +2 -2
  50. data/_includes/components/nav-overview.html +2 -2
  51. data/_includes/components/page-feedback.html +45 -30
  52. data/_includes/components/page-views-init.html +55 -0
  53. data/_includes/components/page-views.html +33 -0
  54. data/_includes/components/post-card.html +22 -22
  55. data/_includes/components/post-type-badge.html +2 -2
  56. data/_includes/components/powered-by.html +2 -2
  57. data/_includes/components/preview-image.html +6 -0
  58. data/_includes/components/quick-index.html +2 -2
  59. data/_includes/components/recipe-card.html +67 -0
  60. data/_includes/components/recipe-duration.html +50 -0
  61. data/_includes/components/recipe-grams.html +58 -0
  62. data/_includes/components/recipe-index.html +96 -0
  63. data/_includes/components/recipe-ingredients.html +90 -0
  64. data/_includes/components/recipe-meta.html +96 -0
  65. data/_includes/components/recipe-nutrition.html +57 -0
  66. data/_includes/components/recipe-qty.html +73 -0
  67. data/_includes/components/recipe-ratio.html +151 -0
  68. data/_includes/components/recipe-scaler.html +73 -0
  69. data/_includes/components/recipe-steps.html +86 -0
  70. data/_includes/components/recipe-temp.html +45 -0
  71. data/_includes/components/search-modal.html +29 -4
  72. data/_includes/components/searchbar.html +2 -2
  73. data/_includes/components/shortcuts-modal.html +3 -0
  74. data/_includes/components/svg-background.html +2 -2
  75. data/_includes/components/theme-customizer.html +2 -2
  76. data/_includes/components/theme-info.html +14 -7
  77. data/_includes/components/theme-preview-gallery.html +22 -22
  78. data/_includes/content/giscus.html +2 -2
  79. data/_includes/content/intro.html +8 -8
  80. data/_includes/content/jsonld-faq.html +2 -2
  81. data/_includes/content/jsonld-software.html +24 -5
  82. data/_includes/content/seo.html +4 -4
  83. data/_includes/content/sitemap.html +27 -27
  84. data/_includes/content/toc.html +183 -183
  85. data/_includes/core/branding.html +6 -6
  86. data/_includes/core/console-capture.html +32 -74
  87. data/_includes/core/favicon.html +49 -7
  88. data/_includes/core/footer-fabs.html +17 -3
  89. data/_includes/core/footer.html +49 -34
  90. data/_includes/core/head.html +110 -86
  91. data/_includes/core/header.html +76 -54
  92. data/_includes/docs/bootstrap-docs.html +8 -8
  93. data/_includes/landing/landing-install-cards.html +2 -2
  94. data/_includes/landing/landing-quick-links.html +1 -1
  95. data/_includes/navigation/admin-nav.html +2 -2
  96. data/_includes/navigation/nav-tree.html +8 -8
  97. data/_includes/navigation/navbar.html +12 -12
  98. data/_includes/navigation/section-sidebar.html +109 -27
  99. data/_includes/navigation/sidebar-config.html +36 -2
  100. data/_includes/navigation/sidebar-left.html +17 -16
  101. data/_includes/navigation/sidebar-right.html +8 -7
  102. data/_includes/obsidian/full-graph.html +2 -2
  103. data/_includes/setup/claude-session.html +72 -0
  104. data/_includes/setup/prereq-checklist.html +90 -0
  105. data/_includes/setup/wizard.html +924 -222
  106. data/_includes/stats/stats-categories.html +8 -8
  107. data/_includes/stats/stats-header.html +14 -14
  108. data/_includes/stats/stats-metrics.html +14 -14
  109. data/_includes/stats/stats-no-data.html +12 -12
  110. data/_includes/stats/stats-overview.html +6 -6
  111. data/_includes/stats/stats-tags.html +8 -8
  112. data/_layouts/404.html +38 -24
  113. data/_layouts/README.md +2 -0
  114. data/_layouts/admin.html +24 -24
  115. data/_layouts/article.html +43 -33
  116. data/_layouts/author.html +20 -20
  117. data/_layouts/authors.html +2 -2
  118. data/_layouts/book-abc.html +12 -12
  119. data/_layouts/book-story.html +15 -15
  120. data/_layouts/book.html +12 -12
  121. data/_layouts/collection.html +33 -33
  122. data/_layouts/cookbook.html +88 -0
  123. data/_layouts/default.html +31 -28
  124. data/_layouts/home.html +23 -23
  125. data/_layouts/index.html +10 -10
  126. data/_layouts/landing.html +17 -17
  127. data/_layouts/news.html +44 -44
  128. data/_layouts/note.html +38 -38
  129. data/_layouts/notebook.html +34 -34
  130. data/_layouts/recipe.html +274 -0
  131. data/_layouts/root.html +92 -55
  132. data/_layouts/section.html +62 -33
  133. data/_layouts/setup.html +3 -3
  134. data/_layouts/sitemap-collection.html +49 -49
  135. data/_layouts/stats.html +40 -40
  136. data/_layouts/tag.html +12 -12
  137. data/_layouts/welcome.html +21 -21
  138. data/_sass/components/_callout.scss +1 -1
  139. data/_sass/components/_footer.scss +37 -1
  140. data/_sass/components/_mermaid.scss +375 -0
  141. data/_sass/components/_page-views.scss +36 -0
  142. data/_sass/components/_recipe.scss +506 -0
  143. data/_sass/components/_setup-wizard.scss +764 -0
  144. data/_sass/components/_ui-enhancements.scss +6 -6
  145. data/_sass/core/_navbar.scss +261 -46
  146. data/_sass/layouts/_landing.scss +2 -2
  147. data/_sass/layouts/_navbar-extras.scss +14 -4
  148. data/_sass/tokens/_color.scss +6 -0
  149. data/_sass/tokens/_index.scss +2 -0
  150. data/_sass/tokens/_radius.scss +21 -0
  151. data/_sass/tokens/_typography.scss +4 -0
  152. data/_sass/utilities/_focus.scss +14 -0
  153. data/assets/css/main.scss +4 -0
  154. data/assets/js/ai-chat.js +47 -5
  155. data/assets/js/fleet-feedback-capture.js +124 -0
  156. data/assets/js/fleet-feedback.js +853 -0
  157. data/assets/js/mermaid-diagrams.js +1267 -0
  158. data/assets/js/modules/navigation/config.js +9 -6
  159. data/assets/js/modules/navigation/navbar.js +55 -0
  160. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  161. data/assets/js/modules/theme/appearance.js +8 -2
  162. data/assets/js/obsidian-wiki-links.js +8 -3
  163. data/assets/js/page-feedback.js +125 -192
  164. data/assets/js/page-views.js +372 -0
  165. data/assets/js/recipe-scaler.js +501 -0
  166. data/assets/js/search-modal.js +36 -0
  167. data/assets/js/setup-wizard.js +2279 -226
  168. data/assets/js/site-builder.js +1834 -0
  169. data/assets/js/ui-enhancements.js +11 -3
  170. data/scripts/README.md +44 -0
  171. data/scripts/ai/README.md +38 -0
  172. data/scripts/ai/api_call.rb +124 -0
  173. data/scripts/ai/usage.rb +314 -0
  174. data/scripts/ai/usage_report.rb +225 -0
  175. data/scripts/bin/audit-consumer +39 -7
  176. data/scripts/bin/giscus-discussions +213 -14
  177. data/scripts/bin/manifest +35 -12
  178. data/scripts/ci/agent_review_result.py +164 -0
  179. data/scripts/ci/test_agent_review_result.py +172 -0
  180. data/scripts/ci/test_visual_evidence_autogen.py +341 -0
  181. data/scripts/ci/visual_evidence_autogen.py +1060 -0
  182. data/scripts/content-review.rb +20 -1
  183. data/scripts/design-system-check.rb +170 -0
  184. data/scripts/lib/audit.sh +42 -2
  185. data/scripts/lint-liquid-raw.rb +137 -0
  186. data/scripts/test/integration/mermaid +22 -8
  187. data/scripts/test/lib/run_tests.sh +3 -1
  188. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  189. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  190. data/scripts/translate.rb +94 -16
  191. metadata +48 -2
@@ -1,307 +1,2360 @@
1
- // Feature: ZER0-067
1
+ // Feature: ZER0-067, ZER0-086, ZER0-087
2
2
  /**
3
3
  * ===================================================================
4
- * Setup Wizard — _config.yml Generator
4
+ * Site Builder — form, generators, drafts (setup-wizard.js)
5
5
  * ===================================================================
6
6
  *
7
- * Reads form inputs from the wizard include, builds a YAML string,
8
- * and offers Download / Copy actions. Pure vanilla JS, no deps
9
- * beyond Bootstrap 5 (already on the page).
7
+ * File: assets/js/setup-wizard.js
8
+ * Markup: _includes/setup/wizard.html (+ setup/prereq-checklist.html)
9
+ * Styles: _sass/components/_setup-wizard.scss
10
+ * Data: #siteBuilderData — _data/site_builder.yml serialised as JSON
10
11
  *
12
+ * This half of the Site Builder is pure client-side and works everywhere the
13
+ * theme renders, GitHub Pages included. It owns:
14
+ *
15
+ * 1. The stepper (done / active / locked), Back / Next, URL hash → step.
16
+ * 2. Every form control, inline validation (email / URL on blur) and the
17
+ * Review warnings for recommended-but-empty fields.
18
+ * 3. The navigation editor and the "kind of site" quick-pick.
19
+ * 4. The generated project — _config.yml, _config_dev.yml, Gemfile,
20
+ * docker-compose.yml, index.md, navigation, about page, welcome post,
21
+ * collection index pages, .gitignore, zer0.install.yml, .env.example,
22
+ * README — regenerated on every input and shown in the file preview.
23
+ * 5. Per-file copy / download and a self-extracting bash bundle.
24
+ * 6. Draft persistence: every value mirrored to localStorage under
25
+ * `zer0-setup-draft` (debounced 300ms, flushed on pagehide), restored
26
+ * on load, cleared by "Start over" or a successful project write.
27
+ * 7. A small public API on window.Zer0SetupWizard that the Claude session
28
+ * (assets/js/site-builder.js) drives: read state, patch fields, override
29
+ * a generated file, jump steps, record prerequisite check results,
30
+ * subscribe to changes. Context therefore flows one way — the form is
31
+ * the single source of truth and everything else observes it.
32
+ *
33
+ * Stable-height contract (T-040 / #408): `.wizard-panes` is a CSS grid with
34
+ * every pane in one cell, so no JS measurement is needed here — see the SCSS.
35
+ *
36
+ * Everything degrades: localStorage failures are swallowed, a missing element
37
+ * short-circuits, and the wizard never throws on a partial DOM.
11
38
  * ===================================================================
12
39
  */
13
40
 
14
41
  (function () {
15
42
  'use strict';
16
43
 
17
- // ── helpers ────────────────────────────────────────────────────────
44
+ var DRAFT_KEY = 'zer0-setup-draft';
45
+ var DRAFT_DEBOUNCE_MS = 300;
46
+ var DRAFT_VERSION = 2;
47
+
48
+ var wizard = document.getElementById('setup-wizard');
49
+ if (!wizard) return;
50
+
51
+ function readJSON(id, fallback) {
52
+ var el = document.getElementById(id);
53
+ if (!el) return fallback;
54
+ try { return JSON.parse(el.textContent); } catch (e) { return fallback; }
55
+ }
56
+
57
+ var DATA = readJSON('siteBuilderData', {}) || {};
58
+ var DEFAULT_PORT = Number(wizard.getAttribute('data-default-port')) || 4000;
59
+
60
+ // Step order is the source of truth for "earlier" / "later" in the stepper.
61
+ var STEP_IDS = (DATA.steps || []).map(function (s) { return 'tab-' + s.id; });
62
+ if (!STEP_IDS.length) {
63
+ STEP_IDS = ['tab-connect', 'tab-prereqs', 'tab-identity', 'tab-urls', 'tab-structure',
64
+ 'tab-appearance', 'tab-voice', 'tab-integrations', 'tab-build'];
65
+ }
66
+
67
+ var draftTimer = null;
68
+ var chipTimer = null;
69
+ var restoring = false; // suppresses draft writes while we replay a draft
70
+ var listeners = {}; // event name → [callbacks]
71
+ var overrides = {}; // generated path → content set by the user / Claude
72
+ var checks = {}; // prerequisite id → {ok, output, version, manual}
73
+ var activeFile = '_config.yml';
74
+ var skinPreviewOriginal = null;
75
+ var previewStyle = null; // <style> injected while "Preview on this page" is on
76
+ var previewFontLink = null; // Google Fonts <link> injected for the preview
77
+
78
+ // ── site plan ────────────────────────────────────────────────────
79
+ // The parts of the plan that have no form control of their own: the landing
80
+ // hero/sections, a navigation override, and the planned example pages. The
81
+ // form-backed parts (template, nav style, sidebar, palette, fonts, radius)
82
+ // live in the controls with `data-key="plan.…"`; getPlan() merges the two.
83
+ var planExtra = { landing: { hero: null, sections: null }, navigation: { items: null }, pages: [] };
84
+
85
+ function catalogItem(name, id) {
86
+ return catalog(name).filter(function (x) { return x.id === id; })[0] || null;
87
+ }
88
+
89
+ function planSchema() { return DATA.plan_schema || { type: 'object' }; }
18
90
 
19
91
  /**
20
- * Escape a YAML scalar string value (wrap in quotes if needed).
21
- * Multi-line input (e.g., textarea newlines) is intentionally collapsed to
22
- * a single line with spaces so the generated _config.yml remains valid YAML.
23
- * All form fields in this wizard expect single-line values.
92
+ * Validate `value` against the JSON-Schema subset used by plan_schema.
93
+ * Supports: type, properties, required, additionalProperties:false, enum,
94
+ * enumFrom (ids of a catalog in the data file), items, maxItems, maxLength,
95
+ * pattern. Returns an array of "path: message" strings (empty = valid).
24
96
  */
25
- function yamlValue(val) {
26
- if (val === '' || val === null || val === undefined) return '""';
27
-
28
- // Normalize newlines and collapse whitespace to keep scalar values single-line
29
- var normalized = String(val)
30
- .replace(/\r\n|\r|\n/g, ' ')
31
- .replace(/\s{2,}/g, ' ')
32
- .trim();
97
+ function validateSchema(value, schema, path, errors) {
98
+ errors = errors || [];
99
+ path = path || 'plan';
100
+ if (!schema) return errors;
101
+ var type = schema.type;
102
+ var actual = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value;
103
+ if (type === 'integer') { if (typeof value !== 'number' || value % 1 !== 0) errors.push(path + ': expected an integer'); return errors; }
104
+ if (type && actual !== type) { errors.push(path + ': expected ' + type + ', got ' + actual); return errors; }
105
+ if (schema.enum && schema.enum.indexOf(value) === -1) errors.push(path + ': must be one of ' + schema.enum.join(', '));
106
+ if (schema.enumFrom) {
107
+ var ids = catalog(schema.enumFrom).map(function (x) { return x.id; });
108
+ if (schema.enumFrom === 'palettes') ids.push('custom');
109
+ if (ids.indexOf(value) === -1) errors.push(path + ': must be one of ' + ids.join(', '));
110
+ }
111
+ if (type === 'string') {
112
+ if (schema.maxLength && value.length > schema.maxLength) errors.push(path + ': longer than ' + schema.maxLength + ' characters');
113
+ if (schema.pattern && !(new RegExp(schema.pattern)).test(value)) errors.push(path + ': does not match ' + schema.pattern);
114
+ }
115
+ if (type === 'array') {
116
+ if (schema.maxItems && value.length > schema.maxItems) errors.push(path + ': more than ' + schema.maxItems + ' items');
117
+ if (schema.items) value.forEach(function (v, i) { validateSchema(v, schema.items, path + '[' + i + ']', errors); });
118
+ }
119
+ if (type === 'object') {
120
+ var props = schema.properties || {};
121
+ (schema.required || []).forEach(function (k) { if (value[k] === undefined || value[k] === null || value[k] === '') errors.push(path + '.' + k + ': required'); });
122
+ Object.keys(value).forEach(function (k) {
123
+ if (props[k]) validateSchema(value[k], props[k], path + '.' + k, errors);
124
+ else if (schema.additionalProperties === false) errors.push(path + '.' + k + ': unknown key');
125
+ });
126
+ }
127
+ return errors;
128
+ }
33
129
 
34
- if (normalized === '') return '""';
35
- if (normalized === 'true' || normalized === 'false') return normalized;
36
- if (/^[0-9]+$/.test(normalized)) return normalized;
37
- // Wrap in quotes if it contains special chars
38
- if (/[:#{}[\],&*?|>!%@`]/.test(normalized) || normalized.includes("'") || normalized.includes('"')) {
39
- return '"' + normalized.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
130
+ /**
131
+ * Minimal YAML emitter for the nested data files the plan produces.
132
+ *
133
+ * Every branch returns COMPLETE lines already indented to `indent`. A list
134
+ * item renders its value at indent + 2 and then overwrites the two spaces in
135
+ * front of the item's first line with "- ", which is the one place block
136
+ * sequences and block mappings have to agree. An earlier version stripped
137
+ * the leading indent from the first line and left the rest at the item's own
138
+ * level, which produced a mapping whose keys sat under the dash — Psych
139
+ * rejected the file ("did not find expected '-' indicator") and every
140
+ * generated site failed to build.
141
+ */
142
+ function toYaml(value, indent) {
143
+ indent = indent || 0;
144
+ var padStr = ' '.repeat(indent);
145
+ if (Array.isArray(value)) {
146
+ if (!value.length) return padStr + '[]\n';
147
+ return value.map(function (v) {
148
+ if (v !== null && typeof v === 'object' && Object.keys(v).length) {
149
+ var inner = toYaml(v, indent + 2);
150
+ return padStr + '- ' + inner.slice(indent + 2); // first line joins the dash
151
+ }
152
+ if (v !== null && typeof v === 'object') return padStr + '- {}\n';
153
+ return padStr + '- ' + yScalar(v) + '\n';
154
+ }).join('');
155
+ }
156
+ if (value !== null && typeof value === 'object') {
157
+ var keys = Object.keys(value).filter(function (k) {
158
+ var v = value[k];
159
+ return v !== undefined && v !== null && !(typeof v === 'object' && !Array.isArray(v) && !Object.keys(v).length);
160
+ });
161
+ if (!keys.length) return padStr + '{}\n';
162
+ return keys.map(function (k) {
163
+ var v = value[k];
164
+ if (Array.isArray(v)) return v.length ? padStr + k + ':\n' + toYaml(v, indent + 2) : padStr + k + ': []\n';
165
+ if (typeof v === 'object') return padStr + k + ':\n' + toYaml(v, indent + 2);
166
+ if (typeof v === 'string' && v.indexOf('\n') !== -1) {
167
+ return padStr + k + ': |-\n' + v.split('\n').map(function (line) { return padStr + ' ' + line; }).join('\n') + '\n';
168
+ }
169
+ return padStr + k + ': ' + yScalar(v) + '\n';
170
+ }).join('');
40
171
  }
41
- return '"' + normalized + '"';
172
+ return padStr + yScalar(value) + '\n';
173
+ }
174
+
175
+ function yScalar(v) {
176
+ if (typeof v === 'boolean' || typeof v === 'number') return String(v);
177
+ return y(v);
178
+ }
179
+
180
+ function hexToRgb(hex) {
181
+ var m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(String(hex || ''));
182
+ return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)].join(', ') : '';
183
+ }
184
+
185
+ function shade(hex, amount) {
186
+ var m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(String(hex || ''));
187
+ if (!m) return hex;
188
+ var out = [m[1], m[2], m[3]].map(function (h) {
189
+ var v = Math.round(parseInt(h, 16) * (1 + amount));
190
+ return ('0' + Math.max(0, Math.min(255, v)).toString(16)).slice(-2);
191
+ });
192
+ return '#' + out.join('');
193
+ }
194
+
195
+ // ── tiny event bus ────────────────────────────────────────────────
196
+
197
+ function on(event, cb) {
198
+ (listeners[event] = listeners[event] || []).push(cb);
199
+ return function () {
200
+ listeners[event] = (listeners[event] || []).filter(function (fn) { return fn !== cb; });
201
+ };
202
+ }
203
+
204
+ function emit(event, payload) {
205
+ (listeners[event] || []).forEach(function (cb) {
206
+ try { cb(payload); } catch (e) { console.warn('Site Builder listener failed', e); }
207
+ });
208
+ try { wizard.dispatchEvent(new CustomEvent('zer0:wizard:' + event, { detail: payload })); } catch (e) { /* old browsers */ }
209
+ }
210
+
211
+ // ── YAML / text helpers ──────────────────────────────────────────
212
+
213
+ /**
214
+ * Quote a YAML scalar. Multi-line input is collapsed to one line — every
215
+ * scalar the generators write is single-line by design (bodies go to
216
+ * Markdown files, not to YAML values).
217
+ */
218
+ function y(val) {
219
+ if (val === '' || val === null || val === undefined) return '""';
220
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
221
+ if (typeof val === 'number') return String(val);
222
+ var s = String(val).replace(/\r\n|\r|\n/g, ' ').replace(/\s{2,}/g, ' ').trim();
223
+ if (s === '') return '""';
224
+ return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
42
225
  }
43
226
 
44
- /** Pad a YAML key to 25 chars for alignment. */
45
227
  function pad(key, width) {
46
228
  width = width || 25;
47
- return (key + ' '.repeat(width)).slice(0, width);
229
+ return (key + ' '.repeat(width)).slice(0, Math.max(width, key.length + 1));
48
230
  }
49
231
 
50
- /** Return the permalink pattern for a given collection name. */
51
- function collectionPermalink(col) {
52
- switch (col) {
53
- case 'posts': return '/posts/:title/';
54
- case 'docs': return '/docs/:path/';
55
- case 'about': return '/about/:path/';
56
- default: return '/' + col + '/:path/';
57
- }
232
+ function slugify(text) {
233
+ return String(text || '').toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
234
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
58
235
  }
59
236
 
60
- // ── YAML builder ──────────────────────────────────────────────────
237
+ /**
238
+ * A timestamp for generated content. It must never be in the FUTURE: Jekyll
239
+ * refuses to publish future-dated documents unless `future: true`, and
240
+ * GitHub Pages builds with the default. Stamping a fixed "09:00Z" meant
241
+ * every post and note a site was generated with stayed invisible on Pages
242
+ * until 09:00 UTC — or a whole day, west of the meridian.
243
+ */
244
+ function nowISO() {
245
+ return new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z');
246
+ }
61
247
 
62
- function buildYAML() {
63
- var fields = {};
248
+ function todayISO() {
249
+ try { return new Date().toISOString().slice(0, 10); } catch (e) { return '2026-01-01'; }
250
+ }
64
251
 
65
- // Read simple fields
66
- document.querySelectorAll('.cfg-field').forEach(function (el) {
252
+ function titleCase(id) {
253
+ return String(id || '').replace(/[-_]/g, ' ').replace(/\b\w/g, function (c) { return c.toUpperCase(); });
254
+ }
255
+
256
+ // ── reading the form ─────────────────────────────────────────────
257
+
258
+ function fieldEls() { return wizard.querySelectorAll('.cfg-field[data-key]'); }
259
+
260
+ function readFields() {
261
+ var fields = {};
262
+ fieldEls().forEach(function (el) {
67
263
  var key = el.getAttribute('data-key');
68
264
  if (!key) return;
69
- var val = (el.tagName === 'TEXTAREA' || el.tagName === 'SELECT')
70
- ? el.value.trim()
71
- : el.value.trim();
72
- if (val) fields[key] = val;
265
+ var val = el.type === 'number' ? el.value.trim() : el.value.trim();
266
+ if (val !== '') fields[key] = val;
267
+ });
268
+ wizard.querySelectorAll('.cfg-radio:checked').forEach(function (el) {
269
+ var key = el.getAttribute('data-key');
270
+ if (key) fields[key] = el.value;
73
271
  });
272
+ wizard.querySelectorAll('.cfg-switch[data-key]').forEach(function (el) {
273
+ fields[el.getAttribute('data-key')] = !!el.checked;
274
+ });
275
+ return fields;
276
+ }
74
277
 
75
- // Read collection toggles
76
- var collections = [];
77
- document.querySelectorAll('.cfg-collection').forEach(function (el) {
278
+ function readCollections() {
279
+ var out = [];
280
+ wizard.querySelectorAll('.cfg-collection').forEach(function (el) {
78
281
  if (el.checked) {
79
- collections.push(el.getAttribute('data-col'));
282
+ out.push({
283
+ id: el.getAttribute('data-col'),
284
+ layout: el.getAttribute('data-layout') || 'default',
285
+ permalink: el.getAttribute('data-permalink') || ('/' + el.getAttribute('data-col') + '/:path/')
286
+ });
80
287
  }
81
288
  });
289
+ return out;
290
+ }
291
+
292
+ function readNavigation() {
293
+ var rows = [];
294
+ wizard.querySelectorAll('#nav-editor .nav-row').forEach(function (row) {
295
+ var title = (row.querySelector('.nav-title') || {}).value || '';
296
+ var url = (row.querySelector('.nav-url') || {}).value || '';
297
+ var icon = (row.querySelector('.nav-icon') || {}).value || '';
298
+ if (title.trim() || url.trim()) rows.push({ title: title.trim(), url: url.trim(), icon: icon.trim() });
299
+ });
300
+ return rows;
301
+ }
82
302
 
83
- // Read PostHog toggle
84
- var posthogEnabled = document.getElementById('cfg-posthog-enabled');
85
- var posthogOn = posthogEnabled && posthogEnabled.checked;
303
+ function catalog(name) { return Array.isArray(DATA[name]) ? DATA[name] : []; }
86
304
 
87
- // Build YAML
305
+ function labelFor(list, id) {
306
+ var hit = list.filter(function (x) { return x.id === id; })[0];
307
+ return hit ? hit.label : titleCase(id);
308
+ }
309
+
310
+ function activeStepId() {
311
+ var active = wizard.querySelector('#wizardTabs .wizard-step.is-active');
312
+ return active ? active.id : STEP_IDS[0];
313
+ }
314
+
315
+ /** The merged site plan: form-backed choices + the AI/user-supplied extras. */
316
+ function getPlan() {
317
+ var f = readFields();
318
+ var palette = { preset: f['plan.theme.palette.preset'] || 'skin' };
319
+ if (palette.preset === 'custom') {
320
+ palette.primary = f['plan.theme.palette.primary'] || '#0d6efd';
321
+ palette.secondary = f['plan.theme.palette.secondary'] || '#6c757d';
322
+ palette.accent = f['plan.theme.palette.accent'] || '#20c997';
323
+ }
324
+ return {
325
+ landing: {
326
+ template: f['plan.landing.template'] || 'hero',
327
+ hero: planExtra.landing.hero || undefined,
328
+ sections: planExtra.landing.sections || undefined
329
+ },
330
+ navigation: {
331
+ style: f['plan.navigation.style'] || 'flat',
332
+ sidebar: f['plan.navigation.sidebar'] || 'none',
333
+ items: planExtra.navigation.items || undefined
334
+ },
335
+ theme: {
336
+ palette: palette,
337
+ fonts: f['plan.theme.fonts'] || 'system',
338
+ radius: f['plan.theme.radius'] || 'soft'
339
+ },
340
+ pages: planExtra.pages.slice()
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Apply a (partial) plan. Form-backed keys go through the controls; the
346
+ * extras are merged. Validates against plan_schema first — an invalid plan
347
+ * changes nothing and the errors come back for the caller (Claude) to fix.
348
+ */
349
+ function setPlan(patch, opts) {
350
+ opts = opts || {};
351
+ if (!patch || typeof patch !== 'object') return { ok: false, errors: ['plan must be an object'] };
352
+ var errors = validateSchema(patch, planSchema(), 'plan');
353
+ if (errors.length) return { ok: false, errors: errors };
354
+
355
+ var fieldPatch = {};
356
+ if (patch.landing) {
357
+ if (patch.landing.template) fieldPatch['plan.landing.template'] = patch.landing.template;
358
+ // A hero patch MERGES field by field. An agent that comes back later to
359
+ // add an image must not silently drop the headline, subheadline and CTAs
360
+ // it set two turns earlier. (sections and pages REPLACE — they are lists,
361
+ // not a bag of settings, so a shorter list has to mean a shorter list.)
362
+ if (patch.landing.hero !== undefined) {
363
+ planExtra.landing.hero = Object.assign({}, planExtra.landing.hero || {}, patch.landing.hero);
364
+ }
365
+ if (patch.landing.sections !== undefined) planExtra.landing.sections = patch.landing.sections;
366
+ }
367
+ if (patch.navigation) {
368
+ if (patch.navigation.style) fieldPatch['plan.navigation.style'] = patch.navigation.style;
369
+ if (patch.navigation.sidebar) fieldPatch['plan.navigation.sidebar'] = patch.navigation.sidebar;
370
+ if (patch.navigation.items !== undefined) planExtra.navigation.items = patch.navigation.items;
371
+ }
372
+ if (patch.theme) {
373
+ if (patch.theme.palette) {
374
+ var p = patch.theme.palette;
375
+ if (p.primary || p.secondary || p.accent) {
376
+ fieldPatch['plan.theme.palette.preset'] = 'custom';
377
+ if (p.primary) fieldPatch['plan.theme.palette.primary'] = p.primary;
378
+ if (p.secondary) fieldPatch['plan.theme.palette.secondary'] = p.secondary;
379
+ if (p.accent) fieldPatch['plan.theme.palette.accent'] = p.accent;
380
+ } else if (p.preset) fieldPatch['plan.theme.palette.preset'] = p.preset;
381
+ }
382
+ if (patch.theme.fonts) fieldPatch['plan.theme.fonts'] = patch.theme.fonts;
383
+ if (patch.theme.radius) fieldPatch['plan.theme.radius'] = patch.theme.radius;
384
+ }
385
+ if (Array.isArray(patch.pages)) {
386
+ var pages = patch.pages.map(normalizePage);
387
+ planExtra.pages = opts.mergePages ? mergePages(planExtra.pages, pages) : pages;
388
+ renderPagesPlanner();
389
+ }
390
+ var applied = Object.keys(fieldPatch).length ? applyFields(fieldPatch, { source: opts.source || 'plan', silent: true }) : { applied: [], ignored: [] };
391
+ refreshAll();
392
+ saveDraft();
393
+ emit('change', { source: opts.source || 'plan', keys: ['plan'] });
394
+ return { ok: true, applied: applied.applied.concat(Object.keys(patch)), ignored: applied.ignored };
395
+ }
396
+
397
+ function normalizePage(p) {
398
+ var page = Object.assign({}, p);
399
+ page.collection = page.collection || 'posts';
400
+ page.title = String(page.title || 'Untitled').trim();
401
+ page.slug = slugify(page.slug || page.title) || 'page';
402
+ if (page.collection === 'posts' && !page.date) page.date = todayISO();
403
+ return page;
404
+ }
405
+
406
+ function mergePages(existing, incoming) {
407
+ var out = existing.slice();
408
+ incoming.forEach(function (p) {
409
+ var i = out.findIndex(function (e) { return e.collection === p.collection && e.slug === p.slug; });
410
+ if (i === -1) out.push(p); else out[i] = Object.assign({}, out[i], p);
411
+ });
412
+ return out.slice(0, 12);
413
+ }
414
+
415
+ function planSummary(plan) {
88
416
  var lines = [];
89
- lines.push('# =============================================================================');
90
- lines.push('# Site Configuration — generated by Setup Wizard');
91
- lines.push('# Docs: https://jekyllrb.com/docs/configuration/');
92
- lines.push('# =============================================================================');
93
- lines.push('');
94
-
95
- // Site Identity
96
- lines.push('# ── Site Identity ──────────────────────────────────────────────────');
97
- lines.push(pad('title') + ': ' + yamlValue(fields.title || 'My Site'));
98
- if (fields.subtitle) {
99
- lines.push(pad('subtitle') + ': ' + yamlValue(fields.subtitle));
100
- }
101
- lines.push(pad('description') + ': ' + yamlValue(fields.description || ''));
102
- lines.push(pad('founder') + ': ' + yamlValue(fields.founder || ''));
103
- if (fields.email) {
104
- lines.push(pad('email') + ': ' + yamlValue(fields.email));
105
- }
106
- lines.push('');
107
-
108
- // GitHub
109
- lines.push('# ── GitHub ─────────────────────────────────────────────────────────');
110
- var ghUser = fields.github_user || 'your-username';
111
- var repoName = fields.repository_name || (ghUser !== 'your-username' ? ghUser + '-site' : 'my-site');
112
- lines.push(pad('github_user') + ': &github_user ' + yamlValue(ghUser));
113
- lines.push(pad('repository_name') + ': &github_repository ' + yamlValue(repoName));
114
- lines.push('');
115
-
116
- // URLs
117
- lines.push('# ── URLs & Deployment ──────────────────────────────────────────────');
118
- lines.push(pad('url') + ': ' + yamlValue(fields.url || 'https://' + ghUser + '.github.io'));
119
- lines.push(pad('baseurl') + ': ' + yamlValue(fields.baseurl || ''));
120
- lines.push(pad('remote_theme') + ': ' + yamlValue(fields.remote_theme || 'bamr87/zer0-mistakes'));
121
- lines.push(pad('permalink') + ': ' + (fields.permalink || '/:categories/:title/'));
122
- lines.push(pad('port') + ': 4000');
123
- lines.push('');
124
-
125
- // Collections
126
- lines.push('# ── Collections ────────────────────────────────────────────────────');
127
- lines.push('collections:');
128
- collections.forEach(function (col) {
129
- lines.push(' ' + col + ':');
130
- lines.push(' output: true');
131
- lines.push(' permalink: ' + collectionPermalink(col));
132
- });
133
- lines.push('');
134
-
135
- // Defaults
136
- lines.push('# ── Default Front Matter ───────────────────────────────────────────');
137
- lines.push('defaults:');
138
- if (collections.indexOf('posts') !== -1) {
139
- lines.push(' - scope:');
140
- lines.push(' path: ""');
141
- lines.push(' type: "posts"');
142
- lines.push(' values:');
143
- lines.push(' layout: "article"');
144
- lines.push(' author: "default"');
145
- }
146
- if (collections.indexOf('docs') !== -1) {
147
- lines.push(' - scope:');
148
- lines.push(' path: ""');
149
- lines.push(' type: "docs"');
150
- lines.push(' values:');
151
- lines.push(' layout: "default"');
152
- }
153
- if (collections.indexOf('about') !== -1) {
154
- lines.push(' - scope:');
155
- lines.push(' path: ""');
156
- lines.push(' type: "about"');
157
- lines.push(' values:');
158
- lines.push(' layout: "default"');
159
- }
160
- lines.push(' - scope:');
161
- lines.push(' path: ""');
162
- lines.push(' type: "pages"');
163
- lines.push(' values:');
164
- lines.push(' layout: "default"');
165
- lines.push('');
166
-
167
- // Build settings
168
- lines.push('# ── Build ──────────────────────────────────────────────────────────');
169
- lines.push(pad('markdown') + ': kramdown');
170
- lines.push(pad('highlighter') + ': rouge');
171
- lines.push(pad('public_folder') + ': assets');
172
- lines.push('');
173
-
174
- // Plugins
175
- lines.push('# ── Plugins ────────────────────────────────────────────────────────');
176
- lines.push('plugins:');
177
- lines.push(' - jekyll-feed');
178
- lines.push(' - jekyll-sitemap');
179
- lines.push(' - jekyll-seo-tag');
180
- lines.push(' - jekyll-remote-theme');
181
- lines.push('');
182
-
183
- // Analytics
184
- lines.push('# ── Analytics ──────────────────────────────────────────────────────');
185
- if (fields.google_analytics) {
186
- lines.push(pad('google_analytics') + ': ' + yamlValue(fields.google_analytics));
417
+ lines.push('Landing: template=' + plan.landing.template + (plan.landing.hero ? ' (custom hero)' : ' (default hero)') + (plan.landing.sections ? ', ' + plan.landing.sections.length + ' custom sections' : ', default sections'));
418
+ lines.push('Navigation: style=' + plan.navigation.style + ' sidebar=' + plan.navigation.sidebar + (plan.navigation.items ? ' (custom items)' : ''));
419
+ var pal = plan.theme.palette;
420
+ lines.push('Theme overrides: palette=' + pal.preset + (pal.primary ? ' ' + pal.primary + '/' + pal.secondary + '/' + pal.accent : '') + ' fonts=' + plan.theme.fonts + ' radius=' + plan.theme.radius);
421
+ lines.push('Planned pages (' + plan.pages.length + '): ' + (plan.pages.map(function (p) { return p.collection + '/' + p.slug + (p.body ? '' : ' (no body yet)'); }).join(', ') || 'none'));
422
+ return lines.join('\n');
423
+ }
424
+
425
+ /** The full, serialisable wizard state — what Claude reads every turn. */
426
+ function getState() {
427
+ var f = readFields();
428
+ var collections = readCollections();
429
+ var slug = slugify(f.target || f.repository_name || f.title) || 'my-site';
430
+ return {
431
+ step: activeStepId().replace(/^tab-/, ''),
432
+ fields: f,
433
+ plan: getPlan(),
434
+ collections: collections.map(function (c) { return c.id; }),
435
+ navigation: readNavigation(),
436
+ checks: JSON.parse(JSON.stringify(checks)),
437
+ target: f.target || slug,
438
+ port: Number(f.port) || DEFAULT_PORT,
439
+ overrides: Object.keys(overrides),
440
+ recommendedMissing: unfilledRecommended()
441
+ };
442
+ }
443
+
444
+ /** Compact prose summary for the Claude system prompt. */
445
+ function getSummary() {
446
+ var s = getState();
447
+ var f = s.fields;
448
+ var lines = [];
449
+ lines.push('Current step: ' + s.step);
450
+ lines.push('Title: ' + (f.title || '(empty)') + ' | Subtitle: ' + (f.subtitle || '(empty)') + ' | Tagline: ' + (f.tagline || '(empty)'));
451
+ lines.push('Description: ' + (f.description || '(empty)'));
452
+ lines.push('Brief: ' + (f.brief || '(empty)'));
453
+ lines.push('Owner: ' + (f.founder || '(empty)') + ' <' + (f.email || 'no email') + '>');
454
+ lines.push('GitHub: ' + (f.github_user || '(user?)') + '/' + (f.repository_name || '(repo?)') + ' | url: ' + (f.url || '(empty)') + ' | baseurl: ' + (f.baseurl || '""') + ' | theme via: ' + (f.deploy_target || 'github-pages') + ' | port: ' + s.port);
455
+ lines.push('Site type: ' + (f.site_type || '(unset)') + ' | Collections: ' + (s.collections.join(', ') || 'none'));
456
+ lines.push('Navigation: ' + (s.navigation.map(function (n) { return n.title + ' → ' + n.url; }).join('; ') || '(empty)'));
457
+ lines.push('Appearance: skin=' + (f.theme_skin || 'air') + ' colour_mode=' + (f.color_mode_default || 'auto') + ' lock=' + !!f.color_mode_lock + ' backgrounds=' + (f.theme_background !== false) + ' icon=' + (f.title_icon || 'robot'));
458
+ lines.push('Voice: tone=' + (f.tone || 'friendly') + ' audience=' + (f.audience || 'developers') + ' | welcome title: ' + (f.welcome_title || '(empty)') + ' | welcome body: ' + (f.welcome_body ? f.welcome_body.length + ' chars' : 'empty') + ' | about body: ' + (f.about_body ? f.about_body.length + ' chars' : 'empty'));
459
+ var ints = catalog('integrations').map(function (i) { return i.id + '=' + (f['integration.' + i.id] ? 'on' : 'off'); });
460
+ lines.push('Integrations: ' + ints.join(', ') + ' | GA: ' + (f.google_analytics || 'none') + ' | giscus ids: ' + (f['giscus.repo_id'] ? 'set' : 'unset'));
461
+ lines.push('AI in the generated site: chat provider=' + (f.ai_provider || 'anthropic') + ' | preview-image renderer=' + (f.image_provider || 'none'));
462
+ var checkLines = Object.keys(s.checks).map(function (id) {
463
+ var c = s.checks[id];
464
+ return id + ':' + (c.manual ? 'done(manual)' : c.ok === true ? 'ok' + (c.version ? ' v' + c.version : '') : c.ok === false ? 'FAIL' : '?');
465
+ });
466
+ lines.push('Prerequisites: ' + (checkLines.join(', ') || 'not checked'));
467
+ lines.push('Target folder: ' + s.target + ' | overwrite: ' + !!f.overwrite + ' | file overrides: ' + (s.overrides.join(', ') || 'none'));
468
+ if (s.recommendedMissing.length) lines.push('Recommended fields still empty: ' + s.recommendedMissing.join(', '));
469
+ lines.push('Site plan:\n' + planSummary(s.plan));
470
+ return lines.join('\n');
471
+ }
472
+
473
+ /** What Claude may set: every data-key with its type, label and options. */
474
+ function schema() {
475
+ var out = [];
476
+ var seen = {};
477
+ function labelOf(el) {
478
+ var lab = el.id ? wizard.querySelector('label[for="' + el.id + '"]') : null;
479
+ if (lab) return lab.textContent.replace(/\s+/g, ' ').trim();
480
+ var fs = el.closest('fieldset');
481
+ var legend = fs ? fs.querySelector('legend') : null;
482
+ return legend ? legend.textContent.replace(/\s+/g, ' ').trim() : el.getAttribute('data-key');
483
+ }
484
+ fieldEls().forEach(function (el) {
485
+ var key = el.getAttribute('data-key');
486
+ if (seen[key]) return;
487
+ seen[key] = true;
488
+ var entry = { key: key, label: labelOf(el), type: el.tagName === 'TEXTAREA' ? 'text (multi-line Markdown allowed)' : el.tagName === 'SELECT' ? 'choice' : (el.type || 'text') };
489
+ if (el.tagName === 'SELECT') entry.options = Array.prototype.map.call(el.options, function (o) { return o.value; });
490
+ out.push(entry);
491
+ });
492
+ var radioGroups = {};
493
+ wizard.querySelectorAll('.cfg-radio[data-key]').forEach(function (el) {
494
+ var key = el.getAttribute('data-key');
495
+ if (!radioGroups[key]) {
496
+ radioGroups[key] = { key: key, label: labelOf(el), type: 'choice', options: [] };
497
+ out.push(radioGroups[key]);
498
+ }
499
+ radioGroups[key].options.push(el.value);
500
+ });
501
+ wizard.querySelectorAll('.cfg-switch[data-key]').forEach(function (el) {
502
+ var key = el.getAttribute('data-key');
503
+ if (seen[key]) return;
504
+ seen[key] = true;
505
+ out.push({ key: key, label: labelOf(el), type: 'boolean' });
506
+ });
507
+ out.push({ key: 'collections', label: 'Enabled collections', type: 'array of ids', options: catalog('collections').map(function (c) { return c.id; }) });
508
+ out.push({ key: 'navigation', label: 'Main navigation', type: 'array of {title, url, icon?} (icon = Bootstrap icon class like bi-book)' });
509
+ return out.filter(function (e) { return e.key.indexOf('plan.') !== 0; }); // plan.* keys are set through set_site_plan
510
+ }
511
+
512
+ // ── writing the form (Claude → wizard) ───────────────────────────
513
+
514
+ function setRadio(key, value) {
515
+ var radios = wizard.querySelectorAll('.cfg-radio[data-key="' + key + '"]');
516
+ var hit = false;
517
+ radios.forEach(function (r) {
518
+ var match = String(r.value) === String(value);
519
+ r.checked = match;
520
+ if (match) hit = true;
521
+ });
522
+ if (hit && key === 'site_type') applySiteType(value);
523
+ return hit;
524
+ }
525
+
526
+ function setCollections(ids) {
527
+ if (!Array.isArray(ids)) return false;
528
+ var wanted = ids.map(String);
529
+ wizard.querySelectorAll('.cfg-collection').forEach(function (el) {
530
+ el.checked = wanted.indexOf(el.getAttribute('data-col')) !== -1;
531
+ });
532
+ return true;
533
+ }
534
+
535
+ function applyFields(patch, opts) {
536
+ opts = opts || {};
537
+ var applied = [];
538
+ var ignored = [];
539
+ if (!patch || typeof patch !== 'object') return { applied: applied, ignored: ['(no fields)'] };
540
+
541
+ Object.keys(patch).forEach(function (key) {
542
+ var value = patch[key];
543
+ if (key === 'collections') {
544
+ if (setCollections(value)) applied.push(key); else ignored.push(key);
545
+ return;
546
+ }
547
+ if (key === 'navigation') {
548
+ if (Array.isArray(value)) { renderNavEditor(value); applied.push(key); } else ignored.push(key);
549
+ return;
550
+ }
551
+ var sw = wizard.querySelector('.cfg-switch[data-key="' + key + '"]');
552
+ if (sw) { sw.checked = value === true || value === 'true' || value === 'on' || value === 1; applied.push(key); return; }
553
+ if (wizard.querySelector('.cfg-radio[data-key="' + key + '"]')) {
554
+ if (setRadio(key, value)) applied.push(key); else ignored.push(key + ' (unknown option ' + value + ')');
555
+ return;
556
+ }
557
+ var el = wizard.querySelector('.cfg-field[data-key="' + key + '"]');
558
+ if (!el) { ignored.push(key); return; }
559
+ if (el.tagName === 'SELECT') {
560
+ var ok = Array.prototype.some.call(el.options, function (o) { return o.value === String(value); });
561
+ if (!ok) { ignored.push(key + ' (unknown option ' + value + ')'); return; }
562
+ }
563
+ el.value = value === null || value === undefined ? '' : String(value);
564
+ el.classList.remove('is-invalid');
565
+ applied.push(key);
566
+ });
567
+
568
+ refreshAll();
569
+ if (!opts.silent) saveDraft();
570
+ emit('change', { source: opts.source || 'api', keys: applied });
571
+ return { applied: applied, ignored: ignored };
572
+ }
573
+
574
+ function setOverride(path, content) {
575
+ if (!path) return false;
576
+ if (content === null || content === undefined) delete overrides[path];
577
+ else overrides[path] = String(content);
578
+ refreshAll();
579
+ saveDraft();
580
+ emit('change', { source: 'override', keys: [path] });
581
+ return true;
582
+ }
583
+
584
+ function setCheckResult(id, result) {
585
+ if (!id) return;
586
+ checks[id] = Object.assign({}, checks[id] || {}, result || {});
587
+ renderPrereqs();
588
+ saveDraft();
589
+ emit('checks', { id: id, result: checks[id] });
590
+ }
591
+
592
+ // ── site type → collections ──────────────────────────────────────
593
+
594
+ function applySiteType(id) {
595
+ var radio = wizard.querySelector('.cfg-radio[data-key="site_type"][value="' + id + '"]');
596
+ var list = radio ? (radio.getAttribute('data-collections') || '').split(',').filter(Boolean) : null;
597
+ if (list && list.length) {
598
+ setCollections(list);
599
+ renderNavEditor(defaultNavigation());
600
+ }
601
+ }
602
+
603
+ // ── navigation editor ────────────────────────────────────────────
604
+
605
+ var COLLECTION_NAV = {
606
+ posts: { title: 'Posts', url: '/posts/', icon: 'bi-journal-text' },
607
+ docs: { title: 'Docs', url: '/docs/', icon: 'bi-book' },
608
+ about: { title: 'About', url: '/about/', icon: 'bi-info-circle' },
609
+ notes: { title: 'Notes', url: '/notes/', icon: 'bi-sticky' },
610
+ quickstart: { title: 'Quickstart', url: '/quickstart/', icon: 'bi-flag' },
611
+ notebooks: { title: 'Notebooks', url: '/notebooks/', icon: 'bi-journal-code' },
612
+ quests: { title: 'Quests', url: '/quests/', icon: 'bi-controller' },
613
+ recipes: { title: 'Recipes', url: '/recipes/', icon: 'bi-egg-fried' }
614
+ };
615
+
616
+ function defaultNavigation() {
617
+ var nav = [{ title: 'Home', url: '/', icon: 'bi-house' }];
618
+ readCollections().forEach(function (c) {
619
+ var item = COLLECTION_NAV[c.id];
620
+ if (item && c.id !== 'about') nav.push(Object.assign({}, item));
621
+ });
622
+ if (readCollections().some(function (c) { return c.id === 'about'; })) nav.push(Object.assign({}, COLLECTION_NAV.about));
623
+ return nav;
624
+ }
625
+
626
+ function navRow(item) {
627
+ var row = document.createElement('div');
628
+ row.className = 'nav-row input-group input-group-sm mb-2';
629
+ var handle = document.createElement('span');
630
+ handle.className = 'input-group-text';
631
+ handle.innerHTML = '<i class="bi bi-grip-vertical" aria-hidden="true"></i>';
632
+ var title = document.createElement('input');
633
+ title.type = 'text';
634
+ title.className = 'form-control nav-title';
635
+ title.placeholder = 'Label';
636
+ title.setAttribute('aria-label', 'Menu label');
637
+ title.value = item.title || '';
638
+ var url = document.createElement('input');
639
+ url.type = 'text';
640
+ url.className = 'form-control nav-url';
641
+ url.placeholder = '/path/';
642
+ url.setAttribute('aria-label', 'Menu URL');
643
+ url.value = item.url || '';
644
+ var icon = document.createElement('input');
645
+ icon.type = 'text';
646
+ icon.className = 'form-control nav-icon';
647
+ icon.placeholder = 'bi-icon';
648
+ icon.setAttribute('aria-label', 'Bootstrap icon class (optional)');
649
+ icon.value = item.icon || '';
650
+ var remove = document.createElement('button');
651
+ remove.type = 'button';
652
+ remove.className = 'btn btn-outline-danger';
653
+ remove.setAttribute('aria-label', 'Remove menu item');
654
+ remove.innerHTML = '<i class="bi bi-x-lg" aria-hidden="true"></i>';
655
+ remove.addEventListener('click', function () {
656
+ row.remove();
657
+ refreshAll();
658
+ saveDraft();
659
+ emit('change', { source: 'nav', keys: ['navigation'] });
660
+ });
661
+ row.appendChild(handle);
662
+ row.appendChild(title);
663
+ row.appendChild(url);
664
+ row.appendChild(icon);
665
+ row.appendChild(remove);
666
+ return row;
667
+ }
668
+
669
+ // ── example-page planner ─────────────────────────────────────────
670
+
671
+ function pageRow(p, index) {
672
+ var row = document.createElement('div');
673
+ row.className = 'page-row input-group input-group-sm';
674
+ row.setAttribute('data-index', String(index));
675
+ var select = document.createElement('select');
676
+ select.className = 'form-select page-collection';
677
+ select.setAttribute('aria-label', 'Collection');
678
+ readCollections().forEach(function (col) {
679
+ var opt = document.createElement('option');
680
+ opt.value = col.id;
681
+ opt.textContent = labelFor(catalog('collections'), col.id);
682
+ if (col.id === p.collection) opt.selected = true;
683
+ select.appendChild(opt);
684
+ });
685
+ if (!Array.prototype.some.call(select.options, function (o) { return o.value === p.collection; })) {
686
+ var extra = document.createElement('option');
687
+ extra.value = p.collection;
688
+ extra.textContent = titleCase(p.collection) + ' (off)';
689
+ extra.selected = true;
690
+ select.appendChild(extra);
691
+ }
692
+ var title = document.createElement('input');
693
+ title.type = 'text';
694
+ title.className = 'form-control page-title';
695
+ title.placeholder = 'Page title';
696
+ title.setAttribute('aria-label', 'Page title');
697
+ title.value = p.title || '';
698
+ var state = document.createElement('span');
699
+ state.className = 'input-group-text small';
700
+ state.title = p.body ? 'Body written' : 'Body will be drafted from the title';
701
+ state.innerHTML = p.body ? '<i class="bi bi-file-earmark-check text-success" aria-hidden="true"></i>' : '<i class="bi bi-file-earmark text-body-secondary" aria-hidden="true"></i>';
702
+ var remove = document.createElement('button');
703
+ remove.type = 'button';
704
+ remove.className = 'btn btn-outline-danger page-remove';
705
+ remove.setAttribute('aria-label', 'Remove planned page');
706
+ remove.innerHTML = '<i class="bi bi-x-lg" aria-hidden="true"></i>';
707
+ row.appendChild(select);
708
+ row.appendChild(title);
709
+ row.appendChild(state);
710
+ row.appendChild(remove);
711
+ return row;
712
+ }
713
+
714
+ function renderPagesPlanner() {
715
+ var host = document.getElementById('pages-planner');
716
+ if (!host) return;
717
+ host.innerHTML = '';
718
+ planExtra.pages.forEach(function (p, i) { host.appendChild(pageRow(p, i)); });
719
+ if (!planExtra.pages.length) {
720
+ var empty = document.createElement('p');
721
+ empty.className = 'small text-body-secondary mb-0';
722
+ empty.textContent = 'No example pages planned yet — add one, or ask Claude to plan them.';
723
+ host.appendChild(empty);
724
+ }
725
+ var count = document.getElementById('pages-count');
726
+ if (count) count.textContent = String(planExtra.pages.length);
727
+ }
728
+
729
+ function updateLandingSummary() {
730
+ var host = document.getElementById('landing-summary');
731
+ if (!host) return;
732
+ var lp = landingPlan(ctx());
733
+ host.textContent = 'Hero: “' + (lp.hero.headline || '') + '” · sections: ' + lp.sections.map(function (s) { return s.type.replace('_', ' '); }).join(', ') + (planExtra.landing.sections ? ' (designed)' : ' (template defaults)');
734
+ }
735
+
736
+ function ensureFontLink(fp) {
737
+ if (!fp || !fp.google) return;
738
+ var id = 'wizard-font-' + fp.id;
739
+ if (document.getElementById(id)) return;
740
+ var link = document.createElement('link');
741
+ link.id = id;
742
+ link.rel = 'stylesheet';
743
+ link.href = 'https://fonts.googleapis.com/css2?' + fp.google + '&display=swap';
744
+ document.head.appendChild(link);
745
+ }
746
+
747
+ function updateFontPreview() {
748
+ var host = document.getElementById('font-preview');
749
+ if (!host) return;
750
+ var fp = catalogItem('font_pairings', readFields()['plan.theme.fonts']);
751
+ if (fp && fp.google) {
752
+ ensureFontLink(fp);
753
+ host.style.fontFamily = fp.body;
754
+ var strong = host.querySelector('strong');
755
+ if (strong) strong.style.fontFamily = fp.heading;
187
756
  } else {
188
- lines.push(pad('google_analytics') + ': ""');
189
- }
190
- lines.push('posthog:');
191
- lines.push(' enabled: ' + (posthogOn ? 'true' : 'false'));
192
- lines.push(' api_key: ' + yamlValue(fields['posthog.api_key'] || ''));
193
- lines.push('');
194
-
195
- // Social
196
- if (fields.twitter_username || fields.linkedin_username) {
197
- lines.push('# ── Social ─────────────────────────────────────────────────────────');
198
- if (fields.twitter_username) {
199
- lines.push(pad('twitter_username') + ': ' + yamlValue(fields.twitter_username));
757
+ host.style.fontFamily = '';
758
+ var s2 = host.querySelector('strong');
759
+ if (s2) s2.style.fontFamily = '';
760
+ }
761
+ }
762
+
763
+ function renderNavEditor(items) {
764
+ var host = document.getElementById('nav-editor');
765
+ if (!host) return;
766
+ host.innerHTML = '';
767
+ (items || []).forEach(function (item) { host.appendChild(navRow(item)); });
768
+ if (!host.children.length) {
769
+ var empty = document.createElement('p');
770
+ empty.className = 'small text-body-secondary mb-0';
771
+ empty.textContent = 'No menu items yet — add one or rebuild from the collections.';
772
+ host.appendChild(empty);
773
+ }
774
+ }
775
+
776
+ // ── generators ───────────────────────────────────────────────────
777
+
778
+ function ctx() {
779
+ var s = getState();
780
+ var f = s.fields;
781
+ var ghUser = f.github_user || 'your-username';
782
+ var repo = f.repository_name || s.target || 'my-site';
783
+ var isUserSite = /\.github\.io$/i.test(repo);
784
+ var remote = f.deploy_target !== 'gem';
785
+ return {
786
+ s: s, f: f, ghUser: ghUser, repo: repo, remote: remote, plan: s.plan,
787
+ title: f.title || 'My Site',
788
+ url: f.url || ('https://' + ghUser + '.github.io'),
789
+ baseurl: f.baseurl !== undefined ? f.baseurl : (isUserSite ? '' : '/' + repo),
790
+ port: s.port,
791
+ date: todayISO(),
792
+ slug: s.target || slugify(repo) || 'my-site',
793
+ collections: readCollections(),
794
+ nav: s.navigation.length ? s.navigation : defaultNavigation(),
795
+ on: function (id) { return !!f['integration.' + id]; }
796
+ };
797
+ }
798
+
799
+ function genConfig(c) {
800
+ var f = c.f;
801
+ var L = [];
802
+ L.push('# =============================================================================');
803
+ L.push('# ' + c.title + ' — Jekyll configuration');
804
+ L.push('# Generated by the zer0-mistakes Site Builder on ' + c.date);
805
+ L.push('# Theme: https://github.com/bamr87/zer0-mistakes · Docs: https://jekyllrb.com/docs/configuration/');
806
+ L.push('# =============================================================================');
807
+ L.push('');
808
+ L.push('# ── Setup flag ─────────────────────────────────────────────────────');
809
+ L.push('# true hides the welcome / setup screens the theme shows to fresh sites.');
810
+ L.push(pad('site_configured') + ': true');
811
+ L.push('');
812
+ L.push('# ── Site identity ──────────────────────────────────────────────────');
813
+ L.push(pad('title') + ': ' + y(c.title));
814
+ L.push(pad('subtitle') + ': ' + y(f.subtitle || ''));
815
+ if (f.tagline) L.push(pad('tagline') + ': ' + y(f.tagline));
816
+ L.push(pad('description') + ': ' + y(f.description || ''));
817
+ L.push(pad('founder') + ': ' + y(f.founder || ''));
818
+ L.push(pad('email') + ': ' + y(f.email || ''));
819
+ L.push(pad('title_icon') + ': ' + y(f.title_icon || 'robot'));
820
+ L.push(pad('locale') + ': "en-US"');
821
+ L.push('# Generated monogram — replace with your own image under assets/images/.');
822
+ L.push(pad('logo') + ': /assets/images/logo.svg');
823
+ L.push('');
824
+ L.push('# ── GitHub ─────────────────────────────────────────────────────────');
825
+ L.push(pad('github_user') + ': &github_user ' + y(c.ghUser));
826
+ L.push(pad('repository_name') + ': &github_repository ' + y(c.repo));
827
+ L.push(pad('repository') + ': [*github_user, "/", *github_repository]');
828
+ L.push('');
829
+ L.push('# ── URLs & deployment ──────────────────────────────────────────────');
830
+ L.push('# user/org site → url: https://<user>.github.io baseurl: ""');
831
+ L.push('# project site → url: https://<user>.github.io baseurl: "/repo"');
832
+ L.push(pad('url') + ': ' + y(c.url));
833
+ L.push(pad('baseurl') + ': ' + y(c.baseurl));
834
+ if (c.remote) {
835
+ L.push(pad('remote_theme') + ': ' + y(f.remote_theme || 'bamr87/zer0-mistakes'));
836
+ } else {
837
+ L.push(pad('theme') + ': "jekyll-theme-zer0"');
838
+ }
839
+ L.push(pad('permalink') + ': ' + (f.permalink || '/:categories/:title/'));
840
+ L.push(pad('port') + ': ' + c.port);
841
+ L.push('');
842
+ L.push('# ── Build ──────────────────────────────────────────────────────────');
843
+ L.push(pad('markdown') + ': kramdown');
844
+ L.push(pad('highlighter') + ': rouge');
845
+ L.push(pad('public_folder') + ': assets');
846
+ L.push('# Collections live under pages/_<name>/ — collections_dir is the key Jekyll');
847
+ L.push('# reads; pages_dir is the theme\'s own alias for the same folder.');
848
+ L.push(pad('collections_dir') + ': pages');
849
+ L.push(pad('pages_dir') + ': pages');
850
+ L.push('kramdown:');
851
+ L.push(' input: GFM');
852
+ L.push(' syntax_highlighter: rouge');
853
+ L.push('');
854
+ L.push('# ── Collections (pages/_<name>/) ───────────────────────────────────');
855
+ L.push('collections:');
856
+ c.collections.forEach(function (col) {
857
+ L.push(' ' + col.id + ':');
858
+ L.push(' output: true');
859
+ L.push(' permalink: ' + col.permalink);
860
+ if (col.id === 'docs' && c.plan.navigation.sidebar === 'docs') {
861
+ L.push(' sidebar: # curated tree in _data/navigation/docs.yml');
862
+ L.push(' nav: docs');
863
+ L.push(' title: "Docs"');
864
+ L.push(' icon: bi-book');
865
+ }
866
+ });
867
+ L.push('');
868
+ L.push('# ── Default front matter ───────────────────────────────────────────');
869
+ L.push('defaults:');
870
+ c.collections.forEach(function (col) {
871
+ L.push(' - scope:');
872
+ L.push(' path: ""');
873
+ L.push(' type: "' + col.id + '"');
874
+ L.push(' values:');
875
+ L.push(' layout: "' + col.layout + '"');
876
+ if (col.id === 'posts') L.push(' author: "default"');
877
+ });
878
+ L.push(' - scope:');
879
+ L.push(' path: ""');
880
+ L.push(' type: "pages"');
881
+ L.push(' values:');
882
+ L.push(' layout: "default"');
883
+ L.push('');
884
+ L.push('# ── Plugins (all GitHub Pages–safe) ────────────────────────────────');
885
+ L.push('plugins:');
886
+ L.push(' - jekyll-feed');
887
+ L.push(' - jekyll-sitemap');
888
+ L.push(' - jekyll-seo-tag');
889
+ L.push(' - jekyll-include-cache');
890
+ L.push(' - jekyll-paginate');
891
+ if (c.remote) L.push(' - jekyll-remote-theme');
892
+ L.push('');
893
+ if (c.plan.navigation.sidebar === 'auto') {
894
+ L.push('# ── Sidebar ────────────────────────────────────────────────────────');
895
+ L.push('# auto = the theme picks the best tree per collection (curated file, folder tree, categories).');
896
+ L.push('sidebar:');
897
+ L.push(' nav: auto');
898
+ L.push('');
899
+ }
900
+ L.push('# ── Appearance ─────────────────────────────────────────────────────');
901
+ L.push('# Skin = base palette + backgrounds; assets/css/user-overrides.css layers the');
902
+ L.push('# palette, fonts and corner radius chosen in the Site Builder on top.');
903
+ L.push('# user_overrides is what makes the theme LINK that stylesheet (core/head.html);');
904
+ L.push('# without it the file ships but nothing on the site changes.');
905
+ L.push(pad('user_overrides') + ': true');
906
+ L.push(pad('theme_skin') + ': ' + y(f.theme_skin || 'air') + ' # air | aqua | dirt | neon | mint | plum | sunrise');
907
+ L.push(pad('color_mode_default') + ': ' + (f.color_mode_default || 'auto') + ' # auto | light | dark');
908
+ L.push(pad('color_mode_lock') + ': ' + (f.color_mode_lock ? 'true' : 'false'));
909
+ L.push('theme_background:');
910
+ if (c.remote) {
911
+ L.push(' enabled: ' + (f.theme_background === false ? 'false' : 'true'));
912
+ } else {
913
+ L.push(' # The published gem ships no assets/backgrounds/ (kept small); the SVG');
914
+ L.push(' # layers are only available with remote_theme. Off to avoid 404s.');
915
+ L.push(' enabled: false');
916
+ }
917
+ L.push('');
918
+ L.push('# ── Author (used by article layouts) ───────────────────────────────');
919
+ L.push('author:');
920
+ L.push(' name: ' + y(f.founder || c.title));
921
+ L.push(' email: ' + y(f.email || ''));
922
+ L.push(' bio: ' + y(f.tagline || ''));
923
+ if (f.twitter_username) L.push(' twitter: ' + y(f.twitter_username));
924
+ if (f.linkedin_username) L.push(' linkedin: ' + y(f.linkedin_username));
925
+ L.push('');
926
+ L.push('# ── Integrations ───────────────────────────────────────────────────');
927
+ L.push('# Keys never live here. Analytics run only in production after consent.');
928
+ L.push(pad('google_analytics') + ': ' + y(f.google_analytics || ''));
929
+ L.push('posthog:');
930
+ L.push(' enabled: ' + (c.on('posthog') && f['posthog.api_key'] ? 'true' : 'false'));
931
+ L.push(' api_key: ' + y(f['posthog.api_key'] || ''));
932
+ L.push('cookie_consent:');
933
+ L.push(' enabled: true');
934
+ L.push(' expiry_days: 365');
935
+ L.push('giscus:');
936
+ L.push(' enabled: ' + (c.on('giscus') && f['giscus.repo_id'] && f['giscus.category_id'] ? 'true' : 'false'));
937
+ L.push(' data-repo-id: ' + y(f['giscus.repo_id'] || ''));
938
+ L.push(' data-category-id: ' + y(f['giscus.category_id'] || ''));
939
+ L.push('obsidian:');
940
+ L.push(' enabled: ' + (c.on('obsidian') ? 'true' : 'false'));
941
+ L.push('page_feedback:');
942
+ L.push(' enabled: ' + (c.on('page_feedback') ? 'true' : 'false'));
943
+ L.push(' mode: url # pre-filled github.com issue forms; no token needed');
944
+ L.push(' default_labels: ["page-feedback"]');
945
+ L.push('ai_chat:');
946
+ L.push(' enabled: ' + (c.on('ai_chat') ? 'true' : 'false'));
947
+ L.push(' provider: ' + (f.ai_provider || 'anthropic') + ' # anthropic (Claude) | xai (Grok) — the proxy needs the matching key');
948
+ L.push(' auth_mode: proxy');
949
+ L.push(' proxy_ready: false # set true once templates/deploy/chat-proxy is deployed');
950
+ L.push(' endpoint: "/api/chat"');
951
+ if (f.image_provider) {
952
+ var ip = catalogItem('image_providers', f.image_provider) || {};
953
+ L.push('# Preview images for posts (theme generator; the key lives in .env).');
954
+ L.push('preview_images:');
955
+ L.push(' enabled: true');
956
+ L.push(' provider: ' + f.image_provider + ' # ' + (ip.label || f.image_provider) + ' — ' + (ip.env || 'API key') + ' in .env');
957
+ L.push(' model: ' + y(ip.model || ''));
958
+ L.push(' output_dir: assets/images/previews');
959
+ }
960
+ L.push('');
961
+ if (f.twitter_username || f.linkedin_username) {
962
+ L.push('# ── Social ─────────────────────────────────────────────────────────');
963
+ if (f.twitter_username) L.push(pad('twitter_username') + ': ' + y(f.twitter_username));
964
+ if (f.linkedin_username) L.push(pad('linkedin_username') + ': ' + y(f.linkedin_username));
965
+ L.push('');
966
+ }
967
+ L.push('# ── Exclude from build ─────────────────────────────────────────────');
968
+ L.push('exclude:');
969
+ ['.sass-cache/', '.jekyll-cache/', '.jekyll-metadata', 'node_modules/', 'vendor/', 'Gemfile.lock',
970
+ 'docker-compose.yml', 'zer0.install.yml', '.env', '.env.example', 'README.md', '"*.log"'].forEach(function (e) {
971
+ L.push(' - ' + e);
972
+ });
973
+ return L.join('\n') + '\n';
974
+ }
975
+
976
+ function genDevConfig(c) {
977
+ var L = [];
978
+ L.push('# Development overrides — layered on top of _config.yml by docker-compose.yml:');
979
+ L.push('# jekyll serve --config _config.yml,_config_dev.yml');
980
+ L.push('');
981
+ if (c.remote) {
982
+ L.push('# Keep the remote theme in dev too (no theme gem needed locally).');
983
+ L.push('remote_theme: ' + y(c.f.remote_theme || 'bamr87/zer0-mistakes'));
984
+ } else {
985
+ L.push('remote_theme: false');
986
+ L.push('theme: "jekyll-theme-zer0"');
987
+ }
988
+ L.push('');
989
+ L.push('url: "http://localhost:' + c.port + '"');
990
+ L.push('baseurl: ""');
991
+ L.push('host: "0.0.0.0"');
992
+ L.push('port: ' + c.port);
993
+ L.push('livereload: true');
994
+ // Incremental regeneration is still experimental in Jekyll and only
995
+ // rebuilds documents whose own source changed — so adding a post leaves
996
+ // every LISTING page (the collection index, the home page's latest-posts
997
+ // section, the feed) stale, showing a site that no longer exists. These
998
+ // sites are small: a full rebuild is a second or two, and always right.
999
+ L.push('incremental: false');
1000
+ L.push('show_drafts: true');
1001
+ L.push('future: true');
1002
+ L.push('');
1003
+ L.push('# Never send analytics from a dev build.');
1004
+ L.push('google_analytics: null');
1005
+ L.push('posthog:');
1006
+ L.push(' enabled: false');
1007
+ L.push('');
1008
+ L.push('# Jekyll REPLACES (does not merge) `exclude` and `plugins` when layering configs,');
1009
+ L.push('# so this list must include everything from _config.yml.');
1010
+ L.push('exclude:');
1011
+ ['.sass-cache/', '.jekyll-cache/', '.jekyll-metadata', 'node_modules/', 'vendor/', 'Gemfile.lock',
1012
+ 'docker-compose.yml', 'zer0.install.yml', '.env', '.env.example', 'README.md', '"*.log"'].forEach(function (e) {
1013
+ L.push(' - ' + e);
1014
+ });
1015
+ return L.join('\n') + '\n';
1016
+ }
1017
+
1018
+ function genGemfile(c) {
1019
+ var L = ['source "https://rubygems.org"', ''];
1020
+ if (c.remote) {
1021
+ L.push('# Current Jekyll + the remote-theme plugin (loads ' + (c.f.remote_theme || 'bamr87/zer0-mistakes') + ' from GitHub).');
1022
+ L.push('gem "jekyll", "~> 4.3"');
1023
+ L.push('gem "jekyll-remote-theme"');
1024
+ } else {
1025
+ L.push('# Jekyll and the theme gem, pinned to the installed major.');
1026
+ L.push('gem "jekyll", "~> 4.3"');
1027
+ L.push('gem "jekyll-theme-zer0"');
1028
+ }
1029
+ L.push('');
1030
+ L.push('group :jekyll_plugins do');
1031
+ L.push(' gem "jekyll-feed"');
1032
+ L.push(' gem "jekyll-sitemap"');
1033
+ L.push(' gem "jekyll-seo-tag"');
1034
+ L.push(' gem "jekyll-include-cache"');
1035
+ L.push(' gem "jekyll-paginate"');
1036
+ L.push('end');
1037
+ L.push('');
1038
+ L.push('# Ruby 3+ no longer bundles webrick (needed by `jekyll serve`).');
1039
+ L.push('gem "webrick", "~> 1.8"');
1040
+ L.push('');
1041
+ L.push('platforms :windows, :jruby do');
1042
+ L.push(' gem "tzinfo"');
1043
+ L.push(' gem "tzinfo-data"');
1044
+ L.push('end');
1045
+ return L.join('\n') + '\n';
1046
+ }
1047
+
1048
+ function genCompose(c) {
1049
+ var L = [];
1050
+ L.push('# ' + c.title + ' — development server');
1051
+ L.push('# Generated by the zer0-mistakes Site Builder. Start with: docker compose up');
1052
+ L.push('services:');
1053
+ L.push(' jekyll:');
1054
+ L.push(' image: ruby:3.3');
1055
+ L.push(' working_dir: /site');
1056
+ var lr = c.port + 1;
1057
+ // A folded block (>-) joins these lines with spaces ONLY while every
1058
+ // continuation line keeps the same indentation as the first. Indenting
1059
+ // them further makes YAML preserve the line breaks, and `sh -c` then runs
1060
+ // the trailing options as a separate command — Jekyll starts without the
1061
+ // dev config and binds to 127.0.0.1, unreachable from the host.
1062
+ L.push(' command: >-');
1063
+ L.push(' sh -c "bundle config set --local path vendor/bundle');
1064
+ L.push(' && (bundle check || bundle install --jobs 4 --retry 3)');
1065
+ L.push(' && bundle exec jekyll serve --watch --livereload --livereload-port ' + lr + ' --force_polling');
1066
+ L.push(' --config _config.yml,_config_dev.yml --host 0.0.0.0 --port ' + c.port + '"');
1067
+ L.push(' ports:');
1068
+ L.push(' - "' + c.port + ':' + c.port + '"');
1069
+ L.push(' - "' + lr + ':' + lr + '" # LiveReload on port+1, so two Jekyll sites can run side by side');
1070
+ L.push(' volumes:');
1071
+ L.push(' - .:/site');
1072
+ L.push(' - bundle_cache:/site/vendor/bundle');
1073
+ L.push(' environment:');
1074
+ L.push(' JEKYLL_ENV: development');
1075
+ L.push(' PAGES_REPO_NWO: ' + c.ghUser + '/' + c.repo);
1076
+ L.push(' stdin_open: true');
1077
+ L.push(' tty: true');
1078
+ L.push('');
1079
+ L.push('volumes:');
1080
+ L.push(' bundle_cache:');
1081
+ L.push(' # Shared across every site generated by the Site Builder, so the second');
1082
+ L.push(' # site does not reinstall the same gems. Drop the name for a private cache.');
1083
+ L.push(' name: zer0-sites-bundle-cache');
1084
+ return L.join('\n') + '\n';
1085
+ }
1086
+
1087
+ // ── landing page (plan-driven) ───────────────────────────────────
1088
+
1089
+ var COLLECTION_BLURB = {
1090
+ posts: { title: 'Posts', text: 'Dated writing, newest first, with an RSS feed.', icon: 'bi-journal-text' },
1091
+ docs: { title: 'Guides', text: 'Evergreen documentation with a reading sidebar.', icon: 'bi-book' },
1092
+ notes: { title: 'Notes', text: 'Short, interlinked notes that grow over time.', icon: 'bi-sticky' },
1093
+ quickstart: { title: 'Quickstart', text: 'Ordered steps to get going in minutes.', icon: 'bi-flag' },
1094
+ recipes: { title: 'Recipes', text: 'Structured recipes that scale to your table.', icon: 'bi-egg-fried' },
1095
+ notebooks: { title: 'Notebooks', text: 'Rendered Jupyter notebooks.', icon: 'bi-journal-code' },
1096
+ quests: { title: 'Quests', text: 'Learning paths with levels and badges.', icon: 'bi-controller' },
1097
+ about: { title: 'About', text: 'Who is behind this site and how to reach them.', icon: 'bi-info-circle' }
1098
+ };
1099
+
1100
+ function hasCollection(c, id) { return c.collections.some(function (x) { return x.id === id; }); }
1101
+
1102
+ function firstSentence(text) {
1103
+ var m = String(text || '').trim().match(/^[^.!?]+[.!?]?/);
1104
+ return m ? m[0].trim() : String(text || '').trim();
1105
+ }
1106
+
1107
+ function defaultCtas(c) {
1108
+ var ctas = [];
1109
+ var lead = c.collections.filter(function (x) { return x.id !== 'about'; })[0];
1110
+ if (lead) {
1111
+ var verb = lead.id === 'docs' ? 'Read the docs' : lead.id === 'recipes' ? 'Browse the recipes' : lead.id === 'notes' ? 'Wander the notes' : lead.id === 'quickstart' ? 'Start here' : 'Read the latest';
1112
+ ctas.push({ label: verb, url: '/' + lead.id + '/', variant: 'primary', icon: (COLLECTION_BLURB[lead.id] || {}).icon });
1113
+ }
1114
+ if (hasCollection(c, 'about')) ctas.push({ label: 'About', url: '/about/', variant: 'outline', icon: 'bi-info-circle' });
1115
+ return ctas;
1116
+ }
1117
+
1118
+ function defaultSection(type, c) {
1119
+ var cols = c.collections.filter(function (x) { return x.id !== 'about'; });
1120
+ var brief = c.f.brief || c.f.description || '';
1121
+ var pages = c.plan.pages || [];
1122
+ switch (type) {
1123
+ case 'features': {
1124
+ var items = cols.slice(0, 3).map(function (col) { return Object.assign({}, COLLECTION_BLURB[col.id] || { title: titleCase(col.id), text: '', icon: 'bi-folder' }); });
1125
+ var fill = [{ title: 'Written to last', text: 'Evergreen pages that stay useful, revised when the facts change.', icon: 'bi-hourglass-split' },
1126
+ { title: 'Easy to follow', text: 'Plain language, worked examples, and no assumed jargon.', icon: 'bi-signpost-2' },
1127
+ { title: 'Made in the open', text: 'Every page has a source link; corrections are welcome.', icon: 'bi-github' }];
1128
+ while (items.length < 3) items.push(fill[items.length]);
1129
+ return { type: 'features', id: 'features', heading: 'What you\'ll find here', lead: firstSentence(brief), variant: 'muted', items: items };
1130
+ }
1131
+ case 'cards': {
1132
+ var cards = pages.slice(0, 6).map(function (p) { return { title: p.title, text: p.description || '', icon: (COLLECTION_BLURB[p.collection] || {}).icon, url: pageUrl(p) }; });
1133
+ if (!cards.length) cards = cols.slice(0, 3).map(function (col) { var b = COLLECTION_BLURB[col.id] || {}; return { title: b.title || titleCase(col.id), text: b.text || '', icon: b.icon, url: '/' + col.id + '/' }; });
1134
+ return { type: 'cards', id: 'highlights', heading: 'Start with these', lead: '', variant: 'default', items: cards };
200
1135
  }
201
- if (fields.linkedin_username) {
202
- lines.push(pad('linkedin_username') + ': ' + yamlValue(fields.linkedin_username));
1136
+ case 'steps': {
1137
+ var lead = cols[0];
1138
+ return { type: 'steps', id: 'get-started', heading: 'Get started in three steps', lead: '', variant: 'default', items: [
1139
+ { title: 'Start here', text: lead ? 'Open the ' + (COLLECTION_BLURB[lead.id] || {}).title.toLowerCase() + ' and read the first page.' : 'Read the first page.' },
1140
+ { title: 'Follow along', text: 'Each page links to the next; the sidebar keeps your place.' },
1141
+ { title: 'Say hello', text: 'Questions and corrections are welcome on the About page.' }] };
203
1142
  }
204
- lines.push('');
205
- }
206
-
207
- // Exclude
208
- lines.push('# ── Exclude from build ─────────────────────────────────────────────');
209
- lines.push('exclude:');
210
- lines.push(' - .sass-cache/');
211
- lines.push(' - .jekyll-cache/');
212
- lines.push(' - node_modules/');
213
- lines.push(' - vendor/');
214
- lines.push(' - Gemfile.lock');
215
- lines.push(' - scripts/');
216
- lines.push(' - test/');
217
- lines.push(' - templates/');
218
- lines.push(' - "*.log"');
1143
+ case 'stats':
1144
+ return { type: 'stats', id: 'by-the-numbers', heading: 'By the numbers', variant: 'muted', items: [
1145
+ { value: String(cols.length), label: cols.length === 1 ? 'collection' : 'collections' },
1146
+ { value: String(Math.max(pages.length, 1)), label: pages.length === 1 ? 'page to start with' : 'pages to start with' },
1147
+ { value: '1', label: 'author who reads every comment' }] };
1148
+ case 'faq':
1149
+ return { type: 'faq', id: 'faq', heading: 'Questions people ask', variant: 'default', items: [
1150
+ { title: 'Who is this for?', text: brief || 'Anyone curious about the subject.' },
1151
+ { title: 'How often is it updated?', text: 'Whenever there is something worth writing down. Subscribe to the feed to hear about it.' },
1152
+ { title: 'Can I suggest a correction?', text: 'Yes — every page has a source link and an Improve-this-page button.' }] };
1153
+ case 'cta':
1154
+ return hasCollection(c, 'posts')
1155
+ ? { type: 'cta', id: 'subscribe', heading: 'Stay in the loop', lead: 'New posts land in the feed first.', variant: 'inverse', cta: { label: 'Subscribe via RSS', url: '/feed.xml' } }
1156
+ : { type: 'cta', id: 'contact', heading: 'Questions?', lead: 'Get in touch — details are on the About page.', variant: 'inverse', cta: { label: 'Get in touch', url: '/about/' } };
1157
+ case 'latest_posts':
1158
+ return { type: 'latest_posts', id: 'latest', heading: 'Latest', limit: 3, variant: 'default' };
1159
+ case 'quote':
1160
+ return { type: 'quote', id: 'why', variant: 'muted', items: [{ quote: firstSentence(brief) || 'Made with care.', author: c.f.founder || '' }] };
1161
+ case 'text':
1162
+ default:
1163
+ return { type: 'text', id: 'about-this-site', heading: 'About this site', variant: 'default', body: brief || 'Edit `_data/landing.yml` to change this text.' };
1164
+ }
1165
+ }
219
1166
 
220
- return lines.join('\n');
1167
+ function pageUrl(p) {
1168
+ if (p.collection === 'posts') return '/posts/' + p.slug + '/';
1169
+ return '/' + p.collection + '/' + p.slug + '/';
1170
+ }
1171
+
1172
+ /** Resolve the landing plan: template defaults filled in wherever the plan is silent. */
1173
+ function landingPlan(c) {
1174
+ var tpl = catalogItem('landing_templates', c.plan.landing.template) || catalogItem('landing_templates', 'hero') || { id: 'hero', sections: ['features', 'latest_posts', 'cta'] };
1175
+ var hero = Object.assign({
1176
+ eyebrow: c.f.subtitle || '',
1177
+ headline: c.f.tagline || c.title,
1178
+ subheadline: c.f.description || firstSentence(c.f.brief),
1179
+ align: tpl.id === 'editorial' ? 'start' : 'center',
1180
+ variant: tpl.id === 'showcase' ? 'inverse' : 'default',
1181
+ ctas: defaultCtas(c)
1182
+ }, c.plan.landing.hero || {});
1183
+ var sections = (c.plan.landing.sections || (tpl.sections || []).map(function (t) { return defaultSection(t, c); }))
1184
+ .filter(function (s) { return !(s.type === 'latest_posts' && !hasCollection(c, 'posts')); })
1185
+ .map(function (s, i) { return Object.assign({ id: (s.type || 'section') + '-' + (i + 1) }, s); });
1186
+ return { template: tpl.id, hero: hero, sections: sections };
1187
+ }
1188
+
1189
+ function genLandingData(c) {
1190
+ var lp = landingPlan(c);
1191
+ var head = ['# _data/landing.yml — content for the landing page (index.md renders it).',
1192
+ '# Generated by the zer0-mistakes Site Builder from your site plan. Edit freely:',
1193
+ '# hero.{eyebrow,headline,subheadline,align,variant,ctas[]} and sections[] of type',
1194
+ '# ' + catalog('section_types').map(function (s) { return s.id; }).join(' | ') + '.', ''];
1195
+ return head.join('\n') + toYaml({ template: lp.template, hero: lp.hero, sections: lp.sections });
221
1196
  }
222
1197
 
223
- // ── actions ────────────────────────────────────────────────────────
1198
+ function genIndex(c) {
1199
+ var lp = landingPlan(c);
1200
+ if (lp.template === 'minimal') {
1201
+ var L = ['---', 'layout: home', 'title: ' + y(c.title), 'description: ' + y(c.f.description || ''), 'permalink: /', '---', ''];
1202
+ if (c.f.tagline) { L.push('## ' + c.f.tagline); L.push(''); }
1203
+ L.push(c.f.brief ? c.f.brief.trim() : 'Welcome. This home page is `index.md` — edit it to say hello your way.');
1204
+ L.push('');
1205
+ if (hasCollection(c, 'posts')) {
1206
+ L.push('## Latest', '', '{% for post in site.posts limit: 5 %}', '- [{{ post.title }}]({{ post.url | relative_url }}) — {{ post.date | date: "%B %-d, %Y" }}', '{% endfor %}', '');
1207
+ }
1208
+ return L.join('\n');
1209
+ }
1210
+ // The landing engine: index.md reads _data/landing.yml so the copy can be
1211
+ // edited without touching markup. Only theme includes and Bootstrap classes.
1212
+ return ['---', 'layout: home', 'title: ' + y(c.title), 'description: ' + y(c.f.description || ''), 'permalink: /', 'hide_title: true', 'rss_subscribe: false', '---',
1213
+ '{%- assign landing = site.data.landing -%}',
1214
+ '{%- assign hero = landing.hero -%}',
1215
+ '<section class="zer0-landing-hero py-5{% if hero.variant == "inverse" %} bg-primary text-white{% elsif hero.variant == "muted" %} bg-body-tertiary{% endif %}" data-landing-template="{{ landing.template }}">',
1216
+ ' <div class="container-xl text-{{ hero.align | default: "center" }}">',
1217
+ ' {% if hero.eyebrow %}<p class="text-uppercase small fw-semibold mb-2 opacity-75">{{ hero.eyebrow }}</p>{% endif %}',
1218
+ ' <h1 class="display-4 fw-bold mb-3">{{ hero.headline | default: site.title }}</h1>',
1219
+ ' {% if hero.subheadline %}<p class="lead mb-4 mx-auto" style="max-width: 44rem;">{{ hero.subheadline }}</p>{% endif %}',
1220
+ ' {% if hero.ctas and hero.ctas.size > 0 %}',
1221
+ ' <div class="d-flex flex-wrap gap-2 justify-content-{{ hero.align | default: "center" }}">',
1222
+ ' {%- for cta in hero.ctas -%}',
1223
+ ' {%- assign cta_variant = cta.variant | default: "primary" -%}',
1224
+ ' {%- comment -%} An outlined button is white on an inverse hero and primary-coloured everywhere else. {%- endcomment -%}',
1225
+ ' {%- if cta_variant == "outline" and hero.variant == "inverse" %}{% assign cta_variant = "outline-light" %}{% endif -%}',
1226
+ ' {% include components/cta-button.html label=cta.label url=cta.url variant=cta_variant icon=cta.icon size="lg" %}',
1227
+ ' {%- endfor -%}',
1228
+ ' </div>',
1229
+ ' {% endif %}',
1230
+ // A hero illustration sits beside the headline it illustrates, so it is
1231
+ // decorative: empty alt keeps a screen reader from hearing the headline
1232
+ // twice. Swap in a real description if the picture carries information.
1233
+ ' {% if hero.image %}<img src="{{ hero.image | relative_url }}" alt="" class="zer0-landing-hero-image img-fluid rounded-3 shadow-sm mt-4" loading="lazy" decoding="async">{% endif %}',
1234
+ ' </div>',
1235
+ '</section>',
1236
+ '',
1237
+ '{% for s in landing.sections %}',
1238
+ '{%- capture body -%}',
1239
+ ' {%- case s.type -%}',
1240
+ ' {%- when "features" -%}',
1241
+ ' <div class="row g-4">{% for it in s.items %}<div class="col-md-4"><div class="card h-100 border-0 shadow-sm"><div class="card-body"><i class="bi {{ it.icon | default: "bi-check-circle" }} fs-2 text-primary" aria-hidden="true"></i><h3 class="h5 mt-2">{{ it.title }}</h3><p class="mb-0 text-body-secondary">{{ it.text }}</p></div></div></div>{% endfor %}</div>',
1242
+ ' {%- when "cards" -%}',
1243
+ ' <div class="row g-4">{% for it in s.items %}<div class="col-md-6 col-lg-4"><a class="card h-100 text-decoration-none" href="{{ it.url | default: "#" | relative_url }}"><div class="card-body"><i class="bi {{ it.icon | default: "bi-arrow-right-circle" }} fs-3 text-primary" aria-hidden="true"></i><h3 class="h5 mt-2 card-title">{{ it.title }}</h3><p class="card-text text-body-secondary mb-0">{{ it.text }}</p></div></a></div>{% endfor %}</div>',
1244
+ ' {%- when "steps" -%}',
1245
+ ' <ol class="list-unstyled row g-4 mb-0">{% for it in s.items %}<li class="col-md-4 d-flex gap-3"><span class="badge rounded-pill text-bg-primary fs-6 align-self-start">{{ forloop.index }}</span><div><h3 class="h5 mb-1">{{ it.title }}</h3><p class="mb-0 text-body-secondary">{{ it.text }}</p></div></li>{% endfor %}</ol>',
1246
+ ' {%- when "stats" -%}',
1247
+ ' <div class="row g-4 text-center">{% for it in s.items %}<div class="col-6 col-md-3"><div class="display-5 fw-bold text-primary">{{ it.value }}</div><div class="text-body-secondary">{{ it.label }}</div></div>{% endfor %}</div>',
1248
+ ' {%- when "faq" -%}',
1249
+ ' <div class="accordion" id="faq-{{ forloop.index }}">{% for it in s.items %}<div class="accordion-item"><h3 class="accordion-header"><button class="accordion-button{% unless forloop.first %} collapsed{% endunless %}" type="button" data-bs-toggle="collapse" data-bs-target="#faq-{{ forloop.parentloop.index }}-{{ forloop.index }}">{{ it.title }}</button></h3><div id="faq-{{ forloop.parentloop.index }}-{{ forloop.index }}" class="accordion-collapse collapse{% if forloop.first %} show{% endif %}" data-bs-parent="#faq-{{ forloop.parentloop.index }}"><div class="accordion-body">{{ it.text }}</div></div></div>{% endfor %}</div>',
1250
+ ' {%- when "cta" -%}',
1251
+ ' <div class="text-center">{% if s.cta %}{% include components/cta-button.html label=s.cta.label url=s.cta.url variant="light" size="lg" %}{% endif %}</div>',
1252
+ ' {%- when "latest_posts" -%}',
1253
+ ' <div class="row g-4">{% for post in site.posts limit: s.limit | default: 3 %}<div class="col-md-4"><a class="card h-100 text-decoration-none" href="{{ post.url | relative_url }}"><div class="card-body"><p class="small text-body-secondary mb-1">{{ post.date | date: "%B %-d, %Y" }}</p><h3 class="h5 card-title">{{ post.title }}</h3><p class="card-text text-body-secondary mb-0">{{ post.description | default: post.excerpt | strip_html | truncate: 140 }}</p></div></a></div>{% endfor %}</div>',
1254
+ ' {%- when "quote" -%}',
1255
+ ' {% assign q = s.items | first %}<figure class="text-center mb-0"><blockquote class="blockquote fs-3"><p>“{{ q.quote }}”</p></blockquote>{% if q.author %}<figcaption class="blockquote-footer mt-2">{{ q.author }}</figcaption>{% endif %}</figure>',
1256
+ ' {%- else -%}',
1257
+ ' <div class="mx-auto" style="max-width: 44rem;">{{ s.body | markdownify }}</div>',
1258
+ ' {%- endcase -%}',
1259
+ '{%- endcapture -%}',
1260
+ '{% include components/section.html id=s.id variant=s.variant heading=s.heading lead=s.lead content=body %}',
1261
+ '{% endfor %}', ''].join('\n');
1262
+ }
1263
+
1264
+ // ── theme overrides (palette · fonts · corners) ─────────────────
1265
+
1266
+ function resolvedPalette(c) {
1267
+ var p = c.plan.theme.palette || { preset: 'skin' };
1268
+ if (p.preset === 'custom') return { id: 'custom', primary: p.primary, secondary: p.secondary, accent: p.accent };
1269
+ var preset = catalogItem('palettes', p.preset);
1270
+ return preset && preset.primary ? preset : null;
1271
+ }
1272
+
1273
+ function genUserOverrides(c) {
1274
+ var L = ['/* assets/css/user-overrides.css — loaded LAST by the theme, so anything here wins.',
1275
+ ' Generated by the zer0-mistakes Site Builder from your site plan (palette,',
1276
+ ' fonts, corners). Edit freely; the theme never overwrites this file. */', ''];
1277
+ var pal = resolvedPalette(c);
1278
+ if (pal) {
1279
+ L.push('/* Palette "' + pal.id + '" layered over the "' + (c.f.theme_skin || 'air') + '" skin. */');
1280
+ L.push(':root, html[data-theme-skin], html[data-theme-skin][data-bs-theme] {');
1281
+ L.push(' --bs-primary: ' + pal.primary + ' !important;');
1282
+ L.push(' --bs-primary-rgb: ' + hexToRgb(pal.primary) + ' !important;');
1283
+ L.push(' --zer0-color-primary: ' + pal.primary + ' !important;');
1284
+ L.push(' --bs-link-color: ' + pal.primary + ' !important;');
1285
+ L.push(' --bs-link-color-rgb: ' + hexToRgb(pal.primary) + ' !important;');
1286
+ L.push(' --bs-link-hover-color: ' + shade(pal.primary, -0.18) + ' !important;');
1287
+ L.push(' --bs-secondary: ' + pal.secondary + ' !important;');
1288
+ L.push(' --bs-secondary-rgb: ' + hexToRgb(pal.secondary) + ' !important;');
1289
+ L.push(' --zer0-color-accent: ' + pal.accent + ' !important;');
1290
+ L.push('}');
1291
+ L.push('.btn-primary, .badge.text-bg-primary, .bg-primary {');
1292
+ L.push(' --bs-btn-bg: ' + pal.primary + '; --bs-btn-border-color: ' + pal.primary + ';');
1293
+ L.push(' --bs-btn-hover-bg: ' + shade(pal.primary, -0.12) + '; --bs-btn-hover-border-color: ' + shade(pal.primary, -0.18) + ';');
1294
+ L.push(' --bs-btn-active-bg: ' + shade(pal.primary, -0.2) + '; --bs-btn-active-border-color: ' + shade(pal.primary, -0.25) + ';');
1295
+ L.push(' background-color: ' + pal.primary + ';');
1296
+ L.push('}');
1297
+ L.push('.btn-outline-primary { --bs-btn-color: ' + pal.primary + '; --bs-btn-border-color: ' + pal.primary + '; --bs-btn-hover-bg: ' + pal.primary + '; --bs-btn-hover-border-color: ' + pal.primary + '; --bs-btn-active-bg: ' + pal.primary + '; }');
1298
+ L.push('');
1299
+ }
1300
+ var fp = catalogItem('font_pairings', c.plan.theme.fonts);
1301
+ if (fp && fp.google) {
1302
+ L.push('/* Fonts: ' + fp.label + ' (loaded from Google Fonts by _includes/custom/head.html). */');
1303
+ L.push(':root {');
1304
+ L.push(' --zer0-font-sans: ' + fp.body + ';');
1305
+ L.push(' --bs-body-font-family: ' + fp.body + ';');
1306
+ L.push(' --zer0-font-heading: ' + fp.heading + ';');
1307
+ L.push('}');
1308
+ L.push('body { font-family: var(--bs-body-font-family); }');
1309
+ L.push('h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, .display-1, .display-2, .display-3, .display-4, .display-5, .display-6, .navbar-brand { font-family: var(--zer0-font-heading); }');
1310
+ L.push('');
1311
+ }
1312
+ var r = catalogItem('radius_options', c.plan.theme.radius);
1313
+ if (r && r.id !== 'soft') {
1314
+ L.push('/* Corners: ' + r.label + '. */');
1315
+ L.push(':root {');
1316
+ L.push(' --zer0-radius-sm: ' + r.sm + '; --zer0-radius: ' + r.base + '; --zer0-radius-md: ' + r.base + '; --zer0-radius-lg: ' + r.lg + '; --zer0-radius-xl: ' + r.xl + ';');
1317
+ L.push(' --bs-border-radius-sm: ' + r.sm + '; --bs-border-radius: ' + r.base + '; --bs-border-radius-lg: ' + r.lg + '; --bs-border-radius-xl: ' + r.xl + ';');
1318
+ L.push('}');
1319
+ L.push('.btn, .card, .form-control, .form-select, .alert, .badge:not(.rounded-pill), .accordion-item, .nav-pills .nav-link { border-radius: var(--bs-border-radius); }');
1320
+ L.push('');
1321
+ }
1322
+ if (!pal && !(fp && fp.google) && !(r && r.id !== 'soft')) L.push('/* Nothing overridden yet — the skin\'s own palette, system fonts and soft corners. */');
1323
+ return L.join('\n') + '\n';
1324
+ }
1325
+
1326
+ /**
1327
+ * `user_overrides: true` makes the theme load BOTH assets/css/user-overrides.css
1328
+ * and assets/js/user-overrides.js (see _includes/components/js-cdn.html). We
1329
+ * always generate the CSS, so without this stub every page of every generated
1330
+ * site fired a 404 for the JS half.
1331
+ */
1332
+ function genUserOverridesJs(c) {
1333
+ return ['// assets/js/user-overrides.js — loaded last on every page of ' + c.title + '.',
1334
+ '//',
1335
+ '// The theme loads this file because `user_overrides: true` is set in',
1336
+ '// _config.yml (the same flag loads assets/css/user-overrides.css, which',
1337
+ '// carries your palette, fonts and corner radius).',
1338
+ '//',
1339
+ '// Put site-specific behaviour here. It runs after the theme\'s own scripts,',
1340
+ '// so you can safely read anything they set up.',
1341
+ '',
1342
+ '(function () {',
1343
+ " 'use strict';",
1344
+ ' // Example: log which skin is active, once, in development.',
1345
+ ' // if (location.hostname === "localhost") {',
1346
+ ' // console.info("skin:", document.documentElement.dataset.themeSkin);',
1347
+ ' // }',
1348
+ '})();',
1349
+ ''].join('\n');
1350
+ }
1351
+
1352
+ function genCustomHead(c) {
1353
+ var fp = catalogItem('font_pairings', c.plan.theme.fonts);
1354
+ if (!fp || !fp.google) return null;
1355
+ return ['{% comment %}', ' _includes/custom/head.html — end-of-<head> hook the theme leaves empty for the site.',
1356
+ ' Generated by the Site Builder to load the "' + fp.label + '" font pairing; assets/css/user-overrides.css applies it.',
1357
+ '{% endcomment %}',
1358
+ '<link rel="preconnect" href="https://fonts.googleapis.com">',
1359
+ '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
1360
+ '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?' + fp.google + '&display=swap">', ''].join('\n');
1361
+ }
1362
+
1363
+ // ── navigation (plan-driven) ─────────────────────────────────────
1364
+
1365
+ function plannedPagesFor(c, collection) {
1366
+ return (c.plan.pages || []).filter(function (p) { return p.collection === collection; });
1367
+ }
1368
+
1369
+ function genNav(c) {
1370
+ var L = ['# Main navigation — rendered by the theme navbar.', '# title, url and an optional Bootstrap icon class; nest `children:` for dropdowns.', ''];
1371
+ if (c.plan.navigation.items) return L.join('\n') + toYaml(c.plan.navigation.items);
1372
+ var grouped = c.plan.navigation.style === 'grouped';
1373
+ var items = c.nav.map(function (item) {
1374
+ var out = { title: item.title || item.url, url: item.url || '/' };
1375
+ if (item.icon) out.icon = item.icon;
1376
+ if (grouped) {
1377
+ var col = (item.url || '').replace(/^\/|\/$/g, '');
1378
+ var children = [];
1379
+ if (col === 'docs') children.push({ title: 'Getting started', url: '/docs/getting-started/' });
1380
+ if (col === 'quickstart') children.push({ title: 'First steps', url: '/quickstart/first-steps/' });
1381
+ plannedPagesFor(c, col).slice(0, 6).forEach(function (p) { children.push({ title: p.title, url: pageUrl(p) }); });
1382
+ if (children.length) out.children = children;
1383
+ }
1384
+ return out;
1385
+ });
1386
+ return L.join('\n') + toYaml(items);
1387
+ }
1388
+
1389
+ function genDocsNav(c) {
1390
+ var children = [{ title: 'Getting started', url: '/docs/getting-started/', icon: 'bi-play-circle' }];
1391
+ plannedPagesFor(c, 'docs').forEach(function (p) { children.push({ title: p.title, url: pageUrl(p), description: p.description || undefined }); });
1392
+ return ['# Curated docs sidebar (used when a docs page has sidebar.nav: docs — set for the whole', '# collection in _config.yml by the Site Builder). Add entries as you write pages.', ''].join('\n')
1393
+ + toYaml([{ title: 'Documentation', icon: 'bi-book', url: '/docs/', expanded: true, children: children }]);
1394
+ }
1395
+
1396
+ // ── planned example pages ────────────────────────────────────────
1397
+
1398
+ function pagePath(p) {
1399
+ if (p.collection === 'posts') return 'pages/_posts/' + (p.date || todayISO()) + '-' + p.slug + '.md';
1400
+ return 'pages/_' + p.collection + '/' + p.slug + '.md';
1401
+ }
1402
+
1403
+ function genPlanPage(c, p) {
1404
+ var fm = ['---', 'title: ' + y(p.title)];
1405
+ if (p.description) fm.push('description: ' + y(p.description));
1406
+ // A plan may pin a date (a back-dated post); anything else is stamped now.
1407
+ fm.push('lastmod: ' + nowISO());
1408
+ if (p.collection === 'posts' || p.collection === 'notes') {
1409
+ fm.splice(fm.length - 1, 0, 'date: ' + (p.date && p.date !== c.date ? p.date + 'T09:00:00.000Z' : nowISO()));
1410
+ }
1411
+ if (p.collection === 'recipes') fm.push('layout: recipe', 'cookbook: ' + c.slug);
1412
+ if (p.collection === 'posts') fm.push('author: ' + y(c.f.founder || 'default'));
1413
+ if (p.categories && p.categories.length) fm.push('categories: [' + p.categories.map(function (x) { return y(x); }).join(', ') + ']');
1414
+ if (p.tags && p.tags.length) fm.push('tags: [' + p.tags.map(function (x) { return y(x); }).join(', ') + ']');
1415
+ fm.push('---', '');
1416
+ var body = (p.body || '').trim() || ('# ' + p.title + '\n\n' + (p.description || firstSentence(c.f.brief) || 'Write this page.') + '\n\n_This page was planned by the Site Builder; replace this draft with the real thing._');
1417
+ return fm.join('\n') + body + '\n';
1418
+ }
1419
+
1420
+ function genAbout(c) {
1421
+ var body = (c.f.about_body || '').trim() || (
1422
+ 'Hi, I am ' + (c.f.founder || 'the author of this site') + '.\n\n' +
1423
+ (c.f.brief ? c.f.brief.trim() + '\n\n' : '') +
1424
+ 'This site is built with [zer0-mistakes](https://github.com/bamr87/zer0-mistakes), a Docker-first Jekyll theme. ' +
1425
+ 'Edit `pages/_about/index.md` to tell your own story.'
1426
+ );
1427
+ var L = ['---', 'title: About', 'description: ' + y('About ' + c.title + (c.f.founder ? ' and ' + c.f.founder : '')), 'permalink: /about/', 'lastmod: ' + c.date, '---', '', '# About', '', body, ''];
1428
+ if (c.f.email) L.push('\nContact: <' + c.f.email + '>\n');
1429
+ return L.join('\n');
1430
+ }
1431
+
1432
+ function genWelcome(c) {
1433
+ var title = c.f.welcome_title || ('Welcome to ' + c.title);
1434
+ var body = (c.f.welcome_body || '').trim() || (
1435
+ 'This is the first post on **' + c.title + '**.' +
1436
+ (c.f.brief ? '\n\n' + c.f.brief.trim() : '') +
1437
+ '\n\nPosts live in `pages/_posts/` and are named `YYYY-MM-DD-title.md`. Delete this one whenever you like.'
1438
+ );
1439
+ return ['---', 'title: ' + y(title), 'description: ' + y(c.f.description || ('The first post on ' + c.title)),
1440
+ 'date: ' + nowISO(), 'lastmod: ' + nowISO(), 'categories: [general]', 'tags: [welcome]',
1441
+ 'author: ' + y(c.f.founder || 'default'), 'preview: /assets/images/previews/welcome.png', '---', '', body, ''].join('\n');
1442
+ }
1443
+
1444
+ function genCollectionIndex(c, col) {
1445
+ var label = labelFor(catalog('collections'), col.id);
1446
+ if (col.id === 'recipes') {
1447
+ // The theme's cookbook layout lists site.recipes grouped by course.
1448
+ var cbTitle = /cookbook|recipes|kitchen/i.test(c.title) ? c.title : c.title + ' Cookbook';
1449
+ return ['---', 'layout: cookbook', 'cookbook: ' + c.slug, 'title: ' + y(cbTitle),
1450
+ 'subtitle: ' + y(c.f.tagline || 'Recipes, scaled to your table'), 'author: ' + y(c.f.founder || ''),
1451
+ 'description: ' + y('Every recipe on ' + c.title + ', with serving scaling and US/metric conversion.'),
1452
+ 'permalink: /recipes/', 'lastmod: ' + c.date, '---', '',
1453
+ 'Recipes live in `pages/_recipes/`. Give each one `layout: recipe`, `cookbook: ' + c.slug + '` and the structured front matter shown in the starter recipe.', ''].join('\n');
1454
+ }
1455
+ return ['---', 'layout: collection', 'collection: ' + col.id, 'title: ' + y(label),
1456
+ 'description: ' + y(label + ' on ' + c.title), 'permalink: /' + col.id + '/', 'sort_order: reverse', '---', '',
1457
+ '# ' + label, '', 'Everything in `pages/_' + col.id + '/`, newest first.', ''].join('\n');
1458
+ }
1459
+
1460
+ // ── sample content, one starter document per enabled collection ─────
1461
+ // Each is a real, valid document for its layout so the very first build has
1462
+ // something on every route and shows the front matter that collection needs.
1463
+
1464
+ function genSampleDoc(c) {
1465
+ return ['---', 'title: "Getting Started"', 'description: ' + y('How to run, edit and publish ' + c.title + '.'),
1466
+ 'lastmod: ' + c.date, 'categories: [docs]', 'tags: [getting-started]', '---', '',
1467
+ '# Getting started', '', 'This is the first document in `pages/_docs/`. Docs render on the `default` layout with the reading sidebar.', '',
1468
+ '## Run the site', '', '```bash', 'docker compose up', '```', '',
1469
+ 'Open <http://localhost:' + c.port + '/>. Edit any file under `pages/` and the page reloads.', '',
1470
+ '## Add a doc', '', 'Create `pages/_docs/<slug>.md` with `title`, `description` and `lastmod` front matter. It appears at `/docs/<slug>/` and in the docs index.', ''].join('\n');
1471
+ }
1472
+
1473
+ function genSampleQuickstart(c) {
1474
+ return ['---', 'title: "First Steps"', 'description: ' + y('The first quickstart step for ' + c.title + '.'),
1475
+ 'lastmod: ' + c.date, 'quickstart:', ' step: 1', ' next: null', ' prev: /quickstart/', '---', '',
1476
+ '# First steps', '', 'Quickstart pages are ordered walkthroughs. Set `quickstart.step`, `quickstart.next` and `quickstart.prev` in the front matter to chain them.', '',
1477
+ '1. Run `docker compose up`.', '2. Edit `_config.yml` and restart the server.', '3. Publish with `gh repo create ' + c.repo + ' --public --source=. --push`.', ''].join('\n');
1478
+ }
1479
+
1480
+ function genSampleNote(c) {
1481
+ return ['---', 'title: "Welcome Note"', 'description: ' + y('The first note in the ' + c.title + ' garden.'),
1482
+ 'date: ' + nowISO(), 'lastmod: ' + nowISO(), 'tags: [meta, welcome]', '---', '',
1483
+ 'Notes are short, evergreen and interlinked. Link to another note with a wiki-link, like [[Getting Started]], and the theme resolves it and shows backlinks on both pages.', '',
1484
+ '> [!tip] Obsidian users', '> Open `pages/_notes/` as a vault; callouts, embeds and `[[links]]` render the same on the site.', ''].join('\n');
1485
+ }
1486
+
1487
+ function genSampleRecipe(c) {
1488
+ return ['---', 'title: "Starter Recipe: Weeknight Tomato Pasta"', 'layout: recipe', 'cookbook: ' + c.slug,
1489
+ 'course: mains', 'cuisine: Italian', 'difficulty: easy',
1490
+ 'description: ' + y('A twenty-minute pantry pasta that shows every field a recipe on ' + c.title + ' can carry.'),
1491
+ 'lastmod: ' + c.date, 'tags: [pasta, quick, vegetarian]', '',
1492
+ 'yield:', ' amount: 2', ' unit: servings', ' singular: serving', '',
1493
+ 'times:', ' prep: 5', ' cook: 15', '',
1494
+ 'equipment:', ' - Large pot', ' - Wide skillet', '',
1495
+ 'ingredients:', ' - group: Pasta', ' items:', ' - item: dried spaghetti', ' qty: 200', ' unit: g',
1496
+ ' - item: salt', ' qty: 1', ' unit: tbsp', ' note: For the pasta water.',
1497
+ ' - group: Sauce', ' items:', ' - item: olive oil', ' qty: 2', ' unit: tbsp',
1498
+ ' - item: garlic cloves', ' singular: garlic clove', ' qty: 2', ' prep: thinly sliced',
1499
+ ' - item: canned whole tomatoes', ' qty: 400', ' unit: g', ' prep: crushed by hand',
1500
+ ' - item: fresh basil', ' qty: 0.25', ' unit: cup', ' prep: torn', '',
1501
+ 'steps:', ' - title: Boil', ' text: >-', ' Bring a large pot of water to the boil, salt it well, and cook the spaghetti one minute short of the packet time.', ' time: 10',
1502
+ ' - title: Sauce', ' text: >-', ' Warm the oil in a wide skillet, soften the garlic without colouring it, add the tomatoes and simmer until glossy.', ' time: 8',
1503
+ ' - title: Finish', ' text: >-', ' Drag the pasta into the sauce with a splash of its water, toss over high heat for a minute, and finish with basil.', ' time: 2', '',
1504
+ 'notes:', ' - Swap the basil for parsley in winter; add chilli flakes with the garlic for heat.', '---', '',
1505
+ 'This starter recipe exists so the cookbook has something to render on day one. Replace it, or copy its front matter for your own recipes — `yield`, `times`, `ingredients` groups and `steps` drive the scaler, the unit switch and the timings bar.', ''].join('\n');
1506
+ }
1507
+
1508
+ /** A one-letter monogram in the skin's colours, so the navbar logo is never a broken image. */
1509
+ function genLogoSvg(c) {
1510
+ var skin = catalog('skins').filter(function (s) { return s.id === (c.f.theme_skin || 'air'); })[0] || { primary: '#e8f4f8', accent: '#6fa8dc' };
1511
+ var letter = (String(c.title).trim().charAt(0) || 'Z').toUpperCase().replace(/[^A-Z0-9]/, 'Z');
1512
+ var dark = /^#([0-9a-f]{2})/i.test(skin.primary) && parseInt(skin.primary.slice(1, 3), 16) < 0x80;
1513
+ return ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="' + escapeXml(c.title) + ' logo">',
1514
+ ' <rect width="64" height="64" rx="14" fill="' + skin.accent + '"/>',
1515
+ ' <text x="32" y="43" text-anchor="middle" font-family="system-ui, -apple-system, Segoe UI, sans-serif" font-size="34" font-weight="700" fill="' + (dark ? '#f8f9fa' : '#1b1f24') + '">' + letter + '</text>',
1516
+ '</svg>', ''].join('\n');
1517
+ }
1518
+
1519
+ function escapeXml(s) {
1520
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1521
+ }
1522
+
1523
+ function genGitignore() {
1524
+ return ['_site/', '.sass-cache/', '.jekyll-cache/', '.jekyll-metadata', 'vendor/', 'Gemfile.lock', 'node_modules/', '.env', '.env.*', '!.env.example', '*.log', '.DS_Store', ''].join('\n');
1525
+ }
1526
+
1527
+ function genInstallYml(c) {
1528
+ var f = c.f;
1529
+ var L = ['# zer0.install.yml — your Site Builder answers, replayable by scripts/bin/install.', '# Keys are never read from this file; they live in your environment / .env.', ''];
1530
+ L.push('profile: ' + (c.remote ? 'github-pages' : 'default'));
1531
+ L.push('site:');
1532
+ L.push(' title: ' + y(c.title));
1533
+ L.push(' description: ' + y(f.description || ''));
1534
+ L.push(' author: ' + y(f.founder || ''));
1535
+ L.push(' email: ' + y(f.email || ''));
1536
+ L.push(' url: ' + y(c.url));
1537
+ L.push('github:');
1538
+ L.push(' user: ' + y(c.ghUser));
1539
+ L.push(' repo: ' + y(c.repo));
1540
+ L.push('theme:');
1541
+ L.push(' source: ' + (c.remote ? 'remote' : 'gem'));
1542
+ L.push(' skin: ' + (f.theme_skin || 'air'));
1543
+ L.push(' color_mode: ' + (f.color_mode_default || 'auto'));
1544
+ L.push('deploy:');
1545
+ L.push(' - github-pages');
1546
+ L.push('collections: [' + c.collections.map(function (x) { return x.id; }).join(', ') + ']');
1547
+ L.push('voice:');
1548
+ L.push(' tone: ' + (f.tone || 'friendly'));
1549
+ L.push(' audience: ' + (f.audience || 'developers'));
1550
+ L.push('agents: [copilot, claude]');
1551
+ L.push('ai:');
1552
+ L.push(' provider: ' + (f.ai_provider === 'xai' ? 'xai' : 'auto'));
1553
+ if (f.image_provider) L.push(' image_provider: ' + f.image_provider);
1554
+ return L.join('\n') + '\n';
1555
+ }
1556
+
1557
+ function genEnvExample(c) {
1558
+ var f = c.f;
1559
+ var L = ['# Copy to .env (git-ignored). Credentials live here, never in _config.yml.', '',
1560
+ '# jekyll-github-metadata', 'PAGES_REPO_NWO=' + c.ghUser + '/' + c.repo, ''];
1561
+ L.push('# AI chat assistant / Site Builder — provider: ' + (f.ai_provider || 'anthropic'));
1562
+ if (f.ai_provider === 'xai') {
1563
+ L.push('# xAI API key from https://console.x.ai/ (Grok chat; also Grok Imagine images)');
1564
+ L.push('# XAI_API_KEY=xai-...');
1565
+ L.push('# CHAT_PROVIDER=xai');
1566
+ } else {
1567
+ L.push('# Claude Code OAuth token from `claude setup-token`, or an Anthropic API key');
1568
+ L.push('# CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...');
1569
+ L.push('# ANTHROPIC_API_KEY=sk-ant-...');
1570
+ }
1571
+ L.push('');
1572
+ if (f.image_provider) {
1573
+ var ip = catalogItem('image_providers', f.image_provider) || {};
1574
+ L.push('# Preview-image renderer (' + (ip.label || f.image_provider) + ')');
1575
+ if (!(f.ai_provider === 'xai' && f.image_provider === 'xai')) L.push('# ' + (ip.env || 'IMAGE_API_KEY') + '=...');
1576
+ else L.push('# (uses XAI_API_KEY above)');
1577
+ L.push('');
1578
+ }
1579
+ L.push('# PostHog (only if enabled in _config.yml)', '# POSTHOG_API_KEY=phc_...', '');
1580
+ return L.join('\n');
1581
+ }
1582
+
1583
+ function genReadme(c) {
1584
+ var L = ['# ' + c.title, ''];
1585
+ if (c.f.description) { L.push(c.f.description); L.push(''); }
1586
+ L.push('Built with [zer0-mistakes](https://github.com/bamr87/zer0-mistakes) (' + (c.remote ? 'remote theme' : 'jekyll-theme-zer0 gem') + '). Generated by the Site Builder on ' + c.date + '.');
1587
+ L.push('');
1588
+ L.push('## Run locally');
1589
+ L.push('');
1590
+ L.push('```bash');
1591
+ L.push('docker compose up # http://localhost:' + c.port + '/');
1592
+ L.push('```');
1593
+ L.push('');
1594
+ L.push('Edit anything under `pages/` and the browser reloads. Site-wide settings live in `_config.yml` (restart the server after changing it).');
1595
+ L.push('');
1596
+ L.push('## Publish to GitHub Pages');
1597
+ L.push('');
1598
+ L.push('```bash');
1599
+ L.push('git init && git add -A && git commit -m "feat: initial site"');
1600
+ L.push('gh repo create ' + c.repo + ' --public --source=. --push');
1601
+ L.push('```');
1602
+ L.push('');
1603
+ L.push('Then in the repository: **Settings → Pages → Source: GitHub Actions** (or the `main` branch). The site appears at `' + c.url + (c.baseurl || '') + '/`.');
1604
+ L.push('');
1605
+ L.push('## Layout');
1606
+ L.push('');
1607
+ L.push('| Path | Purpose |');
1608
+ L.push('| --- | --- |');
1609
+ L.push('| `_config.yml` | production settings |');
1610
+ L.push('| `_config_dev.yml` | local overrides |');
1611
+ L.push('| `_data/navigation/main.yml` | navbar |');
1612
+ c.collections.forEach(function (col) { L.push('| `pages/_' + col.id + '/` | ' + labelFor(catalog('collections'), col.id) + ' |'); });
1613
+ L.push('| `zer0.install.yml` | your Site Builder answers |');
1614
+ L.push('');
1615
+ return L.join('\n');
1616
+ }
1617
+
1618
+ /** Every generated file, in preview order, with overrides applied. */
1619
+ function getFiles() {
1620
+ var c = ctx();
1621
+ var manifest = catalog('generated_files');
1622
+ var files = [];
1623
+ function add(path, content, meta) {
1624
+ var m = meta || {};
1625
+ files.push({
1626
+ path: path,
1627
+ label: m.label || path,
1628
+ description: m.description || '',
1629
+ required: m.required !== false,
1630
+ content: overrides[path] !== undefined ? overrides[path] : content,
1631
+ overridden: overrides[path] !== undefined
1632
+ });
1633
+ }
1634
+ function meta(p) { return manifest.filter(function (m) { return m.path === p; })[0] || { label: p }; }
1635
+
1636
+ add('_config.yml', genConfig(c), meta('_config.yml'));
1637
+ add('_config_dev.yml', genDevConfig(c), meta('_config_dev.yml'));
1638
+ add('Gemfile', genGemfile(c), meta('Gemfile'));
1639
+ add('docker-compose.yml', genCompose(c), meta('docker-compose.yml'));
1640
+ add('index.md', genIndex(c), meta('index.md'));
1641
+ add('_data/navigation/main.yml', genNav(c), meta('_data/navigation/main.yml'));
1642
+ var hasAbout = c.collections.some(function (x) { return x.id === 'about'; });
1643
+ var hasPosts = c.collections.some(function (x) { return x.id === 'posts'; });
1644
+ if (hasAbout) add('pages/_about/index.md', genAbout(c), meta('pages/_about/index.md'));
1645
+ if (hasPosts) add('pages/_posts/' + c.date + '-welcome.md', genWelcome(c), Object.assign({}, meta('pages/_posts/{date}-welcome.md'), { label: 'posts/welcome.md' }));
1646
+ c.collections.forEach(function (col) {
1647
+ if (col.id === 'about') return;
1648
+ add('pages/' + col.id + '.md', genCollectionIndex(c, col), { label: col.id + '.md', description: 'Index page listing the ' + col.id + ' collection.', required: true });
1649
+ if (col.id === 'docs') add('pages/_docs/getting-started.md', genSampleDoc(c), { label: 'docs/getting-started.md', description: 'Starter document for the docs collection.', required: false });
1650
+ if (col.id === 'quickstart') add('pages/_quickstart/first-steps.md', genSampleQuickstart(c), { label: 'quickstart/first-steps.md', description: 'Starter quickstart step.', required: false });
1651
+ if (col.id === 'notes') add('pages/_notes/welcome-note.md', genSampleNote(c), { label: 'notes/welcome-note.md', description: 'Starter note with a wiki-link and a callout.', required: false });
1652
+ if (col.id === 'recipes') add('pages/_recipes/starter-recipe.md', genSampleRecipe(c), { label: 'recipes/starter-recipe.md', description: 'Starter recipe with the structured front matter the recipe layout needs.', required: false });
1653
+ });
1654
+ (c.plan.pages || []).forEach(function (p) {
1655
+ if (!c.collections.some(function (col) { return col.id === p.collection; })) return; // planned for a collection that is off
1656
+ add(pagePath(p), genPlanPage(c, p), { label: p.collection + '/' + p.slug + '.md', description: 'Planned example page' + (p.body ? '.' : ' (draft body — ask the assistant to write it).'), required: false });
1657
+ });
1658
+ if (landingPlan(c).template !== 'minimal') add('_data/landing.yml', genLandingData(c), { label: 'landing.yml', description: 'Hero and sections the landing page renders — edit the copy here.', required: true });
1659
+ if (c.plan.navigation.sidebar === 'docs' && hasCollection(c, 'docs')) add('_data/navigation/docs.yml', genDocsNav(c), { label: 'navigation/docs.yml', description: 'Curated docs sidebar tree.', required: true });
1660
+ add('assets/css/user-overrides.css', genUserOverrides(c), { label: 'user-overrides.css', description: 'Palette, fonts and corners layered over the skin. The theme links this file on every page.', required: true });
1661
+ add('assets/js/user-overrides.js', genUserOverridesJs(c), { label: 'user-overrides.js', description: 'Site-specific JavaScript. The theme loads it alongside the CSS whenever user_overrides is on.', required: true });
1662
+ var customHead = genCustomHead(c);
1663
+ if (customHead) add('_includes/custom/head.html', customHead, { label: 'custom/head.html', description: 'Loads the chosen web fonts through the theme\'s head hook.', required: false });
1664
+ add('assets/images/logo.svg', genLogoSvg(c), { label: 'logo.svg', description: 'Monogram logo in the skin colours; replace with your own.', required: false });
1665
+ add('.gitignore', genGitignore(), meta('.gitignore'));
1666
+ add('zer0.install.yml', genInstallYml(c), meta('zer0.install.yml'));
1667
+ add('.env.example', genEnvExample(c), meta('.env.example'));
1668
+ add('README.md', genReadme(c), meta('README.md'));
1669
+ return files;
1670
+ }
1671
+
1672
+ function getFile(path) {
1673
+ return getFiles().filter(function (f) { return f.path === path; })[0] || null;
1674
+ }
1675
+
1676
+ /** Self-extracting bash bundle: recreates every file under $1 (default slug). */
1677
+ function buildBundle() {
1678
+ var c = ctx();
1679
+ var L = ['#!/usr/bin/env bash', '# ' + c.title + ' — site bundle generated by the zer0-mistakes Site Builder (' + c.date + ')',
1680
+ '# Usage: bash zer0-site-bundle.sh [folder] → creates the folder and every file, then prints next steps.',
1681
+ 'set -euo pipefail', 'TARGET="${1:-' + c.slug + '}"', 'mkdir -p "$TARGET" && cd "$TARGET"', ''];
1682
+ getFiles().forEach(function (f) {
1683
+ var dir = f.path.indexOf('/') !== -1 ? f.path.slice(0, f.path.lastIndexOf('/')) : '';
1684
+ if (dir) L.push('mkdir -p ' + shellQuote(dir));
1685
+ L.push('cat > ' + shellQuote(f.path) + " <<'ZER0_EOF'");
1686
+ L.push(f.content.replace(/\n$/, ''));
1687
+ L.push('ZER0_EOF');
1688
+ L.push('');
1689
+ });
1690
+ L.push('echo "Created $(ls -1A | wc -l | tr -d \' \') entries in $TARGET"');
1691
+ L.push('echo "Next: cd $TARGET && docker compose up → http://localhost:' + c.port + '/"');
1692
+ return L.join('\n') + '\n';
1693
+ }
1694
+
1695
+ function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'"; }
1696
+
1697
+ // ── inline validation ────────────────────────────────────────────
1698
+
1699
+ var VALIDATION_MESSAGES = {
1700
+ email: 'Enter a valid email address, e.g. you@example.com.',
1701
+ url: 'Enter a full URL including https://, e.g. https://example.com.'
1702
+ };
1703
+
1704
+ function validateField(el) {
1705
+ if (!el || !el.classList.contains('cfg-field')) return true;
1706
+ var type = (el.getAttribute('type') || '').toLowerCase();
1707
+ if (type !== 'email' && type !== 'url') return true;
1708
+ var ok = el.value.trim() === '' || el.checkValidity();
1709
+ el.classList.toggle('is-invalid', !ok);
1710
+ var feedback = document.getElementById(el.id + '-feedback');
1711
+ if (feedback) feedback.textContent = ok ? '' : VALIDATION_MESSAGES[type];
1712
+ return ok;
1713
+ }
1714
+
1715
+ function paneIsValid(pane) {
1716
+ if (!pane) return true;
1717
+ var fields = pane.querySelectorAll('.cfg-field');
1718
+ for (var i = 0; i < fields.length; i += 1) {
1719
+ var el = fields[i];
1720
+ var type = (el.getAttribute('type') || '').toLowerCase();
1721
+ if (type !== 'email' && type !== 'url') continue;
1722
+ if (el.value.trim() !== '' && !el.checkValidity()) return false;
1723
+ }
1724
+ return true;
1725
+ }
1726
+
1727
+ function unfilledRecommended() {
1728
+ var missing = [];
1729
+ wizard.querySelectorAll('[data-recommended]').forEach(function (el) {
1730
+ if (el.value.trim() === '') missing.push(el.getAttribute('data-recommended'));
1731
+ });
1732
+ return missing;
1733
+ }
1734
+
1735
+ function renderReviewWarnings() {
1736
+ var host = document.getElementById('wizard-review-warnings');
1737
+ if (!host) return;
1738
+ var missing = unfilledRecommended();
1739
+ host.innerHTML = '';
1740
+ var alert = document.createElement('div');
1741
+ alert.setAttribute('role', 'alert');
1742
+ if (!missing.length) {
1743
+ alert.className = 'alert alert-success mb-0';
1744
+ alert.innerHTML = '<i class="bi bi-check2-circle" aria-hidden="true"></i> Every recommended field is filled in. The project below is ready to write.';
1745
+ host.appendChild(alert);
1746
+ return;
1747
+ }
1748
+ alert.className = 'alert alert-warning mb-0';
1749
+ alert.innerHTML = '<i class="bi bi-exclamation-triangle" aria-hidden="true"></i> <strong>Recommended fields still empty.</strong> The files are valid without them, but filling them in improves SEO and the site chrome:';
1750
+ var ul = document.createElement('ul');
1751
+ ul.className = 'mb-0 mt-2';
1752
+ missing.forEach(function (name) {
1753
+ var li = document.createElement('li');
1754
+ li.textContent = name;
1755
+ ul.appendChild(li);
1756
+ });
1757
+ alert.appendChild(ul);
1758
+ host.appendChild(alert);
1759
+ }
1760
+
1761
+ // ── stepper state ────────────────────────────────────────────────
1762
+
1763
+ function refreshStepper() {
1764
+ var buttons = STEP_IDS.map(function (id) { return document.getElementById(id); });
1765
+ var activeIndex = buttons.findIndex(function (b) { return b && b.classList.contains('is-active'); });
1766
+ if (activeIndex < 0) activeIndex = 0;
1767
+
1768
+ var blocked = false;
1769
+ buttons.forEach(function (btn, i) {
1770
+ if (!btn) return;
1771
+ var pane = document.querySelector(btn.getAttribute('data-bs-target'));
1772
+ var valid = paneIsValid(pane);
1773
+ var locked = blocked && i > activeIndex;
1774
+ btn.classList.toggle('is-active', i === activeIndex);
1775
+ btn.classList.toggle('is-done', i < activeIndex && valid);
1776
+ btn.classList.toggle('is-locked', locked);
1777
+ btn.disabled = locked;
1778
+ btn.setAttribute('aria-selected', i === activeIndex ? 'true' : 'false');
1779
+ var state = btn.querySelector('.wizard-step-state');
1780
+ if (state) {
1781
+ state.textContent = i === activeIndex ? 'current step'
1782
+ : locked ? 'locked until earlier steps are valid'
1783
+ : i < activeIndex && valid ? 'completed' : '';
1784
+ }
1785
+ if (!valid) blocked = true;
1786
+ });
1787
+
1788
+ wizard.querySelectorAll('.btn-next').forEach(function (btn) {
1789
+ var target = document.getElementById(btn.getAttribute('data-next'));
1790
+ btn.disabled = !!(target && target.disabled);
1791
+ });
1792
+
1793
+ var bar = document.getElementById('wizard-progress-bar');
1794
+ var text = document.getElementById('wizard-progress-text');
1795
+ var pct = Math.round(((activeIndex + 1) / STEP_IDS.length) * 100);
1796
+ if (bar) bar.style.width = pct + '%';
1797
+ if (text) text.textContent = 'Step ' + (activeIndex + 1) + ' of ' + STEP_IDS.length;
1798
+ }
1799
+
1800
+ function showStep(btn, opts) {
1801
+ if (typeof btn === 'string') btn = document.getElementById(/^tab-/.test(btn) ? btn : 'tab-' + btn);
1802
+ if (!btn || btn.disabled) return false;
1803
+ wizard.querySelectorAll('#wizardTabs .wizard-step').forEach(function (b) { b.classList.remove('is-active'); });
1804
+ btn.classList.add('is-active');
1805
+ if (window.bootstrap && window.bootstrap.Tab) new bootstrap.Tab(btn).show();
1806
+ else {
1807
+ wizard.querySelectorAll('.wizard-panes .tab-pane').forEach(function (p) { p.classList.remove('show', 'active'); });
1808
+ var pane = document.querySelector(btn.getAttribute('data-bs-target'));
1809
+ if (pane) pane.classList.add('show', 'active');
1810
+ }
1811
+ refreshStepper();
1812
+ if (btn.id === 'tab-build') renderReviewWarnings();
1813
+ if (!(opts && opts.silent)) {
1814
+ try { history.replaceState(null, '', '#' + btn.getAttribute('data-bs-target').slice(1)); } catch (e) { /* ignore */ }
1815
+ }
1816
+ emit('step', { step: btn.id.replace(/^tab-/, '') });
1817
+ saveDraft();
1818
+ return true;
1819
+ }
1820
+
1821
+ // ── prerequisites UI ─────────────────────────────────────────────
1822
+
1823
+ function detectOS() {
1824
+ var p = (navigator.platform || '') + ' ' + (navigator.userAgent || '');
1825
+ if (/Mac|iPhone|iPad/i.test(p)) return 'macos';
1826
+ if (/Win/i.test(p)) return 'windows';
1827
+ return 'linux';
1828
+ }
1829
+
1830
+ function applyOS(os) {
1831
+ wizard.querySelectorAll('.prereq-install .wizard-cmd[data-os]').forEach(function (el) {
1832
+ el.hidden = el.getAttribute('data-os') !== os;
1833
+ });
1834
+ var radio = document.getElementById('prereq-os-' + os);
1835
+ if (radio) radio.checked = true;
1836
+ }
1837
+
1838
+ function renderPrereqs() {
1839
+ var items = wizard.querySelectorAll('.prereq-item');
1840
+ var requiredTotal = 0;
1841
+ var requiredReady = 0;
1842
+ items.forEach(function (li) {
1843
+ var id = li.getAttribute('data-prereq');
1844
+ var c = checks[id] || {};
1845
+ var manual = document.getElementById('prereq-' + id);
1846
+ var isManual = !!(c.manual || (manual && manual.checked));
1847
+ var state = isManual ? 'manual' : c.ok === true ? 'ok' : c.ok === false ? 'fail' : 'unknown';
1848
+ li.setAttribute('data-state', state);
1849
+ var badge = li.querySelector('.prereq-state-badge');
1850
+ if (badge) {
1851
+ badge.className = 'badge rounded-pill prereq-state-badge ' + (
1852
+ state === 'ok' ? 'text-bg-success' : state === 'fail' ? 'text-bg-danger' : state === 'manual' ? 'text-bg-info' : 'text-bg-light border');
1853
+ badge.textContent = state === 'ok' ? ('ready' + (c.version ? ' · v' + c.version : ''))
1854
+ : state === 'fail' ? (c.installed === false ? 'not installed' : 'not ready')
1855
+ : state === 'manual' ? 'done (manual)' : 'not checked';
1856
+ }
1857
+ var out = li.querySelector('.prereq-output');
1858
+ if (out) {
1859
+ var text = c.output ? String(c.output).split('\n').slice(0, 3).join(' · ') : '';
1860
+ out.textContent = text;
1861
+ out.hidden = !text;
1862
+ }
1863
+ if (li.getAttribute('data-required') === 'true') {
1864
+ requiredTotal += 1;
1865
+ if (state === 'ok' || state === 'manual') requiredReady += 1;
1866
+ }
1867
+ });
1868
+ var summary = document.getElementById('prereq-summary');
1869
+ if (summary) summary.textContent = requiredReady + ' of ' + requiredTotal + ' required ready';
1870
+ }
1871
+
1872
+ // ── file preview ─────────────────────────────────────────────────
1873
+
1874
+ /**
1875
+ * Keep the file tabs in sync with the generated set.
1876
+ *
1877
+ * Updates the existing buttons IN PLACE whenever the list of paths is
1878
+ * unchanged. Detaching and re-appending them (the obvious rebuild) breaks a
1879
+ * real click: pressing a tab blurs a text field, the field's `change` event
1880
+ * regenerates the preview, and if that rebuild removes the button under the
1881
+ * pointer between mousedown and mouseup the browser never fires `click`.
1882
+ */
1883
+ function renderFileTabs(files) {
1884
+ var host = document.getElementById('wizard-file-tabs');
1885
+ if (!host) return;
1886
+ var current = Array.prototype.map.call(host.querySelectorAll('.wizard-file-tab'), function (b) { return b.getAttribute('data-file'); });
1887
+ var wanted = files.map(function (f) { return f.path; });
1888
+ var samePaths = current.length === wanted.length && current.every(function (p, i) { return p === wanted[i]; });
1889
+
1890
+ function decorate(b, f) {
1891
+ b.type = 'button';
1892
+ b.className = 'nav-link wizard-file-tab' + (f.path === activeFile ? ' active' : '') + (f.overridden ? ' is-overridden' : '');
1893
+ b.setAttribute('role', 'tab');
1894
+ b.setAttribute('data-file', f.path);
1895
+ b.setAttribute('title', f.description || f.path);
1896
+ b.setAttribute('aria-selected', f.path === activeFile ? 'true' : 'false');
1897
+ if (b.textContent !== f.label) b.textContent = f.label;
1898
+ }
1899
+
1900
+ if (samePaths) {
1901
+ var buttons = host.querySelectorAll('.wizard-file-tab');
1902
+ files.forEach(function (f, i) { decorate(buttons[i], f); });
1903
+ return;
1904
+ }
1905
+
1906
+ var existing = {};
1907
+ host.querySelectorAll('.wizard-file-tab').forEach(function (b) { existing[b.getAttribute('data-file')] = b; });
1908
+ host.innerHTML = '';
1909
+ files.forEach(function (f) {
1910
+ var b = existing[f.path] || document.createElement('button');
1911
+ decorate(b, f);
1912
+ host.appendChild(b);
1913
+ });
1914
+ }
224
1915
 
225
1916
  function updatePreview() {
1917
+ var files = getFiles();
1918
+ if (!files.some(function (f) { return f.path === activeFile; })) activeFile = files[0].path;
1919
+ renderFileTabs(files);
1920
+ var file = files.filter(function (f) { return f.path === activeFile; })[0];
226
1921
  var preview = document.getElementById('yaml-preview');
227
- if (preview) {
228
- preview.textContent = buildYAML();
1922
+ if (preview && file) {
1923
+ preview.textContent = file.content;
1924
+ preview.parentElement.setAttribute('data-file', file.path);
229
1925
  }
1926
+ var desc = document.getElementById('wizard-file-description');
1927
+ if (desc && file) desc.textContent = (file.overridden ? 'Edited by hand or by Claude. ' : '') + (file.description || '');
1928
+ emit('files', { active: activeFile, count: files.length });
1929
+ }
1930
+
1931
+ function refreshAll() {
1932
+ syncSubfields();
1933
+ syncCommands();
1934
+ updatePreview();
1935
+ refreshStepper();
1936
+ renderPrereqs();
1937
+ updateLandingSummary();
1938
+ updateFontPreview();
1939
+ applyPreview();
1940
+ var count = document.getElementById('pages-count');
1941
+ if (count) count.textContent = String(planExtra.pages.length);
1942
+ if (activeStepId() === 'tab-build') renderReviewWarnings();
230
1943
  }
231
1944
 
232
- function downloadYAML() {
233
- var yaml = buildYAML();
234
- var blob = new Blob([yaml], { type: 'text/yaml;charset=utf-8' });
1945
+ function syncSubfields() {
1946
+ wizard.querySelectorAll('.wizard-subfields[data-for]').forEach(function (block) {
1947
+ var sw = document.getElementById(block.getAttribute('data-for'));
1948
+ block.hidden = !(sw && sw.checked);
1949
+ });
1950
+ var iconInput = document.getElementById('cfg-title-icon');
1951
+ var iconPreview = document.getElementById('cfg-title-icon-preview');
1952
+ if (iconInput && iconPreview) {
1953
+ var name = (iconInput.value || 'robot').replace(/^bi-/, '').replace(/[^a-z0-9-]/gi, '');
1954
+ iconPreview.className = 'bi bi-' + (name || 'robot');
1955
+ }
1956
+ }
1957
+
1958
+ function syncCommands() {
1959
+ var c = ctx();
1960
+ var set = function (id, text) { var el = document.getElementById(id); if (el) el.textContent = text; };
1961
+ set('cmd-bundle', 'bash zer0-site-bundle.sh ' + c.slug);
1962
+ set('cmd-up', 'cd ' + c.slug + ' && docker compose up');
1963
+ set('cmd-publish', 'gh repo create ' + c.repo + ' --public --source=. --push');
1964
+ var local = 'http://localhost:' + c.port + '/';
1965
+ ['link-local', 'btn-open-site'].forEach(function (id) {
1966
+ var a = document.getElementById(id);
1967
+ if (a) { a.href = local; if (id === 'link-local') a.textContent = local; }
1968
+ });
1969
+ var target = document.getElementById('cfg-target');
1970
+ if (target && !target.value && !restoring) target.placeholder = c.slug;
1971
+ }
1972
+
1973
+ // ── draft persistence ────────────────────────────────────────────
1974
+
1975
+ function collectDraft() {
1976
+ var data = { v: DRAFT_VERSION, fields: {}, checks: {}, radios: {}, step: null, navigation: readNavigation(), overrides: overrides, prereqs: checks, activeFile: activeFile, planExtra: planExtra };
1977
+ wizard.querySelectorAll('.cfg-field').forEach(function (el) { if (el.id) data.fields[el.id] = el.value; });
1978
+ wizard.querySelectorAll('input[type="checkbox"]').forEach(function (el) { if (el.id) data.checks[el.id] = el.checked; });
1979
+ wizard.querySelectorAll('.cfg-radio:checked').forEach(function (el) { data.radios[el.name] = el.value; });
1980
+ data.step = activeStepId();
1981
+ return data;
1982
+ }
1983
+
1984
+ function showDraftChip() {
1985
+ var chip = document.getElementById('wizard-draft-chip');
1986
+ if (!chip) return;
1987
+ chip.hidden = false;
1988
+ chip.classList.add('is-visible');
1989
+ clearTimeout(chipTimer);
1990
+ chipTimer = setTimeout(function () { chip.classList.remove('is-visible'); }, 2000);
1991
+ }
1992
+
1993
+ function writeDraft() {
1994
+ try { localStorage.setItem(DRAFT_KEY, JSON.stringify(collectDraft())); return true; } catch (e) { /* private mode / quota */ }
1995
+ return false;
1996
+ }
1997
+
1998
+ function saveDraft() {
1999
+ if (restoring) return;
2000
+ clearTimeout(draftTimer);
2001
+ draftTimer = setTimeout(function () {
2002
+ draftTimer = null;
2003
+ if (writeDraft()) showDraftChip();
2004
+ }, DRAFT_DEBOUNCE_MS);
2005
+ }
2006
+
2007
+ function flushDraft() {
2008
+ if (draftTimer === null || restoring) return;
2009
+ clearTimeout(draftTimer);
2010
+ draftTimer = null;
2011
+ writeDraft();
2012
+ }
2013
+
2014
+ function clearDraft() {
2015
+ clearTimeout(draftTimer);
2016
+ draftTimer = null;
2017
+ try { localStorage.removeItem(DRAFT_KEY); } catch (e) { /* ignore */ }
2018
+ var chip = document.getElementById('wizard-draft-chip');
2019
+ if (chip) { chip.classList.remove('is-visible'); chip.hidden = true; }
2020
+ }
2021
+
2022
+ function restoreDraft() {
2023
+ var raw;
2024
+ try { raw = localStorage.getItem(DRAFT_KEY); } catch (e) { return; }
2025
+ if (!raw) return;
2026
+ var data;
2027
+ try { data = JSON.parse(raw); } catch (e) { clearDraft(); return; }
2028
+ if (!data || typeof data !== 'object') return;
2029
+
2030
+ restoring = true;
2031
+ Object.keys(data.fields || {}).forEach(function (id) {
2032
+ var el = document.getElementById(id);
2033
+ if (el && el.classList.contains('cfg-field')) el.value = data.fields[id];
2034
+ });
2035
+ Object.keys(data.checks || {}).forEach(function (id) {
2036
+ var el = document.getElementById(id);
2037
+ if (el && el.type === 'checkbox') el.checked = !!data.checks[id];
2038
+ });
2039
+ Object.keys(data.radios || {}).forEach(function (name) {
2040
+ var el = wizard.querySelector('.cfg-radio[name="' + name + '"][value="' + String(data.radios[name]).replace(/"/g, '') + '"]');
2041
+ if (el) el.checked = true;
2042
+ });
2043
+ if (Array.isArray(data.navigation) && data.navigation.length) renderNavEditor(data.navigation);
2044
+ if (data.overrides && typeof data.overrides === 'object') overrides = data.overrides;
2045
+ if (data.prereqs && typeof data.prereqs === 'object') checks = data.prereqs;
2046
+ if (typeof data.activeFile === 'string') activeFile = data.activeFile;
2047
+ if (data.planExtra && typeof data.planExtra === 'object') {
2048
+ planExtra = {
2049
+ landing: Object.assign({ hero: null, sections: null }, data.planExtra.landing || {}),
2050
+ navigation: Object.assign({ items: null }, data.planExtra.navigation || {}),
2051
+ pages: Array.isArray(data.planExtra.pages) ? data.planExtra.pages.map(normalizePage) : []
2052
+ };
2053
+ renderPagesPlanner();
2054
+ }
2055
+
2056
+ var target = data.step && STEP_IDS.indexOf(data.step) !== -1 ? document.getElementById(data.step) : null;
2057
+ if (target && !target.disabled) showStep(target, { silent: true });
2058
+ restoring = false;
2059
+ }
2060
+
2061
+ function resetAll() {
2062
+ overrides = {};
2063
+ checks = {};
2064
+ planExtra = { landing: { hero: null, sections: null }, navigation: { items: null }, pages: [] };
2065
+ renderPagesPlanner();
2066
+ var previewBtn = document.getElementById('btn-skin-preview');
2067
+ if (previewBtn && previewBtn.getAttribute('aria-pressed') === 'true') toggleSkinPreview(previewBtn);
2068
+ wizard.querySelectorAll('.cfg-field').forEach(function (el) {
2069
+ if (el.tagName === 'SELECT') el.selectedIndex = 0;
2070
+ else if (el.id === 'cfg-port') el.value = String(DEFAULT_PORT);
2071
+ else if (el.id === 'cfg-remote-theme') el.value = 'bamr87/zer0-mistakes';
2072
+ else if (el.id === 'cfg-title-icon') el.value = 'robot';
2073
+ else el.value = '';
2074
+ el.classList.remove('is-invalid');
2075
+ });
2076
+ wizard.querySelectorAll('.cfg-collection').forEach(function (el) { el.checked = ['posts', 'docs', 'about'].indexOf(el.getAttribute('data-col')) !== -1; });
2077
+ wizard.querySelectorAll('.cfg-switch').forEach(function (el) { el.checked = el.hasAttribute('checked'); });
2078
+ wizard.querySelectorAll('.prereq-manual').forEach(function (el) { el.checked = false; });
2079
+ wizard.querySelectorAll('.cfg-radio').forEach(function (el) { el.checked = el.hasAttribute('checked'); });
2080
+ renderNavEditor(defaultNavigation());
2081
+ activeFile = '_config.yml';
2082
+ showStep(STEP_IDS[0]);
2083
+ refreshAll();
2084
+ // Last, so the step change above cannot queue a fresh draft write.
2085
+ clearDraft();
2086
+ emit('reset', {});
2087
+ }
2088
+
2089
+ // ── actions ──────────────────────────────────────────────────────
2090
+
2091
+ function toast(message) {
2092
+ var el = document.getElementById('wizard-toast');
2093
+ var body = document.getElementById('wizard-toast-body');
2094
+ if (body) body.textContent = message;
2095
+ if (el && window.bootstrap && window.bootstrap.Toast) bootstrap.Toast.getOrCreateInstance(el, { delay: 1800 }).show();
2096
+ }
2097
+
2098
+ function downloadText(name, text, mime) {
2099
+ var blob = new Blob([text], { type: (mime || 'text/plain') + ';charset=utf-8' });
235
2100
  var url = URL.createObjectURL(blob);
236
2101
  var a = document.createElement('a');
237
2102
  a.href = url;
238
- a.download = '_config.yml';
2103
+ a.download = name;
239
2104
  document.body.appendChild(a);
240
2105
  a.click();
241
2106
  document.body.removeChild(a);
242
2107
  URL.revokeObjectURL(url);
243
2108
  }
244
2109
 
245
- function copyYAML() {
246
- var yaml = buildYAML();
247
- navigator.clipboard.writeText(yaml).then(function () {
248
- var btn = document.getElementById('btn-copy-full') || document.getElementById('btn-copy');
2110
+ function downloadActive() {
2111
+ var file = getFile(activeFile);
2112
+ if (!file) return;
2113
+ downloadText(file.path.split('/').pop(), file.content, /\.ya?ml$/.test(file.path) ? 'text/yaml' : 'text/plain');
2114
+ toast('Downloaded ' + file.path);
2115
+ }
2116
+
2117
+ function downloadBundle() {
2118
+ downloadText('zer0-site-bundle.sh', buildBundle(), 'text/x-shellscript');
2119
+ toast('Bundle downloaded — run it with bash');
2120
+ }
2121
+
2122
+ function copyText(text, label) {
2123
+ if (!navigator.clipboard) return Promise.reject(new Error('clipboard unavailable'));
2124
+ return navigator.clipboard.writeText(text).then(function () { toast(label || 'Copied'); });
2125
+ }
2126
+
2127
+ function copyActive() {
2128
+ var file = getFile(activeFile);
2129
+ if (!file) return;
2130
+ copyText(file.content, 'Copied ' + file.path).then(function () {
2131
+ var btn = document.getElementById('btn-copy-full');
249
2132
  if (btn) {
250
2133
  var orig = btn.innerHTML;
251
- btn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
2134
+ btn.innerHTML = '<i class="bi bi-check-lg" aria-hidden="true"></i> Copied!';
252
2135
  setTimeout(function () { btn.innerHTML = orig; }, 2000);
253
2136
  }
254
- });
2137
+ }).catch(function () { /* no clipboard */ });
2138
+ }
2139
+
2140
+ function suggestUrls() {
2141
+ var f = readFields();
2142
+ var user = f.github_user || '';
2143
+ var repo = f.repository_name || '';
2144
+ if (!user) return;
2145
+ var patch = { url: 'https://' + user + '.github.io' };
2146
+ patch.baseurl = repo && !/\.github\.io$/i.test(repo) ? '/' + repo : '';
2147
+ applyFields(patch, { source: 'suggest' });
2148
+ toast('URL and base path filled from GitHub fields');
2149
+ }
2150
+
2151
+ /**
2152
+ * "Preview on this page": apply the chosen skin AND the generated
2153
+ * user-overrides.css (palette, fonts, corners) to the wizard page itself,
2154
+ * so the appearance step shows exactly what the new site will ship.
2155
+ */
2156
+ function toggleSkinPreview(btn) {
2157
+ var html = document.documentElement;
2158
+ var active = btn.getAttribute('aria-pressed') === 'true';
2159
+ if (!active) {
2160
+ if (skinPreviewOriginal === null) skinPreviewOriginal = html.getAttribute('data-theme-skin') || 'air';
2161
+ btn.setAttribute('aria-pressed', 'true');
2162
+ btn.innerHTML = '<i class="bi bi-eye-slash" aria-hidden="true"></i> Stop preview';
2163
+ applyPreview();
2164
+ } else {
2165
+ if (skinPreviewOriginal !== null) html.setAttribute('data-theme-skin', skinPreviewOriginal);
2166
+ if (previewStyle) { previewStyle.remove(); previewStyle = null; }
2167
+ btn.setAttribute('aria-pressed', 'false');
2168
+ btn.innerHTML = '<i class="bi bi-eye" aria-hidden="true"></i> Preview on this page';
2169
+ }
2170
+ }
2171
+
2172
+ function applyPreview() {
2173
+ var btn = document.getElementById('btn-skin-preview');
2174
+ if (!btn || btn.getAttribute('aria-pressed') !== 'true') return;
2175
+ var c = ctx();
2176
+ document.documentElement.setAttribute('data-theme-skin', c.f.theme_skin || 'air');
2177
+ var fp = catalogItem('font_pairings', c.plan.theme.fonts);
2178
+ if (fp && fp.google) ensureFontLink(fp);
2179
+ if (!previewStyle) {
2180
+ previewStyle = document.createElement('style');
2181
+ previewStyle.id = 'wizard-preview-overrides';
2182
+ document.head.appendChild(previewStyle);
2183
+ }
2184
+ previewStyle.textContent = genUserOverrides(c);
255
2185
  }
256
2186
 
257
- // ── wiring ─────────────────────────────────────────────────────────
2187
+ // ── wiring ───────────────────────────────────────────────────────
258
2188
 
259
2189
  document.addEventListener('DOMContentLoaded', function () {
260
- // Next / Prev buttons
261
- document.querySelectorAll('.btn-next').forEach(function (btn) {
262
- btn.addEventListener('click', function () {
263
- var target = btn.getAttribute('data-next');
264
- var tab = document.getElementById(target);
265
- if (tab) new bootstrap.Tab(tab).show();
266
- updatePreview();
267
- });
2190
+ wizard.querySelectorAll('.btn-next').forEach(function (btn) {
2191
+ btn.addEventListener('click', function () { showStep(document.getElementById(btn.getAttribute('data-next'))); });
2192
+ });
2193
+ wizard.querySelectorAll('.btn-prev').forEach(function (btn) {
2194
+ btn.addEventListener('click', function () { showStep(document.getElementById(btn.getAttribute('data-prev'))); });
2195
+ });
2196
+ wizard.querySelectorAll('#wizardTabs .wizard-step').forEach(function (btn) {
2197
+ btn.addEventListener('click', function () { showStep(btn); });
268
2198
  });
269
2199
 
270
- document.querySelectorAll('.btn-prev').forEach(function (btn) {
271
- btn.addEventListener('click', function () {
272
- var target = btn.getAttribute('data-prev');
273
- var tab = document.getElementById(target);
274
- if (tab) new bootstrap.Tab(tab).show();
275
- });
2200
+ wizard.addEventListener('input', function (event) {
2201
+ var el = event.target;
2202
+ if (el.classList && el.classList.contains('is-invalid')) validateField(el);
2203
+ refreshAll();
2204
+ saveDraft();
2205
+ emit('change', { source: 'user', keys: [el.getAttribute('data-key') || el.id] });
276
2206
  });
277
2207
 
278
- // Live update on review tab activation
279
- var reviewTab = document.getElementById('tab-review');
280
- if (reviewTab) {
281
- reviewTab.addEventListener('shown.bs.tab', updatePreview);
2208
+ wizard.addEventListener('change', function (event) {
2209
+ var el = event.target;
2210
+ if (el.classList && el.classList.contains('cfg-radio') && el.getAttribute('data-key') === 'site_type' && el.checked) {
2211
+ applySiteType(el.value);
2212
+ }
2213
+ if (el.name === 'prereq-os') applyOS(el.value);
2214
+ if (el.classList && el.classList.contains('prereq-manual')) {
2215
+ var id = el.getAttribute('data-prereq');
2216
+ checks[id] = Object.assign({}, checks[id] || {}, { manual: el.checked });
2217
+ }
2218
+ refreshAll();
2219
+ saveDraft();
2220
+ emit('change', { source: 'user', keys: [el.getAttribute('data-key') || el.getAttribute('data-col') || el.id] });
2221
+ });
2222
+
2223
+ wizard.addEventListener('blur', function (event) {
2224
+ if (!event.target.classList || !event.target.classList.contains('cfg-field')) return;
2225
+ validateField(event.target);
2226
+ refreshStepper();
2227
+ }, true);
2228
+
2229
+ // Copy buttons: data-copy="text" or data-copy-target="#id".
2230
+ wizard.addEventListener('click', function (event) {
2231
+ var btn = event.target.closest('[data-copy], [data-copy-target]');
2232
+ if (!btn) return;
2233
+ var text = btn.getAttribute('data-copy');
2234
+ if (!text) {
2235
+ var target = document.querySelector(btn.getAttribute('data-copy-target') || '');
2236
+ text = target ? target.textContent : '';
2237
+ }
2238
+ if (text) copyText(text).catch(function () { /* ignore */ });
2239
+ });
2240
+
2241
+ // File tabs.
2242
+ var tabs = document.getElementById('wizard-file-tabs');
2243
+ if (tabs) {
2244
+ tabs.addEventListener('click', function (event) {
2245
+ var b = event.target.closest('.wizard-file-tab');
2246
+ if (!b) return;
2247
+ activeFile = b.getAttribute('data-file');
2248
+ updatePreview();
2249
+ saveDraft();
2250
+ });
282
2251
  }
283
2252
 
284
- // Description char count
285
2253
  var descField = document.getElementById('cfg-description');
286
2254
  var descCount = document.getElementById('desc-count');
287
- if (descField && descCount) {
288
- descField.addEventListener('input', function () {
289
- descCount.textContent = descField.value.length;
2255
+ var syncCount = function () { if (descField && descCount) descCount.textContent = descField.value.length; };
2256
+ if (descField) descField.addEventListener('input', syncCount);
2257
+
2258
+ var bind = function (id, fn) { var el = document.getElementById(id); if (el) el.addEventListener('click', fn); };
2259
+ bind('btn-download', downloadActive);
2260
+ bind('btn-download-all', downloadBundle);
2261
+ bind('btn-copy', copyActive);
2262
+ bind('btn-copy-full', copyActive);
2263
+ bind('btn-suggest-urls', suggestUrls);
2264
+ bind('btn-nav-add', function () {
2265
+ var host = document.getElementById('nav-editor');
2266
+ if (!host) return;
2267
+ var empty = host.querySelector('p');
2268
+ if (empty) empty.remove();
2269
+ host.appendChild(navRow({ title: '', url: '/', icon: '' }));
2270
+ host.lastElementChild.querySelector('.nav-title').focus();
2271
+ });
2272
+ bind('btn-nav-regenerate', function () { renderNavEditor(defaultNavigation()); refreshAll(); saveDraft(); });
2273
+
2274
+ // Example-page planner: rows are rendered from planExtra.pages (the source
2275
+ // of truth) and edited in place.
2276
+ bind('btn-page-add', function () {
2277
+ var col = (readCollections().filter(function (x) { return x.id !== 'about'; })[0] || { id: 'posts' }).id;
2278
+ planExtra.pages.push(normalizePage({ collection: col, title: 'New page ' + (planExtra.pages.length + 1) }));
2279
+ renderPagesPlanner();
2280
+ var last = document.querySelector('#pages-planner .page-row:last-child .page-title');
2281
+ if (last) { last.focus(); last.select(); }
2282
+ refreshAll();
2283
+ saveDraft();
2284
+ });
2285
+ var planner = document.getElementById('pages-planner');
2286
+ if (planner) {
2287
+ planner.addEventListener('input', function (event) {
2288
+ var row = event.target.closest('.page-row');
2289
+ if (!row) return;
2290
+ var p = planExtra.pages[Number(row.getAttribute('data-index'))];
2291
+ if (!p) return;
2292
+ if (event.target.classList.contains('page-title')) { p.title = event.target.value; p.slug = slugify(p.title) || p.slug; }
2293
+ if (event.target.classList.contains('page-collection')) p.collection = event.target.value;
2294
+ var count = document.getElementById('pages-count');
2295
+ if (count) count.textContent = String(planExtra.pages.length);
2296
+ });
2297
+ planner.addEventListener('click', function (event) {
2298
+ var btn = event.target.closest('.page-remove');
2299
+ if (!btn) return;
2300
+ var row = btn.closest('.page-row');
2301
+ planExtra.pages.splice(Number(row.getAttribute('data-index')), 1);
2302
+ renderPagesPlanner();
2303
+ refreshAll();
2304
+ saveDraft();
2305
+ emit('change', { source: 'user', keys: ['plan.pages'] });
290
2306
  });
291
2307
  }
2308
+ bind('btn-skin-preview', function (e) { toggleSkinPreview(e.currentTarget); });
2309
+ bind('btn-reset', function () {
2310
+ if (window.confirm('Clear every answer and start the Site Builder again?')) resetAll();
2311
+ });
292
2312
 
293
- // Download / Copy
294
- var dlBtn = document.getElementById('btn-download');
295
- if (dlBtn) dlBtn.addEventListener('click', downloadYAML);
296
-
297
- var copyBtn = document.getElementById('btn-copy');
298
- if (copyBtn) copyBtn.addEventListener('click', copyYAML);
2313
+ window.addEventListener('pagehide', flushDraft);
2314
+ document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'hidden') flushDraft(); });
299
2315
 
300
- var copyFullBtn = document.getElementById('btn-copy-full');
301
- if (copyFullBtn) copyFullBtn.addEventListener('click', copyYAML);
2316
+ // Order matters: restore first so the initial preview and stepper reflect
2317
+ // the real content; hash wins over the draft's remembered step.
2318
+ applyOS(detectOS());
2319
+ renderNavEditor(defaultNavigation());
2320
+ renderPagesPlanner();
2321
+ restoreDraft();
2322
+ if (!readNavigation().length) renderNavEditor(defaultNavigation());
2323
+ var hashStep = (location.hash || '').replace(/^#step-/, '');
2324
+ if (hashStep && STEP_IDS.indexOf('tab-' + hashStep) !== -1) showStep('tab-' + hashStep, { silent: true });
2325
+ syncCount();
2326
+ refreshAll();
302
2327
 
303
- // Initial preview
304
- updatePreview();
2328
+ emit('ready', {});
305
2329
  });
306
2330
 
2331
+ // ── public API (consumed by assets/js/site-builder.js) ───────────
2332
+
2333
+ window.Zer0SetupWizard = {
2334
+ version: 2,
2335
+ data: DATA,
2336
+ steps: STEP_IDS.map(function (id) { return id.replace(/^tab-/, ''); }),
2337
+ getState: getState,
2338
+ getSummary: getSummary,
2339
+ schema: schema,
2340
+ applyFields: applyFields,
2341
+ getPlan: getPlan,
2342
+ setPlan: setPlan,
2343
+ planSchema: planSchema,
2344
+ validatePlan: function (plan) { return validateSchema(plan, planSchema(), 'plan'); },
2345
+ landingPlan: function () { return landingPlan(ctx()); },
2346
+ catalog: catalog,
2347
+ getFiles: getFiles,
2348
+ getFile: getFile,
2349
+ setOverride: setOverride,
2350
+ getStep: function () { return activeStepId().replace(/^tab-/, ''); },
2351
+ setStep: function (id) { return showStep(id); },
2352
+ setCheckResult: setCheckResult,
2353
+ getChecks: function () { return JSON.parse(JSON.stringify(checks)); },
2354
+ setActiveFile: function (path) { activeFile = path; updatePreview(); },
2355
+ buildBundle: buildBundle,
2356
+ clearDraft: clearDraft,
2357
+ toast: toast,
2358
+ on: on
2359
+ };
307
2360
  })();