@tekyzinc/gsd-t 2.73.14 → 2.73.20
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 +45 -0
- package/bin/design-orchestrator.js +1 -1
- package/bin/orchestrator.js +20 -2
- package/commands/gsd-t-design-build.md +1 -1
- package/package.json +1 -1
- package/scripts/gsd-t-design-review-inject.js +141 -9
- package/scripts/gsd-t-design-review-server.js +174 -0
- package/scripts/gsd-t-design-review.html +469 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [2.73.20] - 2026-04-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Editable fixture data** — segment label, value, and color fields in the Data Props tree are now clickable to edit. Color fields use a color picker. Changes tracked alongside CSS/SVG changes in the review output.
|
|
9
|
+
- **Better SVG tree labels** — circle/arc nodes show stroke color, width, and radius. Path nodes show fill/stroke color. SVG root shows viewBox.
|
|
10
|
+
- **Deeper SVG tree traversal** — SVG subtrees traverse up to depth 8 (was 4), ensuring individual arc segments appear in the element tree.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
- **`percentages_shown`** from donut chart fixture — redundant with `segments[].value`.
|
|
14
|
+
|
|
15
|
+
## [2.73.19] - 2026-04-09
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **SVG attribute inspector** — SVG elements (`circle`, `path`, `rect`, `line`, `ellipse`, `text`, `g`, `polyline`, `polygon`) now show an "SVG Attributes" property group with all relevant attributes (stroke-width, r, fill, stroke, stroke-dasharray, viewBox, etc.). Attributes are editable inline with `setAttribute()`. Visual flash zones highlight stroke-width (blue), stroke/fill (matching color), dash patterns (amber dashed), and generic attrs (cyan).
|
|
19
|
+
- **SVG permitted value dropdowns** — `stroke-linecap`, `stroke-linejoin`, `text-anchor`, `dominant-baseline` show `<select>` dropdowns with valid SVG values.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Dropdown flash-on-click** — clicking a permitted-value `<select>` dropdown no longer re-triggers `startEdit()`, which was recreating the dropdown and causing it to flash on/off. Added re-entry guard checking for existing `select`/`input` inside the value element.
|
|
23
|
+
|
|
24
|
+
## [2.73.18] - 2026-04-09
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **Permitted value dropdowns** for enum CSS properties — `display`, `flexDirection`, `textAlign`, `alignItems`, `justifyContent`, `fontWeight`, `overflow`, `position` now show a `<select>` dropdown with valid options instead of a free-text input. Select commits on change.
|
|
28
|
+
- **Enhanced visual cue** — generic flashZone fallback now shows a bright blue outline with a computed-value label overlay. All property name clicks flash the element.
|
|
29
|
+
- **More editable properties** — added `overflow`, `position`, `top`, `left`, `boxShadow`, `fontFamily` to the editable set.
|
|
30
|
+
|
|
31
|
+
## [2.73.17] - 2026-04-08
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **Fixture data tree in property inspector** — when a component is selected, the inspector fetches its test fixture data from `/review/api/fixture` and renders it as an expandable tree. Shows columns, rows, segments, and all nested data with color swatches for hex values. Collapsible at every level.
|
|
35
|
+
|
|
36
|
+
## [2.73.16] - 2026-04-08
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- **Gallery view** — `/review/gallery?cols=N` renders all queued components in a grid layout, proxied through Vite. Vue error boundaries isolate per-component failures so one broken component doesn't crash the gallery. Gallery button in review UI header toggles between single-component and gallery views.
|
|
40
|
+
- **Fixture unwrapping** — when a contract test fixture wraps props in an array (e.g., `{cards: [{value, label}]}`) but the component expects flat props, the first item is auto-unwrapped. Fixes StatCardWithIcon rendering blank in preview.
|
|
41
|
+
|
|
42
|
+
## [2.73.15] - 2026-04-08
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **Reviewer output logging** — review and fix outputs now saved to `build-logs/` as `{phase}-review-{id}-c{cycle}.log` and `{phase}-fix-{id}-c{cycle}.log`. Enables auditing whether reviewers actually ran Playwright, what issues were found, and what fixes were applied.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- **Default parallelism now all-items** — per-item pipeline runs all items concurrently by default (was sequential). Bottleneck is API latency, not CPU/RAM. Use `--parallel N` to limit if needed.
|
|
49
|
+
|
|
5
50
|
## [2.73.14] - 2026-04-08
|
|
6
51
|
|
|
7
52
|
### Fixed (review UI — component preview rendering)
|
|
@@ -518,7 +518,7 @@ ${BOLD}Options:${RESET}
|
|
|
518
518
|
--skip-measure Skip Playwright measurement (human-review only)
|
|
519
519
|
--clean Clear all stale artifacts before starting
|
|
520
520
|
--verbose, -v Show Claude's tool calls and prompts in terminal
|
|
521
|
-
--parallel <N> Run N items concurrently (default:
|
|
521
|
+
--parallel <N> Run N items concurrently (default: all items in parallel)
|
|
522
522
|
--help Show this help
|
|
523
523
|
|
|
524
524
|
${BOLD}Pipeline:${RESET}
|
package/bin/orchestrator.js
CHANGED
|
@@ -164,7 +164,7 @@ ${BOLD}Options:${RESET}
|
|
|
164
164
|
--timeout <sec> Claude timeout per phase in seconds (default: 600)
|
|
165
165
|
--skip-measure Skip automated measurement (human-review only)
|
|
166
166
|
--clean Clear all artifacts from previous runs + delete build output
|
|
167
|
-
--parallel <N> Run N items concurrently (default:
|
|
167
|
+
--parallel <N> Run N items concurrently (default: all items in parallel)
|
|
168
168
|
--verbose, -v Show Claude's tool calls and prompts in terminal
|
|
169
169
|
--help Show this help
|
|
170
170
|
|
|
@@ -784,7 +784,7 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
784
784
|
// Each item is independent: 1 contract + 1 source = tiny context.
|
|
785
785
|
// With --parallel N, runs N items concurrently.
|
|
786
786
|
if (this.wf.buildSingleItemPrompt && this.wf.buildSingleItemReviewPrompt) {
|
|
787
|
-
const concurrency = opts.parallel ||
|
|
787
|
+
const concurrency = opts.parallel || items.length;
|
|
788
788
|
if (concurrency > 1) {
|
|
789
789
|
log(`\n${CYAN} ⚙${RESET} Building and reviewing ${items.length} ${phase} (${concurrency} parallel)...`);
|
|
790
790
|
} else {
|
|
@@ -819,6 +819,12 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
819
819
|
const reviewPrompt = this.wf.buildSingleItemReviewPrompt(phase, item, {}, projectDir, { devPort, reviewPort });
|
|
820
820
|
const reviewResult = await this.spawnClaudeAsync(projectDir, reviewPrompt, perItemTimeout, { label: `${phase}-review-${item.id}-c${cycle}` });
|
|
821
821
|
|
|
822
|
+
// Save review output for auditing (was the reviewer thorough? did it use Playwright?)
|
|
823
|
+
fs.writeFileSync(
|
|
824
|
+
path.join(buildLogDir, `${phase}-review-${item.id}-c${cycle}.log`),
|
|
825
|
+
`Exit code: ${reviewResult.exitCode}\nDuration: ${reviewResult.duration}s\n\n--- REVIEW OUTPUT ---\n${reviewResult.output.slice(0, 10000)}`
|
|
826
|
+
);
|
|
827
|
+
|
|
822
828
|
const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
|
|
823
829
|
const isKilled = [143, 137].includes(reviewResult.exitCode);
|
|
824
830
|
const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
|
|
@@ -849,6 +855,10 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
849
855
|
: this._defaultAutoFixPrompt(phase, itemIssues);
|
|
850
856
|
dim(` ${item.componentName}: fixing...`);
|
|
851
857
|
const fixResult = await this.spawnClaudeAsync(projectDir, fixPrompt, perItemTimeout, { label: `${phase}-fix-${item.id}-c${cycle}` });
|
|
858
|
+
fs.writeFileSync(
|
|
859
|
+
path.join(buildLogDir, `${phase}-fix-${item.id}-c${cycle}.log`),
|
|
860
|
+
`Exit code: ${fixResult.exitCode}\nDuration: ${fixResult.duration}s\n\n--- FIX OUTPUT ---\n${fixResult.output.slice(0, 10000)}`
|
|
861
|
+
);
|
|
852
862
|
if (fixResult.exitCode === 0) success(` ${item.componentName}: fixed (${fixResult.duration}s)`);
|
|
853
863
|
else warn(` ${item.componentName}: fix exit ${fixResult.exitCode}`);
|
|
854
864
|
}
|
|
@@ -924,6 +934,14 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
|
|
|
924
934
|
const reviewResult = this.spawnClaude(projectDir, reviewPrompt, Math.min(reviewTimeout, perItemTimeout), { label: `${phase}-review-c${autoReviewCycle}-${item.id}` });
|
|
925
935
|
totalDuration += reviewResult.duration;
|
|
926
936
|
|
|
937
|
+
// Save review output for auditing
|
|
938
|
+
const reviewLogDir = path.join(this.getReviewDir(projectDir), "build-logs");
|
|
939
|
+
ensureDir(reviewLogDir);
|
|
940
|
+
fs.writeFileSync(
|
|
941
|
+
path.join(reviewLogDir, `${phase}-review-c${autoReviewCycle}-${item.id}.log`),
|
|
942
|
+
`Exit code: ${reviewResult.exitCode}\nDuration: ${reviewResult.duration}s\n\n--- REVIEW OUTPUT ---\n${reviewResult.output.slice(0, 10000)}`
|
|
943
|
+
);
|
|
944
|
+
|
|
927
945
|
const isCrash = reviewResult.exitCode !== 0 && reviewResult.duration < 10;
|
|
928
946
|
const isKilled = [143, 137].includes(reviewResult.exitCode);
|
|
929
947
|
const isEmptyFail = reviewResult.exitCode !== 0 && !reviewResult.output.trim();
|
|
@@ -34,7 +34,7 @@ Pass any of these as `$ARGUMENTS`:
|
|
|
34
34
|
| `--timeout <sec>` | Claude timeout per tier in seconds (default: 600) |
|
|
35
35
|
| `--skip-measure` | Skip Playwright measurement (human-review only) |
|
|
36
36
|
| `--clean` | Clear all stale artifacts + delete build output before each phase |
|
|
37
|
-
| `--parallel <N>` | Run N items concurrently (default:
|
|
37
|
+
| `--parallel <N>` | Run N items concurrently (default: all items in parallel) |
|
|
38
38
|
| `--verbose`, `-v` | Show Claude's tool calls and prompts in terminal |
|
|
39
39
|
|
|
40
40
|
## Prerequisites
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "2.73.
|
|
3
|
+
"version": "2.73.20",
|
|
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",
|
|
@@ -333,12 +333,67 @@
|
|
|
333
333
|
backgroundColor: "rgba(148, 163, 184, 0.08)",
|
|
334
334
|
});
|
|
335
335
|
|
|
336
|
-
// ──
|
|
336
|
+
// ── SVG attribute zones (prefixed with "svg:") ──
|
|
337
|
+
} else if (property.startsWith("svg:")) {
|
|
338
|
+
const attr = property.slice(4);
|
|
339
|
+
if (attr === "stroke-width" || attr === "r" || attr === "rx" || attr === "ry") {
|
|
340
|
+
// Dimension attribute — blue outline with measurement label
|
|
341
|
+
addFlashDiv(rect.top - 2, rect.left - 2, rect.width + 4, rect.height + 4, {
|
|
342
|
+
border: "2px solid #3b82f6",
|
|
343
|
+
backgroundColor: "rgba(59, 130, 246, 0.15)",
|
|
344
|
+
borderRadius: "50%",
|
|
345
|
+
});
|
|
346
|
+
const val = el.getAttribute(attr) || "";
|
|
347
|
+
if (val) {
|
|
348
|
+
const lbl = addFlashDiv(rect.top - 18, rect.left, Math.max(val.length * 8, 40), 16, {
|
|
349
|
+
backgroundColor: "#1e293b", borderRadius: "3px",
|
|
350
|
+
fontSize: "10px", color: "#3b82f6", fontFamily: "monospace",
|
|
351
|
+
fontWeight: "600", textAlign: "center", lineHeight: "16px",
|
|
352
|
+
});
|
|
353
|
+
lbl.textContent = val;
|
|
354
|
+
}
|
|
355
|
+
} else if (attr === "stroke" || attr === "fill") {
|
|
356
|
+
// Color attribute — colored border matching the value
|
|
357
|
+
const color = el.getAttribute(attr) || getComputedStyle(el)[attr] || "#888";
|
|
358
|
+
addFlashDiv(rect.top - 3, rect.left - 3, rect.width + 6, rect.height + 6, {
|
|
359
|
+
border: "3px solid " + color,
|
|
360
|
+
backgroundColor: "transparent",
|
|
361
|
+
borderRadius: "2px",
|
|
362
|
+
});
|
|
363
|
+
} else if (attr === "stroke-dasharray" || attr === "stroke-dashoffset") {
|
|
364
|
+
// Dash pattern — dashed outline
|
|
365
|
+
addFlashDiv(rect.top - 2, rect.left - 2, rect.width + 4, rect.height + 4, {
|
|
366
|
+
border: "2px dashed #f59e0b",
|
|
367
|
+
backgroundColor: "rgba(245, 158, 11, 0.1)",
|
|
368
|
+
borderRadius: "2px",
|
|
369
|
+
});
|
|
370
|
+
} else {
|
|
371
|
+
// Generic SVG attr — cyan outline
|
|
372
|
+
addFlashDiv(rect.top - 2, rect.left - 2, rect.width + 4, rect.height + 4, {
|
|
373
|
+
border: "2px solid #06b6d4",
|
|
374
|
+
backgroundColor: "rgba(6, 182, 212, 0.1)",
|
|
375
|
+
borderRadius: "2px",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Generic fallback — bright outline + pulse for any unhandled property ──
|
|
337
380
|
} else {
|
|
338
|
-
addFlashDiv(rect.top, rect.left, rect.width, rect.height, {
|
|
339
|
-
backgroundColor: "rgba(59, 130, 246, 0.
|
|
340
|
-
border: "
|
|
381
|
+
addFlashDiv(rect.top - 2, rect.left - 2, rect.width + 4, rect.height + 4, {
|
|
382
|
+
backgroundColor: "rgba(59, 130, 246, 0.15)",
|
|
383
|
+
border: "2px solid rgba(59, 130, 246, 0.7)",
|
|
384
|
+
borderRadius: "4px",
|
|
341
385
|
});
|
|
386
|
+
// Property value label
|
|
387
|
+
const propVal = s.getPropertyValue(property.replace(/([A-Z])/g, "-$1").toLowerCase()) || s[property] || "";
|
|
388
|
+
if (propVal && propVal.length < 30) {
|
|
389
|
+
const lbl = addFlashDiv(rect.top - 20, rect.left, Math.max(propVal.length * 7, 50), 16, {
|
|
390
|
+
backgroundColor: "#1e293b", borderRadius: "3px",
|
|
391
|
+
fontSize: "10px", color: "#60a5fa", fontFamily: "monospace",
|
|
392
|
+
fontWeight: "600", textAlign: "center", lineHeight: "16px",
|
|
393
|
+
padding: "0 4px", whiteSpace: "nowrap",
|
|
394
|
+
});
|
|
395
|
+
lbl.textContent = propVal;
|
|
396
|
+
}
|
|
342
397
|
}
|
|
343
398
|
|
|
344
399
|
scheduleFlashFade();
|
|
@@ -408,6 +463,44 @@
|
|
|
408
463
|
};
|
|
409
464
|
}
|
|
410
465
|
|
|
466
|
+
// SVG attribute names to extract per element type
|
|
467
|
+
const SVG_ATTRS = {
|
|
468
|
+
svg: ["viewBox", "width", "height"],
|
|
469
|
+
circle: ["cx", "cy", "r", "stroke", "stroke-width", "fill", "opacity", "stroke-dasharray", "stroke-dashoffset", "transform"],
|
|
470
|
+
ellipse: ["cx", "cy", "rx", "ry", "stroke", "stroke-width", "fill", "opacity"],
|
|
471
|
+
rect: ["x", "y", "width", "height", "rx", "ry", "stroke", "stroke-width", "fill", "opacity"],
|
|
472
|
+
line: ["x1", "y1", "x2", "y2", "stroke", "stroke-width", "opacity"],
|
|
473
|
+
path: ["d", "stroke", "stroke-width", "fill", "opacity", "stroke-linecap", "stroke-linejoin", "transform"],
|
|
474
|
+
text: ["x", "y", "font-size", "font-weight", "fill", "text-anchor", "dominant-baseline"],
|
|
475
|
+
g: ["transform", "opacity", "fill", "stroke"],
|
|
476
|
+
polyline: ["points", "stroke", "stroke-width", "fill"],
|
|
477
|
+
polygon: ["points", "stroke", "stroke-width", "fill"],
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
function extractSvgAttrs(el) {
|
|
481
|
+
const tag = el.tagName.toLowerCase();
|
|
482
|
+
const attrNames = SVG_ATTRS[tag];
|
|
483
|
+
if (!attrNames) return null;
|
|
484
|
+
const attrs = {};
|
|
485
|
+
let hasAny = false;
|
|
486
|
+
for (const name of attrNames) {
|
|
487
|
+
const val = el.getAttribute(name);
|
|
488
|
+
if (val !== null && val !== undefined) {
|
|
489
|
+
attrs[name] = val;
|
|
490
|
+
hasAny = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Also grab computed stroke/fill from CSS if not in attributes
|
|
494
|
+
if (el instanceof SVGElement) {
|
|
495
|
+
const s = getComputedStyle(el);
|
|
496
|
+
if (!attrs["stroke"] && s.stroke && s.stroke !== "none") { attrs["stroke"] = s.stroke; hasAny = true; }
|
|
497
|
+
if (!attrs["fill"] && s.fill && s.fill !== "none") { attrs["fill"] = s.fill; hasAny = true; }
|
|
498
|
+
if (!attrs["stroke-width"] && s.strokeWidth) { attrs["stroke-width"] = s.strokeWidth; hasAny = true; }
|
|
499
|
+
if (!attrs["opacity"] && s.opacity !== "1") { attrs["opacity"] = s.opacity; hasAny = true; }
|
|
500
|
+
}
|
|
501
|
+
return hasAny ? attrs : null;
|
|
502
|
+
}
|
|
503
|
+
|
|
411
504
|
function extractBoxModel(el) {
|
|
412
505
|
const s = getComputedStyle(el);
|
|
413
506
|
const rect = el.getBoundingClientRect();
|
|
@@ -536,6 +629,7 @@
|
|
|
536
629
|
path: getElementPath(el),
|
|
537
630
|
styles: extractStyles(el),
|
|
538
631
|
boxModel: extractBoxModel(el),
|
|
632
|
+
svgAttrs: extractSvgAttrs(el),
|
|
539
633
|
tagName: el.tagName.toLowerCase(),
|
|
540
634
|
className: typeof el.className === "string" ? el.className : "",
|
|
541
635
|
textContent: (el.textContent || "").trim().substring(0, 100),
|
|
@@ -573,6 +667,7 @@
|
|
|
573
667
|
path: getElementPath(el),
|
|
574
668
|
styles: extractStyles(el),
|
|
575
669
|
boxModel: extractBoxModel(el),
|
|
670
|
+
svgAttrs: extractSvgAttrs(el),
|
|
576
671
|
tagName: el.tagName.toLowerCase(),
|
|
577
672
|
className: typeof el.className === "string" ? el.className : "",
|
|
578
673
|
textContent: (el.textContent || "").trim().substring(0, 100),
|
|
@@ -735,6 +830,7 @@
|
|
|
735
830
|
value: msg.value,
|
|
736
831
|
styles: extractStyles(lockedEl),
|
|
737
832
|
boxModel: extractBoxModel(lockedEl),
|
|
833
|
+
svgAttrs: extractSvgAttrs(lockedEl),
|
|
738
834
|
propagated: propagatedCount,
|
|
739
835
|
propagateScope,
|
|
740
836
|
}, "*");
|
|
@@ -760,6 +856,7 @@
|
|
|
760
856
|
path: getElementPath(lockedEl),
|
|
761
857
|
styles: extractStyles(lockedEl),
|
|
762
858
|
boxModel: extractBoxModel(lockedEl),
|
|
859
|
+
svgAttrs: extractSvgAttrs(lockedEl),
|
|
763
860
|
}, "*");
|
|
764
861
|
}
|
|
765
862
|
break;
|
|
@@ -801,7 +898,9 @@
|
|
|
801
898
|
let keyCounter = 0;
|
|
802
899
|
|
|
803
900
|
function buildTree(el, depth) {
|
|
804
|
-
|
|
901
|
+
// SVG subtrees get deeper traversal (arcs are nested)
|
|
902
|
+
const isSvgSubtree = el instanceof SVGElement || el.closest("svg");
|
|
903
|
+
if (depth > (isSvgSubtree ? 8 : 4)) return null;
|
|
805
904
|
const tag = el.tagName.toLowerCase();
|
|
806
905
|
const s = getComputedStyle(el);
|
|
807
906
|
const rect = el.getBoundingClientRect();
|
|
@@ -836,11 +935,20 @@
|
|
|
836
935
|
label = "Col " + colIdx;
|
|
837
936
|
if (text && text.length < 15) label += ' "' + text + '"';
|
|
838
937
|
} else if (tag === "svg") {
|
|
839
|
-
|
|
938
|
+
const vb = el.getAttribute("viewBox");
|
|
939
|
+
label = "svg" + (vb ? ` [${vb}]` : "");
|
|
840
940
|
} else if (tag === "circle") {
|
|
841
|
-
|
|
941
|
+
const stroke = el.getAttribute("stroke") || "";
|
|
942
|
+
const fill = el.getAttribute("fill") || "";
|
|
943
|
+
const color = stroke && stroke !== "none" ? stroke : fill && fill !== "none" ? fill : "";
|
|
944
|
+
const r = el.getAttribute("r") || "";
|
|
945
|
+
const sw = el.getAttribute("stroke-width") || "";
|
|
946
|
+
label = "arc" + (color ? " " + color : "") + (sw ? " w:" + sw : "") + (r ? " r:" + r : "");
|
|
842
947
|
} else if (tag === "path") {
|
|
843
|
-
|
|
948
|
+
const stroke = el.getAttribute("stroke") || "";
|
|
949
|
+
const fill = el.getAttribute("fill") || "";
|
|
950
|
+
const color = stroke && stroke !== "none" ? stroke : fill && fill !== "none" ? fill : "";
|
|
951
|
+
label = "path" + (color ? " " + color : "");
|
|
844
952
|
} else if (s.display === "flex" || s.display === "inline-flex") {
|
|
845
953
|
label = "flex " + (s.flexDirection === "column" ? "col" : "row");
|
|
846
954
|
} else if (s.display === "grid") {
|
|
@@ -876,10 +984,16 @@
|
|
|
876
984
|
node.props.fontWeight = s.fontWeight;
|
|
877
985
|
if (tag === "th") node.props.background = s.backgroundColor;
|
|
878
986
|
}
|
|
879
|
-
if (tag === "circle") {
|
|
987
|
+
if (tag === "circle" || tag === "ellipse") {
|
|
880
988
|
node.props.stroke = el.getAttribute("stroke");
|
|
881
989
|
node.props.strokeWidth = el.getAttribute("stroke-width");
|
|
882
990
|
node.props.r = el.getAttribute("r");
|
|
991
|
+
node.props.fill = el.getAttribute("fill");
|
|
992
|
+
node.props.strokeDasharray = el.getAttribute("stroke-dasharray");
|
|
993
|
+
} else if (tag === "path" || tag === "line" || tag === "rect") {
|
|
994
|
+
node.props.stroke = el.getAttribute("stroke");
|
|
995
|
+
node.props.strokeWidth = el.getAttribute("stroke-width");
|
|
996
|
+
node.props.fill = el.getAttribute("fill");
|
|
883
997
|
}
|
|
884
998
|
if (s.display === "flex" || s.display === "inline-flex") {
|
|
885
999
|
node.props.gap = s.gap;
|
|
@@ -926,6 +1040,7 @@
|
|
|
926
1040
|
path: getElementPath(el),
|
|
927
1041
|
styles: extractStyles(el),
|
|
928
1042
|
boxModel: extractBoxModel(el),
|
|
1043
|
+
svgAttrs: extractSvgAttrs(el),
|
|
929
1044
|
tagName: el.tagName.toLowerCase(),
|
|
930
1045
|
className: typeof el.className === "string" ? el.className : "",
|
|
931
1046
|
textContent: (el.textContent || "").trim().substring(0, 100),
|
|
@@ -933,6 +1048,23 @@
|
|
|
933
1048
|
}, "*");
|
|
934
1049
|
}
|
|
935
1050
|
break;
|
|
1051
|
+
|
|
1052
|
+
case "gsdt-set-svg-attr":
|
|
1053
|
+
// Apply an SVG attribute change to the locked element
|
|
1054
|
+
if (lockedEl && msg.attribute && msg.value !== undefined) {
|
|
1055
|
+
lockedEl.setAttribute(msg.attribute, msg.value);
|
|
1056
|
+
positionOverlay(lockedEl);
|
|
1057
|
+
window.parent.postMessage({
|
|
1058
|
+
type: "gsdt-style-updated",
|
|
1059
|
+
path: getElementPath(lockedEl),
|
|
1060
|
+
property: "svg:" + msg.attribute,
|
|
1061
|
+
value: msg.value,
|
|
1062
|
+
styles: extractStyles(lockedEl),
|
|
1063
|
+
boxModel: extractBoxModel(lockedEl),
|
|
1064
|
+
svgAttrs: extractSvgAttrs(lockedEl),
|
|
1065
|
+
}, "*");
|
|
1066
|
+
}
|
|
1067
|
+
break;
|
|
936
1068
|
}
|
|
937
1069
|
});
|
|
938
1070
|
|
|
@@ -69,6 +69,23 @@ function extractFixtureFromContract(componentPath) {
|
|
|
69
69
|
for (const [k, v] of Object.entries(fixture)) {
|
|
70
70
|
if (!k.startsWith("__")) props[k] = v;
|
|
71
71
|
}
|
|
72
|
+
// Check if component expects different props than fixture provides.
|
|
73
|
+
// If fixture has a single array-of-objects key (e.g., "cards": [{value, label}]),
|
|
74
|
+
// and the component file's defineProps doesn't reference that key,
|
|
75
|
+
// unwrap the first item as individual props.
|
|
76
|
+
const propKeys = Object.keys(props);
|
|
77
|
+
if (propKeys.length === 1) {
|
|
78
|
+
const val = props[propKeys[0]];
|
|
79
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object") {
|
|
80
|
+
try {
|
|
81
|
+
const compSource = fs.readFileSync(path.join(PROJECT_DIR, componentPath), "utf8");
|
|
82
|
+
// If the component doesn't reference this array key in defineProps, unwrap first item
|
|
83
|
+
if (!compSource.includes(propKeys[0])) {
|
|
84
|
+
return val[0];
|
|
85
|
+
}
|
|
86
|
+
} catch { /* can't read component, use fixture as-is */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
72
89
|
return props;
|
|
73
90
|
} catch { return null; }
|
|
74
91
|
}
|
|
@@ -123,6 +140,112 @@ ${linkTags}
|
|
|
123
140
|
</html>`;
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
function generateGalleryHtml(queueItems, cols) {
|
|
144
|
+
const linkTags = GLOBAL_STYLES.map(s => ` <link rel="stylesheet" href="/${s}">`).join("\n");
|
|
145
|
+
// Only include items with source paths (actual components)
|
|
146
|
+
const components = queueItems.filter(item => item.sourcePath);
|
|
147
|
+
|
|
148
|
+
const imports = components.map((item, i) => {
|
|
149
|
+
return ` import Comp${i} from '/${item.sourcePath}'`;
|
|
150
|
+
}).join("\n");
|
|
151
|
+
|
|
152
|
+
const propsData = components.map((item) => {
|
|
153
|
+
const fixture = extractFixtureFromContract(item.sourcePath);
|
|
154
|
+
return fixture ? JSON.stringify(fixture) : "{}";
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let mountScript;
|
|
158
|
+
if (FRAMEWORK === "vue") {
|
|
159
|
+
mountScript = `
|
|
160
|
+
<script type="module">
|
|
161
|
+
import { createApp, h, defineComponent, onErrorCaptured, ref } from 'vue'
|
|
162
|
+
${imports}
|
|
163
|
+
|
|
164
|
+
const components = [${components.map((_, i) => `Comp${i}`).join(", ")}];
|
|
165
|
+
const names = ${JSON.stringify(components.map(c => c.name || c.id))};
|
|
166
|
+
const allProps = [${propsData.join(", ")}];
|
|
167
|
+
|
|
168
|
+
// Error boundary wrapper — catches render errors per-component
|
|
169
|
+
const SafeCell = defineComponent({
|
|
170
|
+
props: { comp: Object, compProps: Object, label: String },
|
|
171
|
+
setup(props) {
|
|
172
|
+
const error = ref(null)
|
|
173
|
+
onErrorCaptured((err) => { error.value = err.message; return false })
|
|
174
|
+
return () => h('div', { class: 'gallery-cell' }, [
|
|
175
|
+
h('div', { class: 'gallery-label' }, props.label),
|
|
176
|
+
h('div', { class: 'gallery-component' }, [
|
|
177
|
+
error.value
|
|
178
|
+
? h('div', { style: 'color:#ef4444;font-size:12px;padding:8px' }, '⚠ ' + error.value)
|
|
179
|
+
: h(props.comp, props.compProps)
|
|
180
|
+
])
|
|
181
|
+
])
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const Gallery = defineComponent({
|
|
186
|
+
render() {
|
|
187
|
+
return h('div', { class: 'gallery-grid' },
|
|
188
|
+
components.map((Comp, i) =>
|
|
189
|
+
h(SafeCell, { comp: Comp, compProps: allProps[i], label: names[i], key: i })
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
createApp(Gallery).mount('#app')
|
|
195
|
+
</script>`;
|
|
196
|
+
} else if (FRAMEWORK === "react") {
|
|
197
|
+
mountScript = `
|
|
198
|
+
<script type="module">
|
|
199
|
+
import React from 'react'
|
|
200
|
+
import { createRoot } from 'react-dom/client'
|
|
201
|
+
${imports}
|
|
202
|
+
|
|
203
|
+
const components = [${components.map((_, i) => `Comp${i}`).join(", ")}];
|
|
204
|
+
const names = ${JSON.stringify(components.map(c => c.name || c.id))};
|
|
205
|
+
const allProps = [${propsData.join(", ")}];
|
|
206
|
+
|
|
207
|
+
function Gallery() {
|
|
208
|
+
return React.createElement('div', { className: 'gallery-grid' },
|
|
209
|
+
components.map((Comp, i) =>
|
|
210
|
+
React.createElement('div', { className: 'gallery-cell', key: i },
|
|
211
|
+
React.createElement('div', { className: 'gallery-label' }, names[i]),
|
|
212
|
+
React.createElement('div', { className: 'gallery-component' },
|
|
213
|
+
React.createElement(Comp, allProps[i])
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
createRoot(document.getElementById('app')).render(React.createElement(Gallery))
|
|
220
|
+
</script>`;
|
|
221
|
+
} else {
|
|
222
|
+
mountScript = `<script type="module">/* unsupported framework */</script>`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return `<!DOCTYPE html>
|
|
226
|
+
<html lang="en">
|
|
227
|
+
<head>
|
|
228
|
+
<meta charset="UTF-8">
|
|
229
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
230
|
+
<title>GSD-T Design Gallery</title>
|
|
231
|
+
${linkTags}
|
|
232
|
+
<style>
|
|
233
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
234
|
+
body { background: #f1f5f9; min-height: 100vh; padding: 24px; font-family: 'Poppins', -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
235
|
+
#app { width: 100%; }
|
|
236
|
+
.gallery-grid { display: grid; grid-template-columns: repeat(${cols}, 1fr); gap: 20px; }
|
|
237
|
+
.gallery-cell { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px; overflow: hidden; }
|
|
238
|
+
.gallery-label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #e2e8f0; font-family: monospace; }
|
|
239
|
+
.gallery-component { min-height: 60px; }
|
|
240
|
+
</style>
|
|
241
|
+
</head>
|
|
242
|
+
<body>
|
|
243
|
+
<div id="app"></div>
|
|
244
|
+
${mountScript}
|
|
245
|
+
</body>
|
|
246
|
+
</html>`;
|
|
247
|
+
}
|
|
248
|
+
|
|
126
249
|
// ── Ensure coordination directory ─────────────────────────────────────
|
|
127
250
|
function ensureDir(dir) {
|
|
128
251
|
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* exists */ }
|
|
@@ -360,6 +483,49 @@ const server = http.createServer((req, res) => {
|
|
|
360
483
|
return;
|
|
361
484
|
}
|
|
362
485
|
|
|
486
|
+
// Gallery — all queued components in a grid, proxied through Vite
|
|
487
|
+
if (pathname === "/review/gallery") {
|
|
488
|
+
const queueItems = readQueue();
|
|
489
|
+
const cols = parseInt(parsed.query.cols || "3", 10);
|
|
490
|
+
if (queueItems.length === 0) {
|
|
491
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
492
|
+
res.end("<html><body><h2>No components queued yet</h2></body></html>");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const html = generateGalleryHtml(queueItems, cols);
|
|
496
|
+
const previewFile = path.join(PROJECT_DIR, "__gsd-preview.html");
|
|
497
|
+
try { fs.writeFileSync(previewFile, html); } catch { /* ignore */ }
|
|
498
|
+
const proxyOpts = {
|
|
499
|
+
hostname: targetUrl.hostname,
|
|
500
|
+
port: targetUrl.port,
|
|
501
|
+
path: "/__gsd-preview.html",
|
|
502
|
+
method: "GET",
|
|
503
|
+
headers: { ...req.headers, host: `${targetUrl.hostname}:${targetUrl.port}` },
|
|
504
|
+
};
|
|
505
|
+
const proxyReq = http.request(proxyOpts, (proxyRes) => {
|
|
506
|
+
const chunks = [];
|
|
507
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
508
|
+
proxyRes.on("end", () => {
|
|
509
|
+
const transformed = Buffer.concat(chunks).toString("utf8");
|
|
510
|
+
const buf = Buffer.from(transformed, "utf8");
|
|
511
|
+
res.writeHead(proxyRes.statusCode, {
|
|
512
|
+
...proxyRes.headers,
|
|
513
|
+
"content-length": buf.length,
|
|
514
|
+
"cache-control": "no-cache",
|
|
515
|
+
});
|
|
516
|
+
res.end(buf);
|
|
517
|
+
try { fs.unlinkSync(previewFile); } catch { /* ignore */ }
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
proxyReq.on("error", () => {
|
|
521
|
+
res.writeHead(502, { "Content-Type": "text/html" });
|
|
522
|
+
res.end("<h1>Dev server unreachable</h1>");
|
|
523
|
+
try { fs.unlinkSync(previewFile); } catch { /* ignore */ }
|
|
524
|
+
});
|
|
525
|
+
proxyReq.end();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
363
529
|
// Component preview — writes temp HTML to project, proxies through Vite for module resolution
|
|
364
530
|
if (pathname === "/review/preview") {
|
|
365
531
|
const component = parsed.query.component;
|
|
@@ -422,6 +588,14 @@ const server = http.createServer((req, res) => {
|
|
|
422
588
|
return;
|
|
423
589
|
}
|
|
424
590
|
|
|
591
|
+
if (pathname === "/review/api/fixture") {
|
|
592
|
+
const component = parsed.query.component;
|
|
593
|
+
const fixture = component ? extractFixtureFromContract(component) : null;
|
|
594
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
595
|
+
res.end(JSON.stringify(fixture || {}));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
425
599
|
if (pathname === "/review/api/feedback" && req.method === "GET") {
|
|
426
600
|
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
427
601
|
res.end(JSON.stringify(readFeedback()));
|
|
@@ -705,6 +705,12 @@
|
|
|
705
705
|
<span class="phase-badge" id="phase-badge">Elements</span>
|
|
706
706
|
</div>
|
|
707
707
|
<div class="header-right">
|
|
708
|
+
<button class="inspect-toggle" id="gallery-toggle" title="Show all components in a grid" style="margin-right:4px">
|
|
709
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
710
|
+
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
|
|
711
|
+
</svg>
|
|
712
|
+
Gallery
|
|
713
|
+
</button>
|
|
708
714
|
<button class="inspect-toggle" id="inspect-toggle" title="Toggle inspect mode (Ctrl+Shift+I)">
|
|
709
715
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
710
716
|
<path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z"/>
|
|
@@ -1045,6 +1051,18 @@
|
|
|
1045
1051
|
renderAIReview(item.aiReview);
|
|
1046
1052
|
}
|
|
1047
1053
|
|
|
1054
|
+
// Fetch and render fixture data (columns, rows, segments, etc.)
|
|
1055
|
+
if (item.sourcePath) {
|
|
1056
|
+
fetch(`/review/api/fixture?component=${encodeURIComponent(item.sourcePath)}`)
|
|
1057
|
+
.then(r => r.json())
|
|
1058
|
+
.then(fixture => {
|
|
1059
|
+
if (fixture && Object.keys(fixture).length > 0) {
|
|
1060
|
+
renderFixtureTree(fixture);
|
|
1061
|
+
}
|
|
1062
|
+
})
|
|
1063
|
+
.catch(() => {});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1048
1066
|
// Hint
|
|
1049
1067
|
if (item.selector) {
|
|
1050
1068
|
const hint = document.createElement("div");
|
|
@@ -1054,6 +1072,128 @@
|
|
|
1054
1072
|
}
|
|
1055
1073
|
}
|
|
1056
1074
|
|
|
1075
|
+
function renderFixtureTree(fixture) {
|
|
1076
|
+
const container = document.createElement("div");
|
|
1077
|
+
container.className = "prop-group";
|
|
1078
|
+
container.innerHTML = `<div class="prop-group-header">▸ Data Props</div>`;
|
|
1079
|
+
const body = document.createElement("div");
|
|
1080
|
+
body.style.cssText = "padding-left:8px;";
|
|
1081
|
+
|
|
1082
|
+
function renderValue(key, val, parent, depth, propPath) {
|
|
1083
|
+
if (Array.isArray(val)) {
|
|
1084
|
+
const row = document.createElement("div");
|
|
1085
|
+
row.className = "prop-group-header";
|
|
1086
|
+
row.style.paddingLeft = (depth * 12) + "px";
|
|
1087
|
+
row.style.fontSize = "11px";
|
|
1088
|
+
row.innerHTML = `<span style="color:#93c5fd">▸ ${key}</span> <span style="color:var(--text-dim);font-size:10px">[${val.length}]</span>`;
|
|
1089
|
+
let expanded = depth < 1;
|
|
1090
|
+
const items = document.createElement("div");
|
|
1091
|
+
items.style.display = expanded ? "" : "none";
|
|
1092
|
+
row.addEventListener("click", () => { expanded = !expanded; items.style.display = expanded ? "" : "none"; row.querySelector("span").textContent = (expanded ? "▾ " : "▸ ") + key; });
|
|
1093
|
+
if (expanded) row.querySelector("span").textContent = "▾ " + key;
|
|
1094
|
+
parent.appendChild(row);
|
|
1095
|
+
val.forEach((item, i) => {
|
|
1096
|
+
const itemPath = propPath + "." + key + "[" + i + "]";
|
|
1097
|
+
if (typeof item === "object" && item !== null) {
|
|
1098
|
+
const itemRow = document.createElement("div");
|
|
1099
|
+
itemRow.style.paddingLeft = ((depth + 1) * 12) + "px";
|
|
1100
|
+
itemRow.style.cssText += "font-size:10px;color:var(--text-dim);margin:2px 0;";
|
|
1101
|
+
itemRow.textContent = `[${i}]`;
|
|
1102
|
+
items.appendChild(itemRow);
|
|
1103
|
+
for (const [k, v] of Object.entries(item)) {
|
|
1104
|
+
renderValue(k, v, items, depth + 2, itemPath);
|
|
1105
|
+
}
|
|
1106
|
+
} else {
|
|
1107
|
+
const itemRow = document.createElement("div");
|
|
1108
|
+
itemRow.className = "prop-row";
|
|
1109
|
+
itemRow.style.paddingLeft = ((depth + 1) * 12) + "px";
|
|
1110
|
+
itemRow.innerHTML = `<span class="prop-name">[${i}]</span><span class="prop-value">${escapeHtml(String(item))}</span>`;
|
|
1111
|
+
items.appendChild(itemRow);
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
parent.appendChild(items);
|
|
1115
|
+
} else if (typeof val === "object" && val !== null) {
|
|
1116
|
+
const row = document.createElement("div");
|
|
1117
|
+
row.className = "prop-group-header";
|
|
1118
|
+
row.style.paddingLeft = (depth * 12) + "px";
|
|
1119
|
+
row.style.fontSize = "11px";
|
|
1120
|
+
row.innerHTML = `<span style="color:#93c5fd">▸ ${key}</span>`;
|
|
1121
|
+
let expanded = depth < 1;
|
|
1122
|
+
const items = document.createElement("div");
|
|
1123
|
+
items.style.display = expanded ? "" : "none";
|
|
1124
|
+
row.addEventListener("click", () => { expanded = !expanded; items.style.display = expanded ? "" : "none"; row.querySelector("span").textContent = (expanded ? "▾ " : "▸ ") + key; });
|
|
1125
|
+
if (expanded) row.querySelector("span").textContent = "▾ " + key;
|
|
1126
|
+
parent.appendChild(row);
|
|
1127
|
+
for (const [k, v] of Object.entries(val)) {
|
|
1128
|
+
renderValue(k, v, items, depth + 1, propPath + "." + key);
|
|
1129
|
+
}
|
|
1130
|
+
parent.appendChild(items);
|
|
1131
|
+
} else {
|
|
1132
|
+
const row = document.createElement("div");
|
|
1133
|
+
row.className = "prop-row";
|
|
1134
|
+
row.style.paddingLeft = (depth * 12) + "px";
|
|
1135
|
+
const isColor = typeof val === "string" && val.match(/^#[0-9a-fA-F]{3,8}$/);
|
|
1136
|
+
const displayVal = isColor
|
|
1137
|
+
? `<span class="color-swatch" style="background:${val}"></span>${val}`
|
|
1138
|
+
: escapeHtml(String(val));
|
|
1139
|
+
|
|
1140
|
+
const nameEl = document.createElement("span");
|
|
1141
|
+
nameEl.className = "prop-name";
|
|
1142
|
+
nameEl.textContent = key;
|
|
1143
|
+
row.appendChild(nameEl);
|
|
1144
|
+
|
|
1145
|
+
const valEl = document.createElement("span");
|
|
1146
|
+
valEl.className = "prop-value editable";
|
|
1147
|
+
valEl.setAttribute("data-prop", "fixture:" + propPath + "." + key);
|
|
1148
|
+
valEl.setAttribute("data-original", String(val));
|
|
1149
|
+
valEl.innerHTML = displayVal;
|
|
1150
|
+
|
|
1151
|
+
valEl.addEventListener("click", () => startFixtureEdit(valEl, propPath + "." + key, String(val), isColor));
|
|
1152
|
+
row.appendChild(valEl);
|
|
1153
|
+
parent.appendChild(row);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
for (const [k, v] of Object.entries(fixture)) {
|
|
1158
|
+
renderValue(k, v, body, 0, "");
|
|
1159
|
+
}
|
|
1160
|
+
container.appendChild(body);
|
|
1161
|
+
|
|
1162
|
+
// Toggle data props section
|
|
1163
|
+
let dataExpanded = true;
|
|
1164
|
+
container.querySelector(".prop-group-header").addEventListener("click", () => {
|
|
1165
|
+
dataExpanded = !dataExpanded;
|
|
1166
|
+
body.style.display = dataExpanded ? "" : "none";
|
|
1167
|
+
container.querySelector(".prop-group-header").textContent = (dataExpanded ? "▾" : "▸") + " Data Props";
|
|
1168
|
+
});
|
|
1169
|
+
container.querySelector(".prop-group-header").textContent = "▾ Data Props";
|
|
1170
|
+
|
|
1171
|
+
inspectorInfo.appendChild(container);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function escapeHtml(str) {
|
|
1175
|
+
return str.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// ── Gallery mode ──────────────────────────────────
|
|
1179
|
+
const galleryToggle = document.getElementById("gallery-toggle");
|
|
1180
|
+
let galleryActive = false;
|
|
1181
|
+
|
|
1182
|
+
galleryToggle.addEventListener("click", () => {
|
|
1183
|
+
galleryActive = !galleryActive;
|
|
1184
|
+
galleryToggle.classList.toggle("active", galleryActive);
|
|
1185
|
+
if (galleryActive) {
|
|
1186
|
+
previewIframe.src = "/review/gallery?cols=3";
|
|
1187
|
+
previewUrl.textContent = "Gallery — all components";
|
|
1188
|
+
} else if (selectedIdx >= 0 && filteredQueue[selectedIdx]) {
|
|
1189
|
+
const item = filteredQueue[selectedIdx];
|
|
1190
|
+
if (item.sourcePath) {
|
|
1191
|
+
previewIframe.src = `/review/preview?component=${encodeURIComponent(item.sourcePath)}`;
|
|
1192
|
+
previewUrl.textContent = item.sourcePath;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1057
1197
|
// ── Inspect mode ──────────────────────────────────
|
|
1058
1198
|
inspectToggle.addEventListener("click", () => {
|
|
1059
1199
|
inspectActive = !inspectActive;
|
|
@@ -1105,6 +1245,7 @@
|
|
|
1105
1245
|
|
|
1106
1246
|
case "gsdt-style-updated":
|
|
1107
1247
|
currentStyles = msg.styles;
|
|
1248
|
+
if (msg.svgAttrs) currentSvgAttrs = msg.svgAttrs;
|
|
1108
1249
|
renderPropertyValues(msg.styles);
|
|
1109
1250
|
// Show propagation feedback with scope
|
|
1110
1251
|
if (msg.propagated > 0) {
|
|
@@ -1130,9 +1271,12 @@
|
|
|
1130
1271
|
});
|
|
1131
1272
|
|
|
1132
1273
|
// ── Inspector rendering ───────────────────────────
|
|
1274
|
+
let currentSvgAttrs = null;
|
|
1275
|
+
|
|
1133
1276
|
function renderInspector(msg) {
|
|
1134
1277
|
const styles = msg.styles;
|
|
1135
1278
|
const boxModel = msg.boxModel;
|
|
1279
|
+
currentSvgAttrs = msg.svgAttrs || null;
|
|
1136
1280
|
|
|
1137
1281
|
// Only update props zone — preserve tree
|
|
1138
1282
|
inspectorProps.innerHTML = "";
|
|
@@ -1186,8 +1330,9 @@
|
|
|
1186
1330
|
"width", "height",
|
|
1187
1331
|
"paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
1188
1332
|
"marginTop", "marginRight", "marginBottom", "marginLeft",
|
|
1189
|
-
"fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign",
|
|
1333
|
+
"fontSize", "fontWeight", "fontFamily", "lineHeight", "letterSpacing", "textAlign",
|
|
1190
1334
|
"backgroundColor", "color", "borderRadius", "border", "opacity",
|
|
1335
|
+
"overflow", "position", "top", "left", "boxShadow",
|
|
1191
1336
|
]);
|
|
1192
1337
|
|
|
1193
1338
|
for (const [groupName, props] of Object.entries(groups)) {
|
|
@@ -1283,6 +1428,267 @@
|
|
|
1283
1428
|
inspectorProps.appendChild(group);
|
|
1284
1429
|
}
|
|
1285
1430
|
}
|
|
1431
|
+
|
|
1432
|
+
// SVG Attributes group (if element is SVG)
|
|
1433
|
+
if (currentSvgAttrs) {
|
|
1434
|
+
const svgGroup = document.createElement("div");
|
|
1435
|
+
svgGroup.className = "prop-group";
|
|
1436
|
+
|
|
1437
|
+
const svgHeader = document.createElement("div");
|
|
1438
|
+
svgHeader.className = "prop-group-header";
|
|
1439
|
+
svgHeader.innerHTML = `<span>▾</span> SVG Attributes`;
|
|
1440
|
+
svgHeader.style.color = "#06b6d4";
|
|
1441
|
+
svgGroup.appendChild(svgHeader);
|
|
1442
|
+
|
|
1443
|
+
const svgRows = document.createElement("div");
|
|
1444
|
+
svgRows.style.display = "block";
|
|
1445
|
+
|
|
1446
|
+
svgHeader.addEventListener("click", () => {
|
|
1447
|
+
const visible = svgRows.style.display !== "none";
|
|
1448
|
+
svgRows.style.display = visible ? "none" : "block";
|
|
1449
|
+
svgHeader.innerHTML = `<span>${visible ? "▸" : "▾"}</span> SVG Attributes`;
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const svgColorAttrs = new Set(["stroke", "fill"]);
|
|
1453
|
+
const svgNumericAttrs = new Set(["stroke-width", "r", "rx", "ry", "cx", "cy", "x", "y",
|
|
1454
|
+
"x1", "y1", "x2", "y2", "width", "height", "opacity", "stroke-dashoffset"]);
|
|
1455
|
+
|
|
1456
|
+
for (const [attr, val] of Object.entries(currentSvgAttrs)) {
|
|
1457
|
+
const row = document.createElement("div");
|
|
1458
|
+
row.className = "prop-row";
|
|
1459
|
+
row.setAttribute("data-prop", "svg:" + attr);
|
|
1460
|
+
|
|
1461
|
+
const nameEl = document.createElement("span");
|
|
1462
|
+
nameEl.className = "prop-name";
|
|
1463
|
+
nameEl.textContent = attr;
|
|
1464
|
+
nameEl.title = `Click to highlight ${attr}`;
|
|
1465
|
+
nameEl.style.cursor = "pointer";
|
|
1466
|
+
nameEl.addEventListener("click", () => {
|
|
1467
|
+
if (previewIframe.contentWindow) {
|
|
1468
|
+
previewIframe.contentWindow.postMessage({
|
|
1469
|
+
type: "gsdt-highlight-zone",
|
|
1470
|
+
property: "svg:" + attr,
|
|
1471
|
+
}, "*");
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
row.appendChild(nameEl);
|
|
1475
|
+
|
|
1476
|
+
const valEl = document.createElement("span");
|
|
1477
|
+
valEl.className = "prop-value editable";
|
|
1478
|
+
valEl.setAttribute("data-prop", "svg:" + attr);
|
|
1479
|
+
valEl.setAttribute("data-original", val);
|
|
1480
|
+
|
|
1481
|
+
if (svgColorAttrs.has(attr) && val && val !== "none") {
|
|
1482
|
+
valEl.innerHTML = `<span class="color-swatch" style="background:${val}"></span>${val}`;
|
|
1483
|
+
} else {
|
|
1484
|
+
valEl.textContent = val;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Check if changed
|
|
1488
|
+
const compId = filteredQueue[selectedIdx]?.id;
|
|
1489
|
+
const compChanges = changes.get(compId) || [];
|
|
1490
|
+
const existing = compChanges.find(c => c.path === currentElementPath && c.property === "svg:" + attr);
|
|
1491
|
+
if (existing) {
|
|
1492
|
+
valEl.classList.add("changed");
|
|
1493
|
+
valEl.textContent = existing.newValue;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
valEl.addEventListener("click", () => startSvgEdit(valEl, attr));
|
|
1497
|
+
row.appendChild(valEl);
|
|
1498
|
+
svgRows.appendChild(row);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
svgGroup.appendChild(svgRows);
|
|
1502
|
+
if (svgRows.children.length > 0) {
|
|
1503
|
+
inspectorProps.appendChild(svgGroup);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function startSvgEdit(valEl, attr) {
|
|
1509
|
+
if (valEl.querySelector("select, input")) return;
|
|
1510
|
+
|
|
1511
|
+
const currentVal = valEl.getAttribute("data-original");
|
|
1512
|
+
const compChanges = changes.get(filteredQueue[selectedIdx]?.id) || [];
|
|
1513
|
+
const existing = compChanges.find(c => c.path === currentElementPath && c.property === "svg:" + attr);
|
|
1514
|
+
const displayVal = existing ? existing.newValue : currentVal;
|
|
1515
|
+
|
|
1516
|
+
const permitted = PERMITTED_VALUES[attr];
|
|
1517
|
+
let inputEl;
|
|
1518
|
+
|
|
1519
|
+
if (permitted) {
|
|
1520
|
+
inputEl = document.createElement("select");
|
|
1521
|
+
inputEl.className = "prop-edit-input";
|
|
1522
|
+
for (const opt of permitted) {
|
|
1523
|
+
const option = document.createElement("option");
|
|
1524
|
+
option.value = opt;
|
|
1525
|
+
option.textContent = opt;
|
|
1526
|
+
if (opt === displayVal) option.selected = true;
|
|
1527
|
+
inputEl.appendChild(option);
|
|
1528
|
+
}
|
|
1529
|
+
} else {
|
|
1530
|
+
inputEl = document.createElement("input");
|
|
1531
|
+
inputEl.className = "prop-edit-input";
|
|
1532
|
+
inputEl.type = "text";
|
|
1533
|
+
inputEl.value = displayVal;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
valEl.innerHTML = "";
|
|
1537
|
+
valEl.appendChild(inputEl);
|
|
1538
|
+
inputEl.focus();
|
|
1539
|
+
if (!permitted) {
|
|
1540
|
+
const numMatch = displayVal.match(/^(-?[\d.]+)/);
|
|
1541
|
+
if (numMatch) {
|
|
1542
|
+
inputEl.setSelectionRange(0, numMatch[1].length);
|
|
1543
|
+
} else {
|
|
1544
|
+
inputEl.select();
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
let cancelled = false;
|
|
1549
|
+
function commit() {
|
|
1550
|
+
if (cancelled) return;
|
|
1551
|
+
const newVal = inputEl.value.trim();
|
|
1552
|
+
if (newVal && newVal !== currentVal) {
|
|
1553
|
+
previewIframe.contentWindow.postMessage({
|
|
1554
|
+
type: "gsdt-set-svg-attr",
|
|
1555
|
+
attribute: attr,
|
|
1556
|
+
value: newVal,
|
|
1557
|
+
}, "*");
|
|
1558
|
+
|
|
1559
|
+
const compId = filteredQueue[selectedIdx]?.id;
|
|
1560
|
+
if (compId) {
|
|
1561
|
+
if (!changes.has(compId)) changes.set(compId, []);
|
|
1562
|
+
const list = changes.get(compId);
|
|
1563
|
+
const existingIdx = list.findIndex(c => c.path === currentElementPath && c.property === "svg:" + attr);
|
|
1564
|
+
const change = {
|
|
1565
|
+
path: currentElementPath,
|
|
1566
|
+
property: "svg:" + attr,
|
|
1567
|
+
oldValue: currentVal,
|
|
1568
|
+
newValue: newVal,
|
|
1569
|
+
};
|
|
1570
|
+
if (existingIdx >= 0) {
|
|
1571
|
+
list[existingIdx] = change;
|
|
1572
|
+
} else {
|
|
1573
|
+
list.push(change);
|
|
1574
|
+
}
|
|
1575
|
+
renderChanges(compId);
|
|
1576
|
+
renderComponentList();
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
valEl.textContent = newVal;
|
|
1580
|
+
valEl.classList.add("changed");
|
|
1581
|
+
} else {
|
|
1582
|
+
valEl.textContent = existing ? existing.newValue : currentVal;
|
|
1583
|
+
if (existing) valEl.classList.add("changed");
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (permitted) {
|
|
1588
|
+
inputEl.addEventListener("change", () => { commit(); cancelled = true; });
|
|
1589
|
+
inputEl.addEventListener("blur", () => { if (!cancelled) { valEl.textContent = existing ? existing.newValue : currentVal; } });
|
|
1590
|
+
} else {
|
|
1591
|
+
inputEl.addEventListener("blur", () => { if (!cancelled) commit(); });
|
|
1592
|
+
}
|
|
1593
|
+
inputEl.addEventListener("keydown", (e) => {
|
|
1594
|
+
if (e.key === "Enter") { commit(); cancelled = true; inputEl.blur(); }
|
|
1595
|
+
if (e.key === "Escape") {
|
|
1596
|
+
cancelled = true;
|
|
1597
|
+
if (inputEl.parentElement) inputEl.remove();
|
|
1598
|
+
valEl.textContent = existing ? existing.newValue : currentVal;
|
|
1599
|
+
if (existing) valEl.classList.add("changed");
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function startFixtureEdit(valEl, fixturePath, currentVal, isColor) {
|
|
1605
|
+
if (valEl.querySelector("select, input")) return;
|
|
1606
|
+
|
|
1607
|
+
const compId = filteredQueue[selectedIdx]?.id;
|
|
1608
|
+
const compChanges = changes.get(compId) || [];
|
|
1609
|
+
const prop = "fixture:" + fixturePath;
|
|
1610
|
+
const existing = compChanges.find(c => c.property === prop);
|
|
1611
|
+
const displayVal = existing ? existing.newValue : currentVal;
|
|
1612
|
+
|
|
1613
|
+
const inputEl = document.createElement("input");
|
|
1614
|
+
inputEl.className = "prop-edit-input";
|
|
1615
|
+
inputEl.type = isColor ? "color" : "text";
|
|
1616
|
+
inputEl.value = displayVal;
|
|
1617
|
+
|
|
1618
|
+
valEl.innerHTML = "";
|
|
1619
|
+
valEl.appendChild(inputEl);
|
|
1620
|
+
inputEl.focus();
|
|
1621
|
+
if (!isColor) {
|
|
1622
|
+
const numMatch = displayVal.match(/^(-?[\d.]+)/);
|
|
1623
|
+
if (numMatch) {
|
|
1624
|
+
inputEl.setSelectionRange(0, numMatch[1].length);
|
|
1625
|
+
} else {
|
|
1626
|
+
inputEl.select();
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
let cancelled = false;
|
|
1631
|
+
function commit() {
|
|
1632
|
+
if (cancelled) return;
|
|
1633
|
+
const newVal = inputEl.value.trim();
|
|
1634
|
+
if (newVal && newVal !== currentVal) {
|
|
1635
|
+
// Track as a fixture data change
|
|
1636
|
+
if (compId) {
|
|
1637
|
+
if (!changes.has(compId)) changes.set(compId, []);
|
|
1638
|
+
const list = changes.get(compId);
|
|
1639
|
+
const existingIdx = list.findIndex(c => c.property === prop);
|
|
1640
|
+
const change = {
|
|
1641
|
+
path: "(fixture data)",
|
|
1642
|
+
property: prop,
|
|
1643
|
+
oldValue: currentVal,
|
|
1644
|
+
newValue: newVal,
|
|
1645
|
+
};
|
|
1646
|
+
if (existingIdx >= 0) {
|
|
1647
|
+
list[existingIdx] = change;
|
|
1648
|
+
} else {
|
|
1649
|
+
list.push(change);
|
|
1650
|
+
}
|
|
1651
|
+
renderChanges(compId);
|
|
1652
|
+
renderComponentList();
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (isColor) {
|
|
1656
|
+
valEl.innerHTML = `<span class="color-swatch" style="background:${newVal}"></span>${newVal}`;
|
|
1657
|
+
} else {
|
|
1658
|
+
valEl.textContent = newVal;
|
|
1659
|
+
}
|
|
1660
|
+
valEl.classList.add("changed");
|
|
1661
|
+
} else {
|
|
1662
|
+
if (isColor && (existing ? existing.newValue : currentVal).match(/^#/)) {
|
|
1663
|
+
const v = existing ? existing.newValue : currentVal;
|
|
1664
|
+
valEl.innerHTML = `<span class="color-swatch" style="background:${v}"></span>${v}`;
|
|
1665
|
+
} else {
|
|
1666
|
+
valEl.textContent = existing ? existing.newValue : currentVal;
|
|
1667
|
+
}
|
|
1668
|
+
if (existing) valEl.classList.add("changed");
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (isColor) {
|
|
1673
|
+
inputEl.addEventListener("change", () => { commit(); cancelled = true; });
|
|
1674
|
+
inputEl.addEventListener("blur", () => { if (!cancelled) { valEl.innerHTML = `<span class="color-swatch" style="background:${currentVal}"></span>${currentVal}`; } });
|
|
1675
|
+
} else {
|
|
1676
|
+
inputEl.addEventListener("blur", () => { if (!cancelled) commit(); });
|
|
1677
|
+
}
|
|
1678
|
+
inputEl.addEventListener("keydown", (e) => {
|
|
1679
|
+
if (e.key === "Enter") { commit(); cancelled = true; inputEl.blur(); }
|
|
1680
|
+
if (e.key === "Escape") {
|
|
1681
|
+
cancelled = true;
|
|
1682
|
+
if (inputEl.parentElement) inputEl.remove();
|
|
1683
|
+
if (isColor) {
|
|
1684
|
+
const v = existing ? existing.newValue : currentVal;
|
|
1685
|
+
valEl.innerHTML = `<span class="color-swatch" style="background:${v}"></span>${v}`;
|
|
1686
|
+
} else {
|
|
1687
|
+
valEl.textContent = existing ? existing.newValue : currentVal;
|
|
1688
|
+
}
|
|
1689
|
+
if (existing) valEl.classList.add("changed");
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1286
1692
|
}
|
|
1287
1693
|
|
|
1288
1694
|
function renderPropertyValues(styles) {
|
|
@@ -1408,32 +1814,71 @@
|
|
|
1408
1814
|
}
|
|
1409
1815
|
|
|
1410
1816
|
// ── Property editing ──────────────────────────────
|
|
1817
|
+
// Permitted values for enum-style CSS properties
|
|
1818
|
+
const PERMITTED_VALUES = {
|
|
1819
|
+
display: ["block", "flex", "inline-flex", "grid", "inline-grid", "inline", "inline-block", "none", "contents"],
|
|
1820
|
+
flexDirection: ["row", "row-reverse", "column", "column-reverse"],
|
|
1821
|
+
alignItems: ["stretch", "flex-start", "flex-end", "center", "baseline"],
|
|
1822
|
+
justifyContent: ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"],
|
|
1823
|
+
textAlign: ["left", "center", "right", "justify"],
|
|
1824
|
+
fontWeight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
|
|
1825
|
+
overflow: ["visible", "hidden", "scroll", "auto", "clip"],
|
|
1826
|
+
position: ["static", "relative", "absolute", "fixed", "sticky"],
|
|
1827
|
+
// SVG attribute permitted values (used by startSvgEdit)
|
|
1828
|
+
"stroke-linecap": ["butt", "round", "square"],
|
|
1829
|
+
"stroke-linejoin": ["miter", "round", "bevel"],
|
|
1830
|
+
"text-anchor": ["start", "middle", "end"],
|
|
1831
|
+
"dominant-baseline": ["auto", "middle", "hanging", "central", "text-top", "text-bottom"],
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1411
1834
|
function startEdit(valEl, prop) {
|
|
1835
|
+
// Prevent re-entry: if already editing (select/input inside), do nothing
|
|
1836
|
+
if (valEl.querySelector("select, input")) return;
|
|
1837
|
+
|
|
1412
1838
|
const currentVal = valEl.getAttribute("data-original");
|
|
1413
1839
|
const compChanges = changes.get(filteredQueue[selectedIdx]?.id) || [];
|
|
1414
1840
|
const existing = compChanges.find(c => c.path === currentElementPath && c.property === prop);
|
|
1415
1841
|
const displayVal = existing ? existing.newValue : currentVal;
|
|
1416
1842
|
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
input.type = "text";
|
|
1420
|
-
input.value = displayVal;
|
|
1843
|
+
const permitted = PERMITTED_VALUES[prop];
|
|
1844
|
+
let inputEl;
|
|
1421
1845
|
let cancelled = false;
|
|
1422
1846
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1847
|
+
if (permitted) {
|
|
1848
|
+
// Dropdown for enum properties
|
|
1849
|
+
inputEl = document.createElement("select");
|
|
1850
|
+
inputEl.className = "prop-edit-input";
|
|
1851
|
+
for (const opt of permitted) {
|
|
1852
|
+
const option = document.createElement("option");
|
|
1853
|
+
option.value = opt;
|
|
1854
|
+
option.textContent = opt;
|
|
1855
|
+
if (opt === displayVal) option.selected = true;
|
|
1856
|
+
inputEl.appendChild(option);
|
|
1857
|
+
}
|
|
1430
1858
|
} else {
|
|
1431
|
-
input
|
|
1859
|
+
// Text input for free-form values
|
|
1860
|
+
inputEl = document.createElement("input");
|
|
1861
|
+
inputEl.className = "prop-edit-input";
|
|
1862
|
+
inputEl.type = "text";
|
|
1863
|
+
inputEl.value = displayVal;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
valEl.innerHTML = "";
|
|
1867
|
+
valEl.appendChild(inputEl);
|
|
1868
|
+
inputEl.focus();
|
|
1869
|
+
if (!permitted) {
|
|
1870
|
+
// Select just the numeric part for values like "24px", "1.5em", "600"
|
|
1871
|
+
const numMatch = displayVal.match(/^(-?[\d.]+)/);
|
|
1872
|
+
if (numMatch) {
|
|
1873
|
+
inputEl.setSelectionRange(0, numMatch[1].length);
|
|
1874
|
+
} else {
|
|
1875
|
+
inputEl.select();
|
|
1876
|
+
}
|
|
1432
1877
|
}
|
|
1433
1878
|
|
|
1434
1879
|
function commit() {
|
|
1435
1880
|
if (cancelled) return;
|
|
1436
|
-
const newVal =
|
|
1881
|
+
const newVal = inputEl.value.trim();
|
|
1437
1882
|
if (newVal && newVal !== currentVal) {
|
|
1438
1883
|
// Apply the style change to the iframe
|
|
1439
1884
|
previewIframe.contentWindow.postMessage({
|
|
@@ -1472,13 +1917,18 @@
|
|
|
1472
1917
|
}
|
|
1473
1918
|
}
|
|
1474
1919
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1920
|
+
if (permitted) {
|
|
1921
|
+
// For select: commit on change, close on blur
|
|
1922
|
+
inputEl.addEventListener("change", () => { commit(); cancelled = true; });
|
|
1923
|
+
inputEl.addEventListener("blur", () => { if (!cancelled) { valEl.textContent = existing ? existing.newValue : currentVal; } });
|
|
1924
|
+
} else {
|
|
1925
|
+
inputEl.addEventListener("blur", () => { if (!cancelled) commit(); });
|
|
1926
|
+
}
|
|
1927
|
+
inputEl.addEventListener("keydown", (e) => {
|
|
1928
|
+
if (e.key === "Enter") { commit(); cancelled = true; inputEl.blur(); }
|
|
1478
1929
|
if (e.key === "Escape") {
|
|
1479
1930
|
cancelled = true;
|
|
1480
|
-
|
|
1481
|
-
if (input.parentElement) input.remove();
|
|
1931
|
+
if (inputEl.parentElement) inputEl.remove();
|
|
1482
1932
|
valEl.textContent = existing ? existing.newValue : currentVal;
|
|
1483
1933
|
if (existing) valEl.classList.add("changed");
|
|
1484
1934
|
}
|