- 10 cites 11 for Add Attachment and the lane-width menu items - 04 uses ⌘/⇧ symbols for click modifiers; card moves scoped to the ⌥⌘ chord; ../WISHLIST.md path fixed - 03 says stepper (plus the menu pair) in the editing-surfaces table - 06 says one commit per debounce window - 03/10 Reduce Motion lists aligned at five items; rubber-band disambiguated as the lane-resize feedback - 11 requires the trash shown for Empty Trash - 02 rewords and-peers Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
12 KiB
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 cardorder), 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 cardorder). 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 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).
- 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" (registry-cached counts — 02-architecture.md); 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
#RRGGBBAAbackground 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).