diff --git a/package-lock.json b/package-lock.json index 289bcdd86..dbe5cc272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4335,6 +4335,45 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@gorhom/bottom-sheet": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.6.tgz", + "integrity": "sha512-vmruJxdiUGDg+ZYcDmS30XDhq/h/+QkINOI5LY/uGjx8cPGwgJW0H6AB902gNTKtccbiKe/rr94EwdmIEz+LAQ==", + "license": "MIT", + "dependencies": { + "@gorhom/portal": "1.0.14", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-native": "*", + "react": "*", + "react-native": "*", + "react-native-gesture-handler": ">=2.16.1", + "react-native-reanimated": ">=3.16.0 || >=4.0.0-" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-native": { + "optional": true + } + } + }, + "node_modules/@gorhom/portal": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz", + "integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -20485,6 +20524,7 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^15.0.2", + "@gorhom/bottom-sheet": "^5.2.6", "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", diff --git a/packages/app/package.json b/packages/app/package.json index db70703a3..a75826a97 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.2", + "@gorhom/bottom-sheet": "^5.2.6", "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 7595ebdd1..2e7b449c1 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -2,6 +2,7 @@ import { Stack } from "expo-router"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; import { SessionProvider } from "@/contexts/session-context"; import { RealtimeProvider } from "@/contexts/realtime-context"; import { useSettings } from "@/hooks/use-settings"; @@ -37,25 +38,27 @@ export default function RootLayout() { - - - - - - - - - + + + + + + + + + + + diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx index e58706356..083990d2c 100644 --- a/packages/app/src/components/create-agent-modal.tsx +++ b/packages/app/src/components/create-agent-modal.tsx @@ -1,16 +1,20 @@ -import { useState } from "react"; -import { - Modal, - View, - Text, - Pressable, - TextInput, - ScrollView, -} from "react-native"; -import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; -import ReanimatedAnimated, { useAnimatedStyle } from "react-native-reanimated"; +import { useState, useRef, useEffect, useMemo, useCallback } from "react"; +import { View, Text, Pressable, ScrollView } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { X } from "lucide-react-native"; +import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; +import Animated, { useAnimatedStyle } from "react-native-reanimated"; +import { + BottomSheetModal, + BottomSheetBackdrop, + BottomSheetView, + BottomSheetScrollView, + BottomSheetTextInput, + BottomSheetFooter, +} from "@gorhom/bottom-sheet"; +import type { + BottomSheetBackdropProps, + BottomSheetFooterProps, +} from "@gorhom/bottom-sheet"; import { StyleSheet } from "react-native-unistyles"; import { theme as defaultTheme } from "@/styles/theme"; import { useRecentPaths } from "@/hooks/use-recent-paths"; @@ -39,25 +43,68 @@ export function CreateAgentModal({ onClose, onCreateAgent, }: CreateAgentModalProps) { + const bottomSheetRef = useRef(null); const insets = useSafeAreaInsets(); - const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); const { recentPaths, addRecentPath } = useRecentPaths(); + const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); const [workingDir, setWorkingDir] = useState(""); const [selectedMode, setSelectedMode] = useState("plan"); const [errorMessage, setErrorMessage] = useState(""); - // Keyboard animation - const bottomInset = insets.bottom; - const animatedKeyboardStyle = useAnimatedStyle(() => { + const snapPoints = useMemo(() => ["90%"], []); + + // Keyboard animation for footer + const animatedFooterStyle = useAnimatedStyle(() => { "worklet"; const absoluteHeight = Math.abs(keyboardHeight.value); - const padding = Math.max(0, absoluteHeight - bottomInset); + const padding = Math.max(0, absoluteHeight - insets.bottom); return { paddingBottom: padding, }; }); + const renderBackdrop = useMemo( + () => (props: BottomSheetBackdropProps) => + ( + + ), + [] + ); + + const renderFooter = useCallback( + (props: BottomSheetFooterProps) => ( + + + + Create Agent + + + + ), + [insets.bottom, workingDir, animatedFooterStyle] + ); + + useEffect(() => { + if (isVisible) { + bottomSheetRef.current?.present(); + } + }, [isVisible]); + async function handleCreate() { if (!workingDir.trim()) { setErrorMessage("Working directory is required"); @@ -79,6 +126,10 @@ export function CreateAgentModal({ } function handleClose() { + bottomSheetRef.current?.dismiss(); + } + + function handleDismiss() { setWorkingDir(""); setSelectedMode("plan"); setErrorMessage(""); @@ -86,174 +137,131 @@ export function CreateAgentModal({ } return ( - - - - + {/* Header */} + + Create New Agent + + + {/* Form */} + - - {/* Header */} - - Create New Agent - - - - + {/* Working Directory Input */} + + Working Directory + { + setWorkingDir(text); + setErrorMessage(""); + }} + autoCapitalize="none" + autoCorrect={false} + /> + {errorMessage && ( + {errorMessage} + )} - {/* Form */} - - {/* Working Directory Input */} - - Working Directory - { - setWorkingDir(text); - setErrorMessage(""); - }} - autoCapitalize="none" - autoCorrect={false} - /> - {errorMessage ? ( - {errorMessage} - ) : ( - - Absolute path to the project directory - - )} - - {/* Recent Paths Chips */} - {recentPaths.length > 0 && ( - 0 && ( + + {recentPaths.map((path) => ( + setWorkingDir(path)} > - {recentPaths.map((path) => ( - setWorkingDir(path)} - > - - {path} - - - ))} - - )} - + + {path} + + + ))} + + )} + - {/* Mode Selector */} - - Mode - - {MODES.map((mode) => ( - setSelectedMode(mode.value)} + {/* Mode Selector */} + + Mode + + {MODES.map((mode) => ( + setSelectedMode(mode.value)} + style={[ + styles.modeOption, + selectedMode === mode.value && styles.modeOptionSelected, + ]} + > + + - - - {selectedMode === mode.value && ( - - )} - - - {mode.label} - - {mode.description} - - - - - ))} - - - - - {/* Fixed Footer with Create Button */} - - - Create Agent - + {selectedMode === mode.value && ( + + )} + + + {mode.label} + + {mode.description} + + + + + ))} - - - + + + ); } const styles = StyleSheet.create((theme) => ({ - modalOverlay: { - flex: 1, - justifyContent: "flex-end", - }, - modalBackdrop: { - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(0,0,0,0.5)", - zIndex: 1, - }, - modalContent: { + sheetBackground: { backgroundColor: theme.colors.card, - borderTopLeftRadius: theme.spacing[6], - borderTopRightRadius: theme.spacing[6], - minHeight: "80%", - zIndex: 2, - elevation: 5, }, - modalInner: { + handleIndicator: { + backgroundColor: theme.colors.border, + }, + sheetContent: { flex: 1, - minHeight: 0, }, header: { - flexDirection: "row", alignItems: "center", - justifyContent: "space-between", + justifyContent: "center", padding: theme.spacing[6], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, @@ -268,7 +276,7 @@ const styles = StyleSheet.create((theme) => ({ }, scrollContent: { padding: theme.spacing[6], - paddingBottom: theme.spacing[4], + paddingBottom: theme.spacing[6], }, formSection: { marginBottom: theme.spacing[6],