Context
#14850 (merged) wired the dock's first gesture — a tab reorders WITHIN its own tabs node. This leaf delivers the next gesture: dragging a tab header OUT of its zone and releasing it over a different zone relocates the item in the committed dockZone.v1 document — the "drop a tab into a new target drop zone" gesture.
Scope note (narrowed 2026-07-05, per PR #14864 review): this leaf is the functional cross-zone model-move slice — hit-test the zone under the pointer on drop → commit moveItem directly. The full drop-zone producer's dockPreview.v1 production, the DockPreview drag-time hover affordance, edge-*/split-* placements, and the hit-test unit are split to #14866 (which is what #14769's previewFor seam binds to). This keeps the close-target honest: the PR delivers exactly this slice.
The Fix (delivered)
Reuse-and-route, no parallel drag system. src/dashboard/DockTabSortZone.mjs (a dock-aware tab-header SortZone) rides the existing drag lifecycle and, on drop, fires a dockCrossZoneDrop event on its tab.Container (before super — the base drag-end + deferred re-projection would otherwise destroy the node). DockLayoutAdapter.projectTabsNode wires the listener beside the within-container moveTo one; the listener's closure holds the reducer (context.onDockCrossZoneDrop, threaded through project()), so it survives config cloning without a component-tree walk. MainContainer.onDockCrossZoneDrop hit-tests the rendered tabs zones (getDomRect) and commits moveItem; a same-zone drop is a no-op.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
DockTabSortZone.processDragEnd |
Neo.draggable.tab.header.toolbar.SortZone; ADR 0029 no-parallel-drag |
Fires dockCrossZoneDrop on the tab.Container with release point + item id |
No owner listener / non-numeric coords → no-op |
JSDoc |
e2e DockCrossZoneDragNL |
MainContainer.onDockCrossZoneDrop reducer |
DockZoneModel.moveItem; landed applyDockZoneOperation/onDockZoneDocumentChange |
Hit-tests rendered zones → commits moveItem cross-zone |
Same-zone / off-zone → no-op, fail-closed on errors[] |
JSDoc |
e2e worker-truth before/after |
Decision Record impact
aligned-with ADR 0029 (rides the existing SortZone lifecycle; converts to a semantic operation on drop). The dockPreview.v1 preview→operation refinement rides #14866.
Acceptance Criteria
Out of Scope (→ #14866)
dockPreview.v1 production · DockPreview hover-affordance rendering · edge-*/split-* placements · hit-test unit test · cross-window transfer (#14769).
Related
Parent #13158 · builds on #14850 · follow-up #14866 (the dockPreview.v1 producer + rendering, which unblocks #14769) · authority ADR 0029 §2.3/§2.6.
Origin Session ID: 9e42a8de-4291-46fc-944e-92ceb0db1748
Retrieval Hint: "dock cross-zone tab drag DockTabSortZone dockCrossZoneDrop moveItem functional slice"
Context
#14850 (merged) wired the dock's first gesture — a tab reorders WITHIN its own tabs node. This leaf delivers the next gesture: dragging a tab header OUT of its zone and releasing it over a different zone relocates the item in the committed
dockZone.v1document — the "drop a tab into a new target drop zone" gesture.Scope note (narrowed 2026-07-05, per PR #14864 review): this leaf is the functional cross-zone model-move slice — hit-test the zone under the pointer on drop → commit
moveItemdirectly. The full drop-zone producer'sdockPreview.v1production, theDockPreviewdrag-time hover affordance,edge-*/split-*placements, and the hit-test unit are split to #14866 (which is what#14769'spreviewForseam binds to). This keeps the close-target honest: the PR delivers exactly this slice.The Fix (delivered)
Reuse-and-route, no parallel drag system.
src/dashboard/DockTabSortZone.mjs(a dock-aware tab-header SortZone) rides the existing drag lifecycle and, on drop, fires adockCrossZoneDropevent on itstab.Container(beforesuper— the base drag-end + deferred re-projection would otherwise destroy the node).DockLayoutAdapter.projectTabsNodewires the listener beside the within-containermoveToone; the listener's closure holds the reducer (context.onDockCrossZoneDrop, threaded throughproject()), so it survives config cloning without a component-tree walk.MainContainer.onDockCrossZoneDrophit-tests the rendered tabs zones (getDomRect) and commitsmoveItem; a same-zone drop is a no-op.Contract Ledger Matrix
DockTabSortZone.processDragEndNeo.draggable.tab.header.toolbar.SortZone; ADR 0029 no-parallel-dragdockCrossZoneDropon the tab.Container with release point + item idDockCrossZoneDragNLMainContainer.onDockCrossZoneDropreducerDockZoneModel.moveItem; landedapplyDockZoneOperation/onDockZoneDocumentChangemoveItemcross-zoneerrors[]Decision Record impact
aligned-with ADR 0029(rides the existing SortZone lifecycle; converts to a semantic operation on drop). ThedockPreview.v1preview→operation refinement rides #14866.Acceptance Criteria
moveItemto that zone (App-Worker truth), fail-closed. — delivered, PR #14864strategy: main-tabs → terminal-tabs), ships green in the PR. — deliveredDockLayoutAdapter+DockZoneModelunit specs stay green; within-container e2e regression green. — deliveredOut of Scope (→ #14866)
dockPreview.v1production ·DockPreviewhover-affordance rendering ·edge-*/split-*placements · hit-test unit test · cross-window transfer (#14769).Related
Parent
#13158· builds on#14850· follow-up#14866(thedockPreview.v1producer + rendering, which unblocks#14769) · authority ADR 0029 §2.3/§2.6.Origin Session ID: 9e42a8de-4291-46fc-944e-92ceb0db1748 Retrieval Hint: "dock cross-zone tab drag DockTabSortZone dockCrossZoneDrop moveItem functional slice"