From e29bce42e27f472d4b669729626d9ef9996d5754 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 27 Apr 2026 12:02:54 +0100 Subject: [PATCH] Implement resizable stack panel with min/max width constraints and update side menu button positioning --- dicom-viewer/src/App.tsx | 85 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index cf00507..f537572 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -187,6 +187,19 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer // Right-side stack panel open state (toggled by button) const [stackPanelOpen, setStackPanelOpen] = useState(false); const [showOpenButtonHover, setShowOpenButtonHover] = useState(false); + const [stackPanelWidth, setStackPanelWidth] = useState(320); + const stackPanelResizeRef = useRef<{ resizing: boolean; startX: number; startWidth: number }>({ + resizing: false, + startX: 0, + startWidth: 320, + }); + + const STACK_PANEL_MIN_WIDTH = 240; + const STACK_PANEL_MAX_WIDTH = 720; + + const SIDE_MENU_WIDTH = 220; + const SIDE_MENU_BUTTON_SIZE = 40; + const SIDE_MENU_BUTTON_PEEK = 14; const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState(null); const [openDropdownIdx, setOpenDropdownIdx] = useState(null); @@ -1296,6 +1309,52 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const [sideMenuOpen, setSideMenuOpen] = useState(false); const [folderLoadMode, setFolderLoadMode] = useState<'add' | 'replace'>('add'); const folderInputRef = useRef(null); + + const beginStackPanelResize = useCallback((event: React.MouseEvent) => { + if (!stackPanelOpen) return; + event.preventDefault(); + event.stopPropagation(); + stackPanelResizeRef.current = { + resizing: true, + startX: event.clientX, + startWidth: stackPanelWidth, + }; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + }, [stackPanelOpen, stackPanelWidth]); + + useEffect(() => { + const onMouseMove = (event: MouseEvent) => { + if (!stackPanelResizeRef.current.resizing) return; + const delta = stackPanelResizeRef.current.startX - event.clientX; + const nextWidth = Math.max( + STACK_PANEL_MIN_WIDTH, + Math.min(STACK_PANEL_MAX_WIDTH, stackPanelResizeRef.current.startWidth + delta) + ); + setStackPanelWidth(nextWidth); + }; + + const onMouseUp = () => { + if (!stackPanelResizeRef.current.resizing) return; + stackPanelResizeRef.current.resizing = false; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + }, []); + + const sideMenuButtonLeft = sideMenuOpen + ? SIDE_MENU_WIDTH - (SIDE_MENU_BUTTON_SIZE / 2) + : -SIDE_MENU_BUTTON_SIZE + SIDE_MENU_BUTTON_PEEK; + useEffect(() => { // Some browsers don't accept non-standard props via JSX typings; set attributes directly. try { @@ -3947,10 +4006,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer position: "absolute", top: "50%", transform: "translateY(-50%)", - left: sideMenuOpen ? 250 : -20, // Slide right when open + left: sideMenuButtonLeft, zIndex: 200, - width: 40, - height: 40, + width: SIDE_MENU_BUTTON_SIZE, + height: SIDE_MENU_BUTTON_SIZE, background: "#222", color: "#fff", border: "none", @@ -3974,7 +4033,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer position: "absolute", top: 0, left: 0, - width: 220, + width: SIDE_MENU_WIDTH, height: "100%", background: "#222", color: "#fff", @@ -4364,7 +4423,23 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer {/* Right-side panel occupies layout space when visible; when closed width is 0 and it doesn't catch pointer events */} -
+
+ {stackPanelOpen && ( +
+ )} {/* Reuse the existing right-side panel markup but ensure pointerEvents are enabled inside */}
{/* Panel (same content as before) */}