The combo's trigger retires — each picker is one rectangle again, taller, and Style becomes Appearance in the sidebar
Both the symbol picker and the background color control give up the two-zone combo chrome from the 2026-08-09 rework: no more face/trigger split, no more trailing chevron square. Each is now a single bordered rectangle with one hit zone, 2.1em tall and 1.5x that wide (a 4:6 ratio, 50% taller than the retired chrome's 18pt). A click anywhere opens the same curated popover the trigger used to gate. The background rectangle's popover is new: a None-plus-sixteen palette grid mirroring the Style… popover's own wells, with an Other… row onto the shared, pre-debounced Colors panel session — replacing the old NSMenu dropdown outright. The symbol rectangle keeps its existing popover (search, curated grid, tint colors, More Symbols…) verbatim; only its entry point collapsed to one zone. ComboFieldControl/ComboFieldMetrics (ComboField.swift) are replaced by PickerRectControl/PickerRectMetrics (PickerRect.swift). ColorComboView and its NSMenu-building pure model are retired wholesale in favor of ColorSwatchPicker. SymbolComboControl becomes SymbolGlyphControl. PaletteSwatch.rectImage, the last caller of which was the retired dropdown's menu rows, goes with it. In the card window sidebar, the "Style" section header becomes "Appearance" (sidebar only — the board context menu's Style… item and StyleEditorView's own naming are untouched), and the symbol and background controls move from stacked rows to side-by-side columns, each captioned above rather than leading. CardStyleSection no longer carries its own debounce Task for background panel picks — the shared Colors-panel session now delivers an already-settled value. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// **A reusable SF Symbol picker** — a two-zone combo showing the resolved symbol, opening a curated
|
||||
/// grid from its trigger and the standalone browser from its face (03-board-ui.md § Styling ▸
|
||||
/// Controls: "its leading well is the level's default symbol and removes the `icon` key … Any other
|
||||
/// SF Symbol name works written by hand"). `StyleEditor.swift`
|
||||
/// **A reusable SF Symbol picker** — a single clickable rectangle (`PickerRect.swift`'s shared
|
||||
/// chrome) showing the resolved symbol, opening a curated grid popover on any click (03-board-ui.md §
|
||||
/// Styling ▸ Controls: "its leading well is the level's default symbol and removes the `icon` key …
|
||||
/// Any other SF Symbol name works written by hand"). `StyleEditor.swift`
|
||||
/// already builds that grid once, aimed at `background`/`icon` together and multiplexed across three
|
||||
/// anchors; this file builds the *symbol half alone*, aimed at any single field a caller names, so a
|
||||
/// control that only ever needs one glyph — a saved search, a smart filter, a future per-item
|
||||
@@ -29,23 +29,22 @@ import SwiftUI
|
||||
/// search with nothing to search would just move the dead end from "no matching well" to "no way to
|
||||
/// look further".
|
||||
///
|
||||
/// ### Shape and grammar (2026-08-09): the two pickers rhyme
|
||||
/// ### Shape and grammar
|
||||
///
|
||||
/// This control used to be a single 20pt bordered square with one hit zone, sitting in a sidebar
|
||||
/// beside a wide two-zone colour combo that had a *second* door onto `NSColorPanel.shared`. Two
|
||||
/// controls setting adjacent frontmatter keys, looking and behaving nothing alike. Now it is a
|
||||
/// `ComboFieldControl` like the colour combo — same field, same height, same trailing chevron, by
|
||||
/// construction rather than by agreement — with the same two-zone grammar:
|
||||
/// This control used to be a single 20pt bordered square with one hit zone, then (2026-08-09) grew a
|
||||
/// second, trigger, zone to rhyme with the colour combo's own two-zone shape — the face opening
|
||||
/// `SymbolBrowserPanel` directly, the trigger popping this curated grid. The owner's 2026-08-10
|
||||
/// reversal (Pipeline card 5004c540: "revert to previous look of the symbol picker, no combo trigger
|
||||
/// … just a rectangle … clickable to show a popover with grid of symbols and colors") retired the
|
||||
/// second zone outright: this is a `PickerRectControl` (`PickerRect.swift`) with exactly **one** hit
|
||||
/// zone, and that one zone always opens the curated popover. `SymbolBrowserPanel`, the standalone
|
||||
/// searchable/categorised browser, is reachable now **only** through the popover's own **More
|
||||
/// Symbols…** row — which is also, as it always was, the only *keyboard* route to it, since a plain
|
||||
/// rectangle has no second key that would mean "the other door."
|
||||
///
|
||||
/// - **the face** opens `SymbolBrowserPanel`, the standalone searchable/categorised browser, exactly
|
||||
/// as the colour combo's swatch opens the Colors panel;
|
||||
/// - **the trigger** pops the curated popover this file always had.
|
||||
///
|
||||
/// And that popover gained the two rows the grammar implies: **More Symbols…** onto the browser (the
|
||||
/// dropdown's **Other…**, one dimension over — and the only *keyboard* route to the face zone's
|
||||
/// door), and an **Other…** under the tint grid onto the Colors panel. The tint grid itself grew from
|
||||
/// 4×2 to 4×3, which is what the palette went from twelve entries to sixteen to make room for
|
||||
/// (`Palette.tints`).
|
||||
/// The popover still carries the two rows that grammar implies: **More Symbols…** onto the browser,
|
||||
/// and an **Other…** under the tint grid onto the Colors panel. The tint grid itself is 4×3, eleven
|
||||
/// tints plus a leading None (`Palette.tints`) — unrelated to this card and unchanged by it.
|
||||
|
||||
// MARK: - The symbol catalogs
|
||||
|
||||
@@ -156,8 +155,8 @@ enum SymbolPickerCatalog {
|
||||
/// is this popover's whole subject, where the style editor's is one section among several), still
|
||||
/// anchored to the shared base so the two components move together at every text size.
|
||||
///
|
||||
/// **The at-rest control's geometry is not here** — it is `ComboFieldMetrics`', shared with the
|
||||
/// colour combo, which is what makes the two the same size (2026-08-09). This type now describes the
|
||||
/// **The at-rest control's geometry is not here** — it is `PickerRectMetrics`', shared with the
|
||||
/// colour rectangle, which is what makes the two the same size. This type now describes the
|
||||
/// popover alone. Only the shape wraps a picker's own frame around them — six
|
||||
/// columns fixed (not a
|
||||
/// caller-configurable count, since a picker has no anchor-width story the way `StyleEditorLayout`'s
|
||||
@@ -220,14 +219,15 @@ struct SymbolPickerLayout: Equatable {
|
||||
|
||||
// MARK: - The control
|
||||
|
||||
/// A two-zone symbol combo — the reusable primitive a caller can aim at one symbol field without
|
||||
/// wiring up a `BoardStore`, a `StyleTarget`, or the two-dimension batch machinery `StyleEditorView`
|
||||
/// carries for the board's own background+icon editor.
|
||||
/// A single-rectangle symbol picker — the reusable primitive a caller can aim at one symbol field
|
||||
/// without wiring up a `BoardStore`, a `StyleTarget`, or the two-dimension batch machinery
|
||||
/// `StyleEditorView` carries for the board's own background+icon editor.
|
||||
///
|
||||
/// **No store, no undo stack, no target set** is known to this type; `onSelect`/`onSelectColor` are
|
||||
/// the whole of its contract with a caller, exactly as a `Picker`'s `selection` binding would be.
|
||||
/// That is unchanged by the 2026-08-09 rework — the API is byte for byte what it was, so every
|
||||
/// existing caller kept working while the control underneath became a `ComboFieldControl`.
|
||||
/// That contract's API is byte for byte what it was through both the 2026-08-09 two-zone rework and
|
||||
/// the 2026-08-10 reversal back to one — every existing caller kept working while the control
|
||||
/// underneath changed shape twice.
|
||||
struct SymbolPicker: View {
|
||||
|
||||
/// The committed symbol name, or `nil` for "no override" — read alongside `fallback` rather than
|
||||
@@ -270,13 +270,12 @@ struct SymbolPicker: View {
|
||||
@State private var isPopoverPresented = false
|
||||
|
||||
var body: some View {
|
||||
SymbolComboView(
|
||||
SymbolGlyphView(
|
||||
current: current,
|
||||
fallback: fallback,
|
||||
isEnabled: isEnabled,
|
||||
currentColor: onSelectColor == nil ? nil : currentColor,
|
||||
onFace: { openBrowser() },
|
||||
onTrigger: { isPopoverPresented = true }
|
||||
onClick: { isPopoverPresented = true }
|
||||
)
|
||||
.help("Symbol")
|
||||
.accessibilityLabel("Symbol")
|
||||
@@ -291,8 +290,9 @@ struct SymbolPicker: View {
|
||||
isPopoverPresented = false
|
||||
onSelect(name)
|
||||
},
|
||||
// **More Symbols…** — the popover's route to the face zone's door, and the only one
|
||||
// the keyboard has (`ComboField.swift`'s note on why the face has no key of its own).
|
||||
// **More Symbols…** — the popover's route to the standalone browser, and the only
|
||||
// way there at all now that the rectangle itself has no second zone or second key
|
||||
// (`PickerRect.swift`'s grammar).
|
||||
onBrowse: {
|
||||
isPopoverPresented = false
|
||||
openBrowser()
|
||||
@@ -304,7 +304,7 @@ struct SymbolPicker: View {
|
||||
select(name)
|
||||
}
|
||||
},
|
||||
// **Other…** under the tint grid — the colour combo's own row, one field over.
|
||||
// **Other…** under the tint grid — the colour rectangle's own row, one field over.
|
||||
onPickCustomColor: onSelectColor.map { select in
|
||||
{
|
||||
isPopoverPresented = false
|
||||
@@ -315,8 +315,8 @@ struct SymbolPicker: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The face zone's door, and **More Symbols…**' — one call, because they are one gesture spelled
|
||||
/// two ways (`ComboField.swift`'s grammar).
|
||||
/// **More Symbols…**'s door onto the standalone browser — the popover's only route there now
|
||||
/// that the rectangle itself has no second zone of its own (`PickerRect.swift`'s grammar).
|
||||
///
|
||||
/// The closure handed over **outlives this view on purpose**. Opening the browser dismisses
|
||||
/// whichever popover this picker was mounted in, which would take a view-owned closure with it and
|
||||
@@ -341,17 +341,17 @@ struct SymbolPicker: View {
|
||||
|
||||
// MARK: - The AppKit bridge
|
||||
|
||||
/// `SymbolPicker`'s face: a `SymbolComboControl` with its two zones wired to the caller's closures.
|
||||
/// `SymbolPicker`'s face: a `SymbolGlyphControl` with its one zone wired to the caller's closure.
|
||||
///
|
||||
/// Deliberately **dumb** — it draws and it reports clicks. Every decision about what a click *means*
|
||||
/// (which popover, which panel, what closure survives what) is `SymbolPicker`'s, in SwiftUI, where
|
||||
/// the presentation modifiers and the state that drives them already live. `ColorComboView` carries a
|
||||
/// coordinator because its dropdown is an `NSMenu` that has to be built in AppKit; this one has no
|
||||
/// such need and gets no such thing.
|
||||
/// the presentation modifiers and the state that drives them already live. Neither this view nor
|
||||
/// `ColorSwatchPicker`'s own AppKit bridge carries a coordinator any more — the last thing that
|
||||
/// needed one was the retired combo's `NSMenu` dropdown.
|
||||
///
|
||||
/// `.disabled(_:)` reaches it through `@Environment(\.isEnabled)` on `SymbolPicker` rather than a
|
||||
/// parameter, since callers already spell the lock that way (`CardStyleSection`, `BoardInfoPopover`).
|
||||
private struct SymbolComboView: NSViewRepresentable {
|
||||
private struct SymbolGlyphView: NSViewRepresentable {
|
||||
|
||||
let current: String?
|
||||
let fallback: String
|
||||
@@ -359,8 +359,7 @@ private struct SymbolComboView: NSViewRepresentable {
|
||||
/// The live tint, or `nil` for the standard label colour — already gated by the caller on whether
|
||||
/// a colour row is offered at all, so this view has no opinion about it.
|
||||
let currentColor: String?
|
||||
let onFace: () -> Void
|
||||
let onTrigger: () -> Void
|
||||
let onClick: () -> Void
|
||||
|
||||
/// What the face draws — `current` if this system can resolve it, `fallback` otherwise. The
|
||||
/// lenient rule `ItemSymbol.name(_:fallback:)` states for a `FieldValue`, restated because this
|
||||
@@ -370,27 +369,27 @@ private struct SymbolComboView: NSViewRepresentable {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> SymbolComboControl {
|
||||
SymbolComboControl(frame: .zero)
|
||||
func makeNSView(context: Context) -> SymbolGlyphControl {
|
||||
SymbolGlyphControl(frame: .zero)
|
||||
}
|
||||
|
||||
func updateNSView(_ control: SymbolComboControl, context: Context) {
|
||||
func updateNSView(_ control: SymbolGlyphControl, context: Context) {
|
||||
control.metrics = .current
|
||||
control.isEnabled = isEnabled
|
||||
control.glyphName = resolvedName
|
||||
control.glyphTint = currentColor.flatMap(Palette.nsColor(for:))
|
||||
control.accessibilityValueText = resolvedName
|
||||
// Rewired on every update rather than once in `makeNSView`: the closures close over this
|
||||
// struct's current values, and a stale one would aim the browser at the field this picker
|
||||
// Rewired on every update rather than once in `makeNSView`: the closure closes over this
|
||||
// struct's current values, and a stale one would open the popover for the field this picker
|
||||
// used to be pointed at.
|
||||
control.onFaceClick = onFace
|
||||
control.onTriggerClick = onTrigger
|
||||
control.onClick = onClick
|
||||
}
|
||||
|
||||
/// Obeys an explicit finite proposal when SwiftUI hands one over, else falls back to the control's
|
||||
/// own intrinsic width — `ColorComboView.sizeThatFits`'s rule, restated so the two combos answer a
|
||||
/// proposal identically and a caller that frames them alike gets two controls the same size.
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, nsView: SymbolComboControl, context: Context) -> CGSize? {
|
||||
/// own intrinsic width — `ColorSwatchView.sizeThatFits`'s rule, restated so the two rectangles
|
||||
/// answer a proposal identically and a caller that frames them alike gets two controls the same
|
||||
/// size.
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, nsView: SymbolGlyphControl, context: Context) -> CGSize? {
|
||||
let width: CGFloat
|
||||
if let proposed = proposal.width, proposed.isFinite {
|
||||
width = proposed
|
||||
@@ -403,9 +402,9 @@ private struct SymbolComboView: NSViewRepresentable {
|
||||
|
||||
// MARK: - The face
|
||||
|
||||
/// `ComboFieldControl` with a **glyph** in its face zone — the symbol half of the shared chrome, and
|
||||
/// the whole of what is specific to symbols about it.
|
||||
final class SymbolComboControl: ComboFieldControl {
|
||||
/// `PickerRectControl` with a **glyph** filling the whole face — the symbol half of the shared
|
||||
/// chrome, and the whole of what is specific to symbols about it.
|
||||
final class SymbolGlyphControl: PickerRectControl {
|
||||
|
||||
/// The resolved name the face draws — the coordinator's `resolvedName`, handed over on every
|
||||
/// SwiftUI update.
|
||||
@@ -433,7 +432,7 @@ final class SymbolComboControl: ComboFieldControl {
|
||||
/// horizontal padding, which read as left-aligned once the glyph was more than a sliver narrower
|
||||
/// than the face. The owner's review named this directly ("for symbol picker center the symbol").
|
||||
///
|
||||
/// **Sized off the actual `rect`, not `ComboFieldMetrics.glyphPointSize`, since the evening
|
||||
/// **Sized off the actual `rect`, not a pure metrics figure, since the evening
|
||||
/// 2026-08-09 review.** That figure (now removed) was a pure derivation from the *intrinsic*
|
||||
/// portrait width, but `sizeThatFits` obeys any finite width SwiftUI proposes, and the card
|
||||
/// sidebar's row proposes real width — so the control renders wide while a glyph pinned at the
|
||||
|
||||
Reference in New Issue
Block a user