jekyll-theme-zer0 1.29.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +715 -2
- data/_data/README.md +2 -0
- data/_data/ai.yml +5 -3
- data/_data/ai_pricing.yml +36 -0
- data/_data/backlog.yml +169 -18
- data/_data/consumers.yml +48 -5
- data/_data/features.yml +146 -20
- data/_data/feedback_types.yml +17 -12
- data/_data/landing.yml +5 -2
- data/_data/navigation/quickstart.yml +4 -0
- data/_data/site_builder.yml +874 -0
- data/_data/theme-manifest.yml +122 -114
- data/_data/ui-text.yml +18 -0
- data/_includes/README.md +11 -1
- data/_includes/analytics/posthog.html +2 -2
- data/_includes/components/admin-links.html +2 -2
- data/_includes/components/admin-tabs.html +2 -2
- data/_includes/components/ai-chat.html +14 -11
- data/_includes/components/analytics-dashboard.html +8 -8
- data/_includes/components/author-bio.html +1 -1
- data/_includes/components/author-card.html +10 -2
- data/_includes/components/author-eeat.html +4 -4
- data/_includes/components/background-customizer.html +8 -8
- data/_includes/components/background-image.html +114 -0
- data/_includes/components/background-settings.html +28 -15
- data/_includes/components/collection-manager.html +5 -5
- data/_includes/components/component-showcase.html +13 -13
- data/_includes/components/config-editor.html +12 -12
- data/_includes/components/config-viewer.html +8 -8
- data/_includes/components/cookie-consent.html +11 -11
- data/_includes/components/cta-button.html +7 -2
- data/_includes/components/dev-shortcuts.html +7 -7
- data/_includes/components/env-dashboard.html +8 -8
- data/_includes/components/env-switcher.html +9 -9
- data/_includes/components/feature-card.html +2 -2
- data/_includes/components/halfmoon.html +2 -2
- data/_includes/components/info-section.html +36 -36
- data/_includes/components/js-cdn.html +15 -15
- data/_includes/components/language-toggle.html +4 -4
- data/_includes/components/mermaid.html +72 -435
- data/_includes/components/nanobar.html +5 -5
- data/_includes/components/nav-editor.html +2 -2
- data/_includes/components/nav-export.html +2 -2
- data/_includes/components/nav-overview.html +2 -2
- data/_includes/components/page-feedback.html +45 -30
- data/_includes/components/page-views-init.html +1 -1
- data/_includes/components/post-card.html +22 -22
- data/_includes/components/post-type-badge.html +2 -2
- data/_includes/components/powered-by.html +2 -2
- data/_includes/components/preview-image.html +6 -0
- data/_includes/components/quick-index.html +2 -2
- data/_includes/components/search-modal.html +27 -2
- data/_includes/components/searchbar.html +2 -2
- data/_includes/components/svg-background.html +2 -2
- data/_includes/components/theme-customizer.html +2 -2
- data/_includes/components/theme-info.html +6 -6
- data/_includes/components/theme-preview-gallery.html +22 -22
- data/_includes/content/giscus.html +2 -2
- data/_includes/content/intro.html +8 -8
- data/_includes/content/jsonld-faq.html +2 -2
- data/_includes/content/jsonld-software.html +2 -2
- data/_includes/content/seo.html +4 -4
- data/_includes/content/sitemap.html +27 -27
- data/_includes/content/toc.html +183 -183
- data/_includes/core/branding.html +6 -6
- data/_includes/core/console-capture.html +32 -74
- data/_includes/core/favicon.html +49 -7
- data/_includes/core/footer-fabs.html +17 -3
- data/_includes/core/footer.html +31 -18
- data/_includes/core/head.html +102 -90
- data/_includes/core/header.html +71 -52
- data/_includes/docs/bootstrap-docs.html +8 -8
- data/_includes/landing/landing-install-cards.html +2 -2
- data/_includes/landing/landing-quick-links.html +1 -1
- data/_includes/navigation/admin-nav.html +2 -2
- data/_includes/navigation/nav-tree.html +8 -8
- data/_includes/navigation/navbar.html +12 -12
- data/_includes/navigation/section-sidebar.html +16 -16
- data/_includes/navigation/sidebar-config.html +36 -2
- data/_includes/navigation/sidebar-left.html +15 -15
- data/_includes/navigation/sidebar-right.html +6 -6
- data/_includes/obsidian/full-graph.html +2 -2
- data/_includes/setup/claude-session.html +72 -0
- data/_includes/setup/prereq-checklist.html +90 -0
- data/_includes/setup/wizard.html +906 -291
- data/_includes/stats/stats-categories.html +8 -8
- data/_includes/stats/stats-header.html +14 -14
- data/_includes/stats/stats-metrics.html +12 -12
- data/_includes/stats/stats-no-data.html +12 -12
- data/_includes/stats/stats-overview.html +6 -6
- data/_includes/stats/stats-tags.html +8 -8
- data/_layouts/404.html +38 -24
- data/_layouts/admin.html +22 -22
- data/_layouts/article.html +39 -34
- data/_layouts/author.html +20 -20
- data/_layouts/authors.html +2 -2
- data/_layouts/book-abc.html +12 -12
- data/_layouts/book-story.html +15 -15
- data/_layouts/book.html +12 -12
- data/_layouts/collection.html +33 -33
- data/_layouts/cookbook.html +12 -12
- data/_layouts/default.html +27 -24
- data/_layouts/home.html +23 -23
- data/_layouts/index.html +10 -10
- data/_layouts/landing.html +17 -17
- data/_layouts/news.html +44 -44
- data/_layouts/note.html +38 -38
- data/_layouts/notebook.html +34 -34
- data/_layouts/recipe.html +24 -24
- data/_layouts/root.html +73 -55
- data/_layouts/section.html +23 -23
- data/_layouts/setup.html +3 -3
- data/_layouts/sitemap-collection.html +49 -49
- data/_layouts/stats.html +40 -40
- data/_layouts/tag.html +12 -12
- data/_layouts/welcome.html +21 -21
- data/_sass/components/_mermaid.scss +375 -0
- data/_sass/components/_setup-wizard.scss +569 -40
- data/_sass/core/_navbar.scss +11 -31
- data/_sass/layouts/_navbar-extras.scss +14 -4
- data/assets/css/main.scss +1 -0
- data/assets/js/ai-chat.js +47 -5
- data/assets/js/fleet-feedback-capture.js +124 -0
- data/assets/js/fleet-feedback.js +853 -0
- data/assets/js/mermaid-diagrams.js +1267 -0
- data/assets/js/modules/navigation/config.js +9 -6
- data/assets/js/modules/navigation/scroll-spy.js +315 -80
- data/assets/js/modules/theme/appearance.js +8 -2
- data/assets/js/obsidian-wiki-links.js +8 -3
- data/assets/js/page-feedback.js +125 -192
- data/assets/js/search-modal.js +26 -0
- data/assets/js/setup-wizard.js +2112 -361
- data/assets/js/site-builder.js +1834 -0
- data/assets/js/ui-enhancements.js +11 -3
- data/scripts/README.md +15 -0
- data/scripts/ai/README.md +38 -0
- data/scripts/ai/api_call.rb +124 -0
- data/scripts/ai/usage.rb +314 -0
- data/scripts/ai/usage_report.rb +225 -0
- data/scripts/ci/test_visual_evidence_autogen.py +341 -0
- data/scripts/ci/visual_evidence_autogen.py +1060 -0
- data/scripts/content-review.rb +20 -1
- data/scripts/test/integration/mermaid +22 -8
- data/scripts/test/lib/run_tests.sh +1 -0
- data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
- data/scripts/translate.rb +23 -1
- metadata +19 -2
data/assets/js/setup-wizard.js
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
|
-
// Feature: ZER0-067
|
|
1
|
+
// Feature: ZER0-067, ZER0-086, ZER0-087
|
|
2
2
|
/**
|
|
3
3
|
* ===================================================================
|
|
4
|
-
*
|
|
4
|
+
* Site Builder — form, generators, drafts (setup-wizard.js)
|
|
5
5
|
* ===================================================================
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* pane on every reflow. An earlier measurePanes() did this in JS — un-hiding
|
|
14
|
-
* each pane, reading offsetHeight, restoring it — and came back ~16px short,
|
|
15
|
-
* which put the shortest step's Back/Next 16px above everyone else's.
|
|
12
|
+
* This half of the Site Builder is pure client-side and works everywhere the
|
|
13
|
+
* theme renders, GitHub Pages included. It owns:
|
|
16
14
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
30
32
|
*
|
|
31
|
-
*
|
|
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.
|
|
33
35
|
*
|
|
36
|
+
* Everything degrades: localStorage failures are swallowed, a missing element
|
|
37
|
+
* short-circuits, and the wizard never throws on a partial DOM.
|
|
34
38
|
* ===================================================================
|
|
35
39
|
*/
|
|
36
40
|
|
|
@@ -39,246 +43,1675 @@
|
|
|
39
43
|
|
|
40
44
|
var DRAFT_KEY = 'zer0-setup-draft';
|
|
41
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;
|
|
42
59
|
|
|
43
60
|
// Step order is the source of truth for "earlier" / "later" in the stepper.
|
|
44
|
-
var STEP_IDS = [
|
|
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
|
+
}
|
|
45
66
|
|
|
46
67
|
var draftTimer = null;
|
|
47
68
|
var chipTimer = null;
|
|
48
|
-
var restoring = false;
|
|
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
|
+
}
|
|
49
88
|
|
|
50
|
-
|
|
89
|
+
function planSchema() { return DATA.plan_schema || { type: 'object' }; }
|
|
51
90
|
|
|
52
91
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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).
|
|
57
96
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
var
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
66
129
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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('');
|
|
73
155
|
}
|
|
74
|
-
|
|
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('');
|
|
171
|
+
}
|
|
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, '\\"') + '"';
|
|
75
225
|
}
|
|
76
226
|
|
|
77
|
-
/** Pad a YAML key to 25 chars for alignment. */
|
|
78
227
|
function pad(key, width) {
|
|
79
228
|
width = width || 25;
|
|
80
|
-
return (key + ' '.repeat(width)).slice(0, width);
|
|
229
|
+
return (key + ' '.repeat(width)).slice(0, Math.max(width, key.length + 1));
|
|
81
230
|
}
|
|
82
231
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
case 'posts': return '/posts/:title/';
|
|
87
|
-
case 'docs': return '/docs/:path/';
|
|
88
|
-
case 'about': return '/about/:path/';
|
|
89
|
-
default: return '/' + col + '/:path/';
|
|
90
|
-
}
|
|
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);
|
|
91
235
|
}
|
|
92
236
|
|
|
93
|
-
|
|
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
|
+
}
|
|
94
247
|
|
|
95
|
-
function
|
|
96
|
-
|
|
248
|
+
function todayISO() {
|
|
249
|
+
try { return new Date().toISOString().slice(0, 10); } catch (e) { return '2026-01-01'; }
|
|
250
|
+
}
|
|
251
|
+
|
|
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]'); }
|
|
97
259
|
|
|
98
|
-
|
|
99
|
-
|
|
260
|
+
function readFields() {
|
|
261
|
+
var fields = {};
|
|
262
|
+
fieldEls().forEach(function (el) {
|
|
100
263
|
var key = el.getAttribute('data-key');
|
|
101
264
|
if (!key) return;
|
|
102
|
-
var val =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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;
|
|
106
271
|
});
|
|
272
|
+
wizard.querySelectorAll('.cfg-switch[data-key]').forEach(function (el) {
|
|
273
|
+
fields[el.getAttribute('data-key')] = !!el.checked;
|
|
274
|
+
});
|
|
275
|
+
return fields;
|
|
276
|
+
}
|
|
107
277
|
|
|
108
|
-
|
|
109
|
-
var
|
|
110
|
-
|
|
278
|
+
function readCollections() {
|
|
279
|
+
var out = [];
|
|
280
|
+
wizard.querySelectorAll('.cfg-collection').forEach(function (el) {
|
|
111
281
|
if (el.checked) {
|
|
112
|
-
|
|
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
|
+
});
|
|
113
287
|
}
|
|
114
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
|
+
}
|
|
302
|
+
|
|
303
|
+
function catalog(name) { return Array.isArray(DATA[name]) ? DATA[name] : []; }
|
|
304
|
+
|
|
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
|
+
}
|
|
115
343
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
396
|
|
|
120
|
-
|
|
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) {
|
|
121
416
|
var lines = [];
|
|
122
|
-
lines.push('
|
|
123
|
-
lines.push('
|
|
124
|
-
|
|
125
|
-
lines.push('
|
|
126
|
-
lines.push('');
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
lines
|
|
154
|
-
lines.push(
|
|
155
|
-
lines.push(
|
|
156
|
-
lines.push('');
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
lines.push('
|
|
160
|
-
lines.push('
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
lines.push('');
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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;
|
|
220
756
|
} else {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 };
|
|
233
1130
|
}
|
|
234
|
-
|
|
235
|
-
|
|
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 };
|
|
236
1135
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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.' }] };
|
|
1142
|
+
}
|
|
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
|
+
}
|
|
252
1166
|
|
|
253
|
-
|
|
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 });
|
|
1196
|
+
}
|
|
1197
|
+
|
|
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');
|
|
254
1262
|
}
|
|
255
1263
|
|
|
256
|
-
// ──
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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 ────────────────────────────────────────────
|
|
257
1698
|
|
|
258
1699
|
var VALIDATION_MESSAGES = {
|
|
259
1700
|
email: 'Enter a valid email address, e.g. you@example.com.',
|
|
260
1701
|
url: 'Enter a full URL including https://, e.g. https://example.com.'
|
|
261
1702
|
};
|
|
262
1703
|
|
|
263
|
-
/**
|
|
264
|
-
* Validate one field. Empty is always OK — nothing in this wizard is
|
|
265
|
-
* required, and flagging an untouched field as an error would be wrong.
|
|
266
|
-
* Returns true when the field is acceptable.
|
|
267
|
-
*/
|
|
268
1704
|
function validateField(el) {
|
|
269
1705
|
if (!el || !el.classList.contains('cfg-field')) return true;
|
|
270
1706
|
var type = (el.getAttribute('type') || '').toLowerCase();
|
|
271
1707
|
if (type !== 'email' && type !== 'url') return true;
|
|
272
|
-
|
|
273
1708
|
var ok = el.value.trim() === '' || el.checkValidity();
|
|
274
1709
|
el.classList.toggle('is-invalid', !ok);
|
|
275
|
-
|
|
276
1710
|
var feedback = document.getElementById(el.id + '-feedback');
|
|
277
1711
|
if (feedback) feedback.textContent = ok ? '' : VALIDATION_MESSAGES[type];
|
|
278
1712
|
return ok;
|
|
279
1713
|
}
|
|
280
1714
|
|
|
281
|
-
/** Is every field inside this pane currently acceptable? */
|
|
282
1715
|
function paneIsValid(pane) {
|
|
283
1716
|
if (!pane) return true;
|
|
284
1717
|
var fields = pane.querySelectorAll('.cfg-field');
|
|
@@ -291,57 +1724,45 @@
|
|
|
291
1724
|
return true;
|
|
292
1725
|
}
|
|
293
1726
|
|
|
294
|
-
/** The recommended-but-empty fields, by their human label. */
|
|
295
1727
|
function unfilledRecommended() {
|
|
296
1728
|
var missing = [];
|
|
297
|
-
|
|
1729
|
+
wizard.querySelectorAll('[data-recommended]').forEach(function (el) {
|
|
298
1730
|
if (el.value.trim() === '') missing.push(el.getAttribute('data-recommended'));
|
|
299
1731
|
});
|
|
300
1732
|
return missing;
|
|
301
1733
|
}
|
|
302
1734
|
|
|
303
|
-
/** Render the Review step's warning list. */
|
|
304
1735
|
function renderReviewWarnings() {
|
|
305
1736
|
var host = document.getElementById('wizard-review-warnings');
|
|
306
1737
|
if (!host) return;
|
|
307
1738
|
var missing = unfilledRecommended();
|
|
308
|
-
|
|
1739
|
+
host.innerHTML = '';
|
|
1740
|
+
var alert = document.createElement('div');
|
|
1741
|
+
alert.setAttribute('role', 'alert');
|
|
309
1742
|
if (!missing.length) {
|
|
310
|
-
|
|
311
|
-
|
|
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);
|
|
312
1746
|
return;
|
|
313
1747
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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) {
|
|
318
1753
|
var li = document.createElement('li');
|
|
319
1754
|
li.textContent = name;
|
|
320
|
-
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
host.
|
|
324
|
-
'<i class="bi bi-exclamation-triangle"></i> ' +
|
|
325
|
-
'<strong>Recommended fields still empty.</strong> The file is valid without them, ' +
|
|
326
|
-
'but filling them in improves SEO and the site chrome:' +
|
|
327
|
-
'<ul class="mb-0 mt-2">' + items + '</ul></div>';
|
|
1755
|
+
ul.appendChild(li);
|
|
1756
|
+
});
|
|
1757
|
+
alert.appendChild(ul);
|
|
1758
|
+
host.appendChild(alert);
|
|
328
1759
|
}
|
|
329
1760
|
|
|
330
|
-
// ── stepper state
|
|
1761
|
+
// ── stepper state ────────────────────────────────────────────────
|
|
331
1762
|
|
|
332
|
-
/**
|
|
333
|
-
* Recompute done / active / locked across the vertical stepper.
|
|
334
|
-
*
|
|
335
|
-
* Rule: a step is reachable when every EARLIER step validates. That makes
|
|
336
|
-
* "Back is always allowed" fall out for free (earlier steps are, by
|
|
337
|
-
* definition, behind a prefix that already validated) while stopping the
|
|
338
|
-
* user from carrying a malformed email or URL forward.
|
|
339
|
-
*/
|
|
340
1763
|
function refreshStepper() {
|
|
341
1764
|
var buttons = STEP_IDS.map(function (id) { return document.getElementById(id); });
|
|
342
|
-
var activeIndex = buttons.findIndex(function (b) {
|
|
343
|
-
return b && b.classList.contains('is-active');
|
|
344
|
-
});
|
|
1765
|
+
var activeIndex = buttons.findIndex(function (b) { return b && b.classList.contains('is-active'); });
|
|
345
1766
|
if (activeIndex < 0) activeIndex = 0;
|
|
346
1767
|
|
|
347
1768
|
var blocked = false;
|
|
@@ -349,43 +1770,214 @@
|
|
|
349
1770
|
if (!btn) return;
|
|
350
1771
|
var pane = document.querySelector(btn.getAttribute('data-bs-target'));
|
|
351
1772
|
var valid = paneIsValid(pane);
|
|
352
|
-
|
|
353
1773
|
var locked = blocked && i > activeIndex;
|
|
354
1774
|
btn.classList.toggle('is-active', i === activeIndex);
|
|
355
1775
|
btn.classList.toggle('is-done', i < activeIndex && valid);
|
|
356
1776
|
btn.classList.toggle('is-locked', locked);
|
|
357
1777
|
btn.disabled = locked;
|
|
358
1778
|
btn.setAttribute('aria-selected', i === activeIndex ? 'true' : 'false');
|
|
359
|
-
|
|
360
1779
|
var state = btn.querySelector('.wizard-step-state');
|
|
361
1780
|
if (state) {
|
|
362
1781
|
state.textContent = i === activeIndex ? 'current step'
|
|
363
1782
|
: locked ? 'locked until earlier steps are valid'
|
|
364
1783
|
: i < activeIndex && valid ? 'completed' : '';
|
|
365
1784
|
}
|
|
366
|
-
|
|
367
1785
|
if (!valid) blocked = true;
|
|
368
1786
|
});
|
|
369
1787
|
|
|
370
|
-
|
|
371
|
-
document.querySelectorAll('.btn-next').forEach(function (btn) {
|
|
1788
|
+
wizard.querySelectorAll('.btn-next').forEach(function (btn) {
|
|
372
1789
|
var target = document.getElementById(btn.getAttribute('data-next'));
|
|
373
1790
|
btn.disabled = !!(target && target.disabled);
|
|
374
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;
|
|
375
1798
|
}
|
|
376
1799
|
|
|
377
|
-
|
|
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
|
+
}
|
|
378
1820
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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);
|
|
383
1913
|
});
|
|
384
|
-
|
|
385
|
-
|
|
1914
|
+
}
|
|
1915
|
+
|
|
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];
|
|
1921
|
+
var preview = document.getElementById('yaml-preview');
|
|
1922
|
+
if (preview && file) {
|
|
1923
|
+
preview.textContent = file.content;
|
|
1924
|
+
preview.parentElement.setAttribute('data-file', file.path);
|
|
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();
|
|
1943
|
+
}
|
|
1944
|
+
|
|
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);
|
|
386
1949
|
});
|
|
387
|
-
var
|
|
388
|
-
|
|
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();
|
|
389
1981
|
return data;
|
|
390
1982
|
}
|
|
391
1983
|
|
|
@@ -399,10 +1991,7 @@
|
|
|
399
1991
|
}
|
|
400
1992
|
|
|
401
1993
|
function writeDraft() {
|
|
402
|
-
try {
|
|
403
|
-
localStorage.setItem(DRAFT_KEY, JSON.stringify(collectDraft()));
|
|
404
|
-
return true;
|
|
405
|
-
} catch (e) { /* private mode or quota — the wizard still works */ }
|
|
1994
|
+
try { localStorage.setItem(DRAFT_KEY, JSON.stringify(collectDraft())); return true; } catch (e) { /* private mode / quota */ }
|
|
406
1995
|
return false;
|
|
407
1996
|
}
|
|
408
1997
|
|
|
@@ -415,18 +2004,6 @@
|
|
|
415
2004
|
}, DRAFT_DEBOUNCE_MS);
|
|
416
2005
|
}
|
|
417
2006
|
|
|
418
|
-
/**
|
|
419
|
-
* Write a PENDING debounced draft out immediately.
|
|
420
|
-
*
|
|
421
|
-
* The 300ms debounce means the last edit before the page goes away is still
|
|
422
|
-
* only a queued timer, and a timer does not survive navigation: check a
|
|
423
|
-
* collection box and hit reload inside that window and the change is simply
|
|
424
|
-
* lost, even though the chip may already have acknowledged an EARLIER save.
|
|
425
|
-
* `pagehide` and the hidden `visibilitychange` are the two events that still
|
|
426
|
-
* fire reliably on reload, tab close and bfcache eviction, so the flush hangs
|
|
427
|
-
* off both. No-op when nothing is queued, so it can never resurrect a draft
|
|
428
|
-
* that `clearDraft()` has just removed.
|
|
429
|
-
*/
|
|
430
2007
|
function flushDraft() {
|
|
431
2008
|
if (draftTimer === null || restoring) return;
|
|
432
2009
|
clearTimeout(draftTimer);
|
|
@@ -436,7 +2013,7 @@
|
|
|
436
2013
|
|
|
437
2014
|
function clearDraft() {
|
|
438
2015
|
clearTimeout(draftTimer);
|
|
439
|
-
draftTimer = null;
|
|
2016
|
+
draftTimer = null;
|
|
440
2017
|
try { localStorage.removeItem(DRAFT_KEY); } catch (e) { /* ignore */ }
|
|
441
2018
|
var chip = document.getElementById('wizard-draft-chip');
|
|
442
2019
|
if (chip) { chip.classList.remove('is-visible'); chip.hidden = true; }
|
|
@@ -446,7 +2023,6 @@
|
|
|
446
2023
|
var raw;
|
|
447
2024
|
try { raw = localStorage.getItem(DRAFT_KEY); } catch (e) { return; }
|
|
448
2025
|
if (!raw) return;
|
|
449
|
-
|
|
450
2026
|
var data;
|
|
451
2027
|
try { data = JSON.parse(raw); } catch (e) { clearDraft(); return; }
|
|
452
2028
|
if (!data || typeof data !== 'object') return;
|
|
@@ -460,150 +2036,325 @@
|
|
|
460
2036
|
var el = document.getElementById(id);
|
|
461
2037
|
if (el && el.type === 'checkbox') el.checked = !!data.checks[id];
|
|
462
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
|
+
}
|
|
463
2055
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// on a step the stepper would refuse to unlock.
|
|
467
|
-
var target = data.step && STEP_IDS.indexOf(data.step) !== -1
|
|
468
|
-
? document.getElementById(data.step) : null;
|
|
469
|
-
if (target && !target.disabled) showStep(target);
|
|
470
|
-
|
|
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 });
|
|
471
2058
|
restoring = false;
|
|
472
2059
|
}
|
|
473
2060
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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', {});
|
|
481
2087
|
}
|
|
482
2088
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
document.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (window.bootstrap && window.bootstrap.Tab) new bootstrap.Tab(btn).show();
|
|
491
|
-
refreshStepper();
|
|
492
|
-
if (btn.id === 'tab-review') renderReviewWarnings();
|
|
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();
|
|
493
2096
|
}
|
|
494
2097
|
|
|
495
|
-
function
|
|
496
|
-
var
|
|
497
|
-
var blob = new Blob([yaml], { type: 'text/yaml;charset=utf-8' });
|
|
2098
|
+
function downloadText(name, text, mime) {
|
|
2099
|
+
var blob = new Blob([text], { type: (mime || 'text/plain') + ';charset=utf-8' });
|
|
498
2100
|
var url = URL.createObjectURL(blob);
|
|
499
2101
|
var a = document.createElement('a');
|
|
500
2102
|
a.href = url;
|
|
501
|
-
a.download =
|
|
2103
|
+
a.download = name;
|
|
502
2104
|
document.body.appendChild(a);
|
|
503
2105
|
a.click();
|
|
504
2106
|
document.body.removeChild(a);
|
|
505
2107
|
URL.revokeObjectURL(url);
|
|
506
|
-
// The draft exists to survive a reload mid-wizard. Once the file is in the
|
|
507
|
-
// user's hands the wizard is finished, so the draft is stale, not useful.
|
|
508
|
-
clearDraft();
|
|
509
2108
|
}
|
|
510
2109
|
|
|
511
|
-
function
|
|
512
|
-
var
|
|
513
|
-
|
|
514
|
-
|
|
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');
|
|
515
2132
|
if (btn) {
|
|
516
2133
|
var orig = btn.innerHTML;
|
|
517
|
-
btn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
|
|
2134
|
+
btn.innerHTML = '<i class="bi bi-check-lg" aria-hidden="true"></i> Copied!';
|
|
518
2135
|
setTimeout(function () { btn.innerHTML = orig; }, 2000);
|
|
519
2136
|
}
|
|
520
|
-
});
|
|
2137
|
+
}).catch(function () { /* no clipboard */ });
|
|
521
2138
|
}
|
|
522
2139
|
|
|
523
|
-
|
|
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
|
+
}
|
|
524
2150
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// ── wiring ───────────────────────────────────────────────────────
|
|
528
2188
|
|
|
529
|
-
|
|
530
|
-
// refreshStepper() when the step it points at has not unlocked.
|
|
2189
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
531
2190
|
wizard.querySelectorAll('.btn-next').forEach(function (btn) {
|
|
532
|
-
btn.addEventListener('click', function () {
|
|
533
|
-
showStep(document.getElementById(btn.getAttribute('data-next')));
|
|
534
|
-
});
|
|
2191
|
+
btn.addEventListener('click', function () { showStep(document.getElementById(btn.getAttribute('data-next'))); });
|
|
535
2192
|
});
|
|
536
|
-
|
|
537
2193
|
wizard.querySelectorAll('.btn-prev').forEach(function (btn) {
|
|
538
|
-
btn.addEventListener('click', function () {
|
|
539
|
-
showStep(document.getElementById(btn.getAttribute('data-prev')));
|
|
540
|
-
});
|
|
2194
|
+
btn.addEventListener('click', function () { showStep(document.getElementById(btn.getAttribute('data-prev'))); });
|
|
541
2195
|
});
|
|
542
|
-
|
|
543
|
-
// Clicking a stepper entry directly.
|
|
544
2196
|
wizard.querySelectorAll('#wizardTabs .wizard-step').forEach(function (btn) {
|
|
545
2197
|
btn.addEventListener('click', function () { showStep(btn); });
|
|
546
2198
|
});
|
|
547
2199
|
|
|
548
|
-
// The preview is persistent now, so it regenerates on EVERY input rather
|
|
549
|
-
// than on entering step 5. Delegated, so fields added later are covered.
|
|
550
2200
|
wizard.addEventListener('input', function (event) {
|
|
551
2201
|
var el = event.target;
|
|
552
|
-
updatePreview();
|
|
553
|
-
// Typing clears a previous error rather than nagging mid-entry; the
|
|
554
|
-
// re-check happens on blur.
|
|
555
2202
|
if (el.classList && el.classList.contains('is-invalid')) validateField(el);
|
|
2203
|
+
refreshAll();
|
|
556
2204
|
saveDraft();
|
|
2205
|
+
emit('change', { source: 'user', keys: [el.getAttribute('data-key') || el.id] });
|
|
557
2206
|
});
|
|
558
2207
|
|
|
559
|
-
wizard.addEventListener('change', function () {
|
|
560
|
-
|
|
561
|
-
|
|
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();
|
|
562
2219
|
saveDraft();
|
|
2220
|
+
emit('change', { source: 'user', keys: [el.getAttribute('data-key') || el.getAttribute('data-col') || el.id] });
|
|
563
2221
|
});
|
|
564
2222
|
|
|
565
|
-
// Validate email / URL on blur (capture: `blur` does not bubble).
|
|
566
2223
|
wizard.addEventListener('blur', function (event) {
|
|
567
2224
|
if (!event.target.classList || !event.target.classList.contains('cfg-field')) return;
|
|
568
2225
|
validateField(event.target);
|
|
569
2226
|
refreshStepper();
|
|
570
2227
|
}, true);
|
|
571
2228
|
|
|
572
|
-
//
|
|
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
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
|
|
573
2253
|
var descField = document.getElementById('cfg-description');
|
|
574
2254
|
var descCount = document.getElementById('desc-count');
|
|
575
|
-
if (descField && descCount)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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'] });
|
|
2306
|
+
});
|
|
579
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
|
+
});
|
|
580
2312
|
|
|
581
|
-
// Download / Copy — always enabled, at every step.
|
|
582
|
-
var dlBtn = document.getElementById('btn-download');
|
|
583
|
-
if (dlBtn) dlBtn.addEventListener('click', downloadYAML);
|
|
584
|
-
|
|
585
|
-
var copyBtn = document.getElementById('btn-copy');
|
|
586
|
-
if (copyBtn) copyBtn.addEventListener('click', copyYAML);
|
|
587
|
-
|
|
588
|
-
var copyFullBtn = document.getElementById('btn-copy-full');
|
|
589
|
-
if (copyFullBtn) copyFullBtn.addEventListener('click', copyYAML);
|
|
590
|
-
|
|
591
|
-
// A queued draft write must not be lost to a reload or a tab close.
|
|
592
2313
|
window.addEventListener('pagehide', flushDraft);
|
|
593
|
-
document.addEventListener('visibilitychange', function () {
|
|
594
|
-
if (document.visibilityState === 'hidden') flushDraft();
|
|
595
|
-
});
|
|
2314
|
+
document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'hidden') flushDraft(); });
|
|
596
2315
|
|
|
597
|
-
// Order matters: restore
|
|
598
|
-
//
|
|
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();
|
|
599
2321
|
restoreDraft();
|
|
600
|
-
if (
|
|
601
|
-
|
|
602
|
-
|
|
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();
|
|
603
2327
|
|
|
604
|
-
|
|
605
|
-
// _setup-wizard.scss keeps `#wizardTabContent` as tall as the tallest pane
|
|
606
|
-
// at every width, without JS having to be told the width changed.
|
|
2328
|
+
emit('ready', {});
|
|
607
2329
|
});
|
|
608
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
|
+
};
|
|
609
2360
|
})();
|