The pickup seeds its own slot — the lift and the shadow become one transaction

"At drag start it replaces the item's original space" is now true from the
very first frame: begin takes the run's own resting slot as a seed, set in
the same transaction that lifts the run out. Without it the vacated gap
closed un-animated and sprang back open at the first dropUpdated — a
shuffle carrying no information. The seed bypasses propose deliberately (a
pickup is not a new landing spot, so no alignment tick), and the first real
sample's re-propose of the same slot is the early-out's ordinary silence. A
⌥-pickup seeds nothing — a copy's resting layout keeps the originals in
place, so there is no vacated space to hold. Card pickups compute the seed
through the same LaneView.rendered rules the retargets count in; lane
pickups seed their strip slot the same way. DRAG-REORDER.md's own-slot
paragraph records the rule, and SeededPickupTests pins all four corners.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-07 11:22:26 -04:00
parent eef0a4539f
commit f126614b56
5 changed files with 176 additions and 9 deletions
+29 -1
View File
@@ -408,11 +408,39 @@ struct CardFaceView: View, Equatable {
// span-cap (03-board-ui.md § Motion).
heights: ordered.map { drops.registry.heights[$0] ?? drops.registry.nominalCardHeight },
container: .board,
source: store
source: store,
seed: ownSlotSeed(dragging: ids, laneID: lanesByCard[card.id], in: snapshot)
)
return payload.itemProvider()
}
/// The pickup's own-slot proposal (`DragSession.begin`): the grabbed card's position among its
/// lane's rendered cards with the dragged run lifted out the same resting-layout index space
/// the retargets and `moveCards` count in, filtered by the same `LaneView.rendered` rules, so
/// the seeded shadow run stands exactly where the lifted cards stood and pickup moves nothing.
///
/// **A -pickup seeds nothing.** A copy's resting layout keeps the originals in place, so there
/// is no vacated space for a shadow to hold; the first `dropUpdated` resolves the copy and opens
/// the run beside the originals, exactly as it does today (DRAG-REORDER.md § Resting-layout
/// zones the modifier reflow is the feedback being asked for).
private func ownSlotSeed(
dragging ids: Set<ItemID>,
laneID: ItemID?,
in snapshot: BoardModel
) -> DropTarget? {
guard !NSEvent.modifierFlags.contains(.option),
let laneID,
let laneCards = snapshot.lanes.first(where: { $0.id == laneID })?.cards
else { return nil }
let index = LaneView.rendered(
Array(laneCards.prefix { $0.id != card.id }),
hiddenByDrag: ids,
filter: store.searchFilter,
renaming: store.transient.renameEditor?.targetID
).count
return DropTarget(boardRoot: store.rootKey, container: .lane(laneID), index: index)
}
/// A trash card's drag out **the restore**, and deliberately not special: an ordinary `.cards`
/// session in the `.trash` container, which `BoardDropContext.commitDrop` hands to the same
/// `moveCards`/`copyCards`/`receiveCards` every board card uses. "Restoring is an ordinary move