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:
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user