@tekyzinc/gsd-t 2.70.16 → 2.71.10

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,1677 @@
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.0">
6
+ <title>GSD-T Design Review</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f172a;
10
+ --bg-surface: #1e293b;
11
+ --bg-hover: #334155;
12
+ --border: #334155;
13
+ --text: #f8fafc;
14
+ --text-muted: #94a3b8;
15
+ --text-dim: #64748b;
16
+ --accent: #3b82f6;
17
+ --accent-hover: #2563eb;
18
+ --green: #22c55e;
19
+ --red: #ef4444;
20
+ --orange: #f97316;
21
+ --yellow: #eab308;
22
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
23
+ --mono: 'JetBrains Mono', 'Fira Code', monospace;
24
+ }
25
+
26
+ * { margin: 0; padding: 0; box-sizing: border-box; }
27
+
28
+ body {
29
+ font-family: var(--font);
30
+ background: var(--bg);
31
+ color: var(--text);
32
+ height: 100vh;
33
+ overflow: hidden;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* ── Header ─────────────────────────────────────── */
39
+ .header {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ padding: 8px 16px;
44
+ background: var(--bg-surface);
45
+ border-bottom: 1px solid var(--border);
46
+ height: 48px;
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .header-left {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 12px;
54
+ }
55
+
56
+ .header h1 {
57
+ font-size: 14px;
58
+ font-weight: 600;
59
+ letter-spacing: -0.01em;
60
+ }
61
+
62
+ .phase-badge {
63
+ font-size: 11px;
64
+ padding: 2px 8px;
65
+ border-radius: 4px;
66
+ background: var(--accent);
67
+ color: white;
68
+ font-weight: 600;
69
+ text-transform: uppercase;
70
+ }
71
+
72
+ .header-right {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 8px;
76
+ }
77
+
78
+ .inspect-toggle {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 6px;
82
+ padding: 4px 10px;
83
+ border-radius: 6px;
84
+ border: 1px solid var(--border);
85
+ background: var(--bg);
86
+ color: var(--text-muted);
87
+ font-size: 12px;
88
+ cursor: pointer;
89
+ transition: all 0.15s;
90
+ }
91
+
92
+ .inspect-toggle.active {
93
+ background: var(--accent);
94
+ border-color: var(--accent);
95
+ color: white;
96
+ }
97
+
98
+ .inspect-toggle:hover {
99
+ border-color: var(--accent);
100
+ }
101
+
102
+ /* ── Main layout ────────────────────────────────── */
103
+ .main {
104
+ display: flex;
105
+ flex: 1;
106
+ overflow: hidden;
107
+ }
108
+
109
+ /* ── Sidebar (left) ─────────────────────────────── */
110
+ .sidebar {
111
+ width: 260px;
112
+ flex-shrink: 0;
113
+ background: var(--bg-surface);
114
+ border-right: 1px solid var(--border);
115
+ display: flex;
116
+ flex-direction: column;
117
+ overflow: hidden;
118
+ }
119
+
120
+ .sidebar-header {
121
+ padding: 12px 12px 8px;
122
+ font-size: 11px;
123
+ font-weight: 600;
124
+ color: var(--text-muted);
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.05em;
127
+ border-bottom: 1px solid var(--border);
128
+ }
129
+
130
+ .component-list {
131
+ flex: 1;
132
+ overflow-y: auto;
133
+ padding: 4px;
134
+ }
135
+
136
+ .component-item {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 8px;
140
+ padding: 8px 10px;
141
+ border-radius: 6px;
142
+ cursor: pointer;
143
+ font-size: 13px;
144
+ transition: background 0.1s;
145
+ position: relative;
146
+ }
147
+
148
+ .component-item:hover { background: var(--bg-hover); }
149
+ .component-item.selected { background: var(--accent); color: white; }
150
+
151
+ .component-status {
152
+ width: 8px;
153
+ height: 8px;
154
+ border-radius: 50%;
155
+ flex-shrink: 0;
156
+ }
157
+
158
+ .component-status.pending { background: var(--text-dim); }
159
+ .component-status.approved { background: var(--green); }
160
+ .component-status.rejected { background: var(--red); }
161
+ .component-status.changed { background: var(--orange); }
162
+
163
+ .component-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
164
+ .component-type { font-size: 10px; color: #94a3b8; }
165
+
166
+ /* ── Submit bar ─────────────────────────────────── */
167
+ .submit-bar {
168
+ padding: 12px;
169
+ border-top: 1px solid var(--border);
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 8px;
173
+ }
174
+
175
+ .submit-stats {
176
+ display: flex;
177
+ gap: 12px;
178
+ font-size: 11px;
179
+ color: var(--text-muted);
180
+ }
181
+
182
+ .submit-stats span { display: flex; align-items: center; gap: 4px; }
183
+
184
+ .btn {
185
+ padding: 8px 16px;
186
+ border-radius: 6px;
187
+ border: none;
188
+ font-size: 13px;
189
+ font-weight: 600;
190
+ cursor: pointer;
191
+ transition: all 0.15s;
192
+ text-align: center;
193
+ }
194
+
195
+ .btn-primary { background: var(--accent); color: white; }
196
+ .btn-primary:hover { background: var(--accent-hover); }
197
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
198
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
199
+ .btn-green { background: var(--green); color: white; }
200
+ .btn-green:hover { background: #16a34a; }
201
+ .btn-red { background: var(--red); color: white; }
202
+ .btn-red:hover { background: #dc2626; }
203
+ .btn-outline {
204
+ background: transparent;
205
+ color: var(--text-muted);
206
+ border: 1px solid var(--border);
207
+ }
208
+ .btn-outline:hover { border-color: var(--text); color: var(--text); }
209
+
210
+ /* ── Center: iframe ─────────────────────────────── */
211
+ .preview-pane {
212
+ flex: 1;
213
+ display: flex;
214
+ flex-direction: column;
215
+ overflow: hidden;
216
+ position: relative;
217
+ }
218
+
219
+ .preview-pane iframe {
220
+ flex: 1;
221
+ border: none;
222
+ background: white;
223
+ }
224
+
225
+ .preview-toolbar {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 8px;
229
+ padding: 6px 12px;
230
+ background: var(--bg);
231
+ border-bottom: 1px solid var(--border);
232
+ font-size: 12px;
233
+ color: var(--text-muted);
234
+ }
235
+
236
+ /* ── Right panel: property inspector ────────────── */
237
+ .inspector {
238
+ width: 320px;
239
+ flex-shrink: 0;
240
+ background: var(--bg-surface);
241
+ border-left: 1px solid var(--border);
242
+ display: flex;
243
+ flex-direction: column;
244
+ overflow: hidden;
245
+ }
246
+
247
+ .inspector-header {
248
+ padding: 10px 12px;
249
+ font-size: 11px;
250
+ font-weight: 600;
251
+ color: var(--text-muted);
252
+ text-transform: uppercase;
253
+ letter-spacing: 0.05em;
254
+ border-bottom: 1px solid var(--border);
255
+ display: flex;
256
+ justify-content: space-between;
257
+ align-items: center;
258
+ }
259
+
260
+ .inspector-body {
261
+ flex: 1;
262
+ overflow-y: auto;
263
+ padding: 8px;
264
+ }
265
+
266
+ .inspector-empty {
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ height: 100%;
271
+ color: var(--text-dim);
272
+ font-size: 13px;
273
+ text-align: center;
274
+ padding: 20px;
275
+ }
276
+
277
+ .element-info {
278
+ padding: 8px;
279
+ margin-bottom: 8px;
280
+ background: var(--bg);
281
+ border-radius: 6px;
282
+ font-family: var(--mono);
283
+ font-size: 11px;
284
+ color: #93c5fd;
285
+ word-break: break-all;
286
+ line-height: 1.5;
287
+ }
288
+
289
+ /* ── Box model diagram ──────────────────────────── */
290
+ .box-model {
291
+ margin: 8px 0 12px;
292
+ display: flex;
293
+ justify-content: center;
294
+ }
295
+
296
+ .box-model-diagram {
297
+ position: relative;
298
+ width: 220px;
299
+ height: 140px;
300
+ }
301
+
302
+ .box-margin {
303
+ position: absolute;
304
+ inset: 0;
305
+ background: rgba(249, 115, 22, 0.35);
306
+ border: 2px solid #f97316;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ }
311
+
312
+ .box-border-area {
313
+ position: absolute;
314
+ inset: 16px;
315
+ background: rgba(251, 191, 36, 0.35);
316
+ border: 2px solid #eab308;
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ }
321
+
322
+ .box-padding {
323
+ position: absolute;
324
+ inset: 30px;
325
+ background: rgba(34, 197, 94, 0.35);
326
+ border: 2px solid #22c55e;
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ }
331
+
332
+ .box-content {
333
+ position: absolute;
334
+ inset: 44px;
335
+ background: rgba(59, 130, 246, 0.35);
336
+ border: 2px solid #3b82f6;
337
+ display: flex;
338
+ align-items: center;
339
+ justify-content: center;
340
+ font-size: 11px;
341
+ color: #93c5fd;
342
+ font-weight: 600;
343
+ font-family: var(--mono);
344
+ }
345
+
346
+ .box-label {
347
+ position: absolute;
348
+ font-size: 10px;
349
+ font-weight: 600;
350
+ font-family: var(--mono);
351
+ color: #f8fafc;
352
+ text-shadow: 0 1px 3px rgba(0,0,0,0.8);
353
+ }
354
+
355
+ .box-label.top { top: 2px; left: 50%; transform: translateX(-50%); }
356
+ .box-label.bottom { bottom: 2px; left: 50%; transform: translateX(-50%); }
357
+ .box-label.left { left: 2px; top: 50%; transform: translateY(-50%); }
358
+ .box-label.right { right: 2px; top: 50%; transform: translateY(-50%); }
359
+
360
+ /* ── Property groups ────────────────────────────── */
361
+ .prop-group {
362
+ margin-bottom: 12px;
363
+ }
364
+
365
+ .prop-group-header {
366
+ font-size: 12px;
367
+ font-weight: 700;
368
+ color: #e2e8f0;
369
+ padding: 6px 8px;
370
+ cursor: pointer;
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 4px;
374
+ }
375
+
376
+ .prop-group-header:hover { color: #ffffff; }
377
+
378
+ .prop-row {
379
+ display: flex;
380
+ align-items: center;
381
+ padding: 3px 8px;
382
+ border-radius: 4px;
383
+ font-size: 12px;
384
+ gap: 4px;
385
+ }
386
+
387
+ .prop-row:hover { background: var(--bg-hover); }
388
+
389
+ .prop-name {
390
+ flex: 0 0 120px;
391
+ font-family: var(--mono);
392
+ font-size: 11px;
393
+ color: #cbd5e1;
394
+ overflow: hidden;
395
+ text-overflow: ellipsis;
396
+ white-space: nowrap;
397
+ cursor: pointer;
398
+ border-radius: 2px;
399
+ padding: 0 2px;
400
+ transition: background 0.1s;
401
+ }
402
+
403
+ .prop-name:hover { background: var(--bg-hover); color: #f8fafc; }
404
+
405
+ .prop-value {
406
+ flex: 1;
407
+ font-family: var(--mono);
408
+ font-size: 11px;
409
+ color: #ffffff;
410
+ font-weight: 500;
411
+ overflow: hidden;
412
+ text-overflow: ellipsis;
413
+ white-space: nowrap;
414
+ }
415
+
416
+ .prop-value.editable {
417
+ cursor: pointer;
418
+ padding: 1px 4px;
419
+ border-radius: 3px;
420
+ border: 1px solid transparent;
421
+ }
422
+
423
+ .prop-value.editable:hover {
424
+ border-color: var(--border);
425
+ background: var(--bg);
426
+ }
427
+
428
+ .prop-value.changed {
429
+ color: var(--orange);
430
+ font-weight: 600;
431
+ }
432
+
433
+ .prop-edit-input {
434
+ width: 100%;
435
+ background: var(--bg);
436
+ border: 1px solid var(--accent);
437
+ border-radius: 3px;
438
+ color: var(--text);
439
+ font-family: var(--mono);
440
+ font-size: 11px;
441
+ padding: 1px 4px;
442
+ outline: none;
443
+ }
444
+
445
+ .color-swatch {
446
+ display: inline-block;
447
+ width: 12px;
448
+ height: 12px;
449
+ border-radius: 2px;
450
+ border: 1px solid var(--border);
451
+ margin-right: 4px;
452
+ vertical-align: middle;
453
+ }
454
+
455
+ /* ── Changes tracker ────────────────────────────── */
456
+ .changes-section {
457
+ border-top: 1px solid var(--border);
458
+ padding: 8px;
459
+ flex-shrink: 0;
460
+ }
461
+
462
+ .changes-header {
463
+ font-size: 11px;
464
+ font-weight: 600;
465
+ color: var(--orange);
466
+ margin-bottom: 6px;
467
+ display: flex;
468
+ justify-content: space-between;
469
+ align-items: center;
470
+ }
471
+
472
+ .change-item {
473
+ font-family: var(--mono);
474
+ font-size: 10px;
475
+ padding: 2px 4px;
476
+ display: flex;
477
+ gap: 4px;
478
+ align-items: center;
479
+ }
480
+
481
+ .change-old { color: var(--red); text-decoration: line-through; }
482
+ .change-new { color: var(--green); }
483
+ .change-prop { color: var(--text-muted); }
484
+
485
+ /* ── Feedback panel ─────────────────────────────── */
486
+ .feedback-panel {
487
+ border-top: 1px solid var(--border);
488
+ padding: 12px;
489
+ flex-shrink: 0;
490
+ }
491
+
492
+ .feedback-actions {
493
+ display: flex;
494
+ gap: 8px;
495
+ margin-bottom: 8px;
496
+ }
497
+
498
+ .feedback-actions .btn { flex: 1; }
499
+
500
+ .feedback-comment {
501
+ width: 100%;
502
+ background: var(--bg);
503
+ border: 1px solid var(--border);
504
+ border-radius: 6px;
505
+ color: var(--text);
506
+ font-family: var(--font);
507
+ font-size: 12px;
508
+ padding: 8px;
509
+ resize: vertical;
510
+ min-height: 60px;
511
+ outline: none;
512
+ }
513
+
514
+ .feedback-comment:focus { border-color: var(--accent); }
515
+ .feedback-comment::placeholder { color: #94a3b8; }
516
+
517
+ /* ── Auto-review results ────────────────────────── */
518
+ .auto-review {
519
+ padding: 8px;
520
+ margin: 8px 0;
521
+ background: var(--bg);
522
+ border-radius: 6px;
523
+ border: 1px solid var(--border);
524
+ }
525
+
526
+ .auto-review-header {
527
+ font-size: 11px;
528
+ font-weight: 600;
529
+ color: var(--text-muted);
530
+ margin-bottom: 6px;
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 6px;
534
+ }
535
+
536
+ .auto-review-row {
537
+ display: flex;
538
+ justify-content: space-between;
539
+ font-size: 11px;
540
+ font-family: var(--mono);
541
+ padding: 2px 0;
542
+ }
543
+
544
+ .auto-review-row .pass { color: var(--green); }
545
+ .auto-review-row .fail { color: var(--red); }
546
+
547
+ /* ── Element tree ──────────────────────────────── */
548
+ .element-tree {
549
+ margin: 8px 0;
550
+ border: 1px solid var(--border);
551
+ border-radius: 6px;
552
+ background: var(--bg);
553
+ max-height: 240px;
554
+ overflow-y: auto;
555
+ }
556
+
557
+ .element-tree-header {
558
+ font-size: 11px;
559
+ font-weight: 600;
560
+ color: var(--text-muted);
561
+ padding: 6px 8px;
562
+ border-bottom: 1px solid var(--border);
563
+ display: flex;
564
+ justify-content: space-between;
565
+ align-items: center;
566
+ }
567
+
568
+ .tree-node {
569
+ display: flex;
570
+ align-items: center;
571
+ padding: 3px 8px;
572
+ cursor: pointer;
573
+ font-size: 11px;
574
+ font-family: var(--mono);
575
+ color: var(--text);
576
+ border-radius: 3px;
577
+ gap: 4px;
578
+ }
579
+
580
+ .tree-node:hover { background: var(--bg-hover); }
581
+ .tree-node.selected { background: var(--accent); color: white; }
582
+
583
+ .tree-toggle {
584
+ width: 14px;
585
+ text-align: center;
586
+ color: var(--text-dim);
587
+ flex-shrink: 0;
588
+ font-size: 10px;
589
+ user-select: none;
590
+ }
591
+
592
+ .tree-tag {
593
+ color: #93c5fd;
594
+ flex-shrink: 0;
595
+ }
596
+
597
+ .tree-node.selected .tree-tag { color: #dbeafe; }
598
+
599
+ .tree-label {
600
+ color: var(--text-muted);
601
+ overflow: hidden;
602
+ text-overflow: ellipsis;
603
+ white-space: nowrap;
604
+ flex: 1;
605
+ }
606
+
607
+ .tree-node.selected .tree-label { color: #e2e8f0; }
608
+
609
+ .tree-dims {
610
+ color: var(--text-dim);
611
+ font-size: 10px;
612
+ flex-shrink: 0;
613
+ }
614
+
615
+ .tree-node.selected .tree-dims { color: #bfdbfe; }
616
+
617
+ .tree-children {
618
+ padding-left: 12px;
619
+ }
620
+
621
+ .tree-children.collapsed { display: none; }
622
+
623
+ /* ── Scrollbar ──────────────────────────────────── */
624
+ ::-webkit-scrollbar { width: 6px; }
625
+ ::-webkit-scrollbar-track { background: transparent; }
626
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
627
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
628
+
629
+ /* ── Propagation badge ──────────────────────────── */
630
+ .propagate-badge {
631
+ font-size: 10px;
632
+ color: var(--green);
633
+ font-weight: 600;
634
+ padding: 2px 6px;
635
+ background: rgba(34, 197, 94, 0.1);
636
+ border-radius: 4px;
637
+ display: none;
638
+ }
639
+
640
+ /* ── Waiting state ──────────────────────────────── */
641
+ .waiting-overlay {
642
+ display: flex;
643
+ flex-direction: column;
644
+ align-items: center;
645
+ justify-content: center;
646
+ height: 100%;
647
+ gap: 16px;
648
+ color: var(--text-dim);
649
+ }
650
+
651
+ .waiting-overlay .spinner {
652
+ width: 32px;
653
+ height: 32px;
654
+ border: 3px solid var(--border);
655
+ border-top-color: var(--accent);
656
+ border-radius: 50%;
657
+ animation: spin 1s linear infinite;
658
+ }
659
+
660
+ @keyframes spin { to { transform: rotate(360deg); } }
661
+
662
+ .waiting-overlay p { font-size: 14px; }
663
+ .waiting-overlay .hint { font-size: 12px; color: var(--text-dim); }
664
+ </style>
665
+ </head>
666
+ <body>
667
+
668
+ <!-- ── Header ──────────────────────────────────────── -->
669
+ <div class="header">
670
+ <div class="header-left">
671
+ <h1>GSD-T Design Review</h1>
672
+ <span class="phase-badge" id="phase-badge">Elements</span>
673
+ </div>
674
+ <div class="header-right">
675
+ <button class="inspect-toggle" id="inspect-toggle" title="Toggle inspect mode (Ctrl+Shift+I)">
676
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
677
+ <path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z"/>
678
+ </svg>
679
+ Inspect
680
+ </button>
681
+ <span style="font-size:12px;color:var(--text-dim)" id="connection-status">Connecting...</span>
682
+ </div>
683
+ </div>
684
+
685
+ <!-- ── Main ────────────────────────────────────────── -->
686
+ <div class="main">
687
+ <!-- Left sidebar: component list -->
688
+ <div class="sidebar">
689
+ <div class="sidebar-header">Components</div>
690
+ <div class="component-list" id="component-list">
691
+ <div class="waiting-overlay" id="waiting-state">
692
+ <div class="spinner"></div>
693
+ <p>Waiting for components...</p>
694
+ <span class="hint">Builder terminal will queue items here</span>
695
+ </div>
696
+ </div>
697
+ <div class="submit-bar" id="submit-bar" style="display:none">
698
+ <div class="submit-stats" id="submit-stats"></div>
699
+ <button class="btn btn-primary" id="submit-all">Submit Review</button>
700
+ </div>
701
+ </div>
702
+
703
+ <!-- Center: preview iframe -->
704
+ <div class="preview-pane">
705
+ <div class="preview-toolbar">
706
+ <span id="preview-url">—</span>
707
+ <span style="flex:1"></span>
708
+ <span id="preview-hint" style="color:var(--text-dim)">Enable Inspect to hover-select elements</span>
709
+ </div>
710
+ <iframe id="preview-iframe" src="about:blank"></iframe>
711
+ </div>
712
+
713
+ <!-- Right panel: property inspector -->
714
+ <div class="inspector">
715
+ <div class="inspector-header">
716
+ <span>Properties</span>
717
+ <div style="display:flex;align-items:center;gap:6px">
718
+ <span class="propagate-badge" id="propagate-badge"></span>
719
+ <button class="btn btn-sm btn-outline" id="reset-styles" style="display:none">Reset</button>
720
+ </div>
721
+ </div>
722
+ <div class="inspector-body" id="inspector-body">
723
+ <div id="inspector-info"></div>
724
+ <div id="inspector-tree"></div>
725
+ <div id="inspector-props">
726
+ <div class="inspector-empty">
727
+ <div>
728
+ <p style="margin-bottom:8px">No element selected</p>
729
+ <p style="font-size:11px;color:var(--text-dim)">Enable Inspect mode, then hover and click an element in the preview</p>
730
+ </div>
731
+ </div>
732
+ </div>
733
+ </div>
734
+ <div class="changes-section" id="changes-section" style="display:none">
735
+ <div class="changes-header">
736
+ <span>Changes (<span id="changes-count">0</span>)</span>
737
+ <button class="btn btn-sm btn-outline" id="undo-all-changes">Undo All</button>
738
+ </div>
739
+ <div id="changes-list"></div>
740
+ </div>
741
+ <div class="feedback-panel" id="feedback-panel" style="display:none">
742
+ <textarea class="feedback-comment" id="feedback-comment" placeholder="Suggest changes, e.g. &quot;make border thinner&quot; or &quot;reduce gap to 8px&quot;..."></textarea>
743
+ </div>
744
+ </div>
745
+ </div>
746
+
747
+ <script>
748
+ (function() {
749
+ "use strict";
750
+
751
+ // ── State ─────────────────────────────────────────
752
+ let queue = [];
753
+ let selectedIdx = -1;
754
+ let inspectActive = false;
755
+ const changes = new Map(); // componentId → [{path, property, oldValue, newValue}]
756
+ const comments = new Map(); // componentId → string
757
+ let currentElementPath = null;
758
+ let currentStyles = null;
759
+
760
+ // ── DOM refs ──────────────────────────────────────
761
+ const componentList = document.getElementById("component-list");
762
+ const waitingState = document.getElementById("waiting-state");
763
+ const submitBar = document.getElementById("submit-bar");
764
+ const submitStats = document.getElementById("submit-stats");
765
+ const submitAll = document.getElementById("submit-all");
766
+ const phaseBadge = document.getElementById("phase-badge");
767
+ const inspectToggle = document.getElementById("inspect-toggle");
768
+ const previewIframe = document.getElementById("preview-iframe");
769
+ const previewUrl = document.getElementById("preview-url");
770
+ const previewHint = document.getElementById("preview-hint");
771
+ const inspectorBody = document.getElementById("inspector-body");
772
+ const changesSection = document.getElementById("changes-section");
773
+ const changesList = document.getElementById("changes-list");
774
+ const changesCount = document.getElementById("changes-count");
775
+ const feedbackPanel = document.getElementById("feedback-panel");
776
+ const feedbackComment = document.getElementById("feedback-comment");
777
+ const resetStyles = document.getElementById("reset-styles");
778
+ const undoAllChanges = document.getElementById("undo-all-changes");
779
+ const connectionStatus = document.getElementById("connection-status");
780
+ const inspectorInfo = document.getElementById("inspector-info");
781
+ const inspectorTree = document.getElementById("inspector-tree");
782
+ const inspectorProps = document.getElementById("inspector-props");
783
+ let currentTree = null;
784
+ let selectedTreeKey = null;
785
+ const propagateBadge = document.getElementById("propagate-badge");
786
+
787
+ // ── SSE connection ────────────────────────────────
788
+ function connectSSE() {
789
+ const evtSource = new EventSource("/review/api/events");
790
+ evtSource.addEventListener("init", (e) => {
791
+ const data = JSON.parse(e.data);
792
+ updateStatus(data.status);
793
+ updateQueue(data.queue);
794
+ connectionStatus.textContent = "Connected";
795
+ connectionStatus.style.color = "var(--green)";
796
+ });
797
+ evtSource.addEventListener("queue-update", (e) => {
798
+ updateQueue(JSON.parse(e.data));
799
+ });
800
+ evtSource.addEventListener("feedback-submitted", () => {
801
+ connectionStatus.textContent = "Feedback sent";
802
+ setTimeout(() => { connectionStatus.textContent = "Connected"; }, 2000);
803
+ });
804
+ evtSource.onerror = () => {
805
+ connectionStatus.textContent = "Disconnected";
806
+ connectionStatus.style.color = "var(--red)";
807
+ };
808
+ }
809
+
810
+ // ── Polling fallback ──────────────────────────────
811
+ async function pollQueue() {
812
+ try {
813
+ const res = await fetch("/review/api/queue");
814
+ const data = await res.json();
815
+ updateQueue(data);
816
+ } catch { /* retry */ }
817
+ try {
818
+ const res = await fetch("/review/api/status");
819
+ const data = await res.json();
820
+ updateStatus(data);
821
+ } catch { /* retry */ }
822
+ }
823
+
824
+ // ── Status ────────────────────────────────────────
825
+ function updateStatus(status) {
826
+ if (status.phase) {
827
+ phaseBadge.textContent = status.phase.charAt(0).toUpperCase() + status.phase.slice(1);
828
+ }
829
+ }
830
+
831
+ // ── Queue ─────────────────────────────────────────
832
+ function updateQueue(items) {
833
+ queue = items || [];
834
+ if (queue.length > 0) {
835
+ waitingState.style.display = "none";
836
+ submitBar.style.display = "flex";
837
+ feedbackPanel.style.display = "block";
838
+ }
839
+ renderComponentList();
840
+ updateSubmitStats();
841
+
842
+ // Auto-select first if none selected
843
+ if (selectedIdx < 0 && queue.length > 0) {
844
+ selectComponent(0);
845
+ }
846
+
847
+ // Load iframe with the app URL
848
+ if (queue.length > 0 && previewIframe.src === "about:blank") {
849
+ // Use the first component's route or default to "/"
850
+ const route = queue[0].route || "/";
851
+ previewIframe.src = route;
852
+ previewUrl.textContent = route;
853
+ }
854
+ }
855
+
856
+ function renderComponentList() {
857
+ // Remove existing items (keep waiting state)
858
+ Array.from(componentList.children).forEach(ch => {
859
+ if (ch !== waitingState) ch.remove();
860
+ });
861
+
862
+ queue.forEach((item, idx) => {
863
+ const itemChanges = changes.get(item.id) || [];
864
+ const itemComment = comments.get(item.id) || "";
865
+ let statusClass = "pending";
866
+ if (itemChanges.length > 0 && itemComment) {
867
+ statusClass = "changed"; // has both changes and comments
868
+ } else if (itemChanges.length > 0) {
869
+ statusClass = "changed";
870
+ } else if (itemComment) {
871
+ statusClass = "rejected"; // comment only = feedback
872
+ }
873
+
874
+ const div = document.createElement("div");
875
+ div.className = `component-item${idx === selectedIdx ? " selected" : ""}`;
876
+ div.innerHTML = `
877
+ <div class="component-status ${statusClass}"></div>
878
+ <div class="component-name">${item.name || item.id}</div>
879
+ <div class="component-type">${item.type || ""}</div>
880
+ `;
881
+ div.addEventListener("click", () => selectComponent(idx));
882
+ componentList.appendChild(div);
883
+ });
884
+ }
885
+
886
+ function selectComponent(idx) {
887
+ // Save comment from current element before switching
888
+ if (selectedIdx >= 0 && queue[selectedIdx]) {
889
+ const comment = feedbackComment.value.trim();
890
+ if (comment) comments.set(queue[selectedIdx].id, comment);
891
+ else comments.delete(queue[selectedIdx].id);
892
+ }
893
+
894
+ selectedIdx = idx;
895
+ renderComponentList();
896
+
897
+ const item = queue[idx];
898
+ if (!item) return;
899
+
900
+ // Always show component info in the inspector immediately
901
+ renderComponentInfo(item);
902
+
903
+ // Scroll iframe to the component if it has a selector
904
+ if (item.selector && previewIframe.contentWindow) {
905
+ // Auto-enable inspect mode so the element gets highlighted
906
+ if (!inspectActive) {
907
+ inspectActive = true;
908
+ inspectToggle.classList.add("active");
909
+ previewHint.textContent = "Hover to inspect, click to lock selection";
910
+ previewIframe.contentWindow.postMessage({ type: "gsdt-activate" }, "*");
911
+ }
912
+ previewIframe.contentWindow.postMessage({
913
+ type: "gsdt-scroll-to",
914
+ selector: item.selector,
915
+ }, "*");
916
+
917
+ // Request component tree for hierarchical control
918
+ currentTree = null;
919
+ selectedTreeKey = null;
920
+ inspectorTree.innerHTML = "";
921
+ // Small delay to let scroll-to complete before querying tree
922
+ setTimeout(() => {
923
+ if (previewIframe.contentWindow) {
924
+ previewIframe.contentWindow.postMessage({
925
+ type: "gsdt-get-tree",
926
+ selector: item.selector,
927
+ }, "*");
928
+ }
929
+ }, 300);
930
+ }
931
+
932
+ // Save comment from previous element before switching
933
+ const prevItem = selectedIdx >= 0 ? queue[selectedIdx] : null;
934
+ // (selectedIdx already updated above via renderComponentList, so use prevItem logic below)
935
+
936
+ // Load existing comment for this element
937
+ feedbackComment.value = comments.get(item.id) || "";
938
+
939
+ // Show component changes
940
+ renderChanges(item.id);
941
+ }
942
+
943
+ function renderComponentInfo(item) {
944
+ // Show component details in the info zone
945
+ inspectorInfo.innerHTML = "";
946
+ inspectorTree.innerHTML = "";
947
+ inspectorProps.innerHTML = "";
948
+
949
+ // Component header
950
+ const info = document.createElement("div");
951
+ info.className = "element-info";
952
+ info.innerHTML = `<strong>${item.name}</strong><br><span style="color:#94a3b8">${item.sourcePath || ""}</span>`;
953
+ inspectorInfo.appendChild(info);
954
+
955
+ // Auto-review results
956
+ if (item.measurements) {
957
+ renderAutoReview(item.measurements);
958
+ }
959
+
960
+ // AI review notes from Term 2
961
+ if (item.aiReview && item.aiReview.notes && item.aiReview.notes.length > 0) {
962
+ renderAIReview(item.aiReview);
963
+ }
964
+
965
+ // Hint
966
+ if (item.selector) {
967
+ const hint = document.createElement("div");
968
+ hint.style.cssText = "font-size:12px;color:#94a3b8;padding:8px;text-align:center;";
969
+ hint.textContent = "Click the element in the preview to inspect its properties";
970
+ inspectorProps.appendChild(hint);
971
+ }
972
+ }
973
+
974
+ // ── Inspect mode ──────────────────────────────────
975
+ inspectToggle.addEventListener("click", () => {
976
+ inspectActive = !inspectActive;
977
+ inspectToggle.classList.toggle("active", inspectActive);
978
+ previewHint.textContent = inspectActive
979
+ ? "Hover to inspect, click to lock selection"
980
+ : "Enable Inspect to hover-select elements";
981
+
982
+ if (previewIframe.contentWindow) {
983
+ previewIframe.contentWindow.postMessage({
984
+ type: inspectActive ? "gsdt-activate" : "gsdt-deactivate",
985
+ }, "*");
986
+ }
987
+ });
988
+
989
+ // Keyboard shortcut
990
+ document.addEventListener("keydown", (e) => {
991
+ if (e.ctrlKey && e.shiftKey && e.key === "I") {
992
+ e.preventDefault();
993
+ inspectToggle.click();
994
+ }
995
+ });
996
+
997
+ // ── Messages from iframe ──────────────────────────
998
+ window.addEventListener("message", (e) => {
999
+ const msg = e.data;
1000
+ if (!msg || !msg.type) return;
1001
+
1002
+ switch (msg.type) {
1003
+ case "gsdt-inject-ready":
1004
+ // Inject script is loaded, activate if inspect is on
1005
+ if (inspectActive && previewIframe.contentWindow) {
1006
+ previewIframe.contentWindow.postMessage({ type: "gsdt-activate" }, "*");
1007
+ }
1008
+ break;
1009
+
1010
+ case "gsdt-hover":
1011
+ currentElementPath = msg.path;
1012
+ currentStyles = msg.styles;
1013
+ renderInspector(msg);
1014
+ break;
1015
+
1016
+ case "gsdt-select":
1017
+ currentElementPath = msg.path;
1018
+ currentStyles = msg.styles;
1019
+ renderInspector(msg);
1020
+ resetStyles.style.display = "inline-block";
1021
+ break;
1022
+
1023
+ case "gsdt-style-updated":
1024
+ currentStyles = msg.styles;
1025
+ renderPropertyValues(msg.styles);
1026
+ // Show propagation feedback with scope
1027
+ if (msg.propagated > 0) {
1028
+ const scope = msg.propagateScope || "similar";
1029
+ propagateBadge.textContent = `→ ${msg.propagated} ${scope}`;
1030
+ propagateBadge.style.display = "inline-block";
1031
+ setTimeout(() => { propagateBadge.style.display = "none"; }, 2500);
1032
+ }
1033
+ break;
1034
+
1035
+ case "gsdt-tree":
1036
+ currentTree = msg.tree;
1037
+ renderTree(msg.tree);
1038
+ break;
1039
+ }
1040
+ });
1041
+
1042
+ // When iframe loads, re-activate inspect if needed
1043
+ previewIframe.addEventListener("load", () => {
1044
+ if (inspectActive && previewIframe.contentWindow) {
1045
+ previewIframe.contentWindow.postMessage({ type: "gsdt-activate" }, "*");
1046
+ }
1047
+ });
1048
+
1049
+ // ── Inspector rendering ───────────────────────────
1050
+ function renderInspector(msg) {
1051
+ const styles = msg.styles;
1052
+ const boxModel = msg.boxModel;
1053
+
1054
+ // Only update props zone — preserve tree
1055
+ inspectorProps.innerHTML = "";
1056
+
1057
+ // Element path
1058
+ const info = document.createElement("div");
1059
+ info.className = "element-info";
1060
+ info.textContent = msg.path || `${msg.tagName}.${msg.className}`;
1061
+ inspectorProps.appendChild(info);
1062
+
1063
+ // Box model diagram
1064
+ if (boxModel) {
1065
+ const bm = document.createElement("div");
1066
+ bm.className = "box-model";
1067
+ bm.innerHTML = `
1068
+ <div class="box-model-diagram">
1069
+ <div class="box-margin">
1070
+ <span class="box-label top" style="color:#fb923c">${boxModel.margin.top}</span>
1071
+ <span class="box-label bottom" style="color:#fb923c">${boxModel.margin.bottom}</span>
1072
+ <span class="box-label left" style="color:#fb923c">${boxModel.margin.left}</span>
1073
+ <span class="box-label right" style="color:#fb923c">${boxModel.margin.right}</span>
1074
+ </div>
1075
+ <div class="box-border-area">
1076
+ <span class="box-label top" style="color:#fbbf24">${boxModel.border.top}</span>
1077
+ </div>
1078
+ <div class="box-padding">
1079
+ <span class="box-label top" style="color:#4ade80">${boxModel.padding.top}</span>
1080
+ <span class="box-label bottom" style="color:#4ade80">${boxModel.padding.bottom}</span>
1081
+ <span class="box-label left" style="color:#4ade80">${boxModel.padding.left}</span>
1082
+ <span class="box-label right" style="color:#4ade80">${boxModel.padding.right}</span>
1083
+ </div>
1084
+ <div class="box-content">${boxModel.content.width}×${boxModel.content.height}</div>
1085
+ </div>
1086
+ `;
1087
+ inspectorProps.appendChild(bm);
1088
+ }
1089
+
1090
+ // Property groups
1091
+ const groups = {
1092
+ "Layout": ["display", "flexDirection", "alignItems", "justifyContent", "gap", "gridTemplateColumns", "gridTemplateRows"],
1093
+ "Size": ["width", "height"],
1094
+ "Spacing": ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "marginTop", "marginRight", "marginBottom", "marginLeft"],
1095
+ "Typography": ["fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign", "fontFamily"],
1096
+ "Visual": ["backgroundColor", "color", "borderRadius", "border", "boxShadow", "opacity"],
1097
+ "Position": ["position", "top", "left", "overflow"],
1098
+ };
1099
+
1100
+ const editableProps = new Set([
1101
+ "display", "flexDirection", "alignItems", "justifyContent", "gap",
1102
+ "gridTemplateColumns", "gridTemplateRows",
1103
+ "width", "height",
1104
+ "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
1105
+ "marginTop", "marginRight", "marginBottom", "marginLeft",
1106
+ "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign",
1107
+ "backgroundColor", "color", "borderRadius", "border", "opacity",
1108
+ ]);
1109
+
1110
+ for (const [groupName, props] of Object.entries(groups)) {
1111
+ const group = document.createElement("div");
1112
+ group.className = "prop-group";
1113
+
1114
+ const header = document.createElement("div");
1115
+ header.className = "prop-group-header";
1116
+ header.innerHTML = `<span>▸</span> ${groupName}`;
1117
+ group.appendChild(header);
1118
+
1119
+ const rows = document.createElement("div");
1120
+ rows.style.display = "block";
1121
+
1122
+ header.addEventListener("click", () => {
1123
+ const visible = rows.style.display !== "none";
1124
+ rows.style.display = visible ? "none" : "block";
1125
+ header.innerHTML = `<span>${visible ? "▸" : "▾"}</span> ${groupName}`;
1126
+ });
1127
+
1128
+ for (const prop of props) {
1129
+ const val = styles[prop];
1130
+ // Always show spacing props (margin/padding) even at 0px so they can be added
1131
+ const alwaysShow = new Set([
1132
+ "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
1133
+ "marginTop", "marginRight", "marginBottom", "marginLeft",
1134
+ "gap", "width", "height",
1135
+ ]);
1136
+ if (!alwaysShow.has(prop) && (!val || val === "none" || val === "normal" || val === "static" || val === "0px")) continue;
1137
+
1138
+ const row = document.createElement("div");
1139
+ row.className = "prop-row";
1140
+ row.setAttribute("data-prop", prop);
1141
+
1142
+ const nameEl = document.createElement("span");
1143
+ nameEl.className = "prop-name";
1144
+ // Show scope hint for table cells
1145
+ const selectedTag = msg.tagName;
1146
+ const columnScopeProps = ["textAlign", "width"];
1147
+ const rowScopeProps = ["height"];
1148
+ let scopeHint = "";
1149
+ if ((selectedTag === "td" || selectedTag === "th") && columnScopeProps.includes(prop)) {
1150
+ scopeHint = " ⟶column";
1151
+ } else if ((selectedTag === "td" || selectedTag === "th") && rowScopeProps.includes(prop)) {
1152
+ scopeHint = " ⟶row";
1153
+ }
1154
+ nameEl.innerHTML = prop + (scopeHint ? `<span style="color:var(--accent);font-size:9px">${scopeHint}</span>` : "");
1155
+ nameEl.title = scopeHint
1156
+ ? `Click to highlight. Changes propagate to ${scopeHint.includes("column") ? "entire column" : "all rows"}`
1157
+ : `Click to highlight ${prop} zone`;
1158
+ nameEl.style.cursor = "pointer";
1159
+ nameEl.addEventListener("click", () => {
1160
+ if (previewIframe.contentWindow) {
1161
+ previewIframe.contentWindow.postMessage({
1162
+ type: "gsdt-highlight-zone",
1163
+ property: prop,
1164
+ }, "*");
1165
+ }
1166
+ });
1167
+ row.appendChild(nameEl);
1168
+
1169
+ const valEl = document.createElement("span");
1170
+ valEl.className = "prop-value" + (editableProps.has(prop) ? " editable" : "");
1171
+ valEl.setAttribute("data-prop", prop);
1172
+ valEl.setAttribute("data-original", val);
1173
+
1174
+ // Color swatch for color properties
1175
+ if ((prop === "backgroundColor" || prop === "color") && val !== "transparent" && val !== "rgba(0, 0, 0, 0)") {
1176
+ valEl.innerHTML = `<span class="color-swatch" style="background:${val}"></span>${val}`;
1177
+ } else {
1178
+ valEl.textContent = val;
1179
+ }
1180
+
1181
+ // Check if this property was changed
1182
+ const compId = queue[selectedIdx]?.id;
1183
+ const compChanges = changes.get(compId) || [];
1184
+ const existing = compChanges.find(c => c.path === currentElementPath && c.property === prop);
1185
+ if (existing) {
1186
+ valEl.classList.add("changed");
1187
+ valEl.textContent = existing.newValue;
1188
+ }
1189
+
1190
+ if (editableProps.has(prop)) {
1191
+ valEl.addEventListener("click", () => startEdit(valEl, prop));
1192
+ }
1193
+
1194
+ row.appendChild(valEl);
1195
+ rows.appendChild(row);
1196
+ }
1197
+
1198
+ group.appendChild(rows);
1199
+ if (rows.children.length > 0) {
1200
+ inspectorProps.appendChild(group);
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ function renderPropertyValues(styles) {
1206
+ // Update existing property values in the inspector
1207
+ inspectorProps.querySelectorAll(".prop-value[data-prop]").forEach(el => {
1208
+ const prop = el.getAttribute("data-prop");
1209
+ if (styles[prop]) {
1210
+ const isColor = prop === "backgroundColor" || prop === "color";
1211
+ if (isColor && styles[prop] !== "transparent") {
1212
+ el.innerHTML = `<span class="color-swatch" style="background:${styles[prop]}"></span>${styles[prop]}`;
1213
+ } else {
1214
+ el.textContent = styles[prop];
1215
+ }
1216
+ }
1217
+ });
1218
+ }
1219
+
1220
+ // ── Tree rendering ────────────────────────────────
1221
+ function renderTree(tree) {
1222
+ inspectorTree.innerHTML = "";
1223
+ if (!tree) return;
1224
+
1225
+ const container = document.createElement("div");
1226
+ container.className = "element-tree";
1227
+
1228
+ const header = document.createElement("div");
1229
+ header.className = "element-tree-header";
1230
+ header.innerHTML = `<span>Element Tree</span><span style="color:var(--text-dim);font-weight:400">${countNodes(tree)} nodes</span>`;
1231
+ container.appendChild(header);
1232
+
1233
+ const treeBody = document.createElement("div");
1234
+ treeBody.style.padding = "2px";
1235
+ renderTreeNode(tree, treeBody, 0);
1236
+ container.appendChild(treeBody);
1237
+
1238
+ inspectorTree.appendChild(container);
1239
+ }
1240
+
1241
+ function countNodes(node) {
1242
+ let count = 1;
1243
+ if (node.children) {
1244
+ for (const child of node.children) count += countNodes(child);
1245
+ }
1246
+ return count;
1247
+ }
1248
+
1249
+ function renderTreeNode(node, parent, depth) {
1250
+ const row = document.createElement("div");
1251
+ row.className = "tree-node";
1252
+ row.style.paddingLeft = (8 + depth * 12) + "px";
1253
+
1254
+ const hasChildren = node.children && node.children.length > 0;
1255
+
1256
+ // Toggle arrow
1257
+ const toggle = document.createElement("span");
1258
+ toggle.className = "tree-toggle";
1259
+ toggle.textContent = hasChildren ? "▸" : " ";
1260
+ row.appendChild(toggle);
1261
+
1262
+ // Tag name
1263
+ const tagEl = document.createElement("span");
1264
+ tagEl.className = "tree-tag";
1265
+ tagEl.textContent = node.label;
1266
+ row.appendChild(tagEl);
1267
+
1268
+ // Dimensions
1269
+ const dims = document.createElement("span");
1270
+ dims.className = "tree-dims";
1271
+ dims.textContent = `${node.width}×${node.height}`;
1272
+ row.appendChild(dims);
1273
+
1274
+ // Auto-expand table structural nodes so columns are visible
1275
+ const tableStructural = new Set(["table", "thead", "tbody", "tfoot"]);
1276
+ const isHeaderRow = node.tag === "tr" && node.label.includes("hdr");
1277
+ const autoExpand = tableStructural.has(node.tag) || isHeaderRow || depth === 0;
1278
+
1279
+ // Children container
1280
+ let childrenEl = null;
1281
+ if (hasChildren) {
1282
+ childrenEl = document.createElement("div");
1283
+ childrenEl.className = autoExpand ? "tree-children" : "tree-children collapsed";
1284
+ for (const child of node.children) {
1285
+ renderTreeNode(child, childrenEl, depth + 1);
1286
+ }
1287
+ }
1288
+
1289
+ // Toggle arrow matches initial state
1290
+ if (hasChildren) {
1291
+ toggle.textContent = autoExpand ? "▾" : "▸";
1292
+ toggle.style.cursor = "pointer";
1293
+ toggle.addEventListener("click", (e) => {
1294
+ e.stopPropagation();
1295
+ const collapsed = childrenEl.classList.toggle("collapsed");
1296
+ toggle.textContent = collapsed ? "▸" : "▾";
1297
+ });
1298
+ }
1299
+
1300
+ // Click to select this sub-element
1301
+ row.addEventListener("click", () => {
1302
+ // Update tree selection
1303
+ const prev = inspectorTree.querySelector(".tree-node.selected");
1304
+ if (prev) prev.classList.remove("selected");
1305
+ row.classList.add("selected");
1306
+ selectedTreeKey = node.key;
1307
+
1308
+ // Tell iframe to select this element
1309
+ if (previewIframe.contentWindow) {
1310
+ previewIframe.contentWindow.postMessage({
1311
+ type: "gsdt-select-by-key",
1312
+ key: node.key,
1313
+ }, "*");
1314
+ }
1315
+
1316
+ // Expand parent if collapsed
1317
+ if (hasChildren && childrenEl.classList.contains("collapsed")) {
1318
+ childrenEl.classList.remove("collapsed");
1319
+ toggle.textContent = "▾";
1320
+ }
1321
+ });
1322
+
1323
+ parent.appendChild(row);
1324
+ if (childrenEl) parent.appendChild(childrenEl);
1325
+ }
1326
+
1327
+ // ── Property editing ──────────────────────────────
1328
+ function startEdit(valEl, prop) {
1329
+ const currentVal = valEl.getAttribute("data-original");
1330
+ const compChanges = changes.get(queue[selectedIdx]?.id) || [];
1331
+ const existing = compChanges.find(c => c.path === currentElementPath && c.property === prop);
1332
+ const displayVal = existing ? existing.newValue : currentVal;
1333
+
1334
+ const input = document.createElement("input");
1335
+ input.className = "prop-edit-input";
1336
+ input.type = "text";
1337
+ input.value = displayVal;
1338
+ let cancelled = false;
1339
+
1340
+ valEl.innerHTML = "";
1341
+ valEl.appendChild(input);
1342
+ input.focus();
1343
+ // Select just the numeric part for values like "24px", "1.5em", "600"
1344
+ const numMatch = displayVal.match(/^(-?[\d.]+)/);
1345
+ if (numMatch) {
1346
+ input.setSelectionRange(0, numMatch[1].length);
1347
+ } else {
1348
+ input.select();
1349
+ }
1350
+
1351
+ function commit() {
1352
+ if (cancelled) return;
1353
+ const newVal = input.value.trim();
1354
+ if (newVal && newVal !== currentVal) {
1355
+ // Apply the style change to the iframe
1356
+ previewIframe.contentWindow.postMessage({
1357
+ type: "gsdt-set-style",
1358
+ property: prop,
1359
+ value: newVal,
1360
+ propagate: true,
1361
+ }, "*");
1362
+
1363
+ // Track the change
1364
+ const compId = queue[selectedIdx]?.id;
1365
+ if (compId) {
1366
+ if (!changes.has(compId)) changes.set(compId, []);
1367
+ const list = changes.get(compId);
1368
+ const existingIdx = list.findIndex(c => c.path === currentElementPath && c.property === prop);
1369
+ const change = {
1370
+ path: currentElementPath,
1371
+ property: prop,
1372
+ oldValue: currentVal,
1373
+ newValue: newVal,
1374
+ };
1375
+ if (existingIdx >= 0) {
1376
+ list[existingIdx] = change;
1377
+ } else {
1378
+ list.push(change);
1379
+ }
1380
+ renderChanges(compId);
1381
+ renderComponentList();
1382
+ }
1383
+
1384
+ valEl.textContent = newVal;
1385
+ valEl.classList.add("changed");
1386
+ } else {
1387
+ valEl.textContent = existing ? existing.newValue : currentVal;
1388
+ if (existing) valEl.classList.add("changed");
1389
+ }
1390
+ }
1391
+
1392
+ input.addEventListener("blur", () => { if (!cancelled) commit(); });
1393
+ input.addEventListener("keydown", (e) => {
1394
+ if (e.key === "Enter") { commit(); cancelled = true; input.blur(); }
1395
+ if (e.key === "Escape") {
1396
+ cancelled = true;
1397
+ // Remove input first, then restore text (avoids blur→commit race)
1398
+ if (input.parentElement) input.remove();
1399
+ valEl.textContent = existing ? existing.newValue : currentVal;
1400
+ if (existing) valEl.classList.add("changed");
1401
+ }
1402
+ });
1403
+ }
1404
+
1405
+ // ── Undo single change ──────────────────────────────
1406
+ function undoChange(compId, index) {
1407
+ const list = changes.get(compId);
1408
+ if (!list || index < 0 || index >= list.length) return;
1409
+ const change = list[index];
1410
+
1411
+ // Revert in iframe
1412
+ if (previewIframe.contentWindow) {
1413
+ previewIframe.contentWindow.postMessage({
1414
+ type: "gsdt-set-style",
1415
+ property: change.property,
1416
+ value: change.oldValue,
1417
+ propagate: true,
1418
+ }, "*");
1419
+ }
1420
+
1421
+ // Remove from change list
1422
+ list.splice(index, 1);
1423
+ if (list.length === 0) changes.delete(compId);
1424
+
1425
+ // Update UI
1426
+ renderChanges(compId);
1427
+ renderComponentList();
1428
+ refreshPropHighlights(compId);
1429
+ }
1430
+
1431
+ // ── Clear .changed class on props that are no longer changed ──
1432
+ function refreshPropHighlights(compId) {
1433
+ const list = changes.get(compId) || [];
1434
+ inspectorProps.querySelectorAll(".prop-value.changed").forEach(el => {
1435
+ const prop = el.getAttribute("data-prop");
1436
+ const stillChanged = list.some(c => c.property === prop && c.path === currentElementPath);
1437
+ if (!stillChanged) {
1438
+ el.classList.remove("changed");
1439
+ const original = el.getAttribute("data-original");
1440
+ if (original) el.textContent = original;
1441
+ }
1442
+ });
1443
+ }
1444
+
1445
+ // ── Ctrl/Cmd+Z to undo last change ──────────────────
1446
+ document.addEventListener("keydown", (e) => {
1447
+ if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1448
+ const compId = queue[selectedIdx]?.id;
1449
+ if (!compId) return;
1450
+ const list = changes.get(compId);
1451
+ if (!list || list.length === 0) return;
1452
+ e.preventDefault();
1453
+ undoChange(compId, list.length - 1);
1454
+ }
1455
+ });
1456
+
1457
+ // ── Changes display ───────────────────────────────
1458
+ function renderChanges(compId) {
1459
+ const list = changes.get(compId) || [];
1460
+ if (list.length === 0) {
1461
+ changesSection.style.display = "none";
1462
+ return;
1463
+ }
1464
+
1465
+ changesSection.style.display = "block";
1466
+ changesCount.textContent = list.length;
1467
+ changesList.innerHTML = "";
1468
+
1469
+ list.forEach((change, idx) => {
1470
+ const div = document.createElement("div");
1471
+ div.className = "change-item";
1472
+
1473
+ const text = document.createElement("span");
1474
+ text.style.cssText = "flex:1;display:flex;gap:4px;align-items:center;overflow:hidden";
1475
+ text.innerHTML = `
1476
+ <span class="change-prop">${change.property}:</span>
1477
+ <span class="change-old">${change.oldValue}</span>
1478
+ <span>→</span>
1479
+ <span class="change-new">${change.newValue}</span>
1480
+ `;
1481
+
1482
+ const undoBtn = document.createElement("span");
1483
+ undoBtn.textContent = "✕";
1484
+ undoBtn.title = "Undo this change";
1485
+ undoBtn.style.cssText = "cursor:pointer;color:var(--text-dim);font-size:10px;padding:0 2px;flex-shrink:0";
1486
+ undoBtn.addEventListener("mouseenter", () => { undoBtn.style.color = "var(--red)"; });
1487
+ undoBtn.addEventListener("mouseleave", () => { undoBtn.style.color = "var(--text-dim)"; });
1488
+ undoBtn.addEventListener("click", () => undoChange(compId, idx));
1489
+
1490
+ div.appendChild(text);
1491
+ div.appendChild(undoBtn);
1492
+ changesList.appendChild(div);
1493
+ });
1494
+ }
1495
+
1496
+ // ── Auto-review display ───────────────────────────
1497
+ function renderAutoReview(measurements) {
1498
+ if (!measurements || measurements.length === 0) return;
1499
+
1500
+ const section = document.createElement("div");
1501
+ section.className = "auto-review";
1502
+
1503
+ const passCount = measurements.filter(m => m.pass).length;
1504
+ const total = measurements.length;
1505
+
1506
+ section.innerHTML = `
1507
+ <div class="auto-review-header">
1508
+ <span>${passCount === total ? "✅" : "⚠️"}</span>
1509
+ <span>Auto-Review: ${passCount}/${total} pass</span>
1510
+ </div>
1511
+ ${measurements.map(m => `
1512
+ <div class="auto-review-row">
1513
+ <span>${m.property}</span>
1514
+ <span class="${m.pass ? "pass" : "fail"}">${m.pass ? "✓" : "✗"} ${m.actual}</span>
1515
+ </div>
1516
+ `).join("")}
1517
+ `;
1518
+
1519
+ inspectorInfo.appendChild(section);
1520
+ }
1521
+
1522
+ function renderAIReview(aiReview) {
1523
+ const section = document.createElement("div");
1524
+ section.className = "auto-review";
1525
+ section.style.borderColor = "var(--accent)";
1526
+
1527
+ const verdict = aiReview.aiVerdict === "pass" ? "✅" : "⚠️";
1528
+ section.innerHTML = `
1529
+ <div class="auto-review-header">
1530
+ <span>${verdict}</span>
1531
+ <span>AI Review (Term 2)</span>
1532
+ </div>
1533
+ ${aiReview.notes.map(n => `
1534
+ <div class="auto-review-row">
1535
+ <span>${n.note}</span>
1536
+ <span class="${n.severity === "medium" || n.severity === "high" ? "fail" : "pass"}">${n.severity}</span>
1537
+ </div>
1538
+ `).join("")}
1539
+ `;
1540
+
1541
+ inspectorInfo.appendChild(section);
1542
+ }
1543
+
1544
+ // ── Save comment on blur ────────────────────────────
1545
+ feedbackComment.addEventListener("blur", () => {
1546
+ const item = queue[selectedIdx];
1547
+ if (!item) return;
1548
+ const comment = feedbackComment.value.trim();
1549
+ if (comment) comments.set(item.id, comment);
1550
+ else comments.delete(item.id);
1551
+ renderComponentList();
1552
+ updateSubmitStats();
1553
+ });
1554
+
1555
+ // ── Submit ────────────────────────────────────────
1556
+ function updateSubmitStats() {
1557
+ const total = queue.length;
1558
+ const changed = Array.from(changes.keys()).filter(id => (changes.get(id) || []).length > 0).length;
1559
+ const commented = comments.size;
1560
+
1561
+ if (changed > 0 || commented > 0) {
1562
+ submitStats.innerHTML = `
1563
+ <span><span class="component-status changed" style="display:inline-block"></span> ${changed} changed</span>
1564
+ <span><span class="component-status rejected" style="display:inline-block"></span> ${commented} commented</span>
1565
+ <span style="color:var(--text-dim)">${total} total</span>
1566
+ `;
1567
+ } else {
1568
+ submitStats.innerHTML = `<span style="color:var(--text-dim)">${total} elements — no changes</span>`;
1569
+ }
1570
+
1571
+ submitAll.textContent = changed > 0 || commented > 0
1572
+ ? `Submit (${changed} changes, ${commented} comments)`
1573
+ : "Submit — Approve All";
1574
+ }
1575
+
1576
+ submitAll.addEventListener("click", async () => {
1577
+ // Save current element's comment
1578
+ if (selectedIdx >= 0 && queue[selectedIdx]) {
1579
+ const comment = feedbackComment.value.trim();
1580
+ if (comment) comments.set(queue[selectedIdx].id, comment);
1581
+ else comments.delete(queue[selectedIdx].id);
1582
+ }
1583
+
1584
+ // Check for non-actionable comments (documentation, not change requests)
1585
+ const actionWords = /change|make|set|move|add|remove|reduce|increase|fix|use|switch|replace|adjust|align|center|should be|needs to|too |bigger|smaller|wider|narrower|thicker|thinner|lighter|darker|bolder|px|rem|%|#[0-9a-f]/i;
1586
+ const docComments = [];
1587
+ for (const [compId, comment] of comments) {
1588
+ if (!actionWords.test(comment)) {
1589
+ const item = queue.find(q => q.id === compId);
1590
+ docComments.push(item ? item.name : compId);
1591
+ }
1592
+ }
1593
+ if (docComments.length > 0) {
1594
+ const proceed = confirm(
1595
+ "These comments don't suggest specific changes:\n\n" +
1596
+ docComments.map(n => " \u2022 " + n).join("\n") +
1597
+ "\n\nComments should describe what to change, e.g.:\n" +
1598
+ ' "make padding 8px"\n "use darker blue"\n "reduce gap between title and chart"\n\n' +
1599
+ "Non-actionable comments will be discarded.\n\nSubmit anyway?"
1600
+ );
1601
+ if (!proceed) return;
1602
+ for (const [compId, comment] of comments) {
1603
+ if (!actionWords.test(comment)) comments.delete(compId);
1604
+ }
1605
+ }
1606
+
1607
+ // Build feedback: each element gets its changes and comments
1608
+ const feedback = queue.map(item => ({
1609
+ id: item.id,
1610
+ changes: changes.get(item.id) || [],
1611
+ comment: comments.get(item.id) || "",
1612
+ }));
1613
+
1614
+ try {
1615
+ const res = await fetch("/review/api/feedback", {
1616
+ method: "POST",
1617
+ headers: { "Content-Type": "application/json" },
1618
+ body: JSON.stringify(feedback),
1619
+ });
1620
+ if (res.ok) {
1621
+ const hasWork = feedback.some(f => f.changes.length > 0 || f.comment);
1622
+ submitAll.textContent = hasWork
1623
+ ? "Submitted — builder will apply changes..."
1624
+ : "Approved — moving to next step...";
1625
+ submitAll.disabled = true;
1626
+
1627
+ // Also send source changes
1628
+ const allChanges = [];
1629
+ for (const [compId, changeList] of changes) {
1630
+ for (const change of changeList) {
1631
+ allChanges.push({ componentId: compId, ...change });
1632
+ }
1633
+ }
1634
+ if (allChanges.length > 0) {
1635
+ await fetch("/review/api/write-source", {
1636
+ method: "POST",
1637
+ headers: { "Content-Type": "application/json" },
1638
+ body: JSON.stringify({ changes: allChanges }),
1639
+ });
1640
+ }
1641
+ }
1642
+ } catch (err) {
1643
+ submitAll.textContent = "Error — retry";
1644
+ submitAll.disabled = false;
1645
+ }
1646
+ });
1647
+
1648
+ // ── Reset/undo ────────────────────────────────────
1649
+ resetStyles.addEventListener("click", () => {
1650
+ if (previewIframe.contentWindow) {
1651
+ previewIframe.contentWindow.postMessage({ type: "gsdt-reset-styles" }, "*");
1652
+ }
1653
+ resetStyles.style.display = "none";
1654
+ });
1655
+
1656
+ undoAllChanges.addEventListener("click", () => {
1657
+ const compId = queue[selectedIdx]?.id;
1658
+ if (compId) {
1659
+ changes.delete(compId);
1660
+ renderChanges(compId);
1661
+ renderComponentList();
1662
+ refreshPropHighlights(compId);
1663
+ // Reset iframe styles
1664
+ if (previewIframe.contentWindow) {
1665
+ previewIframe.contentWindow.postMessage({ type: "gsdt-reset-styles" }, "*");
1666
+ }
1667
+ }
1668
+ });
1669
+
1670
+ // ── Init ──────────────────────────────────────────
1671
+ connectSSE();
1672
+ // Also poll periodically as backup
1673
+ setInterval(pollQueue, 5000);
1674
+ })();
1675
+ </script>
1676
+ </body>
1677
+ </html>