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:
JUSNandClaude Fable 5 committed 2026-07-16 21:32:18 +02:00
1 parent a670cc1758
commit 4ce397026c
12 files changed
+209 -130

No files matched your search

+6 -7
View File
@@ -1,22 +1,23 @@
/**
* 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 { StyleSheet, Text, View } from 'react-native';
import { AmpelStatus } from '../utils/ampel';
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 }) {
return (
<View
style={[styles.banner, { backgroundColor: status.hintergrund, borderColor: status.farbe }]}
style={[styles.banner, { backgroundColor: status.hintergrund, borderLeftColor: status.farbe }]}
accessibilityRole="alert"
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>
</View>
);
@@ -26,12 +27,10 @@ const styles = StyleSheet.create({
banner: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderLeftWidth: 6,
borderRadius: 14,
borderRadius: 12,
padding: abstand.m,
gap: abstand.s,
},
symbol: { fontSize: 28 },
text: { flex: 1, fontSize: schrift.gross, fontWeight: '700' },
});
+23 -30
View File
@@ -1,10 +1,15 @@
/**
* Großer, barrierefreier Button: min. 56px hoch, große Schrift,
* hoher Kontrast. Varianten: primär (gefüllt) und sekundär (Umriss).
* Großer, barrierefreier Button nach iOS-Vorbild:
* - 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 { 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 { Ikone, IkonenName } from './Ikone';
interface Props {
titel: string;
@@ -12,7 +17,7 @@ interface Props {
variante?: 'primaer' | 'sekundaer' | 'gefahr';
deaktiviert?: boolean;
laedt?: boolean;
symbol?: string;
ikone?: IkonenName;
}
export function GrossButton({
@@ -21,10 +26,11 @@ export function GrossButton({
variante = 'primaer',
deaktiviert = false,
laedt = false,
symbol,
ikone,
}: Props) {
const istPrimaer = variante === 'primaer';
const istGefahr = variante === 'gefahr';
const inhaltsFarbe = istPrimaer || istGefahr ? farben.primaerText : farben.primaer;
return (
<Pressable
onPress={onPress}
@@ -41,18 +47,12 @@ export function GrossButton({
]}
>
{laedt ? (
<ActivityIndicator color={istPrimaer || istGefahr ? farben.primaerText : farben.primaer} />
<ActivityIndicator color={inhaltsFarbe} />
) : (
<Text
style={[
styles.text,
(istPrimaer || istGefahr) && { color: farben.primaerText },
variante === 'sekundaer' && { color: farben.primaer },
]}
>
{symbol ? `${symbol} ` : ''}
{titel}
</Text>
<View style={styles.zeile}>
{ikone ? <Ikone name={ikone} farbe={inhaltsFarbe} /> : null}
<Text style={[styles.text, { color: inhaltsFarbe }]}>{titel}</Text>
</View>
)}
</Pressable>
);
@@ -61,24 +61,17 @@ export function GrossButton({
const styles = StyleSheet.create({
button: {
minHeight: TOUCH_TARGET,
borderRadius: 16,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: abstand.l,
paddingVertical: abstand.s,
},
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 },
zeile: { flexDirection: 'row', alignItems: 'center', gap: abstand.xs + 2 },
primaer: { backgroundColor: farben.primaer },
sekundaer: { backgroundColor: farben.flaecheSekundaer },
gefahr: { backgroundColor: farben.fehler },
deaktiviert: { opacity: 0.5 },
gedrueckt: { opacity: 0.8, transform: [{ translateY: 1 }] },
text: { fontSize: schrift.gross, fontWeight: '700', textAlign: 'center' },
deaktiviert: { opacity: 0.4 },
gedrueckt: { opacity: 0.65 },
text: { fontSize: schrift.gross, fontWeight: '600', textAlign: 'center' },
});
+72
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}
/>
);
}