@canmingir/link 1.2.8 → 1.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.github/workflows/publish.yml +64 -7
  2. package/package.json +1 -1
  3. package/src/lib/Flow/connectors/DynamicConnector.jsx +247 -0
  4. package/src/lib/Flow/core/Flow.jsx +79 -0
  5. package/src/lib/Flow/core/FlowNode.jsx +68 -0
  6. package/src/lib/Flow/core/FlowViewport.jsx +259 -0
  7. package/src/lib/Flow/graph/FloatingGraph.jsx +44 -0
  8. package/src/lib/Flow/hooks/useGraphOperations.js +362 -0
  9. package/src/lib/Flow/hooks/useNodeStyle.js +56 -0
  10. package/src/lib/Flow/index.js +1 -1
  11. package/src/lib/Flow/layouts/InfoNode.jsx +115 -56
  12. package/src/lib/Flow/nodes/DefaultCard.jsx +107 -0
  13. package/src/lib/Flow/nodes/DraggableNode.jsx +162 -0
  14. package/src/lib/Flow/nodes/FlowNodeView.jsx +214 -0
  15. package/src/lib/Flow/selection/SelectionContext.jsx +259 -0
  16. package/src/lib/Flow/selection/SelectionOverlay.jsx +31 -0
  17. package/src/lib/Flow/styles.js +59 -19
  18. package/src/lib/Flow/utils/flowUtils.js +268 -0
  19. package/src/lib/index.js +1 -1
  20. package/.idea/codeStyles/Project.xml +0 -84
  21. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  22. package/.idea/copilot.data.migration.agent.xml +0 -6
  23. package/.idea/copilot.data.migration.ask.xml +0 -6
  24. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  25. package/.idea/copilot.data.migration.edit.xml +0 -6
  26. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  27. package/.idea/misc.xml +0 -5
  28. package/.idea/modules.xml +0 -8
  29. package/.idea/platform.iml +0 -9
  30. package/.idea/vcs.xml +0 -6
  31. package/src/lib/Flow/DraggableNode.jsx +0 -62
  32. package/src/lib/Flow/DynamicConnector.jsx +0 -176
  33. package/src/lib/Flow/Flow.jsx +0 -40
  34. package/src/lib/Flow/FlowNode.jsx +0 -371
  35. package/src/lib/Flow/flowUtils.js +0 -111
@@ -0,0 +1,259 @@
1
+ import { Box } from "@mui/material";
2
+ import FloatingGraph from "../graph/FloatingGraph";
3
+ import SelectionOverlay from "../selection/SelectionOverlay";
4
+ import { useSelection } from "../selection/SelectionContext";
5
+
6
+ import React, { useEffect, useRef, useState } from "react";
7
+
8
+ const FlowViewport = ({
9
+ children,
10
+ selectionColor = "#64748b",
11
+ nodesById,
12
+ onPaste,
13
+ onCut,
14
+ onConnect,
15
+ floatingNodes = [],
16
+ variant,
17
+ style,
18
+ plugin,
19
+ }) => {
20
+ const clampZoom = (zoom) => Math.min(2.5, Math.max(0.25, zoom));
21
+
22
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
23
+ const [zoom, setZoom] = useState(1);
24
+ const [isDragging, setIsDragging] = useState(false);
25
+ const [selectionBox, setSelectionBox] = useState(null);
26
+
27
+ const containerRef = useRef(null);
28
+ const selectionBoxRef = useRef(null);
29
+ const mousePositionRef = useRef({ x: 0, y: 0 });
30
+
31
+ const {
32
+ clearSelection,
33
+ selectMultiple,
34
+ addToSelection,
35
+ cutSelectedNodes,
36
+ pasteNodes,
37
+ selectedIds,
38
+ } = useSelection();
39
+
40
+ useEffect(() => {
41
+ const handleMouseMove = (e) => {
42
+ mousePositionRef.current = { x: e.clientX, y: e.clientY };
43
+ };
44
+
45
+ window.addEventListener("mousemove", handleMouseMove);
46
+
47
+ return () => {
48
+ window.removeEventListener("mousemove", handleMouseMove);
49
+ };
50
+ }, []);
51
+
52
+ useEffect(() => {
53
+ const onWheel = (e) => {
54
+ const wantsZoom = e.ctrlKey || e.metaKey;
55
+ if (!wantsZoom) return;
56
+ e.preventDefault();
57
+ const direction = e.deltaY > 0 ? -1 : 1;
58
+ const factor = direction > 0 ? 1.1 : 1 / 1.1;
59
+ setZoom((z) => clampZoom(z * factor));
60
+ };
61
+ window.addEventListener("wheel", onWheel, { passive: false });
62
+ return () => window.removeEventListener("wheel", onWheel);
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ const handleKeyDown = (e) => {
67
+ const isMod = e.ctrlKey || e.metaKey;
68
+ if (!isMod) return;
69
+
70
+ if (e.key === "x" && selectedIds.size > 0 && nodesById) {
71
+ e.preventDefault();
72
+ cutSelectedNodes(nodesById, onCut);
73
+ }
74
+
75
+ if (e.key === "v" && onPaste) {
76
+ e.preventDefault();
77
+ const containerRect = containerRef.current?.getBoundingClientRect();
78
+ const mousePos = mousePositionRef.current;
79
+
80
+ const canvasX = containerRect
81
+ ? (mousePos.x -
82
+ containerRect.left -
83
+ containerRect.width / 2 -
84
+ offset.x) /
85
+ zoom
86
+ : 0;
87
+ const canvasY = containerRect
88
+ ? (mousePos.y -
89
+ containerRect.top -
90
+ containerRect.height / 2 -
91
+ offset.y) /
92
+ zoom
93
+ : 0;
94
+
95
+ pasteNodes(onPaste, canvasX, canvasY);
96
+ }
97
+ };
98
+
99
+ window.addEventListener("keydown", handleKeyDown);
100
+ return () => window.removeEventListener("keydown", handleKeyDown);
101
+ }, [
102
+ cutSelectedNodes,
103
+ pasteNodes,
104
+ selectedIds,
105
+ nodesById,
106
+ onPaste,
107
+ onCut,
108
+ offset,
109
+ zoom,
110
+ ]);
111
+
112
+ const handleViewportMouseDown = (e) => {
113
+ if (e.target?.closest?.(".MuiCard-root") || e.target?.closest?.("button"))
114
+ return;
115
+
116
+ if (e.button !== 0) return;
117
+
118
+ const startX = e.clientX;
119
+ const startY = e.clientY;
120
+
121
+ if (e.shiftKey || e.ctrlKey || e.metaKey) {
122
+ setSelectionBox({ startX, startY, currentX: startX, currentY: startY });
123
+ selectionBoxRef.current = {
124
+ startX,
125
+ startY,
126
+ currentX: startX,
127
+ currentY: startY,
128
+ };
129
+
130
+ const onMove = (ev) => {
131
+ const newBox = {
132
+ startX,
133
+ startY,
134
+ currentX: ev.clientX,
135
+ currentY: ev.clientY,
136
+ };
137
+ setSelectionBox(newBox);
138
+ selectionBoxRef.current = newBox;
139
+ };
140
+
141
+ const onUp = () => {
142
+ if (containerRef.current && selectionBoxRef.current) {
143
+ const box = selectionBoxRef.current;
144
+ const nodes = containerRef.current.querySelectorAll("[data-node-id]");
145
+ const selectedNodeIds = [];
146
+
147
+ const boxLeft = Math.min(box.startX, box.currentX);
148
+ const boxRight = Math.max(box.startX, box.currentX);
149
+ const boxTop = Math.min(box.startY, box.currentY);
150
+ const boxBottom = Math.max(box.startY, box.currentY);
151
+
152
+ nodes.forEach((node) => {
153
+ const rect = node.getBoundingClientRect();
154
+
155
+ if (
156
+ rect.left < boxRight &&
157
+ rect.right > boxLeft &&
158
+ rect.top < boxBottom &&
159
+ rect.bottom > boxTop
160
+ ) {
161
+ const nodeId = node.getAttribute("data-node-id");
162
+ if (nodeId) selectedNodeIds.push(nodeId);
163
+ }
164
+ });
165
+
166
+ if (selectedNodeIds.length > 0) {
167
+ if (e.shiftKey) {
168
+ addToSelection(selectedNodeIds);
169
+ } else {
170
+ selectMultiple(selectedNodeIds);
171
+ }
172
+ }
173
+ }
174
+
175
+ setSelectionBox(null);
176
+ selectionBoxRef.current = null;
177
+ window.removeEventListener("mousemove", onMove);
178
+ window.removeEventListener("mouseup", onUp);
179
+ };
180
+
181
+ window.addEventListener("mousemove", onMove);
182
+ window.addEventListener("mouseup", onUp);
183
+ return;
184
+ }
185
+
186
+ clearSelection();
187
+
188
+ setIsDragging(true);
189
+ const startOffset = { ...offset };
190
+
191
+ const onMove = (ev) => {
192
+ setOffset({
193
+ x: startOffset.x + (ev.clientX - startX),
194
+ y: startOffset.y + (ev.clientY - startY),
195
+ });
196
+ };
197
+
198
+ const onUp = () => {
199
+ setIsDragging(false);
200
+ window.removeEventListener("mousemove", onMove);
201
+ window.removeEventListener("mouseup", onUp);
202
+ };
203
+
204
+ window.addEventListener("mousemove", onMove);
205
+ window.addEventListener("mouseup", onUp);
206
+ };
207
+
208
+ return (
209
+ <Box
210
+ ref={containerRef}
211
+ onMouseDown={handleViewportMouseDown}
212
+ sx={{
213
+ width: "100vw",
214
+ height: "100vh",
215
+ overflow: "hidden",
216
+ bgcolor: "none",
217
+ cursor: isDragging ? "grabbing" : "default",
218
+ userSelect: "none",
219
+ position: "relative",
220
+ }}
221
+ >
222
+ <SelectionOverlay box={selectionBox} selectionColor={selectionColor} />
223
+ <Box
224
+ sx={{
225
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
226
+ transformOrigin: "center center",
227
+ width: "100%",
228
+ height: "100%",
229
+ display: "flex",
230
+ alignItems: "center",
231
+ justifyContent: "center",
232
+ transition: isDragging ? "none" : "transform 0.1s ease-out",
233
+ pointerEvents: "auto",
234
+ position: "relative",
235
+ }}
236
+ >
237
+ {children}
238
+ {floatingNodes.map((structure, index) => {
239
+ const structureKey =
240
+ (structure && (structure.id || structure.key)) ??
241
+ `floating-${index}`;
242
+ return (
243
+ <FloatingGraph
244
+ key={structureKey}
245
+ structure={structure}
246
+ variant={variant}
247
+ style={style}
248
+ plugin={plugin}
249
+ selectionColor={selectionColor}
250
+ onConnect={onConnect}
251
+ />
252
+ );
253
+ })}
254
+ </Box>
255
+ </Box>
256
+ );
257
+ };
258
+
259
+ export default FlowViewport;
@@ -0,0 +1,44 @@
1
+ import { Box } from "@mui/material";
2
+ import FlowNodeView from "../nodes/FlowNodeView";
3
+ import { buildDetachedTree } from "../utils/flowUtils";
4
+
5
+ import React, { useMemo } from "react";
6
+
7
+ const FloatingGraph = ({ structure, variant, style, plugin, onConnect }) => {
8
+ const position = structure?._pastePosition || { x: 0, y: 0 };
9
+
10
+ const treesData = useMemo(() => {
11
+ if (!structure?.roots?.length || !structure?.nodes) return [];
12
+ return structure.roots
13
+ .map((rootId) => buildDetachedTree(rootId, structure.nodes))
14
+ .filter(Boolean);
15
+ }, [structure]);
16
+
17
+ if (!treesData.length) return null;
18
+
19
+ return (
20
+ <Box
21
+ sx={{
22
+ position: "absolute",
23
+ left: "50%",
24
+ top: "50%",
25
+ transform: `translate(${position.x}px, ${position.y}px)`,
26
+ display: "flex",
27
+ gap: 2,
28
+ }}
29
+ >
30
+ {treesData.map((tree, idx) => (
31
+ <FlowNodeView
32
+ key={tree.id || `floating-${idx}`}
33
+ node={tree}
34
+ variant={variant}
35
+ style={style}
36
+ plugin={plugin}
37
+ onConnect={onConnect}
38
+ />
39
+ ))}
40
+ </Box>
41
+ );
42
+ };
43
+
44
+ export default FloatingGraph;
@@ -0,0 +1,362 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+
3
+ import React, { useCallback } from "react";
4
+ import {
5
+ addToNext,
6
+ cleanupReferences,
7
+ collectSubtree,
8
+ getRootsToConnect,
9
+ getSelectedInStructure,
10
+ removeFromNext,
11
+ splitFloatingStructure,
12
+ toNextArray,
13
+ } from "../utils/flowUtils";
14
+
15
+ export const useGraphOperations = ({
16
+ nodesById,
17
+ roots,
18
+ onChange,
19
+ floatingNodes,
20
+ setFloatingNodes,
21
+ editable,
22
+ }) => {
23
+ const findFloatingStructure = useCallback(
24
+ (nodeId) => {
25
+ const index = floatingNodes.findIndex(
26
+ (s) => s?.nodes && nodeId in s.nodes
27
+ );
28
+ return index >= 0 ? { structure: floatingNodes[index], index } : null;
29
+ },
30
+ [floatingNodes]
31
+ );
32
+
33
+ const handleCut = useCallback(
34
+ (nodeIds) => {
35
+ if (!editable || !onChange || !nodesById) return;
36
+
37
+ const cutSet = new Set(nodeIds);
38
+
39
+ const isInMainTree = nodeIds.some((id) => nodesById[id]);
40
+ const isInFloating = nodeIds.some((id) =>
41
+ floatingNodes.some((s) => s.nodes?.[id])
42
+ );
43
+
44
+ if (isInMainTree) {
45
+ const updatedNodes = { ...nodesById };
46
+ let updatedRoots = [...(roots || [])];
47
+
48
+ nodeIds.forEach((cutId) => {
49
+ const cutNode = updatedNodes[cutId];
50
+ if (!cutNode) return;
51
+
52
+ const parentId = cutNode.previous;
53
+ const childIds = toNextArray(cutNode.next);
54
+ const orphanIds = childIds.filter((id) => !cutSet.has(id));
55
+
56
+ orphanIds.forEach((orphanId) => {
57
+ const orphan = updatedNodes[orphanId];
58
+ if (!orphan) return;
59
+
60
+ const hasValidParent =
61
+ parentId && updatedNodes[parentId] && !cutSet.has(parentId);
62
+
63
+ if (hasValidParent) {
64
+ orphan.previous = parentId;
65
+ addToNext(updatedNodes[parentId], [orphanId]);
66
+ } else {
67
+ delete orphan.previous;
68
+ if (!updatedRoots.includes(orphanId)) {
69
+ updatedRoots.push(orphanId);
70
+ }
71
+ }
72
+ });
73
+ });
74
+
75
+ nodeIds.forEach((id) => delete updatedNodes[id]);
76
+ updatedRoots = updatedRoots.filter((r) => !cutSet.has(r));
77
+
78
+ cleanupReferences(updatedNodes, nodeIds);
79
+
80
+ onChange({ nodes: updatedNodes, roots: [...new Set(updatedRoots)] });
81
+ }
82
+
83
+ if (isInFloating) {
84
+ setFloatingNodes((prev) =>
85
+ prev
86
+ .map((structure) => {
87
+ const hasAffectedNodes = nodeIds.some(
88
+ (id) => structure.nodes?.[id]
89
+ );
90
+ if (!hasAffectedNodes) return structure;
91
+
92
+ const newNodes = { ...structure.nodes };
93
+ const newRoots = [...(structure.roots || [])];
94
+
95
+ nodeIds.forEach((id) => {
96
+ if (newNodes[id]) {
97
+ const node = newNodes[id];
98
+ const childIds = toNextArray(node.next);
99
+ const orphanIds = childIds.filter((cid) => !cutSet.has(cid));
100
+
101
+ orphanIds.forEach((orphanId) => {
102
+ if (newNodes[orphanId]) {
103
+ const parent = node.previous;
104
+ if (parent && newNodes[parent] && !cutSet.has(parent)) {
105
+ newNodes[orphanId].previous = parent;
106
+ addToNext(newNodes[parent], [orphanId]);
107
+ } else {
108
+ delete newNodes[orphanId].previous;
109
+ if (!newRoots.includes(orphanId)) {
110
+ newRoots.push(orphanId);
111
+ }
112
+ }
113
+ }
114
+ });
115
+
116
+ delete newNodes[id];
117
+ }
118
+ });
119
+
120
+ const filteredRoots = newRoots.filter(
121
+ (r) => !cutSet.has(r) && newNodes[r]
122
+ );
123
+
124
+ cleanupReferences(newNodes, nodeIds);
125
+
126
+ if (Object.keys(newNodes).length === 0) return null;
127
+
128
+ return {
129
+ ...structure,
130
+ nodes: newNodes,
131
+ roots: [...new Set(filteredRoots)],
132
+ };
133
+ })
134
+ .filter(Boolean)
135
+ );
136
+ }
137
+ },
138
+ [editable, onChange, nodesById, roots, floatingNodes, setFloatingNodes]
139
+ );
140
+
141
+ const handlePaste = useCallback(
142
+ (structure) => {
143
+ if (!editable || !structure?.nodes || !structure?.roots?.length) return;
144
+
145
+ const mainNodeIds = new Set(Object.keys(nodesById || {}));
146
+
147
+ const validNodes = {};
148
+ const validNodeIds = Object.keys(structure.nodes).filter(
149
+ (id) => !mainNodeIds.has(id)
150
+ );
151
+
152
+ if (validNodeIds.length === 0) return;
153
+
154
+ validNodeIds.forEach((id) => {
155
+ validNodes[id] = structure.nodes[id];
156
+ });
157
+
158
+ const internalChildIds = new Set();
159
+
160
+ validNodeIds.forEach((id) => {
161
+ const node = validNodes[id];
162
+
163
+ const nextIds = toNextArray(node.next || node.raw?.next);
164
+ nextIds.forEach((childId) => internalChildIds.add(childId));
165
+ });
166
+
167
+ const validRoots = structure.roots.filter(
168
+ (id) => validNodes[id] && !internalChildIds.has(id)
169
+ );
170
+
171
+ if (validRoots.length === 0) return;
172
+
173
+ const newPasteItem = {
174
+ ...structure,
175
+ nodes: validNodes,
176
+ roots: [...new Set(validRoots)],
177
+ _pasteId: uuidv4(),
178
+ };
179
+
180
+ setFloatingNodes((prev) => {
181
+ const existingFloatingIds = new Set(
182
+ prev.flatMap((item) => Object.keys(item.nodes))
183
+ );
184
+
185
+ const hasOverlap = validNodeIds.some((id) =>
186
+ existingFloatingIds.has(id)
187
+ );
188
+
189
+ if (hasOverlap) {
190
+ console.log("Paste blocked: Nodes already exist in floating layer.");
191
+ return prev;
192
+ }
193
+
194
+ return [...prev, newPasteItem];
195
+ });
196
+ },
197
+ [editable, nodesById, setFloatingNodes]
198
+ );
199
+
200
+ const handleConnect = useCallback(
201
+ (targetNodeId, selectedIds) => {
202
+ if (!editable || !onChange || !selectedIds?.length) return;
203
+
204
+ const targetInTree = nodesById?.[targetNodeId];
205
+ const targetFloating = findFloatingStructure(targetNodeId);
206
+ const sourceInTree = selectedIds.some((id) => nodesById?.[id]);
207
+ const sourceFloating = selectedIds
208
+ .map((id) => findFloatingStructure(id))
209
+ .find(Boolean);
210
+
211
+ if (sourceFloating && targetInTree) {
212
+ const { structure, index } = sourceFloating;
213
+ const selectedInStructure = getSelectedInStructure(
214
+ structure,
215
+ selectedIds
216
+ );
217
+ if (!selectedInStructure.length) return;
218
+
219
+ const rootsToConnect = getRootsToConnect(
220
+ structure,
221
+ selectedInStructure
222
+ );
223
+ const connectedNodeIds = collectSubtree(
224
+ structure,
225
+ rootsToConnect,
226
+ selectedInStructure
227
+ );
228
+
229
+ const updatedNodes = { ...nodesById };
230
+
231
+ connectedNodeIds.forEach((id) => {
232
+ updatedNodes[id] = { ...structure.nodes[id] };
233
+ });
234
+
235
+ rootsToConnect.forEach((rootId) => {
236
+ updatedNodes[rootId].previous = targetNodeId;
237
+ });
238
+
239
+ addToNext(updatedNodes[targetNodeId], rootsToConnect);
240
+
241
+ onChange({ nodes: updatedNodes, roots });
242
+
243
+ setFloatingNodes((prev) => {
244
+ if (connectedNodeIds.size === Object.keys(structure.nodes).length) {
245
+ return prev.filter((_, i) => i !== index);
246
+ }
247
+
248
+ const updated = [...prev];
249
+ updated[index] = splitFloatingStructure(structure, connectedNodeIds);
250
+ return updated.filter((s) => Object.keys(s.nodes).length > 0);
251
+ });
252
+ return;
253
+ }
254
+
255
+ if (sourceInTree && targetFloating) {
256
+ const { structure, index } = targetFloating;
257
+ const updatedNodes = { ...nodesById };
258
+ let updatedRoots = [...(roots || [])];
259
+
260
+ selectedIds.forEach((id) => {
261
+ const node = updatedNodes[id];
262
+ if (!node) return;
263
+
264
+ if (node.previous) {
265
+ removeFromNext(updatedNodes[node.previous], [id]);
266
+ }
267
+
268
+ updatedRoots = updatedRoots.filter((r) => r !== id);
269
+ delete updatedNodes[id];
270
+ });
271
+
272
+ onChange({ nodes: updatedNodes, roots: updatedRoots });
273
+
274
+ setFloatingNodes((prev) => {
275
+ const updated = [...prev];
276
+ const newStructure = {
277
+ ...structure,
278
+ nodes: { ...structure.nodes },
279
+ };
280
+
281
+ selectedIds.forEach((id) => {
282
+ if (nodesById[id]) {
283
+ newStructure.nodes[id] = {
284
+ ...nodesById[id],
285
+ previous: targetNodeId,
286
+ next: undefined,
287
+ };
288
+ }
289
+ });
290
+
291
+ addToNext(newStructure.nodes[targetNodeId], selectedIds);
292
+ updated[index] = newStructure;
293
+ return updated;
294
+ });
295
+ return;
296
+ }
297
+
298
+ if (sourceFloating && targetFloating) {
299
+ const { structure: sourceStruct, index: sourceIndex } = sourceFloating;
300
+ const { index: targetIndex } = targetFloating;
301
+ if (sourceIndex === targetIndex) return;
302
+
303
+ const selectedInStructure = getSelectedInStructure(
304
+ sourceStruct,
305
+ selectedIds
306
+ );
307
+ if (!selectedInStructure.length) return;
308
+
309
+ const rootsToConnect = getRootsToConnect(
310
+ sourceStruct,
311
+ selectedInStructure
312
+ );
313
+ const connectedNodeIds = collectSubtree(
314
+ sourceStruct,
315
+ rootsToConnect,
316
+ selectedInStructure
317
+ );
318
+
319
+ setFloatingNodes((prev) => {
320
+ const updated = prev.filter((_, i) => i !== sourceIndex);
321
+ const adjustedIndex =
322
+ targetIndex > sourceIndex ? targetIndex - 1 : targetIndex;
323
+
324
+ const targetStruct = updated[adjustedIndex];
325
+ const merged = {
326
+ ...targetStruct,
327
+ nodes: { ...targetStruct.nodes },
328
+ };
329
+
330
+ connectedNodeIds.forEach((id) => {
331
+ merged.nodes[id] = { ...sourceStruct.nodes[id] };
332
+ });
333
+
334
+ rootsToConnect.forEach((rootId) => {
335
+ merged.nodes[rootId].previous = targetNodeId;
336
+ });
337
+
338
+ addToNext(merged.nodes[targetNodeId], rootsToConnect);
339
+ updated[adjustedIndex] = merged;
340
+
341
+ if (connectedNodeIds.size < Object.keys(sourceStruct.nodes).length) {
342
+ updated.push(
343
+ splitFloatingStructure(sourceStruct, connectedNodeIds)
344
+ );
345
+ }
346
+
347
+ return updated.filter((s) => Object.keys(s.nodes).length > 0);
348
+ });
349
+ }
350
+ },
351
+ [
352
+ editable,
353
+ onChange,
354
+ nodesById,
355
+ roots,
356
+ findFloatingStructure,
357
+ setFloatingNodes,
358
+ ]
359
+ );
360
+
361
+ return { handleCut, handlePaste, handleConnect };
362
+ };
@@ -0,0 +1,56 @@
1
+ import { useMemo } from "react";
2
+
3
+ import {
4
+ applySemanticTokens,
5
+ getBaseStyleForVariant,
6
+ getDecisionNodeStyle,
7
+ } from "../styles";
8
+
9
+ export const useNodeStyle = ({ node, type, variant, style, plugin }) => {
10
+ return useMemo(() => {
11
+ const baseStyle = getBaseStyleForVariant(variant);
12
+
13
+ const variantTokens =
14
+ variant === "decision" ? getDecisionNodeStyle(node?.type) : {};
15
+
16
+ let styleTokens = {};
17
+ if (typeof style === "function") {
18
+ styleTokens = style(node) || {};
19
+ } else if (style && typeof style === "object") {
20
+ styleTokens = style;
21
+ }
22
+
23
+ let resolvedPlugin = null;
24
+ if (plugin) {
25
+ if (typeof plugin === "function") {
26
+ resolvedPlugin = plugin(type, node) || null;
27
+ } else if (typeof plugin === "object") {
28
+ resolvedPlugin = plugin;
29
+ }
30
+ }
31
+
32
+ let pluginTokens = {};
33
+ if (resolvedPlugin && typeof resolvedPlugin.style === "function") {
34
+ pluginTokens =
35
+ resolvedPlugin.style({
36
+ node,
37
+ style: styleTokens,
38
+ }) || {};
39
+ }
40
+
41
+ const rawNodeStyle = {
42
+ ...baseStyle,
43
+ ...variantTokens,
44
+ ...styleTokens,
45
+ ...pluginTokens,
46
+ };
47
+
48
+ const nodeStyle = applySemanticTokens(rawNodeStyle, baseStyle);
49
+
50
+ return {
51
+ baseStyle,
52
+ nodeStyle,
53
+ plugin: resolvedPlugin,
54
+ };
55
+ }, [node, type, variant, style, plugin]);
56
+ };