Skip to content
Open
1 change: 1 addition & 0 deletions scripts/i18n-raw-literal-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ templates/design/app/components/design/code-workbench/workspace/types.ts|` or `l
templates/design/app/components/design/edit-panel/effects-properties.tsx|, elementKey: string, nextLayers: readonly Pick
templates/design/app/components/design/multi-screen/screen-content-cache.ts|, existingScreenIds: ReadonlySet
templates/design/app/components/design/multi-screen/screen-content-cache.ts|, liveScreenIds: ReadonlySet
templates/design/app/pages/DesignEditor.tsx|Auto-layout children follow their container — adjust the layout instead of aligning.
templates/design/app/pages/DesignEditor.tsx|Could not open component source
templates/design/app/pages/DesignEditor.tsx|Migration failed
templates/design/app/pages/design-editor/pending-edits.ts|, primaryInfo?: Pick
105 changes: 95 additions & 10 deletions templates/design/.generated/bridge/editor-chrome.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export const editorChromeBridgeScript: string = `"use strict";
return false;
}
})();
var selectedLayerDragPriorityEnabled = (function() {
try {
return !!__SELECTED_LAYER_DRAG_PRIORITY__;
} catch (_e) {
return false;
}
})();
var scaleToolEnabled = false;
function dndLog(phase, data) {
if (!window.__DND_DEBUG) return;
Expand Down Expand Up @@ -1596,6 +1603,7 @@ export const editorChromeBridgeScript: string = `"use strict";
clearComponentTag();
}
var selectedEl = null;
var selectionChromeHidden = false;
var hoveredEl = null;
var highlightOverlayStyle = "default";
var activeNodeHtmlPreview = null;
Expand Down Expand Up @@ -2773,7 +2781,11 @@ export const editorChromeBridgeScript: string = `"use strict";
hideSelectionOverlay();
}
} else if (selectedEl) {
positionOverlay(selectionOverlay, selectedEl);
if (selectionChromeHidden) {
hideSelectionOverlay();
} else {
positionOverlay(selectionOverlay, selectedEl);
}
} else {
hideParentAutoLayoutOverlay();
}
Expand Down Expand Up @@ -3338,6 +3350,28 @@ export const editorChromeBridgeScript: string = `"use strict";
window.parent.postMessage({ type: "clear-selection" }, "*");
return;
}
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && selectedEl) {
var stack = collectLayerHitCandidates(e.clientX, e.clientY);
var currentIdx = stack.elements.indexOf(selectedEl);
if (currentIdx !== -1) {
var stackKeys = stack.layerCandidates.map(function(candidate) {
return candidate.key;
});
var nextKey = nextStackCandidate(
stackKeys,
stack.layerCandidates[currentIdx].key
);
var nextEl = nextKey === null ? null : stack.elements[stackKeys.indexOf(nextKey)];
if (nextEl && !isLayerInteractionBlocked(nextEl)) {
selectedSpacingHovered = false;
hoveredSpacingHandleKey = "";
selectedEl = nextEl;
positionOverlay(selectionOverlay, selectedEl);
postElementSelect(selectedEl);
return;
}
}
}
selectedSpacingHovered = false;
hoveredSpacingHandleKey = "";
var previousSelectedEl = selectedEl;
Expand Down Expand Up @@ -3507,29 +3541,27 @@ export const editorChromeBridgeScript: string = `"use strict";
document.addEventListener(events.move, onMove, true);
document.addEventListener(events.up, onUp, true);
}
function openContextMenuAtEvent(e) {
stopNativeInteraction(e);
blurActiveTextEditor();
function collectLayerHitCandidates(clientX, clientY) {
var shieldPointerEvents = shieldOverlay.style.pointerEvents;
var selectionPointerEvents = selectionOverlay.style.pointerEvents;
var highlightPointerEvents = highlightOverlay.style.pointerEvents;
shieldOverlay.style.pointerEvents = "none";
selectionOverlay.style.pointerEvents = "none";
highlightOverlay.style.pointerEvents = "none";
var pointTargets = document.elementsFromPoint ? document.elementsFromPoint(e.clientX, e.clientY) : [document.elementFromPoint(e.clientX, e.clientY)];
var pointTargets = document.elementsFromPoint ? document.elementsFromPoint(clientX, clientY) : [document.elementFromPoint(clientX, clientY)];
shieldOverlay.style.pointerEvents = shieldPointerEvents;
selectionOverlay.style.pointerEvents = selectionPointerEvents;
highlightOverlay.style.pointerEvents = highlightPointerEvents;
var candidateElements = [];
var elements = [];
var layerCandidates = [];
pointTargets.forEach(function(pointTarget) {
if (!pointTarget || pointTarget.nodeType !== 1) return;
if (isOverlayElement(pointTarget)) return;
var candidate = selectionTargetForHit(pointTarget);
if (!candidate || isDocumentRootElement(candidate) || isOverlayElement(candidate) || isLayerInteractionBlocked(candidate) || isTemplateCloneElement(candidate) || candidateElements.indexOf(candidate) !== -1) {
if (!candidate || isDocumentRootElement(candidate) || isOverlayElement(candidate) || isLayerInteractionBlocked(candidate) || isTemplateCloneElement(candidate) || elements.indexOf(candidate) !== -1) {
return;
}
candidateElements.push(candidate);
elements.push(candidate);
var candidateInfo = getElementInfo(candidate);
var explicitLabel = candidate.getAttribute && candidate.getAttribute("data-agent-native-layer-name") || "";
var textLabel = (candidate.textContent || "").trim().replace(/\\s+/g, " ");
Expand All @@ -3541,6 +3573,14 @@ export const editorChromeBridgeScript: string = `"use strict";
info: candidateInfo
});
});
return { elements, layerCandidates };
}
function openContextMenuAtEvent(e) {
stopNativeInteraction(e);
blurActiveTextEditor();
var collected = collectLayerHitCandidates(e.clientX, e.clientY);
var candidateElements = collected.elements;
var layerCandidates = collected.layerCandidates;
var target = candidateElements[0] || null;
var info = null;
if (target) {
Expand Down Expand Up @@ -6238,7 +6278,8 @@ export const editorChromeBridgeScript: string = `"use strict";
height: dragElStartHeight
},
snapCandidateRects,
SNAP_THRESHOLD_PX
// Convert the screen-space base to content px (1/zoom).
SNAP_THRESHOLD_PX * chromeLineScale()
) : { dx: 0, dy: 0, guideV: null, guideH: null };
nextLeft += snapResult.dx;
nextTop += snapResult.dy;
Expand Down Expand Up @@ -6726,6 +6767,29 @@ export const editorChromeBridgeScript: string = `"use strict";
}
pendingShieldDrag = null;
}
function dragTargetForPointerDown(args) {
var selectedEl2 = args.selectedEl;
var hitEl = args.hitEl;
var hitRaw = args.hitRaw || hitEl;
var selectedAlive = !!args.selectedAlive;
if (selectedEl2 && selectedAlive && selectedEl2.contains && selectedEl2.contains(hitRaw)) {
return selectedEl2;
}
if (args.preferSelected && selectedEl2 && selectedAlive) {
var r = args.selectedRect;
var p = args.point;
if (r && p && r.width > 0 && r.height > 0 && p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom) {
return selectedEl2;
}
}
return hitEl;
}
function nextStackCandidate(candidateKeys, currentKey) {
if (!candidateKeys || candidateKeys.length === 0) return null;
var idx = candidateKeys.indexOf(currentKey);
if (idx === -1) return null;
return candidateKeys[(idx + 1) % candidateKeys.length];
}
function beginPotentialShieldDrag(e) {
stopNativeInteraction(e);
if (e.button !== 0) return;
Expand All @@ -6737,7 +6801,17 @@ export const editorChromeBridgeScript: string = `"use strict";
beginMarqueeSelection(e);
return;
}
var dragTarget = selectedEl && document.documentElement.contains(selectedEl) && selectedEl.contains(hit) ? selectedEl : hitTarget;
var selectedAlive = !!selectedEl && document.documentElement.contains(selectedEl);
var selectedRect = selectedAlive && selectedEl.getBoundingClientRect ? selectedEl.getBoundingClientRect() : null;
var dragTarget = dragTargetForPointerDown({
selectedEl,
selectedAlive,
selectedRect,
hitEl: hitTarget,
hitRaw: hit,
point: { x: e.clientX, y: e.clientY },
preferSelected: selectedLayerDragPriorityEnabled
});
var clickTarget = hitTarget;
if (!dragTarget || dragTarget === document.body || dragTarget === document.documentElement || isLayerInteractionBlocked(dragTarget)) {
return;
Expand Down Expand Up @@ -7876,6 +7950,17 @@ export const editorChromeBridgeScript: string = `"use strict";
);
return;
}
if (e.data.type === "set-selection-chrome-hidden") {
selectionChromeHidden = !!e.data.hidden;
if (selectionChromeHidden) {
hideSelectionOverlay();
} else if (selectedEl) {
positionOverlay(selectionOverlay, selectedEl);
updateParentAutoLayoutOverlay(selectedEl);
refreshOverlays();
}
return;
}
if (e.data.type === "select-element") {
var candidates = [];
if (Array.isArray(e.data.selectorCandidates)) {
Expand Down
26 changes: 26 additions & 0 deletions templates/design/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,32 @@ ladder.
bridge URL, and snapshot/state references when moving between actions so later
flow-edge derivation has stable anchors.

## Chrome placement

Editor chrome has fixed homes; put new UI in the right one so surfaces stay
predictable and Figma-clean.

- **Bottom toolbar**: cursor-changing creation/manipulation tools only —
move/hand/scale, frame, shapes, pen, text, comment. Never mode switches
(annotate/edit/interact) or one-off command buttons.
- **Left rail**: orientation and structure — pages, layers, assets, and the
`code` workbench.
- **Right panel**: properties, progressively disclosed (the EditPanel model).
- **Bottom status area**: warnings, sync/offline state, library updates, and the
outbox conflict count; render nothing when all-clear — the strip is
Figma-silent and usually invisible.
- **Long-tail features**: extensions in the left-rail tools/extensions surface,
not new permanent toolbar buttons.
- **No new always-visible controls for an existing command.** Do not add a
permanent button to fix discoverability; use the command menu, context menu, or
an existing surface. New permanent chrome for an existing command is a
regression.
- The mode switch (annotate/edit/interact) belongs in a compact segmented
control near where mode context already lives (top-right), not in the
cursor-tool pill.
- `Cmd+\` hides the whole chrome set (rail + inspector + toolbar); any new chrome
must participate in that hide.

## Application State

- `navigation` tells you the current view, design id, file id, and related UI
Expand Down
17 changes: 17 additions & 0 deletions templates/design/app/components/design/DesignCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,15 @@ ${editorChromeBridgeScript}
*/
const LIVE_REFLOW_ENABLED = true;

/**
* Rollout gate: when on, a pointerdown inside the current selection's box keeps
* the selected element as the drag target even when an overlapping
* non-descendant sibling wins the hit test. Baked into the bridge as
* `__SELECTED_LAYER_DRAG_PRIORITY__`; flip to `false` for descendant-only
* behavior.
*/
const SELECTED_LAYER_DRAG_PRIORITY_ENABLED = true;

interface DesignCanvasProps {
content: string;
contentKey?: string;
Expand Down Expand Up @@ -949,6 +958,10 @@ function buildEditorChromeBridgeScript(args: {
"__LIVE_REFLOW_ENABLED__",
LIVE_REFLOW_ENABLED ? "true" : "false",
)
.replace(
"__SELECTED_LAYER_DRAG_PRIORITY__",
SELECTED_LAYER_DRAG_PRIORITY_ENABLED ? "true" : "false",
)
);
}

Expand Down Expand Up @@ -2130,6 +2143,10 @@ export function DesignCanvas({
.replace(
"__LIVE_REFLOW_ENABLED__",
LIVE_REFLOW_ENABLED ? "true" : "false",
)
.replace(
"__SELECTED_LAYER_DRAG_PRIORITY__",
SELECTED_LAYER_DRAG_PRIORITY_ENABLED ? "true" : "false",
);
// ALWAYS injected (like the other always-on bridges above) so
// MultiScreenCanvas's cross-screen drag hit-testing
Expand Down
Loading
Loading