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:
@@ -90,12 +90,23 @@ struct TrashLaneView: View {
|
||||
/// specifically ("and trash animations all get reduced variants").
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
/// Reduce Transparency, for the column's two washes below — "glass underlays go solid, wherever
|
||||
/// they appear" (10-accessibility.md). The trash plate and its hatched header are the board's
|
||||
/// only translucent surfaces, and they sit over a background the *user* chose (03-board-ui.md §
|
||||
/// Styling), which is exactly the case the setting exists for (`Accommodations.Wash`).
|
||||
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
|
||||
|
||||
/// The live body metric — this column's geometry is `LaneView`'s, derived from the same font
|
||||
/// (`BoardMetrics`), because these are the same cards in a column that must read as their
|
||||
/// sibling.
|
||||
private var pointSize: CGFloat { BoardMetrics.bodyPointSize }
|
||||
|
||||
/// The lane plate's corner radius — matched to `LaneView`'s so the column reads as a sibling of
|
||||
/// the lanes rather than as a different kind of object.
|
||||
private let cornerRadius: CGFloat = 10
|
||||
private var cornerRadius: CGFloat { BoardMetrics.laneCornerRadius(bodyPointSize: pointSize) }
|
||||
|
||||
/// Between the cards — `LaneView.cardSpacing`, because these are the same cards.
|
||||
private let cardSpacing: CGFloat = 8
|
||||
private var cardSpacing: CGFloat { BoardMetrics.cardSpacing(bodyPointSize: pointSize) }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@@ -104,7 +115,7 @@ struct TrashLaneView: View {
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(.quaternary.opacity(0.35))
|
||||
.fill(Accommodations.trashPlateWash(reduceTransparency: reduceTransparency).style)
|
||||
)
|
||||
// **The delete gesture's drop target**, over the whole column — the header included, since
|
||||
// the ruling is "dropping a live card on the shown trash", not on one of its rows. It
|
||||
@@ -186,7 +197,7 @@ struct TrashLaneView: View {
|
||||
/// It carries no gesture at all: no selection (the column "is never selectable as a lane"), no
|
||||
/// reorder drag, no context menu.
|
||||
private var header: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: BoardMetrics.laneHeaderSpacing(bodyPointSize: pointSize)) {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(.secondary)
|
||||
.imageScale(.medium)
|
||||
@@ -198,13 +209,16 @@ struct TrashLaneView: View {
|
||||
countBadge
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, BoardMetrics.trashHeaderHorizontalPadding(bodyPointSize: pointSize))
|
||||
.padding(.vertical, BoardMetrics.trashHeaderVerticalPadding(bodyPointSize: pointSize))
|
||||
.background {
|
||||
UnevenRoundedRectangle(topLeadingRadius: cornerRadius, topTrailingRadius: cornerRadius)
|
||||
.fill(.quaternary.opacity(0.5))
|
||||
.fill(Accommodations.trashHeaderWash(reduceTransparency: reduceTransparency).style)
|
||||
.overlay {
|
||||
DiagonalHatch()
|
||||
// The hatch pitch scales with the text (`BoardMetrics.trashHatchSpacing`): this
|
||||
// is the trash's **non-colour** distinction, and a fixed 7pt pitch behind a
|
||||
// headline twice its usual size reads as a texture rather than as hatching.
|
||||
DiagonalHatch(spacing: BoardMetrics.trashHatchSpacing(bodyPointSize: pointSize))
|
||||
.stroke(.quaternary, lineWidth: 1)
|
||||
.clipShape(UnevenRoundedRectangle(
|
||||
topLeadingRadius: cornerRadius,
|
||||
@@ -224,8 +238,8 @@ struct TrashLaneView: View {
|
||||
.font(.caption)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 1)
|
||||
.padding(.horizontal, BoardMetrics.badgeHorizontalPadding(bodyPointSize: pointSize))
|
||||
.padding(.vertical, BoardMetrics.badgeVerticalPadding(bodyPointSize: pointSize))
|
||||
.background(Capsule().fill(.quaternary))
|
||||
}
|
||||
|
||||
@@ -274,7 +288,7 @@ struct TrashLaneView: View {
|
||||
// The delete gesture's shadow, holding the topmost row open
|
||||
// (04-interactions.md ▸ The trash). At the nominal card height: the cards
|
||||
// being proposed have no face here yet to be measured.
|
||||
DragShadow(cornerRadius: CardFaceMetrics.cornerRadius)
|
||||
DragShadow(cornerRadius: BoardMetrics.cardCornerRadius(bodyPointSize: pointSize))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: LaneDropRegistry.nominalCardHeight)
|
||||
}
|
||||
@@ -306,7 +320,7 @@ struct TrashLaneView: View {
|
||||
// column's gesture surface is full height" (04-interactions.md ▸ The trash, settled)
|
||||
// needs that blank space to actually belong to the view the gesture below is on.
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.padding(6)
|
||||
.padding(BoardMetrics.lanePlatePadding(bodyPointSize: pointSize))
|
||||
.contentShape(Rectangle())
|
||||
// The band's trash-side surface. It arms from the column's empty space, full height
|
||||
// (above) included, so a drag can start from the blank area below the last card exactly
|
||||
@@ -354,8 +368,12 @@ private enum TrashSlot: Identifiable {
|
||||
///
|
||||
/// The lines start a full header-height to the left of the leading edge so the first stroke reaches
|
||||
/// the top-left corner instead of beginning partway across.
|
||||
///
|
||||
/// The pitch is the caller's (`BoardMetrics.trashHatchSpacing`), because it scales with the text:
|
||||
/// the hatch is the trash's non-colour distinction (10-accessibility.md's never-colour-alone rule),
|
||||
/// and a distinction that stops reading at a large text size is not one.
|
||||
private struct DiagonalHatch: Shape {
|
||||
var spacing: CGFloat = 7
|
||||
var spacing: CGFloat
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
Reference in New Issue
Block a user