Realign code with the 2026-07-30 findings-resolution rulings

Delete Immediately is removed entirely (ruling ae1dd96, Redesign card
d40bfac1): the delete vocabulary is purely staged — board → trash,
trash → permanent (confirmed on no-git boards), Empty Trash for bulk.
Gone: File ▸ Delete Immediately (⌥⌘⌫) and its validation, both
⌥-alternate context rows (card + the permanently-disabled lane row),
the VO custom action, BoardStore.deleteImmediately,
TrashModel.canDeleteImmediately, TrashConfirmations' .purge action
(zero surviving callers — trash-side Delete always used
.deleteTrashCards), and the pinning tests. purgePrompt drops its
now-single-purpose container parameter (.trash is the only surviving
caller). BoardWriter.purgeItem survives — create-undo rollback still
needs it — with its comment rewritten. README's trash paragraph drops
the ⌥⌘⌫ sentence.

The other 2026-07-30 rulings (2ec2c95 registry freshness stamp,
c741b02 unified-log-as-coerce-consumer) required no code changes —
already conformant.

Both schemes 1844 tests / 318 suites green.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-30 15:13:31 -04:00
parent 9ca8ed84de
commit 785ef5fe14
15 changed files with 83 additions and 483 deletions
+14 -60
View File
@@ -27,16 +27,14 @@ import SwiftUI
/// so the file-hover highlight is board-only; and the trash's context-menu Delete is *permanent*, so
/// it needs the window's confirmation host (11-command-nexus.md Context menus' Trash cards row).
///
/// **Both sides carry the confirmation host now** (settled Delete's -alternate, Delete
/// Immediately, 11-command-nexus.md Context menus' Card row): the board side's Delete stays the
/// ordinary staged move, but the alternate skips straight to the permanent purge, which needs the
/// same window-level alert the trash side's Delete already does (`TrashConfirmations`).
/// **Only the trash side carries the confirmation host** (settled): the board side's Delete is the
/// ordinary staged move into `.trash/` and never stands an alert, so `board` needs nothing beyond the
/// card opener.
enum CardFaceRole {
/// A card in a lane. Carries the board window's card opener 's pointer twin
/// (04-interactions.md Selection) and the window's purge-alert host, for Delete's
/// -alternate.
case board(openCard: (ItemID) -> Void, confirmations: TrashConfirmations)
/// (04-interactions.md Selection).
case board(openCard: (ItemID) -> Void)
/// A card in `<root>/.trash/`. Carries the window's purge-alert host, because the trash's Delete
/// is the permanent one and "confirms exactly where the loss is real" (03 § Trash).
@@ -149,7 +147,7 @@ struct CardFaceView: View {
@ViewBuilder
var body: some View {
switch role {
case let .board(openCard, confirmations):
case let .board(openCard):
face
// "A fast double-click opens the card window ('s pointer twin)" (04 Selection).
//
@@ -164,18 +162,14 @@ struct CardFaceView: View {
guard ClickModifier.current == .plain else { return }
openCard(card.id)
})
.contextMenu { boardMenu(openCard: openCard, confirmations: confirmations) }
.contextMenu { boardMenu(openCard: openCard) }
// **The menu's rows, additionally as custom actions** "where SwiftUI additionally
// surfaces menu items as custom accessibility actions, that's free improvement, not
// a separate design surface" (10-accessibility.md Actions come from the context
// menu). The menu stays the inventory and stays reachable the standard way (VO--M).
// Style is absent for `LaneView`'s reason: it opens a popover, and the quick-style
// swatch `Picker` beside it is not an action. Delete Immediately rides along too,
// its own row rather than the -alternate above VoiceOver's action rotor has no
// held-key concept, so the alternate needs a first-class custom action of its own
// (11-command-nexus.md Context menus' Card row: "surfaces as its own VO custom
// action per 10's cut").
.accessibilityActions { boardActions(openCard: openCard, confirmations: confirmations) }
// swatch `Picker` beside it is not an action.
.accessibilityActions { boardActions(openCard: openCard) }
.popover(isPresented: styleEditorPresentation(store, anchor: card.id), arrowEdge: .bottom) {
StyleEditorPopover(store: store, recents: appModel.styleRecents)
}
@@ -453,10 +447,10 @@ struct CardFaceView: View {
// MARK: - Context menus
/// Open, Rename, Style, the quick-style recents row, Delete with Delete Immediately as its
/// -alternate 11-command-nexus.md Context menus' Card row, in its order.
/// Open, Rename, Style, the quick-style recents row, Delete 11-command-nexus.md Context
/// menus' Card row, in its order.
@ViewBuilder
private func boardMenu(openCard: @escaping (ItemID) -> Void, confirmations: TrashConfirmations) -> some View {
private func boardMenu(openCard: @escaping (ItemID) -> Void) -> some View {
// Open: Board Open Card's pointer twin (`OpenCardCommand`), restricted to the clicked card
// alone "a card window is tied to one card" (11-command-nexus.md), so unlike Style and
// Delete below it, this row never widens to the selection; Open never opens multiple, even
@@ -484,38 +478,19 @@ struct CardFaceView: View {
// Delete: File Delete's exact store path (`store.delete`, `TrashCommands`'s twin), on the
// widened target set below (`targetIDs`) the successor-selection rule is `delete(_:)`'s own,
// so this row gets it for free.
//
// **Delete Immediately rides as its -alternate** (Finder's own pattern hold and Delete
// becomes Delete Immediately, 11-command-nexus.md Context menus' Card row, settled
// 2026-07-29). `.modifierKeyAlternate(.option)` is SwiftUI's macOS-native mechanism for
// exactly this swap (macOS 15+); the alternate's title matches File Delete Immediately's
// own verbatim (`TrashCommands`), since menu titles are the system remapping key and the two
// rows name the same command. It widens over the same `targetIDs` Delete itself reads, so an
// -held click purges exactly what a plain click would have trashed.
Button("Delete") { deleteTargets() }
.disabled(!store.acceptsBoardMutations)
.modifierKeyAlternate(.option) {
Button("Delete Immediately") { requestDeleteImmediately(confirmations) }
.disabled(!canDeleteImmediately)
}
}
/// `boardMenu`'s plain rows as VoiceOver custom actions every one calling the *same* private
/// method its menu row does, so the two surfaces cannot come to mean different things.
///
/// Delete Immediately gets its own row here rather than riding `modifierKeyAlternate` the
/// action rotor has no held-key concept, so the alternate needs a first-class custom action of
/// its own to be reachable at all ("surfaces as its own VO custom action per 10's cut",
/// 11-command-nexus.md Context menus' Card row).
@ViewBuilder
private func boardActions(openCard: @escaping (ItemID) -> Void, confirmations: TrashConfirmations) -> some View {
private func boardActions(openCard: @escaping (ItemID) -> Void) -> some View {
Button("Open") { openCard(card.id) }
Button("Rename") { beginRename() }
.disabled(!store.acceptsBoardMutations)
Button("Delete") { deleteTargets() }
.disabled(!store.acceptsBoardMutations)
Button("Delete Immediately") { requestDeleteImmediately(confirmations) }
.disabled(!canDeleteImmediately)
}
/// Delete and Reveal in Finder the two rows 11-command-nexus.md gives a trash card, and no
@@ -565,15 +540,6 @@ struct CardFaceView: View {
confirmations.requestTrashDelete(of: targetIDs, in: store)
}
/// The board side's **permanent** delete Delete's -alternate and its VoiceOver custom-action
/// twin through the same window confirmation host `requestPurge` above uses, but
/// `TrashConfirmations`'s board-side entry point (`requestBoardPurge`): the alert (or the
/// git-board shrug) is what stands between this row and an unrecoverable loss, exactly as it does
/// for the trash's own Delete (03 § Trash).
private func requestDeleteImmediately(_ confirmations: TrashConfirmations) {
confirmations.requestBoardPurge(of: targetIDs, in: store)
}
private func revealInFinder() {
NSWorkspace.shared.activateFileViewerSelecting(targetFolders)
}
@@ -604,18 +570,6 @@ struct CardFaceView: View {
return store.selection.ids
}
/// Whether Delete Immediately's -alternate has something to purge File Delete Immediately's
/// own predicate (`TrashModel.canDeleteImmediately`), read over this row's `targetIDs` rather
/// than the standing selection, `targetIDs`' own reason: a context menu names its target by where
/// it was invoked (11-command-nexus.md Context menus' Card row).
private var canDeleteImmediately: Bool {
guard store.acceptsBoardMutations else { return false }
return TrashModel.canDeleteImmediately(
selection: ItemReferenceSet(ids: targetIDs, container: role.container),
in: store.snapshot
)
}
/// The folders Reveal in Finder points at resolved in this face's container, so a trash row
/// reveals `<root>/.trash/<uuid>` and never a lane path that no longer holds the card.
private var targetFolders: [URL] {
@@ -655,7 +609,7 @@ struct CardFaceView: View {
onCommitAndOpen: {
let id = card.id
store.commitRename()
if case let .board(openCard, _) = role { openCard(id) }
if case let .board(openCard) = role { openCard(id) }
}
)
.font(.body)