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.
- package/CHANGELOG.md +110 -0
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/bin/install.js +256 -0
- package/lib/copy-templates.mjs +52 -0
- package/lib/install-deps.mjs +62 -0
- package/lib/prompt-config.mjs +83 -0
- package/lib/verify-env.mjs +19 -0
- package/package.json +63 -0
- package/scripts/sync-templates.mjs +71 -0
- package/templates/commands/build-page.md +490 -0
- package/templates/commands/build-site.md +548 -0
- package/templates/commands/clip-section.md +519 -0
- package/templates/memory/anti-patterns.md +212 -0
- package/templates/memory/design-knowledge.md +225 -0
- package/templates/memory/fixes.md +163 -0
- package/templates/memory/patterns.md +681 -0
- package/templates/presets/shopify-section.yaml +51 -0
- package/templates/presets/wp-elementor.yaml +49 -0
- package/templates/reports/fixtures/mock-run-1.json +115 -0
- package/templates/reports/fixtures/mock-run-2.json +72 -0
- package/templates/reports/report-renderer.mjs +218 -0
- package/templates/reports/report-template.html +571 -0
- package/templates/skills/sb-build-shopify/SKILL.md +104 -0
- package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
- package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
- package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
- package/templates/skills/sb-build-wp/SKILL.md +83 -0
- package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
- package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
- package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
- package/templates/skills/sb-compare-visual/SKILL.md +121 -0
- package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
- package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
- package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
- package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
- package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
- package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
- package/templates/skills/sb-extract-assets/SKILL.md +112 -0
- package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
- package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
- package/templates/skills/sb-inspect-live/SKILL.md +105 -0
- package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
- package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
- package/templates/skills/sb-review-checks/SKILL.md +113 -0
- package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
- package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
- package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
- package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
- package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
- package/templates/skills/sb-tweak/SKILL.md +130 -0
- package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
- package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
- package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
- package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
- package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
- package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
- package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
- package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
- package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
- package/templates/skills/sb-validate-render/SKILL.md +120 -0
- package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
- 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>
|