From c1d7a49bc381e922a6a4fb24ddd44b04259b4256 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Apr 2026 08:46:26 +0200 Subject: [PATCH] feat: add react-resizable-panels dependency and update canvas components for improved layout - Introduced the react-resizable-panels package to enhance panel resizing capabilities. - Refactored CanvasPage to utilize CanvasShell for a cleaner layout. - Updated CanvasSidebar to support a compact mode and improved rendering logic for user entries. - Enhanced CanvasUserMenu with a compact option for better UI adaptability. --- app/(app)/canvas/[canvasId]/page.tsx | 14 +-- components/canvas/canvas-shell.tsx | 77 ++++++++++++ components/canvas/canvas-sidebar.tsx | 156 ++++++++++++++++--------- components/canvas/canvas-user-menu.tsx | 50 +++++++- components/ui/resizable.tsx | 50 ++++++++ package.json | 1 + pnpm-lock.yaml | 14 +++ 7 files changed, 293 insertions(+), 69 deletions(-) create mode 100644 components/canvas/canvas-shell.tsx create mode 100644 components/ui/resizable.tsx diff --git a/app/(app)/canvas/[canvasId]/page.tsx b/app/(app)/canvas/[canvasId]/page.tsx index c382a8e..2de6829 100644 --- a/app/(app)/canvas/[canvasId]/page.tsx +++ b/app/(app)/canvas/[canvasId]/page.tsx @@ -1,8 +1,6 @@ import { notFound, redirect } from "next/navigation"; -import Canvas from "@/components/canvas/canvas"; -import ConnectionBanner from "@/components/canvas/connection-banner"; -import CanvasSidebar from "@/components/canvas/canvas-sidebar"; +import { CanvasShell } from "@/components/canvas/canvas-shell"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { fetchAuthQuery, isAuthenticated } from "@/lib/auth-server"; @@ -48,13 +46,5 @@ export default async function CanvasPage({ notFound(); } - return ( -
- -
- - -
-
- ); + return ; } diff --git a/components/canvas/canvas-shell.tsx b/components/canvas/canvas-shell.tsx new file mode 100644 index 0000000..73be485 --- /dev/null +++ b/components/canvas/canvas-shell.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useCallback, useState } from "react"; + +import Canvas from "@/components/canvas/canvas"; +import ConnectionBanner from "@/components/canvas/connection-banner"; +import CanvasSidebar from "@/components/canvas/canvas-sidebar"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import type { Id } from "@/convex/_generated/dataModel"; + +const SIDEBAR_DEFAULT_SIZE = "18%"; +const SIDEBAR_COLLAPSE_THRESHOLD = "10%"; +const SIDEBAR_MAX_SIZE = "40%"; +const SIDEBAR_COLLAPSED_SIZE = "64px"; +const SIDEBAR_RAIL_MAX_WIDTH_PX = 112; +const MAIN_PANEL_MIN_SIZE = "40%"; + +type CanvasShellProps = { + canvasId: Id<"canvases">; +}; + +type PanelSize = { + asPercentage: number; + inPixels: number; +}; + +export function CanvasShell({ canvasId }: CanvasShellProps) { + const [isSidebarRail, setIsSidebarRail] = useState(false); + + const handleSidebarResize = useCallback((panelSize: PanelSize) => { + setIsSidebarRail(panelSize.inPixels <= SIDEBAR_RAIL_MAX_WIDTH_PX); + }, []); + + return ( +
+ + + + + + + + +
+ + +
+
+
+
+ ); +} diff --git a/components/canvas/canvas-sidebar.tsx b/components/canvas/canvas-sidebar.tsx index 21cf475..3310822 100644 --- a/components/canvas/canvas-sidebar.tsx +++ b/components/canvas/canvas-sidebar.tsx @@ -77,7 +77,13 @@ const CATALOG_ICONS: Partial> = { presentation: Presentation, }; -function SidebarRow({ entry }: { entry: NodeCatalogEntry }) { +function SidebarRow({ + entry, + compact = false, +}: { + entry: NodeCatalogEntry; + compact?: boolean; +}) { const enabled = isNodePaletteEnabled(entry); const Icon = CATALOG_ICONS[entry.type] ?? ClipboardList; @@ -95,28 +101,36 @@ function SidebarRow({ entry }: { entry: NodeCatalogEntry }) { onDragStart={onDragStart} title={enabled ? `${entry.label} — auf den Canvas ziehen` : hint} className={cn( - "flex items-center gap-2 rounded-lg border px-3 py-2 text-sm transition-colors", + "rounded-lg border transition-colors", + compact + ? "flex h-10 w-full items-center justify-center p-0" + : "flex items-center gap-2 px-3 py-2 text-sm", enabled ? "cursor-grab border-border/80 bg-card hover:bg-accent active:cursor-grabbing" : "cursor-not-allowed border-transparent bg-muted/30 text-muted-foreground", )} > - {entry.label} - {entry.phase > 1 ? ( + {!compact ? {entry.label} : null} + {!compact && entry.phase > 1 ? ( P{entry.phase} ) : null} + {compact ? {entry.label} : null} ); } type CanvasSidebarProps = { canvasId: Id<"canvases">; + railMode?: boolean; }; -export default function CanvasSidebar({ canvasId }: CanvasSidebarProps) { +export default function CanvasSidebar({ + canvasId, + railMode = false, +}: CanvasSidebarProps) { const canvas = useAuthQuery(api.canvases.get, { canvasId }); const byCategory = catalogEntriesByCategory(); const [collapsedByCategory, setCollapsedByCategory] = useState< @@ -127,63 +141,95 @@ export default function CanvasSidebar({ canvasId }: CanvasSidebarProps) { ), ); + const railEntries = NODE_CATEGORIES_ORDERED.flatMap( + (categoryId) => byCategory.get(categoryId) ?? [], + ); + return ( -