Merge branch 'codex/canvas-anpassung'

This commit is contained in:
2026-04-03 14:52:56 +02:00
10 changed files with 485 additions and 172 deletions

View File

@@ -139,9 +139,9 @@ Im **Light Mode** wird der eigentliche Edge-`stroke` ebenfalls aus dieser Akzent
| Datei | Zweck |
|-------|-------|
| `canvas-shell.tsx` | Client-Layout-Wrapper für Sidebar/Main inkl. Resizing, Auto-Collapse und Rail-Mode-Umschaltung |
| `canvas-toolbar.tsx` | Werkzeug-Leiste (Select, Pan, Zoom-Controls) |
| `canvas-app-menu.tsx` | App-Menü (Einstellungen, Logout, Canvas-Name) |
| `canvas-sidebar.tsx` | Node-Palette links; unterstützt Full-Mode und Rail-Mode (icon-only) |
| `canvas-toolbar.tsx` | Werkzeug-Leiste (Select, Pan, Zoom-Controls) inkl. Canvas-Name im rechten Cluster neben Credits/Export |
| `canvas-app-menu.tsx` | App-Menü oben rechts (Umbenennen, Löschen, Theme) |
| `canvas-sidebar.tsx` | Node-Palette links; zeigt im Full-Mode das LemonSpace-Wordmark, im Rail-Mode einen kompakten Header und vor dem User-Menü einen visuellen Bottom-Fade |
| `canvas-command-palette.tsx` | Cmd+K Command Palette |
| `canvas-connection-drop-menu.tsx` | Kontext-Menü beim Loslassen einer Verbindung |
| `canvas-node-template-picker.tsx` | Node aus Template einfügen |
@@ -163,8 +163,13 @@ Im **Light Mode** wird der eigentliche Edge-`stroke` ebenfalls aus dieser Akzent
- Sidebar ist `collapsible`; bei Unterschreiten von `minSize` wird auf `collapsedSize` reduziert.
- Eingeklappt bedeutet nicht „unsichtbar“: `collapsedSize` ist absichtlich > 0 (`64px`), damit ein sichtbarer Rail bleibt.
- `canvas-shell.tsx` schaltet per `onResize` abhängig von der tatsächlichen Pixelbreite zwischen Full-Mode und Rail-Mode um (`railMode` Prop an `CanvasSidebar`).
- Im Full-Mode zeigt die Sidebar **nicht** mehr den Canvas-Namen, sondern das LemonSpace-Wordmark aus `public/logos/`:
- Light Mode → `lemonspace-logo-v2-black-rgb.svg`
- Dark Mode → `lemonspace-logo-v2-white-rgb.svg`
- Der Canvas-Name liegt stattdessen in der Toolbar (`canvas-toolbar.tsx`) als kompakter, truncating Label/Chip im rechten Bereich.
- `CanvasUserMenu` unterstützt ebenfalls einen kompakten Rail-Mode über `compact`.
- Scroll-Chaining ist begrenzt (`overscroll-contain` in der Sidebar-Scrollfläche + `overscroll-none` am Shell-Root), um visuelle Artefakte beim Scrollen am Ende zu verhindern.
- Vor dem `CanvasUserMenu` liegt im Sidebar-Body ein `pointer-events-none` Bottom-Fade (schwarz → transparent), der die unteren Palette-Einträge nur visuell ausblendet; Scrollen, Drag-and-Drop und Klicks bleiben unverändert funktionsfähig.
---

View File

@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import NextImage from "next/image";
import {
Bot,
ClipboardList,
@@ -160,77 +161,98 @@ export default function CanvasSidebar({
</div>
</div>
) : (
<div className="border-b border-border/80 px-4 py-4">
{canvas === undefined ? (
<div className="h-12 animate-pulse rounded-md bg-muted/50" />
) : (
<>
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Canvas
</p>
<h1 className="mt-1 line-clamp-2 text-base font-semibold leading-snug text-foreground">
{canvas?.name ?? "…"}
</h1>
</>
)}
<div className="border-b border-border/80 px-4 py-5">
<div className="flex min-h-8 items-center">
<div className="relative">
<NextImage
src="/logos/lemonspace-logo-v2-black-rgb.svg"
alt="LemonSpace"
width={140}
height={27}
className="h-auto w-[8.75rem] dark:hidden"
priority
/>
<NextImage
src="/logos/lemonspace-logo-v2-white-rgb.svg"
alt="LemonSpace"
width={140}
height={27}
className="hidden h-auto w-[8.75rem] dark:block"
priority
/>
</div>
</div>
</div>
)}
<div
className={cn(
"flex-1 overflow-y-auto overscroll-contain",
railMode ? "p-2" : "p-3",
)}
>
{railMode ? (
<div className="flex flex-col gap-1.5">
{railEntries.map((entry) => (
<SidebarRow key={entry.type} entry={entry} compact />
))}
</div>
) : (
<>
{NODE_CATEGORIES_ORDERED.map((categoryId) => {
const entries = byCategory.get(categoryId) ?? [];
if (entries.length === 0) return null;
const { label } = NODE_CATEGORY_META[categoryId];
const isCollapsed = collapsedByCategory[categoryId] ?? categoryId !== "source";
return (
<div key={categoryId} className="mb-4 last:mb-0">
<button
type="button"
onClick={() =>
setCollapsedByCategory((prev) => ({
...prev,
[categoryId]: !(prev[categoryId] ?? categoryId !== "source"),
}))
}
className="mb-2 flex w-full items-center justify-between rounded-md px-0.5 py-1 text-left text-xs font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground"
aria-expanded={!isCollapsed}
aria-controls={`sidebar-category-${categoryId}`}
>
<span>{label}</span>
{isCollapsed ? (
<ChevronRight className="size-3.5 shrink-0" />
) : (
<ChevronDown className="size-3.5 shrink-0" />
)}
</button>
{!isCollapsed ? (
<div id={`sidebar-category-${categoryId}`} className="flex flex-col gap-1.5">
{entries.map((entry) => (
<SidebarRow key={entry.type} entry={entry} />
))}
</div>
) : null}
</div>
);
})}
</>
)}
<div className="relative min-h-0 flex-1">
<div
className={cn(
"h-full overflow-y-auto overscroll-contain",
railMode ? "p-2 pb-20" : "p-3 pb-28",
)}
>
{railMode ? (
<div className="flex flex-col gap-1.5">
{railEntries.map((entry) => (
<SidebarRow key={entry.type} entry={entry} compact />
))}
</div>
) : (
<>
{NODE_CATEGORIES_ORDERED.map((categoryId) => {
const entries = byCategory.get(categoryId) ?? [];
if (entries.length === 0) return null;
const { label } = NODE_CATEGORY_META[categoryId];
const isCollapsed = collapsedByCategory[categoryId] ?? categoryId !== "source";
return (
<div key={categoryId} className="mb-4 last:mb-0">
<button
type="button"
onClick={() =>
setCollapsedByCategory((prev) => ({
...prev,
[categoryId]: !(prev[categoryId] ?? categoryId !== "source"),
}))
}
className="mb-2 flex w-full items-center justify-between rounded-md px-0.5 py-1 text-left text-xs font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground"
aria-expanded={!isCollapsed}
aria-controls={`sidebar-category-${categoryId}`}
>
<span>{label}</span>
{isCollapsed ? (
<ChevronRight className="size-3.5 shrink-0" />
) : (
<ChevronDown className="size-3.5 shrink-0" />
)}
</button>
{!isCollapsed ? (
<div id={`sidebar-category-${categoryId}`} className="flex flex-col gap-1.5">
{entries.map((entry) => (
<SidebarRow key={entry.type} entry={entry} />
))}
</div>
) : null}
</div>
);
})}
</>
)}
</div>
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-x-0 bottom-0 z-10",
railMode
? "h-16 bg-gradient-to-t from-black via-black/80 to-transparent"
: "h-24 bg-gradient-to-t from-black via-black/80 to-transparent",
)}
/>
</div>
<CanvasUserMenu compact={railMode} />
<div className="relative z-20 bg-background">
<CanvasUserMenu compact={railMode} />
</div>
</aside>
);
}

View File

@@ -65,6 +65,7 @@ export default function CanvasToolbar({
};
const byCategory = catalogEntriesByCategory();
const resolvedCanvasName = canvasName?.trim() || "Unbenannter Canvas";
const toolBtn = (tool: CanvasNavTool, icon: React.ReactNode, label: string) => (
<Button
@@ -82,7 +83,7 @@ export default function CanvasToolbar({
);
return (
<div className="absolute top-4 left-1/2 z-10 flex max-w-[min(calc(100vw-12rem),52rem)] -translate-x-1/2 items-center gap-0.5 rounded-xl border border-border/80 bg-card/95 p-1.5 shadow-lg backdrop-blur-sm">
<div className="absolute top-4 left-1/2 z-10 flex w-[min(calc(100vw-9rem),64rem)] items-center gap-0.5 rounded-xl border border-border/80 bg-card/95 p-1.5 shadow-lg backdrop-blur-sm -translate-x-1/2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -181,7 +182,14 @@ export default function CanvasToolbar({
<div className="mx-1 h-6 w-px shrink-0 bg-border/80" />
<div className="flex min-w-0 flex-1 items-center justify-end gap-1 sm:flex-initial">
<div className="flex min-w-0 flex-1 items-center justify-end gap-1">
<div
className="min-w-0 max-w-28 rounded-lg border border-border/70 bg-background/80 px-3 py-1.5 text-sm font-semibold text-foreground shadow-sm sm:max-w-40 md:max-w-52"
title={resolvedCanvasName}
aria-label={`Canvas-Name: ${resolvedCanvasName}`}
>
<span className="block truncate">{resolvedCanvasName}</span>
</div>
<CreditDisplay />
<ExportButton canvasName={canvasName ?? "canvas"} />
</div>

View File

@@ -3037,7 +3037,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
<AssetBrowserTargetContext.Provider value={assetBrowserTargetApi}>
<div className="relative h-full w-full">
<CanvasToolbar
canvasName={canvas?.name ?? "canvas"}
canvasName={canvas?.name}
activeTool={navTool}
onToolChange={handleNavToolChange}
/>