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:
@@ -25,8 +25,9 @@ struct DropTarget: Equatable, Sendable {
|
||||
/// That lane's **masonry**: the index is a position in its logical card order
|
||||
/// (DRAG-REORDER.md § The card masonry).
|
||||
case lane(ItemID)
|
||||
/// The **trash column**, which a board card drag proposes into to delete it
|
||||
/// (04-interactions.md ▸ The trash, settled 2026-07-28). The index is always the topmost row.
|
||||
/// The **trash column**, which a board card — or lane — drag proposes into to delete it
|
||||
/// (04-interactions.md ▸ The trash, settled 2026-07-28, lanes extended 2026-07-29). The index
|
||||
/// is always the topmost row.
|
||||
case trash
|
||||
}
|
||||
|
||||
@@ -47,9 +48,10 @@ struct DropTarget: Equatable, Sendable {
|
||||
|
||||
// MARK: - Dropping on the trash
|
||||
|
||||
/// **Drop-on-trash deletes** (04-interactions.md ▸ The trash, settled 2026-07-28: "the drag becomes
|
||||
/// the pointer's delete gesture — release moves the dragged card(s) into `.trash/`, exactly the ⌫ delete"),
|
||||
/// as the two pure facts the gesture is made of (`TrashDropTests`).
|
||||
/// **Drop-on-trash deletes** (04-interactions.md ▸ The trash, settled 2026-07-28, lanes extended
|
||||
/// 2026-07-29: "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"), as the two pure facts the gesture is
|
||||
/// made of (`TrashDropTests`).
|
||||
///
|
||||
/// Kept out of the drop context so the ruling is checkable without a window, and stated once so the
|
||||
/// **hover** and the **release** cannot disagree about what the trash takes — the hazard being a
|
||||
@@ -69,16 +71,20 @@ enum TrashDrop {
|
||||
/// Whether the shown trash takes this session — the whole of the gate, and every clause is a
|
||||
/// refusal 04 states in its own words:
|
||||
///
|
||||
/// - **Lanes are not deliverable this way** — "a lane drag proposes only lane slots". (The strip's
|
||||
/// slot list has never contained the trash column, so this is belt over braces; it is written down
|
||||
/// because a guard that is only true by construction is one refactor from being false.)
|
||||
/// - **A trash card is already there.** A `.trash` session's vocabulary is restore and copy-out;
|
||||
/// dropping it back where it came from writes nothing.
|
||||
/// - **Cross-board is refused.** "No move or paste ever targets the trash": a foreign card
|
||||
/// - **Both kinds are deliverable** (lanes extended 2026-07-29, retiring "a lane drag proposes
|
||||
/// only lane slots"): "a lane drag over the shown trash proposes the delete alongside its strip
|
||||
/// slots", which is the same sentence the card gesture has always had one level down. A session
|
||||
/// in flight is a session of one of the two kinds, so the clause is `kind != nil` — there is no
|
||||
/// third kind to admit by accident, and a file session never arms this object at all.
|
||||
/// - **A trash item is already there.** A `.trash` session's vocabulary is restore and copy-out,
|
||||
/// at either level; dropping it back where it came from writes nothing.
|
||||
/// - **Cross-board is refused.** "No move or paste ever targets the trash": a foreign item
|
||||
/// delivered *into* this board's trash would be a transfer-and-delete compound, an operation the
|
||||
/// design gives no name and no undo story. The card stays where it is.
|
||||
/// design gives no name and no undo story. It stays where it is.
|
||||
/// - **⌥ is refused.** Copying into the trash is not a thing — the copy grammar promises the
|
||||
/// original stays exactly where it was, and there is nothing to delete but the original.
|
||||
/// original stays exactly where it was, and there is nothing to delete but the original. (A
|
||||
/// within-board lane drag never resolves to `.copy` anyway: ⌥ is ignored there,
|
||||
/// `DragLocality.operation`. The clause is the card gesture's and covers the lane for free.)
|
||||
/// - **Hidden, the trash is invisible to every gesture.** True by construction too (the column is
|
||||
/// not rendered, so it has no drop region), and stated here so the claim is testable.
|
||||
/// - **The mutating-gesture rule**, like every other write the pointer can start.
|
||||
@@ -91,7 +97,7 @@ enum TrashDrop {
|
||||
acceptsMutations: Bool
|
||||
) -> Bool {
|
||||
guard isTrashShown, acceptsMutations else { return false }
|
||||
guard kind == .cards, container == .board, isWithinBoard else { return false }
|
||||
guard kind != nil, container == .board, isWithinBoard else { return false }
|
||||
return operation == .move
|
||||
}
|
||||
}
|
||||
@@ -400,8 +406,12 @@ final class DragSession {
|
||||
/// Always `TrashDrop.landingIndex`, and read through this accessor anyway so the column asks the
|
||||
/// same "is it me?" question every other container asks, and gets the position from the same
|
||||
/// place.
|
||||
///
|
||||
/// **Either kind proposes here** (lanes extended 2026-07-29), so there is no kind clause: what
|
||||
/// makes the proposal legal is `TrashDrop.accepts`, asked at hover and again at release, and a
|
||||
/// second copy of its answer written here could only ever disagree with it.
|
||||
func trashProposal(onBoardRooted root: URL) -> Int? {
|
||||
guard kind == .cards, let proposal, proposal.container == .trash,
|
||||
guard isActive, let proposal, proposal.container == .trash,
|
||||
DragLocality.isSameBoard(proposal.boardRoot, root)
|
||||
else { return nil }
|
||||
return proposal.index
|
||||
|
||||
Reference in New Issue
Block a user