generated from knso/webklar-preview-template
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:
12
App.tsx
12
App.tsx
@@ -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' }} />
|
||||
|
||||
BIN
rechtliches/~$hoerdenKlar-Datenschutz.docx
Normal file
BIN
rechtliches/~$hoerdenKlar-Datenschutz.docx
Normal file
Binary file not shown.
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,31 +111,29 @@ export function HomeScreen({ navigation }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<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}>
|
||||
<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"
|
||||
<Kachel symbol="📖" titel="Glossar" onPress={() => navigation.navigate('Glossar')} />
|
||||
<Kachel
|
||||
symbol="⚙️"
|
||||
titel="Einstellungen"
|
||||
onPress={() => navigation.navigate('Einstellungen')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{briefe.length > 0 && (
|
||||
<>
|
||||
<Text style={styles.abschnittsLabel}>Ihre Briefe</Text>
|
||||
<TextInput
|
||||
style={styles.suche}
|
||||
placeholder="Im Archiv suchen…"
|
||||
@@ -115,32 +142,58 @@ export function HomeScreen({ navigation }: Props) {
|
||||
onChangeText={setSuche}
|
||||
accessibilityLabel="Archiv durchsuchen"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FlatList
|
||||
data={gefiltert}
|
||||
keyExtractor={(b) => b.id}
|
||||
renderItem={renderBrief}
|
||||
contentContainerStyle={{ padding: abstand.m, paddingTop: 0 }}
|
||||
</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}>
|
||||
{briefe.length === 0
|
||||
? 'Noch keine Briefe.\nScannen Sie Ihren ersten Behördenbrief!'
|
||||
: 'Keine Treffer.'}
|
||||
Scannen Sie Ihren ersten Behördenbrief —{'\n'}in einer Minute
|
||||
wissen Sie, was er bedeutet.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.leer}>Keine Treffer.</Text>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
21
src/theme.ts
21
src/theme.ts
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user