high priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

ViewToggleButton renders as a persistent FAB with minimum 48x48dp touch target on all screen sizes
Semantic label reads 'Switch to list view' when map view is active and 'Switch to map view' when list view is active
Tapping the button emits a ViewToggleRequested event to the parent BLoC without clearing FilterState
Active filter values (availability status, specialisation) survive at least 5 consecutive view toggles
Button position is stable and does not overlap map controls or bottom sheet handles at any viewport size
Widget is accessible via keyboard navigation and responds to Enter/Space key events
VoiceOver (iOS) and TalkBack (Android) announce the dynamic semantic label correctly on each toggle
Button visual state (icon and tooltip) updates immediately upon toggle without animation jank above 60fps
Widget is stateless — all view state is derived from the parent BLoC stream, not local StatefulWidget state
Contrast ratio of button icon against FAB background meets WCAG 2.2 AA (minimum 3:1 for non-text, 4.5:1 for text if label is visible)

Technical Requirements

frameworks
Flutter
BLoC
performance requirements
Toggle state update must render within one frame (16ms at 60fps)
No unnecessary BLoC state rebuilds — use BlocSelector to listen only to view mode field
security requirements
No PII stored or emitted by this widget
ui components
FloatingActionButton (Flutter Material)
Semantics widget wrapping FAB with dynamic label
AnimatedSwitcher for icon transition between map/list icons
Design token-based colors from contrast-safe palette

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement as a stateless widget receiving current ViewMode from the parent via BlocSelector. Emit events upward via context.read().add(ViewToggleRequested()). Use AnimatedSwitcher with a CrossFadeState for the icon swap (Icons.map_outlined ↔ Icons.list). The semantic label must be set on the Semantics widget, not just a tooltip, so screen readers pick it up without requiring long-press.

Apply design tokens for FAB background color — do not hardcode hex values. Place the FAB using a Stack within the MapViewScreen scaffold so it floats above both the map canvas and the list view consistently. Avoid Hero animations on this FAB as they can conflict with map provider overlay animations.

Testing Requirements

Unit tests: verify ViewToggleRequested event is emitted on tap; verify semantic label string matches current BLoC view mode. Widget tests: golden test for both map-active and list-active states; verify 48x48dp minimum hit area via tester.getSize(); verify Semantics node label using tester.getSemantics(). Integration tests: toggle 5 times and assert FilterState is unchanged after each toggle. Accessibility test: use flutter_test SemanticsController to verify announcement text.

Target coverage: 100% of ViewToggleButton logic.

Epic Risks (3)
high impact high prob technical

Flutter's map canvas (flutter_map) does not natively support semantic focus traversal for screen readers, meaning map markers may be entirely invisible to VoiceOver/TalkBack users. If the accessible list fallback is not treated as a first-class view, screen reader users will have no access to the feature.

Mitigation & Contingency

Mitigation: From sprint 1, treat mentor-list-fallback as a fully featured primary view, not an afterthought. Implement and test it in parallel with the map canvas. Make the view-toggle-button keyboard focusable and announced on every screen state. Conduct VoiceOver testing on device before submitting each PR touching UI components.

Contingency: If map canvas accessibility cannot be achieved for marker focus traversal, make the view-toggle-button the default focus target on screen load for VoiceOver users (detected via screen-reader-detection-service) so they are immediately directed to the list fallback without needing to discover the toggle.

medium impact medium prob technical

The mentor-info-popup must occupy no more than 40% of visible map area on small screens. On devices with screen heights under 667px (iPhone SE), overlapping with the filter panel or obscuring most of the map could severely degrade usability.

Mitigation & Contingency

Mitigation: Implement the popup as a bottom sheet capped at 40% of screen height with a ScrollView for overflow content. Test on iPhone SE (375x667pt) and the smallest commonly used Android form factor in the device lab. Define max-height as a percentage constant in location-privacy-config or design tokens.

Contingency: If the popup cannot fit all required fields within 40% height on smallest targets, truncate assigned contact count and certification badge to icons-only in the compact view, with a 'View Profile' button always visible at the bottom of the popup regardless of scroll position.

medium impact medium prob integration

Filter state must remain perfectly synchronised between the map view and the list fallback. If the filter panel emits state that is not consumed identically by both views, coordinators switching between views will see inconsistent mentor sets, eroding trust in the feature.

Mitigation & Contingency

Mitigation: Store active filter criteria in a single shared Riverpod provider owned by the map-view-screen and consumed by both map-marker-widget (via mentor-location-service) and mentor-list-fallback. Write integration tests that apply a filter, switch views, and assert identical mentor counts in both views.

Contingency: If filter sync proves brittle, simplify to a single filter state object passed explicitly as a constructor argument to both views on each rebuild, eliminating indirect state sharing.