similarbuild 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +301 -0
  4. package/bin/install.js +256 -0
  5. package/lib/copy-templates.mjs +52 -0
  6. package/lib/install-deps.mjs +62 -0
  7. package/lib/prompt-config.mjs +83 -0
  8. package/lib/verify-env.mjs +19 -0
  9. package/package.json +63 -0
  10. package/scripts/sync-templates.mjs +71 -0
  11. package/templates/commands/build-page.md +490 -0
  12. package/templates/commands/build-site.md +548 -0
  13. package/templates/commands/clip-section.md +519 -0
  14. package/templates/memory/anti-patterns.md +212 -0
  15. package/templates/memory/design-knowledge.md +225 -0
  16. package/templates/memory/fixes.md +163 -0
  17. package/templates/memory/patterns.md +681 -0
  18. package/templates/presets/shopify-section.yaml +51 -0
  19. package/templates/presets/wp-elementor.yaml +49 -0
  20. package/templates/reports/fixtures/mock-run-1.json +115 -0
  21. package/templates/reports/fixtures/mock-run-2.json +72 -0
  22. package/templates/reports/report-renderer.mjs +218 -0
  23. package/templates/reports/report-template.html +571 -0
  24. package/templates/skills/sb-build-shopify/SKILL.md +104 -0
  25. package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
  26. package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
  27. package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
  28. package/templates/skills/sb-build-wp/SKILL.md +83 -0
  29. package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
  30. package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
  31. package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
  32. package/templates/skills/sb-compare-visual/SKILL.md +121 -0
  33. package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
  34. package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
  35. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
  36. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
  37. package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
  38. package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
  39. package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
  40. package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
  41. package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
  42. package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
  43. package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
  44. package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
  45. package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
  46. package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
  47. package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
  48. package/templates/skills/sb-extract-assets/SKILL.md +112 -0
  49. package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
  50. package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
  51. package/templates/skills/sb-inspect-live/SKILL.md +105 -0
  52. package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
  53. package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
  54. package/templates/skills/sb-review-checks/SKILL.md +113 -0
  55. package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
  56. package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
  57. package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
  58. package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
  59. package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
  60. package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
  61. package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
  62. package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
  63. package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
  64. package/templates/skills/sb-tweak/SKILL.md +130 -0
  65. package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
  66. package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
  67. package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
  68. package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
  69. package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
  70. package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
  71. package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
  72. package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
  73. package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
  74. package/templates/skills/sb-validate-render/SKILL.md +120 -0
  75. package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
  76. package/templates/skills/sb-validate-render/scripts/validate-render.mjs +645 -0
@@ -0,0 +1,571 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>{{PROJECT_TITLE}} — SimilarBuild report</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f1115;
10
+ --surface: #161922;
11
+ --surface-2: #1d212c;
12
+ --border: #2a3040;
13
+ --text: #e6e9ef;
14
+ --muted: #9aa3b2;
15
+ --accent: #6ea8ff;
16
+ --ok: #4ade80;
17
+ --warn: #fbbf24;
18
+ --fail: #f87171;
19
+ --diff: #ff5c7a;
20
+ --shadow: 0 6px 24px rgba(0,0,0,.35);
21
+ --radius: 10px;
22
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
23
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
24
+ }
25
+ @media (prefers-color-scheme: light) {
26
+ :root {
27
+ --bg: #f6f7fa;
28
+ --surface: #fff;
29
+ --surface-2: #f1f3f8;
30
+ --border: #d8dde7;
31
+ --text: #1a1d24;
32
+ --muted: #5a6271;
33
+ --accent: #2563eb;
34
+ }
35
+ }
36
+ * { box-sizing: border-box; }
37
+ html, body { margin: 0; padding: 0; }
38
+ body {
39
+ background: var(--bg);
40
+ color: var(--text);
41
+ font-family: var(--sans);
42
+ font-size: 14px;
43
+ line-height: 1.5;
44
+ -webkit-font-smoothing: antialiased;
45
+ }
46
+ .skip-link {
47
+ position: absolute; left: -9999px; top: 0;
48
+ background: var(--accent); color: #fff; padding: 8px 12px; border-radius: 0 0 6px 0;
49
+ }
50
+ .skip-link:focus { left: 0; }
51
+ .wrap { max-width: 1200px; margin: 0 auto; padding: 24px 20px 80px; }
52
+
53
+ header.report-header { padding: 24px 0 16px; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
54
+ header.report-header h1 { font-size: 22px; margin: 0 0 6px; font-weight: 600; }
55
+ header.report-header .subtitle { color: var(--muted); font-size: 13px; margin: 0; }
56
+ header.report-header .subtitle a { color: var(--accent); text-decoration: none; }
57
+ header.report-header .subtitle a:hover { text-decoration: underline; }
58
+
59
+ .stats { display: flex; flex-wrap: wrap; gap: 12px; margin: 18px 0 0; }
60
+ .stat {
61
+ background: var(--surface); border: 1px solid var(--border);
62
+ border-radius: var(--radius); padding: 10px 14px; min-width: 110px;
63
+ }
64
+ .stat .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
65
+ .stat .value { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
66
+ .stat.ok .value { color: var(--ok); }
67
+ .stat.warn .value { color: var(--warn); }
68
+ .stat.fail .value { color: var(--fail); }
69
+
70
+ h2.section-title { font-size: 15px; font-weight: 600; margin: 28px 0 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
71
+
72
+ table.pages {
73
+ width: 100%; border-collapse: separate; border-spacing: 0;
74
+ background: var(--surface); border: 1px solid var(--border);
75
+ border-radius: var(--radius); overflow: hidden;
76
+ }
77
+ table.pages thead th {
78
+ text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
79
+ color: var(--muted); padding: 10px 12px; background: var(--surface-2);
80
+ border-bottom: 1px solid var(--border); font-weight: 500;
81
+ }
82
+ table.pages tbody tr.row-summary { cursor: pointer; }
83
+ table.pages tbody tr.row-summary:hover { background: var(--surface-2); }
84
+ table.pages tbody tr.row-summary.expanded { background: var(--surface-2); }
85
+ table.pages tbody td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
86
+ table.pages tbody tr:last-child td { border-bottom: 0; }
87
+ td.col-status { width: 90px; }
88
+ td.col-type { width: 90px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
89
+ td.col-diff { width: 80px; text-align: right; font-variant-numeric: tabular-nums; font-family: var(--mono); }
90
+ td.col-iter { width: 60px; text-align: right; font-variant-numeric: tabular-nums; font-family: var(--mono); color: var(--muted); }
91
+ td.col-viol { width: 90px; text-align: right; font-variant-numeric: tabular-nums; }
92
+ td.col-page a { color: var(--text); text-decoration: none; }
93
+ td.col-page a:hover { color: var(--accent); }
94
+ td.col-page .url { color: var(--muted); font-size: 12px; display: block; max-width: 480px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
95
+ .badge {
96
+ display: inline-block; padding: 3px 8px; border-radius: 999px;
97
+ font-size: 11px; font-weight: 600; letter-spacing: .03em;
98
+ }
99
+ .badge.ok { background: rgba(74,222,128,.14); color: var(--ok); }
100
+ .badge.warn { background: rgba(251,191,36,.14); color: var(--warn); }
101
+ .badge.fail { background: rgba(248,113,113,.14); color: var(--fail); }
102
+ .badge.sev-high { background: rgba(248,113,113,.14); color: var(--fail); }
103
+ .badge.sev-med, .badge.sev-medium { background: rgba(251,191,36,.14); color: var(--warn); }
104
+ .badge.sev-low { background: rgba(154,163,178,.14); color: var(--muted); }
105
+
106
+ tr.row-detail td { padding: 0 !important; background: var(--bg); border-bottom: 1px solid var(--border); }
107
+ .detail-panel { padding: 18px; display: grid; gap: 18px; grid-template-columns: minmax(0,1fr); }
108
+ @media (min-width: 920px) { .detail-panel { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); } }
109
+ .detail-block { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; }
110
+ .detail-block h3 { margin: 0 0 10px; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 600; }
111
+ .detail-block.full-span { grid-column: 1 / -1; }
112
+
113
+ /* Slider */
114
+ .slider {
115
+ position: relative; width: 100%; aspect-ratio: 16 / 10; max-height: 70vh;
116
+ background: #000; overflow: hidden; border-radius: 6px;
117
+ --clip: 50%;
118
+ }
119
+ .slider img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; user-select: none; -webkit-user-drag: none; }
120
+ .slider .build { z-index: 1; }
121
+ .slider .live { z-index: 2; clip-path: inset(0 calc(100% - var(--clip)) 0 0); }
122
+ .slider .handle {
123
+ position: absolute; top: 0; bottom: 0; width: 2px; background: var(--accent);
124
+ left: var(--clip); transform: translateX(-50%); z-index: 3; pointer-events: none;
125
+ box-shadow: 0 0 0 1px rgba(0,0,0,.4);
126
+ }
127
+ .slider .handle::after {
128
+ content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
129
+ width: 26px; height: 26px; border-radius: 50%; background: var(--accent);
130
+ box-shadow: 0 2px 8px rgba(0,0,0,.4);
131
+ }
132
+ .slider .label {
133
+ position: absolute; top: 8px; padding: 3px 8px; background: rgba(0,0,0,.55); color: #fff;
134
+ font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
135
+ border-radius: 4px; z-index: 4; pointer-events: none;
136
+ }
137
+ .slider .label.left { left: 8px; }
138
+ .slider .label.right { right: 8px; }
139
+ .slider .input-range {
140
+ position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: ew-resize;
141
+ margin: 0; appearance: none; background: transparent; z-index: 5;
142
+ }
143
+ .slider .input-range:focus-visible + .focus-ring {
144
+ position: absolute; inset: 0; outline: 2px solid var(--accent); outline-offset: -3px; border-radius: 6px; pointer-events: none; z-index: 4;
145
+ }
146
+ .slider-help { color: var(--muted); font-size: 11px; margin-top: 6px; }
147
+
148
+ .diff-image { display: block; width: 100%; max-height: 70vh; object-fit: contain; background: #000; border-radius: 6px; }
149
+
150
+ table.kv {
151
+ width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12px;
152
+ }
153
+ table.kv td { padding: 5px 8px; border-bottom: 1px solid var(--border); }
154
+ table.kv td:first-child { color: var(--muted); width: 40%; word-break: break-all; }
155
+ table.kv td.equal { color: var(--muted); }
156
+ table.kv td.changed { color: var(--diff); }
157
+ table.kv tr:last-child td { border-bottom: 0; }
158
+
159
+ .violations-list, .diffs-list, .fixes-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
160
+ .violations-list li, .diffs-list li, .fixes-list li {
161
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px;
162
+ font-size: 12px; line-height: 1.45;
163
+ }
164
+ .violations-list .vid, .diffs-list .key { font-family: var(--mono); color: var(--muted); margin-right: 8px; }
165
+ .violations-list .vmsg { display: block; margin-top: 4px; color: var(--text); }
166
+ .empty { color: var(--muted); font-size: 12px; font-style: italic; margin: 4px 0 0; }
167
+ .meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12px; color: var(--muted); margin-top: 8px; }
168
+ .meta a { color: var(--accent); text-decoration: none; }
169
+ .meta a:hover { text-decoration: underline; }
170
+
171
+ details.previous-run {
172
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 8px;
173
+ }
174
+ details.previous-run > summary {
175
+ cursor: pointer; padding: 12px 14px; list-style: none; display: flex; justify-content: space-between; gap: 12px; align-items: center;
176
+ }
177
+ details.previous-run > summary::-webkit-details-marker { display: none; }
178
+ details.previous-run > summary::before {
179
+ content: "▸"; color: var(--muted); display: inline-block; transition: transform .15s; margin-right: 6px;
180
+ }
181
+ details.previous-run[open] > summary::before { transform: rotate(90deg); }
182
+ details.previous-run .prev-body { padding: 0 14px 14px; }
183
+ details.previous-run .prev-summary-line { display: flex; flex-wrap: wrap; gap: 8px 14px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
184
+ details.previous-run .prev-summary-line strong { color: var(--text); }
185
+
186
+ footer.report-footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
187
+ footer.report-footer details { margin-top: 8px; }
188
+ footer.report-footer pre { font-family: var(--mono); font-size: 11px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px; overflow: auto; max-height: 280px; }
189
+
190
+ .row-summary[aria-expanded="true"] td.col-page::before { content: "▾ "; color: var(--muted); }
191
+ .row-summary[aria-expanded="false"] td.col-page::before { content: "▸ "; color: var(--muted); }
192
+ </style>
193
+ </head>
194
+ <body>
195
+ <a href="#main" class="skip-link">Skip to report</a>
196
+ <div class="wrap" id="main">
197
+ <header class="report-header">
198
+ <h1 id="report-title">Loading…</h1>
199
+ <p class="subtitle" id="report-subtitle"></p>
200
+ <div class="stats" id="report-stats" role="group" aria-label="Run totals"></div>
201
+ </header>
202
+
203
+ <h2 class="section-title">Pages</h2>
204
+ <table class="pages" id="pages-table" aria-describedby="pages-help">
205
+ <thead>
206
+ <tr>
207
+ <th>Status</th>
208
+ <th>Type</th>
209
+ <th>Page</th>
210
+ <th style="text-align:right">Diff%</th>
211
+ <th style="text-align:right">Iter</th>
212
+ <th style="text-align:right">Viol</th>
213
+ </tr>
214
+ </thead>
215
+ <tbody id="pages-body"></tbody>
216
+ </table>
217
+ <p id="pages-help" class="slider-help">Click a row to expand the side-by-side comparison and details.</p>
218
+
219
+ <section id="previous-runs-section" hidden>
220
+ <h2 class="section-title">Previous runs</h2>
221
+ <div id="previous-runs-list"></div>
222
+ </section>
223
+
224
+ <footer class="report-footer">
225
+ <div id="footer-meta"></div>
226
+ <details>
227
+ <summary>Config snapshot</summary>
228
+ <pre id="config-snapshot"></pre>
229
+ </details>
230
+ </footer>
231
+ </div>
232
+
233
+ <script type="application/json" id="sb-report-data">{{REPORT_DATA_JSON}}</script>
234
+ <script>
235
+ (function () {
236
+ "use strict";
237
+ var DATA;
238
+ try {
239
+ DATA = JSON.parse(document.getElementById("sb-report-data").textContent);
240
+ } catch (e) {
241
+ document.getElementById("report-title").textContent = "Failed to parse report data";
242
+ document.getElementById("report-subtitle").textContent = String(e && e.message || e);
243
+ return;
244
+ }
245
+
246
+ var $ = function (sel, root) { return (root || document).querySelector(sel); };
247
+ var el = function (tag, attrs, children) {
248
+ var n = document.createElement(tag);
249
+ if (attrs) for (var k in attrs) {
250
+ if (k === "class") n.className = attrs[k];
251
+ else if (k === "text") n.textContent = attrs[k];
252
+ else if (k === "html") n.innerHTML = attrs[k];
253
+ else if (k.indexOf("on") === 0 && typeof attrs[k] === "function") n.addEventListener(k.slice(2), attrs[k]);
254
+ else if (attrs[k] != null) n.setAttribute(k, attrs[k]);
255
+ }
256
+ if (children) (Array.isArray(children) ? children : [children]).forEach(function (c) {
257
+ if (c == null) return;
258
+ n.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
259
+ });
260
+ return n;
261
+ };
262
+
263
+ function fmtMs(ms) {
264
+ if (!ms && ms !== 0) return "—";
265
+ if (ms < 1000) return ms + "ms";
266
+ var s = ms / 1000;
267
+ if (s < 60) return s.toFixed(1) + "s";
268
+ var m = Math.floor(s / 60); var r = Math.round(s - m * 60);
269
+ return m + "m " + r + "s";
270
+ }
271
+ function fmtPct(n) {
272
+ if (n == null || isNaN(n)) return "—";
273
+ return Number(n).toFixed(2) + "%";
274
+ }
275
+ function statusToken(s) {
276
+ s = String(s || "").toLowerCase();
277
+ if (s === "ok" || s === "pass" || s === "passed" || s === "✅") return "ok";
278
+ if (s === "fail" || s === "failed" || s === "❌") return "fail";
279
+ return "warn";
280
+ }
281
+ function statusLabel(tok) {
282
+ return tok === "ok" ? "✅ ok" : tok === "fail" ? "❌ fail" : "⚠️ warn";
283
+ }
284
+
285
+ // ---- Header ----
286
+ var run = DATA.currentRun || {};
287
+ var totals = run.totals || { ok: 0, warn: 0, fail: 0 };
288
+ $("#report-title").textContent =
289
+ "SimilarBuild report — " + (DATA.projectSlug || "(unnamed project)");
290
+ var sub = $("#report-subtitle");
291
+ sub.appendChild(document.createTextNode("Root: "));
292
+ if (DATA.rootUrl) sub.appendChild(el("a", { href: DATA.rootUrl, target: "_blank", rel: "noreferrer noopener", text: DATA.rootUrl }));
293
+ else sub.appendChild(document.createTextNode("(unknown)"));
294
+ sub.appendChild(document.createTextNode(" · target: " + (DATA.target || "—") + " · run: " + (run.timestamp || "—")));
295
+
296
+ var stats = $("#report-stats");
297
+ function statCard(cls, label, value) {
298
+ return el("div", { class: "stat " + cls }, [
299
+ el("div", { class: "label", text: label }),
300
+ el("div", { class: "value", text: String(value) }),
301
+ ]);
302
+ }
303
+ stats.appendChild(statCard("ok", "Ok", totals.ok || 0));
304
+ stats.appendChild(statCard("warn", "Warn", totals.warn || 0));
305
+ stats.appendChild(statCard("fail", "Fail", totals.fail || 0));
306
+ stats.appendChild(statCard("", "Pages", (totals.ok||0) + (totals.warn||0) + (totals.fail||0)));
307
+ stats.appendChild(statCard("", "Iters", run.totalIterations || 0));
308
+ stats.appendChild(statCard("", "Time", fmtMs(run.durationMs || 0)));
309
+
310
+ // ---- Pages table ----
311
+ var tbody = $("#pages-body");
312
+ (run.pageResults || []).forEach(function (page, idx) {
313
+ var tok = statusToken(page.status);
314
+ var rowId = "row-" + idx;
315
+ var detailId = "detail-" + idx;
316
+
317
+ var summary = el("tr", { class: "row-summary", "data-idx": idx, id: rowId, role: "button", tabindex: "0", "aria-expanded": "false", "aria-controls": detailId }, [
318
+ el("td", { class: "col-status" }, el("span", { class: "badge " + tok, text: statusLabel(tok) })),
319
+ el("td", { class: "col-type", text: page.type || "—" }),
320
+ el("td", { class: "col-page" }, [
321
+ el("strong", { text: page.slug || page.url || "(unnamed)" }),
322
+ el("span", { class: "url", text: page.url || "" }),
323
+ ]),
324
+ el("td", { class: "col-diff", text: fmtPct(page.diffPercent) }),
325
+ el("td", { class: "col-iter", text: String(page.iterations || 0) }),
326
+ el("td", { class: "col-viol", text: String((page.violations || []).length) }),
327
+ ]);
328
+
329
+ var detail = el("tr", { class: "row-detail", id: detailId, hidden: "" }, el("td", { colspan: "6" }));
330
+ var panel = renderDetail(page, idx);
331
+ detail.firstChild.appendChild(panel);
332
+
333
+ function toggle() {
334
+ var open = detail.hasAttribute("hidden") ? false : true;
335
+ if (open) {
336
+ detail.setAttribute("hidden", "");
337
+ summary.setAttribute("aria-expanded", "false");
338
+ summary.classList.remove("expanded");
339
+ } else {
340
+ detail.removeAttribute("hidden");
341
+ summary.setAttribute("aria-expanded", "true");
342
+ summary.classList.add("expanded");
343
+ }
344
+ }
345
+ summary.addEventListener("click", toggle);
346
+ summary.addEventListener("keydown", function (e) {
347
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); }
348
+ });
349
+
350
+ tbody.appendChild(summary);
351
+ tbody.appendChild(detail);
352
+ });
353
+
354
+ if (!(run.pageResults || []).length) {
355
+ tbody.appendChild(el("tr", {}, el("td", { colspan: "6", class: "empty", style: "padding:18px;text-align:center;" }, "No pages in this run.")));
356
+ }
357
+
358
+ // ---- Detail panel for a page ----
359
+ function renderDetail(page, idx) {
360
+ var panel = el("div", { class: "detail-panel" });
361
+
362
+ // Slider block (full width on mobile, left column on desktop)
363
+ var slider = renderSlider(page, idx);
364
+ panel.appendChild(slider);
365
+
366
+ // Right column: tokens + structuredDiffs + violations + fixes + meta
367
+ var right = el("div", { class: "detail-block" }, [
368
+ el("h3", { text: "Page details" }),
369
+ ]);
370
+
371
+ // Meta line
372
+ var meta = el("div", { class: "meta" }, []);
373
+ if (page.outputPath) meta.appendChild(el("a", { href: page.outputPath, target: "_blank", rel: "noreferrer noopener", text: "→ output" }));
374
+ if (page.url) meta.appendChild(el("a", { href: page.url, target: "_blank", rel: "noreferrer noopener", text: "→ live" }));
375
+ if (page.diffMap) meta.appendChild(el("a", { href: page.diffMap, target: "_blank", rel: "noreferrer noopener", text: "→ diff map (full)" }));
376
+ right.appendChild(meta);
377
+
378
+ // Token diffs
379
+ right.appendChild(el("h3", { text: "Token / geometry diffs", style: "margin-top:14px;" }));
380
+ var diffs = page.structuredDiffs || [];
381
+ if (diffs.length) {
382
+ var ul = el("ul", { class: "diffs-list" });
383
+ diffs.slice(0, 30).forEach(function (d) {
384
+ var sev = (d.severity || "low").toLowerCase();
385
+ ul.appendChild(el("li", {}, [
386
+ el("span", { class: "badge sev-" + sev, text: sev }),
387
+ el("span", { class: "key", text: " " + (d.key || d.path || "(?)") }),
388
+ el("span", { text: " live=" + JSON.stringify(d.live) + " · build=" + JSON.stringify(d.build) }),
389
+ ]));
390
+ });
391
+ right.appendChild(ul);
392
+ if (diffs.length > 30) right.appendChild(el("p", { class: "empty", text: "…and " + (diffs.length - 30) + " more (see compare report JSON)." }));
393
+ } else {
394
+ right.appendChild(el("p", { class: "empty", text: "No structured diffs." }));
395
+ }
396
+
397
+ // Violations
398
+ right.appendChild(el("h3", { text: "Anti-pattern violations", style: "margin-top:14px;" }));
399
+ var v = page.violations || [];
400
+ if (v.length) {
401
+ var vul = el("ul", { class: "violations-list" });
402
+ v.forEach(function (it) {
403
+ var sev = (it.severity || "low").toLowerCase();
404
+ vul.appendChild(el("li", {}, [
405
+ el("span", { class: "badge sev-" + sev, text: sev }),
406
+ el("span", { class: "vid", text: " " + (it.id || it.code || "violation") }),
407
+ el("span", { class: "vmsg", text: it.message || it.summary || "" }),
408
+ ]));
409
+ });
410
+ right.appendChild(vul);
411
+ } else {
412
+ right.appendChild(el("p", { class: "empty", text: "No violations." }));
413
+ }
414
+
415
+ // Candidate fixes
416
+ var fixes = page.candidateFixes || [];
417
+ if (fixes.length) {
418
+ right.appendChild(el("h3", { text: "Candidate fixes", style: "margin-top:14px;" }));
419
+ var ful = el("ul", { class: "fixes-list" });
420
+ fixes.forEach(function (f) {
421
+ ful.appendChild(el("li", { text: typeof f === "string" ? f : (f.summary || JSON.stringify(f)) }));
422
+ });
423
+ right.appendChild(ful);
424
+ }
425
+
426
+ // Token table (live vs build) — collapsed if absent
427
+ if (page.tokens && (page.tokens.live || page.tokens.build)) {
428
+ right.appendChild(el("h3", { text: "Tokens (live vs build)", style: "margin-top:14px;" }));
429
+ right.appendChild(renderTokenTable(page.tokens));
430
+ }
431
+
432
+ panel.appendChild(right);
433
+ return panel;
434
+ }
435
+
436
+ function renderSlider(page, idx) {
437
+ var block = el("div", { class: "detail-block" }, el("h3", { text: "Side-by-side (live ⇆ build)" }));
438
+
439
+ if (!page.screenshotsLive || !page.screenshotsBuild) {
440
+ block.appendChild(el("p", { class: "empty", text: "Screenshots not available for this page." }));
441
+ if (page.diffMap) {
442
+ block.appendChild(el("h3", { text: "Diff map", style: "margin-top:12px;" }));
443
+ block.appendChild(el("img", { src: page.diffMap, alt: "Pixel diff map", class: "diff-image", loading: "lazy" }));
444
+ }
445
+ return block;
446
+ }
447
+
448
+ var sliderId = "slider-" + idx;
449
+ var inputId = "slider-range-" + idx;
450
+ var slider = el("div", { class: "slider", id: sliderId }, [
451
+ el("img", { class: "build", src: page.screenshotsBuild, alt: "Build screenshot for " + (page.slug || page.url || ""), loading: "lazy" }),
452
+ el("img", { class: "live", src: page.screenshotsLive, alt: "Live screenshot for " + (page.slug || page.url || ""), loading: "lazy" }),
453
+ el("span", { class: "label left", text: "Live" }),
454
+ el("span", { class: "label right", text: "Build" }),
455
+ el("span", { class: "handle", "aria-hidden": "true" }),
456
+ el("input", {
457
+ class: "input-range",
458
+ id: inputId,
459
+ type: "range", min: "0", max: "100", step: "1", value: "50",
460
+ "aria-label": "Compare live vs build, " + (page.slug || page.url || ""),
461
+ "aria-valuemin": "0", "aria-valuemax": "100", "aria-valuenow": "50",
462
+ role: "slider",
463
+ }),
464
+ el("span", { class: "focus-ring", "aria-hidden": "true" }),
465
+ ]);
466
+
467
+ var range = $("#" + inputId, slider);
468
+ function applyClip() {
469
+ slider.style.setProperty("--clip", range.value + "%");
470
+ range.setAttribute("aria-valuenow", range.value);
471
+ }
472
+ range.addEventListener("input", applyClip);
473
+ range.addEventListener("keydown", function (e) {
474
+ // Default range keys move ±1; Shift = ±5; Home/End jumps to ends. Already
475
+ // covered by the native control — we just stop arrow keys from scrolling.
476
+ if (["ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Home","End","PageUp","PageDown"].indexOf(e.key) !== -1) {
477
+ e.stopPropagation();
478
+ }
479
+ });
480
+ applyClip();
481
+
482
+ block.appendChild(slider);
483
+ block.appendChild(el("p", { class: "slider-help", text: "Drag to compare. Keyboard: ←/→ ±1, Shift+←/→ ±5, Home/End jump to edges." }));
484
+
485
+ if (page.diffMap) {
486
+ block.appendChild(el("h3", { text: "Diff map", style: "margin-top:12px;" }));
487
+ block.appendChild(el("img", { src: page.diffMap, alt: "Pixel diff overlay", class: "diff-image", loading: "lazy" }));
488
+ }
489
+
490
+ return block;
491
+ }
492
+
493
+ function renderTokenTable(tokens) {
494
+ var live = tokens.live || {};
495
+ var build = tokens.build || {};
496
+ var keys = Object.keys(Object.assign({}, live, build));
497
+ keys.sort();
498
+ var tbl = el("table", { class: "kv" });
499
+ keys.forEach(function (k) {
500
+ var lv = live[k]; var bv = build[k];
501
+ var equal = JSON.stringify(lv) === JSON.stringify(bv);
502
+ tbl.appendChild(el("tr", {}, [
503
+ el("td", { text: k }),
504
+ el("td", { class: equal ? "equal" : "changed", text: lv == null ? "—" : (typeof lv === "object" ? JSON.stringify(lv) : String(lv)) }),
505
+ el("td", { class: equal ? "equal" : "changed", text: bv == null ? "—" : (typeof bv === "object" ? JSON.stringify(bv) : String(bv)) }),
506
+ ]));
507
+ });
508
+ if (!keys.length) {
509
+ var n = el("p", { class: "empty", text: "No token snapshot available." });
510
+ var w = el("div"); w.appendChild(n); return w;
511
+ }
512
+ var head = el("tr", {}, [
513
+ el("td", { text: "key" }),
514
+ el("td", { text: "live" }),
515
+ el("td", { text: "build" }),
516
+ ]);
517
+ tbl.insertBefore(head, tbl.firstChild);
518
+ return tbl;
519
+ }
520
+
521
+ // ---- Previous runs (Pattern #38) ----
522
+ var prev = DATA.previousRuns || [];
523
+ if (prev.length) {
524
+ $("#previous-runs-section").hidden = false;
525
+ var list = $("#previous-runs-list");
526
+ prev.forEach(function (r) {
527
+ var t = r.totals || { ok: 0, warn: 0, fail: 0 };
528
+ var det = el("details", { class: "previous-run" }, [
529
+ el("summary", {}, [
530
+ el("strong", { text: r.timestamp || "(no timestamp)" }),
531
+ el("span", {}, " " + (t.ok||0) + "✅ " + (t.warn||0) + "⚠️ " + (t.fail||0) + "❌ · " + fmtMs(r.durationMs || 0) + " · iters " + (r.totalIterations || 0)),
532
+ ]),
533
+ el("div", { class: "prev-body" }, renderPreviousRunBody(r)),
534
+ ]);
535
+ list.appendChild(det);
536
+ });
537
+ }
538
+
539
+ function renderPreviousRunBody(r) {
540
+ var wrap = el("div");
541
+ var rows = (r.pageResults || []).map(function (p) {
542
+ var tok = statusToken(p.status);
543
+ return el("tr", {}, [
544
+ el("td", {}, el("span", { class: "badge " + tok, text: statusLabel(tok) })),
545
+ el("td", { text: p.type || "—" }),
546
+ el("td", { text: p.slug || p.url || "(unnamed)" }),
547
+ el("td", { style: "text-align:right;font-family:var(--mono);font-variant-numeric:tabular-nums;", text: fmtPct(p.diffPercent) }),
548
+ el("td", { style: "text-align:right;font-family:var(--mono);", text: String(p.iterations || 0) }),
549
+ ]);
550
+ });
551
+ if (!rows.length) { wrap.appendChild(el("p", { class: "empty", text: "No pages recorded." })); return wrap; }
552
+ var tbl = el("table", { class: "pages" }, [
553
+ el("thead", {}, el("tr", {}, [
554
+ el("th", { text: "Status" }), el("th", { text: "Type" }), el("th", { text: "Slug" }),
555
+ el("th", { style: "text-align:right", text: "Diff%" }), el("th", { style: "text-align:right", text: "Iter" }),
556
+ ])),
557
+ el("tbody", {}, rows),
558
+ ]);
559
+ wrap.appendChild(tbl);
560
+ return wrap;
561
+ }
562
+
563
+ // ---- Footer ----
564
+ var fmeta = $("#footer-meta");
565
+ fmeta.appendChild(document.createTextNode("Generated " + (DATA.generatedAt || "—") + " · template " + (DATA.templateVersion || "1") + " · runs persisted: " + (1 + prev.length)));
566
+ $("#config-snapshot").textContent = JSON.stringify(run.configSnapshot || {}, null, 2);
567
+
568
+ })();
569
+ </script>
570
+ </body>
571
+ </html>