The board wears a picture — background becomes a mapping, and the window chrome follows it under a thin frost
background is {color:, image:} and only a mapping at every level; the board's image paints the full window under a transparent title bar, with a thin-material frost strip keeping the chrome legible and the standard accommodations intact.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -106,6 +106,13 @@ extension FrontmatterDocument {
|
||||
record(FrontmatterKeys.modifiedBy, modifiedBy)
|
||||
record(FrontmatterKeys.author, author)
|
||||
record(FrontmatterKeys.background, background)
|
||||
// **`background` can contribute two entries**, because the one key holds two readings once
|
||||
// it is written as a mapping (`FrontmatterDocument.backgroundImage`). Both are filed under
|
||||
// the key the schema spells, which is the key an author would go and fix; they are told
|
||||
// apart by their raw text, since each quotes the subvalue that could not be read. A mapping
|
||||
// whose colour reads fine and whose image does not still leaves a trace, which is the whole
|
||||
// contract here.
|
||||
record(FrontmatterKeys.background, backgroundImage)
|
||||
record(FrontmatterKeys.icon, icon)
|
||||
record(FrontmatterKeys.iconColor, iconColor)
|
||||
record(FrontmatterKeys.kind, kind)
|
||||
@@ -143,10 +150,54 @@ extension FrontmatterDocument {
|
||||
/// (`title: 2048` reads as `"2048"`). Only a sequence or mapping — no scalar reading exists
|
||||
/// — is malformed.
|
||||
public var title: FieldValue<String> { read(FrontmatterKeys.title, Self.string) }
|
||||
public var background: FieldValue<String> { read(FrontmatterKeys.background, Self.string) }
|
||||
public var icon: FieldValue<String> { read(FrontmatterKeys.icon, Self.string) }
|
||||
public var iconColor: FieldValue<String> { read(FrontmatterKeys.iconColor, Self.string) }
|
||||
|
||||
/// The `background` mapping's **colour** — a palette name or a `#RRGGBB[AA]` hex, read through
|
||||
/// the same scalar coercion every other string field uses.
|
||||
///
|
||||
/// **`background` is a mapping, and only a mapping** (01-storage-format.md § Frontmatter, ruled
|
||||
/// 2026-08-06): `{color: "#112233", image: sunset.jpg}`, either subkey absent, unknown subkeys
|
||||
/// tolerated. A bare scalar — `background: green` — has **no reading at all** and is
|
||||
/// `.malformed`: it renders as no colour, files a coerce-tier trace, and stays on disk exactly as
|
||||
/// written, which is the same lenient degrade a colour nobody can resolve already gets.
|
||||
///
|
||||
/// That is a ruling about the *schema*, not a migration: nothing had shipped when it was made, so
|
||||
/// there is no legacy spelling to keep alive, no version bump, and no healing machinery. One key,
|
||||
/// one shape, and a field whose type does not depend on which subkeys the author happened to
|
||||
/// want.
|
||||
///
|
||||
/// **One reader for all three levels, deliberately.** A lane's and a card's `background` mean
|
||||
/// colour and nothing else — they are edge accents, and only the board consumes an image
|
||||
/// (03-board-ui.md § Styling ▸ Capabilities) — but the *shape* is uniform, so a lane writes
|
||||
/// `{color: fern}` exactly as the board does and nothing below has to know which level it is
|
||||
/// reading.
|
||||
///
|
||||
/// A mapping carrying no `color` — an image-only background, or an explicit `color: null` —
|
||||
/// reads `.missing`, which is exactly "no colour" and renders the level's default; that is an
|
||||
/// absence, not a failure, and it files no trace. A `color` that is itself a sequence or mapping
|
||||
/// has no scalar reading and is `.malformed` like any other.
|
||||
public var background: FieldValue<String> {
|
||||
backgroundReading(FrontmatterKeys.Background.color, reportsShape: true)
|
||||
}
|
||||
|
||||
/// The `background` mapping's **image** — a path relative to the board root, board-only in
|
||||
/// meaning (`BoardModel` carries it; `Lane` and `Card` deliberately do not).
|
||||
///
|
||||
/// `.missing` for every shape that is not a mapping, including the retired scalar: a value the
|
||||
/// schema cannot read is **one** unreadable value, and the colour reading above already reports
|
||||
/// it. Two `.malformed`s off one key would file the same defect twice and say the file named an
|
||||
/// image when it did nothing of the kind.
|
||||
///
|
||||
/// **Where the path leads is not this layer's question.** Whether it resolves inside the board
|
||||
/// root, and whether the bytes are an image at all, belongs to the renderer
|
||||
/// (`BoardBackdrop.imageURL(named:inBoardRoot:)`); this is the document's reading of what was
|
||||
/// written, and an unresolvable path degrades exactly like an unrecognized colour — paint
|
||||
/// nothing, change nothing on disk.
|
||||
public var backgroundImage: FieldValue<String> {
|
||||
backgroundReading(FrontmatterKeys.Background.image, reportsShape: false)
|
||||
}
|
||||
|
||||
/// Width multiplier. An exact-integer reading — from an int, a double, or a numeric string —
|
||||
/// always coerces: at or above 1 to itself (`"2"`, `2.0` → `2`), below 1 to 1 (**ranges are
|
||||
/// part of the sensible reading**, 01-storage-format.md § Frontmatter, settled — the table's
|
||||
@@ -190,6 +241,36 @@ extension FrontmatterDocument {
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// One subkey's reading out of the `background` mapping. It is `read(_:_:)`'s shape with one
|
||||
/// extra step, and it cannot *be* `read(_:_:)`: that helper's transform answers `nil` for "no
|
||||
/// sensible reading", where a mapping with no such subkey has to answer `.missing` — an absent
|
||||
/// subkey is an absent value, not an unreadable one, and reporting it as a coerce-tier fallback
|
||||
/// would file a defect against every image-only background in existence.
|
||||
///
|
||||
/// `reportsShape` is the whole difference between the two readers above, and it is about the
|
||||
/// **key's** shape rather than the subkey's: a value that is not a mapping at all — the retired
|
||||
/// scalar, a sequence — is one unreadable value, so exactly one reader reports it. The colour is
|
||||
/// that reader because the colour is what the key means when it has no subkeys to speak of; the
|
||||
/// image stays `.missing`, since a file that never wrote a mapping never claimed to name a
|
||||
/// picture.
|
||||
///
|
||||
/// **A subvalue's malformed raw is the parse's rendering, not a source span.** `rawValue(for:)`
|
||||
/// addresses top-level keys, so a subkey has no span to quote; the coerce record takes what the
|
||||
/// parse retained (`YAMLValue.description`), which is the most this shape can honestly offer and
|
||||
/// still names what could not be read. The key's *own* malformed raw is the span, as always.
|
||||
private func backgroundReading(_ subkey: String, reportsShape: Bool) -> FieldValue<String> {
|
||||
guard let value = value(for: FrontmatterKeys.background) else { return .missing }
|
||||
if case .null = value { return .missing }
|
||||
guard case let .mapping(pairs) = value else {
|
||||
guard reportsShape else { return .missing }
|
||||
return .malformed(raw: rawValue(for: FrontmatterKeys.background) ?? value.description)
|
||||
}
|
||||
guard let subvalue = pairs.first(where: { $0.key == .string(subkey) })?.value else { return .missing }
|
||||
if case .null = subvalue { return .missing }
|
||||
let raw = subvalue.description
|
||||
return Self.string(subvalue, raw: raw).map(FieldValue.valid) ?? .malformed(raw: raw)
|
||||
}
|
||||
|
||||
private func read<Value>(_ key: String, _ transform: (YAMLValue, String) -> Value?) -> FieldValue<Value> {
|
||||
guard let value = value(for: key) else { return .missing }
|
||||
if case .null = value { return .missing }
|
||||
|
||||
Reference in New Issue
Block a user