Materialize the trash — faces, menus, and grammar
Phase 3 finishes the pivot at the surface. One card face serves two containers: CardFaceView extracted with a role — board or trash — so stripe, tint, chip, selection stroke, cut dim, marquee registration, and drag are shared by construction, the trash side differing only in its absences: no Open, no rename, no Style, no file-hover highlight, and a Delete that goes through the confirmation host. The column rewrote around the lanes' own single-column masonry so drag reflow reads as positional slides; chrome stays the hatched header, symbol, and count — 11 gives Empty Trash to the File menu alone. Two real grammar bugs die here: plain Backspace on a trash selection purged without the confirmation the menu raises, and the context menu's Delete resolved against the standing selection, so right-clicking a trash card under a board selection silently did nothing — it now stages the clicked set explicitly. Open, Rename, Style, and Empty Trash validation became testable store seams; the column is one named accessibility container of ordinary card elements. The tombstone era is swept: deleteItem, restoreItem, stripTombstonedChildren — dead since lane copies stopped nesting trash — the restore verb, the unreachable put-back banner row, and every quasi-lane doc comment. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -19,14 +19,14 @@ import SwiftUI
|
||||
///
|
||||
/// - **Lane resize** — the right-edge grab strip (above). Deliberately *not* a drag session
|
||||
/// (DRAG-REORDER.md § Adjacent interaction).
|
||||
/// - **Drag & drop** — cards, lanes and trash rows travel as **system drag sessions**, which is what
|
||||
/// - **Drag & drop** — cards (in either container) and lanes travel as **system drag sessions**, which is what
|
||||
/// crosses window boundaries, draws the copy badge and gives the full-size replica
|
||||
/// (`DragSession`, `BoardDrops.swift`, DRAG-REORDER.md). The strip owns the drop geometry
|
||||
/// registry and the strip-level drop target; the lanes own theirs.
|
||||
/// - **The rubber band** — a drag from any empty surface sweeps a selection (`MarqueeSession`,
|
||||
/// `MarqueeMath`); the strip owns the session and the target registry, and hands both down.
|
||||
/// - **The board's fixed grammar keys** (11-command-nexus.md ▸ Fixed grammar keys) — the four
|
||||
/// arrows and their ⇧/⌥ modes, Return's create/rename dispatch, ⌫'s tombstone, Escape's step
|
||||
/// arrows and their ⇧/⌥ modes, Return's create/rename dispatch, ⌫'s staged delete, Escape's step
|
||||
/// outward, and Select All. The chorded commands are the menu's (`BoardCommands`); everything
|
||||
/// here is a plain key or a grammar modifier, which is exactly the split 04-interactions.md ▸
|
||||
/// Configurable bindings draws between what remaps and what does not.
|
||||
@@ -35,7 +35,7 @@ import SwiftUI
|
||||
/// carries those titles, and titles are the remapping mechanism's key, so a second row sharing one
|
||||
/// is ruled out (04-interactions.md ▸ Configurable bindings).
|
||||
///
|
||||
/// - **The trash quasi-lane** — trailing, one fixed unit, joining and leaving the width division as
|
||||
/// - **The trash column** — trailing, one fixed unit, joining and leaving the width division as
|
||||
/// View ▸ Show Trash toggles it (`TrashLaneView`, 03-board-ui.md § Trash).
|
||||
///
|
||||
/// ### The live search filter
|
||||
@@ -91,7 +91,7 @@ struct BoardView: View {
|
||||
@State private var marquee = MarqueeSession()
|
||||
|
||||
/// Where every sweepable item is drawn, in strip coordinates. Owned here because the band is —
|
||||
/// the cards and trash rows only *register* into it (`MarqueeTargetRegistry`).
|
||||
/// the card faces on either side only *register* into it (`MarqueeTargetRegistry`).
|
||||
@State private var marqueeTargets = MarqueeTargetRegistry()
|
||||
|
||||
/// The name of the strip's coordinate space, which is what a drop out of the trash is resolved
|
||||
@@ -252,7 +252,7 @@ struct BoardView: View {
|
||||
case let .lane(lane):
|
||||
laneSlot(lane, standard: standard)
|
||||
// "Appear/disappear is scale + fade … lanes ~0.9" (03-board-ui.md § Motion).
|
||||
// A create, a delete and a Put Back all reach the strip as a lane arriving in
|
||||
// A create, a delete and an undo all reach the strip as a lane arriving in
|
||||
// or leaving this `ForEach`; whether that *performs* is decided upstream, at
|
||||
// the reload that carried it (`Motion.reloadAnimates`) — a transition with no
|
||||
// animated transaction around it is simply an appearance.
|
||||
@@ -267,7 +267,7 @@ struct BoardView: View {
|
||||
}
|
||||
}
|
||||
if isTrashVisible {
|
||||
// Trailing, always — the quasi-lane has no position of its own to lose, which is
|
||||
// Trailing, always — the column has no position of its own to lose, which is
|
||||
// also why it never appears in the drop proposal's inputs (those are built from
|
||||
// `boardLanes`) and why the terminal slot clamps in front of it.
|
||||
TrashLaneView(
|
||||
@@ -436,7 +436,7 @@ struct BoardView: View {
|
||||
|
||||
// MARK: - Trash
|
||||
|
||||
/// Whether the trash quasi-lane is on screen — transient, board-scoped, hidden on every open
|
||||
/// Whether the trash column is on screen — transient, board-scoped, hidden on every open
|
||||
/// (03-board-ui.md § Trash ▸ Visibility). Read in two places (the unit total and the slot), so it
|
||||
/// gets a name rather than being spelled twice.
|
||||
private var isTrashVisible: Bool {
|
||||
@@ -609,23 +609,26 @@ struct BoardView: View {
|
||||
return .handled
|
||||
}
|
||||
|
||||
/// **Plain ⌫ tombstones the live selection** — "a plain-key synonym of File ▸ Delete, kept
|
||||
/// grammar so no second 'Delete' title exists" (11-command-nexus.md; 04-interactions.md ▸ The
|
||||
/// map).
|
||||
/// **Plain ⌫ deletes the selection** — "a plain-key synonym of File ▸ Delete, kept grammar so no
|
||||
/// second 'Delete' title exists" (11-command-nexus.md; 04-interactions.md ▸ The map).
|
||||
///
|
||||
/// **Both stagings**, unlike the tombstone era's live-only reading: "Plain ⌫ performs the same
|
||||
/// delete as fixed grammar" (04-interactions.md ▸ The map, resettled 2026-07-28), and the delete
|
||||
/// is staged by place inside the store (`BoardStore.deleteSelection`) rather than by two menu
|
||||
/// items sharing a chord. Put Back — the reason the bare key had to stay off the trash — is
|
||||
/// retired with the tombstone model.
|
||||
/// delete as fixed grammar" (04-interactions.md ▸ The map, resettled 2026-07-28) — a board
|
||||
/// selection moves into `.trash/`, a trash selection deletes permanently.
|
||||
///
|
||||
/// **Which means the same confirmation, too.** It goes through `TrashConfirmations.requestDelete`
|
||||
/// rather than straight to `BoardStore.deleteSelection`, because "the same chord deletes
|
||||
/// permanently … confirmation per 03's recoverability rule" and a bare key that skipped the alert
|
||||
/// the menu item raises would be the one path in the app where one keystroke destroys a card
|
||||
/// silently. The staging itself is still the store's — the alert is the only thing this adds.
|
||||
///
|
||||
/// Inert while an inline editor is open, like every grammar key: the field owns ⌫ as backspace,
|
||||
/// and a stray one reaching the board mid-edit would delete the item being renamed.
|
||||
private func handleDelete(_ press: KeyPress) -> KeyPress.Result {
|
||||
// **Plain ⌫, spelled out.** The modified chords belong to the menu — ⌘⌫ (Delete / Put Back),
|
||||
// ⌥⌘⌫ (Delete Immediately), ⇧⌘⌫ (Empty Trash…) — and AppKit routes a key equivalent to the
|
||||
// **Plain ⌫, spelled out.** The modified chords belong to the menu — ⌘⌫ (Delete), ⌥⌘⌫
|
||||
// (Delete Immediately), ⇧⌘⌫ (Empty Trash…) — and AppKit routes a key equivalent to the
|
||||
// menu before the view sees it. But ⌥⌫ and ⌃⌫ are nobody's key equivalent, and a fall-through
|
||||
// that tombstoned the selection on a mistyped text-editing chord would be exactly the kind of
|
||||
// that deleted the selection on a mistyped text-editing chord would be exactly the kind of
|
||||
// accident 04-interactions.md's fixed grammar is careful to avoid.
|
||||
guard press.modifiers.intersection([.command, .option, .control, .shift]).isEmpty else {
|
||||
return .ignored
|
||||
@@ -633,7 +636,7 @@ struct BoardView: View {
|
||||
guard !store.isEditingInline, !store.isReadOnly else { return .ignored }
|
||||
let selection = store.selection
|
||||
guard !selection.isEmpty else { return .ignored }
|
||||
store.deleteSelection()
|
||||
confirmations.requestDelete(in: store)
|
||||
return .handled
|
||||
}
|
||||
|
||||
@@ -860,7 +863,7 @@ struct BoardView: View {
|
||||
}
|
||||
|
||||
/// **⌥↑/⌥↓ jump to the current container's first/last card** — the lane's, or the trash
|
||||
/// quasi-lane's when that is where the cursor is.
|
||||
/// trash column's when that is where the cursor is.
|
||||
///
|
||||
/// **⌥↑ escalates into the lane domain** (04 ▸ Grammar, settled — "the keyboard's one entry to
|
||||
/// lane selection"): with the lane's first card already the sole selection, the next ⌥↑ selects
|
||||
|
||||
Reference in New Issue
Block a user