jekyll-theme-zer0 1.30.0 → 1.31.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 +171 -0
- data/_data/backlog.yml +30 -7
- data/_data/consumers.yml +99 -0
- data/_data/features.yml +1 -1
- data/_data/theme-manifest.yml +35 -33
- data/_data/ui-text.yml +1 -0
- data/_includes/README.md +1 -1
- data/_includes/analytics/posthog.html +28 -9
- data/_includes/components/README.md +1 -1
- data/_includes/components/background-image.html +7 -1
- data/_includes/components/cookie-consent.html +30 -30
- data/_includes/components/mermaid.html +14 -7
- data/_includes/components/preview-image.html +22 -3
- data/_includes/components/theme-preview-gallery.html +1 -1
- data/_includes/content/intro.html +7 -0
- data/_includes/content/seo.html +8 -0
- data/_includes/content/sitemap.html +220 -263
- data/_includes/core/branding.html +8 -15
- data/_includes/core/head.html +29 -5
- data/_includes/core/header.html +14 -2
- data/_includes/core/tokens-inline.html +16 -1
- data/_includes/navigation/navbar.html +9 -6
- data/_includes/setup/wizard.html +5 -1
- data/_layouts/article.html +15 -3
- data/_layouts/home.html +13 -1
- data/_layouts/note.html +2 -1
- data/_layouts/notebook.html +2 -1
- data/_layouts/root.html +22 -0
- data/_sass/components/_cookie-banner.scss +48 -0
- data/_sass/components/_setup-wizard.scss +19 -1
- data/_sass/components/_ui-enhancements.scss +11 -0
- data/_sass/core/_navbar.scss +137 -70
- data/_sass/core/_sidebar-extras.scss +2 -14
- data/_sass/core/code-copy.scss +27 -8
- data/assets/css/extension-points-demo.css +27 -0
- data/assets/data/wiki-index.json +19 -0
- data/assets/js/auto-hide-nav.js +53 -13
- data/assets/js/code-copy.js +72 -1
- data/assets/js/extension-points-demo.js +48 -0
- data/assets/js/mermaid-diagrams.js +128 -22
- data/assets/js/obsidian-local-graph.js +27 -5
- data/assets/js/setup-wizard.js +7 -2
- data/scripts/ci/test_visual_evidence_autogen.py +64 -2
- data/scripts/ci/visual_evidence_autogen.py +14 -4
- data/scripts/features/install-preview-generator +1 -1
- data/scripts/lib/README.md +12 -0
- data/scripts/lib/preview_generator.py +2 -2
- metadata +4 -2
|
@@ -12,10 +12,21 @@
|
|
|
12
12
|
* that keeps the diagram source on the page instead of losing it.
|
|
13
13
|
*
|
|
14
14
|
* Loaded by _includes/components/mermaid.html — only on pages that opt in
|
|
15
|
-
* with `mermaid: true`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* with `mermaid: true`. The script is `defer`, so it runs once the DOM is
|
|
16
|
+
* parsed but BEFORE DOMContentLoaded: fences are converted before
|
|
17
|
+
* code-copy.js decorates <pre> blocks, so a diagram never gets a copy button
|
|
18
|
+
* and a line gutter.
|
|
19
|
+
*
|
|
20
|
+
* Lazy loading
|
|
21
|
+
* The Mermaid bundle (~1 MB minified) is NOT loaded by the include. This
|
|
22
|
+
* file converts the fences into figures (skeleton + toolbar) straight away,
|
|
23
|
+
* then injects the bundle only when the page really has a diagram AND the
|
|
24
|
+
* first one comes within LAZY_MARGIN of the viewport (IntersectionObserver).
|
|
25
|
+
* A `mermaid: true` page with no fences never downloads it, and a diagram
|
|
26
|
+
* far below the fold no longer costs the first paint. Once loaded, every
|
|
27
|
+
* figure renders. The bundle URL comes from window.zer0MermaidSrc (set by
|
|
28
|
+
* the include from site.mermaid.src). If a consumer still loads
|
|
29
|
+
* mermaid.min.js itself, the existing global is used and nothing is injected.
|
|
19
30
|
*
|
|
20
31
|
* Reads one JSON block injected by the include:
|
|
21
32
|
* #mermaidConfig — securityLevel, toolbar/fullscreen/download flags, labels
|
|
@@ -36,6 +47,7 @@
|
|
|
36
47
|
* mermaid.render(), so a theme switch re-renders from the original text.
|
|
37
48
|
*
|
|
38
49
|
* Public API (also what the Playwright spec drives):
|
|
50
|
+
* window.zer0Mermaid.load() -> Promise<mermaid> load the bundle now (no-op once loaded)
|
|
39
51
|
* window.zer0Mermaid.renderAll(root?) -> Promise<figure[]> convert + render fences under root
|
|
40
52
|
* window.zer0Mermaid.render(el, src?) -> Promise<figure> render one element (or replace it with src)
|
|
41
53
|
* window.zer0Mermaid.refresh() -> Promise<figure[]> re-derive the palette, re-render every figure
|
|
@@ -94,6 +106,32 @@
|
|
|
94
106
|
var SUPPORTS_DIALOG = typeof HTMLDialogElement !== "undefined" &&
|
|
95
107
|
typeof HTMLDialogElement.prototype.showModal === "function";
|
|
96
108
|
|
|
109
|
+
// How far below (or above) the viewport a diagram may be before the
|
|
110
|
+
// Mermaid bundle is fetched — enough lead time that a reader scrolling at a
|
|
111
|
+
// normal pace finds it rendered.
|
|
112
|
+
var LAZY_MARGIN = "400px 0px";
|
|
113
|
+
|
|
114
|
+
// Bundle URL: the include sets window.zer0MermaidSrc from site.mermaid.src
|
|
115
|
+
// (default /assets/vendor/mermaid/mermaid.min.js) through relative_url. It
|
|
116
|
+
// is a script global, not DOM text, because it ends up in script.src.
|
|
117
|
+
// Fallback for an overridden include that doesn't set it: the vendored path
|
|
118
|
+
// next to this file. Only http(s) URLs are ever injected.
|
|
119
|
+
var SELF_SRC = (document.currentScript && document.currentScript.src) || "";
|
|
120
|
+
var MERMAID_SRC = safeScriptUrl(
|
|
121
|
+
(typeof window.zer0MermaidSrc === "string" && window.zer0MermaidSrc) ||
|
|
122
|
+
(SELF_SRC ? SELF_SRC.replace(/js\/mermaid-diagrams\.js(\?.*)?$/, "vendor/mermaid/mermaid.min.js") : "")
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
function safeScriptUrl(raw) {
|
|
126
|
+
if (!raw) return "";
|
|
127
|
+
try {
|
|
128
|
+
var u = new URL(raw, document.baseURI);
|
|
129
|
+
return (u.protocol === "https:" || u.protocol === "http:") ? u.href : "";
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
97
135
|
var ZOOM_MIN = 0.5;
|
|
98
136
|
var ZOOM_MAX = 4;
|
|
99
137
|
var ZOOM_STEP = 1.25;
|
|
@@ -322,10 +360,11 @@
|
|
|
322
360
|
|
|
323
361
|
var ink = tokenColor("--bs-body-color", dark ? "#dee2e6" : "#212529", bgRgb);
|
|
324
362
|
var inkMuted = tokenColor("--bs-secondary-color", dark ? "#adb5bd" : "#6c757d", bgRgb);
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// buttons and links the reader sees
|
|
363
|
+
// Under a palette skin --bs-primary and --zer0-color-primary now agree
|
|
364
|
+
// (T-048: the skin beats `theme_color`). They still differ under the
|
|
365
|
+
// dark/contrast skins, where `theme_color.main` sets only the theme token.
|
|
366
|
+
// --bs-primary stays first because the buttons and links the reader sees
|
|
367
|
+
// are painted with it.
|
|
329
368
|
var primary = tokenColor("--bs-primary", "var(--zer0-color-primary, #007bff)", bgRgb);
|
|
330
369
|
var accent = tokenColor("--zer0-color-accent", "#ffe484", bgRgb);
|
|
331
370
|
// Keep the brand usable as a border/line colour on this background.
|
|
@@ -1185,7 +1224,11 @@
|
|
|
1185
1224
|
refreshTimer = setTimeout(function () { refresh(false); }, 80);
|
|
1186
1225
|
}
|
|
1187
1226
|
|
|
1227
|
+
var themeWatched = false;
|
|
1228
|
+
|
|
1188
1229
|
function watchTheme() {
|
|
1230
|
+
if (themeWatched) return;
|
|
1231
|
+
themeWatched = true;
|
|
1189
1232
|
if (typeof MutationObserver === "function") {
|
|
1190
1233
|
var observer = new MutationObserver(scheduleRefresh);
|
|
1191
1234
|
// `style` catches the Appearance panel writing token overrides onto
|
|
@@ -1203,19 +1246,77 @@
|
|
|
1203
1246
|
}
|
|
1204
1247
|
}
|
|
1205
1248
|
|
|
1249
|
+
// ---------------------------------------------------------------------
|
|
1250
|
+
// Lazy bundle loading
|
|
1251
|
+
// ---------------------------------------------------------------------
|
|
1252
|
+
var mermaidPromise = null;
|
|
1253
|
+
|
|
1254
|
+
function mermaidReady() {
|
|
1255
|
+
return !!(window.mermaid && typeof window.mermaid.render === "function");
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function markUnavailable() {
|
|
1259
|
+
// Vendor bundle missing: leave the fences as readable code blocks.
|
|
1260
|
+
document.documentElement.classList.add("zer0-diagram-unavailable");
|
|
1261
|
+
if (window.console && console.warn) console.warn("[zer0-mermaid] mermaid.min.js did not load; diagrams left as source.");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Inject the Mermaid bundle once; every caller shares the same promise.
|
|
1265
|
+
function loadMermaid() {
|
|
1266
|
+
if (mermaidReady()) return Promise.resolve(window.mermaid);
|
|
1267
|
+
if (mermaidPromise) return mermaidPromise;
|
|
1268
|
+
mermaidPromise = new Promise(function (resolve, reject) {
|
|
1269
|
+
if (!MERMAID_SRC) { reject(new Error("no Mermaid bundle URL")); return; }
|
|
1270
|
+
var script = document.createElement("script");
|
|
1271
|
+
script.src = MERMAID_SRC;
|
|
1272
|
+
script.async = true;
|
|
1273
|
+
script.setAttribute("data-zer0-mermaid", "");
|
|
1274
|
+
script.onload = function () {
|
|
1275
|
+
if (mermaidReady()) resolve(window.mermaid);
|
|
1276
|
+
else reject(new Error("mermaid global missing after load"));
|
|
1277
|
+
};
|
|
1278
|
+
script.onerror = function () { reject(new Error("failed to load " + MERMAID_SRC)); };
|
|
1279
|
+
document.head.appendChild(script);
|
|
1280
|
+
});
|
|
1281
|
+
mermaidPromise.catch(function () { mermaidPromise = null; });
|
|
1282
|
+
return mermaidPromise;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Resolve when the first of `figures` is within LAZY_MARGIN of the viewport.
|
|
1286
|
+
function whenNearViewport(figures) {
|
|
1287
|
+
return new Promise(function (resolve) {
|
|
1288
|
+
if (typeof window.IntersectionObserver !== "function") { resolve(); return; }
|
|
1289
|
+
var io = new IntersectionObserver(function (entries) {
|
|
1290
|
+
if (entries.some(function (e) { return e.isIntersecting; })) {
|
|
1291
|
+
io.disconnect();
|
|
1292
|
+
resolve();
|
|
1293
|
+
}
|
|
1294
|
+
}, { rootMargin: LAZY_MARGIN });
|
|
1295
|
+
figures.forEach(function (f) { io.observe(f); });
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1206
1299
|
// ---------------------------------------------------------------------
|
|
1207
1300
|
// Public API + boot
|
|
1208
1301
|
// ---------------------------------------------------------------------
|
|
1209
1302
|
function renderAll(root) {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1303
|
+
return loadMermaid().then(function () {
|
|
1304
|
+
initMermaid(false);
|
|
1305
|
+
watchTheme();
|
|
1306
|
+
var figures = collect(root || document);
|
|
1307
|
+
return renderMany(figures);
|
|
1308
|
+
}, function () { return []; });
|
|
1214
1309
|
}
|
|
1215
1310
|
|
|
1216
1311
|
function render(target, source) {
|
|
1217
|
-
|
|
1312
|
+
return loadMermaid().then(function () {
|
|
1313
|
+
return renderOne(target, source);
|
|
1314
|
+
}, function () { return null; });
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function renderOne(target, source) {
|
|
1218
1318
|
initMermaid(false);
|
|
1319
|
+
watchTheme();
|
|
1219
1320
|
var figure;
|
|
1220
1321
|
if (target && registry.has(target)) {
|
|
1221
1322
|
figure = target;
|
|
@@ -1237,9 +1338,10 @@
|
|
|
1237
1338
|
}
|
|
1238
1339
|
|
|
1239
1340
|
window.zer0Mermaid = {
|
|
1240
|
-
version: "3.
|
|
1341
|
+
version: "3.1.0",
|
|
1241
1342
|
config: CONFIG,
|
|
1242
1343
|
labels: LABELS,
|
|
1344
|
+
load: loadMermaid,
|
|
1243
1345
|
renderAll: renderAll,
|
|
1244
1346
|
render: render,
|
|
1245
1347
|
refresh: function () { return refresh(true); },
|
|
@@ -1252,14 +1354,18 @@
|
|
|
1252
1354
|
};
|
|
1253
1355
|
|
|
1254
1356
|
function boot() {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1357
|
+
// Fences become figures (skeleton + toolbar) now; Mermaid is not needed
|
|
1358
|
+
// for that. No diagram on the page → the bundle is never requested.
|
|
1359
|
+
var figures = collect(document);
|
|
1360
|
+
if (!figures.length) return;
|
|
1361
|
+
var start = mermaidReady() ? Promise.resolve() : whenNearViewport(figures);
|
|
1362
|
+
start
|
|
1363
|
+
.then(loadMermaid)
|
|
1364
|
+
.then(function () {
|
|
1365
|
+
initMermaid(false);
|
|
1366
|
+
watchTheme();
|
|
1367
|
+
return renderMany(allFigures.filter(function (f) { return f.isConnected; }));
|
|
1368
|
+
}, markUnavailable);
|
|
1263
1369
|
}
|
|
1264
1370
|
|
|
1265
1371
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Loaded by _includes/navigation/local-graph.html inside a dedicated
|
|
10
10
|
* collapsible side panel. Cytoscape.js is loaded lazily (and only once) from
|
|
11
|
-
* the vendored copy under assets/vendor/cytoscape/ (no CDN)
|
|
11
|
+
* the vendored copy under assets/vendor/cytoscape/ (no CDN), and only when
|
|
12
|
+
* the panel is first opened: the panel is a closed offcanvas on page load, so
|
|
13
|
+
* fetching ~118 KB of cytoscape for a graph nobody has asked to see was pure
|
|
14
|
+
* main-thread cost on every indexed page. Until then the accessible
|
|
15
|
+
* text list of linked pages is kept up to date.
|
|
12
16
|
*
|
|
13
17
|
* Subgraph:
|
|
14
18
|
* - center = current page (matched against entry.url, falling back to
|
|
@@ -592,6 +596,7 @@
|
|
|
592
596
|
syncControls(prefs);
|
|
593
597
|
|
|
594
598
|
var graphAvailable = null; // unknown until loadCytoscape resolves
|
|
599
|
+
var graphRequested = false;
|
|
595
600
|
|
|
596
601
|
function rebuild() {
|
|
597
602
|
var elements = buildSubgraph(entries, lookup, current, prefs.depth, {
|
|
@@ -603,6 +608,11 @@
|
|
|
603
608
|
// Accessible text fallback (also the graceful degradation if
|
|
604
609
|
// cytoscape can't load): a list of linked neighbours below the canvas.
|
|
605
610
|
var fallback = renderTextFallback(container, elements, current);
|
|
611
|
+
if (graphAvailable === null) {
|
|
612
|
+
// Panel not opened yet: cytoscape is not loaded, keep the list.
|
|
613
|
+
setStatus(container, nodeCount + ' pages · ' + edgeCount + ' links', false);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
606
616
|
if (graphAvailable === false) {
|
|
607
617
|
container.hidden = true;
|
|
608
618
|
setStatus(container, 'Showing linked pages (interactive graph unavailable).', false);
|
|
@@ -619,10 +629,22 @@
|
|
|
619
629
|
if (container.__obsidianLocalGraph) rebuild();
|
|
620
630
|
});
|
|
621
631
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
632
|
+
function requestGraph() {
|
|
633
|
+
if (graphRequested) return;
|
|
634
|
+
graphRequested = true;
|
|
635
|
+
setStatus(container, 'Loading graph…', false);
|
|
636
|
+
loadCytoscape(function (ok) {
|
|
637
|
+
graphAvailable = ok !== false;
|
|
638
|
+
rebuild();
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
rebuild(); // text list + counts, no cytoscape yet
|
|
643
|
+
if (!panel || panel.classList.contains('show')) {
|
|
644
|
+
requestGraph();
|
|
645
|
+
} else {
|
|
646
|
+
panel.addEventListener('show.bs.offcanvas', requestGraph);
|
|
647
|
+
}
|
|
626
648
|
})
|
|
627
649
|
.catch(function (err) {
|
|
628
650
|
// Sidebar panel failing is non-fatal — keep it hidden and stay quiet.
|
data/assets/js/setup-wizard.js
CHANGED
|
@@ -1981,10 +1981,15 @@
|
|
|
1981
1981
|
return data;
|
|
1982
1982
|
}
|
|
1983
1983
|
|
|
1984
|
+
// `is-visible` is the ONLY handle on the chip's appearance. It used to also
|
|
1985
|
+
// clear `hidden`, which the 2s timer below never restored (that timer only
|
|
1986
|
+
// drops `is-visible`, an opacity/visibility class) — so the first debounced
|
|
1987
|
+
// save moved the chip into layout permanently and shifted the whole wizard
|
|
1988
|
+
// down 7px (issue #265). _setup-wizard.scss reserves the box and hides it
|
|
1989
|
+
// with `visibility`, so there is nothing here to toggle but the class.
|
|
1984
1990
|
function showDraftChip() {
|
|
1985
1991
|
var chip = document.getElementById('wizard-draft-chip');
|
|
1986
1992
|
if (!chip) return;
|
|
1987
|
-
chip.hidden = false;
|
|
1988
1993
|
chip.classList.add('is-visible');
|
|
1989
1994
|
clearTimeout(chipTimer);
|
|
1990
1995
|
chipTimer = setTimeout(function () { chip.classList.remove('is-visible'); }, 2000);
|
|
@@ -2016,7 +2021,7 @@
|
|
|
2016
2021
|
draftTimer = null;
|
|
2017
2022
|
try { localStorage.removeItem(DRAFT_KEY); } catch (e) { /* ignore */ }
|
|
2018
2023
|
var chip = document.getElementById('wizard-draft-chip');
|
|
2019
|
-
if (chip) { chip.classList.remove('is-visible');
|
|
2024
|
+
if (chip) { chip.classList.remove('is-visible'); }
|
|
2020
2025
|
}
|
|
2021
2026
|
|
|
2022
2027
|
function restoreDraft() {
|
|
@@ -192,6 +192,25 @@ def test_parse_results() -> None:
|
|
|
192
192
|
f.get("diff") == "test/visual-results/output/a/homepage-sunrise-diff.png")
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
def test_parse_results_playwright_158_attachment_names() -> None:
|
|
196
|
+
print("parse_playwright_results — Playwright 1.58+ names attachments with their extension")
|
|
197
|
+
results = {"suites": [{"title": "appearance-snapshot.spec.js", "suites": [{"title": "Theme skins", "suites": [
|
|
198
|
+
{"title": "skin: air", "specs": [{"title": "homepage visual snapshot", "tests": [{"results": [
|
|
199
|
+
{"status": "failed", "error": {"message": "2009 pixels (ratio 0.01 of all image pixels) are different."},
|
|
200
|
+
"attachments": [
|
|
201
|
+
{"name": "homepage-air-expected.png", "path": "/work/test/visual-results/output/a/homepage-air-expected.png"},
|
|
202
|
+
{"name": "homepage-air-actual.png", "path": "/work/test/visual-results/output/a/homepage-air-actual.png"},
|
|
203
|
+
{"name": "homepage-air-diff.png", "path": "/work/test/visual-results/output/a/homepage-air-diff.png"}]}]}]}]},
|
|
204
|
+
]}]}]}
|
|
205
|
+
parsed = vea.parse_playwright_results(results)
|
|
206
|
+
f = parsed["failures"][0] if parsed["failures"] else {}
|
|
207
|
+
check("skin taken from the describe title", f.get("skin") == "air")
|
|
208
|
+
check("pixel count parsed", f.get("diff_px") == 2009)
|
|
209
|
+
check("expected resolved despite the .png suffix", f.get("expected") is not None)
|
|
210
|
+
check("actual resolved despite the .png suffix", f.get("actual") is not None)
|
|
211
|
+
check("diff resolved despite the .png suffix", f.get("diff") is not None)
|
|
212
|
+
|
|
213
|
+
|
|
195
214
|
def test_decide() -> None:
|
|
196
215
|
print("decide — only `intentional` on a failing verify may bless")
|
|
197
216
|
fail = {"snapshots": {"verified": "fail"}}
|
|
@@ -259,6 +278,47 @@ def test_stage_command_output() -> None:
|
|
|
259
278
|
check("one path per line when there is something to add", buf.getvalue() == "test/visual/evidence/x\n")
|
|
260
279
|
|
|
261
280
|
|
|
281
|
+
def test_generic_generator_survives_livereload() -> None:
|
|
282
|
+
"""Run 36932006025 (#495): pr-evidence.mjs died at its third width with
|
|
283
|
+
"page.$: Execution context was destroyed" and wrote no metrics or PNGs.
|
|
284
|
+
|
|
285
|
+
The jammy run's `npm ci` created node_modules/ AFTER `jekyll serve --watch`
|
|
286
|
+
started; jekyll-watch ignores only the `exclude:` paths that exist when it
|
|
287
|
+
starts, so Jekyll regenerated and LiveReload reloaded the open page mid-shot.
|
|
288
|
+
Pinned from both ends: the watcher must ignore node_modules/, and the
|
|
289
|
+
generator must neither listen to LiveReload nor let one shot end the sweep.
|
|
290
|
+
"""
|
|
291
|
+
print("generic generator — a mid-sweep LiveReload never aborts the sweep")
|
|
292
|
+
src = (REPO_ROOT / vea.GENERIC_GENERATOR).read_text(encoding="utf-8")
|
|
293
|
+
check("pr-evidence.mjs aborts the injected livereload.js",
|
|
294
|
+
re.search(r"const LIVERELOAD_URL_RE = /.*livereload", src) is not None
|
|
295
|
+
and "page.route(LIVERELOAD_URL_RE, (r) => r.abort())" in src)
|
|
296
|
+
unguarded = [ln.strip() for ln in src.splitlines()
|
|
297
|
+
if re.search(r"await page\.(\$|\$\$|evaluate)\(", ln) and ".catch(" not in ln]
|
|
298
|
+
check(f"every page-context call in pr-evidence.mjs is guarded ({unguarded or 'none unguarded'})",
|
|
299
|
+
not unguarded)
|
|
300
|
+
|
|
301
|
+
import contextlib
|
|
302
|
+
import io
|
|
303
|
+
import os
|
|
304
|
+
import tempfile
|
|
305
|
+
calls: list[tuple[str, bool]] = []
|
|
306
|
+
real_run, real_http_ok, cwd = vea.run, vea.http_ok, os.getcwd()
|
|
307
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
308
|
+
try:
|
|
309
|
+
os.chdir(tmp)
|
|
310
|
+
vea.http_ok = lambda url, timeout=3.0: any(c[0].startswith("docker compose up") for c in calls)
|
|
311
|
+
vea.run = lambda cmd, **kw: calls.append((" ".join(cmd), Path("node_modules").is_dir()))
|
|
312
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
313
|
+
vea.ensure_jekyll(timeout=5)
|
|
314
|
+
finally:
|
|
315
|
+
vea.run, vea.http_ok = real_run, real_http_ok
|
|
316
|
+
os.chdir(cwd)
|
|
317
|
+
up = [had for cmd, had in calls if cmd.startswith("docker compose up")]
|
|
318
|
+
check("ensure_jekyll creates node_modules/ BEFORE `docker compose up` (so the watcher ignores it)",
|
|
319
|
+
up == [True])
|
|
320
|
+
|
|
321
|
+
|
|
262
322
|
def test_lane_tooling_contract() -> None:
|
|
263
323
|
"""The regression that killed the lane's first real run on a real PR.
|
|
264
324
|
|
|
@@ -328,8 +388,10 @@ def test_workflow_wiring() -> None:
|
|
|
328
388
|
def main() -> int:
|
|
329
389
|
for t in (test_detect_slug, test_plan_pr_454_shape, test_plan_loop_guard_and_budget, test_plan_generic_fallback,
|
|
330
390
|
test_plan_respects_author_evidence, test_plan_out_of_scope, test_styling_matches_ci_filter,
|
|
331
|
-
test_ui_prefixes_match_gate, test_parse_results,
|
|
332
|
-
|
|
391
|
+
test_ui_prefixes_match_gate, test_parse_results, test_parse_results_playwright_158_attachment_names,
|
|
392
|
+
test_decide, test_stage_and_messages,
|
|
393
|
+
test_stage_command_output, test_generic_generator_survives_livereload,
|
|
394
|
+
test_lane_tooling_contract, test_workflow_wiring):
|
|
333
395
|
t()
|
|
334
396
|
print(f"\n{PASSED} passed, {len(FAILURES)} failed")
|
|
335
397
|
for f in FAILURES:
|
|
@@ -400,13 +400,18 @@ def parse_playwright_results(results: dict) -> dict:
|
|
|
400
400
|
m = re.search(r"(\d+) pixels", message)
|
|
401
401
|
exp = act = dif = None
|
|
402
402
|
for name, p in attachments.items():
|
|
403
|
-
|
|
403
|
+
# Playwright 1.58 names screenshot attachments with their
|
|
404
|
+
# file extension (e.g. "homepage-air-expected.png"); older
|
|
405
|
+
# versions did not. Strip it before matching the suffix so
|
|
406
|
+
# both forms resolve to the same three roles.
|
|
407
|
+
stem = re.sub(r"\.(png|jpe?g)$", "", name)
|
|
408
|
+
if stem.endswith("-expected"):
|
|
404
409
|
exp = rel_path(p)
|
|
405
|
-
elif
|
|
410
|
+
elif stem.endswith("-actual"):
|
|
406
411
|
act = rel_path(p)
|
|
407
|
-
elif
|
|
412
|
+
elif stem.endswith("-diff"):
|
|
408
413
|
dif = rel_path(p)
|
|
409
|
-
skin = skin or
|
|
414
|
+
skin = skin or stem[: -len("-diff")].replace("homepage-", "")
|
|
410
415
|
failures.append({
|
|
411
416
|
"skin": skin or spec.get("title", "?"),
|
|
412
417
|
"status": status,
|
|
@@ -447,6 +452,11 @@ def ensure_jekyll(timeout: int) -> bool:
|
|
|
447
452
|
if http_ok(HEAD_URL + "/"):
|
|
448
453
|
log(f"Jekyll already serving {HEAD_URL} — reusing it.")
|
|
449
454
|
return False
|
|
455
|
+
# jekyll-watch ignores only the `exclude:` paths that EXIST when its watcher
|
|
456
|
+
# starts. On a fresh checkout node_modules/ does not, so the jammy run's
|
|
457
|
+
# `npm ci` would set off a regeneration whose LiveReload reloads every open
|
|
458
|
+
# page mid-shot (the pr-evidence.mjs crash on #495). Create it first.
|
|
459
|
+
Path("node_modules").mkdir(exist_ok=True)
|
|
450
460
|
log("Starting Jekyll via docker compose…")
|
|
451
461
|
run(["docker", "compose", "up", "-d"], capture=False)
|
|
452
462
|
log(f"Waiting up to {timeout}s for {HEAD_URL} (cold bundle install + first build)…")
|
|
@@ -262,7 +262,7 @@ update_config() {
|
|
|
262
262
|
preview_images:
|
|
263
263
|
enabled: true
|
|
264
264
|
provider: ${DEFAULT_PROVIDER} # renderer: openai, xai, stability, gemini, local
|
|
265
|
-
model: "" # empty = renderer default (gpt-image-2, grok-
|
|
265
|
+
model: "" # empty = renderer default (gpt-image-2, grok-imagine-image-2.0, ...)
|
|
266
266
|
size: "1536x1024" # Landscape banner size
|
|
267
267
|
quality: auto # auto for GPT Image; standard/hd for DALL-E 3
|
|
268
268
|
style: "retro pixel art, 8-bit video game aesthetic, vibrant colors, nostalgic, clean pixel graphics"
|
data/scripts/lib/README.md
CHANGED
|
@@ -153,6 +153,18 @@ publish_gem "1.2.0"
|
|
|
153
153
|
create_github_release "1.2.0"
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
### 🖼️ `preview_generator.py` - Preview Image Engine
|
|
157
|
+
|
|
158
|
+
The Python engine behind `scripts/generate-preview-images.sh`. Renderer defaults when `preview_images.model` is empty:
|
|
159
|
+
|
|
160
|
+
| provider | default model | credential |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `openai` (default) | `gpt-image-2` | `OPENAI_API_KEY` |
|
|
163
|
+
| `xai` | `grok-imagine-image-2.0` | `XAI_API_KEY` |
|
|
164
|
+
| `gemini` | `gemini-2.5-flash-image` | `GEMINI_API_KEY` |
|
|
165
|
+
|
|
166
|
+
Unit specs: `python3 test/test_preview_generator.py`.
|
|
167
|
+
|
|
156
168
|
## Testing
|
|
157
169
|
|
|
158
170
|
Each library has comprehensive unit tests in `scripts/test/lib/`.
|
|
@@ -24,7 +24,7 @@ Architecture — Claude ORCHESTRATES, an image model RENDERS:
|
|
|
24
24
|
-------- -------------------------------------- ----------------------------------
|
|
25
25
|
openai gpt-image-2 / dall-e-3 (+ --enhance OPENAI_API_KEY
|
|
26
26
|
(default) via /v1/images/edits)
|
|
27
|
-
xai grok-
|
|
27
|
+
xai grok-imagine-image-2.0 XAI_API_KEY
|
|
28
28
|
stability Stable Diffusion XL (v1 API) STABILITY_API_KEY
|
|
29
29
|
gemini gemini-2.5-flash-image GEMINI_API_KEY
|
|
30
30
|
local deterministic template SVG → PNG none (CI-safe; skips
|
|
@@ -1688,7 +1688,7 @@ class XAIProvider(Provider):
|
|
|
1688
1688
|
return "XAI_API_KEY environment variable is required for the xAI provider"
|
|
1689
1689
|
|
|
1690
1690
|
def default_model(self) -> str:
|
|
1691
|
-
return "grok-
|
|
1691
|
+
return "grok-imagine-image-2.0"
|
|
1692
1692
|
|
|
1693
1693
|
def generate(self, prompt, settings, out_base, ctx) -> ImageResult:
|
|
1694
1694
|
model = effective_model(settings, self)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-theme-zer0
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amr Abdel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -345,6 +345,7 @@ files:
|
|
|
345
345
|
- _sass/utilities/_layout.scss
|
|
346
346
|
- _sass/utilities/_motion.scss
|
|
347
347
|
- assets/css/custom.css
|
|
348
|
+
- assets/css/extension-points-demo.css
|
|
348
349
|
- assets/css/main.scss
|
|
349
350
|
- assets/css/stats.css
|
|
350
351
|
- assets/css/theme-npm-entry.scss
|
|
@@ -362,6 +363,7 @@ files:
|
|
|
362
363
|
- assets/js/code-copy.js
|
|
363
364
|
- assets/js/config-utility.js
|
|
364
365
|
- assets/js/docs.min.js
|
|
366
|
+
- assets/js/extension-points-demo.js
|
|
365
367
|
- assets/js/fleet-feedback-capture.js
|
|
366
368
|
- assets/js/fleet-feedback.js
|
|
367
369
|
- assets/js/halfmoon.js
|