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

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { View } from 'react-native';
import { useRouter, useFocusEffect } from 'expo-router';
import { useColors } from '../../lib/theme';
/**
* Placeholder-Screen für den Coach-Tab.
@ -11,6 +12,7 @@ import { useRouter, useFocusEffect } from 'expo-router';
*/
export default function CoachTabRedirect() {
const router = useRouter();
const colors = useColors();
useFocusEffect(
useCallback(() => {
@ -20,5 +22,5 @@ export default function CoachTabRedirect() {
}, [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 { useMailDisconnect } from '../../hooks/useMailDisconnect';
import { useUserPlan } from '../../hooks/useUserPlan';
import { useColors } from '../../lib/theme';
export default function MailScreen() {
const { t } = useTranslation();
const tabBarHeight = useBottomTabBarHeight();
const colors = useColors();
const { plan } = useUserPlan();
@ -72,7 +74,7 @@ export default function MailScreen() {
if (loading) {
return (
<View style={{ flex: 1, backgroundColor: '#fafafa' }}>
<View style={{ flex: 1, backgroundColor: colors.bg }}>
<AppHeader />
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#007AFF" />
@ -82,7 +84,7 @@ export default function MailScreen() {
}
return (
<View style={{ flex: 1, backgroundColor: '#fafafa' }}>
<View style={{ flex: 1, backgroundColor: colors.bg }}>
<AppHeader />
<ScrollView
@ -118,7 +120,7 @@ export default function MailScreen() {
style={{
fontSize: 11,
fontFamily: 'Nunito_700Bold',
color: '#737373',
color: colors.textMuted,
textTransform: 'uppercase',
letterSpacing: 0.8,
}}
@ -129,7 +131,7 @@ export default function MailScreen() {
style={{
fontSize: 11,
fontFamily: 'Nunito_400Regular',
color: '#a3a3a3',
color: colors.textMuted,
marginTop: 2,
}}
>
@ -147,7 +149,7 @@ export default function MailScreen() {
disabled={limitReached}
android_ripple={{ color: '#0066cc' }}
style={{
backgroundColor: limitReached ? '#e5e5e5' : '#007AFF',
backgroundColor: limitReached ? colors.surfaceElevated : '#007AFF',
borderRadius: 12,
opacity: limitReached ? 0.7 : 1,
shadowColor: '#007AFF',
@ -169,14 +171,14 @@ export default function MailScreen() {
<Ionicons
name="add"
size={18}
color={limitReached ? '#737373' : '#fff'}
color={limitReached ? colors.textMuted : '#fff'}
style={{ marginRight: 6 }}
/>
<Text
style={{
fontSize: 13,
fontFamily: 'Nunito_700Bold',
color: limitReached ? '#737373' : '#fff',
color: limitReached ? colors.textMuted : '#fff',
}}
>
{t('mail.add_account')}

View File

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

View File

@ -15,10 +15,11 @@ import { useEffect } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { supabase } from '../../lib/supabase';
import { colors } from '../../lib/theme';
import { useColors } from '../../lib/theme';
export default function AuthCallback() {
const router = useRouter();
const colors = useColors();
const params = useLocalSearchParams<{ access_token?: string; refresh_token?: string }>();
useEffect(() => {
@ -50,7 +51,7 @@ export default function AuthCallback() {
}, []);
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} />
</View>
);

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import {
TetrisGame,
} from '../components/urge/UrgeGames';
import { GameCard } from '../components/games/GameCard';
import { colors } from '../lib/theme';
import { useColors } from '../lib/theme';
import { apiFetch } from '../lib/api';
type GameStat = { avgStars: number; count: number };
@ -31,6 +31,7 @@ type LastScore = { game: GameType; score: number } | null;
export default function GamesScreen() {
const router = useRouter();
const { t } = useTranslation();
const colors = useColors();
const [active, setActive] = useState<GameType | null>(null);
const [lastScore, setLastScore] = useState<LastScore>(null);
const [gameStats, setGameStats] = useState<GameStats>(EMPTY_STATS);
@ -70,7 +71,7 @@ export default function GamesScreen() {
if (active) {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}>
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
<View
style={{
paddingHorizontal: 12,
@ -79,7 +80,7 @@ export default function GamesScreen() {
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.06)',
borderBottomColor: colors.border,
}}
>
<Pressable
@ -102,7 +103,7 @@ export default function GamesScreen() {
</Text>
</View>
</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)}
</Text>
<View style={{ width: 60 }} />
@ -127,7 +128,7 @@ export default function GamesScreen() {
}
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}>
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
<View
style={{
paddingHorizontal: 12,
@ -137,7 +138,7 @@ export default function GamesScreen() {
alignItems: 'center',
gap: 8,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.06)',
borderBottomColor: colors.border,
}}
>
<Pressable
@ -156,7 +157,7 @@ export default function GamesScreen() {
<Ionicons name="chevron-back" size={26} color={colors.text} />
</View>
</Pressable>
<Text style={{ fontSize: 20, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}>
<Text style={{ fontSize: 20, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
{t('games.title')}
</Text>
</View>
@ -169,7 +170,7 @@ export default function GamesScreen() {
<Text
style={{
fontSize: 13,
color: '#737373',
color: colors.textMuted,
fontFamily: 'Nunito_400Regular',
lineHeight: 19,
marginBottom: 18,
@ -232,7 +233,7 @@ export default function GamesScreen() {
style={{
textAlign: 'center',
fontSize: 11,
color: '#a3a3a3',
color: colors.textMuted,
fontFamily: 'Nunito_400Regular',
marginTop: 24,
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 { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { colors } from '../../lib/theme';
import { useColors } from '../../lib/theme';
import { resolveAvatar } from '../../lib/resolveAvatar';
import type { Plan } from '../../hooks/useUserPlan';
@ -52,13 +52,14 @@ type StatProps = {
};
function ForeignStat({ value, label }: StatProps) {
const colors = useColors();
return (
<View
style={{
flex: 1,
backgroundColor: '#ffffff',
backgroundColor: colors.bg,
borderWidth: 1,
borderColor: '#e5e5e5',
borderColor: colors.border,
borderRadius: 14,
paddingVertical: 14,
alignItems: 'center',
@ -85,6 +86,7 @@ function ForeignStat({ value, label }: StatProps) {
export default function ForeignProfileScreen() {
const insets = useSafeAreaInsets();
const router = useRouter();
const colors = useColors();
const { userId } = useLocalSearchParams<{ userId: string }>();
const [imageFailed, setImageFailed] = useState(false);
const [isFollowing, setIsFollowing] = useState(DUMMY_FOREIGN.isFollowing);
@ -99,13 +101,13 @@ export default function ForeignProfileScreen() {
const planStyle = planColors[profile.plan];
return (
<View style={{ flex: 1, backgroundColor: '#ffffff' }}>
<View style={{ flex: 1, backgroundColor: colors.bg }}>
<View
style={{
paddingTop: insets.top,
backgroundColor: '#ffffff',
backgroundColor: colors.bg,
borderBottomWidth: 1,
borderBottomColor: '#e5e5e5',
borderBottomColor: colors.border,
}}
>
<View
@ -147,7 +149,7 @@ export default function ForeignProfileScreen() {
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
backgroundColor: showImage ? '#fafafa' : colors.brandOrange,
backgroundColor: showImage ? colors.surface : colors.brandOrange,
}}
>
{showImage ? (
@ -215,9 +217,9 @@ export default function ForeignProfileScreen() {
<View style={{
paddingVertical: 11,
borderRadius: 12,
backgroundColor: isFollowing ? '#f5f5f5' : colors.brandOrange,
backgroundColor: isFollowing ? colors.surfaceElevated : colors.brandOrange,
borderWidth: 1,
borderColor: isFollowing ? '#e5e5e5' : colors.brandOrange,
borderColor: isFollowing ? colors.border : colors.brandOrange,
alignItems: 'center',
}}>
<Text
@ -244,9 +246,9 @@ export default function ForeignProfileScreen() {
<View style={{
paddingVertical: 11,
borderRadius: 12,
backgroundColor: '#ffffff',
backgroundColor: colors.bg,
borderWidth: 1,
borderColor: '#e5e5e5',
borderColor: colors.border,
alignItems: 'center',
}}>
<Text
@ -292,9 +294,9 @@ export default function ForeignProfileScreen() {
</Text>
<View
style={{
backgroundColor: '#ffffff',
backgroundColor: colors.bg,
borderWidth: 1,
borderColor: '#e5e5e5',
borderColor: colors.border,
borderRadius: 14,
padding: 16,
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
import * as FileSystem from 'expo-file-system/legacy';
import { useTranslation } from 'react-i18next';
import { colors } from '../../lib/theme';
import { useColors } from '../../lib/theme';
import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars';
import { resolveAvatar } from '../../lib/resolveAvatar';
import { apiFetch } from '../../lib/api';
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() {
const router = useRouter();
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const colors = useColors();
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 [avatarId, setAvatarId] = useState<string | null>(me?.avatar ?? 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 { ChatInput, type SendPayload } from '../components/chat/ChatInput';
import { useRoomRealtime } from '../hooks/useChatRealtime';
import { colors } from '../lib/theme';
import { useColors } from '../lib/theme';
const GROUP_GAP_MS = 5 * 60 * 1000;
@ -64,6 +64,8 @@ export default function RoomScreen() {
const { t } = useTranslation();
const router = useRouter();
const insets = useSafeAreaInsets();
const colors = useColors();
const styles = makeStyles(colors);
const queryClient = useQueryClient();
const flatRef = useRef<FlatList>(null);
const [myUserId, setMyUserId] = useState<string | undefined>();
@ -298,7 +300,7 @@ export default function RoomScreen() {
{/* Header */}
<View style={styles.header}>
<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>
<View style={styles.headerCenter}>
<View style={styles.headerAvatar}>
@ -320,7 +322,7 @@ export default function RoomScreen() {
</View>
</View>
<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>
</View>
@ -430,6 +432,8 @@ function RoomSettingsModal({
roomId: string;
}) {
const { t } = useTranslation();
const colors = useColors();
const modal = makeModalStyles(colors);
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
const [loadingReqs, setLoadingReqs] = useState(false);
@ -637,221 +641,225 @@ function RoomSettingsModal({
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fafafa' },
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
backgroundColor: '#fff',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#e5e5e5',
},
iconBtn: {
width: 36,
height: 36,
borderRadius: 12,
backgroundColor: '#f5f5f5',
alignItems: 'center',
justifyContent: 'center',
},
headerCenter: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 8,
},
headerAvatar: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#e5e5e5',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
marginRight: 8,
},
headerAvatarImg: { width: 36, height: 36 },
headerAvatarInitials: {
fontSize: 12,
fontFamily: 'Nunito_700Bold',
color: '#737373',
},
headerName: {
fontSize: 15,
fontFamily: 'Nunito_700Bold',
color: '#171717',
},
headerSub: {
fontSize: 11,
fontFamily: 'Nunito_500Medium',
color: '#737373',
marginTop: 1,
},
loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center' },
joinBox: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
},
joinTitle: {
fontSize: 20,
fontFamily: 'Nunito_700Bold',
color: '#171717',
marginTop: 14,
},
joinDesc: {
fontSize: 13,
fontFamily: 'Nunito_500Medium',
color: '#737373',
marginTop: 6,
textAlign: 'center',
},
joinHint: {
fontSize: 12,
fontFamily: 'Nunito_500Medium',
color: '#a3a3a3',
marginTop: 18,
textAlign: 'center',
},
joinBtn: {
marginTop: 16,
backgroundColor: '#007AFF',
paddingHorizontal: 32,
paddingVertical: 12,
borderRadius: 12,
minWidth: 140,
alignItems: 'center',
},
joinBtnText: {
color: '#fff',
fontSize: 14,
fontFamily: 'Nunito_700Bold',
},
pendingBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fef3c7',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
marginTop: 16,
},
pendingText: {
color: '#92400e',
fontSize: 12,
fontFamily: 'Nunito_700Bold',
marginLeft: 6,
},
});
function makeStyles(colors: ReturnType<typeof useColors>) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bg },
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
backgroundColor: colors.bg,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
iconBtn: {
width: 36,
height: 36,
borderRadius: 12,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
},
headerCenter: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 8,
},
headerAvatar: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
marginRight: 8,
},
headerAvatarImg: { width: 36, height: 36 },
headerAvatarInitials: {
fontSize: 12,
fontFamily: 'Nunito_700Bold',
color: colors.textMuted,
},
headerName: {
fontSize: 15,
fontFamily: 'Nunito_700Bold',
color: colors.text,
},
headerSub: {
fontSize: 11,
fontFamily: 'Nunito_500Medium',
color: colors.textMuted,
marginTop: 1,
},
loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center' },
joinBox: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
},
joinTitle: {
fontSize: 20,
fontFamily: 'Nunito_700Bold',
color: colors.text,
marginTop: 14,
},
joinDesc: {
fontSize: 13,
fontFamily: 'Nunito_500Medium',
color: colors.textMuted,
marginTop: 6,
textAlign: 'center',
},
joinHint: {
fontSize: 12,
fontFamily: 'Nunito_500Medium',
color: colors.textMuted,
marginTop: 18,
textAlign: 'center',
},
joinBtn: {
marginTop: 16,
backgroundColor: '#007AFF',
paddingHorizontal: 32,
paddingVertical: 12,
borderRadius: 12,
minWidth: 140,
alignItems: 'center',
},
joinBtnText: {
color: '#fff',
fontSize: 14,
fontFamily: 'Nunito_700Bold',
},
pendingBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fef3c7',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
marginTop: 16,
},
pendingText: {
color: '#92400e',
fontSize: 12,
fontFamily: 'Nunito_700Bold',
marginLeft: 6,
},
});
}
const modal = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fafafa' },
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#fff',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#e5e5e5',
},
title: { fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#171717' },
section: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 14,
marginBottom: 12,
},
sectionTitle: {
fontSize: 12,
fontFamily: 'Nunito_700Bold',
color: '#737373',
textTransform: 'uppercase',
marginBottom: 10,
letterSpacing: 0.5,
},
avatarWrap: { alignSelf: 'center', marginBottom: 10 },
avatar: { width: 80, height: 80, borderRadius: 40 },
avatarPlaceholder: {
backgroundColor: '#e5e5e5',
alignItems: 'center',
justifyContent: 'center',
},
avatarEdit: {
position: 'absolute',
right: -2,
bottom: -2,
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#007AFF',
borderWidth: 3,
borderColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
roomName: {
fontSize: 17,
fontFamily: 'Nunito_700Bold',
color: '#171717',
textAlign: 'center',
},
roomDesc: {
fontSize: 12,
fontFamily: 'Nunito_500Medium',
color: '#737373',
textAlign: 'center',
marginTop: 4,
},
memberRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#f5f5f5',
},
memberAvatar: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#e5e5e5',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
marginRight: 10,
},
memberAvatarImg: { width: 32, height: 32 },
memberInitials: {
fontSize: 11,
fontFamily: 'Nunito_700Bold',
color: '#737373',
},
memberName: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: '#171717' },
memberRole: { fontSize: 11, color: '#a3a3a3', marginTop: 1, textTransform: 'capitalize' },
actionBtn: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
},
actionText: { fontSize: 11, fontFamily: 'Nunito_700Bold' },
emptyText: { fontSize: 12, color: '#a3a3a3' },
leaveBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fee2e2',
paddingVertical: 12,
borderRadius: 10,
marginTop: 8,
},
leaveText: {
color: '#991b1b',
fontSize: 13,
fontFamily: 'Nunito_700Bold',
marginLeft: 6,
},
});
function makeModalStyles(colors: ReturnType<typeof useColors>) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bg },
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: colors.bg,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
title: { fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text },
section: {
backgroundColor: colors.surface,
borderRadius: 12,
padding: 14,
marginBottom: 12,
},
sectionTitle: {
fontSize: 12,
fontFamily: 'Nunito_700Bold',
color: colors.textMuted,
textTransform: 'uppercase',
marginBottom: 10,
letterSpacing: 0.5,
},
avatarWrap: { alignSelf: 'center', marginBottom: 10 },
avatar: { width: 80, height: 80, borderRadius: 40 },
avatarPlaceholder: {
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
},
avatarEdit: {
position: 'absolute',
right: -2,
bottom: -2,
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#007AFF',
borderWidth: 3,
borderColor: colors.bg,
alignItems: 'center',
justifyContent: 'center',
},
roomName: {
fontSize: 17,
fontFamily: 'Nunito_700Bold',
color: colors.text,
textAlign: 'center',
},
roomDesc: {
fontSize: 12,
fontFamily: 'Nunito_500Medium',
color: colors.textMuted,
textAlign: 'center',
marginTop: 4,
},
memberRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
memberAvatar: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
marginRight: 10,
},
memberAvatarImg: { width: 32, height: 32 },
memberInitials: {
fontSize: 11,
fontFamily: 'Nunito_700Bold',
color: colors.textMuted,
},
memberName: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text },
memberRole: { fontSize: 11, color: colors.textMuted, marginTop: 1, textTransform: 'capitalize' },
actionBtn: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
},
actionText: { fontSize: 11, fontFamily: 'Nunito_700Bold' },
emptyText: { fontSize: 12, color: colors.textMuted },
leaveBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fee2e2',
paddingVertical: 12,
borderRadius: 10,
marginTop: 8,
},
leaveText: {
color: '#991b1b',
fontSize: 13,
fontFamily: 'Nunito_700Bold',
marginLeft: 6,
},
});
}

View File

@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
import { RiveAvatar } from '../components/RiveAvatar';
import { apiFetch } from '../lib/api';
import { supabase } from '../lib/supabase';
import { colors } from '../lib/theme';
import { useColors } from '../lib/theme';
import {
type GameType, GAME_META, MemoryGame, TicTacToeGame, SnakeGame, TetrisGame,
} from '../components/urge/UrgeGames';
@ -41,6 +41,8 @@ export default function SOSScreen() {
const { t, i18n } = useTranslation();
const router = useRouter();
const insets = useSafeAreaInsets();
const colors = useColors();
const st = makeStyles(colors);
const flatRef = useRef<FlatList>(null);
const [messages, setMessages] = useState<SosMsg[]>([]);
@ -1089,7 +1091,7 @@ export default function SOSScreen() {
{/* Header */}
<View style={[st.topBar, { top: insets.top + 6 }]}>
<Pressable style={st.actionBtn} onPress={attemptExit} hitSlop={12}>
<Ionicons name="close" size={22} color="#374151" />
<Ionicons name="close" size={22} color={colors.textMuted} />
</Pressable>
<View style={st.avatarCenter}>
<RiveAvatar emotion={emotion} size="md" />
@ -1286,39 +1288,41 @@ export default function SOSScreen() {
);
}
const st = StyleSheet.create({
container: { flex: 1, backgroundColor: '#ffffff' },
topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 },
topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: '#ffffff' },
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 },
avatarCenter: { flex: 1, alignItems: 'center', gap: 4 },
avatarMeta: { alignItems: 'center', gap: 2 },
avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' },
speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: '#f5f5f5', alignItems: 'center', justifyContent: 'center' },
listContent: { paddingHorizontal: 12, paddingBottom: 4 },
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 },
chip: {
borderRadius: 14,
borderWidth: 1.5,
borderColor: '#9ca3af', // sichtbarer Ring (medium-grau gegen weiß)
backgroundColor: '#ffffff',
paddingHorizontal: 16,
paddingVertical: 11,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.12,
shadowRadius: 6,
elevation: 3,
},
chipPressed: {
backgroundColor: '#f3f4f6',
borderColor: '#6b7280', // dunkler beim Press → spürbares Feedback
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 },
textInput: { flex: 1, minHeight: 40, maxHeight: 120, backgroundColor: '#f3f4f6', borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, fontFamily: 'Nunito_400Regular', color: '#111827' },
sendBtn: { width: 38, height: 38, borderRadius: 19, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center' },
});
function makeStyles(colors: ReturnType<typeof useColors>) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bg },
topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 },
topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: colors.bg },
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 },
avatarCenter: { flex: 1, alignItems: 'center', gap: 4 },
avatarMeta: { alignItems: 'center', gap: 2 },
avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text },
speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center' },
listContent: { paddingHorizontal: 12, paddingBottom: 4 },
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 },
chip: {
borderRadius: 14,
borderWidth: 1.5,
borderColor: colors.border,
backgroundColor: colors.bg,
paddingHorizontal: 16,
paddingVertical: 11,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.12,
shadowRadius: 6,
elevation: 3,
},
chipPressed: {
backgroundColor: colors.surfaceElevated,
borderColor: colors.textMuted,
transform: [{ scale: 0.97 }],
shadowOpacity: 0.05,
},
chipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 14, color: colors.textMuted },
inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.bg, gap: 8 },
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 { resolveAvatar } from '../lib/resolveAvatar';
import { useAuthStore } from '../stores/auth';
import { colors } from '../lib/theme';
import { useColors } from '../lib/theme';
type Props = {
onPosted?: () => void;
@ -25,6 +25,7 @@ type Props = {
export function ComposeCard({ onPosted }: Props) {
const { t } = useTranslation();
const colors = useColors();
const { user } = useAuthStore();
const queryClient = useQueryClient();
const inputRef = useRef<TextInput>(null);
@ -101,7 +102,7 @@ export function ComposeCard({ onPosted }: Props) {
const showActions = focused || content.length > 0;
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">
<Image
source={{ uri: avatarUrl }}
@ -114,10 +115,10 @@ export function ComposeCard({ onPosted }: Props) {
onChangeText={setContent}
onFocus={() => setFocused(true)}
placeholder={t('community.compose_placeholder')}
placeholderTextColor="#a3a3a3"
placeholderTextColor={colors.textMuted}
multiline
className="text-sm text-neutral-900 leading-5 min-h-[40px]"
style={{ textAlignVertical: 'top', fontFamily: 'Nunito_400Regular' }}
className="text-sm leading-5 min-h-[40px]"
style={{ textAlignVertical: 'top', fontFamily: 'Nunito_400Regular', color: colors.text }}
/>
{imageUri && (
<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 { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { colors } from '../lib/theme';
import { useColors } from '../lib/theme';
import { apiFetch } from '../lib/api';
import { useDeviceLimitStore, type DeviceLimitDevice } from '../stores/deviceLimit';
@ -40,6 +40,7 @@ function DeviceLimitRow({
onRemove: (id: string) => void;
}) {
const { t } = useTranslation();
const colors = useColors();
return (
<View
@ -119,6 +120,7 @@ function DeviceLimitRow({
export function DeviceLimitReachedSheet() {
const { t } = useTranslation();
const colors = useColors();
const sheetRef = useRef<TrueSheet>(null);
const { visible, devices, max, plan, hide, removeDevice } = useDeviceLimitStore();
const [removingId, setRemovingId] = useState<string | null>(null);
@ -195,7 +197,7 @@ export function DeviceLimitReachedSheet() {
<View
style={{
backgroundColor: '#ffffff',
backgroundColor: colors.bg,
borderRadius: 14,
overflow: 'hidden',
borderWidth: 1,

View File

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

View File

@ -22,7 +22,7 @@ import {
Easing,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { colors } from '../lib/theme';
import { useColors } from '../lib/theme';
type Option<T> = {
value: T;
@ -51,6 +51,7 @@ export function OptionsBottomSheet<T extends string | number>({
onClose,
}: Props<T>) {
const insets = useSafeAreaInsets();
const colors = useColors();
const translateY = useRef(new Animated.Value(400)).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 { RiveAvatar } from './RiveAvatar';
import { HeroShieldCheck } from './HeroShieldCheck';
import { useColors } from '../lib/theme';
type Props = {
post: CommunityPost;
@ -17,6 +18,7 @@ type Props = {
function PostCardImpl({ post, onCommentPress }: Props) {
const { t } = useTranslation();
const colors = useColors();
const queryClient = useQueryClient();
// Granular selectors — subscribing to the whole store would re-render every
// 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]);
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 */}
{post.repostOf && (
<View className="flex-row items-center gap-1.5 mb-3">
@ -194,35 +196,35 @@ function PostCardImpl({ post, onCommentPress }: Props) {
</View>
)}
<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}
</Text>
{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>
<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)}
</Text>
</View>
{/* Content — hidden for domain_vote (replaced by poll below) */}
{!!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}
</Text>
)}
{/* domain_approved: favicon + domain name + shield badge */}
{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} />
<View className="flex-1 min-w-0">
<Text className="text-xs font-semibold text-neutral-900 truncate" style={{ fontFamily: 'Nunito_700Bold' }}>
<View style={{ flex: 1, minWidth: 0 }}>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: 'Nunito_700Bold' }} numberOfLines={1}>
{approvedDomain}
</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')}
</Text>
</View>

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { View, Text } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { colors } from '../lib/theme';
import { useColors } from '../lib/theme';
type Props = {
days: number;
@ -16,14 +16,18 @@ const sizeMap = {
export function StreakBadge({ days, size = 'md' }: Props) {
const { t } = useTranslation();
const colors = useColors();
const s = sizeMap[size];
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">
<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>
<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')}
</Text>
</View>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import * as Clipboard from 'expo-clipboard';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { resolveAvatar } from '../../lib/resolveAvatar';
import { useColors } from '../../lib/theme';
export type ChatMsg = {
id: string;
@ -63,6 +64,8 @@ export function ChatBubble({
onOpenImage,
}: Props) {
const { t } = useTranslation();
const colors = useColors();
const styles = makeStyles(colors);
const [actionsOpen, setActionsOpen] = useState(false);
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -323,120 +326,122 @@ export function ChatBubble({
);
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
paddingHorizontal: 8,
},
avatarSlot: {
width: 30,
marginRight: 4,
justifyContent: 'flex-end',
},
avatar: {
width: 26,
height: 26,
borderRadius: 13,
backgroundColor: '#e5e5e5',
},
bubbleCol: {
maxWidth: '78%',
},
nickname: {
fontSize: 10,
fontFamily: 'Nunito_700Bold',
color: '#007AFF',
marginBottom: 2,
marginLeft: 10,
},
bubble: {
borderRadius: 18,
paddingHorizontal: 12,
paddingVertical: 6,
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 1,
shadowOffset: { width: 0, height: 1 },
},
bubbleOwn: {
backgroundColor: '#007AFF',
},
bubbleOther: {
backgroundColor: '#ffffff',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#e5e5e5',
},
replyPreview: {
borderLeftWidth: 3,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 4,
marginBottom: 4,
},
imageWrap: {
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
},
image: {
width: 220,
height: 220,
backgroundColor: '#f5f5f5',
},
imageTimeOverlay: {
position: 'absolute',
bottom: 6,
right: 6,
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
flexDirection: 'row',
alignItems: 'center',
},
content: {
fontSize: 14,
lineHeight: 20,
fontFamily: 'Nunito_400Regular',
},
footer: {
position: 'absolute',
bottom: 4,
right: 8,
flexDirection: 'row',
alignItems: 'center',
},
sheetBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
sheet: {
backgroundColor: '#fff',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 8,
paddingBottom: Platform.OS === 'ios' ? 32 : 16,
},
sheetGrabber: {
width: 36,
height: 4,
borderRadius: 2,
backgroundColor: '#d4d4d4',
alignSelf: 'center',
marginBottom: 10,
},
sheetItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 14,
borderRadius: 12,
},
sheetText: {
fontSize: 15,
fontFamily: 'Nunito_600SemiBold',
color: '#171717',
marginLeft: 12,
},
});
function makeStyles(colors: ReturnType<typeof useColors>) {
return StyleSheet.create({
row: {
flexDirection: 'row',
paddingHorizontal: 8,
},
avatarSlot: {
width: 30,
marginRight: 4,
justifyContent: 'flex-end',
},
avatar: {
width: 26,
height: 26,
borderRadius: 13,
backgroundColor: colors.surfaceElevated,
},
bubbleCol: {
maxWidth: '78%',
},
nickname: {
fontSize: 10,
fontFamily: 'Nunito_700Bold',
color: '#007AFF',
marginBottom: 2,
marginLeft: 10,
},
bubble: {
borderRadius: 18,
paddingHorizontal: 12,
paddingVertical: 6,
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 1,
shadowOffset: { width: 0, height: 1 },
},
bubbleOwn: {
backgroundColor: '#007AFF',
},
bubbleOther: {
backgroundColor: colors.surface,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.border,
},
replyPreview: {
borderLeftWidth: 3,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 4,
marginBottom: 4,
},
imageWrap: {
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
},
image: {
width: 220,
height: 220,
backgroundColor: colors.surfaceElevated,
},
imageTimeOverlay: {
position: 'absolute',
bottom: 6,
right: 6,
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
flexDirection: 'row',
alignItems: 'center',
},
content: {
fontSize: 14,
lineHeight: 20,
fontFamily: 'Nunito_400Regular',
},
footer: {
position: 'absolute',
bottom: 4,
right: 8,
flexDirection: 'row',
alignItems: 'center',
},
sheetBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
sheet: {
backgroundColor: colors.bg,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 8,
paddingBottom: Platform.OS === 'ios' ? 32 : 16,
},
sheetGrabber: {
width: 36,
height: 4,
borderRadius: 2,
backgroundColor: colors.border,
alignSelf: 'center',
marginBottom: 10,
},
sheetItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 14,
borderRadius: 12,
},
sheetText: {
fontSize: 15,
fontFamily: 'Nunito_600SemiBold',
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 { useTranslation } from 'react-i18next';
import { supabase } from '../../lib/supabase';
import { useColors } from '../../lib/theme';
type ReplyTo = { id: string; nickname: string; content: string };
@ -45,6 +46,7 @@ export function ChatInput({
onCancelReply,
}: Props) {
const { t } = useTranslation();
const colors = useColors();
const [text, setText] = useState('');
const [attachment, setAttachment] = useState<{
uri: string;
@ -137,6 +139,8 @@ export function ChatInput({
setAttachment(null);
}
const styles = makeStyles(colors);
return (
<View style={styles.container}>
{/* Reply preview */}
@ -231,103 +235,105 @@ function decodeBase64(base64: string): Uint8Array {
return bytes;
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#ffffff',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#e5e5e5',
},
replyBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: '#eff6ff',
borderLeftWidth: 3,
borderLeftColor: '#007AFF',
marginHorizontal: 8,
marginTop: 6,
borderRadius: 8,
},
replyName: {
fontSize: 11,
fontFamily: 'Nunito_700Bold',
color: '#007AFF',
},
replyContent: {
fontSize: 11,
fontFamily: 'Nunito_400Regular',
color: '#525252',
marginTop: 1,
},
attachBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: '#fafafa',
marginHorizontal: 8,
marginTop: 6,
borderRadius: 8,
},
attachImg: {
width: 36,
height: 36,
borderRadius: 6,
marginRight: 8,
},
attachFileIcon: {
width: 36,
height: 36,
borderRadius: 6,
backgroundColor: '#e5e5e5',
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
attachName: {
flex: 1,
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: '#171717',
},
row: {
flexDirection: 'row',
alignItems: 'flex-end',
paddingHorizontal: 8,
paddingTop: 8,
paddingBottom: 8,
},
iconBtn: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginRight: 4,
},
inputWrap: {
flex: 1,
backgroundColor: '#f5f5f5',
borderRadius: 22,
paddingHorizontal: 14,
minHeight: 36,
maxHeight: 120,
justifyContent: 'center',
},
input: {
fontSize: 14,
lineHeight: 19,
fontFamily: 'Nunito_400Regular',
color: '#171717',
paddingVertical: Platform.OS === 'ios' ? 8 : 4,
},
sendBtn: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 6,
},
});
function makeStyles(colors: ReturnType<typeof useColors>) {
return StyleSheet.create({
container: {
backgroundColor: colors.bg,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
},
replyBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: colors.surface,
borderLeftWidth: 3,
borderLeftColor: '#007AFF',
marginHorizontal: 8,
marginTop: 6,
borderRadius: 8,
},
replyName: {
fontSize: 11,
fontFamily: 'Nunito_700Bold',
color: '#007AFF',
},
replyContent: {
fontSize: 11,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
marginTop: 1,
},
attachBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: colors.surface,
marginHorizontal: 8,
marginTop: 6,
borderRadius: 8,
},
attachImg: {
width: 36,
height: 36,
borderRadius: 6,
marginRight: 8,
},
attachFileIcon: {
width: 36,
height: 36,
borderRadius: 6,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
attachName: {
flex: 1,
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: colors.text,
},
row: {
flexDirection: 'row',
alignItems: 'flex-end',
paddingHorizontal: 8,
paddingTop: 8,
paddingBottom: 8,
},
iconBtn: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginRight: 4,
},
inputWrap: {
flex: 1,
backgroundColor: colors.surfaceElevated,
borderRadius: 22,
paddingHorizontal: 14,
minHeight: 36,
maxHeight: 120,
justifyContent: 'center',
},
input: {
fontSize: 14,
lineHeight: 19,
fontFamily: 'Nunito_400Regular',
color: colors.text,
paddingVertical: Platform.OS === 'ios' ? 8 : 4,
},
sendBtn: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 6,
},
});
}

View File

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

View File

@ -1,6 +1,7 @@
import { View, Text, Pressable, Image, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
export type Room = {
id: string;
@ -29,6 +30,8 @@ function formatTime(ts: string, justNow: string) {
export function RoomCard({ room, onPress }: Props) {
const { t } = useTranslation();
const colors = useColors();
const styles = makeStyles(colors);
const initials = room.name
.split(' ')
.slice(0, 2)
@ -42,7 +45,7 @@ export function RoomCard({ room, onPress }: Props) {
<View
style={[
styles.avatar,
{ backgroundColor: room.isPublic ? '#eff6ff' : '#e5e5e5' },
{ backgroundColor: room.isPublic ? colors.surface : colors.surfaceElevated },
]}
>
{room.avatarUrl ? (
@ -102,116 +105,118 @@ export function RoomCard({ room, onPress }: Props) {
);
}
const styles = StyleSheet.create({
row: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 11,
backgroundColor: '#fff',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#f5f5f5',
},
avatar: {
width: 42,
height: 42,
borderRadius: 21,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
marginRight: 10,
},
avatarImg: {
width: 42,
height: 42,
},
avatarInitials: {
fontSize: 13,
fontFamily: 'Nunito_700Bold',
color: '#525252',
},
info: {
flex: 1,
minWidth: 0,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
},
footerRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 3,
},
footerTextWrap: {
flex: 1,
minWidth: 0,
},
metaPill: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 8,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
backgroundColor: '#f5f5f5',
},
name: {
fontSize: 14,
fontFamily: 'Nunito_700Bold',
color: '#171717',
flexShrink: 1,
},
defaultBadge: {
marginLeft: 6,
paddingHorizontal: 6,
paddingVertical: 1,
backgroundColor: '#eff6ff',
borderRadius: 8,
},
defaultBadgeText: {
fontSize: 9,
fontFamily: 'Nunito_700Bold',
color: '#007AFF',
},
lastMessage: {
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: '#737373',
},
description: {
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: '#a3a3a3',
},
right: {
alignItems: 'flex-end',
marginLeft: 8,
},
memberCount: {
fontSize: 11,
fontFamily: 'Nunito_700Bold',
color: '#737373',
marginLeft: 3,
},
time: {
fontSize: 10,
fontFamily: 'Nunito_500Medium',
color: '#a3a3a3',
marginLeft: 'auto',
paddingLeft: 6,
},
joinBadge: {
marginLeft: 6,
paddingHorizontal: 8,
paddingVertical: 3,
backgroundColor: '#eff6ff',
borderRadius: 10,
},
joinBadgeText: {
fontSize: 10,
fontFamily: 'Nunito_700Bold',
color: '#007AFF',
},
});
function makeStyles(colors: ReturnType<typeof useColors>) {
return StyleSheet.create({
row: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 11,
backgroundColor: colors.bg,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
avatar: {
width: 42,
height: 42,
borderRadius: 21,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
marginRight: 10,
},
avatarImg: {
width: 42,
height: 42,
},
avatarInitials: {
fontSize: 13,
fontFamily: 'Nunito_700Bold',
color: colors.textMuted,
},
info: {
flex: 1,
minWidth: 0,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
},
footerRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 3,
},
footerTextWrap: {
flex: 1,
minWidth: 0,
},
metaPill: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 8,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
backgroundColor: colors.surfaceElevated,
},
name: {
fontSize: 14,
fontFamily: 'Nunito_700Bold',
color: colors.text,
flexShrink: 1,
},
defaultBadge: {
marginLeft: 6,
paddingHorizontal: 6,
paddingVertical: 1,
backgroundColor: colors.surface,
borderRadius: 8,
},
defaultBadgeText: {
fontSize: 9,
fontFamily: 'Nunito_700Bold',
color: '#007AFF',
},
lastMessage: {
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
},
description: {
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
},
right: {
alignItems: 'flex-end',
marginLeft: 8,
},
memberCount: {
fontSize: 11,
fontFamily: 'Nunito_700Bold',
color: colors.textMuted,
marginLeft: 3,
},
time: {
fontSize: 10,
fontFamily: 'Nunito_500Medium',
color: colors.textMuted,
marginLeft: 'auto',
paddingLeft: 6,
},
joinBadge: {
marginLeft: 6,
paddingHorizontal: 8,
paddingVertical: 3,
backgroundColor: colors.surface,
borderRadius: 10,
},
joinBadgeText: {
fontSize: 10,
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 { useTranslation } from 'react-i18next';
import { useMailConnect, detectProvider, type MailProvider } from '../../hooks/useMailConnect';
import { useColors } from '../../lib/theme';
const SCREEN_HEIGHT = Dimensions.get('window').height;
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65;
@ -97,6 +98,7 @@ const PROVIDERS: ProviderConfig[] = [
*/
export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
const { t } = useTranslation();
const colors = useColors();
const insets = useSafeAreaInsets();
const { connect, connecting, error: connectError } = useMailConnect();
@ -203,7 +205,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
right: 0,
bottom: 0,
height: SHEET_HEIGHT,
backgroundColor: '#fff',
backgroundColor: colors.bg,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
transform: [{ translateY }],
@ -215,7 +217,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
>
{/* Drag-Handle */}
<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>
{/* Header */}
@ -228,24 +230,24 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
paddingTop: 6,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
borderBottomColor: colors.border,
}}
>
{view === 'form' ? (
<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')}
</Text>
</Pressable>
) : (
<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')}
</Text>
</Pressable>
)}
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{view === 'form' && currentProvider
? t(currentProvider.labelKey)
: t('mail.connect_sheet_title')}
@ -293,6 +295,7 @@ function ProviderGrid({
onSelect: (p: ProviderConfig) => void;
t: (key: string) => string;
}) {
const colors = useColors();
return (
<ScrollView
style={{ flex: 1 }}
@ -303,7 +306,7 @@ function ProviderGrid({
style={{
fontSize: 13,
fontFamily: 'Nunito_400Regular',
color: '#737373',
color: colors.textMuted,
marginBottom: 4,
lineHeight: 18,
}}
@ -325,9 +328,9 @@ function ProviderGrid({
flexDirection: 'row',
alignItems: 'center',
gap: 10,
backgroundColor: '#f9f9f9',
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: '#e5e5e5',
borderColor: colors.border,
borderRadius: 14,
padding: 14,
}}>
@ -345,13 +348,13 @@ function ProviderGrid({
</View>
<View style={{ flex: 1 }}>
<Text
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text }}
numberOfLines={1}
>
{t(p.labelKey)}
</Text>
</View>
<Ionicons name="chevron-forward" size={14} color="#d4d4d4" />
<Ionicons name="chevron-forward" size={14} color={colors.border} />
</View>
</Pressable>
))}
@ -394,6 +397,7 @@ function FormView({
insets,
t,
}: FormViewProps) {
const colors = useColors();
const canConnect = email.trim().length > 0 && password.trim().length > 0 && !connecting;
return (
@ -461,7 +465,7 @@ function FormView({
style={{
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: '#525252',
color: colors.textMuted,
marginBottom: 6,
}}
>
@ -471,19 +475,19 @@ function FormView({
value={email}
onChangeText={onEmailChange}
placeholder={t('mail.form_email_placeholder')}
placeholderTextColor="#a3a3a3"
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
returnKeyType="next"
style={{
backgroundColor: '#f5f5f5',
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 15,
fontFamily: 'Nunito_400Regular',
color: '#0a0a0a',
color: colors.text,
}}
/>
</View>
@ -494,7 +498,7 @@ function FormView({
style={{
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: '#525252',
color: colors.textMuted,
marginBottom: 6,
}}
>
@ -505,21 +509,21 @@ function FormView({
value={password}
onChangeText={onPasswordChange}
placeholder={t('mail.form_password_placeholder')}
placeholderTextColor="#a3a3a3"
placeholderTextColor={colors.textMuted}
secureTextEntry={!passwordVisible}
autoCapitalize="none"
autoCorrect={false}
returnKeyType="done"
onSubmitEditing={onConnect}
style={{
backgroundColor: '#f5f5f5',
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 12,
paddingRight: 46,
fontSize: 15,
fontFamily: 'Nunito_400Regular',
color: '#0a0a0a',
color: colors.text,
}}
/>
<Pressable

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { View, Text, Pressable } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { colors } from '../../lib/theme';
import { useColors } from '../../lib/theme';
type Props = {
onDismiss?: () => void;
@ -8,6 +8,7 @@ type Props = {
};
export function DigaMissionBanner({ onDismiss, onContribute }: Props) {
const colors = useColors();
return (
<View
style={{

View File

@ -2,11 +2,12 @@
import { useEffect, useRef, useState } from 'react';
import { View, Text, Pressable, Animated, StyleSheet } from 'react-native';
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 };
export function BreathingCard({ onDone, onSpeak }: Props) {
const colors = useColors();
const [breathState, setBreathState] = useState<BreathState>('idle');
const [countdown, setCountdown] = useState(3);
const [round, setRound] = useState(1);
@ -86,7 +87,7 @@ export function BreathingCard({ onDone, onSpeak }: Props) {
<View style={{ alignItems: 'center', gap: 16 }}>
<Text style={st.breathTitle}>4-7-8 Atemübung</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>
</Pressable>
</View>
@ -114,6 +115,7 @@ export function BreathingCard({ onDone, onSpeak }: Props) {
// ── BreathingDrawer (bottom sheet, covers input, slides up) ───────────────────
export function BreathingDrawer({ onDone, onSpeak }: Props) {
const colors = useColors();
const slideAnim = useRef(new Animated.Value(500)).current;
useEffect(() => {
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 (
<>
<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} />
<BreathingCard onDone={onDone} onSpeak={onSpeak} />
</Animated.View>
@ -131,14 +133,14 @@ export function BreathingDrawer({ onDone, onSpeak }: Props) {
const st = StyleSheet.create({
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 },
breathCardInner: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 8, alignItems: 'center', gap: 16 },
breathCircleLg: { width: 190, height: 190, borderRadius: 95, alignItems: 'center', justifyContent: 'center', borderWidth: 5 },
breathCountLg: { fontFamily: 'Nunito_800ExtraBold', fontSize: 60, color: '#111827', lineHeight: 68 },
breathTitle: { fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' },
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 },
breathRound: { fontFamily: 'Nunito_600SemiBold', fontSize: 12, color: '#9ca3af' },
breathPhaseLabel: { fontFamily: 'Nunito_700Bold', fontSize: 13 },

View File

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

View File

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

View File

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

View File

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