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:
@@ -21,7 +21,7 @@ public struct CardPlacement: Equatable {
|
||||
/// A named decision rather than a scattering of `if`s, because 05-card-window.md ▸ Deletion &
|
||||
/// lifecycle and 02-architecture.md § Live-reload resilience state the same rule from two directions
|
||||
/// and both have to be true of one piece of code. Making it a value also makes it a *pure* function
|
||||
/// of a snapshot, which is the only way the tombstoned-lane case gets tested without a window.
|
||||
/// of a snapshot, which is the only way the deleted-lane case gets tested without a window.
|
||||
public enum CardWindowFate: Equatable {
|
||||
case shows(CardPlacement)
|
||||
case dismisses
|
||||
@@ -38,10 +38,10 @@ public enum CardWindowFate: Equatable {
|
||||
/// nothing the second time, which is what makes those two paths safe to both exist.
|
||||
///
|
||||
/// **What ending means now**: the Edit buffer's debounce is cancelled and its text written — the
|
||||
/// "window close" third of 05's flush rule, and on a dismissal caused by a tombstone the surgical
|
||||
/// body write 05 ▸ Deletion & lifecycle promises ("a dirty Edit buffer flushes into the tombstoned
|
||||
/// card's folder before the window dismisses ... so the keystrokes survive Put Back").
|
||||
/// `BoardStore.writeCardBody` resolves tombstoned cards on purpose for exactly this.
|
||||
/// "window close" third of 05's flush rule, and on a dismissal caused by a delete the surgical body
|
||||
/// write 05 ▸ Deletion & lifecycle promises ("a dirty Edit buffer flushes into the card's folder at
|
||||
/// its new `.trash/` location before the window dismisses ... so the keystrokes survive a later
|
||||
/// restore"). `BoardStore.writeCardBody` resolves trash cards on purpose for exactly this.
|
||||
///
|
||||
/// A *failing* close flush is not this object's problem to solve: it is `DirtyBufferGuard`'s modal
|
||||
/// moment, which the host runs earlier, on `windowShouldClose`, while there is still a window to
|
||||
@@ -100,10 +100,11 @@ final class CardWindowSession: CardSessionFlushing {
|
||||
/// ### Its whole identity is `(board, card)`
|
||||
///
|
||||
/// Which is why this host is mostly a set of dismissal rules. The window follows its card between
|
||||
/// lanes for free — the key names neither — and it dismisses in the three cases where the key stops
|
||||
/// naming anything: the card is tombstoned, its *lane* is tombstoned (effective liveness is
|
||||
/// ancestor-walked, 02 § Live-reload resilience), or the card is simply not in this board's snapshot
|
||||
/// any more, which is what a cross-board move looks like from here.
|
||||
/// lanes for free — the key names neither — and it dismisses whenever the key stops naming a card
|
||||
/// **on the board**: the card moved into `.trash/` ("entering the trash counts as deleted" —
|
||||
/// 05-card-window.md ▸ Deletion & lifecycle, resettled 2026-07-28), its *lane* was deleted and took
|
||||
/// it along, or the card is simply not in this board's snapshot any more, which is what a cross-board
|
||||
/// move looks like from here.
|
||||
///
|
||||
/// ### It can never outlive its board window
|
||||
///
|
||||
@@ -164,10 +165,12 @@ struct CardWindowHost: View {
|
||||
|
||||
/// Whether a card window keyed on `cardID` still has a card, given this board's snapshot.
|
||||
///
|
||||
/// The three dismissal cases collapse into two lines: a card that is not in the snapshot is gone
|
||||
/// (deleted outright, or moved to another board — the board half of the key no longer names it),
|
||||
/// and a card whose **effective** liveness is trashed renders nowhere, whether the tombstone is
|
||||
/// its own or its lane's. Only a live card in a live lane keeps its window.
|
||||
/// **One walk over the lanes is the whole rule** (05-card-window.md ▸ Deletion & lifecycle,
|
||||
/// resettled 2026-07-28 — the materialized trash): deletion is a *move*, so a trashed card has
|
||||
/// physically left its lane and answers `.dismisses` by simply not being found — "entering the
|
||||
/// trash counts as deleted", with no liveness flag to read and no ancestor walk to run. A card
|
||||
/// whose lane was deleted, one purged outright and one moved to another board all fall out of the
|
||||
/// same absence. Only a card in one of this board's lanes keeps its window.
|
||||
///
|
||||
/// Takes the id as the ref stores it — a raw folder name — and compares it as an `ItemID`, so two
|
||||
/// case-spellings of one UUID are one card here exactly as they are everywhere else.
|
||||
|
||||
Reference in New Issue
Block a user