feat(theme): Dark Theme — global color-system + Wave 1 screens

Theme-switch in Settings (System/Light/Dark) jetzt App-weit wirksam für die
Core-Screens. Wave 2 dokumentiert (siehe unten).

Color-System:
- lib/theme.ts: refactored zu colors.light + colors.dark (gleiche keys)
  Light: bg #fff, surface #fafafa, surfaceElevated #f5f5f5, border #e5e5e5,
         text #0a0a0a, textMuted #737373
  Dark:  bg #000, surface #1c1c1e, surfaceElevated #2c2c2e, border #38383a,
         text #fff, textMuted #8e8e93
  brandOrange unverändert #007AFF (iOS system blue)
  success/error variieren (light: #16a34a/#dc2626, dark: #30d158/#ff453a)
- legacy `colors` export bleibt als Light-Fallback für nicht-migrierte Files
- new `useColors()` hook → liest aktiven scheme aus useThemeStore

stores/theme.ts:
- Appearance.addChangeListener für live System-Theme-Updates (User schaltet
  iOS Dark/Light → App reagiert sofort ohne Reload)

Wave 1 — migrated Files (Core Screens):
- app/_layout.tsx + app/(app)/_layout.tsx + app/(app)/index.tsx (root + home)
- app/settings.tsx (full theme-aware inkl. TrueSheet)
- app/profile/index.tsx (bg + dividers)
- app/devices.tsx (bg, surface, border, icons)
- app/lyra.tsx (chat container, backdrop, bubbles, ThinkingDots, LoadingPulse)
- components/AppHeader (Nativewind classes ersetzt durch theme-aware Styles)
- components/header/HeaderDropdownMenu
- components/profile/* (ProfileHeader, StatsBar, StreakSection, UrgeStatsCard,
  ApprovedDomainsList, DemographicsAccordion)

Wave 2 (TODOs für separate Session):
- app/urge.tsx (~20 hardcoded colors, größter Screen)
- app/room.tsx, app/dm.tsx, app/(app)/chat.tsx, app/(app)/mail.tsx, app/(app)/coach.tsx
- app/games.tsx, app/profile/[userId].tsx
- Nativewind classes in PostCard, ComposeCard, PostCardSkeleton, NotificationsDropdown

StatusBar style dynamisch synchronisiert (light bei dark-mode, dark bei light).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-08 22:15:55 +02:00
parent 8f2b93f881
commit 594a43cbf9
16 changed files with 244 additions and 146 deletions

View File

@ -5,7 +5,7 @@ import * as Notifications from 'expo-notifications';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../../stores/auth'; import { useAuthStore } from '../../stores/auth';
import { useNotificationStore } from '../../stores/notifications'; import { useNotificationStore } from '../../stores/notifications';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
import { NativeTabs } from '../../components/NativeTabs'; import { NativeTabs } from '../../components/NativeTabs';
import { protection } from '../../lib/protection'; import { protection } from '../../lib/protection';
import { preloadTabIcons, getTabIcon } from '../../lib/tabIcons'; import { preloadTabIcons, getTabIcon } from '../../lib/tabIcons';
@ -14,6 +14,7 @@ export default function AppLayout() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { session, loading } = useAuthStore(); const { session, loading } = useAuthStore();
const colors = useColors();
const loadNotifications = useNotificationStore((s) => s.load); const loadNotifications = useNotificationStore((s) => s.load);
const startRealtime = useNotificationStore((s) => s.startRealtime); const startRealtime = useNotificationStore((s) => s.startRealtime);
const stopRealtime = useNotificationStore((s) => s.stopRealtime); const stopRealtime = useNotificationStore((s) => s.stopRealtime);
@ -143,7 +144,7 @@ export default function AppLayout() {
if (loading || !session) { if (loading || !session) {
return ( return (
<View className="flex-1 bg-white items-center justify-center"> <View style={{ flex: 1, backgroundColor: colors.bg, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator color={colors.brandOrange} size="large" /> <ActivityIndicator color={colors.brandOrange} size="large" />
</View> </View>
); );

View File

@ -19,7 +19,7 @@ import { PostCardSkeleton } from '../../components/PostCardSkeleton';
import { PostCommentsSheet } from '../../components/PostCommentsSheet'; import { PostCommentsSheet } from '../../components/PostCommentsSheet';
import { useCommunityStore, type CommunityCategory, type CommunityPost } from '../../stores/community'; import { useCommunityStore, type CommunityCategory, type CommunityPost } from '../../stores/community';
import { useCommunityRealtime } from '../../hooks/useCommunityRealtime'; import { useCommunityRealtime } from '../../hooks/useCommunityRealtime';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
type FilterChip = { type FilterChip = {
value: CommunityCategory; value: CommunityCategory;
@ -30,6 +30,7 @@ type FilterChip = {
export default function HomeScreen() { export default function HomeScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const colors = useColors();
// Granular selectors: subscribing to the whole store (incl. optimisticLikes) // Granular selectors: subscribing to the whole store (incl. optimisticLikes)
// would re-render the screen — and thus the FlatList — on every like. // would re-render the screen — and thus the FlatList — on every like.
const activeCategory = useCommunityStore((s) => s.activeCategory); const activeCategory = useCommunityStore((s) => s.activeCategory);
@ -79,7 +80,7 @@ export default function HomeScreen() {
); );
return ( return (
<View className="flex-1 bg-neutral-50"> <View style={{ flex: 1, backgroundColor: colors.bg }}>
<AppHeader /> <AppHeader />
<FlatList <FlatList
@ -139,23 +140,30 @@ export default function HomeScreen() {
<Pressable <Pressable
key={f.value} key={f.value}
onPress={() => toggleFilter(f.value)} onPress={() => toggleFilter(f.value)}
className={`flex-row items-center gap-1.5 h-8 px-3 rounded-full border ${ style={({ pressed }) => ({
active opacity: pressed ? 0.7 : 1,
? 'bg-rebreak-500 border-rebreak-500' flexDirection: 'row',
: 'bg-white border-neutral-200' alignItems: 'center',
}`} gap: 6,
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} height: 32,
paddingHorizontal: 12,
borderRadius: 999,
borderWidth: 1,
backgroundColor: active ? colors.brandOrange : colors.surface,
borderColor: active ? colors.brandOrange : colors.border,
})}
> >
<Ionicons <Ionicons
name={f.icon} name={f.icon}
size={13} size={13}
color={active ? '#fff' : '#737373'} color={active ? '#fff' : colors.textMuted}
/> />
<Text <Text
className={`text-xs ${ style={{
active ? 'text-white' : 'text-neutral-500' fontSize: 12,
}`} fontFamily: 'Nunito_600SemiBold',
style={{ fontFamily: 'Nunito_600SemiBold' }} color: active ? '#fff' : colors.textMuted,
}}
> >
{f.label} {f.label}
</Text> </Text>
@ -178,9 +186,9 @@ export default function HomeScreen() {
} }
ListEmptyComponent={ ListEmptyComponent={
isLoading ? null : ( isLoading ? null : (
<View className="items-center py-16"> <View style={{ alignItems: 'center', paddingVertical: 64 }}>
<Ionicons name="chatbubbles-outline" size={40} color="#d4d4d4" /> <Ionicons name="chatbubbles-outline" size={40} color={colors.border} />
<Text className="text-sm text-neutral-400 mt-3 text-center" style={{ fontFamily: 'Nunito_400Regular' }}> <Text style={{ fontSize: 14, color: colors.textMuted, marginTop: 12, textAlign: 'center', fontFamily: 'Nunito_400Regular' }}>
{t('community.no_posts')} {t('community.no_posts')}
</Text> </Text>
</View> </View>

View File

@ -16,6 +16,7 @@ import {
} from '@expo-google-fonts/nunito'; } from '@expo-google-fonts/nunito';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
import { useThemeStore } from '../stores/theme'; import { useThemeStore } from '../stores/theme';
import { useColors } from '../lib/theme';
import { useLanguageStore } from '../stores/language'; import { useLanguageStore } from '../stores/language';
import { BrandSplash } from '../components/BrandSplash'; import { BrandSplash } from '../components/BrandSplash';
import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet'; import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet';
@ -45,7 +46,9 @@ const queryClient = new QueryClient({
function RootLayoutInner() { function RootLayoutInner() {
const { loading, init } = useAuthStore(); const { loading, init } = useAuthStore();
const initTheme = useThemeStore((s) => s.init); const initTheme = useThemeStore((s) => s.init);
const colorScheme = useThemeStore((s) => s.colorScheme);
const initLanguage = useLanguageStore((s) => s.init); const initLanguage = useLanguageStore((s) => s.init);
const colors = useColors();
const [fontsLoaded] = useFonts({ const [fontsLoaded] = useFonts({
Nunito_400Regular, Nunito_400Regular,
Nunito_600SemiBold, Nunito_600SemiBold,
@ -71,13 +74,13 @@ function RootLayoutInner() {
return ( return (
<> <>
<StatusBar style="dark" /> <StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
<DeviceLimitReachedSheet /> <DeviceLimitReachedSheet />
<Stack <Stack
screenOptions={{ screenOptions={{
headerShown: false, headerShown: false,
animation: 'slide_from_right', animation: 'slide_from_right',
contentStyle: { backgroundColor: '#ffffff' }, contentStyle: { backgroundColor: colors.bg },
}} }}
> >
<Stack.Screen name="index" /> <Stack.Screen name="index" />

View File

@ -11,7 +11,7 @@ import { useEffect } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
import { useDevicesStore, type UserDevice } from '../stores/devices'; import { useDevicesStore, type UserDevice } from '../stores/devices';
import { AppHeader } from '../components/AppHeader'; import { AppHeader } from '../components/AppHeader';
@ -55,6 +55,7 @@ function DeviceRow({
onRemove: (id: string) => void; onRemove: (id: string) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
function confirmRemove() { function confirmRemove() {
Alert.alert( Alert.alert(
@ -86,7 +87,7 @@ function DeviceRow({
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 12, borderRadius: 12,
backgroundColor: 'rgba(0,0,0,0.04)', backgroundColor: colors.surfaceElevated,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
@ -199,6 +200,7 @@ function DeviceRow({
export default function DevicesScreen() { export default function DevicesScreen() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const { devices, maxDevices, plan, loading, loadDevices, removeDevice } = const { devices, maxDevices, plan, loading, loadDevices, removeDevice } =
useDevicesStore(); useDevicesStore();
@ -210,7 +212,7 @@ export default function DevicesScreen() {
const fillRatio = Math.min(1, devices.length / Math.max(1, maxDevices)); const fillRatio = Math.min(1, devices.length / Math.max(1, maxDevices));
return ( return (
<View style={{ flex: 1, backgroundColor: '#fafafa' }}> <View style={{ flex: 1, backgroundColor: colors.bg }}>
<AppHeader showBack title={t('settings.devices_page_title')} /> <AppHeader showBack title={t('settings.devices_page_title')} />
<ScrollView <ScrollView
@ -225,7 +227,7 @@ export default function DevicesScreen() {
{/* Slot counter card */} {/* Slot counter card */}
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderRadius: 14, borderRadius: 14,
padding: 16, padding: 16,
marginBottom: 16, marginBottom: 16,
@ -316,7 +318,7 @@ export default function DevicesScreen() {
{/* Device list card */} {/* Device list card */}
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderRadius: 14, borderRadius: 14,
overflow: 'hidden', overflow: 'hidden',
shadowColor: '#000', shadowColor: '#000',
@ -353,7 +355,7 @@ export default function DevicesScreen() {
key={device.id} key={device.id}
style={{ style={{
borderBottomWidth: i < devices.length - 1 ? 1 : 0, borderBottomWidth: i < devices.length - 1 ? 1 : 0,
borderBottomColor: 'rgba(0,0,0,0.04)', borderBottomColor: colors.border,
}} }}
> >
<DeviceRow device={device} onRemove={removeDevice} /> <DeviceRow device={device} onRemove={removeDevice} />

View File

@ -31,7 +31,8 @@ import { RiveAvatar, type Emotion } from '../components/RiveAvatar';
import { useCoachStore, type Message } from '../stores/coach'; import { useCoachStore, type Message } from '../stores/coach';
import { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
import { supabase } from '../lib/supabase'; import { supabase } from '../lib/supabase';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
import { useThemeStore } from '../stores/theme';
const EMPATHY_RE = /schwer|rückfall|traurig|schlimm|hoffnungslos|hilf|verloren|scham|schuld|verzweifelt/i; const EMPATHY_RE = /schwer|rückfall|traurig|schlimm|hoffnungslos|hilf|verloren|scham|schuld|verzweifelt/i;
const HAPPY_RE = /toll|super|geschafft|stark|glückwunsch|stolz|fantastisch|weiter so|prima|gut gemacht/i; const HAPPY_RE = /toll|super|geschafft|stark|glückwunsch|stolz|fantastisch|weiter so|prima|gut gemacht/i;
@ -56,6 +57,7 @@ function formatTimestamp(date: Date): string {
// Standard-Spinner — kein zweiter Rive-Avatar (der ist bereits im topBar oben). // Standard-Spinner — kein zweiter Rive-Avatar (der ist bereits im topBar oben).
function LoadingPulse() { function LoadingPulse() {
const colors = useColors();
return ( return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 }}> <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 }}>
<ActivityIndicator size="large" color={colors.textMuted} /> <ActivityIndicator size="large" color={colors.textMuted} />
@ -66,6 +68,7 @@ function LoadingPulse() {
// ── Thinking dots ───────────────────────────────────────────────────────────── // ── Thinking dots ─────────────────────────────────────────────────────────────
function ThinkingDots() { function ThinkingDots() {
const colors = useColors();
const anim = useRef([new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)]).current; const anim = useRef([new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)]).current;
useEffect(() => { useEffect(() => {
@ -89,7 +92,7 @@ function ThinkingDots() {
key={i} key={i}
style={[ style={[
styles.thinkingDot, styles.thinkingDot,
{ transform: [{ translateY: a.interpolate({ inputRange: [0, 1], outputRange: [0, -5] }) }] }, { backgroundColor: colors.border, transform: [{ translateY: a.interpolate({ inputRange: [0, 1], outputRange: [0, -5] }) }] },
]} ]}
/> />
))} ))}
@ -138,13 +141,14 @@ function MessageRow({
item: MessageWithMeta; item: MessageWithMeta;
t: (key: string) => string; t: (key: string) => string;
}) { }) {
const colors = useColors();
const isUser = item.role === 'user'; const isUser = item.role === 'user';
return ( return (
<View style={[styles.msgRow, isUser ? styles.msgRowUser : styles.msgRowAssistant]}> <View style={[styles.msgRow, isUser ? styles.msgRowUser : styles.msgRowAssistant]}>
<View style={[styles.bubbleCol, isUser ? styles.bubbleColUser : styles.bubbleColAssistant]}> <View style={[styles.bubbleCol, isUser ? styles.bubbleColUser : styles.bubbleColAssistant]}>
<View style={[styles.bubble, isUser ? styles.bubbleUser : styles.bubbleAssistant]}> <View style={[styles.bubble, isUser ? styles.bubbleUser : [styles.bubbleAssistant, { backgroundColor: colors.surfaceElevated }]]}>
<Text style={[styles.bubbleText, isUser ? styles.bubbleTextUser : styles.bubbleTextAssistant]}> <Text style={[styles.bubbleText, isUser ? styles.bubbleTextUser : [styles.bubbleTextAssistant, { color: colors.text }]]}>
{item.content} {item.content}
</Text> </Text>
</View> </View>
@ -152,11 +156,11 @@ function MessageRow({
<View style={[styles.metaRow, isUser ? styles.metaRowUser : styles.metaRowAssistant]}> <View style={[styles.metaRow, isUser ? styles.metaRowUser : styles.metaRowAssistant]}>
{item.feedbackSaved && ( {item.feedbackSaved && (
<> <>
<Ionicons name="checkmark-circle" size={11} color="#16a34a" /> <Ionicons name="checkmark-circle" size={11} color={colors.success} />
<Text style={styles.feedbackText}>{t('coach.feedback_saved')}</Text> <Text style={[styles.feedbackText, { color: colors.success }]}>{t('coach.feedback_saved')}</Text>
</> </>
)} )}
<Text style={styles.timestampText}>{formatTimestamp(item.timestamp)}</Text> <Text style={[styles.timestampText, { color: colors.textMuted }]}>{formatTimestamp(item.timestamp)}</Text>
</View> </View>
</View> </View>
</View> </View>
@ -170,6 +174,8 @@ export default function CoachScreen() {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const flatRef = useRef<FlatList>(null); const flatRef = useRef<FlatList>(null);
const colors = useColors();
const colorScheme = useThemeStore((s) => s.colorScheme);
// Reaktive Slices — nur was UI-relevant ist (Re-Render bei diesen). // Reaktive Slices — nur was UI-relevant ist (Re-Render bei diesen).
const messages = useCoachStore((s) => s.messages); const messages = useCoachStore((s) => s.messages);
@ -539,17 +545,17 @@ export default function CoachScreen() {
); );
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={[styles.container, { backgroundColor: colors.bg }]} edges={['top']}>
{/* Backdrop hinter topBar — verhindert dass Chat-Texte hinter dem Avatar */} {/* Backdrop hinter topBar — verhindert dass Chat-Texte hinter dem Avatar */}
{/* sichtbar werden ("kollidieren mit Lyra schriftzug"). */} {/* sichtbar werden ("kollidieren mit Lyra schriftzug"). */}
<View <View
style={[styles.topBarBackdrop, { height: insets.top + 170 }]} style={[styles.topBarBackdrop, { height: insets.top + 170, backgroundColor: colors.bg }]}
pointerEvents="none" pointerEvents="none"
/> />
{/* Floating header — no bar, avatar + 2 icon buttons hover over chat */} {/* Floating header — no bar, avatar + 2 icon buttons hover over chat */}
<View style={[styles.topBar, { top: insets.top + 6 }]}> <View style={[styles.topBar, { top: insets.top + 6 }]}>
<Pressable style={styles.backBtn} onPress={() => router.replace('/(app)' as never)} hitSlop={12}> <Pressable style={[styles.backBtn, { backgroundColor: colorScheme === 'dark' ? 'rgba(44,44,46,0.92)' : 'rgba(255,255,255,0.92)' }]} onPress={() => router.replace('/(app)' as never)} hitSlop={12}>
<Ionicons name="chevron-back" size={24} color={colors.text} /> <Ionicons name="chevron-back" size={24} color={colors.text} />
</Pressable> </Pressable>
@ -558,12 +564,12 @@ export default function CoachScreen() {
<RiveAvatar emotion={emotion} size="md" /> <RiveAvatar emotion={emotion} size="md" />
</View> </View>
<View style={styles.avatarMeta}> <View style={styles.avatarMeta}>
<Text style={styles.avatarName}>{t('coach.lyra')}</Text> <Text style={[styles.avatarName, { color: colors.text }]}>{t('coach.lyra')}</Text>
{isSpeaking && ( {isSpeaking && (
<View style={styles.speakingRow}> <View style={styles.speakingRow}>
<VoiceBars count={5} baseColor={colors.brandOrange} /> <VoiceBars count={5} baseColor={colors.brandOrange} />
<Text style={[styles.speakingLabel, { color: colors.brandOrange }]}>{t('coach.speaking')}</Text> <Text style={[styles.speakingLabel, { color: colors.brandOrange }]}>{t('coach.speaking')}</Text>
<Pressable style={styles.stopBtn} onPress={stopSpeaking} hitSlop={6}> <Pressable style={[styles.stopBtn, { backgroundColor: colors.surfaceElevated }]} onPress={stopSpeaking} hitSlop={6}>
<Ionicons name="square" size={10} color={colors.brandOrange} /> <Ionicons name="square" size={10} color={colors.brandOrange} />
</Pressable> </Pressable>
</View> </View>
@ -571,7 +577,7 @@ export default function CoachScreen() {
</View> </View>
</View> </View>
<Pressable style={styles.newChatBtn} onPress={handleNewChat} hitSlop={12}> <Pressable style={[styles.newChatBtn, { backgroundColor: colorScheme === 'dark' ? 'rgba(44,44,46,0.92)' : 'rgba(255,255,255,0.92)' }]} onPress={handleNewChat} hitSlop={12}>
<Ionicons name="refresh-outline" size={22} color={colors.textMuted} /> <Ionicons name="refresh-outline" size={22} color={colors.textMuted} />
</Pressable> </Pressable>
</View> </View>
@ -603,7 +609,7 @@ export default function CoachScreen() {
ListFooterComponent={ ListFooterComponent={
thinking ? ( thinking ? (
<View style={styles.msgRowAssistant}> <View style={styles.msgRowAssistant}>
<View style={styles.bubbleAssistant}> <View style={[styles.bubbleAssistant, { backgroundColor: colors.surfaceElevated }]}>
<ThinkingDots /> <ThinkingDots />
</View> </View>
</View> </View>
@ -621,7 +627,7 @@ export default function CoachScreen() {
)} )}
{/* Input bar */} {/* Input bar */}
<View style={[styles.inputBar, { paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom) }]}> <View style={[styles.inputBar, { paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom), backgroundColor: colors.bg, borderTopColor: colors.border }]}>
{isRecording ? ( {isRecording ? (
<View style={styles.recordingContainer}> <View style={styles.recordingContainer}>
<Pressable style={styles.cancelBtn} onPress={cancelRecording}> <Pressable style={styles.cancelBtn} onPress={cancelRecording}>
@ -636,13 +642,13 @@ export default function CoachScreen() {
) : isTranscribing ? ( ) : isTranscribing ? (
<View style={styles.transcribingRow}> <View style={styles.transcribingRow}>
<Ionicons name="sync" size={16} color={colors.textMuted} /> <Ionicons name="sync" size={16} color={colors.textMuted} />
<Text style={styles.transcribingText}>{t('coach.transcribing')}</Text> <Text style={[styles.transcribingText, { color: colors.textMuted }]}>{t('coach.transcribing')}</Text>
</View> </View>
) : ( ) : (
<TextInput <TextInput
style={styles.textInput} style={[styles.textInput, { backgroundColor: colors.surfaceElevated, color: colors.text }]}
placeholder={t('coach.placeholder')} placeholder={t('coach.placeholder')}
placeholderTextColor="#a3a3a3" placeholderTextColor={colors.textMuted}
value={input} value={input}
onChangeText={handleInputChange} onChangeText={handleInputChange}
multiline multiline
@ -654,7 +660,7 @@ export default function CoachScreen() {
{!isTranscribing && ( {!isTranscribing && (
<Pressable <Pressable
style={[styles.micBtn, isRecording && styles.micBtnActive, thinking && styles.micBtnDisabled]} style={[styles.micBtn, { backgroundColor: colors.surfaceElevated }, isRecording && styles.micBtnActive, thinking && styles.micBtnDisabled]}
onPressIn={onMicDown} onPressIn={onMicDown}
onPressOut={onMicUp} onPressOut={onMicUp}
disabled={thinking} disabled={thinking}
@ -741,7 +747,7 @@ const styles = StyleSheet.create({
statusLabel: { statusLabel: {
fontSize: 11, fontSize: 11,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: colors.textMuted, color: '#737373',
}, },
speakingRow: { speakingRow: {
flexDirection: 'row', flexDirection: 'row',

View File

@ -13,7 +13,7 @@ import { Ionicons } from '@expo/vector-icons';
import { MenuView, type MenuAction } from '@react-native-menu/menu'; import { MenuView, type MenuAction } from '@react-native-menu/menu';
import { TrueSheet } from '@lodev09/react-native-true-sheet'; import { TrueSheet } from '@lodev09/react-native-true-sheet';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
import { useThemeStore, type ThemeMode } from '../stores/theme'; import { useThemeStore, type ThemeMode } from '../stores/theme';
import { useLanguageStore, type AppLanguage } from '../stores/language'; import { useLanguageStore, type AppLanguage } from '../stores/language';
@ -54,6 +54,7 @@ export default function SettingsScreen() {
const { mode: themeMode, setMode: setThemeMode } = useThemeStore(); const { mode: themeMode, setMode: setThemeMode } = useThemeStore();
const { language, setLanguage } = useLanguageStore(); const { language, setLanguage } = useLanguageStore();
const { plan } = useUserPlan(); const { plan } = useUserPlan();
const colors = useColors();
// Lyra Voice: hardcoded ElevenLabs voice IDs (expandable by user later) // Lyra Voice: hardcoded ElevenLabs voice IDs (expandable by user later)
// Backend endpoint PATCH /api/profile/me/demographics does NOT accept lyraVoiceId. // Backend endpoint PATCH /api/profile/me/demographics does NOT accept lyraVoiceId.
@ -246,7 +247,7 @@ export default function SettingsScreen() {
} }
return ( return (
<View style={{ flex: 1, backgroundColor: '#fafafa' }}> <View style={{ flex: 1, backgroundColor: colors.bg }}>
<AppHeader showBack title={t('settings.title')} /> <AppHeader showBack title={t('settings.title')} />
<ScrollView <ScrollView
@ -263,7 +264,7 @@ export default function SettingsScreen() {
<Text <Text
style={{ style={{
fontSize: 11, fontSize: 11,
color: '#a3a3a3', color: colors.textMuted,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 1, letterSpacing: 1,
@ -275,7 +276,7 @@ export default function SettingsScreen() {
</Text> </Text>
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderRadius: 14, borderRadius: 14,
overflow: 'hidden', overflow: 'hidden',
shadowColor: '#000', shadowColor: '#000',
@ -330,7 +331,7 @@ export default function SettingsScreen() {
paddingVertical: 12, paddingVertical: 12,
minHeight: 56, minHeight: 56,
borderBottomWidth: i < section.rows.length - 1 ? 1 : 0, borderBottomWidth: i < section.rows.length - 1 ? 1 : 0,
borderBottomColor: 'rgba(0,0,0,0.04)', borderBottomColor: colors.border,
opacity: row.soon ? 0.5 : 1, opacity: row.soon ? 0.5 : 1,
}; };
@ -359,6 +360,7 @@ export default function SettingsScreen() {
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 6, paddingVertical: 6,
borderRadius: 8, borderRadius: 8,
backgroundColor: colors.surfaceElevated,
}} }}
> >
{row.value ? ( {row.value ? (
@ -401,7 +403,7 @@ export default function SettingsScreen() {
<Text <Text
style={{ style={{
fontSize: 10, fontSize: 10,
color: '#a3a3a3', color: colors.textMuted,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
@ -425,7 +427,7 @@ export default function SettingsScreen() {
<Ionicons <Ionicons
name="chevron-forward" name="chevron-forward"
size={16} size={16}
color="#d4d4d8" color={colors.border}
/> />
)} )}
</View> </View>
@ -440,7 +442,7 @@ export default function SettingsScreen() {
style={{ style={{
textAlign: 'center', textAlign: 'center',
fontSize: 11, fontSize: 11,
color: '#a3a3a3', color: colors.textMuted,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
marginTop: 6, marginTop: 6,
opacity: 0.7, opacity: 0.7,
@ -452,7 +454,7 @@ export default function SettingsScreen() {
style={{ style={{
textAlign: 'center', textAlign: 'center',
fontSize: 10, fontSize: 10,
color: '#a3a3a3', color: colors.textMuted,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
marginTop: 4, marginTop: 4,
opacity: 0.5, opacity: 0.5,
@ -467,8 +469,9 @@ export default function SettingsScreen() {
detents={['auto', 1]} detents={['auto', 1]}
cornerRadius={20} cornerRadius={20}
grabber grabber
backgroundColor={colors.surface}
> >
<View style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 24 }}> <View style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 24, backgroundColor: colors.surface }}>
<Text <Text
style={{ style={{
fontSize: 22, fontSize: 22,
@ -508,7 +511,7 @@ export default function SettingsScreen() {
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: 16, paddingVertical: 16,
borderBottomWidth: idx < voiceOptions.length - 1 ? 1 : 0, borderBottomWidth: idx < voiceOptions.length - 1 ? 1 : 0,
borderBottomColor: 'rgba(0,0,0,0.06)', borderBottomColor: colors.border,
}} }}
> >
<Text <Text

View File

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
import { useNotificationStore } from '../stores/notifications'; import { useNotificationStore } from '../stores/notifications';
import { resolveAvatar } from '../lib/resolveAvatar'; import { resolveAvatar } from '../lib/resolveAvatar';
import { useColors } from '../lib/theme';
import { useMe } from '../hooks/useMe'; import { useMe } from '../hooks/useMe';
import { NotificationsDropdown } from './NotificationsDropdown'; import { NotificationsDropdown } from './NotificationsDropdown';
import { HeaderDropdownMenu } from './header/HeaderDropdownMenu'; import { HeaderDropdownMenu } from './header/HeaderDropdownMenu';
@ -22,6 +23,7 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { user } = useAuthStore(); const { user } = useAuthStore();
const colors = useColors();
const { me } = useMe(); const { me } = useMe();
const storeUnread = useNotificationStore((s) => s.unread); const storeUnread = useNotificationStore((s) => s.unread);
const badge = notifCount ?? storeUnread; const badge = notifCount ?? storeUnread;
@ -47,8 +49,12 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
return ( return (
<View <View
className="bg-white border-b border-neutral-200" style={{
style={{ paddingTop: insets.top }} paddingTop: insets.top,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: colors.border,
}}
> >
<View className="h-14 flex-row items-center justify-between px-5"> <View className="h-14 flex-row items-center justify-between px-5">
<View className="flex-row items-center" style={{ gap: 8 }}> <View className="flex-row items-center" style={{ gap: 8 }}>
@ -56,14 +62,21 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
<Pressable <Pressable
onPress={() => router.back()} onPress={() => router.back()}
hitSlop={10} hitSlop={10}
className="w-9 h-9 rounded-full items-center justify-center" style={({ pressed }) => ({
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1, marginLeft: -8 })} opacity: pressed ? 0.6 : 1,
marginLeft: -8,
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
})}
accessibilityLabel="Zurück" accessibilityLabel="Zurück"
> >
<Ionicons name="chevron-back" size={22} color="#0a0a0a" /> <Ionicons name="chevron-back" size={22} color={colors.text} />
</Pressable> </Pressable>
) : null} ) : null}
<Text className="text-lg text-midnight-900 tracking-tight" style={{ fontFamily: 'Nunito_700Bold' }}> <Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 18, color: colors.text, letterSpacing: -0.3 }}>
{title ?? t('appHeader.appName')} {title ?? t('appHeader.appName')}
</Text> </Text>
</View> </View>
@ -72,10 +85,17 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
<Pressable <Pressable
onPress={() => setNotifOpen(true)} onPress={() => setNotifOpen(true)}
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }} hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
className="w-9 h-9 rounded-full bg-white items-center justify-center" style={({ pressed }) => ({
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} opacity: pressed ? 0.7 : 1,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.surface,
alignItems: 'center',
justifyContent: 'center',
})}
> >
<Ionicons name="notifications-outline" size={22} color="#737373" /> <Ionicons name="notifications-outline" size={22} color={colors.textMuted} />
{badge > 0 && ( {badge > 0 && (
<View className="absolute top-0 right-0 w-4 h-4 rounded-full bg-rebreak-500 items-center justify-center"> <View className="absolute top-0 right-0 w-4 h-4 rounded-full bg-rebreak-500 items-center justify-center">
<Text className="text-white text-[9px]" style={{ fontFamily: 'Nunito_700Bold' }}> <Text className="text-white text-[9px]" style={{ fontFamily: 'Nunito_700Bold' }}>
@ -89,8 +109,16 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
<Pressable <Pressable
onPress={() => setMenuOpen(true)} onPress={() => setMenuOpen(true)}
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }} hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
className={`w-9 h-9 rounded-full items-center justify-center overflow-hidden ${showAvatarImage ? 'bg-neutral-100' : 'bg-rebreak-500'}`} style={({ pressed }) => ({
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} opacity: pressed ? 0.7 : 1,
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
backgroundColor: showAvatarImage ? colors.surfaceElevated : colors.brandOrange,
})}
> >
{showAvatarImage ? ( {showAvatarImage ? (
<Image <Image

View File

@ -3,6 +3,7 @@ import { useRouter, type RelativePathString } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../../stores/auth'; import { useAuthStore } from '../../stores/auth';
import { useColors } from '../../lib/theme';
// Controlled-Modal-Pattern. Trigger ist NICHT in dieser Komponente — der // Controlled-Modal-Pattern. Trigger ist NICHT in dieser Komponente — der
// Avatar im AppHeader öffnet das Modal via `visible`-Prop (User-Anweisung // Avatar im AppHeader öffnet das Modal via `visible`-Prop (User-Anweisung
@ -33,6 +34,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { signOut } = useAuthStore(); const { signOut } = useAuthStore();
const colors = useColors();
function nav(path: RelativePathString) { function nav(path: RelativePathString) {
onClose(); onClose();
@ -93,7 +95,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
position: 'absolute', position: 'absolute',
top: topOffset, top: topOffset,
right: 12, right: 12,
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderRadius: 18, borderRadius: 18,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 8 }, shadowOffset: { width: 0, height: 8 },
@ -148,7 +150,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#a3a3a3', color: colors.textMuted,
marginTop: 1, marginTop: 1,
}} }}
numberOfLines={1} numberOfLines={1}
@ -156,11 +158,11 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
{t('appHeader.sosTagline')} {t('appHeader.sosTagline')}
</Text> </Text>
</View> </View>
<Ionicons name="chevron-forward" size={16} color="#d4d4d8" /> <Ionicons name="chevron-forward" size={16} color={colors.border} />
</View> </View>
</Pressable> </Pressable>
<View style={{ height: 1, backgroundColor: '#f0f0f0' }} /> <View style={{ height: 1, backgroundColor: colors.border }} />
{/* Profile · Settings · Games · [Debug DEV] */} {/* Profile · Settings · Games · [Debug DEV] */}
{items.map((item) => ( {items.map((item) => (
@ -170,7 +172,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
onClose(); onClose();
void item.onSelect(); void item.onSelect();
}} }}
android_ripple={{ color: '#e5e7eb' }} android_ripple={{ color: colors.surfaceElevated }}
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
> >
<View <View
@ -184,14 +186,14 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
<Ionicons <Ionicons
name={item.icon} name={item.icon}
size={18} size={18}
color="#737373" color={colors.textMuted}
style={{ marginRight: 14 }} style={{ marginRight: 14 }}
/> />
<Text <Text
style={{ style={{
fontSize: 14, fontSize: 14,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#0a0a0a', color: colors.text,
}} }}
> >
{item.label} {item.label}
@ -200,12 +202,12 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
</Pressable> </Pressable>
))} ))}
<View style={{ height: 1, backgroundColor: '#f0f0f0' }} /> <View style={{ height: 1, backgroundColor: colors.border }} />
{/* Abmelden — neutral, nicht rot */} {/* Abmelden — neutral, nicht rot */}
<Pressable <Pressable
onPress={handleLogout} onPress={handleLogout}
android_ripple={{ color: '#e5e7eb' }} android_ripple={{ color: colors.surfaceElevated }}
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
> >
<View <View
@ -219,14 +221,14 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
<Ionicons <Ionicons
name="log-out-outline" name="log-out-outline"
size={18} size={18}
color="#737373" color={colors.textMuted}
style={{ marginRight: 14 }} style={{ marginRight: 14 }}
/> />
<Text <Text
style={{ style={{
fontSize: 14, fontSize: 14,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#0a0a0a', color: colors.text,
}} }}
> >
{t('headerMenu.logout')} {t('headerMenu.logout')}

View File

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { View, Text, Pressable, LayoutAnimation, Platform, UIManager } from 'react-native'; import { View, Text, Pressable, LayoutAnimation, Platform, UIManager } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true); UIManager.setLayoutAnimationEnabledExperimental(true);
@ -18,6 +18,7 @@ type Props = {
}; };
export function ApprovedDomainsList({ domains, loading }: Props) { export function ApprovedDomainsList({ domains, loading }: Props) {
const colors = useColors();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
function toggle() { function toggle() {
@ -36,9 +37,9 @@ export function ApprovedDomainsList({ domains, loading }: Props) {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 16, padding: 16,
}} }}
@ -63,9 +64,9 @@ export function ApprovedDomainsList({ domains, loading }: Props) {
<View <View
style={{ style={{
marginTop: 6, marginTop: 6,
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 12, borderRadius: 12,
paddingVertical: 4, paddingVertical: 4,
}} }}
@ -99,7 +100,7 @@ export function ApprovedDomainsList({ domains, loading }: Props) {
paddingVertical: 10, paddingVertical: 10,
paddingHorizontal: 14, paddingHorizontal: 14,
borderTopWidth: idx === 0 ? 0 : 1, borderTopWidth: idx === 0 ? 0 : 1,
borderTopColor: 'rgba(0,0,0,0.06)', borderTopColor: colors.border,
}} }}
> >
<Text style={{ fontSize: 13, color: colors.text, fontFamily: 'Nunito_600SemiBold' }}> <Text style={{ fontSize: 13, color: colors.text, fontFamily: 'Nunito_600SemiBold' }}>
@ -120,11 +121,12 @@ export function ApprovedDomainsList({ domains, loading }: Props) {
} }
function SkeletonRow() { function SkeletonRow() {
const colors = useColors();
return ( return (
<View <View
style={{ style={{
height: 14, height: 14,
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 4, borderRadius: 4,
marginVertical: 6, marginVertical: 6,
}} }}

View File

@ -12,7 +12,7 @@ import { Ionicons } from '@expo/vector-icons';
import { MenuView } from '@react-native-menu/menu'; import { MenuView } from '@react-native-menu/menu';
import { getCitiesForBundesland } from '../../lib/germanCities'; import { getCitiesForBundesland } from '../../lib/germanCities';
import { WheelPickerModal } from '../WheelPickerModal'; import { WheelPickerModal } from '../WheelPickerModal';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
import type { Plan } from '../../hooks/useUserPlan'; import type { Plan } from '../../hooks/useUserPlan';
// Geburtsjahr-Optionen: 2010 (oldest 13y) → 1920, descending (neueste oben) // Geburtsjahr-Optionen: 2010 (oldest 13y) → 1920, descending (neueste oben)
@ -203,6 +203,7 @@ export function DemographicsAccordion({
setLocal(next); setLocal(next);
} }
const colors = useColors();
const { filled, total } = relevantFieldCount(local); const { filled, total } = relevantFieldCount(local);
const completed = filled === total; const completed = filled === total;
const showProTrialBanner = plan === 'free'; const showProTrialBanner = plan === 'free';
@ -220,9 +221,9 @@ export function DemographicsAccordion({
> >
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 16, padding: 16,
}} }}
@ -282,7 +283,7 @@ export function DemographicsAccordion({
style={{ style={{
flex: 1, flex: 1,
height: 5, height: 5,
backgroundColor: '#e5e5e5', backgroundColor: colors.border,
borderRadius: 999, borderRadius: 999,
overflow: 'hidden', overflow: 'hidden',
}} }}
@ -316,9 +317,9 @@ export function DemographicsAccordion({
<View <View
style={{ style={{
marginTop: 8, marginTop: 8,
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
paddingVertical: 4, paddingVertical: 4,
}} }}
@ -516,7 +517,7 @@ export function DemographicsAccordion({
<Switch <Switch
value={local.shiftWork === true} value={local.shiftWork === true}
onValueChange={(v) => flushSave({ ...local, shiftWork: v })} onValueChange={(v) => flushSave({ ...local, shiftWork: v })}
trackColor={{ false: '#e5e5e5', true: colors.brandOrange }} trackColor={{ false: colors.border, true: colors.brandOrange }}
thumbColor="#ffffff" thumbColor="#ffffff"
/> />
</View> </View>
@ -677,19 +678,6 @@ export function DemographicsAccordion({
); );
} }
const inputStyle = {
fontSize: 14,
color: colors.text,
fontFamily: 'Nunito_600SemiBold',
paddingVertical: 8,
paddingHorizontal: 10,
backgroundColor: '#fafafa',
borderRadius: 8,
borderWidth: 1,
borderColor: '#ececec',
minWidth: 140,
textAlign: 'right' as const,
};
function FieldRow({ function FieldRow({
label, label,
@ -708,13 +696,14 @@ function FieldRow({
filled: boolean; filled: boolean;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const colors = useColors();
return ( return (
<View <View
style={{ style={{
paddingHorizontal: indent ? 20 : 14, paddingHorizontal: indent ? 20 : 14,
paddingVertical: 12, paddingVertical: 12,
borderBottomWidth: isLast ? 0 : 1, borderBottomWidth: isLast ? 0 : 1,
borderBottomColor: 'rgba(0,0,0,0.06)', borderBottomColor: colors.border,
}} }}
> >
<View <View
@ -762,6 +751,7 @@ function FieldRow({
} }
function SelectButton({ value, onPress }: { value: string | null; onPress: () => void }) { function SelectButton({ value, onPress }: { value: string | null; onPress: () => void }) {
const colors = useColors();
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
@ -773,10 +763,10 @@ function SelectButton({ value, onPress }: { value: string | null; onPress: () =>
style={{ style={{
paddingVertical: 6, paddingVertical: 6,
paddingHorizontal: 12, paddingHorizontal: 12,
backgroundColor: '#f4f4f5', backgroundColor: colors.surfaceElevated,
borderRadius: 999, borderRadius: 999,
borderWidth: 1, borderWidth: 1,
borderColor: '#e4e4e7', borderColor: colors.border,
}} }}
> >
<Text <Text

View File

@ -2,7 +2,7 @@ import { useState } from 'react';
import { View, Text, Pressable, Image } from 'react-native'; import { View, Text, Pressable, Image } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import Svg, { Path } from 'react-native-svg'; import Svg, { Path } from 'react-native-svg';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
import { resolveAvatar } from '../../lib/resolveAvatar'; import { resolveAvatar } from '../../lib/resolveAvatar';
import type { Plan } from '../../hooks/useUserPlan'; import type { Plan } from '../../hooks/useUserPlan';
@ -67,6 +67,7 @@ export function ProfileHeader({
onEditNickname, onEditNickname,
onDemographicsHintPress, onDemographicsHintPress,
}: Props) { }: Props) {
const colors = useColors();
const [imageFailed, setImageFailed] = useState(false); const [imageFailed, setImageFailed] = useState(false);
const avatarUrl = resolveAvatar(avatar, nickname); const avatarUrl = resolveAvatar(avatar, nickname);
const initials = nickname.slice(0, 2).toUpperCase(); const initials = nickname.slice(0, 2).toUpperCase();
@ -100,7 +101,7 @@ export function ProfileHeader({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
backgroundColor: showImage ? '#fafafa' : colors.brandOrange, backgroundColor: showImage ? colors.surfaceElevated : colors.brandOrange,
}} }}
> >
{showImage ? ( {showImage ? (
@ -126,9 +127,9 @@ export function ProfileHeader({
width: 30, width: 30,
height: 30, height: 30,
borderRadius: 15, borderRadius: 15,
backgroundColor: '#0a0a0a', backgroundColor: colors.text,
borderWidth: 2, borderWidth: 2,
borderColor: '#ffffff', borderColor: colors.bg,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
@ -212,9 +213,9 @@ export function ProfileHeader({
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 4, paddingVertical: 4,
borderRadius: 999, borderRadius: 999,
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
}} }}
> >
{provider === 'google' ? <GoogleIcon /> : <AppleIcon />} {provider === 'google' ? <GoogleIcon /> : <AppleIcon />}

View File

@ -1,5 +1,5 @@
import { View, Text, Pressable } from 'react-native'; import { View, Text, Pressable } from 'react-native';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
type Props = { type Props = {
postsCount: number; postsCount: number;
@ -17,6 +17,7 @@ type CardProps = {
}; };
function StatPill({ value, label, onPress }: CardProps) { function StatPill({ value, label, onPress }: CardProps) {
const colors = useColors();
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
@ -24,9 +25,9 @@ function StatPill({ value, label, onPress }: CardProps) {
> >
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 999, borderRadius: 999,
paddingVertical: 10, paddingVertical: 10,
paddingHorizontal: 18, paddingHorizontal: 18,

View File

@ -1,6 +1,6 @@
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
export type CooldownEntry = { export type CooldownEntry = {
id: string; id: string;
@ -30,6 +30,7 @@ const statusColor: Record<CooldownEntry['status'], { bg: string; text: string }>
}; };
export function StreakSection({ currentDays, longestDays, startDate, cooldowns }: Props) { export function StreakSection({ currentDays, longestDays, startDate, cooldowns }: Props) {
const colors = useColors();
return ( return (
<View style={{ marginHorizontal: 16, marginTop: 24 }}> <View style={{ marginHorizontal: 16, marginTop: 24 }}>
<View <View
@ -55,9 +56,9 @@ export function StreakSection({ currentDays, longestDays, startDate, cooldowns }
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 16, padding: 16,
}} }}
@ -121,9 +122,9 @@ export function StreakSection({ currentDays, longestDays, startDate, cooldowns }
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 14, padding: 14,
}} }}
@ -153,7 +154,7 @@ export function StreakSection({ currentDays, longestDays, startDate, cooldowns }
style={{ style={{
width: 1, width: 1,
flex: 1, flex: 1,
backgroundColor: 'rgba(0,0,0,0.06)', backgroundColor: colors.border,
marginTop: 2, marginTop: 2,
}} }}
/> />

View File

@ -1,6 +1,6 @@
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
export type HelpedByEntry = { export type HelpedByEntry = {
key: 'breathing' | 'game' | 'talk' | 'other'; key: 'breathing' | 'game' | 'talk' | 'other';
@ -16,6 +16,7 @@ type Props = {
}; };
export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Props) { export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Props) {
const colors = useColors();
const overcomePct = sessions > 0 ? Math.round((overcome / sessions) * 100) : 0; const overcomePct = sessions > 0 ? Math.round((overcome / sessions) * 100) : 0;
const totalHelped = helpedBy.reduce((sum, h) => sum + h.count, 0); const totalHelped = helpedBy.reduce((sum, h) => sum + h.count, 0);
@ -44,9 +45,9 @@ export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Prop
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 16, padding: 16,
}} }}
@ -99,7 +100,7 @@ export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Prop
style={{ style={{
marginTop: 8, marginTop: 8,
height: 6, height: 6,
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 999, borderRadius: 999,
overflow: 'hidden', overflow: 'hidden',
}} }}
@ -169,7 +170,7 @@ export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Prop
<View <View
style={{ style={{
height: 4, height: 4,
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 999, borderRadius: 999,
overflow: 'hidden', overflow: 'hidden',
}} }}

View File

@ -1,3 +1,5 @@
import { useThemeStore } from '../stores/theme';
export const theme = { export const theme = {
bg: 'bg-white', bg: 'bg-white',
surface: 'bg-neutral-50', surface: 'bg-neutral-50',
@ -10,17 +12,55 @@ export const theme = {
brandBlue: 'bg-midnight-800', brandBlue: 'bg-midnight-800',
} as const; } as const;
export const colors = { export type ColorScheme = {
bg: string;
surface: string;
surfaceElevated: string;
border: string;
text: string;
textMuted: string;
brandOrange: string;
brandBlue: string;
success: string;
error: string;
warning: string;
};
const light: ColorScheme = {
bg: '#ffffff', bg: '#ffffff',
surface: '#fafafa', surface: '#fafafa',
surfaceElevated: '#f5f5f5', surfaceElevated: '#f5f5f5',
border: '#e5e5e5', border: '#e5e5e5',
text: '#0a0a0a', text: '#0a0a0a',
textMuted: '#737373', textMuted: '#737373',
// TEMP zum Testen: iOS native blue. Wieder auf '#f59e0b' wenn du zur Brand zurück willst.
brandOrange: '#007AFF', brandOrange: '#007AFF',
brandBlue: '#0e1f3a', brandBlue: '#0e1f3a',
success: '#16a34a', success: '#16a34a',
error: '#dc2626', error: '#dc2626',
warning: '#f59e0b', warning: '#f59e0b',
} as const; };
const dark: ColorScheme = {
bg: '#000000',
surface: '#1c1c1e',
surfaceElevated: '#2c2c2e',
border: '#38383a',
text: '#ffffff',
textMuted: '#8e8e93',
brandOrange: '#007AFF',
brandBlue: '#0e1f3a',
success: '#30d158',
error: '#ff453a',
warning: '#ffd60a',
};
export const colorSchemes = { light, dark };
export function useColors(): ColorScheme {
const scheme = useThemeStore((s) => s.colorScheme);
return scheme === 'dark' ? dark : light;
}
// Legacy flat export — used by files that haven't migrated to useColors() yet.
// Wave 2 should remove this.
export const colors = light;

View File

@ -20,7 +20,15 @@ type ThemeState = {
init: () => Promise<void>; init: () => Promise<void>;
}; };
export const useThemeStore = create<ThemeState>((set) => ({ export const useThemeStore = create<ThemeState>((set, get) => {
// Listen for OS-level theme changes and update store when mode === 'system'.
Appearance.addChangeListener(() => {
if (get().mode === 'system') {
set({ colorScheme: resolveColorScheme('system') });
}
});
return {
mode: 'system', mode: 'system',
colorScheme: 'light', colorScheme: 'light',
@ -35,4 +43,5 @@ export const useThemeStore = create<ThemeState>((set) => ({
await AsyncStorage.setItem(STORAGE_KEY, mode); await AsyncStorage.setItem(STORAGE_KEY, mode);
set({ mode, colorScheme: resolveColorScheme(mode) }); set({ mode, colorScheme: resolveColorScheme(mode) });
}, },
})); };
});