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>
143 lines
4.6 KiB
TypeScript
143 lines
4.6 KiB
TypeScript
import { useRouter } from 'expo-router';
|
||
import { useState } from 'react';
|
||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
import { DitherBackground } from '../src/components/DitherBackground';
|
||
import { colors, font, glow, radius, space } from '../src/theme';
|
||
|
||
/**
|
||
* Onboarding, design option 4a: the dithered gradient runs full-bleed behind
|
||
* the whole screen. The design mocks step 2 of 3; the surrounding steps carry
|
||
* the same layout.
|
||
*/
|
||
|
||
const STEPS = [
|
||
{
|
||
headline: 'Behörden-\npost, ohne\nBauchweh',
|
||
body: 'Dokio nimmt dir die Angst vor dem Briefkasten.',
|
||
},
|
||
{
|
||
headline: 'Wir übersetzen\nBehördendeutsch',
|
||
body: 'Foto machen – und du bekommst jeden Brief in einfachen Worten erklärt.',
|
||
},
|
||
{
|
||
headline: 'Keine Frist\nmehr verpassen',
|
||
body: 'Wir merken uns jeden Termin und erinnern dich rechtzeitig.',
|
||
},
|
||
];
|
||
|
||
export default function Onboarding() {
|
||
const router = useRouter();
|
||
const insets = useSafeAreaInsets();
|
||
const [step, setStep] = useState(1);
|
||
|
||
const finish = () => router.replace('/(tabs)');
|
||
const next = () => (step === STEPS.length - 1 ? finish() : setStep(step + 1));
|
||
|
||
return (
|
||
<View style={styles.screen}>
|
||
{/* Finer cells than the design's default of 3, so the dither reads as
|
||
texture across the full screen instead of chunky blocks. */}
|
||
<DitherBackground color1={colors.blueDeep} color2={colors.blueMist} speed={1} pixel={1.5} />
|
||
|
||
<View style={[styles.content, { paddingTop: insets.top, paddingBottom: insets.top ? 0 : 12 }]}>
|
||
<View style={styles.skipRow}>
|
||
<Pressable onPress={finish} hitSlop={12} accessibilityRole="button">
|
||
<Text style={styles.skip}>Überspringen</Text>
|
||
</Pressable>
|
||
</View>
|
||
|
||
<View style={styles.hero}>
|
||
<View style={styles.illustration}>
|
||
<Text style={styles.illustrationLabel}>Illustration:{'\n'}Brief → Klartext</Text>
|
||
</View>
|
||
<Text style={styles.headline}>{STEPS[step].headline}</Text>
|
||
<Text style={styles.body}>{STEPS[step].body}</Text>
|
||
</View>
|
||
|
||
<View style={styles.dots}>
|
||
{STEPS.map((_, i) => (
|
||
<View key={i} style={[styles.dot, i === step && styles.dotActive]} />
|
||
))}
|
||
</View>
|
||
|
||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, 22) + 12 }]}>
|
||
<Pressable
|
||
style={({ pressed }) => [styles.cta, pressed && styles.ctaPressed]}
|
||
onPress={next}
|
||
accessibilityRole="button"
|
||
>
|
||
<Text style={styles.ctaLabel}>Weiter</Text>
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
screen: { flex: 1, backgroundColor: colors.blueDeep },
|
||
content: { flex: 1 },
|
||
skipRow: { alignItems: 'flex-end', paddingHorizontal: space.gutter, paddingTop: 14 },
|
||
skip: { fontFamily: font.bold, fontSize: 13.5, color: colors.onBlue, opacity: 0.8 },
|
||
|
||
hero: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 36 },
|
||
illustration: {
|
||
width: 230,
|
||
height: 230,
|
||
borderRadius: radius['5xl'],
|
||
backgroundColor: 'rgba(16,30,90,0.25)',
|
||
borderWidth: 1.5,
|
||
borderColor: 'rgba(255,255,255,0.45)',
|
||
borderStyle: 'dashed',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
illustrationLabel: {
|
||
fontSize: 11,
|
||
lineHeight: 16.5,
|
||
textAlign: 'center',
|
||
color: 'rgba(255,255,255,0.9)',
|
||
},
|
||
headline: {
|
||
fontFamily: font.display,
|
||
fontSize: 26,
|
||
lineHeight: 32.5,
|
||
letterSpacing: -0.3,
|
||
textAlign: 'center',
|
||
color: colors.onBlue,
|
||
marginTop: 34,
|
||
textShadowColor: 'rgba(16,30,90,0.3)',
|
||
textShadowOffset: { width: 0, height: 1 },
|
||
textShadowRadius: 12,
|
||
},
|
||
body: {
|
||
fontFamily: font.bold,
|
||
fontSize: 15.5,
|
||
lineHeight: 24.8,
|
||
textAlign: 'center',
|
||
color: colors.onBlue,
|
||
marginTop: 12,
|
||
...glow,
|
||
},
|
||
|
||
dots: { flexDirection: 'row', justifyContent: 'center', gap: 7, paddingBottom: 22 },
|
||
dot: { width: 8, height: 8, borderRadius: 4, backgroundColor: 'rgba(255,255,255,0.4)' },
|
||
dotActive: { width: 24, borderRadius: radius.pill, backgroundColor: '#FFFFFF' },
|
||
|
||
footer: { paddingHorizontal: space.gutter },
|
||
cta: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: radius.lg,
|
||
paddingVertical: 15,
|
||
alignItems: 'center',
|
||
shadowColor: 'rgba(16,30,90,1)',
|
||
shadowOpacity: 0.3,
|
||
shadowRadius: 24,
|
||
shadowOffset: { width: 0, height: 8 },
|
||
elevation: 8,
|
||
},
|
||
ctaPressed: { opacity: 0.9 },
|
||
ctaLabel: { fontFamily: font.extrabold, fontSize: 16, color: colors.blueDeep },
|
||
});
|