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
235 lines
12 KiB
Swift
235 lines
12 KiB
Swift
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).
|
|
///
|
|
/// ### No "Git" header anywhere in this tab
|
|
///
|
|
/// The tab's own label does the naming work the section's header used to (the design's phrasing), so
|
|
/// every posture below renders its content bare where the pre-tab section stacked it under a
|
|
/// `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.
|
|
///
|
|
/// ### Every board has this tab (pivot 2026-08-07)
|
|
///
|
|
/// 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) — a pure function of the board's **mode**, so the posture matrix is
|
|
/// provable without a popover on screen.
|
|
///
|
|
/// 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
|
|
/// 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-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.
|
|
///
|
|
/// **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 {
|
|
|
|
/// 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
|
|
|
|
/// Repo-nested: the honest explanation, no action — and no Board Settings… row either, since
|
|
/// nothing setup-shaped can apply (`BoardSettingsAvailability`).
|
|
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
|
|
/// "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.
|
|
case branch
|
|
|
|
static func resolve(mode: BoardGitMode) -> BoardGitSection {
|
|
switch mode {
|
|
case .none: return .noRepository
|
|
case .git: return .branch
|
|
case .repoNested: return .repoNested
|
|
case .unverifiable: return .unverifiable
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - The notes
|
|
|
|
/// **The no-repository caption** (03-board-ui.md ▸ Board popover ▸ Git tab) — the fact, stated, above
|
|
/// the Board Settings… door.
|
|
///
|
|
/// 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.
|
|
private struct BoardGitNoRepositoryNote: View {
|
|
|
|
var body: some View {
|
|
Text("This board has no git history yet.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
|
|
/// **The repo-nested explanation** (06-history-undo.md ▸ Rules), worded as the design words it:
|
|
/// short prose in place of an action, never a hidden or greyed-out add-git.
|
|
private struct BoardGitNestedNote: View {
|
|
|
|
var body: some View {
|
|
Text("This board lives inside a repository; Lanework leaves it to that repository.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
|
|
/// **The unverifiable explanation** (06-history-undo.md ▸ Rules ▸ Detection, "Denial is not
|
|
/// absence", ruled 2026-07-31), worded as its own honest sentence rather than borrowing
|
|
/// `BoardGitNestedNote`'s — a denied ancestor check is not a found repository, and telling a user
|
|
/// their board is nested when the truth is "couldn't check" would be a lie dressed as caution.
|
|
private struct BoardGitUnverifiableNote: View {
|
|
|
|
var body: some View {
|
|
Text("Lanework couldn't verify whether this board sits inside a repository, so it isn't offering to add one here.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
struct BoardGitTabView: View {
|
|
|
|
let store: BoardStore
|
|
|
|
/// 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 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)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
/// The tab's content, posture by posture — see `BoardGitSection`. No section header and no
|
|
/// dividers: the tab label names this surface, and the tabs already sit under the popover
|
|
/// header's rule.
|
|
@ViewBuilder
|
|
private var posture: some 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.
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
BoardGitNoRepositoryNote()
|
|
boardSettingsRow
|
|
}
|
|
|
|
case .repoNested:
|
|
BoardGitNestedNote()
|
|
|
|
case .unverifiable:
|
|
BoardGitUnverifiableNote()
|
|
|
|
case .branch:
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
if let git {
|
|
BoardGitControls(git: git, isEnabled: store.acceptsBoardMutations)
|
|
}
|
|
boardSettingsRow
|
|
}
|
|
}
|
|
}
|
|
|
|
/// **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(
|
|
mode: git?.mode ?? .none,
|
|
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
|
|
) {
|
|
Button("Board Settings…") {
|
|
dismiss()
|
|
settings.present()
|
|
}
|
|
.accessibilityHint("Opens the board settings sheet")
|
|
}
|
|
}
|
|
}
|