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:
@@ -15,26 +15,25 @@ import SwiftUI
|
||||
/// `sectionHeader("Git")` and a `Divider()`. The tabs already sit under the popover header's own
|
||||
/// divider; a second rule and a second "Git" would be the surface naming itself twice.
|
||||
///
|
||||
/// ### Membership is the posture, so this view never renders an empty state
|
||||
/// ### Every board has this tab (pivot 2026-08-07)
|
||||
///
|
||||
/// `BoardInfoTab.available(tier:mode:hasGitDirectory:)` drops the Git tab from the strip exactly when
|
||||
/// `BoardGitSection` resolves to `.absent`, so this view is never built for the free tier's ordinary
|
||||
/// board. That is 12-editions.md's "absent, no placeholder" rule carried up to the tab strip — a
|
||||
/// standing Git tab on every free board would be the standing ad for Pro that 12 forbids.
|
||||
/// 12-editions.md ▸ PIVOT 2026-08-07 took git off the paywall, and 03's Git-tab note records what
|
||||
/// that does to this surface: with no free-only postures left, every board resolves one of the four
|
||||
/// mode postures below and **the tab is always in the strip**. The membership rule the Git session
|
||||
/// ruled (the strip asks the posture) stands structurally in `BoardInfoTab` — it simply never hears
|
||||
/// "absent" any more, because there is no longer a posture that says nothing.
|
||||
|
||||
// MARK: - The posture
|
||||
|
||||
/// **What the Git tab shows, for one board** (03-board-ui.md ▸ Board popover ▸ Git tab;
|
||||
/// 06-history-undo.md ▸ Rules; 12-editions.md ▸ The free tier and `.git`) — a pure function of two
|
||||
/// facts, so the posture matrix is provable without a popover on screen.
|
||||
/// 06-history-undo.md ▸ Rules) — a pure function of the board's **mode**, so the posture matrix is
|
||||
/// provable without a popover on screen.
|
||||
///
|
||||
/// The free tier's two cases are settled 2026-07-27: absent on an ordinary board, a one-line Pro
|
||||
/// pointer on a board carrying an inert `.git`. The Pro cases are the mode, one to one — and the
|
||||
/// mode-`none` and repo-nested pair is where the design is most insistent: a repo-nested board gets
|
||||
/// **prose, not a disabled button**. "The option is absent because it *can't* apply, and the UI
|
||||
/// should teach that rather than look broken" (06 ▸ Rules). `unverifiable` (the git-detection axis)
|
||||
/// joins as a fourth Pro case, structurally identical to `repoNested` but worded as its own honest
|
||||
/// prose — a denial is not a nesting.
|
||||
/// The four cases are the mode, one to one — and the mode-`none` and repo-nested pair is where the
|
||||
/// design is most insistent: a repo-nested board gets **prose, not a disabled button**. "The option
|
||||
/// is absent because it *can't* apply, and the UI should teach that rather than look broken" (06 ▸
|
||||
/// 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
|
||||
@@ -43,59 +42,49 @@ import SwiftUI
|
||||
/// 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-08-07 tab restructure rehomed the surface, not the matrix** — the same six cases, now
|
||||
/// **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
|
||||
/// single pane. `.absent` grew one further job in the move: it is what `BoardInfoTab.available` reads
|
||||
/// to decide whether the Git tab is in the strip at all, which is why membership can never disagree
|
||||
/// with what the tab would have rendered.
|
||||
/// single pane.
|
||||
///
|
||||
/// **The 2026-08-07 pivot took the tier axis out of it** (12-editions.md ▸ PIVOT 2026-08-07, the
|
||||
/// same day). The two free-tier cases — `.absent` on an ordinary board, `.proPointer` on a board
|
||||
/// carrying an inert `.git`, both settled 2026-07-27 — described a gate that no longer exists: git
|
||||
/// is tier-independent, a `.git` at a board root is live in every tier, and detection runs at every
|
||||
/// board open. So the free-tier branch, the `hasGitDirectory` input it asked for, and both cases are
|
||||
/// **retired**, and what is left is the mode — which is what the postures were always about.
|
||||
enum BoardGitSection: Equatable, CaseIterable {
|
||||
|
||||
/// Nothing at all — the free tier's ordinary board, where "the popover is rename + style,
|
||||
/// complete in itself". Since the restructure this is a **missing tab**, not an empty one
|
||||
/// (`BoardInfoTab.available`).
|
||||
case absent
|
||||
|
||||
/// The free tier's one-line explanation of an inert `.git`, and the app's one in-context pointer
|
||||
/// to Pro (12 ▸ Tier naming).
|
||||
case proPointer
|
||||
|
||||
/// Pro, 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.
|
||||
/// 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.
|
||||
///
|
||||
/// **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.
|
||||
case noRepository
|
||||
|
||||
/// Pro, 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 no Board Settings… row either, since
|
||||
/// nothing setup-shaped can apply (`BoardSettingsAvailability`).
|
||||
case repoNested
|
||||
|
||||
/// Pro, unverifiable: **not** `.repoNested` — a denied ancestor check, not a found repository
|
||||
/// 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
|
||||
/// "nested".
|
||||
case unverifiable
|
||||
|
||||
/// Pro, 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, 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.
|
||||
case branch
|
||||
|
||||
static func resolve(tier: Tier, mode: BoardGitMode, hasGitDirectory: Bool) -> BoardGitSection {
|
||||
switch tier {
|
||||
case .free:
|
||||
// Detection never runs under the free tier, so the mode is not consulted here — the one
|
||||
// question asked is whether the folder carries a `.git`, which is what the pointer is
|
||||
// about (12: "any `.git` is inert … a stray like any other, preserved verbatim").
|
||||
return hasGitDirectory ? .proPointer : .absent
|
||||
case .pro:
|
||||
switch mode {
|
||||
case .none: return .noRepository
|
||||
case .git: return .branch
|
||||
case .repoNested: return .repoNested
|
||||
case .unverifiable: return .unverifiable
|
||||
}
|
||||
static func resolve(mode: BoardGitMode) -> BoardGitSection {
|
||||
switch mode {
|
||||
case .none: return .noRepository
|
||||
case .git: return .branch
|
||||
case .repoNested: return .repoNested
|
||||
case .unverifiable: return .unverifiable
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,71 +133,29 @@ private struct BoardGitUnverifiableNote: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The contextual git note — **a quiet signpost, not a feature** (12-editions.md ▸ The free tier and
|
||||
/// `.git`, settled 2026-07-27, carried through the one-app collapse). The free tier has no git
|
||||
/// integration (that is the Pro subscription's), so this is not a grow-in-place slot the way the old
|
||||
/// `BoardGitSlot` placeholder was: there is nothing here to grow. The free tier's whole git story is
|
||||
/// one line, shown only when it is true — and it is **the one in-context pointer to Pro**, the second
|
||||
/// of the three places the app names it (12 ▸ Tier naming; the other two are `AboutBox` and the
|
||||
/// Settings Pro section).
|
||||
///
|
||||
/// On an ordinary board nothing instantiates this type at all — the board has no Git tab to put it in
|
||||
/// (`BoardInfoTab.available`), matching the card window's absent History section (12: "absent, no
|
||||
/// placeholder"). Only a board that carries an inert `.git` (12 ▸ the inert posture: "any `.git` is
|
||||
/// inert" — the free tier never reads or writes it, whether the board's own or one a lapsed
|
||||
/// subscription left behind) earns this note, worded exactly as 12 rules: an honest explanation of
|
||||
/// what the folder is, named exactly where the question arises, never a standing ad for Pro.
|
||||
///
|
||||
/// Not `private`: `hasGitDirectory(at:)` is the pure seam `BoardInfoPopoverTests.swift` pins directly
|
||||
/// (a fixture board with `.git` → true, without → false), and `BoardInfoView.init` is its one caller
|
||||
/// in the app — the popover asks the question once at open and hands the answer down, both to this
|
||||
/// tab and to `BoardInfoTab.available`.
|
||||
struct BoardGitNote: View {
|
||||
|
||||
var body: some View {
|
||||
Text("This board has a git history. Lanework Pro works with it.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
/// Whether `boardRoot` carries a `.git` entry — the free tier's entire detection story, and a deliberately
|
||||
/// small one: nothing in `BoardStore`, `BoardModel`, or `BoardLoader` tracks this as a live fact
|
||||
/// today, because nothing needs it to be live. `FolderWatcher` filters `.git` out of the folder
|
||||
/// watch by design (§ .git filtering — it exists to ignore git churn), so there is no reload
|
||||
/// event this could hang off even if it wanted to; a plain, read-only `FileManager` check taken
|
||||
/// once, at the moment the popover is built, is the honest amount of machinery for a single quiet
|
||||
/// line. A `.git` added or removed while the popover happens to be open is stale until the next
|
||||
/// open — a gap this note's own posture makes harmless, since it is a signpost, not a control.
|
||||
static func hasGitDirectory(at boardRoot: URL) -> Bool {
|
||||
FileManager.default.fileExists(atPath: boardRoot.appendingPathComponent(".git").path)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The tab
|
||||
|
||||
/// The Git tab's surface: whichever of the six postures this board is in, and the Board Settings…
|
||||
/// The Git tab's surface: whichever of the four postures this board is in, and the Board Settings…
|
||||
/// door where it applies.
|
||||
struct BoardGitTabView: View {
|
||||
|
||||
let store: BoardStore
|
||||
|
||||
/// The tier and the git state this board's **session** composed with, handed down from
|
||||
/// `BoardInfoView` rather than re-derived — 12-editions.md ▸ The entitlement ("a lapse never
|
||||
/// interrupts an open session"). `git` is `@Observable`, so a branch switch or a pause landing
|
||||
/// while the tab is open repaints it.
|
||||
let tier: Tier
|
||||
/// The git state this board's **session** composed with, handed down from `BoardInfoView` rather
|
||||
/// than re-derived — 12-editions.md ▸ The entitlement ("a lapse never interrupts an open
|
||||
/// session"). `git` is `@Observable`, so a branch switch or a pause landing while the tab is open
|
||||
/// repaints it.
|
||||
///
|
||||
/// Optional because a caller with no session in hand (previews, the accessory-installation tests)
|
||||
/// has none to hand over; a `nil` reads as mode `none`, which is the honest description of a
|
||||
/// board nothing has detected anything about yet. Since the 2026-08-07 pivot it is no longer a
|
||||
/// *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 free tier's one question about the board's folder, asked once in `BoardInfoView.init` and
|
||||
/// passed down — the same answer the tab strip's membership was computed from, so the tab and its
|
||||
/// content can never disagree about which posture this board is in.
|
||||
let hasGitDirectory: Bool
|
||||
|
||||
/// 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.
|
||||
@@ -231,18 +178,7 @@ struct BoardGitTabView: View {
|
||||
/// header's rule.
|
||||
@ViewBuilder
|
||||
private var posture: some View {
|
||||
switch BoardGitSection.resolve(tier: tier, mode: git?.mode ?? .none, hasGitDirectory: hasGitDirectory) {
|
||||
case .absent:
|
||||
// Unreachable, and kept honest rather than fatal: `BoardInfoTab.available` drops the Git
|
||||
// tab from the strip on exactly this posture, so nothing ever selects a tab that would
|
||||
// land here. Written as `EmptyView` because that is what the case *means* — if the
|
||||
// membership rule ever loosened, the free tier's ordinary board would render nothing,
|
||||
// which is still the design's answer for it (12: "absent, no placeholder").
|
||||
EmptyView()
|
||||
|
||||
case .proPointer:
|
||||
BoardGitNote()
|
||||
|
||||
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
|
||||
@@ -285,7 +221,6 @@ struct BoardGitTabView: View {
|
||||
@ViewBuilder
|
||||
private var boardSettingsRow: some View {
|
||||
if let settings, BoardSettingsAvailability.resolve(
|
||||
tier: tier,
|
||||
mode: git?.mode ?? .none,
|
||||
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user