/** * SearchBarFloating — sticky-bottom floating search input. * * Inspired by iOS 17/18 sticky-bottom search in Settings.app. Apple uses * UISearchBar with a custom bottom placement introduced progressively: in * iOS 17 via UINavigationItem.searchController + preferredSearchBarPlacement * (.bottomBar), in iOS 18/26 as part of the Liquid Glass system sheet. * * Native RN/Expo support: * - react-native-screens / react-navigation searchBar prop → renders in * navigation header (TOP, not bottom). No bottom placement exposed. * - expo-router exposes no searchBar on native-stack bottom bar. * - Conclusion: bottom-sticky native UISearchBar is NOT achievable from JS * without a custom native module. This component is a JS approximation. * * Vibrancy: iOS uses expo-blur BlurView for real UIKit vibrancy. Android keeps * the solid surface-color fallback — expo-blur's Android path (dimezisBlurView) * war fehleranfällig/crasht, daher rendert der BlurView NUR auf iOS. Android * bleibt damit komplett unangetastet. * * Use-case guidance: * - Lyra voice input → use the existing ChatInput Mic button, not this. * - Chat history search → native UISearchBar via navigation stack (top) is * preferable for familiar iOS placement. * - This component is best for: standalone screens where a persistent * bottom-anchored search/input widget makes sense (e.g. SOS quick-access, * domain search in Blocker page when not scrolled). */ import { useRef, useState } from 'react'; import { Animated, Platform, StyleSheet, TextInput, TouchableOpacity, useColorScheme, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { BlurView } from 'expo-blur'; import { Ionicons } from '@expo/vector-icons'; import { useColors } from '../lib/theme'; const IS_IOS = Platform.OS === 'ios'; type Props = { placeholder?: string; /** Called when user taps the mic icon. Handle voice-input activation here. */ onPressMic?: () => void; /** Called when user submits (keyboard "search" button or enter). */ onSubmit?: (text: string) => void; /** Called on every keystroke. */ onChange?: (text: string) => void; /** When true, shows ActivityIndicator-style pulse on mic icon. */ micActive?: boolean; }; export function SearchBarFloating({ placeholder = 'Suchen…', onPressMic, onSubmit, onChange, micActive = false, }: Props) { const insets = useSafeAreaInsets(); const colors = useColors(); const scheme = useColorScheme(); const [text, setText] = useState(''); const micScale = useRef(new Animated.Value(1)).current; function handleTextChange(val: string) { setText(val); onChange?.(val); } function handleSubmit() { if (text.trim()) onSubmit?.(text.trim()); } function handlePressMic() { Animated.sequence([ Animated.timing(micScale, { toValue: 0.85, duration: 80, useNativeDriver: true }), Animated.timing(micScale, { toValue: 1, duration: 120, useNativeDriver: true }), ]).start(); onPressMic?.(); } const bottomOffset = Math.max(insets.bottom, 8); return ( {IS_IOS ? ( ) : null} {onPressMic ? ( ) : null} ); } const styles = StyleSheet.create({ wrapper: { position: 'absolute', left: 16, right: 16, zIndex: 100, }, pill: { flexDirection: 'row', alignItems: 'center', borderRadius: 22, borderWidth: StyleSheet.hairlineWidth, paddingLeft: 12, paddingRight: 8, paddingVertical: 8, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 6, gap: 8, }, blurFill: { ...StyleSheet.absoluteFillObject, borderRadius: 22, overflow: 'hidden', }, searchIcon: { flexShrink: 0, }, input: { flex: 1, fontSize: 16, fontFamily: 'Nunito_400Regular', padding: 0, minHeight: 28, }, micBtn: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', flexShrink: 0, }, });