Apple-natives Design: SF Symbols statt Emojis, iOS-System-Optik

- Neue Ikone-Komponente (expo-symbols): echte SF Symbols auf iOS,
  Material Symbols auf Android — alle Emojis in Buttons/Kacheln/
  Checkboxen/Ampel/Chevrons ersetzt
- Theme: iOS-Systemgrau (#F2F2F7) statt Papier-Ton, weiße Karten ohne
  Rahmen und Schatten (wie Einstellungen/Health), "graue Taste" für
  Sekundär-Buttons statt Umrandung
- Kernaussage-Karte: dezenter Marken-Tint statt Textmarker-Gelb
- Frist/Termin-Zeilen ohne Emoji-Präfixe
- Layout unverändert (Kacheln, Ampel-Streifen, großer Titel)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 21:32:18 +02:00
parent a670cc1758
commit 4ce397026c
12 changed files with 209 additions and 130 deletions

View File

@@ -49,8 +49,7 @@ export default function App() {
options={{ options={{
title: 'BehördenKlar', title: 'BehördenKlar',
headerLargeTitle: true, headerLargeTitle: true,
headerLargeTitleStyle: { color: farben.primaer, fontWeight: '800' }, headerLargeTitleStyle: { color: farben.primaer },
headerLargeStyle: { backgroundColor: farben.hintergrund },
}} }}
/> />
<Stack.Screen name="Consent" component={ConsentScreen} options={{ title: 'Datenschutz' }} /> <Stack.Screen name="Consent" component={ConsentScreen} options={{ title: 'Datenschutz' }} />

51
package-lock.json generated
View File

@@ -28,6 +28,7 @@
"expo-speech": "~57.0.0", "expo-speech": "~57.0.0",
"expo-splash-screen": "~57.0.2", "expo-splash-screen": "~57.0.2",
"expo-status-bar": "~57.0.0", "expo-status-bar": "~57.0.0",
"expo-symbols": "~57.0.1",
"react": "19.2.3", "react": "19.2.3",
"react-native": "0.86.0", "react-native": "0.86.0",
"react-native-safe-area-context": "~5.7.0", "react-native-safe-area-context": "~5.7.0",
@@ -1275,6 +1276,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@expo-google-fonts/material-symbols": {
"version": "0.4.41",
"resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.41.tgz",
"integrity": "sha512-BKNSJ6oFGWi+aXu6PF+1EdI43J8Ag+Sm7Ste1SXYmC2eYOJasyjXHkKjgkIieBB1eDZQJSPWuPv421Vg6N5tCQ==",
"license": "MIT AND Apache-2.0"
},
"node_modules/@expo/code-signing-certificates": { "node_modules/@expo/code-signing-certificates": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
@@ -4254,6 +4261,20 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-font": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz",
"integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==",
"license": "MIT",
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-image-loader": { "node_modules/expo-image-loader": {
"version": "57.0.0", "version": "57.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.0.tgz", "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.0.tgz",
@@ -4450,6 +4471,22 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-symbols": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-57.0.1.tgz",
"integrity": "sha512-8Zf+a83OywV0vf1NUtSKpNqKcULmO0GTI+zfFnGYl7SLDH9FjL5RcEZoy6CHvCgq2KDrQF21pl3r7Tb4ItPscw==",
"license": "MIT",
"dependencies": {
"@expo-google-fonts/material-symbols": "^0.4.1",
"sf-symbols-typescript": "^2.0.0"
},
"peerDependencies": {
"expo": "*",
"expo-font": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo/node_modules/@expo/cli": { "node_modules/expo/node_modules/@expo/cli": {
"version": "57.0.6", "version": "57.0.6",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.6.tgz", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.6.tgz",
@@ -4761,20 +4798,6 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo/node_modules/expo-font": {
"version": "57.0.0",
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.0.tgz",
"integrity": "sha512-zF+J7WrNFjqyAADwdvDgkFEoIQv9DqcjJ57HVstNEH7/7Tx1ThPEhKraodEQOwSjMYWirP+4BYsVbdb+/Zr4QQ==",
"license": "MIT",
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo/node_modules/expo-keep-awake": { "node_modules/expo/node_modules/expo-keep-awake": {
"version": "57.0.0", "version": "57.0.0",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.0.tgz", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.0.tgz",

View File

@@ -23,6 +23,7 @@
"expo-speech": "~57.0.0", "expo-speech": "~57.0.0",
"expo-splash-screen": "~57.0.2", "expo-splash-screen": "~57.0.2",
"expo-status-bar": "~57.0.0", "expo-status-bar": "~57.0.0",
"expo-symbols": "~57.0.1",
"react": "19.2.3", "react": "19.2.3",
"react-native": "0.86.0", "react-native": "0.86.0",
"react-native-safe-area-context": "~5.7.0", "react-native-safe-area-context": "~5.7.0",

View File

@@ -1,22 +1,23 @@
/** /**
* Dringlichkeits-Ampel als gut sichtbares Banner. * Dringlichkeits-Ampel als gut sichtbares Banner.
* Farbe + Symbol + Text — nicht nur Farbe (Barrierefreiheit: Farbenblindheit). * Farbe + Icon + Text — nicht nur Farbe (Barrierefreiheit: Farbenblindheit).
*/ */
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { AmpelStatus } from '../utils/ampel'; import { AmpelStatus } from '../utils/ampel';
import { abstand, schrift } from '../theme'; import { abstand, schrift } from '../theme';
import { Ikone, IkonenName } from './Ikone';
const SYMBOL: Record<string, string> = { rot: '⚠️', gelb: '', gruen: '' }; const IKONE: Record<string, IkonenName> = { rot: 'warnung', gelb: 'wecker', gruen: 'info' };
export function Ampel({ status }: { status: AmpelStatus }) { export function Ampel({ status }: { status: AmpelStatus }) {
return ( return (
<View <View
style={[styles.banner, { backgroundColor: status.hintergrund, borderColor: status.farbe }]} style={[styles.banner, { backgroundColor: status.hintergrund, borderLeftColor: status.farbe }]}
accessibilityRole="alert" accessibilityRole="alert"
accessibilityLabel={`Dringlichkeit: ${status.text}`} accessibilityLabel={`Dringlichkeit: ${status.text}`}
> >
<Text style={styles.symbol}>{SYMBOL[status.stufe]}</Text> <Ikone name={IKONE[status.stufe]} groesse={26} farbe={status.farbe} />
<Text style={[styles.text, { color: status.farbe }]}>{status.text}</Text> <Text style={[styles.text, { color: status.farbe }]}>{status.text}</Text>
</View> </View>
); );
@@ -26,12 +27,10 @@ const styles = StyleSheet.create({
banner: { banner: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderWidth: 1,
borderLeftWidth: 6, borderLeftWidth: 6,
borderRadius: 14, borderRadius: 12,
padding: abstand.m, padding: abstand.m,
gap: abstand.s, gap: abstand.s,
}, },
symbol: { fontSize: 28 },
text: { flex: 1, fontSize: schrift.gross, fontWeight: '700' }, text: { flex: 1, fontSize: schrift.gross, fontWeight: '700' },
}); });

View File

@@ -1,10 +1,15 @@
/** /**
* Großer, barrierefreier Button: min. 56px hoch, große Schrift, * Großer, barrierefreier Button nach iOS-Vorbild:
* hoher Kontrast. Varianten: primär (gefüllt) und sekundär (Umriss). * - primär: gefüllt mit der Marken-Tintfarbe (wie "prominente" iOS-Buttons)
* - sekundär: hellgraue Fläche mit Tint-Text (wie Apples "graue Taste" —
* bewusst keine Umrandung, das wirkt nativer)
* - gefahr: rot gefüllt
* Min. 56px hoch, große Schrift, System-Icons statt Emojis.
*/ */
import React from 'react'; import React from 'react';
import { ActivityIndicator, Pressable, StyleSheet, Text } from 'react-native'; import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme'; import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
import { Ikone, IkonenName } from './Ikone';
interface Props { interface Props {
titel: string; titel: string;
@@ -12,7 +17,7 @@ interface Props {
variante?: 'primaer' | 'sekundaer' | 'gefahr'; variante?: 'primaer' | 'sekundaer' | 'gefahr';
deaktiviert?: boolean; deaktiviert?: boolean;
laedt?: boolean; laedt?: boolean;
symbol?: string; ikone?: IkonenName;
} }
export function GrossButton({ export function GrossButton({
@@ -21,10 +26,11 @@ export function GrossButton({
variante = 'primaer', variante = 'primaer',
deaktiviert = false, deaktiviert = false,
laedt = false, laedt = false,
symbol, ikone,
}: Props) { }: Props) {
const istPrimaer = variante === 'primaer'; const istPrimaer = variante === 'primaer';
const istGefahr = variante === 'gefahr'; const istGefahr = variante === 'gefahr';
const inhaltsFarbe = istPrimaer || istGefahr ? farben.primaerText : farben.primaer;
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
@@ -41,18 +47,12 @@ export function GrossButton({
]} ]}
> >
{laedt ? ( {laedt ? (
<ActivityIndicator color={istPrimaer || istGefahr ? farben.primaerText : farben.primaer} /> <ActivityIndicator color={inhaltsFarbe} />
) : ( ) : (
<Text <View style={styles.zeile}>
style={[ {ikone ? <Ikone name={ikone} farbe={inhaltsFarbe} /> : null}
styles.text, <Text style={[styles.text, { color: inhaltsFarbe }]}>{titel}</Text>
(istPrimaer || istGefahr) && { color: farben.primaerText }, </View>
variante === 'sekundaer' && { color: farben.primaer },
]}
>
{symbol ? `${symbol} ` : ''}
{titel}
</Text>
)} )}
</Pressable> </Pressable>
); );
@@ -61,24 +61,17 @@ export function GrossButton({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
minHeight: TOUCH_TARGET, minHeight: TOUCH_TARGET,
borderRadius: 16, borderRadius: 14,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: abstand.l, paddingHorizontal: abstand.l,
paddingVertical: abstand.s, paddingVertical: abstand.s,
}, },
primaer: { zeile: { flexDirection: 'row', alignItems: 'center', gap: abstand.xs + 2 },
backgroundColor: farben.primaer, primaer: { backgroundColor: farben.primaer },
// Tiefe fürs wichtigste Element auf dem Bildschirm sekundaer: { backgroundColor: farben.flaecheSekundaer },
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.4 },
gedrueckt: { opacity: 0.8, transform: [{ translateY: 1 }] }, gedrueckt: { opacity: 0.65 },
text: { fontSize: schrift.gross, fontWeight: '700', textAlign: 'center' }, text: { fontSize: schrift.gross, fontWeight: '600', textAlign: 'center' },
}); });

72
src/components/Ikone.tsx Normal file
View File

@@ -0,0 +1,72 @@
/**
* Zentrale Icon-Komponente — echte System-Icons statt Emojis:
* SF Symbols auf iOS (Apple-Look), Material Symbols auf Android.
* Alle in der App verwendeten Icons sind hier benannt, damit die
* Plattform-Zuordnung an einer Stelle liegt.
*/
import React from 'react';
import { SymbolView } from 'expo-symbols';
export type IkonenName =
| 'kamera'
| 'galerie'
| 'dokument'
| 'buch'
| 'zahnrad'
| 'brief'
| 'pfeilRechts'
| 'vorlesen'
| 'stopp'
| 'kalender'
| 'stift'
| 'teilen'
| 'muell'
| 'globus'
| 'checkAn'
| 'checkAus'
| 'warnung'
| 'wecker'
| 'info'
| 'funken';
// `as const` hält die Namen als Literal-Typen — so prüft TypeScript gegen
// die offizielle SF-Symbol-/Material-Namensliste von expo-symbols
const NAMEN = {
kamera: { ios: 'camera.fill', android: 'photo_camera' },
galerie: { ios: 'photo.on.rectangle', android: 'image' },
dokument: { ios: 'doc.fill', android: 'description' },
buch: { ios: 'book.fill', android: 'menu_book' },
zahnrad: { ios: 'gearshape.fill', android: 'settings' },
brief: { ios: 'envelope.open.fill', android: 'mail' },
pfeilRechts: { ios: 'chevron.right', android: 'chevron_right' },
vorlesen: { ios: 'speaker.wave.2.fill', android: 'volume_up' },
stopp: { ios: 'stop.fill', android: 'stop' },
kalender: { ios: 'calendar.badge.plus', android: 'event' },
stift: { ios: 'square.and.pencil', android: 'edit' },
teilen: { ios: 'square.and.arrow.up', android: 'share' },
muell: { ios: 'trash.fill', android: 'delete' },
globus: { ios: 'globe', android: 'language' },
checkAn: { ios: 'checkmark.square.fill', android: 'check_box' },
checkAus: { ios: 'square', android: 'check_box_outline_blank' },
warnung: { ios: 'exclamationmark.triangle.fill', android: 'warning' },
wecker: { ios: 'alarm.fill', android: 'alarm' },
info: { ios: 'info.circle.fill', android: 'info' },
funken: { ios: 'wand.and.stars', android: 'auto_awesome' },
} as const;
interface Props {
name: IkonenName;
groesse?: number;
farbe: string;
}
export function Ikone({ name, groesse = 22, farbe }: Props) {
const eintrag = NAMEN[name];
return (
<SymbolView
name={{ ios: eintrag.ios, android: eintrag.android }}
size={groesse}
tintColor={farbe}
/>
);
}

View File

@@ -23,7 +23,8 @@ 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, kartenSchatten, TOUCH_TARGET } from '../theme'; import { Ikone } from '../components/Ikone';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Analyse'>; type Props = NativeStackScreenProps<RootStackParamList, 'Analyse'>;
@@ -133,9 +134,13 @@ export function AnalyseScreen({ navigation, route }: Props) {
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Sprache ändern. Aktuell: ${sprache.name}`} accessibilityLabel={`Sprache ändern. Aktuell: ${sprache.name}`}
> >
<Text style={styles.sprachButtonText}> <View style={styles.sprachZeileInhalt}>
🌐 {sprache.eigenname} {uebersetzt ? ' (übersetzt…)' : ''} <Ikone name="globus" groesse={20} farbe={farben.primaer} />
</Text> <Text style={styles.sprachButtonText}>
{sprache.eigenname} {uebersetzt ? ' (übersetzt…)' : ''}
</Text>
<Ikone name="pfeilRechts" groesse={16} farbe={farben.textTertiaer} />
</View>
</Pressable> </Pressable>
{/* Kernaussage: Was will das Amt von mir? */} {/* Kernaussage: Was will das Amt von mir? */}
@@ -145,7 +150,7 @@ export function AnalyseScreen({ navigation, route }: Props) {
<View style={{ marginTop: abstand.s }}> <View style={{ marginTop: abstand.s }}>
<GrossButton <GrossButton
titel={liest ? 'Vorlesen stoppen' : 'Vorlesen'} titel={liest ? 'Vorlesen stoppen' : 'Vorlesen'}
symbol={liest ? '⏹️' : '🔊'} ikone={liest ? 'stopp' : 'vorlesen'}
variante="sekundaer" variante="sekundaer"
onPress={vorlesen} onPress={vorlesen}
/> />
@@ -162,7 +167,7 @@ export function AnalyseScreen({ navigation, route }: Props) {
> >
{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>
{'\n'} {'\n'}
{analyse.frist.aktion} {analyse.frist.aktion}
</Text> </Text>
@@ -170,13 +175,13 @@ export function AnalyseScreen({ navigation, route }: Props) {
{analyse.termin && ( {analyse.termin && (
<> <>
<Text style={styles.fristText}> <Text style={styles.fristText}>
🕐 Termin: <Text style={styles.fett}>{formatiereDatum(analyse.termin.datum)}</Text> Termin: <Text style={styles.fett}>{formatiereDatum(analyse.termin.datum)}</Text>
{analyse.termin.uhrzeit ? ` um ${analyse.termin.uhrzeit} Uhr` : ''} {analyse.termin.uhrzeit ? ` um ${analyse.termin.uhrzeit} Uhr` : ''}
{analyse.termin.ort ? `\n📍 ${analyse.termin.ort}` : ''} {analyse.termin.ort ? `\nOrt: ${analyse.termin.ort}` : ''}
</Text> </Text>
<GrossButton <GrossButton
titel="Termin zum Kalender hinzufügen" titel="Termin zum Kalender hinzufügen"
symbol="🗓️" ikone="kalender"
variante="sekundaer" variante="sekundaer"
onPress={kalenderExport} onPress={kalenderExport}
/> />
@@ -203,7 +208,11 @@ export function AnalyseScreen({ navigation, route }: Props) {
accessibilityRole="checkbox" accessibilityRole="checkbox"
accessibilityState={{ checked: !!erledigt[i] }} accessibilityState={{ checked: !!erledigt[i] }}
> >
<Text style={styles.checkBox}>{erledigt[i] ? '☑️' : '⬜'}</Text> <Ikone
name={erledigt[i] ? 'checkAn' : 'checkAus'}
groesse={26}
farbe={erledigt[i] ? farben.ampelGruen : farben.textTertiaer}
/>
<Text style={[styles.text, { flex: 1 }, erledigt[i] && styles.durchgestrichen]}> <Text style={[styles.text, { flex: 1 }, erledigt[i] && styles.durchgestrichen]}>
{punkt} {punkt}
</Text> </Text>
@@ -229,13 +238,13 @@ export function AnalyseScreen({ navigation, route }: Props) {
{analyse.antwort_noetig && ( {analyse.antwort_noetig && (
<GrossButton <GrossButton
titel="Antwort erstellen" titel="Antwort erstellen"
symbol="✍️" ikone="stift"
onPress={() => navigation.navigate('Antwort', { briefId: brief.id })} onPress={() => navigation.navigate('Antwort', { briefId: brief.id })}
/> />
)} )}
<View style={{ height: abstand.m }} /> <View style={{ height: abstand.m }} />
<GrossButton titel="Brief löschen" symbol="🗑️" variante="gefahr" onPress={loeschen} /> <GrossButton titel="Brief löschen" ikone="muell" variante="gefahr" onPress={loeschen} />
<View style={{ height: abstand.xl }} /> <View style={{ height: abstand.xl }} />
{/* Sprach-Auswahl-Dialog */} {/* Sprach-Auswahl-Dialog */}
@@ -275,23 +284,19 @@ 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: 16, borderRadius: 12,
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) // DIE Antwort der App — dezent im Marken-Tint hervorgehoben
karteWichtig: { karteWichtig: {
backgroundColor: farben.markerHintergrund, backgroundColor: farben.hervorhebung,
borderRadius: 16, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderColor: farben.markerRand, borderColor: farben.hervorhebungRand,
padding: abstand.m, padding: abstand.m,
marginTop: abstand.m, marginTop: abstand.m,
...kartenSchatten,
}, },
abschnittTitel: { abschnittTitel: {
fontSize: schrift.gross, fontSize: schrift.gross,
@@ -310,22 +315,19 @@ const styles = StyleSheet.create({
minHeight: 44, minHeight: 44,
paddingVertical: 4, paddingVertical: 4,
}, },
checkBox: { fontSize: 24 },
durchgestrichen: { textDecorationLine: 'line-through', color: farben.textSekundaer }, durchgestrichen: { textDecorationLine: 'line-through', color: farben.textSekundaer },
begriffBlock: { marginBottom: abstand.s }, begriffBlock: { marginBottom: abstand.s },
begriff: { fontSize: schrift.basis, fontWeight: '700', color: farben.text }, begriff: { fontSize: schrift.basis, fontWeight: '700', color: farben.text },
sprachButton: { sprachButton: {
marginTop: abstand.m, marginTop: abstand.m,
minHeight: TOUCH_TARGET, minHeight: TOUCH_TARGET,
borderRadius: 16, borderRadius: 12,
borderWidth: 1,
borderColor: farben.rand,
backgroundColor: farben.flaeche, backgroundColor: farben.flaeche,
justifyContent: 'center', justifyContent: 'center',
paddingHorizontal: abstand.m, paddingHorizontal: abstand.m,
...kartenSchatten,
}, },
sprachButtonText: { fontSize: schrift.basis, color: farben.text, fontWeight: '600' }, sprachZeileInhalt: { flexDirection: 'row', alignItems: 'center', gap: abstand.s },
sprachButtonText: { flex: 1, fontSize: schrift.basis, color: farben.text, fontWeight: '600' },
modalHintergrund: { modalHintergrund: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)', backgroundColor: 'rgba(0,0,0,0.5)',
@@ -333,7 +335,7 @@ const styles = StyleSheet.create({
padding: abstand.l, padding: abstand.l,
}, },
modalKarte: { modalKarte: {
backgroundColor: farben.hintergrund, backgroundColor: farben.flaeche,
borderRadius: 14, borderRadius: 14,
padding: abstand.m, padding: abstand.m,
maxHeight: '75%', maxHeight: '75%',

View File

@@ -146,7 +146,7 @@ export function AntwortScreen({ route }: Props) {
<GrossButton <GrossButton
titel={text ? 'Neuen Entwurf erstellen' : 'Entwurf erstellen'} titel={text ? 'Neuen Entwurf erstellen' : 'Entwurf erstellen'}
symbol="✨" ikone="funken"
onPress={erzeugen} onPress={erzeugen}
deaktiviert={!typ} deaktiviert={!typ}
laedt={laedt} laedt={laedt}
@@ -173,8 +173,8 @@ export function AntwortScreen({ route }: Props) {
accessibilityLabel="Brieftext" accessibilityLabel="Brieftext"
/> />
<View style={{ gap: abstand.s }}> <View style={{ gap: abstand.s }}>
<GrossButton titel="Als PDF exportieren" symbol="📄" onPress={alsPdf} /> <GrossButton titel="Als PDF exportieren" ikone="dokument" onPress={alsPdf} />
<GrossButton titel="Als Text teilen / kopieren" symbol="📤" variante="sekundaer" onPress={alsText} /> <GrossButton titel="Als Text teilen / kopieren" ikone="teilen" variante="sekundaer" onPress={alsText} />
</View> </View>
</> </>
)} )}

View File

@@ -260,7 +260,7 @@ export function EinstellungenScreen({ navigation }: Props) {
<View style={styles.trenner} /> <View style={styles.trenner} />
<Text style={styles.abschnittTitel}>Daten löschen</Text> <Text style={styles.abschnittTitel}>Daten löschen</Text>
<GrossButton titel="Alle Daten löschen" symbol="🗑️" variante="gefahr" onPress={allesLoeschen} /> <GrossButton titel="Alle Daten löschen" ikone="muell" variante="gefahr" onPress={allesLoeschen} />
<View style={{ height: abstand.xl }} /> <View style={{ height: abstand.xl }} />
</ScrollView> </ScrollView>
); );

View File

@@ -18,29 +18,30 @@ 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, kartenSchatten, TOUCH_TARGET } from '../theme'; import { Ikone, IkonenName } from '../components/Ikone';
import { farben, schrift, abstand, TOUCH_TARGET } from '../theme';
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>; type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
/** Quadratische Navigations-Kachel: Symbol oben, Beschriftung darunter. /** Quadratische Navigations-Kachel: Icon oben, Beschriftung darunter.
* (Löst das Platzproblem nebeneinanderstehender Text-Buttons.) */ * (Löst das Platzproblem nebeneinanderstehender Text-Buttons.) */
function Kachel({ function Kachel({
symbol, ikone,
titel, titel,
onPress, onPress,
}: { }: {
symbol: string; ikone: IkonenName;
titel: string; titel: string;
onPress: () => void; onPress: () => void;
}) { }) {
return ( return (
<Pressable <Pressable
style={({ pressed }) => [styles.kachel, pressed && { opacity: 0.7 }]} style={({ pressed }) => [styles.kachel, pressed && { opacity: 0.65 }]}
onPress={onPress} onPress={onPress}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={titel} accessibilityLabel={titel}
> >
<Text style={styles.kachelSymbol}>{symbol}</Text> <Ikone name={ikone} groesse={26} farbe={farben.primaer} />
<Text style={styles.kachelTitel} numberOfLines={1}> <Text style={styles.kachelTitel} numberOfLines={1}>
{titel} {titel}
</Text> </Text>
@@ -105,7 +106,7 @@ export function HomeScreen({ navigation }: Props) {
{ampel.text} {ampel.text}
</Text> </Text>
</View> </View>
<Text style={styles.pfeil}></Text> <Ikone name="pfeilRechts" groesse={18} farbe={farben.textTertiaer} />
</Pressable> </Pressable>
); );
}; };
@@ -122,11 +123,11 @@ export function HomeScreen({ navigation }: Props) {
contentContainerStyle={styles.inhalt} contentContainerStyle={styles.inhalt}
ListHeaderComponent={ ListHeaderComponent={
<View style={styles.kopf}> <View style={styles.kopf}>
<GrossButton titel="Brief scannen" symbol="📷" onPress={scanStarten} /> <GrossButton titel="Brief scannen" ikone="kamera" onPress={scanStarten} />
<View style={styles.reihe}> <View style={styles.reihe}>
<Kachel symbol="📖" titel="Glossar" onPress={() => navigation.navigate('Glossar')} /> <Kachel ikone="buch" titel="Glossar" onPress={() => navigation.navigate('Glossar')} />
<Kachel <Kachel
symbol="⚙️" ikone="zahnrad"
titel="Einstellungen" titel="Einstellungen"
onPress={() => navigation.navigate('Einstellungen')} onPress={() => navigation.navigate('Einstellungen')}
/> />
@@ -149,7 +150,7 @@ export function HomeScreen({ navigation }: Props) {
ListEmptyComponent={ ListEmptyComponent={
briefe.length === 0 ? ( briefe.length === 0 ? (
<View style={styles.leerBox}> <View style={styles.leerBox}>
<Text style={styles.leerSymbol}>📬</Text> <Ikone name="brief" groesse={48} farbe={farben.textTertiaer} />
<Text style={styles.leerTitel}>Noch keine Briefe</Text> <Text style={styles.leerTitel}>Noch keine Briefe</Text>
<Text style={styles.leer}> <Text style={styles.leer}>
Scannen Sie Ihren ersten Behördenbrief {'\n'}in einer Minute Scannen Sie Ihren ersten Behördenbrief {'\n'}in einer Minute
@@ -173,16 +174,12 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
minHeight: TOUCH_TARGET + 22, minHeight: TOUCH_TARGET + 22,
backgroundColor: farben.flaeche, backgroundColor: farben.flaeche,
borderRadius: 16, borderRadius: 12,
borderWidth: 1,
borderColor: farben.rand,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: abstand.s, paddingVertical: abstand.s,
gap: 4, gap: 6,
...kartenSchatten,
}, },
kachelSymbol: { fontSize: 26 },
kachelTitel: { fontSize: schrift.klein + 1, fontWeight: '600', color: farben.primaer }, kachelTitel: { fontSize: schrift.klein + 1, fontWeight: '600', color: farben.primaer },
abschnittsLabel: { abschnittsLabel: {
marginTop: abstand.m, marginTop: abstand.m,
@@ -194,8 +191,6 @@ const styles = StyleSheet.create({
}, },
suche: { suche: {
backgroundColor: farben.flaeche, backgroundColor: farben.flaeche,
borderWidth: 1,
borderColor: farben.rand,
borderRadius: 12, borderRadius: 12,
padding: abstand.s, padding: abstand.s,
fontSize: schrift.basis, fontSize: schrift.basis,
@@ -206,21 +201,16 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: farben.flaeche, backgroundColor: farben.flaeche,
borderRadius: 14, borderRadius: 12,
borderWidth: 1,
borderColor: farben.rand,
borderLeftWidth: 6, borderLeftWidth: 6,
padding: abstand.m, padding: abstand.m,
marginBottom: abstand.s, marginBottom: abstand.s,
gap: abstand.s, gap: abstand.s,
...kartenSchatten,
}, },
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 }, leerBox: { alignItems: 'center', marginTop: abstand.xl, gap: abstand.s },
leerBox: { alignItems: 'center', marginTop: abstand.xl, gap: abstand.xs },
leerSymbol: { fontSize: 52 },
leerTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text }, leerTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text },
leer: { leer: {
textAlign: 'center', textAlign: 'center',

View File

@@ -182,9 +182,9 @@ export function ScanScreen({ navigation }: Props) {
Fotografieren Sie den Brief gut beleuchtet und von oben. Der ganze Text Fotografieren Sie den Brief gut beleuchtet und von oben. Der ganze Text
muss auf dem Foto sein. muss auf dem Foto sein.
</Text> </Text>
<GrossButton titel="Foto aufnehmen" symbol="📷" onPress={fotoAufnehmen} /> <GrossButton titel="Foto aufnehmen" ikone="kamera" onPress={fotoAufnehmen} />
<GrossButton titel="Aus Galerie wählen" symbol="🖼️" variante="sekundaer" onPress={ausGalerie} /> <GrossButton titel="Aus Galerie wählen" ikone="galerie" variante="sekundaer" onPress={ausGalerie} />
<GrossButton titel="PDF hochladen" symbol="📄" variante="sekundaer" onPress={pdfWaehlen} /> <GrossButton titel="PDF hochladen" ikone="dokument" variante="sekundaer" onPress={pdfWaehlen} />
{NUTZT_PROXY && ( {NUTZT_PROXY && (
<Text style={styles.kontingent}> <Text style={styles.kontingent}>
{kontingentLeer {kontingentLeer

View File

@@ -5,18 +5,20 @@
* - Große Touch-Targets (min. 56px Höhe) * - Große Touch-Targets (min. 56px Höhe)
*/ */
export const farben = { export const farben = {
// Grundfarben — warmer Papier-Ton als Markenzeichen (wie die Webseite): // Grundfarben — natives iOS-Vorbild: gruppierter System-Hintergrund,
// Inhalte liegen als weiße "Blätter" auf Papier // Inhalte als weiße Karten ohne Rahmen und Schatten (wie Einstellungen/Health)
hintergrund: '#FAF7F0', hintergrund: '#F2F2F7',
flaeche: '#FFFFFF', flaeche: '#FFFFFF',
primaer: '#1A365D', // dunkles, ruhiges Blau flaecheSekundaer: '#E9EDF3', // "graue Taste" nach Apple-Art (statt Umrandung)
primaer: '#1A365D', // dunkles, ruhiges Blau (Marken-Tint)
primaerText: '#FFFFFF', primaerText: '#FFFFFF',
text: '#1A202C', text: '#1A202C',
textSekundaer: '#4A5568', textSekundaer: '#4A5568',
rand: '#E4DECF', textTertiaer: '#AEAEB2', // z. B. Chevrons (iOS systemGray2)
// Akzent: Textmarker-Gelb (sparsam einsetzen — nur für DIE Kernaussage) rand: '#E2E2E7',
markerHintergrund: '#FFF6D2', // Dezente Hervorhebung für DIE Kernaussage (leichter Marken-Tint)
markerRand: '#EBDD9A', hervorhebung: '#EDF2F9',
hervorhebungRand: '#DCE5F1',
// Dringlichkeits-Ampel (kräftig, gut unterscheidbar) // Dringlichkeits-Ampel (kräftig, gut unterscheidbar)
ampelRot: '#C0392B', ampelRot: '#C0392B',
@@ -48,11 +50,9 @@ 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 = { * Bewusst leer: Nach Apple-Vorbild heben sich weiße Karten allein durch
shadowColor: '#1A202C', * den grauen System-Hintergrund ab — ohne Schatten und Rahmen.
shadowOffset: { width: 0, height: 2 }, * (Als No-Op behalten, damit bestehende Spreads harmlos bleiben.)
shadowOpacity: 0.07, */
shadowRadius: 6, export const kartenSchatten = {} as const;
elevation: 2,
} as const;