diff --git a/docs/plans/2026-02-18-wcag-aaa-design.md b/docs/plans/2026-02-18-wcag-aaa-design.md new file mode 100644 index 0000000..72850b6 --- /dev/null +++ b/docs/plans/2026-02-18-wcag-aaa-design.md @@ -0,0 +1,204 @@ +# WCAG 2.2 AAA Compliance Design + +**Date:** 2026-02-18 +**Approach:** Style-System-First (cascade from SettingsStyle.qml, then per-component fixes) +**Goal:** Full WCAG 2.2 AAA compliance while preserving the current visual identity + +--- + +## Section 1: Color System Overhaul (SettingsStyle.qml) + +Update the central design token singleton to meet AAA contrast ratios. + +| Token | Current | New | Rationale | +|---|---|---|---| +| `accentPurple` | `#7000FF` (1.6:1) | `#B794F6` (7.2:1) | AAA normal text on #121212 | +| `textSecondary` | `#999999` (6.3:1) | `#ABABAB` (8.1:1) | AAA normal text | +| `borderSubtle` | `rgba(1,1,1,0.08)` | `rgba(1,1,1,0.22)` | 3:1 non-text contrast | + +New tokens to add: +- `textDisabled: "#808080"` (4.0:1 minimum for disabled states) +- `focusRingWidth: 2` (consistent focus indicator width) +- `minTargetSize: 24` (WCAG 2.5.8 minimum target) + +--- + +## Section 2: Component Default Fixes + +Bulk fixes that cascade from component-level defaults. + +**ModernSlider.qml:** +- Handle: 18x18 -> 24x24 (meets 24px target size) +- Track: 4px -> 6px height +- Add 2px focus ring on `activeFocus` +- Add `Accessible.role: Accessible.Slider` + +**ModernSwitch.qml:** +- Add "I" (on) / "O" (off) pip marks inside the thumb for non-color state indication +- Switch border to `borderSubtle` token +- Add `Accessible.role: Accessible.CheckBox` with state in name + +**ModernTextField.qml:** +- Placeholder: `#606060` -> `#808080` (3.7:1 minimum) +- Focus border: 1px -> 2px +- Add `Accessible.role: Accessible.EditableText` + +**ModernComboBox.qml:** +- Border -> `borderSubtle` token +- Focus border: 1px -> 2px +- Arrow icon: `#888888` -> `#ABABAB` +- Add `Accessible.role: Accessible.ComboBox` + +**ModernKeySequenceRecorder.qml:** +- Border -> `borderSubtle` token +- Focus border: 1px -> 2px +- "None" text: `#808080` -> `#ABABAB` +- Add `activeFocusOnTab: true` + +**GlowButton.qml:** +- Default text: `#9499b0` -> `#ABABAB` +- Border -> `borderSubtle` token + +--- + +## Section 3: Individual Component Attention + +Per-file hardcoded color fixes that can't be solved by token changes alone. + +**Settings.qml:** +- StatBox accent colors: `#bd93f9` -> `#CAA9FF`, `#ff79c6` -> `#FF8FD0`, `#ff5555` -> `#FF8A8A` +- Close button hover: `#ff4b4b` -> `#FF8A8A` +- Model info text: 10px -> 11px, remove `opacity: 0.6` +- INSTALLED tag: verify contrast of green-on-dark + +**Loader.qml:** +- Subtitle: `#80ffffff` -> `#ABABAB` +- Status text: remove `opacity: 0.8`, use full `textSecondary` +- Border: `#40ffffff` -> `rgba(1,1,1,0.22)` + +**Overlay.qml:** +- Border: `#40ffffff` -> `rgba(1,1,1,0.22)` (non-text contrast) + +--- + +## Section 4: Accessibility Properties + +Add `Accessible.name` and `Accessible.role` to every interactive and informational element. + +**Window-level:** +- Overlay.qml: `Accessible.name: "WhisperVoice Overlay"`, `title: "WhisperVoice"` +- Settings.qml: `Accessible.name: "WhisperVoice Settings"`, `title: "WhisperVoice Settings"` +- Loader.qml: `Accessible.name: "WhisperVoice Loading"`, `title: "WhisperVoice"` + +**Overlay.qml elements:** +- Mic button: `Accessible.name: ui.isRecording ? "Stop recording" : "Start recording"`, `Accessible.role: Accessible.Button` +- Status text: `Accessible.name: statusText.text`, `Accessible.role: Accessible.AlertMessage` +- Transcription text: `Accessible.name: transcriptionText.text`, `Accessible.role: Accessible.StaticText` + +**Settings.qml elements:** +- Nav items: `Accessible.name: modelData`, `Accessible.role: Accessible.Tab` +- Close button: `Accessible.name: "Close settings"`, `Accessible.role: Accessible.Button` +- Each settings section: delegate to ModernSettingsSection +- Tab content areas: `Accessible.role: Accessible.PageTab` + +**Shared components:** +- ModernSettingsSection: `Accessible.name: root.title + " settings group"`, `Accessible.role: Accessible.Grouping` +- ModernSettingsItem: `Accessible.name: root.label`, `Accessible.role: Accessible.Row` + +--- + +## Section 5: Keyboard Navigation + +Make every interactive element operable via keyboard alone. + +**Overlay.qml mic button:** +- Add `activeFocusOnTab: true` +- Add `Keys.onReturnPressed` / `Keys.onSpacePressed` -> toggle recording +- Add visible 2px focus ring using `activeFocus` state + +**Settings.qml nav sidebar:** +- Add `activeFocusOnTab: true` to each nav item +- Add `Keys.onReturnPressed` / `Keys.onSpacePressed` -> select tab +- Add `Keys.onUpPressed` / `Keys.onDownPressed` -> move between tabs +- Visible focus ring on each nav item + +**Settings.qml close button:** +- Add `activeFocusOnTab: true` +- Add `Keys.onReturnPressed` / `Keys.onSpacePressed` -> close + +**All custom controls:** +- Ensure `activeFocusOnTab: true` on ModernSlider, ModernSwitch, ModernComboBox, ModernKeySequenceRecorder, GlowButton +- 2px focus ring visible when `activeFocus` is true + +--- + +## Section 6: Reduced Motion Support + +Three-layer system: OS detection, user toggle, animation conditionals. + +**Config layer (`config.py`):** +- Add `"reduce_motion": false` to DEFAULT_SETTINGS + +**OS detection (`main.py`):** +- On Windows startup, call `SystemParametersInfo(SPI_GETCLIENTAREAANIMATION)` via ctypes +- If OS says reduce motion, set config `reduce_motion: true` (user can override) + +**Bridge layer (`bridge.py`):** +- Add `reduceMotion` Q_PROPERTY (bool, notify signal) +- Reads from config, exposed to QML as `ui.reduceMotion` + +**QML conditionals:** + +Overlay.qml animations (6 total): +- Shimmer: `running: !ui.reduceMotion` +- Gradient blobs shader: `visible: !ui.reduceMotion`, static fallback color +- Glow shader: `visible: !ui.reduceMotion` +- CRT shader: `visible: !ui.reduceMotion` +- Particle emitter: `enabled: !ui.reduceMotion` +- Rainbow wave shader: `visible: !ui.reduceMotion` + +Loader.qml animations (2 total): +- Icon pulse: `running: !ui.reduceMotion` +- Progress shimmer: `running: !ui.reduceMotion` + +**UI toggle:** +- Add "Reduce Motion" toggle in Settings.qml Visuals tab +- Bound to config `reduce_motion` via bridge + +--- + +## Section 7: Remaining WCAG AAA Criteria + +**7a. Status Messages (4.1.3):** +- Overlay status label gets `Accessible.role: Accessible.AlertMessage` (covered in Section 4) + +**7b. Text Scaling (1.4.4, 1.4.10):** +- Already handled by existing `ui_scale` config property. No changes needed. + +**7c. Page Titles (2.4.2):** +- Window titles set in Section 4 window-level properties. + +**7d. Error Identification (3.3.1):** +- Not applicable. No user-submitted forms with validation. + +--- + +## Files Changed (Summary) + +| File | Changes | +|---|---| +| `src/ui/qml/SettingsStyle.qml` | Token updates + new tokens | +| `src/ui/qml/Overlay.qml` | Colors, accessibility, keyboard, reduced motion | +| `src/ui/qml/Settings.qml` | Colors, accessibility, keyboard, reduce motion toggle | +| `src/ui/qml/Loader.qml` | Colors, accessibility, reduced motion | +| `src/ui/qml/ModernSlider.qml` | Size, focus ring, accessibility | +| `src/ui/qml/ModernSwitch.qml` | I/O marks, border, accessibility | +| `src/ui/qml/ModernTextField.qml` | Placeholder color, focus, accessibility | +| `src/ui/qml/ModernComboBox.qml` | Border, arrow color, focus, accessibility | +| `src/ui/qml/ModernKeySequenceRecorder.qml` | Border, colors, focus, accessibility | +| `src/ui/qml/GlowButton.qml` | Text color, border, accessibility | +| `src/ui/qml/ModernSettingsSection.qml` | Accessibility properties | +| `src/ui/qml/ModernSettingsItem.qml` | Accessibility properties | +| `src/core/config.py` | Add `reduce_motion` default | +| `src/ui/bridge.py` | Add `reduceMotion` property | +| `main.py` | Windows reduced-motion OS detection |