Files
rzen c6298c2e41 Build the styling system and shared style editor
One style-editor component, anchor-agnostic: a background grid (None
well plus the 12 palette colors) and a curated symbol grid (the
pathfinder's five-dozen set, leading well removing the icon key for
the level default), selection-aware across cards, lanes, and the
board itself. Batch edits compute per-dimension state — uniform,
mixed (no well selected), or an off-palette value labeled verbatim
outside the grids — and choosing a well applies to the whole target
set as one write bracket, skipping no-ops per field. The popover
tracks its target set live per the freshly ratified rule: targets
re-resolve by UUID on every reload, a vanished target leaves the set,
an emptied set dismisses the editor, and nothing ever silently
retargets to the board. Anchors landing now: Board > Style
(Opt-Cmd-S) and the card/lane context menus, which also carry the
quick-style recents row (app-wide, persisted, capped at six, None
never recorded) and the lane's width control twinning the menu
chords. The styling system's other two renders arrive with it: a
lane's background paints the C7 top-edge band, the board's paints
the window content background — malformed values paint nothing and
stay byte-identical on disk. 31 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 14:47:26 -04:00

57 lines
2.9 KiB
Swift

import AppKit
/// The `icon` field's rendering rule: **a name that resolves is drawn, anything else falls back to
/// the level's default** (03-board-ui.md § Styling ▸ Capabilities, "`icon`: SF Symbol per item with
/// per-level defaults").
///
/// ### Lenient, never an error
///
/// `icon` is a lenient field (01-storage-format.md § Frontmatter): a typo in a hand-written name is
/// not a load failure, not a warning, and not an empty box — it renders as the default and the
/// author's bytes are left exactly as written until they change them. That mirrors the read side's
/// treatment of `width`, and it is what makes "any other SF Symbol name works written by hand"
/// (03 § Controls) safe to promise: the app's curated grid is a convenience, the file is the escape
/// hatch, and a name the running OS does not happen to have simply degrades.
///
/// `NSImage(systemSymbolName:)` is the only honest test — the symbol set is the *OS's*, and it grows
/// between releases, so a hard-coded allow-list would be wrong the day after it was written.
enum ItemSymbol {
/// The per-level defaults, verbatim from 03-board-ui.md § Styling ▸ Capabilities.
static let board = "rectangle.split.3x1"
static let lane = "square.stack"
static let card = "doc.text"
/// `field`'s symbol name if it names a symbol this system can draw, `fallback` otherwise.
///
/// Handles all three `FieldValue` shapes the same way, which is the point: a missing key, a
/// malformed one (a sequence where a scalar belongs), and a valid-but-unknown name are one
/// case to the renderer — *there is no symbol to draw, so draw the default*.
static func name(_ field: FieldValue<String>, fallback: String) -> String {
guard let name = field.value, exists(name) else { return fallback }
return name
}
/// The default for a level — the style editor's symbol grid needs the three defaults as a
/// function rather than as three constants, because its leading well is "the level's default
/// symbol" and the editor is one component serving all three (03-board-ui.md § Styling ▸
/// Controls).
static func `default`(for level: StyleLevel) -> String {
switch level {
case .board: board
case .lane: lane
case .card: card
}
}
/// Whether the running system can draw `name` as an SF Symbol.
///
/// Uncached deliberately. The lookup is a bundle-backed symbol resolution that AppKit itself
/// caches, it runs once per lane per render pass, and a cache here would be one more piece of
/// main-actor state to keep honest across the OS's own symbol availability. If a profile ever
/// says otherwise, this one function is where the memo goes.
static func exists(_ name: String) -> Bool {
!name.isEmpty && NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil
}
}