From 6ab976a82bdc12eccc91e7e9ee5d14832a9db91c Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 22 Sep 2025 09:47:31 +0100 Subject: [PATCH] Add pinnable right-side stack panel with hover functionality --- dicom-viewer/src/App.tsx | 110 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index d09206e..4458625 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -176,6 +176,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer // Control visibility of the global top stack bar (hidden by default) const [topBarVisible, setTopBarVisible] = useState(false); + // Right-side stack panel pinned state (false = auto-hide, true = pinned open) + const [stackPanelPinned, setStackPanelPinned] = useState(false); + // Local hover state for temporary reveal when not pinned + const [stackPanelHover, setStackPanelHover] = useState(false); + const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState(null); const [openDropdownIdx, setOpenDropdownIdx] = useState(null); @@ -3107,6 +3112,111 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer Show Stacks + {/* Right-side stack panel (pinnable, shows on hover when not pinned) */} +
setStackPanelHover(true)} + onMouseLeave={() => setStackPanelHover(false)} + style={{ + position: "absolute", + right: 0, + top: 12, + height: 'calc(100% - 24px)', + width: 320, + zIndex: 2100, + display: 'flex', + alignItems: 'stretch', + pointerEvents: 'auto', + }} + > + {/* Panel container: translateX to hide when not pinned and not hovered */} +
+
+
Stacks
+
+ + +
+
+ +
+ {availableStacks.length === 0 ? ( +
No stacks available
+ ) : ( +
+ {availableStacks.map((stack, idx) => { + const imageIds = Array.isArray(stack.imageIds) ? stack.imageIds : []; + const label = stack.name || (stack.caseId ? `Case: ${stack.caseId}` : `Stack ${idx + 1}`); + const count = imageIds.length; + const invalid = !Array.isArray(imageIds) || count === 0; + const isSelected = selectedStackIdx.includes(idx); + return ( +
{ + if (invalid) return; + e.dataTransfer.setData('text/stack-idx', String(idx)); + e.dataTransfer.effectAllowed = 'copy'; + }} + onClick={() => { if (!invalid) handleLoadStack(0, idx); }} + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '8px 10px', + background: isSelected ? '#1976d2' : 'transparent', + color: isSelected ? '#fff' : '#ddd', + borderRadius: 6, + cursor: invalid ? 'not-allowed' : 'pointer', + }} + > +
+
{label}
+
{count} img{count === 1 ? '' : 's'}
+
+
{stack.studyId || ''}
+
+ ); + })} +
+ )} +
+
+ + {/* Visible handle when panel is hidden to allow hover */} + {!stackPanelPinned && ( +
+
+
+ )} +
+
{/* Side menu toggle button */}