diff --git a/apps/rebreak-native/app/(app)/_layout.tsx b/apps/rebreak-native/app/(app)/_layout.tsx index 9f37f27..1bbd283 100644 --- a/apps/rebreak-native/app/(app)/_layout.tsx +++ b/apps/rebreak-native/app/(app)/_layout.tsx @@ -5,7 +5,7 @@ import * as Notifications from 'expo-notifications'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../../stores/auth'; import { useNotificationStore } from '../../stores/notifications'; -import { colors } from '../../lib/theme'; +import { useColors } from '../../lib/theme'; import { NativeTabs } from '../../components/NativeTabs'; import { protection } from '../../lib/protection'; import { preloadTabIcons, getTabIcon } from '../../lib/tabIcons'; @@ -14,6 +14,7 @@ export default function AppLayout() { const router = useRouter(); const { t } = useTranslation(); const { session, loading } = useAuthStore(); + const colors = useColors(); const loadNotifications = useNotificationStore((s) => s.load); const startRealtime = useNotificationStore((s) => s.startRealtime); const stopRealtime = useNotificationStore((s) => s.stopRealtime); @@ -143,7 +144,7 @@ export default function AppLayout() { if (loading || !session) { return ( - + ); diff --git a/apps/rebreak-native/app/(app)/index.tsx b/apps/rebreak-native/app/(app)/index.tsx index ccc9bdc..9b55ecf 100644 --- a/apps/rebreak-native/app/(app)/index.tsx +++ b/apps/rebreak-native/app/(app)/index.tsx @@ -19,7 +19,7 @@ import { PostCardSkeleton } from '../../components/PostCardSkeleton'; import { PostCommentsSheet } from '../../components/PostCommentsSheet'; import { useCommunityStore, type CommunityCategory, type CommunityPost } from '../../stores/community'; import { useCommunityRealtime } from '../../hooks/useCommunityRealtime'; -import { colors } from '../../lib/theme'; +import { useColors } from '../../lib/theme'; type FilterChip = { value: CommunityCategory; @@ -30,6 +30,7 @@ type FilterChip = { export default function HomeScreen() { const { t } = useTranslation(); const queryClient = useQueryClient(); + const colors = useColors(); // Granular selectors: subscribing to the whole store (incl. optimisticLikes) // would re-render the screen — and thus the FlatList — on every like. const activeCategory = useCommunityStore((s) => s.activeCategory); @@ -79,7 +80,7 @@ export default function HomeScreen() { ); return ( - + toggleFilter(f.value)} - className={`flex-row items-center gap-1.5 h-8 px-3 rounded-full border ${ - active - ? 'bg-rebreak-500 border-rebreak-500' - : 'bg-white border-neutral-200' - }`} - style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} + style={({ pressed }) => ({ + opacity: pressed ? 0.7 : 1, + flexDirection: 'row', + alignItems: 'center', + gap: 6, + height: 32, + paddingHorizontal: 12, + borderRadius: 999, + borderWidth: 1, + backgroundColor: active ? colors.brandOrange : colors.surface, + borderColor: active ? colors.brandOrange : colors.border, + })} > {f.label} @@ -178,9 +186,9 @@ export default function HomeScreen() { } ListEmptyComponent={ isLoading ? null : ( - - - + + + {t('community.no_posts')} diff --git a/apps/rebreak-native/app/_layout.tsx b/apps/rebreak-native/app/_layout.tsx index 54f904d..a52d4df 100644 --- a/apps/rebreak-native/app/_layout.tsx +++ b/apps/rebreak-native/app/_layout.tsx @@ -16,6 +16,7 @@ import { } from '@expo-google-fonts/nunito'; import { useAuthStore } from '../stores/auth'; import { useThemeStore } from '../stores/theme'; +import { useColors } from '../lib/theme'; import { useLanguageStore } from '../stores/language'; import { BrandSplash } from '../components/BrandSplash'; import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet'; @@ -45,7 +46,9 @@ const queryClient = new QueryClient({ function RootLayoutInner() { const { loading, init } = useAuthStore(); const initTheme = useThemeStore((s) => s.init); + const colorScheme = useThemeStore((s) => s.colorScheme); const initLanguage = useLanguageStore((s) => s.init); + const colors = useColors(); const [fontsLoaded] = useFonts({ Nunito_400Regular, Nunito_600SemiBold, @@ -71,13 +74,13 @@ function RootLayoutInner() { return ( <> - + diff --git a/apps/rebreak-native/app/devices.tsx b/apps/rebreak-native/app/devices.tsx index d29fbd7..13b65c9 100644 --- a/apps/rebreak-native/app/devices.tsx +++ b/apps/rebreak-native/app/devices.tsx @@ -11,7 +11,7 @@ import { useEffect } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; -import { colors } from '../lib/theme'; +import { useColors } from '../lib/theme'; import { useDevicesStore, type UserDevice } from '../stores/devices'; import { AppHeader } from '../components/AppHeader'; @@ -55,6 +55,7 @@ function DeviceRow({ onRemove: (id: string) => void; }) { const { t } = useTranslation(); + const colors = useColors(); function confirmRemove() { Alert.alert( @@ -86,7 +87,7 @@ function DeviceRow({ width: 40, height: 40, borderRadius: 12, - backgroundColor: 'rgba(0,0,0,0.04)', + backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center', }} @@ -199,6 +200,7 @@ function DeviceRow({ export default function DevicesScreen() { const insets = useSafeAreaInsets(); const { t } = useTranslation(); + const colors = useColors(); const { devices, maxDevices, plan, loading, loadDevices, removeDevice } = useDevicesStore(); @@ -210,7 +212,7 @@ export default function DevicesScreen() { const fillRatio = Math.min(1, devices.length / Math.max(1, maxDevices)); return ( - + diff --git a/apps/rebreak-native/app/lyra.tsx b/apps/rebreak-native/app/lyra.tsx index 4686c44..569a943 100644 --- a/apps/rebreak-native/app/lyra.tsx +++ b/apps/rebreak-native/app/lyra.tsx @@ -31,7 +31,8 @@ import { RiveAvatar, type Emotion } from '../components/RiveAvatar'; import { useCoachStore, type Message } from '../stores/coach'; import { apiFetch } from '../lib/api'; 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 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). function LoadingPulse() { + const colors = useColors(); return ( @@ -66,6 +68,7 @@ function LoadingPulse() { // ── Thinking dots ───────────────────────────────────────────────────────────── function ThinkingDots() { + const colors = useColors(); const anim = useRef([new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)]).current; useEffect(() => { @@ -89,7 +92,7 @@ function ThinkingDots() { key={i} style={[ 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; t: (key: string) => string; }) { + const colors = useColors(); const isUser = item.role === 'user'; return ( - - + + {item.content} @@ -152,11 +156,11 @@ function MessageRow({ {item.feedbackSaved && ( <> - - {t('coach.feedback_saved')} + + {t('coach.feedback_saved')} )} - {formatTimestamp(item.timestamp)} + {formatTimestamp(item.timestamp)} @@ -170,6 +174,8 @@ export default function CoachScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); const flatRef = useRef(null); + const colors = useColors(); + const colorScheme = useThemeStore((s) => s.colorScheme); // Reaktive Slices — nur was UI-relevant ist (Re-Render bei diesen). const messages = useCoachStore((s) => s.messages); @@ -539,17 +545,17 @@ export default function CoachScreen() { ); return ( - + {/* Backdrop hinter topBar — verhindert dass Chat-Texte hinter dem Avatar */} {/* sichtbar werden ("kollidieren mit Lyra schriftzug"). */} {/* Floating header — no bar, avatar + 2 icon buttons hover over chat */} - router.replace('/(app)' as never)} hitSlop={12}> + router.replace('/(app)' as never)} hitSlop={12}> @@ -558,12 +564,12 @@ export default function CoachScreen() { - {t('coach.lyra')} + {t('coach.lyra')} {isSpeaking && ( {t('coach.speaking')} - + @@ -571,7 +577,7 @@ export default function CoachScreen() { - + @@ -603,7 +609,7 @@ export default function CoachScreen() { ListFooterComponent={ thinking ? ( - + @@ -621,7 +627,7 @@ export default function CoachScreen() { )} {/* Input bar */} - 0 ? 8 : Math.max(12, insets.bottom) }]}> + 0 ? 8 : Math.max(12, insets.bottom), backgroundColor: colors.bg, borderTopColor: colors.border }]}> {isRecording ? ( @@ -636,13 +642,13 @@ export default function CoachScreen() { ) : isTranscribing ? ( - {t('coach.transcribing')} + {t('coach.transcribing')} ) : ( + {row.value ? ( @@ -401,7 +403,7 @@ export default function SettingsScreen() { )} @@ -440,7 +442,7 @@ export default function SettingsScreen() { style={{ textAlign: 'center', fontSize: 11, - color: '#a3a3a3', + color: colors.textMuted, fontFamily: 'Nunito_400Regular', marginTop: 6, opacity: 0.7, @@ -452,7 +454,7 @@ export default function SettingsScreen() { style={{ textAlign: 'center', fontSize: 10, - color: '#a3a3a3', + color: colors.textMuted, fontFamily: 'Nunito_400Regular', marginTop: 4, opacity: 0.5, @@ -467,8 +469,9 @@ export default function SettingsScreen() { detents={['auto', 1]} cornerRadius={20} grabber + backgroundColor={colors.surface} > - + s.unread); const badge = notifCount ?? storeUnread; @@ -47,8 +49,12 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) { return ( @@ -56,14 +62,21 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) { router.back()} hitSlop={10} - className="w-9 h-9 rounded-full items-center justify-center" - style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1, marginLeft: -8 })} + style={({ pressed }) => ({ + opacity: pressed ? 0.6 : 1, + marginLeft: -8, + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + })} accessibilityLabel="Zurück" > - + ) : null} - + {title ?? t('appHeader.appName')} @@ -72,10 +85,17 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) { setNotifOpen(true)} hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }} - className="w-9 h-9 rounded-full bg-white items-center justify-center" - style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} + style={({ pressed }) => ({ + opacity: pressed ? 0.7 : 1, + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: colors.surface, + alignItems: 'center', + justifyContent: 'center', + })} > - + {badge > 0 && ( @@ -89,8 +109,16 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) { setMenuOpen(true)} 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 }) => ({ opacity: pressed ? 0.7 : 1 })} + style={({ pressed }) => ({ + opacity: pressed ? 0.7 : 1, + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + backgroundColor: showAvatarImage ? colors.surfaceElevated : colors.brandOrange, + })} > {showAvatarImage ? ( - + - + {/* Profile · Settings · Games · [Debug DEV] */} {items.map((item) => ( @@ -170,7 +172,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props) onClose(); void item.onSelect(); }} - android_ripple={{ color: '#e5e7eb' }} + android_ripple={{ color: colors.surfaceElevated }} style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} > {item.label} @@ -200,12 +202,12 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props) ))} - + {/* Abmelden — neutral, nicht rot */} ({ opacity: pressed ? 0.7 : 1 })} > {t('headerMenu.logout')} diff --git a/apps/rebreak-native/components/profile/ApprovedDomainsList.tsx b/apps/rebreak-native/components/profile/ApprovedDomainsList.tsx index 141ae0b..a4eaefd 100644 --- a/apps/rebreak-native/components/profile/ApprovedDomainsList.tsx +++ b/apps/rebreak-native/components/profile/ApprovedDomainsList.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { View, Text, Pressable, LayoutAnimation, Platform, UIManager } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; -import { colors } from '../../lib/theme'; +import { useColors } from '../../lib/theme'; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); @@ -18,6 +18,7 @@ type Props = { }; export function ApprovedDomainsList({ domains, loading }: Props) { + const colors = useColors(); const [expanded, setExpanded] = useState(false); function toggle() { @@ -36,9 +37,9 @@ export function ApprovedDomainsList({ domains, loading }: Props) { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - backgroundColor: '#ffffff', + backgroundColor: colors.surface, borderWidth: 1, - borderColor: '#e5e5e5', + borderColor: colors.border, borderRadius: 14, padding: 16, }} @@ -63,9 +64,9 @@ export function ApprovedDomainsList({ domains, loading }: Props) { @@ -120,11 +121,12 @@ export function ApprovedDomainsList({ domains, loading }: Props) { } function SkeletonRow() { + const colors = useColors(); return ( flushSave({ ...local, shiftWork: v })} - trackColor={{ false: '#e5e5e5', true: colors.brandOrange }} + trackColor={{ false: colors.border, true: colors.brandOrange }} thumbColor="#ffffff" /> @@ -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({ label, @@ -708,13 +696,14 @@ function FieldRow({ filled: boolean; children: React.ReactNode; }) { + const colors = useColors(); return ( void }) { + const colors = useColors(); return ( style={{ paddingVertical: 6, paddingHorizontal: 12, - backgroundColor: '#f4f4f5', + backgroundColor: colors.surfaceElevated, borderRadius: 999, borderWidth: 1, - borderColor: '#e4e4e7', + borderColor: colors.border, }} > {showImage ? ( @@ -126,9 +127,9 @@ export function ProfileHeader({ width: 30, height: 30, borderRadius: 15, - backgroundColor: '#0a0a0a', + backgroundColor: colors.text, borderWidth: 2, - borderColor: '#ffffff', + borderColor: colors.bg, alignItems: 'center', justifyContent: 'center', }} @@ -212,9 +213,9 @@ export function ProfileHeader({ paddingHorizontal: 10, paddingVertical: 4, borderRadius: 999, - backgroundColor: '#f5f5f5', + backgroundColor: colors.surfaceElevated, borderWidth: 1, - borderColor: '#e5e5e5', + borderColor: colors.border, }} > {provider === 'google' ? : } diff --git a/apps/rebreak-native/components/profile/StatsBar.tsx b/apps/rebreak-native/components/profile/StatsBar.tsx index 27f808c..0755572 100644 --- a/apps/rebreak-native/components/profile/StatsBar.tsx +++ b/apps/rebreak-native/components/profile/StatsBar.tsx @@ -1,5 +1,5 @@ import { View, Text, Pressable } from 'react-native'; -import { colors } from '../../lib/theme'; +import { useColors } from '../../lib/theme'; type Props = { postsCount: number; @@ -17,6 +17,7 @@ type CardProps = { }; function StatPill({ value, label, onPress }: CardProps) { + const colors = useColors(); return ( }; export function StreakSection({ currentDays, longestDays, startDate, cooldowns }: Props) { + const colors = useColors(); return ( diff --git a/apps/rebreak-native/components/profile/UrgeStatsCard.tsx b/apps/rebreak-native/components/profile/UrgeStatsCard.tsx index 60871b0..d8a9d77 100644 --- a/apps/rebreak-native/components/profile/UrgeStatsCard.tsx +++ b/apps/rebreak-native/components/profile/UrgeStatsCard.tsx @@ -1,6 +1,6 @@ import { View, Text } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; -import { colors } from '../../lib/theme'; +import { useColors } from '../../lib/theme'; export type HelpedByEntry = { key: 'breathing' | 'game' | 'talk' | 'other'; @@ -16,6 +16,7 @@ type Props = { }; export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Props) { + const colors = useColors(); const overcomePct = sessions > 0 ? Math.round((overcome / sessions) * 100) : 0; const totalHelped = helpedBy.reduce((sum, h) => sum + h.count, 0); @@ -44,9 +45,9 @@ export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Prop 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; diff --git a/apps/rebreak-native/stores/theme.ts b/apps/rebreak-native/stores/theme.ts index bcabc2e..d34836f 100644 --- a/apps/rebreak-native/stores/theme.ts +++ b/apps/rebreak-native/stores/theme.ts @@ -20,19 +20,28 @@ type ThemeState = { init: () => Promise; }; -export const useThemeStore = create((set) => ({ - mode: 'system', - colorScheme: 'light', +export const useThemeStore = create((set, get) => { + // Listen for OS-level theme changes and update store when mode === 'system'. + Appearance.addChangeListener(() => { + if (get().mode === 'system') { + set({ colorScheme: resolveColorScheme('system') }); + } + }); - init: async () => { - const stored = await AsyncStorage.getItem(STORAGE_KEY); - const mode: ThemeMode = - stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system'; - set({ mode, colorScheme: resolveColorScheme(mode) }); - }, + return { + mode: 'system', + colorScheme: 'light', - setMode: async (mode) => { - await AsyncStorage.setItem(STORAGE_KEY, mode); - set({ mode, colorScheme: resolveColorScheme(mode) }); - }, -})); + init: async () => { + const stored = await AsyncStorage.getItem(STORAGE_KEY); + const mode: ThemeMode = + stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system'; + set({ mode, colorScheme: resolveColorScheme(mode) }); + }, + + setMode: async (mode) => { + await AsyncStorage.setItem(STORAGE_KEY, mode); + set({ mode, colorScheme: resolveColorScheme(mode) }); + }, + }; +});