Implement visual accommodations and Full Keyboard Access
Full relative text scaling per DESIGN/10: BoardMetrics is the board strip's geometry as a pure function of the body point size (CardWindowMetrics' twin) — lane plate/header/band, card corner/stripe/padding, masonry spacing, the drop model's nominal card height, resize-handle geometry, trash hatch pitch, and both window floors all derive from an em; CardFaceMetrics folded in. The two fixed font sizes (welcome brand/glyph) went relative; the toolbar search field is 17 ems like the transient bar's. The no-horizontal-scroll invariant is pinned by test at six text sizes by twelve lane counts. Accommodations is Motion's sibling for the visual settings: Increase Contrast adds a flat point to strokes (monotone, hierarchy-preserving), gives borderless card/lane plates a resting separator hairline, and takes faded accents to full alpha; Reduce Transparency turns the transient search bar's glass solid and does the same for the alpha washes that composite over a user-chosen board background (trash plate, hatched header, drag shadow). Reduce Motion audited — every animated surface already routes through Motion with a reduced variant; no gaps. Full Keyboard Access: the template chooser's tiles were pointer-only — now focusable, arrow-navigable (clamped, StyleWellGrid's rule), Space picks, Return stays the sheet's default action, focus names the selection one-way. The board's single tab stop shows its focus ring under FKA (focusEffectDisabled inverts). Style editor verified already conformant. Edge accents verified text-free; trash hatch pitch now font-derived so it still reads as hatching at large text. 1549 unit tests green, both schemes build. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -335,26 +335,50 @@ struct CardStyleAnchorTests {
|
||||
@Suite("Card sidebar ▸ style editor layout")
|
||||
struct StyleEditorLayoutTests {
|
||||
|
||||
/// How wide `columns` wells and the gaps between them actually draw.
|
||||
private func gridWidth(columns: Int) -> CGFloat {
|
||||
CGFloat(columns) * StyleEditorLayout.wellSide + CGFloat(columns - 1) * StyleEditorLayout.wellSpacing
|
||||
/// How wide `columns` wells and the gaps between them actually draw, at a given text size.
|
||||
private func gridWidth(columns: Int, bodyPointSize: CGFloat) -> CGFloat {
|
||||
CGFloat(columns) * StyleEditorLayout.wellSide(bodyPointSize: bodyPointSize)
|
||||
+ CGFloat(columns - 1) * StyleEditorLayout.wellSpacing(bodyPointSize: bodyPointSize)
|
||||
}
|
||||
|
||||
@Test("The popover anchor is unchanged by the sidebar's arrival")
|
||||
@Test("The popover anchor keeps its settled geometry at the standard text size")
|
||||
func thePopoverKeepsItsSettledGeometry() {
|
||||
// 268 points and 7 + 6 background wells are 03-board-ui.md's own numbers ("narrow enough to
|
||||
// sit beside a card"), and the two anchors that were here first must not have moved because a
|
||||
// third one needed different ones.
|
||||
#expect(StyleEditorLayout.popover.width == 268)
|
||||
#expect(StyleEditorLayout.popover.padding == 14)
|
||||
#expect(StyleEditorLayout.popover.backgroundColumns == 7)
|
||||
#expect(StyleEditorLayout.popover.symbolColumns == 8)
|
||||
#expect(StyleEditorLayout.popover.symbolGridMaximumHeight == 168)
|
||||
// sit beside a card"). Making the anchor font-derived (10-accessibility.md ▸ Text scaling)
|
||||
// must not have moved them at the size they were settled for — the point of the multiples is
|
||||
// that the default text size renders exactly what it always did.
|
||||
let popover = StyleEditorLayout.popover(bodyPointSize: 13)
|
||||
#expect(popover.width == 268)
|
||||
#expect(popover.padding == 14)
|
||||
#expect(popover.backgroundColumns == 7)
|
||||
#expect(popover.symbolColumns == 8)
|
||||
#expect(popover.symbolGridMaximumHeight == 168)
|
||||
#expect(popover.wellSide == 20)
|
||||
#expect(popover.wellSpacing == 6)
|
||||
}
|
||||
|
||||
/// The other half of the same claim: **the popover grows with the text**, so its seven wells
|
||||
/// still fit at a large system text size instead of overflowing a frame frozen at 268 points.
|
||||
@Test("The popover's frame grows with the text, and its wells keep fitting inside it")
|
||||
func thePopoverScales() {
|
||||
var previousWidth: CGFloat = 0
|
||||
for size in [11.0, 13.0, 16.0, 18.0, 24.0, 36.0] as [CGFloat] {
|
||||
let popover = StyleEditorLayout.popover(bodyPointSize: size)
|
||||
#expect(popover.width! > previousWidth, "the popover must widen with the text at \(size)pt")
|
||||
previousWidth = popover.width!
|
||||
|
||||
// The column counts are the design's and never move; what has to hold is that they
|
||||
// still fit, insets included.
|
||||
#expect(popover.backgroundColumns == 7)
|
||||
let content = popover.width! - 2 * popover.padding
|
||||
#expect(gridWidth(columns: 7, bodyPointSize: size) <= content, "7 wells overflow at \(size)pt")
|
||||
#expect(gridWidth(columns: 8, bodyPointSize: size) <= content, "8 wells overflow at \(size)pt")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("The sidebar anchor takes the column it is given and adds nothing to it")
|
||||
func theSidebarBringsNoGeometryOfItsOwn() {
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: 169)
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: 169, bodyPointSize: 13)
|
||||
|
||||
// No width: the sidebar's is `CardWindowMetrics`' one decision. No padding: the section stack
|
||||
// is already inset by a gutter, and insetting twice would narrow the grids for nothing.
|
||||
@@ -369,15 +393,22 @@ struct StyleEditorLayoutTests {
|
||||
func theGridsFitTheSidebar() {
|
||||
for size in [11.0, 13.0, 16.0, 18.0, 24.0, 36.0] as [CGFloat] {
|
||||
let available = CardWindowMetrics.sidebarContentWidth(bodyPointSize: size)
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: available)
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: available, bodyPointSize: size)
|
||||
|
||||
#expect(layout.backgroundColumns == layout.symbolColumns, "one column count for one column")
|
||||
// Fits — a grid wider than its column puts the last well of every row out of reach of
|
||||
// the pointer, at a text size nobody checks by eye.
|
||||
#expect(gridWidth(columns: layout.backgroundColumns) <= available, "overflows at \(size)pt")
|
||||
#expect(gridWidth(columns: layout.backgroundColumns, bodyPointSize: size) <= available,
|
||||
"overflows at \(size)pt")
|
||||
// And is maximal: one more well would not have fitted, so the wells are as large a set as
|
||||
// the column can show rather than an arbitrary count that happened to be safe.
|
||||
#expect(gridWidth(columns: layout.backgroundColumns + 1) > available, "under-packed at \(size)pt")
|
||||
#expect(gridWidth(columns: layout.backgroundColumns + 1, bodyPointSize: size) > available,
|
||||
"under-packed at \(size)pt")
|
||||
// And the count is *stable* across text sizes, because the column and the wells scale on
|
||||
// the same ruler — the sidebar is 26 body characters wide and a well is 1.55 body ems,
|
||||
// and neither of those ratios moves. Without that the grid would collapse to a strip at
|
||||
// a large text size, which is a palette the user can no longer scan.
|
||||
#expect(layout.backgroundColumns >= 4, "the grid collapsed to a strip at \(size)pt")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,8 +416,11 @@ struct StyleEditorLayoutTests {
|
||||
func theSidebarIsTheNarrowerAnchor() {
|
||||
// Which is the entire reason this type exists: the popover's 7 wells across do not fit a
|
||||
// 26-character column, so an editor with one hard-coded frame could not have both anchors.
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: CardWindowMetrics.sidebarContentWidth(bodyPointSize: 13))
|
||||
#expect(layout.backgroundColumns < StyleEditorLayout.popover.backgroundColumns)
|
||||
let layout = StyleEditorLayout.sidebar(
|
||||
contentWidth: CardWindowMetrics.sidebarContentWidth(bodyPointSize: 13),
|
||||
bodyPointSize: 13
|
||||
)
|
||||
#expect(layout.backgroundColumns < StyleEditorLayout.popover(bodyPointSize: 13).backgroundColumns)
|
||||
#expect(layout.backgroundColumns >= 4, "a grid this narrow would be a strip, not a palette")
|
||||
}
|
||||
|
||||
@@ -395,9 +429,10 @@ struct StyleEditorLayoutTests {
|
||||
// Total over any width, because the caller is a layout system: a zero proposal during a
|
||||
// window's first frame must not produce a grid of zero columns, which is a division by zero
|
||||
// waiting in `LazyVGrid`.
|
||||
#expect(StyleEditorLayout.columns(fitting: 0) == 1)
|
||||
#expect(StyleEditorLayout.columns(fitting: -100) == 1)
|
||||
#expect(StyleEditorLayout.columns(fitting: StyleEditorLayout.wellSide) == 1)
|
||||
#expect(StyleEditorLayout.columns(fitting: 0, bodyPointSize: 13) == 1)
|
||||
#expect(StyleEditorLayout.columns(fitting: -100, bodyPointSize: 13) == 1)
|
||||
#expect(StyleEditorLayout.columns(fitting: StyleEditorLayout.wellSide(bodyPointSize: 13),
|
||||
bodyPointSize: 13) == 1)
|
||||
}
|
||||
|
||||
@Test("The sidebar's content width is the column minus its two gutters")
|
||||
|
||||
Reference in New Issue
Block a user