The two pickers rhyme — one two-zone chrome, a face onto a standalone browser, a trigger onto the popover
The colour combo was a wide two-zone field with a second door onto the Colors panel; the symbol picker was a small square button with one. Both now subclass one `ComboFieldControl`, so they are the same width, height, radius and trigger by construction: click the face for the standalone picker, click the chevron for the quick list. The symbol face opens a new floating browser over the OS's own category, ordering and keyword plists out of CoreGlyphs.bundle — searchable, categorised, trademark-restricted glyphs withheld. The palette grows twelve to sixteen per table, filling the hue ring's four widest gaps with lime, jade, indigo and magenta at each table's own saturation and brightness. That gives the Style… popover's background grid a third row and the tint grid its third row of four, and both grids gain an Other… row onto the system colour picker — which the card sidebar's combo has had all along and the primary styling surface never did. An arbitrary hex already round-tripped; it is asserted now, including that an unquoted one is a YAML comment and no value. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
+250
-71
@@ -1,10 +1,10 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// **A reusable SF Symbol picker** — a single well showing the resolved symbol, opening a curated
|
||||
/// grid with a search escape hatch (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 … No full-browser escape hatch in-app; the raw file is the escape hatch"). `StyleEditor.swift`
|
||||
/// **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`
|
||||
/// 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
|
||||
@@ -28,6 +28,24 @@ import SwiftUI
|
||||
/// general-purpose control cannot assume its 36 will always contain what the caller is after — a
|
||||
/// 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
|
||||
///
|
||||
/// 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:
|
||||
///
|
||||
/// - **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`).
|
||||
|
||||
// MARK: - The symbol catalogs
|
||||
|
||||
@@ -52,15 +70,19 @@ enum SymbolPickerCatalog {
|
||||
/// curated list is a convenience, never a claim about the running system.
|
||||
static var available: [String] { defaultSet.filter(ItemSymbol.exists) }
|
||||
|
||||
/// The colour row's seven tints — `Palette.foregrounds`' hues, minus the four grayscale steps
|
||||
/// (a symbol's *tint* wants colour; "no tint" is the None well's job, not a gray's) and minus
|
||||
/// `deep-cool-granite`, the mutedest of the eight, dropped so the row plus its leading None
|
||||
/// fills the 4×2 grid exactly. Palette names, not hexes, exactly as the style editor's wells
|
||||
/// write them.
|
||||
static let colorSet: [String] = [
|
||||
"carnation", "rich-grapefruit", "smokey-tangerine", "fern",
|
||||
"light-teal", "deep-sky-blue", "pale-violet",
|
||||
]
|
||||
/// The colour row's tints — `Palette.tints`' names, which is the icon-tint palette minus the four
|
||||
/// grayscale steps (a symbol's *tint* wants colour; "no tint" is the None well's job, not a
|
||||
/// gray's) and minus `deep-cool-granite`, the mutedest of the ring. Palette names, not hexes,
|
||||
/// exactly as the style editor's wells write them.
|
||||
///
|
||||
/// **Eleven now, seven before** — the palette grew to sixteen on 2026-08-09 and the exclusions
|
||||
/// did not, so the row gained its third 4-wide row (`SymbolPickerLayout.colorRows`) and its
|
||||
/// leading None still fills the grid exactly. That fit is asserted, not hoped for
|
||||
/// (`SymbolPickerColorSetTests`).
|
||||
///
|
||||
/// Derived from `Palette` rather than hand-listed: the old constant restated seven names the
|
||||
/// palette already knew, which is one rename away from a dead well.
|
||||
static var colorSet: [String] { Palette.tints.map(\.name) }
|
||||
|
||||
/// Where the OS keeps the full SF Symbols inventory — read-only system metadata, present on
|
||||
/// every Mac that ships SF Symbols at all.
|
||||
@@ -132,9 +154,11 @@ enum SymbolPickerCatalog {
|
||||
/// base well side and spacing are read straight off `StyleEditorLayout`'s statics, then the grid's
|
||||
/// wells and glyphs scale up by `gridScale` — a deliberate, user-tuned enlargement (the picker's grid
|
||||
/// 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
|
||||
/// button keeps the unscaled side (`restSide`) — it sits inline with a text field and matches that
|
||||
/// field's height, not the grid's. Only the shape wraps a picker's own frame around them — six
|
||||
/// 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
|
||||
/// 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
|
||||
/// sidebar/popover split does), and a total padded width that is fixed for the same reason the
|
||||
@@ -144,17 +168,14 @@ struct SymbolPickerLayout: Equatable {
|
||||
|
||||
static let columns = 6
|
||||
static let rows = 6
|
||||
/// The colour row's own shape — 4×2, the leading None plus `SymbolPickerCatalog.colorSet`'s
|
||||
/// seven tints.
|
||||
/// The colour row's own shape — 4×3, the leading None plus `SymbolPickerCatalog.colorSet`'s
|
||||
/// eleven tints. Was 4×2 and seven until the palette grew (2026-08-09).
|
||||
static let colorColumns = 4
|
||||
static let colorRows = 2
|
||||
static let colorRows = 3
|
||||
/// The grid's enlargement over the style editor's well size — glyphs read at a glance rather
|
||||
/// than in miniature.
|
||||
static let gridScale: CGFloat = 1.3
|
||||
|
||||
/// The at-rest button's side — the unscaled base, matched to the style editor's wells and to
|
||||
/// the text-field height the button sits beside.
|
||||
var restSide: CGFloat
|
||||
/// The glyph's own point size inside a grid well — the body size under `gridScale`, since a
|
||||
/// symbol renders at the font size, not the frame; a bigger well alone would just add margin.
|
||||
var glyphPointSize: CGFloat
|
||||
@@ -178,14 +199,12 @@ struct SymbolPickerLayout: Equatable {
|
||||
var popoverWidth: CGFloat
|
||||
|
||||
static func metrics(bodyPointSize: CGFloat) -> SymbolPickerLayout {
|
||||
let baseSide = StyleEditorLayout.wellSide(bodyPointSize: bodyPointSize)
|
||||
let side = (baseSide * gridScale).rounded()
|
||||
let side = (StyleEditorLayout.wellSide(bodyPointSize: bodyPointSize) * gridScale).rounded()
|
||||
let spacing = StyleEditorLayout.wellSpacing(bodyPointSize: bodyPointSize)
|
||||
let padding = StyleEditorLayout.sectionSpacing(bodyPointSize: bodyPointSize)
|
||||
let gridWidth = (side * CGFloat(columns) + spacing * CGFloat(columns - 1)).rounded()
|
||||
let gridHeight = (side * CGFloat(rows) + spacing * CGFloat(rows - 1)).rounded()
|
||||
return SymbolPickerLayout(
|
||||
restSide: baseSide,
|
||||
glyphPointSize: (bodyPointSize * gridScale).rounded(),
|
||||
wellSide: side,
|
||||
wellSpacing: spacing,
|
||||
@@ -201,16 +220,14 @@ struct SymbolPickerLayout: Equatable {
|
||||
|
||||
// MARK: - The control
|
||||
|
||||
/// A single symbol well that opens a curated grid — the reusable primitive `03-board-ui.md`'s
|
||||
/// full-browser refusal ("the raw file is the escape hatch") leaves room for: not a new in-app way to
|
||||
/// hand-edit `icon`, but a control any 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 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.
|
||||
///
|
||||
/// **View-local state only** — the popover's presented flag lives here, its search text lives with
|
||||
/// the popover content. Nothing about a store, an undo stack, or a target set is known to this type;
|
||||
/// `onSelect` is the whole of its contract with a caller, exactly as a `Picker`'s `selection` binding
|
||||
/// would be.
|
||||
/// **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`.
|
||||
struct SymbolPicker: View {
|
||||
|
||||
/// The committed symbol name, or `nil` for "no override" — read alongside `fallback` rather than
|
||||
@@ -240,63 +257,200 @@ struct SymbolPicker: View {
|
||||
/// the callers that wanted a symbol picker keep getting exactly one.
|
||||
var onSelectColor: ((String?) -> Void)? = nil
|
||||
|
||||
@State private var isPresented = false
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }
|
||||
|
||||
/// What the well actually draws — `current` if this system can resolve it, `fallback` otherwise.
|
||||
/// The same lenient rule `ItemSymbol.name(_:fallback:)` states for a `FieldValue`, restated here
|
||||
/// because this control's `current` is already a plain optional string by the time it arrives.
|
||||
private var resolvedName: String {
|
||||
if let current, ItemSymbol.exists(current) { return current }
|
||||
return fallback
|
||||
}
|
||||
|
||||
/// The at-rest well's tint, or `nil` for the standard one — `Palette`'s lenient rule, gated on
|
||||
/// the colour row being offered at all.
|
||||
private var resolvedTint: AnyShapeStyle? {
|
||||
guard onSelectColor != nil, let currentColor, let color = Palette.color(named: currentColor) else { return nil }
|
||||
return AnyShapeStyle(color)
|
||||
}
|
||||
/// The curated popover's presented flag — **view-local, and a SwiftUI `.popover` deliberately**.
|
||||
///
|
||||
/// The rework's first cut popped an `NSPopover` straight from the control, which is the natural
|
||||
/// thing for an `NSView` to do and is wrong here: this picker is mounted *inside* another popover
|
||||
/// at one of its anchors (`BoardInfoPopover`, beside the rename field), and a second popover that
|
||||
/// takes key status from the first would dismiss the surface it was opened from. SwiftUI already
|
||||
/// handles that nesting, and did before this card; keeping the presentation where it was is the
|
||||
/// change that is not being made.
|
||||
@State private var isPopoverPresented = false
|
||||
|
||||
var body: some View {
|
||||
let layout = SymbolPickerLayout.metrics(bodyPointSize: pointSize)
|
||||
Button {
|
||||
isPresented = true
|
||||
} label: {
|
||||
Image(systemName: resolvedName)
|
||||
.imageScale(.medium)
|
||||
// The tint the board actually renders with, on the well that states the board's
|
||||
// glyph — shown only where the picker offers the colour row, and lenient exactly
|
||||
// like the glyph itself: an unresolvable value tints nothing.
|
||||
.foregroundStyle(resolvedTint ?? AnyShapeStyle(.primary))
|
||||
.frame(width: layout.restSide, height: layout.restSide)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
SymbolComboView(
|
||||
current: current,
|
||||
fallback: fallback,
|
||||
isEnabled: isEnabled,
|
||||
currentColor: onSelectColor == nil ? nil : currentColor,
|
||||
onFace: { openBrowser() },
|
||||
onTrigger: { isPopoverPresented = true }
|
||||
)
|
||||
.help("Symbol")
|
||||
.accessibilityLabel("Symbol")
|
||||
.accessibilityValue(resolvedName)
|
||||
.popover(isPresented: $isPresented, arrowEdge: .bottom) {
|
||||
.popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) {
|
||||
SymbolPickerPopoverContent(
|
||||
current: current,
|
||||
fallback: fallback,
|
||||
symbols: symbols,
|
||||
searchable: searchable,
|
||||
layout: layout,
|
||||
layout: SymbolPickerLayout.metrics(bodyPointSize: CardWindowMetrics.bodyPointSize),
|
||||
onSelect: { name in
|
||||
isPopoverPresented = false
|
||||
onSelect(name)
|
||||
isPresented = false
|
||||
},
|
||||
// **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).
|
||||
onBrowse: {
|
||||
isPopoverPresented = false
|
||||
openBrowser()
|
||||
},
|
||||
currentColor: currentColor,
|
||||
onSelectColor: onSelectColor.map { select in
|
||||
{ name in
|
||||
isPopoverPresented = false
|
||||
select(name)
|
||||
isPresented = false
|
||||
}
|
||||
},
|
||||
// **Other…** under the tint grid — the colour combo's own row, one field over.
|
||||
onPickCustomColor: onSelectColor.map { select in
|
||||
{
|
||||
isPopoverPresented = false
|
||||
openTintPanel(select)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The face zone's door, and **More Symbols…**' — one call, because they are one gesture spelled
|
||||
/// two ways (`ComboField.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
|
||||
/// leave the browser picking into nothing; the shared panel keeps the last closure it was given
|
||||
/// until another surface claims it or its own window closes (`SymbolBrowserPanel`).
|
||||
private func openBrowser() {
|
||||
SymbolBrowserPanel.shared.present(current: current, fallback: fallback, onSelect: onSelect)
|
||||
}
|
||||
|
||||
/// The tint's **Other…** — the shared, debounced Colors panel, for the same
|
||||
/// outlives-the-popover reason (`SharedColorPanelSession`). Seeded with the live tint, matched
|
||||
/// against the icon palette so a pick landing on one of its colours stores the *name*.
|
||||
private func openTintPanel(_ select: @escaping (String?) -> Void) {
|
||||
SharedColorPanelSession.present(
|
||||
seed: currentColor.flatMap(Palette.nsColor(for:)),
|
||||
matching: Palette.foregrounds
|
||||
) { value in
|
||||
select(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The AppKit bridge
|
||||
|
||||
/// `SymbolPicker`'s face: a `SymbolComboControl` with its two zones wired to the caller's closures.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// `.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 {
|
||||
|
||||
let current: String?
|
||||
let fallback: String
|
||||
let isEnabled: Bool
|
||||
/// 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
|
||||
|
||||
/// 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
|
||||
/// control's `current` arrives as a plain optional string.
|
||||
private var resolvedName: String {
|
||||
if let current, ItemSymbol.exists(current) { return current }
|
||||
return fallback
|
||||
}
|
||||
|
||||
/// The width an unconstrained measuring pass falls back to — a face wide enough for the glyph to
|
||||
/// sit in with room to read as a *field* rather than a button, plus the trigger. The normal case
|
||||
/// is a finite proposal (a sidebar row's `.frame(width:)`), which this never has to stand in for.
|
||||
@MainActor
|
||||
private var defaultFaceWidth: CGFloat {
|
||||
let metrics = ComboFieldMetrics.current
|
||||
return (metrics.height * 2 + metrics.triggerWidth).rounded()
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> SymbolComboControl {
|
||||
SymbolComboControl(frame: .zero)
|
||||
}
|
||||
|
||||
func updateNSView(_ control: SymbolComboControl, 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
|
||||
// used to be pointed at.
|
||||
control.onFaceClick = onFace
|
||||
control.onTriggerClick = onTrigger
|
||||
}
|
||||
|
||||
/// Obeys whatever width SwiftUI proposes, like any other control — `ColorComboView.sizeThatFits`'
|
||||
/// 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? {
|
||||
let width: CGFloat
|
||||
if let proposed = proposal.width, proposed.isFinite {
|
||||
width = proposed
|
||||
} else {
|
||||
width = defaultFaceWidth
|
||||
}
|
||||
return CGSize(width: width, height: nsView.intrinsicContentSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
/// The resolved name the face draws — the coordinator's `resolvedName`, handed over on every
|
||||
/// SwiftUI update.
|
||||
var glyphName: String = ItemSymbol.card {
|
||||
didSet {
|
||||
guard glyphName != oldValue else { return }
|
||||
needsDisplay = true
|
||||
}
|
||||
}
|
||||
|
||||
/// The live `iconColor`, or `nil` for the standard label colour. Lenient exactly like the glyph:
|
||||
/// an unresolvable value tints nothing.
|
||||
var glyphTint: NSColor? {
|
||||
didSet {
|
||||
guard glyphTint != oldValue else { return }
|
||||
needsDisplay = true
|
||||
}
|
||||
}
|
||||
|
||||
/// The glyph, centred and tinted. A name this system cannot draw falls back to the dashed
|
||||
/// question mark the wells use — the same "show that there is nothing here" the grids draw.
|
||||
override func drawFace(in rect: NSRect) {
|
||||
let name = ItemSymbol.exists(glyphName) ? glyphName : "questionmark.square.dashed"
|
||||
let config = NSImage.SymbolConfiguration(pointSize: metrics.glyphPointSize, weight: .regular)
|
||||
.applying(.init(paletteColors: [glyphTint ?? .labelColor]))
|
||||
guard let image = NSImage(systemSymbolName: name, accessibilityDescription: nil)?
|
||||
.withSymbolConfiguration(config)
|
||||
else { return }
|
||||
let size = image.size
|
||||
image.draw(in: NSRect(
|
||||
x: rect.minX + metrics.facePaddingH,
|
||||
y: rect.midY - size.height / 2,
|
||||
width: size.width,
|
||||
height: size.height
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The popover's content
|
||||
@@ -312,9 +466,14 @@ private struct SymbolPickerPopoverContent: View {
|
||||
let searchable: Bool
|
||||
let layout: SymbolPickerLayout
|
||||
let onSelect: (String?) -> Void
|
||||
/// Opens the standalone browser — the popover's route to the face zone's door, present whenever
|
||||
/// the caller wired one.
|
||||
var onBrowse: (() -> Void)? = nil
|
||||
var currentColor: String? = nil
|
||||
/// `nil` is "no colour row" — `SymbolPicker.onSelectColor`'s opt-in, passed through.
|
||||
var onSelectColor: ((String?) -> Void)? = nil
|
||||
/// Opens the Colors panel for the tint. Wired only where the colour row is.
|
||||
var onPickCustomColor: (() -> Void)? = nil
|
||||
|
||||
@State private var query = ""
|
||||
|
||||
@@ -324,17 +483,36 @@ private struct SymbolPickerPopoverContent: View {
|
||||
searchField
|
||||
}
|
||||
resultBody
|
||||
if let onBrowse {
|
||||
deeperRow("More Symbols…", action: onBrowse)
|
||||
}
|
||||
// The colour row rides below whichever grid is up — a search narrows the symbols, not
|
||||
// the tints, so the row keeps standing where the eye left it.
|
||||
if let onSelectColor {
|
||||
Divider()
|
||||
SymbolColorGrid(current: currentColor, layout: layout, onSelect: onSelectColor)
|
||||
if let onPickCustomColor {
|
||||
deeperRow("Other…", action: onPickCustomColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(layout.contentPadding)
|
||||
.frame(width: layout.popoverWidth)
|
||||
}
|
||||
|
||||
/// A row onto a standalone picker — **More Symbols…** under the glyph grid, **Other…** under the
|
||||
/// tint grid. Trailing-aligned link buttons rather than extra wells in the grids above them: a
|
||||
/// well that is not a value is a well that has to be *learned*, and the colour combo's dropdown
|
||||
/// already spells this door as a titled row.
|
||||
private func deeperRow(_ title: String, action: @escaping () -> Void) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Spacer(minLength: 0)
|
||||
Button(title, action: action)
|
||||
.buttonStyle(.link)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
private var searchField: some View {
|
||||
TextField("Search Symbols", text: $query)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
@@ -534,7 +712,8 @@ private struct SymbolColorWell: Identifiable {
|
||||
}
|
||||
|
||||
/// The tint grid under the symbol grid — the leading **None** well and
|
||||
/// `SymbolPickerCatalog.colorSet`'s seven tints, 4×2 (the board popover's colour row, 2026-08-07).
|
||||
/// `SymbolPickerCatalog.colorSet`'s eleven tints, 4×3 (the board popover's colour row, 2026-08-07;
|
||||
/// grown by a row with the palette on 2026-08-09).
|
||||
/// `StyleWellGrid`'s pattern one more time, and `SymbolWellGrid`'s reason for restating it: the
|
||||
/// style editor's grids are that file's own, and this row's shape (wide swatches on a fixed
|
||||
/// four-column re-division of the symbol grid's width) fits neither.
|
||||
|
||||
Reference in New Issue
Block a user