Files
lanework/DESIGN/10-accessibility.md
T
rzen 71e7f85328 Registry-cache the welcome row's title and icon with live write-through
The row's name and icon implicitly required reading each recent board's
root index.md at welcome time — the hang the counts rule was written to
avoid. Settled: title/icon/iconColor live in the registry record; they
refresh whenever an open board's reload changes them (in-app renames land
instantly, foreign renames of open boards ride along), with closed-board
renames stale until next open like the counts.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:42:31 -04:00

12 KiB

Accessibility

The stance is committed in 00-vision.md: accessibility is a requirement of "native to the bone," not an afterthought. This document is the design iteration that stance promised — VoiceOver correctness on the custom board, text scaling, and the system visual accommodations. The keyboard-first contract (04-interactions.md) is the floor this builds on.

Principles

  • The keyboard path is the accessibility path. Every function's drag-free guarantee is its keyboard/menu path (04-interactions.md's contract: every board function has a menu item — the menu home is the keyboard path, with or without a default chord), and VoiceOver rides those same paths. Bespoke accessibility-only UI is a last resort — if a function needs a drag-free alternative, it belongs in the context menu or menu bar, where all users get it. Concretely: card movement is the clipboard (⌘X, select target, ⌘V — 04's paste-lands-after-anchor/appends-to-lane rules make cut/paste a complete move story), not a parallel set of "Move to lane" accessibility actions.
  • Logical order over visual geometry. Assistive traversal follows the board's semantic order (lane order, then card order), never rendering artifacts like masonry column position.
  • Announce what the files do. A live board that changes under external writers must say so — silence about a mutating board is a lie to a VoiceOver user.

The board through VoiceOver

  • Tree shape: window → lanes (accessibility containers, in lane order) → cards (leaf elements, in card order). A lane container is labeled "⟨title⟩, lane, N cards" — the count reads the search filter like the visible badge (04-interactions.md). The lane header's new-card button is a labeled child ("New card in ⟨lane⟩"). A card is one flattened element: label = title (or the untitled placeholder), value carries the attachment count when present, selected state via trait. Face icon and chips are decorative — folded into the element, never separately focusable. The sole-selection attachment carousel (03-board-ui.md) is decorative too — page dots and paging included, nothing focusable: the flattened element already carries the attachment count in its value, and the accessible attachment surface is the card window's keyboard-native section (below).
  • Logical order, not masonry position (decided): within a wide lane, VoiceOver reads cards by order — the interior grid columns are presentation only. This deliberately diverges from on-screen geometry; the spatial arrow-key model (04-interactions.md) remains available alongside, since board keyboard navigation keeps working with VoiceOver running.
  • VO cursor and app selection are independent (Finder-style): moving the VoiceOver cursor never mutates selection. VO-Space on a card toggles its selection (the ⌘-click analogue — a toggle, never plain click's replace, 04-interactions.md ▸ Selection); ⌘↩ opens the card window; arrow keys and ⇧-arrows drive selection exactly as without VoiceOver. Selection state is always readable from the element (trait), and cut cards expose their dimmed pending state in the value ("cut, pending paste").
  • Actions come from the context menu. Context menus are the single inventory of per-item actions (Open, Rename, Delete, Put Back, Delete Immediately, width stepper, …), reachable the standard way (VO-⇧-M); where SwiftUI additionally surfaces menu items as custom accessibility actions, that's free improvement, not a separate design surface.
  • Rotor: lane titles are headings, so the headings rotor jumps lane-to-lane — on a one-dimensional board that is structural navigation; no custom rotors unless practice shows the need.
  • Trash quasi-lane: when shown (View ▸ Show Trash), it is the last container, labeled as Trash with its count; toggling visibility is announced. Tombstoned cards read their deletion state and expose Put Back / Delete Immediately via the context menu; there is no Open (03-board-ui.md's no-editing-in-the-trash). A tombstoned lane's single entry reads "⟨title⟩, deleted lane, N cards" and exposes the same actions; keyboard reachability follows 04-interactions.md's homogeneous-by-kind trash rules.
  • Search: filtered-out cards leave layout and the accessibility tree together — the filter is the single source of truth for "what's on the board" (04-interactions.md), and the tree is one of its readers. Lane counts announced reflect the filter.

Moving without dragging

  • Cards: clipboard. ⌘X the selection, move selection to the destination (arrows), ⌘V — between lanes, within a lane (paste lands after the anchor card), and across boards (04-interactions.md's staged clipboard). This is the committed drag-free move story; it needs no VoiceOver-specific machinery because selection and paste targeting are already keyboard-native.
  • Lanes: the defect this doc originally named (lanes had no keyboard-move path) is closed by the keyboard map — with a lane selected, ⌘←/⌘→ move it (Board ▸ Move Left / Move Right, 04-interactions.md); cards gain ⌥⌘↑/⌥⌘↓ within-lane sorting, and cross lanes drag-free via cut/paste (04-interactions.md's clipboard rules). Lanes carry the clipboard too (resettled, 04-interactions.md ▸ Clipboard), so cross-board lane copy/move — once drag-only, the contract's last gap — is ⌘C/⌘X, then ⌘V with the destination board frontmost.
  • Lane resize: the header context menu's width stepper (03-board-ui.md) — and its keyboard face, the Increase/Decrease Lane Width menu items (⌥⌘→/⌥⌘←, 11-command-nexus.md) — is the accessible path; edge drag is enhancement only.
  • Attachments: the card window's sidebar items expose Open / Reveal in Finder / Remove via context menu, and the section is keyboard-navigable outright (arrows, Space-QuickLook, Return, ⌫ — 05-card-window.md); adding files drag-free is File ▸ Add Attachment… (⇧⌘A, 11-command-nexus.md) alongside Finder-drop.

Live board announcements

  • Foreign changes announce, app-mediated echoes never do. The auto-committer already classifies every observed change as app-mediated or foreign (the EchoLedger — 02-architecture.md ▸ Components) and synthesizes diff summaries for commit messages (06-history-undo.md); announcements reuse that summarizer — one polite (non-interrupting) digest per reload debounce ("Board changed: 2 cards edited, 1 card added"), never per-file chatter. On no-git boards the same classifier runs without the committer — announcements don't depend on git mode.
  • A vanishing focus is called out specifically. If the selected or VO-focused card disappears in a reload (deleted externally, or hidden by a lane tombstone), the announcement names it ("Card 'Fix login' was deleted externally") and focus recovers to the card's lane (mirroring selection's reload-survival rules, 02-architecture.md). When the lane itself vanished, recovery walks up then sideways (settled): focus lands on the lane now occupying the vanished lane's position — the next lane by order, else the previous one — and on the board container only when no lanes remain (the ⌫-successor pattern, 04-interactions.md, applied to external change; never into the trash, which stays hidden — no layout side effects from a foreign edit). The announcement then names the lane, not the card ("Lane 'Doing' was deleted externally, with 5 cards") — the implied-events-don't-steal-the-subject discipline of 06-history-undo.md's composer, applied to speech.
  • Bracketed operations announce once, at completion ("Pulled 3 commits", "Switched to branch 'redesign'") — never their internal churn (02-architecture.md's bracketing). The live-reload-resilience banner (02-architecture.md) is an accessibility element and is announced when it appears and when it clears — including the read-only lock after a failed bracketed reload.

Card window, welcome, template chooser, popover

  • Card window: standard controls, standard labels. The attributes sidebar is a labeled container of labeled sections; attachment rows are elements labeled by filename; the Details section's unknown-key rows read as static text ("⟨key⟩, ⟨value⟩"); the bottom actions are ordinary buttons. Preview renders to the accessibility tree as structured text — headings navigable by rotor, lists and tables read as such; task-list checkboxes are real accessible checkboxes, toggleable without the pointer (05-card-window.md's live checkboxes); body images use Markdown alt text when present, else the filename. Edit and the raw-source outlet are ordinary accessible text editors; the Preview/Edit toggle (⌘E) announces its state.
  • Welcome window: recents rows are elements labeled "⟨name⟩, ⟨location⟩, N lanes, M cards" (name, icon, and counts all registry-cached — 02-architecture.md; the row never reads a board's files); row actions (Open / Reveal in Finder / Forget) via context menu; unavailable rows say so ("unavailable — board not found").
  • Template chooser: templates are elements labeled by title; the mini per-lane previews are decorative and hidden from the tree.
  • Board popover: labeled controls throughout; the ahead/behind indicator's information — counts, queued pushes, last error — must be readable as text, never conveyed by color or shape alone.
  • Style editor (card sidebar section, board popover, Style… popover — 03-board-ui.md ▸ Styling ▸ Controls): grids are arrow-navigable, every well Tab-reachable and labeled by name (palette color, symbol name; leading wells "None" / "Default"); the current value is stated by trait, and a batch selection's mixed state reads as "mixed", never conveyed by highlight alone.

Text scaling & visual accommodations

  • Full relative scaling (decided): relative text styles everywhere, no fixed point sizes. Card face, lane header, and masonry metrics derive from font metrics, so layout survives the largest system text sizes; the no-horizontal-scroll invariant is untouched (lane count is the user's choice; lanes scroll vertically), and 03-board-ui.md's graceful-truncation rules apply at every scale.
  • Contrast is pinned to WCAG AA. Lane and card colors render as edge accents (03-board-ui.md's top-edge band / left-edge stripe), so text never sits on them — they are supplementary decoration, never the sole carrier of information, and carry no text-contrast obligation. The ≥ 4.5:1 automatic-contrast rule binds where text does sit on a user-chosen color: the board background (palette pairs verified at design time; arbitrary hex computes its text color at runtime against that threshold). An #RRGGBBAA background with alpha computes against the color composited over its effective backdrop in the active appearance (the board's over the window background; light and dark resolve differently), recomputed on appearance change. Increase Contrast strengthens borders and the selection indicator.
  • State is never color-alone: selection is a ring plus trait, cut-pending is dim plus stated value, the trash header is hatched plus labeled — all already patterned; kept as a rule.
  • Reduce Motion: reflow-on-drag, search animate-out, the drag replica, the lane-resize rubber-band feedback (03-board-ui.md ▸ Lane — the selection marquee never animates and needs no variant), and trash animations all get reduced variants (crossfade or instant). Reduce Transparency: glass underlays (carousel page dots) go solid.
  • Full Keyboard Access (independent of VoiceOver): the board is one tab stop with arrow-key navigation within; every control — lane buttons, popover, card window, welcome — is Tab-reachable.

Verification

  • Automated audits are test failures: Xcode's accessibility audit (performAccessibilityAudit) runs in UI tests over every surface — board (trash shown and hidden), card window (Preview, Edit, raw source), welcome, template chooser, board popover.
  • A manual VoiceOver smoke script lives with the test plan: create lane → create card → rename → cut/paste to another lane → external edit lands (announcement heard) → delete → Put Back → Empty Trash. Run per release; it is the canonical "does the board actually work blind" check.

Changes from Kanban

  • All new ground — the pathfinder never had an accessibility pass. Nothing carries over.

Open questions

None currently — the lane-move gap and the Add Attachment menu path are both closed by the keyboard map (04-interactions.md's contract, inventoried in 11-command-nexus.md).