import SwiftUI // MARK: - The shared shape /// The shape behind every menu row this milestone ships **before** the window that answers it. /// /// 11-command-nexus.md's own contract runs both directions: "a command absent here doesn't exist, and /// adding one means adding a row here first" — so once a row *is* in the Nexus, shipping the window /// behind it is a validation-and-action change, not a menu change. `FutureCommand` below is that /// reading, applied: the row exists now, stably titled and stably chorded — `NSUserKeyEquivalents` /// already resolves it, so a user can remap it today — with validation pinned to `false` and the /// action a no-op until the milestone named at the call site fills both in. That milestone's whole /// diff then reads as "flip `.disabled`, fill the closure" rather than "add a menu item", which is /// also why every call site below carries the codebase's `mN-` marker for a component still owed. /// /// **The title never moves once a row ships**, disabled or not: a command wired live later must not /// gain a second spelling on the way (04-interactions.md ▸ Configurable bindings — "toggles keep one /// stable title, checkmark state only" — which applies to a row that has not started ticking yet /// exactly as it does to one that has). /// /// There was a `FutureToggleCommand` beside this — a disabled `Toggle` for a checkmark row — and it /// went with the last of its call sites (Raw Source, `RawSourceCommand`). Every row still owed is a /// plain command; a future checkmark row brings its scaffold back with it rather than keeping an /// unused one warm. struct FutureCommand: View { let title: String var key: KeyEquivalent? var modifiers: EventModifiers = .command var body: some View { Button(title) { // No-op: the milestone named at the call site wires this in. } .keyboardShortcut(key.map { KeyboardShortcut($0, modifiers: modifiers) }) .disabled(true) } } // File ▸ Save as Template was the scaffold here and is now live, beside the command it mirrors // (`SaveAsTemplateCommand`, in `AppCommands.swift` next to `DuplicateBoardCommand`, whose flush → // cancellable copy → banner sequence it repeats with a different destination). The diff this file // predicts, once more: the title did not move, the chord stayed absent, the validation and the // action filled in — the last of them being 09's carve-out from the read-only lock, which is why it // could not simply borrow Duplicate's predicate. // // File ▸ Add Attachment… (⇧⌘A) was the scaffold here and is now live, beside the focused value it // reads (`AddAttachmentCommand`, in `CardAttachments.swift`) — the diff this file predicts: the // title and the chord did not move, the validation and the action filled in. The attachment row's // context menu (Open / Remove / Reveal in Finder) shipped with it, on the row view that now exists. // MARK: - Edit ▸ Find Next / Find Previous /// Edit ▸ Find Next / Find Previous (⌘G / ⇧⌘G) — the card window's find bar stepping, /// "disabled in the board window — board search is a live filter, not a cursor" /// (11-command-nexus.md). /// /// ### They are live for exactly one find, and disabled for the others on purpose /// /// The card window has three finds (`CardWindowFindRoute`), and two of them are **`NSTextFinder`**'s — /// the body's and an authoring editor's. `NSTextView` already answers ⌘G and ⇧⌘G through the responder /// chain, and an *enabled* menu item's key equivalent fires before the responder chain is consulted, /// so a row that claimed the chord unconditionally would break the stepping it exists to provide. So /// these validate on the **thread** find alone — the one find with no responder to fall through to, /// because its bar is the app's own — and stay disabled everywhere else, which lets the platform's /// stepping keep working where the platform owns the find. /// /// `.disabled` on the rows rather than a guard in the action, for the reason every menu row here /// wears its validation: a key equivalent that fires and does nothing is a chord the user cannot tell /// from a broken one. struct FindSteppingCommands: View { @FocusedValue(\.cardComments) private var comments /// The row's validation, as a value a test can hold: the pane's find bar is up, which is the only /// state in which this app owns ⌘G. static func isEnabled(_ comments: CardComments?) -> Bool { comments?.find.isShowing == true } var body: some View { Button("Find Next") { comments?.find.step(forward: true) } .keyboardShortcut("g", modifiers: .command) .disabled(!Self.isEnabled(comments)) Button("Find Previous") { comments?.find.step(forward: false) } .keyboardShortcut("g", modifiers: [.shift, .command]) .disabled(!Self.isEnabled(comments)) } } // MARK: - View ▸ Edit Body / Raw Source / History /// View ▸ Edit Body (⌘E) / Raw Source (⌥⌘E) / History — the card window's three view-state rows /// (11-command-nexus.md). /// /// **Edit Body and Raw Source are both live** (`EditBodyCommand`, `RawSourceCommand`, each beside the /// focused value it reads): the body column's Preview/Edit toggle, and the window-level outlet whose /// toggling-off *applies*. Each diff was the one `FutureCommand` promises — the title and the chord /// did not move, the validation and the action filled in — and the pair also carries the clause that /// joins them, "Edit Body disables while Raw Source is active" (05-card-window.md). /// // m7-git: History is a plain command that focuses the sidebar's History section, and disables // outright on mode `none` / repo-nested boards once that section exists (05-card-window.md, // 07-sync-collab.md). It remains unconditionally disabled here — the sidebar reserves the section's // place (`CardWindowView.historySlot`) but draws nothing, so there is still no surface to focus. /// The comments pane's two rows join them (11-command-nexus.md lists Show Comments and Comments /// Beside Body between Edit Body and Raw Source): both are live, both are app-wide persisted bits, /// and both are scoped to the card window (`ShowCommentsCommand`, `CommentsBesideBodyCommand`). struct CardViewCommands: View { var body: some View { EditBodyCommand() ShowCommentsCommand() CommentsBesideBodyCommand() RawSourceCommand() FutureCommand(title: "History") } } // MARK: - Board ▸ Pull / Push /// Board ▸ Pull / Push — no default chord, remote-backed boards only (11-command-nexus.md; /// 07-sync-collab.md: "also Board-menu items"). /// // m7-git: menu-bar twins of the board popover's own Pull/Push buttons (07-sync-collab.md). // Validation will be remote-mode plus 06's abnormal-state pause ("disabled during 06's // abnormal-state pause ... and on an unresolvable remote" — 11-command-nexus.md). Unconditionally // disabled here: there is no remote model, no popover twin, and no git mode to validate against yet. struct RemoteCommands: View { var body: some View { FutureCommand(title: "Pull") FutureCommand(title: "Push") } }