From a6b22ec1c9b247536111dd2c2e8682f9b3f550c5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Mar 2026 11:39:29 -0800 Subject: [PATCH] Fix project drag clicks and sidebar collapse/scroll behavior - prevent accidental project toggles after drag operations - switch project rows to Collapsible for stable expand/collapse animation - add optional hidden scrollbar mode to ScrollArea and apply it in SidebarContent --- apps/web/src/components/Sidebar.tsx | 102 ++++----------------- apps/web/src/components/ui/collapsible.tsx | 2 +- apps/web/src/components/ui/scroll-area.tsx | 13 ++- apps/web/src/components/ui/sidebar.tsx | 4 +- 4 files changed, 32 insertions(+), 89 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 44730796594cd0226ebba952c4436f61c17741a5..914480d077430c24ac459cdc365141176bfd068f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -61,6 +61,7 @@ import { } from "./desktopUpdate.logic"; import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; import { Button } from "./ui/button"; +import { Collapsible, CollapsibleContent } from "./ui/collapsible"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { SidebarContent, @@ -231,18 +232,20 @@ function SortableProjectItem({ const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = useSortable({ id: projectId }); return ( -
{children({ attributes, listeners })} -
+ ); } @@ -294,14 +297,7 @@ export default function Sidebar() { const renamingCommittedRef = useRef(false); const renamingInputRef = useRef(null); const dragInProgressRef = useRef(false); - const suppressProjectClickFromGestureRef = useRef(false); - const suppressProjectClickResetTimerRef = useRef(null); - const projectTitlePointerRef = useRef<{ - pointerId: number; - startX: number; - startY: number; - moved: boolean; - } | null>(null); + const suppressProjectClickAfterDragRef = useRef(false); const [desktopUpdateState, setDesktopUpdateState] = useState(null); const shouldBrowseForProjectImmediately = isElectron; const shouldShowProjectPathEntry = addingProject && !shouldBrowseForProjectImmediately; @@ -861,64 +857,20 @@ export default function Sidebar() { const handleProjectDragStart = useCallback((_event: DragStartEvent) => { dragInProgressRef.current = true; + suppressProjectClickAfterDragRef.current = true; }, []); const handleProjectDragCancel = useCallback((_event: DragCancelEvent) => { dragInProgressRef.current = false; }, []); - const handleProjectTitlePointerDownCapture = useCallback( - (event: React.PointerEvent) => { - projectTitlePointerRef.current = { - pointerId: event.pointerId, - startX: event.clientX, - startY: event.clientY, - moved: false, - }; - }, - [], - ); - - const handleProjectTitlePointerMoveCapture = useCallback( - (event: React.PointerEvent) => { - const pointer = projectTitlePointerRef.current; - if (!pointer || pointer.pointerId !== event.pointerId) return; - const movedX = Math.abs(event.clientX - pointer.startX); - const movedY = Math.abs(event.clientY - pointer.startY); - if (movedX > 3 || movedY > 3) { - pointer.moved = true; - } - }, - [], - ); - - const handleProjectTitlePointerUpCapture = useCallback( - (event: React.PointerEvent) => { - const pointer = projectTitlePointerRef.current; - if (pointer?.pointerId === event.pointerId) { - if (pointer.moved) { - suppressProjectClickFromGestureRef.current = true; - if (suppressProjectClickResetTimerRef.current !== null) { - window.clearTimeout(suppressProjectClickResetTimerRef.current); - } - suppressProjectClickResetTimerRef.current = window.setTimeout(() => { - suppressProjectClickFromGestureRef.current = false; - suppressProjectClickResetTimerRef.current = null; - }, 0); - } - projectTitlePointerRef.current = null; - } - }, - [], - ); - - const handleProjectTitlePointerCancelCapture = useCallback(() => { - projectTitlePointerRef.current = null; + const handleProjectTitlePointerDownCapture = useCallback(() => { + suppressProjectClickAfterDragRef.current = false; }, []); const handleProjectTitleClick = useCallback( (event: React.MouseEvent, projectId: ProjectId) => { - if (dragInProgressRef.current || suppressProjectClickFromGestureRef.current) { + if (dragInProgressRef.current || suppressProjectClickAfterDragRef.current) { event.preventDefault(); event.stopPropagation(); return; @@ -940,14 +892,6 @@ export default function Sidebar() { [toggleProject], ); - useEffect(() => { - return () => { - if (suppressProjectClickResetTimerRef.current !== null) { - window.clearTimeout(suppressProjectClickResetTimerRef.current); - } - }; - }, []); - useEffect(() => { const onWindowKeyDown = (event: KeyboardEvent) => { const activeThread = routeThreadId @@ -1318,7 +1262,10 @@ export default function Sidebar() { return ( {(dragHandleProps) => ( - +
handleProjectTitleClick(event, project.id)} onKeyDown={(event) => handleProjectTitleKeyDown(event, project.id)} onContextMenu={(event) => { @@ -1379,15 +1323,8 @@ export default function Sidebar() {
-
-
- + + {visibleThreads.map((thread) => { const isActive = routeThreadId === thread.id; const threadStatus = resolveThreadStatusPill({ @@ -1558,9 +1495,8 @@ export default function Sidebar() { )} -
-
-
+ + )}
); diff --git a/apps/web/src/components/ui/collapsible.tsx b/apps/web/src/components/ui/collapsible.tsx index 180d0a95b38f4dfdfb7ef8836e7de6c80300d050..e5f3db03c3f5e8bedcc3ac7ba1904656b6cded6d 100644 --- a/apps/web/src/components/ui/collapsible.tsx +++ b/apps/web/src/components/ui/collapsible.tsx @@ -22,7 +22,7 @@ function CollapsiblePanel({ className, ...props }: CollapsiblePrimitive.Panel.Pr return ( @@ -22,14 +24,19 @@ function ScrollArea({ scrollFade && "mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] [--fade-size:1.5rem]", scrollbarGutter && "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5", + hideScrollbars && "[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", )} data-slot="scroll-area-viewport" > {children} - - - + {!hideScrollbars && ( + <> + + + + + )} ); } diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 16b9ec4ed377c1d54298740c94108afa1d5caf17..3876d35132505cb7205625316926f49d9ec72bb2 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -651,10 +651,10 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps) { return ( - +