The popover/sheet split reverses — settings fold into the Git tab, the widget stacks name over branch

The titlebar widget becomes a two-line identity block: the board glyph at
22pt spanning both lines, the title over the branch (git-mode only, smaller
and secondary), the em-dash retired. New Branch… returns to the switch menu
behind a divider, revealing an inline name field — the pre-split shape. The
board settings sheet retires whole: add-git and commit identity render
inline in the Git tab's postures (BoardGitSetup.swift), the availability
rule collapses into BoardGitSetupSection.resolve, and Board ▸ Board
Settings… leaves the menu bar. Where 07's remote/credential setup surfaces
land is deliberately left open — filed on the Redesign board.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-07 21:48:02 -04:00
parent 99ebb69a1d
commit 7414fc8400
22 changed files with 884 additions and 1259 deletions
+73 -46
View File
@@ -10,8 +10,10 @@ import SwiftUI
/// design session: `BoardInfoTabView`, the metrics dossier; `BoardThemeTabView`, the Solid color /
/// Pattern picker (the Background tab's original name, before the same session widened it past the
/// generated-only picker and folded manual styling back out to Style S); and `BoardGitTabView`,
/// which the pre-tab body's mode-aware git section rehomed into whole postures, notes, and the
/// Board Settings row, none of them re-ruled by the move.
/// which the pre-tab body's mode-aware git section rehomed into whole postures and notes, none of
/// them re-ruled by the move. That tab carried a **Board Settings** row to a separate sheet until
/// later the same day, when the 2026-07-31 popover/sheet split was reversed and the sheet's contents
/// rehomed into the tab (see below).
///
/// **Tab membership is the git posture's**, and **selection resets to Info on every open** both
/// the Git session's rulings. Since 12-editions.md PIVOT 2026-08-07 the first of those is a
@@ -24,6 +26,14 @@ import SwiftUI
/// and a second entry would muddy it"). It has exactly two ways in: the widget, and File Board
/// Info I, which is the same widget's popover reached from the keyboard (11-command-nexus.md's
/// class **C** "the keyboard path is reachability not bindings").
///
/// **And it is the board's only configuration home** (ruled 2026-08-07, reversing the 2026-07-31
/// popover/sheet split): setup briefly lived in a board settings sheet with its own menu command and
/// a row here pointing at it. The sheet is retired, its add-git and commit-identity controls render
/// inline in the Git tab's postures (`BoardGitSetup.swift`), branch creation went back into the
/// switch menu (`BoardGitControls`), and Board Board Settings left the menu bar with them. So
/// "one home per control" the split's own promise is now satisfied by there being one surface,
/// and I is the door to all of it.
// MARK: - Presentation state
@@ -64,8 +74,17 @@ extension FocusedValues {
// MARK: - The window-title widget
/// The titlebar widget: the board's name and, on a git-mode board, its branch with a trailing
/// disclosure chevron, whose one job is this popover.
/// The titlebar widget: the board's glyph beside a two-line identity block its name, and on a
/// git-mode board its branch under the name with a trailing disclosure chevron, whose one job is
/// this popover.
///
/// **A two-line stack since 2026-08-07** (03-board-ui.md Board popover, the window-title widget
/// passage). It was one line reading `glyph Title branch `, and the em-dash was the tell: a
/// separator doing a *hierarchy's* job, with the branch competing for the same width as the name it
/// qualifies. So the branch moved under the title in its own smaller, secondary line, the em-dash
/// retired, and the glyph grew to span both lines an icon sized to the block it labels rather than
/// to whichever line it happened to sit on. A board with no branch is the single title line, vertically
/// centred beside the same glyph, which is the same block with one row.
///
/// **The popover is anchored to the widget itself** it hangs from the button rather than from the
/// window or the board which is what makes the affordance and the surface read as one thing. A
@@ -94,12 +113,6 @@ struct BoardInfoWidget: View {
@Bindable var presentation: BoardInfoPresentation
/// The window's settings sheet, so the popover's Git tab can carry the **Board Settings**
/// row that opens it (03-board-ui.md Board popover: "A Board Settings row opens the sheet
/// the popover's one setup affordance"). `nil` where there is no window to present a sheet on,
/// which is the accessory-installation tests' shape and reads as a popover with no row.
let settings: BoardSettingsPresentation?
/// The widget's two strings, computed fresh on every body evaluation rather than cached anywhere.
/// That matters here specifically: `boardInfoTitlebarAccessory` builds this view exactly **once**
/// at install, so a value read anywhere but inside `body` would freeze at the widget's birth and
@@ -118,39 +131,54 @@ struct BoardInfoWidget: View {
Button {
presentation.toggle()
} label: {
HStack(spacing: 4) {
HStack(spacing: 6) {
// The board's own glyph beside its name the identity pair the popover header
// states, restated where the board is named all day (2026-08-07). Read inside
// `body` for `summary`'s reason: `icon`/`iconColor` are `@Observable` fields, so a
// restyle from the popover repaints this widget without reinstalling it. Lenient on
// both dimensions an unresolvable glyph draws the board default, an unresolvable
// tint draws the standard secondary.
//
// **Its own font, not the container's** (the two-line rework, 2026-08-07): at 22pt
// it draws about twice the height `.imageScale(.small)` gave it on the container's
// 13pt, which is what lets one glyph span the title and branch lines instead of
// sitting beside the upper one. The number is the icon's own size rather than a
// scale factor because that is the dimension being chosen the block's height.
Image(systemName: ItemSymbol.name(store.snapshot.icon, fallback: ItemSymbol.board))
.imageScale(.small)
.font(.system(size: 22))
.foregroundStyle(iconTint)
// Decorative beside the name it repeats the button's own label already says
// everything VoiceOver needs (`accessibilityLabel` below).
.accessibilityHidden(true)
Text(summary.title)
// Styled like a titlebar title, because that is what it now stands in for
// (`BoardWindowHost` hides the system title display in favor of this widget).
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.primary)
.lineLimit(1)
.truncationMode(.tail)
// Yields space to the branch string and chevron first when the two don't both
// fit inside the width cap below the board's own name is the more load-bearing
// half of the pair.
.layoutPriority(1)
if let branch = summary.branch {
Text("")
.foregroundStyle(.secondary)
Text(branch)
.foregroundStyle(.secondary)
// The identity block: the name, and the branch beneath it on a git-mode board. On
// any other board this is the single title line and the `HStack`'s own centring puts
// it level with the glyph the stack is the same shape with one row, never a
// special case.
VStack(alignment: .leading, spacing: 1) {
Text(summary.title)
// Styled like a titlebar title, because that is what it now stands in for
// (`BoardWindowHost` hides the system title display in favor of this widget).
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.primary)
.lineLimit(1)
.truncationMode(.tail)
if let branch = summary.branch {
// Smaller and secondary the qualifier under the name it qualifies. The
// em-dash that separated the two on one line retired with the stack: a
// hierarchy that a layout can state does not need punctuation to state it.
Text(branch)
.font(.system(size: 11))
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
}
// Yields space to the chevron first when the block doesn't fit inside the width cap
// below the board's identity is the more load-bearing half of the pair, and both
// its lines truncate rather than the disclosure disappearing.
.layoutPriority(1)
Image(systemName: "chevron.down")
.imageScale(.small)
@@ -159,10 +187,12 @@ struct BoardInfoWidget: View {
}
.font(.system(size: 13))
// A long board name (or branch) must not swallow the whole titlebar capped rather
// than left to grow, with the truncation above doing the rest. Height stays the
// original chevron's, which is what keeps the accessory titlebar-appropriate.
// than left to grow, with the truncation above doing the rest. The height is the
// two-line block's (2026-08-07; it was the original chevron's 18 while the widget was
// one line), which is what keeps the accessory titlebar-appropriate: tall enough for
// name-over-branch, and no taller than a standard title bar carries.
.frame(maxWidth: 400, alignment: .leading)
.frame(height: 18)
.frame(height: 32)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
@@ -176,7 +206,7 @@ struct BoardInfoWidget: View {
// costs nothing on the other four postures.
.task { await git?.refreshBranch() }
.popover(isPresented: $presentation.isPresented, arrowEdge: .bottom) {
BoardInfoView(store: store, recents: recents, git: git, settings: settings)
BoardInfoView(store: store, recents: recents, git: git)
}
}
@@ -246,24 +276,22 @@ struct BoardInfoTitlebarSummary: Equatable {
/// window, removed on detach for the same reason it owns the delegate proxying: the window is
/// SwiftUI's, and anything hung on it has to be taken back off.
@MainActor
/// `git` defaults to no session and `settings` to no sheet, so that a caller with none in hand (the
/// accessory-installation tests, which are about AppKit plumbing rather than about git) describes a
/// board honestly rather than by accident: a Git tab in its no-repository posture, and no
/// Board Settings row behind it. The app's own call site passes the session's values explicitly.
/// `git` defaults to no session, so that a caller with none in hand (the accessory-installation
/// tests, which are about AppKit plumbing rather than about git) describes a board honestly rather
/// than by accident: a Git tab in its no-repository posture, and a widget with no branch line. The
/// app's own call site passes the session's value explicitly.
func boardInfoTitlebarAccessory(
store: BoardStore,
recents: StyleRecents,
git: HistoryStore? = nil,
presentation: BoardInfoPresentation,
settings: BoardSettingsPresentation? = nil
presentation: BoardInfoPresentation
) -> NSTitlebarAccessoryViewController {
let hosting = NSHostingView(
rootView: BoardInfoWidget(
store: store,
recents: recents,
git: git,
presentation: presentation,
settings: settings
presentation: presentation
)
)
// The titlebar lays its accessories out by fitting size, and a hosting view that measured itself
@@ -272,8 +300,10 @@ func boardInfoTitlebarAccessory(
// widget's real content replaces it but that first pass is exactly what a 20×18 placeholder
// (the old chevron-only width) would clamp now that the widget's content can run out to 400pt:
// wide enough that the widest realistic first paint is never visibly clipped before the resize.
// The height is the widget's own two-line figure (2026-08-07), for the same reason the width is
// generous a first pass clamped to the old one-line 18 would paint a clipped block.
hosting.sizingOptions = [.intrinsicContentSize]
hosting.frame = NSRect(x: 0, y: 0, width: 200, height: 18)
hosting.frame = NSRect(x: 0, y: 0, width: 200, height: 32)
let controller = NSTitlebarAccessoryViewController()
controller.view = hosting
@@ -321,7 +351,6 @@ struct BoardInfoView: View {
let store: BoardStore
let recents: StyleRecents
let git: HistoryStore?
let settings: BoardSettingsPresentation?
/// The selected tab, and **it resets to Info on every open** a ruling, not an accident (the
/// Git session, 2026-08-07, closing the question the earlier tab sessions deferred): the popover
@@ -341,13 +370,11 @@ struct BoardInfoView: View {
init(
store: BoardStore,
recents: StyleRecents,
git: HistoryStore? = nil,
settings: BoardSettingsPresentation? = nil
git: HistoryStore? = nil
) {
self.store = store
self.recents = recents
self.git = git
self.settings = settings
}
var body: some View {
@@ -416,7 +443,7 @@ struct BoardInfoView: View {
case .theme:
BoardThemeTabView(store: store, inset: inset)
case .git:
BoardGitTabView(store: store, git: git, settings: settings, inset: inset)
BoardGitTabView(store: store, git: git, inset: inset)
}
}
// The style editor's popover width, taken from the editor rather than restated the number