high priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

Availability segmented selector renders three options: 'Available', 'Busy', 'Any' and exactly one is selected at all times (no deselected state)
Specialisation section renders a chip group where zero or more chips can be selected simultaneously
Each section has a visible form label ('Availability' and 'Specialisation') that is programmatically associated with its controls via Semantics grouping
Selected state of each control is announced by screen readers: availability segment reads 'Available, selected, 1 of 3'; chips read '[name], selected/unselected'
Tapping any control dispatches a FilterChanged event to the BLoC within the same frame, with no intermediate local widget state holding the selection
All chip touch targets meet 48dp minimum height (verified with tester.getSize())
All text and icon elements in filter controls pass WCAG 2.2 AA contrast (≥ 4.5:1 normal text) using design tokens exclusively — no hard-coded colour values
The filter panel can be dismissed via a close button or back gesture; on dismiss, focus returns to the filter toggle button (task-011 requirement)
Specialisation chip list handles 1 to 20 chips without layout overflow (wrapping chip layout)
When zero specialisation chips are selected, no filter is applied on specialisation (show all mentors regardless of specialisation)
A 'Clear filters' action resets all selections to defaults (Any availability, no specialisation chips selected) and dispatches the appropriate BLoC event

Technical Requirements

frameworks
Flutter
BLoC
apis
Supabase REST API (fetch available specialisation categories)
data models
FilterState
AvailabilityFilter
SpecialisationCategory
performance requirements
Chip group must render up to 20 chips without jank using Wrap layout
BLoC event dispatch from filter change must complete synchronously (no async gaps)
security requirements
Specialisation categories fetched from Supabase must be sanitised before rendering as chip labels (escape HTML-like characters for label display)
ui components
MapFilterPanel (242-map-filter-panel)
SegmentedAvailabilitySelector (new sub-widget)
SpecialisationChipGroup (new sub-widget)
FilterSectionLabel (new sub-widget or Text with Semantics header role)
AppButton (clear filters action)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Model the availability selector as a stateless widget receiving the current AvailabilityFilter from BLoC and an onChanged callback that dispatches FilterChanged. Do not use a local ValueNotifier or setState — all state lives in FilterBloc. For the segmented selector, use Flutter's SegmentedButton widget (Material 3) or a custom Wrap of ChoiceChips — ensure the Material 3 component's built-in Semantics cover selected state, otherwise add Semantics(selected: isSelected) explicitly. For specialisation chips use FilterChip widgets which have built-in selected semantics.

Wrap each section in a Semantics widget with header: true on the label text so screen readers announce the section heading. Fetch specialisation categories from Supabase once at panel open (or at BLoC initialisation) and cache them in the BLoC state to avoid repeated network calls on panel reopen. Use design token references (e.g., AppColors.primary, AppSpacing.sm) throughout — never raw hex or pixel values.

Testing Requirements

Write widget tests for MapFilterPanel covering: initial state renders correct defaults, tapping availability segment dispatches correct FilterChanged event, tapping specialisation chip toggles selection and dispatches event, tapping clear button resets to defaults and dispatches reset event. Use tester.getSemantics() to assert selected state on availability segments and chips. Use meetsGuideline(iOSTapTargetGuideline) and meetsGuideline(androidTapTargetGuideline). Write a BLoC unit test for each filter event type.

Test wrapping layout with 20 chips to verify no overflow. Test empty specialisation list (edge case where Supabase returns no categories).

Component
Map Filter Panel
ui medium
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.