@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.
- package/README.md +5 -3
- package/commands/gsd-t-design-build.md +314 -0
- package/commands/gsd-t-design-review.md +155 -0
- package/commands/gsd-t-help.md +2 -0
- package/package.json +2 -2
- package/scripts/gsd-t-design-review-inject.js +941 -0
- package/scripts/gsd-t-design-review-server.js +388 -0
- package/scripts/gsd-t-design-review.html +1677 -0
- package/templates/CLAUDE-global.md +2 -0
|
@@ -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. "make border thinner" or "reduce gap to 8px"..."></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>
|