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:
@@ -114,6 +114,19 @@ public enum AppPreferences {
|
||||
/// the key is declared here with its neighbours for `WindowID`'s reason.
|
||||
public static let quickStyleBackgroundsKey = "quickStyleBackgrounds"
|
||||
|
||||
/// **The board's zoom level** — "app-wide and persisted across restarts" (11-command-nexus.md
|
||||
/// ▸ View ▸ Actual Size; 03-board-ui.md ▸ Layout — zoom). A rung on `BoardZoom.levels`, stored as
|
||||
/// the multiplier itself. Read and written by `BoardZoomStore`, which owns the ladder's rules; the
|
||||
/// key is declared here with its neighbours for `WindowID`'s reason.
|
||||
///
|
||||
/// **Every read goes through `BoardZoom.normalize`**, and this one cannot use the
|
||||
/// `object(forKey:) as? Double ?? default` idiom its neighbours use to tell "off" from "never set"
|
||||
/// — the trap here is worse than an ambiguous default. `double(forKey:)` answers 0 for an unset
|
||||
/// key, and 0 is not merely a wrong level: it drives every `BoardMetrics.em` multiple to its 1pt
|
||||
/// floor and draws a board of hairlines. Normalising is what makes an unset, hand-edited or
|
||||
/// stale-from-a-future-build value indistinguishable from a legal one downstream.
|
||||
public static let boardZoomLevelKey = "boardZoomLevel"
|
||||
|
||||
/// The cached subscription facts behind the tier decision (12-editions.md ▸ The entitlement) —
|
||||
/// JSON-encoded `SubscriptionFacts`, read and written by `ProEntitlement`.
|
||||
///
|
||||
@@ -252,6 +265,12 @@ public final class AppModel {
|
||||
/// board-scoped and this list deliberately is not.
|
||||
public let styleRecents: StyleRecents
|
||||
|
||||
/// The board's app-wide zoom level (03-board-ui.md ▸ Layout — zoom). Owned here for
|
||||
/// `styleRecents`' reason exactly: app-scoped, persisted beside it, and reached by every board
|
||||
/// window through the environment — while the menu rows and the toolbar buttons, which live
|
||||
/// outside every scene's environment, reach it through this object.
|
||||
public let zoom: BoardZoomStore
|
||||
|
||||
/// The app's one drag session (DRAG-REORDER.md; 04-interactions.md ▸ Drag and drop).
|
||||
///
|
||||
/// App-wide for the reason cross-board drags exist at all: **a drag crosses windows**, so the
|
||||
@@ -668,6 +687,7 @@ public final class AppModel {
|
||||
) {
|
||||
boardRegistry = BoardRegistry(storageURL: registryStorageURL)
|
||||
styleRecents = StyleRecents(defaults: preferences)
|
||||
zoom = BoardZoomStore(defaults: preferences)
|
||||
clipboard = ClipboardStore(stagingRoot: clipboardStagingRoot)
|
||||
// Reads the cached facts and nothing else — no StoreKit API is touched until
|
||||
// `ProEntitlement.start()`, which the app's launch calls and a test host never does.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user