Refactor stack panel functionality to remove top bar and implement toggle for right-side panel visibility

This commit is contained in:
Ross
2025-09-22 10:58:05 +01:00
parent 67329d0468
commit e685b8b11d
+40 -152
View File
@@ -173,13 +173,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const [presetsOpen, setPresetsOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
// Control visibility of the global top stack bar (hidden by default)
const [topBarVisible, setTopBarVisible] = useState(false);
// Settings open state
// (top bar removed; stack browser moved to right-side panel)
// 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);
// Local open state toggled by small button (replaces hover-to-open)
const [stackPanelOpen, setStackPanelOpen] = useState(false);
const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState<number | null>(null);
const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
@@ -2990,144 +2990,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
</div>
)}
{/* Top bar stack browser (hidden by default) */}
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 8 }}>
<button
onClick={() => setTopBarVisible(v => !v)}
style={{
background: topBarVisible ? "#1976d2" : "#333",
color: "#fff",
border: "none",
padding: "8px 12px",
borderRadius: 4,
cursor: "pointer",
}}
aria-pressed={topBarVisible}
aria-label={topBarVisible ? "Hide stack bar" : "Show stack bar"}
>
{topBarVisible ? "Hide Stack Bar" : "Show Stack Bar"}
</button>
{topBarVisible && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
minHeight: 54,
background: "#23272bE6", // slightly transparent for layering
color: "#fff",
zIndex: 1001, // higher than most overlays but below modals
display: "flex",
alignItems: "center",
borderBottom: "2px solid #444",
padding: "0 16px",
gap: 18,
overflowX: "auto",
userSelect: "none",
pointerEvents: "auto",
}}
>
{/* Group stacks by studyId or studyInstanceUID or 'Unspecified' */}
{(() => {
const groups: Record<string, { idx: number; stack: StackEntry }[]> = {};
availableStacks.forEach((stackObj, idx) => {
const studyKey = stackObj.studyId || stackObj.studyInstanceUID || 'Unspecified';
if (!groups[studyKey]) groups[studyKey] = [];
groups[studyKey].push({ idx, stack: stackObj });
});
return Object.keys(groups).map(studyKey => (
<div key={studyKey} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ color: '#aaa', fontSize: 13, fontWeight: 700, marginRight: 6 }}>
{studyKey === 'Unspecified' ? 'Unspecified study' : `Study: ${studyKey}`}
</span>
{groups[studyKey].map(({ idx, stack }) => {
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;
// Highlight if this stack is selected in any viewport
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';
}}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
minWidth: 90,
maxWidth: 220,
padding: '7px 14px',
margin: '0 2px',
background: isSelected ? '#1976d2' : '#333',
color: isSelected ? '#fff' : '#eee',
border: isSelected ? '2px solid #90caf9' : 'none',
borderRadius: '4px',
fontWeight: isSelected ? 'bold' : 'normal',
fontSize: 15,
cursor: invalid ? 'not-allowed' : 'grab',
opacity: invalid ? 0.5 : 1,
transition: 'background 0.2s, border 0.2s',
boxShadow: isSelected ? '0 2px 8px #1976d2aa' : undefined,
}}
title={invalid ? 'Invalid stack descriptor' : `${label}${count} image${count === 1 ? '' : 's'}`}
>
<span>{label}</span>
<span style={{ fontSize: 12, opacity: 0.7, marginLeft: 8 }}>{count} img{count === 1 ? '' : 's'}</span>
</div>
);
})}
</div>
));
})()}
</div>
)}
</div>
{/* Persistent floating button to reveal the stack bar */}
<button
onClick={() => setTopBarVisible(true)}
style={{
position: "absolute",
top: 12,
left: 12,
zIndex: 2000,
background: "#1976d2",
color: "#fff",
border: "none",
padding: "8px 10px",
borderRadius: 6,
cursor: "pointer",
boxShadow: "0 2px 10px rgba(0,0,0,0.3)",
}}
aria-label="Show stack bar"
>
Show Stacks
</button>
{/* top stack bar removed; use right-side panel instead */}
{/* 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',
}}
>
<div style={{ position: 'absolute', right: 0, top: 12, height: 'calc(100% - 24px)', zIndex: 2100, display: 'flex', alignItems: 'stretch', pointerEvents: 'auto' }}>
{/* Panel container: translateX to hide when not pinned and not hovered */}
<div
style={{
@@ -3137,7 +3003,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
color: '#fff',
boxShadow: '0 2px 14px rgba(0,0,0,0.45)',
borderLeft: '1px solid #2f3336',
transform: stackPanelPinned || stackPanelHover ? 'translateX(0)' : 'translateX(96%)',
transform: stackPanelPinned || stackPanelOpen ? 'translateX(0)' : 'translateX(96%)',
transition: 'transform 220ms ease',
display: 'flex',
flexDirection: 'column',
@@ -3156,7 +3022,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
</button>
<button
title="Close"
onClick={() => { setStackPanelPinned(false); setStackPanelHover(false); }}
onClick={() => { setStackPanelPinned(false); setStackPanelOpen(false); }}
style={{ background: 'transparent', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 18 }}
>
×
@@ -3209,14 +3075,37 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
</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>
{/* Floating right-edge toggle — always visible so users can open the panel */}
{! (stackPanelPinned || stackPanelOpen) && (
<button
onClick={() => setStackPanelOpen(true)}
aria-pressed={false}
title="Open stacks"
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 2500,
width: 40,
height: 64,
background: '#1976d2',
color: '#fff',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
}}
>
</button>
)}
<div>
{/* Side menu toggle button */}
<button
@@ -3603,13 +3492,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
</div>
{/* Right-side panel occupies layout space when visible or pinned */}
<div style={{ width: stackPanelPinned || stackPanelHover ? 320 : 28, transition: 'width 220ms ease', display: 'flex', flexDirection: 'column', pointerEvents: 'none' }}>
<div style={{ width: stackPanelPinned || stackPanelOpen ? 320 : 28, transition: 'width 220ms ease', display: 'flex', flexDirection: 'column', pointerEvents: 'auto' }}>
{/* Reuse the existing right-side panel markup but ensure pointerEvents are enabled inside */}
<div style={{ pointerEvents: 'auto', width: '100%', height: '100%' }}>
{/* Panel (same content as before) */}
<div
onMouseEnter={() => setStackPanelHover(true)}
onMouseLeave={() => setStackPanelHover(false)}
// panel open/close is controlled via the small toggle button and pin state
style={{
height: '100%',
width: '100%',
@@ -3636,7 +3524,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
</button>
<button
title="Close"
onClick={() => { setStackPanelPinned(false); setStackPanelHover(false); }}
onClick={() => { setStackPanelPinned(false); setStackPanelOpen(false); }}
style={{ background: 'transparent', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 18 }}
>
×