The strip grows a Sync tab — a standing placeholder, nothing in it yet
BoardInfoTab gains .sync last in the strip, rendering BoardSyncTabView's one honest caption. The tab claims the position where 07's remote half will live without ruling where its setup surfaces land — that stays the open Redesign card's question. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -53,7 +53,7 @@ One **style editor** component — a background palette grid and a curated symbo
|
|||||||
|
|
||||||
## Board popover
|
## 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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -313,15 +313,19 @@ func boardInfoTitlebarAccessory(
|
|||||||
|
|
||||||
// MARK: - Tabs
|
// MARK: - Tabs
|
||||||
|
|
||||||
/// The popover's three aspects, one tab each (03-board-ui.md § Board popover, the 2026-08-07 tab
|
/// The popover's aspects, one tab each (03-board-ui.md § Board popover, the 2026-08-07 tab
|
||||||
/// restructure — all three sessions settled): **Info** (`BoardInfoTabView`), **Theme**
|
/// restructure — all three original sessions settled): **Info** (`BoardInfoTabView`), **Theme**
|
||||||
/// (`BoardThemeTabView`), **Git** (`BoardGitTabView`). The raw values are the segmented control's own
|
/// (`BoardThemeTabView`), **Git** (`BoardGitTabView`) — and **Sync** (`BoardSyncTabView`), added
|
||||||
/// labels, so the strip needs no separate label function.
|
/// 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 {
|
enum BoardInfoTab: String, CaseIterable, Identifiable {
|
||||||
|
|
||||||
case info = "Info"
|
case info = "Info"
|
||||||
case theme = "Theme"
|
case theme = "Theme"
|
||||||
case git = "Git"
|
case git = "Git"
|
||||||
|
case sync = "Sync"
|
||||||
|
|
||||||
var id: Self { self }
|
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
|
// 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'
|
// 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`,
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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) {
|
Picker("Board configuration", selection: $tab) {
|
||||||
ForEach(BoardInfoTab.allCases) { tab in
|
ForEach(BoardInfoTab.allCases) { tab in
|
||||||
Text(tab.rawValue)
|
Text(tab.rawValue)
|
||||||
@@ -435,8 +439,9 @@ struct BoardInfoView: View {
|
|||||||
.padding(.horizontal, inset)
|
.padding(.horizontal, inset)
|
||||||
.padding(.top, inset)
|
.padding(.top, inset)
|
||||||
|
|
||||||
// The selected tab's surface — all three settled 2026-08-07, each in its own dedicated
|
// The selected tab's surface — the first three settled 2026-08-07, each in its own
|
||||||
// session and its own file. Each pads itself by `inset`, so the switch adds nothing.
|
// 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 {
|
switch tab {
|
||||||
case .info:
|
case .info:
|
||||||
BoardInfoTabView(store: store, inset: inset)
|
BoardInfoTabView(store: store, inset: inset)
|
||||||
@@ -444,6 +449,8 @@ struct BoardInfoView: View {
|
|||||||
BoardThemeTabView(store: store, inset: inset)
|
BoardThemeTabView(store: store, inset: inset)
|
||||||
case .git:
|
case .git:
|
||||||
BoardGitTabView(store: store, git: git, inset: inset)
|
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
|
// The style editor's popover width, taken from the editor rather than restated — the number
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
/// 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
|
/// 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*,
|
/// 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 —
|
/// 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")
|
@Suite("Board popover ▸ the tab strip")
|
||||||
struct BoardInfoTabStripTests {
|
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() {
|
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")
|
@Test("The raw values are the segmented control's own labels")
|
||||||
func labelsAreTheRawValues() {
|
func labelsAreTheRawValues() {
|
||||||
// The strip needs no separate label function, which is the only reason this enum is
|
// The strip needs no separate label function, which is the only reason this enum is
|
||||||
// `String`-backed at all.
|
// `String`-backed at all.
|
||||||
#expect(BoardInfoTab.allCases.map(\.rawValue) == ["Info", "Theme", "Git"])
|
#expect(BoardInfoTab.allCases.map(\.rawValue) == ["Info", "Theme", "Git", "Sync"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user