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:
@@ -19,6 +19,9 @@ private let card1 = Ident.card1
|
||||
private let card2 = Ident.card2
|
||||
private let card3 = Ident.card3
|
||||
private let card4 = Ident.card4
|
||||
/// A fourth lane identity, used only by the trash's own rows — a row must never share an id with a
|
||||
/// live lane, which on a real board it cannot (board-wide uniqueness spans both containers).
|
||||
private let trashedLaneID = Ident.lane4
|
||||
|
||||
/// Three lanes; two cards in the first, one in the second, none in the third.
|
||||
private func makeBoard() throws -> WriterFixture {
|
||||
@@ -445,4 +448,119 @@ struct ShownTrashDiffTests {
|
||||
#expect(diff.lanes.deleted == [ItemID(rawValue: lane1)])
|
||||
#expect(diff.cards.deleted.isEmpty, "the two cards left with their lane — that is the lane's event")
|
||||
}
|
||||
|
||||
// MARK: - The column's other kind
|
||||
|
||||
/// The crossing rule at the lane level (lanes rejoined the trash 2026-07-29): a lane moved into
|
||||
/// `.trash/` is a **delete**, and it reads the same shown or hidden, because the lane left
|
||||
/// `lanes` either way — which is exactly the "1 lane deleted" event the user watched happen.
|
||||
@Test("A lane moved into the trash is deleted, shown or hidden, and never a move")
|
||||
func laneIntoTheTrashIsADeletion() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let before = try fixture.snapshot()
|
||||
|
||||
try fixture.moveFolder(lane1, to: ".trash/\(lane1)")
|
||||
let after = try fixture.snapshot()
|
||||
|
||||
for shown in [true, false] {
|
||||
let diff = BoardDiff.between(before, after, includingTrash: shown)
|
||||
#expect(diff.lanes.deleted == [ItemID(rawValue: lane1)])
|
||||
#expect(diff.lanes.moved.isEmpty)
|
||||
#expect(diff.cards.deleted.isEmpty, "its cards went with it — that is the lane's event")
|
||||
}
|
||||
}
|
||||
|
||||
/// And back out again: a restored lane is an **arrival**, not a move.
|
||||
@Test("A lane restored out of the trash is an addition")
|
||||
func laneOutOfTheTrashIsAnArrival() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.moveFolder(lane1, to: ".trash/\(lane1)")
|
||||
let before = try fixture.snapshot()
|
||||
|
||||
try fixture.moveFolder(".trash/\(lane1)", to: lane1)
|
||||
let after = try fixture.snapshot()
|
||||
|
||||
for shown in [true, false] {
|
||||
let diff = BoardDiff.between(before, after, includingTrash: shown)
|
||||
#expect(diff.lanes.added == [ItemID(rawValue: lane1)])
|
||||
#expect(diff.lanes.moved.isEmpty)
|
||||
#expect(diff.cards.added.isEmpty, "its cards arrived with it")
|
||||
}
|
||||
}
|
||||
|
||||
/// The news the ruling actually adds: churn that never leaves the container. A purged **row** is
|
||||
/// counted while the column is shown and silent while it is hidden, exactly as a purged card is.
|
||||
@Test("A purged lane row is a lane deletion while the trash is shown, and silence while hidden")
|
||||
func purgedLaneRowCountsWhileShown() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.trashedLane(trashedLaneID, order: "1024", title: "Done")
|
||||
let before = try fixture.snapshot()
|
||||
|
||||
try FileManager.default.removeItem(at: fixture.url(".trash/\(trashedLaneID)"))
|
||||
let after = try fixture.snapshot()
|
||||
|
||||
#expect(BoardDiff.between(before, after, includingTrash: true).lanes.deleted == [ItemID(rawValue: trashedLaneID)])
|
||||
#expect(BoardDiff.between(before, after).isSilent)
|
||||
}
|
||||
|
||||
/// "… 41 cards and 2 lanes containing 9 more cards" is the confirmation's sentence; this is the
|
||||
/// digest's: an Empty Trash over a mixed container must not understate what went.
|
||||
@Test("Empty Trash counts the lane rows alongside the cards")
|
||||
func emptyTrashCountsBothKinds() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.trashCard(card4, order: "1024", title: "Old")
|
||||
try fixture.trashedLane(trashedLaneID, order: "512", title: "Done")
|
||||
let before = try fixture.snapshot()
|
||||
|
||||
try FileManager.default.removeItem(at: fixture.url(".trash/\(card4)"))
|
||||
try FileManager.default.removeItem(at: fixture.url(".trash/\(trashedLaneID)"))
|
||||
let diff = BoardDiff.between(before, try fixture.snapshot(), includingTrash: true)
|
||||
|
||||
#expect(diff.cards.deleted == [ItemID(rawValue: card4)])
|
||||
#expect(diff.lanes.deleted == [ItemID(rawValue: trashedLaneID)])
|
||||
#expect(AccessibilityPhrases.boardChanged(diff) == "Board changed: 1 card deleted, 1 lane deleted")
|
||||
}
|
||||
|
||||
/// The row's *rendered* content is its title, and only that: it takes no styling accents, so a
|
||||
/// colour an agent wrote onto a trashed folder changes nothing anyone can see.
|
||||
@Test("A retitled row is an edit while shown; a restyled one is not an edit at all")
|
||||
func rowContentIsItsTitle() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.trashedLane(trashedLaneID, order: "1024", title: "Done")
|
||||
let before = try fixture.snapshot()
|
||||
|
||||
try fixture.item(".trash/\(trashedLaneID)", "---\nschema: 1\ntitle: Shipped\norder: 1024\nkind: lane\n---\n\n")
|
||||
#expect(BoardDiff.between(before, try fixture.snapshot(), includingTrash: true).lanes.edited
|
||||
== [ItemID(rawValue: trashedLaneID)])
|
||||
|
||||
let retitled = try fixture.snapshot()
|
||||
try fixture.item(
|
||||
".trash/\(trashedLaneID)",
|
||||
"---\nschema: 1\ntitle: Shipped\norder: 1024\nkind: lane\nbackground: blue\n---\n\n"
|
||||
)
|
||||
let styled = BoardDiff.between(retitled, try fixture.snapshot(), includingTrash: true)
|
||||
#expect(styled.lanes.isEmpty, "no accent is rendered, so nothing visible changed")
|
||||
#expect(styled.boardChanged, "the backstop still catches it — the bytes did change")
|
||||
}
|
||||
|
||||
/// A row that moved rank in the column is a move while shown, like a trash card reordered in
|
||||
/// place — the position axis is the same axis whatever the kind.
|
||||
@Test("A reordered lane row is a move while the trash is shown")
|
||||
func reorderedRowIsAMove() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.trashedLane(trashedLaneID, order: "1024", title: "Done")
|
||||
let before = try fixture.snapshot()
|
||||
|
||||
try fixture.trashedLane(trashedLaneID, order: "256", title: "Done")
|
||||
let after = try fixture.snapshot()
|
||||
|
||||
#expect(BoardDiff.between(before, after, includingTrash: true).lanes.moved == [ItemID(rawValue: trashedLaneID)])
|
||||
#expect(BoardDiff.between(before, after).isSilent)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user