@canmingir/link 1.2.8 → 1.2.10

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.
@@ -1,9 +1,66 @@
1
- name: publish
1
+ name: Publish NPM Package
2
+
2
3
  on:
3
- push:
4
- tags:
5
- - v[0-9]+.[0-9]+.[0-9]+
4
+ workflow_dispatch:
5
+ inputs:
6
+ version_type:
7
+ description: 'Version bump type'
8
+ required: true
9
+ default: 'patch'
10
+ type: choice
11
+ options:
12
+ - patch
13
+ - minor
14
+ - major
15
+ release:
16
+ types: [created]
17
+
6
18
  jobs:
7
- deploy:
8
- uses: NucleoidAI/actions/.github/workflows/publish.yml@main
9
- secrets: inherit
19
+ publish:
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: write
23
+ id-token: write
24
+
25
+ steps:
26
+ - name: Checkout code
27
+ uses: actions/checkout@v4
28
+ with:
29
+ token: ${{ secrets.PAT_TOKEN }}
30
+
31
+ - name: Setup Node.js
32
+ uses: actions/setup-node@v4
33
+ with:
34
+ node-version: '20'
35
+ registry-url: 'https://registry.npmjs.org'
36
+
37
+ - name: Configure Git
38
+ run: |
39
+ git config user.name "github-actions[bot]"
40
+ git config user.email "github-actions[bot]@users.noreply.github.com"
41
+
42
+ - name: Install dependencies
43
+ run: npm ci
44
+
45
+ - name: Bump version
46
+ id: version_bump
47
+ run: |
48
+ npm version ${{ github.event.inputs.version_type }} -m "Bump version to %s [skip ci]"
49
+ echo "NEW_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
50
+ git push --follow-tags
51
+
52
+ - name: Publish to NPM
53
+ id: npm_publish
54
+ run: npm publish --access public
55
+ env:
56
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
57
+
58
+ - name: Notify Slack
59
+ uses: slackapi/slack-github-action@v2.0.0
60
+ with:
61
+ webhook: https://hooks.slack.com/services/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
62
+ webhook-type: incoming-webhook
63
+ payload: |
64
+ {
65
+ "text": "Successfully published @canmingir/link@${{ steps.version_bump.outputs.NEW_VERSION }}"
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canmingir/link",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -1,10 +1,44 @@
1
1
  import { Box } from "@mui/material";
2
+ import { useSelection } from "./SelectionContext";
2
3
 
3
- import React, { useRef, useState } from "react";
4
+ import React, { useEffect, useRef, useState } from "react";
4
5
 
5
- const DraggableNode = ({ children, registerRef, onDrag }) => {
6
+ const DraggableNode = ({
7
+ children,
8
+ registerRef,
9
+ onDrag,
10
+ nodeId,
11
+ selectionColor = "#64748b",
12
+ }) => {
6
13
  const [offset, setOffset] = useState({ x: 0, y: 0 });
7
14
  const localRef = useRef(null);
15
+ const lastDeltaRef = useRef({ x: 0, y: 0 });
16
+
17
+ const {
18
+ isSelected,
19
+ selectNode,
20
+ toggleSelection,
21
+ clearSelection,
22
+ registerNodeHandlers,
23
+ moveSelectedNodes,
24
+ selectedIds,
25
+ } = useSelection();
26
+
27
+ const selected = isSelected(nodeId);
28
+ const onDragRef = useRef(onDrag);
29
+
30
+ useEffect(() => {
31
+ onDragRef.current = onDrag;
32
+ }, [onDrag]);
33
+
34
+ useEffect(() => {
35
+ if (nodeId) {
36
+ return registerNodeHandlers(nodeId, {
37
+ setOffset,
38
+ onDrag: () => onDragRef.current?.(),
39
+ });
40
+ }
41
+ }, [nodeId, registerNodeHandlers]);
8
42
 
9
43
  const setRef = (el) => {
10
44
  localRef.current = el;
@@ -15,17 +49,37 @@ const DraggableNode = ({ children, registerRef, onDrag }) => {
15
49
  if (e.button !== 0) return;
16
50
  e.stopPropagation();
17
51
 
52
+ if (e.shiftKey || e.ctrlKey || e.metaKey) {
53
+ toggleSelection(nodeId);
54
+ return;
55
+ }
56
+ if (!selected) {
57
+ clearSelection();
58
+ selectNode(nodeId);
59
+ }
60
+
18
61
  const startX = e.clientX;
19
62
  const startY = e.clientY;
20
63
  const startOffset = { ...offset };
64
+ lastDeltaRef.current = { x: 0, y: 0 };
21
65
 
22
66
  const onMove = (ev) => {
23
67
  const dx = ev.clientX - startX;
24
68
  const dy = ev.clientY - startY;
69
+
70
+ const deltaDx = dx - lastDeltaRef.current.x;
71
+ const deltaDy = dy - lastDeltaRef.current.y;
72
+ lastDeltaRef.current = { x: dx, y: dy };
73
+
25
74
  setOffset({
26
75
  x: startOffset.x + dx,
27
76
  y: startOffset.y + dy,
28
77
  });
78
+
79
+ if (selectedIds.size > 1) {
80
+ moveSelectedNodes(deltaDx, deltaDy, nodeId);
81
+ }
82
+
29
83
  if (onDrag) onDrag();
30
84
  };
31
85
 
@@ -41,6 +95,7 @@ const DraggableNode = ({ children, registerRef, onDrag }) => {
41
95
  return (
42
96
  <Box
43
97
  ref={setRef}
98
+ data-node-id={nodeId}
44
99
  onMouseDown={handleMouseDown}
45
100
  sx={{
46
101
  display: "inline-flex",
@@ -52,6 +107,17 @@ const DraggableNode = ({ children, registerRef, onDrag }) => {
52
107
  "&:active": {
53
108
  cursor: "grabbing",
54
109
  },
110
+ ...(selected && {
111
+ "&::after": {
112
+ content: '""',
113
+ position: "absolute",
114
+ inset: -6,
115
+ border: `2px solid ${selectionColor}`,
116
+ borderRadius: "12px",
117
+ pointerEvents: "none",
118
+ boxShadow: `0 0 8px ${selectionColor}66`,
119
+ },
120
+ }),
55
121
  }}
56
122
  >
57
123
  {children}
@@ -1,22 +1,33 @@
1
- import React, { useLayoutEffect, useState } from "react";
1
+ import React, { useId, useLayoutEffect, useMemo, useState } from "react";
2
2
 
3
3
  const DynamicConnector = ({
4
4
  containerEl,
5
5
  parentEl,
6
6
  childEls,
7
- stroke,
8
- strokeWidth,
9
- lineStyle,
10
- connectorType = "default",
7
+ stroke = "#b1b1b7",
8
+ strokeWidth = 2,
9
+ lineStyle = "solid",
11
10
  tick = 0,
11
+ orientation = "vertical",
12
+ showDots = false,
13
+ dotRadius = 4,
14
+ dotColor,
15
+ showArrow = true,
16
+ arrowSize = 6,
17
+ animated = false,
18
+ animationSpeed = 1,
19
+ gradient = null,
20
+ curvature = 0.5,
12
21
  }) => {
22
+ const uniqueId = useId();
13
23
  const [dims, setDims] = useState(null);
14
24
  const [points, setPoints] = useState({
15
25
  parent: null,
16
26
  children: [],
17
- yMid: null,
18
27
  });
19
28
 
29
+ const isHorizontal = orientation === "horizontal";
30
+
20
31
  useLayoutEffect(() => {
21
32
  if (!containerEl || !parentEl || !childEls?.length) return;
22
33
 
@@ -24,25 +35,40 @@ const DynamicConnector = ({
24
35
  const cRect = containerEl.getBoundingClientRect();
25
36
  const pRect = parentEl.getBoundingClientRect();
26
37
 
27
- const parent = {
28
- x: pRect.left + pRect.width / 2 - cRect.left,
29
- y: pRect.bottom - cRect.top,
30
- };
31
-
32
- const children = childEls.map((el) => {
33
- if (!el) return { x: parent.x, y: parent.y };
34
- const r = el.getBoundingClientRect();
35
- return {
36
- x: r.left + r.width / 2 - cRect.left,
37
- y: r.top - cRect.top,
38
+ let parentPoint;
39
+ let childPoints = [];
40
+
41
+ if (isHorizontal) {
42
+ parentPoint = {
43
+ x: pRect.right - cRect.left,
44
+ y: pRect.top + pRect.height / 2 - cRect.top,
38
45
  };
39
- });
40
46
 
41
- const firstTop = Math.min(...children.map((c) => c.y));
42
- const yMid =
43
- parent.y + Math.max(12, Math.min(24, (firstTop - parent.y) * 0.4));
47
+ childPoints = childEls.map((el) => {
48
+ if (!el) return { x: parentPoint.x + 100, y: parentPoint.y };
49
+ const r = el.getBoundingClientRect();
50
+ return {
51
+ x: r.left - cRect.left,
52
+ y: r.top + r.height / 2 - cRect.top,
53
+ };
54
+ });
55
+ } else {
56
+ parentPoint = {
57
+ x: pRect.left + pRect.width / 2 - cRect.left,
58
+ y: pRect.bottom - cRect.top,
59
+ };
44
60
 
45
- setPoints({ parent, children, yMid });
61
+ childPoints = childEls.map((el) => {
62
+ if (!el) return { x: parentPoint.x, y: parentPoint.y + 100 };
63
+ const r = el.getBoundingClientRect();
64
+ return {
65
+ x: r.left + r.width / 2 - cRect.left,
66
+ y: r.top - cRect.top,
67
+ };
68
+ });
69
+ }
70
+
71
+ setPoints({ parent: parentPoint, children: childPoints });
46
72
  setDims({ w: cRect.width, h: cRect.height });
47
73
  };
48
74
 
@@ -54,119 +80,164 @@ const DynamicConnector = ({
54
80
  childEls.forEach((el) => el && ro.observe(el));
55
81
 
56
82
  return () => ro.disconnect();
57
- }, [containerEl, parentEl, childEls, tick]);
83
+ }, [containerEl, parentEl, childEls, tick, isHorizontal]);
84
+
85
+ const ids = useMemo(
86
+ () => ({
87
+ gradient: `gradient-${uniqueId}`,
88
+ arrow: `arrow-${uniqueId}`,
89
+ }),
90
+ [uniqueId]
91
+ );
58
92
 
59
- if (!dims || !points.parent || !points.children.length) return null;
93
+ const getPath = (from, to) => {
94
+ const dx = Math.abs(to.x - from.x);
95
+ const dy = Math.abs(to.y - from.y);
96
+ const distance = Math.sqrt(dx * dx + dy * dy);
97
+
98
+ const baseCurvature = Math.max(40, Math.min(distance * curvature, 150));
99
+
100
+ if (isHorizontal) {
101
+ const cp1x = from.x + baseCurvature;
102
+ const cp2x = to.x - baseCurvature;
103
+ return `M ${from.x} ${from.y} C ${cp1x} ${from.y}, ${cp2x} ${to.y}, ${to.x} ${to.y}`;
104
+ } else {
105
+ const cp1y = from.y + baseCurvature;
106
+ const cp2y = to.y - baseCurvature;
107
+ return `M ${from.x} ${from.y} C ${from.x} ${cp1y}, ${to.x} ${cp2y}, ${to.x} ${to.y}`;
108
+ }
109
+ };
60
110
 
61
- const dash =
62
- lineStyle === "dashed"
63
- ? `${strokeWidth * 3},${strokeWidth * 2}`
64
- : lineStyle === "dotted"
65
- ? `${strokeWidth},${strokeWidth * 1.5}`
66
- : undefined;
67
-
68
- const onlyOne = points.children.length === 1;
69
-
70
- const svgProps = {
71
- style: {
72
- position: "absolute",
73
- inset: 0,
74
- pointerEvents: "none",
75
- overflow: "visible",
76
- },
77
- width: "100%",
78
- height: "100%",
79
- viewBox: `0 0 ${dims.w} ${dims.h}`,
111
+ const getDashArray = () => {
112
+ if (lineStyle === "dashed") return `${strokeWidth * 4},${strokeWidth * 3}`;
113
+ if (lineStyle === "dotted") return `${strokeWidth},${strokeWidth * 2}`;
114
+ return undefined;
80
115
  };
81
116
 
82
- if (connectorType === "curved" || connectorType === "n8n") {
83
- const createN8nPath = (from, to) => {
84
- const v = Math.max(32, Math.abs(to.y - from.y) * 0.35);
85
- const h = Math.max(24, Math.abs(to.x - from.x) * 0.35);
86
-
87
- const c1 = {
88
- x: to.x > from.x ? from.x + h : from.x - h,
89
- y: from.y + v,
90
- };
91
- const c2 = {
92
- x: to.x > from.x ? to.x - h : to.x + h,
93
- y: to.y - v,
94
- };
95
-
96
- return `M ${from.x} ${from.y} C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${to.x} ${to.y}`;
97
- };
117
+ const animationStyle = animated
118
+ ? `
119
+ @keyframes flowAnimation {
120
+ from { stroke-dashoffset: 24; }
121
+ to { stroke-dashoffset: 0; }
122
+ }
123
+ `
124
+ : "";
98
125
 
99
- return (
100
- <svg {...svgProps}>
101
- {points.children.map((child, i) => (
102
- <path
103
- key={i}
104
- d={createN8nPath(points.parent, child)}
105
- fill="none"
106
- stroke={stroke}
107
- strokeWidth={strokeWidth}
108
- strokeDasharray={dash}
109
- strokeLinecap="round"
110
- strokeLinejoin="round"
111
- />
112
- ))}
113
- </svg>
114
- );
115
- }
126
+ if (!dims || !points.parent || !points.children.length) return null;
127
+
128
+ const effectiveDotColor = dotColor || stroke;
129
+ const dashArray = getDashArray();
116
130
 
117
131
  return (
118
- <svg {...svgProps}>
119
- {onlyOne ? (
120
- <line
121
- x1={points.parent.x}
122
- y1={points.parent.y}
123
- x2={points.children[0].x}
124
- y2={points.children[0].y}
125
- stroke={stroke}
126
- strokeWidth={strokeWidth}
127
- strokeDasharray={dash}
128
- />
129
- ) : (
130
- <>
131
- <line
132
+ <svg
133
+ style={{
134
+ position: "absolute",
135
+ inset: 0,
136
+ pointerEvents: "none",
137
+ overflow: "visible",
138
+ zIndex: 0,
139
+ }}
140
+ width="100%"
141
+ height="100%"
142
+ viewBox={`0 0 ${dims.w} ${dims.h}`}
143
+ >
144
+ <defs>
145
+ {gradient && (
146
+ <linearGradient
147
+ id={ids.gradient}
148
+ gradientUnits="userSpaceOnUse"
132
149
  x1={points.parent.x}
133
150
  y1={points.parent.y}
134
- x2={points.parent.x}
135
- y2={points.yMid}
136
- stroke={stroke}
137
- strokeWidth={strokeWidth}
138
- strokeDasharray={dash}
151
+ x2={points.children[0]?.x || points.parent.x}
152
+ y2={points.children[0]?.y || points.parent.y}
153
+ >
154
+ <stop offset="0%" stopColor={gradient.from} />
155
+ <stop offset="100%" stopColor={gradient.to} />
156
+ </linearGradient>
157
+ )}
158
+
159
+ {showArrow && (
160
+ <marker
161
+ id={ids.arrow}
162
+ viewBox="0 0 10 10"
163
+ refX="9"
164
+ refY="5"
165
+ markerWidth={arrowSize}
166
+ markerHeight={arrowSize}
167
+ orient="auto-start-reverse"
168
+ >
169
+ <path
170
+ d="M 0 0 L 10 5 L 0 10 z"
171
+ fill={gradient ? `url(#${ids.gradient})` : stroke}
172
+ />
173
+ </marker>
174
+ )}
175
+
176
+ {animated && <style>{animationStyle}</style>}
177
+ </defs>
178
+
179
+ {points.children.map((child, i) => {
180
+ const pathD = getPath(points.parent, child);
181
+ const pathStroke = gradient ? `url(#${ids.gradient})` : stroke;
182
+
183
+ return (
184
+ <g key={i}>
185
+ <path
186
+ d={pathD}
187
+ fill="none"
188
+ stroke={pathStroke}
189
+ strokeWidth={strokeWidth}
190
+ strokeDasharray={animated ? "8,4" : dashArray}
191
+ strokeLinecap="round"
192
+ strokeLinejoin="round"
193
+ markerEnd={showArrow ? `url(#${ids.arrow})` : undefined}
194
+ style={{
195
+ transition: "stroke 0.2s ease, stroke-width 0.2s ease",
196
+ ...(animated
197
+ ? {
198
+ animation: `flowAnimation ${
199
+ 0.5 / animationSpeed
200
+ }s linear infinite`,
201
+ }
202
+ : {}),
203
+ }}
204
+ />
205
+ </g>
206
+ );
207
+ })}
208
+
209
+ {showDots && (
210
+ <>
211
+ <circle
212
+ cx={points.parent.x}
213
+ cy={points.parent.y}
214
+ r={dotRadius}
215
+ fill={gradient ? gradient.from : effectiveDotColor}
216
+ stroke="#fff"
217
+ strokeWidth={1.5}
218
+ style={{
219
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.15))",
220
+ }}
139
221
  />
140
222
 
141
- {(() => {
142
- const xs = points.children.map((c) => c.x);
143
- const xMin = Math.min(...xs, points.parent.x);
144
- const xMax = Math.max(...xs, points.parent.x);
223
+ {points.children.map((child, i) => {
224
+ const dotFill = gradient ? gradient.to : effectiveDotColor;
225
+
145
226
  return (
146
- <line
147
- x1={xMin}
148
- y1={points.yMid}
149
- x2={xMax}
150
- y2={points.yMid}
151
- stroke={stroke}
152
- strokeWidth={strokeWidth}
153
- strokeDasharray={dash}
227
+ <circle
228
+ key={i}
229
+ cx={child.x}
230
+ cy={child.y}
231
+ r={dotRadius}
232
+ fill={dotFill}
233
+ stroke="#fff"
234
+ strokeWidth={1.5}
235
+ style={{
236
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.15))",
237
+ }}
154
238
  />
155
239
  );
156
- })()}
157
-
158
- {points.children.map((c, i) => (
159
- <line
160
- key={i}
161
- x1={c.x}
162
- y1={points.yMid}
163
- x2={c.x}
164
- y2={c.y}
165
- stroke={stroke}
166
- strokeWidth={strokeWidth}
167
- strokeDasharray={dash}
168
- />
169
- ))}
240
+ })}
170
241
  </>
171
242
  )}
172
243
  </svg>