diff --git a/apps/rebreak-native/components/KeyboardAwareSheet.tsx b/apps/rebreak-native/components/KeyboardAwareSheet.tsx deleted file mode 100644 index 9e23706..0000000 --- a/apps/rebreak-native/components/KeyboardAwareSheet.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { ReactNode, useEffect, useRef, useState } from 'react'; -import { - Animated, - Dimensions, - Easing, - Keyboard, - Modal, - Platform, - TouchableOpacity, - StyleProp, - View, - ViewStyle, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useColors } from '../lib/theme'; - -/** - * Universal-Bottom-Sheet für Forms mit TextInput. - * - * Pattern (verifiziert auf PostCommentsSheet + EditMailAccountSheet): - * - * 1. Outer-Animated.View hat animated `height` (JS-driver) — Sheet WÄCHST - * bei Tastatur-Open um genau die Tastatur-Höhe. - * 2. Inner-Animated.View hat `transform: translateY` (Native-driver) — - * Slide-In/Out smooth. Driver-Mix-Trennung verhindert - * "Style property 'height' is not supported by native animated module"-Crash. - * 3. iOS: `paddingBottom: keyboardHeight` shifted Form innerhalb des - * gewachsenen Sheets über die Tastatur. Android: `windowSoftInputMode=adjustResize` - * im Manifest schrumpft das Window selbst. - * 4. Flex-Spacer drückt `children` (Form) automatisch an den Sheet-Bottom-Edge — - * sitzt direkt über der Tastatur ohne Gap. - * - * Anti-Pattern (siehe `docs/internal/RECOVERY_LOG_2026-05-10.md` §7.2): - * - `useKeyboardAnimation()` aus `react-native-keyboard-controller` liefert - * in iOS-Modals keine Höhe (separate UIWindow). Hier: plain RN - * `Keyboard.addListener` für die Höhe. - * - `Animated.subtract`/`marginBottom: keyboardHeight` mischen JS+Native-Driver - * auf demselben View → Bouncing oder Crash. - * - * Usage: - * ```tsx - * } - * > - * - * - * - * - * - * ``` - */ -export interface KeyboardAwareSheetProps { - visible: boolean; - onClose: () => void; - /** Sheet-Höhe wenn Tastatur zu. Eng auf Inhalt zuschneiden — typisch 220-340px. */ - collapsedHeight: number; - /** Optionaler Header (Cancel/Title-Row). Rendert direkt unter dem Drag-Handle. */ - header?: ReactNode; - /** Form-Inhalt. Wird per Flex-Spacer an den Sheet-Bottom gedrückt — sitzt - * damit direkt über der Tastatur sobald die offen ist. */ - children: ReactNode; - /** Default true — Tap auf Backdrop schließt das Sheet. */ - dismissOnBackdrop?: boolean; - /** Default true — kleiner Drag-Handle ganz oben am Sheet. */ - showDragHandle?: boolean; - /** Default true — fügt unten eine Safe-Area-Spacer-Höhe ein (insets.bottom). */ - showSafeAreaSpacer?: boolean; - /** Default true — interner Flex-Spacer drückt children zum Sheet-Bottom. - * Auf false setzen wenn der Inhalt seine eigene Scroll-/Flex-Logik hat - * (z.B. ScrollView mit Provider-Grid, Listen). */ - pushChildrenToBottom?: boolean; - /** Border-Radius oben. Default 20. */ - topRadius?: number; - /** Optional zusätzlicher Style für den Sheet-Container. */ - containerStyle?: StyleProp; -} - -export function KeyboardAwareSheet({ - visible, - onClose, - collapsedHeight, - header, - children, - dismissOnBackdrop = true, - showDragHandle = true, - showSafeAreaSpacer = true, - pushChildrenToBottom = true, - topRadius = 20, - containerStyle, -}: KeyboardAwareSheetProps) { - const colors = useColors(); - const insets = useSafeAreaInsets(); - - const slideY = useRef(new Animated.Value(collapsedHeight)).current; - const backdropOpacity = useRef(new Animated.Value(0)).current; - const sheetHeight = useRef(new Animated.Value(collapsedHeight)).current; - const [keyboardHeight, setKeyboardHeight] = useState(0); - - // Slide-In + Backdrop-Fade bei `visible=true` - useEffect(() => { - if (visible) { - slideY.setValue(collapsedHeight); - backdropOpacity.setValue(0); - Animated.parallel([ - Animated.timing(slideY, { - toValue: 0, - duration: 280, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }), - Animated.timing(backdropOpacity, { - toValue: 1, - duration: 220, - useNativeDriver: true, - }), - ]).start(); - } - }, [visible, slideY, backdropOpacity, collapsedHeight]); - - // Sheet-Höhe wächst/schrumpft mit Tastatur - useEffect(() => { - const maxHeight = Dimensions.get('window').height - insets.top - 20; - const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; - const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; - const showSub = Keyboard.addListener(showEvent, (e) => { - const h = e.endCoordinates.height; - setKeyboardHeight(h); - Animated.timing(sheetHeight, { - toValue: Math.min(collapsedHeight + h, maxHeight), - duration: Platform.OS === 'ios' ? (e.duration ?? 250) : 220, - easing: Easing.out(Easing.cubic), - useNativeDriver: false, - }).start(); - }); - const hideSub = Keyboard.addListener(hideEvent, (e) => { - setKeyboardHeight(0); - Animated.timing(sheetHeight, { - toValue: collapsedHeight, - duration: Platform.OS === 'ios' ? (e?.duration ?? 250) : 220, - easing: Easing.out(Easing.cubic), - useNativeDriver: false, - }).start(); - }); - return () => { - showSub.remove(); - hideSub.remove(); - }; - }, [sheetHeight, collapsedHeight, insets.top]); - - return ( - - {/* Backdrop */} - - {dismissOnBackdrop && } - - - {/* Outer: animated height (JS-driver) */} - - {/* Inner: animated transform (Native-driver). Driver-Mix vermeiden - durch zwei verschachtelte Animated.Views. */} - - - {showDragHandle && ( - - - - )} - {header} - {pushChildrenToBottom ? ( - <> - {/* Flex-Spacer drückt children an den Sheet-Bottom */} - - {children} - - ) : ( - {children} - )} - {showSafeAreaSpacer && } - - - - - ); -} diff --git a/apps/rebreak-native/components/devices/AddMacSheet.tsx b/apps/rebreak-native/components/devices/AddMacSheet.tsx index 8beb231..7d157b9 100644 --- a/apps/rebreak-native/components/devices/AddMacSheet.tsx +++ b/apps/rebreak-native/components/devices/AddMacSheet.tsx @@ -2,6 +2,7 @@ import { ActivityIndicator, Alert, Linking, + ScrollView, TouchableOpacity, Text, TextInput, @@ -12,7 +13,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import * as Haptics from 'expo-haptics'; import { useColors } from '../../lib/theme'; -import { KeyboardAwareSheet } from '../KeyboardAwareSheet'; +import { FormSheet } from '../FormSheet'; import { RiveAvatar } from '../RiveAvatar'; import { useProtectedDevicesStore } from '../../stores/protectedDevices'; import { useRouter } from 'expo-router'; @@ -109,70 +110,52 @@ export function AddMacSheet({ router.push('/coach'); } - const collapsedHeight = step === 1 ? 300 : step === 2 ? 620 : 380; + const sheetTitle = + step === 1 + ? t('devices.label_question') + : step === 2 + ? t('devices.download_button') + : t('devices.success_title'); + + const initialHeightPct = step === 1 ? 0.42 : step === 2 ? 0.74 : 0.52; return ( - - - {step === 1 - ? t('devices.label_question') - : step === 2 - ? t('devices.download_button') - : t('devices.success_title')} - - - - - - } + title={sheetTitle} + initialHeightPct={initialHeightPct} + growWithKeyboard={step === 1} > - {step === 1 && } - {step === 2 && } - {step === 3 && } - + {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + )} + ); } @@ -261,7 +244,12 @@ function Step2OnboardingContent({ t: (k: string) => string; }) { return ( - + {/* Lyra intro card */} - + ); } diff --git a/apps/rebreak-native/components/devices/AddWindowsSheet.tsx b/apps/rebreak-native/components/devices/AddWindowsSheet.tsx index 5935c19..1c8df4d 100644 --- a/apps/rebreak-native/components/devices/AddWindowsSheet.tsx +++ b/apps/rebreak-native/components/devices/AddWindowsSheet.tsx @@ -2,6 +2,7 @@ import { ActivityIndicator, Alert, Linking, + ScrollView, TouchableOpacity, Text, TextInput, @@ -12,7 +13,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import * as Haptics from 'expo-haptics'; import { useColors } from '../../lib/theme'; -import { KeyboardAwareSheet } from '../KeyboardAwareSheet'; +import { FormSheet } from '../FormSheet'; import { RiveAvatar } from '../RiveAvatar'; import { useProtectedDevicesStore } from '../../stores/protectedDevices'; import { useRouter } from 'expo-router'; @@ -110,46 +111,22 @@ export function AddWindowsSheet({ router.push('/coach'); } - const collapsedHeight = step === 1 ? 300 : step === 2 ? 700 : 380; + const sheetTitle = + step === 1 + ? t('devices.windows_label_question') + : step === 2 + ? t('devices.windows_download_button') + : t('devices.windows_success_title'); + + const initialHeightPct = step === 1 ? 0.42 : step === 2 ? 0.74 : 0.52; return ( - - - {step === 1 - ? t('devices.windows_label_question') - : step === 2 - ? t('devices.windows_download_button') - : t('devices.windows_success_title')} - - - - - - } + title={sheetTitle} + initialHeightPct={initialHeightPct} + growWithKeyboard={step === 1} > {step === 1 && ( )} - + ); } @@ -268,7 +245,12 @@ function WindowsStep2OnboardingContent({ t: (k: string) => string; }) { return ( - + {/* Lyra intro card */} - + ); }