critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

OrgCard is a StatelessWidget accepting an Organization and an onTap VoidCallback
When logoUrl is non-null and non-empty, a network image is rendered (CachedNetworkImage or Image.network with loadingBuilder)
When logoUrl is null or empty, a CircleAvatar or Container renders the organization's initials derived from displayName (first letter of first two words)
Display name is rendered using the project's typography design token for body/label text
Card uses design token values for padding (minimum 12dp internal), border radius, and background color
The entire tappable area is at least 48×48dp (enforced via ConstrainedBox or SizedBox minimum constraints)
onTap is wired to GestureDetector or InkWell — tapping the card fires the callback
Widget renders correctly in both light and dark themes if the design system supports theming
Widget has no hardcoded colors, font sizes, or spacing values — all come from design tokens
Widget compiles and renders without errors in flutter_test widget test

Technical Requirements

frameworks
Flutter (StatelessWidget)
cached_network_image (if in pubspec) or Image.network
Design token system (colors, typography, spacing, radii)
data models
Organization (id, displayName, logoUrl, isSelected)
performance requirements
Network image must use caching to avoid redundant fetches on list scroll
Widget tree depth should be minimal — avoid unnecessary nesting beyond layout requirements
security requirements
Network image URL must be loaded via HTTPS only
Do not log or expose logoUrl in error messages
ui components
OrgCard
CircleAvatar (initials fallback)
CachedNetworkImage or Image.network
InkWell or GestureDetector

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use the existing design token constants already present in the project (colors, AppSpacing, AppTypography, AppRadius based on CLAUDE.md). For initials derivation, split displayName on whitespace, take the first character of the first two elements, uppercase them, and join — handle single-word names by using the first two characters. For the network image error case, show the same initials avatar rather than an error icon to maintain visual consistency. Wrap the entire card in Material + InkWell rather than GestureDetector to get the ripple ink effect that matches Flutter's Material conventions and provides free press feedback for accessibility.

Avoid AnimatedContainer at this stage — visual state transitions are added in task-003.

Testing Requirements

Write widget tests using flutter_test: (1) pumpWidget OrgCard with a non-null logoUrl and assert an image widget is present in the tree, (2) pumpWidget OrgCard with logoUrl null and assert a Text widget containing the initials is present, (3) tap the card via tester.tap() and verify the onTap callback is called exactly once, (4) assert the widget's render size is at least 48×48 logical pixels using tester.getSize(). Use a MediaQuery wrapper in tests to provide standard pixel density.

Component
Organization Card Widget
ui low
Epic Risks (3)
high impact medium prob technical

Flutter's Semantics system requires explicit configuration for custom card widgets — the default widget tree semantics may merge child nodes incorrectly, causing VoiceOver to announce partial or garbled text for each OrgCard. This is a critical failure for Blindeforbundet and violates the contractual WCAG 2.2 AA requirement.

Mitigation & Contingency

Mitigation: Wrap each OrgCard in a Semantics widget with an explicit label combining organization name and role label, set button role, and excludeSemantics: true on all child widgets to prevent double-announcement. Test on a physical iOS device with VoiceOver enabled before marking the epic complete.

Contingency: If automated semantics are insufficient, implement a dedicated AccessibilityLabel builder in the OrgCard that constructs the announcement string from OrgLabelsProvider output and passes it directly to the Semantics label field.

medium impact medium prob technical

Organization logos must load before the screen is considered ready. If assets are fetched from a remote URL and network latency is high (a realistic scenario for rural NHF users), the screen may render with broken image placeholders, degrading the first-impression quality and potentially confusing screen reader users whose VoiceOver announcements would reference an empty image.

Mitigation & Contingency

Mitigation: Cache organization logos locally after first load using the org-branding-cache pattern. For the initial load, render a styled placeholder with the organization's initials and primary color while the logo loads asynchronously. Ensure the Semantics label is derived from the organization name, not the image asset, so screen reader announcements are never dependent on image load state.

Contingency: If caching implementation is out of scope for this epic, ship with initials-based placeholders as permanent fallbacks and defer logo loading to the org-branding-cache epic. The screen remains fully functional and accessible without logos.

medium impact low prob scope

The user story specifies that single-organization users bypass the selection screen and are routed directly into the app. If the bypass logic is implemented in the UI layer rather than in OrgSelectionService, it creates a tight coupling that breaks if the routing logic is ever reused or tested in isolation. It also creates a potential timing issue where the screen briefly renders before the bypass fires.

Mitigation & Contingency

Mitigation: Implement the single-org bypass entirely in OrgSelectionService.getPersistedSelection() or as a route guard — the screen should never be pushed onto the navigation stack for single-org users. The OrgSelectionScreen widget itself has no knowledge of this bypass.

Contingency: If the route guard approach is deferred to the routing epic, add a local guard in the OrgSelectionScreen's initState that calls OrgSelectionService, auto-selects if count is 1, and navigates forward before the first frame is painted, using a loading indicator to prevent the flash.