critical priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Widget renders as a floating overlay positioned above (or adjacent to) the active report field without obscuring surrounding UI elements
Recording state: animated waveform with bars oscillating at a natural cadence, colored using the primary brand color design token
Processing state: pulsing circle or spinner animation colored with the amber design token
Error state: static or gently pulsing error indicator colored with the destructive design token
When MediaQuery.of(context).disableAnimations is true, all animations are replaced with static representations of the same icons/indicators
Overlay appears and disappears with a smooth fade transition (200ms) that also respects reduce-motion (instant show/hide when motion is disabled)
Widget is invisible and removed from the accessibility tree when TranscriptionState is idle or complete
All color values are sourced exclusively from design tokens — no hardcoded hex values
Widget accepts a required `fieldId` parameter and independently watches the corresponding Riverpod family provider
Overlay does not intercept touch events from underlying widgets (IgnorePointer wrapper applied)
Widget renders correctly on both iOS and Android without platform-specific code

Technical Requirements

frameworks
Flutter
Riverpod
apis
TranscriptionStateManager (via Riverpod provider family)
MediaQuery (for disableAnimations)
data models
TranscriptionState
TranscriptionStatus
performance requirements
Waveform animation must maintain 60fps on mid-range devices — use AnimationController with Tween, not rebuilding the widget tree on each frame
Animation controllers must be disposed when the widget is removed from tree to prevent memory leaks
Overlay must not trigger layout passes in the parent widget tree on each animation frame
security requirements
Overlay must not display any transcript content — it is a state indicator only
ui components
Stack + Positioned for overlay placement
CustomPainter for waveform bars animation
AnimationController + CurvedAnimation for oscillation
AnimatedOpacity for fade transitions
IgnorePointer wrapper
ExcludeSemantics for idle/complete states

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

For the waveform animation, use a CustomPainter driven by an AnimationController value rather than rebuilding the widget tree — this keeps animation work on the raster thread. Expose the animation as a `Listenable` and use `AnimatedBuilder` to repaint only the CustomPainter canvas. Pattern: `AnimationController _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true)`. Check `MediaQuery.of(context).disableAnimations` in `initState()` (and in a `didChangeDependencies()` override for dynamic changes) and skip `_controller.repeat()` if true.

For overlay positioning, the widget likely needs to be used with an `OverlayEntry` or a `Stack` in the field's parent — coordinate with task-001 to decide the compositional approach. Color tokens: use `AppColors.primary` for recording, `AppColors.warning` (amber) for processing, `AppColors.destructive` for error — verify these token names exist in the design token system before using them.

Testing Requirements

Widget tests: (1) Pump with recording state — verify waveform widget is present and AnimationController is ticking (check isAnimating). (2) Pump with processing state — verify spinner/pulse widget is present and waveform is absent. (3) Pump with error state — verify error indicator uses destructive token color. (4) Pump with idle state — verify widget is not visible and semantics tree contains no indicator node.

(5) Pump with MediaQuery.disableAnimations = true in recording state — verify AnimationController is NOT started (isAnimating == false) and a static icon is shown instead. (6) Pump with disableAnimations = true and verify opacity transition is instant (no AnimatedOpacity with duration > 0). (7) Golden tests for each state (recording, processing, error) at standard and large text sizes. (8) Verify IgnorePointer by simulating tap on overlay area and confirming underlying widget receives the tap.

Component
Recording State Indicator
ui low
Epic Risks (3)
medium impact medium prob technical

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.

high impact medium prob technical

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.

medium impact low prob integration

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.