Files
Brief-app/app/onboarding.tsx
KNSO 66bdf646ae 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>
2026-07-20 14:48:50 +02:00

143 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 },
});