The editing surface: the same hosted TextKit-1 text view gains an editable branch with a per-keystroke line-scanner highlighter — chosen over a parser re-parse because a mid-typing buffer is usually invalid Markdown and 05 wants the delimiters themselves dimmed; apply only sets attributes, so presentation-never-transforms is structural. Saves ride a ~700ms injectable debounce through BoardWriter.writeBody — toggleTaskMarker's idiom widened to the body span, frontmatter bytes untouched, refusing to write when disk already holds that body, which enforces all three gates (untouched, reverted, echo) at the layer that owns the bytes with one isDirty predicate above it. Mode grammar lands whole: ⌘E toggles with a checkmark, Return in Preview enters, Escape returns, and every flip flushes first; window close flushes through the existing retry/save-copy/discard modal, and the dismissal flush deliberately reaches a tombstoned card. Dirty-buffer-wins: disk always follows the snapshot, the buffer only when clean, both surfaces render the buffer. Undo is the editor's own session-scoped NSUndoManager; endEditSession names the pro-m1 one-commit-per-session boundary. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
145 lines
7.0 KiB
Swift
145 lines
7.0 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` (a `Button`) and
|
|
/// `FutureToggleCommand` (a `Toggle`) below are 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 toggle 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).
|
|
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)
|
|
}
|
|
}
|
|
|
|
/// `FutureCommand`'s checkmark-state twin, for a row the Nexus already marks "(checkmark toggle)".
|
|
///
|
|
/// `isOn` is a constant `false` rather than real state: there is no session yet for a binding to
|
|
/// read, which is exactly the disabled, unchecked state a not-yet-wired toggle should show.
|
|
struct FutureToggleCommand: View {
|
|
let title: String
|
|
var key: KeyEquivalent?
|
|
var modifiers: EventModifiers = .command
|
|
|
|
var body: some View {
|
|
Toggle(title, isOn: .constant(false))
|
|
.keyboardShortcut(key.map { KeyboardShortcut($0, modifiers: modifiers) })
|
|
.disabled(true)
|
|
}
|
|
}
|
|
|
|
// MARK: - File ▸ Save as Template
|
|
|
|
/// File ▸ Save as Template — no default chord (11-command-nexus.md; 09-templates.md).
|
|
///
|
|
// m9-templates: copies the open board into the user templates store, close-flushed first exactly as
|
|
// Duplicate is (09 ▸ Save as Template: "The copy is preceded by the close flush"), `.git` stripped,
|
|
// tombstones dropped, a `template:` key stamped. Validation will be `acceptsBoardMutations` plus 09's
|
|
// one carve-out from the read-only lock — live under the unwritable-location state unless an open
|
|
// Edit/raw-source session holds unsaved content — so it cannot simply borrow
|
|
// `DuplicateBoardCommand`'s predicate outright.
|
|
struct SaveAsTemplateCommand: View {
|
|
var body: some View {
|
|
FutureCommand(title: "Save as Template")
|
|
}
|
|
}
|
|
|
|
// MARK: - File ▸ Add Attachment…
|
|
|
|
/// File ▸ Add Attachment… (⇧⌘A) — card window only (11-command-nexus.md).
|
|
///
|
|
// m6-card-window: the menu-bar twin of the attachments section's quiet add affordance
|
|
// (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])
|
|
}
|
|
}
|
|
|
|
// 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).
|
|
///
|
|
// m6-card-window: joins `FindCommand` in the Edit menu once the card window's find-in-text exists
|
|
// (05-card-window.md). Both rows are unconditionally disabled here rather than reading `boardStore`
|
|
// to prove "board window" disables them: there is no card-window find session anywhere yet for
|
|
// either validation branch to check.
|
|
struct FindSteppingCommands: View {
|
|
var body: some View {
|
|
FutureCommand(title: "Find Next", key: "g", modifiers: .command)
|
|
FutureCommand(title: "Find Previous", key: "g", modifiers: [.shift, .command])
|
|
}
|
|
}
|
|
|
|
// 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 is live** (`EditBodyCommand`, beside the focused value it reads): the body column's
|
|
/// Preview/Edit toggle, checkmark state and all. Its diff was the one `FutureCommand` promises —
|
|
/// the title and the chord did not move, the validation and the action filled in.
|
|
///
|
|
// m6-raw-source: Raw Source is the same shape one card later — a checkmark toggle over a
|
|
// window-level mode, which also adds the "Edit Body disables while Raw Source is active" clause to
|
|
// the row above (05-card-window.md).
|
|
//
|
|
// m6-card-sidebar: 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). Both remain unconditionally disabled here — neither surface exists yet.
|
|
struct CardViewCommands: View {
|
|
var body: some View {
|
|
EditBodyCommand()
|
|
FutureToggleCommand(title: "Raw Source", key: "e", modifiers: [.option, .command])
|
|
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")
|
|
}
|
|
}
|