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
This commit is contained in:
@@ -38,6 +38,20 @@ struct WelcomeView: View {
|
||||
/// Whether the recents list holds the keyboard, so Return can mean "open the selected row".
|
||||
@FocusState private var listFocused: Bool
|
||||
|
||||
// MARK: - Geometry
|
||||
//
|
||||
// Every figure this window lays out on, as a multiple of the body font — `BoardMetrics`' rule
|
||||
// applied to the welcome window (10-accessibility.md ▸ Text scaling & visual accommodations). At
|
||||
// the standard 13pt body they reproduce the numbers welcome has always drawn.
|
||||
|
||||
@MainActor static var pointSize: CGFloat { BoardMetrics.bodyPointSize }
|
||||
|
||||
@MainActor static var brandingColumnWidth: CGFloat { BoardMetrics.em(23, bodyPointSize: pointSize) }
|
||||
@MainActor static var brandingInset: CGFloat { BoardMetrics.em(2.5, bodyPointSize: pointSize) }
|
||||
@MainActor static var appIconSide: CGFloat { BoardMetrics.em(7.4, bodyPointSize: pointSize) }
|
||||
@MainActor static var minimumWidth: CGFloat { BoardMetrics.em(58.5, bodyPointSize: pointSize) }
|
||||
@MainActor static var minimumHeight: CGFloat { BoardMetrics.em(35.4, bodyPointSize: pointSize) }
|
||||
|
||||
private var derivation: WelcomeRow.Derivation {
|
||||
WelcomeRow.derive(recents: appModel.recents, failures: appModel.launchFailures)
|
||||
}
|
||||
@@ -49,16 +63,20 @@ struct WelcomeView: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
branding
|
||||
.frame(width: 300)
|
||||
.frame(width: Self.brandingColumnWidth)
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(32)
|
||||
.padding(Self.brandingInset)
|
||||
|
||||
Divider()
|
||||
|
||||
recents
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.frame(minWidth: 760, minHeight: 460)
|
||||
// Font-derived, like the board window's floor (10-accessibility.md ▸ Text scaling: "no fixed
|
||||
// point sizes"): at a large system text size a 300-point branding column would clip the app
|
||||
// name it exists to show, and a 760 × 460 floor would leave the recents list too narrow for
|
||||
// the three lines each row carries. At the standard body size these are those numbers.
|
||||
.frame(minWidth: Self.minimumWidth, minHeight: Self.minimumHeight)
|
||||
// The window has no title bar, so the background is the drag handle. `.gesture` rather than
|
||||
// `.highPriorityGesture`: a click on a row or a button belongs to the row or the button.
|
||||
.gesture(WindowDragGesture())
|
||||
@@ -76,12 +94,16 @@ struct WelcomeView: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 96, height: 96)
|
||||
.frame(width: Self.appIconSide, height: Self.appIconSide)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("Lanework")
|
||||
.font(.system(size: 34, weight: .light))
|
||||
.padding(.top, 12)
|
||||
// A **relative** style, not a 34pt literal — "relative text styles everywhere, no
|
||||
// fixed point sizes" (10-accessibility.md ▸ Text scaling & visual accommodations).
|
||||
// `.largeTitle` is the app name's register and it grows with the system text size;
|
||||
// a fixed size would have stayed put while every line beneath it grew past it.
|
||||
.font(.largeTitle.weight(.light))
|
||||
.padding(.top, BoardMetrics.em(0.9, bodyPointSize: Self.pointSize))
|
||||
|
||||
Text(versionSummary)
|
||||
.font(.callout)
|
||||
@@ -90,11 +112,11 @@ struct WelcomeView: View {
|
||||
Text("Folders and Markdown, on your terms.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
.padding(.top, 4)
|
||||
.padding(.top, BoardMetrics.em(0.3, bodyPointSize: Self.pointSize))
|
||||
|
||||
Spacer(minLength: 24)
|
||||
Spacer(minLength: BoardMetrics.em(1.85, bodyPointSize: Self.pointSize))
|
||||
|
||||
VStack(spacing: 8) {
|
||||
VStack(spacing: BoardMetrics.em(0.6, bodyPointSize: Self.pointSize)) {
|
||||
// The menu-bar twin of this button is File ▸ New Board… (⌥⌘N) — same action, and
|
||||
// deliberately the same words, because a button and a menu item that differ read as
|
||||
// two features.
|
||||
@@ -216,7 +238,7 @@ struct WelcomeView: View {
|
||||
}
|
||||
.controlSize(.small)
|
||||
}
|
||||
.padding(16)
|
||||
.padding(BoardMetrics.em(1.25, bodyPointSize: Self.pointSize))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
@@ -249,7 +271,7 @@ private struct WelcomeActionButton: View {
|
||||
Button(action: action) {
|
||||
Label(title, systemImage: systemImage)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.vertical, BoardMetrics.em(0.15, bodyPointSize: WelcomeView.pointSize))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.large)
|
||||
@@ -271,9 +293,13 @@ private struct RecentBoardRow: View {
|
||||
// lenient-fallback shape every other icon site in the app uses (`ItemSymbol`,
|
||||
// `Palette`), applied here to the registry's cached string instead of a live snapshot.
|
||||
Image(systemName: iconName)
|
||||
.font(.system(size: 22))
|
||||
// Relative, like every other size in this window (10-accessibility.md's
|
||||
// full-relative-scaling rule): `.title` is the register a 22pt glyph occupied at the
|
||||
// standard text size, and the well around it is derived from the body font so the
|
||||
// glyph never outgrows it.
|
||||
.font(.title)
|
||||
.foregroundStyle(iconTint)
|
||||
.frame(width: 34, height: 34)
|
||||
.frame(width: iconWell, height: iconWell)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
@@ -292,13 +318,19 @@ private struct RecentBoardRow: View {
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.padding(.vertical, BoardMetrics.em(0.3, bodyPointSize: WelcomeView.pointSize))
|
||||
// Dimmed when the board cannot be reached — the row stays, with Forget, rather than
|
||||
// disappearing (02 § Graceful orphaning).
|
||||
.opacity(row.isAvailable ? 1 : 0.55)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
/// The square the row's glyph sits in — font-derived so the icon column stays proportionate to
|
||||
/// the three lines of text beside it at every system text size.
|
||||
private var iconWell: CGFloat {
|
||||
BoardMetrics.em(2.6, bodyPointSize: BoardMetrics.bodyPointSize)
|
||||
}
|
||||
|
||||
/// `row.icon`'s symbol if it names one this system can draw, the board default otherwise —
|
||||
/// `ItemSymbol.name(_:fallback:)`'s rule, restated for a plain cached string rather than a
|
||||
/// `FieldValue`: a record carries no `FieldValue`, so `missing`/`malformed`/`unrecognized`
|
||||
@@ -365,7 +397,10 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.frame(width: 420)
|
||||
// Font-derived: this pane is `.fixedSize()`, so a 420-point literal would clip its one
|
||||
// toggle's footer sentence at a large system text size with no way to resize out of it
|
||||
// (10-accessibility.md ▸ Text scaling).
|
||||
.frame(width: BoardMetrics.em(32.3, bodyPointSize: BoardMetrics.bodyPointSize))
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user