jekyll-theme-zer0 1.27.0 → 1.29.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 +812 -13
- data/_data/backlog.yml +356 -2
- data/_data/consumers.yml +252 -0
- data/_data/features.yml +234 -5
- data/_data/i18n/fr.yml +103 -0
- data/_data/i18n/manifest.yml +1529 -0
- data/_data/ingredient_densities.yml +122 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +22 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +387 -277
- data/_data/ui-text.yml +8 -0
- data/_includes/README.md +36 -3
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/background-customizer.html +2 -2
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/cookie-consent.html +4 -4
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/info-section.html +8 -3
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/theme-info.html +8 -1
- data/_includes/content/jsonld-software.html +22 -3
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/footer.html +18 -16
- data/_includes/core/head.html +21 -0
- data/_includes/core/header.html +7 -4
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/section-sidebar.html +93 -11
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-left.html +2 -1
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +2 -1
- data/_includes/navigation/unified-drawer.html +8 -2
- data/_includes/obsidian/full-graph.html +165 -136
- data/_includes/setup/wizard.html +173 -86
- data/_includes/stats/stats-metrics.html +2 -2
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +4 -0
- data/_layouts/admin.html +2 -2
- data/_layouts/article.html +5 -0
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +4 -4
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +155 -31
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +38 -5
- data/_layouts/section.html +96 -25
- data/_sass/components/_book.scss +423 -0
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +235 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +263 -15
- data/_sass/core/_obsidian.scss +286 -6
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/theme/_backgrounds.scss +21 -8
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/obsidian-graph.js +702 -264
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +14 -1
- data/assets/js/setup-wizard.js +322 -20
- data/scripts/README.md +29 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +66 -16
- data/scripts/bin/validate +5 -1
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/design-system-check.rb +170 -0
- data/scripts/install/README.md +47 -6
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/propagate.rb +277 -0
- data/scripts/test/lib/run_tests.sh +2 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/translate.rb +161 -15
- metadata +56 -2
data/assets/js/setup-wizard.js
CHANGED
|
@@ -8,12 +8,45 @@
|
|
|
8
8
|
* and offers Download / Copy actions. Pure vanilla JS, no deps
|
|
9
9
|
* beyond Bootstrap 5 (already on the page).
|
|
10
10
|
*
|
|
11
|
+
* The step container's stable height is NOT handled here. `.wizard-panes` is a
|
|
12
|
+
* CSS grid with every pane in one cell, so the browser sizes it to the tallest
|
|
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.
|
|
16
|
+
*
|
|
17
|
+
* T-040 / #408 added, in the order the pieces depend on each other:
|
|
18
|
+
* 1. updatePreview() — now fires on every input anywhere in the wizard,
|
|
19
|
+
* not just on entering step 5, because the preview is
|
|
20
|
+
* a persistent panel rather than a step-5 element.
|
|
21
|
+
* 2. validateField() — email/URL checked on blur, `is-invalid` + a message;
|
|
22
|
+
* cleared as soon as the user types again.
|
|
23
|
+
* 3. saveDraft() — every value mirrored to localStorage under
|
|
24
|
+
* `zer0-setup-draft`, debounced 300ms, restored on
|
|
25
|
+
* load, acknowledged by the "Draft saved" chip, and
|
|
26
|
+
* cleared when the file is downloaded.
|
|
27
|
+
* 4. refreshStepper() — the vertical stepper's done/active/locked state. A
|
|
28
|
+
* step is locked until every EARLIER step validates;
|
|
29
|
+
* going back is always allowed.
|
|
30
|
+
*
|
|
31
|
+
* Everything degrades: localStorage failures (private mode, quota) are
|
|
32
|
+
* swallowed, and a missing element short-circuits rather than throwing.
|
|
33
|
+
*
|
|
11
34
|
* ===================================================================
|
|
12
35
|
*/
|
|
13
36
|
|
|
14
37
|
(function () {
|
|
15
38
|
'use strict';
|
|
16
39
|
|
|
40
|
+
var DRAFT_KEY = 'zer0-setup-draft';
|
|
41
|
+
var DRAFT_DEBOUNCE_MS = 300;
|
|
42
|
+
|
|
43
|
+
// Step order is the source of truth for "earlier" / "later" in the stepper.
|
|
44
|
+
var STEP_IDS = ['tab-identity', 'tab-urls', 'tab-collections', 'tab-analytics', 'tab-review'];
|
|
45
|
+
|
|
46
|
+
var draftTimer = null;
|
|
47
|
+
var chipTimer = null;
|
|
48
|
+
var restoring = false; // suppresses draft writes while we replay a draft
|
|
49
|
+
|
|
17
50
|
// ── helpers ────────────────────────────────────────────────────────
|
|
18
51
|
|
|
19
52
|
/**
|
|
@@ -220,6 +253,224 @@
|
|
|
220
253
|
return lines.join('\n');
|
|
221
254
|
}
|
|
222
255
|
|
|
256
|
+
// ── inline validation ──────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
var VALIDATION_MESSAGES = {
|
|
259
|
+
email: 'Enter a valid email address, e.g. you@example.com.',
|
|
260
|
+
url: 'Enter a full URL including https://, e.g. https://example.com.'
|
|
261
|
+
};
|
|
262
|
+
|
|
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
|
+
function validateField(el) {
|
|
269
|
+
if (!el || !el.classList.contains('cfg-field')) return true;
|
|
270
|
+
var type = (el.getAttribute('type') || '').toLowerCase();
|
|
271
|
+
if (type !== 'email' && type !== 'url') return true;
|
|
272
|
+
|
|
273
|
+
var ok = el.value.trim() === '' || el.checkValidity();
|
|
274
|
+
el.classList.toggle('is-invalid', !ok);
|
|
275
|
+
|
|
276
|
+
var feedback = document.getElementById(el.id + '-feedback');
|
|
277
|
+
if (feedback) feedback.textContent = ok ? '' : VALIDATION_MESSAGES[type];
|
|
278
|
+
return ok;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Is every field inside this pane currently acceptable? */
|
|
282
|
+
function paneIsValid(pane) {
|
|
283
|
+
if (!pane) return true;
|
|
284
|
+
var fields = pane.querySelectorAll('.cfg-field');
|
|
285
|
+
for (var i = 0; i < fields.length; i += 1) {
|
|
286
|
+
var el = fields[i];
|
|
287
|
+
var type = (el.getAttribute('type') || '').toLowerCase();
|
|
288
|
+
if (type !== 'email' && type !== 'url') continue;
|
|
289
|
+
if (el.value.trim() !== '' && !el.checkValidity()) return false;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The recommended-but-empty fields, by their human label. */
|
|
295
|
+
function unfilledRecommended() {
|
|
296
|
+
var missing = [];
|
|
297
|
+
document.querySelectorAll('[data-recommended]').forEach(function (el) {
|
|
298
|
+
if (el.value.trim() === '') missing.push(el.getAttribute('data-recommended'));
|
|
299
|
+
});
|
|
300
|
+
return missing;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Render the Review step's warning list. */
|
|
304
|
+
function renderReviewWarnings() {
|
|
305
|
+
var host = document.getElementById('wizard-review-warnings');
|
|
306
|
+
if (!host) return;
|
|
307
|
+
var missing = unfilledRecommended();
|
|
308
|
+
|
|
309
|
+
if (!missing.length) {
|
|
310
|
+
host.innerHTML = '<div class="alert alert-success mb-0" role="alert">' +
|
|
311
|
+
'<i class="bi bi-check2-circle"></i> Every recommended field is filled in.</div>';
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
var items = missing.map(function (name) {
|
|
316
|
+
// Labels come from our own markup, but build the DOM through
|
|
317
|
+
// textContent-equivalent escaping anyway rather than interpolating.
|
|
318
|
+
var li = document.createElement('li');
|
|
319
|
+
li.textContent = name;
|
|
320
|
+
return li.outerHTML;
|
|
321
|
+
}).join('');
|
|
322
|
+
|
|
323
|
+
host.innerHTML = '<div class="alert alert-warning mb-0" role="alert">' +
|
|
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>';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── stepper state ──────────────────────────────────────────────────
|
|
331
|
+
|
|
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
|
+
function refreshStepper() {
|
|
341
|
+
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
|
+
});
|
|
345
|
+
if (activeIndex < 0) activeIndex = 0;
|
|
346
|
+
|
|
347
|
+
var blocked = false;
|
|
348
|
+
buttons.forEach(function (btn, i) {
|
|
349
|
+
if (!btn) return;
|
|
350
|
+
var pane = document.querySelector(btn.getAttribute('data-bs-target'));
|
|
351
|
+
var valid = paneIsValid(pane);
|
|
352
|
+
|
|
353
|
+
var locked = blocked && i > activeIndex;
|
|
354
|
+
btn.classList.toggle('is-active', i === activeIndex);
|
|
355
|
+
btn.classList.toggle('is-done', i < activeIndex && valid);
|
|
356
|
+
btn.classList.toggle('is-locked', locked);
|
|
357
|
+
btn.disabled = locked;
|
|
358
|
+
btn.setAttribute('aria-selected', i === activeIndex ? 'true' : 'false');
|
|
359
|
+
|
|
360
|
+
var state = btn.querySelector('.wizard-step-state');
|
|
361
|
+
if (state) {
|
|
362
|
+
state.textContent = i === activeIndex ? 'current step'
|
|
363
|
+
: locked ? 'locked until earlier steps are valid'
|
|
364
|
+
: i < activeIndex && valid ? 'completed' : '';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!valid) blocked = true;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// A Next button cannot skip past a step that has not validated.
|
|
371
|
+
document.querySelectorAll('.btn-next').forEach(function (btn) {
|
|
372
|
+
var target = document.getElementById(btn.getAttribute('data-next'));
|
|
373
|
+
btn.disabled = !!(target && target.disabled);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── draft persistence ──────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
function collectDraft() {
|
|
380
|
+
var data = { fields: {}, checks: {}, step: null };
|
|
381
|
+
document.querySelectorAll('#setup-wizard .cfg-field').forEach(function (el) {
|
|
382
|
+
if (el.id) data.fields[el.id] = el.value;
|
|
383
|
+
});
|
|
384
|
+
document.querySelectorAll('#setup-wizard input[type="checkbox"]').forEach(function (el) {
|
|
385
|
+
if (el.id) data.checks[el.id] = el.checked;
|
|
386
|
+
});
|
|
387
|
+
var active = document.querySelector('#wizardTabs .wizard-step.is-active');
|
|
388
|
+
if (active) data.step = active.id;
|
|
389
|
+
return data;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function showDraftChip() {
|
|
393
|
+
var chip = document.getElementById('wizard-draft-chip');
|
|
394
|
+
if (!chip) return;
|
|
395
|
+
chip.hidden = false;
|
|
396
|
+
chip.classList.add('is-visible');
|
|
397
|
+
clearTimeout(chipTimer);
|
|
398
|
+
chipTimer = setTimeout(function () { chip.classList.remove('is-visible'); }, 2000);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
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 */ }
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function saveDraft() {
|
|
410
|
+
if (restoring) return;
|
|
411
|
+
clearTimeout(draftTimer);
|
|
412
|
+
draftTimer = setTimeout(function () {
|
|
413
|
+
draftTimer = null;
|
|
414
|
+
if (writeDraft()) showDraftChip();
|
|
415
|
+
}, DRAFT_DEBOUNCE_MS);
|
|
416
|
+
}
|
|
417
|
+
|
|
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
|
+
function flushDraft() {
|
|
431
|
+
if (draftTimer === null || restoring) return;
|
|
432
|
+
clearTimeout(draftTimer);
|
|
433
|
+
draftTimer = null;
|
|
434
|
+
writeDraft();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function clearDraft() {
|
|
438
|
+
clearTimeout(draftTimer);
|
|
439
|
+
draftTimer = null; // …so a later flush cannot write the draft back
|
|
440
|
+
try { localStorage.removeItem(DRAFT_KEY); } catch (e) { /* ignore */ }
|
|
441
|
+
var chip = document.getElementById('wizard-draft-chip');
|
|
442
|
+
if (chip) { chip.classList.remove('is-visible'); chip.hidden = true; }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function restoreDraft() {
|
|
446
|
+
var raw;
|
|
447
|
+
try { raw = localStorage.getItem(DRAFT_KEY); } catch (e) { return; }
|
|
448
|
+
if (!raw) return;
|
|
449
|
+
|
|
450
|
+
var data;
|
|
451
|
+
try { data = JSON.parse(raw); } catch (e) { clearDraft(); return; }
|
|
452
|
+
if (!data || typeof data !== 'object') return;
|
|
453
|
+
|
|
454
|
+
restoring = true;
|
|
455
|
+
Object.keys(data.fields || {}).forEach(function (id) {
|
|
456
|
+
var el = document.getElementById(id);
|
|
457
|
+
if (el && el.classList.contains('cfg-field')) el.value = data.fields[id];
|
|
458
|
+
});
|
|
459
|
+
Object.keys(data.checks || {}).forEach(function (id) {
|
|
460
|
+
var el = document.getElementById(id);
|
|
461
|
+
if (el && el.type === 'checkbox') el.checked = !!data.checks[id];
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Re-enter the step the user left off on, but only if the draft still
|
|
465
|
+
// validates up to there — otherwise a saved bad value could strand them
|
|
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
|
+
|
|
471
|
+
restoring = false;
|
|
472
|
+
}
|
|
473
|
+
|
|
223
474
|
// ── actions ────────────────────────────────────────────────────────
|
|
224
475
|
|
|
225
476
|
function updatePreview() {
|
|
@@ -229,6 +480,18 @@
|
|
|
229
480
|
}
|
|
230
481
|
}
|
|
231
482
|
|
|
483
|
+
/** Activate a step button, updating stepper state alongside Bootstrap's. */
|
|
484
|
+
function showStep(btn) {
|
|
485
|
+
if (!btn || btn.disabled) return;
|
|
486
|
+
document.querySelectorAll('#wizardTabs .wizard-step').forEach(function (b) {
|
|
487
|
+
b.classList.remove('is-active');
|
|
488
|
+
});
|
|
489
|
+
btn.classList.add('is-active');
|
|
490
|
+
if (window.bootstrap && window.bootstrap.Tab) new bootstrap.Tab(btn).show();
|
|
491
|
+
refreshStepper();
|
|
492
|
+
if (btn.id === 'tab-review') renderReviewWarnings();
|
|
493
|
+
}
|
|
494
|
+
|
|
232
495
|
function downloadYAML() {
|
|
233
496
|
var yaml = buildYAML();
|
|
234
497
|
var blob = new Blob([yaml], { type: 'text/yaml;charset=utf-8' });
|
|
@@ -240,6 +503,9 @@
|
|
|
240
503
|
a.click();
|
|
241
504
|
document.body.removeChild(a);
|
|
242
505
|
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();
|
|
243
509
|
}
|
|
244
510
|
|
|
245
511
|
function copyYAML() {
|
|
@@ -257,40 +523,62 @@
|
|
|
257
523
|
// ── wiring ─────────────────────────────────────────────────────────
|
|
258
524
|
|
|
259
525
|
document.addEventListener('DOMContentLoaded', function () {
|
|
260
|
-
|
|
261
|
-
|
|
526
|
+
var wizard = document.getElementById('setup-wizard');
|
|
527
|
+
if (!wizard) return;
|
|
528
|
+
|
|
529
|
+
// Next / Prev buttons. Back is never blocked; Next is disabled by
|
|
530
|
+
// refreshStepper() when the step it points at has not unlocked.
|
|
531
|
+
wizard.querySelectorAll('.btn-next').forEach(function (btn) {
|
|
262
532
|
btn.addEventListener('click', function () {
|
|
263
|
-
|
|
264
|
-
var tab = document.getElementById(target);
|
|
265
|
-
if (tab) new bootstrap.Tab(tab).show();
|
|
266
|
-
updatePreview();
|
|
533
|
+
showStep(document.getElementById(btn.getAttribute('data-next')));
|
|
267
534
|
});
|
|
268
535
|
});
|
|
269
536
|
|
|
270
|
-
|
|
537
|
+
wizard.querySelectorAll('.btn-prev').forEach(function (btn) {
|
|
271
538
|
btn.addEventListener('click', function () {
|
|
272
|
-
|
|
273
|
-
var tab = document.getElementById(target);
|
|
274
|
-
if (tab) new bootstrap.Tab(tab).show();
|
|
539
|
+
showStep(document.getElementById(btn.getAttribute('data-prev')));
|
|
275
540
|
});
|
|
276
541
|
});
|
|
277
542
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
543
|
+
// Clicking a stepper entry directly.
|
|
544
|
+
wizard.querySelectorAll('#wizardTabs .wizard-step').forEach(function (btn) {
|
|
545
|
+
btn.addEventListener('click', function () { showStep(btn); });
|
|
546
|
+
});
|
|
547
|
+
|
|
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
|
+
wizard.addEventListener('input', function (event) {
|
|
551
|
+
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
|
+
if (el.classList && el.classList.contains('is-invalid')) validateField(el);
|
|
556
|
+
saveDraft();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
wizard.addEventListener('change', function () {
|
|
560
|
+
updatePreview();
|
|
561
|
+
refreshStepper();
|
|
562
|
+
saveDraft();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Validate email / URL on blur (capture: `blur` does not bubble).
|
|
566
|
+
wizard.addEventListener('blur', function (event) {
|
|
567
|
+
if (!event.target.classList || !event.target.classList.contains('cfg-field')) return;
|
|
568
|
+
validateField(event.target);
|
|
569
|
+
refreshStepper();
|
|
570
|
+
}, true);
|
|
283
571
|
|
|
284
572
|
// Description char count
|
|
285
573
|
var descField = document.getElementById('cfg-description');
|
|
286
574
|
var descCount = document.getElementById('desc-count');
|
|
287
575
|
if (descField && descCount) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
576
|
+
var syncCount = function () { descCount.textContent = descField.value.length; };
|
|
577
|
+
descField.addEventListener('input', syncCount);
|
|
578
|
+
descCount.textContent = descField.value.length;
|
|
291
579
|
}
|
|
292
580
|
|
|
293
|
-
// Download / Copy
|
|
581
|
+
// Download / Copy — always enabled, at every step.
|
|
294
582
|
var dlBtn = document.getElementById('btn-download');
|
|
295
583
|
if (dlBtn) dlBtn.addEventListener('click', downloadYAML);
|
|
296
584
|
|
|
@@ -300,8 +588,22 @@
|
|
|
300
588
|
var copyFullBtn = document.getElementById('btn-copy-full');
|
|
301
589
|
if (copyFullBtn) copyFullBtn.addEventListener('click', copyYAML);
|
|
302
590
|
|
|
303
|
-
//
|
|
591
|
+
// A queued draft write must not be lost to a reload or a tab close.
|
|
592
|
+
window.addEventListener('pagehide', flushDraft);
|
|
593
|
+
document.addEventListener('visibilitychange', function () {
|
|
594
|
+
if (document.visibilityState === 'hidden') flushDraft();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Order matters: restore values first so the initial preview and stepper
|
|
598
|
+
// state both reflect the real content.
|
|
599
|
+
restoreDraft();
|
|
600
|
+
if (descField && descCount) descCount.textContent = descField.value.length;
|
|
304
601
|
updatePreview();
|
|
602
|
+
refreshStepper();
|
|
603
|
+
|
|
604
|
+
// No pane measurement and no resize listener: the grid in
|
|
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.
|
|
305
607
|
});
|
|
306
608
|
|
|
307
609
|
})();
|
data/scripts/README.md
CHANGED
|
@@ -25,6 +25,10 @@ scripts/
|
|
|
25
25
|
│ ├── pixelate_images.py # Pure-stdlib pixelate/quantize engine
|
|
26
26
|
│ ├── install-preview-generator # Preview generator installer
|
|
27
27
|
│ └── validate_preview_urls.py # Preview URL validator
|
|
28
|
+
├── ci/ # Helpers called directly by workflow steps
|
|
29
|
+
│ ├── classify_changes.py # Change classifier (issue-pr-auto-merge)
|
|
30
|
+
│ ├── agent_review_result.py # Did the Claude content review actually run?
|
|
31
|
+
│ └── test_agent_review_result.py # …its tests (CI-run via test/lib bridge)
|
|
28
32
|
├── utils/ # Utility scripts
|
|
29
33
|
│ ├── analyze-commits # Commit analyzer for version bumps
|
|
30
34
|
│ ├── fix-markdown # Markdown formatting fixer
|
|
@@ -171,6 +175,31 @@ Validate preview image URLs in frontmatter.
|
|
|
171
175
|
python3 scripts/features/validate_preview_urls.py [--verbose] [--suggestions]
|
|
172
176
|
```
|
|
173
177
|
|
|
178
|
+
### CI Helpers (scripts/ci/)
|
|
179
|
+
|
|
180
|
+
#### `agent_review_result.py`
|
|
181
|
+
|
|
182
|
+
Decides whether the Claude tier of `ai-content-review.yml` **actually produced a review**, and fails the job when it did not.
|
|
183
|
+
|
|
184
|
+
Before this existed, the agent step captured the Claude CLI's exit status, echoed it, and then posted whatever landed on stdout as the review. When the OAuth credential was revoked, `Failed to authenticate. API Error: 401 OAuth access token has been revoked.` became the "review" and every job still reported success (issue #418) — for days, across several PRs, with nothing on the checks list to say the editorial review had stopped happening.
|
|
185
|
+
|
|
186
|
+
Note that a non-zero exit code would not have caught it: the CLI reported the auth failure on stdout. Three conditions are classified — a non-zero exit, no output at all, and a short output that leads with a known CLI failure signature. Any of them writes an explicit failure notice for the sticky comment, emits a `::error::` annotation, and exits 1.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
python3 scripts/ci/agent_review_result.py \
|
|
190
|
+
--status "$status" --stdout /tmp/agent-review.md \
|
|
191
|
+
--stderr /tmp/agent-err.log --out /tmp/agent-review-final.md
|
|
192
|
+
# exit 0 = the review is real · 1 = the tier failed · 2 = bad invocation
|
|
193
|
+
|
|
194
|
+
python3 scripts/ci/test_agent_review_result.py # tests, incl. the verbatim #418 output
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The tests run in CI through `scripts/test/lib/test_agent_review_result.sh`, which `run_tests.sh` sources and `./scripts/bin/test` executes on every PR.
|
|
198
|
+
|
|
199
|
+
#### `classify_changes.py`
|
|
200
|
+
|
|
201
|
+
Classifies a PR's changed files for `issue-pr-auto-merge.yml` (`--content-only`).
|
|
202
|
+
|
|
174
203
|
### Content Validation
|
|
175
204
|
|
|
176
205
|
#### `lint-pages`
|
data/scripts/bin/audit-consumer
CHANGED
|
@@ -118,14 +118,35 @@ done
|
|
|
118
118
|
|
|
119
119
|
CONSUMER_PATH="$(cd "$CONSUMER_PATH" && pwd)"
|
|
120
120
|
|
|
121
|
+
# In --format json, stdout must be PURE JSON so `jq` / `json.load` can consume
|
|
122
|
+
# it directly. The human-readable log lines are still useful, so send them to
|
|
123
|
+
# stderr rather than dropping them. (Left on stdout, they made every --format
|
|
124
|
+
# json run unparseable even after the trailing-comma fix.)
|
|
125
|
+
if [[ "$FORMAT" == "json" ]]; then
|
|
126
|
+
info() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
|
|
127
|
+
step() { echo -e "${CYAN}[STEP]${NC} $1" >&2; }
|
|
128
|
+
success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
|
|
129
|
+
warn() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
|
|
130
|
+
fi
|
|
131
|
+
|
|
121
132
|
# ---------------------------------------------------------------------------
|
|
122
133
|
step "Auditing consumer: $CONSUMER_PATH"
|
|
123
134
|
|
|
124
|
-
#
|
|
135
|
+
# CONSUMER_ROOT holds the config/override files; CONSUMER_PATH is where the
|
|
136
|
+
# theme-shadowing files actually live. They differ when the consumer sets
|
|
137
|
+
# Jekyll's `source:` (zer0-pages keeps its whole site tree under `pages/`).
|
|
138
|
+
CONSUMER_ROOT="$CONSUMER_PATH"
|
|
139
|
+
CONSUMER_SOURCE_REL=$(detect_consumer_source "$CONSUMER_ROOT")
|
|
140
|
+
if [[ -n "$CONSUMER_SOURCE_REL" && -d "$CONSUMER_ROOT/$CONSUMER_SOURCE_REL" ]]; then
|
|
141
|
+
CONSUMER_PATH="$CONSUMER_ROOT/$CONSUMER_SOURCE_REL"
|
|
142
|
+
info "Jekyll source: $CONSUMER_SOURCE_REL/ — scanning there, not the repo root"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Detect or confirm mode (from the repo root, where _config.yml lives)
|
|
125
146
|
if [[ -n "$MODE_OVERRIDE" ]]; then
|
|
126
147
|
MODE="$MODE_OVERRIDE"
|
|
127
148
|
else
|
|
128
|
-
MODE=$(detect_consumer_mode "$
|
|
149
|
+
MODE=$(detect_consumer_mode "$CONSUMER_ROOT")
|
|
129
150
|
info "Detected mode: $MODE"
|
|
130
151
|
fi
|
|
131
152
|
|
|
@@ -133,7 +154,7 @@ fi
|
|
|
133
154
|
if [[ -n "$THEME_PATH_OVERRIDE" ]]; then
|
|
134
155
|
RESOLVED_THEME="$THEME_PATH_OVERRIDE"
|
|
135
156
|
else
|
|
136
|
-
RESOLVED_THEME=$(resolve_theme_path "$MODE" "$
|
|
157
|
+
RESOLVED_THEME=$(resolve_theme_path "$MODE" "$CONSUMER_ROOT" "$THEME_ROOT")
|
|
137
158
|
fi
|
|
138
159
|
|
|
139
160
|
if [[ -z "$RESOLVED_THEME" || ! -d "$RESOLVED_THEME" ]]; then
|
|
@@ -149,7 +170,10 @@ if [[ ! -f "$MANIFEST" ]]; then
|
|
|
149
170
|
fi
|
|
150
171
|
|
|
151
172
|
# Load overrides manifest from consumer
|
|
152
|
-
|
|
173
|
+
# Declared overrides sit next to _config.yml at the repo root; fall back to the
|
|
174
|
+
# source dir so either placement works.
|
|
175
|
+
OVERRIDES_FILE="$CONSUMER_ROOT/.theme-overrides.yml"
|
|
176
|
+
[[ -f "$OVERRIDES_FILE" ]] || OVERRIDES_FILE="$CONSUMER_PATH/.theme-overrides.yml"
|
|
153
177
|
OVERRIDE_PATHS_TMP=$(mktemp)
|
|
154
178
|
trap 'rm -f "$OVERRIDE_PATHS_TMP"' EXIT
|
|
155
179
|
|
|
@@ -190,8 +214,14 @@ if [[ ${#THEMABLE_DIRS[@]} -eq 0 ]]; then
|
|
|
190
214
|
THEMABLE_DIRS=("_layouts" "_includes" "_sass" "assets/css" "assets/js")
|
|
191
215
|
fi
|
|
192
216
|
if [[ ${#REQUIRED_PLUGIN_FILES[@]} -eq 0 && ${#OPTIONAL_PLUGIN_FILES[@]} -eq 0 ]]; then
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
# No manifest at all. Nothing is genuinely required (GitHub Pages loads no
|
|
218
|
+
# local plugins), so every known plugin is offered as optional.
|
|
219
|
+
OPTIONAL_PLUGIN_FILES=(
|
|
220
|
+
"_plugins/obsidian_links.rb"
|
|
221
|
+
"_plugins/admin_page_urls.rb"
|
|
222
|
+
"_plugins/content_statistics_generator.rb"
|
|
223
|
+
"_plugins/theme_version.rb"
|
|
224
|
+
)
|
|
195
225
|
fi
|
|
196
226
|
|
|
197
227
|
# ---------------------------------------------------------------------------
|
|
@@ -316,7 +346,9 @@ fi
|
|
|
316
346
|
# ---------------------------------------------------------------------------
|
|
317
347
|
# Summary
|
|
318
348
|
# ---------------------------------------------------------------------------
|
|
319
|
-
|
|
349
|
+
# Entries are printed without a trailing newline (see print_classification),
|
|
350
|
+
# so close the array on its own line.
|
|
351
|
+
[[ "$FORMAT" == "json" ]] && printf '\n]}\n'
|
|
320
352
|
|
|
321
353
|
if [[ "$FORMAT" == "text" ]]; then
|
|
322
354
|
echo ""
|