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
+68 -24
View File
@@ -1,15 +1,16 @@
import Testing
@testable import Kanban
/// `caretChordsYield(boardInfo:search:)` 04-interactions.md Grammar's caret-chords rule as one
/// expression, and `BoardCommands.swift`'s single seam for it: Board Move Left/Move Right / and
/// the lane-width pair / disable via menu validation whenever *any* text control has keyboard
/// focus, because / are the standard line-start/end caret chords and an enabled key equivalent
/// fires before a field ever sees the key.
/// `caretChordsYield(boardInfo:boardSettings:search:)` 04-interactions.md Grammar's caret-chords
/// rule as one expression, and `BoardCommands.swift`'s single seam for it: Board Move Left/Move
/// Right / and the lane-width pair / disable via menu validation whenever *any* text
/// control has keyboard focus, because / are the standard line-start/end caret chords and an
/// enabled key equivalent fires before a field ever sees the key.
///
/// The function reads exactly two flags and nothing else, so every test here constructs
/// `BoardInfoPresentation` and `BoardSearchPresentation` directly rather than through a `BoardStore`
/// a fixture that stood up a board would be exercising machinery this seam never touches.
/// The function reads exactly three flags and nothing else, so every test here constructs
/// `BoardInfoPresentation`, `BoardSettingsPresentation` and `BoardSearchPresentation` directly rather
/// than through a `BoardStore` a fixture that stood up a board would be exercising machinery this
/// seam never touches.
@MainActor
@Suite("caretChordsYield ▸ the caret-chords rule")
struct CaretChordTests {
@@ -21,7 +22,7 @@ struct CaretChordTests {
// "Card-window fields need nothing: those windows never publish a boardStore, so both items
// are already scopeless there" (caretChordsYield's doc comment) nil/nil is that window's
// steady state, not a corner case.
#expect(caretChordsYield(boardInfo: nil, search: nil) == false)
#expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: nil) == false)
}
// MARK: The board popover, alone
@@ -29,13 +30,46 @@ struct CaretChordTests {
@Test("The board popover open yields; closed, it does not")
func popoverPresence() {
let boardInfo = BoardInfoPresentation()
#expect(caretChordsYield(boardInfo: boardInfo, search: nil) == false, "closed by default")
#expect(caretChordsYield(boardInfo: boardInfo, boardSettings: nil, search: nil) == false, "closed by default")
boardInfo.isPresented = true
#expect(caretChordsYield(boardInfo: boardInfo, search: nil) == true)
#expect(caretChordsYield(boardInfo: boardInfo, boardSettings: nil, search: nil) == true)
boardInfo.isPresented = false
#expect(caretChordsYield(boardInfo: boardInfo, search: nil) == false, "closing re-enables the chords")
#expect(caretChordsYield(boardInfo: boardInfo, boardSettings: nil, search: nil) == false, "closing re-enables the chords")
}
// MARK: The settings sheet, alone
@Test("The settings sheet open yields; closed, it does not")
func settingsSheetPresence() {
// The sheet joined this seam with the 2026-07-31 popover/sheet split: it is where the branch
// name field and the commit-identity fields live now, so the surface that used to be covered
// by "the popover is open" has to be covered by "the sheet is up" as well.
let settings = BoardSettingsPresentation()
#expect(caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == false, "closed by default")
settings.isPresented = true
#expect(caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == true)
settings.isPresented = false
#expect(
caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == false,
"dismissing re-enables the chords"
)
}
@Test("The sheet's own reachability is not this seam's question — a presented sheet yields either way")
func presentationRatherThanReachability() {
// `isReachable` gates the two *doors* (`BoardSettingsAvailability`); this function reads the
// flag that says a sheet is on screen. A presentation left unadopted reads free-tier and
// therefore unreachable, and if something ever put its flag up anyway the chords must still
// yield what a caret chord competes with is a field that exists, not a tier.
let settings = BoardSettingsPresentation()
#expect(settings.isReachable == false, "unadopted reads as the free tier")
settings.isPresented = true
#expect(caretChordsYield(boardInfo: nil, boardSettings: settings, search: nil) == true)
}
// MARK: The search field, alone
@@ -43,13 +77,13 @@ struct CaretChordTests {
@Test("The search field focused yields; unfocused, it does not")
func searchFocus() {
let search = BoardSearchPresentation()
#expect(caretChordsYield(boardInfo: nil, search: search) == false, "unfocused by default")
#expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: search) == false, "unfocused by default")
search.isFocused = true
#expect(caretChordsYield(boardInfo: nil, search: search) == true)
#expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: search) == true)
search.isFocused = false
#expect(caretChordsYield(boardInfo: nil, search: search) == false, "losing focus re-enables the chords")
#expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: search) == false, "losing focus re-enables the chords")
}
// MARK: Both surfaces together
@@ -64,22 +98,32 @@ struct CaretChordTests {
focusedSearch.isFocused = true
// One side published and inert, the other absent (the still-loading-window shape): false.
#expect(caretChordsYield(boardInfo: closedInfo, search: nil) == false)
#expect(caretChordsYield(boardInfo: nil, search: unfocusedSearch) == false)
#expect(caretChordsYield(boardInfo: closedInfo, boardSettings: nil, search: nil) == false)
#expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: unfocusedSearch) == false)
// One side published and active, the other absent: true.
#expect(caretChordsYield(boardInfo: openInfo, search: nil) == true)
#expect(caretChordsYield(boardInfo: nil, search: focusedSearch) == true)
#expect(caretChordsYield(boardInfo: openInfo, boardSettings: nil, search: nil) == true)
#expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: focusedSearch) == true)
// Both published, both inert: false the popover being open at all and the field holding
// focus are each read independently, so neither's mere presence counts on its own.
#expect(caretChordsYield(boardInfo: closedInfo, search: unfocusedSearch) == false)
#expect(caretChordsYield(boardInfo: closedInfo, boardSettings: nil, search: unfocusedSearch) == false)
// Both published, one or both active: true. This is an `||`, not an `&&` one live text
// surface is enough to send the chords to it, whatever the other surface is doing.
#expect(caretChordsYield(boardInfo: openInfo, search: unfocusedSearch) == true)
#expect(caretChordsYield(boardInfo: closedInfo, search: focusedSearch) == true)
#expect(caretChordsYield(boardInfo: openInfo, search: focusedSearch) == true)
#expect(caretChordsYield(boardInfo: openInfo, boardSettings: nil, search: unfocusedSearch) == true)
#expect(caretChordsYield(boardInfo: closedInfo, boardSettings: nil, search: focusedSearch) == true)
#expect(caretChordsYield(boardInfo: openInfo, boardSettings: nil, search: focusedSearch) == true)
// And the third surface composes the same way inert beside two inert siblings, sufficient
// beside them when it is up.
let closedSheet = BoardSettingsPresentation()
let openSheet = BoardSettingsPresentation()
openSheet.isPresented = true
#expect(caretChordsYield(boardInfo: closedInfo, boardSettings: closedSheet, search: unfocusedSearch) == false)
#expect(caretChordsYield(boardInfo: closedInfo, boardSettings: openSheet, search: unfocusedSearch) == true)
#expect(caretChordsYield(boardInfo: openInfo, boardSettings: openSheet, search: focusedSearch) == true)
}
// MARK: Inline editors are a different seam
@@ -98,6 +142,6 @@ struct CaretChordTests {
// No board popover, no search focus caretChordsYield answers false regardless of the open
// editor above, because it never reads isEditingInline at all.
#expect(caretChordsYield(boardInfo: nil, search: nil) == false)
#expect(caretChordsYield(boardInfo: nil, boardSettings: nil, search: nil) == false)
}
}