feat(theme): Dark Mode Wave 2 — blocker, mail, chat, community, notifications, all remaining screens

Wave 2 = ALLE app-files die in Wave 1 noch hardcoded waren. Komplette App-weit
theme-aware-Migration jetzt durch. Legacy `import { colors }` flat export
vollständig eliminiert.

Migrated this wave:

Top-level Screens:
- app/urge.tsx (makeStyles factory mit ~20 colors)
- app/room.tsx + dm.tsx + games.tsx
- app/(app)/chat.tsx + mail.tsx + coach.tsx + notifications.tsx
- app/profile/[userId].tsx + profile/edit.tsx (INPUT_STYLE in body moved)
- app/debug.tsx + auth/callback.tsx

Blocker (7):
- AddDomainSheet, CooldownBanner, DeactivationExplainerSheet, DomainGrid,
  ProtectionCard, ProtectionDetailsSheet, ProtectionLockedCard

Mail (3):
- ConnectMailSheet, EditMailAccountSheet, MailEmptyState

Chat (1):
- ChatBubble, ChatInput

Community/Posts/Notifications:
- PostCard, PostCardSkeleton, ComposeCard, PostCommentsSheet
- NotificationsDropdown
- StreakBadge (Nativewind classes durch inline dynamic styles ersetzt)

Reusable Sheets:
- WheelPickerModal, OptionsBottomSheet, DeviceLimitReachedSheet

Urge subsystem (5):
- InlineRatingDrawer, ShareSuccessDrawer, UrgeStats, SosFeedbackModal,
  Breathing

Profile components:
- DigaMissionBanner

Pattern: useColors() hook in component body, makeStyles(colors) factory wo
StyleSheet.create vorher hardcoded war. 11 base-tokens (bg/surface/
surfaceElevated/border/text/textMuted/brandOrange/brandBlue/success/error/
warning) nutzen colors.light vs colors.dark scheme.

Bewusst NICHT migriert (semantic colors):
- DigaMissionBanner amber (#fffbeb, #854d0e) — DiGA-brand, nicht neutral
- Lyra-thinking #3b82f6 in urge.tsx — Lyra-brand-color
- scrollDownBtn #374151 — intentional dark floating-button

TS clean. Test: Settings → Theme → Dark — alle screens sollen jetzt dunkel
werden ohne white-flashes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-09 14:51:02 +02:00
parent 1abd101d53
commit d7b15e231a
41 changed files with 1354 additions and 1245 deletions

View File

@ -17,7 +17,7 @@ import { apiFetch } from '../../lib/api';
import { AppHeader } from '../../components/AppHeader'; import { AppHeader } from '../../components/AppHeader';
import { RoomCard, type Room } from '../../components/chat/RoomCard'; import { RoomCard, type Room } from '../../components/chat/RoomCard';
import { CreateRoomSheet } from '../../components/chat/CreateRoomSheet'; import { CreateRoomSheet } from '../../components/chat/CreateRoomSheet';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
type DmConversation = { type DmConversation = {
partnerId: string; partnerId: string;
@ -39,6 +39,8 @@ function formatTime(ts: string, justNowLabel: string): string {
function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) { function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const styles = makeStyles(colors);
const hasUnread = conv.unreadCount > 0; const hasUnread = conv.unreadCount > 0;
return ( return (
@ -95,6 +97,8 @@ function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }
export default function ChatScreen() { export default function ChatScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const colors = useColors();
const styles = makeStyles(colors);
const [tab, setTab] = useState<'groups' | 'direct'>('groups'); const [tab, setTab] = useState<'groups' | 'direct'>('groups');
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
@ -199,13 +203,13 @@ export default function ChatScreen() {
<RefreshControl <RefreshControl
refreshing={refetchingRooms} refreshing={refetchingRooms}
onRefresh={refetchRooms} onRefresh={refetchRooms}
tintColor={colors.brandOrange} tintColor="#007AFF"
/> />
} }
ListEmptyComponent={ ListEmptyComponent={
loadingRooms ? ( loadingRooms ? (
<View style={styles.emptyBox}> <View style={styles.emptyBox}>
<ActivityIndicator color={colors.brandOrange} /> <ActivityIndicator color="#007AFF" />
</View> </View>
) : ( ) : (
<View style={styles.emptyBox}> <View style={styles.emptyBox}>
@ -225,13 +229,13 @@ export default function ChatScreen() {
<RefreshControl <RefreshControl
refreshing={refetchingDms} refreshing={refetchingDms}
onRefresh={refetchDms} onRefresh={refetchDms}
tintColor={colors.brandOrange} tintColor="#007AFF"
/> />
} }
ListEmptyComponent={ ListEmptyComponent={
loadingDms ? ( loadingDms ? (
<View style={styles.emptyBox}> <View style={styles.emptyBox}>
<ActivityIndicator color={colors.brandOrange} /> <ActivityIndicator color="#007AFF" />
</View> </View>
) : ( ) : (
<View style={styles.emptyBox}> <View style={styles.emptyBox}>
@ -257,154 +261,155 @@ export default function ChatScreen() {
); );
} }
const styles = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
container: { flex: 1, backgroundColor: '#fafafa' }, return StyleSheet.create({
headerSection: { container: { flex: 1, backgroundColor: colors.bg },
paddingHorizontal: 16, headerSection: {
paddingTop: 14, paddingHorizontal: 16,
paddingBottom: 10, paddingTop: 14,
backgroundColor: '#fff', paddingBottom: 10,
borderBottomWidth: StyleSheet.hairlineWidth, backgroundColor: colors.bg,
borderBottomColor: '#e5e5e5', borderBottomWidth: StyleSheet.hairlineWidth,
}, borderBottomColor: colors.border,
titleRow: { },
flexDirection: 'row', titleRow: {
alignItems: 'center', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
}, justifyContent: 'space-between',
title: { },
fontSize: 22, title: {
fontFamily: 'Nunito_800ExtraBold', fontSize: 22,
color: '#171717', fontFamily: 'Nunito_800ExtraBold',
}, color: colors.text,
createBtn: { },
width: 34, createBtn: {
height: 34, width: 34,
borderRadius: 17, height: 34,
backgroundColor: '#007AFF', borderRadius: 17,
alignItems: 'center', backgroundColor: '#007AFF',
justifyContent: 'center', alignItems: 'center',
}, justifyContent: 'center',
tabs: { },
flexDirection: 'row', tabs: {
marginTop: 12, flexDirection: 'row',
backgroundColor: '#f5f5f5', marginTop: 12,
borderRadius: 10, backgroundColor: colors.surfaceElevated,
padding: 3, borderRadius: 10,
}, padding: 3,
tab: { },
flex: 1, tab: {
flexDirection: 'row', flex: 1,
alignItems: 'center', flexDirection: 'row',
justifyContent: 'center', alignItems: 'center',
paddingVertical: 7, justifyContent: 'center',
borderRadius: 8, paddingVertical: 7,
}, borderRadius: 8,
tabActive: { },
backgroundColor: '#fff', tabActive: {
shadowColor: '#000', backgroundColor: colors.surface,
shadowOpacity: 0.05, shadowColor: '#000',
shadowRadius: 2, shadowOpacity: 0.05,
shadowOffset: { width: 0, height: 1 }, shadowRadius: 2,
}, shadowOffset: { width: 0, height: 1 },
tabText: { },
fontSize: 12, tabText: {
fontFamily: 'Nunito_600SemiBold', fontSize: 12,
color: '#737373', fontFamily: 'Nunito_600SemiBold',
marginLeft: 5, color: colors.textMuted,
}, marginLeft: 5,
tabTextActive: { },
color: '#007AFF', tabTextActive: {
fontFamily: 'Nunito_700Bold', color: '#007AFF',
}, fontFamily: 'Nunito_700Bold',
tabBadge: { },
minWidth: 16, tabBadge: {
height: 16, minWidth: 16,
borderRadius: 8, height: 16,
backgroundColor: '#007AFF', borderRadius: 8,
paddingHorizontal: 4, backgroundColor: '#007AFF',
alignItems: 'center', paddingHorizontal: 4,
justifyContent: 'center', alignItems: 'center',
marginLeft: 5, justifyContent: 'center',
}, marginLeft: 5,
tabBadgeText: { },
fontSize: 9, tabBadgeText: {
fontFamily: 'Nunito_700Bold', fontSize: 9,
color: '#fff', fontFamily: 'Nunito_700Bold',
}, color: '#fff',
emptyBox: { },
alignItems: 'center', emptyBox: {
justifyContent: 'center', alignItems: 'center',
paddingVertical: 60, justifyContent: 'center',
paddingHorizontal: 32, paddingVertical: 60,
}, paddingHorizontal: 32,
emptyText: { },
fontSize: 13, emptyText: {
fontFamily: 'Nunito_600SemiBold', fontSize: 13,
color: '#a3a3a3', fontFamily: 'Nunito_600SemiBold',
marginTop: 12, color: colors.textMuted,
}, marginTop: 12,
// DM row styles },
dmRow: { dmRow: {
width: '100%', width: '100%',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 11, paddingVertical: 11,
backgroundColor: '#fff', backgroundColor: colors.bg,
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#f5f5f5', borderBottomColor: colors.border,
}, },
dmAvatar: { dmAvatar: {
width: 42, width: 42,
height: 42, height: 42,
borderRadius: 21, borderRadius: 21,
backgroundColor: '#e5e5e5', backgroundColor: colors.surfaceElevated,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
marginRight: 10, marginRight: 10,
}, },
dmAvatarImg: { width: 42, height: 42 }, dmAvatarImg: { width: 42, height: 42 },
dmAvatarInitials: { dmAvatarInitials: {
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#525252', color: colors.textMuted,
}, },
dmInfo: { flex: 1, minWidth: 0 }, dmInfo: { flex: 1, minWidth: 0 },
dmHeaderRow: { dmHeaderRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
dmName: { dmName: {
fontSize: 14, fontSize: 14,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#171717', color: colors.text,
flexShrink: 1, flexShrink: 1,
marginRight: 6, marginRight: 6,
}, },
dmTime: { fontSize: 11, fontFamily: 'Nunito_600SemiBold' }, dmTime: { fontSize: 11, fontFamily: 'Nunito_600SemiBold' },
dmBottomRow: { dmBottomRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginTop: 2, marginTop: 2,
}, },
dmLast: { fontSize: 12, flex: 1 }, dmLast: { fontSize: 12, flex: 1 },
unreadBadge: { unreadBadge: {
minWidth: 20, minWidth: 20,
height: 20, height: 20,
paddingHorizontal: 6, paddingHorizontal: 6,
borderRadius: 10, borderRadius: 10,
backgroundColor: '#007AFF', backgroundColor: '#007AFF',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginLeft: 8, marginLeft: 8,
}, },
unreadBadgeText: { unreadBadgeText: {
fontSize: 10, fontSize: 10,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#fff', color: '#fff',
}, },
}); });
}

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { useRouter, useFocusEffect } from 'expo-router'; import { useRouter, useFocusEffect } from 'expo-router';
import { useColors } from '../../lib/theme';
/** /**
* Placeholder-Screen für den Coach-Tab. * Placeholder-Screen für den Coach-Tab.
@ -11,6 +12,7 @@ import { useRouter, useFocusEffect } from 'expo-router';
*/ */
export default function CoachTabRedirect() { export default function CoachTabRedirect() {
const router = useRouter(); const router = useRouter();
const colors = useColors();
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@ -20,5 +22,5 @@ export default function CoachTabRedirect() {
}, [router]), }, [router]),
); );
return <View style={{ flex: 1, backgroundColor: '#ffffff' }} />; return <View style={{ flex: 1, backgroundColor: colors.bg }} />;
} }

View File

@ -20,10 +20,12 @@ import { SuccessAlert } from '../../components/SuccessAlert';
import { useMailStatus } from '../../hooks/useMailStatus'; import { useMailStatus } from '../../hooks/useMailStatus';
import { useMailDisconnect } from '../../hooks/useMailDisconnect'; import { useMailDisconnect } from '../../hooks/useMailDisconnect';
import { useUserPlan } from '../../hooks/useUserPlan'; import { useUserPlan } from '../../hooks/useUserPlan';
import { useColors } from '../../lib/theme';
export default function MailScreen() { export default function MailScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const tabBarHeight = useBottomTabBarHeight(); const tabBarHeight = useBottomTabBarHeight();
const colors = useColors();
const { plan } = useUserPlan(); const { plan } = useUserPlan();
@ -72,7 +74,7 @@ export default function MailScreen() {
if (loading) { if (loading) {
return ( return (
<View style={{ flex: 1, backgroundColor: '#fafafa' }}> <View style={{ flex: 1, backgroundColor: colors.bg }}>
<AppHeader /> <AppHeader />
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#007AFF" /> <ActivityIndicator size="large" color="#007AFF" />
@ -82,7 +84,7 @@ export default function MailScreen() {
} }
return ( return (
<View style={{ flex: 1, backgroundColor: '#fafafa' }}> <View style={{ flex: 1, backgroundColor: colors.bg }}>
<AppHeader /> <AppHeader />
<ScrollView <ScrollView
@ -118,7 +120,7 @@ export default function MailScreen() {
style={{ style={{
fontSize: 11, fontSize: 11,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#737373', color: colors.textMuted,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.8, letterSpacing: 0.8,
}} }}
@ -129,7 +131,7 @@ export default function MailScreen() {
style={{ style={{
fontSize: 11, fontSize: 11,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#a3a3a3', color: colors.textMuted,
marginTop: 2, marginTop: 2,
}} }}
> >
@ -147,7 +149,7 @@ export default function MailScreen() {
disabled={limitReached} disabled={limitReached}
android_ripple={{ color: '#0066cc' }} android_ripple={{ color: '#0066cc' }}
style={{ style={{
backgroundColor: limitReached ? '#e5e5e5' : '#007AFF', backgroundColor: limitReached ? colors.surfaceElevated : '#007AFF',
borderRadius: 12, borderRadius: 12,
opacity: limitReached ? 0.7 : 1, opacity: limitReached ? 0.7 : 1,
shadowColor: '#007AFF', shadowColor: '#007AFF',
@ -169,14 +171,14 @@ export default function MailScreen() {
<Ionicons <Ionicons
name="add" name="add"
size={18} size={18}
color={limitReached ? '#737373' : '#fff'} color={limitReached ? colors.textMuted : '#fff'}
style={{ marginRight: 6 }} style={{ marginRight: 6 }}
/> />
<Text <Text
style={{ style={{
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: limitReached ? '#737373' : '#fff', color: limitReached ? colors.textMuted : '#fff',
}} }}
> >
{t('mail.add_account')} {t('mail.add_account')}

View File

@ -7,11 +7,12 @@ import { HeroShieldCheck } from '../../components/HeroShieldCheck';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { EmptyState } from '../../components/EmptyState'; import { EmptyState } from '../../components/EmptyState';
import { useNotificationStore, type AppNotification } from '../../stores/notifications'; import { useNotificationStore, type AppNotification } from '../../stores/notifications';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
export default function NotificationsScreen() { export default function NotificationsScreen() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const items = useNotificationStore((s) => s.items); const items = useNotificationStore((s) => s.items);
const loaded = useNotificationStore((s) => s.loaded); const loaded = useNotificationStore((s) => s.loaded);
const load = useNotificationStore((s) => s.load); const load = useNotificationStore((s) => s.load);
@ -28,17 +29,16 @@ export default function NotificationsScreen() {
}, []); }, []);
return ( return (
<SafeAreaView className="flex-1 bg-white" edges={['top']}> <SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
<View className="flex-row items-center gap-3 px-5 pt-3 pb-3 border-b border-neutral-200"> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 20, paddingTop: 12, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: colors.border }}>
<Pressable <Pressable
onPress={() => router.back()} onPress={() => router.back()}
className="w-9 h-9 rounded-full bg-neutral-100 border border-neutral-200 items-center justify-center" style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: colors.surfaceElevated, borderWidth: 1, borderColor: colors.border, alignItems: 'center', justifyContent: 'center' }}
> >
<Ionicons name="arrow-back" size={18} color="#737373" /> <Ionicons name="arrow-back" size={18} color={colors.textMuted} />
</Pressable> </Pressable>
<Text <Text
className="text-neutral-900 text-lg flex-1" style={{ color: colors.text, fontSize: 18, flex: 1, fontFamily: 'Nunito_700Bold' }}
style={{ fontFamily: 'Nunito_700Bold' }}
> >
{t('notifications.title')} {t('notifications.title')}
</Text> </Text>
@ -59,7 +59,7 @@ export default function NotificationsScreen() {
<RefreshControl <RefreshControl
refreshing={!loaded} refreshing={!loaded}
onRefresh={load} onRefresh={load}
tintColor={colors.brandOrange} tintColor="#007AFF"
/> />
} }
renderItem={({ item }) => ( renderItem={({ item }) => (
@ -88,6 +88,7 @@ function NotificationRow({
onPress: () => void; onPress: () => void;
onDelete: () => void; onDelete: () => void;
}) { }) {
const colors = useColors();
const isUnread = !notif.readAt; const isUnread = !notif.readAt;
return ( return (
<Pressable <Pressable
@ -103,8 +104,8 @@ function NotificationRow({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f5f5f5', borderBottomColor: colors.border,
backgroundColor: isUnread ? '#fff7ed' : '#fff', backgroundColor: isUnread ? colors.surface : colors.bg,
}} }}
> >
{/* Pure-Icon — KEIN bg-Circle (User-Wunsch: kein extra Rand). */} {/* Pure-Icon — KEIN bg-Circle (User-Wunsch: kein extra Rand). */}
@ -117,7 +118,7 @@ function NotificationRow({
</View> </View>
<View style={{ flex: 1, minWidth: 0, marginRight: 8 }}> <View style={{ flex: 1, minWidth: 0, marginRight: 8 }}>
<Text <Text
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }} style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text }}
numberOfLines={1} numberOfLines={1}
> >
{notif.actorName} {notif.actorName}
@ -127,7 +128,7 @@ function NotificationRow({
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#525252', color: colors.textMuted,
marginTop: 2, marginTop: 2,
}} }}
numberOfLines={2} numberOfLines={2}

View File

@ -15,10 +15,11 @@ import { useEffect } from 'react';
import { View, ActivityIndicator } from 'react-native'; import { View, ActivityIndicator } from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import { supabase } from '../../lib/supabase'; import { supabase } from '../../lib/supabase';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
export default function AuthCallback() { export default function AuthCallback() {
const router = useRouter(); const router = useRouter();
const colors = useColors();
const params = useLocalSearchParams<{ access_token?: string; refresh_token?: string }>(); const params = useLocalSearchParams<{ access_token?: string; refresh_token?: string }>();
useEffect(() => { useEffect(() => {
@ -50,7 +51,7 @@ export default function AuthCallback() {
}, []); }, []);
return ( return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff' }}> <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.bg }}>
<ActivityIndicator size="large" color={colors.brandOrange} /> <ActivityIndicator size="large" color={colors.brandOrange} />
</View> </View>
); );

View File

@ -3,10 +3,11 @@ import { View, Text, ScrollView, Pressable } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
export default function DebugScreen() { export default function DebugScreen() {
const router = useRouter(); const router = useRouter();
const colors = useColors();
useEffect(() => { useEffect(() => {
if (!__DEV__) { if (!__DEV__) {
@ -15,11 +16,11 @@ export default function DebugScreen() {
}, [router]); }, [router]);
if (!__DEV__) { if (!__DEV__) {
return <View style={{ flex: 1, backgroundColor: '#ffffff' }} />; return <View style={{ flex: 1, backgroundColor: colors.bg }} />;
} }
return ( return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}> <SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
<View <View
style={{ style={{
paddingHorizontal: 12, paddingHorizontal: 12,
@ -48,7 +49,7 @@ export default function DebugScreen() {
<Ionicons name="chevron-back" size={26} color={colors.text} /> <Ionicons name="chevron-back" size={26} color={colors.text} />
</View> </View>
</Pressable> </Pressable>
<Text style={{ fontSize: 20, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}> <Text style={{ fontSize: 20, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
Debug Debug
</Text> </Text>
</View> </View>
@ -119,10 +120,11 @@ function DebugStub({
subtitle: string; subtitle: string;
icon: React.ComponentProps<typeof Ionicons>['name']; icon: React.ComponentProps<typeof Ionicons>['name'];
}) { }) {
const colors = useColors();
return ( return (
<View <View
style={{ style={{
backgroundColor: '#fafafa', backgroundColor: colors.surface,
borderRadius: 14, borderRadius: 14,
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)', borderColor: 'rgba(0,0,0,0.05)',
@ -139,19 +141,19 @@ function DebugStub({
width: 36, width: 36,
height: 36, height: 36,
borderRadius: 11, borderRadius: 11,
backgroundColor: '#e5e7eb', backgroundColor: colors.surfaceElevated,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<Ionicons name={icon} size={18} color="#525252" /> <Ionicons name={icon} size={18} color={colors.textMuted} />
</View> </View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}>{title}</Text> <Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_700Bold' }}>{title}</Text>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
color: '#737373', color: colors.textMuted,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
marginTop: 3, marginTop: 3,
lineHeight: 17, lineHeight: 17,

View File

@ -20,7 +20,7 @@ import { supabase } from '../lib/supabase';
import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble'; import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble';
import { ChatInput, type SendPayload } from '../components/chat/ChatInput'; import { ChatInput, type SendPayload } from '../components/chat/ChatInput';
import { useDmRealtime } from '../hooks/useChatRealtime'; import { useDmRealtime } from '../hooks/useChatRealtime';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
type DmHistoryResponse = { type DmHistoryResponse = {
partner: { partner: {
@ -52,6 +52,8 @@ export default function DmScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const colors = useColors();
const styles = makeStyles(colors);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const flatRef = useRef<FlatList>(null); const flatRef = useRef<FlatList>(null);
const [myUserId, setMyUserId] = useState<string | undefined>(undefined); const [myUserId, setMyUserId] = useState<string | undefined>(undefined);
@ -234,7 +236,7 @@ export default function DmScreen() {
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<Pressable style={styles.backBtn} onPress={() => router.back()} hitSlop={8}> <Pressable style={styles.backBtn} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color="#0a0a0a" /> <Ionicons name="chevron-back" size={22} color={colors.text} />
</Pressable> </Pressable>
<View style={styles.headerCenter}> <View style={styles.headerCenter}>
<View style={styles.headerAvatar}> <View style={styles.headerAvatar}>
@ -260,7 +262,7 @@ export default function DmScreen() {
> >
{isLoading && messages.length === 0 ? ( {isLoading && messages.length === 0 ? (
<View style={styles.loadingBox}> <View style={styles.loadingBox}>
<ActivityIndicator color={colors.brandOrange} /> <ActivityIndicator color="#007AFF" />
</View> </View>
) : messages.length === 0 ? ( ) : messages.length === 0 ? (
<View style={styles.loadingBox}> <View style={styles.loadingBox}>
@ -302,64 +304,66 @@ export default function DmScreen() {
); );
} }
const styles = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
container: { flex: 1, backgroundColor: '#fafafa' }, return StyleSheet.create({
header: { container: { flex: 1, backgroundColor: colors.bg },
flexDirection: 'row', header: {
alignItems: 'center', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
paddingHorizontal: 12, justifyContent: 'space-between',
paddingVertical: 10, paddingHorizontal: 12,
backgroundColor: '#fff', paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth, backgroundColor: colors.bg,
borderBottomColor: '#e5e5e5', borderBottomWidth: StyleSheet.hairlineWidth,
}, borderBottomColor: colors.border,
backBtn: { },
width: 36, backBtn: {
height: 36, width: 36,
borderRadius: 12, height: 36,
backgroundColor: '#f5f5f5', borderRadius: 12,
alignItems: 'center', backgroundColor: colors.surfaceElevated,
justifyContent: 'center', alignItems: 'center',
}, justifyContent: 'center',
headerCenter: { },
flex: 1, headerCenter: {
flexDirection: 'row', flex: 1,
alignItems: 'center', flexDirection: 'row',
justifyContent: 'center', alignItems: 'center',
marginHorizontal: 8, justifyContent: 'center',
}, marginHorizontal: 8,
headerAvatar: { },
width: 32, headerAvatar: {
height: 32, width: 32,
borderRadius: 16, height: 32,
backgroundColor: '#e5e5e5', borderRadius: 16,
alignItems: 'center', backgroundColor: colors.surfaceElevated,
justifyContent: 'center', alignItems: 'center',
overflow: 'hidden', justifyContent: 'center',
marginRight: 8, overflow: 'hidden',
}, marginRight: 8,
headerAvatarImg: { width: 32, height: 32 }, },
headerAvatarInitials: { headerAvatarImg: { width: 32, height: 32 },
fontSize: 11, headerAvatarInitials: {
fontFamily: 'Nunito_700Bold', fontSize: 11,
color: '#737373', fontFamily: 'Nunito_700Bold',
}, color: colors.textMuted,
headerName: { },
fontSize: 15, headerName: {
fontFamily: 'Nunito_700Bold', fontSize: 15,
color: '#171717', fontFamily: 'Nunito_700Bold',
flexShrink: 1, color: colors.text,
}, flexShrink: 1,
loadingBox: { },
flex: 1, loadingBox: {
alignItems: 'center', flex: 1,
justifyContent: 'center', alignItems: 'center',
}, justifyContent: 'center',
emptyText: { },
fontSize: 13, emptyText: {
fontFamily: 'Nunito_600SemiBold', fontSize: 13,
color: '#a3a3a3', fontFamily: 'Nunito_600SemiBold',
marginTop: 12, color: colors.textMuted,
}, marginTop: 12,
}); },
});
}

View File

@ -13,7 +13,7 @@ import {
TetrisGame, TetrisGame,
} from '../components/urge/UrgeGames'; } from '../components/urge/UrgeGames';
import { GameCard } from '../components/games/GameCard'; import { GameCard } from '../components/games/GameCard';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
import { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
type GameStat = { avgStars: number; count: number }; type GameStat = { avgStars: number; count: number };
@ -31,6 +31,7 @@ type LastScore = { game: GameType; score: number } | null;
export default function GamesScreen() { export default function GamesScreen() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const [active, setActive] = useState<GameType | null>(null); const [active, setActive] = useState<GameType | null>(null);
const [lastScore, setLastScore] = useState<LastScore>(null); const [lastScore, setLastScore] = useState<LastScore>(null);
const [gameStats, setGameStats] = useState<GameStats>(EMPTY_STATS); const [gameStats, setGameStats] = useState<GameStats>(EMPTY_STATS);
@ -70,7 +71,7 @@ export default function GamesScreen() {
if (active) { if (active) {
return ( return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}> <SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
<View <View
style={{ style={{
paddingHorizontal: 12, paddingHorizontal: 12,
@ -79,7 +80,7 @@ export default function GamesScreen() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.06)', borderBottomColor: colors.border,
}} }}
> >
<Pressable <Pressable
@ -102,7 +103,7 @@ export default function GamesScreen() {
</Text> </Text>
</View> </View>
</Pressable> </Pressable>
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t(GAME_META.find((g) => g.id === active)!.titleKey)} {t(GAME_META.find((g) => g.id === active)!.titleKey)}
</Text> </Text>
<View style={{ width: 60 }} /> <View style={{ width: 60 }} />
@ -127,7 +128,7 @@ export default function GamesScreen() {
} }
return ( return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}> <SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
<View <View
style={{ style={{
paddingHorizontal: 12, paddingHorizontal: 12,
@ -137,7 +138,7 @@ export default function GamesScreen() {
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.06)', borderBottomColor: colors.border,
}} }}
> >
<Pressable <Pressable
@ -156,7 +157,7 @@ export default function GamesScreen() {
<Ionicons name="chevron-back" size={26} color={colors.text} /> <Ionicons name="chevron-back" size={26} color={colors.text} />
</View> </View>
</Pressable> </Pressable>
<Text style={{ fontSize: 20, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}> <Text style={{ fontSize: 20, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
{t('games.title')} {t('games.title')}
</Text> </Text>
</View> </View>
@ -169,7 +170,7 @@ export default function GamesScreen() {
<Text <Text
style={{ style={{
fontSize: 13, fontSize: 13,
color: '#737373', color: colors.textMuted,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
lineHeight: 19, lineHeight: 19,
marginBottom: 18, marginBottom: 18,
@ -232,7 +233,7 @@ export default function GamesScreen() {
style={{ style={{
textAlign: 'center', textAlign: 'center',
fontSize: 11, fontSize: 11,
color: '#a3a3a3', color: colors.textMuted,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
marginTop: 24, marginTop: 24,
opacity: 0.7, opacity: 0.7,

View File

@ -3,7 +3,7 @@ import { View, Text, ScrollView, Pressable, Image } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useLocalSearchParams, useRouter } from 'expo-router'; import { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
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';
@ -52,13 +52,14 @@ type StatProps = {
}; };
function ForeignStat({ value, label }: StatProps) { function ForeignStat({ value, label }: StatProps) {
const colors = useColors();
return ( return (
<View <View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
paddingVertical: 14, paddingVertical: 14,
alignItems: 'center', alignItems: 'center',
@ -85,6 +86,7 @@ function ForeignStat({ value, label }: StatProps) {
export default function ForeignProfileScreen() { export default function ForeignProfileScreen() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter(); const router = useRouter();
const colors = useColors();
const { userId } = useLocalSearchParams<{ userId: string }>(); const { userId } = useLocalSearchParams<{ userId: string }>();
const [imageFailed, setImageFailed] = useState(false); const [imageFailed, setImageFailed] = useState(false);
const [isFollowing, setIsFollowing] = useState(DUMMY_FOREIGN.isFollowing); const [isFollowing, setIsFollowing] = useState(DUMMY_FOREIGN.isFollowing);
@ -99,13 +101,13 @@ export default function ForeignProfileScreen() {
const planStyle = planColors[profile.plan]; const planStyle = planColors[profile.plan];
return ( return (
<View style={{ flex: 1, backgroundColor: '#ffffff' }}> <View style={{ flex: 1, backgroundColor: colors.bg }}>
<View <View
style={{ style={{
paddingTop: insets.top, paddingTop: insets.top,
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e5e5e5', borderBottomColor: colors.border,
}} }}
> >
<View <View
@ -147,7 +149,7 @@ export default function ForeignProfileScreen() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
backgroundColor: showImage ? '#fafafa' : colors.brandOrange, backgroundColor: showImage ? colors.surface : colors.brandOrange,
}} }}
> >
{showImage ? ( {showImage ? (
@ -215,9 +217,9 @@ export default function ForeignProfileScreen() {
<View style={{ <View style={{
paddingVertical: 11, paddingVertical: 11,
borderRadius: 12, borderRadius: 12,
backgroundColor: isFollowing ? '#f5f5f5' : colors.brandOrange, backgroundColor: isFollowing ? colors.surfaceElevated : colors.brandOrange,
borderWidth: 1, borderWidth: 1,
borderColor: isFollowing ? '#e5e5e5' : colors.brandOrange, borderColor: isFollowing ? colors.border : colors.brandOrange,
alignItems: 'center', alignItems: 'center',
}}> }}>
<Text <Text
@ -244,9 +246,9 @@ export default function ForeignProfileScreen() {
<View style={{ <View style={{
paddingVertical: 11, paddingVertical: 11,
borderRadius: 12, borderRadius: 12,
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
alignItems: 'center', alignItems: 'center',
}}> }}>
<Text <Text
@ -292,9 +294,9 @@ export default function ForeignProfileScreen() {
</Text> </Text>
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 16, padding: 16,
alignItems: 'center', alignItems: 'center',

View File

@ -18,29 +18,30 @@ import * as ImagePicker from 'expo-image-picker';
// TODO(sdk54): migrate to new expo-file-system class-based API — see Task #14 // TODO(sdk54): migrate to new expo-file-system class-based API — see Task #14
import * as FileSystem from 'expo-file-system/legacy'; import * as FileSystem from 'expo-file-system/legacy';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars'; import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars';
import { resolveAvatar } from '../../lib/resolveAvatar'; import { resolveAvatar } from '../../lib/resolveAvatar';
import { apiFetch } from '../../lib/api'; import { apiFetch } from '../../lib/api';
import { useMe } from '../../hooks/useMe'; import { useMe } from '../../hooks/useMe';
const INPUT_STYLE = {
fontSize: 16,
lineHeight: 22,
paddingVertical: 14,
paddingHorizontal: 16,
color: colors.text,
fontFamily: 'Nunito_400Regular',
backgroundColor: '#f5f5f5',
borderRadius: 12,
} as const;
export default function ProfileEditScreen() { export default function ProfileEditScreen() {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const { me, reload } = useMe(); const { me, reload } = useMe();
const INPUT_STYLE = {
fontSize: 16,
lineHeight: 22,
paddingVertical: 14,
paddingHorizontal: 16,
color: colors.text,
fontFamily: 'Nunito_400Regular',
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
} as const;
const [nickname, setNickname] = useState(me?.nickname ?? ''); const [nickname, setNickname] = useState(me?.nickname ?? '');
const [avatarId, setAvatarId] = useState<string | null>(me?.avatar ?? null); const [avatarId, setAvatarId] = useState<string | null>(me?.avatar ?? null);
const [photoUri, setPhotoUri] = useState<string | null>(null); const [photoUri, setPhotoUri] = useState<string | null>(null);

View File

@ -27,7 +27,7 @@ import { supabase } from '../lib/supabase';
import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble'; import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble';
import { ChatInput, type SendPayload } from '../components/chat/ChatInput'; import { ChatInput, type SendPayload } from '../components/chat/ChatInput';
import { useRoomRealtime } from '../hooks/useChatRealtime'; import { useRoomRealtime } from '../hooks/useChatRealtime';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
const GROUP_GAP_MS = 5 * 60 * 1000; const GROUP_GAP_MS = 5 * 60 * 1000;
@ -64,6 +64,8 @@ export default function RoomScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const colors = useColors();
const styles = makeStyles(colors);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const flatRef = useRef<FlatList>(null); const flatRef = useRef<FlatList>(null);
const [myUserId, setMyUserId] = useState<string | undefined>(); const [myUserId, setMyUserId] = useState<string | undefined>();
@ -298,7 +300,7 @@ export default function RoomScreen() {
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<Pressable style={styles.iconBtn} onPress={() => router.back()} hitSlop={8}> <Pressable style={styles.iconBtn} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color="#0a0a0a" /> <Ionicons name="chevron-back" size={22} color={colors.text} />
</Pressable> </Pressable>
<View style={styles.headerCenter}> <View style={styles.headerCenter}>
<View style={styles.headerAvatar}> <View style={styles.headerAvatar}>
@ -320,7 +322,7 @@ export default function RoomScreen() {
</View> </View>
</View> </View>
<Pressable style={styles.iconBtn} onPress={() => setSettingsOpen(true)} hitSlop={8}> <Pressable style={styles.iconBtn} onPress={() => setSettingsOpen(true)} hitSlop={8}>
<Ionicons name="ellipsis-horizontal" size={20} color="#0a0a0a" /> <Ionicons name="ellipsis-horizontal" size={20} color={colors.text} />
</Pressable> </Pressable>
</View> </View>
@ -430,6 +432,8 @@ function RoomSettingsModal({
roomId: string; roomId: string;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const modal = makeModalStyles(colors);
const [pendingRequests, setPendingRequests] = useState<any[]>([]); const [pendingRequests, setPendingRequests] = useState<any[]>([]);
const [loadingReqs, setLoadingReqs] = useState(false); const [loadingReqs, setLoadingReqs] = useState(false);
@ -637,221 +641,225 @@ function RoomSettingsModal({
); );
} }
const styles = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
container: { flex: 1, backgroundColor: '#fafafa' }, return StyleSheet.create({
header: { container: { flex: 1, backgroundColor: colors.bg },
flexDirection: 'row', header: {
alignItems: 'center', flexDirection: 'row',
paddingHorizontal: 12, alignItems: 'center',
paddingVertical: 10, paddingHorizontal: 12,
backgroundColor: '#fff', paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth, backgroundColor: colors.bg,
borderBottomColor: '#e5e5e5', borderBottomWidth: StyleSheet.hairlineWidth,
}, borderBottomColor: colors.border,
iconBtn: { },
width: 36, iconBtn: {
height: 36, width: 36,
borderRadius: 12, height: 36,
backgroundColor: '#f5f5f5', borderRadius: 12,
alignItems: 'center', backgroundColor: colors.surfaceElevated,
justifyContent: 'center', alignItems: 'center',
}, justifyContent: 'center',
headerCenter: { },
flex: 1, headerCenter: {
flexDirection: 'row', flex: 1,
alignItems: 'center', flexDirection: 'row',
marginHorizontal: 8, alignItems: 'center',
}, marginHorizontal: 8,
headerAvatar: { },
width: 36, headerAvatar: {
height: 36, width: 36,
borderRadius: 18, height: 36,
backgroundColor: '#e5e5e5', borderRadius: 18,
alignItems: 'center', backgroundColor: colors.surfaceElevated,
justifyContent: 'center', alignItems: 'center',
overflow: 'hidden', justifyContent: 'center',
marginRight: 8, overflow: 'hidden',
}, marginRight: 8,
headerAvatarImg: { width: 36, height: 36 }, },
headerAvatarInitials: { headerAvatarImg: { width: 36, height: 36 },
fontSize: 12, headerAvatarInitials: {
fontFamily: 'Nunito_700Bold', fontSize: 12,
color: '#737373', fontFamily: 'Nunito_700Bold',
}, color: colors.textMuted,
headerName: { },
fontSize: 15, headerName: {
fontFamily: 'Nunito_700Bold', fontSize: 15,
color: '#171717', fontFamily: 'Nunito_700Bold',
}, color: colors.text,
headerSub: { },
fontSize: 11, headerSub: {
fontFamily: 'Nunito_500Medium', fontSize: 11,
color: '#737373', fontFamily: 'Nunito_500Medium',
marginTop: 1, color: colors.textMuted,
}, marginTop: 1,
loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center' }, },
joinBox: { loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center' },
flex: 1, joinBox: {
alignItems: 'center', flex: 1,
justifyContent: 'center', alignItems: 'center',
paddingHorizontal: 32, justifyContent: 'center',
}, paddingHorizontal: 32,
joinTitle: { },
fontSize: 20, joinTitle: {
fontFamily: 'Nunito_700Bold', fontSize: 20,
color: '#171717', fontFamily: 'Nunito_700Bold',
marginTop: 14, color: colors.text,
}, marginTop: 14,
joinDesc: { },
fontSize: 13, joinDesc: {
fontFamily: 'Nunito_500Medium', fontSize: 13,
color: '#737373', fontFamily: 'Nunito_500Medium',
marginTop: 6, color: colors.textMuted,
textAlign: 'center', marginTop: 6,
}, textAlign: 'center',
joinHint: { },
fontSize: 12, joinHint: {
fontFamily: 'Nunito_500Medium', fontSize: 12,
color: '#a3a3a3', fontFamily: 'Nunito_500Medium',
marginTop: 18, color: colors.textMuted,
textAlign: 'center', marginTop: 18,
}, textAlign: 'center',
joinBtn: { },
marginTop: 16, joinBtn: {
backgroundColor: '#007AFF', marginTop: 16,
paddingHorizontal: 32, backgroundColor: '#007AFF',
paddingVertical: 12, paddingHorizontal: 32,
borderRadius: 12, paddingVertical: 12,
minWidth: 140, borderRadius: 12,
alignItems: 'center', minWidth: 140,
}, alignItems: 'center',
joinBtnText: { },
color: '#fff', joinBtnText: {
fontSize: 14, color: '#fff',
fontFamily: 'Nunito_700Bold', fontSize: 14,
}, fontFamily: 'Nunito_700Bold',
pendingBadge: { },
flexDirection: 'row', pendingBadge: {
alignItems: 'center', flexDirection: 'row',
backgroundColor: '#fef3c7', alignItems: 'center',
paddingHorizontal: 14, backgroundColor: '#fef3c7',
paddingVertical: 8, paddingHorizontal: 14,
borderRadius: 20, paddingVertical: 8,
marginTop: 16, borderRadius: 20,
}, marginTop: 16,
pendingText: { },
color: '#92400e', pendingText: {
fontSize: 12, color: '#92400e',
fontFamily: 'Nunito_700Bold', fontSize: 12,
marginLeft: 6, fontFamily: 'Nunito_700Bold',
}, marginLeft: 6,
}); },
});
}
const modal = StyleSheet.create({ function makeModalStyles(colors: ReturnType<typeof useColors>) {
container: { flex: 1, backgroundColor: '#fafafa' }, return StyleSheet.create({
header: { container: { flex: 1, backgroundColor: colors.bg },
flexDirection: 'row', header: {
alignItems: 'center', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
paddingHorizontal: 16, justifyContent: 'space-between',
paddingVertical: 12, paddingHorizontal: 16,
backgroundColor: '#fff', paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth, backgroundColor: colors.bg,
borderBottomColor: '#e5e5e5', borderBottomWidth: StyleSheet.hairlineWidth,
}, borderBottomColor: colors.border,
title: { fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#171717' }, },
section: { title: { fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text },
backgroundColor: '#fff', section: {
borderRadius: 12, backgroundColor: colors.surface,
padding: 14, borderRadius: 12,
marginBottom: 12, padding: 14,
}, marginBottom: 12,
sectionTitle: { },
fontSize: 12, sectionTitle: {
fontFamily: 'Nunito_700Bold', fontSize: 12,
color: '#737373', fontFamily: 'Nunito_700Bold',
textTransform: 'uppercase', color: colors.textMuted,
marginBottom: 10, textTransform: 'uppercase',
letterSpacing: 0.5, marginBottom: 10,
}, letterSpacing: 0.5,
avatarWrap: { alignSelf: 'center', marginBottom: 10 }, },
avatar: { width: 80, height: 80, borderRadius: 40 }, avatarWrap: { alignSelf: 'center', marginBottom: 10 },
avatarPlaceholder: { avatar: { width: 80, height: 80, borderRadius: 40 },
backgroundColor: '#e5e5e5', avatarPlaceholder: {
alignItems: 'center', backgroundColor: colors.surfaceElevated,
justifyContent: 'center', alignItems: 'center',
}, justifyContent: 'center',
avatarEdit: { },
position: 'absolute', avatarEdit: {
right: -2, position: 'absolute',
bottom: -2, right: -2,
width: 28, bottom: -2,
height: 28, width: 28,
borderRadius: 14, height: 28,
backgroundColor: '#007AFF', borderRadius: 14,
borderWidth: 3, backgroundColor: '#007AFF',
borderColor: '#fff', borderWidth: 3,
alignItems: 'center', borderColor: colors.bg,
justifyContent: 'center', alignItems: 'center',
}, justifyContent: 'center',
roomName: { },
fontSize: 17, roomName: {
fontFamily: 'Nunito_700Bold', fontSize: 17,
color: '#171717', fontFamily: 'Nunito_700Bold',
textAlign: 'center', color: colors.text,
}, textAlign: 'center',
roomDesc: { },
fontSize: 12, roomDesc: {
fontFamily: 'Nunito_500Medium', fontSize: 12,
color: '#737373', fontFamily: 'Nunito_500Medium',
textAlign: 'center', color: colors.textMuted,
marginTop: 4, textAlign: 'center',
}, marginTop: 4,
memberRow: { },
flexDirection: 'row', memberRow: {
alignItems: 'center', flexDirection: 'row',
paddingVertical: 8, alignItems: 'center',
borderBottomWidth: StyleSheet.hairlineWidth, paddingVertical: 8,
borderBottomColor: '#f5f5f5', borderBottomWidth: StyleSheet.hairlineWidth,
}, borderBottomColor: colors.border,
memberAvatar: { },
width: 32, memberAvatar: {
height: 32, width: 32,
borderRadius: 16, height: 32,
backgroundColor: '#e5e5e5', borderRadius: 16,
alignItems: 'center', backgroundColor: colors.surfaceElevated,
justifyContent: 'center', alignItems: 'center',
overflow: 'hidden', justifyContent: 'center',
marginRight: 10, overflow: 'hidden',
}, marginRight: 10,
memberAvatarImg: { width: 32, height: 32 }, },
memberInitials: { memberAvatarImg: { width: 32, height: 32 },
fontSize: 11, memberInitials: {
fontFamily: 'Nunito_700Bold', fontSize: 11,
color: '#737373', fontFamily: 'Nunito_700Bold',
}, color: colors.textMuted,
memberName: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: '#171717' }, },
memberRole: { fontSize: 11, color: '#a3a3a3', marginTop: 1, textTransform: 'capitalize' }, memberName: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text },
actionBtn: { memberRole: { fontSize: 11, color: colors.textMuted, marginTop: 1, textTransform: 'capitalize' },
paddingHorizontal: 10, actionBtn: {
paddingVertical: 5, paddingHorizontal: 10,
borderRadius: 6, paddingVertical: 5,
}, borderRadius: 6,
actionText: { fontSize: 11, fontFamily: 'Nunito_700Bold' }, },
emptyText: { fontSize: 12, color: '#a3a3a3' }, actionText: { fontSize: 11, fontFamily: 'Nunito_700Bold' },
leaveBtn: { emptyText: { fontSize: 12, color: colors.textMuted },
flexDirection: 'row', leaveBtn: {
alignItems: 'center', flexDirection: 'row',
justifyContent: 'center', alignItems: 'center',
backgroundColor: '#fee2e2', justifyContent: 'center',
paddingVertical: 12, backgroundColor: '#fee2e2',
borderRadius: 10, paddingVertical: 12,
marginTop: 8, borderRadius: 10,
}, marginTop: 8,
leaveText: { },
color: '#991b1b', leaveText: {
fontSize: 13, color: '#991b1b',
fontFamily: 'Nunito_700Bold', fontSize: 13,
marginLeft: 6, fontFamily: 'Nunito_700Bold',
}, marginLeft: 6,
}); },
});
}

View File

@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
import { RiveAvatar } from '../components/RiveAvatar'; import { RiveAvatar } from '../components/RiveAvatar';
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 { import {
type GameType, GAME_META, MemoryGame, TicTacToeGame, SnakeGame, TetrisGame, type GameType, GAME_META, MemoryGame, TicTacToeGame, SnakeGame, TetrisGame,
} from '../components/urge/UrgeGames'; } from '../components/urge/UrgeGames';
@ -41,6 +41,8 @@ export default function SOSScreen() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const colors = useColors();
const st = makeStyles(colors);
const flatRef = useRef<FlatList>(null); const flatRef = useRef<FlatList>(null);
const [messages, setMessages] = useState<SosMsg[]>([]); const [messages, setMessages] = useState<SosMsg[]>([]);
@ -1089,7 +1091,7 @@ export default function SOSScreen() {
{/* Header */} {/* Header */}
<View style={[st.topBar, { top: insets.top + 6 }]}> <View style={[st.topBar, { top: insets.top + 6 }]}>
<Pressable style={st.actionBtn} onPress={attemptExit} hitSlop={12}> <Pressable style={st.actionBtn} onPress={attemptExit} hitSlop={12}>
<Ionicons name="close" size={22} color="#374151" /> <Ionicons name="close" size={22} color={colors.textMuted} />
</Pressable> </Pressable>
<View style={st.avatarCenter}> <View style={st.avatarCenter}>
<RiveAvatar emotion={emotion} size="md" /> <RiveAvatar emotion={emotion} size="md" />
@ -1286,39 +1288,41 @@ export default function SOSScreen() {
); );
} }
const st = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
container: { flex: 1, backgroundColor: '#ffffff' }, return StyleSheet.create({
topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 }, container: { flex: 1, backgroundColor: colors.bg },
topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: '#ffffff' }, topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 },
actionBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.92)', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 6, elevation: 4 }, topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: colors.bg },
avatarCenter: { flex: 1, alignItems: 'center', gap: 4 }, actionBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.surface, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 6, elevation: 4 },
avatarMeta: { alignItems: 'center', gap: 2 }, avatarCenter: { flex: 1, alignItems: 'center', gap: 4 },
avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }, avatarMeta: { alignItems: 'center', gap: 2 },
speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text },
stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: '#f5f5f5', alignItems: 'center', justifyContent: 'center' }, speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
listContent: { paddingHorizontal: 12, paddingBottom: 4 }, stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center' },
scrollDownBtn: { position: 'absolute', bottom: 8, right: 16, width: 36, height: 36, borderRadius: 18, backgroundColor: '#374151', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 }, listContent: { paddingHorizontal: 12, paddingBottom: 4 },
chip: { scrollDownBtn: { position: 'absolute', bottom: 8, right: 16, width: 36, height: 36, borderRadius: 18, backgroundColor: '#374151', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 },
borderRadius: 14, chip: {
borderWidth: 1.5, borderRadius: 14,
borderColor: '#9ca3af', // sichtbarer Ring (medium-grau gegen weiß) borderWidth: 1.5,
backgroundColor: '#ffffff', borderColor: colors.border,
paddingHorizontal: 16, backgroundColor: colors.bg,
paddingVertical: 11, paddingHorizontal: 16,
shadowColor: '#000', paddingVertical: 11,
shadowOffset: { width: 0, height: 2 }, shadowColor: '#000',
shadowOpacity: 0.12, shadowOffset: { width: 0, height: 2 },
shadowRadius: 6, shadowOpacity: 0.12,
elevation: 3, shadowRadius: 6,
}, elevation: 3,
chipPressed: { },
backgroundColor: '#f3f4f6', chipPressed: {
borderColor: '#6b7280', // dunkler beim Press → spürbares Feedback backgroundColor: colors.surfaceElevated,
transform: [{ scale: 0.97 }], borderColor: colors.textMuted,
shadowOpacity: 0.05, transform: [{ scale: 0.97 }],
}, shadowOpacity: 0.05,
chipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 14, color: '#334155' }, },
inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: '#f3f4f6', backgroundColor: '#fff', gap: 8 }, chipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 14, color: colors.textMuted },
textInput: { flex: 1, minHeight: 40, maxHeight: 120, backgroundColor: '#f3f4f6', borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, fontFamily: 'Nunito_400Regular', color: '#111827' }, inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.bg, gap: 8 },
sendBtn: { width: 38, height: 38, borderRadius: 19, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center' }, textInput: { flex: 1, minHeight: 40, maxHeight: 120, backgroundColor: colors.surfaceElevated, borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, fontFamily: 'Nunito_400Regular', color: colors.text },
}); sendBtn: { width: 38, height: 38, borderRadius: 19, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center' },
});
}

View File

@ -17,7 +17,7 @@ import * as ImagePicker from 'expo-image-picker';
import { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
import { resolveAvatar } from '../lib/resolveAvatar'; import { resolveAvatar } from '../lib/resolveAvatar';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
type Props = { type Props = {
onPosted?: () => void; onPosted?: () => void;
@ -25,6 +25,7 @@ type Props = {
export function ComposeCard({ onPosted }: Props) { export function ComposeCard({ onPosted }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const { user } = useAuthStore(); const { user } = useAuthStore();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const inputRef = useRef<TextInput>(null); const inputRef = useRef<TextInput>(null);
@ -101,7 +102,7 @@ export function ComposeCard({ onPosted }: Props) {
const showActions = focused || content.length > 0; const showActions = focused || content.length > 0;
return ( return (
<View className="bg-white border border-neutral-200 rounded-2xl p-4 mb-4"> <View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 16, marginBottom: 16 }}>
<View className="flex-row items-start gap-3"> <View className="flex-row items-start gap-3">
<Image <Image
source={{ uri: avatarUrl }} source={{ uri: avatarUrl }}
@ -114,10 +115,10 @@ export function ComposeCard({ onPosted }: Props) {
onChangeText={setContent} onChangeText={setContent}
onFocus={() => setFocused(true)} onFocus={() => setFocused(true)}
placeholder={t('community.compose_placeholder')} placeholder={t('community.compose_placeholder')}
placeholderTextColor="#a3a3a3" placeholderTextColor={colors.textMuted}
multiline multiline
className="text-sm text-neutral-900 leading-5 min-h-[40px]" className="text-sm leading-5 min-h-[40px]"
style={{ textAlignVertical: 'top', fontFamily: 'Nunito_400Regular' }} style={{ textAlignVertical: 'top', fontFamily: 'Nunito_400Regular', color: colors.text }}
/> />
{imageUri && ( {imageUri && (
<View className="relative mt-2"> <View className="relative mt-2">

View File

@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react';
import { TrueSheet, type SheetDetent } from '@lodev09/react-native-true-sheet'; import { TrueSheet, type SheetDetent } from '@lodev09/react-native-true-sheet';
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 { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
import { useDeviceLimitStore, type DeviceLimitDevice } from '../stores/deviceLimit'; import { useDeviceLimitStore, type DeviceLimitDevice } from '../stores/deviceLimit';
@ -40,6 +40,7 @@ function DeviceLimitRow({
onRemove: (id: string) => void; onRemove: (id: string) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
return ( return (
<View <View
@ -119,6 +120,7 @@ function DeviceLimitRow({
export function DeviceLimitReachedSheet() { export function DeviceLimitReachedSheet() {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const sheetRef = useRef<TrueSheet>(null); const sheetRef = useRef<TrueSheet>(null);
const { visible, devices, max, plan, hide, removeDevice } = useDeviceLimitStore(); const { visible, devices, max, plan, hide, removeDevice } = useDeviceLimitStore();
const [removingId, setRemovingId] = useState<string | null>(null); const [removingId, setRemovingId] = useState<string | null>(null);
@ -195,7 +197,7 @@ export function DeviceLimitReachedSheet() {
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderRadius: 14, borderRadius: 14,
overflow: 'hidden', overflow: 'hidden',
borderWidth: 1, borderWidth: 1,

View File

@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useNotificationStore, type AppNotification } from '../stores/notifications'; import { useNotificationStore, type AppNotification } from '../stores/notifications';
import { resolveAvatar } from '../lib/resolveAvatar'; import { resolveAvatar } from '../lib/resolveAvatar';
import { HeroShieldCheck } from './HeroShieldCheck'; import { HeroShieldCheck } from './HeroShieldCheck';
import { useColors } from '../lib/theme';
type Props = { type Props = {
visible: boolean; visible: boolean;
@ -16,6 +17,7 @@ type Props = {
export function NotificationsDropdown({ visible, onClose, topOffset }: Props) { export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const router = useRouter(); const router = useRouter();
const items = useNotificationStore((s) => s.items); const items = useNotificationStore((s) => s.items);
const loaded = useNotificationStore((s) => s.loaded); const loaded = useNotificationStore((s) => s.loaded);
@ -71,7 +73,7 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
position: 'absolute', position: 'absolute',
top: topOffset + 6, top: topOffset + 6,
right: 12, right: 12,
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderRadius: 18, borderRadius: 18,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 8 }, shadowOffset: { width: 0, height: 8 },
@ -93,11 +95,11 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f0f0f0', borderBottomColor: colors.border,
}} }}
> >
<Text <Text
style={{ flex: 1, fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }} style={{ flex: 1, fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}
> >
{t('notifications.title')} {t('notifications.title')}
</Text> </Text>
@ -114,13 +116,13 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
{items.length === 0 ? ( {items.length === 0 ? (
<View style={{ paddingVertical: 32, alignItems: 'center' }}> <View style={{ paddingVertical: 32, alignItems: 'center' }}>
<Ionicons name="notifications-off-outline" size={28} color="#a3a3a3" /> <Ionicons name="notifications-off-outline" size={28} color={colors.textMuted} />
<Text <Text
style={{ style={{
marginTop: 8, marginTop: 8,
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#525252', color: colors.textMuted,
}} }}
> >
{t('notifications.empty_title')} {t('notifications.empty_title')}
@ -130,7 +132,7 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
marginTop: 2, marginTop: 2,
fontSize: 11, fontSize: 11,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#a3a3a3', color: colors.textMuted,
textAlign: 'center', textAlign: 'center',
paddingHorizontal: 20, paddingHorizontal: 20,
}} }}
@ -221,6 +223,7 @@ function NotificationRow({
onPress: () => void; onPress: () => void;
t: (k: string, opts?: any) => string; t: (k: string, opts?: any) => string;
}) { }) {
const colors = useColors();
const isUnread = !notif.readAt; const isUnread = !notif.readAt;
const { icon, color, bg } = notifIcon(notif.type); const { icon, color, bg } = notifIcon(notif.type);
const isSocial = const isSocial =
@ -249,8 +252,8 @@ function NotificationRow({
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 11, paddingVertical: 11,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f5f5f5', borderBottomColor: colors.border,
backgroundColor: isUnread ? '#fff7ed' : '#ffffff', backgroundColor: isUnread ? colors.surface : colors.bg,
}} }}
> >
{/* Avatar-Logik: {/* Avatar-Logik:
@ -297,7 +300,7 @@ function NotificationRow({
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#0a0a0a', color: colors.text,
lineHeight: 16, lineHeight: 16,
}} }}
numberOfLines={2} numberOfLines={2}
@ -308,7 +311,7 @@ function NotificationRow({
style={{ style={{
fontSize: 10, fontSize: 10,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#a3a3a3', color: colors.textMuted,
marginTop: 2, marginTop: 2,
}} }}
> >

View File

@ -22,7 +22,7 @@ import {
Easing, Easing,
} from 'react-native'; } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
type Option<T> = { type Option<T> = {
value: T; value: T;
@ -51,6 +51,7 @@ export function OptionsBottomSheet<T extends string | number>({
onClose, onClose,
}: Props<T>) { }: Props<T>) {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const colors = useColors();
const translateY = useRef(new Animated.Value(400)).current; const translateY = useRef(new Animated.Value(400)).current;
const backdropOpacity = useRef(new Animated.Value(0)).current; const backdropOpacity = useRef(new Animated.Value(0)).current;

View File

@ -9,6 +9,7 @@ import { formatRelativeTime } from '../lib/formatTime';
import { useCommunityStore, type CommunityPost } from '../stores/community'; import { useCommunityStore, type CommunityPost } from '../stores/community';
import { RiveAvatar } from './RiveAvatar'; import { RiveAvatar } from './RiveAvatar';
import { HeroShieldCheck } from './HeroShieldCheck'; import { HeroShieldCheck } from './HeroShieldCheck';
import { useColors } from '../lib/theme';
type Props = { type Props = {
post: CommunityPost; post: CommunityPost;
@ -17,6 +18,7 @@ type Props = {
function PostCardImpl({ post, onCommentPress }: Props) { function PostCardImpl({ post, onCommentPress }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Granular selectors — subscribing to the whole store would re-render every // Granular selectors — subscribing to the whole store would re-render every
// PostCard whenever any user likes any post (optimisticLikes mutates). // PostCard whenever any user likes any post (optimisticLikes mutates).
@ -162,7 +164,7 @@ function PostCardImpl({ post, onCommentPress }: Props) {
}, [isLiking, localLike, localCount, post.id, post.userLike, post.likesCount, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]); }, [isLiking, localLike, localCount, post.id, post.userLike, post.likesCount, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]);
return ( return (
<View className="bg-white border border-neutral-200 rounded-2xl p-3 mb-3"> <View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 12, marginBottom: 12 }}>
{/* Repost header */} {/* Repost header */}
{post.repostOf && ( {post.repostOf && (
<View className="flex-row items-center gap-1.5 mb-3"> <View className="flex-row items-center gap-1.5 mb-3">
@ -194,35 +196,35 @@ function PostCardImpl({ post, onCommentPress }: Props) {
</View> </View>
)} )}
<View className="flex-1 min-w-0"> <View className="flex-1 min-w-0">
<Text className="text-sm text-neutral-900" numberOfLines={1} style={{ fontFamily: 'Nunito_600SemiBold' }}> <Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_600SemiBold' }} numberOfLines={1}>
{authorLabel} {authorLabel}
</Text> </Text>
{authorDescription !== undefined && ( {authorDescription !== undefined && (
<Text className="text-xs text-neutral-400" style={{ fontFamily: 'Nunito_400Regular' }}>{authorDescription}</Text> <Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>{authorDescription}</Text>
)} )}
</View> </View>
</View> </View>
<Text className="text-xs text-neutral-400 shrink-0 ml-2 mt-0.5" style={{ fontFamily: 'Nunito_400Regular' }}> <Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular', flexShrink: 0, marginLeft: 8, marginTop: 2 }}>
{formatRelativeTime(post.createdAt)} {formatRelativeTime(post.createdAt)}
</Text> </Text>
</View> </View>
{/* Content — hidden for domain_vote (replaced by poll below) */} {/* Content — hidden for domain_vote (replaced by poll below) */}
{!!displayContent && post.category !== 'domain_vote' && ( {!!displayContent && post.category !== 'domain_vote' && (
<Text className="text-sm text-neutral-800 leading-relaxed mb-0" style={{ fontFamily: 'Nunito_400Regular' }}> <Text style={{ fontSize: 14, color: colors.textMuted, fontFamily: 'Nunito_400Regular', lineHeight: 21 }}>
{displayContent} {displayContent}
</Text> </Text>
)} )}
{/* domain_approved: favicon + domain name + shield badge */} {/* domain_approved: favicon + domain name + shield badge */}
{post.category === 'domain_approved' && !!approvedDomain && ( {post.category === 'domain_approved' && !!approvedDomain && (
<View className="flex-row items-center gap-2.5 mt-3 rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2"> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 12, borderRadius: 12, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface, paddingHorizontal: 12, paddingVertical: 8 }}>
<DomainFavicon domain={approvedDomain} size={24} /> <DomainFavicon domain={approvedDomain} size={24} />
<View className="flex-1 min-w-0"> <View style={{ flex: 1, minWidth: 0 }}>
<Text className="text-xs font-semibold text-neutral-900 truncate" style={{ fontFamily: 'Nunito_700Bold' }}> <Text style={{ fontSize: 12, color: colors.text, fontFamily: 'Nunito_700Bold' }} numberOfLines={1}>
{approvedDomain} {approvedDomain}
</Text> </Text>
<Text className="text-xs text-neutral-400" style={{ fontFamily: 'Nunito_400Regular' }}> <Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>
{t('community.domain_added_to_blocklist')} {t('community.domain_added_to_blocklist')}
</Text> </Text>
</View> </View>

View File

@ -1,18 +1,20 @@
import { View } from 'react-native'; import { View } from 'react-native';
import { useColors } from '../lib/theme';
export function PostCardSkeleton() { export function PostCardSkeleton() {
const colors = useColors();
return ( return (
<View className="bg-white border border-neutral-200 rounded-2xl p-4 mb-3"> <View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 16, marginBottom: 12 }}>
<View className="flex-row items-center gap-3 mb-3"> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 12 }}>
<View className="w-9 h-9 rounded-full bg-neutral-200" /> <View style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: colors.surfaceElevated }} />
<View className="flex-1 gap-1.5"> <View style={{ flex: 1, gap: 6 }}>
<View className="h-3 bg-neutral-200 rounded w-1/3" /> <View style={{ height: 12, backgroundColor: colors.surfaceElevated, borderRadius: 6, width: '33%' }} />
<View className="h-2.5 bg-neutral-100 rounded w-1/4" /> <View style={{ height: 10, backgroundColor: colors.surface, borderRadius: 6, width: '25%' }} />
</View> </View>
</View> </View>
<View className="h-3 bg-neutral-200 rounded w-full mb-2" /> <View style={{ height: 12, backgroundColor: colors.surfaceElevated, borderRadius: 6, width: '100%', marginBottom: 8 }} />
<View className="h-3 bg-neutral-200 rounded w-3/4 mb-2" /> <View style={{ height: 12, backgroundColor: colors.surfaceElevated, borderRadius: 6, width: '75%', marginBottom: 8 }} />
<View className="h-3 bg-neutral-100 rounded w-1/2" /> <View style={{ height: 12, backgroundColor: colors.surface, borderRadius: 6, width: '50%' }} />
</View> </View>
); );
} }

View File

@ -19,7 +19,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
import { formatRelativeTime } from '../lib/formatTime'; import { formatRelativeTime } from '../lib/formatTime';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
import type { CommunityComment } from '../stores/community'; import type { CommunityComment } from '../stores/community';
const EMOJIS = ['❤️', '🙌', '🔥', '👏', '😢', '😍', '😮', '😂']; const EMOJIS = ['❤️', '🙌', '🔥', '👏', '😢', '😍', '😮', '😂'];
@ -33,6 +33,7 @@ type Props = {
export function PostCommentsSheet({ postId, visible, onClose }: Props) { export function PostCommentsSheet({ postId, visible, onClose }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const inputRef = useRef<TextInput>(null); const inputRef = useRef<TextInput>(null);
@ -230,7 +231,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
<Animated.View <Animated.View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderTopLeftRadius: 24, borderTopLeftRadius: 24,
borderTopRightRadius: 24, borderTopRightRadius: 24,
overflow: 'hidden', overflow: 'hidden',
@ -255,7 +256,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
width: 36, width: 36,
height: 5, height: 5,
borderRadius: 3, borderRadius: 3,
backgroundColor: '#d4d4d8', backgroundColor: colors.border,
}} }}
/> />
</View> </View>
@ -268,10 +269,10 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
paddingTop: 6, paddingTop: 6,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e5e5e5', borderBottomColor: colors.border,
}} }}
> >
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('community.comments_title')} {t('community.comments_title')}
</Text> </Text>
</View> </View>
@ -323,7 +324,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 8, paddingVertical: 8,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#f5f5f5', borderTopColor: colors.border,
}} }}
> >
{EMOJIS.map((e) => ( {EMOJIS.map((e) => (
@ -342,10 +343,10 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 8, paddingVertical: 8,
backgroundColor: '#fafafa', backgroundColor: colors.surface,
}} }}
> >
<Text style={{ fontSize: 12, color: '#737373', fontFamily: 'Nunito_400Regular' }}> <Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>
{t('community.reply_to')}{' '} {t('community.reply_to')}{' '}
<Text style={{ fontFamily: 'Nunito_700Bold' }}>@{replyTarget.nickname}</Text> <Text style={{ fontFamily: 'Nunito_700Bold' }}>@{replyTarget.nickname}</Text>
</Text> </Text>
@ -366,7 +367,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
// sonst Safe-Area // sonst Safe-Area
paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom), paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom),
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#e5e5e5', borderTopColor: colors.border,
}} }}
> >
<TextInput <TextInput
@ -374,18 +375,18 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
value={text} value={text}
onChangeText={setText} onChangeText={setText}
placeholder={t('community.comment_placeholder')} placeholder={t('community.comment_placeholder')}
placeholderTextColor="#a3a3a3" placeholderTextColor={colors.textMuted}
style={{ style={{
flex: 1, flex: 1,
backgroundColor: '#fafafa', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 999, borderRadius: 999,
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 10, paddingVertical: 10,
fontSize: 14, fontSize: 14,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#0a0a0a', color: colors.text,
marginRight: 8, marginRight: 8,
}} }}
returnKeyType="send" returnKeyType="send"
@ -430,6 +431,7 @@ type CommentRowProps = {
function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowProps) { function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const heartScale = useRef(new Animated.Value(1)).current; const heartScale = useRef(new Animated.Value(1)).current;
const handleLikeWithPop = useCallback(() => { const handleLikeWithPop = useCallback(() => {
heartScale.setValue(1); heartScale.setValue(1);
@ -447,26 +449,26 @@ function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowPro
width: isReply ? 24 : 32, width: isReply ? 24 : 32,
height: isReply ? 24 : 32, height: isReply ? 24 : 32,
borderRadius: isReply ? 12 : 16, borderRadius: isReply ? 12 : 16,
backgroundColor: '#e5e5e5', backgroundColor: colors.surfaceElevated,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginTop: 2, marginTop: 2,
}} }}
> >
<Text style={{ fontSize: isReply ? 9 : 11, fontFamily: 'Nunito_700Bold', color: '#737373' }}> <Text style={{ fontSize: isReply ? 9 : 11, fontFamily: 'Nunito_700Bold', color: colors.textMuted }}>
{(comment.authorNickname ?? 'AN').slice(0, 2).toUpperCase()} {(comment.authorNickname ?? 'AN').slice(0, 2).toUpperCase()}
</Text> </Text>
</View> </View>
<View style={{ flex: 1, minWidth: 0 }}> <View style={{ flex: 1, minWidth: 0 }}>
<Text style={{ fontSize: 12, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 12, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{comment.authorNickname ?? t('community.anonymous_label')} {comment.authorNickname ?? t('community.anonymous_label')}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: 14, fontSize: 14,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#404040', color: colors.textMuted,
lineHeight: 20, lineHeight: 20,
marginTop: 2, marginTop: 2,
}} }}
@ -474,12 +476,12 @@ function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowPro
{comment.content} {comment.content}
</Text> </Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16, marginTop: 6 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16, marginTop: 6 }}>
<Text style={{ fontSize: 10, color: '#a3a3a3', fontFamily: 'Nunito_400Regular' }}> <Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>
{formatRelativeTime(comment.createdAt)} {formatRelativeTime(comment.createdAt)}
</Text> </Text>
{!isReply && onReply && ( {!isReply && onReply && (
<Pressable onPress={onReply}> <Pressable onPress={onReply}>
<Text style={{ fontSize: 11, color: '#a3a3a3', fontFamily: 'Nunito_600SemiBold' }}> <Text style={{ fontSize: 11, color: colors.textMuted, fontFamily: 'Nunito_600SemiBold' }}>
{t('community.reply')} {t('community.reply')}
</Text> </Text>
</Pressable> </Pressable>

View File

@ -1,7 +1,7 @@
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 { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
type Props = { type Props = {
days: number; days: number;
@ -16,14 +16,18 @@ const sizeMap = {
export function StreakBadge({ days, size = 'md' }: Props) { export function StreakBadge({ days, size = 'md' }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const s = sizeMap[size]; const s = sizeMap[size];
return ( return (
<View className={`items-center bg-white border border-neutral-200 rounded-3xl ${s.padding}`}> <View
className={`items-center rounded-3xl ${s.padding}`}
style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border }}
>
<View className="flex-row items-center gap-2 mb-1"> <View className="flex-row items-center gap-2 mb-1">
<Ionicons name="flame" size={s.icon} color={colors.brandOrange} /> <Ionicons name="flame" size={s.icon} color={colors.brandOrange} />
<Text className={`${s.number} text-neutral-900 tabular-nums`} style={{ fontFamily: 'Nunito_800ExtraBold' }}>{days}</Text> <Text className={`${s.number} tabular-nums`} style={{ fontFamily: 'Nunito_800ExtraBold', color: colors.text }}>{days}</Text>
</View> </View>
<Text className={`${s.label} text-neutral-500 tracking-wide uppercase`} style={{ fontFamily: 'Nunito_600SemiBold' }}> <Text className={`${s.label} tracking-wide uppercase`} style={{ fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}>
{days === 1 ? t('streak.label_one') : t('streak.label_other')} {t('streak.label_suffix')} {days === 1 ? t('streak.label_one') : t('streak.label_other')} {t('streak.label_suffix')}
</Text> </Text>
</View> </View>

View File

@ -14,7 +14,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Modal, View, Text, Pressable } from 'react-native'; import { Modal, View, Text, Pressable } from 'react-native';
import { Picker } from '@react-native-picker/picker'; import { Picker } from '@react-native-picker/picker';
import { colors } from '../lib/theme'; import { useColors } from '../lib/theme';
type Option<T> = { value: T; label: string }; type Option<T> = { value: T; label: string };
@ -35,6 +35,7 @@ export function WheelPickerModal<T extends string | number>({
onSelect, onSelect,
onClose, onClose,
}: Props<T>) { }: Props<T>) {
const colors = useColors();
// Tracks the wheel's current selection (separate from confirmed value). // Tracks the wheel's current selection (separate from confirmed value).
// Initialized from `value` prop on each open. // Initialized from `value` prop on each open.
const [tempValue, setTempValue] = useState<T | null>(value); const [tempValue, setTempValue] = useState<T | null>(value);
@ -72,7 +73,7 @@ export function WheelPickerModal<T extends string | number>({
<Pressable onPress={() => {}}> <Pressable onPress={() => {}}>
<View <View
style={{ style={{
backgroundColor: '#ffffff', backgroundColor: colors.bg,
borderTopLeftRadius: 18, borderTopLeftRadius: 18,
borderTopRightRadius: 18, borderTopRightRadius: 18,
paddingBottom: 24, paddingBottom: 24,
@ -87,7 +88,7 @@ export function WheelPickerModal<T extends string | number>({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e5e5e5', borderBottomColor: colors.border,
}} }}
> >
<Pressable onPress={onClose} hitSlop={10}> <Pressable onPress={onClose} hitSlop={10}>

View File

@ -21,6 +21,7 @@ import {
normalizeDomain, normalizeDomain,
type Tier, type Tier,
} from '../../hooks/useCustomDomains'; } from '../../hooks/useCustomDomains';
import { useColors } from '../../lib/theme';
const SCREEN_HEIGHT = Dimensions.get('window').height; const SCREEN_HEIGHT = Dimensions.get('window').height;
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65; // wie bei PostCommentsSheet — 65% der Screen-Höhe const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65; // wie bei PostCommentsSheet — 65% der Screen-Höhe
@ -34,6 +35,7 @@ type Props = {
export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [confirmPermanent, setConfirmPermanent] = useState(false); const [confirmPermanent, setConfirmPermanent] = useState(false);
@ -122,7 +124,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
right: 0, right: 0,
bottom: 0, bottom: 0,
height: SHEET_HEIGHT, height: SHEET_HEIGHT,
backgroundColor: '#fff', backgroundColor: colors.bg,
borderTopLeftRadius: 20, borderTopLeftRadius: 20,
borderTopRightRadius: 20, borderTopRightRadius: 20,
transform: [{ translateY }], transform: [{ translateY }],
@ -134,7 +136,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
> >
{/* Drag-handle */} {/* Drag-handle */}
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}> <View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: '#d4d4d4' }} /> <View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: colors.border }} />
</View> </View>
{/* Header */} {/* Header */}
@ -147,15 +149,15 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
paddingTop: 6, paddingTop: 6,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f0f0f0', borderBottomColor: colors.border,
}} }}
> >
<Pressable onPress={close} hitSlop={10}> <Pressable onPress={close} hitSlop={10}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('common.cancel')} {t('common.cancel')}
</Text> </Text>
</Pressable> </Pressable>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('blocker.add_sheet_title')} {t('blocker.add_sheet_title')}
</Text> </Text>
<View style={{ width: 60 }} /> <View style={{ width: 60 }} />
@ -168,7 +170,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#525252', color: colors.textMuted,
marginBottom: 6, marginBottom: 6,
}} }}
> >
@ -181,7 +183,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
setError(null); setError(null);
}} }}
placeholder={t('blocker.add_sheet_placeholder')} placeholder={t('blocker.add_sheet_placeholder')}
placeholderTextColor="#a3a3a3" placeholderTextColor={colors.textMuted}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
autoFocus autoFocus
@ -189,13 +191,13 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
returnKeyType="done" returnKeyType="done"
onSubmitEditing={handleAdd} onSubmitEditing={handleAdd}
style={{ style={{
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 12, borderRadius: 12,
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 12, paddingVertical: 12,
fontSize: 15, fontSize: 15,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#0a0a0a', color: colors.text,
}} }}
/> />
{input && !valid && ( {input && !valid && (
@ -220,7 +222,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
alignItems: 'center', alignItems: 'center',
gap: 10, gap: 10,
padding: 12, padding: 12,
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 12, borderRadius: 12,
}} }}
> >
@ -235,7 +237,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
flex: 1, flex: 1,
fontSize: 14, fontSize: 14,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#0a0a0a', color: colors.text,
}} }}
numberOfLines={1} numberOfLines={1}
> >
@ -289,8 +291,8 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
height: 22, height: 22,
borderRadius: 6, borderRadius: 6,
borderWidth: 1.5, borderWidth: 1.5,
borderColor: confirmPermanent ? '#16a34a' : '#d4d4d4', borderColor: confirmPermanent ? colors.success : colors.border,
backgroundColor: confirmPermanent ? '#16a34a' : '#fff', backgroundColor: confirmPermanent ? colors.success : colors.bg,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginTop: 1, marginTop: 1,
@ -303,7 +305,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
flex: 1, flex: 1,
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#0a0a0a', color: colors.text,
lineHeight: 18, lineHeight: 18,
}} }}
> >

View File

@ -2,6 +2,7 @@ import { View, Text, Pressable, ActivityIndicator } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
type Props = { type Props = {
remainingFormatted: string; // "23:59:42" remainingFormatted: string; // "23:59:42"
@ -10,6 +11,7 @@ type Props = {
export function CooldownBanner({ remainingFormatted, onCancel }: Props) { export function CooldownBanner({ remainingFormatted, onCancel }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const [cancelling, setCancelling] = useState(false); const [cancelling, setCancelling] = useState(false);
async function handleCancel() { async function handleCancel() {

View File

@ -2,6 +2,7 @@ import { Modal, View, Text, Pressable, ScrollView, ActionSheetIOS, Platform, Ale
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
type Props = { type Props = {
visible: boolean; visible: boolean;
@ -26,6 +27,7 @@ export function DeactivationExplainerSheet({
onStartCooldown, onStartCooldown,
}: Props) { }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
function showFinalConfirm() { function showFinalConfirm() {
@ -74,7 +76,7 @@ export function DeactivationExplainerSheet({
presentationStyle="pageSheet" presentationStyle="pageSheet"
onRequestClose={onClose} onRequestClose={onClose}
> >
<View style={{ flex: 1, backgroundColor: '#fff' }}> <View style={{ flex: 1, backgroundColor: colors.bg }}>
{/* Header */} {/* Header */}
<View <View
style={{ style={{
@ -85,29 +87,29 @@ export function DeactivationExplainerSheet({
paddingTop: 14, paddingTop: 14,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f0f0f0', borderBottomColor: colors.border,
}} }}
> >
<Pressable onPress={onClose} hitSlop={10}> <Pressable onPress={onClose} hitSlop={10}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('common.back')} {t('common.back')}
</Text> </Text>
</Pressable> </Pressable>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('blocker.deactivation_heading')} {t('blocker.deactivation_heading')}
</Text> </Text>
<View style={{ width: 50 }} /> <View style={{ width: 50 }} />
</View> </View>
<ScrollView contentContainerStyle={{ padding: 20, gap: 18 }}> <ScrollView contentContainerStyle={{ padding: 20, gap: 18 }}>
<Text style={{ fontSize: 22, fontFamily: 'Nunito_800ExtraBold', color: '#0a0a0a' }}> <Text style={{ fontSize: 22, fontFamily: 'Nunito_800ExtraBold', color: colors.text }}>
{t('blocker.deactivation_title')} {t('blocker.deactivation_title')}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: 15, fontSize: 15,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#404040', color: colors.textMuted,
lineHeight: 22, lineHeight: 22,
}} }}
> >
@ -195,6 +197,7 @@ function BulletRow({
title: string; title: string;
text: string; text: string;
}) { }) {
const colors = useColors();
return ( return (
<View style={{ flexDirection: 'row', gap: 12 }}> <View style={{ flexDirection: 'row', gap: 12 }}>
<View <View
@ -202,22 +205,22 @@ function BulletRow({
width: 36, width: 36,
height: 36, height: 36,
borderRadius: 10, borderRadius: 10,
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<Ionicons name={icon} size={18} color="#525252" /> <Ionicons name={icon} size={18} color={colors.textMuted} />
</View> </View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{title} {title}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#525252', color: colors.textMuted,
marginTop: 2, marginTop: 2,
lineHeight: 17, lineHeight: 17,
}} }}

View File

@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
import { SuccessAlert } from '../SuccessAlert'; import { SuccessAlert } from '../SuccessAlert';
import { ConfirmAlert } from '../ConfirmAlert'; import { ConfirmAlert } from '../ConfirmAlert';
import type { CustomDomain, Tier } from '../../hooks/useCustomDomains'; import type { CustomDomain, Tier } from '../../hooks/useCustomDomains';
import { useColors } from '../../lib/theme';
// ─── Helpers ───────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────
@ -65,6 +66,7 @@ const STATUS_PRIORITY: Record<string, number> = {
export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) { export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
// Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority, // Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority,
// innerhalb gleicher Priority dann newest-first by addedAt. // innerhalb gleicher Priority dann newest-first by addedAt.
const visible = useMemo(() => { const visible = useMemo(() => {
@ -85,7 +87,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
<View style={{ gap: 12 }}> <View style={{ gap: 12 }}>
{/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */} {/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('blocker.domain_section_title')} {t('blocker.domain_section_title')}
</Text> </Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
@ -122,7 +124,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
const pct = (tier.usedSlots / tier.domainLimit) * 100; const pct = (tier.usedSlots / tier.domainLimit) * 100;
const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a'; const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a';
return ( return (
<View style={{ height: 4, borderRadius: 2, backgroundColor: '#f0f0f0', overflow: 'hidden' }}> <View style={{ height: 4, borderRadius: 2, backgroundColor: colors.surfaceElevated, overflow: 'hidden' }}>
<View <View
style={{ style={{
height: '100%', height: '100%',
@ -174,16 +176,16 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
borderRadius: 14, borderRadius: 14,
borderWidth: 1, borderWidth: 1,
borderStyle: 'dashed', borderStyle: 'dashed',
borderColor: '#d4d4d4', borderColor: colors.border,
alignItems: 'center', alignItems: 'center',
}} }}
> >
<Ionicons name="globe-outline" size={28} color="#a3a3a3" /> <Ionicons name="globe-outline" size={28} color={colors.textMuted} />
<Text <Text
style={{ style={{
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#737373', color: colors.textMuted,
marginTop: 8, marginTop: 8,
textAlign: 'center', textAlign: 'center',
}} }}
@ -205,8 +207,9 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
// ─── SlotPill ───────────────────────────────────────────────────────────── // ─── SlotPill ─────────────────────────────────────────────────────────────
function SlotPill({ tier }: { tier: Tier }) { function SlotPill({ tier }: { tier: Tier }) {
const bg = tier.atLimit ? '#fee2e2' : '#f5f5f5'; const colors = useColors();
const fg = tier.atLimit ? '#dc2626' : '#525252'; const bg = tier.atLimit ? '#fee2e2' : colors.surfaceElevated;
const fg = tier.atLimit ? '#dc2626' : colors.textMuted;
return ( return (
<View <View
style={{ style={{
@ -258,6 +261,7 @@ function DomainTile({
onSubmit?: (id: string) => Promise<{ ok: boolean }>; onSubmit?: (id: string) => Promise<{ ok: boolean }>;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [imgError, setImgError] = useState(false); const [imgError, setImgError] = useState(false);
const [successVisible, setSuccessVisible] = useState(false); const [successVisible, setSuccessVisible] = useState(false);
@ -346,9 +350,9 @@ function DomainTile({
return ( return (
<View <View
style={{ style={{
backgroundColor: '#fff', backgroundColor: colors.bg,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 8, padding: 8,
// KEIN aspectRatio:1 mehr — der hat den Button auf 0 Höhe gepresst. // KEIN aspectRatio:1 mehr — der hat den Button auf 0 Höhe gepresst.
@ -417,7 +421,7 @@ function DomainTile({
style={{ style={{
fontSize: 10, fontSize: 10,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#0a0a0a', color: colors.text,
textAlign: 'center', textAlign: 'center',
width: '100%', width: '100%',
}} }}

View File

@ -2,7 +2,7 @@ import { View, Text, Switch, Pressable, ActivityIndicator } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ProtectionState } from '../../lib/protection'; import type { ProtectionState } from '../../lib/protection';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
type Props = { type Props = {
state: ProtectionState; state: ProtectionState;
@ -15,6 +15,7 @@ type Props = {
export function ProtectionCard({ state, loading, onActivate, onPressSettings }: Props) { export function ProtectionCard({ state, loading, onActivate, onPressSettings }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const isActive = state.phase === 'active' || state.phase === 'cooldownActive'; const isActive = state.phase === 'active' || state.phase === 'cooldownActive';
const isCooldown = state.phase === 'cooldownActive'; const isCooldown = state.phase === 'cooldownActive';
@ -30,8 +31,8 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
return t('blocker.protection_subtitle_pro'); return t('blocker.protection_subtitle_pro');
})(); })();
const cardBg = isCooldown ? '#fef3c7' : isActive ? '#dcfce7' : '#ffffff'; const cardBg = isCooldown ? '#fef3c7' : isActive ? '#dcfce7' : colors.bg;
const cardBorder = isCooldown ? '#fcd34d' : isActive ? '#86efac' : '#e5e5e5'; const cardBorder = isCooldown ? '#fcd34d' : isActive ? '#86efac' : colors.border;
const iconBg = isCooldown ? '#fde68a' : isActive ? '#bbf7d0' : '#f5f5f5'; const iconBg = isCooldown ? '#fde68a' : isActive ? '#bbf7d0' : '#f5f5f5';
const iconColor = isCooldown ? '#d97706' : isActive ? '#16a34a' : '#a3a3a3'; const iconColor = isCooldown ? '#d97706' : isActive ? '#16a34a' : '#a3a3a3';
@ -67,7 +68,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
style={{ style={{
fontSize: 15, fontSize: 15,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#0a0a0a', color: colors.text,
}} }}
> >
{t('blocker.protection_card_title')} {t('blocker.protection_card_title')}
@ -76,7 +77,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#525252', color: colors.textMuted,
marginTop: 2, marginTop: 2,
}} }}
> >
@ -100,7 +101,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
width: 36, width: 36,
height: 36, height: 36,
borderRadius: 18, borderRadius: 18,
backgroundColor: '#ffffff', backgroundColor: colors.surface,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
shadowColor: '#000', shadowColor: '#000',
@ -108,7 +109,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
shadowOpacity: 0.05, shadowOpacity: 0.05,
shadowRadius: 2, shadowRadius: 2,
}}> }}>
<Ionicons name="settings-outline" size={18} color="#525252" /> <Ionicons name="settings-outline" size={18} color={colors.textMuted} />
</View> </View>
</Pressable> </Pressable>
) : ( ) : (
@ -146,18 +147,19 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
function Stat({ function Stat({
label, label,
value, value,
valueColor = '#0a0a0a', valueColor,
}: { }: {
label: string; label: string;
value: string; value: string;
valueColor?: string; valueColor?: string;
}) { }) {
const colors = useColors();
return ( return (
<View style={{ flex: 1, alignItems: 'center' }}> <View style={{ flex: 1, alignItems: 'center' }}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor ?? colors.text }}>
{value} {value}
</Text> </Text>
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: '#737373' }}> <Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{label} {label}
</Text> </Text>
</View> </View>

View File

@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
import Svg, { Path, Circle } from 'react-native-svg'; import Svg, { Path, Circle } from 'react-native-svg';
import type { ProtectionState } from '../../lib/protection'; import type { ProtectionState } from '../../lib/protection';
import { apiFetch } from '../../lib/api'; import { apiFetch } from '../../lib/api';
import { useColors } from '../../lib/theme';
type Props = { type Props = {
visible: boolean; visible: boolean;
@ -55,6 +56,7 @@ export function ProtectionDetailsSheet({
onRequestDeactivation, onRequestDeactivation,
}: Props) { }: Props) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const colors = useColors();
const localeTag = i18n.language === 'de' ? 'de-DE' : 'en-US'; const localeTag = i18n.language === 'de' ? 'de-DE' : 'en-US';
const sheetHeight = useRef(new Animated.Value(DEFAULT_HEIGHT)).current; const sheetHeight = useRef(new Animated.Value(DEFAULT_HEIGHT)).current;
@ -162,7 +164,7 @@ export function ProtectionDetailsSheet({
<Animated.View <Animated.View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: '#fff', backgroundColor: colors.bg,
borderTopLeftRadius: 24, borderTopLeftRadius: 24,
borderTopRightRadius: 24, borderTopRightRadius: 24,
overflow: 'hidden', overflow: 'hidden',
@ -178,7 +180,7 @@ export function ProtectionDetailsSheet({
{...panResponder.panHandlers} {...panResponder.panHandlers}
style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 6 }} style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 6 }}
> >
<View style={{ width: 36, height: 5, borderRadius: 3, backgroundColor: '#d4d4d8' }} /> <View style={{ width: 36, height: 5, borderRadius: 3, backgroundColor: colors.border }} />
</View> </View>
{/* Header */} {/* Header */}
@ -192,15 +194,15 @@ export function ProtectionDetailsSheet({
paddingTop: 4, paddingTop: 4,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f0f0f0', borderBottomColor: colors.border,
}} }}
> >
<View style={{ width: 50 }} /> <View style={{ width: 50 }} />
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('blocker.details_title')} {t('blocker.details_title')}
</Text> </Text>
<Pressable onPress={handleClose} hitSlop={10} style={{ width: 50, alignItems: 'flex-end' }}> <Pressable onPress={handleClose} hitSlop={10} style={{ width: 50, alignItems: 'flex-end' }}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('blocker.details_done')} {t('blocker.details_done')}
</Text> </Text>
</Pressable> </Pressable>
@ -249,16 +251,16 @@ export function ProtectionDetailsSheet({
padding: 18, padding: 18,
borderRadius: 16, borderRadius: 16,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
backgroundColor: '#fff', backgroundColor: colors.bg,
gap: 8, gap: 8,
}} }}
> >
<View> <View>
<Text style={{ fontSize: 13, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}> <Text style={{ fontSize: 13, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
{t('blocker.kpi_submissions_title')} {t('blocker.kpi_submissions_title')}
</Text> </Text>
<Text style={{ fontSize: 11, color: '#737373', fontFamily: 'Nunito_400Regular', marginTop: 2 }}> <Text style={{ fontSize: 11, color: colors.textMuted, fontFamily: 'Nunito_400Regular', marginTop: 2 }}>
{t('blocker.kpi_submissions_subtitle')} {t('blocker.kpi_submissions_subtitle')}
</Text> </Text>
</View> </View>
@ -323,14 +325,14 @@ export function ProtectionDetailsSheet({
flex: 1, flex: 1,
fontSize: 11, fontSize: 11,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#737373', color: colors.textMuted,
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase', textTransform: 'uppercase',
}} }}
> >
{t('blocker.faq_heading')} {t('blocker.faq_heading')}
</Text> </Text>
<Ionicons name="help-circle-outline" size={18} color="#737373" /> <Ionicons name="help-circle-outline" size={18} color={colors.textMuted} />
</View> </View>
{[1, 2, 3, 4].map((n) => ( {[1, 2, 3, 4].map((n) => (
<FaqItem <FaqItem
@ -355,7 +357,7 @@ export function ProtectionDetailsSheet({
borderRadius: 12, borderRadius: 12,
borderWidth: 1.5, borderWidth: 1.5,
borderColor: HERO_COLOR, borderColor: HERO_COLOR,
backgroundColor: '#fff7ed', backgroundColor: colors.surface,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -479,6 +481,7 @@ function KpiCard({
decimals?: number; decimals?: number;
suffix?: string; suffix?: string;
}) { }) {
const colors = useColors();
return ( return (
<View <View
style={{ style={{
@ -486,14 +489,14 @@ function KpiCard({
padding: 12, padding: 12,
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
backgroundColor: '#fafafa', backgroundColor: colors.surface,
gap: 6, gap: 6,
}} }}
> >
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<Ionicons name={icon} size={14} color="#737373" /> <Ionicons name={icon} size={14} color={colors.textMuted} />
<Text style={{ flex: 1, fontSize: 10, color: '#737373', fontFamily: 'Nunito_400Regular', lineHeight: 13 }}> <Text style={{ flex: 1, fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular', lineHeight: 13 }}>
{label} {label}
</Text> </Text>
</View> </View>
@ -502,10 +505,10 @@ function KpiCard({
value={value} value={value}
locale={locale} locale={locale}
decimals={decimals} decimals={decimals}
style={{ fontSize: 18, fontFamily: 'Nunito_900Black', color: '#0a0a0a', letterSpacing: -0.3 }} style={{ fontSize: 18, fontFamily: 'Nunito_900Black', color: colors.text, letterSpacing: -0.3 }}
/> />
{suffix ? ( {suffix ? (
<Text style={{ fontSize: 10, color: '#737373', fontFamily: 'Nunito_700Bold' }}>{suffix}</Text> <Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_700Bold' }}>{suffix}</Text>
) : null} ) : null}
</View> </View>
</View> </View>
@ -522,13 +525,14 @@ function LegendItem({
label: string; label: string;
value: number; value: number;
}) { }) {
const colors = useColors();
return ( return (
<View style={{ alignItems: 'center', gap: 4 }}> <View style={{ alignItems: 'center', gap: 4 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} /> <View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} />
<Text style={{ fontSize: 11, color: '#525252', fontFamily: 'Nunito_700Bold' }}>{value}</Text> <Text style={{ fontSize: 11, color: colors.textMuted, fontFamily: 'Nunito_700Bold' }}>{value}</Text>
</View> </View>
<Text style={{ fontSize: 10, color: '#737373', fontFamily: 'Nunito_400Regular' }}>{label}</Text> <Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>{label}</Text>
</View> </View>
); );
} }
@ -543,6 +547,7 @@ function HalfDonut({
centerValue: number; centerValue: number;
centerLabel: string; centerLabel: string;
}) { }) {
const colors = useColors();
const total = Math.max(1, segments.reduce((s, x) => s + x.value, 0)); const total = Math.max(1, segments.reduce((s, x) => s + x.value, 0));
const W = 220; const W = 220;
@ -582,7 +587,7 @@ function HalfDonut({
{/* Background track */} {/* Background track */}
<Path <Path
d={arcPath(cx, cy, r, 180, 360)} d={arcPath(cx, cy, r, 180, 360)}
stroke="#f0f0f0" stroke={colors.surfaceElevated}
strokeWidth={stroke} strokeWidth={stroke}
fill="none" fill="none"
strokeLinecap="round" strokeLinecap="round"
@ -618,10 +623,10 @@ function HalfDonut({
alignItems: 'center', alignItems: 'center',
}} }}
> >
<Text style={{ fontSize: 30, fontFamily: 'Nunito_900Black', color: '#0a0a0a', letterSpacing: -0.5 }}> <Text style={{ fontSize: 30, fontFamily: 'Nunito_900Black', color: colors.text, letterSpacing: -0.5 }}>
{centerValue} {centerValue}
</Text> </Text>
<Text style={{ fontSize: 10, color: '#737373', fontFamily: 'Nunito_400Regular', marginTop: -2 }}> <Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular', marginTop: -2 }}>
{centerLabel} {centerLabel}
</Text> </Text>
</View> </View>
@ -643,6 +648,7 @@ function polar(cx: number, cy: number, r: number, angleDeg: number) {
// ─── FAQ Item (chevron AT END of header row, on right) ───────────────────── // ─── FAQ Item (chevron AT END of header row, on right) ─────────────────────
function FaqItem({ question, answer }: { question: string; answer: string }) { function FaqItem({ question, answer }: { question: string; answer: string }) {
const colors = useColors();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const rotateAnim = useRef(new Animated.Value(0)).current; const rotateAnim = useRef(new Animated.Value(0)).current;
@ -664,10 +670,10 @@ function FaqItem({ question, answer }: { question: string; answer: string }) {
style={{ style={{
alignSelf: 'stretch', alignSelf: 'stretch',
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 12, borderRadius: 12,
overflow: 'hidden', overflow: 'hidden',
backgroundColor: '#fff', backgroundColor: colors.bg,
}} }}
> >
<Pressable <Pressable
@ -678,7 +684,7 @@ function FaqItem({ question, answer }: { question: string; answer: string }) {
> >
<View style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14 }}> <View style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14 }}>
<View style={{ flex: 1, paddingRight: 12 }}> <View style={{ flex: 1, paddingRight: 12 }}>
<Text style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: '#0a0a0a', lineHeight: 18 }}> <Text style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text, lineHeight: 18 }}>
{question} {question}
</Text> </Text>
</View> </View>
@ -687,19 +693,19 @@ function FaqItem({ question, answer }: { question: string; answer: string }) {
width: 28, width: 28,
height: 28, height: 28,
borderRadius: 14, borderRadius: 14,
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
transform: [{ rotate }], transform: [{ rotate }],
}} }}
> >
<Ionicons name="chevron-down" size={16} color="#525252" /> <Ionicons name="chevron-down" size={16} color={colors.textMuted} />
</Animated.View> </Animated.View>
</View> </View>
</Pressable> </Pressable>
{open && ( {open && (
<View style={{ paddingHorizontal: 14, paddingBottom: 14, paddingTop: 0 }}> <View style={{ paddingHorizontal: 14, paddingBottom: 14, paddingTop: 0 }}>
<Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: '#525252', lineHeight: 19 }}> <Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 19 }}>
{answer} {answer}
</Text> </Text>
</View> </View>

View File

@ -2,6 +2,7 @@ import { View, Text, Pressable } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ProtectionState } from '../../lib/protection'; import type { ProtectionState } from '../../lib/protection';
import { useColors } from '../../lib/theme';
type Props = { type Props = {
state: ProtectionState; state: ProtectionState;
@ -16,6 +17,7 @@ type Props = {
*/ */
export function ProtectionLockedCard({ state, onPressSettings }: Props) { export function ProtectionLockedCard({ state, onPressSettings }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const isCooldown = state.phase === 'cooldownActive'; const isCooldown = state.phase === 'cooldownActive';
const cardBg = isCooldown ? '#fef3c7' : '#dcfce7'; const cardBg = isCooldown ? '#fef3c7' : '#dcfce7';
const cardBorder = isCooldown ? '#fcd34d' : '#86efac'; const cardBorder = isCooldown ? '#fcd34d' : '#86efac';
@ -57,14 +59,14 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
<Ionicons name="shield-checkmark" size={22} color={iconColor} /> <Ionicons name="shield-checkmark" size={22} color={iconColor} />
</View> </View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('blocker.protection_card_locked_title')} {t('blocker.protection_card_locked_title')}
</Text> </Text>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#525252', color: colors.textMuted,
marginTop: 2, marginTop: 2,
}} }}
> >
@ -84,7 +86,7 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
width: 36, width: 36,
height: 36, height: 36,
borderRadius: 18, borderRadius: 18,
backgroundColor: '#ffffff', backgroundColor: colors.surface,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
shadowColor: '#000', shadowColor: '#000',
@ -92,7 +94,7 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
shadowOpacity: 0.05, shadowOpacity: 0.05,
shadowRadius: 2, shadowRadius: 2,
}}> }}>
<Ionicons name="settings-outline" size={18} color="#525252" /> <Ionicons name="settings-outline" size={18} color={colors.textMuted} />
</View> </View>
</Pressable> </Pressable>
</View> </View>
@ -118,11 +120,12 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
); );
} }
function Stat({ label, value, valueColor = '#0a0a0a' }: { label: string; value: string; valueColor?: string }) { function Stat({ label, value, valueColor }: { label: string; value: string; valueColor?: string }) {
const colors = useColors();
return ( return (
<View style={{ flex: 1, alignItems: 'center' }}> <View style={{ flex: 1, alignItems: 'center' }}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor }}>{value}</Text> <Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor ?? colors.text }}>{value}</Text>
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: '#737373' }}>{label}</Text> <Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>{label}</Text>
</View> </View>
); );
} }

View File

@ -13,6 +13,7 @@ import * as Clipboard from 'expo-clipboard';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { resolveAvatar } from '../../lib/resolveAvatar'; import { resolveAvatar } from '../../lib/resolveAvatar';
import { useColors } from '../../lib/theme';
export type ChatMsg = { export type ChatMsg = {
id: string; id: string;
@ -63,6 +64,8 @@ export function ChatBubble({
onOpenImage, onOpenImage,
}: Props) { }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const styles = makeStyles(colors);
const [actionsOpen, setActionsOpen] = useState(false); const [actionsOpen, setActionsOpen] = useState(false);
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -323,120 +326,122 @@ export function ChatBubble({
); );
} }
const styles = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
row: { return StyleSheet.create({
flexDirection: 'row', row: {
paddingHorizontal: 8, flexDirection: 'row',
}, paddingHorizontal: 8,
avatarSlot: { },
width: 30, avatarSlot: {
marginRight: 4, width: 30,
justifyContent: 'flex-end', marginRight: 4,
}, justifyContent: 'flex-end',
avatar: { },
width: 26, avatar: {
height: 26, width: 26,
borderRadius: 13, height: 26,
backgroundColor: '#e5e5e5', borderRadius: 13,
}, backgroundColor: colors.surfaceElevated,
bubbleCol: { },
maxWidth: '78%', bubbleCol: {
}, maxWidth: '78%',
nickname: { },
fontSize: 10, nickname: {
fontFamily: 'Nunito_700Bold', fontSize: 10,
color: '#007AFF', fontFamily: 'Nunito_700Bold',
marginBottom: 2, color: '#007AFF',
marginLeft: 10, marginBottom: 2,
}, marginLeft: 10,
bubble: { },
borderRadius: 18, bubble: {
paddingHorizontal: 12, borderRadius: 18,
paddingVertical: 6, paddingHorizontal: 12,
shadowColor: '#000', paddingVertical: 6,
shadowOpacity: 0.05, shadowColor: '#000',
shadowRadius: 1, shadowOpacity: 0.05,
shadowOffset: { width: 0, height: 1 }, shadowRadius: 1,
}, shadowOffset: { width: 0, height: 1 },
bubbleOwn: { },
backgroundColor: '#007AFF', bubbleOwn: {
}, backgroundColor: '#007AFF',
bubbleOther: { },
backgroundColor: '#ffffff', bubbleOther: {
borderWidth: StyleSheet.hairlineWidth, backgroundColor: colors.surface,
borderColor: '#e5e5e5', borderWidth: StyleSheet.hairlineWidth,
}, borderColor: colors.border,
replyPreview: { },
borderLeftWidth: 3, replyPreview: {
borderRadius: 8, borderLeftWidth: 3,
paddingHorizontal: 8, borderRadius: 8,
paddingVertical: 4, paddingHorizontal: 8,
marginBottom: 4, paddingVertical: 4,
}, marginBottom: 4,
imageWrap: { },
borderRadius: 12, imageWrap: {
overflow: 'hidden', borderRadius: 12,
position: 'relative', overflow: 'hidden',
}, position: 'relative',
image: { },
width: 220, image: {
height: 220, width: 220,
backgroundColor: '#f5f5f5', height: 220,
}, backgroundColor: colors.surfaceElevated,
imageTimeOverlay: { },
position: 'absolute', imageTimeOverlay: {
bottom: 6, position: 'absolute',
right: 6, bottom: 6,
backgroundColor: 'rgba(0,0,0,0.5)', right: 6,
borderRadius: 10, backgroundColor: 'rgba(0,0,0,0.5)',
paddingHorizontal: 6, borderRadius: 10,
paddingVertical: 2, paddingHorizontal: 6,
flexDirection: 'row', paddingVertical: 2,
alignItems: 'center', flexDirection: 'row',
}, alignItems: 'center',
content: { },
fontSize: 14, content: {
lineHeight: 20, fontSize: 14,
fontFamily: 'Nunito_400Regular', lineHeight: 20,
}, fontFamily: 'Nunito_400Regular',
footer: { },
position: 'absolute', footer: {
bottom: 4, position: 'absolute',
right: 8, bottom: 4,
flexDirection: 'row', right: 8,
alignItems: 'center', flexDirection: 'row',
}, alignItems: 'center',
sheetBackdrop: { },
flex: 1, sheetBackdrop: {
backgroundColor: 'rgba(0,0,0,0.5)', flex: 1,
justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.5)',
}, justifyContent: 'flex-end',
sheet: { },
backgroundColor: '#fff', sheet: {
borderTopLeftRadius: 20, backgroundColor: colors.bg,
borderTopRightRadius: 20, borderTopLeftRadius: 20,
padding: 8, borderTopRightRadius: 20,
paddingBottom: Platform.OS === 'ios' ? 32 : 16, padding: 8,
}, paddingBottom: Platform.OS === 'ios' ? 32 : 16,
sheetGrabber: { },
width: 36, sheetGrabber: {
height: 4, width: 36,
borderRadius: 2, height: 4,
backgroundColor: '#d4d4d4', borderRadius: 2,
alignSelf: 'center', backgroundColor: colors.border,
marginBottom: 10, alignSelf: 'center',
}, marginBottom: 10,
sheetItem: { },
flexDirection: 'row', sheetItem: {
alignItems: 'center', flexDirection: 'row',
paddingHorizontal: 16, alignItems: 'center',
paddingVertical: 14, paddingHorizontal: 16,
borderRadius: 12, paddingVertical: 14,
}, borderRadius: 12,
sheetText: { },
fontSize: 15, sheetText: {
fontFamily: 'Nunito_600SemiBold', fontSize: 15,
color: '#171717', fontFamily: 'Nunito_600SemiBold',
marginLeft: 12, color: colors.text,
}, marginLeft: 12,
}); },
});
}

View File

@ -16,6 +16,7 @@ import * as FileSystem from 'expo-file-system/legacy';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { supabase } from '../../lib/supabase'; import { supabase } from '../../lib/supabase';
import { useColors } from '../../lib/theme';
type ReplyTo = { id: string; nickname: string; content: string }; type ReplyTo = { id: string; nickname: string; content: string };
@ -45,6 +46,7 @@ export function ChatInput({
onCancelReply, onCancelReply,
}: Props) { }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const [text, setText] = useState(''); const [text, setText] = useState('');
const [attachment, setAttachment] = useState<{ const [attachment, setAttachment] = useState<{
uri: string; uri: string;
@ -137,6 +139,8 @@ export function ChatInput({
setAttachment(null); setAttachment(null);
} }
const styles = makeStyles(colors);
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/* Reply preview */} {/* Reply preview */}
@ -231,103 +235,105 @@ function decodeBase64(base64: string): Uint8Array {
return bytes; return bytes;
} }
const styles = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
container: { return StyleSheet.create({
backgroundColor: '#ffffff', container: {
borderTopWidth: StyleSheet.hairlineWidth, backgroundColor: colors.bg,
borderTopColor: '#e5e5e5', borderTopWidth: StyleSheet.hairlineWidth,
}, borderTopColor: colors.border,
replyBar: { },
flexDirection: 'row', replyBar: {
alignItems: 'center', flexDirection: 'row',
paddingHorizontal: 12, alignItems: 'center',
paddingVertical: 8, paddingHorizontal: 12,
backgroundColor: '#eff6ff', paddingVertical: 8,
borderLeftWidth: 3, backgroundColor: colors.surface,
borderLeftColor: '#007AFF', borderLeftWidth: 3,
marginHorizontal: 8, borderLeftColor: '#007AFF',
marginTop: 6, marginHorizontal: 8,
borderRadius: 8, marginTop: 6,
}, borderRadius: 8,
replyName: { },
fontSize: 11, replyName: {
fontFamily: 'Nunito_700Bold', fontSize: 11,
color: '#007AFF', fontFamily: 'Nunito_700Bold',
}, color: '#007AFF',
replyContent: { },
fontSize: 11, replyContent: {
fontFamily: 'Nunito_400Regular', fontSize: 11,
color: '#525252', fontFamily: 'Nunito_400Regular',
marginTop: 1, color: colors.textMuted,
}, marginTop: 1,
attachBar: { },
flexDirection: 'row', attachBar: {
alignItems: 'center', flexDirection: 'row',
paddingHorizontal: 12, alignItems: 'center',
paddingVertical: 6, paddingHorizontal: 12,
backgroundColor: '#fafafa', paddingVertical: 6,
marginHorizontal: 8, backgroundColor: colors.surface,
marginTop: 6, marginHorizontal: 8,
borderRadius: 8, marginTop: 6,
}, borderRadius: 8,
attachImg: { },
width: 36, attachImg: {
height: 36, width: 36,
borderRadius: 6, height: 36,
marginRight: 8, borderRadius: 6,
}, marginRight: 8,
attachFileIcon: { },
width: 36, attachFileIcon: {
height: 36, width: 36,
borderRadius: 6, height: 36,
backgroundColor: '#e5e5e5', borderRadius: 6,
alignItems: 'center', backgroundColor: colors.surfaceElevated,
justifyContent: 'center', alignItems: 'center',
marginRight: 8, justifyContent: 'center',
}, marginRight: 8,
attachName: { },
flex: 1, attachName: {
fontSize: 12, flex: 1,
fontFamily: 'Nunito_600SemiBold', fontSize: 12,
color: '#171717', fontFamily: 'Nunito_600SemiBold',
}, color: colors.text,
row: { },
flexDirection: 'row', row: {
alignItems: 'flex-end', flexDirection: 'row',
paddingHorizontal: 8, alignItems: 'flex-end',
paddingTop: 8, paddingHorizontal: 8,
paddingBottom: 8, paddingTop: 8,
}, paddingBottom: 8,
iconBtn: { },
width: 36, iconBtn: {
height: 36, width: 36,
borderRadius: 18, height: 36,
alignItems: 'center', borderRadius: 18,
justifyContent: 'center', alignItems: 'center',
marginRight: 4, justifyContent: 'center',
}, marginRight: 4,
inputWrap: { },
flex: 1, inputWrap: {
backgroundColor: '#f5f5f5', flex: 1,
borderRadius: 22, backgroundColor: colors.surfaceElevated,
paddingHorizontal: 14, borderRadius: 22,
minHeight: 36, paddingHorizontal: 14,
maxHeight: 120, minHeight: 36,
justifyContent: 'center', maxHeight: 120,
}, justifyContent: 'center',
input: { },
fontSize: 14, input: {
lineHeight: 19, fontSize: 14,
fontFamily: 'Nunito_400Regular', lineHeight: 19,
color: '#171717', fontFamily: 'Nunito_400Regular',
paddingVertical: Platform.OS === 'ios' ? 8 : 4, color: colors.text,
}, paddingVertical: Platform.OS === 'ios' ? 8 : 4,
sendBtn: { },
width: 36, sendBtn: {
height: 36, width: 36,
borderRadius: 18, height: 36,
alignItems: 'center', borderRadius: 18,
justifyContent: 'center', alignItems: 'center',
marginLeft: 6, justifyContent: 'center',
}, marginLeft: 6,
}); },
});
}

View File

@ -12,6 +12,7 @@ import {
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { apiFetch } from '../../lib/api'; import { apiFetch } from '../../lib/api';
import { useColors } from '../../lib/theme';
type Props = { type Props = {
visible: boolean; visible: boolean;
@ -21,6 +22,8 @@ type Props = {
export function CreateRoomSheet({ visible, onClose, onCreated }: Props) { export function CreateRoomSheet({ visible, onClose, onCreated }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const styles = makeStyles(colors);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [isPublic, setIsPublic] = useState(true); const [isPublic, setIsPublic] = useState(true);
@ -145,138 +148,140 @@ export function CreateRoomSheet({ visible, onClose, onCreated }: Props) {
); );
} }
const styles = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
backdrop: { return StyleSheet.create({
flex: 1, backdrop: {
backgroundColor: 'rgba(0,0,0,0.5)', flex: 1,
justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.5)',
}, justifyContent: 'flex-end',
sheet: { },
backgroundColor: '#fff', sheet: {
borderTopLeftRadius: 22, backgroundColor: colors.bg,
borderTopRightRadius: 22, borderTopLeftRadius: 22,
padding: 18, borderTopRightRadius: 22,
paddingBottom: Platform.OS === 'ios' ? 32 : 18, padding: 18,
}, paddingBottom: Platform.OS === 'ios' ? 32 : 18,
grabber: { },
width: 36, grabber: {
height: 4, width: 36,
borderRadius: 2, height: 4,
backgroundColor: '#d4d4d4', borderRadius: 2,
alignSelf: 'center', backgroundColor: colors.border,
marginBottom: 12, alignSelf: 'center',
}, marginBottom: 12,
title: { },
fontSize: 17, title: {
fontFamily: 'Nunito_700Bold', fontSize: 17,
color: '#171717', fontFamily: 'Nunito_700Bold',
marginBottom: 14, color: colors.text,
}, marginBottom: 14,
input: { },
backgroundColor: '#f5f5f5', input: {
borderRadius: 12, backgroundColor: colors.surfaceElevated,
paddingHorizontal: 14, borderRadius: 12,
paddingVertical: 12, paddingHorizontal: 14,
fontSize: 14, paddingVertical: 12,
fontFamily: 'Nunito_400Regular', fontSize: 14,
color: '#171717', fontFamily: 'Nunito_400Regular',
marginBottom: 10, color: colors.text,
}, marginBottom: 10,
toggleRow: { },
flexDirection: 'row', toggleRow: {
alignItems: 'center', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
paddingVertical: 6, justifyContent: 'space-between',
marginTop: 4, paddingVertical: 6,
}, marginTop: 4,
toggleLabel: { },
fontSize: 14, toggleLabel: {
fontFamily: 'Nunito_600SemiBold', fontSize: 14,
color: '#171717', fontFamily: 'Nunito_600SemiBold',
}, color: colors.text,
toggle: { },
width: 46, toggle: {
height: 28, width: 46,
borderRadius: 14, height: 28,
backgroundColor: '#e5e5e5', borderRadius: 14,
padding: 2, backgroundColor: colors.surfaceElevated,
justifyContent: 'center', padding: 2,
}, justifyContent: 'center',
toggleOn: { },
backgroundColor: '#007AFF', toggleOn: {
}, backgroundColor: '#007AFF',
toggleKnob: { },
width: 24, toggleKnob: {
height: 24, width: 24,
borderRadius: 12, height: 24,
backgroundColor: '#fff', borderRadius: 12,
shadowColor: '#000', backgroundColor: colors.bg,
shadowOpacity: 0.15, shadowColor: '#000',
shadowRadius: 2, shadowOpacity: 0.15,
shadowOffset: { width: 0, height: 1 }, shadowRadius: 2,
elevation: 2, shadowOffset: { width: 0, height: 1 },
}, elevation: 2,
toggleKnobOn: { },
transform: [{ translateX: 18 }], toggleKnobOn: {
}, transform: [{ translateX: 18 }],
subLabel: { },
fontSize: 12, subLabel: {
fontFamily: 'Nunito_600SemiBold', fontSize: 12,
color: '#737373', fontFamily: 'Nunito_600SemiBold',
marginBottom: 6, color: colors.textMuted,
}, marginBottom: 6,
modeRow: { },
flexDirection: 'row', modeRow: {
}, flexDirection: 'row',
modeBtn: { },
flex: 1, modeBtn: {
paddingVertical: 8, flex: 1,
borderRadius: 10, paddingVertical: 8,
borderWidth: 1, borderRadius: 10,
borderColor: '#e5e5e5', borderWidth: 1,
alignItems: 'center', borderColor: colors.border,
marginRight: 6, alignItems: 'center',
}, marginRight: 6,
modeBtnActive: { },
backgroundColor: '#eff6ff', modeBtnActive: {
borderColor: '#007AFF', backgroundColor: colors.surface,
}, borderColor: '#007AFF',
modeBtnText: { },
fontSize: 12, modeBtnText: {
fontFamily: 'Nunito_600SemiBold', fontSize: 12,
color: '#737373', fontFamily: 'Nunito_600SemiBold',
}, color: colors.textMuted,
modeBtnTextActive: { },
color: '#007AFF', modeBtnTextActive: {
}, color: '#007AFF',
actions: { },
flexDirection: 'row', actions: {
marginTop: 20, flexDirection: 'row',
}, marginTop: 20,
cancelBtn: { },
flex: 1, cancelBtn: {
backgroundColor: '#f5f5f5', flex: 1,
paddingVertical: 12, backgroundColor: colors.surfaceElevated,
borderRadius: 12, paddingVertical: 12,
alignItems: 'center', borderRadius: 12,
marginRight: 6, alignItems: 'center',
}, marginRight: 6,
cancelText: { },
fontSize: 14, cancelText: {
fontFamily: 'Nunito_600SemiBold', fontSize: 14,
color: '#171717', fontFamily: 'Nunito_600SemiBold',
}, color: colors.text,
createBtn: { },
flex: 1, createBtn: {
backgroundColor: '#007AFF', flex: 1,
paddingVertical: 12, backgroundColor: '#007AFF',
borderRadius: 12, paddingVertical: 12,
alignItems: 'center', borderRadius: 12,
marginLeft: 6, alignItems: 'center',
}, marginLeft: 6,
createText: { },
fontSize: 14, createText: {
fontFamily: 'Nunito_700Bold', fontSize: 14,
color: '#fff', fontFamily: 'Nunito_700Bold',
}, color: '#fff',
}); },
});
}

View File

@ -1,6 +1,7 @@
import { View, Text, Pressable, Image, StyleSheet } from 'react-native'; import { View, Text, Pressable, Image, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
export type Room = { export type Room = {
id: string; id: string;
@ -29,6 +30,8 @@ function formatTime(ts: string, justNow: string) {
export function RoomCard({ room, onPress }: Props) { export function RoomCard({ room, onPress }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const styles = makeStyles(colors);
const initials = room.name const initials = room.name
.split(' ') .split(' ')
.slice(0, 2) .slice(0, 2)
@ -42,7 +45,7 @@ export function RoomCard({ room, onPress }: Props) {
<View <View
style={[ style={[
styles.avatar, styles.avatar,
{ backgroundColor: room.isPublic ? '#eff6ff' : '#e5e5e5' }, { backgroundColor: room.isPublic ? colors.surface : colors.surfaceElevated },
]} ]}
> >
{room.avatarUrl ? ( {room.avatarUrl ? (
@ -102,116 +105,118 @@ export function RoomCard({ room, onPress }: Props) {
); );
} }
const styles = StyleSheet.create({ function makeStyles(colors: ReturnType<typeof useColors>) {
row: { return StyleSheet.create({
width: '100%', row: {
flexDirection: 'row', width: '100%',
alignItems: 'center', flexDirection: 'row',
paddingHorizontal: 14, alignItems: 'center',
paddingVertical: 11, paddingHorizontal: 14,
backgroundColor: '#fff', paddingVertical: 11,
borderBottomWidth: StyleSheet.hairlineWidth, backgroundColor: colors.bg,
borderBottomColor: '#f5f5f5', borderBottomWidth: StyleSheet.hairlineWidth,
}, borderBottomColor: colors.border,
avatar: { },
width: 42, avatar: {
height: 42, width: 42,
borderRadius: 21, height: 42,
alignItems: 'center', borderRadius: 21,
justifyContent: 'center', alignItems: 'center',
overflow: 'hidden', justifyContent: 'center',
marginRight: 10, overflow: 'hidden',
}, marginRight: 10,
avatarImg: { },
width: 42, avatarImg: {
height: 42, width: 42,
}, height: 42,
avatarInitials: { },
fontSize: 13, avatarInitials: {
fontFamily: 'Nunito_700Bold', fontSize: 13,
color: '#525252', fontFamily: 'Nunito_700Bold',
}, color: colors.textMuted,
info: { },
flex: 1, info: {
minWidth: 0, flex: 1,
}, minWidth: 0,
headerRow: { },
flexDirection: 'row', headerRow: {
alignItems: 'center', flexDirection: 'row',
}, alignItems: 'center',
footerRow: { },
flexDirection: 'row', footerRow: {
alignItems: 'center', flexDirection: 'row',
marginTop: 3, alignItems: 'center',
}, marginTop: 3,
footerTextWrap: { },
flex: 1, footerTextWrap: {
minWidth: 0, flex: 1,
}, minWidth: 0,
metaPill: { },
flexDirection: 'row', metaPill: {
alignItems: 'center', flexDirection: 'row',
marginLeft: 8, alignItems: 'center',
paddingHorizontal: 6, marginLeft: 8,
paddingVertical: 2, paddingHorizontal: 6,
borderRadius: 8, paddingVertical: 2,
backgroundColor: '#f5f5f5', borderRadius: 8,
}, backgroundColor: colors.surfaceElevated,
name: { },
fontSize: 14, name: {
fontFamily: 'Nunito_700Bold', fontSize: 14,
color: '#171717', fontFamily: 'Nunito_700Bold',
flexShrink: 1, color: colors.text,
}, flexShrink: 1,
defaultBadge: { },
marginLeft: 6, defaultBadge: {
paddingHorizontal: 6, marginLeft: 6,
paddingVertical: 1, paddingHorizontal: 6,
backgroundColor: '#eff6ff', paddingVertical: 1,
borderRadius: 8, backgroundColor: colors.surface,
}, borderRadius: 8,
defaultBadgeText: { },
fontSize: 9, defaultBadgeText: {
fontFamily: 'Nunito_700Bold', fontSize: 9,
color: '#007AFF', fontFamily: 'Nunito_700Bold',
}, color: '#007AFF',
lastMessage: { },
fontSize: 12, lastMessage: {
fontFamily: 'Nunito_400Regular', fontSize: 12,
color: '#737373', fontFamily: 'Nunito_400Regular',
}, color: colors.textMuted,
description: { },
fontSize: 12, description: {
fontFamily: 'Nunito_400Regular', fontSize: 12,
color: '#a3a3a3', fontFamily: 'Nunito_400Regular',
}, color: colors.textMuted,
right: { },
alignItems: 'flex-end', right: {
marginLeft: 8, alignItems: 'flex-end',
}, marginLeft: 8,
memberCount: { },
fontSize: 11, memberCount: {
fontFamily: 'Nunito_700Bold', fontSize: 11,
color: '#737373', fontFamily: 'Nunito_700Bold',
marginLeft: 3, color: colors.textMuted,
}, marginLeft: 3,
time: { },
fontSize: 10, time: {
fontFamily: 'Nunito_500Medium', fontSize: 10,
color: '#a3a3a3', fontFamily: 'Nunito_500Medium',
marginLeft: 'auto', color: colors.textMuted,
paddingLeft: 6, marginLeft: 'auto',
}, paddingLeft: 6,
joinBadge: { },
marginLeft: 6, joinBadge: {
paddingHorizontal: 8, marginLeft: 6,
paddingVertical: 3, paddingHorizontal: 8,
backgroundColor: '#eff6ff', paddingVertical: 3,
borderRadius: 10, backgroundColor: colors.surface,
}, borderRadius: 10,
joinBadgeText: { },
fontSize: 10, joinBadgeText: {
fontFamily: 'Nunito_700Bold', fontSize: 10,
color: '#007AFF', fontFamily: 'Nunito_700Bold',
}, color: '#007AFF',
}); },
});
}

View File

@ -18,6 +18,7 @@ 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 { useMailConnect, detectProvider, type MailProvider } from '../../hooks/useMailConnect'; import { useMailConnect, detectProvider, type MailProvider } from '../../hooks/useMailConnect';
import { useColors } from '../../lib/theme';
const SCREEN_HEIGHT = Dimensions.get('window').height; const SCREEN_HEIGHT = Dimensions.get('window').height;
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65; const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65;
@ -97,6 +98,7 @@ const PROVIDERS: ProviderConfig[] = [
*/ */
export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) { export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { connect, connecting, error: connectError } = useMailConnect(); const { connect, connecting, error: connectError } = useMailConnect();
@ -203,7 +205,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
right: 0, right: 0,
bottom: 0, bottom: 0,
height: SHEET_HEIGHT, height: SHEET_HEIGHT,
backgroundColor: '#fff', backgroundColor: colors.bg,
borderTopLeftRadius: 20, borderTopLeftRadius: 20,
borderTopRightRadius: 20, borderTopRightRadius: 20,
transform: [{ translateY }], transform: [{ translateY }],
@ -215,7 +217,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
> >
{/* Drag-Handle */} {/* Drag-Handle */}
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}> <View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: '#d4d4d4' }} /> <View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: colors.border }} />
</View> </View>
{/* Header */} {/* Header */}
@ -228,24 +230,24 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
paddingTop: 6, paddingTop: 6,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f0f0f0', borderBottomColor: colors.border,
}} }}
> >
{view === 'form' ? ( {view === 'form' ? (
<Pressable onPress={handleBack} hitSlop={10}> <Pressable onPress={handleBack} hitSlop={10}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('common.back')} {t('common.back')}
</Text> </Text>
</Pressable> </Pressable>
) : ( ) : (
<Pressable onPress={handleClose} hitSlop={10}> <Pressable onPress={handleClose} hitSlop={10}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('common.cancel')} {t('common.cancel')}
</Text> </Text>
</Pressable> </Pressable>
)} )}
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{view === 'form' && currentProvider {view === 'form' && currentProvider
? t(currentProvider.labelKey) ? t(currentProvider.labelKey)
: t('mail.connect_sheet_title')} : t('mail.connect_sheet_title')}
@ -293,6 +295,7 @@ function ProviderGrid({
onSelect: (p: ProviderConfig) => void; onSelect: (p: ProviderConfig) => void;
t: (key: string) => string; t: (key: string) => string;
}) { }) {
const colors = useColors();
return ( return (
<ScrollView <ScrollView
style={{ flex: 1 }} style={{ flex: 1 }}
@ -303,7 +306,7 @@ function ProviderGrid({
style={{ style={{
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#737373', color: colors.textMuted,
marginBottom: 4, marginBottom: 4,
lineHeight: 18, lineHeight: 18,
}} }}
@ -325,9 +328,9 @@ function ProviderGrid({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 10, gap: 10,
backgroundColor: '#f9f9f9', backgroundColor: colors.surface,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
borderRadius: 14, borderRadius: 14,
padding: 14, padding: 14,
}}> }}>
@ -345,13 +348,13 @@ function ProviderGrid({
</View> </View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text <Text
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }} style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text }}
numberOfLines={1} numberOfLines={1}
> >
{t(p.labelKey)} {t(p.labelKey)}
</Text> </Text>
</View> </View>
<Ionicons name="chevron-forward" size={14} color="#d4d4d4" /> <Ionicons name="chevron-forward" size={14} color={colors.border} />
</View> </View>
</Pressable> </Pressable>
))} ))}
@ -394,6 +397,7 @@ function FormView({
insets, insets,
t, t,
}: FormViewProps) { }: FormViewProps) {
const colors = useColors();
const canConnect = email.trim().length > 0 && password.trim().length > 0 && !connecting; const canConnect = email.trim().length > 0 && password.trim().length > 0 && !connecting;
return ( return (
@ -461,7 +465,7 @@ function FormView({
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#525252', color: colors.textMuted,
marginBottom: 6, marginBottom: 6,
}} }}
> >
@ -471,19 +475,19 @@ function FormView({
value={email} value={email}
onChangeText={onEmailChange} onChangeText={onEmailChange}
placeholder={t('mail.form_email_placeholder')} placeholder={t('mail.form_email_placeholder')}
placeholderTextColor="#a3a3a3" placeholderTextColor={colors.textMuted}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
keyboardType="email-address" keyboardType="email-address"
returnKeyType="next" returnKeyType="next"
style={{ style={{
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 12, borderRadius: 12,
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 12, paddingVertical: 12,
fontSize: 15, fontSize: 15,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#0a0a0a', color: colors.text,
}} }}
/> />
</View> </View>
@ -494,7 +498,7 @@ function FormView({
style={{ style={{
fontSize: 12, fontSize: 12,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
color: '#525252', color: colors.textMuted,
marginBottom: 6, marginBottom: 6,
}} }}
> >
@ -505,21 +509,21 @@ function FormView({
value={password} value={password}
onChangeText={onPasswordChange} onChangeText={onPasswordChange}
placeholder={t('mail.form_password_placeholder')} placeholder={t('mail.form_password_placeholder')}
placeholderTextColor="#a3a3a3" placeholderTextColor={colors.textMuted}
secureTextEntry={!passwordVisible} secureTextEntry={!passwordVisible}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
returnKeyType="done" returnKeyType="done"
onSubmitEditing={onConnect} onSubmitEditing={onConnect}
style={{ style={{
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 12, borderRadius: 12,
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 12, paddingVertical: 12,
paddingRight: 46, paddingRight: 46,
fontSize: 15, fontSize: 15,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#0a0a0a', color: colors.text,
}} }}
/> />
<Pressable <Pressable

View File

@ -16,6 +16,7 @@ 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 { useMailConnect } from '../../hooks/useMailConnect'; import { useMailConnect } from '../../hooks/useMailConnect';
import { useColors } from '../../lib/theme';
const SCREEN_HEIGHT = Dimensions.get('window').height; const SCREEN_HEIGHT = Dimensions.get('window').height;
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.5; const SHEET_HEIGHT = SCREEN_HEIGHT * 0.5;
@ -33,6 +34,7 @@ type Props = {
*/ */
export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Props) { export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { connect, connecting, error: connectError } = useMailConnect(); const { connect, connecting, error: connectError } = useMailConnect();
@ -104,7 +106,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
right: 0, right: 0,
bottom: 0, bottom: 0,
height: SHEET_HEIGHT, height: SHEET_HEIGHT,
backgroundColor: '#fff', backgroundColor: colors.bg,
borderTopLeftRadius: 20, borderTopLeftRadius: 20,
borderTopRightRadius: 20, borderTopRightRadius: 20,
transform: [{ translateY }], transform: [{ translateY }],
@ -116,7 +118,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
> >
{/* Drag-Handle */} {/* Drag-Handle */}
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}> <View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: '#d4d4d4' }} /> <View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: colors.border }} />
</View> </View>
{/* Header */} {/* Header */}
@ -129,15 +131,15 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
paddingTop: 6, paddingTop: 6,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f0f0f0', borderBottomColor: colors.border,
}} }}
> >
<Pressable onPress={onClose} hitSlop={10}> <Pressable onPress={onClose} hitSlop={10}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('common.cancel')} {t('common.cancel')}
</Text> </Text>
</Pressable> </Pressable>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}> <Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{t('mail.edit_account_title')} {t('mail.edit_account_title')}
</Text> </Text>
<View style={{ width: 60 }} /> <View style={{ width: 60 }} />
@ -148,7 +150,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
style={{ style={{
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#737373', color: colors.textMuted,
lineHeight: 18, lineHeight: 18,
}} }}
> >
@ -159,13 +161,13 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#f5f5f5', backgroundColor: colors.surfaceElevated,
borderRadius: 12, borderRadius: 12,
paddingHorizontal: 14, paddingHorizontal: 14,
gap: 10, gap: 10,
}} }}
> >
<Ionicons name="lock-closed-outline" size={16} color="#a3a3a3" /> <Ionicons name="lock-closed-outline" size={16} color={colors.textMuted} />
<TextInput <TextInput
value={password} value={password}
onChangeText={(v) => { onChangeText={(v) => {
@ -173,7 +175,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
setFormError(null); setFormError(null);
}} }}
placeholder={t('mail.app_password_placeholder')} placeholder={t('mail.app_password_placeholder')}
placeholderTextColor="#a3a3a3" placeholderTextColor={colors.textMuted}
secureTextEntry={!passwordVisible} secureTextEntry={!passwordVisible}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
@ -182,7 +184,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
paddingVertical: 14, paddingVertical: 14,
fontSize: 15, fontSize: 15,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#0a0a0a', color: colors.text,
}} }}
/> />
<Pressable onPress={() => setPasswordVisible((p) => !p)} hitSlop={8}> <Pressable onPress={() => setPasswordVisible((p) => !p)} hitSlop={8}>

View File

@ -1,6 +1,7 @@
import { Pressable, Text, View } from 'react-native'; import { Pressable, Text, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
type Props = { type Props = {
onConnectPress: () => void; onConnectPress: () => void;
@ -12,14 +13,15 @@ type Props = {
*/ */
export function MailEmptyState({ onConnectPress }: Props) { export function MailEmptyState({ onConnectPress }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
return ( return (
<View <View
style={{ style={{
backgroundColor: '#fff', backgroundColor: colors.bg,
borderRadius: 20, borderRadius: 20,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e5e5', borderColor: colors.border,
padding: 28, padding: 28,
alignItems: 'center', alignItems: 'center',
}} }}
@ -45,7 +47,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
style={{ style={{
fontSize: 17, fontSize: 17,
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#0a0a0a', color: colors.text,
textAlign: 'center', textAlign: 'center',
marginBottom: 8, marginBottom: 8,
}} }}
@ -57,7 +59,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
style={{ style={{
fontSize: 13, fontSize: 13,
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
color: '#737373', color: colors.textMuted,
textAlign: 'center', textAlign: 'center',
lineHeight: 19, lineHeight: 19,
marginBottom: 20, marginBottom: 20,
@ -71,7 +73,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
{(['privacy_1', 'privacy_2', 'privacy_3'] as const).map((key) => ( {(['privacy_1', 'privacy_2', 'privacy_3'] as const).map((key) => (
<View key={key} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}> <View key={key} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Ionicons name="checkmark-circle" size={15} color="#16a34a" /> <Ionicons name="checkmark-circle" size={15} color="#16a34a" />
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: '#525252', flex: 1 }}> <Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, flex: 1 }}>
{t(`mail.${key}`)} {t(`mail.${key}`)}
</Text> </Text>
</View> </View>

View File

@ -1,6 +1,6 @@
import { View, Text, Pressable } from 'react-native'; import { View, Text, Pressable } 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';
type Props = { type Props = {
onDismiss?: () => void; onDismiss?: () => void;
@ -8,6 +8,7 @@ type Props = {
}; };
export function DigaMissionBanner({ onDismiss, onContribute }: Props) { export function DigaMissionBanner({ onDismiss, onContribute }: Props) {
const colors = useColors();
return ( return (
<View <View
style={{ style={{

View File

@ -2,11 +2,12 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { View, Text, Pressable, Animated, StyleSheet } from 'react-native'; import { View, Text, Pressable, Animated, StyleSheet } from 'react-native';
import { BREATH_PHASES, TOTAL_ROUNDS, type BreathState } from '../../lib/sosConstants'; import { BREATH_PHASES, TOTAL_ROUNDS, type BreathState } from '../../lib/sosConstants';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
type Props = { onDone: () => void; onSpeak?: (text: string) => Promise<void> | void }; type Props = { onDone: () => void; onSpeak?: (text: string) => Promise<void> | void };
export function BreathingCard({ onDone, onSpeak }: Props) { export function BreathingCard({ onDone, onSpeak }: Props) {
const colors = useColors();
const [breathState, setBreathState] = useState<BreathState>('idle'); const [breathState, setBreathState] = useState<BreathState>('idle');
const [countdown, setCountdown] = useState(3); const [countdown, setCountdown] = useState(3);
const [round, setRound] = useState(1); const [round, setRound] = useState(1);
@ -86,7 +87,7 @@ export function BreathingCard({ onDone, onSpeak }: Props) {
<View style={{ alignItems: 'center', gap: 16 }}> <View style={{ alignItems: 'center', gap: 16 }}>
<Text style={st.breathTitle}>4-7-8 Atemübung</Text> <Text style={st.breathTitle}>4-7-8 Atemübung</Text>
<Text style={st.breathSub}>3 Runden · beruhigt dein Nervensystem</Text> <Text style={st.breathSub}>3 Runden · beruhigt dein Nervensystem</Text>
<Pressable style={st.breathStartBtn} onPress={() => { setCountdown(3); setBreathState('countdown'); }}> <Pressable style={[st.breathStartBtn, { backgroundColor: colors.brandOrange }]} onPress={() => { setCountdown(3); setBreathState('countdown'); }}>
<Text style={st.breathStartTxt}>Starten</Text> <Text style={st.breathStartTxt}>Starten</Text>
</Pressable> </Pressable>
</View> </View>
@ -114,6 +115,7 @@ export function BreathingCard({ onDone, onSpeak }: Props) {
// ── BreathingDrawer (bottom sheet, covers input, slides up) ─────────────────── // ── BreathingDrawer (bottom sheet, covers input, slides up) ───────────────────
export function BreathingDrawer({ onDone, onSpeak }: Props) { export function BreathingDrawer({ onDone, onSpeak }: Props) {
const colors = useColors();
const slideAnim = useRef(new Animated.Value(500)).current; const slideAnim = useRef(new Animated.Value(500)).current;
useEffect(() => { useEffect(() => {
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, damping: 22, mass: 1, stiffness: 200 }).start(); Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, damping: 22, mass: 1, stiffness: 200 }).start();
@ -121,7 +123,7 @@ export function BreathingDrawer({ onDone, onSpeak }: Props) {
return ( return (
<> <>
<View style={st.breathBackdrop} pointerEvents="none" /> <View style={st.breathBackdrop} pointerEvents="none" />
<Animated.View style={[st.breathDrawerContainer, { transform: [{ translateY: slideAnim }] }]}> <Animated.View style={[st.breathDrawerContainer, { transform: [{ translateY: slideAnim }], backgroundColor: colors.bg }]}>
<View style={st.breathDrawerHandle} /> <View style={st.breathDrawerHandle} />
<BreathingCard onDone={onDone} onSpeak={onSpeak} /> <BreathingCard onDone={onDone} onSpeak={onSpeak} />
</Animated.View> </Animated.View>
@ -131,14 +133,14 @@ export function BreathingDrawer({ onDone, onSpeak }: Props) {
const st = StyleSheet.create({ const st = StyleSheet.create({
breathBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.28)', zIndex: 20 }, breathBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.28)', zIndex: 20 },
breathDrawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 21, backgroundColor: '#ffffff', borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingBottom: 36, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 24 }, breathDrawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 21, borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingBottom: 36, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 24 },
breathDrawerHandle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#d1d5db', alignSelf: 'center', marginTop: 14, marginBottom: 4 }, breathDrawerHandle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#d1d5db', alignSelf: 'center', marginTop: 14, marginBottom: 4 },
breathCardInner: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 8, alignItems: 'center', gap: 16 }, breathCardInner: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 8, alignItems: 'center', gap: 16 },
breathCircleLg: { width: 190, height: 190, borderRadius: 95, alignItems: 'center', justifyContent: 'center', borderWidth: 5 }, breathCircleLg: { width: 190, height: 190, borderRadius: 95, alignItems: 'center', justifyContent: 'center', borderWidth: 5 },
breathCountLg: { fontFamily: 'Nunito_800ExtraBold', fontSize: 60, color: '#111827', lineHeight: 68 }, breathCountLg: { fontFamily: 'Nunito_800ExtraBold', fontSize: 60, color: '#111827', lineHeight: 68 },
breathTitle: { fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }, breathTitle: { fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' },
breathSub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', textAlign: 'center' }, breathSub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', textAlign: 'center' },
breathStartBtn: { borderRadius: 12, backgroundColor: colors.brandOrange, paddingHorizontal: 28, paddingVertical: 10, marginTop: 4 }, breathStartBtn: { borderRadius: 12, paddingHorizontal: 28, paddingVertical: 10, marginTop: 4 },
breathStartTxt: { color: '#fff', fontFamily: 'Nunito_700Bold', fontSize: 14 }, breathStartTxt: { color: '#fff', fontFamily: 'Nunito_700Bold', fontSize: 14 },
breathRound: { fontFamily: 'Nunito_600SemiBold', fontSize: 12, color: '#9ca3af' }, breathRound: { fontFamily: 'Nunito_600SemiBold', fontSize: 12, color: '#9ca3af' },
breathPhaseLabel: { fontFamily: 'Nunito_700Bold', fontSize: 13 }, breathPhaseLabel: { fontFamily: 'Nunito_700Bold', fontSize: 13 },

View File

@ -11,7 +11,7 @@ import {
ScrollView, ScrollView,
} from 'react-native'; } 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';
import type { SosFeedback } from './SosFeedbackModal'; import type { SosFeedback } from './SosFeedbackModal';
/** /**
@ -25,6 +25,7 @@ export function InlineRatingDrawer({
onSubmit: (feedback: SosFeedback) => Promise<void> | void; onSubmit: (feedback: SosFeedback) => Promise<void> | void;
onClose: () => void; onClose: () => void;
}) { }) {
const colors = useColors();
const slide = useRef(new Animated.Value(600)).current; const slide = useRef(new Animated.Value(600)).current;
const [better, setBetter] = useState<boolean | null>(null); const [better, setBetter] = useState<boolean | null>(null);
const [rating, setRating] = useState(0); const [rating, setRating] = useState(0);
@ -58,7 +59,7 @@ export function InlineRatingDrawer({
return ( return (
<> <>
<Pressable style={s.backdrop} onPress={onClose} /> <Pressable style={s.backdrop} onPress={onClose} />
<Animated.View style={[s.drawer, { transform: [{ translateY: slide }] }]}> <Animated.View style={[s.drawer, { transform: [{ translateY: slide }], backgroundColor: colors.bg }]}>
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0} keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0}
@ -71,13 +72,13 @@ export function InlineRatingDrawer({
> >
<View style={s.header}> <View style={s.header}>
<Ionicons name="star" size={20} color="#f59e0b" /> <Ionicons name="star" size={20} color="#f59e0b" />
<Text style={s.title}>Bewerte diese Session</Text> <Text style={[s.title, { color: colors.text }]}>Bewerte diese Session</Text>
</View> </View>
<Text style={s.sub}> <Text style={[s.sub, { color: colors.textMuted }]}>
Dein Feedback hilft uns, Lyra besser zu machen. Dein Feedback hilft uns, Lyra besser zu machen.
</Text> </Text>
<Text style={s.q}>Fühlst du dich besser?</Text> <Text style={[s.q, { color: colors.textMuted }]}>Fühlst du dich besser?</Text>
<View style={s.btnRow}> <View style={s.btnRow}>
<Pressable <Pressable
style={[s.choiceBtn, better === true && s.choiceBtnYes]} style={[s.choiceBtn, better === true && s.choiceBtnYes]}
@ -103,7 +104,7 @@ export function InlineRatingDrawer({
</Pressable> </Pressable>
</View> </View>
<Text style={s.q}>Bewertung</Text> <Text style={[s.q, { color: colors.textMuted }]}>Bewertung</Text>
<View style={s.starsRow}> <View style={s.starsRow}>
{[1, 2, 3, 4, 5].map((n) => ( {[1, 2, 3, 4, 5].map((n) => (
<Pressable key={n} onPress={() => setRating(n)} hitSlop={6}> <Pressable key={n} onPress={() => setRating(n)} hitSlop={6}>
@ -116,9 +117,9 @@ export function InlineRatingDrawer({
))} ))}
</View> </View>
<Text style={s.q}>Bemerkung (optional)</Text> <Text style={[s.q, { color: colors.textMuted }]}>Bemerkung (optional)</Text>
<TextInput <TextInput
style={s.textArea} style={[s.textArea, { backgroundColor: colors.surfaceElevated, borderColor: colors.border, color: colors.text }]}
placeholder="Was war hilfreich? Was nicht?" placeholder="Was war hilfreich? Was nicht?"
placeholderTextColor="#94a3b8" placeholderTextColor="#94a3b8"
multiline multiline
@ -132,7 +133,7 @@ export function InlineRatingDrawer({
<Text style={s.cancelTxt}>Abbrechen</Text> <Text style={s.cancelTxt}>Abbrechen</Text>
</Pressable> </Pressable>
<Pressable <Pressable
style={[s.submitBtn, submitting && { opacity: 0.6 }]} style={[s.submitBtn, { backgroundColor: colors.brandOrange }, submitting && { opacity: 0.6 }]}
onPress={submit} onPress={submit}
disabled={submitting} disabled={submitting}
> >
@ -199,7 +200,7 @@ const s = StyleSheet.create({
cancelTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' }, cancelTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' },
submitBtn: { submitBtn: {
flex: 2, paddingVertical: 12, borderRadius: 12, flex: 2, paddingVertical: 12, borderRadius: 12,
alignItems: 'center', backgroundColor: colors.brandOrange, alignItems: 'center',
}, },
submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' }, submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' },
}); });

View File

@ -12,7 +12,7 @@ import {
ScrollView, ScrollView,
} from 'react-native'; } 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 interface ShareSuccessPayload { export interface ShareSuccessPayload {
text: string; text: string;
@ -35,6 +35,7 @@ export function ShareSuccessDrawer({
onClose: () => void; onClose: () => void;
onRegenerate?: () => void; onRegenerate?: () => void;
}) { }) {
const colors = useColors();
const slide = useRef(new Animated.Value(600)).current; const slide = useRef(new Animated.Value(600)).current;
const [text, setText] = useState(initialText); const [text, setText] = useState(initialText);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@ -67,7 +68,7 @@ export function ShareSuccessDrawer({
return ( return (
<> <>
<Pressable style={s.backdrop} onPress={onClose} /> <Pressable style={s.backdrop} onPress={onClose} />
<Animated.View style={[s.drawer, { transform: [{ translateY: slide }] }]}> <Animated.View style={[s.drawer, { transform: [{ translateY: slide }], backgroundColor: colors.bg }]}>
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0} keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0}
@ -80,9 +81,9 @@ export function ShareSuccessDrawer({
> >
<View style={s.header}> <View style={s.header}>
<Ionicons name="sparkles" size={20} color={colors.brandOrange} /> <Ionicons name="sparkles" size={20} color={colors.brandOrange} />
<Text style={s.title}>Erfolg teilen</Text> <Text style={[s.title, { color: colors.text }]}>Erfolg teilen</Text>
</View> </View>
<Text style={s.sub}> <Text style={[s.sub, { color: colors.textMuted }]}>
Inspiriere andere dein Beitrag wird anonym in der Community gepostet. Inspiriere andere dein Beitrag wird anonym in der Community gepostet.
</Text> </Text>
@ -93,7 +94,7 @@ export function ShareSuccessDrawer({
</View> </View>
) : ( ) : (
<TextInput <TextInput
style={s.textArea} style={[s.textArea, { backgroundColor: colors.surfaceElevated, borderColor: colors.border, color: colors.text }]}
multiline multiline
value={text} value={text}
onChangeText={setText} onChangeText={setText}
@ -118,7 +119,7 @@ export function ShareSuccessDrawer({
<Text style={s.cancelTxt}>Abbrechen</Text> <Text style={s.cancelTxt}>Abbrechen</Text>
</Pressable> </Pressable>
<Pressable <Pressable
style={[s.shareBtn, (!text.trim() || submitting || generating) && s.shareBtnDisabled]} style={[s.shareBtn, { backgroundColor: colors.brandOrange }, (!text.trim() || submitting || generating) && s.shareBtnDisabled]}
onPress={handleShare} onPress={handleShare}
disabled={!text.trim() || submitting || generating} disabled={!text.trim() || submitting || generating}
> >
@ -203,7 +204,6 @@ const s = StyleSheet.create({
shareBtn: { shareBtn: {
flex: 1, minWidth: 110, flex: 1, minWidth: 110,
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6,
backgroundColor: colors.brandOrange,
borderRadius: 12, paddingVertical: 12, borderRadius: 12, paddingVertical: 12,
}, },
shareBtnDisabled: { opacity: 0.5 }, shareBtnDisabled: { opacity: 0.5 },

View File

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { View, Text, Pressable, TextInput, Modal, StyleSheet, Platform, KeyboardAvoidingView, ScrollView } from 'react-native'; import { View, Text, Pressable, TextInput, Modal, StyleSheet, Platform, KeyboardAvoidingView, ScrollView } 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 interface SosFeedback { export interface SosFeedback {
better: boolean | null; better: boolean | null;
@ -18,6 +18,7 @@ export function SosFeedbackModal({
onSubmit: (feedback: SosFeedback) => void; onSubmit: (feedback: SosFeedback) => void;
onSkip: () => void; onSkip: () => void;
}) { }) {
const colors = useColors();
const [better, setBetter] = useState<boolean | null>(null); const [better, setBetter] = useState<boolean | null>(null);
const [rating, setRating] = useState<number>(0); const [rating, setRating] = useState<number>(0);
const [text, setText] = useState(''); const [text, setText] = useState('');
@ -43,12 +44,12 @@ export function SosFeedbackModal({
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<View style={s.card}> <View style={[s.card, { backgroundColor: colors.bg }]}>
<Text style={s.title}>Wie war diese Session?</Text> <Text style={[s.title, { color: colors.text }]}>Wie war diese Session?</Text>
<Text style={s.sub}>Dein Feedback hilft Lyra besser zu werden.</Text> <Text style={[s.sub, { color: colors.textMuted }]}>Dein Feedback hilft Lyra besser zu werden.</Text>
{/* Better Yes/No */} {/* Better Yes/No */}
<Text style={s.q}>Fühlst du dich besser?</Text> <Text style={[s.q, { color: colors.textMuted }]}>Fühlst du dich besser?</Text>
<View style={s.btnRow}> <View style={s.btnRow}>
<Pressable <Pressable
style={[s.choiceBtn, better === true && s.choiceBtnYes]} style={[s.choiceBtn, better === true && s.choiceBtnYes]}
@ -67,7 +68,7 @@ export function SosFeedbackModal({
</View> </View>
{/* Stars */} {/* Stars */}
<Text style={s.q}>Bewertung</Text> <Text style={[s.q, { color: colors.textMuted }]}>Bewertung</Text>
<View style={s.starsRow}> <View style={s.starsRow}>
{[1, 2, 3, 4, 5].map((n) => ( {[1, 2, 3, 4, 5].map((n) => (
<Pressable key={n} onPress={() => setRating(n)} hitSlop={6}> <Pressable key={n} onPress={() => setRating(n)} hitSlop={6}>
@ -81,9 +82,9 @@ export function SosFeedbackModal({
</View> </View>
{/* Comment */} {/* Comment */}
<Text style={s.q}>Bemerkung (optional)</Text> <Text style={[s.q, { color: colors.textMuted }]}>Bemerkung (optional)</Text>
<TextInput <TextInput
style={s.textArea} style={[s.textArea, { backgroundColor: colors.surfaceElevated, borderColor: colors.border, color: colors.text }]}
placeholder="Was war hilfreich? Was nicht?" placeholder="Was war hilfreich? Was nicht?"
placeholderTextColor="#94a3b8" placeholderTextColor="#94a3b8"
multiline multiline
@ -98,7 +99,7 @@ export function SosFeedbackModal({
<Pressable style={s.skipBtn} onPress={skip}> <Pressable style={s.skipBtn} onPress={skip}>
<Text style={s.skipTxt}>Überspringen</Text> <Text style={s.skipTxt}>Überspringen</Text>
</Pressable> </Pressable>
<Pressable style={s.submitBtn} onPress={submit}> <Pressable style={[s.submitBtn, { backgroundColor: colors.brandOrange }]} onPress={submit}>
<Text style={s.submitTxt}>Senden</Text> <Text style={s.submitTxt}>Senden</Text>
</Pressable> </Pressable>
</View> </View>
@ -136,6 +137,6 @@ const s = StyleSheet.create({
actions: { flexDirection: 'row', gap: 10, marginTop: 18 }, actions: { flexDirection: 'row', gap: 10, marginTop: 18 },
skipBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: '#f1f5f9' }, skipBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: '#f1f5f9' },
skipTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' }, skipTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' },
submitBtn: { flex: 2, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: colors.brandOrange }, submitBtn: { flex: 2, paddingVertical: 12, borderRadius: 12, alignItems: 'center' },
submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' }, submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' },
}); });

View File

@ -3,7 +3,7 @@ import { View, Text, ActivityIndicator } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { apiFetch } from '../../lib/api'; import { apiFetch } from '../../lib/api';
import { colors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
type Emotion = 'stress' | 'sadness' | 'anger' | 'empty' | 'boredom' | 'other'; type Emotion = 'stress' | 'sadness' | 'anger' | 'empty' | 'boredom' | 'other';
@ -30,6 +30,7 @@ function emotionLabel(key: string, t: (k: string) => string): string {
} }
function StatCard({ label, value, color }: { label: string; value: string; color: string }) { function StatCard({ label, value, color }: { label: string; value: string; color: string }) {
const colors = useColors();
return ( return (
<View <View
style={{ style={{
@ -37,8 +38,8 @@ function StatCard({ label, value, color }: { label: string; value: string; color
marginHorizontal: 4, marginHorizontal: 4,
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderColor: '#f3f4f6', borderColor: colors.border,
backgroundColor: '#fafafa', backgroundColor: colors.surface,
paddingVertical: 10, paddingVertical: 10,
alignItems: 'center', alignItems: 'center',
}} }}
@ -50,7 +51,7 @@ function StatCard({ label, value, color }: { label: string; value: string; color
textAlign: 'center', textAlign: 'center',
fontFamily: 'Nunito_400Regular', fontFamily: 'Nunito_400Regular',
fontSize: 11, fontSize: 11,
color: '#6b7280', color: colors.textMuted,
}} }}
> >
{label} {label}
@ -61,6 +62,7 @@ function StatCard({ label, value, color }: { label: string; value: string; color
export function UrgeStats() { export function UrgeStats() {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors();
const [logs, setLogs] = useState<UrgeLog[]>([]); const [logs, setLogs] = useState<UrgeLog[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -175,18 +177,18 @@ export function UrgeStats() {
style={{ style={{
borderRadius: 18, borderRadius: 18,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e7eb', borderColor: colors.border,
backgroundColor: '#fff', backgroundColor: colors.bg,
padding: 14, padding: 14,
}} }}
> >
<Text <Text
style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827', marginBottom: 10 }} style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text, marginBottom: 10 }}
> >
{t('urge.this_week')} {t('urge.this_week')}
</Text> </Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<StatCard label={t('urge.total_urges')} value={String(weeklyStats.total)} color="#111827" /> <StatCard label={t('urge.total_urges')} value={String(weeklyStats.total)} color={colors.text} />
<StatCard <StatCard
label={t('urge.overcome_count')} label={t('urge.overcome_count')}
value={String(weeklyStats.overcome)} value={String(weeklyStats.overcome)}
@ -207,8 +209,8 @@ export function UrgeStats() {
style={{ style={{
borderRadius: 18, borderRadius: 18,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e7eb', borderColor: colors.border,
backgroundColor: '#fff', backgroundColor: colors.bg,
padding: 14, padding: 14,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -218,7 +220,7 @@ export function UrgeStats() {
<Text <Text
style={{ style={{
marginLeft: 8, marginLeft: 8,
color: '#374151', color: colors.textMuted,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
flex: 1, flex: 1,
}} }}
@ -232,12 +234,12 @@ export function UrgeStats() {
style={{ style={{
borderRadius: 18, borderRadius: 18,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e7eb', borderColor: colors.border,
backgroundColor: '#fff', backgroundColor: colors.bg,
padding: 14, padding: 14,
}} }}
> >
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }}> <Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text }}>
{t('urge.chart_weekday_title')} {t('urge.chart_weekday_title')}
</Text> </Text>
<View style={{ flexDirection: 'row', alignItems: 'flex-end', height: 70, marginTop: 10 }}> <View style={{ flexDirection: 'row', alignItems: 'flex-end', height: 70, marginTop: 10 }}>
@ -254,7 +256,7 @@ export function UrgeStats() {
}} }}
/> />
<Text <Text
style={{ fontFamily: 'Nunito_600SemiBold', fontSize: 10, color: '#6b7280' }} style={{ fontFamily: 'Nunito_600SemiBold', fontSize: 10, color: colors.textMuted }}
> >
{day.label} {day.label}
</Text> </Text>
@ -268,12 +270,12 @@ export function UrgeStats() {
style={{ style={{
borderRadius: 18, borderRadius: 18,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e7eb', borderColor: colors.border,
backgroundColor: '#fff', backgroundColor: colors.bg,
padding: 14, padding: 14,
}} }}
> >
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }}> <Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text }}>
{t('urge.chart_time_title')} {t('urge.chart_time_title')}
</Text> </Text>
<View style={{ marginTop: 8, gap: 8 }}> <View style={{ marginTop: 8, gap: 8 }}>
@ -284,7 +286,7 @@ export function UrgeStats() {
style={{ style={{
width: 74, width: 74,
fontSize: 12, fontSize: 12,
color: '#6b7280', color: colors.textMuted,
fontFamily: 'Nunito_600SemiBold', fontFamily: 'Nunito_600SemiBold',
}} }}
> >
@ -295,7 +297,7 @@ export function UrgeStats() {
flex: 1, flex: 1,
height: 7, height: 7,
borderRadius: 4, borderRadius: 4,
backgroundColor: '#e5e7eb', backgroundColor: colors.surfaceElevated,
}} }}
> >
<View <View
@ -312,7 +314,7 @@ export function UrgeStats() {
width: 24, width: 24,
textAlign: 'right', textAlign: 'right',
fontSize: 12, fontSize: 12,
color: '#6b7280', color: colors.textMuted,
marginLeft: 6, marginLeft: 6,
}} }}
> >
@ -328,12 +330,12 @@ export function UrgeStats() {
style={{ style={{
borderRadius: 18, borderRadius: 18,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e7eb', borderColor: colors.border,
backgroundColor: '#fff', backgroundColor: colors.bg,
padding: 14, padding: 14,
}} }}
> >
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }}> <Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text }}>
{t('urge.chart_top_emotions')} {t('urge.chart_top_emotions')}
</Text> </Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }}> <View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }}>
@ -341,9 +343,9 @@ export function UrgeStats() {
<View <View
key={emo} key={emo}
style={{ style={{
backgroundColor: '#f3f4f6', backgroundColor: colors.surfaceElevated,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e7eb', borderColor: colors.border,
borderRadius: 999, borderRadius: 999,
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 6, paddingVertical: 6,
@ -352,7 +354,7 @@ export function UrgeStats() {
}} }}
> >
<Text <Text
style={{ fontSize: 12, color: '#374151', fontFamily: 'Nunito_600SemiBold' }} style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_600SemiBold' }}
> >
{emotionLabel(emo, t)} x{c} {emotionLabel(emo, t)} x{c}
</Text> </Text>