Refactor stack panel functionality to remove top bar and implement toggle for right-side panel visibility
This commit is contained in:
+40
-152
@@ -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 }}
|
||||
>
|
||||
×
|
||||
|
||||
Reference in New Issue
Block a user