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