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 { 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -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,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 />}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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) });
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user