The board settings sheet — setup leaves the popover for a home a stray click can't dismiss

The popover/sheet split lands: a board-scoped, titled, sectioned sheet on
the board window hosts everything setup-shaped, opened from the popover's
Board Settings… row and the new Board ▸ Board Settings… menu row. The three
existing setup controls relocate — add-git (whose noteFormVisible lines now
make the sheet the form-anchored failure surface), branch creation (a
standing field; create-and-switch runs the identical settle sequence), and
the commit-identity fields (the 2s visibility-scoped poll rides with them).
The popover keeps the daily face and its postures; its Pro/mode-none
section becomes header + door (.addGit renamed .noRepository). Availability
is derived from the section inventory (Pro + mode none or git), so pro-m2's
sections can't drift from the doors; the sheet's fields join the
caret-chord disable set. The audit suite pins what the free fixture can
reach; the sheet's own audit is manual until a tier override is ruled.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 12:07:12 -04:00
parent 988a7245a3
commit 8345378972
12 changed files with 1168 additions and 321 deletions
+61 -6
View File
@@ -87,13 +87,17 @@ extension BoardStore {
/// > enabled menu key equivalent fires before the field ever sees the key, and / are the
/// > standard line-start/end caret chords.
///
/// Four text surfaces, answered four ways, and only two of them are here:
/// Five text surfaces, answered four ways, and only three of them are here:
///
/// - **Inline title editors** are `acceptsBoardMutations`', through the focused-editor rule a
/// broader lockdown that already covers these two items.
/// - **The board popover's fields** are covered by disabling while the popover is open at all
/// coarser than per-field focus, but it is a configuration surface (04's carve-out) and no lane
/// move belongs under it.
/// - **The board settings sheet's fields** are the popover's rule for the popover's reason, and the
/// surface the 2026-07-31 split moved most of those fields *to* (branch name, commit identity, and
/// pro-m2's remote URL and credentials): the sheet is the other half of 04's configuration
/// carve-out, and a lane move under a modal settings surface is not a gesture that exists.
/// - **The search field** is per-focus and exact (`BoardSearchPresentation.isFocused`), which it has
/// to be: the field's own rule is that board commands *stay enabled* while it holds the keyboard
/// (04 § Search), so these two are the narrow exception to it and nothing coarser would do.
@@ -103,8 +107,14 @@ extension BoardStore {
/// Stated once because the two command groups must not drift: a rule with two implementations is a
/// rule with two chances to forget a surface.
@MainActor
func caretChordsYield(boardInfo: BoardInfoPresentation?, search: BoardSearchPresentation?) -> Bool {
boardInfo?.isPresented == true || search?.isFocused == true
func caretChordsYield(
boardInfo: BoardInfoPresentation?,
boardSettings: BoardSettingsPresentation?,
search: BoardSearchPresentation?
) -> Bool {
boardInfo?.isPresented == true
|| boardSettings?.isPresented == true
|| search?.isFocused == true
}
// MARK: - Open Card
@@ -276,11 +286,13 @@ struct MoveCardCommands: View {
/// **Caret chords yield to any focused text control** (04-interactions.md Grammar, settled):
/// / are the standard line-start/end chords, and an enabled key equivalent fires before a
/// field ever sees the key. Which surfaces that covers, and how each is answered, is
/// `caretChordsYield(boardInfo:search:)`'s doc comment shared verbatim with the width pair below.
/// `caretChordsYield(boardInfo:boardSettings:search:)`'s doc comment shared verbatim with the
/// width pair below.
struct MoveLaneCommands: View {
@FocusedValue(\.boardStore) private var store
@FocusedValue(\.boardInfo) private var boardInfo
@FocusedValue(\.boardSettings) private var boardSettings
@FocusedValue(\.boardSearch) private var search
var body: some View {
@@ -298,7 +310,7 @@ struct MoveLaneCommands: View {
}
private var yieldsCaretChords: Bool {
caretChordsYield(boardInfo: boardInfo, search: search)
caretChordsYield(boardInfo: boardInfo, boardSettings: boardSettings, search: search)
}
/// The sole selected live lane and the display slot one step would put it in `nil` when there
@@ -415,6 +427,48 @@ struct BoardInfoCommand: View {
}
}
// MARK: - Board Settings
/// Board Board Settings no default chord (11-command-nexus.md: " (no default)"), and the board
/// settings sheet's second door, the popover's row being the first (03-board-ui.md Board settings
/// sheet).
///
/// **"Remappable" asks nothing of this file.** The remapping mechanism is macOS's own System
/// Settings Keyboard App Shortcuts, keyed on the menu item's *title* (04-interactions.md
/// Configurable bindings) so all a chordless row owes it is a stable title, which the Nexus fixes.
/// A `Button` with no `keyboardShortcut` is therefore the whole implementation, exactly as File
/// Save as Template and Board Rename are.
///
/// ### It opens; it never toggles
///
/// Board Info I toggles because a popover reached by a chord would otherwise have no keyboard way
/// out. A sheet has one built in (Done, and Escape through it), and the menu is behind the sheet
/// while it is up so a toggling row would be a second exit nobody can reach.
///
/// ### Validation: scope, then reachability and never the lock
///
/// The row stays **visible and disabled** where the sheet cannot exist (`BoardSettingsAvailability`,
/// which carries the reasoning): the free tier, and a Pro board nested inside someone else's
/// repository. That is standard menu validation, and it is the deliberate asymmetry with the popover
/// row, which is *absent* there instead a menu is an inventory of the app, a popover section is a
/// description of this board.
///
/// The read-only lock does not close it, for Board Info's reason: a settings sheet is *configuration*
/// (04-interactions.md The map's carve-out), a locked board is exactly when a user may want to read
/// its git setup, and the controls inside disable themselves in place (03 Board settings sheet).
struct BoardSettingsCommand: View {
@FocusedValue(\.boardStore) private var store
@FocusedValue(\.boardSettings) private var presentation
var body: some View {
Button("Board Settings…") {
presentation?.present()
}
.disabled(store == nil || presentation?.isReachable != true)
}
}
// MARK: - Rename
/// Board Rename no default chord, deliberately (11-command-nexus.md: " (cards: Return in
@@ -491,6 +545,7 @@ struct LaneWidthCommands: View {
@FocusedValue(\.boardStore) private var store
@FocusedValue(\.boardInfo) private var boardInfo
@FocusedValue(\.boardSettings) private var boardSettings
@FocusedValue(\.boardSearch) private var search
var body: some View {
@@ -508,7 +563,7 @@ struct LaneWidthCommands: View {
}
private var yieldsCaretChords: Bool {
caretChordsYield(boardInfo: boardInfo, search: search)
caretChordsYield(boardInfo: boardInfo, boardSettings: boardSettings, search: search)
}
/// The selected live lanes, in snapshot order the batch, and the items' validation.