Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 7 additions & 16 deletions client/src/components/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
startTransition,
} from 'react';
import { useRecoilValue } from 'recoil';
import { motion } from 'framer-motion';
import { Skeleton, useMediaQuery } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
Expand Down Expand Up @@ -278,24 +277,16 @@ const Nav = memo(
);
}

// Desktop: Inline sidebar with width transition
return (
<div
className="flex-shrink-0 overflow-hidden"
style={{ width: navVisible ? sidebarWidth : 0, transition: 'width 0.2s ease-out' }}
data-testid="nav"
className={cn(
'nav h-full bg-surface-primary-alt',
navVisible && 'active',
!navVisible && 'hidden',
)}
>
<motion.div
data-testid="nav"
className={cn('nav h-full bg-surface-primary-alt', navVisible && 'active')}
style={{ width: sidebarWidth }}
initial={false}
animate={{
x: navVisible ? 0 : -sidebarWidth,
}}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
{sidebarContent}
</motion.div>
{sidebarContent}
</div>
);
},
Expand Down
110 changes: 110 additions & 0 deletions client/src/components/Nav/ResizableNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useRef, useCallback, useEffect } from 'react';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { ResizablePanelGroup, ResizablePanel, ResizableHandleAlt } from '@librechat/client';
import type { ContextType } from '~/common';
import Nav, { NAV_WIDTH } from './Nav';
import MobileNav from './MobileNav';

interface ResizableNavProps {
navVisible: boolean;
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
isSmallScreen: boolean;
children: (context: ContextType) => React.ReactNode;
}

export default function ResizableNav({
navVisible,
setNavVisible,
isSmallScreen,
children,
}: ResizableNavProps) {
const navPanelRef = useRef<ImperativePanelHandle>(null);

// Sync panel state with navVisible prop on mount and when switching to desktop
useEffect(() => {
if (!isSmallScreen && navPanelRef.current) {
const isCollapsed = navPanelRef.current.isCollapsed();

// Only update if there's a mismatch between prop and panel state
if (navVisible && isCollapsed) {
navPanelRef.current.expand();
} else if (!navVisible && !isCollapsed) {
navPanelRef.current.collapse();
}
}
}, [navVisible, isSmallScreen]);

// Custom setNavVisible that also controls the resizable panel
const handleSetNavVisible = useCallback(
(value: boolean | ((prev: boolean) => boolean)) => {
setNavVisible((prev) => {
const newValue = typeof value === 'function' ? value(prev) : value;

// Control the resizable panel on desktop
if (!isSmallScreen && navPanelRef.current) {
if (newValue) {
navPanelRef.current.expand();
} else {
navPanelRef.current.collapse();
}
}

if (typeof value === 'boolean') {
localStorage.setItem('navVisible', JSON.stringify(newValue));
}
return newValue;
});
},
[isSmallScreen, setNavVisible],
);

const context: ContextType = { navVisible, setNavVisible: handleSetNavVisible };

// Mobile: Original overlay behavior
if (isSmallScreen) {
return (
<>
<Nav navVisible={navVisible} setNavVisible={handleSetNavVisible} />
<div
className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden"
style={{
transform: navVisible ? `translateX(${NAV_WIDTH.MOBILE}px)` : 'translateX(0)',
transition: 'transform 0.2s ease-out',
}}
>
<MobileNav navVisible={navVisible} setNavVisible={handleSetNavVisible} />
{children(context)}
</div>
</>
);
}

// Desktop: Resizable panel layout
return (
<ResizablePanelGroup direction="horizontal" className="h-full w-full" autoSaveId="nav-layout">
<ResizablePanel
ref={navPanelRef}
defaultSize={navVisible ? 15 : 0}
minSize={15}
maxSize={35}
collapsible={true}
collapsedSize={0}
onCollapse={() => {
handleSetNavVisible(false);
}}
onExpand={() => {
handleSetNavVisible(true);
}}
>
<Nav navVisible={navVisible} setNavVisible={handleSetNavVisible} />
</ResizablePanel>
<ResizableHandleAlt withHandle className="w-px" />
<ResizablePanel defaultSize={85} minSize={65}>
<div className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden">
<MobileNav navVisible={navVisible} setNavVisible={handleSetNavVisible} />
{children(context)}
</div>
</ResizablePanel>
</ResizablePanelGroup>
);
}
1 change: 1 addition & 0 deletions client/src/components/Nav/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export { default as MobileNav } from './MobileNav';
export { default as Nav, NAV_WIDTH } from './Nav';
export { default as NavLink } from './NavLink';
export { default as NewChat } from './NewChat';
export { default as ResizableNav } from './ResizableNav';
export { default as SearchBar } from './SearchBar';
export { default as Settings } from './Settings';
25 changes: 7 additions & 18 deletions client/src/routes/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { useMediaQuery } from '@librechat/client';
import type { ContextType } from '~/common';
import {
useSearchEnabled,
useAssistantsMap,
Expand All @@ -17,7 +16,7 @@ import {
FileMapContext,
} from '~/Providers';
import { useUserTermsQuery, useGetStartupConfig } from '~/data-provider';
import { Nav, MobileNav, NAV_WIDTH } from '~/components/Nav';
import { ResizableNav } from '~/components/Nav';
import { TermsAndConditionsModal } from '~/components/ui';
import { useHealthCheck } from '~/data-provider';
import { Banner } from '~/components/Banners';
Expand Down Expand Up @@ -75,23 +74,13 @@ export default function Root() {
<Banner onHeightChange={setBannerHeight} />
<div className="flex" style={{ height: `calc(100dvh - ${bannerHeight}px)` }}>
<div className="relative z-0 flex h-full w-full overflow-hidden">
<Nav navVisible={navVisible} setNavVisible={setNavVisible} />
<div
className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden"
style={
isSmallScreen
? {
transform: navVisible
? `translateX(${NAV_WIDTH.MOBILE}px)`
: 'translateX(0)',
transition: 'transform 0.2s ease-out',
}
: undefined
}
<ResizableNav
navVisible={navVisible}
setNavVisible={setNavVisible}
isSmallScreen={isSmallScreen}
>
<MobileNav navVisible={navVisible} setNavVisible={setNavVisible} />
<Outlet context={{ navVisible, setNavVisible } satisfies ContextType} />
</div>
{(context) => <Outlet context={context} />}
</ResizableNav>
</div>
</div>
</PromptGroupsProvider>
Expand Down