The popover/sheet split lands: a board-scoped, titled, sectioned sheet on the board window hosts everything setup-shaped, opened from the popover's Board Settings… row and the new Board ▸ Board Settings… menu row. The three existing setup controls relocate — add-git (whose noteFormVisible lines now make the sheet the form-anchored failure surface), branch creation (a standing field; create-and-switch runs the identical settle sequence), and the commit-identity fields (the 2s visibility-scoped poll rides with them). The popover keeps the daily face and its postures; its Pro/mode-none section becomes header + door (.addGit renamed .noRepository). Availability is derived from the section inventory (Pro + mode none or git), so pro-m2's sections can't drift from the doors; the sheet's fields join the caret-chord disable set. The audit suite pins what the free fixture can reach; the sheet's own audit is manual until a tier override is ruled. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
192 lines
8.3 KiB
Swift
192 lines
8.3 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - The git-mode section's state
|
|
|
|
/// **What the popover's git section shows on a git-mode board** — a pure function of four facts, so
|
|
/// the surface 06-history-undo.md describes is assertable without a popover on screen.
|
|
///
|
|
/// It exists for the same reason `BoardGitSection.resolve` does one level up: the *posture* is the
|
|
/// part worth pinning, and the SwiftUI that renders it is not. Three rules live here —
|
|
///
|
|
/// - **A paused repository names its state and disables the controls** (06 ▸ Rules ▸ Abnormal repo
|
|
/// states: "Undo/Redo and the branch controls disable … the popover's git section names the state
|
|
/// plainly … and says resolving it belongs to the tool that created it").
|
|
/// - **A read-only board disables them too** (02-architecture.md ▸ The lock's scope, which names "the
|
|
/// popover's git controls" outright).
|
|
/// - **A switch in flight disables them**, so a second click cannot start a second checkout.
|
|
struct BoardGitBranchSurface: Equatable {
|
|
|
|
/// What the branch line reads — the branch name, the short hash on a detached HEAD
|
|
/// (`GitRepository.branchName` decides which), or the placeholder while the first read is in
|
|
/// flight.
|
|
let branchLabel: String
|
|
|
|
/// Whether that label is the placeholder rather than an answer.
|
|
let isReadingBranch: Bool
|
|
|
|
/// The pause's own sentence (`GitRepositoryPause.explanation`), or `nil` when the surface is live.
|
|
let pauseExplanation: String?
|
|
|
|
/// Whether the branch controls accept a click — the popover's switch picker, and the settings
|
|
/// sheet's create field (`BoardSettingsSheet`), which resolves this same surface so that a
|
|
/// paused repository, a read-only board and a switch in flight close both by one rule.
|
|
let controlsEnabled: Bool
|
|
|
|
/// The line the branch display is read as by VoiceOver.
|
|
var accessibilityLabel: String {
|
|
isReadingBranch ? "Reading branch" : "Branch \(branchLabel)"
|
|
}
|
|
|
|
static let placeholder = "…"
|
|
|
|
/// The second half of the paused sentence — 06's "says resolving it belongs to the tool that
|
|
/// created it", said in the app's own voice and paired with the promise that makes it safe to
|
|
/// wait: the app is not going to touch the repository behind the user's back.
|
|
static let pauseCaption =
|
|
"Finishing it belongs to the tool that started it; Lanework leaves the repository untouched."
|
|
|
|
static func resolve(
|
|
branch: String?,
|
|
pause: GitRepositoryPause?,
|
|
isSwitching: Bool,
|
|
isWritable: Bool
|
|
) -> BoardGitBranchSurface {
|
|
BoardGitBranchSurface(
|
|
branchLabel: branch ?? placeholder,
|
|
isReadingBranch: branch == nil,
|
|
pauseExplanation: pause?.explanation,
|
|
controlsEnabled: pause == nil && isWritable && !isSwitching
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - The git-mode section
|
|
|
|
/// **The popover's git section on a board that has a repository** (03-board-ui.md ▸ Board popover;
|
|
/// 06-history-undo.md ▸ Branch switching).
|
|
///
|
|
/// **The daily face, and only that** (the 2026-07-31 popover/sheet split): the branch display with
|
|
/// its **switch** picker, and the pause explanation when the surface is held. Branch *creation* and
|
|
/// the commit-identity fields left with the split — they are setup, and setup's home is the board
|
|
/// settings sheet (`BoardSettingsSheet`), which the section's Board Settings… row opens.
|
|
///
|
|
/// **Shaped for the half that is not here yet.** Remote tracking, Pull/Push and the status badges are
|
|
/// 07-sync-collab.md's own cards, and this section is arranged so they join as one more block under
|
|
/// the branch controls — nothing here is nested inside anything they would have to be pulled out of,
|
|
/// and nothing about the branch controls assumes there is no upstream to show beside them.
|
|
struct BoardGitControls: View {
|
|
|
|
let git: HistoryStore
|
|
|
|
/// The read-only lock's reach (02-architecture.md ▸ The lock's scope): a board that refuses writes
|
|
/// refuses a checkout most of all — it rewrites the tree the lock exists to stop describing.
|
|
let isEnabled: Bool
|
|
|
|
private var surface: BoardGitBranchSurface {
|
|
BoardGitBranchSurface.resolve(
|
|
branch: git.branch,
|
|
pause: git.committer?.pause,
|
|
isSwitching: git.switcher?.isSwitching ?? false,
|
|
isWritable: isEnabled
|
|
)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
branchRow
|
|
|
|
if let explanation = surface.pauseExplanation {
|
|
pauseNote(explanation)
|
|
}
|
|
|
|
if let failure = git.switcher?.lastFailure {
|
|
caption(failure.message, tone: .red)
|
|
}
|
|
}
|
|
// Every read the section needs, taken when it appears rather than held live: the popover is
|
|
// built fresh on each open (`BoardInfoWidget`), and none of these is a fact the board's
|
|
// watcher could deliver — `.git` is filtered out of the watch by design.
|
|
.task {
|
|
await git.refreshBranch()
|
|
await git.committer?.refreshPause()
|
|
await git.switcher?.refreshBranches()
|
|
}
|
|
}
|
|
|
|
// MARK: The branch line
|
|
|
|
/// The branch display and the switch, as one control: the line *is* the picker, which is what
|
|
/// makes "branch/source display and switching" one affordance rather than a label with a button
|
|
/// beside it.
|
|
///
|
|
/// **Switch entries and nothing else** since the 2026-07-31 split — the "New Branch…" entry that
|
|
/// used to close the menu is the settings sheet's standing Create field now. On a board with one
|
|
/// branch the menu is therefore *empty*, and that is left honest rather than papered over: the
|
|
/// label still reads the current branch and still disables with the rest of the git surface, so
|
|
/// the line goes on saying the one thing it is here to say.
|
|
private var branchRow: some View {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "arrow.triangle.branch")
|
|
.imageScale(.small)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Menu {
|
|
ForEach(otherBranches, id: \.self) { name in
|
|
Button(name) {
|
|
Task { await git.switcher?.switchTo(name) }
|
|
}
|
|
}
|
|
} label: {
|
|
Text(surface.branchLabel)
|
|
.font(.callout)
|
|
.foregroundStyle(surface.isReadingBranch ? .secondary : .primary)
|
|
}
|
|
.menuStyle(.borderlessButton)
|
|
.fixedSize()
|
|
.disabled(!surface.controlsEnabled)
|
|
.accessibilityLabel(surface.accessibilityLabel)
|
|
.accessibilityHint("Switch branches")
|
|
|
|
if git.switcher?.isSwitching == true {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
.accessibilityLabel("Switching branches")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Every local branch except the one already checked out — a picker offering the current branch
|
|
/// would be offering a no-op, and the switch refuses one anyway.
|
|
private var otherBranches: [String] {
|
|
(git.switcher?.branches ?? []).filter { $0 != git.branch }
|
|
}
|
|
|
|
// MARK: The pause
|
|
|
|
/// **The abnormal-state surface** (06 ▸ Rules ▸ Abnormal repo states) — deferred here from the
|
|
/// auto-commit card, which built the hold this explains.
|
|
///
|
|
/// Two sentences, both of them the design's: what the repository is doing, and whose job it is to
|
|
/// finish. Never a Repair button — "the app never mutates repo state it didn't create".
|
|
private func pauseNote(_ explanation: String) -> some View {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(explanation)
|
|
.font(.caption)
|
|
.foregroundStyle(.primary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
Text(BoardGitBranchSurface.pauseCaption)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
.accessibilityElement(children: .combine)
|
|
}
|
|
|
|
private func caption(_ text: String, tone: Color) -> some View {
|
|
Text(text)
|
|
.font(.caption)
|
|
.foregroundStyle(tone)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|