feat(canvas): allow mixer renders and improve edge insert visibility

This commit is contained in:
2026-04-11 10:31:51 +02:00
parent ae2fa1d269
commit cda97f614b
4 changed files with 72 additions and 4 deletions

View File

@@ -8,6 +8,12 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import DefaultEdge from "@/components/canvas/edges/default-edge";
const mockViewport = {
x: 0,
y: 0,
zoom: 1,
};
vi.mock("@xyflow/react", async () => {
const actual = await vi.importActual<typeof import("@xyflow/react")>(
"@xyflow/react",
@@ -18,6 +24,7 @@ vi.mock("@xyflow/react", async () => {
EdgeLabelRenderer: ({ children }: { children: ReactNode }) => (
<foreignObject>{children}</foreignObject>
),
useViewport: () => mockViewport,
};
});
@@ -95,6 +102,8 @@ describe("DefaultEdge", () => {
let container: HTMLDivElement | null = null;
afterEach(() => {
mockViewport.zoom = 1;
if (root) {
act(() => {
root?.unmount();
@@ -185,4 +194,33 @@ describe("DefaultEdge", () => {
expect(edgePath).not.toBeNull();
expect(edgePath?.getAttribute("d")).toBeTruthy();
});
it("applies zoom-aware scaling with clamps to keep the plus legible", () => {
const onInsertClick = vi.fn<(anchor: EdgeInsertAnchor) => void>();
mockViewport.zoom = 0.2;
({ container, root } = renderEdge({ onInsertClick, isMenuOpen: true }));
const insertButton = getInsertButton(container);
expect(insertButton.style.transform).toContain("scale(2.2)");
mockViewport.zoom = 4;
act(() => {
root?.render(
<svg>
<DefaultEdgeComponent {...baseProps} onInsertClick={onInsertClick} isMenuOpen />
</svg>,
);
});
expect(insertButton.style.transform).toContain("scale(0.95)");
});
it("uses stronger visual styling for distant zoom visibility", () => {
const onInsertClick = vi.fn<(anchor: EdgeInsertAnchor) => void>();
({ container, root } = renderEdge({ onInsertClick, isMenuOpen: true }));
const insertButton = getInsertButton(container);
expect(insertButton.className).toContain("border-2");
expect(insertButton.className).toContain("ring-1");
expect(insertButton.className).toContain("shadow");
});
});

View File

@@ -5,10 +5,26 @@ 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;
@@ -41,6 +57,7 @@ export default function DefaultEdge({
}: DefaultEdgeProps) {
const [isEdgeHovered, setIsEdgeHovered] = useState(false);
const [isButtonHovered, setIsButtonHovered] = useState(false);
const { zoom } = useViewport();
const [edgePath, labelX, labelY] = useMemo(
() =>
@@ -58,6 +75,7 @@ export default function DefaultEdge({
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) {
@@ -97,9 +115,10 @@ export default function DefaultEdge({
aria-label="Insert node"
aria-hidden={!isInsertVisible}
disabled={!canInsert}
className="nodrag nopan absolute h-7 w-7 items-center justify-center rounded-full border border-border bg-background text-foreground shadow-sm transition-opacity"
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)`,
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px) scale(${buttonScale})`,
transformOrigin: "center center",
opacity: isInsertVisible ? 1 : 0,
pointerEvents: isInsertVisible ? "all" : "none",
display: "flex",
@@ -108,7 +127,7 @@ export default function DefaultEdge({
onMouseLeave={() => setIsButtonHovered(false)}
onClick={handleInsertClick}
>
<Plus className="h-4 w-4" aria-hidden="true" />
<Plus className="h-[18px] w-[18px] stroke-[2.5]" aria-hidden="true" />
</button>
</EdgeLabelRenderer>
</>