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) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-15 20:48:20 +02:00
parent eac005109c
commit c5582ffc65
2 changed files with 137 additions and 88 deletions

View File

@@ -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 (
<div>
{/* 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. */}
<div
className="group mt-2 flex cursor-pointer items-center gap-1 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:text-text"
onClick={() => setExpanded((v) => !v)}
aria-expanded={expanded}
role="button"
>
<button className="rounded p-0.5 hover:bg-surface-offset">
<button
className="rounded p-0.5 hover:bg-surface-offset"
onClick={(e) => {
e.stopPropagation()
setExpanded((v) => !v)
}}
aria-label="Toggle heaps"
>
{expanded ? (
<ChevronDown className="h-3 w-3" />
) : (
@@ -209,6 +233,7 @@ export function HeapsPanel() {
className="flex items-center gap-1 px-2 py-0.5"
style={{ paddingLeft: '20px' }}
>
<div className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
<Input
autoFocus
type="text"
@@ -237,10 +262,11 @@ export function HeapsPanel() {
{heaps.length === 0 && !creating && (
<div
className="px-2 py-0.5 text-[11px] text-text-faint"
className="flex items-center gap-1 px-2 py-0.5 text-[11px] text-text-faint"
style={{ paddingLeft: '20px' }}
>
No heaps yet
<div className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
<span>No heaps yet</span>
</div>
)}
@@ -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. */}
<div className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
<ShoppingBasket
className={cn(
'h-3.5 w-3.5 flex-shrink-0',