high priority low complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

Disabled cards render at 38% opacity (Material Design disabled opacity standard) relative to their enabled state
Disabled cards do not respond to tap or long-press gestures
An explanatory reason string is passed alongside each disabled type ID (e.g., 'Cannot combine with km allowance')
The reason is displayed as either: (a) an inline subtitle below the card label, or (b) a tooltip revealed on long-press, matching the design spec
The reason text meets WCAG 2.2 AA contrast ratio (4.5:1 minimum) even at reduced opacity
Disabled state is announced by screen readers via the Semantics widget (enabled: false, hint includes the reason)
Transitioning from enabled to disabled animates smoothly (opacity transition, ~150ms) and vice versa
Golden tests confirm disabled card appearance with and without a reason string provided

Technical Requirements

frameworks
Flutter
flutter_test
data models
ExpenseTypeDisabledReason (new: { String typeId, String reason })
performance requirements
Opacity animation must run on the GPU compositor thread — use FadeTransition or AnimatedOpacity, not manual setState
security requirements
Disabled reason strings must be localized — no hardcoded English text in widget code
ui components
ExpenseTypeOptionCard (extend from task-001/002)
Tooltip (Flutter built-in) or inline subtitle Text widget
AnimatedOpacity

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Extend ExpenseTypeOptionCard to accept an optional String? disabledReason parameter. When isDisabled is true and disabledReason is non-null, render the reason either as a small Text widget below the label or wrap the card in a Tooltip widget (prefer inline label for visibility since tooltips require a long-press on mobile which is a discoverability risk). Use AnimatedOpacity with a 150ms duration to transition between enabled and disabled states.

Do not use IgnorePointer alone to block taps — also set the Semantics widget's enabled: false to prevent screen readers from focusing disabled cards. The disabledReason strings are produced by the mutual exclusion rule engine in the BLoC layer and passed through the ExpenseSelectionState; this widget only renders what it receives. For the HLF use case, the typical reason will be something like 'Not compatible with selected kilometer allowance'.

Testing Requirements

Widget tests: (1) verify disabled card ignores tap callbacks, (2) verify opacity wrapper applies 0.38 opacity value, (3) verify reason string appears in the widget tree when provided, (4) verify Semantics hint contains the reason string, (5) golden test disabled card with reason vs disabled card without reason. Also test the animation: use tester.pump(Duration(milliseconds: 200)) and verify the opacity value mid-transition. Manual test: confirm the reason tooltip/label is legible under system-level display accessibility settings (large text, high contrast mode).

Component
Expense Type Picker Widget
ui medium
Epic Risks (2)
medium impact medium prob technical

If the expense calculation preview subscribes to the full BLoC state stream, every unrelated state property change (e.g. a loading flag toggle) triggers a widget rebuild. With complex card animations for the disabled-state transition, this could cause frame drops on low-end Android devices used by some peer mentors.

Mitigation & Contingency

Mitigation: Use select() on the Riverpod provider to subscribe only to the specific state slice each widget needs; write a performance test asserting rebuild count on a rapid sequence of toggle events.

Contingency: If jank is detected in device testing, replace animated disabled-state transitions with instant opacity changes and defer animation polish to a follow-up sprint.

medium impact low prob integration

The disabled card state requires a specific contrast-safe colour combination that communicates unavailability without relying solely on colour (WCAG 1.4.1). If the current design token palette does not include a disabled-state token with sufficient contrast for text on the disabled background, the widget will either fail WCAG AA or require a last-minute design token addition that could break other components.

Mitigation & Contingency

Mitigation: Audit the existing design token manifest for disabled-state tokens at the start of the epic; if missing, raise with the design lead and add a contrast-validated token before widget implementation begins.

Contingency: If no design review is available, use the established --color-text-disabled and --color-surface-disabled tokens with an added strikethrough or lock icon to satisfy WCAG 1.4.1 non-colour requirement, and document the deviation for design review.