Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions templates/design/.generated/bridge/editor-chrome.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2266,6 +2266,7 @@ export const editorChromeBridgeScript: string = `"use strict";
}
var passiveSelectionEls = [];
var passiveSelectionOverlays = [];
var multiSelectionBoundsOverlay = null;
var activeMarqueeSelection = null;
var activeTextEditEl = null;
var activeTextEditOriginalMinWidth = "";
Expand Down Expand Up @@ -2397,7 +2398,6 @@ export const editorChromeBridgeScript: string = `"use strict";
overlay.setAttribute("data-agent-native-soft-chrome", "true");
}
overlay.style.cssText = style === "soft" ? "position:fixed;pointer-events:none;z-index:99996;border:1px solid color-mix(in srgb,var(--design-editor-accent-color) 64%,transparent);background:color-mix(in srgb,var(--design-editor-accent-color) 5%,transparent);display:none;box-sizing:border-box;" : "position:fixed;pointer-events:none;z-index:99996;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;";
if (style !== "soft") appendPassiveSelectionHandles(overlay);
document.body.appendChild(overlay);
return overlay;
}
Expand Down Expand Up @@ -2430,6 +2430,7 @@ export const editorChromeBridgeScript: string = `"use strict";
passiveSelectionOverlays.push(overlay);
positionOverlay(overlay, el);
});
positionMultiSelectionBounds();
}
function preservePreviousSelectedElementForShiftClick(previous, next, e) {
if (!e?.shiftKey || !previous || !next || previous === next || !document.documentElement.contains(previous) || isLayerInteractionBlocked(previous)) {
Expand Down Expand Up @@ -3401,6 +3402,67 @@ export const editorChromeBridgeScript: string = `"use strict";
overlay.style.transformOrigin = "50% 50%";
return true;
}
function ensureMultiSelectionBoundsOverlay() {
if (multiSelectionBoundsOverlay) return multiSelectionBoundsOverlay;
var overlay = document.createElement("div");
overlay.setAttribute("data-agent-native-edit-overlay", "multi-selection");
overlay.setAttribute("data-agent-native-multi-selection-bounds", "true");
overlay.style.cssText = "position:fixed;pointer-events:none;z-index:99996;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;";
appendPassiveSelectionHandles(overlay);
document.body.appendChild(overlay);
multiSelectionBoundsOverlay = overlay;
return overlay;
}
function positionMultiSelectionBounds() {
var members = [];
if (selectedEl && document.documentElement.contains(selectedEl)) {
members.push(selectedEl);
}
passiveSelectionEls.forEach(function(el) {
if (el && document.documentElement.contains(el)) members.push(el);
});
if (members.length < 2 || selectionChromeHidden) {
if (multiSelectionBoundsOverlay) {
multiSelectionBoundsOverlay.style.display = "none";
}
return;
}
var rects = members.map(function(el) {
return el.getBoundingClientRect();
});
var left = Math.min.apply(
null,
rects.map(function(r) {
return r.left;
})
);
var top = Math.min.apply(
null,
rects.map(function(r) {
return r.top;
})
);
var right = Math.max.apply(
null,
rects.map(function(r) {
return r.right;
})
);
var bottom = Math.max.apply(
null,
rects.map(function(r) {
return r.bottom;
})
);
var overlay = ensureMultiSelectionBoundsOverlay();
overlay.style.display = "block";
overlay.style.transform = "none";
overlay.style.left = left + "px";
overlay.style.top = top + "px";
overlay.style.width = Math.max(0, right - left) + "px";
overlay.style.height = Math.max(0, bottom - top) + "px";
scalePassiveSelectionOverlay(overlay);
}
function positionOverlay(overlay, el) {
if (!el || !document.documentElement.contains(el)) {
overlay.style.display = "none";
Expand Down Expand Up @@ -3460,6 +3522,7 @@ export const editorChromeBridgeScript: string = `"use strict";
var overlay = passiveSelectionOverlays[index];
if (overlay) positionOverlay(overlay, el);
});
positionMultiSelectionBounds();
positionGradientOverlay();
syncOverlayObservers();
}
Expand Down Expand Up @@ -3725,10 +3788,12 @@ export const editorChromeBridgeScript: string = `"use strict";
if (isOverlayElement(target)) continue;
if (isLayerInteractionBlocked(target)) {
lastEditorPointWasBlocked = true;
dndLog("select:blocked", { el: getSelector(target) });
return null;
}
return target;
}
dndLog("select:nothing-at-point", { x: clientX, y: clientY });
return null;
}
function stopNativeInteraction(e) {
Expand Down Expand Up @@ -5319,8 +5384,7 @@ export const editorChromeBridgeScript: string = `"use strict";
function isAbsolutePrimitiveContainer(el) {
if (!el || (el.tagName || "").toLowerCase() !== "div") return false;
var primitive = (el.getAttribute("data-an-primitive") || el.getAttribute("data-agent-native-primitive") || "").toLowerCase();
if (primitive !== "rectangle" && primitive !== "rect" && primitive !== "frame")
return false;
if (primitive !== "frame") return false;
var cs = window.getComputedStyle(el);
return cs.position === "absolute" || cs.position === "fixed";
}
Expand Down Expand Up @@ -5584,6 +5648,8 @@ export const editorChromeBridgeScript: string = `"use strict";
if (!el || el === document.documentElement) return false;
if (isOverlayElement(el) || isLayerInteractionBlocked(el)) return false;
if (el === document.body) return true;
var primitiveKind = el.getAttribute("data-an-primitive");
if (primitiveKind && primitiveKind !== "frame") return false;
var tag = (el.tagName || "").toLowerCase();
if (BRIDGE_LEAF_TAGS.indexOf(tag) !== -1 || BRIDGE_TEXT_TAGS.indexOf(tag) !== -1)
return false;
Expand Down Expand Up @@ -8823,6 +8889,7 @@ export const editorChromeBridgeScript: string = `"use strict";
} else {
positionOverlay(selectionOverlay, target);
}
positionMultiSelectionBounds();
if (hoveredEl === selectedEl) highlightOverlay.style.display = "none";
if (selectionChangedByHost) {
postElementSelect(target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@ describe("responsive mirrored selection chrome", () => {
expect(bridgeSource).toContain(
"color-mix(in srgb,var(--design-editor-accent-color) 64%,transparent)",
);
expect(bridgeSource).toContain(
// Handles live on the combined multi-selection bounds box, so no
// per-element passive overlay — soft or default — grows its own.
expect(bridgeSource).not.toContain(
'if (style !== "soft") appendPassiveSelectionHandles(overlay);',
);
expect(
bridgeSource.split("appendPassiveSelectionHandles(overlay)").length - 1,
"handles must be appended only to the combined bounds overlay",
).toBe(1);
expect(bridgeSource).toContain(
'e.data.passiveSelectionStyle === "soft" ? "soft" : "default"',
);
Expand Down
12 changes: 12 additions & 0 deletions templates/design/app/components/design/DesignCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4444,6 +4444,18 @@ export function DesignCanvas({
readOnly,
})}
data-design-preview-iframe
onLoad={(event) => {
// The bridge logs into the IFRAME console and cannot read
// import.meta.env, so dev has to switch it on from out here.
if (!import.meta.env?.DEV) return;
try {
const win = event.currentTarget.contentWindow as
| (Window & { __DND_DEBUG?: boolean })
| null;
if (win) win.__DND_DEBUG = true;
// coercion-ok: a cross-origin preview exposes no contentWindow
} catch {}
}}
{...{
[SESSION_REPLAY_IFRAME_ATTRIBUTE]: !externalPreviewUrl
? ""
Expand Down
21 changes: 19 additions & 2 deletions templates/design/app/components/design/LayersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,9 @@ const LayerRow = memo(function LayerRow({
// doing so.
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
if (event.key === "Enter" || event.key === " " || event.key === "Space") {
// Figma drills into the selection on Enter and walks up on Shift+Enter;
// re-selecting an already-selected row here would swallow both.
if (event.key === "Enter" && isSelected) return;
event.preventDefault();
if (!selectable) return;
onSelect(node.id, {
Expand All @@ -1797,12 +1800,21 @@ const LayerRow = memo(function LayerRow({
onStartRename(node);
return;
}
if (event.key === "ArrowRight" && hasChildren && !isExpanded) {
// Only the PLAIN chord toggles the chevron: Shift+Arrow is Figma's big
// nudge and every modified arrow belongs to the canvas hotkeys below.
const plainArrow =
!event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey;
if (
plainArrow &&
event.key === "ArrowRight" &&
hasChildren &&
!isExpanded
) {
event.preventDefault();
onToggleExpanded(node.id, true);
return;
}
if (event.key === "ArrowLeft" && hasChildren && isExpanded) {
if (plainArrow && event.key === "ArrowLeft" && hasChildren && isExpanded) {
event.preventDefault();
onToggleExpanded(node.id, false);
return;
Expand Down Expand Up @@ -2077,17 +2089,22 @@ const LayerRow = memo(function LayerRow({
>
{activeDrop === "before" ? (
<span
data-layer-drop-indicator="before"
className="pointer-events-none absolute right-2 top-0 z-10 h-px bg-[var(--design-editor-accent-color)]"
style={{ left: rowIndent(depth) }}
/>
) : null}
{activeDrop === "after" ? (
<span
data-layer-drop-indicator="after"
className="pointer-events-none absolute bottom-0 right-2 z-10 h-px bg-[var(--design-editor-accent-color)]"
style={{ left: rowIndent(depth) }}
/>
) : null}
<div
data-layer-drop-indicator={
activeDrop === "inside" ? "inside" : undefined
}
className={cn(
"group flex h-7 w-max min-w-full items-center gap-1 rounded-[5px] pr-1 text-[12px] bg-[var(--design-editor-panel-bg)]",
activeDrop === "inside" &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2029,3 +2029,26 @@ describe("getOutsideFrameDraftFallback", () => {
).toBeUndefined();
});
});

describe("board surface background follows the editor theme", () => {
const preview = (background?: string) =>
getBoardSurfaceStaticPreviewContent({
html: `<!doctype html><html><head></head><body><div data-agent-native-node-id="a" style="position:absolute;left:0;top:0;width:10px;height:10px"></div></body></html>`,
logicalGeometry: { x: 0, y: 0, width: 1000, height: 1000 },
viewport: { width: 500, height: 500 },
background,
});

it("paints the themed canvas colour when one is supplied", () => {
// The board is its own iframe and cannot read the host's CSS vars, so a
// hardcoded dark fill made the canvas black in the light theme.
const content = preview("hsl(0 0% 92%)");
expect(content).toContain("hsl(0 0% 92%)");
expect(content).not.toContain("hsl(0, 0%, 10%)");
});

it("falls back to the dark default when no theme colour is resolved", () => {
expect(preview()).toContain("hsl(0, 0%, 10%)");
expect(preview(" ")).toContain("hsl(0, 0%, 10%)");
});
});
Loading
Loading