From 5b12f14a9025a4ba6de41a57234bb44d896b82d7 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 11 May 2026 22:58:59 +0200 Subject: [PATCH] feat(rebreak-native): Nuxt-style splash, domain normalization on blur, app-wide keyboard fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/index.tsx: replaced the placeholder landing with the BrandSplash look (#0f172a bg, SVG radial glows, breathing animation, staggered fade/bounce-ins for app name / logo / tagline / CTAs, "Made in Germany" footer). Dropped the "v0.1.0 RN Migration Phase 1 Skeleton" line; landing.version removed from locales. - AddDomainSheet: onBlur runs normalizeDomain() (strips scheme/www./path/query and email local-part) so the user sees the cleaned registrable domain before adding; also swapped the two leftover Pressables → TouchableOpacity (no-Pressable rule). - KeyboardAwareSheet: clamp the sheet height to (screenHeight - insets.top - 20) while the keyboard is up, so tall sheets (e.g. AddDomainSheet's 600px) don't grow off-screen and clip the inputs at the top. - ConnectMailSheet: automaticallyAdjustKeyboardInsets on iOS so focused inputs scroll into view. Covered sheets: AddDomainSheet, ConnectMailSheet, EditMailAccountSheet, AddMacSheet, AddWindowsSheet. JS-only (hot-reloadable). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/rebreak-native/app/index.tsx | 247 ++++++++++++++++-- .../components/KeyboardAwareSheet.tsx | 6 +- .../components/blocker/AddDomainSheet.tsx | 14 +- .../components/mail/ConnectMailSheet.tsx | 2 + apps/rebreak-native/locales/de.json | 3 +- apps/rebreak-native/locales/en.json | 3 +- 6 files changed, 245 insertions(+), 30 deletions(-) diff --git a/apps/rebreak-native/app/index.tsx b/apps/rebreak-native/app/index.tsx index dbd7fbf..301e86d 100644 --- a/apps/rebreak-native/app/index.tsx +++ b/apps/rebreak-native/app/index.tsx @@ -1,32 +1,241 @@ -import { View, Text, TouchableOpacity } from 'react-native'; +import { useEffect, useRef } from 'react'; +import { Animated, Dimensions, Image, Text, TouchableOpacity, View } from 'react-native'; +import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg'; import { useRouter } from 'expo-router'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; -export default function HomeScreen() { +const { width: SW, height: SH } = Dimensions.get('window'); + +export default function LandingScreen() { const router = useRouter(); + const insets = useSafeAreaInsets(); const { t } = useTranslation(); + const glowTopOpacity = useRef(new Animated.Value(0.5)).current; + const glowCenterOpacity = useRef(new Animated.Value(0)).current; + const glowCenterScale = useRef(new Animated.Value(0.6)).current; + + const nameOpacity = useRef(new Animated.Value(0)).current; + const nameTranslateY = useRef(new Animated.Value(12)).current; + + const logoOpacity = useRef(new Animated.Value(0)).current; + const logoScale = useRef(new Animated.Value(0.82)).current; + const logoTranslateY = useRef(new Animated.Value(8)).current; + const logoPulse = useRef(new Animated.Value(1)).current; + + const taglineOpacity = useRef(new Animated.Value(0)).current; + const taglineTranslateY = useRef(new Animated.Value(8)).current; + + const ctaOpacity = useRef(new Animated.Value(0)).current; + const ctaTranslateY = useRef(new Animated.Value(10)).current; + + const footerOpacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.loop( + Animated.sequence([ + Animated.timing(glowTopOpacity, { toValue: 0.9, duration: 2000, useNativeDriver: true }), + Animated.timing(glowTopOpacity, { toValue: 0.5, duration: 2000, useNativeDriver: true }), + ]), + ).start(); + + const ease = (toValue: number, duration: number) => ({ toValue, duration, useNativeDriver: true }); + + Animated.parallel([ + Animated.timing(glowCenterOpacity, ease(1, 900)), + Animated.timing(glowCenterScale, ease(1, 900)), + ]).start(); + + setTimeout(() => { + Animated.parallel([ + Animated.timing(nameOpacity, ease(1, 600)), + Animated.timing(nameTranslateY, ease(0, 600)), + ]).start(); + }, 300); + + setTimeout(() => { + Animated.parallel([ + Animated.timing(logoOpacity, ease(1, 650)), + Animated.spring(logoScale, { toValue: 1, useNativeDriver: true, friction: 6, tension: 80 }), + Animated.timing(logoTranslateY, ease(0, 650)), + ]).start(); + }, 700); + + setTimeout(() => { + Animated.loop( + Animated.sequence([ + Animated.timing(logoPulse, { toValue: 1.04, duration: 1300, useNativeDriver: true }), + Animated.timing(logoPulse, { toValue: 1, duration: 1300, useNativeDriver: true }), + ]), + ).start(); + }, 1100); + + setTimeout(() => { + Animated.parallel([ + Animated.timing(taglineOpacity, ease(1, 550)), + Animated.timing(taglineTranslateY, ease(0, 550)), + ]).start(); + }, 1300); + + setTimeout(() => { + Animated.parallel([ + Animated.timing(ctaOpacity, ease(1, 500)), + Animated.timing(ctaTranslateY, ease(0, 500)), + Animated.timing(footerOpacity, ease(1, 600)), + ]).start(); + }, 1700); + }, [ + glowTopOpacity, glowCenterOpacity, glowCenterScale, + nameOpacity, nameTranslateY, logoOpacity, logoScale, logoTranslateY, + logoPulse, taglineOpacity, taglineTranslateY, ctaOpacity, ctaTranslateY, footerOpacity, + ]); + return ( - - - {t('landing.appName')} - - {t('landing.tagline')} - + + {/* Top breathing glow */} + + + + + + + + + + + - router.push('/signin')} - activeOpacity={0.8} - className="bg-rebreak-500 px-8 py-4 rounded-full" + {/* Center indigo halo */} + + + + + + + + + + + + + {/* Content */} + + - {t('landing.start')} - + {t('appHeader.appName')} + - - {t('landing.version')} - + + + + + + {t('splash.tagline')} + + + + router.push('/signin')} + activeOpacity={0.85} + style={{ + backgroundColor: '#6366f1', + borderRadius: 16, + paddingVertical: 16, + alignItems: 'center', + }} + > + + {t('auth.signin')} + + + + router.push('/signup')} + activeOpacity={0.85} + style={{ + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.25)', + borderRadius: 16, + paddingVertical: 16, + alignItems: 'center', + }} + > + + {t('landing.start')} + + + - + + {/* Footer */} + + {t('splash.madeInGermany')} + + ); } diff --git a/apps/rebreak-native/components/KeyboardAwareSheet.tsx b/apps/rebreak-native/components/KeyboardAwareSheet.tsx index c89d0a8..9e23706 100644 --- a/apps/rebreak-native/components/KeyboardAwareSheet.tsx +++ b/apps/rebreak-native/components/KeyboardAwareSheet.tsx @@ -1,6 +1,7 @@ import { ReactNode, useEffect, useRef, useState } from 'react'; import { Animated, + Dimensions, Easing, Keyboard, Modal, @@ -121,13 +122,14 @@ export function KeyboardAwareSheet({ // 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: collapsedHeight + h, + toValue: Math.min(collapsedHeight + h, maxHeight), duration: Platform.OS === 'ios' ? (e.duration ?? 250) : 220, easing: Easing.out(Easing.cubic), useNativeDriver: false, @@ -146,7 +148,7 @@ export function KeyboardAwareSheet({ showSub.remove(); hideSub.remove(); }; - }, [sheetHeight, collapsedHeight]); + }, [sheetHeight, collapsedHeight, insets.top]); return ( diff --git a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx index ca4d351..23fea79 100644 --- a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx +++ b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx @@ -3,7 +3,6 @@ import { View, Text, TextInput, - Pressable, TouchableOpacity, Image, ActivityIndicator, @@ -82,11 +81,11 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { borderBottomColor: colors.border, }} > - + {t('common.cancel')} - + {t('blocker.add_sheet_title')} @@ -121,6 +120,10 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { setInput(v); setError(null); }} + onBlur={() => { + const n = normalizeDomain(input); + if (n !== input) setInput(n); + }} placeholder={t('blocker.add_sheet_placeholder')} placeholderTextColor={colors.textMuted} autoCapitalize="none" @@ -215,8 +218,9 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { {/* Confirm-Checkbox */} {valid && ( - setConfirmPermanent((v) => !v)} + activeOpacity={0.7} style={{ flexDirection: 'row', alignItems: 'flex-start', @@ -250,7 +254,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { > {t('blocker.add_sheet_confirm_permanent')} - + )} {/* Error */} diff --git a/apps/rebreak-native/components/mail/ConnectMailSheet.tsx b/apps/rebreak-native/components/mail/ConnectMailSheet.tsx index 1855956..4a46be0 100644 --- a/apps/rebreak-native/components/mail/ConnectMailSheet.tsx +++ b/apps/rebreak-native/components/mail/ConnectMailSheet.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { ActivityIndicator, Linking, + Platform, TouchableOpacity, ScrollView, Text, @@ -337,6 +338,7 @@ function FormView({ style={{ flex: 1 }} contentContainerStyle={{ padding: 20, gap: 14 }} keyboardShouldPersistTaps="handled" + automaticallyAdjustKeyboardInsets={Platform.OS === 'ios'} showsVerticalScrollIndicator={false} > {/* App-Password-Guide-Hinweis */} diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 44cceb5..979f55b 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -81,8 +81,7 @@ "landing": { "appName": "Rebreak", "tagline": "Du gehst nicht allein.", - "start": "Loslegen", - "version": "v0.1.0 — RN Migration Phase 1 Skeleton" + "start": "Registrieren" }, "splash": { "tagline": "You will never walk alone!", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index 745502a..75f9bc5 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -81,8 +81,7 @@ "landing": { "appName": "Rebreak", "tagline": "You're not walking alone.", - "start": "Get started", - "version": "v0.1.0 — RN Migration Phase 1 Skeleton" + "start": "Sign up" }, "splash": { "tagline": "You will never walk alone!",