@canmingir/link 1.2.10 → 1.2.12

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,40 +0,0 @@
1
- import FlowNode from "./FlowNode";
2
-
3
- import React, { useMemo } from "react";
4
- import { assertLinkedGraph, buildTreeFromLinked } from "./flowUtils";
5
-
6
- export const Flow = ({ data, variant = "simple", style, plugin }) => {
7
- const { nodesById, roots } = useMemo(() => assertLinkedGraph(data), [data]);
8
-
9
- const treeData = useMemo(() => {
10
- if (!roots?.length)
11
- return { id: "__empty__", label: "(empty)", children: [] };
12
-
13
- if (roots.length === 1) {
14
- return (
15
- buildTreeFromLinked(roots[0], nodesById) || {
16
- id: roots[0],
17
- children: [],
18
- }
19
- );
20
- }
21
-
22
- const children = roots
23
- .map((r) => buildTreeFromLinked(r, nodesById))
24
- .filter(Boolean);
25
-
26
- return { id: "__root__", label: "Root", children };
27
- }, [nodesById, roots]);
28
-
29
- return (
30
- <FlowNode
31
- node={treeData}
32
- variant={variant}
33
- style={style}
34
- plugin={plugin}
35
- isRoot={true}
36
- />
37
- );
38
- };
39
-
40
- export default Flow;
@@ -1,519 +0,0 @@
1
- import DraggableNode from "./DraggableNode";
2
- import DynamicConnector from "./DynamicConnector";
3
- import { getContentParts } from "./flowUtils";
4
-
5
- import { Box, Card, Typography } from "@mui/material";
6
- import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
7
- import { SelectionProvider, useSelection } from "./SelectionContext";
8
- import {
9
- applySemanticTokens,
10
- getBaseStyleForVariant,
11
- getDecisionNodeStyle,
12
- toPxNumber,
13
- } from "./styles";
14
-
15
- const NodeContent = ({
16
- node,
17
- type,
18
- variant,
19
- style,
20
- plugin,
21
- registerRef,
22
- onDrag,
23
- }) => {
24
- const baseStyle = getBaseStyleForVariant(variant);
25
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
26
-
27
- const variantTokens =
28
- variant === "decision" ? getDecisionNodeStyle(node.type) : {};
29
-
30
- let styleTokens = {};
31
- if (typeof style === "function") {
32
- styleTokens = style(node) || {};
33
- } else if (style && typeof style === "object") {
34
- styleTokens = style;
35
- }
36
-
37
- let _plugin = null;
38
- if (plugin) {
39
- if (typeof plugin === "function") {
40
- _plugin = plugin(type, node) || null;
41
- } else if (typeof plugin === "object") {
42
- _plugin = plugin;
43
- }
44
- }
45
-
46
- let pluginTokens = {};
47
- if (_plugin && typeof _plugin.style === "function") {
48
- pluginTokens =
49
- _plugin.style({
50
- node,
51
- style: styleTokens,
52
- }) || {};
53
- }
54
-
55
- const rawNodeStyle = {
56
- ...baseStyle,
57
- ...variantTokens,
58
- ...styleTokens,
59
- ...pluginTokens,
60
- };
61
-
62
- const nodeStyle = applySemanticTokens(rawNodeStyle, baseStyle);
63
-
64
- const {
65
- direction = "vertical",
66
- lineColor = baseStyle.lineColor,
67
- lineWidth = baseStyle.lineWidth,
68
- lineStyle = baseStyle.lineStyle,
69
- gap = baseStyle.gap,
70
- levelGap = baseStyle.levelGap ?? 2.5,
71
- nodeSx = {},
72
- borderWidth,
73
- borderColor = baseStyle.borderColor,
74
- cardWidth,
75
- shape,
76
- shadowLevel,
77
- minHeight,
78
- showDots = baseStyle.showDots ?? false,
79
- dotRadius = baseStyle.dotRadius ?? 4,
80
- dotColor = baseStyle.dotColor,
81
- showArrow = baseStyle.showArrow ?? true,
82
- arrowSize = baseStyle.arrowSize ?? 6,
83
- animated = baseStyle.animated ?? false,
84
- animationSpeed = baseStyle.animationSpeed ?? 1,
85
- gradient = baseStyle.gradient ?? null,
86
- curvature = baseStyle.curvature ?? 0.5,
87
- selectionColor = baseStyle.selectionColor ?? "#64748b",
88
- } = nodeStyle;
89
-
90
- const isHorizontal = direction === "horizontal";
91
-
92
- const strokeWidth = toPxNumber(lineWidth, 1.5);
93
- const dashStyle =
94
- lineStyle === "dashed" || lineStyle === "dotted" ? lineStyle : "solid";
95
-
96
- const containerRef = useRef(null);
97
- const parentRef = useRef(null);
98
- const childRefs = useRef({});
99
- const [childElList, setChildElList] = useState([]);
100
-
101
- const [connectorTick, setConnectorTick] = useState(0);
102
-
103
- const handleDrag = (newOffset) => {
104
- setConnectorTick((t) => t + 1);
105
- if (onDrag) onDrag(newOffset);
106
- };
107
-
108
- useLayoutEffect(() => {
109
- const els = (node.children || [])
110
- .map((c) => childRefs.current[c.id])
111
- .filter(Boolean);
112
- setChildElList(els);
113
- }, [node.children]);
114
-
115
- useEffect(() => {
116
- const t = setTimeout(() => {
117
- const els = (node.children || [])
118
- .map((c) => childRefs.current[c.id])
119
- .filter(Boolean);
120
- setChildElList(els);
121
- }, 0);
122
- return () => clearTimeout(t);
123
- }, [node.children]);
124
-
125
- const { title, subtitle, metaEntries } = getContentParts(node);
126
-
127
- const renderDefaultCard = () => {
128
- const effectiveWidth = cardWidth || 220;
129
- const effectiveBorderWidth = borderWidth || 1;
130
- const effectiveRadius =
131
- typeof shape === "number" ? shape : baseStyle.shape || 4;
132
- const effectiveShadow =
133
- typeof shadowLevel === "number"
134
- ? shadowLevel
135
- : variant === "card"
136
- ? 2
137
- : 1;
138
- const effectiveMinHeight = minHeight || 80;
139
-
140
- return (
141
- <Card
142
- sx={{
143
- p: 2,
144
- width: effectiveWidth,
145
- minHeight: effectiveMinHeight,
146
- display: "flex",
147
- flexDirection: "row",
148
- alignItems: "center",
149
- justifyContent: "center",
150
- gap: 1,
151
- position: "relative",
152
- borderRadius: effectiveRadius,
153
- bgcolor: nodeStyle.bg || "background.paper",
154
- border: `${effectiveBorderWidth}px solid ${
155
- borderColor || "transparent"
156
- }`,
157
- boxShadow: effectiveShadow,
158
- transition: "background-color 0.3s ease, box-shadow 0.3s ease",
159
- "&:hover": {
160
- bgcolor: nodeStyle.hoverBg || nodeStyle.bg || "grey.100",
161
- boxShadow: effectiveShadow + 1,
162
- cursor: "pointer",
163
- },
164
- ...nodeSx,
165
- }}
166
- >
167
- <Box sx={{ textAlign: "left", width: "100%" }}>
168
- <Typography
169
- variant="subtitle2"
170
- sx={{
171
- textAlign: "center",
172
- fontWeight: 600,
173
- fontSize: 13,
174
- mb: subtitle ? 0.5 : 0,
175
- }}
176
- >
177
- {title}
178
- </Typography>
179
-
180
- {subtitle && (
181
- <Typography
182
- variant="body2"
183
- color="text.secondary"
184
- sx={{
185
- textAlign: "center",
186
- fontSize: 11,
187
- mb: metaEntries.length ? 0.5 : 0,
188
- }}
189
- >
190
- {subtitle}
191
- </Typography>
192
- )}
193
-
194
- {metaEntries.length > 0 && (
195
- <Box sx={{ mt: 0.25 }}>
196
- {metaEntries.map(([key, value]) => (
197
- <Typography
198
- key={key}
199
- variant="caption"
200
- color="text.secondary"
201
- sx={{
202
- textAlign: "center",
203
- display: "block",
204
- fontSize: 10,
205
- }}
206
- >
207
- {key}: {String(value)}
208
- </Typography>
209
- ))}
210
- </Box>
211
- )}
212
- </Box>
213
- </Card>
214
- );
215
- };
216
-
217
- const renderContent = () => {
218
- if (_plugin && typeof _plugin.node === "function") {
219
- return _plugin.node({
220
- node,
221
- title,
222
- subtitle,
223
- metaEntries,
224
- nodeStyle,
225
- baseStyle,
226
- });
227
- }
228
- return renderDefaultCard();
229
- };
230
-
231
- return (
232
- <Box
233
- ref={containerRef}
234
- sx={{
235
- display: "inline-flex",
236
- flexDirection: isHorizontal ? "row" : "column",
237
- alignItems: "center",
238
- position: "relative",
239
- }}
240
- >
241
- <DraggableNode
242
- registerRef={(el) => {
243
- parentRef.current = el;
244
- if (registerRef) registerRef(el);
245
- }}
246
- onDrag={handleDrag}
247
- nodeId={node.id}
248
- selectionColor={selectionColor}
249
- >
250
- {renderContent()}
251
- </DraggableNode>
252
-
253
- {hasChildren && (
254
- <>
255
- <DynamicConnector
256
- containerEl={containerRef.current}
257
- parentEl={parentRef.current}
258
- childEls={childElList}
259
- stroke={lineColor}
260
- strokeWidth={strokeWidth}
261
- lineStyle={dashStyle}
262
- tick={connectorTick}
263
- orientation={direction}
264
- showDots={showDots}
265
- dotRadius={dotRadius}
266
- dotColor={dotColor}
267
- showArrow={showArrow}
268
- arrowSize={arrowSize}
269
- animated={animated}
270
- animationSpeed={animationSpeed}
271
- gradient={gradient}
272
- curvature={curvature}
273
- />
274
-
275
- <Box
276
- sx={{
277
- display: "flex",
278
- flexDirection: isHorizontal ? "column" : "row",
279
- ...(isHorizontal
280
- ? {
281
- marginLeft: levelGap,
282
- rowGap: gap,
283
- }
284
- : {
285
- marginTop: levelGap,
286
- columnGap: gap,
287
- }),
288
- position: "relative",
289
- alignItems: "flex-start",
290
- justifyContent: "center",
291
- }}
292
- >
293
- {node.children.map((child) => (
294
- <FlowNode
295
- key={child.id}
296
- node={child}
297
- type={type}
298
- variant={variant}
299
- style={style}
300
- plugin={plugin}
301
- registerRef={(el) => (childRefs.current[child.id] = el)}
302
- onDrag={() => setConnectorTick((t) => t + 1)}
303
- isRoot={false}
304
- />
305
- ))}
306
- </Box>
307
- </>
308
- )}
309
- </Box>
310
- );
311
- };
312
-
313
- const hexToRgba = (hex, alpha) => {
314
- const r = parseInt(hex.slice(1, 3), 16);
315
- const g = parseInt(hex.slice(3, 5), 16);
316
- const b = parseInt(hex.slice(5, 7), 16);
317
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
318
- };
319
-
320
- const SelectionBox = ({ box, selectionColor = "#64748b" }) => {
321
- if (!box) return null;
322
-
323
- const { startX, startY, currentX, currentY } = box;
324
- const left = Math.min(startX, currentX);
325
- const top = Math.min(startY, currentY);
326
- const width = Math.abs(currentX - startX);
327
- const height = Math.abs(currentY - startY);
328
-
329
- return (
330
- <Box
331
- sx={{
332
- position: "fixed",
333
- left,
334
- top,
335
- width,
336
- height,
337
- border: `2px solid ${selectionColor}`,
338
- backgroundColor: hexToRgba(selectionColor, 0.1),
339
- pointerEvents: "none",
340
- zIndex: 9999,
341
- borderRadius: "4px",
342
- }}
343
- />
344
- );
345
- };
346
-
347
- const FlowCanvas = ({ children, selectionColor = "#64748b" }) => {
348
- const [offset, setOffset] = useState({ x: 0, y: 0 });
349
- const [isDragging, setIsDragging] = useState(false);
350
- const [zoom, setZoom] = useState(1);
351
- const [selectionBox, setSelectionBox] = useState(null);
352
- const containerRef = useRef(null);
353
- const selectionBoxRef = useRef(null);
354
-
355
- const { clearSelection, selectMultiple, addToSelection } = useSelection();
356
-
357
- const clampZoom = (z) => Math.min(2.5, Math.max(0.25, z));
358
-
359
- useEffect(() => {
360
- const onWheel = (e) => {
361
- const wantsZoom = e.ctrlKey || e.metaKey;
362
- if (!wantsZoom) return;
363
- e.preventDefault();
364
- const direction = e.deltaY > 0 ? -1 : 1;
365
- const factor = direction > 0 ? 1.1 : 1 / 1.1;
366
- setZoom((z) => clampZoom(z * factor));
367
- };
368
- window.addEventListener("wheel", onWheel, { passive: false });
369
- return () => window.removeEventListener("wheel", onWheel);
370
- }, []);
371
-
372
- const handleCanvasMouseDown = (e) => {
373
- if (e.target?.closest?.(".MuiCard-root") || e.target?.closest?.("button"))
374
- return;
375
-
376
- if (e.button !== 0) return;
377
-
378
- const startX = e.clientX;
379
- const startY = e.clientY;
380
-
381
- if (e.shiftKey || e.ctrlKey || e.metaKey) {
382
- setSelectionBox({ startX, startY, currentX: startX, currentY: startY });
383
- selectionBoxRef.current = {
384
- startX,
385
- startY,
386
- currentX: startX,
387
- currentY: startY,
388
- };
389
-
390
- const onMove = (ev) => {
391
- const newBox = {
392
- startX,
393
- startY,
394
- currentX: ev.clientX,
395
- currentY: ev.clientY,
396
- };
397
- setSelectionBox(newBox);
398
- selectionBoxRef.current = newBox;
399
- };
400
-
401
- const onUp = () => {
402
- if (containerRef.current && selectionBoxRef.current) {
403
- const box = selectionBoxRef.current;
404
- const nodes = containerRef.current.querySelectorAll("[data-node-id]");
405
- const selectedNodeIds = [];
406
-
407
- const boxLeft = Math.min(box.startX, box.currentX);
408
- const boxRight = Math.max(box.startX, box.currentX);
409
- const boxTop = Math.min(box.startY, box.currentY);
410
- const boxBottom = Math.max(box.startY, box.currentY);
411
-
412
- nodes.forEach((node) => {
413
- const rect = node.getBoundingClientRect();
414
-
415
- if (
416
- rect.left < boxRight &&
417
- rect.right > boxLeft &&
418
- rect.top < boxBottom &&
419
- rect.bottom > boxTop
420
- ) {
421
- const nodeId = node.getAttribute("data-node-id");
422
- if (nodeId) selectedNodeIds.push(nodeId);
423
- }
424
- });
425
-
426
- if (selectedNodeIds.length > 0) {
427
- if (e.shiftKey) {
428
- addToSelection(selectedNodeIds);
429
- } else {
430
- selectMultiple(selectedNodeIds);
431
- }
432
- }
433
- }
434
-
435
- setSelectionBox(null);
436
- selectionBoxRef.current = null;
437
- window.removeEventListener("mousemove", onMove);
438
- window.removeEventListener("mouseup", onUp);
439
- };
440
-
441
- window.addEventListener("mousemove", onMove);
442
- window.addEventListener("mouseup", onUp);
443
- return;
444
- }
445
-
446
- clearSelection();
447
-
448
- setIsDragging(true);
449
- const startOffset = { ...offset };
450
-
451
- const onMove = (ev) => {
452
- setOffset({
453
- x: startOffset.x + (ev.clientX - startX),
454
- y: startOffset.y + (ev.clientY - startY),
455
- });
456
- };
457
-
458
- const onUp = () => {
459
- setIsDragging(false);
460
- window.removeEventListener("mousemove", onMove);
461
- window.removeEventListener("mouseup", onUp);
462
- };
463
-
464
- window.addEventListener("mousemove", onMove);
465
- window.addEventListener("mouseup", onUp);
466
- };
467
-
468
- return (
469
- <Box
470
- ref={containerRef}
471
- onMouseDown={handleCanvasMouseDown}
472
- sx={{
473
- width: "100vw",
474
- height: "100vh",
475
- overflow: "hidden",
476
- bgcolor: "none",
477
- cursor: isDragging ? "grabbing" : "default",
478
- userSelect: "none",
479
- position: "relative",
480
- }}
481
- >
482
- <SelectionBox box={selectionBox} selectionColor={selectionColor} />
483
- <Box
484
- sx={{
485
- transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
486
- transformOrigin: "center center",
487
- width: "100%",
488
- height: "100%",
489
- display: "flex",
490
- alignItems: "center",
491
- justifyContent: "center",
492
- transition: isDragging ? "none" : "transform 0.1s ease-out",
493
- pointerEvents: "auto",
494
- }}
495
- >
496
- {children}
497
- </Box>
498
- </Box>
499
- );
500
- };
501
-
502
- const FlowNode = ({ isRoot = false, onAddNode, variant, ...props }) => {
503
- if (!isRoot) {
504
- return <NodeContent onAddNode={onAddNode} variant={variant} {...props} />;
505
- }
506
-
507
- const baseStyle = getBaseStyleForVariant(variant);
508
- const selectionColor = baseStyle.selectionColor ?? "#64748b";
509
-
510
- return (
511
- <SelectionProvider>
512
- <FlowCanvas selectionColor={selectionColor}>
513
- <NodeContent onAddNode={onAddNode} variant={variant} {...props} />
514
- </FlowCanvas>
515
- </SelectionProvider>
516
- );
517
- };
518
-
519
- export default FlowNode;
@@ -1,123 +0,0 @@
1
- import React, {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useRef,
6
- useState,
7
- } from "react";
8
-
9
- const SelectionContext = createContext(null);
10
-
11
- export const SelectionProvider = ({ children }) => {
12
- const [selectedIds, setSelectedIds] = useState(new Set());
13
- const nodeHandlersRef = useRef(new Map());
14
-
15
- const selectNode = useCallback((id, addToSelection = false) => {
16
- setSelectedIds((prev) => {
17
- const next = new Set(addToSelection ? prev : []);
18
- next.add(id);
19
- return next;
20
- });
21
- }, []);
22
-
23
- const deselectNode = useCallback((id) => {
24
- setSelectedIds((prev) => {
25
- const next = new Set(prev);
26
- next.delete(id);
27
- return next;
28
- });
29
- }, []);
30
-
31
- const toggleSelection = useCallback((id) => {
32
- setSelectedIds((prev) => {
33
- const next = new Set(prev);
34
- if (next.has(id)) {
35
- next.delete(id);
36
- } else {
37
- next.add(id);
38
- }
39
- return next;
40
- });
41
- }, []);
42
-
43
- const clearSelection = useCallback(() => {
44
- setSelectedIds(new Set());
45
- }, []);
46
-
47
- const selectMultiple = useCallback((ids) => {
48
- setSelectedIds(new Set(ids));
49
- }, []);
50
-
51
- const addToSelection = useCallback((ids) => {
52
- setSelectedIds((prev) => new Set([...prev, ...ids]));
53
- }, []);
54
-
55
- const isSelected = useCallback((id) => selectedIds.has(id), [selectedIds]);
56
-
57
- const registerNodeHandlers = useCallback((id, handlers) => {
58
- nodeHandlersRef.current.set(id, handlers);
59
- return () => nodeHandlersRef.current.delete(id);
60
- }, []);
61
-
62
- const moveSelectedNodes = useCallback(
63
- (deltaX, deltaY, excludeId = null) => {
64
- selectedIds.forEach((id) => {
65
- if (id !== excludeId) {
66
- const handlers = nodeHandlersRef.current.get(id);
67
- if (handlers) {
68
- if (handlers.setOffset) {
69
- handlers.setOffset((prev) => ({
70
- x: prev.x + deltaX,
71
- y: prev.y + deltaY,
72
- }));
73
- }
74
- if (handlers.onDrag) {
75
- handlers.onDrag();
76
- }
77
- }
78
- }
79
- });
80
- },
81
- [selectedIds]
82
- );
83
-
84
- return (
85
- <SelectionContext.Provider
86
- value={{
87
- selectedIds,
88
- selectNode,
89
- deselectNode,
90
- toggleSelection,
91
- clearSelection,
92
- selectMultiple,
93
- addToSelection,
94
- isSelected,
95
- registerNodeHandlers,
96
- moveSelectedNodes,
97
- }}
98
- >
99
- {children}
100
- </SelectionContext.Provider>
101
- );
102
- };
103
-
104
- export const useSelection = () => {
105
- const context = useContext(SelectionContext);
106
- if (!context) {
107
- return {
108
- selectedIds: new Set(),
109
- selectNode: () => {},
110
- deselectNode: () => {},
111
- toggleSelection: () => {},
112
- clearSelection: () => {},
113
- selectMultiple: () => {},
114
- addToSelection: () => {},
115
- isSelected: () => false,
116
- registerNodeHandlers: () => () => {},
117
- moveSelectedNodes: () => {},
118
- };
119
- }
120
- return context;
121
- };
122
-
123
- export default SelectionContext;