inspectra-engine 0.0.0-stage → 1.0.3

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.
@@ -0,0 +1,946 @@
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>UI Differences</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f7f8fa;
10
+ --card: #fff;
11
+ --fg: #14161a;
12
+ --mut: #667085;
13
+ --line: #e6e8ec;
14
+ --soft: #f2f4f7;
15
+ --acc: #5b5bf0;
16
+ --acc2: #8b5cf6;
17
+ --good: #0a7a3d;
18
+ --avg: #c33300;
19
+ --bad: #c7221f;
20
+ --r: 14px;
21
+ }
22
+ @media(prefers-color-scheme:dark){
23
+ :root {
24
+ --bg: #0d0f13;
25
+ --card: #161920;
26
+ --fg: #eceef2;
27
+ --mut: #98a2b3;
28
+ --line: #262a33;
29
+ --soft: #1d212a;
30
+ --good: #3ddc84;
31
+ --avg: #ffb020;
32
+ --bad: #ff6b61;
33
+ }
34
+ }
35
+ * { box-sizing: border-box; }
36
+ body {
37
+ font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
38
+ max-width: 1180px;
39
+ margin: 0 auto;
40
+ padding: 2.2rem 1.2rem 4rem;
41
+ color: var(--fg);
42
+ background: var(--bg);
43
+ -webkit-font-smoothing: antialiased;
44
+ }
45
+ .nav {
46
+ display: flex;
47
+ gap: .6rem;
48
+ margin-bottom: 1.8rem;
49
+ border-bottom: 1px solid var(--line);
50
+ padding-bottom: .8rem;
51
+ }
52
+ .nav-link {
53
+ text-decoration: none;
54
+ font-weight: 600;
55
+ font-size: .9rem;
56
+ padding: .4rem .9rem;
57
+ border-radius: 8px;
58
+ color: var(--mut);
59
+ transition: .15s;
60
+ }
61
+ .nav-link:hover { color: var(--fg); background: var(--soft); }
62
+ .nav-link.active { color: #fff; background: linear-gradient(135deg, var(--acc), var(--acc2)); }
63
+
64
+ h1 {
65
+ font-size: 1.9rem;
66
+ letter-spacing: -.03em;
67
+ margin: 0 0 .4rem;
68
+ background: linear-gradient(90deg, var(--acc), var(--acc2));
69
+ -webkit-background-clip: text;
70
+ background-clip: text;
71
+ color: transparent;
72
+ }
73
+ p.subtitle {
74
+ margin: 0 0 1.5rem;
75
+ color: var(--mut);
76
+ font-size: .95rem;
77
+ }
78
+
79
+ .sources-grid {
80
+ display: grid;
81
+ grid-template-columns: 1fr 1fr;
82
+ gap: 1.2rem;
83
+ margin-bottom: 1.2rem;
84
+ }
85
+ @media(max-width: 760px){
86
+ .sources-grid { grid-template-columns: 1fr; }
87
+ }
88
+
89
+ .source-card {
90
+ background: var(--card);
91
+ border: 1px solid var(--line);
92
+ border-radius: var(--r);
93
+ padding: 1.2rem;
94
+ box-shadow: 0 1px 2px rgba(16,24,40,.04);
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: .9rem;
98
+ }
99
+ .card-header {
100
+ display: flex;
101
+ justify-content: space-between;
102
+ align-items: center;
103
+ }
104
+ .badge {
105
+ font-size: .75rem;
106
+ font-weight: 700;
107
+ text-transform: uppercase;
108
+ letter-spacing: .05em;
109
+ padding: .2rem .6rem;
110
+ border-radius: 99px;
111
+ }
112
+ .badge-ref { background: color-mix(in srgb, var(--acc) 18%, transparent); color: var(--acc); }
113
+ .badge-act { background: color-mix(in srgb, var(--acc2) 18%, transparent); color: var(--acc2); }
114
+
115
+ .mode-switch {
116
+ display: flex;
117
+ gap: .3rem;
118
+ background: var(--soft);
119
+ padding: .25rem;
120
+ border-radius: 8px;
121
+ }
122
+ .mode-btn {
123
+ border: 0;
124
+ background: transparent;
125
+ color: var(--mut);
126
+ font-size: .82rem;
127
+ font-weight: 600;
128
+ padding: .35rem .6rem;
129
+ border-radius: 6px;
130
+ cursor: pointer;
131
+ transition: .15s;
132
+ }
133
+ .mode-btn.active {
134
+ background: var(--card);
135
+ color: var(--fg);
136
+ box-shadow: 0 1px 3px rgba(0,0,0,.08);
137
+ }
138
+
139
+ label.field-lbl {
140
+ font-size: .8rem;
141
+ font-weight: 600;
142
+ color: var(--mut);
143
+ display: block;
144
+ margin-bottom: .3rem;
145
+ }
146
+ input[type=url], input[type=text], input[type=password], input[type=number], select {
147
+ width: 100%;
148
+ padding: .65rem .85rem;
149
+ font: inherit;
150
+ color: inherit;
151
+ background: var(--bg);
152
+ border: 1px solid var(--line);
153
+ border-radius: 10px;
154
+ outline: 0;
155
+ transition: .15s;
156
+ }
157
+ input[type=url]:focus, input[type=text]:focus, input[type=password]:focus, input[type=number]:focus, select:focus {
158
+ border-color: var(--acc);
159
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--acc) 22%, transparent);
160
+ }
161
+
162
+ .file-dropzone {
163
+ border: 2px dashed var(--line);
164
+ border-radius: 10px;
165
+ padding: 1.2rem 1rem;
166
+ text-align: center;
167
+ background: var(--bg);
168
+ cursor: pointer;
169
+ transition: .15s;
170
+ position: relative;
171
+ }
172
+ .file-dropzone:hover, .file-dropzone.dragover {
173
+ border-color: var(--acc);
174
+ background: color-mix(in srgb, var(--acc) 6%, transparent);
175
+ }
176
+ .file-dropzone input[type=file] {
177
+ position: absolute;
178
+ inset: 0;
179
+ opacity: 0;
180
+ cursor: pointer;
181
+ width: 100%;
182
+ height: 100%;
183
+ }
184
+ .file-preview {
185
+ display: none;
186
+ align-items: center;
187
+ gap: .8rem;
188
+ margin-top: .5rem;
189
+ padding: .5rem;
190
+ background: var(--soft);
191
+ border-radius: 8px;
192
+ }
193
+ .file-preview img {
194
+ width: 48px;
195
+ height: 48px;
196
+ object-fit: cover;
197
+ border-radius: 6px;
198
+ border: 1px solid var(--line);
199
+ }
200
+ .file-preview-info {
201
+ font-size: .8rem;
202
+ color: var(--mut);
203
+ text-align: left;
204
+ overflow: hidden;
205
+ }
206
+ .file-preview-info b {
207
+ display: block;
208
+ color: var(--fg);
209
+ white-space: nowrap;
210
+ overflow: hidden;
211
+ text-overflow: ellipsis;
212
+ }
213
+
214
+ .options-card {
215
+ background: var(--card);
216
+ border: 1px solid var(--line);
217
+ border-radius: var(--r);
218
+ padding: 1.2rem;
219
+ margin-bottom: 1.2rem;
220
+ box-shadow: 0 1px 2px rgba(16,24,40,.04);
221
+ }
222
+ .options-grid {
223
+ display: grid;
224
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
225
+ gap: 1rem;
226
+ align-items: end;
227
+ }
228
+ .custom-dim {
229
+ display: none;
230
+ grid-template-columns: 1fr 1fr;
231
+ gap: .5rem;
232
+ }
233
+
234
+ .threshold-wrap {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: .4rem;
238
+ }
239
+ .range-header {
240
+ display: flex;
241
+ justify-content: space-between;
242
+ font-size: .8rem;
243
+ color: var(--mut);
244
+ font-weight: 500;
245
+ }
246
+ input[type=range] {
247
+ width: 100%;
248
+ accent-color: var(--acc);
249
+ }
250
+
251
+ .checkbox-group {
252
+ display: flex;
253
+ flex-direction: column;
254
+ gap: .4rem;
255
+ font-size: .85rem;
256
+ color: var(--mut);
257
+ padding-top: .5rem;
258
+ }
259
+ .checkbox-group label {
260
+ display: flex;
261
+ align-items: center;
262
+ gap: .4rem;
263
+ cursor: pointer;
264
+ }
265
+ input[type=checkbox] { accent-color: var(--acc); }
266
+
267
+ .action-row {
268
+ display: flex;
269
+ justify-content: flex-end;
270
+ margin-top: 1.2rem;
271
+ }
272
+ .btn-submit {
273
+ padding: .75rem 1.8rem;
274
+ font: 600 1rem ui-sans-serif, system-ui, sans-serif;
275
+ border: 0;
276
+ border-radius: 10px;
277
+ background: linear-gradient(135deg, var(--acc), var(--acc2));
278
+ color: #fff;
279
+ cursor: pointer;
280
+ transition: .15s;
281
+ display: inline-flex;
282
+ align-items: center;
283
+ gap: .5rem;
284
+ }
285
+ .btn-submit:hover { filter: brightness(1.08); }
286
+ .btn-submit:active { transform: scale(.98); }
287
+ .btn-submit:disabled { opacity: .55; cursor: progress; }
288
+
289
+ /* Results Styling */
290
+ #results-container {
291
+ margin-top: 2rem;
292
+ animation: fadeIn .25s ease-in-out;
293
+ }
294
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
295
+
296
+ .stat-grid {
297
+ display: grid;
298
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
299
+ gap: .8rem;
300
+ margin-bottom: 1.2rem;
301
+ }
302
+ .stat-box {
303
+ background: var(--card);
304
+ border: 1px solid var(--line);
305
+ border-radius: var(--r);
306
+ padding: .9rem 1.1rem;
307
+ }
308
+ .stat-box small {
309
+ display: block;
310
+ color: var(--mut);
311
+ font-size: .8rem;
312
+ font-weight: 500;
313
+ margin-bottom: .2rem;
314
+ }
315
+ .stat-box .val {
316
+ font-size: 1.5rem;
317
+ font-weight: 700;
318
+ letter-spacing: -.02em;
319
+ }
320
+ .val-good { color: var(--good); }
321
+ .val-avg { color: var(--avg); }
322
+ .val-bad { color: var(--bad); }
323
+
324
+ .warning-box {
325
+ background: color-mix(in srgb, var(--avg) 12%, transparent);
326
+ border: 1px solid color-mix(in srgb, var(--avg) 35%, transparent);
327
+ color: var(--fg);
328
+ padding: .7rem 1rem;
329
+ border-radius: 10px;
330
+ font-size: .88rem;
331
+ margin-bottom: 1.2rem;
332
+ }
333
+
334
+ /* Diff Navigator Toolbar */
335
+ .diff-nav-bar {
336
+ background: var(--card);
337
+ border: 1px solid var(--line);
338
+ border-radius: var(--r);
339
+ padding: .8rem 1.1rem;
340
+ margin-bottom: 1.2rem;
341
+ display: flex;
342
+ flex-wrap: wrap;
343
+ align-items: center;
344
+ justify-content: space-between;
345
+ gap: .8rem;
346
+ }
347
+ .diff-nav-left {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: .7rem;
351
+ flex-wrap: wrap;
352
+ }
353
+ .diff-chip-group {
354
+ display: flex;
355
+ gap: .4rem;
356
+ overflow-x: auto;
357
+ max-width: 550px;
358
+ padding-bottom: .2rem;
359
+ }
360
+ .diff-chip {
361
+ border: 1px solid var(--line);
362
+ background: var(--soft);
363
+ color: var(--fg);
364
+ font-size: .78rem;
365
+ font-weight: 600;
366
+ padding: .25rem .6rem;
367
+ border-radius: 6px;
368
+ cursor: pointer;
369
+ white-space: nowrap;
370
+ transition: .15s;
371
+ }
372
+ .diff-chip:hover { border-color: var(--acc); background: color-mix(in srgb, var(--acc) 10%, transparent); }
373
+ .diff-chip.active { background: var(--acc); color: #fff; border-color: var(--acc); }
374
+
375
+ .diff-nav-btns {
376
+ display: flex;
377
+ gap: .3rem;
378
+ }
379
+ .btn-nav-step {
380
+ border: 1px solid var(--line);
381
+ background: var(--card);
382
+ color: var(--fg);
383
+ font-size: .82rem;
384
+ font-weight: 600;
385
+ padding: .3rem .7rem;
386
+ border-radius: 6px;
387
+ cursor: pointer;
388
+ transition: .15s;
389
+ }
390
+ .btn-nav-step:hover { background: var(--soft); }
391
+
392
+ .view-controls-bar {
393
+ display: flex;
394
+ justify-content: space-between;
395
+ align-items: center;
396
+ flex-wrap: wrap;
397
+ gap: .6rem;
398
+ margin-bottom: .8rem;
399
+ }
400
+ .view-tabs {
401
+ display: flex;
402
+ gap: .3rem;
403
+ background: var(--soft);
404
+ padding: .25rem;
405
+ border-radius: 8px;
406
+ }
407
+ .view-tab {
408
+ background: transparent;
409
+ border: 0;
410
+ font-size: .84rem;
411
+ font-weight: 600;
412
+ color: var(--mut);
413
+ padding: .35rem .75rem;
414
+ border-radius: 6px;
415
+ cursor: pointer;
416
+ transition: .15s;
417
+ }
418
+ .view-tab:hover { color: var(--fg); }
419
+ .view-tab.active { color: #fff; background: var(--acc); }
420
+
421
+ .zoom-controls {
422
+ display: flex;
423
+ gap: .3rem;
424
+ align-items: center;
425
+ }
426
+ .btn-zoom {
427
+ border: 1px solid var(--line);
428
+ background: var(--card);
429
+ color: var(--fg);
430
+ font-size: .78rem;
431
+ font-weight: 600;
432
+ padding: .25rem .55rem;
433
+ border-radius: 6px;
434
+ cursor: pointer;
435
+ }
436
+ .btn-zoom.active {
437
+ background: var(--fg);
438
+ color: var(--bg);
439
+ }
440
+
441
+ /* Synced Side-by-Side Panels */
442
+ .diff-wrapper {
443
+ position: relative;
444
+ display: flex;
445
+ gap: .6rem;
446
+ }
447
+ .diff-panels {
448
+ flex: 1;
449
+ display: grid;
450
+ grid-template-columns: 1fr 1fr 1fr;
451
+ gap: .8rem;
452
+ }
453
+ @media(max-width: 860px){
454
+ .diff-panels { grid-template-columns: 1fr; }
455
+ }
456
+
457
+ .diff-panel {
458
+ background: var(--card);
459
+ border: 1px solid var(--line);
460
+ border-radius: var(--r);
461
+ overflow: hidden;
462
+ display: flex;
463
+ flex-direction: column;
464
+ }
465
+ .panel-title {
466
+ padding: .5rem .8rem;
467
+ font-size: .8rem;
468
+ font-weight: 700;
469
+ color: var(--mut);
470
+ background: var(--soft);
471
+ border-bottom: 1px solid var(--line);
472
+ display: flex;
473
+ justify-content: space-between;
474
+ align-items: center;
475
+ }
476
+ .panel-body-scroll {
477
+ overflow-y: scroll;
478
+ max-height: 650px;
479
+ background: #fff;
480
+ scroll-behavior: smooth;
481
+ position: relative;
482
+ }
483
+ .panel-body-scroll.full-height {
484
+ max-height: none;
485
+ overflow-y: visible;
486
+ }
487
+ .panel-body-scroll img {
488
+ width: 100%;
489
+ height: auto;
490
+ display: block;
491
+ transform-origin: top left;
492
+ }
493
+
494
+ /* Minimap Heatstrip */
495
+ .minimap-bar {
496
+ width: 24px;
497
+ background: var(--card);
498
+ border: 1px solid var(--line);
499
+ border-radius: 8px;
500
+ position: relative;
501
+ cursor: pointer;
502
+ overflow: hidden;
503
+ }
504
+ .minimap-viewport-indicator {
505
+ position: absolute;
506
+ width: 100%;
507
+ background: rgba(91, 91, 240, 0.25);
508
+ border-top: 2px solid var(--acc);
509
+ border-bottom: 2px solid var(--acc);
510
+ pointer-events: none;
511
+ }
512
+ .minimap-marker {
513
+ position: absolute;
514
+ left: 2px;
515
+ right: 2px;
516
+ background: #ff3b30;
517
+ border-radius: 2px;
518
+ opacity: 0.85;
519
+ pointer-events: none;
520
+ }
521
+
522
+ /* Interactive Split Slider */
523
+ .split-wrap-outer {
524
+ max-height: 700px;
525
+ overflow-y: auto;
526
+ border-radius: var(--r);
527
+ border: 1px solid var(--line);
528
+ background: #000;
529
+ position: relative;
530
+ }
531
+ .split-viewer {
532
+ position: relative;
533
+ width: 100%;
534
+ overflow: hidden;
535
+ user-select: none;
536
+ -webkit-user-select: none;
537
+ }
538
+ .split-viewer img {
539
+ display: block;
540
+ width: 100%;
541
+ height: auto;
542
+ pointer-events: none;
543
+ }
544
+ .split-top {
545
+ position: absolute;
546
+ top: 0;
547
+ left: 0;
548
+ bottom: 0;
549
+ overflow: hidden;
550
+ border-right: 3px solid #fff;
551
+ box-shadow: 2px 0 10px rgba(0,0,0,.5);
552
+ }
553
+ .split-top img {
554
+ position: absolute;
555
+ top: 0;
556
+ left: 0;
557
+ height: 100%;
558
+ width: auto;
559
+ max-width: none;
560
+ }
561
+ .split-handle {
562
+ position: absolute;
563
+ top: 50%;
564
+ width: 40px;
565
+ height: 40px;
566
+ background: #fff;
567
+ border-radius: 50%;
568
+ box-shadow: 0 2px 10px rgba(0,0,0,.5);
569
+ transform: translate(-50%, -50%);
570
+ cursor: ew-resize;
571
+ display: flex;
572
+ align-items: center;
573
+ justify-content: center;
574
+ font-size: 16px;
575
+ font-weight: bold;
576
+ color: #111;
577
+ z-index: 10;
578
+ }
579
+ .split-labels {
580
+ position: sticky;
581
+ top: 12px;
582
+ display: flex;
583
+ justify-content: space-between;
584
+ pointer-events: none;
585
+ padding: 0 16px;
586
+ z-index: 20;
587
+ }
588
+ .split-lbl {
589
+ background: rgba(0,0,0,.75);
590
+ color: #fff;
591
+ font-size: .78rem;
592
+ font-weight: 600;
593
+ padding: .25rem .7rem;
594
+ border-radius: 6px;
595
+ }
596
+
597
+ /* Onion Skin */
598
+ .onion-skin-wrap {
599
+ background: var(--card);
600
+ border: 1px solid var(--line);
601
+ border-radius: var(--r);
602
+ padding: 1.2rem;
603
+ }
604
+ .onion-ctrl {
605
+ display: flex;
606
+ align-items: center;
607
+ gap: .8rem;
608
+ margin-bottom: .8rem;
609
+ font-size: .85rem;
610
+ }
611
+ .onion-scroll {
612
+ max-height: 700px;
613
+ overflow-y: auto;
614
+ position: relative;
615
+ border-radius: 8px;
616
+ border: 1px solid var(--line);
617
+ }
618
+ .onion-canvas {
619
+ position: relative;
620
+ width: 100%;
621
+ }
622
+ .onion-canvas img {
623
+ width: 100%;
624
+ display: block;
625
+ }
626
+ .onion-overlay {
627
+ position: absolute;
628
+ inset: 0;
629
+ }
630
+
631
+ .downloads {
632
+ display: flex;
633
+ flex-wrap: wrap;
634
+ gap: .6rem;
635
+ margin-top: 1.4rem;
636
+ }
637
+ .btn-outline {
638
+ font-size: .85rem;
639
+ font-weight: 600;
640
+ padding: .45rem .9rem;
641
+ border-radius: 8px;
642
+ border: 1px solid var(--line);
643
+ background: var(--card);
644
+ color: var(--fg);
645
+ text-decoration: none;
646
+ cursor: pointer;
647
+ transition: .15s;
648
+ display: inline-flex;
649
+ align-items: center;
650
+ gap: .4rem;
651
+ }
652
+ .btn-outline:hover { background: var(--soft); }
653
+
654
+ .msg-error {
655
+ background: color-mix(in srgb, var(--bad) 12%, transparent);
656
+ border: 1px solid color-mix(in srgb, var(--bad) 35%, transparent);
657
+ color: var(--bad);
658
+ padding: .8rem 1rem;
659
+ border-radius: 10px;
660
+ font-size: .9rem;
661
+ margin-top: 1.2rem;
662
+ }
663
+
664
+ .dom-section { margin-bottom: 1.5rem; }
665
+ .dom-toolbar { display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; margin:.8rem 0; }
666
+ .dom-toolbar input[type=text] { flex:1; min-width:180px; }
667
+ .dom-filter { border:1px solid var(--line); background:#fff; border-radius:8px; padding:.35rem .7rem; font-size:.8rem; font-weight:600; cursor:pointer; color:var(--mut); }
668
+ .dom-filter.active { background:var(--acc); border-color:var(--acc); color:#fff; }
669
+ .dom-list { max-height:560px; overflow:auto; border:1px solid var(--line); border-radius:12px; background:#fff; }
670
+ .dom-item { padding:.7rem 1rem; border-bottom:1px solid var(--line); font-size:.85rem; }
671
+ .dom-item:last-child { border-bottom:0; }
672
+ .dom-item-head { display:flex; gap:.5rem; align-items:center; flex-wrap:wrap; }
673
+ .dom-tag { font-family:ui-monospace,Menlo,monospace; font-weight:600; }
674
+ .dom-pill { font-size:.68rem; font-weight:700; padding:.1rem .5rem; border-radius:999px; text-transform:uppercase; }
675
+ .dom-pill.changed { background:#fef3c7; color:#92400e; }
676
+ .dom-pill.missing { background:#fee2e2; color:#991b1b; }
677
+ .dom-pill.extra { background:#dbeafe; color:#1e40af; }
678
+ .dom-text { color:var(--mut); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:420px; }
679
+ .dom-path { font-family:ui-monospace,Menlo,monospace; font-size:.72rem; color:var(--mut); margin-top:.15rem; word-break:break-all; }
680
+ .dom-diff-table { width:100%; border-collapse:collapse; margin-top:.5rem; font-size:.8rem; }
681
+ .dom-diff-table th { text-align:left; color:var(--mut); font-weight:600; padding:.2rem .5rem; }
682
+ .dom-diff-table td { padding:.2rem .5rem; font-family:ui-monospace,Menlo,monospace; }
683
+ .dom-diff-table td.ref { color:#166534; }
684
+ .dom-diff-table td.act { color:#b91c1c; }
685
+ .dom-swatch { display:inline-block; width:.8rem; height:.8rem; border-radius:3px; border:1px solid #0003; vertical-align:-1px; margin-right:.3rem; }
686
+
687
+ .sev { font-size:.68rem; font-weight:700; padding:.12rem .55rem; border-radius:999px; text-transform:uppercase; white-space:nowrap; }
688
+ .sev.high { background:#fee2e2; color:#991b1b; }
689
+ .sev.medium { background:#fef3c7; color:#92400e; }
690
+ .sev.low { background:#e0e7ff; color:#3730a3; }
691
+ .sev.info { background:#e5e7eb; color:#374151; }
692
+ .tabs { display:flex; gap:.3rem; flex-wrap:wrap; margin:1rem 0 .8rem; background:var(--soft); padding:.3rem; border-radius:10px; width:fit-content; }
693
+ .tab { border:0; background:transparent; padding:.45rem .9rem; border-radius:8px; font-weight:600; font-size:.85rem; color:var(--mut); cursor:pointer; }
694
+ .tab.active { background:var(--acc); color:#fff; }
695
+ .finding { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:.75rem 1rem; margin-bottom:.5rem; }
696
+ .finding-head { display:flex; gap:.6rem; align-items:center; flex-wrap:wrap; font-weight:600; }
697
+ .finding-detail { color:var(--mut); font-size:.86rem; margin-top:.25rem; }
698
+ .finding code { font-size:.78rem; background:var(--soft); padding:.05rem .35rem; border-radius:5px; }
699
+ .vp-title { font-size:1rem; margin:1.2rem 0 .5rem; }
700
+ .chk-row { display:flex; gap:1rem; flex-wrap:wrap; }
701
+ textarea { width:100%; font:inherit; padding:.6rem .8rem; border:1px solid var(--line); border-radius:10px; background:var(--soft); color:var(--fg); resize:vertical; min-height:84px; }
702
+ .hint { font-size:.78rem; color:var(--mut); margin-top:.3rem; }
703
+ .dtable { width:100%; border-collapse:collapse; margin-top:.5rem; font-size:.8rem; }
704
+ .dtable th { text-align:left; color:var(--mut); font-weight:600; padding:.2rem .5rem; }
705
+ .dtable td { padding:.2rem .5rem; font-family:ui-monospace,Menlo,monospace; }
706
+ .dtable td.ref { color:#166534; } .dtable td.act { color:#b91c1c; }
707
+ .empty { padding:1.2rem; color:var(--good); font-weight:600; background:var(--card); border:1px solid var(--line); border-radius:12px; }
708
+
709
+ .viewer { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; max-height:78vh; overflow:auto; border:1px solid var(--line); border-radius:12px; background:var(--card); padding:.6rem; align-items:start; }
710
+ .vcol-title { position:sticky; top:0; z-index:5; background:var(--card); font-weight:700; font-size:.8rem; padding:.3rem .2rem; color:var(--mut); }
711
+ .shot { position:relative; line-height:0; }
712
+ .shot img { width:100%; display:block; border:1px solid var(--line); }
713
+ .hl { position:absolute; border:2px solid #f43f5e; background:rgba(244,63,94,.14); border-radius:3px; pointer-events:none; }
714
+ .hl.spacing { border-color:#f59e0b; background:rgba(245,158,11,.16); }
715
+ .hl.flash { animation:flash 1.2s ease 2; z-index:4; }
716
+ @keyframes flash { 50% { background:rgba(244,63,94,.55); } }
717
+ .legend { display:flex; gap:1rem; font-size:.8rem; color:var(--mut); margin:.6rem 0; flex-wrap:wrap; }
718
+ .legend i { display:inline-block; width:.9rem; height:.9rem; border-radius:3px; margin-right:.35rem; vertical-align:-2px; }
719
+ .flt { border:1px solid var(--line); background:var(--card); border-radius:8px; padding:.35rem .7rem; font-size:.8rem; font-weight:600; cursor:pointer; color:var(--mut); }
720
+ .flt.active { background:var(--acc); border-color:var(--acc); color:#fff; }
721
+ .toolbar { display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; margin:.8rem 0; }
722
+ .toolbar input[type=text] { flex:1; min-width:180px; }
723
+ .dlist { max-height:520px; overflow:auto; }
724
+ .finding { cursor:pointer; }
725
+ .finding:hover { border-color:var(--acc); }
726
+ </style>
727
+ </head>
728
+ <body>
729
+
730
+ <nav class="nav">
731
+ <a href="/" class="nav-link">Accessibility Scanner</a>
732
+ <a href="/compare" class="nav-link">Visual Regression Comparison</a>
733
+ <a href="/compare-2" class="nav-link active">UI Differences</a>
734
+ <a href="/compare-3" class="nav-link">UI Completeness Check</a>
735
+ </nav>
736
+
737
+ <h1>UI Differences (Typography &amp; Spacing)</h1>
738
+ <p class="subtitle">Highlights where the target's <b>font, font size, weight, line height, letter spacing, word spacing and spacing (padding, margin, gap)</b> differ from the reference. Text, images and colours are ignored, so dynamic content is never flagged. Spelling mistakes on the target are listed separately.</p>
739
+
740
+ <form id="f">
741
+ <div class="sources-grid">
742
+ <div class="source-card">
743
+ <div class="card-header"><span class="badge badge-ref">Reference (Baseline)</span></div>
744
+ <label class="field-lbl" for="ref-url">Reference URL</label>
745
+ <input type="url" id="ref-url" placeholder="https://reference-site.example">
746
+ </div>
747
+ <div class="source-card">
748
+ <div class="card-header"><span class="badge badge-act">Actual (Target)</span></div>
749
+ <label class="field-lbl" for="tgt-url">Target URL</label>
750
+ <input type="url" id="tgt-url" placeholder="http://localhost:4015/">
751
+ </div>
752
+ </div>
753
+
754
+ <div class="options-card">
755
+ <div class="options-grid">
756
+ <div>
757
+ <label class="field-lbl" for="vp">Viewport</label>
758
+ <select id="vp">
759
+ <option value="1440x900" selected>Desktop (1440 × 900)</option>
760
+ <option value="1536x864">Desktop (1536 × 864)</option>
761
+ <option value="1920x1080">Desktop Full HD (1920 × 1080)</option>
762
+ <option value="1280x800">Laptop (1280 × 800)</option>
763
+ <option value="768x1024">Tablet (768 × 1024)</option>
764
+ <option value="390x844">Mobile (390 × 844)</option>
765
+ </select>
766
+ </div>
767
+ <div>
768
+ <label class="field-lbl" for="tol">Spacing tolerance (px)</label>
769
+ <input type="number" id="tol" min="0" max="50" step="0.5" value="2">
770
+ </div>
771
+ <div class="checkbox-group">
772
+ <label><input type="checkbox" id="do-spelling" checked> Spelling check (en-US / en-GB)</label>
773
+ </div>
774
+ </div>
775
+ <div style="margin-top:1rem">
776
+ <label class="field-lbl" for="custom-words">Custom words (brand, pharmacy and medical terms, comma or line separated)</label>
777
+ <textarea id="custom-words" placeholder="onecare, Gateshead, GPhC, Munir"></textarea>
778
+ </div>
779
+ <div style="margin-top:1rem">
780
+ <label class="field-lbl" for="ignore">Ignore elements (CSS selectors, comma-separated)</label>
781
+ <input type="text" id="ignore" placeholder=".cookie-banner, [data-dynamic]">
782
+ </div>
783
+ <div class="action-row">
784
+ <button type="submit" class="btn-submit" id="btn"><span>Run UI Comparison</span></button>
785
+ </div>
786
+ </div>
787
+ </form>
788
+
789
+ <div id="status-msg"></div>
790
+ <div id="results" style="display:none"></div>
791
+
792
+ <script>
793
+ const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
794
+ const PROP_LABEL = { fontFamily: 'Font family', fontSize: 'Font size', fontWeight: 'Font weight', lineHeight: 'Line height',
795
+ letterSpacing: 'Letter spacing', wordSpacing: 'Word spacing', paddingTop: 'Padding top', paddingRight: 'Padding right',
796
+ paddingBottom: 'Padding bottom', paddingLeft: 'Padding left', marginTop: 'Margin top', marginBottom: 'Margin bottom', gap: 'Gap' };
797
+ const form = document.getElementById('f');
798
+ const statusMsg = document.getElementById('status-msg');
799
+ const resultsEl = document.getElementById('results');
800
+ const btn = document.getElementById('btn');
801
+
802
+ form.addEventListener('submit', async e => {
803
+ e.preventDefault();
804
+ const referenceUrl = document.getElementById('ref-url').value.trim();
805
+ const targetUrl = document.getElementById('tgt-url').value.trim();
806
+ if (!referenceUrl || !targetUrl) return alert('Please enter both the Reference URL and the Target URL');
807
+ const [width, height] = document.getElementById('vp').value.split('x').map(Number);
808
+ btn.disabled = true;
809
+ resultsEl.style.display = 'none';
810
+ statusMsg.innerHTML = '<p style="color:var(--mut);font-size:.9rem;padding:1rem 0">Measuring both pages… (about 20–40 seconds)</p>';
811
+ try {
812
+ const res = await fetch('/api/compare-dom', {
813
+ method: 'POST',
814
+ headers: { 'Content-Type': 'application/json' },
815
+ body: JSON.stringify({
816
+ referenceUrl, targetUrl, viewport: { width, height },
817
+ tolerance: parseFloat(document.getElementById('tol').value) || 0,
818
+ customWords: document.getElementById('custom-words').value,
819
+ ignoreSelectors: document.getElementById('ignore').value,
820
+ spelling: document.getElementById('do-spelling').checked,
821
+ }),
822
+ });
823
+ const data = await res.json();
824
+ if (!res.ok || data.error) throw new Error(data.error || 'Comparison failed');
825
+ statusMsg.innerHTML = '';
826
+ render(data);
827
+ } catch (err) {
828
+ statusMsg.innerHTML = `<div class="msg-error"><b>Error:</b> ${esc(err.message)}</div>`;
829
+ } finally {
830
+ btn.disabled = false;
831
+ }
832
+ });
833
+
834
+ function boxHtml(rect, page, idx, cat) {
835
+ const l = rect.x / page.width * 100, t = rect.y / page.height * 100;
836
+ const w = rect.width / page.width * 100, h = rect.height / page.height * 100;
837
+ return `<div class="hl ${cat}" data-i="${idx}" style="left:${l}%;top:${t}%;width:${w}%;height:${h}%"></div>`;
838
+ }
839
+
840
+ function render(d) {
841
+ const dom = d.dom, s = dom.summary;
842
+ const stamp = Date.now();
843
+ const spell = d.spelling;
844
+
845
+ resultsEl.innerHTML = `
846
+ <h2>Results</h2>
847
+ <div class="stat-grid">
848
+ <div class="stat-box"><small>Elements with UI differences</small><div class="val ${s.changedElements ? 'val-bad' : 'val-good'}">${s.changedElements}</div></div>
849
+ <div class="stat-box"><small>Typography differences</small><div class="val">${s.typographyDiffs}</div></div>
850
+ <div class="stat-box"><small>Spacing differences (&gt;${s.tolerance}px)</small><div class="val">${s.spacingDiffs}</div></div>
851
+ <div class="stat-box"><small>Misspelled words</small><div class="val ${spell && spell.length ? 'val-avg' : 'val-good'}">${spell ? spell.length : '–'}</div></div>
852
+ </div>
853
+ <div style="font-size:.8rem;color:var(--mut)">${s.matched} of ${s.referenceElements} reference elements were paired with a target element by page structure.</div>
854
+ ${s.truncated ? '<div class="warning-box">Showing only the first 600 differences.</div>' : ''}
855
+ <div class="tabs">
856
+ <button type="button" class="tab active" data-tab="ui">UI Differences (${s.changedElements})</button>
857
+ <button type="button" class="tab" data-tab="spell">Spelling${spell ? ` (${spell.length})` : ''}</button>
858
+ </div>
859
+ <div id="p-ui"></div>
860
+ <div id="p-spell" style="display:none"></div>`;
861
+ resultsEl.style.display = 'block';
862
+
863
+ // --- Spelling panel ---
864
+ const ps = resultsEl.querySelector('#p-spell');
865
+ ps.innerHTML = !spell ? '<div class="empty" style="color:var(--mut)">Spelling check was turned off.</div>'
866
+ : spell.length ? spell.map(w => `<div class="finding" style="cursor:default"><div class="finding-head"><span class="sev medium">spelling</span><span>${esc(w.word)} → ${esc(w.suggestions[0] || 'no suggestion')}</span>${w.tag ? `<code>${esc(w.tag)}</code>` : ''}</div><div class="finding-detail">Wrong: “${esc(w.word)}”. ${w.suggestions.length ? 'Should be: ' + w.suggestions.map(x => `<code>${esc(x)}</code>`).join(' ') : 'No suggestion found.'}</div><div class="finding-detail">${esc(w.context)}</div></div>`).join('')
867
+ : '<div class="empty">✓ No spelling issues found</div>';
868
+
869
+ // --- UI panel ---
870
+ const pu = resultsEl.querySelector('#p-ui');
871
+ if (!dom.items.length) {
872
+ pu.innerHTML = '<div class="empty">✓ No typography or spacing differences found</div>';
873
+ } else {
874
+ const cat = it => it.diffs.some(x => x.kind === 'typography') ? 'typography' : 'spacing';
875
+ pu.innerHTML = `
876
+ <div class="legend"><span><i style="background:rgba(244,63,94,.5)"></i>Typography differs</span><span><i style="background:rgba(245,158,11,.55)"></i>Only spacing differs</span><span>Click a difference below to jump to it.</span></div>
877
+ <div class="viewer" id="viewer">
878
+ <div><div class="vcol-title">Reference (Baseline)</div>
879
+ <div class="shot"><img src="/${dom.reference.image}?t=${stamp}" alt="Reference">${dom.items.map((it, i) => boxHtml(it.refRect, dom.reference, i, cat(it))).join('')}</div></div>
880
+ <div><div class="vcol-title">Actual (Target)</div>
881
+ <div class="shot"><img src="/${dom.target.image}?t=${stamp}" alt="Target">${dom.items.map((it, i) => boxHtml(it.actRect, dom.target, i, cat(it))).join('')}</div></div>
882
+ </div>
883
+ <div class="toolbar">
884
+ <button type="button" class="flt active" data-f="all">All</button>
885
+ <button type="button" class="flt" data-f="typography">Typography</button>
886
+ <button type="button" class="flt" data-f="spacing">Spacing</button>
887
+ <input type="text" id="q" placeholder="Filter by element, text or property…">
888
+ </div>
889
+ <div class="dlist" id="dlist"></div>`;
890
+
891
+ let filter = 'all';
892
+ const list = pu.querySelector('#dlist');
893
+ const q = pu.querySelector('#q');
894
+ const viewer = pu.querySelector('#viewer');
895
+
896
+ function draw() {
897
+ const needle = q.value.trim().toLowerCase();
898
+ const rows = [];
899
+ dom.items.forEach((it, i) => {
900
+ const diffs = filter === 'all' ? it.diffs : it.diffs.filter(x => x.kind === filter);
901
+ if (!diffs.length) return;
902
+ const hay = `${it.label} ${it.text} ${diffs.map(x => PROP_LABEL[x.prop]).join(' ')}`.toLowerCase();
903
+ if (needle && !hay.includes(needle)) return;
904
+ rows.push(`<div class="finding" data-i="${i}">
905
+ <div class="finding-head"><span class="sev ${cat(it) === 'typography' ? 'high' : 'medium'}">${cat(it)}</span><code>${esc(it.label)}</code>${it.text ? `<span style="color:var(--mut);font-weight:400">“${esc(it.text)}”</span>` : ''}<span style="margin-left:auto;color:var(--mut);font-size:.75rem;font-weight:400">Y ${Math.round(it.y)}px</span></div>
906
+ <table class="dtable"><tr><th>Property</th><th>Reference</th><th>Target</th></tr>${diffs.map(x => `<tr><td>${PROP_LABEL[x.prop]}</td><td class="ref">${esc(x.ref)}</td><td class="act">${esc(x.act)}</td></tr>`).join('')}</table>
907
+ </div>`);
908
+ });
909
+ list.innerHTML = rows.join('') || '<div class="empty" style="color:var(--mut)">No matching differences.</div>';
910
+ }
911
+
912
+ // Hide boxes whose differences are filtered out
913
+ function syncBoxes() {
914
+ viewer.querySelectorAll('.hl').forEach(b => {
915
+ const it = dom.items[b.dataset.i];
916
+ b.style.display = (filter === 'all' || it.diffs.some(x => x.kind === filter)) ? '' : 'none';
917
+ });
918
+ }
919
+
920
+ pu.querySelectorAll('.flt').forEach(b => b.addEventListener('click', () => {
921
+ pu.querySelectorAll('.flt').forEach(x => x.classList.remove('active'));
922
+ b.classList.add('active');
923
+ filter = b.dataset.f;
924
+ draw(); syncBoxes();
925
+ }));
926
+ q.addEventListener('input', draw);
927
+ list.addEventListener('click', ev => {
928
+ const row = ev.target.closest('.finding');
929
+ if (!row) return;
930
+ const boxes = viewer.querySelectorAll(`.hl[data-i="${row.dataset.i}"]`);
931
+ if (!boxes.length) return;
932
+ viewer.scrollTo({ top: Math.max(0, boxes[0].offsetTop - 120), behavior: 'smooth' });
933
+ boxes.forEach(b => { b.classList.remove('flash'); void b.offsetWidth; b.classList.add('flash'); });
934
+ });
935
+ draw();
936
+ }
937
+
938
+ resultsEl.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
939
+ resultsEl.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x === t));
940
+ resultsEl.querySelector('#p-ui').style.display = t.dataset.tab === 'ui' ? '' : 'none';
941
+ resultsEl.querySelector('#p-spell').style.display = t.dataset.tab === 'spell' ? '' : 'none';
942
+ }));
943
+ }
944
+ </script>
945
+ </body>
946
+ </html>