@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
@@ -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.11",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -0,0 +1,247 @@
1
+ import React, { useId, useLayoutEffect, useMemo, useState } from "react";
2
+
3
+ const DynamicConnector = ({
4
+ containerEl,
5
+ parentEl,
6
+ childEls,
7
+ stroke = "#b1b1b7",
8
+ strokeWidth = 2,
9
+ lineStyle = "solid",
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,
21
+ }) => {
22
+ const uniqueId = useId();
23
+ const [dims, setDims] = useState(null);
24
+ const [points, setPoints] = useState({
25
+ parent: null,
26
+ children: [],
27
+ });
28
+
29
+ const isHorizontal = orientation === "horizontal";
30
+
31
+ useLayoutEffect(() => {
32
+ if (!containerEl || !parentEl || !childEls?.length) return;
33
+
34
+ const update = () => {
35
+ const cRect = containerEl.getBoundingClientRect();
36
+ const pRect = parentEl.getBoundingClientRect();
37
+
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,
45
+ };
46
+
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
+ };
60
+
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 });
72
+ setDims({ w: cRect.width, h: cRect.height });
73
+ };
74
+
75
+ update();
76
+
77
+ const ro = new ResizeObserver(update);
78
+ ro.observe(containerEl);
79
+ ro.observe(parentEl);
80
+ childEls.forEach((el) => el && ro.observe(el));
81
+
82
+ return () => ro.disconnect();
83
+ }, [containerEl, parentEl, childEls, tick, isHorizontal]);
84
+
85
+ const ids = useMemo(
86
+ () => ({
87
+ gradient: `gradient-${uniqueId}`,
88
+ arrow: `arrow-${uniqueId}`,
89
+ }),
90
+ [uniqueId]
91
+ );
92
+
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
+ };
110
+
111
+ const getDashArray = () => {
112
+ if (lineStyle === "dashed") return `${strokeWidth * 4},${strokeWidth * 3}`;
113
+ if (lineStyle === "dotted") return `${strokeWidth},${strokeWidth * 2}`;
114
+ return undefined;
115
+ };
116
+
117
+ const animationStyle = animated
118
+ ? `
119
+ @keyframes flowAnimation {
120
+ from { stroke-dashoffset: 24; }
121
+ to { stroke-dashoffset: 0; }
122
+ }
123
+ `
124
+ : "";
125
+
126
+ if (!dims || !points.parent || !points.children.length) return null;
127
+
128
+ const effectiveDotColor = dotColor || stroke;
129
+ const dashArray = getDashArray();
130
+
131
+ return (
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"
149
+ x1={points.parent.x}
150
+ y1={points.parent.y}
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
+ }}
221
+ />
222
+
223
+ {points.children.map((child, i) => {
224
+ const dotFill = gradient ? gradient.to : effectiveDotColor;
225
+
226
+ return (
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
+ }}
238
+ />
239
+ );
240
+ })}
241
+ </>
242
+ )}
243
+ </svg>
244
+ );
245
+ };
246
+
247
+ export default DynamicConnector;
@@ -0,0 +1,79 @@
1
+ import FlowNode from "./FlowNode";
2
+ import { useGraphOperations } from "../hooks/useGraphOperations";
3
+
4
+ import React, { useMemo, useState } from "react";
5
+ import { assertLinkedGraph, buildTreeFromLinked } from "../utils/flowUtils";
6
+
7
+ export const Flow = ({
8
+ data,
9
+ variant = "simple",
10
+ style,
11
+ plugin,
12
+ editable = false,
13
+ onChange,
14
+ }) => {
15
+ const [floatingNodes, setFloatingNodes] = useState([]);
16
+
17
+ const { nodesById, roots } = useMemo(() => assertLinkedGraph(data), [data]);
18
+
19
+ const { handleCut, handlePaste, handleConnect } = useGraphOperations({
20
+ nodesById,
21
+ roots,
22
+ onChange,
23
+ floatingNodes,
24
+ setFloatingNodes,
25
+ editable,
26
+ });
27
+
28
+ const allNodesById = useMemo(() => {
29
+ if (!floatingNodes.length) return nodesById;
30
+
31
+ const merged = { ...nodesById };
32
+
33
+ for (const structure of floatingNodes) {
34
+ if (structure?.nodes) {
35
+ Object.assign(merged, structure.nodes);
36
+ }
37
+ }
38
+
39
+ return merged;
40
+ }, [nodesById, floatingNodes]);
41
+
42
+ const treeData = useMemo(() => {
43
+ if (!roots?.length) return null;
44
+
45
+ if (roots.length === 1) {
46
+ return (
47
+ buildTreeFromLinked(roots[0], nodesById) || {
48
+ id: roots[0],
49
+ children: [],
50
+ }
51
+ );
52
+ }
53
+
54
+ const children = roots
55
+ .map((r) => buildTreeFromLinked(r, nodesById))
56
+ .filter(Boolean);
57
+
58
+ return children.length > 0
59
+ ? { id: "__root__", label: "Start", children }
60
+ : null;
61
+ }, [nodesById, roots]);
62
+
63
+ return (
64
+ <FlowNode
65
+ node={treeData}
66
+ variant={variant}
67
+ style={style}
68
+ plugin={plugin}
69
+ isRoot={true}
70
+ nodesById={allNodesById}
71
+ onPaste={editable ? handlePaste : undefined}
72
+ onCut={editable ? handleCut : undefined}
73
+ onConnect={editable ? handleConnect : undefined}
74
+ floatingNodes={floatingNodes}
75
+ />
76
+ );
77
+ };
78
+
79
+ export default Flow;
@@ -0,0 +1,68 @@
1
+ import FlowNodeView from "../nodes/FlowNodeView";
2
+ import FlowViewport from "./FlowViewport";
3
+ import React from "react";
4
+ import { SelectionProvider } from "../selection/SelectionContext";
5
+ import { getBaseStyleForVariant } from "../styles";
6
+
7
+ const FlowNode = ({
8
+ isRoot = false,
9
+ onAddNode,
10
+ variant,
11
+ nodesById,
12
+ onPaste,
13
+ onCut,
14
+ onConnect,
15
+ floatingNodes,
16
+ style,
17
+ plugin,
18
+ node,
19
+ ...props
20
+ }) => {
21
+ if (!isRoot) {
22
+ if (!node) return null;
23
+ return (
24
+ <FlowNodeView
25
+ node={node}
26
+ onAddNode={onAddNode}
27
+ variant={variant}
28
+ style={style}
29
+ plugin={plugin}
30
+ onConnect={onConnect}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ const baseStyle = getBaseStyleForVariant(variant);
37
+ const selectionColor = baseStyle.selectionColor ?? "#64748b";
38
+
39
+ return (
40
+ <SelectionProvider>
41
+ <FlowViewport
42
+ selectionColor={selectionColor}
43
+ nodesById={nodesById}
44
+ onPaste={onPaste}
45
+ onCut={onCut}
46
+ onConnect={onConnect}
47
+ floatingNodes={floatingNodes}
48
+ variant={variant}
49
+ style={style}
50
+ plugin={plugin}
51
+ >
52
+ {node && (
53
+ <FlowNodeView
54
+ node={node}
55
+ onAddNode={onAddNode}
56
+ variant={variant}
57
+ style={style}
58
+ plugin={plugin}
59
+ onConnect={onConnect}
60
+ {...props}
61
+ />
62
+ )}
63
+ </FlowViewport>
64
+ </SelectionProvider>
65
+ );
66
+ };
67
+
68
+ export default FlowNode;