Skip to content
Merged
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
2 changes: 2 additions & 0 deletions ui/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ const mockElectron = {
getAllowedExtensions: vi.fn().mockResolvedValue([]),
platform: 'darwin',
createChatWindow: vi.fn(),
getSetting: vi.fn().mockResolvedValue(null),
setSetting: vi.fn().mockResolvedValue(undefined),
};

// Mock appConfig
Expand Down
114 changes: 105 additions & 9 deletions ui/desktop/src/components/Layout/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { motion } from 'framer-motion';
import { Menu } from 'lucide-react';
Expand Down Expand Up @@ -34,6 +34,82 @@ const AppLayoutContent: React.FC<AppLayoutContentProps> = ({ activeSessions }) =
isCondensedIconOnly,
} = useNavigationContext();

const [navWidth, setNavWidth] = useState<number | null>(null);
const navWidthRef = useRef<number | null>(null);

useEffect(() => {
window.electron.getSetting('navExpandedWidth').then((delta) => {
if (delta !== null) {
setNavWidth(
Math.min(
NAV_DIMENSIONS.MAX_NAV_WIDTH,
Math.max(NAV_DIMENSIONS.MIN_NAV_WIDTH, NAV_DIMENSIONS.CONDENSED_WIDTH + delta)
)
);
}
});
}, []);

const isResizable =
!isHorizontalNav && !isCondensedIconOnly && effectiveNavigationMode === 'push' && isNavExpanded;

const dragStateRef = useRef<{ startX: number; startWidth: number; direction: 1 | -1 } | null>(
null
);
const navRef = useRef<HTMLDivElement>(null);

const onMouseMove = useCallback((e: MouseEvent) => {
if (!dragStateRef.current) return;
const delta = (e.clientX - dragStateRef.current.startX) * dragStateRef.current.direction;
const newWidth = Math.min(
NAV_DIMENSIONS.MAX_NAV_WIDTH,
Math.max(NAV_DIMENSIONS.MIN_NAV_WIDTH, dragStateRef.current.startWidth + delta)
);
navWidthRef.current = newWidth;
setNavWidth(newWidth);
}, []);

const onMouseUp = useCallback(() => {
dragStateRef.current = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
if (navWidthRef.current !== null) {
window.electron.setSetting(
'navExpandedWidth',
navWidthRef.current - NAV_DIMENSIONS.CONDENSED_WIDTH
);
}
}, [onMouseMove]);

const onHandleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
const currentWidth =
navRef.current?.getBoundingClientRect().width ?? NAV_DIMENSIONS.CONDENSED_WIDTH;
dragStateRef.current = {
startX: e.clientX,
startWidth: currentWidth,
direction: navigationPosition === 'right' ? -1 : 1,
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel sidebar drag when pointer leaves the window

The drag lifecycle only ends on window's mouseup, so if the user starts resizing and releases the mouse outside the app window, cleanup never runs: dragStateRef stays set, userSelect/cursor overrides remain, and moving the mouse back over the app can keep resizing unexpectedly until another in-window mouseup happens. This is a user-visible interaction regression for common edge-drags and should be handled by also terminating drag on window blur/leave (or by using pointer capture).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jh-block these are edge cases I think we can skip it for now

},
[navigationPosition, onMouseMove, onMouseUp]
);

useEffect(() => {
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
Comment thread
Abhijay007 marked this conversation as resolved.
}, [onMouseMove, onMouseUp]);

if (!chatContext) {
throw new Error('AppLayoutContent must be used within ChatProvider');
}
Expand Down Expand Up @@ -134,17 +210,18 @@ const AppLayoutContent: React.FC<AppLayoutContentProps> = ({ activeSessions }) =
{/* Push mode navigation (inline) with animation */}
{effectiveNavigationMode === 'push' && (
<motion.div
ref={navRef}
key="push-nav"
initial={false}
animate={{
width: isHorizontalNav
? '100%'
: isNavExpanded
? effectiveNavigationStyle === 'expanded'
? '30%'
? (navWidth ?? '30%')
: isCondensedIconOnly
? NAV_DIMENSIONS.CONDENSED_ICON_ONLY_WIDTH
: NAV_DIMENSIONS.CONDENSED_WIDTH
: (navWidth ?? NAV_DIMENSIONS.CONDENSED_WIDTH)
: 0,
height: isHorizontalNav
? isNavExpanded
Expand All @@ -161,7 +238,9 @@ const AppLayoutContent: React.FC<AppLayoutContentProps> = ({ activeSessions }) =
}}
style={{
maxWidth:
!isHorizontalNav && effectiveNavigationStyle === 'expanded' ? '400px' : undefined,
!isHorizontalNav && effectiveNavigationStyle === 'expanded'
? NAV_DIMENSIONS.MAX_NAV_WIDTH
: undefined,
minWidth:
!isHorizontalNav && effectiveNavigationStyle === 'condensed' && isNavExpanded
? isCondensedIconOnly
Expand All @@ -177,14 +256,31 @@ const AppLayoutContent: React.FC<AppLayoutContentProps> = ({ activeSessions }) =
height: !isHorizontalNav ? '100%' : undefined,
}}
className={cn(
'flex-shrink-0',
effectiveNavigationStyle === 'condensed' && !isHorizontalNav
? 'overflow-visible'
: 'overflow-hidden',
'relative flex-shrink-0 overflow-visible',
isHorizontalNav ? 'w-full' : 'h-full'
)}
>
<Navigation />
<div
className={cn(
'w-full h-full',
effectiveNavigationStyle === 'condensed' && !isHorizontalNav
? 'overflow-visible'
: 'overflow-hidden'
)}
>
<Navigation />
</div>
{isResizable && (
<div
onMouseDown={onHandleMouseDown}
className={cn(
'absolute top-0 w-2 h-full z-20 cursor-col-resize group flex items-center justify-center',
navigationPosition === 'right' ? '-left-1' : '-right-1'
)}
>
<div className="w-px h-full bg-border-subtle opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
)}
</motion.div>
)}

Expand Down
4 changes: 4 additions & 0 deletions ui/desktop/src/components/Layout/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export const NAV_DIMENSIONS = {
EXPANDED_HEIGHT: 180,
/** Height of condensed navigation (horizontal mode) */
CONDENSED_HEIGHT: 46,
/** Minimum width when resizing the navigation panel */
MIN_NAV_WIDTH: 200,
/** Maximum width when resizing the navigation panel */
MAX_NAV_WIDTH: 600,
} as const;

export const Z_INDEX = {
Expand Down
1 change: 1 addition & 0 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,7 @@ const validSettingKeys: Set<string> = new Set([
'showPricing',
'sessionSharing',
'seenAnnouncementIds',
'navExpandedWidth',
]);

ipcMain.handle('set-setting', (_event, key: SettingKey, value: unknown) => {
Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/src/utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface Settings {
showPricing: boolean;
sessionSharing: SessionSharingConfig;
seenAnnouncementIds: string[];
navExpandedWidth: number | null;
}

export type SettingKey = keyof Settings;
Expand Down Expand Up @@ -85,6 +86,7 @@ export const defaultSettings: Settings = {
baseUrl: '',
},
seenAnnouncementIds: [],
navExpandedWidth: null,
};

export function getKeyboardShortcuts(settings: Settings): KeyboardShortcuts {
Expand Down
Loading