Phase 3: drag pane past window edge to detach

Extends the existing header-drag gesture (which swaps panes inside
the window) with an "outside the window" case: release the drag more
than 60px past any viewport edge and the pane detaches into a new
window via the same moveToNewWindow path the right-click menu uses.

The 60px slop avoids triggering on accidental release over the OS
titlebar / window chrome — without it any drag that ended above
clientY=0 would fire as a detach, which is wrong because that area is
still inside the user's window.

No backend changes — Phase 2's transfer mechanism already handles
everything; this just wires a second entry point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-28 18:59:48 +01:00
parent 8ad51787fc
commit 6faf7e5e19
3 changed files with 32 additions and 6 deletions

View file

@ -268,6 +268,12 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
[orch.beginHeaderDrag, orch.setHeaderDragOver, leaf.id],
);
/** How far past a viewport edge the cursor must travel before a release
* is treated as "drag pane out of window" instead of "drop on empty
* space inside this window". Picked so an accidental release on the OS
* titlebar (~30px tall) stays inside the threshold. */
const PANE_DRAG_OUT_MARGIN = 60;
const onToolbarPointerUp = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => {
const st = dragStartRef.current;
@ -275,12 +281,26 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
const wasDragging = st.dragging;
dragStartRef.current = null;
if (wasDragging) {
document.body.style.cursor = "";
if (!wasDragging) return;
document.body.style.cursor = "";
const releasedFarOutside =
e.clientX < -PANE_DRAG_OUT_MARGIN ||
e.clientX > window.innerWidth + PANE_DRAG_OUT_MARGIN ||
e.clientY < -PANE_DRAG_OUT_MARGIN ||
e.clientY > window.innerHeight + PANE_DRAG_OUT_MARGIN;
if (releasedFarOutside) {
// Cancel any in-flight swap state without committing, then pop
// this pane into a fresh window. moveToNewWindow handles the
// PTY-handoff + closeLeaf in the source.
orch.endHeaderDrag(false);
orch.moveToNewWindow(leaf.id);
} else {
orch.endHeaderDrag(true);
}
},
[orch.endHeaderDrag],
[orch.endHeaderDrag, orch.moveToNewWindow, leaf.id],
);
const onToolbarPointerCancel = useCallback(