Files
contexta_mb/src/components/ui/Button.tsx
belviskhoremk 9e663bdc8b Initial commit
2026-05-08 13:01:47 +00:00

133 lines
3.7 KiB
TypeScript

import React, { useRef } from 'react';
import {
Animated,
TouchableWithoutFeedback,
Text,
StyleSheet,
ActivityIndicator,
ViewStyle,
TextStyle,
View,
} from 'react-native';
import { COLORS, RADIUS, SPACING, FONT_SIZE, FONT, SHADOWS } from '../../theme';
import { useTheme } from '../../theme';
type Variant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
type Size = 'sm' | 'md' | 'lg';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: Variant;
size?: Size;
loading?: boolean;
disabled?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
fullWidth?: boolean;
}
export function Button({
title,
onPress,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
style,
textStyle,
fullWidth = false,
}: ButtonProps) {
const { theme, isDark } = useTheme();
const scale = useRef(new Animated.Value(1)).current;
const onPressIn = () =>
Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, speed: 50, bounciness: 0 }).start();
const onPressOut = () =>
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 30, bounciness: 4 }).start();
const containerBg = getContainerBg(variant, theme, isDark);
const labelColor = getLabelColor(variant);
const shadow = variant === 'primary' ? SHADOWS.primary : variant === 'danger' ? SHADOWS.sm : {};
return (
<TouchableWithoutFeedback
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={disabled || loading}>
<Animated.View
style={[
styles.base,
styles[`size_${size}`],
containerBg,
shadow,
fullWidth && styles.fullWidth,
(disabled || loading) && styles.disabled,
{ transform: [{ scale }] },
style,
]}>
{loading ? (
<ActivityIndicator
size="small"
color={variant === 'primary' || variant === 'danger' ? COLORS.white : COLORS.primary}
/>
) : (
<Text style={[styles.label, styles[`label_${size}`], { color: labelColor }, textStyle]}>
{title}
</Text>
)}
</Animated.View>
</TouchableWithoutFeedback>
);
}
function getContainerBg(variant: Variant, theme: any, isDark: boolean): ViewStyle {
switch (variant) {
case 'primary':
return { backgroundColor: COLORS.primary };
case 'secondary':
return { backgroundColor: isDark ? theme.surfaceHover : theme.bgSecondary };
case 'outline':
return { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: COLORS.primary };
case 'ghost':
return { backgroundColor: 'transparent' };
case 'danger':
return { backgroundColor: COLORS.error };
}
}
function getLabelColor(variant: Variant): string {
switch (variant) {
case 'primary':
case 'danger':
return COLORS.white;
case 'secondary':
return COLORS.primary;
case 'outline':
case 'ghost':
return COLORS.primary;
}
}
const styles = StyleSheet.create({
base: {
borderRadius: RADIUS.md,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
fullWidth: { width: '100%' },
disabled: { opacity: 0.55 },
size_sm: { paddingVertical: SPACING.xs + 2, paddingHorizontal: SPACING.md, borderRadius: RADIUS.sm },
size_md: { paddingVertical: SPACING.md - 1, paddingHorizontal: SPACING.xl, borderRadius: RADIUS.md },
size_lg: { paddingVertical: SPACING.md + 1, paddingHorizontal: SPACING.xxl, borderRadius: RADIUS.lg, minHeight: 52 },
label: { fontWeight: FONT.semibold as any, letterSpacing: 0.1 },
label_sm: { fontSize: FONT_SIZE.sm },
label_md: { fontSize: FONT_SIZE.md },
label_lg: { fontSize: FONT_SIZE.md },
});