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
+75
View File
@@ -2050,6 +2050,81 @@ public final class BoardStore: HealHost {
return true
}
// MARK: - Solid background
/// **Applies a solid colour background to this board** the `background.color` subkey set and
/// the `background.image` subkey removed, in one bracket (03-board-ui.md § Styling Theme tab;
/// the Solid color half of `BoardThemeTabView`'s picker).
///
/// Modeled line-for-line on `applyGeneratedBackground` **minus the file write**: there is no
/// picture to land, so the bracket holds a single `updateIndex` rather than an image write ahead
/// of one. Everything else is that gesture's, restated here rather than shared because the two
/// brackets differ in exactly the one place that matters (one write versus two): the same
/// undo-restores-both-fields shape, the same `WriteOperation.setBoardBackground`, the same
/// swallowed failure.
///
/// ### `facets.png` survives on disk
///
/// Choosing a solid colour over a generated background does **not** delete the picture the
/// generator wrote. Undo restores the `image` *field*, and a field cannot point an undo back at
/// bytes this gesture just erased so the file has to survive for the same reason
/// `applyGeneratedBackground`'s own overwrite-in-place does. A board that regenerates after
/// choosing solid still finds `facets.png` free to overwrite in place; the file becomes an orphan
/// only when nothing in the frontmatter ever points at it again, which is the same quiet leftover
/// a hand-deleted `image:` line already leaves.
///
/// - Parameter colorHex: the solid colour to write, `#RRGGBB` one of the Theme tab's Solid color
/// swatches, which read `FacetsRecipe.primaryColorHex` at the filters' tone/saturation level.
/// - Returns: whether bytes reached disk (`applyGeneratedBackground`'s same rule). Discardable:
/// the picker has nothing to do with the answer.
@discardableResult
public func applySolidBackground(colorHex: String) -> Bool {
let root = rootURL
let priorImage = snapshot.backgroundImage
let priorColor = snapshot.background
let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
try BoardWriter.updateIndex(
inItemFolder: root, kind: .board, operation: .setBoardBackground
) { document in
Self.pointSolidBackground(color: colorHex, in: &document)
}
}
guard landed != nil else { return false }
// restyle prior style (13-native-undo.md Rules). The board's own stack, never a window's:
// there is no card here to have a session.
registerStep(
HistoryPhrase.name(.restyle, kind: .board),
undoExpects: [.present(root, .background(colorHex), .backgroundImage(nil))],
redoExpects: [.present(root, .background(priorColor.value), .backgroundImage(priorImage.value))]
) { _ in
try BoardWriter.updateIndex(
inItemFolder: root, kind: .board, operation: .setBoardBackground
) { document in
// A malformed prior reads as a removal on both subkeys, `restore(_:to:in:)`'s own
// rule and the one the redo expectation above is written against.
document.setBackgroundImage(priorImage.value)
Self.restore(priorColor, to: FrontmatterKeys.background, in: &document)
}
} redo: { _ in
try BoardWriter.updateIndex(
inItemFolder: root, kind: .board, operation: .setBoardBackground
) { document in
Self.pointSolidBackground(color: colorHex, in: &document)
}
}
return true
}
/// The one subkey write and the one subkey removal, spelled once so the gesture and its redo
/// cannot drift apart on the order they land in `pointBackground(at:color:in:)`'s sibling, one
/// image write short.
private static func pointSolidBackground(color: String, in document: inout FrontmatterDocument) {
document.setStyleValue(color, for: FrontmatterKeys.background)
document.setBackgroundImage(nil)
}
/// The name a generated background is written under: **ours to overwrite**, or the next free one.
///
/// `current` is what `background.image` says now. When that is already the generated name the