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
+19 -7
View File
@@ -64,6 +64,15 @@ final class LaneDropRegistry {
}
}
/// The board's current ruler, written by `BoardView` from the zoom environment (03-board-ui.md
/// Layout zoom).
///
/// It lives here for `stripFrame`'s reason: the drop delegates run at event time, outside any
/// body evaluation, so a number they need has to be *on* the registry rather than captured from a
/// view's last render. The default is the system size, which is what an un-zoomed board and every
/// test that never sets it get.
var bodyPointSize: CGFloat = BoardMetrics.bodyPointSize
/// The height a card with no registered measurement is assumed to have a lane whose faces have
/// not laid out yet. Nominal rather than zero, so the resting rows still tile.
///
@@ -73,9 +82,12 @@ final class LaneDropRegistry {
/// every un-measured slot boundary in the wrong place at a large system text size and the drop
/// model is forbidden from reading measured frames mid-flight (03-board-ui.md § Motion), so this
/// guess is all it has until the lane lays out.
@MainActor
static var nominalCardHeight: CGFloat {
BoardMetrics.nominalCardHeight(bodyPointSize: BoardMetrics.bodyPointSize)
///
/// An instance property rather than a static for exactly that reason extended to zoom: the guess
/// has to be made on the ruler the board is *currently* drawing with, and the level is a property
/// of the window this registry belongs to.
var nominalCardHeight: CGFloat {
BoardMetrics.nominalCardHeight(bodyPointSize: bodyPointSize)
}
/// The board strip's own frame in the window's SwiftUI global space the origin the strip
@@ -246,7 +258,7 @@ final class RestingLayoutCache {
heights.reserveCapacity(lane.cards.count)
for card in lane.cards where !hidden.contains(card.id) {
cardIDs.append(card.id)
heights.append(registry.heights[card.id] ?? LaneDropRegistry.nominalCardHeight)
heights.append(registry.heights[card.id] ?? registry.nominalCardHeight)
}
let layout = LaneRestingLayout(cardIDs: cardIDs, heights: heights)
@@ -449,7 +461,7 @@ struct BoardDropContext {
heights: resting.heights,
// The run's footprint at the landing spot: the first dragged card's frozen height, which
// is the trigger rect the cursor is over (the rest stack below it).
draggedHeight: session.cardHeights.first ?? LaneDropRegistry.nominalCardHeight,
draggedHeight: session.cardHeights.first ?? registry.nominalCardHeight,
current: session.laneProposal(onBoardRooted: store.rootKey, laneID: laneID)
)
guard let slot else { return } // a dead region: hold
@@ -661,7 +673,7 @@ struct BoardDropContext {
}
let rendered = lane.cards
let heights = rendered.map { registry.heights[$0.id] ?? LaneDropRegistry.nominalCardHeight }
let heights = rendered.map { registry.heights[$0.id] ?? registry.nominalCardHeight }
let count = FinderDrop.shadowCount(info.itemProviders(for: [.fileURL]))
let landing = FileDropZones.landing(
@@ -669,7 +681,7 @@ struct BoardDropContext {
headerBottom: registry.headers[laneID]?.maxY,
placement: grid.placement,
heights: heights,
nominalHeight: LaneDropRegistry.nominalCardHeight,
nominalHeight: registry.nominalCardHeight,
current: session.fileLaneProposal(onBoardRooted: store.rootKey, laneID: laneID)?.index
)