@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,214 @@
1
+ import { Box } from "@mui/material";
2
+ import DefaultNodeCard from "./DefaultCard";
3
+ import DraggableNode from "./DraggableNode";
4
+ import DynamicConnector from "../connectors/DynamicConnector";
5
+ import FlowNode from "../core/FlowNode";
6
+ import { getContentParts } from "../utils/flowUtils";
7
+ import { toPxNumber } from "../styles";
8
+ import { useNodeStyle } from "../hooks/useNodeStyle";
9
+
10
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
11
+
12
+ const FlowNodeView = ({
13
+ node,
14
+ type,
15
+ variant,
16
+ style,
17
+ plugin,
18
+ registerRef,
19
+ onDrag,
20
+ onConnect,
21
+ }) => {
22
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
23
+
24
+ const {
25
+ baseStyle,
26
+ nodeStyle,
27
+ plugin: _plugin,
28
+ } = useNodeStyle({
29
+ node,
30
+ type,
31
+ variant,
32
+ style,
33
+ plugin,
34
+ });
35
+
36
+ const {
37
+ direction = "vertical",
38
+ lineColor = baseStyle.lineColor,
39
+ lineWidth = baseStyle.lineWidth,
40
+ lineStyle = baseStyle.lineStyle,
41
+ gap = baseStyle.gap,
42
+ levelGap = baseStyle.levelGap ?? 2.5,
43
+ nodeSx = {},
44
+ borderWidth,
45
+ borderColor = baseStyle.borderColor,
46
+ cardWidth,
47
+ shape,
48
+ shadowLevel,
49
+ minHeight,
50
+ showDots = baseStyle.showDots ?? false,
51
+ dotRadius = baseStyle.dotRadius ?? 4,
52
+ dotColor = baseStyle.dotColor,
53
+ showArrow = baseStyle.showArrow ?? true,
54
+ arrowSize = baseStyle.arrowSize ?? 6,
55
+ animated = baseStyle.animated ?? false,
56
+ animationSpeed = baseStyle.animationSpeed ?? 1,
57
+ gradient = baseStyle.gradient ?? null,
58
+ curvature = baseStyle.curvature ?? 0.5,
59
+ selectionColor = baseStyle.selectionColor ?? "#64748b",
60
+ } = nodeStyle;
61
+
62
+ const isHorizontal = direction === "horizontal";
63
+
64
+ const strokeWidth = toPxNumber(lineWidth, 1.5);
65
+ const dashStyle =
66
+ lineStyle === "dashed" || lineStyle === "dotted" ? lineStyle : "solid";
67
+
68
+ const containerRef = useRef(null);
69
+ const parentRef = useRef(null);
70
+ const childRefs = useRef({});
71
+ const [childElList, setChildElList] = useState([]);
72
+
73
+ const [connectorTick, setConnectorTick] = useState(0);
74
+
75
+ const handleDrag = (newOffset) => {
76
+ setConnectorTick((t) => t + 1);
77
+ if (onDrag) onDrag(newOffset);
78
+ };
79
+
80
+ useLayoutEffect(() => {
81
+ const els = (node.children || [])
82
+ .map((c) => childRefs.current[c.id])
83
+ .filter(Boolean);
84
+ setChildElList(els);
85
+ }, [node.children]);
86
+
87
+ useEffect(() => {
88
+ const t = setTimeout(() => {
89
+ const els = (node.children || [])
90
+ .map((c) => childRefs.current[c.id])
91
+ .filter(Boolean);
92
+ setChildElList(els);
93
+ }, 0);
94
+ return () => clearTimeout(t);
95
+ }, [node.children]);
96
+
97
+ const { title, subtitle, metaEntries } = getContentParts(node);
98
+
99
+ const renderContent = () => {
100
+ if (_plugin && typeof _plugin.node === "function") {
101
+ return _plugin.node({
102
+ node,
103
+ title,
104
+ subtitle,
105
+ metaEntries,
106
+ nodeStyle,
107
+ baseStyle,
108
+ });
109
+ }
110
+ return (
111
+ <DefaultNodeCard
112
+ title={title}
113
+ subtitle={subtitle}
114
+ metaEntries={metaEntries}
115
+ nodeStyle={nodeStyle}
116
+ baseStyle={baseStyle}
117
+ variant={variant}
118
+ borderWidth={borderWidth}
119
+ borderColor={borderColor}
120
+ cardWidth={cardWidth}
121
+ shape={shape}
122
+ shadowLevel={shadowLevel}
123
+ minHeight={minHeight}
124
+ nodeSx={nodeSx}
125
+ />
126
+ );
127
+ };
128
+
129
+ return (
130
+ <Box
131
+ ref={containerRef}
132
+ sx={{
133
+ display: "inline-flex",
134
+ flexDirection: isHorizontal ? "row" : "column",
135
+ alignItems: "center",
136
+ position: "relative",
137
+ }}
138
+ >
139
+ <DraggableNode
140
+ registerRef={(el) => {
141
+ parentRef.current = el;
142
+ if (registerRef) registerRef(el);
143
+ }}
144
+ onDrag={handleDrag}
145
+ nodeId={node.id}
146
+ selectionColor={selectionColor}
147
+ initialPosition={node._pastePosition}
148
+ onConnect={onConnect}
149
+ >
150
+ {renderContent()}
151
+ </DraggableNode>
152
+
153
+ {hasChildren && (
154
+ <>
155
+ <DynamicConnector
156
+ containerEl={containerRef.current}
157
+ parentEl={parentRef.current}
158
+ childEls={childElList}
159
+ stroke={lineColor}
160
+ strokeWidth={strokeWidth}
161
+ lineStyle={dashStyle}
162
+ tick={connectorTick}
163
+ orientation={direction}
164
+ showDots={showDots}
165
+ dotRadius={dotRadius}
166
+ dotColor={dotColor}
167
+ showArrow={showArrow}
168
+ arrowSize={arrowSize}
169
+ animated={animated}
170
+ animationSpeed={animationSpeed}
171
+ gradient={gradient}
172
+ curvature={curvature}
173
+ />
174
+
175
+ <Box
176
+ sx={{
177
+ display: "flex",
178
+ flexDirection: isHorizontal ? "column" : "row",
179
+ ...(isHorizontal
180
+ ? {
181
+ marginLeft: levelGap,
182
+ rowGap: gap,
183
+ }
184
+ : {
185
+ marginTop: levelGap,
186
+ columnGap: gap,
187
+ }),
188
+ position: "relative",
189
+ alignItems: "flex-start",
190
+ justifyContent: "center",
191
+ }}
192
+ >
193
+ {node.children.map((child) => (
194
+ <FlowNode
195
+ key={child.id}
196
+ node={child}
197
+ type={type}
198
+ variant={variant}
199
+ style={style}
200
+ plugin={plugin}
201
+ registerRef={(el) => (childRefs.current[child.id] = el)}
202
+ onDrag={() => setConnectorTick((t) => t + 1)}
203
+ isRoot={false}
204
+ onConnect={onConnect}
205
+ />
206
+ ))}
207
+ </Box>
208
+ </>
209
+ )}
210
+ </Box>
211
+ );
212
+ };
213
+
214
+ export default FlowNodeView;
@@ -0,0 +1,259 @@
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+
10
+ const SelectionContext = createContext(null);
11
+
12
+ const DEFAULT_CONTEXT = {
13
+ selectedIds: new Set(),
14
+ selectNode: () => {},
15
+ deselectNode: () => {},
16
+ toggleSelection: () => {},
17
+ clearSelection: () => {},
18
+ selectMultiple: () => {},
19
+ addToSelection: () => {},
20
+ isSelected: () => false,
21
+ registerNodeHandlers: () => () => {},
22
+ moveSelectedNodes: () => {},
23
+ cutSelectedNodes: () => {},
24
+ pasteNodes: () => {},
25
+ hasClipboard: false,
26
+ };
27
+
28
+ const filterNextToSelection = (next, selectedSet) => {
29
+ if (!next) return undefined;
30
+
31
+ if (Array.isArray(next)) {
32
+ const filtered = next.filter((id) => selectedSet.has(id));
33
+ return filtered.length > 0 ? filtered : undefined;
34
+ }
35
+
36
+ return selectedSet.has(next) ? next : undefined;
37
+ };
38
+
39
+ const buildClipboardNode = (id, node, selectedSet) => {
40
+ const filteredNext = filterNextToSelection(node.next, selectedSet);
41
+ const hasPreviousInSelection =
42
+ node.previous && selectedSet.has(node.previous);
43
+
44
+ return {
45
+ ...node,
46
+ id,
47
+ _originalId: id,
48
+ next: filteredNext,
49
+ previous: hasPreviousInSelection ? node.previous : undefined,
50
+ };
51
+ };
52
+
53
+ const buildPasteStructure = (clipboardNodes, position) => {
54
+ if (!clipboardNodes?.length) return null;
55
+
56
+ const seenIds = new Set();
57
+ const nodes = {};
58
+
59
+ clipboardNodes.forEach((clipNode) => {
60
+ const nodeId = clipNode._originalId || clipNode.id;
61
+ if (!nodeId || seenIds.has(nodeId)) return;
62
+
63
+ seenIds.add(nodeId);
64
+
65
+ nodes[nodeId] = {
66
+ id: nodeId,
67
+ label: clipNode.label,
68
+ next: clipNode.next,
69
+ previous: clipNode.previous,
70
+ };
71
+
72
+ Object.keys(clipNode).forEach((key) => {
73
+ if (!["_originalId", "id", "next", "previous", "label"].includes(key)) {
74
+ nodes[nodeId][key] = clipNode[key];
75
+ }
76
+ });
77
+ });
78
+
79
+ const rootIds = Object.keys(nodes).filter((id) => !nodes[id].previous);
80
+ const uniqueRoots = [...new Set(rootIds)];
81
+ const finalRoots =
82
+ uniqueRoots.length > 0 ? uniqueRoots : [Object.keys(nodes)[0]];
83
+
84
+ return {
85
+ nodes,
86
+ roots: finalRoots,
87
+ _pastePosition: position,
88
+ };
89
+ };
90
+
91
+ export const SelectionProvider = ({ children }) => {
92
+ const [selectedIds, setSelectedIds] = useState(new Set());
93
+ const [clipboard, setClipboard] = useState([]);
94
+ const nodeHandlersRef = useRef(new Map());
95
+ const isPastingRef = useRef(false);
96
+
97
+ const selectNode = useCallback((id, addToSelection = false) => {
98
+ setSelectedIds((prev) => {
99
+ const next = new Set(addToSelection ? prev : []);
100
+ next.add(id);
101
+ return next;
102
+ });
103
+ }, []);
104
+
105
+ const deselectNode = useCallback((id) => {
106
+ setSelectedIds((prev) => {
107
+ const next = new Set(prev);
108
+ next.delete(id);
109
+ return next;
110
+ });
111
+ }, []);
112
+
113
+ const toggleSelection = useCallback((id) => {
114
+ setSelectedIds((prev) => {
115
+ const next = new Set(prev);
116
+ next.has(id) ? next.delete(id) : next.add(id);
117
+ return next;
118
+ });
119
+ }, []);
120
+
121
+ const clearSelection = useCallback(() => setSelectedIds(new Set()), []);
122
+
123
+ const selectMultiple = useCallback((ids) => setSelectedIds(new Set(ids)), []);
124
+
125
+ const addToSelection = useCallback(
126
+ (ids) => setSelectedIds((prev) => new Set([...prev, ...ids])),
127
+ []
128
+ );
129
+
130
+ const isSelected = useCallback((id) => selectedIds.has(id), [selectedIds]);
131
+
132
+ const registerNodeHandlers = useCallback((id, handlers) => {
133
+ nodeHandlersRef.current.set(id, handlers);
134
+ return () => nodeHandlersRef.current.delete(id);
135
+ }, []);
136
+
137
+ const moveSelectedNodes = useCallback(
138
+ (deltaX, deltaY, excludeId = null) => {
139
+ selectedIds.forEach((id) => {
140
+ if (id === excludeId) return;
141
+
142
+ const handlers = nodeHandlersRef.current.get(id);
143
+ if (!handlers) return;
144
+
145
+ handlers.setOffset?.((prev) => ({
146
+ x: prev.x + deltaX,
147
+ y: prev.y + deltaY,
148
+ }));
149
+ handlers.onDrag?.();
150
+ });
151
+ },
152
+ [selectedIds]
153
+ );
154
+
155
+ const cutSelectedNodes = useCallback(
156
+ (nodesById, onCut) => {
157
+ if (!nodesById || selectedIds.size === 0) return;
158
+
159
+ const selectedSet = new Set(selectedIds);
160
+ const seenIds = new Set();
161
+ const cutNodes = [];
162
+
163
+ [...selectedIds].forEach((id) => {
164
+ if (!nodesById[id] || seenIds.has(id)) return;
165
+ seenIds.add(id);
166
+ cutNodes.push(buildClipboardNode(id, nodesById[id], selectedSet));
167
+ });
168
+
169
+ setClipboard(cutNodes);
170
+ onCut?.([...selectedIds]);
171
+ setSelectedIds(new Set());
172
+ },
173
+ [selectedIds]
174
+ );
175
+
176
+ const pasteNodes = useCallback(
177
+ (callback, x = 0, y = 0) => {
178
+ if (!clipboard.length || !callback || isPastingRef.current) return;
179
+
180
+ isPastingRef.current = true;
181
+ const nodesToPaste = [...clipboard];
182
+
183
+ const pasteData = buildPasteStructure(nodesToPaste, { x, y });
184
+ if (!pasteData) {
185
+ isPastingRef.current = false;
186
+ return;
187
+ }
188
+
189
+ let result;
190
+ try {
191
+ result = callback(pasteData, { x, y });
192
+ } catch (error) {
193
+ isPastingRef.current = false;
194
+ throw error;
195
+ }
196
+
197
+ if (result && typeof result.then === "function") {
198
+ return result
199
+ .then(() => {
200
+ setClipboard([]);
201
+ })
202
+ .finally(() => {
203
+ isPastingRef.current = false;
204
+ });
205
+ }
206
+
207
+ setClipboard([]);
208
+ isPastingRef.current = false;
209
+ return result;
210
+ },
211
+ [clipboard]
212
+ );
213
+
214
+ const hasClipboard = clipboard.length > 0;
215
+
216
+ const contextValue = useMemo(
217
+ () => ({
218
+ selectedIds,
219
+ selectNode,
220
+ deselectNode,
221
+ toggleSelection,
222
+ clearSelection,
223
+ selectMultiple,
224
+ addToSelection,
225
+ isSelected,
226
+ registerNodeHandlers,
227
+ moveSelectedNodes,
228
+ cutSelectedNodes,
229
+ pasteNodes,
230
+ hasClipboard,
231
+ }),
232
+ [
233
+ selectedIds,
234
+ selectNode,
235
+ deselectNode,
236
+ toggleSelection,
237
+ clearSelection,
238
+ selectMultiple,
239
+ addToSelection,
240
+ isSelected,
241
+ registerNodeHandlers,
242
+ moveSelectedNodes,
243
+ cutSelectedNodes,
244
+ pasteNodes,
245
+ hasClipboard,
246
+ ]
247
+ );
248
+
249
+ return (
250
+ <SelectionContext.Provider value={contextValue}>
251
+ {children}
252
+ </SelectionContext.Provider>
253
+ );
254
+ };
255
+
256
+ export const useSelection = () =>
257
+ useContext(SelectionContext) || DEFAULT_CONTEXT;
258
+
259
+ export default SelectionContext;
@@ -0,0 +1,31 @@
1
+ import { Box } from "@mui/material";
2
+ import { hexToRgba } from "../utils/flowUtils";
3
+
4
+ const SelectionOverlay = ({ box, selectionColor = "#64748b" }) => {
5
+ if (!box) return null;
6
+
7
+ const { startX, startY, currentX, currentY } = box;
8
+ const left = Math.min(startX, currentX);
9
+ const top = Math.min(startY, currentY);
10
+ const width = Math.abs(currentX - startX);
11
+ const height = Math.abs(currentY - startY);
12
+
13
+ return (
14
+ <Box
15
+ sx={{
16
+ position: "fixed",
17
+ left,
18
+ top,
19
+ width,
20
+ height,
21
+ border: `2px solid ${selectionColor}`,
22
+ backgroundColor: hexToRgba(selectionColor, 0.1),
23
+ pointerEvents: "none",
24
+ zIndex: 9999,
25
+ borderRadius: "4px",
26
+ }}
27
+ />
28
+ );
29
+ };
30
+
31
+ export default SelectionOverlay;
@@ -9,37 +9,80 @@ export const getBaseStyleForVariant = (v) => {
9
9
  switch (v) {
10
10
  case "card":
11
11
  return {
12
- lineColor: "#BDBDBD",
12
+ lineColor: "#b1b1b7",
13
13
  lineWidth: "2px",
14
14
  lineStyle: "solid",
15
15
  gap: 56,
16
- shape: 8,
16
+ shape: 10,
17
17
  bg: "background.paper",
18
- hoverBg: "grey.100",
19
- borderColor: "#9E9E9E",
18
+ hoverBg: "grey.50",
19
+ borderColor: "#e2e8f0",
20
+ selectionColor: "#64748b",
21
+ showDots: false,
22
+ showArrow: true,
23
+ arrowSize: 6,
24
+ animated: false,
25
+ animationSpeed: 1,
26
+ gradient: null,
27
+ curvature: 0.5,
20
28
  };
21
29
  case "pill":
22
30
  return {
23
- lineColor: "#9C27B0",
31
+ lineColor: "#8b5cf6",
24
32
  lineWidth: "2px",
25
- lineStyle: "dashed",
33
+ lineStyle: "solid",
26
34
  gap: 48,
27
35
  shape: 9999,
28
- bg: "rgba(156, 39, 176, 0.08)",
29
- hoverBg: "rgba(156, 39, 176, 0.16)",
30
- borderColor: "#9C27B0",
36
+ bg: "rgba(139, 92, 246, 0.08)",
37
+ hoverBg: "rgba(139, 92, 246, 0.16)",
38
+ borderColor: "#8b5cf6",
39
+ selectionColor: "#8b5cf6",
40
+ showDots: false,
41
+ showArrow: true,
42
+ arrowSize: 6,
43
+ animated: false,
44
+ animationSpeed: 1,
45
+ gradient: null,
46
+ curvature: 0.4,
47
+ };
48
+ case "n8n":
49
+ return {
50
+ lineColor: "#b1b1b7",
51
+ lineWidth: "2px",
52
+ lineStyle: "solid",
53
+ gap: 60,
54
+ shape: 8,
55
+ bg: "#ffffff",
56
+ hoverBg: "#f8fafc",
57
+ borderColor: "#e2e8f0",
58
+ selectionColor: "#ff6d5a",
59
+ showDots: false,
60
+ showArrow: true,
61
+ arrowSize: 6,
62
+ animated: false,
63
+ animationSpeed: 1,
64
+ gradient: null,
65
+ curvature: 0.5,
31
66
  };
32
67
  case "simple":
33
68
  default:
34
69
  return {
35
- lineColor: "#E0E0E0",
36
- lineWidth: "1.5px",
70
+ lineColor: "#b1b1b7",
71
+ lineWidth: "2px",
37
72
  lineStyle: "solid",
38
- gap: 40,
39
- shape: 4,
40
- bg: "background.default",
41
- hoverBg: "grey.100",
42
- borderColor: "#E0E0E0",
73
+ gap: 50,
74
+ shape: 8,
75
+ bg: "background.paper",
76
+ hoverBg: "grey.50",
77
+ borderColor: "#e2e8f0",
78
+ selectionColor: "#64748b",
79
+ showDots: false,
80
+ showArrow: true,
81
+ arrowSize: 6,
82
+ animated: false,
83
+ animationSpeed: 1,
84
+ gradient: null,
85
+ curvature: 0.5,
43
86
  };
44
87
  }
45
88
  };
@@ -74,7 +117,6 @@ export const applySemanticTokens = (styleObj, base) => {
74
117
  s.borderColor = s.border;
75
118
  }
76
119
 
77
- // size: small | medium | large
78
120
  const sizeMap = {
79
121
  small: { cardWidth: 140, gap: 20 },
80
122
  medium: { cardWidth: 180, gap: 30 },
@@ -86,7 +128,6 @@ export const applySemanticTokens = (styleObj, base) => {
86
128
  if (s.gap == null) s.gap = gap;
87
129
  }
88
130
 
89
- // shape: "square" | "vertical" (affects dimensions)
90
131
  if (s.shape === "square") {
91
132
  const defaultSize = 140;
92
133
  if (s.cardWidth != null && s.minHeight == null) s.minHeight = s.cardWidth;
@@ -104,7 +145,6 @@ export const applySemanticTokens = (styleObj, base) => {
104
145
  if (s.minHeight == null) s.minHeight = Math.max(s.cardWidth * 1.3, 110);
105
146
  }
106
147
 
107
- // shadow: "none" | "soft" | "heavy"
108
148
  const shadowVariantMap = { none: 0, soft: 2, heavy: 6 };
109
149
  if (s.shadow && shadowVariantMap[s.shadow] != null && s.shadowLevel == null) {
110
150
  s.shadowLevel = shadowVariantMap[s.shadow];