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