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:
@@ -15,19 +15,69 @@ import Testing
|
||||
|
||||
struct PaletteTableTests {
|
||||
|
||||
@Test func bothTablesHoldTheTwelveNamedColoursTheDesignCarriesOver() {
|
||||
/// The pathfinder's twelve, plus the four 2026-08-09 hue-gap fills, each **at its hue position**
|
||||
/// rather than appended — the order is the ring, and this list is what pins it.
|
||||
@Test func bothTablesHoldTheSixteenNamedColoursInHueOrder() {
|
||||
#expect(Palette.foregrounds.map(\.name) == [
|
||||
"obsidian", "aluminum", "soapstone", "chalk",
|
||||
"carnation", "rich-grapefruit", "smokey-tangerine", "fern",
|
||||
"light-teal", "deep-sky-blue", "pale-violet", "deep-cool-granite",
|
||||
"carnation", "rich-grapefruit", "smokey-tangerine", "rich-lime",
|
||||
"fern", "light-jade", "light-teal", "deep-sky-blue",
|
||||
"rich-indigo", "pale-violet", "rich-magenta", "deep-cool-granite",
|
||||
])
|
||||
#expect(Palette.backgrounds.map(\.name) == [
|
||||
"obsidian", "shale", "aluminum", "chalk",
|
||||
"light-cayenne", "light-mocha", "smokey-mocha", "smokey-fern",
|
||||
"dark-teal", "smokey-ocean", "smokey-rich-eggplant", "intense-cool-shale",
|
||||
"light-cayenne", "light-mocha", "smokey-mocha", "smokey-lime",
|
||||
"smokey-fern", "dark-jade", "dark-teal", "smokey-ocean",
|
||||
"smokey-indigo", "smokey-rich-eggplant", "smokey-magenta", "intense-cool-shale",
|
||||
])
|
||||
}
|
||||
|
||||
/// The two tables are **one structure** (`Palette`'s own doc comment): four neutrals plus a
|
||||
/// twelve-stop ring each, paired stop for stop. A table grown on one side alone would break the
|
||||
/// pairing silently — every picker would still work, and the design would quietly stop being a
|
||||
/// design.
|
||||
@Test func theTwoTablesAreTheSameShape() {
|
||||
#expect(Palette.foregrounds.count == 16)
|
||||
#expect(Palette.backgrounds.count == 16)
|
||||
#expect(Set(Palette.foregrounds.map(\.name)).count == 16, "a name is listed twice")
|
||||
#expect(Set(Palette.backgrounds.map(\.name)).count == 16, "a name is listed twice")
|
||||
// Four neutrals lead each table; the twelve after them are the ring.
|
||||
#expect(Palette.foregrounds.dropFirst(4).count == 12)
|
||||
#expect(Palette.backgrounds.dropFirst(4).count == 12)
|
||||
}
|
||||
|
||||
/// Every hex is spelled the one way the app emits them — uppercase `#RRGGBB` — so a panel pick
|
||||
/// that lands on a palette colour matches by string (`ColorComboModel.match`'s hex branch) and
|
||||
/// comes back as the *name*. A lowercase entry would still resolve and would still round-trip;
|
||||
/// it would just quietly stop being recognised as the palette colour it is.
|
||||
@Test func everyHexIsSixUppercaseDigits() {
|
||||
for entry in Palette.foregrounds + Palette.backgrounds {
|
||||
#expect(entry.hex == entry.hex.uppercased(), "'\(entry.name)' is not uppercase")
|
||||
#expect(entry.hex.count == 7 && entry.hex.hasPrefix("#"), "'\(entry.name)' is not #RRGGBB")
|
||||
let digitsAreHex = entry.hex.dropFirst().allSatisfy { $0.isHexDigit }
|
||||
#expect(digitsAreHex, "'\(entry.name)' has a non-hex digit")
|
||||
}
|
||||
}
|
||||
|
||||
/// The tint row's source: the icon palette minus the four greys and minus `deep-cool-granite`.
|
||||
///
|
||||
/// **Eleven is load-bearing**, not incidental — `SymbolPickerLayout`'s colour grid is four wide
|
||||
/// and its leading None takes the first cell, so eleven is exactly what fills three rows. The
|
||||
/// palette grew to sixteen partly to make that true; this is the assertion that keeps the two
|
||||
/// facts tied together.
|
||||
@Test func theTintRingIsTheIconPaletteWithoutItsNeutrals() {
|
||||
#expect(Palette.tints.map(\.name) == [
|
||||
"carnation", "rich-grapefruit", "smokey-tangerine", "rich-lime",
|
||||
"fern", "light-jade", "light-teal", "deep-sky-blue",
|
||||
"rich-indigo", "pale-violet", "rich-magenta",
|
||||
])
|
||||
#expect(Palette.tints.count == SymbolPickerLayout.colorColumns * SymbolPickerLayout.colorRows - 1)
|
||||
// Every tint is a foreground, and no grey slipped in.
|
||||
for tint in Palette.tints {
|
||||
#expect(Palette.foregrounds.contains { $0.name == tint.name && $0.hex == tint.hex })
|
||||
}
|
||||
}
|
||||
|
||||
@Test func everyPaletteNameResolvesToItsOwnHex() throws {
|
||||
for entry in Palette.foregrounds + Palette.backgrounds {
|
||||
let byName = try #require(
|
||||
|
||||
Reference in New Issue
Block a user