critical priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

ContrastSafePair is an immutable Dart record with fields: foreground (Color), background (Color), contrastRatio (double), roleName (String)
ContrastSafeColorPalette class exposes a static `palette` getter returning a List<ContrastSafePair> derived from the project's ColorScheme
All pairs in the palette have a pre-validated contrastRatio >= 4.5 (WCAG AA normal text)
Pairs with ratio < 4.5 are excluded at construction time — they must not appear in the palette or be accessible via any public getter
A static `lookup(String roleName)` method returns the ContrastSafePair for a named semantic role (e.g., 'onPrimary', 'onSurface') or throws an AssertionError in debug if the role is unknown
ContrastSafeColorPalette widget renders all palette pairs as a scrollable swatch grid showing the foreground color label on the background color, along with the numeric ratio
Swatch grid is for development/design-review use and is accessible via a debug-only route or settings screen toggle
All 10 semantic ColorScheme roles (primary, onPrimary, secondary, onSecondary, surface, onSurface, error, onError, background, onBackground) are evaluated during palette construction
Unit test confirms palette contains zero pairs with ratio < 4.5
Widget test confirms swatch grid renders correct number of swatches equal to the palette size

Technical Requirements

frameworks
Flutter
flutter_test
apis
ContrastRatioValidator (from task-004)
data models
ContrastSafePair (new Dart record)
ColorScheme (Material)
performance requirements
Palette construction (validation of all color pairs) must complete in under 10ms on first access
Palette is computed once and cached — repeated calls to `palette` must not revalidate
security requirements
No user data involved; swatch grid widget must be conditionally compiled out or hidden in production builds to avoid exposing design internals
ui components
ContrastSafeColorPalette (display widget)
ColorSwatchTile (private sub-widget per pair)
GridView / Wrap (swatch layout)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define ContrastSafePair as a Dart record: `record ContrastSafePair(Color foreground, Color background, double contrastRatio, String roleName)`. Build the palette by iterating all semantic ColorScheme role pairs (foreground → background), calling `ContrastRatioValidator.compute(foreground, background)` for each, and collecting only those pairs where ratio >= 4.5. Use a `late final` static field with a factory initializer to ensure single-construction caching. The swatch grid widget should be wrapped in `kDebugMode ?

ContrastSafeColorPalette() : const SizedBox.shrink()` at any usage site, or gated behind a feature flag. For the 4.5:1 exact boundary: use `>= 4.5` (inclusive) to match WCAG AA definition. Ensure the contrast ratio computation uses relative luminance per WCAG 2.2 formula, not perceived brightness shortcuts.

Testing Requirements

Unit tests (flutter_test): verify that ContrastSafeColorPalette.palette contains zero entries with contrastRatio < 4.5; verify that known passing pairs (e.g., black on white = 21:1) appear in the palette; verify that known failing pairs (e.g., #767676 on white = exactly 4.5:1 boundary — should be included) are handled correctly at the exact boundary; verify that pairs below 4.5:1 are excluded. Test the `lookup` method for existing and non-existing role names. Widget tests: pump ContrastSafeColorPalette widget and verify it renders exactly `palette.length` swatch tiles; verify each tile displays both the foreground color and the numeric ratio. Performance test: call palette construction 100 times and confirm total time under 50ms (caching verification).

Epic Risks (4)
medium impact high prob integration

Flutter's textScaleFactor behaviour differs between iOS and Android, and third-party widgets used across the app (date pickers, bottom sheets, chips) may not respect the per-role scale caps applied by the dynamic-type-scale-service, causing overflow in screens this epic cannot directly control.

Mitigation & Contingency

Mitigation: Enumerate all third-party widget usages that render text. For each, verify whether they honour the inherited DefaultTextStyle and MediaQuery.textScaleFactor or use hardcoded sizes. File issues with upstream packages and wrap non-compliant widgets in MediaQuery overrides scoped to the safe cap for that role.

Contingency: If upstream packages cannot be patched within the sprint, implement a global MediaQuery wrapper at the app root that clamps textScaleFactor to the highest per-role safe value (typically 1.6–2.0), accepting that users at extreme OS scales see a safe cap rather than full scaling for those widgets.

high impact medium prob dependency

The CI accessibility lint runner depends on the Dart CLI toolchain and potentially custom_lint or a bespoke Dart script. CI environments differ from local dev environments in Dart SDK version, pub cache configuration, and platform availability, risking intermittent CI failures that block all pull requests.

Mitigation & Contingency

Mitigation: Pin the Dart SDK version in the CI workflow configuration. Package the lint runner as a self-contained Dart script with all dependencies vendored or declared in a dedicated pubspec.yaml. Add a CI smoke test that runs the runner against a known-compliant fixture and a known-violating fixture to verify the exit codes are correct.

Contingency: If the custom runner proves too fragile, fall back to running dart analyze with the flutter-accessibility-lint-config rules as the sole CI gate, and schedule the custom manifest validation as a separate non-blocking advisory check until the runner is stabilised.

medium impact medium prob technical

Wrapping all interactive widgets with a 44 pt minimum hit area via HitTestBehavior.opaque may cause unintended tap interception in widgets where interactive elements are closely stacked, particularly in the expense type selector, bulk confirmation screen, and notification filter bar.

Mitigation & Contingency

Mitigation: Conduct integration testing of the touch target wrapper specifically in dense layout scenarios (expense selector, filter bars, bottom sheets with multiple buttons). Use the Flutter Inspector to visualise hit areas and confirm no overlaps. Pair with the interactive-control-spacing-system to ensure minimum 8 dp gaps between expanded hit areas.

Contingency: If overlapping hit areas cause mis-tap regressions in specific screens, allow the touch target wrapper to accept an explicit hitAreaSize parameter that can be reduced below 44 pt only in contexts where the interactive-control-spacing-system guarantees sufficient gap, with a mandatory code review flag for any such override.

high impact medium prob scope

The contrast-safe-color-palette must guarantee WCAG AA ratios for both light and dark mode token sets. Dark mode color derivation is non-trivial — simply inverting a light palette often produces pairs that pass in one mode but fail in the other, and the token manifest must encode both sets explicitly.

Mitigation & Contingency

Mitigation: Define both light and dark token sets explicitly in the accessibility-token-manifest rather than deriving one from the other programmatically. Run the contrast-ratio-validator against both sets as part of the token manifest generation process and include both in the CI lint runner's validation scope.

Contingency: If time pressure forces a dark mode deferral, ship with light mode only and add a prominent in-app notice. Gate dark mode colour tokens behind a feature flag until the full dual-palette validation is complete.