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:
@@ -157,6 +157,31 @@ struct NavigationMathTests {
|
||||
NavigationMath.nearest(from: originFrame, direction: .down, among: all, where: { $0.container == .board }) == card3
|
||||
)
|
||||
}
|
||||
|
||||
/// **Inside the trash, plain arrows walk every row and extension stops at the kind boundary**
|
||||
/// (04-interactions.md ▸ The trash: "plain arrows walk across, extension stops"), which is the two
|
||||
/// halves of the arrow handler expressed over one registry of drawn frames — a lane row registers
|
||||
/// like a card face precisely so ↓ can reach it.
|
||||
@Test("A trash lane row is a plain arrow's neighbour, and an extension's dead end")
|
||||
func trashLaneRowsAreNavigableButNotExtendable() {
|
||||
let origin = target(card1, x: 0, y: 0, container: .trash)
|
||||
let row = target(lane2, x: 0, y: 120, kind: .lane, container: .trash)
|
||||
let below = target(card2, x: 0, y: 240, container: .trash)
|
||||
let all = [origin, row, below]
|
||||
|
||||
// Plain: the next row down, whatever its kind.
|
||||
#expect(NavigationMath.nearest(from: origin.frame, direction: .down, among: all) == lane2)
|
||||
|
||||
// ⇧: the handler takes the *same* unrestricted neighbour and then tests it, so a crossing
|
||||
// row makes the press inert rather than being stepped over in search of a legal one — the
|
||||
// rule exists so a held range is never silently widened past what the user asked for.
|
||||
let next = NavigationMath.nearest(from: origin.frame, direction: .down, among: all)
|
||||
#expect(next == lane2)
|
||||
#expect(row.kind != origin.kind, "so the extension stops here")
|
||||
|
||||
// From the row itself, ↓ reaches the card below it: navigation crosses back.
|
||||
#expect(NavigationMath.nearest(from: row.frame, direction: .down, among: all) == card2)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SortMath
|
||||
|
||||
Reference in New Issue
Block a user