The whole board-name area opens the popover — the chevron alone was the trigger

The titlebar widget grows from a 20×18 chevron into one button saying
the board's name and, on a git-mode Pro board, its branch — click
anywhere across it and the popover opens as before, anchored to the
widget. BoardInfoTitlebarSummary is the pure seam for both strings
(title falls back to the folder name per 01's naming rule; branch only
under pro + git mode, live off the observable HistoryStore.branch).
Board windows now hide the system title display through the same
hideTitle slot card windows adopted — the widget says the name, so the
chrome would only repeat it — while navigationTitle keeps feeding
window.title to the Window menu, Exposé, VoiceOver and restoration.

The widget also refreshes the branch eagerly at appearance: it used to
populate only once the popover had been opened, which would have left
the new branch line empty on a freshly opened board.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 08:28:53 -04:00
parent 6f2e0d15da
commit 94e60cd444
6 changed files with 253 additions and 34 deletions
+69
View File
@@ -77,3 +77,72 @@ struct BoardGitSectionTests {
#expect(resolved == Set(BoardGitSection.allCases))
}
}
/// **The window-title widget's two strings** (03-board-ui.md Board popover, the card that widened
/// the widget from a chevron to the whole board-name area): `BoardInfoTitlebarSummary` is the pure
/// function this pins, exactly as `BoardGitSectionTests` above pins the section it shares its
/// tier/mode inputs with. No disk I/O the title half of this seam takes a raw `URL`, not a
/// `BoardStore`, so a plain `/tmp/...` path is enough.
@Suite("Board popover ▸ the widget's strings")
struct BoardInfoTitlebarSummaryTests {
private let root = URL(fileURLWithPath: "/tmp/My Board.board")
@Test("The title is the on-disk title, falling back to the folder name sans extension")
func titleFallsBackToTheFolderName() {
let named = BoardInfoTitlebarSummary(
snapshotTitle: "Sprint 12", rootURL: root, tier: .free, mode: .none, branch: nil
)
#expect(named.title == "Sprint 12")
let untitled = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .free, mode: .none, branch: nil
)
#expect(untitled.title == "My Board")
let emptyTitled = BoardInfoTitlebarSummary(
snapshotTitle: "", rootURL: root, tier: .free, mode: .none, branch: nil
)
#expect(
emptyTitled.title == "My Board",
"an empty title reads the same as no title at all — the window-title rule this seam restates"
)
}
@Test("The branch shows under Pro, in git mode, once it has been read")
func branchShowsForProGitMode() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: "main"
)
#expect(summary.branch == "main")
}
@Test("The free tier never shows a branch, whatever the mode or the git state hands it")
func theFreeTierNeverShowsABranch() {
for mode in BoardGitMode.allCases {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .free, mode: mode, branch: "main"
)
#expect(summary.branch == nil, "detection never runs under the free tier, so a stray branch value must never surface")
}
}
@Test("An inert .git under Pro — mode none or repo-nested — never shows a branch")
func inertGitNeverShowsABranch() {
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .none, branch: "main").branch == nil
)
#expect(
BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .repoNested, branch: "main").branch
== nil
)
}
@Test("A git-mode board whose branch has not been read yet shows none, honestly")
func unreadBranchShowsNone() {
let summary = BoardInfoTitlebarSummary(
snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: nil
)
#expect(summary.branch == nil)
}
}
+39 -6
View File
@@ -972,14 +972,47 @@ struct BoardInfoAccessoryTests {
#expect(window.titlebarAccessoryViewControllers.count == 1)
controller.detach()
}
/// **The card that widened the widget from a chevron to the whole board-name area also hides the
/// title bar's own title on board windows** (`BoardWindowHost.configureWindow`'s two calls, in the
/// same order: install the widget, then `hideTitle()`) the widget now says the board's name, so
/// the system title display would only repeat it, exactly the card-window seam
/// `CardWindowTitleVisibilityTests` pins below. This test cannot drive `BoardWindowHost.body`
/// itself (it needs a live `AppModel` session), so it pins the two calls composed the same way, in
/// the same order, against a real window which is what would break silently if a later change
/// separated them or reordered them.
@Test("Installing the widget and hiding the title compose without one clobbering the other")
func hidesTitleAlongsideTheAccessory() throws {
let fixture = try makeBoardRoot(richBoardIndex)
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let (recents, teardown) = makeRecents()
defer { teardown() }
let window = makeWindow()
window.title = "My Board"
let controller = HostedWindowController()
controller.attach(to: window)
controller.installTitlebarAccessory(
boardInfoTitlebarAccessory(store: store, recents: recents, presentation: BoardInfoPresentation())
)
controller.hideTitle()
#expect(window.titlebarAccessoryViewControllers.count == 1)
#expect(window.titleVisibility == .hidden, "the widget says the name now; the chrome shouldn't say it twice")
#expect(window.title == "My Board", "the Window menu, Exposé and restoration still need the string")
}
}
// MARK: - Card window chrome hidden title
/// **Card windows show no title in the title bar** the card's name is shown as part of the card's
/// body instead (05-card-window.md Window: "card title is shown as part of card's body"), while the
/// toolbar renders exactly as it always has. `HostedWindowController.hideTitle()` is the seam
/// `CardWindowHost` reaches for; board windows call no such thing and keep AppKit's own default. Like
/// **Card and board windows show no title in the title bar** a card's name is shown as part of its
/// body instead (05-card-window.md Window: "card title is shown as part of card's body"), and a
/// board's is said by the board-popover widget in the titlebar instead (03-board-ui.md Board
/// popover) while the toolbar renders exactly as it always has on both. `HostedWindowController
/// .hideTitle()` is the seam both `CardWindowHost` and `BoardWindowHost` reach for; only the
/// restore-bootstrap window calls no such thing and keeps AppKit's own default. Like
/// `BoardInfoAccessoryTests` above, this cannot see what a user sees on screen only that the
/// mechanism this app owns (`NSWindow.titleVisibility`) lands correctly, survives the macOS 26
/// provisional-window swap (`HostedWindowController.detach`'s doc comment chrome held by the
@@ -1013,8 +1046,8 @@ struct CardWindowTitleVisibilityTests {
#expect(window.title == "Fix login", "the Window menu, Exposé and restoration still need the string")
}
@Test("A window nobody calls hideTitle() on keeps AppKit's default — the board window's posture")
func boardWindowsKeepTheDefault() {
@Test("A window nobody calls hideTitle() on keeps AppKit's default — the restore-bootstrap window's posture")
func unconfiguredWindowsKeepTheDefault() {
let window = makeWindow()
let controller = HostedWindowController()
controller.attach(to: window)