The board learns to zoom — eight rungs on one ruler, and Actual Size is the untouched board

View ▸ Zoom In / Zoom Out / Actual Size (⌘+ / ⌘− / ⌘0): 75%–200% in eight
rungs, app-wide and persisted (the Show Comments precedent) — a viewing
comfort, not a property of any one board. The level travels as
BoardZoomContext in the environment, injected on BoardView alone so the
banner strip, search bar, sheets and popovers stay at the system size; the
environment is also what carries it through CardFaceView's equality gate,
which compares nothing that moves with the level. Every BoardMetrics figure
follows zoom.bodyPointSize — card and lane chrome, drag replicas and the
count badge, the resize handle, the trash column — and the drop registry
carries the ruler for event-time reads, with the autoscroller's three
reaches turning font-derived (reachSide named as the stripGap it always
equalled). Lanes still divide the window; zoom never moves the window or
its floor. The toolbar gains a catalog-only Zoom In/Out pair mirroring the
menu rows' predicate; zoom holds shut mid-drag (frozen geometry), each rung
announces itself to VoiceOver, and the render suite pins both invariants:
a rung repaints every face, a no-op Actual Size repaints nothing.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-07 11:22:02 -04:00
parent d5ad21c3da
commit cfee4a4b41
29 changed files with 1491 additions and 101 deletions
+34 -9
View File
@@ -50,22 +50,47 @@ enum DragAutoScrollMath {
/// How far above the visible area the pointer may sit and still drive it enough to cover the
/// lane's header, which is where a drag naturally goes to scroll up.
static let reachAbove: CGFloat = 48
///
/// **Font-derived, unlike the three figures above it, and the split is the point.** `band`,
/// `minSpeed` and `maxSpeed` describe the *cursor's* relationship to an edge how near counts as
/// near, how fast the content should answer and none of that changes because the board is drawn
/// larger; they stay fixed for `LaneResizeSession.reentry`'s reason. The three reaches describe
/// **where the lane's furniture is**, and that furniture moves: this one is specified as "enough
/// to cover the lane's header", and a header at 200% zoom is twice as tall as the 48 points that
/// covered it at 13pt (03-board-ui.md Layout zoom).
///
/// 3.7 em: 48 points at the standard body size, which is what the reach has always been.
static func reachAbove(bodyPointSize: CGFloat) -> CGFloat {
BoardMetrics.em(3.7, bodyPointSize: bodyPointSize)
}
/// The same below, covering the lane's bottom padding.
static let reachBelow: CGFloat = 24
/// The same below, covering the lane's bottom padding 1.85 em, the standard size's 24 points.
static func reachBelow(bodyPointSize: CGFloat) -> CGFloat {
BoardMetrics.em(1.85, bodyPointSize: bodyPointSize)
}
/// The sideways reach kept under half the distance between two lanes' scroll areas so only
/// one lane ever engages.
static let reachSide: CGFloat = 12
///
/// **It is the inter-lane gap, named as itself** rather than as a coincidentally equal number:
/// the distance between two lanes' scroll areas is the gap plus a plate padding on each side
/// (0.9 + 2 × 0.45 em), so half of it is exactly `stripGap`. Writing it that way is what keeps
/// the sentence above true at every zoom level instead of only at 13pt, where 12 happened to be
/// the answer.
static func reachSide(bodyPointSize: CGFloat) -> CGFloat {
BoardMetrics.stripGap(bodyPointSize: bodyPointSize)
}
/// The region in the visible area's own coordinates, `(0, 0)` at its top-left a pointer
/// must be in to drive this scroller at all.
static func engagementRect(viewport: CGSize) -> CGRect {
CGRect(x: -reachSide,
y: -reachAbove,
width: viewport.width + reachSide * 2,
height: viewport.height + reachAbove + reachBelow)
static func engagementRect(viewport: CGSize, bodyPointSize: CGFloat) -> CGRect {
let above = reachAbove(bodyPointSize: bodyPointSize)
let below = reachBelow(bodyPointSize: bodyPointSize)
let side = reachSide(bodyPointSize: bodyPointSize)
return CGRect(x: -side,
y: -above,
width: viewport.width + side * 2,
height: viewport.height + above + below)
}
/// Signed scroll velocity in points/second for a pointer at `position` along an axis whose