Files
lanework/Kanban/UI/ItemSymbol.swift
T
rzen b35566e0fe Build lane chrome — title bar, badge, inline rename
The lane title bar becomes real: leading SF Symbol (hand-written names
render leniently, unknown ones fall back to the level default), title
or secondary untitled placeholder, a quiet count badge that counts
exactly the cards the body renders (so the m5 search filter is
followed by construction), and a new-card button. The whole bar is
the reorder drag surface — no grip — with click-vs-movement splitting
select from drag; a pure proposal function maps the drag to an
insertion index and release commits through the Writer's same-parent
degenerate reorder, compacting and retrying when midpoint precision
runs out. Clicking never edits: inline rename is Return on the sole
selected card or Board > Rename for either kind, a third transient
editor beside the placeholder that tracks its target by UUID, commits
on focus loss, discards silently when the target vanishes, and
removes the title key on an empty commit. The new-card placeholder
renders at last — the settled Cmd-N target rule (pure, tested) files
it after the anchor card, at a selected lane's bottom, or into the
last-active lane; Return commits and re-selects the lane, Cmd-Return
also opens the card window, and a failed create discards the overlay.
New Card / New Lane / Rename land in the menus with focused-editor
and read-only validation; rename gets its own WriteOperation case in
the banner vocabulary. 59 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 08:52:24 -04:00

45 lines
2.4 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
}
/// 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
}
}