Give the popover git slot the contextual Pro posture

12's settled ruling supersedes the m4 placeholder: on an ordinary board
the git section is simply absent — the popover is complete in itself —
and on a board carrying an inert .git it shows only the calm one-line
note, 'This board has a git history. Lanework Pro works with it.' The
detection is a pure one-line seam checking the board root at popover
open, deliberately non-live: .git is filtered from the watch by design,
so there is no reload to hang a live fact off, and a quiet signpost
self-corrects on next open.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 13:22:40 -04:00
parent f37892c9a9
commit d61ce422a3
3 changed files with 89 additions and 34 deletions
+52 -29
View File
@@ -4,10 +4,11 @@ import SwiftUI
/// **The board popover** "the one board-level surface" (03-board-ui.md § Board popover), and the /// **The board popover** "the one board-level surface" (03-board-ui.md § Board popover), and the
/// widget in the window's titlebar that opens it. /// widget in the window's titlebar that opens it.
/// ///
/// Three sections, in the design's own order: the board rename, the embedded style editor aimed at /// Two sections, in the design's own order: the board rename and the embedded style editor aimed at
/// the board, and the git slot. The first two are this milestone's; the third is a *reserved place* /// the board. A third, contextual note joins them only on a board that carries an inert `.git` see
/// see `BoardGitSlot` for what m7 grows there and why an honest placeholder beats an absent /// `BoardGitNote` for the wording and why base's posture there is a quiet signpost rather than a
/// section. /// standing section (12-editions.md § Base and `.git`, ruled 2026-07-27, superseding the earlier
/// reserved-slot placeholder this file once carried).
/// ///
/// ### One home, deliberately /// ### One home, deliberately
/// ///
@@ -120,7 +121,8 @@ func boardInfoTitlebarAccessory(
// MARK: - The popover's content // MARK: - The popover's content
/// The three sections, one view (03-board-ui.md § Board popover). /// Two sections and, on a `.git`-bearing board, a contextual note one view (03-board-ui.md §
/// Board popover; 12-editions.md § Base and `.git`).
/// ///
/// Width is the style editor's 268 points, the number that keeps the Style popover narrow enough /// Width is the style editor's 268 points, the number that keeps the Style popover narrow enough
/// to sit beside a card so the embedded editor lays out here exactly as it does at its other two /// to sit beside a card so the embedded editor lays out here exactly as it does at its other two
@@ -130,10 +132,21 @@ struct BoardInfoView: View {
let store: BoardStore let store: BoardStore
let recents: StyleRecents let recents: StyleRecents
/// Whether this board carries a `.git` checked once, off disk, when the view is built (which
/// is every time the popover opens, since `BoardInfoWidget` hands `.popover` a fresh instance).
/// See `BoardGitNote.hasGitDirectory(at:)` for why a live-updating fact isn't needed here.
private let hasGitDirectory: Bool
/// The style editor brings its own padding, so the sections around it carry the same number by /// The style editor brings its own padding, so the sections around it carry the same number by
/// hand instead of an outer padding that would double up on it. /// hand instead of an outer padding that would double up on it.
private let inset: CGFloat = 14 private let inset: CGFloat = 14
init(store: BoardStore, recents: StyleRecents) {
self.store = store
self.recents = recents
self.hasGitDirectory = BoardGitNote.hasGitDirectory(at: store.rootURL)
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
@@ -155,19 +168,21 @@ struct BoardInfoView: View {
StyleEditorView(store: store, recents: recents, target: .board) StyleEditorView(store: store, recents: recents, target: .board)
} }
// Contextual, not standing (12-editions.md, settled 2026-07-27): an ordinary board adds
// nothing here at all no header, no divider, no placeholder and the popover ends at
// Styling, complete in itself. Only a board that actually carries an inert `.git` earns
// this closing note.
if hasGitDirectory {
Divider() Divider()
BoardGitNote()
VStack(alignment: .leading, spacing: 8) {
sectionHeader("Git")
BoardGitSlot()
}
.padding(inset) .padding(inset)
} }
}
.frame(width: 268) .frame(width: 268)
} }
/// The section titles, matching the style editor's own headers so the popover reads as one /// The section titles, matching the style editor's own headers so the popover reads as one
/// surface rather than three borrowed ones. /// surface rather than borrowed ones.
private func sectionHeader(_ title: String) -> some View { private func sectionHeader(_ title: String) -> some View {
Text(title) Text(title)
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
@@ -252,31 +267,39 @@ private struct BoardRenameField: View {
// MARK: - Git // MARK: - Git
/// The git section **a reserved slot, not a feature** (03-board-ui.md § Board popover). /// The contextual git note **a quiet signpost, not a feature** (12-editions.md § Base and `.git`,
/// settled 2026-07-27). Base has no git integration and never will (that is Pro's), so this is not a
/// grow-in-place slot the way the old `BoardGitSlot` placeholder was: there is nothing here to grow.
/// Base's whole git story is one line, shown only when it is true.
/// ///
/// Every board is mode-none today, because the app has no git at all yet: there is no detection, no /// On an ordinary board `BoardInfoView` never instantiates this type at all the section is
/// repository, and nothing to init. So this renders the one honest thing a mode-none board can say /// *absent*, matching the card window's absent History section (12: "absent, no placeholder"). Only
/// and offers its action disabled, rather than hiding the section an absent section would teach /// a board that carries an inert `.git` (12-editions.md § The inert posture: "any `.git` is inert"
/// that the board popover has two parts, and the design says it has three. /// base never reads or writes it, whether the board's own or a Pro user's) 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.
/// ///
/// m7-git: this slot grows the whole inventory in 11-command-nexus.md § Configuration controls /// Not `private`: `hasGitDirectory(at:)` is the pure seam `BoardInfoPopoverTests.swift` pins directly
/// Board popover add-git on mode none, the this-board-lives-inside-a-repository explanation on /// (a fixture board with `.git` true, without false), which needs it visible past this file even
/// repo-nested boards (06-history-undo.md), branch display/switch/create, the commit-identity /// though nothing outside `BoardInfoPopover.swift` calls it in the app itself.
/// name/email fields, add/change remote, the credential and SSH-key surfaces (machine key with Copy struct BoardGitNote: View {
/// and Verify, key import, the per-host picker, confirm-gated regeneration), the
/// Authentication-needed badge, and ahead/behind with Pull/Push and push-on-commit
/// (07-sync-collab.md). The mode-aware branching starts here, where this placeholder is.
private struct BoardGitSlot: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { Text("This board has a git history. Lanework Pro works with it.")
Text("This board has no repository. Version history, undo, and sync arrive with Lanework's git integration.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
}
Button("Add Git…") {} /// Whether `boardRoot` carries a `.git` entry base's entire detection story, and a deliberately
.disabled(true) /// 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)
} }
} }
+32
View File
@@ -0,0 +1,32 @@
import Foundation
import Testing
@testable import Kanban
/// **The board popover's git-note detection** (12-editions.md § Base and `.git`, ruled 2026-07-27):
/// base's whole git story is one line, shown only on a board that carries an inert `.git`. The seam
/// that decides *whether* to show it `BoardGitNote.hasGitDirectory(at:)` is a pure, read-only
/// `FileManager` check, pinned here against real bytes on disk. Everything else about the note (its
/// wording, its placement in `BoardInfoView`) is SwiftUI rendering and deliberately untested.
struct BoardInfoPopoverTests {
@Test("A board with a .git at its root reports true")
func trueForABoardWithGit() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
#expect(BoardGitNote.hasGitDirectory(at: fixture.root))
}
@Test("An ordinary board with no .git reports false")
func falseForAnOrdinaryBoard() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
#expect(!BoardGitNote.hasGitDirectory(at: fixture.root))
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ Lanework is in early development. This list tracks what has actually shipped and
- **Raw source** — View ▸ Raw Source (⌥⌘E) swaps the card window's whole content area — title, body and sidebar — for the literal `index.md` in a monospaced editor with Cancel and Apply. It's the escape hatch that keeps everything reachable in-app: unknown keys an agent added, hand-written comments, exotic YAML the app has no control for. Entering flushes whatever you were typing and then reads the file fresh off disk, never an in-memory copy. Apply validates through the very same fail-fast parse the loader uses — a broken proposal stops with a detailed alert naming the line, source mode stays open with your text, and the file on disk is untouched — and a valid one is written byte for byte, the only write in the app that neither stamps `modified` nor clears a `modified-by` you typed or kept, because you wrote those bytes and nothing may quietly edit them. The reload then refreshes every window. Escape is Cancel, ⌘↩ is Apply, ⌥⌘E toggled off applies too, Return just types; Cancel and closing the window discard without ceremony, and a card deleted out from under an open buffer discards it rather than letting a stale Apply undelete the card. ⌘E stands down while source mode is up, ⌘F still finds, and ⌘Z is the editor's own undo. A file that isn't valid UTF-8 declines to open as source rather than showing you a lossy guess of it. - **Raw source** — View ▸ Raw Source (⌥⌘E) swaps the card window's whole content area — title, body and sidebar — for the literal `index.md` in a monospaced editor with Cancel and Apply. It's the escape hatch that keeps everything reachable in-app: unknown keys an agent added, hand-written comments, exotic YAML the app has no control for. Entering flushes whatever you were typing and then reads the file fresh off disk, never an in-memory copy. Apply validates through the very same fail-fast parse the loader uses — a broken proposal stops with a detailed alert naming the line, source mode stays open with your text, and the file on disk is untouched — and a valid one is written byte for byte, the only write in the app that neither stamps `modified` nor clears a `modified-by` you typed or kept, because you wrote those bytes and nothing may quietly edit them. The reload then refreshes every window. Escape is Cancel, ⌘↩ is Apply, ⌥⌘E toggled off applies too, Return just types; Cancel and closing the window discard without ceremony, and a card deleted out from under an open buffer discards it rather than letting a stale Apply undelete the card. ⌘E stands down while source mode is up, ⌘F still finds, and ⌘Z is the editor's own undo. A file that isn't valid UTF-8 declines to open as source rather than showing you a lossy guess of it.
- **The board popover** — a quiet chevron beside the window title (File ▸ Board Info, ⌘I, which toggles it) opens the board's one configuration surface. Renaming edits the board's frontmatter `title` and nothing else — the folder is never renamed, so the app's display name and the Finder document name are free to diverge — and clearing the field removes the key entirely, dropping the window title back to the folder name rather than to "Untitled"; the edit commits on Return and on click-away, Escape abandons it, and an unchanged title writes nothing at all. Below it sits the same style editor every other anchor uses, aimed permanently at the board, and below that a Git section that says plainly that this board has no repository yet. The read-only lock disables the surface without closing it. - **The board popover** — a quiet chevron beside the window title (File ▸ Board Info, ⌘I, which toggles it) opens the board's one configuration surface. Renaming edits the board's frontmatter `title` and nothing else — the folder is never renamed, so the app's display name and the Finder document name are free to diverge — and clearing the field removes the key entirely, dropping the window title back to the folder name rather than to "Untitled"; the edit commits on Return and on click-away, Escape abandons it, and an unchanged title writes nothing at all. Below it sits the same style editor every other anchor uses, aimed permanently at the board; on an ordinary board the popover ends there, and only a board carrying a `.git` gets a closing note — "This board has a git history. Lanework Pro works with it." The read-only lock disables the surface without closing it.
- **Customizable toolbars** — both windows carry a real macOS toolbar: right-click ▸ Customize Toolbar…, drag to rearrange, the system overflow, and the Icon and Text / Icon Only / Text Only display options, with your arrangement remembered across launches. They are pure enhancement — every item is a menu command with a shortcut, so removing all of them costs you nothing but a click. The board ships with the search field alone, trailing, and offers New Card, New Lane, Undo, Redo and Show Trash in the palette (Undo and Redo validate exactly as the Edit menu's rows do, and keep static labels because the menu's titles rewrite themselves); the board popover deliberately has no item, since the window-title chevron is its home. Take the search field out and ⌘F still summons search — the field appears in a strip just under the title bar and stays until the search clears, keeping the keyboard while you type. The card window ships Edit Body · Raw Source · Add Attachment, the first two as toggles showing their on-state, with Edit Body disabling while raw source is up and Add Attachment live in every mode. - **Customizable toolbars** — both windows carry a real macOS toolbar: right-click ▸ Customize Toolbar…, drag to rearrange, the system overflow, and the Icon and Text / Icon Only / Text Only display options, with your arrangement remembered across launches. They are pure enhancement — every item is a menu command with a shortcut, so removing all of them costs you nothing but a click. The board ships with the search field alone, trailing, and offers New Card, New Lane, Undo, Redo and Show Trash in the palette (Undo and Redo validate exactly as the Edit menu's rows do, and keep static labels because the menu's titles rewrite themselves); the board popover deliberately has no item, since the window-title chevron is its home. Take the search field out and ⌘F still summons search — the field appears in a strip just under the title bar and stays until the search clears, keeping the keyboard while you type. The card window ships Edit Body · Raw Source · Add Attachment, the first two as toggles showing their on-state, with Edit Body disabling while raw source is up and Add Attachment live in every mode.