Files
lemonspace_app/components/canvas/canvas-connection-drop-menu.tsx
Matthias Meister fa6a41f775 feat(canvas): implement edge insertion reflow and enhance connection validation
- Introduced a new CSS transition for edge insertion reflowing to improve visual feedback during node adjustments.
- Enhanced the connection validation logic to include options for optimistic edges, ensuring better handling of edge creation scenarios.
- Updated the canvas connection drop menu to support additional templates and improved edge insertion handling.
- Refactored edge insertion logic to accommodate local node position adjustments during reflow operations.
- Added tests for new edge insertion features and connection validation improvements.
2026-04-05 23:25:26 +02:00

121 lines
3.2 KiB
TypeScript

"use client";
import { useEffect, useRef, type CSSProperties } from "react";
import { CanvasNodeTemplatePicker } from "@/components/canvas/canvas-node-template-picker";
import {
Command,
CommandEmpty,
CommandInput,
CommandList,
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import type { CanvasNodeTemplate } from "@/lib/canvas-node-templates";
import type { Id } from "@/convex/_generated/dataModel";
export type ConnectionDropMenuState = {
screenX: number;
screenY: number;
flowX: number;
flowY: number;
fromNodeId: Id<"nodes">;
fromHandleId: string | undefined;
fromHandleType: "source" | "target";
};
export type CanvasMenuAnchor = {
screenX: number;
screenY: number;
};
type CanvasConnectionDropMenuProps = {
anchor: CanvasMenuAnchor | null;
onClose: () => void;
onPick: (template: CanvasNodeTemplate) => void;
templates?: readonly CanvasNodeTemplate[];
};
const PANEL_MAX_W = 360;
const PANEL_MAX_H = 420;
export function CanvasConnectionDropMenu({
anchor,
onClose,
onPick,
templates,
}: CanvasConnectionDropMenuProps) {
const panelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!anchor) return;
const onEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", onEscape);
const onPointerDownCapture = (e: PointerEvent) => {
const panel = panelRef.current;
if (panel && !panel.contains(e.target as Node)) {
onClose();
}
};
document.addEventListener("pointerdown", onPointerDownCapture, true);
return () => {
document.removeEventListener("keydown", onEscape);
document.removeEventListener("pointerdown", onPointerDownCapture, true);
};
}, [anchor, onClose]);
if (!anchor) return null;
const vw =
typeof window !== "undefined" ? window.innerWidth : PANEL_MAX_W + 16;
const vh =
typeof window !== "undefined" ? window.innerHeight : PANEL_MAX_H + 16;
const left = Math.max(
8,
Math.min(anchor.screenX, vw - PANEL_MAX_W - 8),
);
const top = Math.max(
8,
Math.min(anchor.screenY, vh - PANEL_MAX_H - 8),
);
return (
<div
ref={panelRef}
role="dialog"
aria-label="Knoten wählen zur Verbindung"
className={cn(
"fixed z-100 flex max-h-(--panel-max-h) w-[min(100vw-1rem,360px)] max-w-[calc(100vw-1rem)] flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground ring-1 ring-foreground/10 shadow-lg",
)}
style={
{
left,
top,
"--panel-max-h": `${PANEL_MAX_H}px`,
} as CSSProperties
}
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<Command className="rounded-xl! bg-popover">
<CommandInput placeholder="Knoten suchen …" autoFocus />
<CommandList className="max-h-72">
<CommandEmpty>Keine Treffer.</CommandEmpty>
<CanvasNodeTemplatePicker
onPick={(template) => {
onPick(template);
onClose();
}}
groupHeading="Knoten"
templates={templates}
/>
</CommandList>
</Command>
</div>
);
}