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:
@@ -39,51 +39,55 @@ import SwiftUI
|
||||
/// than per board or per app): one per window, `@State` in `BoardWindowHost`, published through the
|
||||
/// focus system so Board ▸ Board Settings… means "the board in front".
|
||||
///
|
||||
/// **It carries the session's tier and git state** where the popover flag carries nothing, and for a
|
||||
/// reason the popover does not have: both of this sheet's doors have to *validate*, and one of them
|
||||
/// is a menu row with no view around it to ask. The facts are adopted once, from the session, at the
|
||||
/// same moment the titlebar widget adopts them (`BoardWindowHost.configureWindow`) and never
|
||||
/// re-derived — 12-editions.md ▸ The entitlement, "a lapse never interrupts an open session". The
|
||||
/// *mode* inside the git state is `@Observable` and does move, by add-git alone, which is exactly the
|
||||
/// transition this sheet is where the user performs: the sections re-resolve under it live.
|
||||
/// **It carries the session's git state** where the popover flag carries nothing, and for a reason
|
||||
/// the popover does not have: both of this sheet's doors have to *validate*, and one of them is a
|
||||
/// menu row with no view around it to ask. The fact is adopted once, from the session, at the same
|
||||
/// moment the titlebar widget adopts it (`BoardWindowHost.configureWindow`) and never re-derived —
|
||||
/// 12-editions.md ▸ The entitlement, "a lapse never interrupts an open session". The *mode* inside
|
||||
/// the git state is `@Observable` and does move, by add-git alone, which is exactly the transition
|
||||
/// this sheet is where the user performs: the sections re-resolve under it live.
|
||||
///
|
||||
/// **The tier came out at 12 ▸ PIVOT 2026-08-07.** It was adopted here alongside the git state until
|
||||
/// that day, because the sheet was Pro's; git is tier-independent now, so what the doors validate on
|
||||
/// is the board's mode alone.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class BoardSettingsPresentation {
|
||||
|
||||
var isPresented = false
|
||||
|
||||
/// The tier this window's board composed under — the free tier's default until a session says
|
||||
/// otherwise, which is the harmless direction (an unreachable sheet).
|
||||
private(set) var tier: Tier = .free
|
||||
|
||||
/// This window's board git state, `nil` under the free tier and on a window whose session has not
|
||||
/// been adopted yet.
|
||||
/// This window's board git state, `nil` on a window whose session has not been adopted yet —
|
||||
/// which reads as mode `none`, the harmless direction (an add-git sheet, offered to a board that
|
||||
/// may well already have a repository, is nothing anyone can act on before adoption lands).
|
||||
private(set) var git: HistoryStore?
|
||||
|
||||
/// Called once per window, from the same place the titlebar widget is handed the same two facts.
|
||||
func adopt(tier: Tier, git: HistoryStore?) {
|
||||
self.tier = tier
|
||||
/// Called once per window, from the same place the titlebar widget is handed the same fact.
|
||||
func adopt(git: HistoryStore?) {
|
||||
self.git = git
|
||||
}
|
||||
|
||||
/// What the sheet would show right now — and therefore, when empty, that there is no sheet to
|
||||
/// show (`BoardSettingsAvailability`).
|
||||
///
|
||||
/// **An unadopted window has none**, and that is a `nil` check rather than a mode reading: every
|
||||
/// control this sheet hosts writes *through* the git state (`section(_:)` renders nothing without
|
||||
/// one), so a window that has not been handed its session yet would otherwise offer a sheet of
|
||||
/// bare headers. It was the free tier's default that kept this shut before 12 ▸ PIVOT 2026-08-07;
|
||||
/// what keeps it shut now is the honest absence of a session, which is the only thing a `nil`
|
||||
/// ever meant here.
|
||||
var sections: [BoardSettingsSection] {
|
||||
BoardSettingsSection.resolve(
|
||||
tier: tier,
|
||||
mode: git?.mode ?? .none,
|
||||
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
|
||||
guard let git else { return [] }
|
||||
return BoardSettingsSection.resolve(
|
||||
mode: git.mode,
|
||||
isRepositoryUnreadable: git.isRepositoryUnreadable
|
||||
)
|
||||
}
|
||||
|
||||
/// Both doors' validation: the menu row's `disabled` state and whether the popover shows its row
|
||||
/// at all.
|
||||
/// at all. Derived from `sections` rather than from `BoardSettingsAvailability` directly, so the
|
||||
/// unadopted case above cannot answer one way here and another there.
|
||||
var isReachable: Bool {
|
||||
BoardSettingsAvailability.resolve(
|
||||
tier: tier,
|
||||
mode: git?.mode ?? .none,
|
||||
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
|
||||
)
|
||||
!sections.isEmpty
|
||||
}
|
||||
|
||||
/// **Opening, not toggling** — unlike ⌘I. A sheet is modal to its window and carries its own
|
||||
@@ -118,22 +122,30 @@ extension FocusedValues {
|
||||
|
||||
// MARK: - What the sheet holds
|
||||
|
||||
/// **The sheet's inventory for one board**, as a pure function of the tier and the mode — the shape
|
||||
/// **The sheet's inventory for one board**, as a pure function of the mode — the shape
|
||||
/// `BoardGitSection.resolve` has one surface over, and for the same reason: the *contents* are the
|
||||
/// part worth pinning and the SwiftUI that renders them is not.
|
||||
///
|
||||
/// **The tier axis came out at 12-editions.md ▸ PIVOT 2026-08-07**: this resolved `guard tier ==
|
||||
/// .pro else { return [] }` first and the mode second until git left the paywall. Every board can
|
||||
/// reach the setup it has now, and what it has is still the mode's answer — the sheet's whole
|
||||
/// vocabulary is repository-shaped, so a board with nothing repository-shaped to say still hosts
|
||||
/// nothing.
|
||||
///
|
||||
/// Ordered as the sheet lays them out, top to bottom. pro-m2's cards each add a case here and a
|
||||
/// branch in `BoardSettingsSheet.section(_:)` — nothing else.
|
||||
enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
|
||||
|
||||
/// Pro, mode `none`: **add-git** (06-history-undo.md ▸ Rules ▸ Opt-in init).
|
||||
/// Mode `none`: **add-git** (06-history-undo.md ▸ Rules ▸ Opt-in init) — the offer, on every
|
||||
/// tier since the pivot, and still never an auto-init (git is opt-in per board, 12 ▸ PIVOT
|
||||
/// 2026-08-07).
|
||||
case git
|
||||
|
||||
/// Pro, mode `git`: **branch creation**. Switching stays in the popover (03 ▸ Board popover);
|
||||
/// Mode `git`: **branch creation**. Switching stays in the popover (03 ▸ Board popover);
|
||||
/// create-and-switch runs 06's identical settle sequence from here.
|
||||
case branch
|
||||
|
||||
/// Pro, mode `git`: the **commit identity** name/email that repo-local `.git/config` carries
|
||||
/// Mode `git`: the **commit identity** name/email that repo-local `.git/config` carries
|
||||
/// (06 ▸ Interaction with external writers).
|
||||
case commitIdentity
|
||||
|
||||
@@ -154,14 +166,9 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
|
||||
/// `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.
|
||||
static func resolve(
|
||||
tier: Tier,
|
||||
mode: BoardGitMode,
|
||||
isRepositoryUnreadable: Bool = false
|
||||
) -> [BoardSettingsSection] {
|
||||
// **The free tier has no setup to host** (12-editions.md ▸ The free tier and `.git`): git is
|
||||
// the Pro subscription's, "any `.git` is inert", and 03 gives the free tier's whole git story
|
||||
// as the popover's one-line pointer. There is nothing for a sheet to be about.
|
||||
guard tier == .pro else { return [] }
|
||||
switch mode {
|
||||
case .none:
|
||||
return [.git]
|
||||
@@ -204,15 +211,14 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
|
||||
/// Reachable **iff the sheet has something to show**, which is the rule rather than a shortcut: a
|
||||
/// surface whose whole job is hosting setup controls has no honest empty state, and deriving the
|
||||
/// answer from the inventory is what keeps the two from drifting when pro-m2's sections land. In
|
||||
/// today's terms that reads: Pro, on a board whose mode is `none` or `git`.
|
||||
/// today's terms that reads: a board whose mode is `none` or `git`, on **any tier** — the Pro
|
||||
/// requirement that stood beside it retired with 12-editions.md ▸ PIVOT 2026-08-07.
|
||||
///
|
||||
/// The two unreachable postures are unreachable for *different* reasons, and both are the design's:
|
||||
///
|
||||
/// - **The free tier**: no setup exists there at all (03-board-ui.md ▸ Board popover;
|
||||
/// 12-editions.md). The popover's own postures are untouched by this card — an ordinary board
|
||||
/// shows nothing, a board carrying an inert `.git` shows the one-line Pro pointer.
|
||||
/// - **Pro, repo-nested**: `BoardSettingsSection.resolve`'s own comment carries this one — nothing
|
||||
/// setup-shaped can apply, so the popover's prose stands and no door opens.
|
||||
/// The unreachable postures are unreachable for reasons that are all the board's, and all the
|
||||
/// design's — `BoardSettingsSection.resolve`'s own comments carry them one by one: **repo-nested**
|
||||
/// and **unverifiable**, where nothing setup-shaped can apply, and **git mode over a repository that
|
||||
/// will not open**, where both sections would be writes into something the app cannot open. In each,
|
||||
/// the popover's prose stands and no door opens.
|
||||
///
|
||||
/// The menu **row stays visible and disabled** either way (standard menu validation — a command that
|
||||
/// does not apply here is still a command this app has), while the **popover row appears only where
|
||||
@@ -221,12 +227,10 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
|
||||
enum BoardSettingsAvailability {
|
||||
|
||||
static func resolve(
|
||||
tier: Tier,
|
||||
mode: BoardGitMode,
|
||||
isRepositoryUnreadable: Bool = false
|
||||
) -> Bool {
|
||||
!BoardSettingsSection.resolve(
|
||||
tier: tier,
|
||||
mode: mode,
|
||||
isRepositoryUnreadable: isRepositoryUnreadable
|
||||
).isEmpty
|
||||
|
||||
Reference in New Issue
Block a user