Add pinnable right-side stack panel with hover functionality
This commit is contained in:
@@ -176,6 +176,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
// Control visibility of the global top stack bar (hidden by default)
|
// Control visibility of the global top stack bar (hidden by default)
|
||||||
const [topBarVisible, setTopBarVisible] = useState(false);
|
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<number | null>(null);
|
const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState<number | null>(null);
|
||||||
const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
|
const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
@@ -3107,6 +3112,111 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
Show Stacks
|
Show Stacks
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Right-side stack panel (pinnable, shows on hover when not pinned) */}
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => 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 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
background: '#1f2428',
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 2px 14px rgba(0,0,0,0.45)',
|
||||||
|
borderLeft: '1px solid #2f3336',
|
||||||
|
transform: stackPanelPinned || stackPanelHover ? 'translateX(0)' : 'translateX(96%)',
|
||||||
|
transition: 'transform 220ms ease',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', borderBottom: '1px solid #2b2f31' }}>
|
||||||
|
<div style={{ fontWeight: 700 }}>Stacks</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
<button
|
||||||
|
title={stackPanelPinned ? 'Unpin panel' : 'Pin panel'}
|
||||||
|
onClick={() => setStackPanelPinned(p => !p)}
|
||||||
|
style={{ background: stackPanelPinned ? '#1976d2' : '#333', color: '#fff', border: 'none', padding: '6px 8px', borderRadius: 4, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{stackPanelPinned ? 'Pinned' : 'Pin'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Close"
|
||||||
|
onClick={() => { setStackPanelPinned(false); setStackPanelHover(false); }}
|
||||||
|
style={{ background: 'transparent', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 18 }}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ overflowY: 'auto', padding: 8, flex: 1 }}>
|
||||||
|
{availableStacks.length === 0 ? (
|
||||||
|
<div style={{ color: '#999', padding: 12 }}>No stacks available</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
draggable={!invalid}
|
||||||
|
onDragStart={e => {
|
||||||
|
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',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<div style={{ fontWeight: 600 }}>{label}</div>
|
||||||
|
<div style={{ fontSize: 12, opacity: 0.7 }}>{count} img{count === 1 ? '' : 's'}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, opacity: 0.8 }}>{stack.studyId || ''}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visible handle when panel is hidden to allow hover */}
|
||||||
|
{!stackPanelPinned && (
|
||||||
|
<div style={{ width: 24, height: 64, background: 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
|
||||||
|
<div style={{ width: 6, height: 36, background: '#444', borderRadius: 4, opacity: 0.9 }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{/* Side menu toggle button */}
|
{/* Side menu toggle button */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user