/** * OptionsBottomSheet — Pixel-perfect iOS UIAlertController.actionSheet replication. * * Pattern: 2 separate Cards (Options + Cancel) mit Gap dazwischen, horizontal margin, * native iOS-Typografie + Spacing. Auf iOS 26 funktioniert das native ActionSheetIOS * nicht mehr klassisch (centered popover) → eigene Implementation für konsistenten * bottom-up sheet-look auf jeder iOS-Version. * * Use für: * - Geschlecht (3), kurze Auswahl-Listen, Confirm-Dialogs mit destructive-Action * Use NICHT für: * - Lange Listen (>7) → WheelPickerModal * - Free-text input → eigene Sheet (z.B. AddDomainSheet) */ import { useEffect, useRef } from 'react'; import { Modal, View, Text, Pressable, Animated, Easing, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { colors } from '../lib/theme'; type Option = { value: T; label: string; /** Rendert in roter Farbe (für „Löschen", „Reset" etc) */ destructive?: boolean; }; type Props = { visible: boolean; title?: string; message?: string; options: Option[]; value?: T | null; onSelect: (value: T) => void; onClose: () => void; }; export function OptionsBottomSheet({ visible, title, message, options, value, onSelect, onClose, }: Props) { const insets = useSafeAreaInsets(); const translateY = useRef(new Animated.Value(400)).current; const backdropOpacity = useRef(new Animated.Value(0)).current; useEffect(() => { if (visible) { translateY.setValue(400); backdropOpacity.setValue(0); Animated.parallel([ Animated.timing(translateY, { toValue: 0, duration: 280, easing: Easing.out(Easing.cubic), useNativeDriver: true, }), Animated.timing(backdropOpacity, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); } }, [visible, translateY, backdropOpacity]); function close() { Animated.parallel([ Animated.timing(translateY, { toValue: 400, duration: 220, easing: Easing.in(Easing.cubic), useNativeDriver: true, }), Animated.timing(backdropOpacity, { toValue: 0, duration: 180, useNativeDriver: true, }), ]).start(() => { onClose(); }); } const hasHeader = !!(title || message); return ( {/* Backdrop */} {/* Sheet — bottom-aligned, horizontal margin, 2 separate cards */} {/* Options-Card */} {hasHeader ? ( {title ? ( {title} ) : null} {message ? ( {message} ) : null} ) : null} {options.map((opt, idx) => { const isLast = idx === options.length - 1; const isSelected = value !== null && value !== undefined && opt.value === value; return ( { onSelect(opt.value); close(); }} style={({ pressed }) => ({ backgroundColor: pressed ? 'rgba(0,0,0,0.06)' : 'transparent', })} > {opt.label} ); })} {/* Cancel-Card — separat, bold */} {({ pressed }) => ( Abbrechen )} ); }