From ae2fa1d269b0a3e5087fb00551611f14f11368c1 Mon Sep 17 00:00:00 2001 From: Matthias Meister Date: Sat, 11 Apr 2026 08:49:05 +0200 Subject: [PATCH] feat(canvas): improve collapsed sidebar scanability and branding --- .../canvas/__tests__/canvas-sidebar.test.tsx | 77 +++++++++++++++++++ components/canvas/canvas-shell.tsx | 4 +- components/canvas/canvas-sidebar.tsx | 71 +++++++++++------ vitest.config.ts | 1 + 4 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 components/canvas/__tests__/canvas-sidebar.test.tsx diff --git a/components/canvas/__tests__/canvas-sidebar.test.tsx b/components/canvas/__tests__/canvas-sidebar.test.tsx new file mode 100644 index 0000000..0e578bf --- /dev/null +++ b/components/canvas/__tests__/canvas-sidebar.test.tsx @@ -0,0 +1,77 @@ +// @vitest-environment jsdom + +import React, { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import CanvasSidebar from "@/components/canvas/canvas-sidebar"; +import type { Id } from "@/convex/_generated/dataModel"; + +vi.mock("@/hooks/use-auth-query", () => ({ + useAuthQuery: () => ({ name: "Demo Canvas" }), +})); + +vi.mock("@/components/canvas/canvas-user-menu", () => ({ + CanvasUserMenu: ({ compact }: { compact?: boolean }) => ( +
+ ), +})); + +vi.mock("@/components/ui/progressive-blur", () => ({ + ProgressiveBlur: () =>
, +})); + +vi.mock("next/image", () => ({ + __esModule: true, + default: ({ alt }: { alt?: string }) => , +})); + +(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +describe("CanvasSidebar", () => { + let container: HTMLDivElement | null = null; + let root: Root | null = null; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(async () => { + if (root) { + await act(async () => { + root?.unmount(); + }); + } + container?.remove(); + container = null; + root = null; + }); + + it("shows divider-separated category sections with two-column grids in rail mode", async () => { + await act(async () => { + root?.render( + } railMode />, + ); + }); + + const text = container?.textContent ?? ""; + expect(text).not.toContain("QUELLE"); + expect(text).not.toContain("KI-AUSGABE"); + + const sourceGrid = container?.querySelector( + '[data-testid="sidebar-rail-category-source-grid"]', + ); + expect(sourceGrid).not.toBeNull(); + expect(sourceGrid?.className).toContain("grid-cols-2"); + + const aiOutputDivider = container?.querySelector( + '[data-testid="sidebar-rail-category-ai-output-divider"]', + ); + expect(aiOutputDivider).not.toBeNull(); + + const allCategoryGrids = container?.querySelectorAll('[data-testid$="-grid"]'); + expect(allCategoryGrids?.length ?? 0).toBeGreaterThan(1); + }); +}); diff --git a/components/canvas/canvas-shell.tsx b/components/canvas/canvas-shell.tsx index e7249f7..e37afca 100644 --- a/components/canvas/canvas-shell.tsx +++ b/components/canvas/canvas-shell.tsx @@ -15,8 +15,8 @@ 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 SIDEBAR_COLLAPSED_SIZE = "84px"; +const SIDEBAR_RAIL_MAX_WIDTH_PX = 148; const MAIN_PANEL_MIN_SIZE = "40%"; type CanvasShellProps = { diff --git a/components/canvas/canvas-sidebar.tsx b/components/canvas/canvas-sidebar.tsx index 6855b53..d1b3cd0 100644 --- a/components/canvas/canvas-sidebar.tsx +++ b/components/canvas/canvas-sidebar.tsx @@ -32,8 +32,6 @@ import { import { CanvasUserMenu } from "@/components/canvas/canvas-user-menu"; import { ProgressiveBlur } from "@/components/ui/progressive-blur"; -import { useAuthQuery } from "@/hooks/use-auth-query"; -import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { NODE_CATEGORY_META, @@ -107,14 +105,14 @@ function SidebarRow({ className={cn( "rounded-lg border transition-colors", compact - ? "flex h-10 w-full items-center justify-center p-0" + ? "flex aspect-square min-h-0 w-full items-center justify-center rounded-md 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", )} > - + {!compact ? {entry.label} : null} {!compact && entry.phase > 1 ? ( @@ -135,31 +133,39 @@ export default function CanvasSidebar({ canvasId, railMode = false, }: CanvasSidebarProps) { - const canvas = useAuthQuery(api.canvases.get, { canvasId }); + void canvasId; const byCategory = catalogEntriesByCategory(); const [collapsedByCategory, setCollapsedByCategory] = useState< Partial> >(() => Object.fromEntries( - NODE_CATEGORIES_ORDERED.map((categoryId) => [categoryId, categoryId !== "source"]), + NODE_CATEGORIES_ORDERED.map((categoryId) => [categoryId, false]), ), ); - const railEntries = NODE_CATEGORIES_ORDERED.flatMap( - (categoryId) => byCategory.get(categoryId) ?? [], - ); - return (