feat(ui): animate sidebar open/close
Sidebars now slide open/closed with a 200ms cubic-bezier(0.22, 1, 0.36, 1) transition on width. Inline transitionProperty/Duration/TimingFunction are used directly on the aside so the change is browser-driven rather than relying on Tailwind arbitrary classes that didn't reliably trigger the width transition. Inner sidebar content is pinned to the open width with shrink-0 so it stays full-size while the outer aside collapses around it, eliminating the vertical jitter that came from content reflowing when its container shrank. The chat-frame keeps a constant 1px border and 10px radius on all four sides — only the left/right border color toggles between border/50 and transparent — so no layout reflow happens when sidebars open. Mask-corner overlays are always mounted and animate their left/right offset and opacity in lockstep with the sidebars instead of mounting/unmounting.
This commit is contained in:
@@ -36,7 +36,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children, cl
|
||||
return;
|
||||
}
|
||||
|
||||
sidebar.style.setProperty('--oc-left-sidebar-width', `${nextWidth}px`);
|
||||
sidebar.style.width = `${nextWidth}px`;
|
||||
sidebar.style.minWidth = `${nextWidth}px`;
|
||||
sidebar.style.maxWidth = `${nextWidth}px`;
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -56,10 +58,11 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children, cl
|
||||
return null;
|
||||
}
|
||||
|
||||
const appliedWidth = isOpen ? Math.min(
|
||||
const openWidth = Math.min(
|
||||
SIDEBAR_MAX_WIDTH,
|
||||
Math.max(SIDEBAR_MIN_WIDTH, sidebarWidth || SIDEBAR_CONTENT_WIDTH)
|
||||
) : 0;
|
||||
);
|
||||
const appliedWidth = isOpen ? openWidth : 0;
|
||||
|
||||
const handlePointerDown = (event: React.PointerEvent) => {
|
||||
if (!isOpen) {
|
||||
@@ -114,22 +117,25 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children, cl
|
||||
setSidebarWidth(finalWidth);
|
||||
};
|
||||
|
||||
const currentWidth = isResizing ? (resizingWidthRef.current ?? appliedWidth) : appliedWidth;
|
||||
|
||||
return (
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
className={cn(
|
||||
'relative flex h-full overflow-hidden border-r border-border/40',
|
||||
'relative flex h-full overflow-hidden border-r border-border/40 will-change-[width] motion-reduce:transition-none',
|
||||
'bg-sidebar',
|
||||
isResizing ? 'transition-none' : 'transition-[width] duration-300 ease-in-out',
|
||||
!isOpen && 'border-r-0',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
width: 'var(--oc-left-sidebar-width)',
|
||||
minWidth: 'var(--oc-left-sidebar-width)',
|
||||
maxWidth: 'var(--oc-left-sidebar-width)',
|
||||
['--oc-left-sidebar-width' as string]: `${isResizing ? (resizingWidthRef.current ?? appliedWidth) : appliedWidth}px`,
|
||||
width: `${currentWidth}px`,
|
||||
minWidth: `${currentWidth}px`,
|
||||
maxWidth: `${currentWidth}px`,
|
||||
overflowX: 'clip',
|
||||
transitionProperty: isResizing ? 'none' : 'width, min-width, max-width',
|
||||
transitionDuration: '200ms',
|
||||
transitionTimingFunction: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
}}
|
||||
aria-hidden={!isOpen || appliedWidth === 0}
|
||||
>
|
||||
@@ -150,11 +156,11 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children, cl
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex h-full flex-col transition-opacity duration-300 ease-in-out',
|
||||
'relative z-10 flex h-full shrink-0 flex-col transition-opacity duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
|
||||
isResizing && 'pointer-events-none',
|
||||
!isOpen && 'pointer-events-none select-none opacity-0'
|
||||
)}
|
||||
style={{ width: 'var(--oc-left-sidebar-width)', overflowX: 'hidden' }}
|
||||
style={{ width: `${openWidth}px`, overflowX: 'hidden' }}
|
||||
aria-hidden={!isOpen}
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
|
||||
Reference in New Issue
Block a user