critical priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

SearchResultsList renders a ListView with Semantics(explicitChildNodes: true) wrapping the list
Each list item renders either a ContactCard or PeerMentorCard based on the result type
An offline indicator banner appears at the top of the list when connectivity is lost; it disappears automatically when connectivity is restored
The offline banner has a Semantics label: 'You are offline. Showing cached results.'
When query returns zero results, a text-only empty state is shown (no image-only fallback); text reads 'No results found for "<query>"'
Empty state widget announces its text via Semantics so screen readers read it without user navigation
While a query is in-flight, skeleton placeholder cards are shown (minimum 3 skeletons) instead of a spinner-only indicator
Skeleton cards have Semantics(label: 'Loading results', excludeSemantics: true) to prevent screen reader noise
ListView uses Semantics sortKey or explicit semanticIndexOffset so reading order matches visual order
Widget accepts a typed SearchResultsState (loading / loaded / empty / error) and renders the correct UI for each state

Technical Requirements

frameworks
Flutter
flutter_test
Riverpod or BLoC
apis
Flutter Semantics API
Flutter Connectivity (offline detection)
data models
Contact
PeerMentor
performance requirements
ListView must use itemExtent or itemBuilder (not Column) for large result sets to enable virtualization
Skeleton cards must render within one frame (no async dependency during loading state)
security requirements
Contact data displayed in cards must be sourced only from the validated state object; no direct Supabase queries in this widget
ui components
ContactCard
PeerMentorCard
OfflineBanner
EmptyStateWidget
SkeletonCard

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Define a sealed class SearchResultsState with four variants (loading, loaded, empty, error) and pass it as the single input to SearchResultsList — avoid multiple nullable parameters. Reuse the existing ContactCard and PeerMentorCard widgets verbatim; do not inline their layout here. For offline detection, subscribe to a connectivity stream from a shared Riverpod provider rather than instantiating a new listener inside this widget. Use the design token spacing and color values for the offline banner background (use the warning/caution token, not a hardcoded color).

Skeleton cards should mirror the exact height of a real card item so the layout does not jump when results load.

Testing Requirements

Widget tests using flutter_test: render in each state (loading, loaded with mixed results, empty, offline+results) and assert correct child widgets are present. Use SemanticsController to verify Semantics labels on skeleton cards and empty state. Test that offline banner appears/disappears when connectivity mock toggles. Snapshot or golden test for skeleton layout to prevent regression.

Integration test: end-to-end query → result display with real Supabase test data in a staging environment.

Component
Search Results List
ui low
Epic Risks (2)
medium impact medium prob technical

Flutter's Semantics live region support for announcing dynamic result count changes may behave inconsistently between VoiceOver (iOS) and TalkBack (Android), particularly regarding announcement throttling and focus management, causing the feature to pass testing on one platform and fail on the other.

Mitigation & Contingency

Mitigation: Test live region announcements on both iOS (VoiceOver) and Android (TalkBack) early in development using the existing accessibility test harness. Reference the existing LiveRegionAnnouncer component (608-live-region-announcer) patterns used elsewhere in the app.

Contingency: If cross-platform consistency cannot be achieved, implement a platform-specific announcement strategy using the SemanticsService.announce API with platform-conditional announcement timing to work around OS-specific throttling behaviour.

low impact low prob dependency

Voice-to-text progressive enhancement for Blindeforbundet may not be available or may behave unpredictably on all device/OS combinations, particularly older Android devices, potentially causing crashes or silent failures that degrade the search experience.

Mitigation & Contingency

Mitigation: Implement voice-to-text as a strictly optional enhancement: detect availability at runtime, show the microphone button only when the platform speech API reports availability, and wrap all voice invocations in try/catch with graceful degradation to standard text input.

Contingency: If voice-to-text causes instability on a subset of devices discovered during TestFlight/beta, disable the feature flag for that platform version while a fix is investigated, without impacting the core text-based search functionality.