Implement the keyboard grammar and full command map

The board's fixed grammar keys and the menu-backed chords of
04-interactions.md § Keyboard, per the Command Nexus inventory:

- Spatial arrow navigation (NavigationMath.nearest over the marquee
  registry's frames — one geometry source), walking across interior
  masonry columns, lanes, and into the shown trash; ⇧-arrows extend via
  the same range function as ⇧-click and go inert at the liveness and
  kind boundaries; ⌥-jumps with the ⌥↑ lane-domain escalation and ↓
  descent; the empty selection seeds at the first lane's first card;
  selection scrolls into view.
- selectionHead — the navigation cursor beside the anchor, set by every
  click, moved by every arrow, dropped by the reload vanish rule.
- Board ▸ Open Card ⌘↩ (the one command enabled mid-edit: commits the
  placeholder or rename and opens), Move Up/Move Down ⌥⌘↑/⌥⌘↓
  (within-lane sort, gather-then-step, rank-permuting writes in one
  bracket), Move Left/Move Right ⌘←/⌘→ (sole lane, one slot, never the
  trash) — all validating and acting off one shared answer.
- Delete now selects the Finder-style successor sibling from the
  pre-write snapshot, so repeated ⌫ walks down a lane; external
  vanishing still only shrinks the selection.
- handleReturn rejects modified Returns; the trash column renders
  eagerly so every row stays registered for navigation and the marquee.

686 unit tests (27 new).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 19:32:30 -04:00
parent 2e229735b1
commit 4035ba7986
13 changed files with 1582 additions and 75 deletions
+24 -7
View File
@@ -44,6 +44,11 @@ struct BoardWindowHost: View {
/// menu-bar items, and a menu item cannot present anything of its own.
@State private var trashConfirmations = TrashConfirmations()
/// How Board Open Card reaches this window's card windows. `@State` for `boardInfo`'s reason,
/// and published the same way: a menu item has no window of its own, and only this view holds
/// the board half of a card window's `(board, card)` identity see `CardOpener`.
@State private var cardOpener = CardOpener()
@State private var phase: Phase = .opening
private enum Phase {
@@ -78,17 +83,12 @@ struct BoardWindowHost: View {
// attaches after this body first runs, and the lane-resize drag needs the *live*
// window to grow at its right edge (03-board-ui.md § Lane).
//
// `openCard` is the host's too, for a different reason: a card window's identity is
// `(board, card)` and only this view holds the board half. `openWindow(value:)` with
// a ref that already has a window focuses it, so "at most one card window per card
// (reopen focuses)" needs no bookkeeping here (02-architecture.md § Windows).
// `openCard` is the host's too, for a different reason see the property below.
BoardView(
store: store,
window: { windowController.window },
confirmations: trashConfirmations,
openCard: { cardID in
openWindow(id: WindowID.card, value: CardWindowRef(board: ref, cardID: cardID))
}
openCard: openCard
)
}
// "The board in front", for the menu items that act on it (`LaneWidthCommands`), and
@@ -100,6 +100,18 @@ struct BoardWindowHost: View {
.focusedSceneValue(\.boardWindowRef, ref)
.focusedSceneValue(\.boardInfo, boardInfo)
.focusedSceneValue(\.trashConfirmations, trashConfirmations)
// Board Open Card's second half the same closure `BoardView` gets, so the menu item
// and the double-click open one window per card by construction.
.focusedSceneValue(\.cardOpener, cardOpener)
}
}
/// Opens a card's window. `openWindow(value:)` with a ref that already has a window focuses it,
/// so "at most one card window per card (reopen focuses)" needs no bookkeeping here
/// (02-architecture.md § Windows).
private var openCard: (ItemID) -> Void {
{ cardID in
openWindow(id: WindowID.card, value: CardWindowRef(board: ref, cardID: cardID))
}
}
@@ -152,6 +164,11 @@ struct BoardWindowHost: View {
/// Wires the window: the saved frame on the way in, frame changes on the way back out, the
/// close interception that makes the flush unavoidable, and the title-bar widget.
private func configureWindow(store: BoardStore, recordID: UUID) {
// Filled in here rather than at declaration because the closure captures `openWindow`, an
// environment action; until the board has loaded there is also nothing for Open Card to act
// on, which is exactly what the item's `nil` check reads.
cardOpener.open = openCard
windowController.onAttach = { window in
guard let saved = appModel.boardRegistry.record(id: recordID)?.windowFrame else { return }
window.setFrame(HostedWindowController.placementOnCurrentScreens(for: saved), display: true)