feat(ui): bring back make sidebar resizable with drag handle and persisted width (#10795)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { IpcRendererEvent } from 'electron';
|
import { IpcRendererEvent } from 'electron';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -9,7 +9,7 @@ import ChatSessionsContainer from '../ChatSessionsContainer';
|
|||||||
import { useChatContext } from '../../contexts/ChatContext';
|
import { useChatContext } from '../../contexts/ChatContext';
|
||||||
import { NavigationProvider, useNavigationContext } from './NavigationContext';
|
import { NavigationProvider, useNavigationContext } from './NavigationContext';
|
||||||
import { Navigation } from './NavigationPanel';
|
import { Navigation } from './NavigationPanel';
|
||||||
import { NAV_DIMENSIONS, Z_INDEX } from './constants';
|
import { Z_INDEX } from './constants';
|
||||||
import { cn } from '../../utils';
|
import { cn } from '../../utils';
|
||||||
import { UserInput } from '../../types/message';
|
import { UserInput } from '../../types/message';
|
||||||
|
|
||||||
@@ -54,7 +54,41 @@ const AppLayoutContent: React.FC<AppLayoutContentProps> = ({ activeSessions }) =
|
|||||||
return () => window.electron.off('fullscreen-change', handler);
|
return () => window.electron.off('fullscreen-change', handler);
|
||||||
}, [safeIsMacOS]);
|
}, [safeIsMacOS]);
|
||||||
|
|
||||||
const { isNavExpanded, setIsNavExpanded } = useNavigationContext();
|
const { isNavExpanded, setIsNavExpanded, navWidth, setNavWidth } = useNavigationContext();
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const isResizing = useRef(false);
|
||||||
|
const startX = useRef(0);
|
||||||
|
const startWidth = useRef(0);
|
||||||
|
|
||||||
|
const handleResizeMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
isResizing.current = true;
|
||||||
|
startX.current = e.clientX;
|
||||||
|
startWidth.current = navWidth;
|
||||||
|
setIsDragging(true);
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
[navWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isResizing.current) return;
|
||||||
|
setNavWidth(startWidth.current + (e.clientX - startX.current));
|
||||||
|
};
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
if (isResizing.current) {
|
||||||
|
isResizing.current = false;
|
||||||
|
setIsDragging(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [setNavWidth]);
|
||||||
|
|
||||||
if (!chatContext) {
|
if (!chatContext) {
|
||||||
throw new Error('AppLayoutContent must be used within ChatProvider');
|
throw new Error('AppLayoutContent must be used within ChatProvider');
|
||||||
@@ -93,14 +127,22 @@ const AppLayoutContent: React.FC<AppLayoutContentProps> = ({ activeSessions }) =
|
|||||||
<motion.div
|
<motion.div
|
||||||
key="nav"
|
key="nav"
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={{ width: isNavExpanded ? NAV_DIMENSIONS.NAV_WIDTH : 0 }}
|
animate={{ width: isNavExpanded ? navWidth : 0 }}
|
||||||
transition={{ type: 'spring', stiffness: 400, damping: 40 }}
|
transition={
|
||||||
|
isDragging ? { duration: 0 } : { type: 'spring', stiffness: 400, damping: 40 }
|
||||||
|
}
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
className="relative flex-shrink-0 overflow-hidden h-full p-2"
|
className="relative flex-shrink-0 overflow-hidden h-full p-2"
|
||||||
>
|
>
|
||||||
<div className="w-full h-full overflow-hidden rounded-xl border border-border-primary">
|
<div className="w-full h-full overflow-hidden rounded-xl border border-border-primary">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
</div>
|
</div>
|
||||||
|
{isNavExpanded && (
|
||||||
|
<div
|
||||||
|
className="absolute right-0 top-0 h-full w-2 cursor-col-resize hover:bg-border-primary/30 transition-colors"
|
||||||
|
onMouseDown={handleResizeMouseDown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Main content — no border / no card; just flows on the canvas. */}
|
{/* Main content — no border / no card; just flows on the canvas. */}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { NAV_DIMENSIONS } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the window is narrower than this many CSS pixels, we auto-collapse
|
* When the window is narrower than this many CSS pixels, we auto-collapse
|
||||||
@@ -15,9 +16,14 @@ import React, {
|
|||||||
*/
|
*/
|
||||||
const NARROW_WINDOW_THRESHOLD = 700;
|
const NARROW_WINDOW_THRESHOLD = 700;
|
||||||
|
|
||||||
|
export const MIN_NAV_WIDTH = 160;
|
||||||
|
export const MAX_NAV_WIDTH = 400;
|
||||||
|
|
||||||
interface NavigationContextValue {
|
interface NavigationContextValue {
|
||||||
isNavExpanded: boolean;
|
isNavExpanded: boolean;
|
||||||
setIsNavExpanded: (expanded: boolean) => void;
|
setIsNavExpanded: (expanded: boolean) => void;
|
||||||
|
navWidth: number;
|
||||||
|
setNavWidth: (width: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavigationContext = createContext<NavigationContextValue | null>(null);
|
const NavigationContext = createContext<NavigationContextValue | null>(null);
|
||||||
@@ -49,6 +55,23 @@ export const NavigationProvider: React.FC<NavigationProviderProps> = ({ children
|
|||||||
localStorage.setItem('navigation_expanded', String(expanded));
|
localStorage.setItem('navigation_expanded', String(expanded));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [navWidth, setNavWidthState] = useState<number>(() => {
|
||||||
|
const stored = localStorage.getItem('navigation_width');
|
||||||
|
if (stored) {
|
||||||
|
const parsed = parseInt(stored, 10);
|
||||||
|
if (!isNaN(parsed) && parsed >= MIN_NAV_WIDTH && parsed <= MAX_NAV_WIDTH) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NAV_DIMENSIONS.NAV_WIDTH;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setNavWidth = useCallback((width: number) => {
|
||||||
|
const clamped = Math.min(MAX_NAV_WIDTH, Math.max(MIN_NAV_WIDTH, width));
|
||||||
|
setNavWidthState(clamped);
|
||||||
|
localStorage.setItem('navigation_width', String(clamped));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isNavExpandedRef = useRef(isNavExpanded);
|
const isNavExpandedRef = useRef(isNavExpanded);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isNavExpandedRef.current = isNavExpanded;
|
isNavExpandedRef.current = isNavExpanded;
|
||||||
@@ -90,6 +113,8 @@ export const NavigationProvider: React.FC<NavigationProviderProps> = ({ children
|
|||||||
const value: NavigationContextValue = {
|
const value: NavigationContextValue = {
|
||||||
isNavExpanded,
|
isNavExpanded,
|
||||||
setIsNavExpanded,
|
setIsNavExpanded,
|
||||||
|
navWidth,
|
||||||
|
setNavWidth,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;
|
return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;
|
||||||
|
|||||||
Reference in New Issue
Block a user