diff --git a/package.json b/package.json index 7e680e1a..3080857a 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "react-email": "^6.1.3", "react-hook-form": "^7.75.0", "react-image-crop": "^11.0.10", + "react-resizable-panels": "^3.0.6", "recharts": "^3.8.1", "sharp": "^0.34.5", "socket.io": "^4.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9808f0ea..232e5d69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,6 +220,9 @@ importers: react-image-crop: specifier: ^11.0.10 version: 11.0.10(react@19.2.6) + react-resizable-panels: + specifier: ^3.0.6 + version: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) recharts: specifier: ^3.8.1 version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1) @@ -5139,6 +5142,12 @@ packages: '@types/react': optional: true + react-resizable-panels@3.0.6: + resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-simple-animate@3.5.3: resolution: {integrity: sha512-Ob+SmB5J1tXDEZyOe2Hf950K4M8VaWBBmQ3cS2BUnTORqHjhK0iKG8fB+bo47ZL15t8d3g/Y0roiqH05UBjG7A==} peerDependencies: @@ -10859,6 +10868,11 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-resizable-panels@3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-simple-animate@3.5.3(react-dom@19.2.6(react@19.2.6)): dependencies: react-dom: 19.2.6(react@19.2.6) diff --git a/src/components/documents/documents-hub.tsx b/src/components/documents/documents-hub.tsx index fab5a9c6..7fa66c15 100644 --- a/src/components/documents/documents-hub.tsx +++ b/src/components/documents/documents-hub.tsx @@ -25,9 +25,10 @@ import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { useDocumentFolders, type FolderNode } from '@/hooks/use-document-folders'; import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider'; import { useUIStore } from '@/stores/ui-store'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; import { FolderActionsMenu } from './folder-actions-menu'; import { FolderBreadcrumb } from './folder-breadcrumb'; -import { FolderTreeSidebar } from './folder-tree-sidebar'; +import { FolderTreeBody, FolderTreeSidebar } from './folder-tree-sidebar'; import { HubRootView } from './hub-root-view'; import { EntityFolderView } from './entity-folder-view'; import { NewDocumentMenu } from './new-document-menu'; @@ -147,68 +148,104 @@ export function DocumentsHub({ portSlug }: DocumentsHubProps) { setSelectedFolderId(id); }; - return ( -
- + - handleFolderSelect(undefined)} - /> - - } + onAfterDelete={() => handleFolderSelect(undefined)} /> -
-
- - {selectedFolderId !== undefined && ( - - )} -
+ + ); - {selectedFolderId === undefined ? ( - <> - } - variant="gradient" - /> - - - ) : isEntityFolder && isEntityType(folderEntityType) ? ( - +
+ + {selectedFolderId !== undefined && ( + - - - ) : ( - - - + entityType={ + isEntityFolder && isEntityType(folderEntityType) ? folderEntityType : undefined + } + entityId={isEntityFolder ? (selectedFolder!.entityId ?? undefined) : undefined} + size="sm" + /> )}
+ + {selectedFolderId === undefined ? ( + <> + } + variant="gradient" + /> + + + ) : isEntityFolder && isEntityType(folderEntityType) ? ( + + + + ) : ( + + + + )} +
+ ); + + return ( +
+ {/* Mobile: stacked, with a Sheet drawer for folders. */} +
+ + {contentPane} +
+ + {/* Desktop (sm+): resizable two-pane split. autoSaveId persists the + * user's chosen split width to localStorage so it survives reloads. + * Min/max defends against the user collapsing the sidebar to zero + * width or starving the content pane. */} + + + + + + +
{contentPane}
+
+
); } diff --git a/src/components/documents/folder-tree-sidebar.tsx b/src/components/documents/folder-tree-sidebar.tsx index a23d9382..f2bc1015 100644 --- a/src/components/documents/folder-tree-sidebar.tsx +++ b/src/components/documents/folder-tree-sidebar.tsx @@ -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 + * `` so power users on wide monitors can drag the + * split. Both surfaces render the same `` 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+. */} -
- - - - - - - Folders - -
- -
-
-
-
- - {/* Desktop sidebar: hidden on mobile (the Sheet trigger replaces it). */} - - +
+ + + + + + + Folders + +
+ +
+
+
+
); } -function TreeBody({ +export function FolderTreeBody({ selectedFolderId, onSelect, footer, diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx new file mode 100644 index 00000000..71fb4a04 --- /dev/null +++ b/src/components/ui/resizable.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { GripVertical } from 'lucide-react'; +import * as ResizablePrimitive from 'react-resizable-panels'; + +import { cn } from '@/lib/utils'; + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +const ResizablePanel = ResizablePrimitive.Panel; + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean; +}) => ( + div]:rotate-90', + className, + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+); + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle };