From c5582ffc6580bd52536e984827edf67427058c1d Mon Sep 17 00:00:00 2001 From: root Date: Wed, 15 Apr 2026 20:48:20 +0200 Subject: [PATCH] ui(sidebar): collapsible Library section with Heaps nested inside Wraps Views, Folders, Shared-with-me, and Heaps in a single click-to-toggle Library section with a consistent h-9 eyebrow header (matching the new Date header). Heaps keeps its own eyebrow sub-section so it sits alongside Folders, and heap rows now reserve the same chevron-slot spacer as leaf folder rows so indentation lines up across hierarchies. ActiveHeapCard moves to the very top of the sidebar so it stays visible under any panel state. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/heaps/HeapsPanel.tsx | 44 ++++- .../src/components/layout/LeftSidebar.tsx | 181 ++++++++++-------- 2 files changed, 137 insertions(+), 88 deletions(-) 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. */}
setExpanded((v) => !v)} + aria-expanded={expanded} + role="button" > -
-
- {/* 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 && ( -
-
- Shared with me -
- {sharedFolders.map((sf) => { - const isSelected = currentSection === `folder-${sf.id}` - return ( -
- navigateToSection(`folder-${sf.id}`, { folderId: sf.id }) - } - > - - - {sf.name} - - - {sf.owner_username} - - - {sf.permission} - - {sf.photo_count > 0 && ( - - {sf.photo_count} - - )} + {/* Shared with me — folders shared by other users */} + {sharedFolders.length > 0 && ( +
+
+ Shared with me
- ) - })} + {sharedFolders.map((sf) => { + const isSelected = currentSection === `folder-${sf.id}` + return ( +
+ navigateToSection(`folder-${sf.id}`, { folderId: sf.id }) + } + > + + + {sf.name} + + + {sf.owner_username} + + + {sf.permission} + + {sf.photo_count > 0 && ( + + {sf.photo_count} + + )} +
+ ) + })} +
+ )} + + {/* Heaps — nested inside the Library section; HeapsPanel owns + * its own eyebrow header + collapse state. */} +
)} - -
{/* Bottom panel — user identity + settings, pinned below the tree. */} @@ -1016,25 +1036,24 @@ function DateFilterSection() { : `${dateFrom ?? '…'} → ${dateTo ?? '…'}` : 'Any date' return ( -
- + {active && ( + + Filtering + + )} +
{open && ( -
+