fix(canvas): plain drag out of parent un-nests again
Un-nest used to require holding Alt (or Cmd to force-detach). That
was too conservative — when a user dragged a child clearly outside
its parent's bbox, nothing happened on release, because the default
branch soft-clamped back and only the Alt branch actually opened
the "Extract?" confirm. Matches the exact bug the user just flagged
("I can put agents in other agent, but when I drag it out, it does
not move out").
New rules:
* Past the 20 % hysteresis → confirm un-nest. Plain drag, no
modifier. This is what most users expect (Miro / Figma behave
the same way — drag outside the frame and the shape leaves it).
* Inside or within 20 % of the edge → soft-clamp back inside.
Guards against twitchy releases that momentarily overshoot the
edge by a few pixels.
* Cmd / Ctrl → force un-nest regardless of overlap. Escape-hatch
for when the user dragged within the hysteresis zone but really
wants out.
* Dropping onto a different parent → nest there (unchanged).
Alt is no longer a required modifier for un-nesting. Keeps it as
a non-gesture modifier only; no meaning unless we re-bind it later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2a4b6e0d3
commit
512fdfd59d
@ -138,25 +138,22 @@ export function useDragHandlers(): DragHandlers {
|
||||
|
||||
const nodeName = node.data.name;
|
||||
const currentParentId = node.data.parentId;
|
||||
const altHeld = event.altKey || dragModifiersRef.current.alt;
|
||||
const forceDetach =
|
||||
event.metaKey || event.ctrlKey || dragModifiersRef.current.meta;
|
||||
const droppingIntoAnotherParent =
|
||||
!!dragOverNodeId && dragOverNodeId !== currentParentId;
|
||||
|
||||
// Soft clamp (plain drag, no modifier, not re-parenting): snap
|
||||
// the child back inside its current parent. Alt or Cmd bypass.
|
||||
if (
|
||||
currentParentId &&
|
||||
!altHeld &&
|
||||
!forceDetach &&
|
||||
!droppingIntoAnotherParent &&
|
||||
shouldDetach(node.id, currentParentId, getInternalNode)
|
||||
) {
|
||||
clampChildIntoParent(node.id, currentParentId, getInternalNode);
|
||||
}
|
||||
// Past the 20 %-overlap hysteresis? Treat the gesture as a
|
||||
// deliberate drag-out. Below that threshold we soft-clamp the
|
||||
// child back inside so a twitchy release doesn't un-nest
|
||||
// accidentally (same intent as before, just: plain drag works
|
||||
// without a modifier now).
|
||||
const pastHysteresis =
|
||||
!!currentParentId &&
|
||||
shouldDetach(node.id, currentParentId, getInternalNode);
|
||||
|
||||
if (droppingIntoAnotherParent) {
|
||||
// Explicit drop onto another workspace always wins over
|
||||
// clamp/detach — the user pointed at a new target.
|
||||
const targetNode = allNodes.find((n) => n.id === dragOverNodeId);
|
||||
const targetName = targetNode?.data.name || "Unknown";
|
||||
setPendingNest({
|
||||
@ -165,11 +162,9 @@ export function useDragHandlers(): DragHandlers {
|
||||
nodeName,
|
||||
targetName,
|
||||
});
|
||||
} else if (
|
||||
currentParentId &&
|
||||
(forceDetach ||
|
||||
(altHeld && shouldDetach(node.id, currentParentId, getInternalNode)))
|
||||
) {
|
||||
} else if (currentParentId && (forceDetach || pastHysteresis)) {
|
||||
// Dragged past the edge (or Cmd-held as a force override): the
|
||||
// user wants out of the parent. Confirm the un-nest.
|
||||
const parentNode = allNodes.find((n) => n.id === currentParentId);
|
||||
const parentName = parentNode?.data.name || "Unknown";
|
||||
setPendingNest({
|
||||
@ -178,6 +173,11 @@ export function useDragHandlers(): DragHandlers {
|
||||
nodeName,
|
||||
targetName: parentName,
|
||||
});
|
||||
} else if (currentParentId) {
|
||||
// Still inside parent but the drag ended slightly past the
|
||||
// edge (under 20 % outside). Snap back in so the card doesn't
|
||||
// visually spill — Miro frame behaviour.
|
||||
clampChildIntoParent(node.id, currentParentId, getInternalNode);
|
||||
}
|
||||
|
||||
const internal = getInternalNode(node.id);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user