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") } } }