medium priority low complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

StatsSummaryCards skeleton renders exactly 4 grey placeholder card widgets that match the real cards' dimensions and layout
Chart skeleton renders a grey rectangle with label placeholder rows that visually mirror the real chart's bounding box
PeerMentorStatsList skeleton renders exactly 6 row placeholders matching the real list item height and padding
All skeleton containers are wrapped in a Semantics widget with label: 'Loading' and liveRegion: true so VoiceOver/TalkBack announces the loading state on first render
Skeleton shimmer animation runs at 1.0–1.5 second cycle and respects the device's 'Reduce Motion' accessibility setting (no animation when enabled)
Skeletons are only shown when the BLoC state is CoordinatorStatsLoadingState; they are replaced immediately on data or error state transitions
No skeleton element is focusable by the screen reader beyond the single 'Loading' announcement — ExcludeSemantics wraps the individual placeholder shapes
Skeleton widget dimensions are defined using design token spacing/sizing values, not hardcoded pixel values
Snapshot tests confirm the skeleton layout for all three widget types in both light and dark themes

Technical Requirements

frameworks
Flutter
BLoC
performance requirements
Skeleton shimmer must run at 60fps on mid-range Android devices — use a single AnimationController shared across all skeleton widgets on the same screen to avoid multiple Ticker overhead
Skeleton widgets must build in under 2ms per frame — avoid expensive layout in skeleton trees
ui components
StatsSummaryCardsSkeleton — 4-column shimmer card row
ChartSkeleton — shimmer rectangle with 3 label line placeholders
PeerMentorStatsListSkeleton — 6-row shimmer list
ShimmerContainer — reusable shimmer base widget with AnimationController injection
Semantics wrapper with label 'Loading' at skeleton root
ExcludeSemantics wrapper around individual shimmer shape children

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Create a shared ShimmerContainer widget that accepts width, height, borderRadius, and an optional AnimationController. Instantiate one AnimationController at the dashboard screen level and pass it down via InheritedWidget or a simple constructor parameter to avoid multiple Tickers. For 'Reduce Motion' support, check MediaQuery.of(context).disableAnimations — when true, render static grey containers instead of animated shimmer. Use design token color values (e.g., colorSurface, colorOnSurface with low opacity) for skeleton fill colors, never hardcoded hex values.

Ensure all three skeleton widgets are co-located alongside their real widget counterparts (same file or nearby sibling file) to make side-by-side maintenance easy.

Testing Requirements

Write flutter_test widget tests for each skeleton widget: (1) verify correct number of placeholder children are rendered, (2) verify the root Semantics node has label 'Loading', (3) verify individual shimmer shapes are excluded from semantics tree via ExcludeSemantics, (4) use flutter_test's AccessibilityGuideline to confirm no spurious focusable nodes, (5) golden/snapshot tests for all three skeletons in loading state against approved baseline images. Tests should use a mock AnimationController to avoid flakiness from real animation timing.

Epic Risks (4)
medium impact medium prob technical

PeerMentorStatsList must handle rosters of 40+ mentors efficiently. A naive ListView implementation that re-renders all rows on filter change may cause frame drops on mid-range devices, degrading the experience for coordinators managing large chapters.

Mitigation & Contingency

Mitigation: Use ListView.builder with const constructors for row widgets. Profile the list with Flutter DevTools on a release build against a 60-mentor dataset before submitting for review. Implement sort in the BLoC layer, not in the widget.

Contingency: If frame drops persist, introduce pagination (load 20 mentors, scroll to load more) and add a search filter to reduce visible row count in practice.

medium impact low prob integration

The ActivityTypeDonutChart must render org-configured activity type labels from the org-labels system. If the terminology provider is not available or returns stale labels, chart segments will display raw key strings instead of human-readable organisation-specific names, confusing coordinators.

Mitigation & Contingency

Mitigation: Always resolve labels through the OrganizationLabelsProvider before passing data to the donut chart widget. Implement a fallback that formats the raw key as a readable string (e.g., 'peer_support' → 'Peer Support') if the provider returns null.

Contingency: If the org-labels system is unavailable, display the formatted fallback label and log a warning. Do not block chart rendering on label resolution — render with fallbacks immediately.

high impact high prob technical

fl_chart widgets do not natively expose semantic labels for individual bars and donut segments. Without explicit Semantics wrappers, VoiceOver and TalkBack users will receive no meaningful chart information, failing accessibility requirements critical for Blindeforbundet and NHF deployments.

Mitigation & Contingency

Mitigation: Wrap each fl_chart widget in a Semantics widget with a descriptive label summarising the chart data. Implement the data table fallback toggle from the start, not as an afterthought. Validate with VoiceOver on iOS and TalkBack on Android during development.

Contingency: If fl_chart's rendering pipeline prevents semantic overlay from working reliably, replace chart widgets with a custom Canvas-based implementation that has full semantic control, or use a different charting library with better accessibility support.

medium impact medium prob technical

The period filter state must be preserved when navigating away from the dashboard (e.g., drilling into a mentor's detail screen and pressing back). If the BLoC is re-created on navigation, the filter resets to default, forcing coordinators to re-select their period every time they drill down, degrading operational workflow efficiency.

Mitigation & Contingency

Mitigation: Provide the StatsBloc at a route level above the dashboard screen using a BlocProvider scoped to the statistics navigation shell, not inside the screen widget itself. Verify persistence with a widget integration test that navigates away and back.

Contingency: If route-level scoping is not achievable within the current navigation architecture, persist the last-used filter to a local session store and restore it on screen re-entry.