| ... | @@ -61,6 +61,7 @@ import { | ... | @@ -61,6 +61,7 @@ import { |
| 61 | } from "./desktopUpdate.logic"; | 61 | } from "./desktopUpdate.logic"; |
| 62 | import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; | 62 | import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; |
| 63 | import { Button } from "./ui/button"; | 63 | import { Button } from "./ui/button"; |
| | 64 | import { Collapsible, CollapsibleContent } from "./ui/collapsible"; |
| 64 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 65 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 65 | import { | 66 | import { |
| 66 | SidebarContent, | 67 | SidebarContent, |
| ... | @@ -231,18 +232,20 @@ function SortableProjectItem({ | ... | @@ -231,18 +232,20 @@ function SortableProjectItem({ |
| 231 | const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = | 232 | const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = |
| 232 | useSortable({ id: projectId }); | 233 | useSortable({ id: projectId }); |
| 233 | return ( | 234 | return ( |
| 234 | <div | 235 | <li |
| 235 | ref={setNodeRef} | 236 | ref={setNodeRef} |
| 236 | style={{ | 237 | style={{ |
| 237 | transform: CSS.Translate.toString(transform), | 238 | transform: CSS.Translate.toString(transform), |
| 238 | transition, | 239 | transition, |
| 239 | }} | 240 | }} |
| 240 | className={`rounded-md ${ | 241 | className={`group/menu-item relative rounded-md ${ |
| 241 | isDragging ? "z-20 opacity-80" : "" | 242 | isDragging ? "z-20 opacity-80" : "" |
| 242 | } ${isOver && !isDragging ? "ring-1 ring-primary/40" : ""}`} | 243 | } ${isOver && !isDragging ? "ring-1 ring-primary/40" : ""}`} |
| | 244 | data-sidebar="menu-item" |
| | 245 | data-slot="sidebar-menu-item" |
| 243 | > | 246 | > |
| 244 | {children({ attributes, listeners })} | 247 | {children({ attributes, listeners })} |
| 245 | </div> | 248 | </li> |
| 246 | ); | 249 | ); |
| 247 | } | 250 | } |
| 248 | | 251 | |
| ... | @@ -294,14 +297,7 @@ export default function Sidebar() { | ... | @@ -294,14 +297,7 @@ export default function Sidebar() { |
| 294 | const renamingCommittedRef = useRef(false); | 297 | const renamingCommittedRef = useRef(false); |
| 295 | const renamingInputRef = useRef<HTMLInputElement | null>(null); | 298 | const renamingInputRef = useRef<HTMLInputElement | null>(null); |
| 296 | const dragInProgressRef = useRef(false); | 299 | const dragInProgressRef = useRef(false); |
| 297 | const suppressProjectClickFromGestureRef = useRef(false); | 300 | const suppressProjectClickAfterDragRef = useRef(false); |
| 298 | const suppressProjectClickResetTimerRef = useRef<number | null>(null); | | |
| 299 | const projectTitlePointerRef = useRef<{ | | |
| 300 | pointerId: number; | | |
| 301 | startX: number; | | |
| 302 | startY: number; | | |
| 303 | moved: boolean; | | |
| 304 | } | null>(null); | | |
| 305 | const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null); | 301 | const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null); |
| 306 | const shouldBrowseForProjectImmediately = isElectron; | 302 | const shouldBrowseForProjectImmediately = isElectron; |
| 307 | const shouldShowProjectPathEntry = addingProject && !shouldBrowseForProjectImmediately; | 303 | const shouldShowProjectPathEntry = addingProject && !shouldBrowseForProjectImmediately; |
| ... | @@ -861,64 +857,20 @@ export default function Sidebar() { | ... | @@ -861,64 +857,20 @@ export default function Sidebar() { |
| 861 | | 857 | |
| 862 | const handleProjectDragStart = useCallback((_event: DragStartEvent) => { | 858 | const handleProjectDragStart = useCallback((_event: DragStartEvent) => { |
| 863 | dragInProgressRef.current = true; | 859 | dragInProgressRef.current = true; |
| | 860 | suppressProjectClickAfterDragRef.current = true; |
| 864 | }, []); | 861 | }, []); |
| 865 | | 862 | |
| 866 | const handleProjectDragCancel = useCallback((_event: DragCancelEvent) => { | 863 | const handleProjectDragCancel = useCallback((_event: DragCancelEvent) => { |
| 867 | dragInProgressRef.current = false; | 864 | dragInProgressRef.current = false; |
| 868 | }, []); | 865 | }, []); |
| 869 | | 866 | |
| 870 | const handleProjectTitlePointerDownCapture = useCallback( | 867 | const handleProjectTitlePointerDownCapture = useCallback(() => { |
| 871 | (event: React.PointerEvent<HTMLButtonElement>) => { | 868 | suppressProjectClickAfterDragRef.current = false; |
| 872 | projectTitlePointerRef.current = { | | |
| 873 | pointerId: event.pointerId, | | |
| 874 | startX: event.clientX, | | |
| 875 | startY: event.clientY, | | |
| 876 | moved: false, | | |
| 877 | }; | | |
| 878 | }, | | |
| 879 | [], | | |
| 880 | ); | | |
| 881 | | | |
| 882 | const handleProjectTitlePointerMoveCapture = useCallback( | | |
| 883 | (event: React.PointerEvent<HTMLButtonElement>) => { | | |
| 884 | const pointer = projectTitlePointerRef.current; | | |
| 885 | if (!pointer || pointer.pointerId !== event.pointerId) return; | | |
| 886 | const movedX = Math.abs(event.clientX - pointer.startX); | | |
| 887 | const movedY = Math.abs(event.clientY - pointer.startY); | | |
| 888 | if (movedX > 3 || movedY > 3) { | | |
| 889 | pointer.moved = true; | | |
| 890 | } | | |
| 891 | }, | | |
| 892 | [], | | |
| 893 | ); | | |
| 894 | | | |
| 895 | const handleProjectTitlePointerUpCapture = useCallback( | | |
| 896 | (event: React.PointerEvent<HTMLButtonElement>) => { | | |
| 897 | const pointer = projectTitlePointerRef.current; | | |
| 898 | if (pointer?.pointerId === event.pointerId) { | | |
| 899 | if (pointer.moved) { | | |
| 900 | suppressProjectClickFromGestureRef.current = true; | | |
| 901 | if (suppressProjectClickResetTimerRef.current !== null) { | | |
| 902 | window.clearTimeout(suppressProjectClickResetTimerRef.current); | | |
| 903 | } | | |
| 904 | suppressProjectClickResetTimerRef.current = window.setTimeout(() => { | | |
| 905 | suppressProjectClickFromGestureRef.current = false; | | |
| 906 | suppressProjectClickResetTimerRef.current = null; | | |
| 907 | }, 0); | | |
| 908 | } | | |
| 909 | projectTitlePointerRef.current = null; | | |
| 910 | } | | |
| 911 | }, | | |
| 912 | [], | | |
| 913 | ); | | |
| 914 | | | |
| 915 | const handleProjectTitlePointerCancelCapture = useCallback(() => { | | |
| 916 | projectTitlePointerRef.current = null; | | |
| 917 | }, []); | 869 | }, []); |
| 918 | | 870 | |
| 919 | const handleProjectTitleClick = useCallback( | 871 | const handleProjectTitleClick = useCallback( |
| 920 | (event: React.MouseEvent<HTMLButtonElement>, projectId: ProjectId) => { | 872 | (event: React.MouseEvent<HTMLButtonElement>, projectId: ProjectId) => { |
| 921 | if (dragInProgressRef.current || suppressProjectClickFromGestureRef.current) { | 873 | if (dragInProgressRef.current || suppressProjectClickAfterDragRef.current) { |
| 922 | event.preventDefault(); | 874 | event.preventDefault(); |
| 923 | event.stopPropagation(); | 875 | event.stopPropagation(); |
| 924 | return; | 876 | return; |
| ... | @@ -940,14 +892,6 @@ export default function Sidebar() { | ... | @@ -940,14 +892,6 @@ export default function Sidebar() { |
| 940 | [toggleProject], | 892 | [toggleProject], |
| 941 | ); | 893 | ); |
| 942 | | 894 | |
| 943 | useEffect(() => { | | |
| 944 | return () => { | | |
| 945 | if (suppressProjectClickResetTimerRef.current !== null) { | | |
| 946 | window.clearTimeout(suppressProjectClickResetTimerRef.current); | | |
| 947 | } | | |
| 948 | }; | | |
| 949 | }, []); | | |
| 950 | | | |
| 951 | useEffect(() => { | 895 | useEffect(() => { |
| 952 | const onWindowKeyDown = (event: KeyboardEvent) => { | 896 | const onWindowKeyDown = (event: KeyboardEvent) => { |
| 953 | const activeThread = routeThreadId | 897 | const activeThread = routeThreadId |
| ... | @@ -1318,7 +1262,10 @@ export default function Sidebar() { | ... | @@ -1318,7 +1262,10 @@ export default function Sidebar() { |
| 1318 | return ( | 1262 | return ( |
| 1319 | <SortableProjectItem key={project.id} projectId={project.id}> | 1263 | <SortableProjectItem key={project.id} projectId={project.id}> |
| 1320 | {(dragHandleProps) => ( | 1264 | {(dragHandleProps) => ( |
| 1321 | <SidebarMenuItem className="group/collapsible"> | 1265 | <Collapsible |
| | 1266 | className="group/collapsible" |
| | 1267 | open={project.expanded} |
| | 1268 | > |
| 1322 | <div className="group/project-header relative"> | 1269 | <div className="group/project-header relative"> |
| 1323 | <SidebarMenuButton | 1270 | <SidebarMenuButton |
| 1324 | size="sm" | 1271 | size="sm" |
| ... | @@ -1326,9 +1273,6 @@ export default function Sidebar() { | ... | @@ -1326,9 +1273,6 @@ export default function Sidebar() { |
| 1326 | {...dragHandleProps.attributes} | 1273 | {...dragHandleProps.attributes} |
| 1327 | {...dragHandleProps.listeners} | 1274 | {...dragHandleProps.listeners} |
| 1328 | onPointerDownCapture={handleProjectTitlePointerDownCapture} | 1275 | onPointerDownCapture={handleProjectTitlePointerDownCapture} |
| 1329 | onPointerMoveCapture={handleProjectTitlePointerMoveCapture} | | |
| 1330 | onPointerUpCapture={handleProjectTitlePointerUpCapture} | | |
| 1331 | onPointerCancelCapture={handleProjectTitlePointerCancelCapture} | | |
| 1332 | onClick={(event) => handleProjectTitleClick(event, project.id)} | 1276 | onClick={(event) => handleProjectTitleClick(event, project.id)} |
| 1333 | onKeyDown={(event) => handleProjectTitleKeyDown(event, project.id)} | 1277 | onKeyDown={(event) => handleProjectTitleKeyDown(event, project.id)} |
| 1334 | onContextMenu={(event) => { | 1278 | onContextMenu={(event) => { |
| ... | @@ -1379,15 +1323,8 @@ export default function Sidebar() { | ... | @@ -1379,15 +1323,8 @@ export default function Sidebar() { |
| 1379 | </Tooltip> | 1323 | </Tooltip> |
| 1380 | </div> | 1324 | </div> |
| 1381 | | 1325 | |
| 1382 | <div | 1326 | <CollapsibleContent keepMounted> |
| 1383 | className={`grid transition-[grid-template-rows,opacity] duration-200 ease-out ${ | 1327 | <SidebarMenuSub className="mx-1 my-0 w-full translate-x-0 gap-0 px-1.5 py-0"> |
| 1384 | project.expanded | | |
| 1385 | ? "grid-rows-[1fr] opacity-100" | | |
| 1386 | : "pointer-events-none grid-rows-[0fr] opacity-0" | | |
| 1387 | }`} | | |
| 1388 | > | | |
| 1389 | <div className="min-h-0 overflow-hidden"> | | |
| 1390 | <SidebarMenuSub className="mx-1 my-0 w-full translate-x-0 gap-0 px-1.5 py-0"> | | |
| 1391 | {visibleThreads.map((thread) => { | 1328 | {visibleThreads.map((thread) => { |
| 1392 | const isActive = routeThreadId === thread.id; | 1329 | const isActive = routeThreadId === thread.id; |
| 1393 | const threadStatus = resolveThreadStatusPill({ | 1330 | const threadStatus = resolveThreadStatusPill({ |
| ... | @@ -1558,9 +1495,8 @@ export default function Sidebar() { | ... | @@ -1558,9 +1495,8 @@ export default function Sidebar() { |
| 1558 | </SidebarMenuSubItem> | 1495 | </SidebarMenuSubItem> |
| 1559 | )} | 1496 | )} |
| 1560 | </SidebarMenuSub> | 1497 | </SidebarMenuSub> |
| 1561 | </div> | 1498 | </CollapsibleContent> |
| 1562 | </div> | 1499 | </Collapsible> |
| 1563 | </SidebarMenuItem> | | |
| 1564 | )} | 1500 | )} |
| 1565 | </SortableProjectItem> | 1501 | </SortableProjectItem> |
| 1566 | ); | 1502 | ); |