Three curated symbol vocabularies — boards, lanes, and cards each pick from their own shelf

`CuratedSymbols` was one flat ~65-glyph list serving every style-editor target alike. It is
now three level-specific sets — `boards` (project/container/identity), `lanes`
(workflow/stage/status), `cards` (work-item/content) — each ~30-40 entries, seeded from the
original list and `SymbolPickerCatalog.defaultSet`, reorganized by which level a glyph actually
reads as being about. Overlap is kept where a glyph genuinely fits everywhere (`flag`, `star`).

Wiring:
- `BoardInfoPopover`'s board-glyph `SymbolPicker` now passes `CuratedSymbols.availableBoards`
  instead of the picker's domain-agnostic default.
- The card sidebar's `SymbolPicker` (`CardSidebarSections`) now passes
  `CuratedSymbols.availableCards` instead of the old flat `available`.
- The style editor's own curated grid (`StyleEditorView`, the Style… popover's only remaining
  anchor) reads `CuratedSymbols.availableForStyleEditor(level:spansLevels:)`: a homogeneous
  target reads its own level's set, and a target that somehow spans more than one level (today
  unreachable — 04-interactions.md's cards-XOR-lanes rule keeps a live selection homogeneous)
  reads the three combined, via a new `BoardStore.styleTargetSpansLevels` seam that asks the
  question `styleLevel(of:)` deliberately collapses.
- `CuratedSymbols.combined` (the three sets' stable-order union) also replaces the old `.all`
  in `SymbolPickerCatalog`'s full-catalog fallback.
- `SymbolPickerCatalog.defaultSet` is kept as the fallback for a caller naming no level (a
  future saved-search picker, say) rather than retired.

DESIGN/03-board-ui.md and DESIGN/05-card-window.md's Styling/sidebar prose amended minimally
where they named "the curated set" as a single list.

Tests: three new/rewritten suites in KanbanTests/StyleModelTests.swift (set shape, availability,
overlap, `combined`, the style-editor level/span decision, the board-anchor width tripwire), one
new test in KanbanTests/StyleWriteTests.swift (`styleTargetSpansLevels`), and the old
single-list-pinning tests in KanbanTests/SymbolPickerTests.swift and KanbanTests/CardSidebarTests.swift
updated to the new set names. 2834 tests, 487 suites green (KanbanTests, arm64); one unrelated
flaky failure (RootRecoveryTests.vanishAndReturn under full-suite load) passed clean in isolation.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 01:26:36 -04:00
parent c87616f3fb
commit db863ba011
11 changed files with 288 additions and 79 deletions
+15 -12
View File
@@ -13,10 +13,13 @@ import SwiftUI
///
/// ### Why the curated set differs from `CuratedSymbols`
///
/// `CuratedSymbols.all` is grouped by what a *board item* is (status/flow, containers, people)
/// this control has no board item in mind, so `SymbolPickerCatalog.defaultSet` is a smaller,
/// ungrouped 36 chosen for the general "boards and projects" case instead. The two lists are free to
/// diverge; nothing here reads the other.
/// `CuratedSymbols` is three sets grouped by *level* boards, lanes, cards (2026-08-09) because a
/// board's identity, a lane's stage and a card's content want different glyphs. This control has no
/// level built in; a caller aimed at one names it explicitly (`BoardInfoPopover` passes
/// `CuratedSymbols.availableBoards`, the card sidebar `CuratedSymbols.availableCards`), and a caller
/// with no level in mind a saved search, a smart filter falls back to `SymbolPickerCatalog.defaultSet`,
/// a smaller, ungrouped 36 chosen for the general "boards and projects" case instead. The lists are
/// free to diverge; nothing here reads the others beyond the merge below.
///
/// ### The one thing `CuratedSymbols` never needed
///
@@ -32,11 +35,11 @@ import SwiftUI
/// inventory it searches into once the grid alone isn't enough.
enum SymbolPickerCatalog {
/// The picker's curated grid, in order a general "boards and projects" set rather than the
/// style editor's kanban-item groupings, chosen so a first-run picker with no caller-supplied
/// `symbols` still shows something broadly useful. A stored constant, not a computed property,
/// for `CuratedSymbols.all`'s own reason: the list is the design decision, and `available` is the
/// only thing the OS gets a say in.
/// The picker's curated grid, in order a general "boards and projects" set rather than one of
/// the style editor's level-specific groupings, chosen so a first-run picker with no
/// caller-supplied `symbols` still shows something broadly useful. A stored constant, not a
/// computed property, for `CuratedSymbols`' own reason: the list is the design decision, and
/// `available` is the only thing the OS gets a say in.
static let defaultSet: [String] = [
"star", "flag", "heart", "bolt", "flame", "leaf", "drop", "sun.max", "moon", "sparkles",
"tag", "bookmark", "pin", "bell", "paperplane", "tray", "folder", "archivebox", "doc.text",
@@ -45,8 +48,8 @@ enum SymbolPickerCatalog {
"airplane", "gamecontroller", "globe",
]
/// The set this Mac can actually draw `CuratedSymbols.available`'s rule, mirrored: a curated
/// list is a convenience, never a claim about the running system.
/// The set this Mac can actually draw `CuratedSymbols.available(for:)`'s rule, mirrored: a
/// curated list is a convenience, never a claim about the running system.
static var available: [String] { defaultSet.filter(ItemSymbol.exists) }
/// The colour row's seven tints `Palette.foregrounds`' hues, minus the four grayscale steps
@@ -98,7 +101,7 @@ enum SymbolPickerCatalog {
let root = plist as? [String: Any],
let symbols = root["symbols"] as? [String: Any]
else {
return Set(defaultSet + CuratedSymbols.all).sorted()
return Set(defaultSet + CuratedSymbols.combined).sorted()
}
return symbols.keys.sorted()
}