Build per-surface context menus

The remaining rows of 11-command-nexus.md § Context menus, every entry a
twin of an existing command path, never a parallel implementation:

- Card: Open (the double-click's own openCard closure, always the clicked
  card alone), Rename (Board ▸ Rename's beginRename path), Style… and the
  quick-style recents (already present), Delete (File ▸ Delete's
  store.delete on the standard widened target — selection when the clicked
  card is a member, else the card alone).
- Lane (one menu, header and empty space): Rename and Delete join the
  existing Style…/recents/Width rows, in table order.
- Trash entries and welcome recents verified already exact against the
  table; the attachment row's menu is marked for m6 beside its command.
- File ▸ Reveal in Finder gains its board-window scope, the branch the m4
  comment deferred here: the selection's folders on either side of the
  trash boundary — enabled under every lock, inspection being a read — or
  the board root with nothing selected; a selection resolving to no
  folders disables rather than guessing.

904 unit tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 23:42:37 -04:00
parent c1f304d3fe
commit 6c490ec71c
3 changed files with 120 additions and 25 deletions
+29 -10
View File
@@ -186,27 +186,46 @@ struct DuplicateBoardCommand: View {
// MARK: - Reveal in Finder
/// File Reveal in Finder **the welcome scope** (11-command-nexus.md: "welcome: the selected
/// recent's folder (disabled on unavailable rows) the context-menu entry's required twin").
/// File Reveal in Finder the welcome and board scopes (11-command-nexus.md: "Board window: the
/// selection's folder(s), or the board root with nothing selected; welcome: the selected recent's
/// folder (disabled on unavailable rows) the context-menu entry's required twin").
///
/// It is here because the welcome row's context menu is: 11 files the menu-bar item as that entry's
/// *required* twin, so shipping one without the other would leave the context menu as the only path
/// to a command the thing 04's contract forbids.
///
// m5-context-menus, m6-card-window: the item's other two scopes. Board window the selection's
// folder(s), or the board root with nothing selected arrives with the board's own context menus;
// card window the card's folder, or the selected attachment's file when the attachments section is
// focused with the card window. Each adds a focused value and a branch here; the welcome branch
// does not move.
/// **The board scope reveals either side of the trash boundary and ignores every lock.** Reveal "is
/// not edit-shaped and stays enabled on tombstoned selections" (04 The trash), and inspection is a
/// read, so neither the read-only lock nor the focused-editor rule applies the same posture the
/// trash row's own Reveal takes. A selection whose ids resolve to no folders (one the next reload
/// will drop) disables rather than falling back to the root: revealing the wrong thing is worse
/// than nothing, and only a genuinely empty selection means "the board".
///
// m6-card-window: the item's third scope the card's folder, or the selected attachment's file
// when the attachments section is focused adds a focused value and a branch here; the two below
// do not move.
struct RevealInFinderCommand: View {
@FocusedValue(\.boardStore) private var store
@FocusedValue(\.welcomeSelection) private var selection
var body: some View {
Button("Reveal in Finder") {
guard let url = selection?.url else { return }
NSWorkspace.shared.activateFileViewerSelecting([url])
NSWorkspace.shared.activateFileViewerSelecting(urls)
}
.disabled(selection?.canReveal != true)
.disabled(urls.isEmpty)
}
/// What the item would reveal, and therefore whether it is enabled one answer for both, the
/// codebase's usual shape. The board in front wins; the welcome branch stands when no board is.
private var urls: [URL] {
if let store {
let ids = store.selection.ids
guard !ids.isEmpty else { return [store.rootURL] }
return TrashModel.paths(of: ids, on: store.selection.liveness, in: store.snapshot)
.map { $0.folder(under: store.rootURL) }
}
guard let selection, selection.canReveal, let url = selection.url else { return [] }
return [url]
}
}
+8
View File
@@ -72,6 +72,14 @@ struct SaveAsTemplateCommand: View {
// (05-card-window.md § Attachments) and of a whole-window Finder file drop. Validation will be scope
// alone a card window in front, the read-only lock aside `BoardInfoCommand`'s shape for its own
// scope-only item.
//
// m5-context-menus, m6-card-window: the attachment row's own context menu is a second thing this
// milestone owes Open, Remove (system Trash), Reveal in Finder (11-command-nexus.md Context
// menus' Attachment row), twinning the focused section's grammar keys (Return open / remove 05
// Attachments) and Reveal in Finder's attachments-focused scope, exactly the way this milestone's
// card and lane menus twin their own grammar and menu-bar commands: no new store method, no parallel
// implementation. There is no row view to hang a `.contextMenu` off yet, so nothing scaffolds here
// beyond this marker.
struct AddAttachmentCommand: View {
var body: some View {
FutureCommand(title: "Add Attachment…", key: "a", modifiers: [.shift, .command])