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 [presetsOpen, setPresetsOpen] = useState(false);
|
||||||
|
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
// Control visibility of the global top stack bar (hidden by default)
|
// Settings open state
|
||||||
const [topBarVisible, setTopBarVisible] = useState(false);
|
// (top bar removed; stack browser moved to right-side panel)
|
||||||
|
|
||||||
// Right-side stack panel pinned state (false = auto-hide, true = pinned open)
|
// Right-side stack panel pinned state (false = auto-hide, true = pinned open)
|
||||||
const [stackPanelPinned, setStackPanelPinned] = useState(false);
|
const [stackPanelPinned, setStackPanelPinned] = useState(false);
|
||||||
// Local hover state for temporary reveal when not pinned
|
// Local open state toggled by small button (replaces hover-to-open)
|
||||||
const [stackPanelHover, setStackPanelHover] = useState(false);
|
const [stackPanelOpen, setStackPanelOpen] = 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);
|
||||||
@@ -2990,144 +2990,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Top bar stack browser (hidden by default) */}
|
{/* top stack bar removed; use right-side panel instead */}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Right-side stack panel (pinnable, shows on hover when not pinned) */}
|
{/* Right-side stack panel (pinnable, shows on hover when not pinned) */}
|
||||||
<div
|
<div style={{ position: 'absolute', right: 0, top: 12, height: 'calc(100% - 24px)', zIndex: 2100, display: 'flex', alignItems: 'stretch', pointerEvents: 'auto' }}>
|
||||||
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 */}
|
{/* Panel container: translateX to hide when not pinned and not hovered */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -3137,7 +3003,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 2px 14px rgba(0,0,0,0.45)',
|
boxShadow: '0 2px 14px rgba(0,0,0,0.45)',
|
||||||
borderLeft: '1px solid #2f3336',
|
borderLeft: '1px solid #2f3336',
|
||||||
transform: stackPanelPinned || stackPanelHover ? 'translateX(0)' : 'translateX(96%)',
|
transform: stackPanelPinned || stackPanelOpen ? 'translateX(0)' : 'translateX(96%)',
|
||||||
transition: 'transform 220ms ease',
|
transition: 'transform 220ms ease',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -3156,7 +3022,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Close"
|
title="Close"
|
||||||
onClick={() => { setStackPanelPinned(false); setStackPanelHover(false); }}
|
onClick={() => { setStackPanelPinned(false); setStackPanelOpen(false); }}
|
||||||
style={{ background: 'transparent', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 18 }}
|
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>
|
||||||
</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>
|
||||||
|
|
||||||
|
{/* 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>
|
<div>
|
||||||
{/* Side menu toggle button */}
|
{/* Side menu toggle button */}
|
||||||
<button
|
<button
|
||||||
@@ -3603,13 +3492,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right-side panel occupies layout space when visible or pinned */}
|
{/* 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 */}
|
{/* Reuse the existing right-side panel markup but ensure pointerEvents are enabled inside */}
|
||||||
<div style={{ pointerEvents: 'auto', width: '100%', height: '100%' }}>
|
<div style={{ pointerEvents: 'auto', width: '100%', height: '100%' }}>
|
||||||
{/* Panel (same content as before) */}
|
{/* Panel (same content as before) */}
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => setStackPanelHover(true)}
|
// panel open/close is controlled via the small toggle button and pin state
|
||||||
onMouseLeave={() => setStackPanelHover(false)}
|
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -3636,7 +3524,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Close"
|
title="Close"
|
||||||
onClick={() => { setStackPanelPinned(false); setStackPanelHover(false); }}
|
onClick={() => { setStackPanelPinned(false); setStackPanelOpen(false); }}
|
||||||
style={{ background: 'transparent', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 18 }}
|
style={{ background: 'transparent', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 18 }}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
|||||||
Reference in New Issue
Block a user