Build the menu bar per the Command Nexus
Nexus parity, audited row by row (11-command-nexus.md § Menu commands) — every already-built item's title, chord, and placement matched exactly; this pass fills what remained: - The future-window rows, present with stable titles and validation-driven disablement until their milestones fill the actions: Save as Template (m9), Add Attachment… ⇧⌘A, Find Next/Previous ⌘G/⇧⌘G, the View-menu card triplet Edit Body ⌘E / Raw Source ⌥⌘E / History (m6), Board ▸ Pull/Push (git milestones) — one shared disabled-row shape in FutureCommands.swift so later milestones only flip validation. - No Print story in v1: the print group is removed. - Help carries the Nexus's one remap-teaching line — Customize Keyboard Shortcuts…, opening System Settings' Keyboard ▸ Shortcuts extension directly (the modern extension URL, verified to launch the appex). - The launch-restore decision now runs through a pure, tested AppModel.shouldRestoreAtLaunch gate; the Settings pane's caption rides a proper Form section footer. 904 unit tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
+63
-25
@@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// The scene graph (02-architecture.md § Windows, § Launch and window lifecycle).
|
||||
@@ -40,8 +41,10 @@ struct KanbanApp: App {
|
||||
init() {
|
||||
let model = AppModel()
|
||||
_appModel = State(initialValue: model)
|
||||
shouldRestoreAtLaunch = AppPreferences.restoreOpenBoardsAtLaunch
|
||||
&& !model.boardRegistry.restorables().isEmpty
|
||||
shouldRestoreAtLaunch = AppModel.shouldRestoreAtLaunch(
|
||||
preference: AppPreferences.restoreOpenBoardsAtLaunch,
|
||||
hasRestorables: !model.boardRegistry.restorables().isEmpty
|
||||
)
|
||||
// The delegate is constructed by the adaptor before this runs, so this is the one place the
|
||||
// app's model and its AppKit half meet.
|
||||
appDelegate.appModel = model
|
||||
@@ -118,20 +121,24 @@ struct KanbanApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
/// The menu items the app has built so far.
|
||||
/// Every menu command 11-command-nexus.md inventories, at full row parity: built and validated
|
||||
/// where the window behind it already exists, present-but-`FutureCommand`-disabled where it
|
||||
/// doesn't (`FutureCommands.swift`).
|
||||
///
|
||||
/// **The titles are API** (04-interactions.md ▸ Configurable bindings): macOS's App Shortcuts
|
||||
/// mechanism remaps menu items *by title*, so these strings are the keys a user's custom binding
|
||||
/// is stored under. They are spelled exactly as 11-command-nexus.md inventories them, and
|
||||
/// changing one silently breaks every remap of it.
|
||||
/// mechanism — `NSUserKeyEquivalents` under the hood — remaps menu items *by title*, so these
|
||||
/// strings are the keys a user's custom binding is stored under. They are spelled exactly as
|
||||
/// 11-command-nexus.md inventories them, unique across the whole menu bar, and changing one
|
||||
/// silently breaks every remap of it. Nothing below builds that mechanism — a stable title is the
|
||||
/// whole of the contract, and the system supplies the rest.
|
||||
@CommandsBuilder
|
||||
private var menuCommands: some Commands {
|
||||
// The File group, in 11-command-nexus.md's own row order: New Card, New Lane, New Board…,
|
||||
// Open…, Open Recent ▸, Board Info, Duplicate, (Save as Template, still owed — 09),
|
||||
// Reveal in Finder, then the trash trio and Empty Trash…. The board-scoped ones validate
|
||||
// against the frontmost board through the focus system, so each is simply absent-of-effect
|
||||
// when no board is in front; New Board… and Open Recent are available everywhere, including
|
||||
// with no window at all.
|
||||
// Open…, Open Recent ▸, Board Info, Duplicate, Save as Template, Reveal in Finder, Add
|
||||
// Attachment…, then the trash trio and Empty Trash…. The board-scoped ones validate against
|
||||
// the frontmost board through the focus system, so each is simply absent-of-effect when no
|
||||
// board is in front; New Board… and Open Recent are available everywhere, including with no
|
||||
// window at all. Close is the system's own item — no row of ours to add.
|
||||
CommandGroup(after: .newItem) {
|
||||
BoardCreationCommands()
|
||||
NewBoardCommand(appModel: appModel)
|
||||
@@ -152,37 +159,42 @@ struct KanbanApp: App {
|
||||
Divider()
|
||||
|
||||
DuplicateBoardCommand(appModel: appModel)
|
||||
SaveAsTemplateCommand()
|
||||
RevealInFinderCommand()
|
||||
AddAttachmentCommand()
|
||||
|
||||
Divider()
|
||||
|
||||
TrashCommands()
|
||||
}
|
||||
|
||||
// The Edit menu's one row of ours: Find (⌘F), placed after the standard Cut/Copy/Paste/
|
||||
// Select All group, which is where macOS puts Find. Undo/Redo and the clipboard items are
|
||||
// the system's and the board answers them as a responder (`ClipboardCommands.swift`) — a
|
||||
// second item sharing one of those titles is what titles-are-API forbids.
|
||||
//
|
||||
// m6-card-window: Find Next / Find Previous (⌘G/⇧⌘G) join here, scoped to the card window's
|
||||
// find bar and "disabled in the board window — board search is a live filter, not a cursor"
|
||||
// (11-command-nexus.md). They wait for the window that owns them rather than shipping as two
|
||||
// permanently disabled rows.
|
||||
// The Edit menu: Find (⌘F), then its card-window stepping twins, placed after the standard
|
||||
// Cut/Copy/Paste/Select All group, which is where macOS puts Find. Undo/Redo and the
|
||||
// clipboard items are the system's and the board answers them as a responder
|
||||
// (`ClipboardCommands.swift`) — a second item sharing one of those titles is what
|
||||
// titles-are-API forbids.
|
||||
CommandGroup(after: .pasteboard) {
|
||||
FindCommand()
|
||||
FindSteppingCommands()
|
||||
}
|
||||
|
||||
// The View menu. `CommandGroupPlacement.toolbar` *is* View — the menu the toolbar's own
|
||||
// items live in — which is where 11-command-nexus.md files Show Trash, alongside the card
|
||||
// window's Edit Body / Raw Source / History still to come.
|
||||
// items live in — which is where 11-command-nexus.md files Show Trash. A divider separates it
|
||||
// from the card window's three view-state rows below: one board-scoped toggle, then a
|
||||
// card-scoped trio.
|
||||
CommandGroup(after: .toolbar) {
|
||||
ShowTrashCommand()
|
||||
|
||||
Divider()
|
||||
|
||||
CardViewCommands()
|
||||
}
|
||||
|
||||
// The Board menu (11-command-nexus.md), complete and in its inventoried row order — Open
|
||||
// Card, Rename, Style…, the card moves, the lane moves, the width pair. Its items act on the
|
||||
// frontmost board window, which they reach through the focus system rather than through the
|
||||
// app model — see `BoardCommands.swift`, which also owns their validation.
|
||||
// Card, Rename, Style…, the card moves, the lane moves, the width pair, then the remote pair.
|
||||
// Its items act on the frontmost board window, which they reach through the focus system
|
||||
// rather than through the app model — see `BoardCommands.swift`, which also owns their
|
||||
// validation.
|
||||
CommandMenu("Board") {
|
||||
OpenCardCommand()
|
||||
BoardRenameCommand()
|
||||
@@ -196,6 +208,10 @@ struct KanbanApp: App {
|
||||
Divider()
|
||||
|
||||
LaneWidthCommands()
|
||||
|
||||
Divider()
|
||||
|
||||
RemoteCommands()
|
||||
}
|
||||
|
||||
CommandGroup(after: .windowList) {
|
||||
@@ -205,5 +221,27 @@ struct KanbanApp: App {
|
||||
appModel.showWelcome()
|
||||
}
|
||||
}
|
||||
|
||||
// "No Print story in v1 (⌘P unused)" (11-command-nexus.md ▸ Standard macOS furniture) — the
|
||||
// system's default Print item is removed outright rather than left dead, since a menu item
|
||||
// with nothing behind it is exactly what the Nexus's "a command absent here doesn't exist"
|
||||
// rules out in the other direction too.
|
||||
CommandGroup(replacing: .printItem) {}
|
||||
|
||||
// Help carries "the one line teaching the System Settings remap path" (11-command-nexus.md ▸
|
||||
// Standard macOS furniture) — this app's whole Help menu, since there is no other content to
|
||||
// give it. The pane identifier is the modern System Settings extension id, not the legacy
|
||||
// `com.apple.preference.keyboard` prefpane path: verified on macOS 26 by observing
|
||||
// `KeyboardSettings.appex` (service `com.apple.Keyboard-Settings.extension`) launch in
|
||||
// response to this exact URL. `?Shortcuts` is as deep as a URL reaches; **App Shortcuts**
|
||||
// itself is one more click, the sidebar row inside that pane.
|
||||
CommandGroup(after: .help) {
|
||||
Button("Customize Keyboard Shortcuts…") {
|
||||
guard let url = URL(
|
||||
string: "x-apple.systempreferences:com.apple.Keyboard-Settings.extension?Shortcuts"
|
||||
) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user