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
+177 -63
View File
@@ -2,11 +2,21 @@ import Foundation
import SwiftUI
/// **The board popover's Git tab** (03-board-ui.md § Board popover Git tab, settled 2026-08-07)
/// the pre-tab closing git section rehomed *whole*, and nothing more: **the daily face, and only
/// that** (the 2026-07-31 popover/sheet split stands, unchanged by the move). A repository-facts
/// dossier in the Info tab's register commit counts, last-commit dates was considered in the Git
/// session and declined: the popover's git surface is for *operating*, and per-item history is the
/// card window's History section (05-card-window.md).
/// the pre-tab closing git section rehomed *whole*, and then, later the same day, **the board
/// settings sheet's contents rehomed into it too**. A repository-facts dossier in the Info tab's
/// register commit counts, last-commit dates was considered in the Git session and declined: the
/// popover's git surface is for *operating* and *setting up*, and per-item history is the card
/// window's History section (05-card-window.md).
///
/// ### Operating and setup, one surface again (ruled 2026-08-07)
///
/// The tab settled as "the daily face, and only that" the 2026-07-31 popover/sheet split's half
/// with a **Board Settings** row pointing at the sheet that held add-git, branch creation and the
/// commit identity. That split is **reversed**: the sheet retires, the row with it, and the two setup
/// controls render inline in the postures they belong to (`BoardGitSetup.swift`). Branch creation
/// went back where it came from, the switch menu (`BoardGitControls.branchRow`). What is left is one
/// configuration home per board the popover reached by the widget or Board Info I, and no
/// surface that has to be validated into existence before a door can point at it.
///
/// ### No "Git" header anywhere in this tab
///
@@ -35,12 +45,14 @@ import SwiftUI
/// Rules). `unverifiable` (the git-detection axis) is structurally identical to `repoNested` but
/// worded as its own honest prose a denial is not a nesting.
///
/// **The 2026-07-31 popover/sheet split thinned two of these cases without removing either.** Setup
/// left the popover for the board settings sheet, so mode `none` no longer renders an action here at
/// all (the case was called `.addGit` when it did a name that would now be describing a control
/// that lives in another file, so it is `.noRepository`), and the git-mode case lost branch creation
/// and the identity fields. What each case still *is* is a posture, which is why the matrix and its
/// test survived the move unchanged.
/// **The 2026-07-31 popover/sheet split thinned two of these cases without removing either, and the
/// 2026-08-07 reversal filled them back in.** Setup left the popover for the board settings sheet,
/// so mode `none` rendered no action here at all for a week (the case was called `.addGit` when it
/// did, and was renamed `.noRepository` when the control left) and the git-mode case lost branch
/// creation and the identity fields; the reversal retired that sheet and brought all three back
/// inline. The case name stays `.noRepository` it describes the board, which is what a posture is
/// for, and it survived the round trip precisely because it never named a control. What each case
/// *is* is a posture, which is why the matrix and its test survived both moves unchanged.
///
/// **The 2026-08-07 tab restructure rehomed the surface, not the matrix** the same cases, now
/// rendered as one tab each by `BoardGitTabView` rather than as a closing section of the popover's
@@ -55,28 +67,30 @@ import SwiftUI
enum BoardGitSection: Equatable, CaseIterable {
/// Mode `none`: a board that could have a history and has none. There is no daily surface for
/// that the tab is one caption stating the fact above the Board Settings door, where add-git
/// now lives (03 Board settings sheet). The header-plus-door posture blessed 2026-08-06,
/// restated for a surface whose header is now the tab label.
/// that the tab is one caption stating the fact, and **add-git directly under it** since the
/// 2026-08-07 reversal (`BoardGitAddAction`), where the Board Settings door stood for the week
/// the sheet existed. The fact-then-offer posture blessed 2026-08-06, restated for a surface
/// whose header is the tab label and whose offer is the control itself rather than a door to it.
///
/// **Every tier's posture since the pivot** (03 Git tab, pivot note 2026-08-07), and git stays
/// **opt-in per board**: the door is an offer, never an auto-init.
/// **opt-in per board**: the button is an offer, never an auto-init.
case noRepository
/// Repo-nested: the honest explanation, no action and no Board Settings row either, since
/// nothing setup-shaped can apply (`BoardSettingsAvailability`).
/// Repo-nested: the honest explanation, no action and nothing setup-shaped either, since
/// nothing setup-shaped can apply (`BoardGitSetupSection.resolve`, empty here).
case repoNested
/// Unverifiable: **not** `.repoNested` a denied ancestor check, not a found repository
/// (06 Rules Detection, "Denial is not absence"). Structurally identical to `.repoNested`
/// (no action, no Board Settings row, `BoardSettingsAvailability` false), but its own case so
/// the view renders its own honest prose rather than the nested sentence "unverifiable" is not
/// (no action, no setup `BoardGitSetupSection.resolve` empty), but its own case so the view
/// renders its own honest prose rather than the nested sentence "unverifiable" is not
/// "nested".
case unverifiable
/// Git mode: the branch/source line with the **switch** picker, the abnormal-state explanation
/// when the surface is held, and the Board Settings row. The remote half tracking, Pull/Push,
/// the status badges is 07-sync-collab.md's own card and joins this same posture.
/// Git mode: the branch/source line with the **switch** picker (which carries New Branch again
/// since 2026-08-07), the abnormal-state explanation when the surface is held, and the commit
/// identity block. The remote half tracking, Pull/Push, the status badges is
/// 07-sync-collab.md's own card and joins this same posture.
case branch
static func resolve(mode: BoardGitMode) -> BoardGitSection {
@@ -89,14 +103,100 @@ enum BoardGitSection: Equatable, CaseIterable {
}
}
// MARK: - The setup inventory
/// **Which setup controls this tab hosts for one board** a pure function of the mode and of whether
/// the repository opens, so the rehomed inventory is provable without a popover on screen.
///
/// It is `BoardSettingsSection.resolve`'s successor, and deliberately its same shape: that enum was
/// the *sheet's* inventory (2026-07-31 2026-08-07) and it retired with the sheet, but the rule it
/// carried is about the **board**, not about the container, so it survives the reversal re-pointed at
/// the tab. Branch creation is not a case here for the same reason it was one there and is not now:
/// it went back into the switch menu (`BoardGitControls.branchRow`), which is a daily control with an
/// inline reveal rather than a standing form. pro-m2's remote and credential cards each add a case
/// here and a block in `BoardGitTabView.posture` nothing else.
///
/// Ordered as the tab lays them out, and the order is trivially the postures' own: no board is ever
/// in both modes, so the array is one element or none. It stays an array rather than an `Optional`
/// because the pro-m2 cards land in the git-mode posture beside `.commitIdentity`.
enum BoardGitSetupSection: String, Equatable, CaseIterable, Identifiable {
/// Mode `none`: **add-git** (06-history-undo.md Rules Opt-in init) the offer, on every tier
/// since 12-editions.md PIVOT 2026-08-07, and still never an auto-init.
case addGit
/// Mode `git`: the **commit identity** name/email that repo-local `.git/config` carries
/// (06 Interaction with external writers).
case commitIdentity
var id: String { rawValue }
/// The block's header a heading VoiceOver navigates by (10-accessibility.md's navigable-header
/// rule, carried over from the sheet's sections). `.addGit` has none: it renders directly under
/// the no-repository note, which already states what the posture is, and a "Git" header inside
/// the Git tab would be the surface naming itself twice (this file's opening note).
var title: String? {
switch self {
case .addGit: nil
case .commitIdentity: "Commit Identity"
}
}
/// - Parameter isRepositoryUnreadable: whether the board's `.git` exists and will not open
/// (`HistoryStore.isRepositoryUnreadable`). Defaulted, because it can only ever be true in mode
/// `git` every other mode has no repository for the probe to have failed on, and a caller
/// that has no git state to ask is describing one of those boards.
///
/// `nonisolated` for `BoardGitControls.switchTargets`' reason: a pure answer over plain values,
/// reachable from a test with no actor to hop to.
nonisolated static func resolve(
mode: BoardGitMode,
isRepositoryUnreadable: Bool = false
) -> [BoardGitSetupSection] {
switch mode {
case .none:
return [.addGit]
case .git:
// **An unreadable repository hosts no setup** (06-history-undo.md Rules, the
// corrupt-`.git` loud failure, ruled 2026-07-31: "the whole git surface paused
// Lanework leaves the repository untouched"), and it lands on repo-nested's emptiness by
// repo-nested's own reasoning, one step further along: writing an identity is a *write*
// into the repository's own config, and there is no repository the app can open to write
// it into. The posture is not empty, though that is the difference the sheet could not
// express and the tab can: `BoardGitControls` still renders, holding, with its own
// sentence explaining the state (`BoardGitBranchSurface.unreadableNote`). What the
// unreadable board loses is the setup block alone.
//
// The mode stays `.git` throughout this is emptiness *within* git mode, never a fall
// to mode none, which is what would let add-git be offered against an existing `.git`.
return isRepositoryUnreadable ? [] : [.commitIdentity]
case .repoNested:
// **Nothing setup-shaped can apply** (06 Rules): the board lives inside a repository
// Lanework leaves alone, so there is no add-git (the design is insistent that the option
// is *absent*, "prose, not a disabled button") and no repo-local config of ours to write.
// The posture's whole content is its explanation.
return []
case .unverifiable:
// **Structurally the same emptiness as `.repoNested`, for the same reason** (06 Rules
// Detection, "Denial is not absence"): a denied ancestor check can never be told apart
// from a repository actually being there, so add-git stays unreachable. Only the
// posture's *prose* tells the two apart this inventory does not, because there is
// nothing to set up on either.
return []
}
}
}
// MARK: - The notes
/// **The no-repository caption** (03-board-ui.md Board popover Git tab) the fact, stated, above
/// the Board Settings door.
/// the add-git button that offers to change it.
///
/// One sentence in its siblings' register, and deliberately not a header: the tab label already says
/// "Git", so what is left to say is what this board's git story currently *is*. "Yet" is the whole
/// posture in a word the door directly below it is where a user says otherwise.
/// posture in a word the button directly below it is where a user says otherwise. (It read "above
/// the Board Settings door" between 2026-07-31 and the 2026-08-07 reversal; the sentence never
/// changed, only what stands under it.)
private struct BoardGitNoRepositoryNote: View {
var body: some View {
@@ -135,8 +235,8 @@ private struct BoardGitUnverifiableNote: View {
// MARK: - The tab
/// The Git tab's surface: whichever of the four postures this board is in, and the Board Settings
/// door where it applies.
/// The Git tab's surface: whichever of the four postures this board is in, with the setup controls
/// that posture hosts rendered inline in it (`BoardGitSetupSection`).
struct BoardGitTabView: View {
let store: BoardStore
@@ -152,21 +252,11 @@ struct BoardGitTabView: View {
/// *tier* signal every session composes one (12 PIVOT 2026-08-07).
let git: HistoryStore?
/// The window's settings sheet, so this tab can carry the **Board Settings** row that opens it.
/// `nil` where there is no window to present a sheet on, which reads as a tab with no door.
let settings: BoardSettingsPresentation?
/// The popover's own padding figure (`BoardInfoView.inset`), matching `BoardInfoTabView`'s and
/// `BoardThemeTabView`'s own parameter the tab pads by this amount instead of restating the
/// derivation.
let inset: CGFloat
/// **The popover's own dismissal**, used by exactly one control: the Board Settings row, whose
/// job is to close this surface and open the sheet. The popover is presented by `isPresented`, so
/// the environment action drives the same flag the widget's button does nothing here has to be
/// handed the widget's binding to put it down.
@Environment(\.dismiss) private var dismiss
var body: some View {
posture
.padding(inset)
@@ -181,11 +271,16 @@ struct BoardGitTabView: View {
switch BoardGitSection.resolve(mode: git?.mode ?? .none) {
case .noRepository:
// Nothing daily to show on a board with no repository so the tab is the fact and the
// door. Add-git itself moved to the sheet with the 2026-07-31 split; what stays here is
// the honest signpost that this board *could* have a history and where to say so.
// offer. Add-git spent a week behind the settings sheet's door (2026-07-31 the
// 2026-08-07 reversal) and is back inline under the note that says why it is there. A
// `nil` git previews, the accessory-installation tests renders the note alone: the
// fact is the honest thing to say about a board nothing has been detected about, and
// there is nothing to add a repository *to*.
VStack(alignment: .leading, spacing: 6) {
BoardGitNoRepositoryNote()
boardSettingsRow
if let git, setup.contains(.addGit) {
BoardGitAddAction(git: git, isEnabled: store.acceptsBoardMutations)
}
}
case .repoNested:
@@ -195,40 +290,59 @@ struct BoardGitTabView: View {
BoardGitUnverifiableNote()
case .branch:
VStack(alignment: .leading, spacing: 6) {
// The daily face first the branch line with its switch menu, which carries New Branch
// again since the 2026-08-07 reversal then the one setup block this posture hosts.
// The identity block is gated on the setup inventory rather than on a condition spelled
// out here, which is what keeps "an unreadable repository hosts no setup" one rule with
// one test (`BoardGitSetupSection.resolve`); `BoardGitControls`' own unreadable sentence
// stands alone under it.
//
// `inset` as the gap rather than the 6pt row rhythm: the identity block is a *section*
// under its own heading, and the popover's section spacing is the figure the retired
// sheet used between its sections for the same reason (`BoardInfoView.inset`).
VStack(alignment: .leading, spacing: inset) {
if let git {
BoardGitControls(git: git, isEnabled: store.acceptsBoardMutations)
}
boardSettingsRow
if let git, setup.contains(.commitIdentity) {
setupBlock(.commitIdentity) {
BoardGitIdentityFields(git: git, isEnabled: store.acceptsBoardMutations)
}
}
}
}
}
/// **The popover's one setup affordance** (03-board-ui.md Board popover) the sheet's first
/// door, the menu row being the second (11-command-nexus.md).
///
/// **Shown only where the sheet is reachable** (`BoardSettingsAvailability`): this tab describes
/// *this board*, so a row pointing at a surface this board cannot have would be the disabled
/// button 06 rules out one level up. The menu row is the opposite case and stays visible a menu
/// is an inventory of the app.
///
/// **Dismiss first, then present.** The popover is transient and the sheet is not; leaving a
/// transient surface hanging over a modal one would read as two surfaces arguing about which the
/// user is in.
///
/// Not disabled by the read-only lock: opening a configuration surface is not a mutation, and the
/// controls inside it disable themselves (the Board Info I rule).
@ViewBuilder
private var boardSettingsRow: some View {
if let settings, BoardSettingsAvailability.resolve(
/// What this board's setup half holds the sheet's inventory rule, re-pointed at the tab it
/// rehomed into (`BoardGitSetupSection`). Read once per posture branch rather than re-derived
/// beside each block.
private var setup: [BoardGitSetupSection] {
BoardGitSetupSection.resolve(
mode: git?.mode ?? .none,
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
) {
Button("Board Settings…") {
dismiss()
settings.present()
)
}
/// A setup block under its own header, where the section carries one.
///
/// **The header is an accessibility structure, not decoration** (10-accessibility.md's
/// navigable-header rule, which the retired sheet's sections carried and which came along with
/// them): the rotor jumps between headings rather than walking one flat run of controls. This is
/// the tab's *only* header the no-repository posture's control has none, and the tab label
/// still does the naming for the surface as a whole (this file's opening note).
@ViewBuilder
private func setupBlock(
_ section: BoardGitSetupSection,
@ViewBuilder content: () -> some View
) -> some View {
VStack(alignment: .leading, spacing: 6) {
if let title = section.title {
Text(title)
.font(.subheadline.weight(.semibold))
.accessibilityAddTraits(.isHeader)
}
.accessibilityHint("Opens the board settings sheet")
content()
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}