Files
lemonspace_app/components/canvas/edges/default-edge.tsx

136 lines
3.6 KiB
TypeScript

"use client";
import { useMemo, useState, type MouseEvent } from "react";
import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
useViewport,
type EdgeProps,
} from "@xyflow/react";
import { Plus } from "lucide-react";
const MIN_EDGE_INSERT_BUTTON_SCALE = 0.95;
const MAX_EDGE_INSERT_BUTTON_SCALE = 2.2;
function getEdgeInsertButtonScale(zoom: number): number {
if (!Number.isFinite(zoom) || zoom <= 0) {
return 1;
}
const inverseZoom = 1 / zoom;
return Math.min(
MAX_EDGE_INSERT_BUTTON_SCALE,
Math.max(MIN_EDGE_INSERT_BUTTON_SCALE, inverseZoom),
);
}
export type DefaultEdgeInsertAnchor = {
edgeId: string;
screenX: number;
screenY: number;
};
export type DefaultEdgeProps = EdgeProps & {
edgeId?: string;
isMenuOpen?: boolean;
disabled?: boolean;
onInsertClick?: (anchor: DefaultEdgeInsertAnchor) => void;
};
export default function DefaultEdge({
id,
edgeId,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerStart,
markerEnd,
style,
interactionWidth,
isMenuOpen = false,
disabled = false,
onInsertClick,
}: DefaultEdgeProps) {
const [isEdgeHovered, setIsEdgeHovered] = useState(false);
const [isButtonHovered, setIsButtonHovered] = useState(false);
const { zoom } = useViewport();
const [edgePath, labelX, labelY] = useMemo(
() =>
getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}),
[sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY],
);
const resolvedEdgeId = edgeId ?? id;
const canInsert = Boolean(onInsertClick) && !disabled;
const isInsertVisible = canInsert && (isMenuOpen || isEdgeHovered || isButtonHovered);
const buttonScale = getEdgeInsertButtonScale(zoom);
const handleInsertClick = (event: MouseEvent<HTMLButtonElement>) => {
if (!onInsertClick || disabled) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
onInsertClick({
edgeId: resolvedEdgeId,
screenX: rect.left + rect.width / 2,
screenY: rect.top + rect.height / 2,
});
};
return (
<>
<g
data-testid="default-edge"
onMouseEnter={() => setIsEdgeHovered(true)}
onMouseLeave={() => setIsEdgeHovered(false)}
>
<BaseEdge
id={id}
path={edgePath}
style={style}
markerStart={markerStart}
markerEnd={markerEnd}
interactionWidth={interactionWidth}
/>
</g>
<EdgeLabelRenderer>
<button
type="button"
data-testid="default-edge-insert-button"
data-visible={isInsertVisible ? "true" : "false"}
aria-label="Insert node"
aria-hidden={!isInsertVisible}
disabled={!canInsert}
className="nodrag nopan absolute h-8 w-8 items-center justify-center rounded-full border-2 border-border/90 bg-background/95 text-foreground shadow-[0_2px_10px_rgba(0,0,0,0.28)] ring-1 ring-background/90 transition-opacity transition-shadow"
style={{
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px) scale(${buttonScale})`,
transformOrigin: "center center",
opacity: isInsertVisible ? 1 : 0,
pointerEvents: isInsertVisible ? "all" : "none",
display: "flex",
}}
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
onClick={handleInsertClick}
>
<Plus className="h-[18px] w-[18px] stroke-[2.5]" aria-hidden="true" />
</button>
</EdgeLabelRenderer>
</>
);
}