The Colors panel joins the palette — the combo ratified, and each anchor composes the halves it needs

Four rulings close Redesign Contradiction 3452893f (2026-08-06): the in-app
escape hatch is ratified in full, reversing 2026-07-29's palette-only rule —
the combo's Other… opens the system Colors panel, a pick landing on a palette
color stores the name, anything else the hex. Free-picked colors change no
contrast story: they land on the same runtime ink computation hand-written hex
always got (10 amended to say so; no warning surface is owed). Anchor
ownership: the card sidebar's background story is the combo alone — the well
grid's background half stays with the other anchors (StyleEditorView gains
showsBackground beside showsSymbols; the popover's symbol half already went to
its inline SymbolPicker). Quick-style recents stay palette-vocabulary — a
panel pick never enters them.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-06 22:05:48 -04:00
parent 73698cd77b
commit 9766e1f61c
9 changed files with 981 additions and 18 deletions
+55 -6
View File
@@ -62,11 +62,13 @@ enum Palette {
]
}
// The pathfinder's panel round-trip helpers (`NSColor.paletteHexString`, `Palette.name(forHex:)`)
// stay unported: they exist to turn a colour the *system picker* returned back into a palette name,
// and this app has no colour picker "custom hex is not pickable in-app" (03 § Styling Controls)
// makes the whole round trip a surface that doesn't exist. Its swatch drawing, on the other hand, is
// below: a menu can only render `Image`/`Text`, so the quick-style row's dots have to be pictures.
// The pathfinder's panel round-trip helpers, ported below (`NSColor.paletteHexString`,
// `Palette.name(forHex:in:)`): the reusable colour-picker combo (`ColorComboView`, ColorCombo.swift)
// is the surface that finally needs them a colour the *system picker* returns has to become a
// stored value the same way a palette pick already does: the palette NAME when the colour lands
// exactly on one of the twelve, the hex otherwise. Its swatch drawing, unchanged in spirit, is
// below: a menu can only render `Image`/`Text`, so both the quick-style row's dots and the combo's
// rows have to be pictures.
// MARK: - Menu swatches
@@ -102,6 +104,28 @@ enum PaletteSwatch {
return true
}
}
/// A wide rectangular swatch for `value` `ColorComboView`'s own rows and its collapsed face,
/// which are wide and short rather than the quick-style row's small dots (hence a sibling
/// function rather than a parameter on `circleImage`: the two shapes are never interchangeable at
/// their call sites). `nil` draws the border alone, exactly `circleImage`'s "there is no colour,
/// so show none" the collapsed face's **None** state and the dropdown's own **None** row both
/// call this with `nil` rather than a sentinel string.
static func rectImage(for value: String?, size: NSSize) -> NSImage {
let color = value.flatMap(Palette.nsColor(for:))
return NSImage(size: size, flipped: false) { rect in
let inset = rect.insetBy(dx: 0.5, dy: 0.5)
let path = NSBezierPath(rect: inset)
NSColor.textBackgroundColor.setFill()
path.fill()
color?.setFill()
path.fill()
NSColor.separatorColor.setStroke()
path.lineWidth = 1
path.stroke()
return true
}
}
}
extension Palette {
@@ -134,9 +158,18 @@ extension Palette {
guard let value = field.value else { return nil }
return color(named: value)
}
/// The name of `palette`'s entry whose hex matches `hex`, case-insensitively the pathfinder's
/// round trip, ported for `ColorComboView`'s panel handoff: a colour the system picker returns
/// comes back as `NSColor.paletteHexString`'s canonical `#RRGGBB[AA]`, and this is what turns
/// that back into "the user picked Light Cayenne" instead of leaving it as an anonymous hex.
/// `nil` when nothing in `palette` matches, which the caller reads as "store the hex instead."
static func name(forHex hex: String, in palette: [PaletteColor]) -> String? {
palette.first { $0.hex.caseInsensitiveCompare(hex) == .orderedSame }?.name
}
}
// MARK: - Hex colour
// MARK: - Hex colour
extension NSColor {
/// `#RRGGBB` or `#RRGGBBAA` `NSColor` in **sRGB** the colour space the hex digits name,
@@ -160,4 +193,20 @@ extension NSColor {
alpha: alpha
)
}
/// The reverse of `init?(paletteHex:)`: `#RRGGBB`, or `#RRGGBBAA` when the colour is
/// translucent full opacity collapses to six digits rather than a trailing `FF`, so a colour
/// that round-trips through the panel without the user touching the opacity slider is written
/// exactly as a curated palette entry would be. `nil` only for a colour space **sRGB** cannot
/// convert into, which no picker swatch or palette entry here ever is.
var paletteHexString: String? {
guard let srgb = usingColorSpace(.sRGB) else { return nil }
let red = Int((srgb.redComponent * 255).rounded())
let green = Int((srgb.greenComponent * 255).rounded())
let blue = Int((srgb.blueComponent * 255).rounded())
let alpha = Int((srgb.alphaComponent * 255).rounded())
return alpha >= 255
? String(format: "#%02X%02X%02X", red, green, blue)
: String(format: "#%02X%02X%02X%02X", red, green, blue, alpha)
}
}