Drop a card on the shown trash to delete it

04's ruling makes the drag the pointer's delete gesture: the shown
trash column accepts live same-board card drags, the shadow pinned
topmost — honest, since the trash sorts by deleted newest-first — and
release tombstones through the same write path as Backspace, extracted
so the two gestures cannot drift. DropTarget grew a container case for
the quasi-lane (it has no lane id by construction); lane drags,
cross-board arrivals, option-copies (re-checked at release, the one
input that can flip without a callback), trashed-side payloads, hidden
trash, and the read-only lock all refuse — and a refusal falls through
to the strip retarget, never cancelling the drag. The settle draws the
tombstoned rows in the trash under the cards' own GUIDs, so the echo is
an invisible content swap and nothing winks out for a round trip.
Selection needs no surgery: the reload's resolve rule ejects tombstoned
members as the vanish it is, pinned by a test contrasting both gestures.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 08:12:09 -04:00
parent 1020d9fca4
commit 33bf425f25
6 changed files with 819 additions and 72 deletions
+47 -5
View File
@@ -1918,11 +1918,8 @@ public final class BoardStore {
// walks the filtered lane rather than selecting a card the query has hidden.
let successor = SelectionGrammar.successor(afterDeleting: ids, in: snapshot, filter: searchFilter)
try? performWrite { () throws(BoardWriteError) -> Void in
for folder in folders {
try BoardWriter.deleteItem(at: folder)
}
}
tombstone(folders)
if let successor {
select([successor], liveness: .live, anchor: successor, head: successor)
} else {
@@ -1930,6 +1927,51 @@ public final class BoardStore {
}
}
/// **Drop-on-trash deletes** (04-interactions.md The trash, settled 2026-07-28): "the drag
/// becomes the pointer's delete gesture release tombstones the dragged card(s), exactly the
/// tombstone".
///
/// *Exactly* the tombstone is a claim about the disk, and `tombstone(_:)` is what makes it
/// structural rather than a matter of two call sites staying in step: one write op, one bracket,
/// one set of stamps, so a card deleted by drop and a card deleted by keystroke are
/// byte-indistinguishable afterwards (`TrashDropWriteTests`).
///
/// ### The one thing it does not share is the successor
///
/// moves the selection to the deleted item's successor sibling because *the selection* lost its
/// cards and "repeated walks down a lane" the rule exists to keep a keyboard gesture
/// repeatable. A drag has no such continuation, and its run is **not necessarily the selection at
/// all**: dragging a card outside the selection drags that card alone and leaves the selection
/// exactly where it was (`LaneView.startCardDrag`), so picking a successor for it would re-point a
/// selection that never lost anything.
///
/// So this writes and says nothing about the selection, and the ordinary reload does the rest: a
/// live-side set ejects members that flip to tombstoned, as the vanish it is (02-architecture.md's
/// reload-survival rule). Drag the selection itself onto the trash and the selection empties;
/// drag something else and it is untouched. Neither case needs surgery here.
///
/// Cards only, by the gesture's own gate (`TrashDrop.accepts`) but nothing here depends on
/// that: the paths resolve on the live side exactly as `delete(_:)`'s do.
public func deleteByDrag(cardIDs: [ItemID]) {
let folders = TrashModel.paths(of: Set(cardIDs), on: .live, in: snapshot)
.map { $0.folder(under: rootURL) }
guard !folders.isEmpty else { return }
tombstone(folders)
}
/// The tombstone write itself **one `performWrite` bracket, whatever the set's size and
/// whichever gesture asked** (DRAG-REORDER.md § The drop commits; the style batch's rule).
///
/// Spelled once so and drop-on-trash cannot drift apart on disk; everything that differs
/// between them is about the *selection*, and lives in the callers.
private func tombstone(_ folders: [URL]) {
try? performWrite { () throws(BoardWriteError) -> Void in
for folder in folders {
try BoardWriter.deleteItem(at: folder)
}
}
}
/// Put Back: removes `deleted:` from every tombstoned item in `ids`, in one bracket
/// (03-board-ui.md § Trash).
///