feat(canvas): enhance edge insertion animations and update reflow duration

- Added a new CSS animation for edge insertion to improve visual feedback during node creation.
- Updated the edge insertion reflow duration from 997ms to 1297ms for smoother transitions.
- Refactored related components to support the new animation and ensure consistent behavior across the canvas.
- Enhanced tests to validate the new edge insertion features and animations.
This commit is contained in:
2026-04-06 11:08:32 +02:00
parent b47720a50b
commit b7771764d8
5 changed files with 199 additions and 15 deletions

View File

@@ -746,8 +746,6 @@ export function useCanvasSyncEngine({
]);
});
const createNodeWithEdgeSplitMut = useMutation(api.nodes.createWithEdgeSplit);
const createEdge = useMutation(api.edges.create).withOptimisticUpdate(
(localStore, args) => {
const edgeList = localStore.getQuery(api.edges.list, {
@@ -849,7 +847,10 @@ export function useCanvasSyncEngine({
const addOptimisticNodeLocally = useCallback(
(
args: Parameters<typeof createNode>[0] & { clientRequestId: string },
args: Parameters<typeof createNode>[0] & {
clientRequestId: string;
className?: string;
},
): Id<"nodes"> => {
const optimisticNodeId = `${OPTIMISTIC_NODE_PREFIX}${args.clientRequestId}`;
setNodes((current) => {
@@ -866,6 +867,7 @@ export function useCanvasSyncEngine({
style: { width: args.width, height: args.height },
parentId: args.parentId as string | undefined,
zIndex: args.zIndex,
className: args.className,
selected: false,
},
];
@@ -1390,15 +1392,24 @@ export function useCanvasSyncEngine({
);
const runCreateNodeWithEdgeSplitOnlineOnly = useCallback(
async (args: Parameters<typeof createNodeWithEdgeSplitMut>[0]) => {
async (args: Parameters<typeof createNodeWithEdgeSplitRaw>[0]) => {
const clientRequestId = args.clientRequestId ?? crypto.randomUUID();
const payload = { ...args, clientRequestId };
const splitEdgeId = payload.splitEdgeId as string;
if (isSyncOnline) {
return await createNodeWithEdgeSplitMut(payload);
}
controller.pendingConnectionCreatesRef.current.add(clientRequestId);
const optimisticNodeId = addOptimisticNodeLocally(payload);
const originalSplitEdge = edgesRef.current.find(
(edge) =>
edge.id === splitEdgeId &&
edge.className !== "temp" &&
!isOptimisticEdgeId(edge.id),
);
const optimisticNodeId = addOptimisticNodeLocally({
...payload,
className: "canvas-edge-insert-enter",
});
const splitApplied = applyEdgeSplitLocally({
clientRequestId,
splitEdgeId: payload.splitEdgeId,
@@ -1411,6 +1422,34 @@ export function useCanvasSyncEngine({
positionY: payload.positionY,
});
if (isSyncOnline) {
try {
const realId = await trackPendingNodeCreate(
clientRequestId,
createNodeWithEdgeSplitRaw({ ...payload }),
);
await remapOptimisticNodeLocally(clientRequestId, realId);
return realId;
} catch (error) {
removeOptimisticCreateLocally({
clientRequestId,
removeNode: true,
removeEdge: true,
});
if (splitApplied && originalSplitEdge) {
setEdges((current) => {
if (current.some((edge) => edge.id === originalSplitEdge.id)) {
return current;
}
return [...current, originalSplitEdge];
});
}
throw error;
}
}
if (splitApplied) {
await enqueueSyncMutation("createNodeWithEdgeSplit", payload);
} else {
@@ -1430,7 +1469,19 @@ export function useCanvasSyncEngine({
return optimisticNodeId;
},
[addOptimisticNodeLocally, applyEdgeSplitLocally, createNodeWithEdgeSplitMut, enqueueSyncMutation, isSyncOnline],
[
addOptimisticNodeLocally,
applyEdgeSplitLocally,
controller.pendingConnectionCreatesRef,
createNodeWithEdgeSplitRaw,
edgesRef,
enqueueSyncMutation,
isSyncOnline,
remapOptimisticNodeLocally,
removeOptimisticCreateLocally,
setEdges,
trackPendingNodeCreate,
],
);
const runBatchRemoveNodesMutation = useCallback<RunBatchRemoveNodesMutation>(