Realign code with the 2026-07-29 findings-resolution rulings
Nine rulings land as code. Reorders don't stamp — one container-change predicate (WriteOperation.rewritesOrderOnly): within-container reorders and the renumber rescale rewrite only order, while cross-lane, cross-board, and trash moves stamp modified and clear modified-by; no trash special case exists, and the m8 undo inverses conform through the same seam. Copies are transactions: the root-strict/nested-lenient split retires for a whole-subtree stampability preflight that refuses loudly naming the offender, and every item-level copy severs remote/remote-state at every level (whole-board forks carry them verbatim). Paste refuses, never degrades: the embedded-index.md materialization and its loss row retire; a missing staged snapshot produces nothing and posts an error-tone one-shot named from manifest metadata. Coerce-tier fallbacks log through the Defect stream with path context attached loader-side. Displacement is level-uniform: a file squatting attachments inside a card heals by the same rename ladder as board-root squatters; comments stays tolerated. Delete Immediately joins card and lane context menus as Delete's ⌥-alternate with its own VO custom action, routed through an explicit container so the menu target outranks standing selection. Agent guide v7 teaches the stamp discipline and the card-level attachments claim, and sheds two stale v6 lines (lanes trash now; kind is taught). Verified conformant, unchanged: edition-aware Undo/Redo disable, trash marquee full-height backdrop. Both schemes 1854 tests / 318 suites green; verify-editions 30/30. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -26,11 +26,17 @@ import SwiftUI
|
||||
/// Plus the two that are not about editing: Finder file drops are inert over the trash (▸ The trash),
|
||||
/// 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`).
|
||||
enum CardFaceRole {
|
||||
|
||||
/// A card in a lane. Carries the board window's card opener — ⌘↩'s pointer twin
|
||||
/// (04-interactions.md ▸ Selection).
|
||||
case board(openCard: (ItemID) -> Void)
|
||||
/// (04-interactions.md ▸ Selection) — and the window's purge-alert host, for Delete's
|
||||
/// ⌥-alternate.
|
||||
case board(openCard: (ItemID) -> Void, confirmations: TrashConfirmations)
|
||||
|
||||
/// 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).
|
||||
@@ -143,7 +149,7 @@ struct CardFaceView: View {
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
switch role {
|
||||
case let .board(openCard):
|
||||
case let .board(openCard, confirmations):
|
||||
face
|
||||
// "A fast double-click opens the card window (⌘↩'s pointer twin)" (04 ▸ Selection).
|
||||
//
|
||||
@@ -158,14 +164,18 @@ struct CardFaceView: View {
|
||||
guard ClickModifier.current == .plain else { return }
|
||||
openCard(card.id)
|
||||
})
|
||||
.contextMenu { boardMenu(openCard: openCard) }
|
||||
.contextMenu { boardMenu(openCard: openCard, confirmations: confirmations) }
|
||||
// **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.
|
||||
.accessibilityActions { boardActions(openCard: openCard) }
|
||||
// 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) }
|
||||
.popover(isPresented: styleEditorPresentation(store, anchor: card.id), arrowEdge: .bottom) {
|
||||
StyleEditorPopover(store: store, recents: appModel.styleRecents)
|
||||
}
|
||||
@@ -443,10 +453,10 @@ struct CardFaceView: View {
|
||||
|
||||
// MARK: - Context menus
|
||||
|
||||
/// Open, Rename, Style…, the quick-style recents row, Delete — 11-command-nexus.md ▸ Context
|
||||
/// menus' Card row, in its order.
|
||||
/// 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.
|
||||
@ViewBuilder
|
||||
private func boardMenu(openCard: @escaping (ItemID) -> Void) -> some View {
|
||||
private func boardMenu(openCard: @escaping (ItemID) -> Void, confirmations: TrashConfirmations) -> 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
|
||||
@@ -474,19 +484,38 @@ 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) -> some View {
|
||||
private func boardActions(openCard: @escaping (ItemID) -> Void, confirmations: TrashConfirmations) -> 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
|
||||
@@ -536,6 +565,15 @@ 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)
|
||||
}
|
||||
@@ -566,6 +604,18 @@ 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] {
|
||||
@@ -605,7 +655,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)
|
||||
|
||||
Reference in New Issue
Block a user