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
screenOptions={{
headerStyle: { backgroundColor: farben.hintergrund },
headerShadowVisible: false,
headerTintColor: farben.primaer,
headerTitleStyle: { fontSize: schrift.gross, fontWeight: '700' },
headerBackTitle: 'Zurück',
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="Scan" component={ScanScreen} options={{ title: 'Brief scannen' }} />
<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: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 2,
borderRadius: 12,
borderWidth: 1,
borderLeftWidth: 6,
borderRadius: 14,
padding: abstand.m,
gap: abstand.s,
},

View File

@@ -61,16 +61,24 @@ export function GrossButton({
const styles = StyleSheet.create({
button: {
minHeight: TOUCH_TARGET,
borderRadius: 14,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: abstand.l,
paddingVertical: abstand.s,
},
primaer: { backgroundColor: farben.primaer },
sekundaer: { backgroundColor: farben.hintergrund, borderWidth: 2, borderColor: farben.primaer },
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 },
deaktiviert: { opacity: 0.5 },
gedrueckt: { opacity: 0.8 },
gedrueckt: { opacity: 0.8, transform: [{ translateY: 1 }] },
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 { Ampel } from '../components/Ampel';
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'>;
@@ -154,7 +154,12 @@ export function AnalyseScreen({ navigation, route }: Props) {
{/* Frist / 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 && (
<Text style={styles.fristText}>
📅 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' },
karte: {
backgroundColor: farben.flaeche,
borderRadius: 14,
borderRadius: 16,
borderWidth: 1,
borderColor: farben.rand,
padding: abstand.m,
marginTop: abstand.m,
gap: abstand.s,
...kartenSchatten,
},
// DIE Antwort der App — als "Textmarker"-Karte hervorgehoben (Markenzeichen)
karteWichtig: {
backgroundColor: farben.flaeche,
borderRadius: 14,
borderWidth: 2,
borderColor: farben.primaer,
backgroundColor: farben.markerHintergrund,
borderRadius: 16,
borderWidth: 1,
borderColor: farben.markerRand,
padding: abstand.m,
marginTop: abstand.m,
...kartenSchatten,
},
abschnittTitel: {
fontSize: schrift.gross,
@@ -289,7 +299,7 @@ const styles = StyleSheet.create({
color: farben.primaer,
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 },
fett: { fontWeight: '700' },
fristText: { fontSize: schrift.basis, color: farben.text, lineHeight: 28 },
@@ -307,11 +317,13 @@ const styles = StyleSheet.create({
sprachButton: {
marginTop: abstand.m,
minHeight: TOUCH_TARGET,
borderRadius: 14,
borderWidth: 2,
borderRadius: 16,
borderWidth: 1,
borderColor: farben.rand,
backgroundColor: farben.flaeche,
justifyContent: 'center',
paddingHorizontal: abstand.m,
...kartenSchatten,
},
sprachButtonText: { fontSize: schrift.basis, color: farben.text, fontWeight: '600' },
modalHintergrund: {

View File

@@ -18,10 +18,36 @@ import { holeConsent } from '../services/storage';
import { berechneAmpel } from '../utils/ampel';
import { formatiereDatum } from '../services/erinnerungen';
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'>;
/** 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) {
const briefe = useAppStore((s) => s.briefe);
const removeBrief = useAppStore((s) => s.removeBrief);
@@ -58,13 +84,16 @@ export function HomeScreen({ navigation }: Props) {
const ampel = berechneAmpel(item.analyse);
return (
<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 })}
onLongPress={() => loeschenBestaetigen(item)}
accessibilityRole="button"
accessibilityLabel={`Brief: ${item.analyse.brieftyp}. ${ampel.text}. Lange drücken zum Löschen.`}
>
<View style={[styles.ampelPunkt, { backgroundColor: ampel.farbe }]} />
<View style={{ flex: 1 }}>
<Text style={styles.kartenTitel} numberOfLines={1}>
{item.analyse.brieftyp}
@@ -82,65 +111,89 @@ export function HomeScreen({ navigation }: Props) {
};
return (
<View style={styles.container}>
<View style={styles.kopf}>
<GrossButton titel="Brief scannen" symbol="📷" onPress={scanStarten} />
<View style={styles.reihe}>
<View style={{ flex: 1 }}>
<GrossButton
titel="Glossar"
variante="sekundaer"
symbol="📖"
onPress={() => navigation.navigate('Glossar')}
/>
</View>
<View style={{ width: abstand.s }} />
<View style={{ flex: 1 }}>
<GrossButton
titel="Einstellungen"
variante="sekundaer"
<FlatList
style={styles.container}
// iOS: sorgt beim großen Titel für korrekte Abstände + Kollabieren beim Scrollen
contentInsetAdjustmentBehavior="automatic"
keyboardShouldPersistTaps="handled"
data={gefiltert}
keyExtractor={(b) => b.id}
renderItem={renderBrief}
contentContainerStyle={styles.inhalt}
ListHeaderComponent={
<View style={styles.kopf}>
<GrossButton titel="Brief scannen" symbol="📷" onPress={scanStarten} />
<View style={styles.reihe}>
<Kachel symbol="📖" titel="Glossar" onPress={() => navigation.navigate('Glossar')} />
<Kachel
symbol="⚙️"
titel="Einstellungen"
onPress={() => navigation.navigate('Einstellungen')}
/>
</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>
{briefe.length > 0 && (
<TextInput
style={styles.suche}
placeholder="Im Archiv suchen…"
placeholderTextColor={farben.textSekundaer}
value={suche}
onChangeText={setSuche}
accessibilityLabel="Archiv durchsuchen"
/>
)}
<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>
}
ListEmptyComponent={
briefe.length === 0 ? (
<View style={styles.leerBox}>
<Text style={styles.leerSymbol}>📬</Text>
<Text style={styles.leerTitel}>Noch keine Briefe</Text>
<Text style={styles.leer}>
Scannen Sie Ihren ersten Behördenbrief {'\n'}in einer Minute
wissen Sie, was er bedeutet.
</Text>
</View>
) : (
<Text style={styles.leer}>Keine Treffer.</Text>
)
}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: farben.hintergrund },
kopf: { padding: abstand.m, gap: abstand.s },
reihe: { flexDirection: 'row' },
inhalt: { paddingHorizontal: abstand.m, paddingBottom: abstand.l },
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: {
marginHorizontal: abstand.m,
marginBottom: abstand.s,
backgroundColor: farben.flaeche,
borderWidth: 1,
borderColor: farben.rand,
borderRadius: 12,
@@ -154,20 +207,25 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: farben.flaeche,
borderRadius: 14,
borderWidth: 1,
borderColor: farben.rand,
borderLeftWidth: 6,
padding: abstand.m,
marginBottom: abstand.s,
gap: abstand.s,
...kartenSchatten,
},
ampelPunkt: { width: 16, height: 16, borderRadius: 8 },
kartenTitel: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
kartenUntertitel: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 },
kartenAmpelText: { fontSize: schrift.klein, fontWeight: '600', marginTop: 2 },
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: {
textAlign: 'center',
fontSize: schrift.basis,
color: farben.textSekundaer,
marginTop: abstand.xl,
lineHeight: 28,
},
});

View File

@@ -5,14 +5,18 @@
* - Große Touch-Targets (min. 56px Höhe)
*/
export const farben = {
// Grundfarben
hintergrund: '#FFFFFF',
flaeche: '#F2F5F9',
// Grundfarben — warmer Papier-Ton als Markenzeichen (wie die Webseite):
// Inhalte liegen als weiße "Blätter" auf Papier
hintergrund: '#FAF7F0',
flaeche: '#FFFFFF',
primaer: '#1A365D', // dunkles, ruhiges Blau
primaerText: '#FFFFFF',
text: '#1A202C',
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)
ampelRot: '#C0392B',
@@ -43,3 +47,12 @@ export const abstand = {
/** Mindesthöhe für alle interaktiven Elemente (Barrierefreiheit). */
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;