diff --git a/DESIGN/03-board-ui.md b/DESIGN/03-board-ui.md index fa1cce8..eaebb18 100644 --- a/DESIGN/03-board-ui.md +++ b/DESIGN/03-board-ui.md @@ -53,7 +53,7 @@ One **style editor** component — a background palette grid and a curated symbo ## Board popover -**The tabbed popover (2026-08-07, restructure complete — all three tab sessions settled).** The symbol/name header stays at the top — the board's glyph with its tint row beside the rename field; below it sit tabs — **Info**, **Theme**, **Git** — each the settings surface for one aspect of board configuration, each settled in its own dedicated session. **Tab membership is the git posture's** (the Git session's ruling): the Git tab joins the strip only when the git section has something true to say — `BoardGitSection` resolves to anything but absent — so a free-tier board with no `.git` shows Info | Theme alone. This carries 12-editions.md's "absent, no placeholder" rule up to the tab strip: a standing Git tab on every free board would be the standing ad for Pro that 12 forbids. *(Pivot 2026-08-07, same day — 12: git left the paywall, so the absent posture is unreachable and every board carries all three tabs. The membership rule stands structurally — the strip still asks the posture — it just never hears "absent" anymore.)* **Selection resets to Info on every open** (ruled in the Git session, closing the question the earlier tab sessions deferred): the popover is transient and Info is the board's face — and a remembered tab could strand selection on a tab the next posture doesn't offer. +**The tabbed popover (2026-08-07, restructure complete — all three tab sessions settled).** The symbol/name header stays at the top — the board's glyph with its tint row beside the rename field; below it sit tabs — **Info**, **Theme**, **Git** — each the settings surface for one aspect of board configuration, each settled in its own dedicated session. **Tab membership is the git posture's** (the Git session's ruling): the Git tab joins the strip only when the git section has something true to say — `BoardGitSection` resolves to anything but absent — so a free-tier board with no `.git` shows Info | Theme alone. This carries 12-editions.md's "absent, no placeholder" rule up to the tab strip: a standing Git tab on every free board would be the standing ad for Pro that 12 forbids. *(Pivot 2026-08-07, same day — 12: git left the paywall, so the absent posture is unreachable and every board carries all three tabs. The membership rule stands structurally — the strip still asks the posture — it just never hears "absent" anymore.)* **Selection resets to Info on every open** (ruled in the Git session, closing the question the earlier tab sessions deferred): the popover is transient and Info is the board's face — and a remembered tab could strand selection on a tab the next posture doesn't offer. *(Added 2026-08-07, the same night the settings sheet retired: a fourth tab, **Sync** — a standing placeholder, last in the strip, rendering one honest "Nothing here yet." caption. It claims the position where the remote half of the git story will live — tracking, Pull/Push, the badges, and whatever home 07-sync-collab.md's setup surfaces are ruled into — without ruling any of that: the open Redesign card owns the question, and the placeholder is deliberately empty rather than a greyed-out preview.)* **The Theme tab** (settled 2026-08-07, its dedicated session; reworked same day from the first "Background" cut — the style-editor embed was in, then ruled out): a **preset-only** surface — no palette grid, no "Board" subtitle, no manual controls (the manual surface is Style… with nothing selected; the raw file remains the image escape hatch). One **Solid color / Pattern** segmented choice at the top, then the filters that apply to the chosen kind, then one carousel of the eight wheel hues with tall skinny chevrons flanking it (compact-chevron paging buttons, ~three swatches per press, each disabling at its end of the strip). **Solid color** shows Tone (defaults to the current appearance) and Saturation only; the carousel holds flat swatches — each hue at the selected tonality's base saturation and lightness, exactly the primary color the matching facets recipe would write — and clicking one sets `background.color` and *removes* `background.image` in one write (the generated PNG stays on disk so undo can restore the field that pointed at it). **Pattern** is the faceted-background picker (DESIGN/explorations/board-backgrounds.md ▸ Faceted gallery, the reviewed recipe): four filters — Tone and Saturation shared with Solid, plus Colors mono/duo/trio and Mesh coarse/medium/fine — over rendered previews, one seed per hue, plus a Reroll button that re-mints the seeds (filters change the treatment, Reroll the geometry; preview and file share a seed, so what's clicked is what lands). Clicking a pattern swatch renders the recipe at the 3072 px decode ceiling off-main and lands it in one write: the PNG into the board root as `facets.png` (Finder-ladder rename only when a foreign file owns the name), `background.image` pointed at it, and `background.color` set to the recipe's primary color — the underlay that stands in while the image decodes or if the file ever goes missing. The mode opens on whichever kind the board currently wears (Pattern when `background.image` is the generated file, Solid otherwise). **Backgrounds ship as static pixels, never live-rendered views** (the perf/sync ruling, 2026-08-07): the generator runs at pick time, the render loop only ever composites a decoded bitmap. Native undo restores the two fields, not overwritten bytes — regenerating over our own PNG is destructive, documented, and accepted. The whole tab disables under the read-only lock as one surface. diff --git a/Kanban/UI/Board/BoardInfoPopover.swift b/Kanban/UI/Board/BoardInfoPopover.swift index c6be33a..fe04c18 100644 --- a/Kanban/UI/Board/BoardInfoPopover.swift +++ b/Kanban/UI/Board/BoardInfoPopover.swift @@ -313,15 +313,19 @@ func boardInfoTitlebarAccessory( // MARK: - Tabs -/// The popover's three aspects, one tab each (03-board-ui.md § Board popover, the 2026-08-07 tab -/// restructure — all three sessions settled): **Info** (`BoardInfoTabView`), **Theme** -/// (`BoardThemeTabView`), **Git** (`BoardGitTabView`). The raw values are the segmented control's own -/// labels, so the strip needs no separate label function. +/// The popover's aspects, one tab each (03-board-ui.md § Board popover, the 2026-08-07 tab +/// restructure — all three original sessions settled): **Info** (`BoardInfoTabView`), **Theme** +/// (`BoardThemeTabView`), **Git** (`BoardGitTabView`) — and **Sync** (`BoardSyncTabView`), added +/// 2026-08-07 as a standing placeholder: the strip claims the position now, the surface says +/// honestly that nothing lives there yet, and 07-sync-collab.md's cards are where its contents get +/// ruled. The raw values are the segmented control's own labels, so the strip needs no separate +/// label function. enum BoardInfoTab: String, CaseIterable, Identifiable { case info = "Info" case theme = "Theme" case git = "Git" + case sync = "Sync" var id: Self { self } @@ -334,7 +338,7 @@ enum BoardInfoTab: String, CaseIterable, Identifiable { // absent posture is unreachable and every board carries all three tabs. The membership rule // stands structurally — the strip still asks the posture — it just never hears 'absent' // anymore." So there is no `available(…)` filter here to ask it with: membership is `allCases`, - // in `allCases`' own order (Info, Theme, Git), which is what the filter answered anyway. + // in `allCases`' own order (Info, Theme, Git, Sync), which is what the filter answered anyway. } // MARK: - The popover's content @@ -424,7 +428,7 @@ struct BoardInfoView: View { // the control's accessibility name. It iterates `allCases` — the strip's membership is // still the board's git posture in principle (the Git session's ruling), and since // 12-editions.md ▸ PIVOT 2026-08-07 that posture is never "absent", so every board - // carries all three tabs (03-board-ui.md ▸ Board popover, the same-day pivot note). + // carries the whole strip (03-board-ui.md ▸ Board popover, the same-day pivot note). Picker("Board configuration", selection: $tab) { ForEach(BoardInfoTab.allCases) { tab in Text(tab.rawValue) @@ -435,8 +439,9 @@ struct BoardInfoView: View { .padding(.horizontal, inset) .padding(.top, inset) - // The selected tab's surface — all three settled 2026-08-07, each in its own dedicated - // session and its own file. Each pads itself by `inset`, so the switch adds nothing. + // The selected tab's surface — the first three settled 2026-08-07, each in its own + // dedicated session and its own file; Sync is that day's standing placeholder. Each pads + // itself by `inset`, so the switch adds nothing. switch tab { case .info: BoardInfoTabView(store: store, inset: inset) @@ -444,6 +449,8 @@ struct BoardInfoView: View { BoardThemeTabView(store: store, inset: inset) case .git: BoardGitTabView(store: store, git: git, inset: inset) + case .sync: + BoardSyncTabView(inset: inset) } } // The style editor's popover width, taken from the editor rather than restated — the number diff --git a/Kanban/UI/Board/BoardSyncTabView.swift b/Kanban/UI/Board/BoardSyncTabView.swift new file mode 100644 index 0000000..3b52bd4 --- /dev/null +++ b/Kanban/UI/Board/BoardSyncTabView.swift @@ -0,0 +1,32 @@ +import SwiftUI + +/// **The board popover's Sync tab — a standing placeholder** (03-board-ui.md § Board popover, +/// added 2026-08-07, the same night the settings sheet retired). +/// +/// The tab exists ahead of its contents, deliberately: the strip claims the position where the +/// remote half of the git story will live — tracking, Pull/Push, the status badges, and whatever +/// home the setup surfaces (remote, credentials, SSH) are ruled into — so that when +/// 07-sync-collab.md's cards land they land *in* a surface rather than re-arguing the strip. None +/// of that is ruled by this file: the open question is the Redesign board's +/// ("Rule a home for 07's remote and credential setup surfaces"), and a placeholder that decided it +/// by accident would be the worst way to answer it. +/// +/// What renders meanwhile is one honest sentence in the posture notes' register +/// (`BoardGitTabView`'s siblings): the fact, stated plainly — never a mock control, never a +/// greyed-out preview of features that do not exist (06-history-undo.md ▸ Rules: teach, never look +/// broken). +struct BoardSyncTabView: View { + + /// The popover's own padding figure (`BoardInfoView.inset`), matching every other tab's + /// parameter — the tab pads by this amount instead of restating the derivation. + let inset: CGFloat + + var body: some View { + Text("Nothing here yet.") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + .padding(inset) + .frame(maxWidth: .infinity, alignment: .leading) + } +} diff --git a/KanbanTests/BoardInfoPopoverTests.swift b/KanbanTests/BoardInfoPopoverTests.swift index 919b29c..796162e 100644 --- a/KanbanTests/BoardInfoPopoverTests.swift +++ b/KanbanTests/BoardInfoPopoverTests.swift @@ -78,27 +78,29 @@ struct BoardGitSectionTests { } } -/// **The tab strip carries all three tabs, always** (03-board-ui.md ▸ Board popover, the tabbed- +/// **The tab strip carries every tab, always** (03-board-ui.md ▸ Board popover, the tabbed- /// popover paragraph as the 2026-08-07 pivot note leaves it): "the absent posture is unreachable and /// every board carries all three tabs. The membership rule stands structurally — the strip still asks -/// the posture — it just never hears 'absent' anymore." +/// the posture — it just never hears 'absent' anymore." (Three became four the same night: **Sync** +/// joined as a standing placeholder, membership still `allCases`.) /// /// So there is no `available(…)` seam left to pin; what is worth keeping is the strip's *order*, /// which the popover's `Picker` takes from `allCases` and which the Git session fixed: Info first — -/// it is the board's face and the tab selection resets to it on every open — then Theme, then Git. +/// it is the board's face and the tab selection resets to it on every open — then Theme, then Git, +/// then the Sync placeholder at the end of the line, where a surface with nothing in it belongs. @Suite("Board popover ▸ the tab strip") struct BoardInfoTabStripTests { - @Test("Info, Theme, Git — in that order, on every board") + @Test("Info, Theme, Git, Sync — in that order, on every board") func theStripIsTheWholeSet() { - #expect(BoardInfoTab.allCases == [.info, .theme, .git]) + #expect(BoardInfoTab.allCases == [.info, .theme, .git, .sync]) } @Test("The raw values are the segmented control's own labels") func labelsAreTheRawValues() { // The strip needs no separate label function, which is the only reason this enum is // `String`-backed at all. - #expect(BoardInfoTab.allCases.map(\.rawValue) == ["Info", "Theme", "Git"]) + #expect(BoardInfoTab.allCases.map(\.rawValue) == ["Info", "Theme", "Git", "Sync"]) } }