The Background tab becomes Theme — solid colors or patterns, presets only, chevron-paged

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-07 18:03:06 -04:00
parent fb96e30df0
commit 3d231d6454
10 changed files with 895 additions and 440 deletions
+163 -3
View File
@@ -2,9 +2,9 @@ import Foundation
import Testing
@testable import Kanban
/// The write half of generated board backgrounds: `BoardWriter.writeBoardImage` and
/// `BoardStore.applyGeneratedBackground` (03-board-ui.md § Styling Capabilities;
/// DESIGN/explorations/board-backgrounds.md).
/// The write half of the Theme tab's two picture-adjacent gestures: `BoardWriter.writeBoardImage` and
/// `BoardStore.applyGeneratedBackground` for Pattern, `BoardStore.applySolidBackground` for Solid
/// color (03-board-ui.md § Board popover Theme tab; DESIGN/explorations/board-backgrounds.md).
///
/// Like every other write suite here these drive a real writer or a real store over a real temp
/// board and read the **bytes on disk** back rather than the app's own read path: the claims are
@@ -384,3 +384,163 @@ struct GeneratedBackgroundUndoTests {
#expect(store.banners.signposts.isEmpty == false, "the skip says so on the strip")
}
}
// MARK: - Solid background
/// The Theme tab's Solid color half: `BoardStore.applySolidBackground` (03-board-ui.md § Board
/// popover Theme tab; `BoardThemeTabView.applySolid`). Modeled line-for-line on
/// `applyGeneratedBackground` minus the file write, so these suites mirror the write and undo suites
/// above with the one difference the method itself has: no picture, and `facets.png` when there is
/// one is deliberately left on disk rather than deleted.
@MainActor
@Suite("BoardStore ▸ applySolidBackground")
struct SolidBackgroundWriteTests {
@Test("The colour lands and there is no image subkey to point anywhere")
func writesTheColourAloneOnAPlainBoard() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, _) = try makeStore(fixture)
#expect(store.applySolidBackground(colorHex: "#E0E5EB"))
let after = try document(fixture)
#expect(after.background == .valid("#E0E5EB"))
#expect(after.backgroundImage == .missing)
#expect(try fixture.indexText("").contains("background: {color: \"#E0E5EB\"}"))
#expect(store.banners.oneShots.isEmpty)
}
/// The write's whole point on a board that already carries a generated picture: the colour
/// changes, the `image` subkey goes, and every other subkey the app does not own rides through
/// untouched.
@Test("An existing image subkey is removed and unrelated subkeys survive")
func removesTheImageSubkeyAndKeepsForeignOnes() throws {
let fixture = try makeBoard(background: "{blend: multiply, color: fern, image: facets.png, opacity: 0.5}")
defer { fixture.tearDown() }
let (store, _) = try makeStore(fixture)
store.applySolidBackground(colorHex: "#513D1A")
let text = try fixture.indexText("")
#expect(text.contains("blend: \"multiply\""))
#expect(text.contains("color: \"#513D1A\""))
#expect(text.contains("opacity: 0.5"))
#expect(!text.contains("image:"))
let after = try document(fixture)
#expect(after.background == .valid("#513D1A"))
#expect(after.backgroundImage == .missing)
}
/// **The deliberate half of the contract**: choosing a solid colour over a generated background
/// does not delete the picture on disk only the field that pointed at it. Undo has to have
/// something to point back to (`SolidBackgroundUndoTests.restoresAPriorGeneratedImage`), and even
/// without undo the file is the user's now, not litter the app cleans up on its own.
@Test("facets.png stays on disk when the board had one")
func leavesTheGeneratedFileOnDisk() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, _) = try makeStore(fixture)
store.applyGeneratedBackground(png: png, colorHex: "#E0E5EB")
store.applySolidBackground(colorHex: "#513D1A")
#expect(try fixture.data("facets.png") == png, "the bytes are untouched")
#expect(try document(fixture).backgroundImage == .missing, "only the field is gone")
}
@Test("One bracket for the one file it touches")
func oneBracketForTheWrite() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, _) = try makeStore(fixture)
var begins = 0
var ends = 0
store.watcherBrackets = (begin: { begins += 1 }, end: { ends += 1 })
store.applySolidBackground(colorHex: "#E0E5EB")
#expect(begins == 1)
#expect(ends == 1)
}
/// A locked board writes nothing at all.
@Test("A read-only board refuses before anything is written")
func refusesUnderTheLock() throws {
let fixture = try makeBoard(background: "{color: fern}")
defer { fixture.tearDown() }
let (store, _) = try makeStore(fixture)
store.enterVanishedRootLock()
#expect(store.applySolidBackground(colorHex: "#E0E5EB") == false)
#expect(try document(fixture).background == .valid("fern"))
}
}
@MainActor
@Suite("Undo ▸ solid background")
struct SolidBackgroundUndoTests {
/// The first solid colour's undo is a clean return: the board had no background, and afterwards
/// it has none again.
@Test("Undo removes the colour and redo puts it back")
func roundTrip() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
store.applySolidBackground(colorHex: "#E0E5EB")
#expect(history.undoActionName == "Restyle Board")
history.undo()
let undone = try document(fixture)
#expect(undone.background == .missing)
#expect(undone.backgroundImage == .missing)
#expect(!undone.contains(FrontmatterKeys.background))
history.redo()
let redone = try document(fixture)
#expect(redone.background == .valid("#E0E5EB"))
#expect(redone.backgroundImage == .missing)
}
/// **Both prior fields come back** the colour a board had before, and the generated image the
/// solid choice pointed away from which is what makes the file surviving on disk
/// (`SolidBackgroundWriteTests.leavesTheGeneratedFileOnDisk`) worth doing: an undo with nothing to
/// point back at would make the surviving bytes an orphan from the moment they landed.
@Test("A prior colour and generated image are both restored")
func restoresAPriorGeneratedImage() throws {
let fixture = try makeBoard(background: "{color: fern, image: facets.png}")
defer { fixture.tearDown() }
try fixture.file("facets.png", png)
let (store, history) = try makeStore(fixture)
store.applySolidBackground(colorHex: "#E0E5EB")
history.undo()
let undone = try document(fixture)
#expect(undone.background == .valid("fern"))
#expect(undone.backgroundImage == .valid("facets.png"))
#expect(try fixture.data("facets.png") == png, "undo restored the field, not new bytes")
}
/// A foreign edit to the field the step wrote stales it the same field-level predicate
/// `GeneratedBackgroundUndoTests.foreignEditStalesTheStep` pins for the generated path, applied to
/// the colour subkey this gesture owns.
@Test("A foreign edit to the colour subkey skips the undo")
func foreignEditStalesTheStep() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
store.applySolidBackground(colorHex: "#E0E5EB")
var foreign = try document(fixture)
foreign.setStyleValue("obsidian", for: FrontmatterKeys.background)
try fixture.item("", foreign.serialized())
history.undo()
#expect(try document(fixture).background == .valid("obsidian"))
#expect(store.banners.signposts.isEmpty == false, "the skip says so on the strip")
}
}