import React from 'react'; import { Pressable, Text, StyleSheet, ViewStyle, ActivityIndicator } from 'react-native'; import { spacing, radius, componentSizes, type ThemePalette } from '../theme/tokens'; import { useColors } from '../theme/colors'; /** * The webmail's standard-theme buttons (components/ui/button.tsx): * - default: bg-primary text-primary-foreground * - secondary: bg-muted text-foreground * - outline: border, transparent * - ghost: no border or fill until pressed * - destructive: red fill, white text * - destructiveOutline: red border and text, for a reset in a settings row * All 36 dp tall (lg 44 for a page's main action), radius 6, 14px medium. */ type ButtonVariant = 'default' | 'secondary' | 'ghost' | 'outline' | 'destructive' | 'destructiveOutline'; type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'; interface ButtonProps { variant?: ButtonVariant; size?: ButtonSize; children?: React.ReactNode; onPress?: () => void; disabled?: boolean; loading?: boolean; icon?: React.ReactNode; style?: ViewStyle; /** Needed for an icon-only button; a text button reads its text. */ accessibilityLabel?: string; } // Extra touch area so every size reaches a 48 dp target. const HIT_SLOP: Record = { sm: 6, md: 6, lg: 2, icon: 4 }; export default function Button({ variant = 'default', size = 'md', children, onPress, disabled = false, loading = false, icon, style, accessibilityLabel, }: ButtonProps) { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); const isDisabled = disabled || loading; const spinnerColor = variant === 'default' ? c.primaryForeground : variant === 'destructive' ? '#ffffff' : c.text; return ( [ styles.base, styles[`size_${size}` as const], styles[`bg_${variant}` as const], pressed && !isDisabled && styles[`pressed_${variant}` as const], isDisabled && styles.disabled, style, ]} onPress={onPress} disabled={isDisabled} hitSlop={HIT_SLOP[size]} accessibilityRole="button" accessibilityLabel={accessibilityLabel} accessibilityState={{ disabled: isDisabled, busy: loading }} > {loading ? ( ) : ( <> {icon} {typeof children === 'string' ? ( {children} ) : ( children )} )} ); } function makeStyles(c: ThemePalette) { return StyleSheet.create({ base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', borderRadius: radius.sm, gap: spacing.sm, }, text: { fontSize: 14, fontWeight: '500', lineHeight: 20 }, disabled: { opacity: 0.5 }, // minHeight, so a larger system font grows the button instead of clipping. size_sm: { minHeight: componentSizes.buttonSm, paddingHorizontal: spacing.md }, size_md: { minHeight: componentSizes.buttonSm, paddingHorizontal: 14 }, size_lg: { minHeight: componentSizes.buttonLg, paddingHorizontal: spacing.xxl }, size_icon: { height: componentSizes.buttonMd, width: componentSizes.buttonMd, paddingHorizontal: 0 }, bg_default: { backgroundColor: c.primary }, bg_secondary: { backgroundColor: c.muted }, bg_ghost: { backgroundColor: 'transparent' }, bg_outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: c.border }, bg_destructive: { backgroundColor: c.error }, bg_destructiveOutline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: c.error }, pressed_default: { opacity: 0.9 }, pressed_secondary: { opacity: 0.8 }, pressed_ghost: { backgroundColor: c.muted }, pressed_outline: { backgroundColor: c.muted }, pressed_destructive: { opacity: 0.9 }, pressed_destructiveOutline: { backgroundColor: c.errorBg }, text_default: { color: c.primaryForeground }, text_secondary: { color: c.text }, text_ghost: { color: c.text }, text_outline: { color: c.text }, text_destructive: { color: '#ffffff' }, text_destructiveOutline: { color: c.error }, }); }