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:
@@ -73,6 +73,78 @@ extension ItemContainer {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The trash's two kinds, in one order
|
||||
|
||||
/// One row of the trash column — a card, or a trashed lane's opaque unit (03-board-ui.md § Trash,
|
||||
/// re-ruled 2026-07-29: "Lane rows and cards interleave in the one trash column purely by trash
|
||||
/// rank").
|
||||
///
|
||||
/// **It exists so the interleave is written once.** The container's two kinds are two arrays on the
|
||||
/// snapshot, for the reason `BoardModel.trash` states — a trashed card is an ordinary card every
|
||||
/// card-shaped surface already reads, a trashed lane is an opaque row none of them may — but *rank
|
||||
/// order* is a question about the container as a whole, and it is asked by the column that draws the
|
||||
/// rows, by the grammar that ranges and navigates over them, and by the path resolver that batches
|
||||
/// them. Three merges would be three chances to disagree about what "the row below this one" is.
|
||||
///
|
||||
/// It carries the values rather than only the ids because the column needs them to draw; every other
|
||||
/// consumer reads the two derived facts, `id` and `kind`.
|
||||
public enum TrashEntry: Identifiable, Sendable, Equatable {
|
||||
|
||||
case card(Card)
|
||||
case lane(TrashedLane)
|
||||
|
||||
public var id: ItemID {
|
||||
switch self {
|
||||
case let .card(card): card.id
|
||||
case let .lane(lane): lane.id
|
||||
}
|
||||
}
|
||||
|
||||
/// The rank that decides where this row sits among the others — the one field both kinds carry
|
||||
/// for the same purpose.
|
||||
public var order: Double {
|
||||
switch self {
|
||||
case let .card(card): card.order
|
||||
case let .lane(lane): lane.order
|
||||
}
|
||||
}
|
||||
|
||||
/// Which of the board's two selectable levels this row is — the kind axis, which reaches into
|
||||
/// the trash exactly as it governs the live board (04-interactions.md ▸ The trash: "a trash
|
||||
/// selection is either cards or lane rows, kind-homogeneous like the live board's own grammar").
|
||||
public var kind: SelectionKind {
|
||||
switch self {
|
||||
case .card: .card
|
||||
case .lane: .lane
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder this row names.
|
||||
public var path: ItemPath {
|
||||
switch self {
|
||||
case let .card(card): .trashCard(card.id)
|
||||
case let .lane(lane): .trashLane(lane.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BoardModel {
|
||||
|
||||
/// The trash's rows, top to bottom — **the container's one order**, both kinds interleaved by
|
||||
/// rank (03-board-ui.md § Trash).
|
||||
///
|
||||
/// The tie-break is the folder name's, `Ranks.sortedForDisplay`'s own, which is what the loader
|
||||
/// already applied within each kind: two rows minted the same rank by two writers order the same
|
||||
/// way twice.
|
||||
public var trashEntries: [TrashEntry] {
|
||||
Ranks.sortedForDisplay(
|
||||
trash.map(TrashEntry.card) + trashedLanes.map(TrashEntry.lane),
|
||||
order: \.order,
|
||||
name: { $0.id.rawValue }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ItemPath
|
||||
|
||||
/// Where an item's folder sits under a board root, as identity components rather than as a URL.
|
||||
@@ -159,9 +231,9 @@ extension ItemPath {
|
||||
/// for the same reason).
|
||||
///
|
||||
/// **The trash's order interleaves its two kinds by rank** (03-board-ui.md § Trash: "lane rows
|
||||
/// and cards interleave in the one trash column purely by trash rank"), which is why the two
|
||||
/// arrays are merged here rather than concatenated: the column's order is the batch's order, and
|
||||
/// a selection is kind-homogeneous anyway, so the merge costs nothing the one time it matters.
|
||||
/// and cards interleave in the one trash column purely by trash rank"), which is why the walk is
|
||||
/// `trashEntries` rather than the two arrays concatenated: the column's order is the batch's
|
||||
/// order, and it is stated in exactly one place.
|
||||
///
|
||||
/// Ids the container does not hold are simply absent, which is every caller's standing posture:
|
||||
/// a selection the next reload will drop writes nothing rather than being refused.
|
||||
@@ -181,10 +253,7 @@ extension ItemPath {
|
||||
}
|
||||
}
|
||||
case .trash:
|
||||
let entries = snapshot.trash.map { (order: $0.order, path: ItemPath.trashCard($0.id)) }
|
||||
+ snapshot.trashedLanes.map { (order: $0.order, path: ItemPath.trashLane($0.id)) }
|
||||
for entry in Ranks.sortedForDisplay(entries, order: \.order, name: { $0.path.id.rawValue })
|
||||
where ids.contains(entry.path.id) {
|
||||
for entry in snapshot.trashEntries where ids.contains(entry.id) {
|
||||
result.append(entry.path)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user