@tekyzinc/gsd-t 2.73.11 → 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 +7 -0
- package/package.json +1 -1
- package/scripts/gsd-t-design-review-server.js +105 -0
- package/scripts/gsd-t-design-review.html +125 -42
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
|
|
5
12
|
## [2.73.11] - 2026-04-08
|
|
6
13
|
|
|
7
14
|
### Changed (reviewer — Playwright-first visual inspection)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "2.73.
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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";
|
|
942
|
+
statusClass = "changed";
|
|
868
943
|
} else if (itemChanges.length > 0) {
|
|
869
944
|
statusClass = "changed";
|
|
870
945
|
} else if (itemComment) {
|
|
871
|
-
statusClass = "rejected";
|
|
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 &&
|
|
963
|
+
if (selectedIdx >= 0 && filteredQueue[selectedIdx]) {
|
|
889
964
|
const comment = feedbackComment.value.trim();
|
|
890
|
-
if (comment) comments.set(
|
|
891
|
-
else comments.delete(
|
|
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 =
|
|
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
|
-
//
|
|
904
|
-
if (item.
|
|
905
|
-
|
|
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
|
|
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:
|
|
1012
|
+
selector: selector,
|
|
927
1013
|
}, "*");
|
|
928
1014
|
}
|
|
929
|
-
},
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 &&
|
|
1661
|
+
if (selectedIdx >= 0 && filteredQueue[selectedIdx]) {
|
|
1579
1662
|
const comment = feedbackComment.value.trim();
|
|
1580
|
-
if (comment) comments.set(
|
|
1581
|
-
else comments.delete(
|
|
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 =
|
|
1740
|
+
const compId = filteredQueue[selectedIdx]?.id;
|
|
1658
1741
|
if (compId) {
|
|
1659
1742
|
changes.delete(compId);
|
|
1660
1743
|
renderChanges(compId);
|