diff --git a/frontend/src/components/heaps/HeapsPanel.tsx b/frontend/src/components/heaps/HeapsPanel.tsx
index a4a4b08..32e70d2 100644
--- a/frontend/src/components/heaps/HeapsPanel.tsx
+++ b/frontend/src/components/heaps/HeapsPanel.tsx
@@ -44,13 +44,27 @@ import {
* - filter heapId: which heap is currently filtered to (visual)
* - heap.is_active: which heap T adds to (server-side, single per row)
*/
-export function HeapsPanel() {
+interface HeapsPanelProps {
+ /** Controlled expand state. When both props are supplied the panel
+ * defers to the parent for collapse/expand, so the sidebar's pane
+ * sizing layer can flex the Heaps pane only when it is open. */
+ expanded?: boolean
+ onExpandedChange?: (expanded: boolean) => void
+}
+
+export function HeapsPanel({ expanded: expandedProp, onExpandedChange }: HeapsPanelProps = {}) {
const { data: heaps = [] } = useHeapsQuery()
const navigateToSection = useFilterStore((s) => s.navigateToSection)
const currentSection = useFilterStore((s) => s.currentSection)
const queryClient = useQueryClient()
- const [expanded, setExpanded] = useState(true)
+ const [expandedInternal, setExpandedInternal] = useState(true)
+ const expanded = expandedProp ?? expandedInternal
+ const setExpanded = (next: boolean | ((prev: boolean) => boolean)) => {
+ const resolved = typeof next === 'function' ? (next as (p: boolean) => boolean)(expanded) : next
+ if (onExpandedChange) onExpandedChange(resolved)
+ else setExpandedInternal(resolved)
+ }
const [creating, setCreating] = useState(false)
const [newName, setNewName] = useState('')
// Which heap row is currently being hovered with a drag — used to render
@@ -174,13 +188,23 @@ export function HeapsPanel() {
return (
- {/* Section header — eyebrow style to match the LeftSidebar
- * library/folders headers. */}
+ {/* Section header — mirrors the Folders section eyebrow in
+ * LeftSidebar so Heaps sits alongside it at the same visual
+ * tier, with heap rows indented the same as folder rows. */}
)}
@@ -308,6 +334,10 @@ export function HeapsPanel() {
}
}}
>
+ {/* Chevron-slot spacer — heap rows don't expand, but this
+ * reserves the same width that leaf folder rows use so
+ * icons and labels line up across the two hierarchies. */}
+
>(new Set(['library', 'folders', 'heaps']))
+ // Library pane collapse. Its body holds Views, Folders, Shared, and
+ // Heaps, so a single toggle hides the whole navigation area.
+ const [libraryPaneOpen, setLibraryPaneOpen] = useState(true)
// Inline rename state for source-root rows. Stores the id being edited
// and the draft name. Double-click a folder row to start.
const [renamingId, setRenamingId] = useState(null)
@@ -836,15 +838,40 @@ export function LeftSidebar() {
return (
- {/* Header with collapse button. Matches the right sidebar header
- * so both panels have symmetric affordances. */}
-
-
- Library
-
-
+ {/* Active heap card — pinned at the very top so it stays visible
+ * even when the date filter is expanded into a tall calendar.
+ * Returns null when no heap is active, so the layout collapses
+ * cleanly. */}
+
+
+ {/* Date range filter — always visible, collapsible. Drives the
+ * global dateFrom/dateTo on the filter store, so it applies to
+ * every section regardless of which tree item is selected. */}
+
+
+ {/* Resizable two-pane region: Library (Views + Folders + Shared)
+ * on top, Heaps on the bottom. Each pane collapses to its
+ * header; when both are expanded a drag divider splits the
+ * vertical space between them (persisted to localStorage). */}
+ {/* Library pane — the sole navigation section. Contains Views,
+ * Folders, Shared-with-me, and Heaps in a single scroll area;
+ * collapses to its header when hidden. */}
+
- {/* Date range filter — always visible, collapsible. Drives the
- * global dateFrom/dateTo on the filter store, so it applies to
- * every section regardless of which tree item is selected. */}
-
+ {libraryPaneOpen && (
+
+ {libraryTree.map((item) => renderTreeItem(item))}
- {/* Active heap card — pinned just below the Library header so
- * toasts (bottom-left fixed) can't cover it. Returns null when
- * no heap is active, so the layout collapses cleanly. */}
-
-
- {/* Tree View */}
-
- {libraryTree.map((item) => renderTreeItem(item))}
-
- {/* Shared with me — folders shared by other users */}
- {sharedFolders.length > 0 && (
-