critical priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

SpeechToTextFieldOverlay accepts required props: onTranscribed(String text), and optional: fieldLabel (String) for Semantics labelling, isEnabled (bool, default true)
Microphone IconButton renders in idle state; tapping it starts recording and transitions icon to listening state with an animated waveform (3-bar or ripple animation at 1.2Hz)
Listening state: a floating preview card appears above the trigger button (using Overlay or Stack positioning) showing partial transcription text updated in real time; card has a max height with scroll if text overflows
Processing state (speech engine processing final result): waveform stops, spinner shown in button, preview card frozen with last partial text
Error state: icon transitions to error indicator (mic with slash), preview card shows localised error message for 2 seconds then auto-dismisses back to idle
Tap anywhere outside the overlay card while listening dismisses the recording (calls stopListening on adapter) and returns to idle without invoking onTranscribed
ESC key press (hardware keyboard) cancels recording — handled via FocusNode + KeyboardListener
On FinalResult received: preview card dismisses, onTranscribed(text) invoked exactly once, widget returns to idle
All state transitions announce themselves via Semantics: SemanticsProperties(liveRegion: true) with a descriptive label e.g. 'Microphone active, listening' / 'Recording stopped' / 'Transcription ready'
Widget is disabled (button non-tappable, opacity 0.4) when isEnabled=false or when SpeechToTextAdapter emits SttError.unavailable on init
Waveform animation is paused when Flutter app is backgrounded (AppLifecycleState.paused) to avoid battery drain
Widget passes flutter_test golden tests for all four visual states: idle, listening, processing, error

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
ISpeechToTextAdapter (task-004)
Flutter Overlay API or Stack+Positioned for floating card
Flutter Semantics API (SemanticsProperties, liveRegion)
AnimationController for waveform
AppLifecycleListener for background detection
data models
RecordingState (from task-004)
TranscriptionEvent (from task-004)
performance requirements
Waveform animation must run at 60fps with no jank; use AnimationController with vsync, avoid rebuilding parent widget on each animation tick
Partial transcription text updates must appear within 300ms of receiving PartialResult event
Overlay card positioning must recompute within one frame when keyboard appears/disappears
security requirements
Partial transcription displayed in the preview card must not be logged or transmitted — it is ephemeral UI state only
The widget must not retain transcription text after onTranscribed is invoked — clear internal state immediately
ui components
SpeechToTextFieldOverlay (stateful widget, public API)
_WaveformAnimationWidget (private, animates 3 bars or ripple)
_TranscriptionPreviewCard (private, floating Overlay entry)
_RecordingStateIcon (private, switches between mic/waveform/spinner/error icons)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use a StatefulWidget with TickerProviderStateMixin for the AnimationController. Subscribe to ISpeechToTextAdapter.stream in initState() and cancel in dispose() — never in build(). For the floating preview card, prefer the Flutter Overlay API (OverlayEntry inserted into Overlay.of(context)) over a Stack approach, as it naturally renders above all other widgets including the keyboard. Position the card using CompositedTransformFollower/Leader pair anchored to the mic button's RenderBox.

For the tap-outside-to-dismiss, wrap the listening state with a ModalBarrier (color: Colors.transparent) that calls _cancelRecording() on tap. The waveform animation: three AnimationControllers offset by 120ms each, driving a scale or height tween on three Container bars — extract into _WaveformAnimationWidget to keep the parent widget lean. AppLifecycleListener: pause/resume the waveform AnimationController in the lifecycle callback. For WCAG 2.2 AA: wrap state-change messages in a Semantics widget with liveRegion: true inside the overlay card — this fires on iOS VoiceOver and Android TalkBack.

Design tokens: use the project's token system for colors, spacing, and radii — no hardcoded values.

Testing Requirements

Widget tests (flutter_test): (1) idle state — mic button rendered, tappable, no overlay visible; (2) tap mic → adapter startListening called, waveform animation running, preview card visible; (3) inject PartialResult event → preview card text updated; (4) inject FinalResult event → onTranscribed called with correct text, overlay dismissed, idle state restored; (5) inject ErrorEvent → error icon shown, error message in preview card, auto-dismiss after 2 seconds (use fake async); (6) tap outside overlay during listening → stopListening called, onTranscribed NOT called; (7) ESC key press → stopListening called; (8) isEnabled=false → button non-tappable (no gesture response); (9) SttError.unavailable on init → button opacity 0.4, no tap response. Golden tests for all 4 visual states (idle, listening, processing, error) on a 390×844 canvas. Accessibility test: SemanticsController assertions for liveRegion announcements on state transitions. Use a FakeSpeechToTextAdapter (StreamController-based) as the Riverpod override in all tests.

Component
Speech-to-Text Field Overlay
ui medium
Epic Risks (3)
high impact medium prob technical

Dynamically rendered form fields built from runtime JSON schema are significantly harder to make accessible than statically declared widgets — Flutter's Semantics tree must be correct for every possible field type and every validation state. Failures here block the entire feature for Blindeforbundet's visually impaired peer mentors.

Mitigation & Contingency

Mitigation: Define WCAG 2.2 AA semantics requirements for each field type before implementation and write widget tests using Flutter's SemanticsController for every type. Include a real-device VoiceOver test session in the acceptance gate for this epic before marking it done.

Contingency: If dynamic semantics prove too difficult to get right generically, implement field-type-specific Semantics wrappers (one per supported field type) instead of a single generic renderer, accepting slightly more code duplication in exchange for reliable accessibility.

high impact medium prob technical

The report-form-orchestrator must manage a complex state machine — schema loading, draft persistence, per-field validation, submission retries, and error recovery — across multiple async operations. Incorrect state transitions could result in lost user data, double submissions, or UI freezes.

Mitigation & Contingency

Mitigation: Define all Bloc states and events explicitly as sealed classes before writing any logic. Use a state machine diagram reviewed by the team before implementation. Write exhaustive Bloc unit tests covering every state transition, including concurrent events and network interruption mid-submission.

Contingency: If Bloc complexity becomes unmanageable, extract draft persistence into a separate DraftManagerCubit and keep report-form-orchestrator focused solely on the submit workflow. The additional granularity makes each component independently testable.

medium impact low prob scope

Organisations may require field types beyond the five currently specified (text, multiline, checkbox group, radio, date). If a new type is discovered during pilot testing, the dynamic-field-renderer must be extended, potentially requiring changes across multiple layers.

Mitigation & Contingency

Mitigation: Design dynamic-field-renderer as a registry of field-type renderers with a clear extension point. Document the pattern for adding a new field type so that it can be done in one file without touching existing renderers.

Contingency: If an unhandled field type is encountered at runtime, dynamic-field-renderer renders a labelled plain-text fallback widget and logs a warning so the missing type is surfaced in monitoring, preventing a crash while making the gap visible.