Both the symbol picker and the background color control give up the two-zone combo chrome from the 2026-08-09 rework: no more face/trigger split, no more trailing chevron square. Each is now a single bordered rectangle with one hit zone, 2.1em tall and 1.5x that wide (a 4:6 ratio, 50% taller than the retired chrome's 18pt). A click anywhere opens the same curated popover the trigger used to gate. The background rectangle's popover is new: a None-plus-sixteen palette grid mirroring the Style… popover's own wells, with an Other… row onto the shared, pre-debounced Colors panel session — replacing the old NSMenu dropdown outright. The symbol rectangle keeps its existing popover (search, curated grid, tint colors, More Symbols…) verbatim; only its entry point collapsed to one zone. ComboFieldControl/ComboFieldMetrics (ComboField.swift) are replaced by PickerRectControl/PickerRectMetrics (PickerRect.swift). ColorComboView and its NSMenu-building pure model are retired wholesale in favor of ColorSwatchPicker. SymbolComboControl becomes SymbolGlyphControl. PaletteSwatch.rectImage, the last caller of which was the retired dropdown's menu rows, goes with it. In the card window sidebar, the "Style" section header becomes "Appearance" (sidebar only — the board context menu's Style… item and StyleEditorView's own naming are untouched), and the symbol and background controls move from stacked rows to side-by-side columns, each captioned above rather than leading. CardStyleSection no longer carries its own debounce Task for background panel picks — the shared Colors-panel session now delivers an already-settled value. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
91 lines
4.9 KiB
Swift
91 lines
4.9 KiB
Swift
import AppKit
|
|
import Testing
|
|
@testable import Kanban
|
|
|
|
/// **`ColorSwatchPicker`'s pure pieces** (`ColorSwatchPicker.swift`), the 2026-08-10 rework that
|
|
/// retired `ColorComboView`'s `NSMenu` dropdown and its pure model — `ColorComboItem`, `ColorComboMatch`,
|
|
/// `ColorComboMenu`, `ColorComboModel.match`/`.menu`/`.displayName`/`.normalizedHex` — wholesale. This
|
|
/// file used to hold that model's tests; nothing in it survives the dropdown's removal, so it now
|
|
/// tests what replaced it: which palette a role offers, and the popover's own grid geometry.
|
|
|
|
// MARK: - Role
|
|
|
|
@Suite("ColorSwatchRole ▸ which palette")
|
|
struct ColorSwatchRoleTests {
|
|
|
|
@Test("Background lists the backgrounds table, foreground the foregrounds table")
|
|
func rolesListTheirOwnTable() {
|
|
#expect(ColorSwatchRole.background.palette.map(\.name) == Palette.backgrounds.map(\.name))
|
|
#expect(ColorSwatchRole.foreground.palette.map(\.name) == Palette.foregrounds.map(\.name))
|
|
}
|
|
}
|
|
|
|
// MARK: - Popover geometry
|
|
|
|
/// **`SymbolPickerLayout`'s pattern, restated for a plain colour grid.** The layout is pure and
|
|
/// font-derived (10-accessibility.md's full-relative-scaling rule), so every claim below is
|
|
/// assertable without a popover on screen.
|
|
@Suite("ColorSwatchPopoverLayout ▸ geometry")
|
|
struct ColorSwatchPopoverLayoutTests {
|
|
|
|
/// How wide `columns` wells and the gaps between them actually draw, at a given text size —
|
|
/// `StyleEditorLayoutTests.gridWidth`'s own helper, restated for this layout's fixed seven.
|
|
private func gridWidth(bodyPointSize: CGFloat) -> CGFloat {
|
|
let columns = ColorSwatchPopoverLayout.columns
|
|
let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: bodyPointSize)
|
|
return CGFloat(columns) * layout.wellSide + CGFloat(columns - 1) * layout.wellSpacing
|
|
}
|
|
|
|
@Test("Seven columns — None + sixteen wells fall as 7 + 7 + 3, StyleEditorLayout.popover's own settled fit")
|
|
func columnsAreSeven() {
|
|
#expect(ColorSwatchPopoverLayout.columns == 7)
|
|
// None + Palette.backgrounds (16) is 17 wells; seven columns is the count that keeps the
|
|
// last row from overflowing to four rows or wasting a nearly-empty one.
|
|
let wellCount = 1 + Palette.backgrounds.count
|
|
let rows = (wellCount + ColorSwatchPopoverLayout.columns - 1) / ColorSwatchPopoverLayout.columns
|
|
#expect(rows == 3)
|
|
}
|
|
|
|
@Test("The grid's well side matches the symbol popover's own scale-up over the shared base")
|
|
func wellSideMatchesTheSymbolPopoversScale() {
|
|
for size in [11.0, 13.0, 17.0, 24.0] as [CGFloat] {
|
|
let color = ColorSwatchPopoverLayout.metrics(bodyPointSize: size)
|
|
let symbol = SymbolPickerLayout.metrics(bodyPointSize: size)
|
|
// Both scale `StyleEditorLayout.wellSide` by the identical `gridScale` — the two
|
|
// popovers introduced together (2026-08-10) read at one scale.
|
|
#expect(color.wellSide == symbol.wellSide, "the two popovers' wells drifted apart at \(size)pt")
|
|
#expect(ColorSwatchPopoverLayout.gridScale == SymbolPickerLayout.gridScale)
|
|
}
|
|
}
|
|
|
|
@Test("The popover's frame grows with the text, and its wells keep fitting inside it")
|
|
func popoverScalesAndFits() {
|
|
var previousWidth: CGFloat = 0
|
|
for size in [11.0, 13.0, 16.0, 18.0, 24.0, 36.0] as [CGFloat] {
|
|
let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: size)
|
|
#expect(layout.popoverWidth > previousWidth, "the popover must widen with the text at \(size)pt")
|
|
previousWidth = layout.popoverWidth
|
|
|
|
let content = layout.popoverWidth - 2 * layout.contentPadding
|
|
#expect(gridWidth(bodyPointSize: size) <= content, "the grid overflows its popover at \(size)pt")
|
|
}
|
|
}
|
|
|
|
@Test("The popover's settled geometry at the standard text size")
|
|
func settledGeometryAtTheStandardBody() {
|
|
let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: 13)
|
|
// `StyleEditorLayout`'s own statics at 13pt: wellSpacing 6, sectionSpacing 14. Well side is
|
|
// the style editor's own 20pt scaled by `gridScale`, computed the identical way the
|
|
// implementation does rather than restated as a literal — `20 * 1.3` is not exactly
|
|
// representable in binary floating point, and comparing two independently-rounded literals
|
|
// is exactly the kind of thing that can drift a bit apart from the real computation.
|
|
#expect(layout.wellSpacing == 6)
|
|
#expect(layout.contentPadding == 14)
|
|
let expectedSide = (StyleEditorLayout.wellSide(bodyPointSize: 13) * ColorSwatchPopoverLayout.gridScale).rounded()
|
|
#expect(layout.wellSide == expectedSide)
|
|
#expect(layout.wellSide == 26)
|
|
#expect(layout.gridWidth == layout.wellSide * 7 + layout.wellSpacing * 6)
|
|
#expect(layout.popoverWidth == layout.gridWidth + layout.contentPadding * 2)
|
|
}
|
|
}
|