replace symbol grid in card window sidebar with a compact symbol picker

The card window sidebar's Style section embedded StyleEditorView symbols-only
(showsBackground: false, showsSymbols: true), drawing the curated grid whole
inside the sidebar's own scroll view — a permanently open 5-7 row grid ahead
of every other section. Replace it with the reusable compact SymbolPicker
(already used by BoardInfoPopover for the board's glyph): a single well at
rest, the grid only inside its own popover.

- CardStyleSection no longer instantiates StyleEditorView at all. A new
  "Symbol" row mirrors the existing "Background" row's inspector shape
  (caption leading, control trailing), wiring SymbolPicker's onSelect/
  onSelectColor to icon/iconColor through StyleCommand.apply(...on: undo),
  the exact funnel the background combo already rides — so the card window's
  undo session semantics (13-native-undo.md) are unchanged.
- The picker is fed CuratedSymbols.available rather than its own smaller
  general-purpose default, so a card's curated vocabulary doesn't shrink.
- StyleEditorLayout.sidebar and showsBackground stay in StyleEditor.swift
  (still correct, still tested) rather than being cut as dead code — a
  separate, larger cleanup this card doesn't make (recorded on the card).
- DESIGN/03-board-ui.md and DESIGN/05-card-window.md: updated the sentences
  describing the sidebar hosting the style editor's symbol grid to describe
  the compact picker instead.
- Tests: CardSessionUndoTests gains a symbol/tint analogue of the existing
  background-combo session-routing test; CardSidebarTests gains a curated-set
  tripwire and an updated file-header note.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-08 22:14:40 -04:00
parent 898facebe0
commit 05ae703989
6 changed files with 216 additions and 44 deletions
+52
View File
@@ -109,6 +109,19 @@ private func body(_ fixture: WriterFixture, _ path: String) throws -> String {
try FrontmatterDocument.parse(fixture.indexText(path)).body
}
/// A `StyleRecents` of this test's own `StyleRecentsTests`' own pattern (`StyleModelTests.swift`),
/// restated here rather than shared across files: the list is a real user preference, and a suite
/// that wrote into `UserDefaults.standard` would be editing the developer's own quick-style row.
@MainActor
private func makeRecents() -> (recents: StyleRecents, teardown: () -> Void) {
let name = "CardSessionUndoTests-\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: name) else {
Issue.record("could not create a defaults suite")
return (StyleRecents(defaults: .standard), {})
}
return (StyleRecents(defaults: defaults), { defaults.removePersistentDomain(forName: name) })
}
// MARK: - Routing
@MainActor
@@ -452,6 +465,45 @@ struct CardSessionCloseTests {
let document = try FrontmatterDocument.parse(fixture.indexText(cardPath))
#expect(document.background.isMissing, "the session's net effect, walked back")
}
/// The sidebar's compact `SymbolPicker` (2026-08-08, replacing the embedded curated grid) writes
/// through the same funnel the test above exercises for the background combo
/// `StyleCommand.apply`, exactly as `CardStyleSection.applySymbol`/`applyIconColor` call it so
/// this is that same claim, `icon`/`iconColor` instead of `background`. A control swap that left
/// this funnel unrouted would still look correct on screen (the well updates) right up until the
/// window closes and the change never reaches board history.
@Test("A symbol and tint change made in the window's sidebar joins the session, not the board")
func sidebarSymbolPickingIsASessionGesture() async throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
_ = try makeCommentBoard(fixture)
let window = try makeWindow(fixture)
let (recents, teardown) = makeRecents()
defer { teardown() }
StyleCommand.apply(
icon: .set("flame"),
iconColor: .set("carnation"),
to: CardStyleSection.target(forCard: cardID),
in: window.store,
recents: recents,
on: window.window
)
#expect(window.window.stack.undoActionName == "Restyle Card")
#expect(!window.board.canUndo)
// A symbol pick is not a background pick "only `.set` reaches `StyleRecents.record`" is
// `StyleCommand.apply`'s rule for `background` alone (`StyleEditor.swift`) so the picker's
// glyph and tint wells never touch the quick-style row.
#expect(recents.backgrounds.isEmpty)
await window.session.endSession()
#expect(window.board.undoActionName == coarseStep)
window.board.undo()
let document = try FrontmatterDocument.parse(fixture.indexText(cardPath))
#expect(document.icon.isMissing, "the session's net effect, walked back")
#expect(document.iconColor.isMissing, "and its tint with it")
}
}
// MARK: - Transactional staleness