Board search reaches comment bodies through a search-owned transient
index: the first live-query keystroke sweeps comments/*/index.md
off-actor (.draft and comments/.trash excluded), keystrokes re-filter
in memory, the index discards on clear — the snapshot stays O(cards).
⌘F routes by focus: the comments pane gets an app-owned find bar
spanning the whole rendered thread (next/prev cross rows with
wraparound); body and composer keep NSTextFinder; Find Next/Previous
graduate from FutureCommands. Foreign comment changes speak
path-shaped beside the announcer's ladder ("New comment on 'X'",
plural folds), narrowed by EchoLedger receipts consumed through
CommentPath.classify — and that read fixed a latent footprint bug
where a comment receipt resolved against the card's attachment
listing, read .absent, and classified the user's own write as
foreign. The pane completes its a11y story: flattened comment
elements with Edit/Delete/Reveal custom actions (un-flattening
during inline edit), phrase-table vocabulary, labeled composer and
sort control, and an audit over the open pane on a comment-seeded
fixture (runnable only where automation permission exists).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
133 lines
7.1 KiB
Swift
133 lines
7.1 KiB
Swift
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")
|
|
}
|
|
}
|