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.
111 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|