high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Widget renders a compressed image thumbnail from Uint8List image bytes provided via the Receipt model — no network loading in this task
Thumbnail image is displayed in a fixed aspect ratio container (4:3 or configurable) with object-fit cover cropping
File metadata row below the thumbnail displays filename (truncated with ellipsis at 30 chars), human-readable file size (e.g., '45 KB'), and format badge (e.g., 'JPG')
Placeholder state renders when Receipt model has null imageBytes — shows a neutral grey rectangle with a camera icon and 'No receipt' label using design token colors
Widget accepts a Receipt model (or nullable Receipt) as its sole external input — no BLoC or Riverpod dependency at widget level in this task
Widget is stateless (StatelessWidget) — all state is derived from the input model
Widget uses only design token values for colors, typography, spacing, and border radii — no hardcoded hex values or pixel literals outside of tokens
Widget renders correctly at 320dp and 428dp screen widths without overflow
Widget is exported from the component's barrel file and has a named constructor with clear parameter names
Golden test or widget test confirms the two visual states (with image, placeholder) render without errors

Technical Requirements

frameworks
Flutter
Dart
data models
Receipt
performance requirements
Thumbnail display must not cause jank — image decoding from bytes should be deferred to a FutureBuilder or use Image.memory with the cacheWidth parameter to limit decode resolution to thumbnail size (e.g., 200px width)
Widget build method must not perform byte transformations inline
security requirements
Image bytes must not be written to any cache layer or temp file within this widget — display only
ui components
ReceiptThumbnailPreview (new)
Design token color, typography, and spacing references
Image.memory for byte-based rendering
Placeholder container with Icon widget

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Create the widget in lib/features/receipts/widgets/receipt_thumbnail_preview.dart. Define a Receipt model (or reuse the existing one from the foundation epic) with fields: Uint8List? imageBytes, String? filename, int?

fileSizeBytes, String? format. For size formatting, use a helper that converts bytes to KB/MB with one decimal place. Use ClipRRect with the design token border radius for the image container.

The placeholder icon should use Icons.receipt_long or a custom SVG from the asset bundle if one exists. Keep the widget fully dumb — no callbacks, no state — so it composes cleanly when actions are added in task-002 and task-003.

Testing Requirements

Write flutter_test widget tests covering: (1) widget renders thumbnail when Receipt has valid imageBytes, (2) widget renders placeholder when Receipt is null or imageBytes is null, (3) filename truncation at 30 characters shows ellipsis, (4) metadata row shows correctly formatted size string. Use pumpWidget with a MaterialApp wrapper. No golden tests required unless the team has an established golden test pipeline.

Epic Risks (2)
medium impact medium prob technical

Flutter's accessibility live region support (SemanticsProperties.liveRegion) has known inconsistencies between iOS VoiceOver and Android TalkBack, and between Flutter versions. Threshold-crossing announcements may fail to fire or double-fire, breaking the accessibility contract for Blindeforbundet users.

Mitigation & Contingency

Mitigation: Test live region announcements on physical devices with VoiceOver and TalkBack enabled from the first iteration. Use the AccessibilityLiveRegionAnnouncer component pattern already established in the project. Verify announcement timing relative to Bloc state emissions to avoid double-fires.

Contingency: If Flutter live regions prove unreliable, implement a platform-channel fallback that calls UIAccessibility.post(notification:) on iOS and AccessibilityManager.sendAccessibilityEvent() on Android directly, bypassing Flutter's abstraction.

medium impact low prob integration

The org-configurable threshold must be available at form-render time. If the threshold configuration is fetched asynchronously and not cached, the indicator may briefly show the wrong state (e.g., 'optional' before the threshold loads), confusing users and potentially allowing invalid submissions.

Mitigation & Contingency

Mitigation: Ensure the receipt threshold validator loads and caches the org configuration at app startup or organization selection time, not lazily on form open. Use a loading state in the indicator widget rather than defaulting to 'optional' while configuration is pending.

Contingency: If startup caching is not feasible, treat an unknown threshold as 'receipt required' (fail safe) and surface a clear loading indicator until the configuration resolves, preventing invalid submissions while the config loads.