App-Design-Politur: Papier-Look, Kacheln, Ampel-Streifen, großer Titel

- Theme: warmer Papier-Hintergrund (#FAF7F0) wie die Webseite, Inhalte
  als weiße Karten mit dezentem Schatten + warmem Rand; Marker-Gelb
  als Akzentfarbe für die Kernaussage
- Home: iOS-Großtitel (kollabiert beim Scrollen), Hero-Button mit
  Schatten, Kachel-Navigation statt halbbreiter Text-Buttons (behebt
  Umbruch "Einstellunge/n"), Abschnitts-Label, Brief-Karten mit
  Ampel-Farbstreifen links, freundlicher Leerzustand
- Analyse: Kernaussage als Textmarker-Karte (Markenzeichen), Frist/
  Termin-Karte in Ampel-Tönung mit Farbstreifen, einheitliche Karten
- Ampel-Banner: Streifen-Look statt Vollrahmen
- Layout-Fix: Home als FlatList-Wurzel mit automatischem Inset (vorher
  verdeckte der große Titel die Kopf-Buttons)

Barrierefreiheit unverändert: Schriftgrößen, Touch-Targets, Kontraste.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 21:14:11 +02:00
parent 7cfe830e9f
commit fdfd2d392a
7 changed files with 176 additions and 74 deletions

12
App.tsx
View File

@@ -36,13 +36,23 @@ export default function App() {
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
headerStyle: { backgroundColor: farben.hintergrund }, headerStyle: { backgroundColor: farben.hintergrund },
headerShadowVisible: false,
headerTintColor: farben.primaer, headerTintColor: farben.primaer,
headerTitleStyle: { fontSize: schrift.gross, fontWeight: '700' }, headerTitleStyle: { fontSize: schrift.gross, fontWeight: '700' },
headerBackTitle: 'Zurück', headerBackTitle: 'Zurück',
contentStyle: { backgroundColor: farben.hintergrund }, contentStyle: { backgroundColor: farben.hintergrund },
}} }}
> >
<Stack.Screen name="Home" component={HomeScreen} options={{ title: 'BehördenKlar' }} /> <Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: 'BehördenKlar',
headerLargeTitle: true,
headerLargeTitleStyle: { color: farben.primaer, fontWeight: '800' },
headerLargeStyle: { backgroundColor: farben.hintergrund },
}}
/>
<Stack.Screen name="Consent" component={ConsentScreen} options={{ title: 'Datenschutz' }} /> <Stack.Screen name="Consent" component={ConsentScreen} options={{ title: 'Datenschutz' }} />
<Stack.Screen name="Scan" component={ScanScreen} options={{ title: 'Brief scannen' }} /> <Stack.Screen name="Scan" component={ScanScreen} options={{ title: 'Brief scannen' }} />
<Stack.Screen name="Analyse" component={AnalyseScreen} options={{ title: 'Ihr Brief erklärt' }} /> <Stack.Screen name="Analyse" component={AnalyseScreen} options={{ title: 'Ihr Brief erklärt' }} />

Binary file not shown.

View File

@@ -26,8 +26,9 @@ const styles = StyleSheet.create({
banner: { banner: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderWidth: 2, borderWidth: 1,
borderRadius: 12, borderLeftWidth: 6,
borderRadius: 14,
padding: abstand.m, padding: abstand.m,
gap: abstand.s, gap: abstand.s,
}, },

View File

@@ -61,16 +61,24 @@ export function GrossButton({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
minHeight: TOUCH_TARGET, minHeight: TOUCH_TARGET,
borderRadius: 14, borderRadius: 16,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: abstand.l, paddingHorizontal: abstand.l,
paddingVertical: abstand.s, paddingVertical: abstand.s,
}, },
primaer: { backgroundColor: farben.primaer }, primaer: {
sekundaer: { backgroundColor: farben.hintergrund, borderWidth: 2, borderColor: farben.primaer }, backgroundColor: farben.primaer,
// Tiefe fürs wichtigste Element auf dem Bildschirm
shadowColor: farben.primaer,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.28,
shadowRadius: 6,
elevation: 4,
},
sekundaer: { backgroundColor: farben.flaeche, borderWidth: 2, borderColor: farben.primaer },
gefahr: { backgroundColor: farben.fehler }, gefahr: { backgroundColor: farben.fehler },
deaktiviert: { opacity: 0.5 }, deaktiviert: { opacity: 0.5 },
gedrueckt: { opacity: 0.8 }, gedrueckt: { opacity: 0.8, transform: [{ translateY: 1 }] },
text: { fontSize: schrift.gross, fontWeight: '700', textAlign: 'center' }, text: { fontSize: schrift.gross, fontWeight: '700', textAlign: 'center' },
}); });

View File

@@ -23,7 +23,7 @@ import { formatiereDatum } from '../services/erinnerungen';
import { berechneAmpel } from '../utils/ampel'; import { berechneAmpel } from '../utils/ampel';
import { Ampel } from '../components/Ampel'; import { Ampel } from '../components/Ampel';
import { GrossButton } from '../components/GrossButton'; import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme'; import { farben, schrift, abstand, kartenSchatten, TOUCH_TARGET } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Analyse'>; type Props = NativeStackScreenProps<RootStackParamList, 'Analyse'>;
@@ -154,7 +154,12 @@ export function AnalyseScreen({ navigation, route }: Props) {
{/* Frist / Termin */} {/* Frist / Termin */}
{(analyse.frist || analyse.termin) && ( {(analyse.frist || analyse.termin) && (
<View style={[styles.karte, { borderColor: ampel.farbe, borderWidth: 2 }]}> <View
style={[
styles.karte,
{ backgroundColor: ampel.hintergrund, borderLeftWidth: 6, borderLeftColor: ampel.farbe },
]}
>
{analyse.frist && ( {analyse.frist && (
<Text style={styles.fristText}> <Text style={styles.fristText}>
📅 Frist: <Text style={styles.fett}>{formatiereDatum(analyse.frist.datum)}</Text> 📅 Frist: <Text style={styles.fett}>{formatiereDatum(analyse.frist.datum)}</Text>
@@ -270,18 +275,23 @@ const styles = StyleSheet.create({
zentriert: { flex: 1, justifyContent: 'center', alignItems: 'center' }, zentriert: { flex: 1, justifyContent: 'center', alignItems: 'center' },
karte: { karte: {
backgroundColor: farben.flaeche, backgroundColor: farben.flaeche,
borderRadius: 14, borderRadius: 16,
borderWidth: 1,
borderColor: farben.rand,
padding: abstand.m, padding: abstand.m,
marginTop: abstand.m, marginTop: abstand.m,
gap: abstand.s, gap: abstand.s,
...kartenSchatten,
}, },
// DIE Antwort der App — als "Textmarker"-Karte hervorgehoben (Markenzeichen)
karteWichtig: { karteWichtig: {
backgroundColor: farben.flaeche, backgroundColor: farben.markerHintergrund,
borderRadius: 14, borderRadius: 16,
borderWidth: 2, borderWidth: 1,
borderColor: farben.primaer, borderColor: farben.markerRand,
padding: abstand.m, padding: abstand.m,
marginTop: abstand.m, marginTop: abstand.m,
...kartenSchatten,
}, },
abschnittTitel: { abschnittTitel: {
fontSize: schrift.gross, fontSize: schrift.gross,
@@ -289,7 +299,7 @@ const styles = StyleSheet.create({
color: farben.primaer, color: farben.primaer,
marginBottom: abstand.xs, marginBottom: abstand.xs,
}, },
kernaussage: { fontSize: schrift.gross, color: farben.text, lineHeight: 30 }, kernaussage: { fontSize: schrift.gross, color: farben.text, lineHeight: 30, fontWeight: '500' },
text: { fontSize: schrift.basis, color: farben.text, lineHeight: 27 }, text: { fontSize: schrift.basis, color: farben.text, lineHeight: 27 },
fett: { fontWeight: '700' }, fett: { fontWeight: '700' },
fristText: { fontSize: schrift.basis, color: farben.text, lineHeight: 28 }, fristText: { fontSize: schrift.basis, color: farben.text, lineHeight: 28 },
@@ -307,11 +317,13 @@ const styles = StyleSheet.create({
sprachButton: { sprachButton: {
marginTop: abstand.m, marginTop: abstand.m,
minHeight: TOUCH_TARGET, minHeight: TOUCH_TARGET,
borderRadius: 14, borderRadius: 16,
borderWidth: 2, borderWidth: 1,
borderColor: farben.rand, borderColor: farben.rand,
backgroundColor: farben.flaeche,
justifyContent: 'center', justifyContent: 'center',
paddingHorizontal: abstand.m, paddingHorizontal: abstand.m,
...kartenSchatten,
}, },
sprachButtonText: { fontSize: schrift.basis, color: farben.text, fontWeight: '600' }, sprachButtonText: { fontSize: schrift.basis, color: farben.text, fontWeight: '600' },
modalHintergrund: { modalHintergrund: {

View File

@@ -18,10 +18,36 @@ import { holeConsent } from '../services/storage';
import { berechneAmpel } from '../utils/ampel'; import { berechneAmpel } from '../utils/ampel';
import { formatiereDatum } from '../services/erinnerungen'; import { formatiereDatum } from '../services/erinnerungen';
import { GrossButton } from '../components/GrossButton'; import { GrossButton } from '../components/GrossButton';
import { farben, schrift, abstand } from '../theme'; import { farben, schrift, abstand, kartenSchatten, TOUCH_TARGET } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>; type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
/** Quadratische Navigations-Kachel: Symbol oben, Beschriftung darunter.
* (Löst das Platzproblem nebeneinanderstehender Text-Buttons.) */
function Kachel({
symbol,
titel,
onPress,
}: {
symbol: string;
titel: string;
onPress: () => void;
}) {
return (
<Pressable
style={({ pressed }) => [styles.kachel, pressed && { opacity: 0.7 }]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={titel}
>
<Text style={styles.kachelSymbol}>{symbol}</Text>
<Text style={styles.kachelTitel} numberOfLines={1}>
{titel}
</Text>
</Pressable>
);
}
export function HomeScreen({ navigation }: Props) { export function HomeScreen({ navigation }: Props) {
const briefe = useAppStore((s) => s.briefe); const briefe = useAppStore((s) => s.briefe);
const removeBrief = useAppStore((s) => s.removeBrief); const removeBrief = useAppStore((s) => s.removeBrief);
@@ -58,13 +84,16 @@ export function HomeScreen({ navigation }: Props) {
const ampel = berechneAmpel(item.analyse); const ampel = berechneAmpel(item.analyse);
return ( return (
<Pressable <Pressable
style={({ pressed }) => [styles.karte, pressed && { opacity: 0.7 }]} style={({ pressed }) => [
styles.karte,
{ borderLeftColor: ampel.farbe },
pressed && { opacity: 0.7 },
]}
onPress={() => navigation.navigate('Analyse', { briefId: item.id })} onPress={() => navigation.navigate('Analyse', { briefId: item.id })}
onLongPress={() => loeschenBestaetigen(item)} onLongPress={() => loeschenBestaetigen(item)}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Brief: ${item.analyse.brieftyp}. ${ampel.text}. Lange drücken zum Löschen.`} accessibilityLabel={`Brief: ${item.analyse.brieftyp}. ${ampel.text}. Lange drücken zum Löschen.`}
> >
<View style={[styles.ampelPunkt, { backgroundColor: ampel.farbe }]} />
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.kartenTitel} numberOfLines={1}> <Text style={styles.kartenTitel} numberOfLines={1}>
{item.analyse.brieftyp} {item.analyse.brieftyp}
@@ -82,65 +111,89 @@ export function HomeScreen({ navigation }: Props) {
}; };
return ( return (
<View style={styles.container}> <FlatList
<View style={styles.kopf}> style={styles.container}
<GrossButton titel="Brief scannen" symbol="📷" onPress={scanStarten} /> // iOS: sorgt beim großen Titel für korrekte Abstände + Kollabieren beim Scrollen
<View style={styles.reihe}> contentInsetAdjustmentBehavior="automatic"
<View style={{ flex: 1 }}> keyboardShouldPersistTaps="handled"
<GrossButton data={gefiltert}
titel="Glossar" keyExtractor={(b) => b.id}
variante="sekundaer" renderItem={renderBrief}
symbol="📖" contentContainerStyle={styles.inhalt}
onPress={() => navigation.navigate('Glossar')} ListHeaderComponent={
/> <View style={styles.kopf}>
</View> <GrossButton titel="Brief scannen" symbol="📷" onPress={scanStarten} />
<View style={{ width: abstand.s }} /> <View style={styles.reihe}>
<View style={{ flex: 1 }}> <Kachel symbol="📖" titel="Glossar" onPress={() => navigation.navigate('Glossar')} />
<GrossButton <Kachel
titel="Einstellungen"
variante="sekundaer"
symbol="⚙️" symbol="⚙️"
titel="Einstellungen"
onPress={() => navigation.navigate('Einstellungen')} onPress={() => navigation.navigate('Einstellungen')}
/> />
</View> </View>
{briefe.length > 0 && (
<>
<Text style={styles.abschnittsLabel}>Ihre Briefe</Text>
<TextInput
style={styles.suche}
placeholder="Im Archiv suchen…"
placeholderTextColor={farben.textSekundaer}
value={suche}
onChangeText={setSuche}
accessibilityLabel="Archiv durchsuchen"
/>
</>
)}
</View> </View>
</View> }
ListEmptyComponent={
{briefe.length > 0 && ( briefe.length === 0 ? (
<TextInput <View style={styles.leerBox}>
style={styles.suche} <Text style={styles.leerSymbol}>📬</Text>
placeholder="Im Archiv suchen…" <Text style={styles.leerTitel}>Noch keine Briefe</Text>
placeholderTextColor={farben.textSekundaer} <Text style={styles.leer}>
value={suche} Scannen Sie Ihren ersten Behördenbrief {'\n'}in einer Minute
onChangeText={setSuche} wissen Sie, was er bedeutet.
accessibilityLabel="Archiv durchsuchen" </Text>
/> </View>
)} ) : (
<Text style={styles.leer}>Keine Treffer.</Text>
<FlatList )
data={gefiltert} }
keyExtractor={(b) => b.id} />
renderItem={renderBrief}
contentContainerStyle={{ padding: abstand.m, paddingTop: 0 }}
ListEmptyComponent={
<Text style={styles.leer}>
{briefe.length === 0
? 'Noch keine Briefe.\nScannen Sie Ihren ersten Behördenbrief!'
: 'Keine Treffer.'}
</Text>
}
/>
</View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund }, container: { flex: 1, backgroundColor: farben.hintergrund },
kopf: { padding: abstand.m, gap: abstand.s }, inhalt: { paddingHorizontal: abstand.m, paddingBottom: abstand.l },
reihe: { flexDirection: 'row' }, kopf: { paddingTop: abstand.s, paddingBottom: abstand.s, gap: abstand.s },
reihe: { flexDirection: 'row', gap: abstand.s },
kachel: {
flex: 1,
minHeight: TOUCH_TARGET + 22,
backgroundColor: farben.flaeche,
borderRadius: 16,
borderWidth: 1,
borderColor: farben.rand,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: abstand.s,
gap: 4,
...kartenSchatten,
},
kachelSymbol: { fontSize: 26 },
kachelTitel: { fontSize: schrift.klein + 1, fontWeight: '600', color: farben.primaer },
abschnittsLabel: {
marginTop: abstand.m,
fontSize: schrift.klein - 1,
fontWeight: '600',
letterSpacing: 1.2,
textTransform: 'uppercase',
color: farben.textSekundaer,
},
suche: { suche: {
marginHorizontal: abstand.m, backgroundColor: farben.flaeche,
marginBottom: abstand.s,
borderWidth: 1, borderWidth: 1,
borderColor: farben.rand, borderColor: farben.rand,
borderRadius: 12, borderRadius: 12,
@@ -154,20 +207,25 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
backgroundColor: farben.flaeche, backgroundColor: farben.flaeche,
borderRadius: 14, borderRadius: 14,
borderWidth: 1,
borderColor: farben.rand,
borderLeftWidth: 6,
padding: abstand.m, padding: abstand.m,
marginBottom: abstand.s, marginBottom: abstand.s,
gap: abstand.s, gap: abstand.s,
...kartenSchatten,
}, },
ampelPunkt: { width: 16, height: 16, borderRadius: 8 },
kartenTitel: { fontSize: schrift.basis, fontWeight: '700', color: farben.text }, kartenTitel: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
kartenUntertitel: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 }, kartenUntertitel: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
kartenAmpelText: { fontSize: schrift.klein, fontWeight: '600', marginTop: 2 }, kartenAmpelText: { fontSize: schrift.klein, fontWeight: '600', marginTop: 2 },
pfeil: { fontSize: 32, color: farben.textSekundaer }, pfeil: { fontSize: 32, color: farben.textSekundaer },
leerBox: { alignItems: 'center', marginTop: abstand.xl, gap: abstand.xs },
leerSymbol: { fontSize: 52 },
leerTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
leer: { leer: {
textAlign: 'center', textAlign: 'center',
fontSize: schrift.basis, fontSize: schrift.basis,
color: farben.textSekundaer, color: farben.textSekundaer,
marginTop: abstand.xl,
lineHeight: 28, lineHeight: 28,
}, },
}); });

View File

@@ -5,14 +5,18 @@
* - Große Touch-Targets (min. 56px Höhe) * - Große Touch-Targets (min. 56px Höhe)
*/ */
export const farben = { export const farben = {
// Grundfarben // Grundfarben — warmer Papier-Ton als Markenzeichen (wie die Webseite):
hintergrund: '#FFFFFF', // Inhalte liegen als weiße "Blätter" auf Papier
flaeche: '#F2F5F9', hintergrund: '#FAF7F0',
flaeche: '#FFFFFF',
primaer: '#1A365D', // dunkles, ruhiges Blau primaer: '#1A365D', // dunkles, ruhiges Blau
primaerText: '#FFFFFF', primaerText: '#FFFFFF',
text: '#1A202C', text: '#1A202C',
textSekundaer: '#4A5568', textSekundaer: '#4A5568',
rand: '#CBD5E0', rand: '#E4DECF',
// Akzent: Textmarker-Gelb (sparsam einsetzen — nur für DIE Kernaussage)
markerHintergrund: '#FFF6D2',
markerRand: '#EBDD9A',
// Dringlichkeits-Ampel (kräftig, gut unterscheidbar) // Dringlichkeits-Ampel (kräftig, gut unterscheidbar)
ampelRot: '#C0392B', ampelRot: '#C0392B',
@@ -43,3 +47,12 @@ export const abstand = {
/** Mindesthöhe für alle interaktiven Elemente (Barrierefreiheit). */ /** Mindesthöhe für alle interaktiven Elemente (Barrierefreiheit). */
export const TOUCH_TARGET = 56; export const TOUCH_TARGET = 56;
/** Dezenter Schatten, damit weiße Karten sich vom Papier-Grund abheben. */
export const kartenSchatten = {
shadowColor: '#1A202C',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.07,
shadowRadius: 6,
elevation: 2,
} as const;