refactor: simplify footer architecture by removing global footer pattern
Replace portal/registry pattern with direct rendering where each screen owns its footer. This eliminates FooterControlsContext indirection and makes the code follow standard React composition patterns. - Remove GlobalFooter and FooterControlsProvider from _layout.tsx - Add HomeFooter component rendered directly in home screen - Render AgentInputArea directly in agent screen (not via registration) - Add keyboard animation and safe area handling to AgentInputArea - Move FOOTER_HEIGHT constant to shared constants/layout.ts - Delete footer-controls-context.tsx and global-footer.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,7 @@ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
|||||||
import { RealtimeProvider } from "@/contexts/realtime-context";
|
import { RealtimeProvider } from "@/contexts/realtime-context";
|
||||||
import { useAppSettings } from "@/hooks/use-settings";
|
import { useAppSettings } from "@/hooks/use-settings";
|
||||||
import { View, ActivityIndicator, Text } from "react-native";
|
import { View, ActivityIndicator, Text } from "react-native";
|
||||||
import { GlobalFooter } from "@/components/global-footer";
|
|
||||||
import { useUnistyles } from "react-native-unistyles";
|
import { useUnistyles } from "react-native-unistyles";
|
||||||
import { FooterControlsProvider } from "@/contexts/footer-controls-context";
|
|
||||||
import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
||||||
import { DaemonConnectionsProvider } from "@/contexts/daemon-connections-context";
|
import { DaemonConnectionsProvider } from "@/contexts/daemon-connections-context";
|
||||||
import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
|
import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
|
||||||
@@ -111,29 +109,26 @@ export default function RootLayout() {
|
|||||||
<DaemonConnectionsProvider>
|
<DaemonConnectionsProvider>
|
||||||
<MultiDaemonSessionHost />
|
<MultiDaemonSessionHost />
|
||||||
<ProvidersWrapper>
|
<ProvidersWrapper>
|
||||||
<FooterControlsProvider>
|
<AppContainer>
|
||||||
<AppContainer>
|
<Stack
|
||||||
<Stack
|
screenOptions={{
|
||||||
screenOptions={{
|
headerShown: false,
|
||||||
headerShown: false,
|
animation: "none",
|
||||||
animation: "none",
|
gestureEnabled: true,
|
||||||
gestureEnabled: true,
|
gestureDirection: "horizontal",
|
||||||
gestureDirection: "horizontal",
|
fullScreenGestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Stack.Screen name="index" />
|
||||||
<Stack.Screen name="index" />
|
<Stack.Screen name="orchestrator" />
|
||||||
<Stack.Screen name="orchestrator" />
|
<Stack.Screen name="agent/[id]" />
|
||||||
<Stack.Screen name="agent/[id]" />
|
<Stack.Screen name="agent/[serverId]/[agentId]" />
|
||||||
<Stack.Screen name="agent/[serverId]/[agentId]" />
|
<Stack.Screen name="settings" />
|
||||||
<Stack.Screen name="settings" />
|
<Stack.Screen name="audio-test" />
|
||||||
<Stack.Screen name="audio-test" />
|
<Stack.Screen name="git-diff" />
|
||||||
<Stack.Screen name="git-diff" />
|
<Stack.Screen name="file-explorer" />
|
||||||
<Stack.Screen name="file-explorer" />
|
</Stack>
|
||||||
</Stack>
|
</AppContainer>
|
||||||
<GlobalFooter />
|
|
||||||
</AppContainer>
|
|
||||||
</FooterControlsProvider>
|
|
||||||
</ProvidersWrapper>
|
</ProvidersWrapper>
|
||||||
</DaemonConnectionsProvider>
|
</DaemonConnectionsProvider>
|
||||||
</DaemonRegistryProvider>
|
</DaemonRegistryProvider>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { BackHeader } from "@/components/headers/back-header";
|
|||||||
import { AgentStreamView } from "@/components/agent-stream-view";
|
import { AgentStreamView } from "@/components/agent-stream-view";
|
||||||
import { AgentInputArea } from "@/components/agent-input-area";
|
import { AgentInputArea } from "@/components/agent-input-area";
|
||||||
import { CreateAgentModal, ImportAgentModal, type CreateAgentInitialValues } from "@/components/create-agent-modal";
|
import { CreateAgentModal, ImportAgentModal, type CreateAgentInitialValues } from "@/components/create-agent-modal";
|
||||||
import { useFooterControls } from "@/contexts/footer-controls-context";
|
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
import { formatConnectionStatus } from "@/utils/daemons";
|
import { formatConnectionStatus } from "@/utils/daemons";
|
||||||
@@ -177,7 +176,6 @@ function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentPro
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { registerFooterControls, unregisterFooterControls } = useFooterControls();
|
|
||||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||||
const [menuVisible, setMenuVisible] = useState(false);
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
|
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
|
||||||
@@ -348,24 +346,6 @@ function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentPro
|
|||||||
initializeAgent({ agentId: resolvedAgentId });
|
initializeAgent({ agentId: resolvedAgentId });
|
||||||
}, [resolvedAgentId, initializeAgent, isInitializingFromMap]);
|
}, [resolvedAgentId, initializeAgent, isInitializingFromMap]);
|
||||||
|
|
||||||
const agentControls = useMemo(() => {
|
|
||||||
if (!resolvedAgentId) return null;
|
|
||||||
return <AgentInputArea agentId={resolvedAgentId} serverId={serverId} />;
|
|
||||||
}, [resolvedAgentId, serverId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!agentControls || !agent || isInitializing) {
|
|
||||||
unregisterFooterControls();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFooterControls(agentControls);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unregisterFooterControls();
|
|
||||||
};
|
|
||||||
}, [agentControls, agent, isInitializing, registerFooterControls, unregisterFooterControls]);
|
|
||||||
|
|
||||||
const recalculateMenuPosition = useCallback(
|
const recalculateMenuPosition = useCallback(
|
||||||
(onMeasured?: () => void) => {
|
(onMeasured?: () => void) => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -567,6 +547,11 @@ function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentPro
|
|||||||
</ReanimatedAnimated.View>
|
</ReanimatedAnimated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Agent Input Area */}
|
||||||
|
{!isInitializing && agent && resolvedAgentId && (
|
||||||
|
<AgentInputArea agentId={resolvedAgentId} serverId={serverId} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
{/* Dropdown Menu */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={menuVisible}
|
visible={menuVisible}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller
|
|||||||
import ReanimatedAnimated, { useAnimatedStyle } from "react-native-reanimated";
|
import ReanimatedAnimated, { useAnimatedStyle } from "react-native-reanimated";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { HomeHeader } from "@/components/headers/home-header";
|
import { HomeHeader } from "@/components/headers/home-header";
|
||||||
|
import { HomeFooter } from "@/components/home-footer";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { AgentList } from "@/components/agent-list";
|
import { AgentList } from "@/components/agent-list";
|
||||||
import { CreateAgentModal, ImportAgentModal } from "@/components/create-agent-modal";
|
import { CreateAgentModal, ImportAgentModal } from "@/components/create-agent-modal";
|
||||||
@@ -133,6 +134,9 @@ export default function HomeScreen() {
|
|||||||
)}
|
)}
|
||||||
</ReanimatedAnimated.View>
|
</ReanimatedAnimated.View>
|
||||||
|
|
||||||
|
{/* Home Footer */}
|
||||||
|
<HomeFooter />
|
||||||
|
|
||||||
{/* Create Agent Modal */}
|
{/* Create Agent Modal */}
|
||||||
<CreateAgentModal isVisible={showCreateModal} onClose={handleCloseCreateModal} />
|
<CreateAgentModal isVisible={showCreateModal} onClose={handleCloseCreateModal} />
|
||||||
<ImportAgentModal
|
<ImportAgentModal
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import Animated, {
|
|||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useRealtime } from "@/contexts/realtime-context";
|
import { useRealtime } from "@/contexts/realtime-context";
|
||||||
import { useDictation } from "@/hooks/use-dictation";
|
import { useDictation } from "@/hooks/use-dictation";
|
||||||
import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
|
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||||
import { VoiceNoteRecordingOverlay } from "./voice-note-recording-overlay";
|
import { VoiceNoteRecordingOverlay } from "./voice-note-recording-overlay";
|
||||||
import { DictationStatusNotice, type DictationToastVariant } from "./dictation-status-notice";
|
import { DictationStatusNotice, type DictationToastVariant } from "./dictation-status-notice";
|
||||||
import { generateMessageId } from "@/types/stream";
|
import { generateMessageId } from "@/types/stream";
|
||||||
@@ -85,6 +87,8 @@ type DictationToastConfig = {
|
|||||||
|
|
||||||
export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
||||||
|
|
||||||
const ws = useSessionStore((state) => state.sessions[serverId]?.ws);
|
const ws = useSessionStore((state) => state.sessions[serverId]?.ws);
|
||||||
|
|
||||||
@@ -825,6 +829,15 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const keyboardAnimatedStyle = useAnimatedStyle(() => {
|
||||||
|
"worklet";
|
||||||
|
const absoluteHeight = Math.abs(keyboardHeight.value);
|
||||||
|
const shift = Math.max(0, absoluteHeight - insets.bottom);
|
||||||
|
return {
|
||||||
|
transform: [{ translateY: -shift }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
async function handleRealtimePress() {
|
async function handleRealtimePress() {
|
||||||
try {
|
try {
|
||||||
if (isRealtimeMode) {
|
if (isRealtimeMode) {
|
||||||
@@ -923,7 +936,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<Animated.View style={[styles.container, { paddingBottom: insets.bottom }, keyboardAnimatedStyle]}>
|
||||||
{/* Border separator */}
|
{/* Border separator */}
|
||||||
<View style={styles.borderSeparator} />
|
<View style={styles.borderSeparator} />
|
||||||
|
|
||||||
@@ -1104,7 +1117,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-34
@@ -1,12 +1,12 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { View, Pressable, Text, Platform, Modal, Alert } from "react-native";
|
import { View, Pressable, Text, Platform, Modal, Alert } from "react-native";
|
||||||
import { usePathname, useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { AudioLines, Users, Plus, Download } from "lucide-react-native";
|
import { AudioLines, Users, Plus, Download } from "lucide-react-native";
|
||||||
import { useRealtime } from "@/contexts/realtime-context";
|
import { useRealtime } from "@/contexts/realtime-context";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { useFooterControls, FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
|
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||||
import { RealtimeControls } from "./realtime-controls";
|
import { RealtimeControls } from "./realtime-controls";
|
||||||
import { CreateAgentModal, ImportAgentModal } from "./create-agent-modal";
|
import { CreateAgentModal, ImportAgentModal } from "./create-agent-modal";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -16,14 +16,12 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||||
|
|
||||||
export function GlobalFooter() {
|
export function HomeFooter() {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const pathname = usePathname();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isRealtimeMode, startRealtime } = useRealtime();
|
const { isRealtimeMode, startRealtime } = useRealtime();
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
const { controls } = useFooterControls();
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [showImportModal, setShowImportModal] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
const [showRealtimeHostPicker, setShowRealtimeHostPicker] = useState(false);
|
const [showRealtimeHostPicker, setShowRealtimeHostPicker] = useState(false);
|
||||||
@@ -33,12 +31,6 @@ export function GlobalFooter() {
|
|||||||
const realtimeFadeIn = shouldDisableEntryExitAnimations ? undefined : FadeIn.duration(250);
|
const realtimeFadeIn = shouldDisableEntryExitAnimations ? undefined : FadeIn.duration(250);
|
||||||
const realtimeFadeOut = shouldDisableEntryExitAnimations ? undefined : FadeOut.duration(250);
|
const realtimeFadeOut = shouldDisableEntryExitAnimations ? undefined : FadeOut.duration(250);
|
||||||
|
|
||||||
// Determine current screen type
|
|
||||||
const isAgentScreen = pathname?.startsWith("/agent/");
|
|
||||||
|
|
||||||
const hasRegisteredControls = !!controls;
|
|
||||||
const showAgentControls = isAgentScreen && hasRegisteredControls;
|
|
||||||
|
|
||||||
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
||||||
const bottomInset = insets.bottom;
|
const bottomInset = insets.bottom;
|
||||||
|
|
||||||
@@ -76,7 +68,7 @@ export function GlobalFooter() {
|
|||||||
}
|
}
|
||||||
if (realtimeEligibleHosts.length === 1) {
|
if (realtimeEligibleHosts.length === 1) {
|
||||||
void startRealtime(realtimeEligibleHosts[0].daemon.id).catch((error) => {
|
void startRealtime(realtimeEligibleHosts[0].daemon.id).catch((error) => {
|
||||||
console.error("[GlobalFooter] Failed to start realtime", error);
|
console.error("[HomeFooter] Failed to start realtime", error);
|
||||||
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
|
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -88,7 +80,7 @@ export function GlobalFooter() {
|
|||||||
(daemonId: string) => {
|
(daemonId: string) => {
|
||||||
setShowRealtimeHostPicker(false);
|
setShowRealtimeHostPicker(false);
|
||||||
void startRealtime(daemonId).catch((error) => {
|
void startRealtime(daemonId).catch((error) => {
|
||||||
console.error("[GlobalFooter] Failed to start realtime", error);
|
console.error("[HomeFooter] Failed to start realtime", error);
|
||||||
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
|
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -99,26 +91,6 @@ export function GlobalFooter() {
|
|||||||
setShowRealtimeHostPicker(false);
|
setShowRealtimeHostPicker(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (showAgentControls) {
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.container,
|
|
||||||
{
|
|
||||||
paddingBottom: insets.bottom,
|
|
||||||
},
|
|
||||||
keyboardAnimatedStyle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{controls}
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAgentScreen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For home and orchestrator screens, show action buttons with realtime stacked on top
|
// For home and orchestrator screens, show action buttons with realtime stacked on top
|
||||||
const nonAgentFooterHeight = isRealtimeMode
|
const nonAgentFooterHeight = isRealtimeMode
|
||||||
? FOOTER_HEIGHT * 2 + insets.bottom
|
? FOOTER_HEIGHT * 2 + insets.bottom
|
||||||
@@ -191,7 +163,7 @@ export function GlobalFooter() {
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
console.log("[GlobalFooter] New Agent button pressed");
|
console.log("[HomeFooter] New Agent button pressed");
|
||||||
setShowCreateModal(true);
|
setShowCreateModal(true);
|
||||||
}}
|
}}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
@@ -3,7 +3,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|||||||
import { MicOff, Square } from "lucide-react-native";
|
import { MicOff, Square } from "lucide-react-native";
|
||||||
import { VolumeMeter } from "./volume-meter";
|
import { VolumeMeter } from "./volume-meter";
|
||||||
import { useRealtime } from "@/contexts/realtime-context";
|
import { useRealtime } from "@/contexts/realtime-context";
|
||||||
import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
|
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||||
|
|
||||||
const CONTROL_BUTTON_SIZE = 48;
|
const CONTROL_BUTTON_SIZE = 48;
|
||||||
const VERTICAL_PADDING = (FOOTER_HEIGHT - CONTROL_BUTTON_SIZE) / 2;
|
const VERTICAL_PADDING = (FOOTER_HEIGHT - CONTROL_BUTTON_SIZE) / 2;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { View, Text, Pressable, ActivityIndicator } from "react-native";
|
|||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { X, ArrowUp, RefreshCcw } from "lucide-react-native";
|
import { X, ArrowUp, RefreshCcw } from "lucide-react-native";
|
||||||
import { VolumeMeter } from "./volume-meter";
|
import { VolumeMeter } from "./volume-meter";
|
||||||
import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
|
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||||
import type { DictationStatus } from "@/hooks/use-dictation";
|
import type { DictationStatus } from "@/hooks/use-dictation";
|
||||||
|
|
||||||
interface VoiceNoteRecordingOverlayProps {
|
interface VoiceNoteRecordingOverlayProps {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const FOOTER_HEIGHT = 75;
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
|
|
||||||
export const FOOTER_HEIGHT = 75;
|
|
||||||
|
|
||||||
interface FooterControlsContextValue {
|
|
||||||
controls: ReactNode | null;
|
|
||||||
registerFooterControls: (controls: ReactNode) => void;
|
|
||||||
unregisterFooterControls: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FooterControlsContext = createContext<FooterControlsContextValue | undefined>(undefined);
|
|
||||||
|
|
||||||
export function FooterControlsProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [controls, setControls] = useState<ReactNode | null>(null);
|
|
||||||
|
|
||||||
const registerFooterControls = useCallback((nextControls: ReactNode) => {
|
|
||||||
setControls(nextControls);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const unregisterFooterControls = useCallback(() => {
|
|
||||||
setControls(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({
|
|
||||||
controls,
|
|
||||||
registerFooterControls,
|
|
||||||
unregisterFooterControls,
|
|
||||||
}),
|
|
||||||
[controls, registerFooterControls, unregisterFooterControls],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FooterControlsContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</FooterControlsContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFooterControls() {
|
|
||||||
const context = useContext(FooterControlsContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useFooterControls must be used within a FooterControlsProvider");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user