refactor: implement footer controls context for smooth transitions
Codex implemented a portal/context architecture to eliminate layout jank: - Created FooterControlsProvider context with register/unregister methods - GlobalFooter now reserves fixed height (88px + insets) for all modes - Both AgentInputArea and RealtimeControls stack absolutely in same space - Single shared Reanimated value drives cross-fade (250ms) - Agent screen registers controls via context instead of rendering inline - Content padding adjusted to account for footer height - Borders and safe area moved to shared container - AgentInputArea simplified to pure component (no internal animations) This guarantees: - No layout shifts during transitions - Smooth cross-fade between control modes - No gaps or double borders - Both controls never occupy layout space simultaneously Implementation by OpenAI Codex per architectural recommendation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,16 @@
|
||||
import { View, TextInput, Pressable } from "react-native";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Mic, ArrowUp, AudioLines, Square } from "lucide-react-native";
|
||||
import { useSession } from "@/contexts/session-context";
|
||||
import { useRealtime } from "@/contexts/realtime-context";
|
||||
import { useAudioRecorder } from "@/hooks/use-audio-recorder";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
useSharedValue,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
interface AgentInputAreaProps {
|
||||
agentId: string;
|
||||
isRealtimeMode: boolean;
|
||||
}
|
||||
|
||||
export function AgentInputArea({ agentId, isRealtimeMode }: AgentInputAreaProps) {
|
||||
export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { ws, sendAgentMessage, sendAgentAudio } = useSession();
|
||||
const { startRealtime } = useRealtime();
|
||||
@@ -26,23 +20,6 @@ export function AgentInputArea({ agentId, isRealtimeMode }: AgentInputAreaProps)
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// Animated opacity for smooth transitions
|
||||
const opacity = useSharedValue(isRealtimeMode ? 0 : 1);
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = withTiming(isRealtimeMode ? 0 : 1, { duration: 250 });
|
||||
}, [isRealtimeMode]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: opacity.value,
|
||||
// When hidden, use absolute positioning to remove from layout flow
|
||||
position: opacity.value < 0.5 ? ("absolute" as const) : ("relative" as const),
|
||||
// When hidden, disable pointer events so GlobalFooter's controls are interactive
|
||||
pointerEvents: opacity.value < 0.5 ? ("none" as const) : ("auto" as const),
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (!userInput.trim() || !ws.isConnected) return;
|
||||
|
||||
@@ -99,7 +76,7 @@ export function AgentInputArea({ agentId, isRealtimeMode }: AgentInputAreaProps)
|
||||
const hasText = userInput.trim().length > 0;
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, animatedStyle]}>
|
||||
<View style={styles.container}>
|
||||
{/* Text input */}
|
||||
<TextInput
|
||||
value={userInput}
|
||||
@@ -159,7 +136,7 @@ export function AgentInputArea({ agentId, isRealtimeMode }: AgentInputAreaProps)
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user