page-analyzer 1.0.0 → 1.1.1
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/LICENSE +21 -0
- package/README.md +430 -0
- package/index.js +186 -6
- package/llm/analyzers/event-analyzer/event-analyzer-blocks.js +23 -2
- package/llm/analyzers/event-analyzer/event-analyzer-constants.js +1 -1
- package/llm/analyzers/event-analyzer/event-analyzer.js +1 -1
- package/package.json +5 -3
- package/page-extractor.js +364 -17
- package/result-viewer.html +879 -0
- package/scripts/analyze.js +51 -0
- package/scripts/build-result-viewer.js +891 -0
- package/scripts/serve-result-viewer.js +68 -0
- package/test/smoke.test.js +213 -0
|
@@ -0,0 +1,879 @@
|
|
|
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>Page Analyzer Result Viewer</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f4f0e8;
|
|
10
|
+
--paper: #fffaf0;
|
|
11
|
+
--ink: #171511;
|
|
12
|
+
--muted: #696255;
|
|
13
|
+
--line: #d5cbb9;
|
|
14
|
+
--accent: #0f766e;
|
|
15
|
+
--accent-2: #b45309;
|
|
16
|
+
--accent-3: #7c2d12;
|
|
17
|
+
--panel: #ede2d0;
|
|
18
|
+
--shadow: rgba(45, 35, 20, 0.14);
|
|
19
|
+
--missing: #8f1d1d;
|
|
20
|
+
--ok: #126b48;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
margin: 0;
|
|
29
|
+
min-width: 320px;
|
|
30
|
+
color: var(--ink);
|
|
31
|
+
background:
|
|
32
|
+
linear-gradient(90deg, rgba(15, 118, 110, 0.06) 0 1px, transparent 1px 100%),
|
|
33
|
+
linear-gradient(180deg, rgba(180, 83, 9, 0.05) 0 1px, transparent 1px 100%),
|
|
34
|
+
var(--bg);
|
|
35
|
+
background-size: 44px 44px;
|
|
36
|
+
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
|
37
|
+
letter-spacing: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
button,
|
|
41
|
+
input {
|
|
42
|
+
font: inherit;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.shell {
|
|
46
|
+
min-height: 100vh;
|
|
47
|
+
display: grid;
|
|
48
|
+
grid-template-columns: minmax(270px, 340px) minmax(0, 1fr);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.sidebar {
|
|
52
|
+
position: sticky;
|
|
53
|
+
top: 0;
|
|
54
|
+
height: 100vh;
|
|
55
|
+
overflow: auto;
|
|
56
|
+
border-right: 1px solid var(--line);
|
|
57
|
+
background: rgba(255, 250, 240, 0.88);
|
|
58
|
+
backdrop-filter: blur(14px);
|
|
59
|
+
padding: 22px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.brand {
|
|
63
|
+
margin-bottom: 18px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.eyebrow {
|
|
67
|
+
color: var(--accent-2);
|
|
68
|
+
font-size: 12px;
|
|
69
|
+
font-weight: 800;
|
|
70
|
+
text-transform: uppercase;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
h1,
|
|
74
|
+
h2,
|
|
75
|
+
h3 {
|
|
76
|
+
margin: 0;
|
|
77
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
letter-spacing: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
h1 {
|
|
83
|
+
margin-top: 6px;
|
|
84
|
+
font-size: 28px;
|
|
85
|
+
line-height: 1.06;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.title {
|
|
89
|
+
margin-top: 10px;
|
|
90
|
+
color: var(--muted);
|
|
91
|
+
font-size: 13px;
|
|
92
|
+
line-height: 1.45;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.load-state {
|
|
96
|
+
margin-top: 12px;
|
|
97
|
+
border: 1px solid var(--line);
|
|
98
|
+
background: #fffdf7;
|
|
99
|
+
color: var(--muted);
|
|
100
|
+
padding: 10px;
|
|
101
|
+
font-size: 12px;
|
|
102
|
+
line-height: 1.45;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.load-state.error {
|
|
106
|
+
border-color: rgba(143, 29, 29, 0.28);
|
|
107
|
+
background: rgba(143, 29, 29, 0.08);
|
|
108
|
+
color: var(--missing);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.file-loader {
|
|
112
|
+
display: grid;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
margin-top: 8px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.file-loader input {
|
|
118
|
+
width: 100%;
|
|
119
|
+
border: 1px solid var(--line);
|
|
120
|
+
background: #fffdf7;
|
|
121
|
+
padding: 8px;
|
|
122
|
+
color: var(--ink);
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.metrics {
|
|
127
|
+
display: grid;
|
|
128
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
129
|
+
gap: 8px;
|
|
130
|
+
margin: 18px 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.metric {
|
|
134
|
+
min-height: 64px;
|
|
135
|
+
border: 1px solid var(--line);
|
|
136
|
+
background: var(--paper);
|
|
137
|
+
padding: 10px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.metric strong {
|
|
141
|
+
display: block;
|
|
142
|
+
font-size: 22px;
|
|
143
|
+
line-height: 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.metric span {
|
|
147
|
+
display: block;
|
|
148
|
+
margin-top: 6px;
|
|
149
|
+
color: var(--muted);
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.toolbar {
|
|
154
|
+
display: grid;
|
|
155
|
+
gap: 10px;
|
|
156
|
+
margin: 12px 0 14px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.search {
|
|
160
|
+
width: 100%;
|
|
161
|
+
height: 38px;
|
|
162
|
+
border: 1px solid var(--line);
|
|
163
|
+
border-radius: 0;
|
|
164
|
+
background: #fffdf7;
|
|
165
|
+
color: var(--ink);
|
|
166
|
+
padding: 0 10px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.filters {
|
|
170
|
+
display: grid;
|
|
171
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
172
|
+
border: 1px solid var(--line);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.filter {
|
|
176
|
+
height: 34px;
|
|
177
|
+
border: 0;
|
|
178
|
+
border-right: 1px solid var(--line);
|
|
179
|
+
background: #fffdf7;
|
|
180
|
+
color: var(--muted);
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.filter:last-child {
|
|
185
|
+
border-right: 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.filter[aria-pressed="true"] {
|
|
189
|
+
background: var(--accent);
|
|
190
|
+
color: white;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.block-list {
|
|
194
|
+
display: grid;
|
|
195
|
+
gap: 8px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.block-tab {
|
|
199
|
+
width: 100%;
|
|
200
|
+
min-height: 72px;
|
|
201
|
+
border: 1px solid var(--line);
|
|
202
|
+
background: rgba(255, 253, 247, 0.9);
|
|
203
|
+
color: var(--ink);
|
|
204
|
+
text-align: left;
|
|
205
|
+
padding: 10px;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.block-tab:hover,
|
|
210
|
+
.block-tab.active {
|
|
211
|
+
border-color: var(--accent);
|
|
212
|
+
box-shadow: inset 4px 0 0 var(--accent);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.tab-top {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
justify-content: space-between;
|
|
219
|
+
gap: 8px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.tab-name {
|
|
223
|
+
overflow: hidden;
|
|
224
|
+
text-overflow: ellipsis;
|
|
225
|
+
white-space: nowrap;
|
|
226
|
+
font-weight: 800;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.tab-index {
|
|
230
|
+
flex: 0 0 auto;
|
|
231
|
+
color: var(--muted);
|
|
232
|
+
font-family: "SFMono-Regular", Consolas, monospace;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.tab-desc {
|
|
237
|
+
display: -webkit-box;
|
|
238
|
+
margin-top: 6px;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
color: var(--muted);
|
|
241
|
+
font-size: 12px;
|
|
242
|
+
line-height: 1.35;
|
|
243
|
+
-webkit-line-clamp: 2;
|
|
244
|
+
-webkit-box-orient: vertical;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.content {
|
|
248
|
+
min-width: 0;
|
|
249
|
+
padding: 28px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.summary-row {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: flex-start;
|
|
255
|
+
justify-content: space-between;
|
|
256
|
+
gap: 18px;
|
|
257
|
+
margin-bottom: 18px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.summary-copy {
|
|
261
|
+
max-width: 940px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.summary-copy h2 {
|
|
265
|
+
font-size: 34px;
|
|
266
|
+
line-height: 1.08;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.summary-copy p {
|
|
270
|
+
margin: 10px 0 0;
|
|
271
|
+
color: var(--muted);
|
|
272
|
+
line-height: 1.55;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.actions {
|
|
276
|
+
display: flex;
|
|
277
|
+
gap: 8px;
|
|
278
|
+
flex-wrap: wrap;
|
|
279
|
+
justify-content: flex-end;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.action {
|
|
283
|
+
min-height: 36px;
|
|
284
|
+
border: 1px solid var(--ink);
|
|
285
|
+
background: var(--ink);
|
|
286
|
+
color: white;
|
|
287
|
+
padding: 0 12px;
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
text-decoration: none;
|
|
290
|
+
display: inline-flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.action.secondary {
|
|
295
|
+
background: transparent;
|
|
296
|
+
color: var(--ink);
|
|
297
|
+
border-color: var(--line);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.detail {
|
|
301
|
+
display: grid;
|
|
302
|
+
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
|
|
303
|
+
gap: 18px;
|
|
304
|
+
align-items: start;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.panel {
|
|
308
|
+
border: 1px solid var(--line);
|
|
309
|
+
background: rgba(255, 250, 240, 0.9);
|
|
310
|
+
box-shadow: 0 18px 40px var(--shadow);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.panel-head {
|
|
314
|
+
border-bottom: 1px solid var(--line);
|
|
315
|
+
padding: 16px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.panel-head h3 {
|
|
319
|
+
font-size: 22px;
|
|
320
|
+
line-height: 1.1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.panel-body {
|
|
324
|
+
padding: 16px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.shot-box {
|
|
328
|
+
display: grid;
|
|
329
|
+
gap: 10px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.screenshot-frame {
|
|
333
|
+
min-height: 260px;
|
|
334
|
+
max-height: 620px;
|
|
335
|
+
overflow: auto;
|
|
336
|
+
border: 1px solid var(--line);
|
|
337
|
+
background:
|
|
338
|
+
linear-gradient(45deg, rgba(23, 21, 17, 0.05) 25%, transparent 25% 75%, rgba(23, 21, 17, 0.05) 75%),
|
|
339
|
+
linear-gradient(45deg, rgba(23, 21, 17, 0.05) 25%, transparent 25% 75%, rgba(23, 21, 17, 0.05) 75%),
|
|
340
|
+
#fffdf7;
|
|
341
|
+
background-position: 0 0, 10px 10px;
|
|
342
|
+
background-size: 20px 20px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.screenshot-frame img {
|
|
346
|
+
display: block;
|
|
347
|
+
width: 100%;
|
|
348
|
+
height: auto;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.missing-shot {
|
|
352
|
+
min-height: 260px;
|
|
353
|
+
display: grid;
|
|
354
|
+
place-items: center;
|
|
355
|
+
color: var(--missing);
|
|
356
|
+
text-align: center;
|
|
357
|
+
padding: 22px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.info-grid {
|
|
361
|
+
display: grid;
|
|
362
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
363
|
+
gap: 10px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.info {
|
|
367
|
+
border: 1px solid var(--line);
|
|
368
|
+
background: #fffdf7;
|
|
369
|
+
padding: 10px;
|
|
370
|
+
min-height: 58px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.info label {
|
|
374
|
+
display: block;
|
|
375
|
+
color: var(--muted);
|
|
376
|
+
font-size: 11px;
|
|
377
|
+
font-weight: 800;
|
|
378
|
+
text-transform: uppercase;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.info span {
|
|
382
|
+
display: block;
|
|
383
|
+
margin-top: 5px;
|
|
384
|
+
overflow-wrap: anywhere;
|
|
385
|
+
font-size: 13px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.wide {
|
|
389
|
+
grid-column: 1 / -1;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.chips {
|
|
393
|
+
display: flex;
|
|
394
|
+
gap: 6px;
|
|
395
|
+
flex-wrap: wrap;
|
|
396
|
+
margin-top: 10px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.chip {
|
|
400
|
+
border: 1px solid var(--line);
|
|
401
|
+
background: var(--panel);
|
|
402
|
+
color: var(--ink);
|
|
403
|
+
padding: 4px 7px;
|
|
404
|
+
font-size: 12px;
|
|
405
|
+
line-height: 1.2;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.chip.ok {
|
|
409
|
+
border-color: rgba(18, 107, 72, 0.35);
|
|
410
|
+
background: rgba(18, 107, 72, 0.1);
|
|
411
|
+
color: var(--ok);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.chip.warn {
|
|
415
|
+
border-color: rgba(143, 29, 29, 0.28);
|
|
416
|
+
background: rgba(143, 29, 29, 0.08);
|
|
417
|
+
color: var(--missing);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
pre {
|
|
421
|
+
max-height: 420px;
|
|
422
|
+
margin: 0;
|
|
423
|
+
overflow: auto;
|
|
424
|
+
border: 1px solid var(--line);
|
|
425
|
+
background: #171511;
|
|
426
|
+
color: #f7eddc;
|
|
427
|
+
padding: 12px;
|
|
428
|
+
font-family: "SFMono-Regular", Consolas, monospace;
|
|
429
|
+
font-size: 12px;
|
|
430
|
+
line-height: 1.55;
|
|
431
|
+
white-space: pre-wrap;
|
|
432
|
+
overflow-wrap: anywhere;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.all-blocks {
|
|
436
|
+
display: grid;
|
|
437
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
438
|
+
gap: 12px;
|
|
439
|
+
margin-top: 18px;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.mini {
|
|
443
|
+
border: 1px solid var(--line);
|
|
444
|
+
background: rgba(255, 250, 240, 0.82);
|
|
445
|
+
padding: 10px;
|
|
446
|
+
cursor: pointer;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.mini img {
|
|
450
|
+
width: 100%;
|
|
451
|
+
aspect-ratio: 16 / 9;
|
|
452
|
+
object-fit: cover;
|
|
453
|
+
border: 1px solid var(--line);
|
|
454
|
+
background: #fffdf7;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.mini h3 {
|
|
458
|
+
margin-top: 9px;
|
|
459
|
+
font-size: 17px;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.mini p {
|
|
463
|
+
margin: 6px 0 0;
|
|
464
|
+
color: var(--muted);
|
|
465
|
+
font-size: 12px;
|
|
466
|
+
line-height: 1.35;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.empty-thumb {
|
|
470
|
+
width: 100%;
|
|
471
|
+
aspect-ratio: 16 / 9;
|
|
472
|
+
display: grid;
|
|
473
|
+
place-items: center;
|
|
474
|
+
border: 1px solid var(--line);
|
|
475
|
+
background: #fffdf7;
|
|
476
|
+
color: var(--missing);
|
|
477
|
+
font-size: 12px;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@media (max-width: 980px) {
|
|
481
|
+
.shell {
|
|
482
|
+
grid-template-columns: 1fr;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.sidebar {
|
|
486
|
+
position: relative;
|
|
487
|
+
height: auto;
|
|
488
|
+
border-right: 0;
|
|
489
|
+
border-bottom: 1px solid var(--line);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.detail {
|
|
493
|
+
grid-template-columns: 1fr;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
@media (max-width: 640px) {
|
|
498
|
+
.content,
|
|
499
|
+
.sidebar {
|
|
500
|
+
padding: 16px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.summary-row {
|
|
504
|
+
display: grid;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.summary-copy h2 {
|
|
508
|
+
font-size: 28px;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.info-grid {
|
|
512
|
+
grid-template-columns: 1fr;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
</style>
|
|
516
|
+
</head>
|
|
517
|
+
<body>
|
|
518
|
+
<div class="shell">
|
|
519
|
+
<aside class="sidebar">
|
|
520
|
+
<div class="brand">
|
|
521
|
+
<div class="eyebrow">Page Analyzer</div>
|
|
522
|
+
<h1>Block Review</h1>
|
|
523
|
+
<div class="title" id="page-title"></div>
|
|
524
|
+
<div class="load-state" id="load-state">Loading result.json...</div>
|
|
525
|
+
<label class="file-loader" id="file-loader" hidden>
|
|
526
|
+
<span>Choose result.json manually</span>
|
|
527
|
+
<input id="result-file" type="file" accept="application/json,.json">
|
|
528
|
+
</label>
|
|
529
|
+
</div>
|
|
530
|
+
<div class="metrics" id="metrics"></div>
|
|
531
|
+
<div class="toolbar">
|
|
532
|
+
<input class="search" id="search" type="search" placeholder="Search blocks, selectors, semantics">
|
|
533
|
+
<div class="filters" role="group" aria-label="Block filter">
|
|
534
|
+
<button class="filter" data-filter="all" aria-pressed="true">All</button>
|
|
535
|
+
<button class="filter" data-filter="shots" aria-pressed="false">Shots</button>
|
|
536
|
+
<button class="filter" data-filter="missing" aria-pressed="false">Missing</button>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="block-list" id="block-list"></div>
|
|
540
|
+
</aside>
|
|
541
|
+
<main class="content">
|
|
542
|
+
<section class="summary-row">
|
|
543
|
+
<div class="summary-copy">
|
|
544
|
+
<h2 id="selected-title"></h2>
|
|
545
|
+
<p id="selected-description"></p>
|
|
546
|
+
</div>
|
|
547
|
+
<div class="actions">
|
|
548
|
+
<a class="action secondary" id="full-page-link" target="_blank" rel="noreferrer">Open full page shot</a>
|
|
549
|
+
<button class="action" id="copy-selector" type="button">Copy selector</button>
|
|
550
|
+
</div>
|
|
551
|
+
</section>
|
|
552
|
+
|
|
553
|
+
<section class="detail">
|
|
554
|
+
<article class="panel">
|
|
555
|
+
<div class="panel-head">
|
|
556
|
+
<h3>Screenshot</h3>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="panel-body">
|
|
559
|
+
<div class="shot-box" id="screenshot"></div>
|
|
560
|
+
</div>
|
|
561
|
+
</article>
|
|
562
|
+
|
|
563
|
+
<article class="panel">
|
|
564
|
+
<div class="panel-head">
|
|
565
|
+
<h3>Block Data</h3>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="panel-body">
|
|
568
|
+
<div class="info-grid" id="block-info"></div>
|
|
569
|
+
</div>
|
|
570
|
+
</article>
|
|
571
|
+
</section>
|
|
572
|
+
|
|
573
|
+
<section class="panel" style="margin-top: 18px;">
|
|
574
|
+
<div class="panel-head">
|
|
575
|
+
<h3>All Blocks</h3>
|
|
576
|
+
</div>
|
|
577
|
+
<div class="panel-body">
|
|
578
|
+
<div class="all-blocks" id="all-blocks"></div>
|
|
579
|
+
</div>
|
|
580
|
+
</section>
|
|
581
|
+
|
|
582
|
+
<section class="panel" style="margin-top: 18px;">
|
|
583
|
+
<div class="panel-head">
|
|
584
|
+
<h3>Raw Block JSON</h3>
|
|
585
|
+
</div>
|
|
586
|
+
<div class="panel-body">
|
|
587
|
+
<pre id="raw-json"></pre>
|
|
588
|
+
</div>
|
|
589
|
+
</section>
|
|
590
|
+
</main>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<script>
|
|
594
|
+
let data = null;
|
|
595
|
+
let blocks = [];
|
|
596
|
+
let screenshotRows = [];
|
|
597
|
+
let screenshotByBlockIdx = new Map();
|
|
598
|
+
let selectedIndex = 0;
|
|
599
|
+
let activeFilter = 'all';
|
|
600
|
+
let query = '';
|
|
601
|
+
|
|
602
|
+
const els = {
|
|
603
|
+
pageTitle: document.getElementById('page-title'),
|
|
604
|
+
metrics: document.getElementById('metrics'),
|
|
605
|
+
list: document.getElementById('block-list'),
|
|
606
|
+
search: document.getElementById('search'),
|
|
607
|
+
selectedTitle: document.getElementById('selected-title'),
|
|
608
|
+
selectedDescription: document.getElementById('selected-description'),
|
|
609
|
+
screenshot: document.getElementById('screenshot'),
|
|
610
|
+
info: document.getElementById('block-info'),
|
|
611
|
+
raw: document.getElementById('raw-json'),
|
|
612
|
+
allBlocks: document.getElementById('all-blocks'),
|
|
613
|
+
copySelector: document.getElementById('copy-selector'),
|
|
614
|
+
fullPageLink: document.getElementById('full-page-link'),
|
|
615
|
+
loadState: document.getElementById('load-state'),
|
|
616
|
+
fileLoader: document.getElementById('file-loader'),
|
|
617
|
+
resultFile: document.getElementById('result-file')
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
function pathToUrl(value) {
|
|
621
|
+
const text = String(value || '');
|
|
622
|
+
if (!text) return '';
|
|
623
|
+
if (/^(https?:|file:|data:)/i.test(text)) return text;
|
|
624
|
+
const snapshotIndex = text.lastIndexOf('/snapshots/');
|
|
625
|
+
if (snapshotIndex >= 0) {
|
|
626
|
+
return encodeURI('./snapshots/' + text.slice(snapshotIndex + '/snapshots/'.length));
|
|
627
|
+
}
|
|
628
|
+
if (text.startsWith('/')) return 'file://' + encodeURI(text);
|
|
629
|
+
return text;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function getShot(block, index) {
|
|
633
|
+
const direct = Array.isArray(block.blockScreenshotPaths) ? block.blockScreenshotPaths[0] : '';
|
|
634
|
+
if (direct) return { path: direct };
|
|
635
|
+
return screenshotByBlockIdx.get(index) || null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function formatPosition(position) {
|
|
639
|
+
if (!position || typeof position !== 'object') return 'n/a';
|
|
640
|
+
const left = Number(position.left || 0).toFixed(0);
|
|
641
|
+
const top = Number(position.top || 0).toFixed(0);
|
|
642
|
+
const width = Number(position.width || 0).toFixed(0);
|
|
643
|
+
const height = Number(position.height || 0).toFixed(0);
|
|
644
|
+
return 'x=' + left + ', y=' + top + ', ' + width + 'x' + height;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function textMatches(block, index) {
|
|
648
|
+
const haystack = [
|
|
649
|
+
index,
|
|
650
|
+
block.blockName,
|
|
651
|
+
block.blockDescription,
|
|
652
|
+
block.blockCssPath,
|
|
653
|
+
block.blockIdxs,
|
|
654
|
+
...(block.blockSemantics || []),
|
|
655
|
+
...(block.blockPossibleEvents || [])
|
|
656
|
+
].join(' ').toLowerCase();
|
|
657
|
+
return haystack.includes(query.toLowerCase());
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function isVisibleByFilter(block, index) {
|
|
661
|
+
const hasShot = Boolean(getShot(block, index));
|
|
662
|
+
if (activeFilter === 'shots') return hasShot;
|
|
663
|
+
if (activeFilter === 'missing') return !hasShot;
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function visibleBlocks() {
|
|
668
|
+
return blocks
|
|
669
|
+
.map((block, index) => ({ block, index }))
|
|
670
|
+
.filter(({ block, index }) => textMatches(block, index) && isVisibleByFilter(block, index));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function renderMetrics() {
|
|
674
|
+
const stats = data.analysis?.block_analysis?.stats || {};
|
|
675
|
+
const metrics = [
|
|
676
|
+
['Blocks', blocks.length],
|
|
677
|
+
['Screenshots', screenshotRows.length],
|
|
678
|
+
['Elements', data.parseMetrics?.elementsCount || 0],
|
|
679
|
+
['Parse ms', data.parseMetrics?.parseMs || 0]
|
|
680
|
+
];
|
|
681
|
+
els.metrics.innerHTML = metrics.map(([label, value]) => (
|
|
682
|
+
'<div class="metric"><strong>' + value + '</strong><span>' + label + '</span></div>'
|
|
683
|
+
)).join('');
|
|
684
|
+
if (stats.total_blocks && stats.total_blocks !== blocks.length) {
|
|
685
|
+
els.metrics.insertAdjacentHTML('beforeend',
|
|
686
|
+
'<div class="metric"><strong>' + stats.total_blocks + '</strong><span>Reported blocks</span></div>'
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function renderList() {
|
|
692
|
+
const rows = visibleBlocks();
|
|
693
|
+
els.list.innerHTML = rows.map(({ block, index }) => {
|
|
694
|
+
const hasShot = Boolean(getShot(block, index));
|
|
695
|
+
const status = hasShot ? 'shot' : 'no shot';
|
|
696
|
+
const name = block.blockName || 'Unnamed block';
|
|
697
|
+
const desc = block.blockDescription || block.blockCssPath || 'No description';
|
|
698
|
+
return '<button class="block-tab ' + (index === selectedIndex ? 'active' : '') + '" data-index="' + index + '">' +
|
|
699
|
+
'<div class="tab-top"><span class="tab-name">' + escapeHtml(name) + '</span><span class="tab-index">#' + index + ' ' + status + '</span></div>' +
|
|
700
|
+
'<div class="tab-desc">' + escapeHtml(desc) + '</div>' +
|
|
701
|
+
'</button>';
|
|
702
|
+
}).join('');
|
|
703
|
+
|
|
704
|
+
for (const button of els.list.querySelectorAll('.block-tab')) {
|
|
705
|
+
button.addEventListener('click', () => selectBlock(Number(button.dataset.index)));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function renderAllBlocks() {
|
|
710
|
+
els.allBlocks.innerHTML = visibleBlocks().map(({ block, index }) => {
|
|
711
|
+
const shot = getShot(block, index);
|
|
712
|
+
const image = shot
|
|
713
|
+
? '<img src="' + pathToUrl(shot.path) + '" alt="Screenshot for block ' + index + '">'
|
|
714
|
+
: '<div class="empty-thumb">No selector screenshot</div>';
|
|
715
|
+
return '<article class="mini" data-index="' + index + '">' +
|
|
716
|
+
image +
|
|
717
|
+
'<h3>#' + index + ' ' + escapeHtml(block.blockName || 'Unnamed block') + '</h3>' +
|
|
718
|
+
'<p>' + escapeHtml(block.blockDescription || block.blockCssPath || 'No description') + '</p>' +
|
|
719
|
+
'</article>';
|
|
720
|
+
}).join('');
|
|
721
|
+
|
|
722
|
+
for (const item of els.allBlocks.querySelectorAll('.mini')) {
|
|
723
|
+
item.addEventListener('click', () => {
|
|
724
|
+
selectBlock(Number(item.dataset.index));
|
|
725
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function renderSelected() {
|
|
731
|
+
const block = blocks[selectedIndex] || {};
|
|
732
|
+
const shot = getShot(block, selectedIndex);
|
|
733
|
+
els.selectedTitle.textContent = '#' + selectedIndex + ' ' + (block.blockName || 'Unnamed block');
|
|
734
|
+
els.selectedDescription.textContent = block.blockDescription || 'No description available.';
|
|
735
|
+
els.copySelector.disabled = !block.blockCssPath;
|
|
736
|
+
els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
|
|
737
|
+
els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
|
|
738
|
+
|
|
739
|
+
if (shot?.path) {
|
|
740
|
+
els.screenshot.innerHTML =
|
|
741
|
+
'<div class="screenshot-frame"><img src="' + pathToUrl(shot.path) + '" alt="Screenshot for selected block"></div>' +
|
|
742
|
+
'<div class="info wide"><label>Screenshot path</label><span>' + escapeHtml(shot.path) + '</span></div>';
|
|
743
|
+
} else {
|
|
744
|
+
els.screenshot.innerHTML =
|
|
745
|
+
'<div class="missing-shot">No screenshot was generated for this block.<br>Most likely the selector was empty, hidden, or not screenshotable.</div>';
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const semantics = block.blockSemantics || [];
|
|
749
|
+
const events = block.blockPossibleEvents || [];
|
|
750
|
+
const groups = block.blockSemanticGroups || [];
|
|
751
|
+
els.info.innerHTML = [
|
|
752
|
+
info('Block name', block.blockName || 'n/a'),
|
|
753
|
+
info('Block idxs', block.blockIdxs || String(selectedIndex)),
|
|
754
|
+
info('Mode', block.mode || 'n/a'),
|
|
755
|
+
info('Rows', block.rowCount ?? 'n/a'),
|
|
756
|
+
info('Position', formatPosition(block.blockPosition), true),
|
|
757
|
+
info('Selector', block.blockCssPath || 'n/a', true),
|
|
758
|
+
chipInfo('Semantics', semantics, 'No semantics'),
|
|
759
|
+
chipInfo('Possible events', events, 'No possible events'),
|
|
760
|
+
chipInfo('Semantic groups', groups.map((item) => item.blockSemantic + ': ' + item.blockIdxs), 'No semantic groups')
|
|
761
|
+
].join('');
|
|
762
|
+
els.raw.textContent = JSON.stringify(block, null, 2);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function info(label, value, wide = false) {
|
|
766
|
+
return '<div class="info ' + (wide ? 'wide' : '') + '"><label>' + escapeHtml(label) + '</label><span>' + escapeHtml(value) + '</span></div>';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function chipInfo(label, values, emptyText) {
|
|
770
|
+
const list = Array.isArray(values) ? values.filter(Boolean) : [];
|
|
771
|
+
const chips = list.length
|
|
772
|
+
? list.map((value) => '<span class="chip ok">' + escapeHtml(value) + '</span>').join('')
|
|
773
|
+
: '<span class="chip warn">' + escapeHtml(emptyText) + '</span>';
|
|
774
|
+
return '<div class="info wide"><label>' + escapeHtml(label) + '</label><div class="chips">' + chips + '</div></div>';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function selectBlock(index) {
|
|
778
|
+
selectedIndex = Number.isInteger(index) ? index : 0;
|
|
779
|
+
renderList();
|
|
780
|
+
renderSelected();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function escapeHtml(value) {
|
|
784
|
+
return String(value ?? '')
|
|
785
|
+
.replace(/&/g, '&')
|
|
786
|
+
.replace(/</g, '<')
|
|
787
|
+
.replace(/>/g, '>')
|
|
788
|
+
.replace(/"/g, '"')
|
|
789
|
+
.replace(/'/g, ''');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
els.search.addEventListener('input', () => {
|
|
793
|
+
query = els.search.value;
|
|
794
|
+
renderList();
|
|
795
|
+
renderAllBlocks();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
for (const button of document.querySelectorAll('.filter')) {
|
|
799
|
+
button.addEventListener('click', () => {
|
|
800
|
+
activeFilter = button.dataset.filter;
|
|
801
|
+
for (const item of document.querySelectorAll('.filter')) {
|
|
802
|
+
item.setAttribute('aria-pressed', String(item === button));
|
|
803
|
+
}
|
|
804
|
+
renderList();
|
|
805
|
+
renderAllBlocks();
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
els.copySelector.addEventListener('click', async () => {
|
|
810
|
+
const selector = blocks[selectedIndex]?.blockCssPath || '';
|
|
811
|
+
if (!selector) return;
|
|
812
|
+
try {
|
|
813
|
+
await navigator.clipboard.writeText(selector);
|
|
814
|
+
els.copySelector.textContent = 'Copied';
|
|
815
|
+
setTimeout(() => { els.copySelector.textContent = 'Copy selector'; }, 900);
|
|
816
|
+
} catch {
|
|
817
|
+
window.prompt('Copy selector', selector);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
function setLoadState(message, isError = false) {
|
|
822
|
+
els.loadState.textContent = message;
|
|
823
|
+
els.loadState.classList.toggle('error', isError);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function initialize(nextData, sourceLabel) {
|
|
827
|
+
data = nextData || {};
|
|
828
|
+
blocks = data.analysis?.block_analysis?.blocks || [];
|
|
829
|
+
screenshotRows = data.screenshots?.blocks || [];
|
|
830
|
+
screenshotByBlockIdx = new Map(screenshotRows.map((item) => [Number(item.blockIdx), item]));
|
|
831
|
+
selectedIndex = 0;
|
|
832
|
+
query = '';
|
|
833
|
+
els.search.value = '';
|
|
834
|
+
els.pageTitle.textContent = data.title || 'Untitled page';
|
|
835
|
+
setLoadState(sourceLabel + ' loaded. ' + blocks.length + ' blocks, ' + screenshotRows.length + ' screenshots.');
|
|
836
|
+
els.fileLoader.hidden = true;
|
|
837
|
+
renderMetrics();
|
|
838
|
+
renderList();
|
|
839
|
+
renderAllBlocks();
|
|
840
|
+
renderSelected();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function loadResultJson() {
|
|
844
|
+
try {
|
|
845
|
+
const response = await fetch('./result.json', { cache: 'no-store' });
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
throw new Error('HTTP ' + response.status + ' while loading result.json');
|
|
848
|
+
}
|
|
849
|
+
initialize(await response.json(), 'result.json');
|
|
850
|
+
} catch (error) {
|
|
851
|
+
setLoadState(
|
|
852
|
+
'Could not load ./result.json automatically. If this page is opened with file://, your browser may block local JSON reads. Use the picker below or serve this folder locally.',
|
|
853
|
+
true
|
|
854
|
+
);
|
|
855
|
+
els.fileLoader.hidden = false;
|
|
856
|
+
els.selectedTitle.textContent = 'result.json not loaded';
|
|
857
|
+
els.selectedDescription.textContent = error.message || 'Unknown load error';
|
|
858
|
+
els.screenshot.innerHTML = '<div class="missing-shot">Load result.json to inspect blocks.</div>';
|
|
859
|
+
els.info.innerHTML = '';
|
|
860
|
+
els.raw.textContent = '';
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
els.resultFile.addEventListener('change', async () => {
|
|
865
|
+
const file = els.resultFile.files?.[0];
|
|
866
|
+
if (!file) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
initialize(JSON.parse(await file.text()), file.name);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
setLoadState('Could not parse selected JSON: ' + error.message, true);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
loadResultJson();
|
|
877
|
+
</script>
|
|
878
|
+
</body>
|
|
879
|
+
</html>
|