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 */}
-
+
);
}