critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

AccessibleTextStyleSystem exposes a static AppTextStyle provider (InheritedWidget or Riverpod provider) that widgets use instead of Theme.of(context).textTheme
Every bodyLarge, bodyMedium, bodySmall, labelLarge, labelMedium, and labelSmall TextStyle in the provided TextTheme uses FontWeight.w400 or higher
Font sizes are capped at their budgeted maximum values when MediaQuery.textScaleFactor exceeds 2.0 (200% system scale)
DynamicTypeScaleService is consulted at style-resolution time to supply the effective scale factor and per-role size budget
Attempting to retrieve a TextStyle via the provider with FontWeight below w400 results in a debug assert with a message identifying the role and actual weight
AppTextStyle provider is accessible from any widget in the tree without requiring a local BuildContext that has Theme
All 13 Material TextTheme roles are covered (display, headline, title, body, label variants)
Existing screens that consume Theme.of(context).textTheme continue to compile; a migration guide comment is added to the provider describing the replacement pattern
Widget-level golden test confirms text renders at capped size under 200% scale without layout overflow on a 375pt-wide screen
Unit test confirms all 6 body/label roles have FontWeight >= w400 in the resolved theme

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
data models
DesignToken (typography tokens)
DynamicTypeScaleService (scale budgets per role)
performance requirements
Style resolution must complete synchronously within the build phase — no async lookups
TextTheme object must be cached and only recomputed when textScaleFactor changes, not on every build
security requirements
No user data processed; provider must not leak PII through debug messages
ui components
AppTextStyle (InheritedWidget or Riverpod provider)
AccessibleTextStyleSystem (configuration class)
TextTheme (Material — all 13 roles)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement as a thin wrapper around Flutter's ThemeData.textTheme rather than replacing it entirely — this preserves compatibility with third-party widgets. Use a Riverpod `Provider` that reads from a `textScaleFactorProvider` (derived from MediaQuery) and the typography design token map. In the provider's build function, iterate all 13 TextTheme roles and apply two transformations: (1) clamp FontWeight to a minimum of w400, and (2) clamp fontSize to `DynamicTypeScaleService.getBudget(role) / scaleFactor` when scaleFactor > 1.5. Expose convenience accessors `AppTextStyle.bodyLarge(context)` etc.

as static methods for ergonomic call sites. Add a `// MIGRATE: replace Theme.of(context).textTheme.bodyLarge with AppTextStyle.bodyLarge(context)` comment block at the top of the provider file. Avoid overriding the theme globally via ThemeData because it would silently affect third-party packages — the provider approach gives explicit opt-in.

Testing Requirements

Unit tests (flutter_test): verify all 6 body/label roles have FontWeight >= w400; verify font sizes are capped correctly at 1.0x, 1.5x, and 2.0x textScaleFactor for at least 3 roles; verify that a TextStyle with FontWeight.w300 triggers a debug assert when resolved through the provider. Widget tests: pump a minimal app with AccessibleTextStyleSystem at the root, render a Text widget using AppTextStyle.bodyMedium, and verify the rendered style matches the expected capped size at 200% scale. Golden tests: capture a form screen with dense text at 100%, 150%, and 200% scale; confirm no overflow boxes appear in goldens. Integration test: confirm that replacing Theme.of(context).textTheme with AppTextStyle.of(context) on the activity registration form screen does not change visual output at 100% scale.

Component
Accessible Text Style System
ui medium
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.