@tekyzinc/gsd-t 2.73.10 → 2.73.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [2.73.12] - 2026-04-08
6
+
7
+ ### Added (review UI — isolated component preview + tier tabs)
8
+ - **`/review/preview` endpoint** — mounts a single component in isolation via Vite module resolution. Framework-aware: auto-detects Vue/React/Svelte from package.json. Includes global styles and Vite HMR client. Components now render in the review iframe instead of showing a blank page.
9
+ - **Tier tabs** — Elements | Widgets | Pages tabs in the sidebar filter components by tier. Counts update as items are queued. All tab shows everything.
10
+ - **Framework detection** — review server reads project's package.json to determine mount strategy. Logs detected framework and global styles on startup.
11
+
12
+ ## [2.73.11] - 2026-04-08
13
+
14
+ ### Changed (reviewer — Playwright-first visual inspection)
15
+ - **Playwright is now the PRIMARY reviewer method** — every contract-specified visual property is verified via `getComputedStyle()` in a real browser. Code review demoted to supplement for non-visual concerns (props, events, accessibility). CSS box math (cascade, specificity, flex/grid computation, relative units) can only be verified at render time, not from source code.
16
+
5
17
  ## [2.73.10] - 2026-04-08
6
18
 
7
19
  ### Added (orchestrator — parallel execution)
@@ -365,25 +365,39 @@ ${componentList}
365
365
  ${measurementContext}
366
366
  ## Review Process
367
367
 
368
- **Step 1 — Code review (do this FIRST for ALL components):**
369
- For each component, read the design contract file and the source file. Check that every contract-specified value (colors, sizes, spacing, border-radius, font, layout, chart type, etc.) is correctly implemented in the code. This is your primary review — most issues are catchable from code alone.
370
-
371
- **Step 2 Playwright spot-check (do this AFTER code review):**
372
- Use Playwright to render components at http://localhost:${ports.reviewPort}/ and verify:
373
- - Components render without errors and have correct dimensions
374
- - Chart types, orientations, and data structures are correct
375
- - Interactive elements respond correctly (hover, click, states)
376
-
377
- Focus Playwright on components where code review raised concerns or where visual behavior can't be verified from code alone (e.g., SVG rendering, computed layouts). You do NOT need to re-measure every CSS property — the orchestrator already ran Playwright measurements above.
368
+ **Step 1 — Playwright visual inspection (PRIMARY — do this FIRST):**
369
+ Every UI component is visual and MUST be visually inspected. For each component:
370
+ 1. Open http://localhost:${ports.reviewPort}/ in Playwright
371
+ 2. Navigate to / render the component using its selector
372
+ 3. For EVERY contract-specified visual property, use \`page.locator(selector).evaluate(el => getComputedStyle(el))\` to measure the actual computed value:
373
+ - **Box model**: width, height, padding, margin, gap, border-width, border-radius
374
+ - **Colors**: background-color, color, border-color (compare computed rgb/rgba values)
375
+ - **Typography**: font-family, font-size, font-weight, line-height, letter-spacing
376
+ - **Layout**: display, flex-direction, justify-content, align-items, grid-template-columns
377
+ - **Spacing**: gap, row-gap, column-gap, padding (all sides), margin (all sides)
378
+ - **Visual**: opacity, box-shadow, overflow, text-overflow
379
+ 4. Compare each measured value against the contract specification
380
+ 5. Check that the component renders without console errors
381
+ 6. For charts/SVGs: verify chart type, orientation, data structure, and proportions visually
382
+
383
+ Code review alone CANNOT verify CSS box math — cascade, specificity, flex/grid computation, relative units, and parent constraints all affect the final rendered result. Only computed styles from a running browser are authoritative.
384
+
385
+ **Step 2 — Code review (supplement for non-visual concerns):**
386
+ Read the source file to check:
387
+ - Prop interfaces match contract definitions
388
+ - Event handlers and interactivity are wired correctly
389
+ - Data binding and state management are correct
390
+ - Accessibility attributes (aria-*, role) are present
391
+ - Component structure matches contract hierarchy
378
392
 
379
393
  ## Output Format
380
394
 
381
- Output your findings between these markers. Each issue must have component, severity (critical/high/medium/low), and description with SPECIFIC contract vs. actual values:
395
+ Output your findings between these markers. Each issue must have component, severity (critical/high/medium/low), and description with SPECIFIC contract value vs. actual computed value:
382
396
 
383
397
  [REVIEW_ISSUES]
384
398
  [
385
399
  {"component": "ComponentName", "severity": "critical", "description": "Contract specifies donut chart but rendered as pie chart (no inner radius)"},
386
- {"component": "ComponentName", "severity": "high", "description": "Grid gap: contract 16px, actual 24px"}
400
+ {"component": "ComponentName", "severity": "high", "description": "Grid gap: contract 16px, computed 24px"}
387
401
  ]
388
402
  [/REVIEW_ISSUES]
389
403
 
@@ -395,9 +409,10 @@ If ALL components match their contracts, output:
395
409
  ## CRITICAL — Output Rules
396
410
  - Output MUST contain the [REVIEW_ISSUES] markers — the orchestrator parses your result from these markers. Without them, your review is lost.
397
411
  - You write ZERO code. You ONLY review.
412
+ - You MUST run Playwright for every component. Skipping visual inspection is a review failure.
398
413
  - Be HARSH. Your value is in catching what the builder missed.
399
- - NEVER say "looks close" or "appears to match" — give SPECIFIC values.
400
- - Every contract property must be verified. Missing verification = missed issue.
414
+ - NEVER say "looks close" or "appears to match" — give SPECIFIC computed values.
415
+ - Every contract property must be verified via computed style. Missing verification = missed issue.
401
416
  - Severity guide: critical = wrong component type, missing element, broken render. high = wrong dimensions, colors, layout. medium = spacing/padding off. low = minor visual difference.`;
402
417
  }
403
418
 
@@ -421,15 +436,30 @@ function buildSingleItemReviewPrompt(phase, item, measurements, projectDir, port
421
436
  ${measurementContext}
422
437
  ## Review Process
423
438
 
424
- 1. Read the design contract file note every specified property value
425
- 2. Read the source filecheck that every contract-specified value is implemented correctly
426
- 3. If needed, use Playwright to render at http://localhost:${ports.reviewPort}/ and verify visual behavior
439
+ **Step 1 — Playwright visual inspection (PRIMARY):**
440
+ This is a UI componentvisual inspection is mandatory, not optional.
441
+ 1. Read the design contract file note every specified visual property value
442
+ 2. Open http://localhost:${ports.reviewPort}/ in Playwright
443
+ 3. Render the component using its selector
444
+ 4. For EVERY contract-specified visual property, measure the actual computed value:
445
+ \`page.locator(selector).evaluate(el => getComputedStyle(el))\`
446
+ - Box model: width, height, padding, margin, gap, border-width, border-radius
447
+ - Colors: background-color, color, border-color (compare computed rgb/rgba)
448
+ - Typography: font-family, font-size, font-weight, line-height
449
+ - Layout: display, flex-direction, justify-content, align-items
450
+ 5. Compare each computed value against the contract specification
451
+ 6. For charts/SVGs: verify type, orientation, proportions visually
452
+
453
+ Code review alone cannot verify CSS — cascade, specificity, flex/grid, and relative units all affect computed output.
454
+
455
+ **Step 2 — Code review (non-visual concerns):**
456
+ Read the source file to check prop interfaces, event handlers, data binding, and accessibility attributes.
427
457
 
428
458
  ## Output Format
429
459
 
430
460
  [REVIEW_ISSUES]
431
461
  [
432
- {"component": "${item.componentName}", "severity": "high", "description": "Contract specifies X, code has Y"}
462
+ {"component": "${item.componentName}", "severity": "high", "description": "Contract specifies X, computed value is Y"}
433
463
  ]
434
464
  [/REVIEW_ISSUES]
435
465
 
@@ -439,8 +469,9 @@ If the component matches its contract, output:
439
469
  [/REVIEW_ISSUES]
440
470
 
441
471
  ## Rules
472
+ - You MUST run Playwright. Skipping visual inspection is a review failure.
442
473
  - You write ZERO code. You ONLY review.
443
- - Be HARSH — specific values only, no "looks close."
474
+ - Be HARSH — specific computed values only, no "looks close."
444
475
  - Output MUST contain [REVIEW_ISSUES] markers.`;
445
476
  }
446
477
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "2.73.10",
3
+ "version": "2.73.12",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 56 slash commands with headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
@@ -26,6 +26,78 @@ const TARGET = getArg("target", "http://localhost:5173");
26
26
  const PROJECT_DIR = getArg("project", process.cwd());
27
27
  const REVIEW_DIR = path.join(PROJECT_DIR, ".gsd-t", "design-review");
28
28
 
29
+ // ── Framework detection ──────────────────────────────────────────────
30
+ function detectFramework() {
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_DIR, "package.json"), "utf8"));
33
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
34
+ if (deps.vue || deps["vue-router"]) return "vue";
35
+ if (deps.react || deps["react-dom"]) return "react";
36
+ if (deps.svelte) return "svelte";
37
+ if (deps["@angular/core"]) return "angular";
38
+ } catch { /* no package.json */ }
39
+ return "vue"; // default
40
+ }
41
+
42
+ function findGlobalStyles() {
43
+ const candidates = [
44
+ "src/assets/main.css", "src/assets/index.css", "src/assets/global.css",
45
+ "src/styles/main.css", "src/styles/index.css", "src/styles/global.css",
46
+ "src/index.css", "src/main.css", "src/app.css",
47
+ ];
48
+ return candidates.filter(f => fs.existsSync(path.join(PROJECT_DIR, f)));
49
+ }
50
+
51
+ const FRAMEWORK = detectFramework();
52
+ const GLOBAL_STYLES = findGlobalStyles();
53
+
54
+ function generatePreviewHtml(componentPath) {
55
+ const linkTags = GLOBAL_STYLES.map(s => ` <link rel="stylesheet" href="/${s}">`).join("\n");
56
+
57
+ let mountScript;
58
+ if (FRAMEWORK === "vue") {
59
+ mountScript = `
60
+ <script type="module">
61
+ import { createApp } from 'vue'
62
+ import Component from '/${componentPath}'
63
+ const app = createApp(Component)
64
+ app.mount('#app')
65
+ </script>`;
66
+ } else if (FRAMEWORK === "react") {
67
+ mountScript = `
68
+ <script type="module">
69
+ import React from 'react'
70
+ import { createRoot } from 'react-dom/client'
71
+ import Component from '/${componentPath}'
72
+ createRoot(document.getElementById('app')).render(React.createElement(Component))
73
+ </script>`;
74
+ } else {
75
+ mountScript = `<script type="module">import '/${componentPath}'</script>`;
76
+ }
77
+
78
+ return `<!DOCTYPE html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="UTF-8">
82
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
+ <title>Preview: ${path.basename(componentPath)}</title>
84
+ <script type="module" src="/@vite/client"></script>
85
+ ${linkTags}
86
+ <style>
87
+ * { margin: 0; padding: 0; box-sizing: border-box; }
88
+ body { background: #ffffff; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
89
+ #app { width: 100%; max-width: 800px; }
90
+ .preview-error { color: #ef4444; font-size: 14px; padding: 16px; border: 1px solid #ef4444; border-radius: 8px; }
91
+ </style>
92
+ </head>
93
+ <body>
94
+ <div id="app"></div>
95
+ ${mountScript}
96
+ <script src="/review/inject.js"></script>
97
+ </body>
98
+ </html>`;
99
+ }
100
+
29
101
  // ── Ensure coordination directory ─────────────────────────────────────
30
102
  function ensureDir(dir) {
31
103
  try { fs.mkdirSync(dir, { recursive: true }); } catch { /* exists */ }
@@ -263,6 +335,35 @@ const server = http.createServer((req, res) => {
263
335
  return;
264
336
  }
265
337
 
338
+ // Component preview — mounts a single component in isolation via Vite
339
+ if (pathname === "/review/preview") {
340
+ const component = parsed.query.component;
341
+ if (!component) {
342
+ res.writeHead(400, { "Content-Type": "text/plain" });
343
+ res.end("Missing ?component= parameter");
344
+ return;
345
+ }
346
+ // Proxy this HTML through Vite so module imports resolve correctly
347
+ const html = generatePreviewHtml(component);
348
+ // Send to Vite's HTML transform endpoint via proxy
349
+ const proxyOpts = {
350
+ hostname: targetUrl.hostname,
351
+ port: targetUrl.port,
352
+ path: "/__gsd_preview",
353
+ method: "GET",
354
+ headers: { ...req.headers, host: `${targetUrl.hostname}:${targetUrl.port}` },
355
+ };
356
+ // Vite won't know this path — serve directly but let browser resolve modules from Vite
357
+ const buf = Buffer.from(html, "utf8");
358
+ res.writeHead(200, {
359
+ "Content-Type": "text/html",
360
+ "Content-Length": buf.length,
361
+ "Cache-Control": "no-cache",
362
+ });
363
+ res.end(buf);
364
+ return;
365
+ }
366
+
266
367
  // ── Review API ──────────────────────────────────────────────────
267
368
  if (pathname === "/review/api/status") {
268
369
  res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
@@ -388,5 +489,9 @@ server.listen(PORT, () => {
388
489
  console.log(`${GREEN} ✓${RESET} Review UI: ${CYAN}http://localhost:${PORT}/review${RESET}`);
389
490
  console.log(`${GREEN} ✓${RESET} Proxying: ${DIM}${TARGET} → http://localhost:${PORT}/${RESET}`);
390
491
  console.log(`${GREEN} ✓${RESET} Project: ${DIM}${PROJECT_DIR}${RESET}`);
492
+ console.log(`${GREEN} ✓${RESET} Framework: ${DIM}${FRAMEWORK}${RESET}`);
493
+ if (GLOBAL_STYLES.length > 0) {
494
+ console.log(`${GREEN} ✓${RESET} Styles: ${DIM}${GLOBAL_STYLES.join(", ")}${RESET}`);
495
+ }
391
496
  console.log(`${DIM} Coordination: ${REVIEW_DIR}${RESET}\n`);
392
497
  });
@@ -127,6 +127,39 @@
127
127
  border-bottom: 1px solid var(--border);
128
128
  }
129
129
 
130
+ /* ── Tier tabs ──────────────────────────────────── */
131
+ .tier-tabs {
132
+ display: flex;
133
+ padding: 4px;
134
+ gap: 2px;
135
+ border-bottom: 1px solid var(--border);
136
+ flex-shrink: 0;
137
+ }
138
+
139
+ .tier-tab {
140
+ flex: 1;
141
+ padding: 5px 4px;
142
+ font-size: 11px;
143
+ font-weight: 600;
144
+ text-align: center;
145
+ border: none;
146
+ border-radius: 4px;
147
+ background: transparent;
148
+ color: var(--text-dim);
149
+ cursor: pointer;
150
+ transition: all 0.1s;
151
+ }
152
+
153
+ .tier-tab:hover { color: var(--text-muted); background: var(--bg-hover); }
154
+ .tier-tab.active { color: white; background: var(--accent); }
155
+
156
+ .tier-tab .tier-count {
157
+ font-size: 9px;
158
+ font-weight: 400;
159
+ opacity: 0.7;
160
+ margin-left: 2px;
161
+ }
162
+
130
163
  .component-list {
131
164
  flex: 1;
132
165
  overflow-y: auto;
@@ -687,6 +720,12 @@
687
720
  <!-- Left sidebar: component list -->
688
721
  <div class="sidebar">
689
722
  <div class="sidebar-header">Components</div>
723
+ <div class="tier-tabs" id="tier-tabs">
724
+ <button class="tier-tab active" data-tier="all">All</button>
725
+ <button class="tier-tab" data-tier="element">Elements</button>
726
+ <button class="tier-tab" data-tier="widget">Widgets</button>
727
+ <button class="tier-tab" data-tier="page">Pages</button>
728
+ </div>
690
729
  <div class="component-list" id="component-list">
691
730
  <div class="waiting-overlay" id="waiting-state">
692
731
  <div class="spinner"></div>
@@ -750,7 +789,9 @@
750
789
 
751
790
  // ── State ─────────────────────────────────────────
752
791
  let queue = [];
753
- let selectedIdx = -1;
792
+ let filteredQueue = []; // queue filtered by active tier
793
+ let activeTier = "all";
794
+ let selectedIdx = -1; // index into filteredQueue
754
795
  let inspectActive = false;
755
796
  const changes = new Map(); // componentId → [{path, property, oldValue, newValue}]
756
797
  const comments = new Map(); // componentId → string
@@ -783,6 +824,46 @@
783
824
  let currentTree = null;
784
825
  let selectedTreeKey = null;
785
826
  const propagateBadge = document.getElementById("propagate-badge");
827
+ const tierTabs = document.getElementById("tier-tabs");
828
+
829
+ // ── Tier tab handling ────────────────────────────
830
+ function filterByTier() {
831
+ if (activeTier === "all") {
832
+ filteredQueue = [...queue];
833
+ } else {
834
+ filteredQueue = queue.filter(item => item.type === activeTier);
835
+ }
836
+ }
837
+
838
+ function updateTierCounts() {
839
+ const counts = { all: queue.length, element: 0, widget: 0, page: 0 };
840
+ for (const item of queue) {
841
+ if (counts[item.type] !== undefined) counts[item.type]++;
842
+ }
843
+ tierTabs.querySelectorAll(".tier-tab").forEach(tab => {
844
+ const tier = tab.getAttribute("data-tier");
845
+ const count = counts[tier] || 0;
846
+ const label = tier === "all" ? "All" : tier.charAt(0).toUpperCase() + tier.slice(1) + "s";
847
+ tab.innerHTML = count > 0 ? `${label} <span class="tier-count">${count}</span>` : label;
848
+ // Hide tabs with 0 items (except All)
849
+ if (tier !== "all") {
850
+ tab.style.display = count > 0 ? "" : "none";
851
+ }
852
+ });
853
+ }
854
+
855
+ tierTabs.addEventListener("click", (e) => {
856
+ const tab = e.target.closest(".tier-tab");
857
+ if (!tab) return;
858
+ activeTier = tab.getAttribute("data-tier");
859
+ tierTabs.querySelectorAll(".tier-tab").forEach(t => t.classList.remove("active"));
860
+ tab.classList.add("active");
861
+ selectedIdx = -1;
862
+ filterByTier();
863
+ renderComponentList();
864
+ updateSubmitStats();
865
+ if (filteredQueue.length > 0) selectComponent(0);
866
+ });
786
867
 
787
868
  // ── SSE connection ────────────────────────────────
788
869
  function connectSSE() {
@@ -836,21 +917,15 @@
836
917
  submitBar.style.display = "flex";
837
918
  feedbackPanel.style.display = "block";
838
919
  }
920
+ filterByTier();
921
+ updateTierCounts();
839
922
  renderComponentList();
840
923
  updateSubmitStats();
841
924
 
842
925
  // Auto-select first if none selected
843
- if (selectedIdx < 0 && queue.length > 0) {
926
+ if (selectedIdx < 0 && filteredQueue.length > 0) {
844
927
  selectComponent(0);
845
928
  }
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
929
  }
855
930
 
856
931
  function renderComponentList() {
@@ -859,16 +934,16 @@
859
934
  if (ch !== waitingState) ch.remove();
860
935
  });
861
936
 
862
- queue.forEach((item, idx) => {
937
+ filteredQueue.forEach((item, idx) => {
863
938
  const itemChanges = changes.get(item.id) || [];
864
939
  const itemComment = comments.get(item.id) || "";
865
940
  let statusClass = "pending";
866
941
  if (itemChanges.length > 0 && itemComment) {
867
- statusClass = "changed"; // has both changes and comments
942
+ statusClass = "changed";
868
943
  } else if (itemChanges.length > 0) {
869
944
  statusClass = "changed";
870
945
  } else if (itemComment) {
871
- statusClass = "rejected"; // comment only = feedback
946
+ statusClass = "rejected";
872
947
  }
873
948
 
874
949
  const div = document.createElement("div");
@@ -885,53 +960,61 @@
885
960
 
886
961
  function selectComponent(idx) {
887
962
  // Save comment from current element before switching
888
- if (selectedIdx >= 0 && queue[selectedIdx]) {
963
+ if (selectedIdx >= 0 && filteredQueue[selectedIdx]) {
889
964
  const comment = feedbackComment.value.trim();
890
- if (comment) comments.set(queue[selectedIdx].id, comment);
891
- else comments.delete(queue[selectedIdx].id);
965
+ if (comment) comments.set(filteredQueue[selectedIdx].id, comment);
966
+ else comments.delete(filteredQueue[selectedIdx].id);
892
967
  }
893
968
 
894
969
  selectedIdx = idx;
895
970
  renderComponentList();
896
971
 
897
- const item = queue[idx];
972
+ const item = filteredQueue[idx];
898
973
  if (!item) return;
899
974
 
900
975
  // Always show component info in the inspector immediately
901
976
  renderComponentInfo(item);
902
977
 
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
978
+ // Load isolated component preview in iframe
979
+ if (item.sourcePath) {
980
+ const previewSrc = `/review/preview?component=${encodeURIComponent(item.sourcePath)}`;
981
+ previewIframe.src = previewSrc;
982
+ previewUrl.textContent = item.sourcePath;
983
+ }
984
+
985
+ // Auto-enable inspect mode and request tree after iframe loads
986
+ const onPreviewLoad = () => {
987
+ previewIframe.removeEventListener("load", onPreviewLoad);
988
+
906
989
  if (!inspectActive) {
907
990
  inspectActive = true;
908
991
  inspectToggle.classList.add("active");
909
992
  previewHint.textContent = "Hover to inspect, click to lock selection";
993
+ }
994
+
995
+ if (previewIframe.contentWindow) {
910
996
  previewIframe.contentWindow.postMessage({ type: "gsdt-activate" }, "*");
911
997
  }
912
- previewIframe.contentWindow.postMessage({
913
- type: "gsdt-scroll-to",
914
- selector: item.selector,
915
- }, "*");
916
998
 
917
- // Request component tree for hierarchical control
999
+ // Request component tree
918
1000
  currentTree = null;
919
1001
  selectedTreeKey = null;
920
1002
  inspectorTree.innerHTML = "";
921
- // Small delay to let scroll-to complete before querying tree
922
1003
  setTimeout(() => {
923
1004
  if (previewIframe.contentWindow) {
1005
+ const selector = item.selector || "#app > *";
1006
+ previewIframe.contentWindow.postMessage({
1007
+ type: "gsdt-scroll-to",
1008
+ selector: selector,
1009
+ }, "*");
924
1010
  previewIframe.contentWindow.postMessage({
925
1011
  type: "gsdt-get-tree",
926
- selector: item.selector,
1012
+ selector: selector,
927
1013
  }, "*");
928
1014
  }
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)
1015
+ }, 500);
1016
+ };
1017
+ previewIframe.addEventListener("load", onPreviewLoad);
935
1018
 
936
1019
  // Load existing comment for this element
937
1020
  feedbackComment.value = comments.get(item.id) || "";
@@ -1179,7 +1262,7 @@
1179
1262
  }
1180
1263
 
1181
1264
  // Check if this property was changed
1182
- const compId = queue[selectedIdx]?.id;
1265
+ const compId = filteredQueue[selectedIdx]?.id;
1183
1266
  const compChanges = changes.get(compId) || [];
1184
1267
  const existing = compChanges.find(c => c.path === currentElementPath && c.property === prop);
1185
1268
  if (existing) {
@@ -1327,7 +1410,7 @@
1327
1410
  // ── Property editing ──────────────────────────────
1328
1411
  function startEdit(valEl, prop) {
1329
1412
  const currentVal = valEl.getAttribute("data-original");
1330
- const compChanges = changes.get(queue[selectedIdx]?.id) || [];
1413
+ const compChanges = changes.get(filteredQueue[selectedIdx]?.id) || [];
1331
1414
  const existing = compChanges.find(c => c.path === currentElementPath && c.property === prop);
1332
1415
  const displayVal = existing ? existing.newValue : currentVal;
1333
1416
 
@@ -1361,7 +1444,7 @@
1361
1444
  }, "*");
1362
1445
 
1363
1446
  // Track the change
1364
- const compId = queue[selectedIdx]?.id;
1447
+ const compId = filteredQueue[selectedIdx]?.id;
1365
1448
  if (compId) {
1366
1449
  if (!changes.has(compId)) changes.set(compId, []);
1367
1450
  const list = changes.get(compId);
@@ -1445,7 +1528,7 @@
1445
1528
  // ── Ctrl/Cmd+Z to undo last change ──────────────────
1446
1529
  document.addEventListener("keydown", (e) => {
1447
1530
  if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1448
- const compId = queue[selectedIdx]?.id;
1531
+ const compId = filteredQueue[selectedIdx]?.id;
1449
1532
  if (!compId) return;
1450
1533
  const list = changes.get(compId);
1451
1534
  if (!list || list.length === 0) return;
@@ -1543,7 +1626,7 @@
1543
1626
 
1544
1627
  // ── Save comment on blur ────────────────────────────
1545
1628
  feedbackComment.addEventListener("blur", () => {
1546
- const item = queue[selectedIdx];
1629
+ const item = filteredQueue[selectedIdx];
1547
1630
  if (!item) return;
1548
1631
  const comment = feedbackComment.value.trim();
1549
1632
  if (comment) comments.set(item.id, comment);
@@ -1575,10 +1658,10 @@
1575
1658
 
1576
1659
  submitAll.addEventListener("click", async () => {
1577
1660
  // Save current element's comment
1578
- if (selectedIdx >= 0 && queue[selectedIdx]) {
1661
+ if (selectedIdx >= 0 && filteredQueue[selectedIdx]) {
1579
1662
  const comment = feedbackComment.value.trim();
1580
- if (comment) comments.set(queue[selectedIdx].id, comment);
1581
- else comments.delete(queue[selectedIdx].id);
1663
+ if (comment) comments.set(filteredQueue[selectedIdx].id, comment);
1664
+ else comments.delete(filteredQueue[selectedIdx].id);
1582
1665
  }
1583
1666
 
1584
1667
  // Check for non-actionable comments (documentation, not change requests)
@@ -1654,7 +1737,7 @@
1654
1737
  });
1655
1738
 
1656
1739
  undoAllChanges.addEventListener("click", () => {
1657
- const compId = queue[selectedIdx]?.id;
1740
+ const compId = filteredQueue[selectedIdx]?.id;
1658
1741
  if (compId) {
1659
1742
  changes.delete(compId);
1660
1743
  renderChanges(compId);