feat(app): add sliding sidebar navigation for mobile

- Homepage now shows "New Agent" form, agent list moves to sidebar
- Hamburger menu replaces back arrow on all screens
- Mobile: full-screen sidebar overlay with swipe-to-close gesture
- Desktop: fixed 320px sidebar, toggle via hamburger
- Smooth bezier-eased animations (220ms)
- Sidebar closes on agent select (mobile only)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2026-01-03 21:16:27 +07:00
parent 1f33dcb4c1
commit 020d244b26
9 changed files with 1556 additions and 1427 deletions

View File

@@ -1,4 +1,4 @@
import { Stack } from "expo-router";
import { Stack, useLocalSearchParams, usePathname } 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";
@@ -6,12 +6,13 @@ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { RealtimeProvider } from "@/contexts/realtime-context";
import { useAppSettings } from "@/hooks/use-settings";
import { View, ActivityIndicator, Text } from "react-native";
import { useUnistyles } from "react-native-unistyles";
import { UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { DaemonConnectionsProvider } from "@/contexts/daemon-connections-context";
import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";
import { useState, type ReactNode, useMemo } from "react";
import { SlidingSidebar } from "@/components/sliding-sidebar";
function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(
@@ -32,12 +33,23 @@ function QueryProvider({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
function AppContainer({ children }: { children: ReactNode }) {
interface AppContainerProps {
children: ReactNode;
selectedAgentId?: string;
}
function AppContainer({ children, selectedAgentId }: AppContainerProps) {
const { theme } = useUnistyles();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
return (
<View style={{ flex: 1, backgroundColor: theme.colors.background }}>
{children}
<View style={{ flex: 1, flexDirection: "row" }}>
{!isMobile && <SlidingSidebar selectedAgentId={selectedAgentId} />}
<View style={{ flex: 1 }}>{children}</View>
</View>
{isMobile && <SlidingSidebar selectedAgentId={selectedAgentId} />}
</View>
);
}
@@ -58,6 +70,22 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
return <RealtimeProvider>{children}</RealtimeProvider>;
}
function AppWithSidebar({ children }: { children: ReactNode }) {
const pathname = usePathname();
const params = useLocalSearchParams<{ agentId?: string }>();
const selectedAgentId = useMemo(() => {
if (pathname.startsWith("/agent/") && params.agentId) {
return params.agentId;
}
return undefined;
}, [pathname, params.agentId]);
return (
<AppContainer selectedAgentId={selectedAgentId}>{children}</AppContainer>
);
}
function LoadingView() {
return (
<View
@@ -109,7 +137,7 @@ export default function RootLayout() {
<DaemonConnectionsProvider>
<MultiDaemonSessionHost />
<ProvidersWrapper>
<AppContainer>
<AppWithSidebar>
<Stack
screenOptions={{
headerShown: false,
@@ -128,7 +156,7 @@ export default function RootLayout() {
<Stack.Screen name="git-diff" />
<Stack.Screen name="file-explorer" />
</Stack>
</AppContainer>
</AppWithSidebar>
</ProvidersWrapper>
</DaemonConnectionsProvider>
</DaemonRegistryProvider>

View File

@@ -24,17 +24,15 @@ import {
GitBranch,
Folder,
RotateCcw,
Plus,
Download,
Users,
ChevronRight,
PlusIcon,
} from "lucide-react-native";
import { MenuHeader } from "@/components/headers/menu-header";
import { BackHeader } from "@/components/headers/back-header";
import { AgentStreamView } from "@/components/agent-stream-view";
import { AgentInputArea } from "@/components/agent-input-area";
import { AgentList } from "@/components/agent-list";
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
import { ImportAgentModal } from "@/components/create-agent-modal";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
@@ -148,7 +146,6 @@ export default function AgentScreen() {
<AgentScreenContent
serverId={resolvedServerId}
agentId={resolvedAgentId}
onBack={handleBackToHome}
/>
);
}
@@ -156,28 +153,17 @@ export default function AgentScreen() {
type AgentScreenContentProps = {
serverId: string;
agentId?: string;
onBack: () => void;
};
const SIDEBAR_WIDTH = 280;
const LARGE_SCREEN_BREAKPOINT = 768;
function AgentScreenContent({
serverId,
agentId,
onBack,
}: AgentScreenContentProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const router = useRouter();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const isLargeScreen = windowWidth >= LARGE_SCREEN_BREAKPOINT;
const {
agents: aggregatedAgents,
isRevalidating,
refreshAll,
} = useAggregatedAgents();
const [menuVisible, setMenuVisible] = useState(false);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
const [menuContentHeight, setMenuContentHeight] = useState(0);
@@ -193,27 +179,6 @@ function AgentScreenContent({
: undefined
);
// Get parent agent ID for back navigation
const parentAgentId = agent?.parentAgentId;
// Navigate to parent agent if this is a child, otherwise go to homepage
const handleBack = useCallback(() => {
if (parentAgentId) {
// Child agent: navigate back to parent
router.push({
pathname: "/agent/[serverId]/[agentId]",
params: {
serverId: serverId,
agentId: parentAgentId,
},
});
} else {
// Root agent: navigate to homepage
onBack();
}
}, [parentAgentId, router, serverId, onBack]);
// Select the agents Map directly - this is a stable reference that only changes when agents are added/removed
const allAgents = useSessionStore(
(state) => state.sessions[serverId]?.agents
@@ -560,7 +525,7 @@ function AgentScreenContent({
if (agentModel) {
params.model = agentModel;
}
router.push({ pathname: "/agent/new", params });
router.push({ pathname: "/", params });
}, [agent, agentModel, handleCloseMenu, router, serverId]);
const handleImportAgent = useCallback(() => {
@@ -598,7 +563,7 @@ function AgentScreenContent({
return (
<>
<View style={styles.container}>
<BackHeader onBack={onBack} />
<MenuHeader title="Agent" />
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
@@ -611,87 +576,48 @@ function AgentScreenContent({
return (
<>
<View style={styles.container}>
<View
style={[styles.mainLayout, isLargeScreen && styles.mainLayoutRow]}
>
{/* Sidebar - only on large screens */}
{isLargeScreen && (
<View style={[styles.sidebar, { width: SIDEBAR_WIDTH }]}>
<View style={styles.sidebarHeader}>
<Pressable
style={[
styles.newAgentButton,
{ backgroundColor: theme.colors.primary },
]}
onPress={handleCreateNewAgent}
>
<Plus size={18} color={theme.colors.primaryForeground} />
<Text
style={[
styles.newAgentButtonText,
{ color: theme.colors.primaryForeground },
]}
>
New Agent
</Text>
</Pressable>
{/* Header */}
<MenuHeader
title={agent.title || "Agent"}
rightContent={
<View ref={menuButtonRef} collapsable={false}>
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
<MoreVertical size={20} color={theme.colors.mutedForeground} />
</Pressable>
</View>
}
/>
{/* Content Area with Keyboard Animation */}
<View style={styles.contentContainer}>
<ReanimatedAnimated.View
style={[styles.content, animatedKeyboardStyle]}
>
{isInitializing ? (
<View style={styles.loadingContainer}>
<ActivityIndicator
size="large"
color={theme.colors.primary}
/>
<Text style={styles.loadingText}>Loading agent...</Text>
</View>
<AgentList
agents={aggregatedAgents}
isRefreshing={isRevalidating}
onRefresh={refreshAll}
selectedAgentId={resolvedAgentId}
) : (
<AgentStreamView
agentId={agent.id}
serverId={serverId}
agent={agent}
streamItems={streamItems}
pendingPermissions={pendingPermissions}
/>
</View>
)}
{/* Main agent panel */}
<View style={styles.agentPanel}>
{/* Header */}
<BackHeader
title={agent.title || "Agent"}
onBack={handleBack}
rightContent={
<View ref={menuButtonRef} collapsable={false}>
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
<MoreVertical size={20} color={theme.colors.mutedForeground} />
</Pressable>
</View>
}
/>
{/* Content Area with Keyboard Animation */}
<View style={styles.contentContainer}>
<ReanimatedAnimated.View
style={[styles.content, animatedKeyboardStyle]}
>
{isInitializing ? (
<View style={styles.loadingContainer}>
<ActivityIndicator
size="large"
color={theme.colors.primary}
/>
<Text style={styles.loadingText}>Loading agent...</Text>
</View>
) : (
<AgentStreamView
agentId={agent.id}
serverId={serverId}
agent={agent}
streamItems={streamItems}
pendingPermissions={pendingPermissions}
/>
)}
</ReanimatedAnimated.View>
</View>
{/* Agent Input Area */}
{!isInitializing && agent && resolvedAgentId && (
<AgentInputArea agentId={resolvedAgentId} serverId={serverId} autoFocus />
)}
</View>
</ReanimatedAnimated.View>
</View>
{/* Agent Input Area */}
{!isInitializing && agent && resolvedAgentId && (
<AgentInputArea agentId={resolvedAgentId} serverId={serverId} autoFocus />
)}
{/* Dropdown Menu */}
<Modal
visible={menuVisible}
@@ -931,36 +857,6 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
backgroundColor: theme.colors.background,
},
mainLayout: {
flex: 1,
},
mainLayoutRow: {
flexDirection: "row",
},
sidebar: {
borderRightWidth: 1,
borderRightColor: theme.colors.border,
},
sidebarHeader: {
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[4],
paddingBottom: theme.spacing[2],
},
newAgentButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: theme.spacing[2],
paddingVertical: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
},
newAgentButtonText: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
},
agentPanel: {
flex: 1,
},
contentContainer: {
flex: 1,
overflow: "hidden",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -12,9 +12,10 @@ interface AgentListProps {
isRefreshing?: boolean;
onRefresh?: () => void;
selectedAgentId?: string;
onAgentSelect?: () => void;
}
export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAgentId }: AgentListProps) {
export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAgentId, onAgentSelect }: AgentListProps) {
const { theme } = useUnistyles();
const pathname = usePathname();
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
@@ -65,8 +66,9 @@ export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAge
agentId,
},
});
onAgentSelect?.();
},
[isActionSheetVisible, pathname]
[isActionSheetVisible, pathname, onAgentSelect]
);
const handleAgentLongPress = useCallback((agent: AggregatedAgent) => {

View File

@@ -0,0 +1,57 @@
import type { ReactNode } from "react";
import { Pressable, Text } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Menu } from "lucide-react-native";
import { ScreenHeader } from "./screen-header";
import { useSidebarStore } from "@/stores/sidebar-store";
interface MenuHeaderProps {
title?: string;
rightContent?: ReactNode;
}
export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
const { theme } = useUnistyles();
const { toggle } = useSidebarStore();
return (
<ScreenHeader
left={
<>
<Pressable onPress={toggle} style={styles.menuButton}>
<Menu size={20} color={theme.colors.mutedForeground} />
</Pressable>
{title && (
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
)}
</>
}
right={rightContent}
leftStyle={styles.left}
/>
);
}
const styles = StyleSheet.create((theme) => ({
left: {
gap: theme.spacing[2],
},
menuButton: {
padding: {
xs: theme.spacing[3],
md: theme.spacing[2],
},
borderRadius: theme.borderRadius.lg,
},
title: {
flex: 1,
fontSize: theme.fontSize.lg,
fontWeight: {
xs: theme.fontWeight.semibold,
md: "400",
},
color: theme.colors.foreground,
},
}));

View File

@@ -163,7 +163,7 @@ export function HomeFooter() {
<Pressable
onPress={() => {
console.log("[HomeFooter] New Agent button pressed");
router.push("/agent/new");
router.push("/");
}}
style={({ pressed }) => [
styles.footerButton,

View File

@@ -0,0 +1,354 @@
import { useCallback, useEffect } from "react";
import { View, Pressable, useWindowDimensions, Text } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
interpolate,
Extrapolation,
runOnJS,
Easing,
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { Plus, Settings } from "lucide-react-native";
import { router } from "expo-router";
import { useSidebarStore } from "@/stores/sidebar-store";
import { AgentList } from "./agent-list";
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
const DESKTOP_SIDEBAR_WIDTH = 320;
const ANIMATION_DURATION = 220;
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
interface SlidingSidebarProps {
selectedAgentId?: string;
}
export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const { width: windowWidth } = useWindowDimensions();
const { isOpen, open, close } = useSidebarStore();
const { agents, isRevalidating, refreshAll } = useAggregatedAgents();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
// Mobile sidebar is full width
const sidebarWidth = isMobile ? windowWidth : DESKTOP_SIDEBAR_WIDTH;
const translateX = useSharedValue(isOpen ? 0 : -sidebarWidth);
const backdropOpacity = useSharedValue(isOpen ? 1 : 0);
// Track if we're currently in a gesture (to prevent useEffect from interfering)
const isGesturing = useSharedValue(false);
useEffect(() => {
// Don't animate if we're in the middle of a gesture
if (isGesturing.value) return;
const width = isMobile ? windowWidth : DESKTOP_SIDEBAR_WIDTH;
translateX.value = withTiming(isOpen ? 0 : -width, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(isOpen ? 1 : 0, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
}, [isOpen, translateX, backdropOpacity, isMobile, windowWidth, isGesturing]);
const handleClose = useCallback(() => {
close();
}, [close]);
const handleOpen = useCallback(() => {
open();
}, [open]);
// Mobile: close sidebar and navigate
const handleCreateAgentMobile = useCallback(() => {
close();
router.push("/");
}, [close]);
// Desktop: just navigate, don't close
const handleCreateAgentDesktop = useCallback(() => {
router.push("/");
}, []);
// Mobile: close sidebar and navigate
const handleSettingsMobile = useCallback(() => {
close();
router.push("/settings");
}, [close]);
// Desktop: just navigate, don't close
const handleSettingsDesktop = useCallback(() => {
router.push("/settings");
}, []);
// Mobile: close sidebar when agent is selected
const handleAgentSelectMobile = useCallback(() => {
close();
}, [close]);
// Close gesture (swipe left to close when sidebar is open)
const closeGesture = Gesture.Pan()
// Only activate after 15px horizontal movement
.activeOffsetX([-15, 15])
// Fail if 10px vertical movement happens first (allow vertical scroll)
.failOffsetY([-10, 10])
.onStart(() => {
isGesturing.value = true;
})
.onUpdate((event) => {
if (!isMobile) return;
// Only allow swiping left (closing)
const newTranslateX = Math.min(0, Math.max(-windowWidth, event.translationX));
translateX.value = newTranslateX;
backdropOpacity.value = interpolate(
newTranslateX,
[-windowWidth, 0],
[0, 1],
Extrapolation.CLAMP
);
})
.onEnd((event) => {
isGesturing.value = false;
if (!isMobile) return;
const shouldClose = event.translationX < -windowWidth / 3 || event.velocityX < -500;
if (shouldClose) {
translateX.value = withTiming(-windowWidth, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(0, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
runOnJS(handleClose)();
} else {
translateX.value = withTiming(0, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(1, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
}
})
.onFinalize(() => {
isGesturing.value = false;
});
// Open gesture (swipe right from left edge to open when sidebar is closed)
const openGesture = Gesture.Pan()
.hitSlop({ right: windowWidth * 0.5 })
// Only activate after 15px horizontal movement to the right
.activeOffsetX(15)
// Fail if 10px vertical movement happens first (allow vertical scroll)
.failOffsetY([-10, 10])
.onStart(() => {
isGesturing.value = true;
})
.onUpdate((event) => {
if (!isMobile) return;
// Start from closed position (-windowWidth) and move towards 0
const newTranslateX = Math.min(0, -windowWidth + event.translationX);
translateX.value = newTranslateX;
backdropOpacity.value = interpolate(
newTranslateX,
[-windowWidth, 0],
[0, 1],
Extrapolation.CLAMP
);
})
.onEnd((event) => {
isGesturing.value = false;
if (!isMobile) return;
// Open if dragged more than 1/3 of sidebar or fast swipe
const shouldOpen = event.translationX > windowWidth / 3 || event.velocityX > 500;
if (shouldOpen) {
translateX.value = withTiming(0, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(1, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
runOnJS(handleOpen)();
} else {
translateX.value = withTiming(-windowWidth, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(0, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
}
})
.onFinalize(() => {
isGesturing.value = false;
});
const swipeGesture = Gesture.Simultaneous(
isOpen ? closeGesture : openGesture,
Gesture.Native()
);
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const backdropAnimatedStyle = useAnimatedStyle(() => ({
opacity: backdropOpacity.value,
pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none",
}));
// Render mobile sidebar with edge swipe
if (isMobile) {
return (
<View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
{/* Backdrop */}
<Animated.View style={[styles.backdrop, backdropAnimatedStyle]}>
<Pressable style={styles.backdropPressable} onPress={handleClose} />
</Animated.View>
{/* Sidebar */}
<GestureDetector gesture={swipeGesture}>
<Animated.View
style={[
styles.mobileSidebar,
{ width: windowWidth, paddingTop: insets.top },
sidebarAnimatedStyle,
]}
>
<View style={styles.sidebarHeader}>
<Pressable
style={styles.headerIconButton}
onPress={handleSettingsMobile}
>
<Settings size={20} color={theme.colors.foreground} />
</Pressable>
<Pressable
style={[
styles.newAgentButton,
{ backgroundColor: theme.colors.primary },
]}
onPress={handleCreateAgentMobile}
>
<Plus size={18} color={theme.colors.primaryForeground} />
<Text
style={[
styles.newAgentButtonText,
{ color: theme.colors.primaryForeground },
]}
>
New Agent
</Text>
</Pressable>
</View>
<AgentList
agents={agents}
isRefreshing={isRevalidating}
onRefresh={refreshAll}
selectedAgentId={selectedAgentId}
onAgentSelect={handleAgentSelectMobile}
/>
</Animated.View>
</GestureDetector>
</View>
);
}
// Desktop: no edge swipe, just show/hide based on isOpen
if (!isOpen) {
return null;
}
return (
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
<View style={styles.sidebarHeader}>
<Pressable style={styles.headerIconButton} onPress={handleSettingsDesktop}>
<Settings size={20} color={theme.colors.foreground} />
</Pressable>
<Pressable
style={[
styles.newAgentButton,
{ backgroundColor: theme.colors.primary },
]}
onPress={handleCreateAgentDesktop}
>
<Plus size={18} color={theme.colors.primaryForeground} />
<Text
style={[
styles.newAgentButtonText,
{ color: theme.colors.primaryForeground },
]}
>
New Agent
</Text>
</Pressable>
</View>
<AgentList
agents={agents}
isRefreshing={isRevalidating}
onRefresh={refreshAll}
selectedAgentId={selectedAgentId}
/>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0, 0, 0, 0.5)",
},
backdropPressable: {
flex: 1,
},
mobileSidebar: {
position: "absolute",
top: 0,
left: 0,
bottom: 0,
backgroundColor: theme.colors.background,
},
desktopSidebar: {
borderRightWidth: 1,
borderRightColor: theme.colors.border,
backgroundColor: theme.colors.background,
},
sidebarHeader: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[4],
paddingBottom: theme.spacing[2],
gap: theme.spacing[2],
},
headerIconButton: {
padding: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
},
newAgentButton: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: theme.spacing[2],
paddingVertical: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
},
newAgentButtonText: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
},
}));

View File

@@ -0,0 +1,30 @@
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";
interface SidebarState {
isOpen: boolean;
toggle: () => void;
open: () => void;
close: () => void;
}
export const useSidebarStore = create<SidebarState>()(
persist(
(set) => ({
isOpen: false,
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
}),
{
name: "sidebar-state",
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({ isOpen: state.isOpen }),
}
)
);
export function useSidebar() {
return useSidebarStore();
}