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
+33 -13
View File
@@ -1698,8 +1698,8 @@ public final class BoardStore: HealHost {
// MARK: - Styling
/// One item a style gesture is about to act on: where its `index.md` is, and what the two styled
/// keys currently say there.
/// One item a style gesture is about to act on: where its `index.md` is, and what the three
/// styled keys currently say there.
///
/// The editor reads these for its per-dimension current-value display (`StyleFieldState.resolve`)
/// and `applyStyle` reads the *same* values to decide what is a no-op, so the display and the
@@ -1710,6 +1710,7 @@ public final class BoardStore: HealHost {
public let folder: URL
public let background: FieldValue<String>
public let icon: FieldValue<String>
public let iconColor: FieldValue<String>
}
/// The live items `target` names, in display order lanes left to right, each lane's cards top
@@ -1727,7 +1728,8 @@ public final class BoardStore: HealHost {
id: nil,
folder: rootURL,
background: snapshot.background,
icon: snapshot.icon
icon: snapshot.icon,
iconColor: snapshot.iconColor
)]
case let .items(ids):
@@ -1739,7 +1741,8 @@ public final class BoardStore: HealHost {
id: lane.id,
folder: laneFolder,
background: lane.background,
icon: lane.icon
icon: lane.icon,
iconColor: lane.iconColor
))
}
for card in lane.cards where ids.contains(card.id) {
@@ -1747,7 +1750,8 @@ public final class BoardStore: HealHost {
id: card.id,
folder: laneFolder.appendingPathComponent(card.id.rawValue),
background: card.background,
icon: card.icon
icon: card.icon,
iconColor: card.iconColor
))
}
}
@@ -1790,9 +1794,10 @@ public final class BoardStore: HealHost {
/// no-ops is dropped entirely; and a gesture that changes nothing anywhere never opens the
/// bracket at all.
///
/// **`iconColor` is not a parameter, and that is the design**: it is "resolved schema yes,
/// control no" (§ Capabilities). The field renders when hand-written and the app offers no
/// control for it, so there is nothing here to pass.
/// **`iconColor` joined as the third dimension with the board popover's colour row**
/// (2026-08-07). It had been "resolved schema yes, control no" (§ Capabilities); the symbol
/// picker's colour grid is the control that ended that, and it rides this funnel exactly as the
/// other two dimensions do per-dimension no-op skipping, one bracket, one history step.
///
/// Failure is `performWrite`'s: the banner is posted before the rethrow, which is swallowed here
/// like every other gesture with no second thing to do. A batch that fails partway leaves the
@@ -1816,6 +1821,7 @@ public final class BoardStore: HealHost {
to target: StyleTarget,
background: StyleChange = .keep,
icon: StyleChange = .keep,
iconColor: StyleChange = .keep,
on window: CardWindowUndo? = nil
) {
// A session gesture anchors to the card, everything else to the folder it resolved (above).
@@ -1829,13 +1835,16 @@ public final class BoardStore: HealHost {
anchor: HistoryAnchor,
background: StyleChange,
icon: StyleChange,
iconColor: StyleChange,
priorBackground: FieldValue<String>,
priorIcon: FieldValue<String>
priorIcon: FieldValue<String>,
priorIconColor: FieldValue<String>
)] = styleSubjects(of: target)
.compactMap { subject in
let background = Self.effective(background, against: subject.background)
let icon = Self.effective(icon, against: subject.icon)
guard background != .keep || icon != .keep else { return nil }
let iconColor = Self.effective(iconColor, against: subject.iconColor)
guard background != .keep || icon != .keep || iconColor != .keep else { return nil }
let anchor: HistoryAnchor = if anchorsByIdentity, let id = subject.id {
.card(id)
} else {
@@ -1847,8 +1856,10 @@ public final class BoardStore: HealHost {
anchor: anchor,
background: background,
icon: icon,
iconColor: iconColor,
priorBackground: subject.background,
priorIcon: subject.icon
priorIcon: subject.icon,
priorIconColor: subject.iconColor
)
}
guard !edits.isEmpty else { return }
@@ -1866,6 +1877,7 @@ public final class BoardStore: HealHost {
) { document in
Self.apply(edit.background, to: FrontmatterKeys.background, in: &document)
Self.apply(edit.icon, to: FrontmatterKeys.icon, in: &document)
Self.apply(edit.iconColor, to: FrontmatterKeys.iconColor, in: &document)
}
}
}
@@ -1891,14 +1903,20 @@ public final class BoardStore: HealHost {
subject: subject,
on: window,
undoExpects: edits.map {
.present($0.anchor, fields: Self.styledFields(background: $0.background, icon: $0.icon))
.present($0.anchor, fields: Self.styledFields(
background: $0.background,
icon: $0.icon,
iconColor: $0.iconColor
))
},
redoExpects: edits.map {
.present($0.anchor, fields: Self.restoredStyleFields(
background: $0.background,
priorBackground: $0.priorBackground,
icon: $0.icon,
priorIcon: $0.priorIcon
priorIcon: $0.priorIcon,
iconColor: $0.iconColor,
priorIconColor: $0.priorIconColor
))
}
) { store in
@@ -1910,6 +1928,7 @@ public final class BoardStore: HealHost {
) { document in
Self.restore(edit.priorBackground, to: FrontmatterKeys.background, in: &document)
Self.restore(edit.priorIcon, to: FrontmatterKeys.icon, in: &document)
Self.restore(edit.priorIconColor, to: FrontmatterKeys.iconColor, in: &document)
}
}
} redo: { store in
@@ -1921,6 +1940,7 @@ public final class BoardStore: HealHost {
) { document in
Self.apply(edit.background, to: FrontmatterKeys.background, in: &document)
Self.apply(edit.icon, to: FrontmatterKeys.icon, in: &document)
Self.apply(edit.iconColor, to: FrontmatterKeys.iconColor, in: &document)
}
}
}