A reusable symbol picker — the board's glyph joins its name in the info popover

SymbolPicker: one well at rest, a 6x6 curated grid in a popover (leading
well = the level default, clearing the key), and an optional search over
the OS's full symbol inventory read from CoreGlyphs metadata. Geometry
font-derived off StyleEditorLayout's base, grid enlarged by a deliberate
1.3x. Wired inline with the rename field in the board info popover
through the StyleCommand funnel; the Title header retires. Curated set
and the search's standing vs 03's full-browser refusal await ratification.
This commit is contained in:
2026-08-06 21:18:35 -04:00
parent b1ea97c03e
commit 73698cd77b
3 changed files with 575 additions and 2 deletions
+87
View File
@@ -0,0 +1,87 @@
import Testing
@testable import Kanban
/// **The symbol picker's pure seams** (03-board-ui.md § Styling Controls, the general-purpose
/// picker `SymbolPicker.swift` builds beside the style editor's own curated grid): the curated
/// default set, the full-catalog loader and its cache, and the search filter's AND semantics.
/// SwiftUI rendering the well grid, the popover's arrow-key navigation is deliberately untested,
/// exactly as `StyleEditor.swift`'s own wells are.
@Suite("SymbolPicker ▸ the curated default set")
struct SymbolPickerCatalogDefaultSetTests {
@Test("Exactly 36 entries, all unique")
func shape() {
#expect(SymbolPickerCatalog.defaultSet.count == 36)
#expect(Set(SymbolPickerCatalog.defaultSet).count == SymbolPickerCatalog.defaultSet.count)
}
@Test("Every entry is one this system can actually draw — pins the list against typos")
func everyNameResolves() {
let missing = SymbolPickerCatalog.defaultSet.filter { !ItemSymbol.exists($0) }
#expect(missing.isEmpty, "unknown SF Symbol names: \(missing)")
#expect(SymbolPickerCatalog.available.count == SymbolPickerCatalog.defaultSet.count)
}
}
@Suite("SymbolPicker ▸ filter")
struct SymbolPickerFilterTests {
@Test("An empty or whitespace-only query returns the input unchanged")
func emptyQueryIsANoOp() {
let symbols = ["star", "flag", "heart"]
#expect(SymbolPickerCatalog.filter("", in: symbols) == symbols)
#expect(SymbolPickerCatalog.filter(" ", in: symbols) == symbols)
#expect(SymbolPickerCatalog.filter("\t\n", in: symbols) == symbols)
}
@Test("A single token matches case-insensitively, as a substring")
func singleTokenSubstring() {
let symbols = ["star", "star.fill", "flag", "flag.checkered"]
#expect(SymbolPickerCatalog.filter("star", in: symbols) == ["star", "star.fill"])
#expect(SymbolPickerCatalog.filter("STAR", in: symbols) == ["star", "star.fill"])
#expect(SymbolPickerCatalog.filter("Fla", in: symbols) == ["flag", "flag.checkered"])
}
@Test("Multiple tokens are an AND — every token must appear somewhere in the name")
func multiTokenIsAnAnd() {
let symbols = ["wrench.and.screwdriver", "screwdriver", "wrench"]
#expect(SymbolPickerCatalog.filter("wrench screw", in: symbols) == ["wrench.and.screwdriver"])
#expect(SymbolPickerCatalog.filter("screw wrench", in: symbols) == ["wrench.and.screwdriver"])
}
@Test("Input order is preserved")
func orderPreserved() {
let symbols = ["zebra.star", "apple.star", "mango.star"]
#expect(SymbolPickerCatalog.filter("star", in: symbols) == symbols)
}
@Test("No match returns an empty array")
func noMatchIsEmpty() {
#expect(SymbolPickerCatalog.filter("xyzzy-nonexistent", in: ["star", "flag"]).isEmpty)
}
}
@Suite("SymbolPicker ▸ the full catalog")
struct SymbolPickerFullCatalogTests {
@Test("The default path returns a sorted, unique, non-empty list containing well-known names")
func defaultPathLoads() {
let catalog = SymbolPickerCatalog.fullCatalog()
#expect(!catalog.isEmpty)
#expect(catalog == catalog.sorted())
#expect(Set(catalog).count == catalog.count)
#expect(catalog.contains("star"))
#expect(catalog.contains("folder"))
}
@Test("Two calls against the default path return identical results — the cache is coherent")
func cacheIsCoherent() {
#expect(SymbolPickerCatalog.fullCatalog() == SymbolPickerCatalog.fullCatalog())
}
@Test("A nonexistent bundle path falls back to the merged curated set, sorted and unique")
func nonexistentPathFallsBack() {
let expected = Set(SymbolPickerCatalog.defaultSet + CuratedSymbols.all).sorted()
#expect(SymbolPickerCatalog.fullCatalog(bundlePath: "/nonexistent") == expected)
}
}