Implement DictationMicrophoneButton base widget
epic-speech-to-text-input-user-interface-task-001 — Create the DictationMicrophoneButton Flutter widget that renders inline within designated free-text report fields. The button must observe TranscriptionStateManager to show/hide based on dictation availability, display correct icon states (idle, active, processing), and dispatch start/stop intents. Apply accessible touch target sizing and semantic labels.
Acceptance Criteria
Technical Requirements
Implementation Notes
Use `Consumer` or `ConsumerWidget` with `ref.watch(transcriptionProvider(fieldId).select((s) => s.status))` to minimize rebuilds. Wrap the IconButton in a `SizedBox(width: 44, height: 44)` to guarantee minimum touch target even if the icon itself is smaller. Use `AnimatedSwitcher` with a fade transition for smooth icon state changes — this also helps users with cognitive disabilities by providing a clear visual change cue. Semantic labels should be pulled from the app's localization system (even if that means adding 3 new ARB keys) — do not hardcode English strings.
For the design token usage, import from `AppColors`, `AppSpacing`, and `AppSizing` constants — match the pattern used by existing `AppButton` and `AppTextField` widgets in the shared component library.
Testing Requirements
Widget tests (flutter_test): (1) Pump widget with provider override in idle state — verify mic_none icon is rendered and Semantics label is 'Start dictation'. (2) Override provider with recording state — verify filled mic icon and 'Stop dictation' label. (3) Override provider with processing state — verify CircularProgressIndicator is visible and button is non-interactive. (4) Tap in idle state — verify startDictation() was called on mock state manager.
(5) Tap in recording state — verify stopDictation() was called. (6) Verify touch target is at least 44×44 using tester.getSize(). (7) Golden test: render all three icon states side-by-side and compare to approved screenshot.
Merging dictated text at the current cursor position in a TextField that already contains user-typed content is non-trivial in Flutter — TextEditingController cursor offsets can behave unexpectedly with IME composition, emoji, or RTL characters, potentially corrupting the user's existing notes.
Mitigation & Contingency
Mitigation: Implement the merge logic using TextEditingController.value replacement with explicit selection range calculation rather than direct text manipulation. Write targeted widget tests covering edge cases: cursor at start, cursor at end, cursor mid-word, existing content with emoji, and content that was modified during an active partial-results stream.
Contingency: If cursor-position merging proves too fragile for the initial release, scope the merge behaviour to always append dictated text at the end of the existing field content and add the cursor-position insertion as a follow-on task after the feature is in TestFlight with real user feedback.
VoiceOver on iOS and TalkBack on Android handle rapid sequential live region announcements differently. If recording start, partial-result, and recording-stop announcements arrive within a short window, they may queue, overlap, or be dropped, leaving screen reader users without critical state information.
Mitigation & Contingency
Mitigation: Implement announcement queuing in AccessibilityLiveRegionAnnouncer with a minimum inter-announcement delay and priority ordering (assertive recording start/stop always takes precedence over polite partial-result updates). Test announcement behaviour on physical iOS and Android devices with VoiceOver/TalkBack enabled as part of the acceptance test plan.
Contingency: If platform differences make reliable queuing impossible, reduce partial-result announcements to a single 'transcription updating' message with debouncing, preserving the critical start/stop announcements. Coordinate with the screen-reader-support feature team to leverage the existing SemanticsServiceFacade patterns already established in the codebase.
The DictationMicrophoneButton must integrate with the dynamic-field-renderer which generates form fields from org-specific schemas at runtime. If the renderer does not expose a stable field metadata API for dictation eligibility checks, the scope guard and button visibility logic will require invasive changes to the report form architecture.
Mitigation & Contingency
Mitigation: Coordinate with the post-session report feature team early in the epic to confirm that dynamic-field-renderer exposes a field metadata interface including field type and sensitivity flags. Add a dictation_eligible flag to the field schema that the renderer passes to DictationMicrophoneButton as a constructor parameter.
Contingency: If the renderer cannot be modified without breaking changes, implement dictation eligibility as a separate lookup against org-field-config-loader using the field key as the lookup identifier, bypassing the renderer integration and keeping the dictation components fully decoupled from the report form architecture.