@agent-native/core 0.80.6 → 0.80.8
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +70 -27
- package/corpus/core/src/client/analytics.ts +3 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +45 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +42 -4
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +23 -9
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +72 -1
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +34 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +88 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -1
- package/corpus/templates/analytics/changelog/2026-06-29-dashboards-can-now-include-extension-panels-that-embed-a-san.md +6 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +69 -16
- package/corpus/templates/content/changelog/2026-06-29-read-only-pages-no-longer-try-to-autosave-or-open-editor-onl.md +6 -0
- package/corpus/templates/design/AGENTS.md +4 -2
- package/corpus/templates/design/actions/edit-design.ts +6 -3
- package/corpus/templates/design/actions/generate-screens.ts +5 -3
- package/corpus/templates/design/actions/update-design.ts +47 -10
- package/corpus/templates/design/actions/update-file.ts +33 -8
- package/corpus/templates/design/actions/view-screen.ts +14 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +75 -17
- package/corpus/templates/design/app/components/design/EditPanel.tsx +121 -22
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +51 -8
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +148 -33
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +11 -11
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +27 -13
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +3 -37
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +18 -2
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +15 -3
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -2
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +18 -4
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +10 -4
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +448 -259
- package/corpus/templates/design/app/hooks/use-variant-flow.ts +176 -92
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +10 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +220 -40
- package/corpus/templates/design/changelog/2026-06-29-agent-edits-to-a-design-are-no-longer-occasionally-dropped-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-box-shadow-and-gradient-colors-keep-named-css-colors-like-re.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-cut-cmd-ctrl-x-now-removes-the-selected-element-to-the-clipb.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-deleting-with-multiple-layers-selected-in-the-layers-panel-n.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-duplicating-a-screen-now-gives-the-copy-its-own-layer-ids-so.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-hiding-and-re-showing-a-gradient-fill-now-preserves-each-sto.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-picking-a-design-direction-now-submits-to-the-agent-right-aw.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-style-changes-that-set-several-properties-at-once-fixed-size.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-editor-no-longer-crashes-when-a-screen-ends-up-with-dupl.md +6 -0
- package/corpus/templates/design/e2e/README.md +1 -0
- package/corpus/templates/design/e2e/global-setup.ts +13 -1
- package/corpus/templates/design/playwright.config.ts +10 -1
- package/corpus/templates/design/shared/color-utils.ts +159 -0
- package/corpus/templates/design/shared/generation-session.ts +1 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +54 -24
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +2 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts +10 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +34 -3
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -480,7 +480,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
480
480
|
mixBlendMode: cs.mixBlendMode,
|
|
481
481
|
zIndex: cs.zIndex,
|
|
482
482
|
},
|
|
483
|
-
boundingRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
483
|
+
boundingRect: { x: rect.x + (window.scrollX || window.pageXOffset || 0), y: rect.y + (window.scrollY || window.pageYOffset || 0), width: rect.width, height: rect.height },
|
|
484
484
|
textContent: el.textContent ? el.textContent.slice(0, 200) : undefined,
|
|
485
485
|
htmlContent: el.innerHTML && el.innerHTML !== el.textContent ? el.innerHTML.slice(0, 4000) : undefined,
|
|
486
486
|
isFlexContainer: cs.display === 'flex' || cs.display === 'inline-flex',
|
|
@@ -789,13 +789,34 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
789
789
|
overlay.style.display = 'none';
|
|
790
790
|
return;
|
|
791
791
|
}
|
|
792
|
+
// For the selection overlay, prefer the CSS box + rotation transform so
|
|
793
|
+
// the handles hug the rotated element rather than its inflated AABB.
|
|
794
|
+
if (overlay === selectionOverlay) {
|
|
795
|
+
var elCs = window.getComputedStyle(el);
|
|
796
|
+
var elLeft = readPx(el.style.left || elCs.left);
|
|
797
|
+
var elTop = readPx(el.style.top || elCs.top);
|
|
798
|
+
var elW = readPx(el.style.width || elCs.width);
|
|
799
|
+
var elH = readPx(el.style.height || elCs.height);
|
|
800
|
+
var elRot = currentRotation(el);
|
|
801
|
+
// Convert element-local left/top to viewport coords by walking to the
|
|
802
|
+
// nearest positioned ancestor (same reference frame as getBoundingClientRect).
|
|
803
|
+
var parentRect = (el.offsetParent || document.documentElement).getBoundingClientRect();
|
|
804
|
+
overlay.style.display = 'block';
|
|
805
|
+
overlay.style.left = (parentRect.left + elLeft) + 'px';
|
|
806
|
+
overlay.style.top = (parentRect.top + elTop) + 'px';
|
|
807
|
+
overlay.style.width = elW + 'px';
|
|
808
|
+
overlay.style.height = elH + 'px';
|
|
809
|
+
overlay.style.transform = elRot ? 'rotate(' + elRot + 'deg)' : '';
|
|
810
|
+
overlay.style.transformOrigin = '0 0';
|
|
811
|
+
updatePaddingOverlay(el);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
792
814
|
var rect = el.getBoundingClientRect();
|
|
793
815
|
overlay.style.display = 'block';
|
|
794
816
|
overlay.style.top = rect.top + 'px';
|
|
795
817
|
overlay.style.left = rect.left + 'px';
|
|
796
818
|
overlay.style.width = rect.width + 'px';
|
|
797
819
|
overlay.style.height = rect.height + 'px';
|
|
798
|
-
if (overlay === selectionOverlay) updatePaddingOverlay(el);
|
|
799
820
|
}
|
|
800
821
|
|
|
801
822
|
function refreshOverlays() {
|
|
@@ -1381,10 +1402,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1381
1402
|
}
|
|
1382
1403
|
}
|
|
1383
1404
|
|
|
1384
|
-
function postVisualStructureChange(el, target) {
|
|
1405
|
+
function postVisualStructureChange(el, target, origin) {
|
|
1385
1406
|
if (!el || !target || !target.anchor) return;
|
|
1386
1407
|
var requestId = 'move-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
|
1387
|
-
pendingStructureMove = { requestId: requestId, el: el, target: target };
|
|
1408
|
+
pendingStructureMove = { requestId: requestId, el: el, target: target, origin: origin || null };
|
|
1388
1409
|
window.parent.postMessage({
|
|
1389
1410
|
type: 'visual-structure-change',
|
|
1390
1411
|
requestId: requestId,
|
|
@@ -1458,11 +1479,15 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1458
1479
|
applyRuntimeReorder(reorderEl, currentTarget);
|
|
1459
1480
|
postVisualDuplicateChange(originalSelectedEl, reorderEl, currentTarget);
|
|
1460
1481
|
} else {
|
|
1482
|
+
// Capture the pre-drag DOM anchor so we can revert if the parent
|
|
1483
|
+
// reports applied===false on the structure-ack.
|
|
1484
|
+
var prevParent = reorderEl.parentElement;
|
|
1485
|
+
var prevNextSibling = reorderEl.nextSibling;
|
|
1461
1486
|
// Optimistically apply the reorder in the DOM for immediate
|
|
1462
1487
|
// visual feedback; the visual-structure-ack handler will confirm
|
|
1463
1488
|
// or revert once the parent processes the change.
|
|
1464
1489
|
applyRuntimeReorder(reorderEl, currentTarget);
|
|
1465
|
-
postVisualStructureChange(reorderEl, currentTarget);
|
|
1490
|
+
postVisualStructureChange(reorderEl, currentTarget, { prevParent: prevParent, prevNextSibling: prevNextSibling });
|
|
1466
1491
|
}
|
|
1467
1492
|
}
|
|
1468
1493
|
document.addEventListener('mousemove', onReorderMove, true);
|
|
@@ -1479,7 +1504,12 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1479
1504
|
// clear-selection postMessage cannot swap selectedEl mid-drag and cause
|
|
1480
1505
|
// mutations on the wrong element or a null-deref in onUp.
|
|
1481
1506
|
var dragEl = selectedEl;
|
|
1507
|
+
var moved = false;
|
|
1508
|
+
var DRAG_THRESHOLD = 3;
|
|
1482
1509
|
function onMove(ev) {
|
|
1510
|
+
if (!moved && Math.hypot(ev.clientX - startX, ev.clientY - startY) > DRAG_THRESHOLD) {
|
|
1511
|
+
moved = true;
|
|
1512
|
+
}
|
|
1483
1513
|
var nextLeft = originLeft + ev.clientX - startX;
|
|
1484
1514
|
var nextTop = originTop + ev.clientY - startY;
|
|
1485
1515
|
dragEl.style.left = Math.round(nextLeft) + 'px';
|
|
@@ -1492,6 +1522,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1492
1522
|
document.removeEventListener('mouseup', onUp, true);
|
|
1493
1523
|
hideTransformBadge();
|
|
1494
1524
|
if (!dragEl) return;
|
|
1525
|
+
if (duplicatedForDrag && !moved) {
|
|
1526
|
+
// Alt-click with no real drag — remove the premature clone and restore the original selection.
|
|
1527
|
+
if (dragEl.parentElement) dragEl.parentElement.removeChild(dragEl);
|
|
1528
|
+
selectedEl = originalSelectedEl;
|
|
1529
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
1530
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1495
1533
|
if (duplicatedForDrag) {
|
|
1496
1534
|
postVisualDuplicateChange(originalSelectedEl, dragEl);
|
|
1497
1535
|
} else {
|
|
@@ -1535,9 +1573,19 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1535
1573
|
// Snapshot the element so a concurrent clear-selection postMessage cannot
|
|
1536
1574
|
// cause a null-deref in onMove/onUp.
|
|
1537
1575
|
var resizeEl = selectedEl;
|
|
1576
|
+
// Capture the element rotation once at drag-start so per-move projection is
|
|
1577
|
+
// cheap and consistent even if the transform changes during the drag.
|
|
1578
|
+
var resizeTheta = currentRotation(resizeEl) * Math.PI / 180;
|
|
1538
1579
|
function nextRect(ev) {
|
|
1539
|
-
var
|
|
1540
|
-
var
|
|
1580
|
+
var screenDx = ev.clientX - startX;
|
|
1581
|
+
var screenDy = ev.clientY - startY;
|
|
1582
|
+
// Project the screen-space pointer delta into the element's local
|
|
1583
|
+
// (un-rotated) coordinate frame so handles behave relative to the
|
|
1584
|
+
// visible rotated box rather than screen axes.
|
|
1585
|
+
var cosT = Math.cos(resizeTheta);
|
|
1586
|
+
var sinT = Math.sin(resizeTheta);
|
|
1587
|
+
var dx = screenDx * cosT + screenDy * sinT;
|
|
1588
|
+
var dy = -screenDx * sinT + screenDy * cosT;
|
|
1541
1589
|
var left = origin.left;
|
|
1542
1590
|
var top = origin.top;
|
|
1543
1591
|
var width = origin.width;
|
|
@@ -1570,14 +1618,13 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1570
1618
|
}
|
|
1571
1619
|
}
|
|
1572
1620
|
// Clamp after ratio correction.
|
|
1573
|
-
var rawWidth = width;
|
|
1574
|
-
var rawHeight = height;
|
|
1575
1621
|
width = Math.max(8, width);
|
|
1576
1622
|
height = Math.max(8, height);
|
|
1577
|
-
// Re-anchor the pinned edge for w/n handles after
|
|
1578
|
-
//
|
|
1579
|
-
|
|
1580
|
-
if (handle.indexOf('
|
|
1623
|
+
// Re-anchor the pinned edge for w/n handles after aspect-ratio lock and
|
|
1624
|
+
// clamping so the opposite (e/s) edge stays fixed regardless of whether
|
|
1625
|
+
// the dimension change was driven by raw dx/dy or by the ratio lock.
|
|
1626
|
+
if (handle.indexOf('w') !== -1) left = origin.left + (origin.width - width);
|
|
1627
|
+
if (handle.indexOf('n') !== -1) top = origin.top + (origin.height - height);
|
|
1581
1628
|
if (ev.altKey) {
|
|
1582
1629
|
if (handle.indexOf('w') !== -1 || handle.indexOf('e') !== -1) left = origin.left - (width - origin.width) / 2;
|
|
1583
1630
|
if (handle.indexOf('n') !== -1 || handle.indexOf('s') !== -1) top = origin.top - (height - origin.height) / 2;
|
|
@@ -1958,10 +2005,20 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1958
2005
|
var move = pendingStructureMove;
|
|
1959
2006
|
pendingStructureMove = null;
|
|
1960
2007
|
if (e.data.applied) {
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2008
|
+
if (move.el && move.el.isConnected) {
|
|
2009
|
+
applyRuntimeReorder(move.el, move.target);
|
|
2010
|
+
selectedEl = move.el;
|
|
2011
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2012
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2013
|
+
}
|
|
2014
|
+
} else {
|
|
2015
|
+
// Revert the optimistic reorder to its pre-drag position.
|
|
2016
|
+
if (move.el && move.el.isConnected && move.origin && move.origin.prevParent && move.origin.prevParent.isConnected) {
|
|
2017
|
+
move.origin.prevParent.insertBefore(move.el, move.origin.prevNextSibling);
|
|
2018
|
+
selectedEl = move.el;
|
|
2019
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2020
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2021
|
+
}
|
|
1965
2022
|
}
|
|
1966
2023
|
return;
|
|
1967
2024
|
}
|
|
@@ -2660,6 +2717,7 @@ export function DesignCanvas({
|
|
|
2660
2717
|
visible={!!drawMode}
|
|
2661
2718
|
canvasInteractive={!pinMode}
|
|
2662
2719
|
queuedAnnotationCount={queuedAnnotationPins.length}
|
|
2720
|
+
zoom={zoom}
|
|
2663
2721
|
onClose={() => onExitDrawMode?.()}
|
|
2664
2722
|
onSend={(annotations, instruction, canvasSize) => {
|
|
2665
2723
|
const summary = annotations
|
|
@@ -137,10 +137,23 @@ interface EditPanelProps {
|
|
|
137
137
|
* valid CSS length. Lets users type "32" and get the expected "32px" when
|
|
138
138
|
* the field is committed.
|
|
139
139
|
*/
|
|
140
|
-
function normalizeLengthValue(raw: string, defaultUnit: string): string {
|
|
140
|
+
function normalizeLengthValue(raw: string, defaultUnit: string): string | null {
|
|
141
141
|
const trimmed = raw.trim();
|
|
142
|
-
|
|
142
|
+
// Empty / invalid input returns null so the caller reverts the field instead
|
|
143
|
+
// of committing an empty or garbage CSS value (e.g. fontSize:"" or
|
|
144
|
+
// flexBasis:"abc") to the element's inline style.
|
|
145
|
+
if (!trimmed) return null;
|
|
143
146
|
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return `${trimmed}${defaultUnit}`;
|
|
147
|
+
// Validate free-form CSS so junk text never reaches the style. Fall back to
|
|
148
|
+
// accepting the value when CSS.supports is unavailable (SSR/tests) to keep
|
|
149
|
+
// prior behavior in non-DOM environments.
|
|
150
|
+
if (typeof CSS !== "undefined" && typeof CSS.supports === "function") {
|
|
151
|
+
const ok =
|
|
152
|
+
CSS.supports("width", trimmed) ||
|
|
153
|
+
CSS.supports("font-size", trimmed) ||
|
|
154
|
+
CSS.supports("flex-basis", trimmed);
|
|
155
|
+
return ok ? trimmed : null;
|
|
156
|
+
}
|
|
144
157
|
return trimmed;
|
|
145
158
|
}
|
|
146
159
|
|
|
@@ -180,6 +193,11 @@ function PropInput({
|
|
|
180
193
|
return;
|
|
181
194
|
}
|
|
182
195
|
const next = normalizeLengthValue(draft, defaultUnit);
|
|
196
|
+
if (next === null) {
|
|
197
|
+
// Invalid or empty — revert the field to the last committed value.
|
|
198
|
+
setDraft(value);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
183
201
|
if (next !== draft) setDraft(next);
|
|
184
202
|
if (next !== value) onChange(next);
|
|
185
203
|
};
|
|
@@ -745,7 +763,11 @@ function gradientTypeFromCss(
|
|
|
745
763
|
layer: string,
|
|
746
764
|
): DesignGradientType {
|
|
747
765
|
if (functionName.toLowerCase() === "conic") return "angular";
|
|
748
|
-
|
|
766
|
+
// Recognize both diamond serializations — EditPanel's "closest-corner" and
|
|
767
|
+
// GradientEditor's "ellipse closest-side" — so a diamond authored in either
|
|
768
|
+
// place round-trips as diamond instead of flipping to radial.
|
|
769
|
+
if (/closest-corner/i.test(layer) || /ellipse\s+closest-side/i.test(layer))
|
|
770
|
+
return "diamond";
|
|
749
771
|
if (functionName.toLowerCase() === "radial") return "radial";
|
|
750
772
|
return "linear";
|
|
751
773
|
}
|
|
@@ -1170,9 +1192,39 @@ function resolveLineHeight(
|
|
|
1170
1192
|
return Number.isFinite(numeric) && numeric > 0 ? numeric : 1.2;
|
|
1171
1193
|
}
|
|
1172
1194
|
|
|
1195
|
+
// Matches a 2D rotate()/rotateZ() with any CSS angle unit (not rotateX/Y/3d).
|
|
1196
|
+
const ROTATE_FN_PATTERN =
|
|
1197
|
+
/rotate[Zz]?\(\s*([+-]?[\d.]+(?:e[+-]?\d+)?)(deg|rad|turn|grad)?\s*\)/i;
|
|
1198
|
+
|
|
1173
1199
|
function parseRotationValue(transform: string | undefined): number {
|
|
1174
|
-
|
|
1175
|
-
|
|
1200
|
+
if (!transform || transform === "none") return 0;
|
|
1201
|
+
const match = transform.match(ROTATE_FN_PATTERN);
|
|
1202
|
+
if (match) {
|
|
1203
|
+
const value = Number(match[1]);
|
|
1204
|
+
if (Number.isFinite(value)) {
|
|
1205
|
+
const unit = (match[2] || "deg").toLowerCase();
|
|
1206
|
+
const deg =
|
|
1207
|
+
unit === "rad"
|
|
1208
|
+
? value * (180 / Math.PI)
|
|
1209
|
+
: unit === "turn"
|
|
1210
|
+
? value * 360
|
|
1211
|
+
: unit === "grad"
|
|
1212
|
+
? value * 0.9
|
|
1213
|
+
: value;
|
|
1214
|
+
return Math.round(deg * 10) / 10;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
// Fallback for rotate3d()/matrix()/skew composites: read the 2D rotation
|
|
1218
|
+
// component off the resolved matrix so the panel doesn't report 0.
|
|
1219
|
+
if (typeof DOMMatrixReadOnly !== "undefined") {
|
|
1220
|
+
try {
|
|
1221
|
+
const m = new DOMMatrixReadOnly(transform);
|
|
1222
|
+
return Math.round(((Math.atan2(m.b, m.a) * 180) / Math.PI) * 10) / 10;
|
|
1223
|
+
} catch {
|
|
1224
|
+
// Unparseable transform — fall through to 0.
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return 0;
|
|
1176
1228
|
}
|
|
1177
1229
|
|
|
1178
1230
|
/**
|
|
@@ -1191,8 +1243,10 @@ function parseScaleValue(value: string | undefined): [number, number] {
|
|
|
1191
1243
|
function mergeRotationValue(transform: string | undefined, degrees: number) {
|
|
1192
1244
|
const nextRotate = `rotate(${Math.round(degrees * 10) / 10}deg)`;
|
|
1193
1245
|
if (!transform || transform === "none") return nextRotate;
|
|
1194
|
-
|
|
1195
|
-
|
|
1246
|
+
// Replace an existing rotate()/rotateZ() in ANY unit so we don't append a
|
|
1247
|
+
// second rotate() (which would compound, e.g. "rotate(0.5turn) rotate(30deg)").
|
|
1248
|
+
if (ROTATE_FN_PATTERN.test(transform)) {
|
|
1249
|
+
return transform.replace(ROTATE_FN_PATTERN, nextRotate);
|
|
1196
1250
|
}
|
|
1197
1251
|
return `${transform} ${nextRotate}`;
|
|
1198
1252
|
}
|
|
@@ -2025,10 +2079,10 @@ function StrokeLayerControl({
|
|
|
2025
2079
|
const nextPrefix = next === "outside" ? "outline" : "border";
|
|
2026
2080
|
onStyleChange(`${nextPrefix}Color`, color);
|
|
2027
2081
|
onStyleChange(`${nextPrefix}Width`, width || "1px");
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
);
|
|
2082
|
+
// Preserve the original border-style so a hidden stroke (style:none, kept
|
|
2083
|
+
// visible as a row because width>0) stays hidden when its position moves
|
|
2084
|
+
// between inside/outside. Only default to solid when there's no style at all.
|
|
2085
|
+
onStyleChange(`${nextPrefix}Style`, styleValue || "solid");
|
|
2032
2086
|
onRemove();
|
|
2033
2087
|
};
|
|
2034
2088
|
|
|
@@ -2149,7 +2203,12 @@ function parseShadowLayer(layer: string, index: number): ShadowLayer {
|
|
|
2149
2203
|
const tokens = splitCssTokens(layer);
|
|
2150
2204
|
const inset = tokens.includes("inset");
|
|
2151
2205
|
const colorToken =
|
|
2152
|
-
tokens.find((token) => parseCssColor(token) || token === "transparent")
|
|
2206
|
+
tokens.find((token) => parseCssColor(token) || token === "transparent") ??
|
|
2207
|
+
// Preserve a color we don't parse into RGBA (currentColor, var(--x), or any
|
|
2208
|
+
// unrecognized keyword): the color is the non-inset token that doesn't look
|
|
2209
|
+
// like a numeric length. Without this, tweaking x/y/blur would reset it to
|
|
2210
|
+
// the hardcoded default below.
|
|
2211
|
+
tokens.find((token) => token !== "inset" && !/^[-+]?[\d.]/.test(token)) ??
|
|
2153
2212
|
"rgba(0, 0, 0, 0.25)";
|
|
2154
2213
|
const numericTokens = tokens
|
|
2155
2214
|
.filter((token) => token !== "inset" && token !== colorToken)
|
|
@@ -3408,6 +3467,12 @@ function FillProperties({
|
|
|
3408
3467
|
const [hiddenFillStash, setHiddenFillStash] = useState<
|
|
3409
3468
|
Record<string, string>
|
|
3410
3469
|
>({});
|
|
3470
|
+
// Same non-destructive idea for background gradient/image layers: stash the
|
|
3471
|
+
// exact layer string on hide so per-stop opacity survives a hide→show toggle
|
|
3472
|
+
// instead of being flattened to all-0 then all-100.
|
|
3473
|
+
const [hiddenLayerStash, setHiddenLayerStash] = useState<
|
|
3474
|
+
Record<string, string>
|
|
3475
|
+
>({});
|
|
3411
3476
|
const stashKey = `${elementIdentityKey(element)}:${fillProperty}`;
|
|
3412
3477
|
const isHidden = fillValue === "transparent" || fillValue === "";
|
|
3413
3478
|
const handleFillVisibilityToggle = () => {
|
|
@@ -3664,16 +3729,50 @@ function FillProperties({
|
|
|
3664
3729
|
}
|
|
3665
3730
|
onClick={() => {
|
|
3666
3731
|
if (!gradient) return;
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3732
|
+
const layerStashKey = `${elementIdentityKey(
|
|
3733
|
+
element,
|
|
3734
|
+
)}:layer:${index}`;
|
|
3735
|
+
if (opacity <= 0) {
|
|
3736
|
+
// Show: restore the exact pre-hide layer if stashed,
|
|
3737
|
+
// otherwise fall back to forcing every stop opaque.
|
|
3738
|
+
const stashed = hiddenLayerStash[layerStashKey];
|
|
3739
|
+
if (stashed !== undefined) {
|
|
3740
|
+
replaceLayer(stashed);
|
|
3741
|
+
setHiddenLayerStash((prev) => {
|
|
3742
|
+
const next = { ...prev };
|
|
3743
|
+
delete next[layerStashKey];
|
|
3744
|
+
return next;
|
|
3745
|
+
});
|
|
3746
|
+
} else {
|
|
3747
|
+
replaceLayer(
|
|
3748
|
+
buildGradientLayer(
|
|
3749
|
+
gradient.type,
|
|
3750
|
+
gradient.stops.map((stop) => ({
|
|
3751
|
+
...stop,
|
|
3752
|
+
opacity: 100,
|
|
3753
|
+
})),
|
|
3754
|
+
gradient.prefix,
|
|
3755
|
+
),
|
|
3756
|
+
);
|
|
3757
|
+
}
|
|
3758
|
+
} else {
|
|
3759
|
+
// Hide: stash the current layer (with its real per-stop
|
|
3760
|
+
// opacities) before zeroing every stop's alpha.
|
|
3761
|
+
setHiddenLayerStash((prev) => ({
|
|
3762
|
+
...prev,
|
|
3763
|
+
[layerStashKey]: layer,
|
|
3764
|
+
}));
|
|
3765
|
+
replaceLayer(
|
|
3766
|
+
buildGradientLayer(
|
|
3767
|
+
gradient.type,
|
|
3768
|
+
gradient.stops.map((stop) => ({
|
|
3769
|
+
...stop,
|
|
3770
|
+
opacity: 0,
|
|
3771
|
+
})),
|
|
3772
|
+
gradient.prefix,
|
|
3773
|
+
),
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3677
3776
|
}}
|
|
3678
3777
|
>
|
|
3679
3778
|
{opacity <= 0 ? (
|
|
@@ -161,8 +161,24 @@ interface FlatLayerRow {
|
|
|
161
161
|
depth: number;
|
|
162
162
|
ancestorIds: string[];
|
|
163
163
|
hasChildren: boolean;
|
|
164
|
+
canAcceptChildren: boolean;
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
// Node types that can contain children even when currently empty.
|
|
168
|
+
// Leaf / void types (text, image, shape, rectangle) are excluded so we don't
|
|
169
|
+
// offer an "inside" drop zone on genuinely non-container elements.
|
|
170
|
+
const CONTAINER_TYPES = new Set<LayersPanelNodeType | undefined>([
|
|
171
|
+
"file",
|
|
172
|
+
"screen",
|
|
173
|
+
"frame",
|
|
174
|
+
"group",
|
|
175
|
+
"section",
|
|
176
|
+
"component",
|
|
177
|
+
"instance",
|
|
178
|
+
"code",
|
|
179
|
+
"element",
|
|
180
|
+
]);
|
|
181
|
+
|
|
166
182
|
const SECTION_CODE_ID = "__design_layers_code__";
|
|
167
183
|
const SECTION_ELEMENT_ID = "__design_layers_elements__";
|
|
168
184
|
|
|
@@ -170,6 +186,14 @@ const SECTION_ELEMENT_ID = "__design_layers_elements__";
|
|
|
170
186
|
// per spec; the source row stores the drag payload here on dragstart instead.
|
|
171
187
|
let activeDragState: { sourceId: string; draggedIds: string[] } | null = null;
|
|
172
188
|
|
|
189
|
+
const ROW_BASE_INDENT = 4;
|
|
190
|
+
const ROW_INDENT_STEP = 28;
|
|
191
|
+
const ROW_MAX_INDENT = 96;
|
|
192
|
+
|
|
193
|
+
function rowIndent(depth: number): number {
|
|
194
|
+
return Math.min(ROW_BASE_INDENT + depth * ROW_INDENT_STEP, ROW_MAX_INDENT);
|
|
195
|
+
}
|
|
196
|
+
|
|
173
197
|
function defaultLabels(t: ReturnType<typeof useT>): LayersPanelLabels {
|
|
174
198
|
return {
|
|
175
199
|
title: t("layersPanel.title"),
|
|
@@ -307,8 +331,16 @@ function flattenRows(
|
|
|
307
331
|
nodes.forEach((node, index) => {
|
|
308
332
|
const children = node.children ?? [];
|
|
309
333
|
const hasChildren = children.length > 0;
|
|
334
|
+
const canAcceptChildren = CONTAINER_TYPES.has(node.type);
|
|
310
335
|
const rowKey = `${parentKey}/${node.id}:${index}`;
|
|
311
|
-
rows.push({
|
|
336
|
+
rows.push({
|
|
337
|
+
node,
|
|
338
|
+
rowKey,
|
|
339
|
+
depth,
|
|
340
|
+
ancestorIds,
|
|
341
|
+
hasChildren,
|
|
342
|
+
canAcceptChildren,
|
|
343
|
+
});
|
|
312
344
|
if (hasChildren && (forceExpanded || expandedIds.has(node.id))) {
|
|
313
345
|
flattenRows(
|
|
314
346
|
children,
|
|
@@ -487,9 +519,20 @@ export function LayersPanel({
|
|
|
487
519
|
) => {
|
|
488
520
|
let nextIds: string[];
|
|
489
521
|
if (options.range && lastSelectionAnchorRef.current) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
522
|
+
let anchor = lastSelectionAnchorRef.current;
|
|
523
|
+
if (selectableVisibleIds.indexOf(anchor) < 0) {
|
|
524
|
+
// Stale anchor (deleted / filtered / collapsed out of view): pivot from
|
|
525
|
+
// the last selected layer that is still visible & selectable, matching
|
|
526
|
+
// Figma's behavior instead of dropping the range to a single select.
|
|
527
|
+
const fallback = [...selectedIds]
|
|
528
|
+
.reverse()
|
|
529
|
+
.find((sid) => selectableVisibleIds.includes(sid));
|
|
530
|
+
if (fallback) {
|
|
531
|
+
anchor = fallback;
|
|
532
|
+
lastSelectionAnchorRef.current = fallback;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const from = selectableVisibleIds.indexOf(anchor);
|
|
493
536
|
const to = selectableVisibleIds.indexOf(id);
|
|
494
537
|
if (from >= 0 && to >= 0) {
|
|
495
538
|
const [start, end] = from < to ? [from, to] : [to, from];
|
|
@@ -812,7 +855,7 @@ function LayerRow({
|
|
|
812
855
|
selectedIds: readonly string[];
|
|
813
856
|
visibleRows: FlatLayerRow[];
|
|
814
857
|
}) {
|
|
815
|
-
const { node, depth, hasChildren } = row;
|
|
858
|
+
const { node, depth, hasChildren, canAcceptChildren } = row;
|
|
816
859
|
const selectable = node.selectable !== false;
|
|
817
860
|
const lockable = node.lockable !== false && Boolean(onToggleLocked);
|
|
818
861
|
const hideable = node.hideable !== false && Boolean(onToggleHidden);
|
|
@@ -959,7 +1002,7 @@ function LayerRow({
|
|
|
959
1002
|
const intent = {
|
|
960
1003
|
draggedIds: cleanedIds,
|
|
961
1004
|
targetId: node.id,
|
|
962
|
-
placement: dropPlacementForEvent(event,
|
|
1005
|
+
placement: dropPlacementForEvent(event, canAcceptChildren),
|
|
963
1006
|
} satisfies LayersPanelMoveIntent;
|
|
964
1007
|
if (canMoveLayer && !canMoveLayer(intent)) return;
|
|
965
1008
|
event.preventDefault();
|
|
@@ -997,7 +1040,7 @@ function LayerRow({
|
|
|
997
1040
|
const intent = {
|
|
998
1041
|
draggedIds: cleanedIds,
|
|
999
1042
|
targetId: node.id,
|
|
1000
|
-
placement: dropPlacementForEvent(event,
|
|
1043
|
+
placement: dropPlacementForEvent(event, canAcceptChildren),
|
|
1001
1044
|
} satisfies LayersPanelMoveIntent;
|
|
1002
1045
|
if (!canMoveLayer || canMoveLayer(intent)) {
|
|
1003
1046
|
onMoveLayer(intent);
|
|
@@ -1058,7 +1101,7 @@ function LayerRow({
|
|
|
1058
1101
|
"text-foreground/90 hover:bg-[var(--design-editor-layer-hover-color)] hover:text-foreground",
|
|
1059
1102
|
node.hidden && "text-muted-foreground",
|
|
1060
1103
|
)}
|
|
1061
|
-
style={{ paddingLeft:
|
|
1104
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
1062
1105
|
>
|
|
1063
1106
|
{hasChildren ? (
|
|
1064
1107
|
<Button
|