Files
lanework/Kanban/App/FutureCommands.swift
T
rzen 40c0a75c24 Build the Raw Source outlet
The escape hatch: View > Raw Source (opt-cmd-E) unmounts the whole
content area for the literal on-disk index.md in a plain monospaced
editor with Cancel/Apply. Raw source is window-level state, not a third
body mode — entry rides setMode(.preview), which flushes the Edit
session by construction, then reads the file fresh; exit reveals
Preview, and an empty body after Apply does not reopen Edit (openIfNeeded
already ran). Apply validates the proposed bytes through the loader's
own card checks — parseDocument's strict UTF-8/BOM rejection, schema,
order — deliberately skipping the uneditable-shape refusal, since a
flow-mapping card is exactly what the hatch repairs; invalid bytes
alert in place with the loader's own error and no bracket opens. The
write is byte-for-byte with no modified stamp and no modified-by clear,
per 01's explicit carve-out — the verbatim contract outranks stamping —
and identical bytes write nothing. Escape cancels, cmd-Return applies,
toggle-off applies too, and cmd-E disables while raw is active via a
testable predicate. Tombstoned targets refuse as vanished: a foreign
delete is never reverted by a stale buffer.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-28 11:25:25 -04:00

131 lines
6.6 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)
}
}
// 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 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).
///
// 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). It remains unconditionally disabled here — that surface does not exist yet.
struct CardViewCommands: View {
var body: some View {
EditBodyCommand()
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")
}
}