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:
@@ -53,6 +53,10 @@ let clipboardCard2 = ItemID(rawValue: Ident.card2)
|
||||
let clipboardCard3 = ItemID(rawValue: Ident.card3)
|
||||
let clipboardCard4 = ItemID(rawValue: Ident.card4)
|
||||
|
||||
/// The trash's **lane row** in the harness below — the opaque unit ⌘X restores (03-board-ui.md §
|
||||
/// Trash, lanes rejoined 2026-07-29).
|
||||
let clipboardTrashedLane = ItemID(rawValue: Ident.lane3)
|
||||
|
||||
/// An ordinary card body, for the trash's resident.
|
||||
func trashResidentItem(order: String, title: String) -> String {
|
||||
"""
|
||||
@@ -126,6 +130,21 @@ func makeClipboardHarness() throws -> ClipboardHarness {
|
||||
try ClipboardHarness(fixture: try makeClipboardBoard())
|
||||
}
|
||||
|
||||
/// The same board with a **lane row in its trash**, carrying one card — the clipboard's other trash
|
||||
/// subject (04-interactions.md ▸ The trash: "⌘X works … a trashed lane pastes after the anchor
|
||||
/// lane"). Its own fixture rather than a line in `makeClipboardBoard`, so every suite that counts the
|
||||
/// trash's cards keeps counting exactly what it did.
|
||||
@MainActor
|
||||
func makeTrashedLaneHarness() throws -> ClipboardHarness {
|
||||
let fixture = try makeClipboardBoard()
|
||||
try fixture.item(
|
||||
".trash/\(Ident.lane3)",
|
||||
"---\nschema: 1\ntitle: Done\norder: 512\nkind: lane\nproject: lanework\n---\nDone body.\n"
|
||||
)
|
||||
try fixture.item("\(".trash/\(Ident.lane3)")/\(Ident.indexless)", Item.rich(order: "1024", title: "Freight"))
|
||||
return try ClipboardHarness(fixture: fixture)
|
||||
}
|
||||
|
||||
// MARK: - The manifest
|
||||
|
||||
@Suite("ClipboardManifest")
|
||||
@@ -627,6 +646,31 @@ struct ClipboardAvailabilityTests {
|
||||
#expect(harness.clipboard.canCut(from: harness.store))
|
||||
}
|
||||
|
||||
/// The row is a lane on the clipboard's own axis: "the payload kinds never mix because the
|
||||
/// selection never does" (04-interactions.md ▸ The trash), so a cut row writes a **lane** payload
|
||||
/// recorded in the **trash** container.
|
||||
@Test("A trashed lane row copies and cuts, and its payload is a lane in the trash")
|
||||
func trashedLaneRowTakesCopyAndCut() throws {
|
||||
let harness = try makeTrashedLaneHarness()
|
||||
defer { harness.tearDown() }
|
||||
|
||||
harness.store.transient.isTrashVisible = true
|
||||
harness.store.select([clipboardTrashedLane], in: .trash)
|
||||
#expect(harness.clipboard.canCopy(from: harness.store))
|
||||
#expect(harness.clipboard.canCut(from: harness.store))
|
||||
|
||||
harness.clipboard.cut(from: harness.store)
|
||||
let manifest = try #require(harness.clipboard.payload)
|
||||
#expect(manifest.kind == .lane)
|
||||
#expect(manifest.container == .trash)
|
||||
#expect(manifest.entries.map(\.title) == ["Done"])
|
||||
// The opaque unit describes itself and not its subtree: the *content* travels through the
|
||||
// staged folder, which is copied whole.
|
||||
#expect(manifest.entries.first?.cards.isEmpty == true)
|
||||
#expect(harness.store.transient.pendingCut
|
||||
== ItemReferenceSet(ids: [clipboardTrashedLane], container: .trash))
|
||||
}
|
||||
|
||||
@Test("The read-only lock blocks cut but never copy")
|
||||
func lockBlocksCutOnly() throws {
|
||||
let harness = try makeClipboardHarness()
|
||||
|
||||
Reference in New Issue
Block a user