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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user