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:
parent
8f2b93f881
commit
594a43cbf9
@ -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 (
|
||||
<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" />
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<View className="flex-1 bg-neutral-50">
|
||||
<View style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||
<AppHeader />
|
||||
|
||||
<FlatList
|
||||
@ -139,23 +140,30 @@ export default function HomeScreen() {
|
||||
<Pressable
|
||||
key={f.value}
|
||||
onPress={() => 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,
|
||||
})}
|
||||
>
|
||||
<Ionicons
|
||||
name={f.icon}
|
||||
size={13}
|
||||
color={active ? '#fff' : '#737373'}
|
||||
color={active ? '#fff' : colors.textMuted}
|
||||
/>
|
||||
<Text
|
||||
className={`text-xs ${
|
||||
active ? 'text-white' : 'text-neutral-500'
|
||||
}`}
|
||||
style={{ fontFamily: 'Nunito_600SemiBold' }}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontFamily: 'Nunito_600SemiBold',
|
||||
color: active ? '#fff' : colors.textMuted,
|
||||
}}
|
||||
>
|
||||
{f.label}
|
||||
</Text>
|
||||
@ -178,9 +186,9 @@ export default function HomeScreen() {
|
||||
}
|
||||
ListEmptyComponent={
|
||||
isLoading ? null : (
|
||||
<View className="items-center py-16">
|
||||
<Ionicons name="chatbubbles-outline" size={40} color="#d4d4d4" />
|
||||
<Text className="text-sm text-neutral-400 mt-3 text-center" style={{ fontFamily: 'Nunito_400Regular' }}>
|
||||
<View style={{ alignItems: 'center', paddingVertical: 64 }}>
|
||||
<Ionicons name="chatbubbles-outline" size={40} color={colors.border} />
|
||||
<Text style={{ fontSize: 14, color: colors.textMuted, marginTop: 12, textAlign: 'center', fontFamily: 'Nunito_400Regular' }}>
|
||||
{t('community.no_posts')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<StatusBar style="dark" />
|
||||
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
|
||||
<DeviceLimitReachedSheet />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: 'slide_from_right',
|
||||
contentStyle: { backgroundColor: '#ffffff' },
|
||||
contentStyle: { backgroundColor: colors.bg },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" />
|
||||
|
||||
@ -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 (
|
||||
<View style={{ flex: 1, backgroundColor: '#fafafa' }}>
|
||||
<View style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||
<AppHeader showBack title={t('settings.devices_page_title')} />
|
||||
|
||||
<ScrollView
|
||||
@ -225,7 +227,7 @@ export default function DevicesScreen() {
|
||||
{/* Slot counter card */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
@ -316,7 +318,7 @@ export default function DevicesScreen() {
|
||||
{/* Device list card */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
@ -353,7 +355,7 @@ export default function DevicesScreen() {
|
||||
key={device.id}
|
||||
style={{
|
||||
borderBottomWidth: i < devices.length - 1 ? 1 : 0,
|
||||
borderBottomColor: 'rgba(0,0,0,0.04)',
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
>
|
||||
<DeviceRow device={device} onRemove={removeDevice} />
|
||||
|
||||
@ -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 (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
||||
<ActivityIndicator size="large" color={colors.textMuted} />
|
||||
@ -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 (
|
||||
<View style={[styles.msgRow, isUser ? styles.msgRowUser : styles.msgRowAssistant]}>
|
||||
<View style={[styles.bubbleCol, isUser ? styles.bubbleColUser : styles.bubbleColAssistant]}>
|
||||
<View style={[styles.bubble, isUser ? styles.bubbleUser : styles.bubbleAssistant]}>
|
||||
<Text style={[styles.bubbleText, isUser ? styles.bubbleTextUser : styles.bubbleTextAssistant]}>
|
||||
<View style={[styles.bubble, isUser ? styles.bubbleUser : [styles.bubbleAssistant, { backgroundColor: colors.surfaceElevated }]]}>
|
||||
<Text style={[styles.bubbleText, isUser ? styles.bubbleTextUser : [styles.bubbleTextAssistant, { color: colors.text }]]}>
|
||||
{item.content}
|
||||
</Text>
|
||||
</View>
|
||||
@ -152,11 +156,11 @@ function MessageRow({
|
||||
<View style={[styles.metaRow, isUser ? styles.metaRowUser : styles.metaRowAssistant]}>
|
||||
{item.feedbackSaved && (
|
||||
<>
|
||||
<Ionicons name="checkmark-circle" size={11} color="#16a34a" />
|
||||
<Text style={styles.feedbackText}>{t('coach.feedback_saved')}</Text>
|
||||
<Ionicons name="checkmark-circle" size={11} color={colors.success} />
|
||||
<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>
|
||||
@ -170,6 +174,8 @@ export default function CoachScreen() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
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).
|
||||
const messages = useCoachStore((s) => s.messages);
|
||||
@ -539,17 +545,17 @@ export default function CoachScreen() {
|
||||
);
|
||||
|
||||
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 */}
|
||||
{/* sichtbar werden ("kollidieren mit Lyra schriftzug"). */}
|
||||
<View
|
||||
style={[styles.topBarBackdrop, { height: insets.top + 170 }]}
|
||||
style={[styles.topBarBackdrop, { height: insets.top + 170, backgroundColor: colors.bg }]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
{/* Floating header — no bar, avatar + 2 icon buttons hover over chat */}
|
||||
<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} />
|
||||
</Pressable>
|
||||
|
||||
@ -558,12 +564,12 @@ export default function CoachScreen() {
|
||||
<RiveAvatar emotion={emotion} size="md" />
|
||||
</View>
|
||||
<View style={styles.avatarMeta}>
|
||||
<Text style={styles.avatarName}>{t('coach.lyra')}</Text>
|
||||
<Text style={[styles.avatarName, { color: colors.text }]}>{t('coach.lyra')}</Text>
|
||||
{isSpeaking && (
|
||||
<View style={styles.speakingRow}>
|
||||
<VoiceBars count={5} baseColor={colors.brandOrange} />
|
||||
<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} />
|
||||
</Pressable>
|
||||
</View>
|
||||
@ -571,7 +577,7 @@ export default function CoachScreen() {
|
||||
</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} />
|
||||
</Pressable>
|
||||
</View>
|
||||
@ -603,7 +609,7 @@ export default function CoachScreen() {
|
||||
ListFooterComponent={
|
||||
thinking ? (
|
||||
<View style={styles.msgRowAssistant}>
|
||||
<View style={styles.bubbleAssistant}>
|
||||
<View style={[styles.bubbleAssistant, { backgroundColor: colors.surfaceElevated }]}>
|
||||
<ThinkingDots />
|
||||
</View>
|
||||
</View>
|
||||
@ -621,7 +627,7 @@ export default function CoachScreen() {
|
||||
)}
|
||||
|
||||
{/* 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 ? (
|
||||
<View style={styles.recordingContainer}>
|
||||
<Pressable style={styles.cancelBtn} onPress={cancelRecording}>
|
||||
@ -636,13 +642,13 @@ export default function CoachScreen() {
|
||||
) : isTranscribing ? (
|
||||
<View style={styles.transcribingRow}>
|
||||
<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>
|
||||
) : (
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
style={[styles.textInput, { backgroundColor: colors.surfaceElevated, color: colors.text }]}
|
||||
placeholder={t('coach.placeholder')}
|
||||
placeholderTextColor="#a3a3a3"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={input}
|
||||
onChangeText={handleInputChange}
|
||||
multiline
|
||||
@ -654,7 +660,7 @@ export default function CoachScreen() {
|
||||
|
||||
{!isTranscribing && (
|
||||
<Pressable
|
||||
style={[styles.micBtn, isRecording && styles.micBtnActive, thinking && styles.micBtnDisabled]}
|
||||
style={[styles.micBtn, { backgroundColor: colors.surfaceElevated }, isRecording && styles.micBtnActive, thinking && styles.micBtnDisabled]}
|
||||
onPressIn={onMicDown}
|
||||
onPressOut={onMicUp}
|
||||
disabled={thinking}
|
||||
@ -741,7 +747,7 @@ const styles = StyleSheet.create({
|
||||
statusLabel: {
|
||||
fontSize: 11,
|
||||
fontFamily: 'Nunito_400Regular',
|
||||
color: colors.textMuted,
|
||||
color: '#737373',
|
||||
},
|
||||
speakingRow: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@ -13,7 +13,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { MenuView, type MenuAction } from '@react-native-menu/menu';
|
||||
import { TrueSheet } from '@lodev09/react-native-true-sheet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { colors } from '../lib/theme';
|
||||
import { useColors } from '../lib/theme';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useThemeStore, type ThemeMode } from '../stores/theme';
|
||||
import { useLanguageStore, type AppLanguage } from '../stores/language';
|
||||
@ -54,6 +54,7 @@ export default function SettingsScreen() {
|
||||
const { mode: themeMode, setMode: setThemeMode } = useThemeStore();
|
||||
const { language, setLanguage } = useLanguageStore();
|
||||
const { plan } = useUserPlan();
|
||||
const colors = useColors();
|
||||
|
||||
// Lyra Voice: hardcoded ElevenLabs voice IDs (expandable by user later)
|
||||
// Backend endpoint PATCH /api/profile/me/demographics does NOT accept lyraVoiceId.
|
||||
@ -246,7 +247,7 @@ export default function SettingsScreen() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: '#fafafa' }}>
|
||||
<View style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||
<AppHeader showBack title={t('settings.title')} />
|
||||
|
||||
<ScrollView
|
||||
@ -263,7 +264,7 @@ export default function SettingsScreen() {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: '#a3a3a3',
|
||||
color: colors.textMuted,
|
||||
fontFamily: 'Nunito_600SemiBold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
@ -275,7 +276,7 @@ export default function SettingsScreen() {
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
@ -330,7 +331,7 @@ export default function SettingsScreen() {
|
||||
paddingVertical: 12,
|
||||
minHeight: 56,
|
||||
borderBottomWidth: i < section.rows.length - 1 ? 1 : 0,
|
||||
borderBottomColor: 'rgba(0,0,0,0.04)',
|
||||
borderBottomColor: colors.border,
|
||||
opacity: row.soon ? 0.5 : 1,
|
||||
};
|
||||
|
||||
@ -359,6 +360,7 @@ export default function SettingsScreen() {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
}}
|
||||
>
|
||||
{row.value ? (
|
||||
@ -401,7 +403,7 @@ export default function SettingsScreen() {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: '#a3a3a3',
|
||||
color: colors.textMuted,
|
||||
fontFamily: 'Nunito_600SemiBold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
@ -425,7 +427,7 @@ export default function SettingsScreen() {
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={16}
|
||||
color="#d4d4d8"
|
||||
color={colors.border}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@ -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}
|
||||
>
|
||||
<View style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 24 }}>
|
||||
<View style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 24, backgroundColor: colors.surface }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
@ -508,7 +511,7 @@ export default function SettingsScreen() {
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: idx < voiceOptions.length - 1 ? 1 : 0,
|
||||
borderBottomColor: 'rgba(0,0,0,0.06)',
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
|
||||
@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useNotificationStore } from '../stores/notifications';
|
||||
import { resolveAvatar } from '../lib/resolveAvatar';
|
||||
import { useColors } from '../lib/theme';
|
||||
import { useMe } from '../hooks/useMe';
|
||||
import { NotificationsDropdown } from './NotificationsDropdown';
|
||||
import { HeaderDropdownMenu } from './header/HeaderDropdownMenu';
|
||||
@ -22,6 +23,7 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuthStore();
|
||||
const colors = useColors();
|
||||
const { me } = useMe();
|
||||
const storeUnread = useNotificationStore((s) => s.unread);
|
||||
const badge = notifCount ?? storeUnread;
|
||||
@ -47,8 +49,12 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
||||
|
||||
return (
|
||||
<View
|
||||
className="bg-white border-b border-neutral-200"
|
||||
style={{ paddingTop: insets.top }}
|
||||
style={{
|
||||
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="flex-row items-center" style={{ gap: 8 }}>
|
||||
@ -56,14 +62,21 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
||||
<Pressable
|
||||
onPress={() => 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"
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color="#0a0a0a" />
|
||||
<Ionicons name="chevron-back" size={22} color={colors.text} />
|
||||
</Pressable>
|
||||
) : 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')}
|
||||
</Text>
|
||||
</View>
|
||||
@ -72,10 +85,17 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
||||
<Pressable
|
||||
onPress={() => 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',
|
||||
})}
|
||||
>
|
||||
<Ionicons name="notifications-outline" size={22} color="#737373" />
|
||||
<Ionicons name="notifications-outline" size={22} color={colors.textMuted} />
|
||||
{badge > 0 && (
|
||||
<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' }}>
|
||||
@ -89,8 +109,16 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
||||
<Pressable
|
||||
onPress={() => 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 ? (
|
||||
<Image
|
||||
|
||||
@ -3,6 +3,7 @@ import { useRouter, type RelativePathString } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useColors } from '../../lib/theme';
|
||||
|
||||
// Controlled-Modal-Pattern. Trigger ist NICHT in dieser Komponente — der
|
||||
// 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 { t } = useTranslation();
|
||||
const { signOut } = useAuthStore();
|
||||
const colors = useColors();
|
||||
|
||||
function nav(path: RelativePathString) {
|
||||
onClose();
|
||||
@ -93,7 +95,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
position: 'absolute',
|
||||
top: topOffset,
|
||||
right: 12,
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 18,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
@ -148,7 +150,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontFamily: 'Nunito_400Regular',
|
||||
color: '#a3a3a3',
|
||||
color: colors.textMuted,
|
||||
marginTop: 1,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
@ -156,11 +158,11 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
{t('appHeader.sosTagline')}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color="#d4d4d8" />
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.border} />
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
<View style={{ height: 1, backgroundColor: '#f0f0f0' }} />
|
||||
<View style={{ height: 1, backgroundColor: colors.border }} />
|
||||
|
||||
{/* 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 })}
|
||||
>
|
||||
<View
|
||||
@ -184,14 +186,14 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={18}
|
||||
color="#737373"
|
||||
color={colors.textMuted}
|
||||
style={{ marginRight: 14 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontFamily: 'Nunito_600SemiBold',
|
||||
color: '#0a0a0a',
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
@ -200,12 +202,12 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
</Pressable>
|
||||
))}
|
||||
|
||||
<View style={{ height: 1, backgroundColor: '#f0f0f0' }} />
|
||||
<View style={{ height: 1, backgroundColor: colors.border }} />
|
||||
|
||||
{/* Abmelden — neutral, nicht rot */}
|
||||
<Pressable
|
||||
onPress={handleLogout}
|
||||
android_ripple={{ color: '#e5e7eb' }}
|
||||
android_ripple={{ color: colors.surfaceElevated }}
|
||||
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
|
||||
>
|
||||
<View
|
||||
@ -219,14 +221,14 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
<Ionicons
|
||||
name="log-out-outline"
|
||||
size={18}
|
||||
color="#737373"
|
||||
color={colors.textMuted}
|
||||
style={{ marginRight: 14 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontFamily: 'Nunito_600SemiBold',
|
||||
color: '#0a0a0a',
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
{t('headerMenu.logout')}
|
||||
|
||||
@ -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) {
|
||||
<View
|
||||
style={{
|
||||
marginTop: 6,
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5e5',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
@ -99,7 +100,7 @@ export function ApprovedDomainsList({ domains, loading }: Props) {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 14,
|
||||
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' }}>
|
||||
@ -120,11 +121,12 @@ export function ApprovedDomainsList({ domains, loading }: Props) {
|
||||
}
|
||||
|
||||
function SkeletonRow() {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: 14,
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
borderRadius: 4,
|
||||
marginVertical: 6,
|
||||
}}
|
||||
|
||||
@ -12,7 +12,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { MenuView } from '@react-native-menu/menu';
|
||||
import { getCitiesForBundesland } from '../../lib/germanCities';
|
||||
import { WheelPickerModal } from '../WheelPickerModal';
|
||||
import { colors } from '../../lib/theme';
|
||||
import { useColors } from '../../lib/theme';
|
||||
import type { Plan } from '../../hooks/useUserPlan';
|
||||
|
||||
// Geburtsjahr-Optionen: 2010 (oldest 13y) → 1920, descending (neueste oben)
|
||||
@ -203,6 +203,7 @@ export function DemographicsAccordion({
|
||||
setLocal(next);
|
||||
}
|
||||
|
||||
const colors = useColors();
|
||||
const { filled, total } = relevantFieldCount(local);
|
||||
const completed = filled === total;
|
||||
const showProTrialBanner = plan === 'free';
|
||||
@ -220,9 +221,9 @@ export function DemographicsAccordion({
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5e5',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
}}
|
||||
@ -282,7 +283,7 @@ export function DemographicsAccordion({
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 5,
|
||||
backgroundColor: '#e5e5e5',
|
||||
backgroundColor: colors.border,
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
@ -316,9 +317,9 @@ export function DemographicsAccordion({
|
||||
<View
|
||||
style={{
|
||||
marginTop: 8,
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5e5',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 14,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
@ -516,7 +517,7 @@ export function DemographicsAccordion({
|
||||
<Switch
|
||||
value={local.shiftWork === true}
|
||||
onValueChange={(v) => flushSave({ ...local, shiftWork: v })}
|
||||
trackColor={{ false: '#e5e5e5', true: colors.brandOrange }}
|
||||
trackColor={{ false: colors.border, true: colors.brandOrange }}
|
||||
thumbColor="#ffffff"
|
||||
/>
|
||||
</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({
|
||||
label,
|
||||
@ -708,13 +696,14 @@ function FieldRow({
|
||||
filled: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: indent ? 20 : 14,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: isLast ? 0 : 1,
|
||||
borderBottomColor: 'rgba(0,0,0,0.06)',
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@ -762,6 +751,7 @@ function FieldRow({
|
||||
}
|
||||
|
||||
function SelectButton({ value, onPress }: { value: string | null; onPress: () => void }) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
@ -773,10 +763,10 @@ function SelectButton({ value, onPress }: { value: string | null; onPress: () =>
|
||||
style={{
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#f4f4f5',
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e4e4e7',
|
||||
borderColor: colors.border,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
|
||||
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { View, Text, Pressable, Image } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
import { colors } from '../../lib/theme';
|
||||
import { useColors } from '../../lib/theme';
|
||||
import { resolveAvatar } from '../../lib/resolveAvatar';
|
||||
import type { Plan } from '../../hooks/useUserPlan';
|
||||
|
||||
@ -67,6 +67,7 @@ export function ProfileHeader({
|
||||
onEditNickname,
|
||||
onDemographicsHintPress,
|
||||
}: Props) {
|
||||
const colors = useColors();
|
||||
const [imageFailed, setImageFailed] = useState(false);
|
||||
const avatarUrl = resolveAvatar(avatar, nickname);
|
||||
const initials = nickname.slice(0, 2).toUpperCase();
|
||||
@ -100,7 +101,7 @@ export function ProfileHeader({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: showImage ? '#fafafa' : colors.brandOrange,
|
||||
backgroundColor: showImage ? colors.surfaceElevated : colors.brandOrange,
|
||||
}}
|
||||
>
|
||||
{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' ? <GoogleIcon /> : <AppleIcon />}
|
||||
|
||||
@ -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 (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
@ -24,9 +25,9 @@ function StatPill({ value, label, onPress }: CardProps) {
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5e5',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 999,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 18,
|
||||
|
||||
@ -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 CooldownEntry = {
|
||||
id: string;
|
||||
@ -30,6 +30,7 @@ const statusColor: Record<CooldownEntry['status'], { bg: string; text: string }>
|
||||
};
|
||||
|
||||
export function StreakSection({ currentDays, longestDays, startDate, cooldowns }: Props) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={{ marginHorizontal: 16, marginTop: 24 }}>
|
||||
<View
|
||||
@ -55,9 +56,9 @@ export function StreakSection({ currentDays, longestDays, startDate, cooldowns }
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5e5',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
}}
|
||||
@ -121,9 +122,9 @@ export function StreakSection({ currentDays, longestDays, startDate, cooldowns }
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5e5',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
}}
|
||||
@ -153,7 +154,7 @@ export function StreakSection({ currentDays, longestDays, startDate, cooldowns }
|
||||
style={{
|
||||
width: 1,
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||
backgroundColor: colors.border,
|
||||
marginTop: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5e5',
|
||||
borderColor: colors.border,
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
}}
|
||||
@ -99,7 +100,7 @@ export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Prop
|
||||
style={{
|
||||
marginTop: 8,
|
||||
height: 6,
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
@ -169,7 +170,7 @@ export function UrgeStatsCard({ sessions, overcome, helpedBy, topEmotion }: Prop
|
||||
<View
|
||||
style={{
|
||||
height: 4,
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { useThemeStore } from '../stores/theme';
|
||||
|
||||
export const theme = {
|
||||
bg: 'bg-white',
|
||||
surface: 'bg-neutral-50',
|
||||
@ -10,17 +12,55 @@ export const theme = {
|
||||
brandBlue: 'bg-midnight-800',
|
||||
} 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',
|
||||
surface: '#fafafa',
|
||||
surfaceElevated: '#f5f5f5',
|
||||
border: '#e5e5e5',
|
||||
text: '#0a0a0a',
|
||||
textMuted: '#737373',
|
||||
// TEMP zum Testen: iOS native blue. Wieder auf '#f59e0b' wenn du zur Brand zurück willst.
|
||||
brandOrange: '#007AFF',
|
||||
brandBlue: '#0e1f3a',
|
||||
success: '#16a34a',
|
||||
error: '#dc2626',
|
||||
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;
|
||||
|
||||
@ -20,19 +20,28 @@ type ThemeState = {
|
||||
init: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const useThemeStore = create<ThemeState>((set) => ({
|
||||
mode: 'system',
|
||||
colorScheme: 'light',
|
||||
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') });
|
||||
}
|
||||
});
|
||||
|
||||
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) });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user