Lanes delete into the trash — rendering, grammar, drag, clipboard, a11y
Phase 2 completes the lanes-in-trash card. TrashEntry merges the trash's two kinds by rank in exactly ONE place (ItemPath.resolve's own merge deleted in favor of it — the three-merge-points finding shrinks instead of growing). TrashLaneRowView renders the opaque row — tertiary plate, level-default lane glyph never the lane's own icon, title + card count, no accents, no expansion; the column badge counts rendered rows. Selection grammar: kind-homogeneous trash selections — ranges skip the other kind, ⇧-extension stops at the kind boundary, plain arrows walk the merged order, marquee stays card-only (now load-bearing: rows register frames for arrows), Select All card-scoped; successor-on-purge crosses kinds like navigation as the interim for open Gap 7b5cbc90. Drag: TrashDrop accepts lane sessions (drop on shown trash deletes), restoreLanes routes a trash-sourced strip drop as an arrival-ranked within-board move with an undo step. Clipboard: ⌘X/⌘V lane restore via opaque lane subjects; fixed boardRoot(ofLaneFolder:) returning .trash as the root — a same-board restore looked like an import and would have reminted the lane it was restoring (pinned by test). A11y: row = one flattened "title, deleted lane, N cards" element with Delete/Reveal actions; BoardDiff crossings read lanes as deleted/restored, shown-trash churn digested at row level. Agent guide stays v7 — the literal already teaches lanes-trash-by-move and kind stamping; drift-guard pins those lines. README trash paragraph notes lanes. Both schemes 1893 tests / 322 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -188,10 +188,19 @@ struct TrashDropTests {
|
||||
#expect(accepts(operation: .move))
|
||||
}
|
||||
|
||||
/// "Lanes are not deliverable this way (a lane drag proposes only lane slots)."
|
||||
@Test("A lane drag never proposes into the trash")
|
||||
func lanesAreNotDeliverable() {
|
||||
#expect(!accepts(kind: .lanes))
|
||||
/// "Dropping a live card — **or lane** — on the shown trash deletes it … a lane drag over the
|
||||
/// shown trash proposes the delete alongside its strip slots" (04-interactions.md ▸ The trash,
|
||||
/// lanes extended 2026-07-29, retiring "a lane drag proposes only lane slots").
|
||||
@Test("A live lane drag proposes the delete too — and no session proposes nothing")
|
||||
func lanesAreDeliverable() {
|
||||
#expect(accepts(kind: .lanes))
|
||||
// Every other clause binds the lane exactly as it binds the card: a trashed row dragged out
|
||||
// is not deletable back into the place it already is, a foreign board's lane is refused, and
|
||||
// a hidden column takes nothing.
|
||||
#expect(!accepts(kind: .lanes, container: .trash))
|
||||
#expect(!accepts(kind: .lanes, isWithinBoard: false))
|
||||
#expect(!accepts(kind: .lanes, isTrashShown: false))
|
||||
#expect(!accepts(kind: .lanes, acceptsMutations: false))
|
||||
// And no session at all is no proposal either — the column is inert between drags.
|
||||
#expect(!accepts(kind: nil))
|
||||
}
|
||||
@@ -451,8 +460,11 @@ struct DropSettleTests {
|
||||
#expect(session.hiddenMembers(onBoardRooted: store.rootURL) == [Self.card1, Self.card2])
|
||||
}
|
||||
|
||||
@Test("A lane session never proposes into the trash")
|
||||
func laneSessionsHaveNoTrashLanding() throws {
|
||||
/// The column draws the delete gesture's shadow for a **lane** session too (lanes extended
|
||||
/// 2026-07-29): the accessor asks "is this proposal mine", and the kind question belongs to
|
||||
/// `TrashDrop.accepts`, which is asked at hover and again at release.
|
||||
@Test("A lane session's trash proposal reaches the column, and only on its own board")
|
||||
func laneSessionsProposeIntoTheTrash() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
@@ -465,7 +477,10 @@ struct DropSettleTests {
|
||||
)
|
||||
session.propose(DropTarget(boardRoot: store.rootURL, container: .trash, index: 0))
|
||||
|
||||
#expect(session.trashProposal(onBoardRooted: store.rootURL) == nil)
|
||||
#expect(session.trashProposal(onBoardRooted: store.rootURL) == 0)
|
||||
#expect(session.shadowCount == 1)
|
||||
// Another board's column draws nothing, like every other proposal accessor.
|
||||
#expect(session.trashProposal(onBoardRooted: URL(filePath: "/tmp/other-board")) == nil)
|
||||
}
|
||||
|
||||
// MARK: The hand-off
|
||||
|
||||
Reference in New Issue
Block a user