Files
lanework/Kanban/UI/Board/DragShadow.swift
T
rzen 8564814754 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
2026-07-29 08:48:25 -04:00

81 lines
3.8 KiB
Swift

import SwiftUI
/// The **shadow placeholder** — the outline occupying an item's proposed landing spot
/// (DRAG-REORDER.md § The pieces).
///
/// One shape for both the drag's shadows and the lane resize's resting footprint, because they are
/// the same thing said twice: "here is the space this item occupies in the layout". They differ only
/// in whether the space is a *proposal* — the drag's is dashed, because a dashed outline is what
/// 03-board-ui.md's placeholder vocabulary asks for and what makes "the drop lands exactly here"
/// legible; the resize's is a plain fill, because the lane is already there.
///
/// **Hit-transparent, always.** The strip's own drop target has to stay live beneath the shadows
/// (DRAG-REORDER.md § Single-target dispatch: "shadow placeholders are hit-transparent, so the strip
/// target stays live beneath them"), and a shadow that swallowed the release would strand the drop.
struct DragShadow: View {
/// Matched to the surface it stands in for — the lane's plate radius or the card's, both
/// font-derived (`BoardMetrics`) and both supplied by the caller, which is the one that knows
/// which surface this shadow is standing in for.
let cornerRadius: CGFloat
/// A drop proposal (dashed) or a resting footprint (plain).
var dashed: Bool = true
/// Increase Contrast and Reduce Transparency, for the outline and the fill below
/// (10-accessibility.md; `Accommodations`). A shadow's whole job is to say "here", and both
/// halves of how it says so are alpha-based by default.
@Environment(\.colorSchemeContrast) private var contrast
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
var body: some View {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(Accommodations.dragShadowWash(reduceTransparency: reduceTransparency).style)
.overlay {
if dashed {
RoundedRectangle(cornerRadius: cornerRadius)
.strokeBorder(
Color.accentColor.opacity(Accommodations.accentOpacity(0.55, contrast: contrast)),
style: StrokeStyle(
lineWidth: Accommodations.borderWidth(1.5, contrast: contrast),
dash: [6]
)
)
}
}
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}
/// The count badge a multi-drag's replica wears — Finder's own affordance for "this many are coming
/// with me" (04-interactions.md ▸ Drag and drop: "dragging any member of a multi-selection drags the
/// whole selection").
///
/// Draws nothing for a single item, so every replica can carry it unconditionally.
struct DragCountBadge: View {
let count: Int
/// The badge is a disc around a numeral, so every figure in it follows the numeral's font
/// (`BoardMetrics`, 10-accessibility.md's full-relative-scaling rule) — at the standard body size
/// they are the 6, 3 and 10 points it has always drawn.
private var pointSize: CGFloat { BoardMetrics.bodyPointSize }
var body: some View {
if count > 1 {
Text("\(count)")
.font(.caption2.bold())
.monospacedDigit()
.foregroundStyle(.white)
.padding(.horizontal, BoardMetrics.em(0.45, bodyPointSize: pointSize))
.padding(.vertical, BoardMetrics.em(0.25, bodyPointSize: pointSize))
.background(Circle().fill(Color.accentColor))
.offset(
x: BoardMetrics.em(0.75, bodyPointSize: pointSize),
y: -BoardMetrics.em(0.75, bodyPointSize: pointSize)
)
}
}
}