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
+59 -4
View File
@@ -5,7 +5,31 @@ import SwiftUI
/// (03-board-ui.md § Styling Capabilities): **a kebab-case palette name, or a `#RRGGBB[AA]`
/// hex**. This file is the single source for the namehex mapping the pathfinder's twelve icon
/// tints and twelve backgrounds, carried over verbatim as the starting point ("The pathfinder's
/// palettes (12 icon tints, 12 backgrounds) carry over").
/// palettes (12 icon tints, 12 backgrounds) carry over"), **grown to sixteen each on 2026-08-09**.
///
/// ### The two tables are one structure
///
/// Each is **four neutrals plus a twelve-stop hue ring**, and the two rings are paired stop for
/// stop `carnation``light-cayenne`, `fern``smokey-fern`, `light-teal``dark-teal`,
/// `pale-violet``smokey-rich-eggplant`. The tint ring is light and the background ring is deep;
/// otherwise they name the same hues in the same order. Anything added to one belongs in the other
/// at the same position, which is why the 2026-08-09 additions come in pairs.
///
/// ### How the four new stops were chosen
///
/// Not by taste by the holes. The original ring sat at 352°, 19°, 38°, 110°, 180°, 205° and 268°
/// (plus one desaturated slate at 218° that is a neutral in all but name), and its four widest gaps
/// were 38110, 268352, 110205 and 205268. So: **yellow-green at 64°, jade at 158°, indigo at
/// 238°, magenta at 312°**, each inserted at its hue position rather than appended, so the ring
/// stays a ring. Saturation and brightness were set to each table's own existing statistics rather
/// than picked by eye, which is what keeps the new swatches *inside* the family: the tints land at
/// 2.184.67 contrast on white against the old set's 2.184.94, and the backgrounds at 6.614.7
/// against the old 5.716.6. `rich-lime` is deliberately darkened to `smokey-tangerine`'s exact
/// 2.18 an untamed yellow-green would have been the brightest thing in the table by a wide
/// margin.
///
/// The design's AA promise is not a claim this file makes; it is `ContrastMathTests`' two computed
/// assertions over `backgrounds`, which is precisely what made growing the table a safe thing to do.
///
/// ### Lenient, never an error
///
@@ -28,7 +52,9 @@ struct PaletteColor: Identifiable, Sendable {
enum Palette {
/// Icon-tint palette (`iconColor`).
/// Icon-tint palette (`iconColor`) **four neutrals, then the hue ring, then the one muted
/// slate**, in that order. See `Palette`'s own doc comment for the ring and how the four
/// 2026-08-09 additions were placed in it.
static let foregrounds: [PaletteColor] = [
PaletteColor(name: "obsidian", hex: "#000000"),
PaletteColor(name: "aluminum", hex: "#9B9B9B"),
@@ -37,15 +63,23 @@ enum Palette {
PaletteColor(name: "carnation", hex: "#FF576C"),
PaletteColor(name: "rich-grapefruit", hex: "#FF864C"),
PaletteColor(name: "smokey-tangerine", hex: "#E5A334"),
PaletteColor(name: "rich-lime", hex: "#ADB812"),
PaletteColor(name: "fern", hex: "#50B23D"),
PaletteColor(name: "light-jade", hex: "#21B880"),
PaletteColor(name: "light-teal", hex: "#00B7B7"),
PaletteColor(name: "deep-sky-blue", hex: "#0084E5"),
PaletteColor(name: "rich-indigo", hex: "#6065E6"),
PaletteColor(name: "pale-violet", hex: "#8C59C5"),
PaletteColor(name: "rich-magenta", hex: "#D952BE"),
PaletteColor(name: "deep-cool-granite", hex: "#597199"),
]
/// Background palette (`background`) the twelve wells the style editor will offer, "every
/// pair AA-verified at design time" (03-board-ui.md § Styling Controls).
/// Background palette (`background`) the wells the style editor offers, "every pair
/// AA-verified at design time" (03-board-ui.md § Styling Controls). The verification is
/// `ContrastMathTests`' two computed assertions over this table, which is what makes growing it
/// safe: a new well with no readable ink fails there rather than shipping.
///
/// Same shape as `foregrounds` and paired to it hue for hue see `Palette`'s doc comment.
static let backgrounds: [PaletteColor] = [
PaletteColor(name: "obsidian", hex: "#000000"),
PaletteColor(name: "shale", hex: "#5B5B5B"),
@@ -54,12 +88,33 @@ enum Palette {
PaletteColor(name: "light-cayenne", hex: "#B6071E"),
PaletteColor(name: "light-mocha", hex: "#B73C14"),
PaletteColor(name: "smokey-mocha", hex: "#674611"),
PaletteColor(name: "smokey-lime", hex: "#5B610E"),
PaletteColor(name: "smokey-fern", hex: "#145312"),
PaletteColor(name: "dark-jade", hex: "#035437"),
PaletteColor(name: "dark-teal", hex: "#005152"),
PaletteColor(name: "smokey-ocean", hex: "#003168"),
PaletteColor(name: "smokey-indigo", hex: "#14177A"),
PaletteColor(name: "smokey-rich-eggplant", hex: "#290659"),
PaletteColor(name: "smokey-magenta", hex: "#5C074B"),
PaletteColor(name: "intense-cool-shale", hex: "#1F2E45"),
]
/// The tint names the icon-colour pickers offer: `foregrounds` **minus the four neutrals and
/// minus `deep-cool-granite`**.
///
/// A symbol's *tint* wants colour "no tint" is the None well's job, not a grey's and the
/// mutedest entry of the ring is the one that reads least like a deliberate choice at glyph
/// size. Eleven remain, which is exactly what a leading None needs to fill a 4×3 grid
/// (`SymbolPickerLayout`), and that is not a coincidence: the ring was grown to sixteen partly
/// so this row could gain its third row (2026-08-09).
///
/// Derived rather than hand-listed, so a palette addition joins the tint row automatically and
/// a rename can never leave a dead name behind the failure mode the old hand-written constant
/// had.
static let tints: [PaletteColor] = {
let neutrals: Set<String> = ["obsidian", "aluminum", "soapstone", "chalk", "deep-cool-granite"]
return foregrounds.filter { !neutrals.contains($0.name) }
}()
}
// The pathfinder's panel round-trip helpers, ported below (`NSColor.paletteHexString`,