mojmajstor/components/ui/button.tsx
2026-06-06 03:54:22 +02:00

120 lines
3.5 KiB
TypeScript

import { Pressable, Text, ActivityIndicator, View, type ViewStyle } from "react-native";
import { useTheme } from "../theme";
type Variant = "primary" | "secondary" | "outline" | "ghost" | "destructive" | "success";
type Size = "xs" | "sm" | "md" | "lg";
interface ButtonProps {
variant?: Variant;
loading?: boolean;
size?: Size;
disabled?: boolean;
fullWidth?: boolean;
onPress?: () => void;
children: React.ReactNode;
style?: ViewStyle;
accessibilityLabel?: string;
}
function renderChildren(children: React.ReactNode, textColor: string, size: Size) {
const fontSize = size === "xs" ? 12 : size === "sm" ? 13 : size === "md" ? 14 : 16;
if (typeof children === "string") {
return (
<Text style={{ color: textColor, fontWeight: "600", fontSize, letterSpacing: 0.3 }}>
{children}
</Text>
);
}
if (Array.isArray(children)) {
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
{children.map((child, i) =>
typeof child === "string" ? (
<Text key={i} style={{ color: textColor, fontWeight: "600", fontSize, letterSpacing: 0.3 }}>
{child}
</Text>
) : (
child
)
)}
</View>
);
}
return children;
}
export function Button({
variant = "primary",
loading = false,
size = "md",
disabled = false,
fullWidth = false,
children,
style,
onPress,
accessibilityLabel,
}: ButtonProps) {
const theme = useTheme();
const sizeStyles: Record<Size, ViewStyle> = {
xs: { paddingVertical: 6, paddingHorizontal: 12, borderRadius: theme.radius.sm },
sm: { paddingVertical: 8, paddingHorizontal: 16, borderRadius: theme.radius.md },
md: { paddingVertical: 12, paddingHorizontal: 22, borderRadius: theme.radius.md },
lg: { paddingVertical: 16, paddingHorizontal: 28, borderRadius: theme.radius.lg },
};
const variantStyles: Record<Variant, ViewStyle> = {
primary: { backgroundColor: theme.colors.primary },
secondary: { backgroundColor: theme.colors.surfaceAlt, borderWidth: 1, borderColor: theme.colors.border },
outline: { backgroundColor: "transparent", borderWidth: 1.5, borderColor: theme.colors.primary },
ghost: { backgroundColor: "transparent" },
destructive: { backgroundColor: theme.colors.error },
success: { backgroundColor: theme.colors.success },
};
const textColors: Record<Variant, string> = {
primary: theme.colors.textInverse,
secondary: theme.colors.text,
outline: theme.colors.primary,
ghost: theme.colors.primary,
destructive: theme.colors.textInverse,
success: theme.colors.textInverse,
};
const label = typeof children === "string" ? children : accessibilityLabel;
return (
<Pressable
accessible
accessibilityLabel={label}
accessibilityRole="button"
accessibilityState={{ disabled: disabled || loading }}
disabled={disabled || loading}
onPress={onPress}
style={({ pressed }) => [
{
alignItems: "center",
justifyContent: "center",
flexDirection: "row",
},
sizeStyles[size],
variantStyles[variant],
variant === "primary" && theme.shadows.sm,
fullWidth && { width: "100%" },
disabled && { opacity: 0.45 },
pressed && { opacity: 0.82, transform: [{ scale: 0.98 }] },
style,
]}
>
{loading ? (
<ActivityIndicator color={textColors[variant]} size="small" />
) : (
renderChildren(children, textColors[variant], size)
)}
</Pressable>
);
}