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
+16 -1
View File
@@ -138,6 +138,11 @@ struct BoardWindowHost: View {
// 10-accessibility.md's full-relative-scaling rule): at a large system text size a
// 640×400 floor would be narrower than two lane headers, and "every lane is always on
// screen" would degrade into a strip of truncation.
//
// **The *system* size, not the zoomed one** (03-board-ui.md Layout zoom: "Zoom never
// moves the window"). Zooming to 200% must not push a floor up under a window the user
// already sized: moving the window belongs to the right-edge lane drag alone, and a
// minimum that grew with the level would resize every open board from a menu item.
.frame(
minWidth: BoardMetrics.windowMinimumSize(bodyPointSize: BoardMetrics.bodyPointSize).width,
minHeight: BoardMetrics.windowMinimumSize(bodyPointSize: BoardMetrics.bodyPointSize).height
@@ -193,6 +198,11 @@ struct BoardWindowHost: View {
openCard: openCard,
search: boardSearch
)
// **The zoom level enters here and nowhere else** (03-board-ui.md Layout zoom).
// On `BoardView` rather than on the `VStack`, deliberately: the level is the *board's*
// ruler, so the banner strip and the transient search bar above it chrome, not the
// board stay at the system's size, as do the sheets and popovers this window hosts.
.environment(\.boardZoom, appModel.zoom.context)
}
// The transient strip's two dismissal inputs (`BoardSearchPresentation
// .transientPersists`): it stays while a query is filtering the board or while the field
@@ -761,7 +771,12 @@ struct BoardWindowHost: View {
// accessory's reason exactly: it carries the store, and it is a board window's, not every
// hosted window's. Its search item is the search field's home, and it is what tells
// `boardSearch` whether that home still exists.
windowController.installToolbar(BoardToolbar.controller(store: store, search: boardSearch))
windowController.installToolbar(BoardToolbar.controller(
store: store,
search: boardSearch,
zoom: appModel.zoom,
session: appModel.dragSession
))
}
// MARK: - Closing