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:
@@ -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
|
||||
|
||||
@@ -11,11 +11,16 @@ import Testing
|
||||
/// - **Details** shows frontmatter the app deliberately does not understand, so nothing downstream
|
||||
/// 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** embeds a shared, selection-aware component in a window that has no selection. An
|
||||
/// anchor wired to the wrong target would look completely normal until it restyled something else.
|
||||
/// - **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
|
||||
/// 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:
|
||||
/// at the wrong column count the last well in each row is simply unreachable, at no text size
|
||||
/// anyone tests by eye.
|
||||
/// anyone tests by eye. (The `StyleEditorLayout.sidebar` variant below now describes geometry no
|
||||
/// view in the app currently requests — the Style section's grid moved into `SymbolPicker`'s own
|
||||
/// popover on 2026-08-08 — but the pure function is still correct and still worth pinning.)
|
||||
|
||||
// MARK: - Details ▸ which keys
|
||||
|
||||
@@ -328,6 +333,21 @@ struct CardStyleAnchorTests {
|
||||
let upper = ItemID(rawValue: Ident.card1.uppercased())
|
||||
#expect(CardStyleSection.target(forCard: lower) == CardStyleSection.target(forCard: upper))
|
||||
}
|
||||
|
||||
/// **The sidebar's `SymbolPicker` is fed `CuratedSymbols.available`, 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.
|
||||
@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)
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Style ▸ the anchor's geometry
|
||||
|
||||
Reference in New Issue
Block a user