Implement Dokio screens from the design doc (turns 3 + 4)
Scaffold an Expo + TypeScript app for Dokio, the app that scans Behördenpost and explains it in plain language, and build the four screens specified in "Dokio Design.dc.html": 4a Onboarding, full-bleed dither gradient 4b Home, dithered scan card 3a Kamera, mocked viewfinder 3b Archiv, search + filters + month groups Design tokens from the turn 4 blue palette live in src/theme.ts. Content is mocked in src/data/letters.ts, taken verbatim from the doc. DitherBackground is a GLSL port of the doc's dither-bg.js, which drew a low-resolution canvas and upscaled it with image-rendering: pixelated; the fragment shader snaps to the same grid instead. Two notes on the port: - The original's fract(sin(x) * 43758.5453) hash only distributes well in float64. A fragment shader's highp float has ~24 mantissa bits, so it collapsed to a near-constant field and the gradient bands rendered straight rather than warped. Replaced with Hoskins' hash, verified against the original running side by side. - The 4x4 Bayer matrix is built recursively rather than as a lookup, which avoids dynamic array indexing; the construction reproduces all 16 entries of the original exactly. The camera screen deliberately stops short of expo-camera: the viewfinder is a static stand-in, so the flow is navigable without permissions. The 2a result screen is not part of turns 3 and 4 and is not included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
82
src/components/LetterRow.tsx
Normal file
82
src/components/LetterRow.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { colors, font, radius, shadow } from '../theme';
|
||||
import type { Letter } from '../data/letters';
|
||||
import { CheckIcon, EnvelopeIcon } from './Icons';
|
||||
|
||||
type Props = {
|
||||
letter: Letter;
|
||||
/** The archive shows "Sender · Kind" and a date prefix; Home shows neither. */
|
||||
variant?: 'home' | 'archive';
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
export function LetterRow({ letter, variant = 'home', onPress }: Props) {
|
||||
const needsAction = letter.deadlineDays !== undefined;
|
||||
|
||||
const title =
|
||||
variant === 'archive' && letter.kind ? `${letter.sender} · ${letter.kind}` : letter.sender;
|
||||
const subtitle =
|
||||
variant === 'archive' ? `${letter.date} · ${letter.summary}` : letter.summary;
|
||||
|
||||
const badge =
|
||||
variant === 'archive'
|
||||
? needsAction
|
||||
? `${letter.deadlineDays} Tage`
|
||||
: null
|
||||
: needsAction
|
||||
? 'Frist'
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.row, pressed && styles.pressed]}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${title}. ${subtitle}`}
|
||||
>
|
||||
<View style={[styles.tile, { backgroundColor: needsAction ? colors.tintStrong : colors.tint }]}>
|
||||
{needsAction ? <EnvelopeIcon color={colors.blue} /> : <CheckIcon />}
|
||||
</View>
|
||||
<View style={styles.text}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||
</View>
|
||||
{badge ? (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{badge}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.xl,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
...shadow.card,
|
||||
},
|
||||
pressed: { opacity: 0.7 },
|
||||
tile: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: radius.md,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
text: { flex: 1 },
|
||||
title: { fontFamily: font.extrabold, fontSize: 14.5, color: colors.ink },
|
||||
subtitle: { fontFamily: font.semibold, fontSize: 12, color: colors.slate, marginTop: 1 },
|
||||
badge: {
|
||||
backgroundColor: colors.tintStrong,
|
||||
borderRadius: radius.pill,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
badgeText: { fontFamily: font.extrabold, fontSize: 11, color: colors.blueInk },
|
||||
});
|
||||
Reference in New Issue
Block a user