chahinebrini e4ac3ae51c refactor(native): central Button component + sweep across devices/plan flows
Replaces ad-hoc TouchableOpacity+styled-Text pairs with a single
`<Button>` covering the four variants we actually use (primary,
secondary, ghost, destructive), with size (sm/md/lg), loading,
disabled, icon, iconPosition, and a style escape hatch.

Migrated files: AddMacSheet, AddWindowsSheet, PlanChangeSheet,
devices.tsx CTA, settings SubscriptionSheet CTA.

Skipped (kept as-is to avoid hostile overrides): auth flow buttons
(Google/Apple OAuth with custom SVGs), list-row Touchables, blocker
& mail components (separate sweep when those screens come up).

paddingVertical default 12 (md) — matches the slimmer-buttons direction
we landed on in the devices-page redesign.
2026-05-15 23:31:26 +02:00

111 lines
2.7 KiB
TypeScript

import {
ActivityIndicator,
Text,
TouchableOpacity,
View,
type ViewStyle,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useColors } from '../lib/theme';
type Variant = 'primary' | 'secondary' | 'ghost' | 'destructive';
type Size = 'sm' | 'md' | 'lg';
type Props = {
title: string;
onPress?: () => void;
variant?: Variant;
size?: Size;
loading?: boolean;
disabled?: boolean;
icon?: React.ComponentProps<typeof Ionicons>['name'];
iconPosition?: 'left' | 'right';
style?: ViewStyle;
};
const PADDING: Record<Size, number> = { sm: 8, md: 12, lg: 16 };
const FONT_SIZE: Record<Size, number> = { sm: 14, md: 15, lg: 16 };
export function Button({
title,
onPress,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
icon,
iconPosition = 'left',
style,
}: Props) {
const colors = useColors();
const isDisabled = disabled || loading;
const paddingVertical = PADDING[size];
const fontSize = FONT_SIZE[size];
const containerBase: ViewStyle = {
borderRadius: 14,
paddingVertical,
paddingHorizontal: 16,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
gap: 8,
opacity: isDisabled ? 0.5 : 1,
};
let containerVariant: ViewStyle;
let textColor: string;
let indicatorColor: string;
switch (variant) {
case 'secondary':
containerVariant = {
borderWidth: 1.5,
borderColor: colors.brandOrange,
};
textColor = colors.brandOrange;
indicatorColor = colors.brandOrange;
break;
case 'ghost':
containerVariant = {};
textColor = colors.brandOrange;
indicatorColor = colors.brandOrange;
break;
case 'destructive':
containerVariant = { backgroundColor: colors.error };
textColor = '#ffffff';
indicatorColor = '#ffffff';
break;
default:
containerVariant = { backgroundColor: colors.brandOrange };
textColor = '#ffffff';
indicatorColor = '#ffffff';
break;
}
const iconEl = icon ? (
<Ionicons name={icon} size={fontSize + 2} color={loading ? 'transparent' : textColor} />
) : null;
return (
<TouchableOpacity
onPress={onPress}
disabled={isDisabled}
activeOpacity={0.7}
style={[containerBase, containerVariant, style]}
>
{loading ? (
<ActivityIndicator color={indicatorColor} size="small" />
) : (
<>
{iconEl && iconPosition === 'left' ? iconEl : null}
<Text style={{ fontSize, color: textColor, fontFamily: 'Nunito_600SemiBold' }}>
{title}
</Text>
{iconEl && iconPosition === 'right' ? iconEl : null}
</>
)}
</TouchableOpacity>
);
}