mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 08:07:11 +00:00
feat: enhance SettingsScreen with dynamic localization and theming
- Integrated useColors hook for dynamic theming in SettingsScreen. - Updated tab labels and titles to use localized strings. - Refactored styles to utilize theme palette for consistent styling. - Improved locale subscription to re-render labels on language change. feat: implement useSheetDrag hook for bottom sheet gesture handling - Added useSheetDrag hook to manage drag-to-dismiss functionality for bottom sheets. - Configured pan responder for handling drag gestures and dismissing the sheet based on distance and velocity. refactor: restructure color and token management - Introduced useColors and useResolvedTheme hooks for better theme management. - Separated light and dark color palettes for improved maintainability. - Updated tokens to reflect new theming structure and ensure consistency across components.
This commit is contained in:
+28
-45
@@ -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 (
|
||||
<View style={[styles.base, variantStyles[variant], style]}>
|
||||
<Text style={[styles.text, textVariantStyles[variant]]}>
|
||||
<View style={[styles.base, styles[`bg_${variant}` as const], style]}>
|
||||
<Text style={[styles.text, styles[`text_${variant}` as const]]}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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<string, ViewStyle> = {
|
||||
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<string, { color: string }> = {
|
||||
default: { color: colors.text },
|
||||
primary: { color: colors.primary },
|
||||
destructive: { color: colors.error },
|
||||
outline: { color: colors.text },
|
||||
muted: { color: colors.mutedForeground },
|
||||
};
|
||||
|
||||
+37
-105
@@ -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 (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
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 ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={variant === 'default' ? colors.primaryForeground : colors.primary}
|
||||
color={variant === 'default' ? c.primaryForeground : c.primary}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{icon}
|
||||
{typeof children === 'string' ? (
|
||||
<Text style={[styles.text, textVariantStyles[variant], isDisabled && styles.textDisabled]}>
|
||||
<Text style={[styles.text, styles[`text_${variant}` as const], isDisabled && styles.textDisabled]}>
|
||||
{children}
|
||||
</Text>
|
||||
) : (
|
||||
@@ -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<ButtonSize, ViewStyle> = {
|
||||
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<ButtonVariant, ViewStyle> = {
|
||||
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<ButtonVariant, ViewStyle> = {
|
||||
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<ButtonVariant, TextStyle> = {
|
||||
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 },
|
||||
});
|
||||
}
|
||||
|
||||
+19
-10
@@ -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 <View style={[styles.card, style]}>{children}</View>;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
@@ -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 (
|
||||
<View style={[styles.settingRow, bottomBorder && styles.settingRowBorder]}>
|
||||
{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,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={onCancel}>
|
||||
<View style={styles.backdrop}>
|
||||
@@ -41,7 +44,7 @@ export default function Dialog({
|
||||
<View style={styles.content}>
|
||||
{variant === 'destructive' && (
|
||||
<View style={styles.iconBadge}>
|
||||
<AlertTriangle size={20} color={colors.error} />
|
||||
<AlertTriangle size={20} color={c.error} />
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
<View style={styles.banner}>
|
||||
{externalContentPolicy === 'ask' && (
|
||||
<Pressable style={styles.bannerButton} onPress={onLoadImages} hitSlop={8}>
|
||||
<ImageIcon size={14} color={colors.textSecondary} />
|
||||
<ImageIcon size={14} color={c.textSecondary} />
|
||||
<Text style={styles.bannerButtonText}>Load external content</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
{senderEmail ? (
|
||||
<Pressable style={styles.bannerButton} onPress={onTrustSender} hitSlop={8}>
|
||||
<ShieldCheck size={14} color={colors.textSecondary} />
|
||||
<ShieldCheck size={14} color={c.textSecondary} />
|
||||
<Text style={styles.bannerButtonText}>Trust sender</Text>
|
||||
</Pressable>
|
||||
) : 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',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+16
-11
@@ -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({
|
||||
<RNTextInput
|
||||
{...props}
|
||||
style={[styles.input, style]}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
placeholderTextColor={c.textMuted}
|
||||
onFocus={(e) => { 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.bar}>
|
||||
<CloudOff size={14} color={colors.primaryForeground} />
|
||||
<CloudOff size={14} color={c.primaryForeground} />
|
||||
<Text style={styles.text} numberOfLines={1}>
|
||||
You are offline{hint ? ` — ${hint}` : ''}
|
||||
</Text>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<View style={[styles.container, style]}>
|
||||
{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,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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 (
|
||||
<View style={styles.counts}>
|
||||
@@ -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 (
|
||||
<Pressable
|
||||
@@ -108,9 +113,9 @@ function SidebarRow({
|
||||
style={styles.chevron}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={12} color={colors.textMuted} />
|
||||
<ChevronDown size={12} color={c.textMuted} />
|
||||
) : (
|
||||
<ChevronRight size={12} color={colors.textMuted} />
|
||||
<ChevronRight size={12} color={c.textMuted} />
|
||||
)}
|
||||
</Pressable>
|
||||
) : (
|
||||
@@ -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 */}
|
||||
<View style={styles.header}>
|
||||
<Pressable onPress={onClose} style={styles.headerClose} hitSlop={8}>
|
||||
<X size={20} color={colors.text} />
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => setAccountMenuOpen((v) => !v)}
|
||||
@@ -284,7 +291,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
</View>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
color={colors.textMuted}
|
||||
color={c.textMuted}
|
||||
style={accountMenuOpen ? styles.accountChevronOpen : undefined}
|
||||
/>
|
||||
</Pressable>
|
||||
@@ -323,7 +330,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
</View>
|
||||
{isActive && (
|
||||
<View style={styles.accountMenuCheckBadge}>
|
||||
<Check size={10} color={colors.primaryForeground} strokeWidth={3} />
|
||||
<Check size={10} color={c.primaryForeground} strokeWidth={3} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -364,7 +371,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
navigation.navigate('AddAccount');
|
||||
}}
|
||||
>
|
||||
<Plus size={16} color={colors.textSecondary} />
|
||||
<Plus size={16} color={c.textSecondary} />
|
||||
<Text style={styles.accountMenuActionText}>Add account</Text>
|
||||
</Pressable>
|
||||
</>
|
||||
@@ -382,7 +389,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
]}
|
||||
onPress={() => setDefaultAccount(active.id)}
|
||||
>
|
||||
<Star size={16} color={colors.textSecondary} />
|
||||
<Star size={16} color={c.textSecondary} />
|
||||
<Text style={styles.accountMenuActionText}>Set as default</Text>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -394,7 +401,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
]}
|
||||
onPress={() => { setAccountMenuOpen(false); onClose(); void logout(); }}
|
||||
>
|
||||
<LogOut size={16} color={colors.textSecondary} />
|
||||
<LogOut size={16} color={c.textSecondary} />
|
||||
<Text style={styles.accountMenuActionText} numberOfLines={1}>
|
||||
Sign out of {accountEmailFull || 'account'}
|
||||
</Text>
|
||||
@@ -407,8 +414,8 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
]}
|
||||
onPress={() => { setAccountMenuOpen(false); onClose(); void logoutAll(); }}
|
||||
>
|
||||
<LogOut size={16} color={colors.error} />
|
||||
<Text style={[styles.accountMenuActionText, { color: colors.error }]}>
|
||||
<LogOut size={16} color={c.error} />
|
||||
<Text style={[styles.accountMenuActionText, { color: c.error }]}>
|
||||
Sign out all
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -420,14 +427,14 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
{/* Folders section */}
|
||||
<Pressable style={styles.sectionHeader} onPress={toggleFoldersSection}>
|
||||
{foldersExpanded ? (
|
||||
<ChevronDown size={14} color={colors.textMuted} />
|
||||
<ChevronDown size={14} color={c.textMuted} />
|
||||
) : (
|
||||
<ChevronRight size={14} color={colors.textMuted} />
|
||||
<ChevronRight size={14} color={c.textMuted} />
|
||||
)}
|
||||
<Text style={styles.sectionHeaderText}>Folders</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={styles.sectionSettings}>
|
||||
<Settings size={14} color={colors.textMuted} />
|
||||
<Settings size={14} color={c.textMuted} />
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Pressable
|
||||
style={[
|
||||
@@ -37,7 +40,8 @@ export default function ToggleSwitch({ value, onValueChange, disabled = false }:
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
track: {
|
||||
width: componentSizes.toggleWidth, // w-11 = 44
|
||||
height: componentSizes.toggleHeight, // h-6 = 24
|
||||
@@ -45,18 +49,19 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
trackOn: {
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: c.primary,
|
||||
},
|
||||
trackOff: {
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
thumb: {
|
||||
width: componentSizes.toggleThumb, // h-4 w-4 = 16
|
||||
height: componentSizes.toggleThumb,
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: colors.background,
|
||||
backgroundColor: c.background,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import React from 'react';
|
||||
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||
import { Download, X } from 'lucide-react-native';
|
||||
import { useUpdatesStore } from '../stores/updates-store';
|
||||
import { colors, spacing, radius, typography } from '../theme/tokens';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
|
||||
import { useColors } from '../theme/colors';
|
||||
|
||||
export function UpdateBanner(): React.ReactElement | null {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => 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 (
|
||||
<View style={styles.banner}>
|
||||
<Download size={16} color={colors.primaryForeground} />
|
||||
<Download size={16} color={c.primaryForeground} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.title}>Update available</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
@@ -33,29 +36,31 @@ export function UpdateBanner(): React.ReactElement | null {
|
||||
<Text style={styles.installText}>{installing ? '…' : 'Install'}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.dismiss} onPress={dismissCurrent} hitSlop={8}>
|
||||
<X size={14} color={colors.primaryForeground} />
|
||||
<X size={14} color={c.primaryForeground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.empty}>
|
||||
<CalendarDays size={40} color={colors.surfaceActive} />
|
||||
<CalendarDays size={40} color={c.surfaceActive} />
|
||||
<Text style={styles.emptyTitle}>No upcoming events</Text>
|
||||
<Text style={styles.emptySubtitle}>The next {daysAhead} days are clear.</Text>
|
||||
</View>
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<SafeAreaView style={styles.drawerSafe} edges={['top', 'bottom', 'left']}>
|
||||
<View style={styles.header}>
|
||||
<Pressable onPress={onClose} style={styles.headerClose} hitSlop={8}>
|
||||
<X size={20} color={colors.text} />
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
<Text style={styles.headerTitle}>Calendars</Text>
|
||||
</View>
|
||||
@@ -134,15 +137,17 @@ function Section({
|
||||
hiddenSet: Set<string>;
|
||||
onToggle: (id: string) => void;
|
||||
}) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
{calendars.map((c) => {
|
||||
const visible = !hiddenSet.has(c.id);
|
||||
{calendars.map((cal) => {
|
||||
const visible = !hiddenSet.has(cal.id);
|
||||
return (
|
||||
<Pressable
|
||||
key={c.id}
|
||||
onPress={() => 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 && <Check size={12} color={colors.textInverse} />}
|
||||
{visible && <Check size={12} color={c.textInverse} />}
|
||||
</View>
|
||||
<Text style={[styles.rowName, !visible && styles.rowNameMuted]}>
|
||||
{c.name}
|
||||
{cal.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
</View>
|
||||
{!event.showWithoutTime && (
|
||||
<View style={styles.detailRow}>
|
||||
<Clock size={12} color={colors.textMuted} />
|
||||
<Clock size={12} color={c.textMuted} />
|
||||
<Text style={styles.detailText}>{time}</Text>
|
||||
</View>
|
||||
)}
|
||||
{event.description ? (
|
||||
<View style={styles.detailRow}>
|
||||
<MapPin size={12} color={colors.textMuted} />
|
||||
<MapPin size={12} color={c.textMuted} />
|
||||
<Text style={styles.detailText} numberOfLines={1}>
|
||||
{event.description}
|
||||
</Text>
|
||||
@@ -62,7 +65,7 @@ export function EventCard({ event, calendars, onPress, onLongPress }: EventCardP
|
||||
) : null}
|
||||
{count > 0 && (
|
||||
<View style={styles.detailRow}>
|
||||
<Users size={12} color={colors.textMuted} />
|
||||
<Users size={12} color={c.textMuted} />
|
||||
<Text style={styles.detailText}>{count} participants</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<SafeAreaView edges={['bottom']} style={styles.sheetSafe}>
|
||||
<View style={styles.handle} />
|
||||
<View {...dragHandlers}>
|
||||
<View style={styles.handleHit}>
|
||||
<View style={styles.handle} />
|
||||
</View>
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.colorBar, { backgroundColor: color }]} />
|
||||
<View style={styles.headerText}>
|
||||
<Text style={styles.title}>{event.title || 'Untitled'}</Text>
|
||||
{calendar && <Text style={styles.subtitle}>{calendar.name}</Text>}
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.colorBar, { backgroundColor: color }]} />
|
||||
<View style={styles.headerText}>
|
||||
<Text style={styles.title}>{event.title || 'Untitled'}</Text>
|
||||
{calendar && <Text style={styles.subtitle}>{calendar.name}</Text>}
|
||||
</View>
|
||||
<Pressable onPress={onClose} style={styles.closeBtn} hitSlop={8}>
|
||||
<X size={20} color={c.textMuted} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable onPress={onClose} style={styles.closeBtn} hitSlop={8}>
|
||||
<X size={20} color={colors.textMuted} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
@@ -184,29 +197,29 @@ export function EventDetailSheet({
|
||||
contentContainerStyle={styles.bodyContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<DetailRow icon={<Clock size={16} color={colors.textMuted} />} text={range} />
|
||||
<DetailRow icon={<Clock size={16} color={c.textMuted} />} text={range} />
|
||||
{event.description ? (
|
||||
<DetailRow
|
||||
icon={<AlignLeft size={16} color={colors.textMuted} />}
|
||||
icon={<AlignLeft size={16} color={c.textMuted} />}
|
||||
text={event.description}
|
||||
multiline
|
||||
/>
|
||||
) : null}
|
||||
{recurrence && (
|
||||
<DetailRow
|
||||
icon={<Repeat size={16} color={colors.textMuted} />}
|
||||
icon={<Repeat size={16} color={c.textMuted} />}
|
||||
text={recurrence}
|
||||
/>
|
||||
)}
|
||||
{alert && (
|
||||
<DetailRow
|
||||
icon={<Bell size={16} color={colors.textMuted} />}
|
||||
icon={<Bell size={16} color={c.textMuted} />}
|
||||
text={alert}
|
||||
/>
|
||||
)}
|
||||
{calendar && (
|
||||
<DetailRow
|
||||
icon={<CalendarIcon size={16} color={colors.textMuted} />}
|
||||
icon={<CalendarIcon size={16} color={c.textMuted} />}
|
||||
text={calendar.name}
|
||||
/>
|
||||
)}
|
||||
@@ -214,7 +227,7 @@ export function EventDetailSheet({
|
||||
{participants.length > 0 && (
|
||||
<View style={styles.participantsBlock}>
|
||||
<View style={styles.participantsHeader}>
|
||||
<Users size={16} color={colors.textMuted} />
|
||||
<Users size={16} color={c.textMuted} />
|
||||
<Text style={styles.participantsHeaderText}>
|
||||
{participants.length} participants
|
||||
</Text>
|
||||
@@ -244,21 +257,21 @@ export function EventDetailSheet({
|
||||
<View style={styles.actions}>
|
||||
{onEdit && (
|
||||
<ActionButton
|
||||
icon={<Pencil size={18} color={colors.text} />}
|
||||
icon={<Pencil size={18} color={c.text} />}
|
||||
label="Edit"
|
||||
onPress={() => onEdit(event)}
|
||||
/>
|
||||
)}
|
||||
{onDuplicate && (
|
||||
<ActionButton
|
||||
icon={<Copy size={18} color={colors.text} />}
|
||||
icon={<Copy size={18} color={c.text} />}
|
||||
label="Duplicate"
|
||||
onPress={() => onDuplicate(event)}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<ActionButton
|
||||
icon={<Trash2 size={18} color={colors.error} />}
|
||||
icon={<Trash2 size={18} color={c.error} />}
|
||||
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 (
|
||||
<View style={styles.detailRow}>
|
||||
<View style={styles.detailIcon}>{icon}</View>
|
||||
@@ -301,6 +316,8 @@ function ActionButton({
|
||||
onPress: () => void;
|
||||
destructive?: boolean;
|
||||
}) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
@@ -320,13 +337,13 @@ function ActionButton({
|
||||
function statusColor(status?: string): string {
|
||||
switch (status) {
|
||||
case 'accepted':
|
||||
return colors.success;
|
||||
return c.success;
|
||||
case 'declined':
|
||||
return colors.error;
|
||||
return c.error;
|
||||
case 'tentative':
|
||||
return colors.warning;
|
||||
return c.warning;
|
||||
default:
|
||||
return colors.textMuted;
|
||||
return c.textMuted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +362,8 @@ function statusLabel(status?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
@@ -357,20 +375,23 @@ const styles = StyleSheet.create({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
maxHeight: '85%',
|
||||
backgroundColor: colors.background,
|
||||
backgroundColor: c.background,
|
||||
borderTopLeftRadius: radius.xl,
|
||||
borderTopRightRadius: radius.xl,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
borderTopColor: c.border,
|
||||
},
|
||||
sheetSafe: { paddingTop: spacing.sm },
|
||||
handleHit: {
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing.xs,
|
||||
paddingBottom: spacing.sm,
|
||||
},
|
||||
handle: {
|
||||
alignSelf: 'center',
|
||||
width: 36,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: colors.surfaceActive,
|
||||
marginBottom: spacing.sm,
|
||||
backgroundColor: c.surfaceActive,
|
||||
},
|
||||
|
||||
header: {
|
||||
@@ -382,8 +403,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
colorBar: { width: 4, alignSelf: 'stretch', borderRadius: 2 },
|
||||
headerText: { flex: 1 },
|
||||
title: { ...typography.h3, color: colors.text },
|
||||
subtitle: { ...typography.caption, color: colors.textMuted, marginTop: 2 },
|
||||
title: { ...typography.h3, color: c.text },
|
||||
subtitle: { ...typography.caption, color: c.textMuted, marginTop: 2 },
|
||||
closeBtn: { padding: 4 },
|
||||
|
||||
body: { maxHeight: 400 },
|
||||
@@ -394,7 +415,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
detailRow: { flexDirection: 'row', gap: spacing.md, alignItems: 'flex-start' },
|
||||
detailIcon: { width: 16, paddingTop: 2 },
|
||||
detailText: { flex: 1, ...typography.body, color: colors.text },
|
||||
detailText: { flex: 1, ...typography.body, color: c.text },
|
||||
|
||||
participantsBlock: {
|
||||
marginTop: spacing.sm,
|
||||
@@ -406,7 +427,7 @@ const styles = StyleSheet.create({
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
participantsHeaderText: { ...typography.bodyMedium, color: colors.text },
|
||||
participantsHeaderText: { ...typography.bodyMedium, color: c.text },
|
||||
participantRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -414,13 +435,13 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 4,
|
||||
},
|
||||
participantDot: { width: 8, height: 8, borderRadius: 4 },
|
||||
participantName: { flex: 1, ...typography.body, color: colors.text },
|
||||
participantStatus: { ...typography.caption, color: colors.textMuted },
|
||||
participantName: { flex: 1, ...typography.body, color: c.text },
|
||||
participantStatus: { ...typography.caption, color: c.textMuted },
|
||||
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
borderTopColor: c.border,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingTop: spacing.sm,
|
||||
},
|
||||
@@ -432,7 +453,8 @@ const styles = StyleSheet.create({
|
||||
gap: 4,
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
actionBtnPressed: { backgroundColor: colors.surfaceHover },
|
||||
actionLabel: { ...typography.caption, color: colors.text },
|
||||
actionLabelDestructive: { color: colors.error },
|
||||
});
|
||||
actionBtnPressed: { backgroundColor: c.surfaceHover },
|
||||
actionLabel: { ...typography.caption, color: c.text },
|
||||
actionLabelDestructive: { color: c.error },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
} from 'lucide-react-native';
|
||||
import { addHours, addMinutes, format } from 'date-fns';
|
||||
import type { Calendar, CalendarEvent, Participant } 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 {
|
||||
buildAllDayDuration,
|
||||
getCalendarColor,
|
||||
@@ -150,6 +151,8 @@ export function EventModal({
|
||||
onDelete,
|
||||
onClose,
|
||||
}: EventModalProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const isEdit = !!event;
|
||||
|
||||
const [title, setTitle] = React.useState('');
|
||||
@@ -264,7 +267,7 @@ export function EventModal({
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
<View style={styles.header}>
|
||||
<Pressable onPress={onClose} hitSlop={8} style={styles.headerBtn}>
|
||||
<X size={20} color={colors.text} />
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
<Text style={styles.headerTitle}>{isEdit ? 'Edit event' : 'New event'}</Text>
|
||||
<Button
|
||||
@@ -287,12 +290,12 @@ export function EventModal({
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
placeholder="Title"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
placeholderTextColor={c.textMuted}
|
||||
style={styles.titleInput}
|
||||
/>
|
||||
|
||||
<Section
|
||||
icon={<CalendarIcon size={16} color={colors.textMuted} />}
|
||||
icon={<CalendarIcon size={16} color={c.textMuted} />}
|
||||
label="Calendar"
|
||||
>
|
||||
<Pressable
|
||||
@@ -340,14 +343,14 @@ export function EventModal({
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section icon={<Clock size={16} color={colors.textMuted} />} label="Time">
|
||||
<Section icon={<Clock size={16} color={c.textMuted} />} label="Time">
|
||||
<View style={styles.allDayRow}>
|
||||
<Text style={styles.fieldText}>All-day</Text>
|
||||
<Switch
|
||||
value={allDay}
|
||||
onValueChange={setAllDay}
|
||||
thumbColor={allDay ? colors.primary : colors.textMuted}
|
||||
trackColor={{ false: colors.surface, true: colors.primaryBg }}
|
||||
thumbColor={allDay ? c.primary : c.textMuted}
|
||||
trackColor={{ false: c.surface, true: c.primaryBg }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -392,7 +395,7 @@ export function EventModal({
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section icon={<Repeat size={16} color={colors.textMuted} />} label="Repeat">
|
||||
<Section icon={<Repeat size={16} color={c.textMuted} />} label="Repeat">
|
||||
<Pressable
|
||||
style={styles.fieldButton}
|
||||
onPress={() => setRecurrenceOpen((v) => !v)}
|
||||
@@ -420,7 +423,7 @@ export function EventModal({
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section icon={<Bell size={16} color={colors.textMuted} />} label="Reminder">
|
||||
<Section icon={<Bell size={16} color={c.textMuted} />} label="Reminder">
|
||||
<Pressable
|
||||
style={styles.fieldButton}
|
||||
onPress={() => setAlertOpen((v) => !v)}
|
||||
@@ -449,21 +452,21 @@ export function EventModal({
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
icon={<MapPin size={16} color={colors.textMuted} />}
|
||||
icon={<MapPin size={16} color={c.textMuted} />}
|
||||
label="Description"
|
||||
>
|
||||
<TextInput
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholder="Notes, location, link…"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
placeholderTextColor={c.textMuted}
|
||||
multiline
|
||||
style={styles.descriptionInput}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
icon={<Users size={16} color={colors.textMuted} />}
|
||||
icon={<Users size={16} color={c.textMuted} />}
|
||||
label="Participants"
|
||||
>
|
||||
<ParticipantInput
|
||||
@@ -480,7 +483,7 @@ export function EventModal({
|
||||
]}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
<Trash2 size={16} color={colors.error} />
|
||||
<Trash2 size={16} color={c.error} />
|
||||
<Text style={styles.deleteBtnText}>Delete event</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -560,6 +563,8 @@ function Section({
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
@@ -571,15 +576,16 @@ function Section({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: c.background },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
gap: spacing.md,
|
||||
},
|
||||
headerBtn: {
|
||||
@@ -589,23 +595,23 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
borderRadius: radius.full,
|
||||
},
|
||||
headerTitle: { flex: 1, ...typography.h3, color: colors.text },
|
||||
headerTitle: { flex: 1, ...typography.h3, color: c.text },
|
||||
|
||||
body: { flex: 1 },
|
||||
bodyContent: { padding: spacing.lg, gap: spacing.lg, paddingBottom: spacing.xxxl },
|
||||
|
||||
titleInput: {
|
||||
...typography.h2,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
|
||||
section: { gap: spacing.sm },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
||||
sectionIcon: { width: 16 },
|
||||
sectionLabel: { ...typography.bodyMedium, color: colors.textSecondary },
|
||||
sectionLabel: { ...typography.bodyMedium, color: c.textSecondary },
|
||||
sectionBody: { gap: spacing.sm },
|
||||
|
||||
fieldButton: {
|
||||
@@ -614,20 +620,20 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.surface,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
fieldText: { ...typography.body, color: colors.text },
|
||||
fieldText: { ...typography.body, color: c.text },
|
||||
|
||||
calendarRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
||||
calendarSwatch: { width: 12, height: 12, borderRadius: 6 },
|
||||
|
||||
popover: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.surface,
|
||||
backgroundColor: c.surface,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
popoverRow: {
|
||||
@@ -637,10 +643,10 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderLight,
|
||||
borderBottomColor: c.borderLight,
|
||||
},
|
||||
popoverRowActive: { backgroundColor: colors.primaryBg },
|
||||
popoverRowText: { ...typography.body, color: colors.text },
|
||||
popoverRowActive: { backgroundColor: c.primaryBg },
|
||||
popoverRowText: { ...typography.body, color: c.text },
|
||||
|
||||
allDayRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -649,9 +655,9 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.surface,
|
||||
backgroundColor: c.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
},
|
||||
dateRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -660,20 +666,20 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.surface,
|
||||
},
|
||||
dateLabel: { ...typography.body, color: colors.textSecondary, flex: 1 },
|
||||
dateLabel: { ...typography.body, color: c.textSecondary, flex: 1 },
|
||||
datePickers: { flexDirection: 'row', gap: spacing.sm },
|
||||
dateBtn: {
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.xs,
|
||||
backgroundColor: colors.background,
|
||||
backgroundColor: c.background,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
},
|
||||
dateText: { ...typography.body, color: colors.text },
|
||||
dateText: { ...typography.body, color: c.text },
|
||||
|
||||
descriptionInput: {
|
||||
minHeight: 80,
|
||||
@@ -681,9 +687,9 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.text,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.surface,
|
||||
color: c.text,
|
||||
...typography.body,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
@@ -696,9 +702,10 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.errorBorder,
|
||||
borderColor: c.errorBorder,
|
||||
marginTop: spacing.lg,
|
||||
},
|
||||
deleteBtnPressed: { backgroundColor: colors.errorBg },
|
||||
deleteBtnText: { ...typography.bodyMedium, color: colors.error },
|
||||
});
|
||||
deleteBtnPressed: { backgroundColor: c.errorBg },
|
||||
deleteBtnText: { ...typography.bodyMedium, color: c.error },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
startOfWeek,
|
||||
} from 'date-fns';
|
||||
import type { Calendar, CalendarEvent } from '../../api/types';
|
||||
import { colors, componentSizes, radius, spacing, typography } from '../../theme/tokens';
|
||||
import { componentSizes, radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import {
|
||||
buildEventDayIndex,
|
||||
eventsOnDayFromIndex,
|
||||
@@ -41,6 +42,8 @@ function MonthViewInner({
|
||||
onSelectDate,
|
||||
onLongPressDate,
|
||||
}: MonthViewProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const index = React.useMemo(
|
||||
() => eventsByDay ?? buildEventDayIndex(events),
|
||||
[eventsByDay, events],
|
||||
@@ -120,14 +123,15 @@ function MonthViewInner({
|
||||
|
||||
export const MonthView = React.memo(MonthViewInner);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
grid: { paddingHorizontal: spacing.sm },
|
||||
weekdayRow: { flexDirection: 'row', paddingHorizontal: spacing.xs, marginBottom: 4 },
|
||||
weekdayLabel: {
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
...typography.small,
|
||||
color: colors.textMuted,
|
||||
color: c.textMuted,
|
||||
},
|
||||
daysGrid: { flexDirection: 'row', flexWrap: 'wrap' },
|
||||
dayCell: { width: '14.28%', alignItems: 'center', paddingVertical: 4 },
|
||||
@@ -138,12 +142,12 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dayText: { ...typography.body, color: colors.text },
|
||||
dayTextMuted: { color: colors.textMuted },
|
||||
todayCircle: { backgroundColor: colors.primary },
|
||||
todayText: { color: colors.textInverse, fontWeight: '700' },
|
||||
selectedCircle: { backgroundColor: colors.primaryBg, borderWidth: 1.5, borderColor: colors.primary },
|
||||
selectedText: { color: colors.primary, fontWeight: '600' },
|
||||
dayText: { ...typography.body, color: c.text },
|
||||
dayTextMuted: { color: c.textMuted },
|
||||
todayCircle: { backgroundColor: c.primary },
|
||||
todayText: { color: c.textInverse, fontWeight: '700' },
|
||||
selectedCircle: { backgroundColor: c.primaryBg, borderWidth: 1.5, borderColor: c.primary },
|
||||
selectedText: { color: c.primary, fontWeight: '600' },
|
||||
dotsRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -160,9 +164,10 @@ const styles = StyleSheet.create({
|
||||
borderRadius: componentSizes.eventDot / 2,
|
||||
},
|
||||
overflowText: {
|
||||
color: colors.textMuted,
|
||||
color: c.textMuted,
|
||||
fontSize: 9,
|
||||
lineHeight: 10,
|
||||
marginLeft: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { X } from 'lucide-react-native';
|
||||
import { colors, radius, spacing, typography } from '../../theme/tokens';
|
||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useContactsStore } from '../../stores/contacts-store';
|
||||
import type { Participant } from '../../api/types';
|
||||
|
||||
@@ -44,6 +45,8 @@ function flattenContactEmails(): Suggestion[] {
|
||||
}
|
||||
|
||||
export function ParticipantInput({ participants, onChange }: ParticipantInputProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [draft, setDraft] = React.useState('');
|
||||
const [allSuggestions] = React.useState(() => flattenContactEmails());
|
||||
|
||||
@@ -110,7 +113,7 @@ export function ParticipantInput({ participants, onChange }: ParticipantInputPro
|
||||
{p.name || p.email || 'Unknown'}
|
||||
</Text>
|
||||
<Pressable onPress={() => removeParticipant(id)} hitSlop={6}>
|
||||
<X size={12} color={colors.textMuted} />
|
||||
<X size={12} color={c.textMuted} />
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
@@ -121,7 +124,7 @@ export function ParticipantInput({ participants, onChange }: ParticipantInputPro
|
||||
value={draft}
|
||||
onChangeText={setDraft}
|
||||
placeholder="Add participant by email"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
placeholderTextColor={c.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="email-address"
|
||||
@@ -155,45 +158,47 @@ export function ParticipantInput({ participants, onChange }: ParticipantInputPro
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.sm },
|
||||
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
|
||||
chip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
backgroundColor: colors.surface,
|
||||
backgroundColor: c.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.full,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 4,
|
||||
maxWidth: '100%',
|
||||
},
|
||||
chipText: { ...typography.caption, color: colors.text, maxWidth: 200 },
|
||||
chipText: { ...typography.caption, color: c.text, maxWidth: 200 },
|
||||
input: {
|
||||
height: 40,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
...typography.body,
|
||||
},
|
||||
suggestions: {
|
||||
maxHeight: 180,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.surface,
|
||||
backgroundColor: c.surface,
|
||||
},
|
||||
suggestionRow: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderLight,
|
||||
borderBottomColor: c.borderLight,
|
||||
},
|
||||
suggestionRowPressed: { backgroundColor: colors.surfaceHover },
|
||||
suggestionName: { ...typography.bodyMedium, color: colors.text },
|
||||
suggestionEmail: { ...typography.caption, color: colors.textMuted, marginTop: 1 },
|
||||
});
|
||||
suggestionRowPressed: { backgroundColor: c.surfaceHover },
|
||||
suggestionName: { ...typography.bodyMedium, color: c.text },
|
||||
suggestionEmail: { ...typography.caption, color: c.textMuted, marginTop: 1 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Modal, View, Text, Pressable, StyleSheet } from 'react-native';
|
||||
import { Repeat, Trash2 } from 'lucide-react-native';
|
||||
import { colors, radius, spacing, typography } from '../../theme/tokens';
|
||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { Button } from '..';
|
||||
|
||||
export type RecurrenceEditScope = 'this' | 'this_and_future' | 'all';
|
||||
@@ -25,6 +26,8 @@ export function RecurrenceScopeDialog({
|
||||
onSelect,
|
||||
onClose,
|
||||
}: RecurrenceScopeDialogProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [selected, setSelected] = React.useState<RecurrenceEditScope>('this');
|
||||
const isDelete = actionType === 'delete';
|
||||
|
||||
@@ -39,9 +42,9 @@ export function RecurrenceScopeDialog({
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.iconBadge, isDelete ? styles.iconBadgeDanger : styles.iconBadgePrimary]}>
|
||||
{isDelete ? (
|
||||
<Trash2 size={20} color={colors.error} />
|
||||
<Trash2 size={20} color={c.error} />
|
||||
) : (
|
||||
<Repeat size={20} color={colors.primary} />
|
||||
<Repeat size={20} color={c.primary} />
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.headerText}>
|
||||
@@ -90,7 +93,8 @@ export function RecurrenceScopeDialog({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
@@ -101,9 +105,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,
|
||||
padding: spacing.xxl,
|
||||
gap: spacing.lg,
|
||||
@@ -116,11 +120,11 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconBadgeDanger: { backgroundColor: colors.errorBg },
|
||||
iconBadgePrimary: { backgroundColor: colors.primaryBg },
|
||||
iconBadgeDanger: { backgroundColor: c.errorBg },
|
||||
iconBadgePrimary: { backgroundColor: c.primaryBg },
|
||||
headerText: { flex: 1 },
|
||||
title: { ...typography.h3, color: colors.text },
|
||||
description: { ...typography.body, color: colors.textMuted, marginTop: 4 },
|
||||
title: { ...typography.h3, color: c.text },
|
||||
description: { ...typography.body, color: c.textMuted, marginTop: 4 },
|
||||
options: { gap: spacing.xs },
|
||||
option: {
|
||||
flexDirection: 'row',
|
||||
@@ -132,27 +136,28 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
optionActive: { backgroundColor: colors.primaryBg, borderColor: colors.primary },
|
||||
optionActive: { backgroundColor: c.primaryBg, borderColor: c.primary },
|
||||
radio: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
radioActive: { borderColor: colors.primary },
|
||||
radioActive: { borderColor: c.primary },
|
||||
radioDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: c.primary,
|
||||
},
|
||||
optionLabel: { ...typography.body, color: colors.text, flex: 1 },
|
||||
optionLabel: { ...typography.body, color: c.text, flex: 1 },
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: spacing.md,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
} from 'react-native';
|
||||
import { addDays, format, isSameDay, isToday, startOfWeek } 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 {
|
||||
buildEventDayIndex,
|
||||
buildTimedFullDayWeekSegments,
|
||||
@@ -49,6 +50,8 @@ function WeekViewInner({
|
||||
onCreateAtTime,
|
||||
weekStartsOn = 0,
|
||||
}: WeekViewProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const scrollRef = React.useRef<ScrollView>(null);
|
||||
|
||||
const index = React.useMemo(
|
||||
@@ -311,12 +314,13 @@ function minutesToTimeLabel(minutes: number): string {
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: c.background },
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
},
|
||||
gutter: { width: GUTTER_WIDTH },
|
||||
dayHeaders: { flex: 1, flexDirection: 'row' },
|
||||
@@ -328,7 +332,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
dayHeaderWeekday: {
|
||||
...typography.small,
|
||||
color: colors.textMuted,
|
||||
color: c.textMuted,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
dayHeaderNumber: {
|
||||
@@ -338,29 +342,29 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dayHeaderNumberToday: { backgroundColor: colors.primary },
|
||||
dayHeaderNumberToday: { backgroundColor: c.primary },
|
||||
dayHeaderNumberSelected: {
|
||||
backgroundColor: colors.primaryBg,
|
||||
backgroundColor: c.primaryBg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary,
|
||||
borderColor: c.primary,
|
||||
},
|
||||
dayHeaderNumberText: { ...typography.bodyMedium, color: colors.text },
|
||||
dayHeaderNumberTextToday: { color: colors.textInverse, fontWeight: '700' },
|
||||
dayHeaderNumberTextSelected: { color: colors.primary, fontWeight: '600' },
|
||||
dayHeaderNumberText: { ...typography.bodyMedium, color: c.text },
|
||||
dayHeaderNumberTextToday: { color: c.textInverse, fontWeight: '700' },
|
||||
dayHeaderNumberTextSelected: { color: c.primary, fontWeight: '600' },
|
||||
|
||||
allDayStrip: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
allDayLabelWrap: { alignItems: 'flex-end', justifyContent: 'flex-start', paddingRight: 4 },
|
||||
allDayLabel: { ...typography.small, color: colors.textMuted },
|
||||
allDayLabel: { ...typography.small, color: c.textMuted },
|
||||
allDayGrid: { flex: 1, flexDirection: 'row', position: 'relative' },
|
||||
allDayCol: {
|
||||
flex: 1,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: colors.borderLight,
|
||||
borderLeftColor: c.borderLight,
|
||||
},
|
||||
allDayOverlay: {
|
||||
position: 'absolute',
|
||||
@@ -376,7 +380,7 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 4,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
allDayChipText: { fontSize: 10, color: colors.text, fontWeight: '500' },
|
||||
allDayChipText: { fontSize: 10, color: c.text, fontWeight: '500' },
|
||||
|
||||
scroll: { flex: 1 },
|
||||
scrollContent: {},
|
||||
@@ -385,19 +389,19 @@ const styles = StyleSheet.create({
|
||||
hourLabelCell: { paddingRight: 4, alignItems: 'flex-end' },
|
||||
hourLabel: {
|
||||
...typography.small,
|
||||
color: colors.textMuted,
|
||||
color: c.textMuted,
|
||||
transform: [{ translateY: -6 }],
|
||||
},
|
||||
dayCols: { flex: 1, flexDirection: 'row' },
|
||||
dayCol: {
|
||||
flex: 1,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: colors.border,
|
||||
borderLeftColor: c.border,
|
||||
position: 'relative',
|
||||
},
|
||||
hourSlot: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderLight,
|
||||
borderBottomColor: c.borderLight,
|
||||
},
|
||||
eventBlock: {
|
||||
position: 'absolute',
|
||||
@@ -407,8 +411,8 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 2,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
eventBlockTitle: { fontSize: 10, color: colors.text, fontWeight: '600' },
|
||||
eventBlockTime: { fontSize: 9, color: colors.textMuted, marginTop: 1 },
|
||||
eventBlockTitle: { fontSize: 10, color: c.text, fontWeight: '600' },
|
||||
eventBlockTime: { fontSize: 9, color: c.textMuted, marginTop: 1 },
|
||||
|
||||
nowLine: {
|
||||
position: 'absolute',
|
||||
@@ -421,7 +425,8 @@ const styles = StyleSheet.create({
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: colors.error,
|
||||
backgroundColor: c.error,
|
||||
},
|
||||
nowBar: { flex: 1, height: 1, backgroundColor: colors.error },
|
||||
});
|
||||
nowBar: { flex: 1, height: 1, backgroundColor: c.error },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import type { RootStackParamList } from '../../navigation/types';
|
||||
import { getEmails, queryEmailsByFilter } from '../../api/email';
|
||||
import { useCalendarStore } from '../../stores/calendar-store';
|
||||
import SenderAvatar from '../SenderAvatar';
|
||||
import { colors, radius, spacing, typography } from '../../theme/tokens';
|
||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
|
||||
const EMAIL_LIMIT = 5;
|
||||
const EVENT_LIMIT = 5;
|
||||
@@ -82,6 +83,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ContactActivity({ contact }: Props) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const navigation = useNavigation<Nav>();
|
||||
const calendarEvents = useCalendarStore((s) => s.events);
|
||||
|
||||
@@ -142,12 +145,12 @@ export function ContactActivity({ contact }: Props) {
|
||||
<View style={styles.wrap}>
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MailIcon size={14} color={colors.primary} />
|
||||
<MailIcon size={14} color={c.primary} />
|
||||
<Text style={styles.sectionTitle}>Recent emails</Text>
|
||||
</View>
|
||||
{emailsLoading ? (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<ActivityIndicator size="small" color={c.primary} />
|
||||
</View>
|
||||
) : emailsError ? (
|
||||
<Text style={styles.emptyText}>Couldn't load emails.</Text>
|
||||
@@ -190,7 +193,7 @@ export function ContactActivity({ contact }: Props) {
|
||||
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<CalendarDays size={14} color={colors.primary} />
|
||||
<CalendarDays size={14} color={c.primary} />
|
||||
<Text style={styles.sectionTitle}>Upcoming events</Text>
|
||||
</View>
|
||||
{upcomingEvents.length === 0 ? (
|
||||
@@ -215,21 +218,22 @@ export function ContactActivity({ contact }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
wrap: { gap: spacing.md, paddingHorizontal: spacing.lg, marginBottom: spacing.lg },
|
||||
section: {
|
||||
backgroundColor: colors.card,
|
||||
backgroundColor: c.card,
|
||||
borderRadius: radius.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
gap: spacing.sm,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: colors.primary,
|
||||
borderLeftColor: c.primary,
|
||||
},
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs },
|
||||
sectionTitle: { ...typography.bodySemibold, color: colors.text },
|
||||
sectionTitle: { ...typography.bodySemibold, color: c.text },
|
||||
loading: { paddingVertical: spacing.sm, alignItems: 'flex-start' },
|
||||
emptyText: { ...typography.caption, color: colors.textMuted },
|
||||
emptyText: { ...typography.caption, color: c.textMuted },
|
||||
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
@@ -238,21 +242,22 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 6,
|
||||
borderRadius: radius.xs,
|
||||
},
|
||||
rowPressed: { backgroundColor: colors.surface },
|
||||
rowPressed: { backgroundColor: c.surface },
|
||||
rowContent: { flex: 1, minWidth: 0 },
|
||||
rowTop: { flexDirection: 'row', justifyContent: 'space-between', gap: spacing.sm },
|
||||
rowSender: { ...typography.bodyMedium, color: colors.text, flex: 1 },
|
||||
rowDate: { ...typography.caption, color: colors.textMuted },
|
||||
rowSubject: { ...typography.body, color: colors.text },
|
||||
rowPreview: { ...typography.caption, color: colors.textMuted },
|
||||
rowSender: { ...typography.bodyMedium, color: c.text, flex: 1 },
|
||||
rowDate: { ...typography.caption, color: c.textMuted },
|
||||
rowSubject: { ...typography.body, color: c.text },
|
||||
rowPreview: { ...typography.caption, color: c.textMuted },
|
||||
|
||||
eventRow: { flexDirection: 'row', gap: spacing.sm, alignItems: 'baseline', paddingVertical: 4 },
|
||||
eventTime: {
|
||||
...typography.caption,
|
||||
color: colors.textMuted,
|
||||
color: c.textMuted,
|
||||
width: 64,
|
||||
},
|
||||
eventInfo: { flex: 1, minWidth: 0 },
|
||||
eventTitle: { ...typography.body, color: colors.text },
|
||||
eventDate: { ...typography.caption, color: colors.textMuted, marginTop: 2 },
|
||||
});
|
||||
eventTitle: { ...typography.body, color: c.text },
|
||||
eventDate: { ...typography.caption, color: c.textMuted, marginTop: 2 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
isGroup,
|
||||
} from '../../lib/contact-utils';
|
||||
import SenderAvatar from '../SenderAvatar';
|
||||
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 ContactListRowProps {
|
||||
contact: ContactCard;
|
||||
@@ -29,6 +30,8 @@ export default function ContactListRow({
|
||||
selectionMode,
|
||||
secondaryText,
|
||||
}: ContactListRowProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const name = getContactDisplayName(contact) || 'Unnamed';
|
||||
const email = getContactPrimaryEmail(contact);
|
||||
const org = getPrimaryOrg(contact);
|
||||
@@ -50,7 +53,7 @@ export default function ContactListRow({
|
||||
>
|
||||
{selectionMode && (
|
||||
<View style={[styles.checkbox, selected && styles.checkboxSelected]}>
|
||||
{selected && <Check size={14} color={colors.primaryForeground} />}
|
||||
{selected && <Check size={14} color={c.primaryForeground} />}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -58,7 +61,7 @@ export default function ContactListRow({
|
||||
<Image source={{ uri: photoUri }} style={styles.avatarImage} />
|
||||
) : group ? (
|
||||
<View style={styles.groupAvatar}>
|
||||
<Users size={18} color={colors.primary} />
|
||||
<Users size={18} color={c.primary} />
|
||||
</View>
|
||||
) : (
|
||||
<SenderAvatar name={name} email={email} size={componentSizes.avatarMd} />
|
||||
@@ -77,7 +80,8 @@ export default function ContactListRow({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -85,37 +89,38 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.md,
|
||||
},
|
||||
rowPressed: { backgroundColor: colors.surfaceHover },
|
||||
rowSelected: { backgroundColor: colors.primaryBg },
|
||||
rowPressed: { backgroundColor: c.surfaceHover },
|
||||
rowSelected: { backgroundColor: c.primaryBg },
|
||||
checkbox: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: radius.xs,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
checkboxSelected: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: c.primary,
|
||||
borderColor: c.primary,
|
||||
},
|
||||
avatarImage: {
|
||||
width: componentSizes.avatarMd,
|
||||
height: componentSizes.avatarMd,
|
||||
borderRadius: componentSizes.avatarMd / 2,
|
||||
backgroundColor: colors.surface,
|
||||
backgroundColor: c.surface,
|
||||
},
|
||||
groupAvatar: {
|
||||
width: componentSizes.avatarMd,
|
||||
height: componentSizes.avatarMd,
|
||||
borderRadius: componentSizes.avatarMd / 2,
|
||||
backgroundColor: colors.primaryBg,
|
||||
backgroundColor: c.primaryBg,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
info: { flex: 1, minWidth: 0 },
|
||||
name: { ...typography.bodyMedium, color: colors.text },
|
||||
subtitle: { ...typography.caption, color: colors.textMuted, marginTop: 1 },
|
||||
tertiary: { ...typography.small, color: colors.textSecondary, marginTop: 1 },
|
||||
});
|
||||
name: { ...typography.bodyMedium, color: c.text },
|
||||
subtitle: { ...typography.caption, color: c.textMuted, marginTop: 1 },
|
||||
tertiary: { ...typography.small, color: c.textSecondary, marginTop: 1 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
} from '../../stores/contacts-store';
|
||||
import { matchesContactSearch, isGroup } from '../../lib/contact-utils';
|
||||
import ContactListRow from './ContactListRow';
|
||||
import { colors, spacing, radius, typography, componentSizes } from '../../theme/tokens';
|
||||
import { useSheetDrag } from '../../lib/use-sheet-drag';
|
||||
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -26,9 +28,11 @@ interface Props {
|
||||
export default function ContactPickerSheet({
|
||||
visible, onClose, onSelect, title = 'Add Contact', excludedIds, multi = true,
|
||||
}: Props) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const allContacts = useContactsStore((s) => s.contacts);
|
||||
const sorted = React.useMemo(
|
||||
() => sortContactsByDisplayName(allContacts.filter((c) => !isGroup(c))),
|
||||
() => sortContactsByDisplayName(allContacts.filter((contact) => !isGroup(contact))),
|
||||
[allContacts],
|
||||
);
|
||||
const [query, setQuery] = React.useState('');
|
||||
@@ -36,6 +40,11 @@ export default function ContactPickerSheet({
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
|
||||
const overlay = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({
|
||||
slideY,
|
||||
closedY: Dimensions.get('window').height,
|
||||
onClose,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
@@ -90,37 +99,41 @@ export default function ContactPickerSheet({
|
||||
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
|
||||
<View style={styles.handle} />
|
||||
<View style={styles.header}>
|
||||
<Pressable onPress={onClose} style={styles.headerBtn} hitSlop={8}>
|
||||
<X size={20} color={colors.text} />
|
||||
</Pressable>
|
||||
<Text style={styles.headerTitle}>{title}</Text>
|
||||
{multi && (
|
||||
<Pressable
|
||||
onPress={confirm}
|
||||
disabled={selected.size === 0}
|
||||
style={[styles.doneBtn, selected.size === 0 && styles.doneBtnDisabled]}
|
||||
hitSlop={8}
|
||||
>
|
||||
<Text style={styles.doneLabel}>Add{selected.size > 0 ? ` (${selected.size})` : ''}</Text>
|
||||
<View {...dragHandlers}>
|
||||
<View style={styles.handleHit}>
|
||||
<View style={styles.handle} />
|
||||
</View>
|
||||
<View style={styles.header}>
|
||||
<Pressable onPress={onClose} style={styles.headerBtn} hitSlop={8}>
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
)}
|
||||
<Text style={styles.headerTitle}>{title}</Text>
|
||||
{multi && (
|
||||
<Pressable
|
||||
onPress={confirm}
|
||||
disabled={selected.size === 0}
|
||||
style={[styles.doneBtn, selected.size === 0 && styles.doneBtnDisabled]}
|
||||
hitSlop={8}
|
||||
>
|
||||
<Text style={styles.doneLabel}>Add{selected.size > 0 ? ` (${selected.size})` : ''}</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.searchBar}>
|
||||
<Search size={16} color={colors.textMuted} />
|
||||
<Search size={16} color={c.textMuted} />
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Search contacts..."
|
||||
placeholderTextColor={colors.textMuted}
|
||||
placeholderTextColor={c.textMuted}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
autoFocus
|
||||
/>
|
||||
{query.length > 0 && (
|
||||
<Pressable onPress={() => setQuery('')} hitSlop={8}>
|
||||
<X size={16} color={colors.textMuted} />
|
||||
<X size={16} color={c.textMuted} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
@@ -150,24 +163,28 @@ export default function ContactPickerSheet({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' },
|
||||
overlayPress: { flex: 1 },
|
||||
sheet: {
|
||||
position: 'absolute',
|
||||
left: 0, right: 0, bottom: 0,
|
||||
top: '15%',
|
||||
backgroundColor: colors.background,
|
||||
backgroundColor: c.background,
|
||||
borderTopLeftRadius: radius.lg,
|
||||
borderTopRightRadius: radius.lg,
|
||||
},
|
||||
handleHit: {
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing.sm,
|
||||
paddingBottom: spacing.xs,
|
||||
},
|
||||
handle: {
|
||||
alignSelf: 'center',
|
||||
width: 40,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: colors.border,
|
||||
marginTop: spacing.sm,
|
||||
backgroundColor: c.border,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
@@ -176,23 +193,23 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.sm,
|
||||
gap: spacing.xs,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderLight,
|
||||
borderBottomColor: c.borderLight,
|
||||
},
|
||||
headerBtn: {
|
||||
width: 40, height: 40,
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
borderRadius: radius.full,
|
||||
},
|
||||
headerTitle: { ...typography.h3, color: colors.text, flex: 1, marginLeft: spacing.xs },
|
||||
headerTitle: { ...typography.h3, color: c.text, flex: 1, marginLeft: spacing.xs },
|
||||
doneBtn: {
|
||||
paddingHorizontal: spacing.md,
|
||||
height: 36,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: c.primary,
|
||||
borderRadius: radius.full,
|
||||
},
|
||||
doneBtnDisabled: { opacity: 0.5 },
|
||||
doneLabel: { ...typography.bodyMedium, color: colors.primaryForeground },
|
||||
doneLabel: { ...typography.bodyMedium, color: c.primaryForeground },
|
||||
|
||||
searchBar: {
|
||||
flexDirection: 'row',
|
||||
@@ -202,14 +219,15 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
height: componentSizes.inputHeight,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
searchInput: { flex: 1, ...typography.body, color: colors.text, paddingVertical: 0 },
|
||||
searchInput: { flex: 1, ...typography.body, color: c.text, paddingVertical: 0 },
|
||||
|
||||
separator: { height: 1, backgroundColor: colors.borderLight, marginLeft: 68 },
|
||||
separator: { height: 1, backgroundColor: c.borderLight, marginLeft: 68 },
|
||||
|
||||
empty: { paddingVertical: spacing.xxxl, alignItems: 'center' },
|
||||
emptyText: { ...typography.body, color: colors.textMuted },
|
||||
});
|
||||
emptyText: { ...typography.body, color: c.textMuted },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
import type { ContactCategory } from '../../stores/contacts-store';
|
||||
import { useContactsStore } from '../../stores/contacts-store';
|
||||
import { getContactDisplayName, getContactKeywords, isGroup } from '../../lib/contact-utils';
|
||||
import { colors, spacing, radius, typography } from '../../theme/tokens';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -25,6 +26,8 @@ function isSameCategory(a: ContactCategory, b: ContactCategory): boolean {
|
||||
}
|
||||
|
||||
export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const selectedCategory = useContactsStore((s) => s.selectedCategory);
|
||||
const setSelectedCategory = useContactsStore((s) => s.setSelectedCategory);
|
||||
const contacts = useContactsStore((s) => s.contacts);
|
||||
@@ -93,14 +96,14 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
<SafeAreaView style={styles.drawerSafe} edges={['top', 'bottom', 'left']}>
|
||||
<View style={styles.header}>
|
||||
<Pressable onPress={onClose} style={styles.headerClose} hitSlop={8}>
|
||||
<X size={20} color={colors.text} />
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
<Text style={styles.headerTitle}>Contacts</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.scroll} contentContainerStyle={styles.scrollContent}>
|
||||
<CategoryRow
|
||||
icon={<Inbox size={16} color={colors.primary} />}
|
||||
icon={<Inbox size={16} color={c.primary} />}
|
||||
label="All Contacts"
|
||||
count={totalCount}
|
||||
active={selectedCategory.type === 'all'}
|
||||
@@ -115,7 +118,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
{expanded.books && books.map((book) => (
|
||||
<CategoryRow
|
||||
key={book.id}
|
||||
icon={<BookUser size={16} color={colors.textSecondary} />}
|
||||
icon={<BookUser size={16} color={c.textSecondary} />}
|
||||
label={book.name}
|
||||
count={book.count}
|
||||
active={isSameCategory(selectedCategory, { type: 'addressBook', addressBookId: book.id })}
|
||||
@@ -124,7 +127,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
))}
|
||||
{expanded.books && (
|
||||
<CategoryRow
|
||||
icon={<BookUser size={16} color={colors.textMuted} />}
|
||||
icon={<BookUser size={16} color={c.textMuted} />}
|
||||
label="Uncategorized"
|
||||
count={0}
|
||||
active={selectedCategory.type === 'uncategorized'}
|
||||
@@ -142,7 +145,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
{expanded.groups && groups.map((g) => (
|
||||
<CategoryRow
|
||||
key={g.id}
|
||||
icon={<Users size={16} color={colors.textSecondary} />}
|
||||
icon={<Users size={16} color={c.textSecondary} />}
|
||||
label={getContactDisplayName(g) || 'Group'}
|
||||
count={g.members ? Object.keys(g.members).length : 0}
|
||||
active={isSameCategory(selectedCategory, { type: 'group', groupId: g.id })}
|
||||
@@ -160,7 +163,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
{expanded.tags && keywords.map((kw) => (
|
||||
<CategoryRow
|
||||
key={kw.keyword}
|
||||
icon={<Tag size={16} color={colors.textSecondary} />}
|
||||
icon={<Tag size={16} color={c.textSecondary} />}
|
||||
label={kw.keyword}
|
||||
count={kw.count}
|
||||
active={isSameCategory(selectedCategory, { type: 'keyword', keyword: kw.keyword })}
|
||||
@@ -175,12 +178,14 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
}
|
||||
|
||||
function SectionHeader({ label, expanded, onPress }: { label: string; expanded: boolean; onPress: () => void }) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<Pressable style={styles.sectionHeader} onPress={onPress}>
|
||||
{expanded ? (
|
||||
<ChevronDown size={14} color={colors.textMuted} />
|
||||
<ChevronDown size={14} color={c.textMuted} />
|
||||
) : (
|
||||
<ChevronRight size={14} color={colors.textMuted} />
|
||||
<ChevronRight size={14} color={c.textMuted} />
|
||||
)}
|
||||
<Text style={styles.sectionHeaderText}>{label}</Text>
|
||||
</Pressable>
|
||||
@@ -196,6 +201,8 @@ function CategoryRow({
|
||||
active: boolean;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
@@ -212,7 +219,8 @@ function CategoryRow({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' },
|
||||
overlayPress: { flex: 1 },
|
||||
drawer: {
|
||||
@@ -222,7 +230,7 @@ const styles = StyleSheet.create({
|
||||
maxWidth: 340,
|
||||
backgroundColor: '#262626',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: colors.border,
|
||||
borderRightColor: c.border,
|
||||
},
|
||||
drawerSafe: { flex: 1 },
|
||||
|
||||
@@ -233,14 +241,14 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
},
|
||||
headerClose: {
|
||||
width: 36, height: 36,
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
headerTitle: { ...typography.h3, color: colors.text },
|
||||
headerTitle: { ...typography.h3, color: c.text },
|
||||
|
||||
scroll: { flex: 1 },
|
||||
scrollContent: { paddingBottom: spacing.md },
|
||||
@@ -253,7 +261,7 @@ const styles = StyleSheet.create({
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
sectionHeaderText: { ...typography.bodySemibold, color: colors.text },
|
||||
sectionHeaderText: { ...typography.bodySemibold, color: c.text },
|
||||
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
@@ -263,14 +271,15 @@ const styles = StyleSheet.create({
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: 'transparent',
|
||||
},
|
||||
rowPressed: { backgroundColor: colors.surfaceHover },
|
||||
rowActive: { backgroundColor: colors.accent, borderLeftColor: colors.primary },
|
||||
rowPressed: { backgroundColor: c.surfaceHover },
|
||||
rowActive: { backgroundColor: c.accent, borderLeftColor: c.primary },
|
||||
rowIcon: {
|
||||
width: 20, height: 20,
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
rowLabel: { flex: 1, ...typography.body, color: colors.text },
|
||||
rowLabelActive: { ...typography.bodySemibold, color: colors.text },
|
||||
rowCount: { ...typography.caption, color: colors.textMuted, marginLeft: spacing.sm },
|
||||
});
|
||||
rowLabel: { flex: 1, ...typography.body, color: c.text },
|
||||
rowLabelActive: { ...typography.bodySemibold, color: c.text },
|
||||
rowCount: { ...typography.caption, color: c.textMuted, marginLeft: spacing.sm },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||
import { Plus, X } 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';
|
||||
|
||||
export type CategoryAccent = 'contact' | 'work' | 'location' | 'personal' | 'digital' | 'calendar' | 'notes';
|
||||
|
||||
const ACCENT_COLORS: Record<CategoryAccent, string> = {
|
||||
contact: colors.primary,
|
||||
work: colors.calendar.orange,
|
||||
location: colors.calendar.green,
|
||||
personal: colors.calendar.pink,
|
||||
digital: colors.calendar.teal,
|
||||
calendar: colors.calendar.indigo,
|
||||
notes: colors.calendar.purple,
|
||||
};
|
||||
function accentColor(category: CategoryAccent, c: ThemePalette): string {
|
||||
switch (category) {
|
||||
case 'contact': return c.primary;
|
||||
case 'work': return c.calendar.orange;
|
||||
case 'location': return c.calendar.green;
|
||||
case 'personal': return c.calendar.pink;
|
||||
case 'digital': return c.calendar.teal;
|
||||
case 'calendar': return c.calendar.indigo;
|
||||
case 'notes': return c.calendar.purple;
|
||||
}
|
||||
}
|
||||
|
||||
interface FieldBlockProps {
|
||||
title: string;
|
||||
@@ -27,7 +30,9 @@ interface FieldBlockProps {
|
||||
export default function FieldBlock({
|
||||
title, category = 'contact', icon, onAdd, addLabel, children,
|
||||
}: FieldBlockProps) {
|
||||
const accent = ACCENT_COLORS[category];
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const accent = accentColor(category, c);
|
||||
return (
|
||||
<View style={[styles.block, { borderLeftColor: accent }]}>
|
||||
<View style={styles.header}>
|
||||
@@ -36,7 +41,7 @@ export default function FieldBlock({
|
||||
<View style={{ flex: 1 }} />
|
||||
{onAdd && (
|
||||
<Pressable onPress={onAdd} hitSlop={8} style={styles.addBtn}>
|
||||
<Plus size={14} color={colors.primary} />
|
||||
<Plus size={14} color={c.primary} />
|
||||
{!!addLabel && <Text style={styles.addLabel}>{addLabel}</Text>}
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -52,59 +57,63 @@ interface FieldRowProps {
|
||||
}
|
||||
|
||||
export function FieldRow({ onRemove, children }: FieldRowProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 1 }}>{children}</View>
|
||||
{onRemove && (
|
||||
<Pressable onPress={onRemove} hitSlop={8} style={styles.removeBtn}>
|
||||
<X size={14} color={colors.textMuted} />
|
||||
<X size={14} color={c.textMuted} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
block: {
|
||||
marginHorizontal: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
paddingLeft: spacing.md,
|
||||
paddingRight: spacing.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
borderLeftWidth: 3,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.card,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
title: { ...typography.bodySemibold, color: colors.text },
|
||||
addBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: colors.primaryBg,
|
||||
},
|
||||
addLabel: { ...typography.caption, color: colors.primary },
|
||||
body: { gap: spacing.sm },
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
removeBtn: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: colors.surface,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
block: {
|
||||
marginHorizontal: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
paddingLeft: spacing.md,
|
||||
paddingRight: spacing.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
borderLeftWidth: 3,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: c.card,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
title: { ...typography.bodySemibold, color: c.text },
|
||||
addBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: c.primaryBg,
|
||||
},
|
||||
addLabel: { ...typography.caption, color: c.primary },
|
||||
body: { gap: spacing.sm },
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
removeBtn: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: c.surface,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import Constants from 'expo-constants';
|
||||
import { ExternalLink } 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';
|
||||
|
||||
const APP_VERSION = Constants.expoConfig?.version ?? '0.0.0';
|
||||
@@ -18,6 +19,8 @@ const DEBUG_CATEGORIES = [
|
||||
];
|
||||
|
||||
export function AboutDataSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const senderFavicons = useSettingsStore((s) => s.senderFavicons);
|
||||
const setSenderFaviconsStore = useSettingsStore((s) => s.setSenderFavicons);
|
||||
const hydrated = useSettingsStore((s) => s.hydrated);
|
||||
@@ -64,7 +67,7 @@ export function AboutDataSettings() {
|
||||
onPress={() => Linking.openURL('https://github.com/bulwarkmail/webmail')}
|
||||
>
|
||||
<Text style={styles.ghText}>GitHub</Text>
|
||||
<ExternalLink size={12} color={colors.mutedForeground} />
|
||||
<ExternalLink size={12} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -122,14 +125,15 @@ export function AboutDataSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.xxxl },
|
||||
aboutBox: {
|
||||
padding: spacing.xl,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.card,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.card,
|
||||
},
|
||||
aboutRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -140,30 +144,31 @@ const styles = StyleSheet.create({
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: radius.md,
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: c.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
logoText: { fontSize: 24, fontWeight: '700', color: colors.primaryForeground },
|
||||
aboutTitle: { ...typography.bodyMedium, color: colors.text },
|
||||
aboutVersion: { ...typography.caption, color: colors.mutedForeground, marginTop: 2 },
|
||||
aboutCommit: { color: colors.mutedForeground, opacity: 0.6 },
|
||||
logoText: { fontSize: 24, fontWeight: '700', color: c.primaryForeground },
|
||||
aboutTitle: { ...typography.bodyMedium, color: c.text },
|
||||
aboutVersion: { ...typography.caption, color: c.mutedForeground, marginTop: 2 },
|
||||
aboutCommit: { color: c.mutedForeground, opacity: 0.6 },
|
||||
ghLink: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
ghText: { ...typography.caption, color: colors.mutedForeground },
|
||||
ghText: { ...typography.caption, color: c.mutedForeground },
|
||||
debugCategoriesBox: {
|
||||
marginLeft: spacing.lg,
|
||||
paddingLeft: spacing.lg,
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: colors.muted,
|
||||
borderLeftColor: c.muted,
|
||||
gap: 4,
|
||||
},
|
||||
debugCategoriesHint: {
|
||||
...typography.caption,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { Key, Smartphone, Lock, Eye, EyeOff, Shield, Monitor, Trash2 } from 'luc
|
||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||
import Input from '../Input';
|
||||
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 Session {
|
||||
id: string;
|
||||
@@ -19,6 +20,8 @@ const MOCK_SESSIONS: Session[] = [
|
||||
];
|
||||
|
||||
export function AccountSecuritySettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [currentPwd, setCurrentPwd] = useState('');
|
||||
const [newPwd, setNewPwd] = useState('');
|
||||
const [confirmPwd, setConfirmPwd] = useState('');
|
||||
@@ -42,9 +45,9 @@ export function AccountSecuritySettings() {
|
||||
/>
|
||||
<Pressable style={styles.eyeBtn} onPress={() => setShowCurrent((v) => !v)}>
|
||||
{showCurrent ? (
|
||||
<EyeOff size={16} color={colors.mutedForeground} />
|
||||
<EyeOff size={16} color={c.mutedForeground} />
|
||||
) : (
|
||||
<Eye size={16} color={colors.mutedForeground} />
|
||||
<Eye size={16} color={c.mutedForeground} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -61,9 +64,9 @@ export function AccountSecuritySettings() {
|
||||
/>
|
||||
<Pressable style={styles.eyeBtn} onPress={() => setShowNew((v) => !v)}>
|
||||
{showNew ? (
|
||||
<EyeOff size={16} color={colors.mutedForeground} />
|
||||
<EyeOff size={16} color={c.mutedForeground} />
|
||||
) : (
|
||||
<Eye size={16} color={colors.mutedForeground} />
|
||||
<Eye size={16} color={c.mutedForeground} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -79,7 +82,7 @@ export function AccountSecuritySettings() {
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Button size="sm" icon={<Key size={14} color={colors.primaryForeground} />}>
|
||||
<Button size="sm" icon={<Key size={14} color={c.primaryForeground} />}>
|
||||
Change Password
|
||||
</Button>
|
||||
</View>
|
||||
@@ -93,7 +96,7 @@ export function AccountSecuritySettings() {
|
||||
|
||||
{twoFactor && (
|
||||
<View style={styles.infoBox}>
|
||||
<Smartphone size={16} color={colors.primary} />
|
||||
<Smartphone size={16} color={c.primary} />
|
||||
<Text style={styles.infoText}>
|
||||
Scan the QR code in your authenticator app to finish setup.
|
||||
</Text>
|
||||
@@ -104,7 +107,7 @@ export function AccountSecuritySettings() {
|
||||
<SettingsSection title="App Passwords" description="Generate passwords for other mail clients.">
|
||||
{appPwds.length === 0 ? (
|
||||
<View style={styles.empty}>
|
||||
<Lock size={24} color={colors.mutedForeground} />
|
||||
<Lock size={24} color={c.mutedForeground} />
|
||||
<Text style={styles.emptyText}>No app passwords yet.</Text>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -119,7 +122,7 @@ export function AccountSecuritySettings() {
|
||||
{MOCK_SESSIONS.map((s) => (
|
||||
<View key={s.id} style={styles.sessionRow}>
|
||||
<View style={styles.sessionIcon}>
|
||||
<Monitor size={18} color={colors.text} />
|
||||
<Monitor size={18} color={c.text} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.sessionDevice}>{s.device}</Text>
|
||||
@@ -129,7 +132,7 @@ export function AccountSecuritySettings() {
|
||||
</View>
|
||||
{!s.current && (
|
||||
<Pressable style={styles.trashBtn}>
|
||||
<Trash2 size={16} color={colors.error} />
|
||||
<Trash2 size={16} color={c.error} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.densityPreview}>
|
||||
@@ -46,7 +49,7 @@ function DensityPreview({ density }: { density: Density }) {
|
||||
<Text
|
||||
style={[
|
||||
styles.densitySender,
|
||||
row.unread ? { color: colors.text, fontWeight: '600' } : { color: colors.mutedForeground },
|
||||
row.unread ? { color: c.text, fontWeight: '600' } : { color: c.mutedForeground },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@@ -57,7 +60,7 @@ function DensityPreview({ density }: { density: Density }) {
|
||||
<Text
|
||||
style={[
|
||||
styles.densitySubject,
|
||||
row.unread ? { color: colors.text, fontWeight: '500' } : { color: 'rgba(250,250,250,0.8)' },
|
||||
row.unread ? { color: c.text, fontWeight: '500' } : { color: 'rgba(250,250,250,0.8)' },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<View key={kw} style={styles.chip}>
|
||||
<Text style={styles.chipText}>{kw}</Text>
|
||||
<Pressable onPress={() => removeKeyword(kw)} hitSlop={6}>
|
||||
<X size={12} color={colors.textSecondary} />
|
||||
<X size={12} color={c.textSecondary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
@@ -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={<Plus size={14} color={colors.primaryForeground} />}
|
||||
icon={<Plus size={14} color={c.primaryForeground} />}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon={<Download size={14} color={colors.text} />}
|
||||
icon={<Download size={14} color={c.text} />}
|
||||
onPress={() => { void handleExport(); }}
|
||||
disabled={exportable.length === 0 || exporting}
|
||||
loading={exporting}
|
||||
@@ -80,7 +82,7 @@ export function ContactsSettings() {
|
||||
label="Import contacts"
|
||||
description="Importing vCard files is currently available on the web client only."
|
||||
>
|
||||
<Text style={[typography.caption, { color: colors.textMuted }]}>Web only</Text>
|
||||
<Text style={[typography.caption, { color: c.textMuted }]}>Web only</Text>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
||||
@@ -5,10 +5,13 @@ import {
|
||||
import { ChevronRight, X, Plus, Trash2 } from 'lucide-react-native';
|
||||
import { SettingsSection, SettingItem, RadioGroup, 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, type ExternalContentPolicy } from '../../stores/settings-store';
|
||||
|
||||
export function ContentSendersSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const externalContentPolicy = useSettingsStore((s) => s.externalContentPolicy);
|
||||
const setExternalContentPolicy = useSettingsStore((s) => s.setExternalContentPolicy);
|
||||
const emailAlwaysLightMode = useSettingsStore((s) => s.emailAlwaysLightMode);
|
||||
@@ -64,7 +67,7 @@ export function ContentSendersSettings() {
|
||||
style={({ pressed }) => [styles.trustedButton, pressed && styles.trustedButtonPressed]}
|
||||
>
|
||||
<Text style={styles.trustedButtonText}>{trustedLabel}</Text>
|
||||
<ChevronRight size={14} color={colors.textMuted} />
|
||||
<ChevronRight size={14} color={c.textMuted} />
|
||||
</Pressable>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
@@ -80,7 +83,7 @@ export function ContentSendersSettings() {
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Trusted senders</Text>
|
||||
<Pressable onPress={() => setModalOpen(false)} hitSlop={6}>
|
||||
<X size={18} color={colors.textSecondary} />
|
||||
<X size={18} color={c.textSecondary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -89,7 +92,7 @@ export function ContentSendersSettings() {
|
||||
value={newSender}
|
||||
onChangeText={setNewSender}
|
||||
placeholder="sender@example.com"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
placeholderTextColor={c.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="email-address"
|
||||
@@ -111,7 +114,7 @@ export function ContentSendersSettings() {
|
||||
}
|
||||
}}
|
||||
disabled={!newSender.trim()}
|
||||
icon={<Plus size={14} color={colors.primaryForeground} />}
|
||||
icon={<Plus size={14} color={c.primaryForeground} />}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
@@ -133,7 +136,7 @@ export function ContentSendersSettings() {
|
||||
hitSlop={6}
|
||||
style={({ pressed }) => [styles.removeBtn, pressed && styles.removeBtnPressed]}
|
||||
>
|
||||
<Trash2 size={16} color={colors.error} />
|
||||
<Trash2 size={16} color={c.error} />
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 <Folder size={sz} color={colored ? '#60a5fa' : colors.mutedForeground} />;
|
||||
if (file.isFolder) return <Folder size={sz} color={colored ? '#60a5fa' : c.mutedForeground} />;
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'jpg': case 'png': case 'gif':
|
||||
return <ImageIcon size={sz} color={colored ? '#4ade80' : colors.mutedForeground} />;
|
||||
return <ImageIcon size={sz} color={colored ? '#4ade80' : c.mutedForeground} />;
|
||||
case 'mp3': case 'wav':
|
||||
return <FileAudio size={sz} color={colored ? '#a78bfa' : colors.mutedForeground} />;
|
||||
return <FileAudio size={sz} color={colored ? '#a78bfa' : c.mutedForeground} />;
|
||||
case 'pdf':
|
||||
return <FileText size={sz} color={colored ? '#f87171' : colors.mutedForeground} />;
|
||||
return <FileText size={sz} color={colored ? '#f87171' : c.mutedForeground} />;
|
||||
case 'md': case 'json': case 'js': case 'ts':
|
||||
return <FileCode size={sz} color={colored ? '#fbbf24' : colors.mutedForeground} />;
|
||||
return <FileCode size={sz} color={colored ? '#fbbf24' : c.mutedForeground} />;
|
||||
default:
|
||||
return <File size={sz} color={colors.mutedForeground} />;
|
||||
return <File size={sz} color={c.mutedForeground} />;
|
||||
}
|
||||
}
|
||||
|
||||
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<FilesPrefs>(DEFAULTS);
|
||||
const update = (patch: Partial<FilesPrefs>) => setPrefs((p) => ({ ...p, ...patch }));
|
||||
|
||||
@@ -188,7 +193,7 @@ export function FilesSettings() {
|
||||
<SettingItem label="Show icons" description="Display icons for files and folders.">
|
||||
<ToggleSwitch checked={prefs.showIcons} onChange={(v) => update({ showIcons: v })} />
|
||||
</SettingItem>
|
||||
<SettingItem label="Colored icons" description="Use file-type-specific colors.">
|
||||
<SettingItem label="Colored icons" description="Use file-type-specific c.">
|
||||
<ToggleSwitch
|
||||
checked={prefs.coloredIcons}
|
||||
onChange={(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' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<Rule[]>(SAMPLE_RULES);
|
||||
const [expandedView, setExpandedView] = useState(false);
|
||||
const [isOpaque] = useState(false);
|
||||
@@ -45,7 +48,7 @@ export function FilterSettings() {
|
||||
<Text style={styles.linkText}>Open Sieve Editor</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.resetLink}>
|
||||
<RotateCcw size={12} color={colors.error} />
|
||||
<RotateCcw size={12} color={c.error} />
|
||||
<Text style={styles.resetText}>Reset to visual</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -55,7 +58,7 @@ export function FilterSettings() {
|
||||
|
||||
{rules.length === 0 && !isOpaque && (
|
||||
<View style={styles.emptyState}>
|
||||
<Filter size={40} color={colors.mutedForeground} style={{ opacity: 0.4 }} />
|
||||
<Filter size={40} color={c.mutedForeground} style={{ opacity: 0.4 }} />
|
||||
<Text style={styles.emptyText}>No rules yet.</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -65,7 +68,7 @@ export function FilterSettings() {
|
||||
{rules.map((rule) => (
|
||||
<View key={rule.id} style={[styles.ruleRow, !rule.enabled && styles.ruleDisabled]}>
|
||||
<View style={styles.grip}>
|
||||
<GripVertical size={16} color={colors.mutedForeground} />
|
||||
<GripVertical size={16} color={c.mutedForeground} />
|
||||
</View>
|
||||
|
||||
<ToggleSwitch checked={rule.enabled} onChange={() => toggle(rule.id)} />
|
||||
@@ -88,7 +91,7 @@ export function FilterSettings() {
|
||||
</View>
|
||||
) : (
|
||||
<Pressable style={styles.iconBtn} onPress={() => setDeleteId(rule.id)}>
|
||||
<X size={16} color={colors.mutedForeground} />
|
||||
<X size={16} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
@@ -100,11 +103,11 @@ export function FilterSettings() {
|
||||
<View style={styles.footer}>
|
||||
<View style={{ flexDirection: 'row', gap: spacing.sm }}>
|
||||
{!isOpaque && (
|
||||
<Button variant="outline" size="sm" icon={<Plus size={14} color={colors.text} />}>
|
||||
<Button variant="outline" size="sm" icon={<Plus size={14} color={c.text} />}>
|
||||
Add Rule
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" icon={<Code size={14} color={colors.text} />}>
|
||||
<Button variant="outline" size="sm" icon={<Code size={14} color={c.text} />}>
|
||||
Raw Editor
|
||||
</Button>
|
||||
</View>
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<ActivityIndicator size="small" color={c.primary} />
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
@@ -58,7 +61,7 @@ export function FolderSettings() {
|
||||
<View style={styles.folderLeft}>
|
||||
<Icon
|
||||
size={16}
|
||||
color={mb.role ? colors.primary : colors.mutedForeground}
|
||||
color={mb.role ? c.primary : c.mutedForeground}
|
||||
/>
|
||||
<Text style={styles.folderName} numberOfLines={1}>{mb.name}</Text>
|
||||
{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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
>
|
||||
<View style={styles.row}>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<ActivityIndicator size="small" color={c.primary} />
|
||||
) : (
|
||||
<Text style={styles.count}>{countLabel}</Text>
|
||||
)}
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<View key={kw.id} style={styles.kwRow}>
|
||||
<View style={[styles.kwDot, { backgroundColor: palette.dot }]} />
|
||||
@@ -61,10 +67,10 @@ export function KeywordSettings() {
|
||||
<Text style={styles.kwId}>$label:{kw.id}</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 2 }}>
|
||||
<Pressable style={styles.iconBtn} onPress={() => setEditingId(kw.id)}>
|
||||
<Pencil size={14} color={colors.mutedForeground} />
|
||||
<Pencil size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn} onPress={() => deleteKeyword(kw.id)}>
|
||||
<Trash2 size={14} color={colors.mutedForeground} />
|
||||
<Trash2 size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -82,11 +88,11 @@ export function KeywordSettings() {
|
||||
{!isAdding && editingId === null && (
|
||||
<View style={styles.bottomActions}>
|
||||
<Pressable style={styles.outlineBtn} onPress={() => setIsAdding(true)}>
|
||||
<Plus size={14} color={colors.mutedForeground} />
|
||||
<Plus size={14} color={c.mutedForeground} />
|
||||
<Text style={styles.outlineBtnText}>Add keyword</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.outlineBtn} onPress={resetDefaults}>
|
||||
<RotateCcw size={14} color={colors.mutedForeground} />
|
||||
<RotateCcw size={14} color={c.mutedForeground} />
|
||||
<Text style={styles.outlineBtnText}>Reset defaults</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -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<keyof typeof colors.tags>(initial?.color ?? 'blue');
|
||||
const [color, setColor] = useState<keyof typeof DARK_COLORS.tags>(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
|
||||
<Text style={styles.formLabel}>Color</Text>
|
||||
<View style={styles.palette}>
|
||||
{PALETTE_KEYS.map((key) => {
|
||||
const p = colors.tags[key];
|
||||
const p = c.tags[key];
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
@@ -160,7 +168,7 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
|
||||
<View style={styles.formActions}>
|
||||
<Pressable style={styles.cancelFormBtn} onPress={onCancel}>
|
||||
<X size={14} color={colors.text} />
|
||||
<X size={14} color={c.text} />
|
||||
<Text style={styles.cancelFormText}>Cancel</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
@@ -168,7 +176,7 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
onPress={handleSave}
|
||||
disabled={!isValid}
|
||||
>
|
||||
<Check size={14} color={colors.primaryForeground} />
|
||||
<Check size={14} color={c.primaryForeground} />
|
||||
<Text style={styles.saveBtnText}>{initial ? 'Save' : 'Add'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -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' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
{sublabel ? <Text style={styles.sublabel}>{sublabel}</Text> : null}
|
||||
</View>
|
||||
{active ? <Check size={16} color={colors.primary} /> : null}
|
||||
{active ? <Check size={16} color={c.primary} /> : null}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<SettingsSection title="Layout" description="Tune the email list interactions for mobile.">
|
||||
<View style={{ gap: spacing.sm }}>
|
||||
<View style={styles.row}>
|
||||
<ArrowRight size={14} color={colors.mutedForeground} />
|
||||
<ArrowRight size={14} color={c.mutedForeground} />
|
||||
<Text style={styles.rowLabel}>Swipe right (left → right)</Text>
|
||||
</View>
|
||||
<Text style={styles.rowDescription}>
|
||||
@@ -43,7 +46,7 @@ export function LayoutSettings() {
|
||||
|
||||
<View style={{ gap: spacing.sm, marginTop: spacing.lg }}>
|
||||
<View style={styles.row}>
|
||||
<ArrowLeft size={14} color={colors.mutedForeground} />
|
||||
<ArrowLeft size={14} color={c.mutedForeground} />
|
||||
<Text style={styles.rowLabel}>Swipe left (right → left)</Text>
|
||||
</View>
|
||||
<Text style={styles.rowDescription}>
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<View style={styles.statusRow}>
|
||||
{pushStatus.kind === 'busy' && (
|
||||
<>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<ActivityIndicator size="small" color={c.primary} />
|
||||
<Text style={styles.statusText}>{pushStatus.message}</Text>
|
||||
</>
|
||||
)}
|
||||
{pushStatus.kind === 'enabled' && (
|
||||
<>
|
||||
<CheckCircle2 size={14} color={colors.success} />
|
||||
<Text style={[styles.statusText, { color: colors.success }]}>Active</Text>
|
||||
<CheckCircle2 size={14} color={c.success} />
|
||||
<Text style={[styles.statusText, { color: c.success }]}>Active</Text>
|
||||
</>
|
||||
)}
|
||||
{pushStatus.kind === 'error' && (
|
||||
<>
|
||||
<XCircle size={14} color={colors.error} />
|
||||
<Text style={[styles.statusText, { color: colors.error }]}>
|
||||
<XCircle size={14} color={c.error} />
|
||||
<Text style={[styles.statusText, { color: c.error }]}>
|
||||
{pushStatus.message}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{pushStatus.kind === 'idle' && (
|
||||
<Text style={[styles.statusText, { color: colors.mutedForeground }]}>
|
||||
<Text style={[styles.statusText, { color: c.mutedForeground }]}>
|
||||
Not configured
|
||||
</Text>
|
||||
)}
|
||||
@@ -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() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
icon={<Volume2 size={16} color={colors.text} />}
|
||||
icon={<Volume2 size={16} color={c.text} />}
|
||||
/>
|
||||
<Select
|
||||
value={sound}
|
||||
@@ -229,12 +232,13 @@ export function NotificationSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.xxxl },
|
||||
row: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
||||
pushCard: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.md,
|
||||
padding: spacing.lg,
|
||||
gap: spacing.sm,
|
||||
@@ -247,18 +251,18 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
fieldLabel: {
|
||||
...typography.bodyMedium,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
fieldDescription: {
|
||||
...typography.caption,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
},
|
||||
urlInput: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
backgroundColor: colors.surface,
|
||||
color: c.text,
|
||||
backgroundColor: c.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 8,
|
||||
@@ -266,6 +270,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 4,
|
||||
},
|
||||
statusRow: { flexDirection: 'row', alignItems: 'center', gap: 6, flexShrink: 1 },
|
||||
statusText: { ...typography.caption, color: colors.text },
|
||||
statusText: { ...typography.caption, color: c.text },
|
||||
actions: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm, marginTop: spacing.md },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||
import { Upload, Puzzle, AlertTriangle, Lock, Trash2, ChevronDown, ChevronRight } 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';
|
||||
|
||||
interface Plugin {
|
||||
id: string;
|
||||
@@ -20,14 +21,16 @@ interface Plugin {
|
||||
}
|
||||
|
||||
const STATUS_STYLE: Record<Plugin['status'], { bg: string; fg: string }> = {
|
||||
installed: { bg: colors.muted, fg: colors.mutedForeground },
|
||||
installed: { bg: c.muted, fg: c.mutedForeground },
|
||||
enabled: { bg: 'rgba(59, 130, 246, 0.15)', fg: '#60a5fa' },
|
||||
running: { bg: 'rgba(34, 197, 94, 0.15)', fg: '#4ade80' },
|
||||
disabled: { bg: colors.muted, fg: colors.mutedForeground },
|
||||
disabled: { bg: c.muted, fg: c.mutedForeground },
|
||||
error: { bg: 'rgba(239, 68, 68, 0.15)', fg: '#f87171' },
|
||||
};
|
||||
|
||||
export function PluginsSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [plugins, setPlugins] = useState<Plugin[]>([]);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
@@ -52,7 +55,7 @@ export function PluginsSettings() {
|
||||
>
|
||||
{plugins.length === 0 ? (
|
||||
<View style={styles.empty}>
|
||||
<Puzzle size={48} color={colors.mutedForeground} style={{ opacity: 0.3 }} />
|
||||
<Puzzle size={48} color={c.mutedForeground} style={{ opacity: 0.3 }} />
|
||||
<Text style={styles.emptyTitle}>No plugins installed</Text>
|
||||
<Text style={styles.emptyDesc}>Upload a plugin .zip file to get started.</Text>
|
||||
</View>
|
||||
@@ -66,7 +69,7 @@ export function PluginsSettings() {
|
||||
key={plugin.id}
|
||||
style={[
|
||||
styles.card,
|
||||
plugin.status === 'error' && { borderColor: colors.errorBorder },
|
||||
plugin.status === 'error' && { borderColor: c.errorBorder },
|
||||
]}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
@@ -107,7 +110,7 @@ export function PluginsSettings() {
|
||||
|
||||
{plugin.error && (
|
||||
<View style={styles.errorBox}>
|
||||
<AlertTriangle size={14} color={colors.error} />
|
||||
<AlertTriangle size={14} color={c.error} />
|
||||
<Text style={styles.errorText}>{plugin.error}</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -145,7 +148,7 @@ export function PluginsSettings() {
|
||||
)}
|
||||
|
||||
<SettingItem label="Upload Plugin" description="Install a new plugin from a .zip file.">
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={colors.text} />}>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={c.text} />}>
|
||||
Upload .zip
|
||||
</Button>
|
||||
</SettingItem>
|
||||
@@ -153,18 +156,19 @@ export function PluginsSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xxxl,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
emptyTitle: { ...typography.body, color: colors.mutedForeground },
|
||||
emptyDesc: { ...typography.caption, color: colors.mutedForeground },
|
||||
emptyTitle: { ...typography.body, color: c.mutedForeground },
|
||||
emptyDesc: { ...typography.caption, color: c.mutedForeground },
|
||||
card: {
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -173,7 +177,7 @@ const styles = StyleSheet.create({
|
||||
padding: spacing.md,
|
||||
},
|
||||
headerRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, flexWrap: 'wrap' },
|
||||
pluginName: { ...typography.bodyMedium, color: colors.text },
|
||||
pluginName: { ...typography.bodyMedium, color: c.text },
|
||||
statusPill: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: radius.full },
|
||||
statusText: { fontSize: 10, fontWeight: '500' },
|
||||
forcedPill: {
|
||||
@@ -187,38 +191,39 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
forcedText: { fontSize: 10, fontWeight: '500', color: '#fbbf24' },
|
||||
metaRow: { flexDirection: 'row', gap: spacing.sm, marginTop: 2 },
|
||||
meta: { ...typography.caption, color: colors.mutedForeground },
|
||||
metaDot: { ...typography.caption, color: colors.mutedForeground, opacity: 0.6 },
|
||||
meta: { ...typography.caption, color: c.mutedForeground },
|
||||
metaDot: { ...typography.caption, color: c.mutedForeground, opacity: 0.6 },
|
||||
cardBody: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
borderTopColor: c.border,
|
||||
padding: spacing.md,
|
||||
gap: spacing.md,
|
||||
},
|
||||
description: { ...typography.caption, color: colors.mutedForeground },
|
||||
description: { ...typography.caption, color: c.mutedForeground },
|
||||
errorBox: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
padding: spacing.sm,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.errorBg,
|
||||
backgroundColor: c.errorBg,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
errorText: { ...typography.caption, color: colors.error, flex: 1 },
|
||||
sectionLabel: { ...typography.captionMedium, color: colors.text, marginBottom: 4 },
|
||||
errorText: { ...typography.caption, color: c.error, flex: 1 },
|
||||
sectionLabel: { ...typography.captionMedium, color: c.text, marginBottom: 4 },
|
||||
permWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 4 },
|
||||
permPill: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: radius.xs,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
permText: { fontSize: 10, color: colors.mutedForeground },
|
||||
permText: { fontSize: 10, color: c.mutedForeground },
|
||||
uninstallRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
paddingTop: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
borderTopColor: c.border,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, Pressable, TextInput } from 'react-native';
|
||||
import { AlertTriangle, FolderSync, Mail, X } from 'lucide-react-native';
|
||||
import { SettingsSection, SettingItem, Select, 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 ExternalContentPolicy } from '../../stores/settings-store';
|
||||
|
||||
type MailLayout = 'split' | 'focus';
|
||||
@@ -14,6 +15,8 @@ type AttachmentPosition = 'beside-sender' | 'below-header';
|
||||
const DEFAULT_KEYWORDS = ['attached', 'attachment', 'enclosed', 'attaching'];
|
||||
|
||||
function MailLayoutPreview({ value }: { value: MailLayout }) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const isSplit = value === 'split';
|
||||
return (
|
||||
<View style={styles.layoutPreview}>
|
||||
@@ -66,6 +69,8 @@ function MailLayoutPreview({ value }: { value: MailLayout }) {
|
||||
}
|
||||
|
||||
export function ReadingSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const externalContentPolicy = useSettingsStore((s) => s.externalContentPolicy);
|
||||
const setExternalContentPolicyStore = useSettingsStore((s) => s.setExternalContentPolicy);
|
||||
const trustedSenders = useSettingsStore((s) => s.trustedSenders);
|
||||
@@ -124,7 +129,7 @@ export function ReadingSettings() {
|
||||
/>
|
||||
{deleteAction === 'permanent' && (
|
||||
<View style={styles.warning}>
|
||||
<AlertTriangle size={14} color={colors.error} />
|
||||
<AlertTriangle size={14} color={c.error} />
|
||||
<Text style={styles.warningText}>Permanent deletion cannot be undone.</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -144,7 +149,7 @@ export function ReadingSettings() {
|
||||
/>
|
||||
{archiveMode !== 'single' && (
|
||||
<Pressable style={styles.inlineBtn}>
|
||||
<FolderSync size={14} color={colors.text} />
|
||||
<FolderSync size={14} color={c.text} />
|
||||
<Text style={styles.inlineBtnText}>Reorganize existing archive</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -206,7 +211,7 @@ export function ReadingSettings() {
|
||||
}
|
||||
hitSlop={6}
|
||||
>
|
||||
<X size={12} color={colors.mutedForeground} />
|
||||
<X size={12} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
@@ -216,7 +221,7 @@ export function ReadingSettings() {
|
||||
value={newKeyword}
|
||||
onChangeText={setNewKeyword}
|
||||
placeholder="Add a keyword"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={styles.keywordInput}
|
||||
onSubmitEditing={() => {
|
||||
const t = newKeyword.trim().toLowerCase();
|
||||
@@ -286,7 +291,8 @@ export function ReadingSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
group: {},
|
||||
warning: {
|
||||
flexDirection: 'row',
|
||||
@@ -298,12 +304,12 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
warningText: {
|
||||
...typography.caption,
|
||||
color: colors.error,
|
||||
color: c.error,
|
||||
flex: 1,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: colors.border,
|
||||
backgroundColor: c.border,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
inlineBtn: {
|
||||
@@ -312,23 +318,23 @@ const styles = StyleSheet.create({
|
||||
gap: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 6,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
borderRadius: radius.sm,
|
||||
alignSelf: 'flex-start',
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
inlineBtnText: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
subLabel: {
|
||||
...typography.bodyMedium,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
subDesc: {
|
||||
...typography.caption,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
marginTop: 2,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
@@ -344,11 +350,11 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 2,
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
chipText: {
|
||||
...typography.caption,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
addKeywordRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -360,37 +366,37 @@ const styles = StyleSheet.create({
|
||||
height: 32,
|
||||
paddingHorizontal: spacing.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.background,
|
||||
color: colors.text,
|
||||
backgroundColor: c.background,
|
||||
color: c.text,
|
||||
...typography.body,
|
||||
},
|
||||
addKeywordBtn: {
|
||||
paddingHorizontal: spacing.md,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
addKeywordText: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
layoutPreview: {
|
||||
marginTop: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.background,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.background,
|
||||
padding: spacing.md,
|
||||
},
|
||||
layoutTitle: {
|
||||
...typography.bodyMedium,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
layoutDesc: {
|
||||
...typography.caption,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
marginTop: 2,
|
||||
},
|
||||
layoutFrame: {
|
||||
@@ -399,27 +405,27 @@ const styles = StyleSheet.create({
|
||||
marginTop: spacing.md,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(24,24,27,0.4)',
|
||||
},
|
||||
layoutRail: {
|
||||
width: 32,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: colors.border,
|
||||
borderRightColor: c.border,
|
||||
backgroundColor: 'rgba(24,24,27,0.6)',
|
||||
},
|
||||
layoutList: {
|
||||
width: 96,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: colors.border,
|
||||
backgroundColor: colors.background,
|
||||
borderRightColor: c.border,
|
||||
backgroundColor: c.background,
|
||||
},
|
||||
layoutRow: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
},
|
||||
layoutRowSel: {
|
||||
backgroundColor: 'rgba(59,130,246,0.1)',
|
||||
@@ -427,16 +433,16 @@ const styles = StyleSheet.create({
|
||||
layoutRowName: {
|
||||
fontSize: 9,
|
||||
fontWeight: '500',
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
layoutRowSub: {
|
||||
fontSize: 9,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
},
|
||||
layoutReader: {
|
||||
flex: 1,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.background,
|
||||
backgroundColor: c.background,
|
||||
},
|
||||
layoutBar: {
|
||||
height: 6,
|
||||
@@ -455,14 +461,14 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
layoutFocusText: {
|
||||
fontSize: 9,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
trustedList: {
|
||||
marginTop: spacing.sm,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.muted,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
trustedRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -472,7 +478,8 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
},
|
||||
trustedEmail: { ...typography.caption, color: colors.text, flex: 1 },
|
||||
trustedEmail: { ...typography.caption, color: c.text, flex: 1 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
} 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';
|
||||
|
||||
interface SidebarApp {
|
||||
id: string;
|
||||
@@ -17,6 +18,8 @@ interface SidebarApp {
|
||||
}
|
||||
|
||||
export function SidebarAppsSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [keepLoaded, setKeepLoaded] = useState(false);
|
||||
const [apps, setApps] = useState<SidebarApp[]>([]);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
@@ -63,9 +66,9 @@ export function SidebarAppsSettings() {
|
||||
}
|
||||
return (
|
||||
<View key={app.id} style={styles.appRow}>
|
||||
<GripVertical size={16} color={colors.mutedForeground} style={{ opacity: 0.5 }} />
|
||||
<GripVertical size={16} color={c.mutedForeground} style={{ opacity: 0.5 }} />
|
||||
<View style={styles.appIcon}>
|
||||
<Globe size={16} color={colors.text} />
|
||||
<Globe size={16} color={c.text} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.appName} numberOfLines={1}>{app.name}</Text>
|
||||
@@ -77,16 +80,16 @@ export function SidebarAppsSettings() {
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.modeBadgeText,
|
||||
{ color: app.openMode === 'inline' ? '#60a5fa' : colors.mutedForeground },
|
||||
{ color: app.openMode === 'inline' ? '#60a5fa' : c.mutedForeground },
|
||||
]}>
|
||||
{app.openMode === 'inline' ? 'Inline' : 'Tab'}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable style={styles.iconBtn} onPress={() => setEditingId(app.id)}>
|
||||
<Pencil size={14} color={colors.mutedForeground} />
|
||||
<Pencil size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn} onPress={() => removeApp(app.id)}>
|
||||
<Trash2 size={14} color={colors.mutedForeground} />
|
||||
<Trash2 size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -101,7 +104,7 @@ export function SidebarAppsSettings() {
|
||||
|
||||
{!isAdding && editingId === null && (
|
||||
<Pressable style={styles.addBtn} onPress={() => setIsAdding(true)}>
|
||||
<Plus size={16} color={colors.text} />
|
||||
<Plus size={16} color={c.text} />
|
||||
<Text style={styles.addBtnText}>Add app</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -133,7 +136,7 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="App name"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={formStyles.input}
|
||||
/>
|
||||
</View>
|
||||
@@ -144,7 +147,7 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
value={url}
|
||||
onChangeText={setUrl}
|
||||
placeholder="https://example.com"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={formStyles.input}
|
||||
autoCapitalize="none"
|
||||
keyboardType="url"
|
||||
@@ -158,8 +161,8 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
onPress={() => setOpenMode('tab')}
|
||||
style={[formStyles.modeBtn, openMode === 'tab' && formStyles.modeBtnActive]}
|
||||
>
|
||||
<ExternalLink size={14} color={openMode === 'tab' ? colors.primary : colors.text} />
|
||||
<Text style={[formStyles.modeBtnText, openMode === 'tab' && { color: colors.primary }]}>
|
||||
<ExternalLink size={14} color={openMode === 'tab' ? c.primary : c.text} />
|
||||
<Text style={[formStyles.modeBtnText, openMode === 'tab' && { color: c.primary }]}>
|
||||
New tab
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -167,8 +170,8 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
onPress={() => setOpenMode('inline')}
|
||||
style={[formStyles.modeBtn, openMode === 'inline' && formStyles.modeBtnActive]}
|
||||
>
|
||||
<PanelRight size={14} color={openMode === 'inline' ? colors.primary : colors.text} />
|
||||
<Text style={[formStyles.modeBtnText, openMode === 'inline' && { color: colors.primary }]}>
|
||||
<PanelRight size={14} color={openMode === 'inline' ? c.primary : c.text} />
|
||||
<Text style={[formStyles.modeBtnText, openMode === 'inline' && { color: c.primary }]}>
|
||||
Inline
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -194,11 +197,12 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.xxxl },
|
||||
emptyText: {
|
||||
...typography.body,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
@@ -209,25 +213,25 @@ const styles = StyleSheet.create({
|
||||
padding: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
},
|
||||
appIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
appName: { ...typography.bodyMedium, color: colors.text },
|
||||
appUrl: { ...typography.caption, color: colors.mutedForeground, marginTop: 2 },
|
||||
appName: { ...typography.bodyMedium, color: c.text },
|
||||
appUrl: { ...typography.caption, color: c.mutedForeground, marginTop: 2 },
|
||||
modeBadge: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: radius.full,
|
||||
},
|
||||
modeBadgeInline: { backgroundColor: 'rgba(59, 130, 246, 0.1)' },
|
||||
modeBadgeTab: { backgroundColor: colors.muted },
|
||||
modeBadgeTab: { backgroundColor: c.muted },
|
||||
modeBadgeText: { fontSize: 10, fontWeight: '500' },
|
||||
iconBtn: {
|
||||
width: 28,
|
||||
@@ -244,10 +248,11 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
},
|
||||
addBtnText: { ...typography.body, color: colors.text },
|
||||
addBtnText: { ...typography.body, color: c.text },
|
||||
});
|
||||
}
|
||||
|
||||
const formStyles = StyleSheet.create({
|
||||
form: {
|
||||
@@ -255,19 +260,19 @@ const formStyles = StyleSheet.create({
|
||||
padding: spacing.lg,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.muted,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
label: { ...typography.captionMedium, color: colors.text },
|
||||
label: { ...typography.captionMedium, color: c.text },
|
||||
input: {
|
||||
marginTop: 4,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 8,
|
||||
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,
|
||||
},
|
||||
modeBtn: {
|
||||
@@ -278,11 +283,11 @@ const formStyles = StyleSheet.create({
|
||||
paddingVertical: 8,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
},
|
||||
modeBtnActive: {
|
||||
borderColor: colors.primary,
|
||||
borderColor: c.primary,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
modeBtnText: { ...typography.caption, color: colors.text },
|
||||
modeBtnText: { ...typography.caption, color: c.text },
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
} from 'lucide-react-native';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch, Select } 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 KeyRecord {
|
||||
id: string;
|
||||
@@ -31,6 +32,8 @@ const MOCK_KEYS: KeyRecord[] = [];
|
||||
const MOCK_CERTS: PublicCert[] = [];
|
||||
|
||||
export function SmimeSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [keys, setKeys] = useState<KeyRecord[]>(MOCK_KEYS);
|
||||
const [certs, setCerts] = useState<PublicCert[]>(MOCK_CERTS);
|
||||
const [bindings, setBindings] = useState<Record<string, string>>({});
|
||||
@@ -64,9 +67,9 @@ export function SmimeSettings() {
|
||||
<View style={styles.certLeft}>
|
||||
<View style={[styles.certIcon, expired ? styles.certIconError : styles.certIconOk]}>
|
||||
{expired ? (
|
||||
<ShieldAlert size={16} color={colors.error} />
|
||||
<ShieldAlert size={16} color={c.error} />
|
||||
) : (
|
||||
<ShieldCheck size={16} color={colors.primary} />
|
||||
<ShieldCheck size={16} color={c.primary} />
|
||||
)}
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
@@ -80,19 +83,19 @@ export function SmimeSettings() {
|
||||
<View style={styles.certActions}>
|
||||
<Pressable style={styles.iconBtn} onPress={() => toggleUnlock(k.id)}>
|
||||
{k.unlocked ? (
|
||||
<Unlock size={16} color={colors.success} />
|
||||
<Unlock size={16} color={c.success} />
|
||||
) : (
|
||||
<Lock size={16} color={colors.text} />
|
||||
<Lock size={16} color={c.text} />
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn}>
|
||||
<Eye size={16} color={colors.text} />
|
||||
<Eye size={16} color={c.text} />
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn}>
|
||||
<Download size={16} color={colors.text} />
|
||||
<Download size={16} color={c.text} />
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn} onPress={() => deleteKey(k.id)}>
|
||||
<Trash2 size={16} color={colors.error} />
|
||||
<Trash2 size={16} color={c.error} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -105,7 +108,7 @@ export function SmimeSettings() {
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-start', marginTop: spacing.sm }}>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={colors.text} />}>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={c.text} />}>
|
||||
Import PKCS#12
|
||||
</Button>
|
||||
</View>
|
||||
@@ -122,7 +125,7 @@ export function SmimeSettings() {
|
||||
<View key={c.id} style={styles.certRow}>
|
||||
<View style={styles.certLeft}>
|
||||
<View style={[styles.certIcon, styles.certIconMuted]}>
|
||||
<Users size={16} color={colors.mutedForeground} />
|
||||
<Users size={16} color={c.mutedForeground} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.certName} numberOfLines={1}>{c.email || c.subject}</Text>
|
||||
@@ -134,10 +137,10 @@ export function SmimeSettings() {
|
||||
</View>
|
||||
<View style={styles.certActions}>
|
||||
<Pressable style={styles.iconBtn}>
|
||||
<Eye size={16} color={colors.text} />
|
||||
<Eye size={16} color={c.text} />
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn} onPress={() => deleteCert(c.id)}>
|
||||
<Trash2 size={16} color={colors.error} />
|
||||
<Trash2 size={16} color={c.error} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -150,7 +153,7 @@ export function SmimeSettings() {
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-start', marginTop: spacing.sm }}>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={colors.text} />}>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={c.text} />}>
|
||||
Import Certificate
|
||||
</Button>
|
||||
</View>
|
||||
@@ -185,7 +188,8 @@ export function SmimeSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.xxxl },
|
||||
certRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -195,7 +199,7 @@ const styles = StyleSheet.create({
|
||||
padding: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
},
|
||||
certLeft: {
|
||||
flexDirection: 'row',
|
||||
@@ -211,12 +215,12 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
certIconOk: { backgroundColor: colors.primaryBg },
|
||||
certIconError: { backgroundColor: colors.errorBg },
|
||||
certIconMuted: { backgroundColor: colors.muted },
|
||||
certName: { ...typography.bodyMedium, color: colors.text },
|
||||
certMeta: { ...typography.caption, color: colors.mutedForeground, marginTop: 2 },
|
||||
expiredText: { color: colors.error },
|
||||
certIconOk: { backgroundColor: c.primaryBg },
|
||||
certIconError: { backgroundColor: c.errorBg },
|
||||
certIconMuted: { backgroundColor: c.muted },
|
||||
certName: { ...typography.bodyMedium, color: c.text },
|
||||
certMeta: { ...typography.caption, color: c.mutedForeground, marginTop: 2 },
|
||||
expiredText: { color: c.error },
|
||||
certActions: { flexDirection: 'row', gap: 2 },
|
||||
iconBtn: {
|
||||
width: 32,
|
||||
@@ -227,8 +231,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
empty: {
|
||||
...typography.body,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ import { View, Text, StyleSheet } from 'react-native';
|
||||
import { FileText, Download, Upload } from 'lucide-react-native';
|
||||
import { SettingsSection } from './settings-section';
|
||||
import Button from '../Button';
|
||||
import { colors, spacing, typography } from '../../theme/tokens';
|
||||
import { spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
|
||||
export function TemplateSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [templates] = useState<{ id: string; name: string }[]>([]);
|
||||
|
||||
return (
|
||||
@@ -13,7 +16,7 @@ export function TemplateSettings() {
|
||||
<SettingsSection title="Templates" description="Reusable email templates for common responses.">
|
||||
<View style={styles.row}>
|
||||
<View style={styles.countRow}>
|
||||
<FileText size={16} color={colors.mutedForeground} />
|
||||
<FileText size={16} color={c.mutedForeground} />
|
||||
<Text style={styles.count}>
|
||||
{templates.length === 1 ? '1 template' : `${templates.length} templates`}
|
||||
</Text>
|
||||
@@ -28,11 +31,11 @@ export function TemplateSettings() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={templates.length === 0}
|
||||
icon={<Download size={14} color={colors.text} />}
|
||||
icon={<Download size={14} color={c.text} />}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={colors.text} />}>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={c.text} />}>
|
||||
Import
|
||||
</Button>
|
||||
</View>
|
||||
@@ -41,7 +44,8 @@ export function TemplateSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.xxxl },
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
@@ -50,6 +54,7 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
countRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
||||
count: { ...typography.body, color: colors.mutedForeground },
|
||||
count: { ...typography.body, color: c.mutedForeground },
|
||||
actions: { flexDirection: 'row', gap: spacing.md, paddingVertical: spacing.md },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||
import { Upload, Check, Palette, Trash2 } from 'lucide-react-native';
|
||||
import { SettingsSection, SettingItem } 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 Theme {
|
||||
id: string | null;
|
||||
@@ -20,6 +21,8 @@ const BUILT_IN: Theme[] = [
|
||||
];
|
||||
|
||||
export function ThemesSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [themes] = useState<Theme[]>(BUILT_IN);
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
|
||||
@@ -40,12 +43,12 @@ export function ThemesSettings() {
|
||||
style={[styles.card, isActive && styles.cardActive]}
|
||||
>
|
||||
<View style={styles.preview}>
|
||||
<Palette size={32} color={colors.mutedForeground} style={{ opacity: 0.4 }} />
|
||||
<Palette size={32} color={c.mutedForeground} style={{ opacity: 0.4 }} />
|
||||
</View>
|
||||
<View style={{ width: '100%' }}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={styles.cardName} numberOfLines={1}>{theme.name}</Text>
|
||||
{isActive && <Check size={16} color={colors.primary} />}
|
||||
{isActive && <Check size={16} color={c.primary} />}
|
||||
</View>
|
||||
<Text style={styles.cardAuthor} numberOfLines={1}>{theme.author}</Text>
|
||||
{theme.variants && (
|
||||
@@ -67,7 +70,7 @@ export function ThemesSettings() {
|
||||
label="Upload Theme"
|
||||
description="Install a custom theme from a .zip file."
|
||||
>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={colors.text} />}>
|
||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={c.text} />}>
|
||||
Upload .zip
|
||||
</Button>
|
||||
</SettingItem>
|
||||
@@ -75,7 +78,8 @@ export function ThemesSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
@@ -87,19 +91,19 @@ const styles = StyleSheet.create({
|
||||
padding: spacing.md,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.card,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.card,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
cardActive: {
|
||||
borderColor: colors.primary,
|
||||
borderColor: c.primary,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.05)',
|
||||
},
|
||||
preview: {
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 10,
|
||||
borderRadius: radius.md,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -108,14 +112,15 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
cardName: { ...typography.bodyMedium, color: colors.text, flex: 1 },
|
||||
cardAuthor: { ...typography.caption, color: colors.mutedForeground },
|
||||
cardName: { ...typography.bodyMedium, color: c.text, flex: 1 },
|
||||
cardAuthor: { ...typography.caption, color: c.mutedForeground },
|
||||
variants: { flexDirection: 'row', gap: 4, marginTop: 4 },
|
||||
variantPill: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: radius.xs,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
variantText: { fontSize: 10, color: colors.mutedForeground },
|
||||
variantText: { fontSize: 10, color: c.mutedForeground },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, Linking, Alert } from '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 { useUpdatesStore } from '../../stores/updates-store';
|
||||
import type { InstallProgress } from '../../lib/install-update';
|
||||
|
||||
@@ -38,6 +39,8 @@ function installButtonLabel(
|
||||
}
|
||||
|
||||
export function UpdatesSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const hydrated = useUpdatesStore((s) => s.hydrated);
|
||||
const hydrate = useUpdatesStore((s) => s.hydrate);
|
||||
const checking = useUpdatesStore((s) => s.checking);
|
||||
@@ -158,19 +161,20 @@ export function UpdatesSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.xxxl },
|
||||
value: { ...typography.bodyMedium, color: colors.text },
|
||||
value: { ...typography.bodyMedium, color: c.text },
|
||||
notesBox: {
|
||||
padding: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.muted,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.muted,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
notesTitle: { ...typography.caption, color: colors.mutedForeground, textTransform: 'uppercase' },
|
||||
notesBody: { ...typography.body, color: colors.text, lineHeight: 20 },
|
||||
notesTitle: { ...typography.caption, color: c.mutedForeground, textTransform: 'uppercase' },
|
||||
notesBody: { ...typography.body, color: c.text, lineHeight: 20 },
|
||||
progressBox: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
@@ -179,12 +183,13 @@ const styles = StyleSheet.create({
|
||||
progressTrack: {
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressFill: {
|
||||
height: '100%',
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: c.primary,
|
||||
},
|
||||
progressLabel: { ...typography.caption, color: colors.mutedForeground },
|
||||
progressLabel: { ...typography.caption, color: c.mutedForeground },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { View, Text, StyleSheet, TextInput, Pressable, ActivityIndicator, Alert
|
||||
import { AlertTriangle, Eye, EyeOff } 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 { useVacationStore } from '../../stores/vacation-store';
|
||||
|
||||
function toJmapDate(input: string): string | null {
|
||||
@@ -23,6 +24,8 @@ function fromJmapDate(value: string | null): string {
|
||||
}
|
||||
|
||||
export function VacationSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const store = useVacationStore();
|
||||
|
||||
const [enabled, setEnabled] = useState(store.isEnabled);
|
||||
@@ -75,7 +78,7 @@ export function VacationSettings() {
|
||||
if (store.isLoading && !store.hasLoaded) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator color={colors.primary} />
|
||||
<ActivityIndicator color={c.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -84,7 +87,7 @@ export function VacationSettings() {
|
||||
return (
|
||||
<SettingsSection title="Vacation Responder" description="Automatically reply while you are away.">
|
||||
<View style={styles.unsupported}>
|
||||
<AlertTriangle size={16} color={colors.warning} />
|
||||
<AlertTriangle size={16} color={c.warning} />
|
||||
<Text style={styles.unsupportedText}>
|
||||
Your server does not advertise support for the JMAP vacation responder.
|
||||
</Text>
|
||||
@@ -114,7 +117,7 @@ export function VacationSettings() {
|
||||
value={fromDate}
|
||||
onChangeText={setFromDate}
|
||||
placeholder="YYYY-MM-DD HH:MM"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={styles.dateInput}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
@@ -124,7 +127,7 @@ export function VacationSettings() {
|
||||
value={toDate}
|
||||
onChangeText={setToDate}
|
||||
placeholder="YYYY-MM-DD HH:MM"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={styles.dateInput}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
@@ -137,7 +140,7 @@ export function VacationSettings() {
|
||||
value={subject}
|
||||
onChangeText={setSubject}
|
||||
placeholder="Out of office"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={styles.subjectInput}
|
||||
/>
|
||||
</SettingItem>
|
||||
@@ -148,7 +151,7 @@ export function VacationSettings() {
|
||||
value={body}
|
||||
onChangeText={setBody}
|
||||
placeholder="I am away from…"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={styles.bodyInput}
|
||||
multiline
|
||||
numberOfLines={6}
|
||||
@@ -161,9 +164,9 @@ export function VacationSettings() {
|
||||
<SettingsSection title="Preview">
|
||||
<Pressable style={styles.previewToggle} onPress={() => setShowPreview((v) => !v)}>
|
||||
{showPreview ? (
|
||||
<EyeOff size={14} color={colors.primary} />
|
||||
<EyeOff size={14} color={c.primary} />
|
||||
) : (
|
||||
<Eye size={14} color={colors.primary} />
|
||||
<Eye size={14} color={c.primary} />
|
||||
)}
|
||||
<Text style={styles.previewToggleText}>
|
||||
{showPreview ? 'Hide preview' : 'Show preview'}
|
||||
@@ -182,7 +185,7 @@ export function VacationSettings() {
|
||||
<View style={styles.warnings}>
|
||||
{warnings.map((w, i) => (
|
||||
<View key={i} style={styles.warnRow}>
|
||||
<AlertTriangle size={14} color={colors.warning} />
|
||||
<AlertTriangle size={14} color={c.warning} />
|
||||
<Text style={styles.warnText}>{w}</Text>
|
||||
</View>
|
||||
))}
|
||||
@@ -204,7 +207,8 @@ export function VacationSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
container: { gap: spacing.xl },
|
||||
centered: { paddingVertical: 40, alignItems: 'center' },
|
||||
unsupported: {
|
||||
@@ -215,7 +219,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'rgba(202,138,4,0.12)',
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
unsupportedText: { ...typography.body, color: colors.warning, flex: 1 },
|
||||
unsupportedText: { ...typography.body, color: c.warning, flex: 1 },
|
||||
statusRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.md },
|
||||
pill: {
|
||||
paddingHorizontal: 8,
|
||||
@@ -223,19 +227,19 @@ const styles = StyleSheet.create({
|
||||
borderRadius: radius.full,
|
||||
},
|
||||
pillActive: { backgroundColor: 'rgba(22,163,74,0.2)' },
|
||||
pillInactive: { backgroundColor: colors.muted },
|
||||
pillInactive: { backgroundColor: c.muted },
|
||||
pillText: { fontSize: 11, fontWeight: '500' },
|
||||
pillTextActive: { color: '#4ade80' },
|
||||
pillTextInactive: { color: colors.mutedForeground },
|
||||
pillTextInactive: { color: c.mutedForeground },
|
||||
dateInput: {
|
||||
minWidth: 180,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 6,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
borderColor: c.border,
|
||||
color: c.text,
|
||||
...typography.body,
|
||||
},
|
||||
subjectInput: {
|
||||
@@ -243,47 +247,48 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 6,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
borderColor: c.border,
|
||||
color: c.text,
|
||||
...typography.body,
|
||||
},
|
||||
bodyBlock: { paddingVertical: spacing.md },
|
||||
bodyLabel: { ...typography.bodyMedium, color: colors.text },
|
||||
bodyDesc: { ...typography.caption, color: colors.mutedForeground, marginTop: 2, marginBottom: spacing.sm },
|
||||
bodyLabel: { ...typography.bodyMedium, color: c.text },
|
||||
bodyDesc: { ...typography.caption, color: c.mutedForeground, marginTop: 2, marginBottom: spacing.sm },
|
||||
bodyInput: {
|
||||
width: '100%',
|
||||
minHeight: 96,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
borderColor: c.border,
|
||||
color: c.text,
|
||||
...typography.body,
|
||||
},
|
||||
previewToggle: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
previewToggleText: { ...typography.body, color: colors.primary },
|
||||
previewToggleText: { ...typography.body, color: c.primary },
|
||||
previewBox: {
|
||||
marginTop: spacing.sm,
|
||||
padding: spacing.lg,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.background,
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.background,
|
||||
},
|
||||
previewSubject: { ...typography.bodyMedium, color: colors.text, marginBottom: spacing.sm },
|
||||
previewBody: { ...typography.body, color: colors.mutedForeground },
|
||||
previewSubject: { ...typography.bodyMedium, color: c.text, marginBottom: spacing.sm },
|
||||
previewBody: { ...typography.body, color: c.mutedForeground },
|
||||
warnings: { gap: spacing.sm },
|
||||
warnRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
||||
warnText: { ...typography.body, color: colors.warning, flex: 1 },
|
||||
warnText: { ...typography.body, color: c.warning, flex: 1 },
|
||||
errorBox: {
|
||||
padding: spacing.md,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.errorBg,
|
||||
backgroundColor: c.errorBg,
|
||||
},
|
||||
errorText: { ...typography.body, color: colors.error },
|
||||
errorText: { ...typography.body, color: c.error },
|
||||
saveRow: { alignItems: 'flex-end' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { FlaskConical, Lock, ChevronDown, Check } 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 ToggleSwitchComponent from '../ToggleSwitch';
|
||||
|
||||
/**
|
||||
@@ -36,6 +37,8 @@ export function SettingsSection({
|
||||
experimental,
|
||||
experimentalDescription,
|
||||
}: SettingsSectionProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
{experimental && (
|
||||
@@ -73,12 +76,14 @@ export function SettingItem({
|
||||
locked,
|
||||
noBorder,
|
||||
}: SettingItemProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<View style={[styles.settingItem, !noBorder && styles.settingItemBorder, locked && styles.locked]}>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingLabelRow}>
|
||||
<Text style={styles.settingLabel}>{label}</Text>
|
||||
{locked && <Lock size={12} color={colors.mutedForeground} />}
|
||||
{locked && <Lock size={12} color={c.mutedForeground} />}
|
||||
</View>
|
||||
{description && <Text style={styles.settingDescription}>{description}</Text>}
|
||||
</View>
|
||||
@@ -111,6 +116,8 @@ interface RadioGroupProps {
|
||||
}
|
||||
|
||||
export function RadioGroup({ value, onChange, options, style }: RadioGroupProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
<View style={[styles.radioGroup, style]}>
|
||||
{options.map((opt) => {
|
||||
@@ -145,6 +152,8 @@ interface SelectProps {
|
||||
}
|
||||
|
||||
export function Select({ value, onChange, options, style }: SelectProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const current = options.find((o) => o.value === value);
|
||||
|
||||
@@ -152,7 +161,7 @@ export function Select({ value, onChange, options, style }: SelectProps) {
|
||||
<>
|
||||
<Pressable style={[styles.select, style]} onPress={() => setOpen(true)}>
|
||||
<Text style={styles.selectText}>{current?.label ?? ''}</Text>
|
||||
<ChevronDown size={14} color={colors.mutedForeground} />
|
||||
<ChevronDown size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
<Modal
|
||||
visible={open}
|
||||
@@ -178,7 +187,7 @@ export function Select({ value, onChange, options, style }: SelectProps) {
|
||||
}}
|
||||
>
|
||||
<Text style={styles.selectItemText}>{opt.label}</Text>
|
||||
{selected && <Check size={16} color={colors.primary} />}
|
||||
{selected && <Check size={16} color={c.primary} />}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
@@ -190,7 +199,8 @@ export function Select({ value, onChange, options, style }: SelectProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
section: {
|
||||
gap: spacing.lg,
|
||||
},
|
||||
@@ -199,11 +209,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
sectionTitle: {
|
||||
...typography.h3,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
sectionDescription: {
|
||||
...typography.body,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
marginTop: 4,
|
||||
},
|
||||
experimentalBanner: {
|
||||
@@ -233,7 +243,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
settingItemBorder: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomColor: c.border,
|
||||
},
|
||||
locked: {
|
||||
opacity: 0.6,
|
||||
@@ -249,11 +259,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
settingLabel: {
|
||||
...typography.bodyMedium,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
settingDescription: {
|
||||
...typography.caption,
|
||||
color: colors.mutedForeground,
|
||||
color: c.mutedForeground,
|
||||
marginTop: 2,
|
||||
},
|
||||
settingRight: {
|
||||
@@ -270,20 +280,20 @@ const styles = StyleSheet.create({
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
radioSelected: {
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: c.primary,
|
||||
},
|
||||
radioUnselected: {
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
},
|
||||
radioText: {
|
||||
...typography.caption,
|
||||
},
|
||||
radioTextSelected: {
|
||||
color: colors.primaryForeground,
|
||||
color: c.primaryForeground,
|
||||
fontWeight: '500',
|
||||
},
|
||||
radioTextUnselected: {
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
select: {
|
||||
flexDirection: 'row',
|
||||
@@ -292,14 +302,14 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 6,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.muted,
|
||||
backgroundColor: c.muted,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
minWidth: 120,
|
||||
},
|
||||
selectText: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
flex: 1,
|
||||
},
|
||||
modalBackdrop: {
|
||||
@@ -313,10 +323,10 @@ const styles = StyleSheet.create({
|
||||
width: '100%',
|
||||
maxWidth: 320,
|
||||
maxHeight: '70%',
|
||||
backgroundColor: colors.popover,
|
||||
backgroundColor: c.popover,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderColor: c.border,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
selectItem: {
|
||||
@@ -327,10 +337,11 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
selectItemPressed: {
|
||||
backgroundColor: colors.accent,
|
||||
backgroundColor: c.accent,
|
||||
},
|
||||
selectItemText: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
color: c.text,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user