diff --git a/App.tsx b/App.tsx index b85e0399..c9c46e40 100644 --- a/App.tsx +++ b/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; import { StatusBar } from 'expo-status-bar'; +import { useColorScheme } from 'react-native'; import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator, type NativeStackScreenProps } from '@react-navigation/native-stack'; @@ -39,7 +40,8 @@ import { useLocaleStore } from './src/stores/locale-store'; import { useNetworkStore } from './src/stores/network-store'; import { useUpdatesStore } from './src/stores/updates-store'; import { UpdateBanner } from './src/components/UpdateBanner'; -import { colors, spacing, typography } from './src/theme/tokens'; +import { spacing, typography, type ThemePalette } from './src/theme/tokens'; +import { useColors } from './src/theme/colors'; const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); @@ -55,32 +57,32 @@ function navigateToNotificationTap(payload: NotificationTapPayload): void { } function LoadingScreen({ message }: { message: string }) { + const c = useColors(); return ( - - - {message} + + + {message} ); } function MainTabsNavigator({ navigation }: NativeStackScreenProps) { + const c = useColors(); const mailboxes = useEmailStore((state) => state.mailboxes); const logout = useAuthStore((state) => state.logout); const inboxUnreadCount = mailboxes.find((mailbox) => mailbox.role === 'inbox')?.unreadEmails ?? 0; return ( - + , tabBarBadge: inboxUnreadCount > 0 ? (inboxUnreadCount > 99 ? '99+' : inboxUnreadCount) : undefined, tabBarBadgeStyle: { - backgroundColor: colors.error, - color: colors.primaryForeground, + backgroundColor: c.error, + color: c.primaryForeground, fontSize: 10, fontWeight: '700', minWidth: 16, @@ -163,6 +165,15 @@ export default function App() { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const client = useAuthStore((state) => state.client); const restoreSession = useAuthStore((state) => state.restoreSession); + + // Resolve the user's theme preference to a concrete light/dark style for the + // system status bar. The rest of the app's colors are still hard-coded dark + // until the StyleSheet migration to a theme-aware `useColors` hook lands. + const themePref = useSettingsStore((state) => state.theme); + const systemScheme = useColorScheme(); + const resolvedScheme: 'light' | 'dark' = + themePref === 'system' ? (systemScheme === 'light' ? 'light' : 'dark') : themePref; + const statusBarStyle: 'light' | 'dark' = resolvedScheme === 'light' ? 'dark' : 'light'; // Persisted active account is the signal that the user was already signed // in on the previous launch. When present we render the main UI with the // cached mail list instead of the "Restoring session" spinner; the real @@ -309,7 +320,7 @@ export default function App() { if (!hasRestoredSession && !hasPersistedAccount) { return ( <> - + ); @@ -318,7 +329,7 @@ export default function App() { if (hasRestoredSession && !isAuthenticated) { return ( <> - + ); @@ -326,7 +337,7 @@ export default function App() { return ( - + @@ -374,12 +385,10 @@ const styles = StyleSheet.create({ flex: 1, alignItems: 'center', justifyContent: 'center', - backgroundColor: colors.background, gap: spacing.md, }, loadingText: { ...typography.body, - color: colors.textSecondary, }, }); diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index f23e2464..c0432812 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { View, Text, StyleSheet, ViewStyle } from 'react-native'; -import { colors, spacing, radius, typography } from '../theme/tokens'; +import { radius, typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; interface BadgeProps { children: string; @@ -8,57 +9,39 @@ interface BadgeProps { style?: ViewStyle; } -/** - * Matches webmail badge patterns: - * - rounded-full px-2 py-0.5 text-xs font-medium - */ export default function Badge({ children, variant = 'default', style }: BadgeProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( - - + + {children} ); } -const styles = StyleSheet.create({ - base: { - borderRadius: radius.full, - paddingHorizontal: 8, // px-2 - paddingVertical: 2, // py-0.5 - alignSelf: 'flex-start', - }, - text: { - ...typography.caption, - fontWeight: '500', - }, -}); +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ + base: { + borderRadius: radius.full, + paddingHorizontal: 8, + paddingVertical: 2, + alignSelf: 'flex-start', + }, + text: { ...typography.caption, fontWeight: '500' }, -const variantStyles: Record = { - default: { - backgroundColor: colors.surface, - }, - primary: { - backgroundColor: colors.primaryBg, - }, - destructive: { - backgroundColor: colors.errorBg, - }, - outline: { - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: colors.border, - }, - muted: { - backgroundColor: colors.muted, - }, -}; + bg_default: { backgroundColor: c.surface }, + bg_primary: { backgroundColor: c.primaryBg }, + bg_destructive: { backgroundColor: c.errorBg }, + bg_outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: c.border }, + bg_muted: { backgroundColor: c.muted }, + + text_default: { color: c.text }, + text_primary: { color: c.primary }, + text_destructive: { color: c.error }, + text_outline: { color: c.text }, + text_muted: { color: c.mutedForeground }, + }); +} -const textVariantStyles: Record = { - default: { color: colors.text }, - primary: { color: colors.primary }, - destructive: { color: colors.error }, - outline: { color: colors.text }, - muted: { color: colors.mutedForeground }, -}; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index a3ceefd5..62ec7309 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Pressable, Text, StyleSheet, ViewStyle, TextStyle, ActivityIndicator } from 'react-native'; -import { colors, spacing, radius, typography, componentSizes } from '../theme/tokens'; +import { Pressable, Text, StyleSheet, ViewStyle, ActivityIndicator } from 'react-native'; +import { spacing, radius, typography, componentSizes, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; type ButtonVariant = 'default' | 'ghost' | 'outline' | 'destructive'; type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'; @@ -26,15 +27,17 @@ export default function Button({ icon, style, }: ButtonProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const isDisabled = disabled || loading; return ( [ styles.base, - sizeStyles[size], - variantStyles[variant], - pressed && !isDisabled && pressedStyles[variant], + styles[`size_${size}` as const], + styles[`bg_${variant}` as const], + pressed && !isDisabled && styles[`pressed_${variant}` as const], isDisabled && styles.disabled, style, ]} @@ -44,13 +47,13 @@ export default function Button({ {loading ? ( ) : ( <> {icon} {typeof children === 'string' ? ( - + {children} ) : ( @@ -62,105 +65,34 @@ export default function Button({ ); } -const styles = StyleSheet.create({ - base: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: radius.md, // rounded-md (web) - gap: spacing.sm, - }, - text: { - ...typography.bodyMedium, // text-sm font-medium (web) - }, - disabled: { - opacity: 0.5, - }, - textDisabled: { - opacity: 0.5, - }, -}); +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ + base: { + flexDirection: 'row', alignItems: 'center', justifyContent: 'center', + borderRadius: radius.md, gap: spacing.sm, + }, + text: { ...typography.bodyMedium }, + disabled: { opacity: 0.5 }, + textDisabled: { opacity: 0.5 }, -// Matches web: h-9 (sm), h-10 (md), h-11 (lg), h-10 w-10 (icon) -const sizeStyles: Record = { - sm: { - height: componentSizes.buttonSm, - paddingHorizontal: spacing.md, // px-3 - }, - md: { - height: componentSizes.buttonMd, - paddingHorizontal: spacing.lg, // px-4 - paddingVertical: spacing.sm, // py-2 - }, - lg: { - height: componentSizes.buttonLg, - paddingHorizontal: spacing.xxxl, // px-8 - }, - icon: { - height: componentSizes.buttonMd, - width: componentSizes.buttonMd, - paddingHorizontal: 0, - }, -}; + size_sm: { height: componentSizes.buttonSm, paddingHorizontal: spacing.md }, + size_md: { height: componentSizes.buttonMd, paddingHorizontal: spacing.lg, paddingVertical: spacing.sm }, + size_lg: { height: componentSizes.buttonLg, paddingHorizontal: spacing.xxxl }, + size_icon: { height: componentSizes.buttonMd, width: componentSizes.buttonMd, paddingHorizontal: 0 }, -// Matches web button variant classes exactly -const variantStyles: Record = { - default: { - backgroundColor: colors.primary, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 1, - elevation: 1, - }, - ghost: { - backgroundColor: 'transparent', - }, - outline: { - backgroundColor: colors.background, - borderWidth: 1, - borderColor: colors.border, - }, - destructive: { - backgroundColor: colors.error, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 1, - elevation: 1, - }, -}; + bg_default: { backgroundColor: c.primary, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 1, elevation: 1 }, + bg_ghost: { backgroundColor: 'transparent' }, + bg_outline: { backgroundColor: c.background, borderWidth: 1, borderColor: c.border }, + bg_destructive: { backgroundColor: c.error, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 1, elevation: 1 }, -const pressedStyles: Record = { - default: { - backgroundColor: colors.primaryDark, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 2, - elevation: 2, - }, - ghost: { - backgroundColor: colors.accent, - }, - outline: { - backgroundColor: colors.accent, - }, - destructive: { - opacity: 0.9, - }, -}; + pressed_default: { backgroundColor: c.primaryDark, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2 }, + pressed_ghost: { backgroundColor: c.accent }, + pressed_outline: { backgroundColor: c.accent }, + pressed_destructive: { opacity: 0.9 }, -const textVariantStyles: Record = { - default: { - color: colors.primaryForeground, - }, - ghost: { - color: colors.text, - }, - outline: { - color: colors.text, - }, - destructive: { - color: colors.errorForeground, - }, -}; + text_default: { color: c.primaryForeground }, + text_ghost: { color: c.text }, + text_outline: { color: c.text }, + text_destructive: { color: c.errorForeground }, + }); +} diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 978aeb04..1a0a71da 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { View, Text, StyleSheet, ViewStyle } from 'react-native'; -import { colors, spacing, radius, typography } from '../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; interface CardProps { children: React.ReactNode; @@ -13,6 +14,8 @@ interface CardProps { * - card-foreground for text */ export function Card({ children, style }: CardProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return {children}; } @@ -27,6 +30,8 @@ interface SectionHeaderProps { * - text-sm text-muted-foreground for description */ export function SectionHeader({ title, description }: SectionHeaderProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( {title} @@ -51,6 +56,8 @@ interface SettingItemRowProps { * - description: text-xs text-muted-foreground mt-1 */ export function SettingItemRow({ label, description, right, icon, iconBg, bottomBorder = true }: SettingItemRowProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( {icon && ( @@ -67,12 +74,13 @@ export function SettingItemRow({ label, description, right, icon, iconBg, bottom ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ card: { - backgroundColor: colors.card, + backgroundColor: c.card, borderRadius: radius.lg, // rounded-lg borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, overflow: 'hidden', }, sectionHeader: { @@ -81,11 +89,11 @@ const styles = StyleSheet.create({ }, sectionTitle: { ...typography.h3, // text-lg font-medium - color: colors.text, + color: c.text, }, sectionDescription: { ...typography.body, - color: colors.mutedForeground, // text-muted-foreground + color: c.mutedForeground, // text-muted-foreground marginTop: 4, }, settingRow: { @@ -97,7 +105,7 @@ const styles = StyleSheet.create({ }, settingRowBorder: { borderBottomWidth: 1, - borderBottomColor: colors.border, // border-b border-border + borderBottomColor: c.border, // border-b border-border }, settingIcon: { width: 32, @@ -112,14 +120,15 @@ const styles = StyleSheet.create({ }, settingLabel: { ...typography.bodyMedium, // text-sm font-medium text-foreground - color: colors.text, + color: c.text, }, settingDescription: { ...typography.caption, // text-xs text-muted-foreground - color: colors.mutedForeground, + color: c.mutedForeground, marginTop: 2, }, settingRight: { flexShrink: 0, }, -}); + }); +} diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 495b26c6..15763014 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { View, Text, Pressable, StyleSheet, Modal } from 'react-native'; import { AlertTriangle } from 'lucide-react-native'; import Button from './Button'; -import { colors, spacing, radius, typography } from '../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; interface DialogProps { visible: boolean; @@ -34,6 +35,8 @@ export default function Dialog({ onConfirm, onCancel, }: DialogProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( @@ -41,7 +44,7 @@ export default function Dialog({ {variant === 'destructive' && ( - + )} {title} @@ -61,7 +64,8 @@ export default function Dialog({ ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', @@ -72,9 +76,9 @@ const styles = StyleSheet.create({ dialog: { width: '100%', maxWidth: 400, - backgroundColor: colors.background, + backgroundColor: c.background, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, borderRadius: radius.lg, shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, @@ -89,18 +93,18 @@ const styles = StyleSheet.create({ width: 40, height: 40, borderRadius: radius.full, - backgroundColor: colors.errorBg, + backgroundColor: c.errorBg, alignItems: 'center', justifyContent: 'center', marginBottom: spacing.md, }, title: { ...typography.h3, // text-lg font-semibold - color: colors.text, + color: c.text, }, message: { ...typography.body, // text-sm - color: colors.mutedForeground, // text-muted-foreground + color: c.mutedForeground, // text-muted-foreground marginTop: spacing.sm, }, footer: { @@ -110,4 +114,5 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.xxl, // px-6 paddingBottom: spacing.xxl, // pb-6 }, -}); + }); +} diff --git a/src/components/EmailBodyView.tsx b/src/components/EmailBodyView.tsx index fb1db733..813a50c0 100644 --- a/src/components/EmailBodyView.tsx +++ b/src/components/EmailBodyView.tsx @@ -6,7 +6,8 @@ import type { Email } from '../api/types'; import { wrapEmailHtml, wrapPlainTextEmail, plainTextToSafeHtml, extractCidRefs, hasRemoteContent, hasMeaningfulHtmlBody } from '../lib/email-html'; import { jmapClient } from '../api/jmap-client'; import { useSettingsStore } from '../stores/settings-store'; -import { colors, spacing, typography } from '../theme/tokens'; +import { spacing, typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; // Largest inline image we'll pull inline as a data: URI. Anything bigger gets // skipped so we don't freeze the JS thread base64-encoding megabytes. @@ -129,6 +130,8 @@ const HEIGHT_REPORTER = ` `; export default function EmailBodyView({ email, senderEmail }: EmailBodyViewProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const externalContentPolicy = useSettingsStore((s) => s.externalContentPolicy); const isSenderTrusted = useSettingsStore((s) => s.isSenderTrusted); const addTrustedSender = useSettingsStore((s) => s.addTrustedSender); @@ -247,13 +250,13 @@ export default function EmailBodyView({ email, senderEmail }: EmailBodyViewProps {externalContentPolicy === 'ask' && ( - + Load external content )} {senderEmail ? ( - + Trust sender ) : null} @@ -308,7 +311,8 @@ export default function EmailBodyView({ email, senderEmail }: EmailBodyViewProps ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ wrapper: { width: '100%' }, banner: { flexDirection: 'row', @@ -317,9 +321,9 @@ const styles = StyleSheet.create({ gap: spacing.md, paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, - backgroundColor: colors.surfaceHover, + backgroundColor: c.surfaceHover, borderBottomWidth: 1, - borderBottomColor: colors.border, + borderBottomColor: c.border, }, bannerButton: { flexDirection: 'row', @@ -329,7 +333,7 @@ const styles = StyleSheet.create({ }, bannerButtonText: { ...typography.caption, - color: colors.textSecondary, + color: c.textSecondary, }, webContainer: { width: '100%', @@ -343,4 +347,5 @@ const styles = StyleSheet.create({ webviewContainer: { backgroundColor: 'transparent', }, -}); + }); +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 089a40d8..b83de1a2 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { TextInput as RNTextInput, View, Text, StyleSheet, ViewStyle, TextInputProps } from 'react-native'; -import { colors, spacing, radius, typography, componentSizes } from '../theme/tokens'; +import { spacing, radius, typography, componentSizes, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; interface InputProps extends TextInputProps { label?: string; @@ -28,6 +29,8 @@ export default function Input({ style, ...props }: InputProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const [focused, setFocused] = React.useState(false); return ( @@ -43,7 +46,7 @@ export default function Input({ { setFocused(true); props.onFocus?.(e); }} onBlur={(e) => { setFocused(false); props.onBlur?.(e); }} /> @@ -54,10 +57,11 @@ export default function Input({ ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ label: { ...typography.bodyMedium, - color: colors.textSecondary, + color: c.textSecondary, marginBottom: spacing.sm, }, inputWrapper: { @@ -65,17 +69,17 @@ const styles = StyleSheet.create({ alignItems: 'center', height: componentSizes.inputHeight, // h-10 = 40px borderWidth: 1, - borderColor: colors.border, // border-input + borderColor: c.border, // border-input borderRadius: radius.sm, // rounded-md = 6px - backgroundColor: colors.background, // bg-background + backgroundColor: c.background, // bg-background paddingHorizontal: spacing.md, // px-3 = 12px }, inputFocused: { - borderColor: colors.borderFocus, // border-ring (blue-500) + borderColor: c.borderFocus, // border-ring (blue-500) borderWidth: 2, }, inputError: { - borderColor: colors.error, + borderColor: c.error, }, inputDisabled: { opacity: 0.5, @@ -83,7 +87,7 @@ const styles = StyleSheet.create({ input: { flex: 1, ...typography.body, // text-sm = 14px - color: colors.text, // text-foreground + color: c.text, // text-foreground paddingVertical: 0, }, iconLeft: { @@ -94,7 +98,8 @@ const styles = StyleSheet.create({ }, errorText: { ...typography.caption, - color: colors.error, + color: c.error, marginTop: spacing.xs, }, -}); + }); +} diff --git a/src/components/OfflineBanner.tsx b/src/components/OfflineBanner.tsx index e3559aa9..b27c9e85 100644 --- a/src/components/OfflineBanner.tsx +++ b/src/components/OfflineBanner.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { CloudOff } from 'lucide-react-native'; import { useNetworkStore } from '../stores/network-store'; -import { colors, spacing, typography } from '../theme/tokens'; +import { spacing, typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; interface OfflineBannerProps { /** Optional extra notice (e.g. "Showing cached mail"). */ @@ -10,11 +11,13 @@ interface OfflineBannerProps { } export function OfflineBanner({ hint }: OfflineBannerProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const online = useNetworkStore((s) => s.online); if (online) return null; return ( - + You are offline{hint ? ` — ${hint}` : ''} @@ -22,18 +25,20 @@ export function OfflineBanner({ hint }: OfflineBannerProps) { ); } -const styles = StyleSheet.create({ - bar: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - paddingHorizontal: spacing.md, - paddingVertical: 6, - backgroundColor: '#a16207', - }, - text: { - ...typography.caption, - color: colors.primaryForeground, - flex: 1, - }, -}); +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ + bar: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingHorizontal: spacing.md, + paddingVertical: 6, + backgroundColor: '#a16207', + }, + text: { + ...typography.caption, + color: c.primaryForeground, + flex: 1, + }, + }); +} diff --git a/src/components/RadioGroup.tsx b/src/components/RadioGroup.tsx index 91043f1e..4183d619 100644 --- a/src/components/RadioGroup.tsx +++ b/src/components/RadioGroup.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { View, Text, Pressable, StyleSheet, ViewStyle } from 'react-native'; -import { colors, spacing, radius, typography } from '../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; interface RadioOption { label: string; @@ -22,6 +23,8 @@ interface RadioGroupProps { * - unselected: bg-muted text-foreground */ export default function RadioGroup({ options, value, onChange, style }: RadioGroupProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( {options.map((opt) => { @@ -42,7 +45,8 @@ export default function RadioGroup({ options, value, onChange, style }: RadioGro ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ container: { flexDirection: 'row', gap: 6, // gap-1.5 @@ -53,19 +57,20 @@ const styles = StyleSheet.create({ borderRadius: radius.sm, // rounded-md }, optionSelected: { - backgroundColor: colors.primary, + backgroundColor: c.primary, }, optionUnselected: { - backgroundColor: colors.muted, + backgroundColor: c.muted, }, optionText: { ...typography.caption, // text-xs }, textSelected: { - color: colors.primaryForeground, + color: c.primaryForeground, fontWeight: '500', }, textUnselected: { - color: colors.text, + color: c.text, }, -}); + }); +} diff --git a/src/components/SidebarDrawer.tsx b/src/components/SidebarDrawer.tsx index cdf88bea..e8c99f8d 100644 --- a/src/components/SidebarDrawer.tsx +++ b/src/components/SidebarDrawer.tsx @@ -10,7 +10,8 @@ import { Folder, FolderOpen, ChevronDown, ChevronRight, X, Settings, LogOut, Check, Plus, } from 'lucide-react-native'; import type { LucideIcon } from 'lucide-react-native'; -import { colors, spacing, radius, typography } from '../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; import { useEmailStore } from '../stores/email-store'; import { useAuthStore } from '../stores/auth-store'; import { useAccountStore } from '../stores/account-store'; @@ -52,17 +53,19 @@ const ROLE_COLOR: Record = { inbox: '#60a5fa', sent: '#4ade80', drafts: '#a78bfa', - trash: colors.textMuted, + trash: c.textMuted, junk: '#f87171', archive: '#fbbf24', }; function iconColor(role: string | null | undefined, isSelected: boolean): string { if (role && ROLE_COLOR[role]) return ROLE_COLOR[role]; - return isSelected ? colors.text : colors.textSecondary; + return isSelected ? c.text : c.textSecondary; } function RowCounts({ unread, total }: { unread: number; total: number }) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); if (unread === 0 && total === 0) return null; return ( @@ -90,6 +93,8 @@ function SidebarRow({ icon, label, depth, isSelected, unread, total, hasChildren, isExpanded, onPress, onToggleExpand, }: SidebarRowProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const leftPad = ROW_PX_BASE + depth * INDENT_STEP; return ( {isExpanded ? ( - + ) : ( - + )} ) : ( @@ -135,6 +140,8 @@ interface SidebarDrawerProps { } export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const mailboxes = useEmailStore((s) => s.mailboxes); const currentMailboxId = useEmailStore((s) => s.currentMailboxId); const selectMailbox = useEmailStore((s) => s.selectMailbox); @@ -237,7 +244,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) [accountEmail], ); const avatarBg = React.useMemo( - () => (accountEmail ? generateAvatarColor(accountEmail) : colors.primary), + () => (accountEmail ? generateAvatarColor(accountEmail) : c.primary), [accountEmail], ); const hostname = React.useMemo(() => { @@ -267,7 +274,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) {/* Header: close + account switcher */} - + setAccountMenuOpen((v) => !v)} @@ -284,7 +291,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) @@ -323,7 +330,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) {isActive && ( - + )} @@ -364,7 +371,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) navigation.navigate('AddAccount'); }} > - + Add account @@ -382,7 +389,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) ]} onPress={() => setDefaultAccount(active.id)} > - + Set as default ); @@ -394,7 +401,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) ]} onPress={() => { setAccountMenuOpen(false); onClose(); void logout(); }} > - + Sign out of {accountEmailFull || 'account'} @@ -407,8 +414,8 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) ]} onPress={() => { setAccountMenuOpen(false); onClose(); void logoutAll(); }} > - - + + Sign out all @@ -420,14 +427,14 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) {/* Folders section */} {foldersExpanded ? ( - + ) : ( - + )} Folders - + @@ -465,7 +472,8 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps) ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)', @@ -478,7 +486,7 @@ const styles = StyleSheet.create({ maxWidth: 340, backgroundColor: '#262626', // matches webmail bg-secondary (dark mode) borderRightWidth: 1, - borderRightColor: colors.border, + borderRightColor: c.border, }, drawerSafe: { flex: 1 }, @@ -490,7 +498,7 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.sm, paddingVertical: spacing.sm, borderBottomWidth: 1, - borderBottomColor: colors.border, + borderBottomColor: c.border, }, headerClose: { width: 36, height: 36, @@ -506,7 +514,7 @@ const styles = StyleSheet.create({ paddingVertical: 6, borderRadius: radius.sm, }, - accountPressed: { backgroundColor: colors.surfaceHover }, + accountPressed: { backgroundColor: c.surfaceHover }, accountChevronOpen: { transform: [{ rotate: '180deg' }] }, accountAvatar: { width: 36, height: 36, @@ -515,11 +523,11 @@ const styles = StyleSheet.create({ }, accountAvatarText: { ...typography.bodySemibold, - color: colors.primaryForeground, + color: c.primaryForeground, }, accountInfo: { flex: 1, minWidth: 0 }, - accountName: { ...typography.bodyMedium, color: colors.text }, - accountEmail: { ...typography.caption, color: colors.textMuted, marginTop: 1 }, + accountName: { ...typography.bodyMedium, color: c.text }, + accountEmail: { ...typography.caption, color: c.textMuted, marginTop: 1 }, // Account menu - floating popover card under the header accountMenu: { @@ -529,7 +537,7 @@ const styles = StyleSheet.create({ backgroundColor: '#1f1f1f', borderRadius: radius.md, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, overflow: 'hidden', // Elevation / shadow shadowColor: '#000', @@ -566,14 +574,14 @@ const styles = StyleSheet.create({ accountMenuAvatarText: { ...typography.caption, fontWeight: '600', - color: colors.primaryForeground, + color: c.primaryForeground, }, accountMenuCheckBadge: { position: 'absolute', right: -3, bottom: -3, width: 16, height: 16, borderRadius: 8, - backgroundColor: colors.primary, + backgroundColor: c.primary, alignItems: 'center', justifyContent: 'center', borderWidth: 2, borderColor: '#1f1f1f', @@ -586,13 +594,13 @@ const styles = StyleSheet.create({ }, accountMenuName: { ...typography.bodyMedium, - color: colors.text, + color: c.text, flexShrink: 1, fontWeight: '600', }, accountMenuHost: { ...typography.caption, - color: colors.textMuted, + color: c.textMuted, marginTop: 2, }, accountMenuStatusRow: { @@ -607,17 +615,17 @@ const styles = StyleSheet.create({ backgroundColor: '#22c55e', }, accountMenuStatusDotOffline: { - backgroundColor: colors.textMuted, + backgroundColor: c.textMuted, }, accountMenuStatusText: { fontSize: 10, lineHeight: 12, - color: colors.textMuted, + color: c.textMuted, flexShrink: 1, }, accountMenuDivider: { height: StyleSheet.hairlineWidth, - backgroundColor: colors.border, + backgroundColor: c.border, }, accountMenuAction: { flexDirection: 'row', @@ -627,10 +635,10 @@ const styles = StyleSheet.create({ paddingVertical: 10, minHeight: 40, }, - accountMenuActionPressed: { backgroundColor: colors.surfaceHover }, + accountMenuActionPressed: { backgroundColor: c.surfaceHover }, accountMenuActionText: { ...typography.body, - color: colors.text, + color: c.text, flexShrink: 1, fontSize: 13, }, @@ -650,13 +658,13 @@ const styles = StyleSheet.create({ }, sectionHeaderText: { ...typography.bodySemibold, - color: colors.text, + color: c.text, }, sectionSettings: { padding: 4 }, empty: { ...typography.body, - color: colors.textMuted, + color: c.textMuted, paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, }, @@ -670,10 +678,10 @@ const styles = StyleSheet.create({ borderLeftWidth: 2, borderLeftColor: 'transparent', }, - rowPressed: { backgroundColor: colors.surfaceHover }, + rowPressed: { backgroundColor: c.surfaceHover }, rowSelected: { - backgroundColor: colors.accent, - borderLeftColor: colors.primary, + backgroundColor: c.accent, + borderLeftColor: c.primary, }, rowIndent: { flexDirection: 'row', @@ -692,11 +700,11 @@ const styles = StyleSheet.create({ rowLabel: { flex: 1, ...typography.body, - color: colors.text, + color: c.text, }, rowLabelSelected: { ...typography.bodySemibold, - color: colors.text, + color: c.text, }, counts: { flexDirection: 'row', @@ -707,16 +715,17 @@ const styles = StyleSheet.create({ countUnread: { ...typography.caption, fontWeight: '600', - color: colors.text, + color: c.text, }, countSep: { ...typography.caption, - color: colors.textMuted, + color: c.textMuted, opacity: 0.6, }, countTotal: { ...typography.caption, - color: colors.textMuted, + color: c.textMuted, }, -}); + }); +} diff --git a/src/components/SwipeableRow.tsx b/src/components/SwipeableRow.tsx index 02d7e7bd..e3c9fc47 100644 --- a/src/components/SwipeableRow.tsx +++ b/src/components/SwipeableRow.tsx @@ -6,7 +6,8 @@ import { Archive, Trash2, ShieldAlert, MailOpen, Mail, Star, type LucideIcon, } from 'lucide-react-native'; -import { colors, typography } from '../theme/tokens'; +import { typography, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; import type { SwipeAction } from '../stores/settings-store'; interface SwipeableRowProps { @@ -44,6 +45,8 @@ function actionLabel(action: SwipeAction, context: { unread: boolean; starred: b export function SwipeableRow({ children, leftAction, rightAction, context, onAction, }: SwipeableRowProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const dx = useRef(new Animated.Value(0)).current; const claimed = useRef(false); const widthRef = useRef(Dimensions.get('window').width); @@ -159,9 +162,10 @@ export function SwipeableRow({ ); } -const styles = StyleSheet.create({ - wrap: { position: 'relative', backgroundColor: colors.background }, - content: { backgroundColor: colors.background }, +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ + wrap: { position: 'relative', backgroundColor: c.background }, + content: { backgroundColor: c.background }, band: { position: 'absolute', top: 0, @@ -171,4 +175,5 @@ const styles = StyleSheet.create({ }, bandInner: { alignItems: 'center', gap: 4 }, bandLabel: { color: '#fff', fontWeight: '600', ...typography.caption }, -}); + }); +} diff --git a/src/components/ToggleSwitch.tsx b/src/components/ToggleSwitch.tsx index e28dcb5b..ce0944ee 100644 --- a/src/components/ToggleSwitch.tsx +++ b/src/components/ToggleSwitch.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Pressable, View, StyleSheet } from 'react-native'; -import { colors, radius, componentSizes } from '../theme/tokens'; +import { radius, componentSizes, type ThemePalette } from '../theme/tokens'; +import { useColors } from '../theme/colors'; interface ToggleSwitchProps { value: boolean; @@ -16,6 +17,8 @@ interface ToggleSwitchProps { * - translate-x-6 (checked) / translate-x-1 (unchecked) */ export default function ToggleSwitch({ value, onValueChange, disabled = false }: ToggleSwitchProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( makeStyles(c), [c]); const cachedLatest = useUpdatesStore((s) => s.cachedLatest); const dismissedTag = useUpdatesStore((s) => s.dismissedTag); const installing = useUpdatesStore((s) => s.installing); @@ -18,7 +21,7 @@ export function UpdateBanner(): React.ReactElement | null { return ( - + Update available @@ -33,29 +36,31 @@ export function UpdateBanner(): React.ReactElement | null { {installing ? '…' : 'Install'} - + ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ banner: { flexDirection: 'row', alignItems: 'center', gap: spacing.md, paddingHorizontal: spacing.lg, paddingVertical: spacing.md, - backgroundColor: colors.primary, + backgroundColor: c.primary, }, - title: { ...typography.bodyMedium, color: colors.primaryForeground }, - subtitle: { ...typography.caption, color: colors.primaryForeground, opacity: 0.85 }, + title: { ...typography.bodyMedium, color: c.primaryForeground }, + subtitle: { ...typography.caption, color: c.primaryForeground, opacity: 0.85 }, installButton: { paddingHorizontal: spacing.md, paddingVertical: 6, borderRadius: radius.sm, backgroundColor: 'rgba(255,255,255,0.2)', }, - installText: { ...typography.captionMedium, color: colors.primaryForeground }, + installText: { ...typography.captionMedium, color: c.primaryForeground }, dismiss: { padding: 4 }, -}); + }); +} diff --git a/src/components/calendar/AgendaView.tsx b/src/components/calendar/AgendaView.tsx index 79ff9071..2a1e4715 100644 --- a/src/components/calendar/AgendaView.tsx +++ b/src/components/calendar/AgendaView.tsx @@ -3,7 +3,8 @@ import { SectionList, StyleSheet, Text, View } from 'react-native'; import { format, isSameDay, isToday, isTomorrow } from 'date-fns'; import { CalendarDays } from 'lucide-react-native'; import type { Calendar, CalendarEvent } from '../../api/types'; -import { colors, spacing, typography } from '../../theme/tokens'; +import { spacing, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { buildEventDayIndex, eventsOnDayFromIndex, @@ -41,6 +42,8 @@ export function AgendaView({ eventsByDay, onSelectEvent, }: AgendaViewProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const index = React.useMemo( () => eventsByDay ?? buildEventDayIndex(events), [eventsByDay, events], @@ -78,7 +81,7 @@ export function AgendaView({ if (sections.length === 0) { return ( - + No upcoming events The next {daysAhead} days are clear. @@ -118,7 +121,8 @@ export function AgendaView({ ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ listContent: { paddingBottom: 80 }, sectionHeader: { flexDirection: 'row', @@ -126,16 +130,16 @@ const styles = StyleSheet.create({ gap: spacing.sm, paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, - backgroundColor: colors.surface, + backgroundColor: c.surface, borderBottomWidth: 1, - borderBottomColor: colors.border, + borderBottomColor: c.border, }, - sectionTitle: { ...typography.bodyMedium, color: colors.text }, - sectionTitleToday: { color: colors.primary }, - sectionSub: { ...typography.caption, color: colors.textMuted }, + sectionTitle: { ...typography.bodyMedium, color: c.text }, + sectionTitleToday: { color: c.primary }, + sectionSub: { ...typography.caption, color: c.textMuted }, itemWrap: { paddingHorizontal: spacing.lg, paddingTop: spacing.sm }, emptyDayWrap: { paddingHorizontal: spacing.lg, paddingVertical: spacing.md }, - emptyDayText: { ...typography.caption, color: colors.textMuted, textAlign: 'center' }, + emptyDayText: { ...typography.caption, color: c.textMuted, textAlign: 'center' }, empty: { flex: 1, alignItems: 'center', @@ -143,6 +147,7 @@ const styles = StyleSheet.create({ gap: spacing.sm, paddingVertical: spacing.xxxl, }, - emptyTitle: { ...typography.bodyMedium, color: colors.textSecondary }, - emptySubtitle: { ...typography.caption, color: colors.textMuted }, -}); + emptyTitle: { ...typography.bodyMedium, color: c.textSecondary }, + emptySubtitle: { ...typography.caption, color: c.textMuted }, + }); +} diff --git a/src/components/calendar/CalendarSidebarDrawer.tsx b/src/components/calendar/CalendarSidebarDrawer.tsx index 9a6768c0..0af9040f 100644 --- a/src/components/calendar/CalendarSidebarDrawer.tsx +++ b/src/components/calendar/CalendarSidebarDrawer.tsx @@ -13,7 +13,8 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { Check, X } from 'lucide-react-native'; import type { Calendar } from '../../api/types'; -import { colors, radius, spacing, typography } from '../../theme/tokens'; +import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { getCalendarColor } from '../../lib/calendar-utils'; interface CalendarSidebarDrawerProps { @@ -31,6 +32,8 @@ export function CalendarSidebarDrawer({ onToggle, onClose, }: CalendarSidebarDrawerProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const slideX = React.useRef(new Animated.Value(-Dimensions.get('window').width)).current; const overlayOpacity = React.useRef(new Animated.Value(0)).current; @@ -89,7 +92,7 @@ export function CalendarSidebarDrawer({ - + Calendars @@ -134,15 +137,17 @@ function Section({ hiddenSet: Set; onToggle: (id: string) => void; }) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( {title} - {calendars.map((c) => { - const visible = !hiddenSet.has(c.id); + {calendars.map((cal) => { + const visible = !hiddenSet.has(cal.id); return ( onToggle(c.id)} + key={cal.id} + onPress={() => onToggle(cal.id)} style={({ pressed }) => [ styles.row, pressed && styles.rowPressed, @@ -152,15 +157,15 @@ function Section({ style={[ styles.swatch, { - backgroundColor: visible ? getCalendarColor(c) : 'transparent', - borderColor: getCalendarColor(c), + backgroundColor: visible ? getCalendarColor(cal) : 'transparent', + borderColor: getCalendarColor(cal), }, ]} > - {visible && } + {visible && } - {c.name} + {cal.name} ); @@ -169,7 +174,8 @@ function Section({ ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)', @@ -184,7 +190,7 @@ const styles = StyleSheet.create({ maxWidth: 340, backgroundColor: '#262626', borderRightWidth: 1, - borderRightColor: colors.border, + borderRightColor: c.border, }, drawerSafe: { flex: 1 }, @@ -195,7 +201,7 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.sm, paddingVertical: spacing.sm, borderBottomWidth: 1, - borderBottomColor: colors.border, + borderBottomColor: c.border, }, headerClose: { width: 36, @@ -204,14 +210,14 @@ const styles = StyleSheet.create({ justifyContent: 'center', borderRadius: radius.sm, }, - headerTitle: { ...typography.h3, color: colors.text }, + headerTitle: { ...typography.h3, color: c.text }, scroll: { flex: 1 }, scrollContent: { paddingBottom: spacing.lg }, empty: { ...typography.body, - color: colors.textMuted, + color: c.textMuted, paddingHorizontal: spacing.lg, paddingVertical: spacing.lg, }, @@ -219,7 +225,7 @@ const styles = StyleSheet.create({ section: { paddingTop: spacing.md }, sectionTitle: { ...typography.bodySemibold, - color: colors.text, + color: c.text, paddingHorizontal: spacing.lg, paddingBottom: spacing.xs, }, @@ -231,7 +237,7 @@ const styles = StyleSheet.create({ paddingVertical: spacing.sm, minHeight: 44, }, - rowPressed: { backgroundColor: colors.surfaceHover }, + rowPressed: { backgroundColor: c.surfaceHover }, swatch: { width: 18, height: 18, @@ -240,6 +246,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - rowName: { flex: 1, ...typography.body, color: colors.text }, - rowNameMuted: { color: colors.textMuted }, -}); + rowName: { flex: 1, ...typography.body, color: c.text }, + rowNameMuted: { color: c.textMuted }, + }); +} diff --git a/src/components/calendar/EventCard.tsx b/src/components/calendar/EventCard.tsx index fd82d6bb..1aad5561 100644 --- a/src/components/calendar/EventCard.tsx +++ b/src/components/calendar/EventCard.tsx @@ -3,7 +3,8 @@ import { View, Text, StyleSheet, Pressable } from 'react-native'; import { Clock, MapPin, Users } from 'lucide-react-native'; import { format } from 'date-fns'; import type { Calendar, CalendarEvent } from '../../api/types'; -import { colors, radius, spacing, typography } from '../../theme/tokens'; +import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { eventTimeRange, getEventColor } from '../../lib/calendar-utils'; interface EventCardProps { @@ -24,6 +25,8 @@ function formatTimeRange(event: CalendarEvent): string { } export function EventCard({ event, calendars, onPress, onLongPress }: EventCardProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const color = getEventColor(event, calendars); const time = formatTimeRange(event); const count = participantCount(event); @@ -48,13 +51,13 @@ export function EventCard({ event, calendars, onPress, onLongPress }: EventCardP {!event.showWithoutTime && ( - + {time} )} {event.description ? ( - + {event.description} @@ -62,7 +65,7 @@ export function EventCard({ event, calendars, onPress, onLongPress }: EventCardP ) : null} {count > 0 && ( - + {count} participants )} @@ -71,17 +74,18 @@ export function EventCard({ event, calendars, onPress, onLongPress }: EventCardP ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ card: { flexDirection: 'row', - backgroundColor: colors.card, + backgroundColor: c.card, borderRadius: radius.lg, marginBottom: spacing.sm, overflow: 'hidden', borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, }, - cardPressed: { backgroundColor: colors.surfaceHover }, + cardPressed: { backgroundColor: c.surfaceHover }, colorBar: { width: 3 }, body: { flex: 1, padding: spacing.md, gap: 4 }, headerRow: { @@ -90,14 +94,15 @@ const styles = StyleSheet.create({ alignItems: 'center', gap: spacing.sm, }, - title: { ...typography.bodyMedium, color: colors.text, flex: 1 }, + title: { ...typography.bodyMedium, color: c.text, flex: 1 }, allDayBadge: { - backgroundColor: colors.primaryBg, + backgroundColor: c.primaryBg, paddingHorizontal: 8, paddingVertical: 2, borderRadius: radius.full, }, - allDayText: { ...typography.small, color: colors.primary }, + allDayText: { ...typography.small, color: c.primary }, detailRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, - detailText: { ...typography.caption, color: colors.textMuted, flexShrink: 1 }, -}); + detailText: { ...typography.caption, color: c.textMuted, flexShrink: 1 }, + }); +} diff --git a/src/components/calendar/EventDetailSheet.tsx b/src/components/calendar/EventDetailSheet.tsx index 9d01dc66..fea92d1e 100644 --- a/src/components/calendar/EventDetailSheet.tsx +++ b/src/components/calendar/EventDetailSheet.tsx @@ -26,12 +26,14 @@ import { } from 'lucide-react-native'; import { format } from 'date-fns'; import type { Calendar, CalendarEvent } from '../../api/types'; -import { colors, radius, spacing, typography } from '../../theme/tokens'; +import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { eventTimeRange, getEventColor, getPrimaryCalendarId, } from '../../lib/calendar-utils'; +import { useSheetDrag } from '../../lib/use-sheet-drag'; interface EventDetailSheetProps { event: CalendarEvent | null; @@ -106,9 +108,16 @@ export function EventDetailSheet({ onDelete, onDuplicate, }: EventDetailSheetProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current; const overlayOpacity = React.useRef(new Animated.Value(0)).current; const visible = !!event; + const dragHandlers = useSheetDrag({ + slideY, + closedY: Dimensions.get('window').height, + onClose, + }); React.useEffect(() => { if (visible) { @@ -166,17 +175,21 @@ export function EventDetailSheet({ - + + + + - - - - {event.title || 'Untitled'} - {calendar && {calendar.name}} + + + + {event.title || 'Untitled'} + {calendar && {calendar.name}} + + + + - - - - } text={range} /> + } text={range} /> {event.description ? ( } + icon={} text={event.description} multiline /> ) : null} {recurrence && ( } + icon={} text={recurrence} /> )} {alert && ( } + icon={} text={alert} /> )} {calendar && ( } + icon={} text={calendar.name} /> )} @@ -214,7 +227,7 @@ export function EventDetailSheet({ {participants.length > 0 && ( - + {participants.length} participants @@ -244,21 +257,21 @@ export function EventDetailSheet({ {onEdit && ( } + icon={} label="Edit" onPress={() => onEdit(event)} /> )} {onDuplicate && ( } + icon={} label="Duplicate" onPress={() => onDuplicate(event)} /> )} {onDelete && ( } + icon={} label="Delete" onPress={() => onDelete(event)} destructive @@ -280,6 +293,8 @@ function DetailRow({ text: string; multiline?: boolean; }) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( {icon} @@ -301,6 +316,8 @@ function ActionButton({ onPress: () => void; destructive?: boolean; }) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); return ( makeStyles(c), [c]); const isEdit = !!event; const [title, setTitle] = React.useState(''); @@ -264,7 +267,7 @@ export function EventModal({ - + {isEdit ? 'Edit event' : 'New event'} @@ -93,7 +96,7 @@ export function AccountSecuritySettings() { {twoFactor && ( - + Scan the QR code in your authenticator app to finish setup. @@ -104,7 +107,7 @@ export function AccountSecuritySettings() { {appPwds.length === 0 ? ( - + No app passwords yet. ) : null} @@ -119,7 +122,7 @@ export function AccountSecuritySettings() { {MOCK_SESSIONS.map((s) => ( - + {s.device} @@ -129,7 +132,7 @@ export function AccountSecuritySettings() { {!s.current && ( - + )} @@ -139,23 +142,24 @@ export function AccountSecuritySettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ container: { gap: spacing.xxxl }, pwField: { gap: 4 }, - pwLabel: { ...typography.caption, color: colors.mutedForeground }, + pwLabel: { ...typography.caption, color: c.mutedForeground }, pwInputRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, eyeBtn: { padding: spacing.sm }, infoBox: { flexDirection: 'row', gap: spacing.sm, padding: spacing.md, - backgroundColor: colors.primaryBg, + backgroundColor: c.primaryBg, borderRadius: radius.sm, alignItems: 'center', }, infoText: { ...typography.caption, - color: colors.primary, + color: c.primary, flex: 1, }, empty: { @@ -165,7 +169,7 @@ const styles = StyleSheet.create({ }, emptyText: { ...typography.body, - color: colors.mutedForeground, + color: c.mutedForeground, }, sessionRow: { flexDirection: 'row', @@ -173,18 +177,18 @@ const styles = StyleSheet.create({ gap: spacing.md, paddingVertical: spacing.md, borderBottomWidth: 1, - borderBottomColor: colors.border, + borderBottomColor: c.border, }, sessionIcon: { width: 36, height: 36, borderRadius: radius.sm, - backgroundColor: colors.muted, + backgroundColor: c.muted, alignItems: 'center', justifyContent: 'center', }, - sessionDevice: { ...typography.bodyMedium, color: colors.text }, - sessionMeta: { ...typography.caption, color: colors.mutedForeground, marginTop: 2 }, + sessionDevice: { ...typography.bodyMedium, color: c.text }, + sessionMeta: { ...typography.caption, color: c.mutedForeground, marginTop: 2 }, trashBtn: { width: 32, height: 32, @@ -193,3 +197,4 @@ const styles = StyleSheet.create({ borderRadius: radius.sm, }, }); +} diff --git a/src/components/settings/AccountSettings.tsx b/src/components/settings/AccountSettings.tsx index 89d7106f..c1c4f578 100644 --- a/src/components/settings/AccountSettings.tsx +++ b/src/components/settings/AccountSettings.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { SettingsSection, SettingItem } from './settings-section'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useAuthStore } from '../../stores/auth-store'; import { useAccountStore } from '../../stores/account-store'; @@ -29,6 +30,8 @@ function formatFileSize(bytes: number): string { } export function AccountSettings(props: AccountSettingsProps = {}) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const authUsername = useAuthStore((s) => s.username); const authServerUrl = useAuthStore((s) => s.serverUrl); const activeAccountId = useAuthStore((s) => s.activeAccountId); @@ -99,10 +102,11 @@ export function AccountSettings(props: AccountSettingsProps = {}) { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ value: { ...typography.body, - color: colors.text, + color: c.text, maxWidth: 240, }, storage: { @@ -112,13 +116,13 @@ const styles = StyleSheet.create({ bar: { width: 128, height: 8, - backgroundColor: colors.muted, + backgroundColor: c.muted, borderRadius: radius.full, overflow: 'hidden', }, barFill: { height: '100%', - backgroundColor: colors.primary, + backgroundColor: c.primary, borderRadius: radius.full, }, demoRow: { @@ -137,3 +141,4 @@ const styles = StyleSheet.create({ color: '#fbbf24', }, }); +} diff --git a/src/components/settings/AppearanceSettings.tsx b/src/components/settings/AppearanceSettings.tsx index c2546195..673cc3b3 100644 --- a/src/components/settings/AppearanceSettings.tsx +++ b/src/components/settings/AppearanceSettings.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useSettingsStore, type ThemeMode, type FontSize, type Density } from '../../stores/settings-store'; type Theme = ThemeMode; @@ -20,6 +21,8 @@ const PREVIEW_ROWS = [ ]; function DensityPreview({ density }: { density: Density }) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const cfg = DENSITY_PREVIEW[density]; return ( @@ -46,7 +49,7 @@ function DensityPreview({ density }: { density: Density }) { @@ -57,7 +60,7 @@ function DensityPreview({ density }: { density: Density }) { @@ -76,6 +79,8 @@ function DensityPreview({ density }: { density: Density }) { } export function AppearanceSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const theme = useSettingsStore((s) => s.theme); const setTheme = useSettingsStore((s) => s.setTheme); const fontSize = useSettingsStore((s) => s.fontSize); @@ -146,24 +151,25 @@ export function AppearanceSettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ densityPreview: { marginTop: spacing.md, borderRadius: radius.md, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, overflow: 'hidden', - backgroundColor: colors.background, + backgroundColor: c.background, }, densityRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md, - borderBottomColor: colors.border, + borderBottomColor: c.border, }, densityAvatar: { borderRadius: radius.full, - backgroundColor: colors.muted, + backgroundColor: c.muted, }, densityHeader: { flexDirection: 'row', @@ -177,7 +183,7 @@ const styles = StyleSheet.create({ }, densityTime: { fontSize: 10, - color: colors.mutedForeground, + color: c.mutedForeground, }, densitySubject: { ...typography.caption, @@ -188,7 +194,8 @@ const styles = StyleSheet.create({ }, divider: { height: 1, - backgroundColor: colors.border, + backgroundColor: c.border, marginTop: spacing.md, }, }); +} diff --git a/src/components/settings/ComposingSettings.tsx b/src/components/settings/ComposingSettings.tsx index 4ff43f7f..c4f8e87c 100644 --- a/src/components/settings/ComposingSettings.tsx +++ b/src/components/settings/ComposingSettings.tsx @@ -3,10 +3,13 @@ import { View, Text, StyleSheet, Pressable, TextInput } from 'react-native'; import { X, Plus } from 'lucide-react-native'; import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section'; import Button from '../Button'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useSettingsStore } from '../../stores/settings-store'; export function ComposingSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const autoSelectReplyIdentity = useSettingsStore((s) => s.autoSelectReplyIdentity); const setAutoSelectReplyIdentity = useSettingsStore((s) => s.setAutoSelectReplyIdentity); const attachmentReminderEnabled = useSettingsStore((s) => s.attachmentReminderEnabled); @@ -68,7 +71,7 @@ export function ComposingSettings() { {kw} removeKeyword(kw)} hitSlop={6}> - + ))} @@ -79,7 +82,7 @@ export function ComposingSettings() { value={newKeyword} onChangeText={setNewKeyword} placeholder="e.g. attached" - placeholderTextColor={colors.textMuted} + placeholderTextColor={c.textMuted} autoCapitalize="none" autoCorrect={false} style={styles.input} @@ -91,7 +94,7 @@ export function ComposingSettings() { size="sm" onPress={addKeyword} disabled={!newKeyword.trim()} - icon={} + icon={} > Add @@ -102,10 +105,11 @@ export function ComposingSettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ subBlock: { paddingVertical: spacing.md, gap: spacing.sm }, - subLabel: { ...typography.bodyMedium, color: colors.text }, - subDescription: { ...typography.caption, color: colors.mutedForeground }, + subLabel: { ...typography.bodyMedium, color: c.text }, + subDescription: { ...typography.caption, color: c.mutedForeground }, chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 }, chip: { flexDirection: 'row', @@ -114,19 +118,20 @@ const styles = StyleSheet.create({ paddingHorizontal: 8, paddingVertical: 3, borderRadius: radius.full, - backgroundColor: colors.muted, + backgroundColor: c.muted, }, - chipText: { ...typography.caption, color: colors.text }, + chipText: { ...typography.caption, color: c.text }, addRow: { flexDirection: 'row', gap: spacing.sm, marginTop: 4 }, input: { flex: 1, - backgroundColor: colors.background, + backgroundColor: c.background, borderRadius: radius.sm, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, paddingHorizontal: spacing.md, paddingVertical: 8, - color: colors.text, + color: c.text, ...typography.body, }, }); +} diff --git a/src/components/settings/ContactsSettings.tsx b/src/components/settings/ContactsSettings.tsx index e2d842ee..70f34272 100644 --- a/src/components/settings/ContactsSettings.tsx +++ b/src/components/settings/ContactsSettings.tsx @@ -9,9 +9,11 @@ import { useSettingsStore } from '../../stores/settings-store'; import { useContactsStore } from '../../stores/contacts-store'; import { contactsToVCard } from '../../lib/vcard'; import { isGroup } from '../../lib/contact-utils'; -import { colors, typography } from '../../theme/tokens'; +import { typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; export function ContactsSettings() { + const c = useColors(); const groupByLetter = useSettingsStore((s) => s.groupContactsByLetter); const setGroupByLetter = useSettingsStore((s) => s.setGroupContactsByLetter); const contacts = useContactsStore((s) => s.contacts); @@ -67,7 +69,7 @@ export function ContactsSettings() { @@ -133,7 +136,7 @@ export function ContentSendersSettings() { hitSlop={6} style={({ pressed }) => [styles.removeBtn, pressed && styles.removeBtnPressed]} > - + )} @@ -146,18 +149,19 @@ export function ContentSendersSettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ trustedButton: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: spacing.md, paddingVertical: 6, - backgroundColor: colors.muted, + backgroundColor: c.muted, borderRadius: radius.sm, }, trustedButtonPressed: { opacity: 0.7 }, - trustedButtonText: { ...typography.body, color: colors.text }, + trustedButtonText: { ...typography.body, color: c.text }, modalBackdrop: { flex: 1, @@ -166,10 +170,10 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.lg, }, modalCard: { - backgroundColor: colors.card, + backgroundColor: c.card, borderRadius: radius.lg, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, padding: spacing.lg, maxHeight: '80%', }, @@ -179,34 +183,35 @@ const styles = StyleSheet.create({ alignItems: 'center', marginBottom: spacing.md, }, - modalTitle: { ...typography.h3, color: colors.text }, + modalTitle: { ...typography.h3, color: c.text }, addRow: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.md }, input: { flex: 1, - backgroundColor: colors.background, + backgroundColor: c.background, borderRadius: radius.sm, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, paddingHorizontal: spacing.md, paddingVertical: 8, - color: colors.text, + color: c.text, ...typography.body, }, list: { maxHeight: 300 }, - separator: { height: 1, backgroundColor: colors.border }, + separator: { height: 1, backgroundColor: c.border }, row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: spacing.md, }, - rowText: { ...typography.body, color: colors.text, flex: 1, marginRight: spacing.sm }, + rowText: { ...typography.body, color: c.text, flex: 1, marginRight: spacing.sm }, removeBtn: { padding: 6, borderRadius: radius.sm }, - removeBtnPressed: { backgroundColor: colors.muted }, + removeBtnPressed: { backgroundColor: c.muted }, emptyText: { ...typography.body, - color: colors.textMuted, + color: c.textMuted, textAlign: 'center', paddingVertical: spacing.xl, }, }); +} diff --git a/src/components/settings/FilesSettings.tsx b/src/components/settings/FilesSettings.tsx index 37930441..9c6b25c3 100644 --- a/src/components/settings/FilesSettings.tsx +++ b/src/components/settings/FilesSettings.tsx @@ -4,7 +4,8 @@ import { Folder, FileText, FileCode, FileAudio, File, Image as ImageIcon, } from 'lucide-react-native'; import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; type FolderLayout = 'inline' | 'sidebar'; type ViewMode = 'list' | 'grid'; @@ -58,23 +59,25 @@ function formatSize(b: number) { } function getIcon(file: SampleFile, colored: boolean, sz: number) { - if (file.isFolder) return ; + if (file.isFolder) return ; const ext = file.name.split('.').pop()?.toLowerCase(); switch (ext) { case 'jpg': case 'png': case 'gif': - return ; + return ; case 'mp3': case 'wav': - return ; + return ; case 'pdf': - return ; + return ; case 'md': case 'json': case 'js': case 'ts': - return ; + return ; default: - return ; + return ; } } function FilesPreview({ prefs }: { prefs: FilesPrefs }) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const files = SAMPLE.filter((f) => { if (!prefs.showHiddenFiles && f.hidden) return false; return true; @@ -127,6 +130,8 @@ function FilesPreview({ prefs }: { prefs: FilesPrefs }) { } export function FilesSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const [prefs, setPrefs] = useState(DEFAULTS); const update = (patch: Partial) => setPrefs((p) => ({ ...p, ...patch })); @@ -188,7 +193,7 @@ export function FilesSettings() { update({ showIcons: v })} /> - + update({ coloredIcons: v })} @@ -212,14 +217,15 @@ export function FilesSettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ container: { gap: spacing.xxxl }, - previewLabel: { ...typography.bodyMedium, color: colors.text, marginBottom: spacing.sm }, + previewLabel: { ...typography.bodyMedium, color: c.text, marginBottom: spacing.sm }, previewBox: { borderRadius: radius.md, borderWidth: 1, - borderColor: colors.border, - backgroundColor: colors.background, + borderColor: c.border, + backgroundColor: c.background, overflow: 'hidden', minHeight: 160, }, @@ -230,10 +236,10 @@ const styles = StyleSheet.create({ paddingVertical: 4, paddingHorizontal: spacing.sm, borderBottomWidth: 1, - borderBottomColor: colors.border, - backgroundColor: colors.muted, + borderBottomColor: c.border, + backgroundColor: c.muted, }, - listHeaderText: { fontSize: 10, fontWeight: '500', color: colors.mutedForeground }, + listHeaderText: { fontSize: 10, fontWeight: '500', color: c.mutedForeground }, listRow: { flexDirection: 'row', alignItems: 'center', @@ -241,10 +247,10 @@ const styles = StyleSheet.create({ paddingVertical: 6, paddingHorizontal: spacing.sm, borderBottomWidth: 1, - borderBottomColor: colors.border, + borderBottomColor: c.border, }, - listName: { fontSize: 11, color: colors.text, flex: 1 }, - listMeta: { fontSize: 10, color: colors.mutedForeground, width: 60, textAlign: 'right' }, + listName: { fontSize: 11, color: c.text, flex: 1 }, + listMeta: { fontSize: 10, color: c.mutedForeground, width: 60, textAlign: 'right' }, gridWrap: { flexDirection: 'row', flexWrap: 'wrap', @@ -258,5 +264,6 @@ const styles = StyleSheet.create({ width: 72, borderRadius: radius.sm, }, - gridName: { fontSize: 9, color: colors.text, textAlign: 'center' }, + gridName: { fontSize: 9, color: c.text, textAlign: 'center' }, }); +} diff --git a/src/components/settings/FilterSettings.tsx b/src/components/settings/FilterSettings.tsx index 9a25e0df..2341c1ea 100644 --- a/src/components/settings/FilterSettings.tsx +++ b/src/components/settings/FilterSettings.tsx @@ -3,7 +3,8 @@ import { View, Text, StyleSheet, Pressable } from 'react-native'; import { Plus, GripVertical, X, Code, Filter, AlertTriangle, RotateCcw } from 'lucide-react-native'; import { SettingsSection, ToggleSwitch } from './settings-section'; import Button from '../Button'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; interface Rule { id: string; @@ -16,6 +17,8 @@ interface Rule { const SAMPLE_RULES: Rule[] = []; export function FilterSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const [rules, setRules] = useState(SAMPLE_RULES); const [expandedView, setExpandedView] = useState(false); const [isOpaque] = useState(false); @@ -45,7 +48,7 @@ export function FilterSettings() { Open Sieve Editor - + Reset to visual @@ -55,7 +58,7 @@ export function FilterSettings() { {rules.length === 0 && !isOpaque && ( - + No rules yet. )} @@ -65,7 +68,7 @@ export function FilterSettings() { {rules.map((rule) => ( - + toggle(rule.id)} /> @@ -88,7 +91,7 @@ export function FilterSettings() { ) : ( setDeleteId(rule.id)}> - + )} @@ -100,11 +103,11 @@ export function FilterSettings() { {!isOpaque && ( - )} - @@ -120,7 +123,8 @@ export function FilterSettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ container: { gap: spacing.xl }, opaqueWarning: { flexDirection: 'row', @@ -134,11 +138,11 @@ const styles = StyleSheet.create({ }, opaqueText: { ...typography.caption, color: '#fde68a' }, opaqueActions: { flexDirection: 'row', gap: spacing.md, marginTop: spacing.sm }, - linkText: { ...typography.caption, color: colors.primary, fontWeight: '500' }, + linkText: { ...typography.caption, color: c.primary, fontWeight: '500' }, resetLink: { flexDirection: 'row', alignItems: 'center', gap: 4 }, - resetText: { ...typography.caption, color: colors.error, fontWeight: '500' }, + resetText: { ...typography.caption, color: c.error, fontWeight: '500' }, emptyState: { alignItems: 'center', paddingVertical: spacing.xxl, gap: spacing.md }, - emptyText: { ...typography.body, color: colors.mutedForeground }, + emptyText: { ...typography.body, color: c.mutedForeground }, ruleRow: { flexDirection: 'row', alignItems: 'flex-start', @@ -146,12 +150,12 @@ const styles = StyleSheet.create({ padding: spacing.md, borderRadius: radius.sm, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, }, ruleDisabled: { opacity: 0.6 }, grip: { paddingTop: 2 }, - ruleName: { ...typography.bodyMedium, color: colors.text }, - ruleSummary: { ...typography.caption, color: colors.mutedForeground, marginTop: 2 }, + ruleName: { ...typography.bodyMedium, color: c.text }, + ruleSummary: { ...typography.caption, color: c.mutedForeground, marginTop: 2 }, iconBtn: { width: 28, height: 28, @@ -167,5 +171,6 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', }, expandedToggle: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, - expandedLabel: { ...typography.caption, color: colors.mutedForeground }, + expandedLabel: { ...typography.caption, color: c.mutedForeground }, }); +} diff --git a/src/components/settings/FolderSettings.tsx b/src/components/settings/FolderSettings.tsx index d55c4526..effa14e4 100644 --- a/src/components/settings/FolderSettings.tsx +++ b/src/components/settings/FolderSettings.tsx @@ -4,7 +4,8 @@ import { Folder, Inbox, Send, FileText, Trash, ShieldAlert, Archive, } from 'lucide-react-native'; import { SettingsSection, SettingItem } from './settings-section'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useEmailStore } from '../../stores/email-store'; import type { Mailbox } from '../../api/types'; @@ -23,6 +24,8 @@ function getIcon(mb: Mailbox) { } export function FolderSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const mailboxes = useEmailStore((s) => s.mailboxes); const fetchMailboxes = useEmailStore((s) => s.fetchMailboxes); @@ -47,7 +50,7 @@ export function FolderSettings() { > {mailboxes.length === 0 ? ( - + ) : ( @@ -58,7 +61,7 @@ export function FolderSettings() { {mb.name} {mb.role && ROLE_LABEL[mb.role] && ( @@ -94,7 +97,8 @@ export function FolderSettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ container: { gap: spacing.xxxl }, loading: { paddingVertical: 40, alignItems: 'center' }, folderRow: { @@ -106,22 +110,23 @@ const styles = StyleSheet.create({ borderRadius: radius.sm, }, folderLeft: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, flex: 1 }, - folderName: { ...typography.body, color: colors.text, flexShrink: 1 }, + folderName: { ...typography.body, color: c.text, flexShrink: 1 }, rolePill: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: radius.full, - backgroundColor: colors.primaryBg, + backgroundColor: c.primaryBg, }, - rolePillText: { fontSize: 10, fontWeight: '500', color: colors.primary }, + rolePillText: { fontSize: 10, fontWeight: '500', color: c.primary }, folderRight: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, unreadBadge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: radius.full, - backgroundColor: colors.primary, + backgroundColor: c.primary, }, - unreadText: { fontSize: 10, fontWeight: '500', color: colors.primaryForeground }, - total: { ...typography.caption, color: colors.mutedForeground, minWidth: 32, textAlign: 'right' }, - caption: { ...typography.caption, color: colors.mutedForeground }, + unreadText: { fontSize: 10, fontWeight: '500', color: c.primaryForeground }, + total: { ...typography.caption, color: c.mutedForeground, minWidth: 32, textAlign: 'right' }, + caption: { ...typography.caption, color: c.mutedForeground }, }); +} diff --git a/src/components/settings/IdentitySettings.tsx b/src/components/settings/IdentitySettings.tsx index 819f1590..7c068d96 100644 --- a/src/components/settings/IdentitySettings.tsx +++ b/src/components/settings/IdentitySettings.tsx @@ -2,10 +2,13 @@ import React, { useEffect } from 'react'; import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; import { SettingsSection, SettingItem } from './settings-section'; import Button from '../Button'; -import { colors, typography, spacing, radius } from '../../theme/tokens'; +import { typography, spacing, radius, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useSettingsStore } from '../../stores/settings-store'; export function IdentitySettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const identities = useSettingsStore((s) => s.identities); const loading = useSettingsStore((s) => s.loading); const error = useSettingsStore((s) => s.error); @@ -30,7 +33,7 @@ export function IdentitySettings() { > {loading ? ( - + ) : ( {countLabel} )} @@ -67,7 +70,8 @@ export function IdentitySettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ row: { flexDirection: 'row', alignItems: 'center', @@ -75,14 +79,14 @@ const styles = StyleSheet.create({ }, count: { ...typography.body, - color: colors.text, + color: c.text, }, errorBox: { padding: spacing.md, borderRadius: radius.sm, - backgroundColor: colors.errorBg, + backgroundColor: c.errorBg, }, - errorText: { ...typography.caption, color: colors.error }, + errorText: { ...typography.caption, color: c.error }, identityRow: { flexDirection: 'row', alignItems: 'center', @@ -90,15 +94,16 @@ const styles = StyleSheet.create({ paddingVertical: spacing.md, paddingHorizontal: spacing.md, borderRadius: radius.sm, - backgroundColor: colors.muted, + backgroundColor: c.muted, }, - identityName: { ...typography.bodyMedium, color: colors.text }, - identityEmail: { ...typography.caption, color: colors.mutedForeground, marginTop: 2 }, + identityName: { ...typography.bodyMedium, color: c.text }, + identityEmail: { ...typography.caption, color: c.mutedForeground, marginTop: 2 }, badge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: radius.full, backgroundColor: 'rgba(59,130,246,0.15)', }, - badgeText: { fontSize: 10, fontWeight: '500', color: colors.primary }, + badgeText: { fontSize: 10, fontWeight: '500', color: c.primary }, }); +} diff --git a/src/components/settings/KeywordSettings.tsx b/src/components/settings/KeywordSettings.tsx index 3eca29af..108b1508 100644 --- a/src/components/settings/KeywordSettings.tsx +++ b/src/components/settings/KeywordSettings.tsx @@ -2,14 +2,20 @@ import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, Pressable, TextInput } from 'react-native'; import { Plus, Pencil, Trash2, Check, X, RotateCcw } from 'lucide-react-native'; import { SettingsSection } from './settings-section'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useKeywordsStore, type KeywordDef } from '../../stores/keywords-store'; +import { DARK_COLORS } from '../../theme/tokens'; type Keyword = KeywordDef; -const PALETTE_KEYS = Object.keys(colors.tags) as (keyof typeof colors.tags)[]; +// Palette keys are theme-agnostic (same names in both palettes), so use DARK_COLORS +// at module load. The actual rendered swatch colors come from the active theme via `c.tags[key]`. +const PALETTE_KEYS = Object.keys(DARK_COLORS.tags) as (keyof typeof DARK_COLORS.tags)[]; export function KeywordSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const keywords = useKeywordsStore((s) => s.keywords); const addKeyword = useKeywordsStore((s) => s.add); const updateKeyword = useKeywordsStore((s) => s.update); @@ -53,7 +59,7 @@ export function KeywordSettings() { /> ); } - const palette = colors.tags[kw.color]; + const palette = c.tags[kw.color]; return ( @@ -61,10 +67,10 @@ export function KeywordSettings() { $label:{kw.id} setEditingId(kw.id)}> - + deleteKeyword(kw.id)}> - + @@ -82,11 +88,11 @@ export function KeywordSettings() { {!isAdding && editingId === null && ( setIsAdding(true)}> - + Add keyword - + Reset defaults @@ -104,8 +110,10 @@ interface KeywordFormProps { } function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProps) { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const [label, setLabel] = useState(initial?.label ?? ''); - const [color, setColor] = useState(initial?.color ?? 'blue'); + const [color, setColor] = useState(initial?.color ?? 'blue'); const normalizedId = label .trim() @@ -130,7 +138,7 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp value={label} onChangeText={setLabel} placeholder="e.g. Important" - placeholderTextColor={colors.mutedForeground} + placeholderTextColor={c.mutedForeground} style={styles.input} maxLength={30} autoFocus @@ -142,7 +150,7 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp Color {PALETTE_KEYS.map((key) => { - const p = colors.tags[key]; + const p = c.tags[key]; return ( - + Cancel - + {initial ? 'Save' : 'Add'} @@ -176,7 +184,8 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ kwRow: { flexDirection: 'row', alignItems: 'center', @@ -184,12 +193,12 @@ const styles = StyleSheet.create({ padding: spacing.md, borderRadius: radius.sm, borderWidth: 1, - borderColor: colors.border, - backgroundColor: colors.background, + borderColor: c.border, + backgroundColor: c.background, }, kwDot: { width: 20, height: 20, borderRadius: 10 }, - kwLabel: { ...typography.bodyMedium, color: colors.text, flex: 1 }, - kwId: { fontSize: 10, color: colors.mutedForeground, fontFamily: 'monospace' }, + kwLabel: { ...typography.bodyMedium, color: c.text, flex: 1 }, + kwId: { fontSize: 10, color: c.mutedForeground, fontFamily: 'monospace' }, iconBtn: { width: 28, height: 28, @@ -211,34 +220,34 @@ const styles = StyleSheet.create({ borderRadius: radius.sm, borderWidth: 1, borderStyle: 'dashed', - borderColor: colors.border, + borderColor: c.border, }, - outlineBtnText: { ...typography.caption, color: colors.mutedForeground }, + outlineBtnText: { ...typography.caption, color: c.mutedForeground }, form: { gap: spacing.md, padding: spacing.md, borderRadius: radius.sm, borderWidth: 1, - borderColor: colors.primaryBorder, - backgroundColor: colors.accent, + borderColor: c.primaryBorder, + backgroundColor: c.accent, }, - formLabel: { ...typography.caption, color: colors.mutedForeground, marginBottom: 4 }, + formLabel: { ...typography.caption, color: c.mutedForeground, marginBottom: 4 }, input: { paddingHorizontal: spacing.sm, paddingVertical: 6, borderRadius: radius.sm, - backgroundColor: colors.background, + backgroundColor: c.background, borderWidth: 1, - borderColor: colors.border, - color: colors.text, + borderColor: c.border, + color: c.text, ...typography.body, }, - errorText: { ...typography.caption, color: colors.error, marginTop: 4 }, + errorText: { ...typography.caption, color: c.error, marginTop: 4 }, palette: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 }, colorSwatch: { width: 24, height: 24, borderRadius: 12 }, colorSwatchSelected: { borderWidth: 2, - borderColor: colors.text, + borderColor: c.text, }, formActions: { flexDirection: 'row', @@ -253,9 +262,9 @@ const styles = StyleSheet.create({ paddingVertical: 6, borderRadius: radius.sm, borderWidth: 1, - borderColor: colors.border, + borderColor: c.border, }, - cancelFormText: { ...typography.caption, color: colors.text }, + cancelFormText: { ...typography.caption, color: c.text }, saveBtn: { flexDirection: 'row', alignItems: 'center', @@ -263,7 +272,8 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.md, paddingVertical: 6, borderRadius: radius.sm, - backgroundColor: colors.primary, + backgroundColor: c.primary, }, - saveBtnText: { ...typography.caption, color: colors.primaryForeground, fontWeight: '500' }, + saveBtnText: { ...typography.caption, color: c.primaryForeground, fontWeight: '500' }, }); +} diff --git a/src/components/settings/LanguageSettings.tsx b/src/components/settings/LanguageSettings.tsx index 805b65e4..5c48f20a 100644 --- a/src/components/settings/LanguageSettings.tsx +++ b/src/components/settings/LanguageSettings.tsx @@ -2,11 +2,14 @@ import React, { useEffect } from 'react'; import { View, Text, StyleSheet, Pressable } from 'react-native'; import { Check } from 'lucide-react-native'; import { SettingsSection, SettingItem } from './settings-section'; -import { colors, spacing, radius, typography } from '../../theme/tokens'; +import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useLocaleStore } from '../../stores/locale-store'; import { SUPPORTED_LOCALES, detectDeviceLocale, type LocaleCode } from '../../i18n'; export function LanguageSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const override = useLocaleStore((s) => s.override); const setOverride = useLocaleStore((s) => s.setOverride); const hydrated = useLocaleStore((s) => s.hydrated); @@ -30,7 +33,7 @@ export function LanguageSettings() { {label} {sublabel ? {sublabel} : null} - {active ? : null} + {active ? : null} ); }; @@ -57,8 +60,9 @@ export function LanguageSettings() { ); } -const styles = StyleSheet.create({ - list: { borderRadius: radius.md, overflow: 'hidden', backgroundColor: colors.background }, +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ + list: { borderRadius: radius.md, overflow: 'hidden', backgroundColor: c.background }, row: { flexDirection: 'row', alignItems: 'center', @@ -66,8 +70,9 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.md, gap: spacing.sm, }, - rowPressed: { backgroundColor: colors.muted }, - divider: { height: 1, backgroundColor: colors.border, marginHorizontal: spacing.md }, - label: { ...typography.body, color: colors.text }, - sublabel: { ...typography.caption, color: colors.textMuted, marginTop: 2 }, + rowPressed: { backgroundColor: c.muted }, + divider: { height: 1, backgroundColor: c.border, marginHorizontal: spacing.md }, + label: { ...typography.body, color: c.text }, + sublabel: { ...typography.caption, color: c.textMuted, marginTop: 2 }, }); +} diff --git a/src/components/settings/LayoutSettings.tsx b/src/components/settings/LayoutSettings.tsx index da56bd0a..3581a110 100644 --- a/src/components/settings/LayoutSettings.tsx +++ b/src/components/settings/LayoutSettings.tsx @@ -2,7 +2,8 @@ import React, { useEffect } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { ArrowRight, ArrowLeft } from 'lucide-react-native'; import { SettingsSection, SettingItem, RadioGroup } from './settings-section'; -import { colors, spacing, typography } from '../../theme/tokens'; +import { spacing, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useSettingsStore, type SwipeAction } from '../../stores/settings-store'; const SWIPE_OPTIONS: { value: SwipeAction; label: string }[] = [ @@ -15,6 +16,8 @@ const SWIPE_OPTIONS: { value: SwipeAction; label: string }[] = [ ]; export function LayoutSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const swipeLeftAction = useSettingsStore((s) => s.swipeLeftAction); const setSwipeLeftAction = useSettingsStore((s) => s.setSwipeLeftAction); const swipeRightAction = useSettingsStore((s) => s.swipeRightAction); @@ -28,7 +31,7 @@ export function LayoutSettings() { - + Swipe right (left → right) @@ -43,7 +46,7 @@ export function LayoutSettings() { - + Swipe left (right → left) @@ -64,8 +67,10 @@ export function LayoutSettings() { ); } -const styles = StyleSheet.create({ +function makeStyles(c: ThemePalette) { + return StyleSheet.create({ row: { flexDirection: 'row', alignItems: 'center', gap: 6 }, - rowLabel: { ...typography.bodyMedium, color: colors.text }, - rowDescription: { ...typography.caption, color: colors.mutedForeground }, + rowLabel: { ...typography.bodyMedium, color: c.text }, + rowDescription: { ...typography.caption, color: c.mutedForeground }, }); +} diff --git a/src/components/settings/NotificationSettings.tsx b/src/components/settings/NotificationSettings.tsx index fd8d8c5e..5a62e621 100644 --- a/src/components/settings/NotificationSettings.tsx +++ b/src/components/settings/NotificationSettings.tsx @@ -10,7 +10,8 @@ import { CheckCircle2, Volume2, XCircle } from 'lucide-react-native'; import Button from '../Button'; import Dialog from '../Dialog'; import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section'; -import { colors, radius, spacing, typography } from '../../theme/tokens'; +import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; +import { useColors } from '../../theme/colors'; import { useAuthStore } from '../../stores/auth-store'; import { DEFAULT_RELAY_BASE_URL, @@ -35,6 +36,8 @@ type PushStatus = | { kind: 'error'; message: string }; export function NotificationSettings() { + const c = useColors(); + const styles = React.useMemo(() => makeStyles(c), [c]); const username = useAuthStore((s) => s.username); const [sound, setSound] = useState('default'); @@ -120,26 +123,26 @@ export function NotificationSettings() { {pushStatus.kind === 'busy' && ( <> - + {pushStatus.message} )} {pushStatus.kind === 'enabled' && ( <> - - Active + + Active )} {pushStatus.kind === 'error' && ( <> - - + + {pushStatus.message} )} {pushStatus.kind === 'idle' && ( - + Not configured )} @@ -152,7 +155,7 @@ export function NotificationSettings() { value={relayUrl} onChangeText={setRelayUrl} placeholder={DEFAULT_RELAY_BASE_URL} - placeholderTextColor={colors.textMuted} + placeholderTextColor={c.textMuted} autoCapitalize="none" autoCorrect={false} keyboardType="url" @@ -183,7 +186,7 @@ export function NotificationSettings() {