feat(deps): react-resizable-panels for docs hub desktop split

Docs hub's desktop sidebar is now drag-resizable. Mobile path is
unchanged — still uses the FolderTreeSidebar Sheet drawer.

- Extracted `FolderTreeBody` from `folder-tree-sidebar.tsx` so the
  same tree renders inside the mobile Sheet AND the desktop panel
  without forking the component.
- `FolderTreeSidebar` is now mobile-only (just the Sheet trigger);
  documents-hub composes the desktop layout itself.
- `<ResizablePanelGroup autoSaveId="documents-hub-split">` persists
  the user's chosen split width via localStorage automatically.
  Min 14% / max 40% defends against starvation.
- shadcn-style `<Resizable*>` primitives in `src/components/ui/`
  match the rest of the UI kit; uses react-resizable-panels v3
  (the v4 release renamed exports to `Group`/`Separator` and broke
  the shadcn convention — pinned v3 for now).

Verified: tsc clean, vitest 1315/1315, next build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 22:30:06 +02:00
parent 4879b17cff
commit 699ae52827
5 changed files with 178 additions and 101 deletions

View File

@@ -18,17 +18,11 @@ interface FolderTreeSidebarProps {
}
/**
* Collapsed-by-default tree. Each row shows a chevron that toggles its
* children; clicking the row label selects the folder. The "All
* documents" + "Root" pseudo-rows at the top let reps filter to the
* full set or to docs without a folder.
* Mobile-only Sheet trigger that opens the folder tree in a drawer.
*
* Designed for unlimited depth — only the top level renders by default
* so deep trees don't blow out the page; reps drill in by expanding.
*
* On mobile (< sm) the sidebar collapses into a Sheet drawer triggered by
* a "Show folders" button so the main listing isn't pushed below a
* full-width folder stack.
* Desktop rendering lives in `documents-hub.tsx` as one panel of a
* `<ResizablePanelGroup>` so power users on wide monitors can drag the
* split. Both surfaces render the same `<FolderTreeBody>` underneath.
*/
export function FolderTreeSidebar({ selectedFolderId, onSelect, footer }: FolderTreeSidebarProps) {
const [mobileOpen, setMobileOpen] = useState(false);
@@ -39,43 +33,32 @@ export function FolderTreeSidebar({ selectedFolderId, onSelect, footer }: Folder
};
return (
<>
{/* Mobile-only trigger that opens the drawer; hidden at sm+. */}
<div className="sm:hidden px-3 pt-3">
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger asChild>
<Button variant="outline" size="sm" className="min-h-[44px]">
<FolderTree className="mr-2 h-4 w-4" />
Show folders
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-3/4 max-w-xs p-0">
<SheetHeader className="border-b px-3 py-3">
<SheetTitle className="text-sm">Folders</SheetTitle>
</SheetHeader>
<div className="p-2 overflow-y-auto">
<TreeBody
selectedFolderId={selectedFolderId}
onSelect={handleMobileSelect}
footer={footer}
/>
</div>
</SheetContent>
</Sheet>
</div>
{/* Desktop sidebar: hidden on mobile (the Sheet trigger replaces it). */}
<aside className="hidden sm:block w-60 shrink-0 border-b sm:border-b-0 sm:border-r bg-muted/40 p-2">
<div className="mb-2 px-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
Folders
</div>
<TreeBody selectedFolderId={selectedFolderId} onSelect={onSelect} footer={footer} />
</aside>
</>
<div className="sm:hidden px-3 pt-3">
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger asChild>
<Button variant="outline" size="sm" className="min-h-[44px]">
<FolderTree className="mr-2 h-4 w-4" />
Show folders
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-3/4 max-w-xs p-0">
<SheetHeader className="border-b px-3 py-3">
<SheetTitle className="text-sm">Folders</SheetTitle>
</SheetHeader>
<div className="p-2 overflow-y-auto">
<FolderTreeBody
selectedFolderId={selectedFolderId}
onSelect={handleMobileSelect}
footer={footer}
/>
</div>
</SheetContent>
</Sheet>
</div>
);
}
function TreeBody({
export function FolderTreeBody({
selectedFolderId,
onSelect,
footer,