The board's symbol takes a tint — a 4×2 colour row under the picker's glyphs, and the glyph itself moves into the titlebar

iconColor stops being hand-written-only (user-ruled, superseding 03's
"schema yes, control no"): it rides StyleCommand.apply → applyStyle as
the third styled dimension — per-dimension no-op skip, one bracket, one
history step, ExpectedField.iconColor for staleness. The SymbolPicker
grows an opt-in colour row (leading None plus seven Palette.foregrounds
hues, None removes the key); the board popover is its one caller. The
window-title widget now draws the board's resolved glyph in that tint
beside the name. Doc realignment filed on the Redesign board (Minor).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-07 18:36:49 -04:00
parent 3d231d6454
commit 7ac34651a2
12 changed files with 342 additions and 23 deletions
+30 -4
View File
@@ -33,7 +33,8 @@ private func styled(order: String, title: String, keys: [String] = []) -> String
}
/// The board root's own `index.md` styled like any other item (`StyleTarget.board`), and carrying
/// an `iconColor` the app must never touch (schema yes, control no).
/// an `iconColor` a gesture that never names that dimension must leave alone (it stopped being
/// hand-written-only when the board popover's symbol picker grew its colour row, 2026-08-07).
private let boardIndex = """
---
schema: 1
@@ -143,13 +144,38 @@ struct StyleWriteTests {
let after = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)")
#expect(after.contains("background: {color: \"dark-teal\"}"))
#expect(after.contains("icon: flag"))
// "iconColor: resolved schema yes, control no" (03 § Styling Capabilities): the field
// renders when hand-written and the app offers no control for it, so a style write must
// carry it through untouched like any unknown key.
// The third dimension is a control now (the symbol picker's colour row), but a gesture
// that never names it must still carry it through untouched per-dimension `.keep`.
#expect(after.contains("iconColor: chalk"))
#expect(!after.contains("fern"), "the old value is replaced, not duplicated")
}
@Test("The colour row's dimension: iconColor sets as a plain scalar, removes as an absent key, and no-ops in place")
func iconColorIsTheThirdDimension() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let log = BracketLog()
log.attach(to: store)
// The fixture's board already carries `iconColor: carnation` re-choosing it is a no-op
// against the loaded snapshot, `skipsNoOps`'s rule at the third dimension.
store.applyStyle(to: .board, iconColor: .set("carnation"))
#expect(log.begins == 0, "re-choosing the tint an item already carries opens no bracket")
store.applyStyle(to: .board, iconColor: .set("fern"))
let set = try fixture.indexText("")
#expect(set.contains("iconColor: fern"))
#expect(!set.contains("carnation"), "the old tint is replaced, not duplicated")
#expect(!set.contains("icon: "), "the untouched icon dimension writes no key of its own")
store.applyStyle(to: .board, iconColor: .remove)
let removed = try fixture.indexText("")
#expect(!removed.contains("iconColor"))
#expect(!removed.contains("\"\""), "a removal is a missing key, never an empty string")
#expect(store.banners.oneShots.isEmpty)
}
@Test("The None and default wells remove their key rather than writing a blank value")
func removesTheKey() throws {
let fixture = try makeBoard()
+17
View File
@@ -23,6 +23,23 @@ struct SymbolPickerCatalogDefaultSetTests {
}
}
@Suite("SymbolPicker ▸ the colour row")
struct SymbolPickerColorSetTests {
@Test("Seven unique tints — the leading None plus these fills the 4×2 grid exactly")
func shape() {
#expect(SymbolPickerCatalog.colorSet.count
== SymbolPickerLayout.colorColumns * SymbolPickerLayout.colorRows - 1)
#expect(Set(SymbolPickerCatalog.colorSet).count == SymbolPickerCatalog.colorSet.count)
}
@Test("Every entry is a palette name that resolves — pins the list against typos and palette drift")
func everyNameResolves() {
let missing = SymbolPickerCatalog.colorSet.filter { Palette.color(named: $0) == nil }
#expect(missing.isEmpty, "unresolvable palette names: \(missing)")
}
}
@Suite("SymbolPicker ▸ filter")
struct SymbolPickerFilterTests {
+22
View File
@@ -43,6 +43,7 @@ order: 3072
project: lanework # agent overlay
background: {color: blue}
icon: star
iconColor: chalk
created: 2026-01-01T09:00:00Z
---
Styled body.
@@ -249,6 +250,27 @@ struct RestyleUndoTests {
let undone = try document(fixture, card3Path)
#expect(undone.background.value == "blue")
#expect(undone.icon.value == "star", "a dimension the gesture did not touch is not touched back")
#expect(undone.iconColor.value == "chalk", "the third dimension rides the same rule")
}
@Test("The colour row's dimension round-trips: a tint undoes to the prior tint, and to absence where there was none")
func iconColorRoundTrip() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
// card3 carries `iconColor: chalk`; card1 carries none.
store.applyStyle(to: .items([card3]), iconColor: .set("fern"))
#expect(try document(fixture, card3Path).iconColor.value == "fern")
history.undo()
#expect(try document(fixture, card3Path).iconColor.value == "chalk")
history.redo()
#expect(try document(fixture, card3Path).iconColor.value == "fern")
store.applyStyle(to: .items([card1]), iconColor: .set("carnation"))
history.undo()
#expect(try document(fixture, card1Path).iconColor.isMissing, "no prior tint undoes to no key at all")
}
@Test("A styling batch is one step, with a plural title")