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:
2026-08-09 09:44:30 -04:00
parent ca0328be2e
commit ece33bbf78
17 changed files with 2030 additions and 322 deletions
+37 -3
View File
@@ -240,7 +240,8 @@ struct StyleEditorLayout: Equatable {
/// The Style popover and the board popover's styling area: a fixed frame, its own padding, and
/// a symbol grid that scrolls within it.
///
/// Thirteen background wells (None + the twelve) fall as 7 + 6, which keeps the popover narrow
/// Seventeen background wells (None + the sixteen) fall as 7 + 7 + 3 two rows until the
/// palette grew on 2026-08-09, three since which keeps the popover narrow
/// enough to sit beside a card without covering the lane it came from; the symbol grid's cap is
/// eight rows or so enough that it reads as a set rather than as a strip, short enough that the
/// popover fits beside a card on a laptop screen.
@@ -395,8 +396,15 @@ struct StyleEditorView: View {
// MARK: - Background
/// The twelve palette wells and their leading None (03-board-ui.md § Styling Controls:
/// "palette-only in-app plus a leading **None** well that removes the `background` key").
/// The palette wells and their leading None (03-board-ui.md § Styling Controls: "palette-only
/// in-app plus a leading **None** well that removes the `background` key"), and since
/// 2026-08-09 an **Other** row onto the system Colors panel.
///
/// **"Palette-only in-app" is the sentence that changed**, and it changed before this card:
/// `ColorComboView` shipped an **Other** row of its own, so the card window's sidebar could
/// already write an arbitrary hex while the Style popover the *primary* styling surface
/// could not. This closes that gap rather than opening a new one. Seventeen wells at seven
/// columns is three rows where the old twelve made two, which is the other half of the same change.
private func backgroundSection(_ state: StyleFieldState, layout: StyleEditorLayout) -> some View {
VStack(alignment: .leading, spacing: layout.wellSpacing) {
sectionHeader("Background", current: backgroundCurrent(state), layout: layout)
@@ -408,6 +416,32 @@ struct StyleEditorView: View {
StyleCommand.apply(background: change, to: target, in: store, recents: recents, on: undo)
}
)
HStack(spacing: 0) {
Spacer(minLength: 0)
Button("Other…") { openBackgroundPanel(state) }
.buttonStyle(.link)
.font(.caption)
}
}
}
/// Opens the Colors panel on the background dimension, seeded with what the target set currently
/// reads (nothing, for a mixed set there is no one colour to start from).
///
/// **The panel fires continuously**, so this debounces exactly as `CardStyleSection` does for the
/// colour combo's own drag: ~400ms trailing, cancelled and replaced on every tick, so a drag
/// writes once it settles instead of once per pixel it passes through.
///
/// It goes to `store.applyStyle` rather than through `StyleCommand.apply`, which is the same call
/// the sidebar's debounce makes and for the same reason: `StyleRecents` remembers *deliberate
/// palette picks*, and a drag that sweeps through six palette-exact colours on its way somewhere
/// else would otherwise flush the row it is supposed to be helping.
private func openBackgroundPanel(_ state: StyleFieldState) {
let seed: NSColor? = if case let .uniform(value) = state { Palette.nsColor(for: value) } else { nil }
let target = self.target
SharedColorPanelSession.present(seed: seed, matching: Palette.backgrounds) { [weak store, weak undo] value in
guard let store else { return }
store.applyStyle(to: target, background: .set(value), icon: .keep, on: undo)
}
}