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
+13 -11
View File
@@ -12,8 +12,9 @@ import Testing
/// can notice when a key goes missing, arrives out of order, or renders as a YAML indicator. The
/// only check on it is a test that reads a file and says what the rows must be.
/// - **Style** pins its own target this window's card, always, never the selection-aware target the
/// Style popover carries and (2026-08-08) hands the compact `SymbolPicker` the kanban-relevant
/// `CuratedSymbols` set rather than the picker's own smaller general-purpose default. A target wired
/// Style popover carries and (2026-08-08) hands the compact `SymbolPicker` the card-flavored
/// `CuratedSymbols.availableCards` set (one of the three level-specific sets `CuratedSymbols` split
/// into on 2026-08-09) rather than the picker's own smaller general-purpose default. A target wired
/// to the wrong card, or a curated list quietly narrowed back to the picker's default, would look
/// completely normal until it restyled the wrong card or hid a symbol the vocabulary used to offer.
/// - **The sidebar's geometry** is a grid of fixed-size wells in a column sized from font metrics:
@@ -334,19 +335,20 @@ struct CardStyleAnchorTests {
#expect(CardStyleSection.target(forCard: lower) == CardStyleSection.target(forCard: upper))
}
/// **The sidebar's `SymbolPicker` is fed `CuratedSymbols.available`, not the picker's own
/// **The sidebar's `SymbolPicker` is fed `CuratedSymbols.availableCards`, not the picker's own
/// smaller default** the design ruling behind the 2026-08-08 grid-to-picker swap ("so a card's
/// curated vocabulary doesn't shrink"). Not a test *of* the view (this file's own stance), but of
/// the two catalogs the ruling is a claim about: if `CuratedSymbols` ever shrank to
/// `SymbolPickerCatalog.defaultSet`'s size or below, the sidebar's explicit `symbols:` argument
/// would have quietly become a no-op, and this is the tripwire for that.
/// curated vocabulary doesn't shrink"), restated for the 2026-08-09 three-set split. Not a test
/// *of* the view (this file's own stance), but of the two catalogs the ruling is a claim about: if
/// `CuratedSymbols.cards` ever shrank to `SymbolPickerCatalog.defaultSet`'s size or below, the
/// sidebar's explicit `symbols:` argument would have quietly become a no-op, and this is the
/// tripwire for that.
@Test("The card sidebar's curated symbol vocabulary is at least as wide as the picker's own default")
func theCuratedSetPassedToTheSidebarPickerDoesNotShrinkIt() {
#expect(CuratedSymbols.available.count > SymbolPickerCatalog.defaultSet.count)
#expect(CuratedSymbols.availableCards.count > SymbolPickerCatalog.defaultSet.count)
// And it is still the same *available* set every other curated surface reads filtered
// through `ItemSymbol.exists` (`CuratedSymbols.available`'s own doc comment), never the
// unfiltered `all`, so the sidebar never offers a well this Mac cannot draw.
#expect(CuratedSymbols.available.allSatisfy(ItemSymbol.exists))
// through `ItemSymbol.exists` (`CuratedSymbols.availableCards`'s own doc comment), never the
// unfiltered `cards`, so the sidebar never offers a well this Mac cannot draw.
#expect(CuratedSymbols.availableCards.allSatisfy(ItemSymbol.exists))
}
}