The tier axis comes out of the git stack — compose unconditional, postures mode-driven
HistoryStore.compose(boardRoot📒) returns non-optional and runs for every session — the nil the gate produced was the only nil it ever had. makeHistoryProvider is a one-axis decision: git-mode boards bind the git provider, everything else native, in every tier; Session.tier stays recorded, dormant. BoardGitSection shrinks to the four mode postures (.absent and .proPointer die, BoardGitNote and the .git probe with them); every board carries all three popover tabs (BoardInfoTab.available retired); the titlebar branch shows on any git-mode board; the settings sheet and card History section stop reading tier. InertGitTests is repurposed as UntouchedGitTests — the file layer still never opens .git, now load-bearing for mode-none boards. The accessibility audit reaches the settings sheet at last: the fixture board hosts it in every tier, so the free-fixture disabled-row test becomes an open-and-audit test. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -13,8 +13,10 @@ import SwiftUI
|
||||
/// 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.
|
||||
///
|
||||
/// **Tab membership is the git posture's** (`BoardInfoTab.available(tier:mode:hasGitDirectory:)`),
|
||||
/// and **selection resets to Info on every open** — both the Git session's rulings.
|
||||
/// **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
|
||||
/// structural rule with nothing left to exclude: every board carries all three tabs (03 ▸ Board
|
||||
/// popover, the same-day pivot note) — see `BoardInfoTab`.
|
||||
///
|
||||
/// ### One home, deliberately
|
||||
///
|
||||
@@ -62,8 +64,8 @@ extension FocusedValues {
|
||||
|
||||
// MARK: - The window-title widget
|
||||
|
||||
/// The titlebar widget: the board's name — and, on a git-mode Pro board, its branch — with a
|
||||
/// trailing disclosure chevron, whose one job is this popover.
|
||||
/// 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 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
|
||||
@@ -80,12 +82,14 @@ struct BoardInfoWidget: View {
|
||||
let store: BoardStore
|
||||
let recents: StyleRecents
|
||||
|
||||
/// The tier and the git state this board's **session** composed with — read once, at the moment
|
||||
/// the widget is installed, and never re-derived (12-editions.md ▸ The entitlement: "a lapse
|
||||
/// never interrupts an open session"). `git` is a reference type and `@Observable`, so add-git
|
||||
/// flipping the mode, or a branch switch, redraws the widget without anything here being
|
||||
/// re-created.
|
||||
let tier: Tier
|
||||
/// The git state this board's **session** composed with — read once, at the moment the widget is
|
||||
/// installed, and never re-derived (12-editions.md ▸ The entitlement: "a lapse never interrupts
|
||||
/// an open session"). `git` is a reference type and `@Observable`, so add-git flipping the mode,
|
||||
/// or a branch switch, redraws the widget without anything here being re-created.
|
||||
///
|
||||
/// It stopped being a tier signal at 12 ▸ PIVOT 2026-08-07 — every session composes a git state
|
||||
/// now, whatever the tier — so a `nil` here means only "this caller had no session to hand over"
|
||||
/// (previews, the accessory-installation tests), which reads as mode `none`.
|
||||
let git: HistoryStore?
|
||||
|
||||
@Bindable var presentation: BoardInfoPresentation
|
||||
@@ -105,7 +109,6 @@ struct BoardInfoWidget: View {
|
||||
BoardInfoTitlebarSummary(
|
||||
snapshotTitle: store.snapshot.title.value,
|
||||
rootURL: store.rootURL,
|
||||
tier: tier,
|
||||
mode: git?.mode ?? .none,
|
||||
branch: git?.branch
|
||||
)
|
||||
@@ -173,7 +176,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, tier: tier, git: git, settings: settings)
|
||||
BoardInfoView(store: store, recents: recents, git: git, settings: settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,23 +215,26 @@ struct BoardInfoWidget: View {
|
||||
/// with plain values and no fixture board on disk — the one duplication this card leaves behind
|
||||
/// rather than reshaping `AppModel.displayName(of:)`'s signature to fit both call sites.
|
||||
///
|
||||
/// **Branch.** Shown only when the board is actually git-mode under Pro — `tier == .pro && mode ==
|
||||
/// .git` with a non-`nil` branch — the same condition family `BoardGitSection.resolve`'s `.branch`
|
||||
/// case covers. The free tier and an inert `.git` (mode `.none` or `.repoNested`) show no branch;
|
||||
/// neither does a git-mode board whose branch has not been read yet (`HistoryStore.branch` starts
|
||||
/// `nil` until `refreshBranch()` answers, which the widget's own `.task` kicks off at open).
|
||||
/// **Branch.** Shown only when the board is actually git-mode — `mode == .git` with a non-`nil`
|
||||
/// branch — the same condition `BoardGitSection.resolve`'s `.branch` case covers. **The tier clause
|
||||
/// is gone** (12-editions.md ▸ PIVOT 2026-08-07: git is tier-independent, so a git-mode board is a
|
||||
/// git-mode board and the widget says so on every tier); the rule it read `tier == .pro && mode ==
|
||||
/// .git` until that day is recorded here rather than restated in code. A board with no repository or
|
||||
/// an inert one (mode `.none`, `.repoNested`, `.unverifiable`) shows no branch; neither does a
|
||||
/// git-mode board whose branch has not been read yet (`HistoryStore.branch` starts `nil` until
|
||||
/// `refreshBranch()` answers, which the widget's own `.task` kicks off at open).
|
||||
struct BoardInfoTitlebarSummary: Equatable {
|
||||
|
||||
let title: String
|
||||
let branch: String?
|
||||
|
||||
init(snapshotTitle: String?, rootURL: URL, tier: Tier, mode: BoardGitMode, branch: String?) {
|
||||
init(snapshotTitle: String?, rootURL: URL, mode: BoardGitMode, branch: String?) {
|
||||
if let snapshotTitle, !snapshotTitle.isEmpty {
|
||||
self.title = snapshotTitle
|
||||
} else {
|
||||
self.title = rootURL.deletingPathExtension().lastPathComponent
|
||||
}
|
||||
self.branch = (tier == .pro && mode == .git) ? branch : nil
|
||||
self.branch = mode == .git ? branch : nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,14 +246,13 @@ 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
|
||||
/// `tier`/`git` default to the free tier's posture — a popover with no git section at all — and
|
||||
/// `settings` to no sheet, so that a caller with no session in hand (the accessory-installation
|
||||
/// tests, which are about AppKit plumbing rather than about git) describes a board honestly rather
|
||||
/// than by accident. The app's own call site passes the session's values explicitly.
|
||||
/// `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.
|
||||
func boardInfoTitlebarAccessory(
|
||||
store: BoardStore,
|
||||
recents: StyleRecents,
|
||||
tier: Tier = .free,
|
||||
git: HistoryStore? = nil,
|
||||
presentation: BoardInfoPresentation,
|
||||
settings: BoardSettingsPresentation? = nil
|
||||
@@ -256,7 +261,6 @@ func boardInfoTitlebarAccessory(
|
||||
rootView: BoardInfoWidget(
|
||||
store: store,
|
||||
recents: recents,
|
||||
tier: tier,
|
||||
git: git,
|
||||
presentation: presentation,
|
||||
settings: settings
|
||||
@@ -291,26 +295,16 @@ enum BoardInfoTab: String, CaseIterable, Identifiable {
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
/// **Which tabs this board actually offers** — membership *is* the git posture (03-board-ui.md
|
||||
/// § Board popover, the Git session's ruling, 2026-08-07): the Git tab joins the strip only when
|
||||
/// the git section has something true to say, and drops out exactly where `BoardGitSection`
|
||||
/// resolves to `.absent` — the free tier's ordinary board, which shows Info | Theme alone.
|
||||
///
|
||||
/// That carries 12-editions.md's "absent, no placeholder" rule up to the tab strip: a standing
|
||||
/// Git tab on every free board would be the standing ad for Pro that 12 forbids, and an empty tab
|
||||
/// behind it would be the placeholder 12 forbids twice over.
|
||||
///
|
||||
/// **Delegated, never restated.** The posture matrix is settled, test-pinned design
|
||||
/// (`BoardGitSectionTests`), and a second reading of "is there git to show here" is a reading
|
||||
/// that can disagree with the tab it labels — so this asks `BoardGitSection.resolve` the same
|
||||
/// question `BoardGitTabView` asks, from the same three facts.
|
||||
///
|
||||
/// Order is `allCases`' own — Info, then Theme, then Git — so the Git tab, where present, is
|
||||
/// always last and the two standing tabs never move under a user who switched boards.
|
||||
static func available(tier: Tier, mode: BoardGitMode, hasGitDirectory: Bool) -> [BoardInfoTab] {
|
||||
let posture = BoardGitSection.resolve(tier: tier, mode: mode, hasGitDirectory: hasGitDirectory)
|
||||
return allCases.filter { $0 != .git || posture != .absent }
|
||||
}
|
||||
// **Membership is the git posture's, and the posture never says "absent" any more.** The Git
|
||||
// session ruled (2026-08-07) that the Git tab joins the strip only where `BoardGitSection` has
|
||||
// something true to say, which then meant dropping it on a free board with no `.git` — the
|
||||
// "absent, no placeholder" rule carried up to the strip. **12-editions.md ▸ PIVOT 2026-08-07**,
|
||||
// the same day, retired the free-only postures wholesale: git left the paywall, `.absent` and
|
||||
// `.proPointer` died with it, and 03-board-ui.md ▸ Board popover records the consequence — "the
|
||||
// absent posture is unreachable and every board carries all three tabs. The membership rule
|
||||
// stands structurally — the strip still asks the posture — it just never hears 'absent'
|
||||
// anymore." So there is no `available(…)` filter here to ask it with: membership is `allCases`,
|
||||
// in `allCases`' own order (Info, Theme, Git), which is what the filter answered anyway.
|
||||
}
|
||||
|
||||
// MARK: - The popover's content
|
||||
@@ -326,7 +320,6 @@ struct BoardInfoView: View {
|
||||
|
||||
let store: BoardStore
|
||||
let recents: StyleRecents
|
||||
let tier: Tier
|
||||
let git: HistoryStore?
|
||||
let settings: BoardSettingsPresentation?
|
||||
|
||||
@@ -337,17 +330,6 @@ struct BoardInfoView: View {
|
||||
/// `BoardInfoWidget` hands `.popover` fresh on every open, is exactly that rule and nothing more.
|
||||
@State private var tab: BoardInfoTab = .info
|
||||
|
||||
/// Whether this board carries a `.git` — checked once, off disk, when the view is built (which
|
||||
/// is every time the popover opens, since `BoardInfoWidget` hands `.popover` a fresh instance).
|
||||
/// See `BoardGitNote.hasGitDirectory(at:)` for why a live-updating fact isn't needed here.
|
||||
///
|
||||
/// **The free tier's input only.** Under Pro the Git tab reads the session's detected mode
|
||||
/// instead — a fact settled at open, which is where 06-history-undo.md puts detection — and this
|
||||
/// stays what it always was: the one quiet question the free tier asks of a board's folder. Since
|
||||
/// the tab restructure it answers for the tab *strip* as well as for the tab's content
|
||||
/// (`tabs` below), which is why it is read once here and handed down rather than asked twice.
|
||||
private let hasGitDirectory: Bool
|
||||
|
||||
/// The style editor brings its own padding, so the sections around it carry the same number by
|
||||
/// hand instead of an outer padding that would double up on it — **the editor's own figure**
|
||||
/// (`StyleEditorLayout.sectionSpacing`), which is font-derived, so the popover's chrome scales
|
||||
@@ -356,36 +338,16 @@ struct BoardInfoView: View {
|
||||
StyleEditorLayout.sectionSpacing(bodyPointSize: CardWindowMetrics.bodyPointSize)
|
||||
}
|
||||
|
||||
/// **The tab strip's membership** (`BoardInfoTab.available`), read inside `body` so the mode it
|
||||
/// delegates to is live — `HistoryStore` is `@Observable`, and add-git flipping a board's mode
|
||||
/// must not leave a stale strip behind it.
|
||||
///
|
||||
/// **Stable across an open, in practice and by construction.** `tier` is the session's, fixed at
|
||||
/// the widget's birth (12-editions.md ▸ The entitlement: "a lapse never interrupts an open
|
||||
/// session"), and `hasGitDirectory` is one `init`-time read; the mode is the only live input, and
|
||||
/// `BoardGitSection.resolve` answers `.absent` only under the free tier, where it never consults
|
||||
/// the mode at all. So no live change can move the Git tab in or out from under the selection.
|
||||
/// Add-git — the one gesture that flips a mode — runs from the settings sheet anyway, which the
|
||||
/// Board Settings… row dismisses this popover to reach.
|
||||
private var tabs: [BoardInfoTab] {
|
||||
BoardInfoTab.available(tier: tier, mode: git?.mode ?? .none, hasGitDirectory: hasGitDirectory)
|
||||
}
|
||||
|
||||
init(
|
||||
store: BoardStore,
|
||||
recents: StyleRecents,
|
||||
tier: Tier = .free,
|
||||
git: HistoryStore? = nil,
|
||||
settings: BoardSettingsPresentation? = nil
|
||||
) {
|
||||
self.store = store
|
||||
self.recents = recents
|
||||
self.tier = tier
|
||||
self.git = git
|
||||
self.settings = settings
|
||||
// Asked only where it is the answer: under Pro the mode already knows, and a free-tier
|
||||
// board is the only one this question is for (12-editions.md ▸ The free tier and `.git`).
|
||||
self.hasGitDirectory = tier == .free && BoardGitNote.hasGitDirectory(at: store.rootURL)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -432,10 +394,12 @@ struct BoardInfoView: View {
|
||||
// The tab bar: a segmented control rather than a `TabView`, because the popover is a
|
||||
// compact settings surface and the segmented idiom is the macOS shape for switching
|
||||
// between a handful of peer panes inside one. The label is hidden visually but stays
|
||||
// the control's accessibility name. It iterates `tabs`, not `allCases` — membership is
|
||||
// this board's git posture (`BoardInfoTab.available`).
|
||||
// the control's accessibility name. It iterates `allCases` — the strip's membership is
|
||||
// still the board's git posture in principle (the Git session's ruling), and since
|
||||
// 12-editions.md ▸ PIVOT 2026-08-07 that posture is never "absent", so every board
|
||||
// carries all three tabs (03-board-ui.md ▸ Board popover, the same-day pivot note).
|
||||
Picker("Board configuration", selection: $tab) {
|
||||
ForEach(tabs) { tab in
|
||||
ForEach(BoardInfoTab.allCases) { tab in
|
||||
Text(tab.rawValue)
|
||||
}
|
||||
}
|
||||
@@ -452,14 +416,7 @@ struct BoardInfoView: View {
|
||||
case .theme:
|
||||
BoardThemeTabView(store: store, inset: inset)
|
||||
case .git:
|
||||
BoardGitTabView(
|
||||
store: store,
|
||||
tier: tier,
|
||||
git: git,
|
||||
settings: settings,
|
||||
hasGitDirectory: hasGitDirectory,
|
||||
inset: inset
|
||||
)
|
||||
BoardGitTabView(store: store, git: git, settings: settings, inset: inset)
|
||||
}
|
||||
}
|
||||
// The style editor's popover width, taken from the editor rather than restated — the number
|
||||
|
||||
Reference in New Issue
Block a user