Materialize the trash — store, undo, and the container universe
Phase 2 swaps every consumer: Liveness and its ancestor walk are gone, replaced by ItemContainer — a UUID set plus the container side it lives on, presence the whole test, one selection boundary instead of the old liveness law. Deletion stages by place: board cards move to the trash at a store-minted head rank, trash-side delete is permanent behind its confirmation, Delete Immediately skips the trash from anywhere, lane delete captures the subtree and removes the folder. Restore has no method at all — moveCards resolves members in either container, so drag-out and cut-paste are the ordinary moves 13 calls them, registering ordinary Move steps. The delete inverse moves the card back to its captured lane and rank; redo replays the captured trash rank, a value the gesture actually wrote; lane undo recreates the subtree byte-faithfully in session. Purges register nothing — where 13's trash section contradicts its own Rules on that, Rules wins, filed for ruling. Staleness collapsed to present-or-absent: a container is a path, so a foreign restore fails the delete step's expectation structurally. Legacy tombstones migrate on the loose-file tail hook, cards oldest-first so minting above top reproduces the retired newest-first column, lanes returning live, one folded loss row naming both directions. Put Back, restoreByDrag, receiveRestoredCards, TrashEntry, and the kind machinery are deleted; the trash column renders the container correctly with its full face rework left to phase 3. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -141,14 +141,14 @@ struct OpenCardCommand: View {
|
||||
return store.isEditingInline || soleSelectedCard != nil
|
||||
}
|
||||
|
||||
/// The sole selected **live card**, or `nil`. A lane, a multi-selection and a tombstoned
|
||||
/// selection all answer `nil` — "everything edit-shaped is disabled on tombstoned selections"
|
||||
/// (04 ▸ The trash), and a card window is tied to one card.
|
||||
/// The sole selected **board card**, or `nil`. A lane, a multi-selection and a trash
|
||||
/// selection all answer `nil` — "everything edit-shaped is disabled on trash selections … Open
|
||||
/// Card, Rename, Style…" (04 ▸ The trash), and a card window is tied to one card.
|
||||
private var soleSelectedCard: ItemID? {
|
||||
guard let store else { return nil }
|
||||
let selection = store.selection
|
||||
guard selection.liveness == .live, selection.ids.count == 1, let id = selection.ids.first,
|
||||
BoardStore.liveItem(id, in: store.snapshot)?.cardID != nil
|
||||
guard selection.container == .board, selection.ids.count == 1, let id = selection.ids.first,
|
||||
BoardStore.boardItem(id, in: store.snapshot)?.cardID != nil
|
||||
else { return nil }
|
||||
return id
|
||||
}
|
||||
@@ -161,8 +161,8 @@ struct OpenCardCommand: View {
|
||||
// holds it — and re-checked after, because one of those paths is *the lane vanished*.
|
||||
let lane = placeholder.laneID
|
||||
let created = store.commitPlaceholder()
|
||||
if store.snapshot.lanes.contains(where: { $0.id == lane && !$0.isDeleted }) {
|
||||
store.select([lane], liveness: .live)
|
||||
if store.snapshot.lanes.contains(where: { $0.id == lane }) {
|
||||
store.select([lane], in: .board)
|
||||
}
|
||||
if let created { open(created) }
|
||||
return
|
||||
@@ -170,7 +170,7 @@ struct OpenCardCommand: View {
|
||||
|
||||
if let editor = store.transient.renameEditor {
|
||||
let target = editor.targetID
|
||||
let isCard = BoardStore.liveItem(target, in: store.snapshot)?.cardID != nil
|
||||
let isCard = BoardStore.boardItem(target, in: store.snapshot)?.cardID != nil
|
||||
store.commitRename()
|
||||
if isCard { open(target) }
|
||||
return
|
||||
@@ -269,8 +269,8 @@ struct MoveLaneCommands: View {
|
||||
private func destination(_ delta: Int) -> (lane: ItemID, index: Int)? {
|
||||
guard let store, store.acceptsBoardMutations else { return nil }
|
||||
let selection = store.selection
|
||||
guard selection.liveness == .live, selection.ids.count == 1, let id = selection.ids.first else { return nil }
|
||||
let lanes = SelectionGrammar.liveLanes(in: store.snapshot)
|
||||
guard selection.container == .board, selection.ids.count == 1, let id = selection.ids.first else { return nil }
|
||||
let lanes = SelectionGrammar.lanes(in: store.snapshot)
|
||||
// A card id is in no lane order, so this is also the "not a lane" test.
|
||||
guard let from = lanes.firstIndex(of: id) else { return nil }
|
||||
let to = from + delta
|
||||
@@ -382,9 +382,9 @@ struct BoardInfoCommand: View {
|
||||
/// **lane's only rename path**: Return on a lane creates a card, so without this item a lane could
|
||||
/// never be renamed at all (04-interactions.md ▸ Selection).
|
||||
///
|
||||
/// Validation is the sole-selected-live-item rule — card or lane, either kind, exactly one. A
|
||||
/// tombstoned selection never enables it: "everything edit-shaped is disabled on tombstoned
|
||||
/// selections" (04 ▸ The trash), which `ItemReferenceSet`'s liveness side answers directly.
|
||||
/// Validation is the sole-selected-board-item rule — card or lane, either kind, exactly one. A
|
||||
/// trash selection never enables it: "everything edit-shaped is disabled on trash selections"
|
||||
/// (04 ▸ The trash), which `ItemReferenceSet`'s container answers directly.
|
||||
struct BoardRenameCommand: View {
|
||||
|
||||
@FocusedValue(\.boardStore) private var store
|
||||
@@ -400,8 +400,8 @@ struct BoardRenameCommand: View {
|
||||
private var renameTarget: (id: ItemID, title: String?)? {
|
||||
guard let store, store.acceptsBoardMutations else { return nil }
|
||||
let selection = store.selection
|
||||
guard selection.liveness == .live, selection.ids.count == 1, let id = selection.ids.first,
|
||||
let item = BoardStore.liveItem(id, in: store.snapshot)
|
||||
guard selection.container == .board, selection.ids.count == 1, let id = selection.ids.first,
|
||||
let item = BoardStore.boardItem(id, in: store.snapshot)
|
||||
else { return nil }
|
||||
return (id: id, title: item.title)
|
||||
}
|
||||
@@ -441,7 +441,7 @@ struct BoardStyleCommand: View {
|
||||
guard let store, store.acceptsBoardMutations else { return nil }
|
||||
let selection = store.selection
|
||||
guard !selection.isEmpty else { return .board }
|
||||
guard selection.liveness == .live else { return nil }
|
||||
guard selection.container == .board else { return nil }
|
||||
// Re-resolved against the snapshot on the way in, so the session starts out holding only
|
||||
// items that render — the same universe its own reload rule will hold it to.
|
||||
let live = selection.resolved(against: store.snapshot).ids
|
||||
@@ -498,8 +498,8 @@ struct LaneWidthCommands: View {
|
||||
private var selectedLanes: [Lane] {
|
||||
guard let store, store.acceptsBoardMutations else { return [] }
|
||||
let selection = store.selection
|
||||
guard selection.liveness == .live, !selection.isEmpty else { return [] }
|
||||
return store.snapshot.lanes.filter { selection.ids.contains($0.id) && !$0.isDeleted }
|
||||
guard selection.container == .board, !selection.isEmpty else { return [] }
|
||||
return store.snapshot.lanes.filter { selection.ids.contains($0.id) }
|
||||
}
|
||||
|
||||
/// `width` is ≥ 1 (03-board-ui.md § Lane), and an item whose only outcome is a no-op reads
|
||||
|
||||
Reference in New Issue
Block a user