@canmingir/link 1.2.6 → 1.2.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canmingir/link",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -30,6 +30,7 @@ const DynamicConnector = ({
30
30
  };
31
31
 
32
32
  const children = childEls.map((el) => {
33
+ if (!el) return { x: parent.x, y: parent.y };
33
34
  const r = el.getBoundingClientRect();
34
35
  return {
35
36
  x: r.left + r.width / 2 - cRect.left,
@@ -45,12 +46,13 @@ const DynamicConnector = ({
45
46
  setDims({ w: cRect.width, h: cRect.height });
46
47
  };
47
48
 
49
+ update();
50
+
48
51
  const ro = new ResizeObserver(update);
49
52
  ro.observe(containerEl);
50
53
  ro.observe(parentEl);
51
54
  childEls.forEach((el) => el && ro.observe(el));
52
55
 
53
- update();
54
56
  return () => ro.disconnect();
55
57
  }, [containerEl, parentEl, childEls, tick]);
56
58
 
@@ -65,6 +67,18 @@ const DynamicConnector = ({
65
67
 
66
68
  const onlyOne = points.children.length === 1;
67
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}`,
80
+ };
81
+
68
82
  if (connectorType === "curved" || connectorType === "n8n") {
69
83
  const createN8nPath = (from, to) => {
70
84
  const v = Math.max(32, Math.abs(to.y - from.y) * 0.35);
@@ -83,12 +97,7 @@ const DynamicConnector = ({
83
97
  };
84
98
 
85
99
  return (
86
- <svg
87
- style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
88
- width="100%"
89
- height="100%"
90
- viewBox={`0 0 ${dims.w} ${dims.h}`}
91
- >
100
+ <svg {...svgProps}>
92
101
  {points.children.map((child, i) => (
93
102
  <path
94
103
  key={i}
@@ -106,12 +115,7 @@ const DynamicConnector = ({
106
115
  }
107
116
 
108
117
  return (
109
- <svg
110
- style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
111
- width="100%"
112
- height="100%"
113
- viewBox={`0 0 ${dims.w} ${dims.h}`}
114
- >
118
+ <svg {...svgProps}>
115
119
  {onlyOne ? (
116
120
  <line
117
121
  x1={points.parent.x}
@@ -136,8 +140,8 @@ const DynamicConnector = ({
136
140
 
137
141
  {(() => {
138
142
  const xs = points.children.map((c) => c.x);
139
- const xMin = Math.min(...xs);
140
- const xMax = Math.max(...xs);
143
+ const xMin = Math.min(...xs, points.parent.x);
144
+ const xMax = Math.max(...xs, points.parent.x);
141
145
  return (
142
146
  <line
143
147
  x1={xMin}
@@ -27,7 +27,13 @@ export const Flow = ({ data, variant = "simple", style, plugin }) => {
27
27
  }, [nodesById, roots]);
28
28
 
29
29
  return (
30
- <FlowNode node={treeData} variant={variant} style={style} plugin={plugin} />
30
+ <FlowNode
31
+ node={treeData}
32
+ variant={variant}
33
+ style={style}
34
+ plugin={plugin}
35
+ isRoot={true}
36
+ />
31
37
  );
32
38
  };
33
39
 
@@ -11,7 +11,15 @@ import {
11
11
  toPxNumber,
12
12
  } from "./styles";
13
13
 
14
- const FlowNode = ({ node, type, variant, style, plugin }) => {
14
+ const NodeContent = ({
15
+ node,
16
+ type,
17
+ variant,
18
+ style,
19
+ plugin,
20
+ registerRef,
21
+ onDrag,
22
+ }) => {
15
23
  const baseStyle = getBaseStyleForVariant(variant);
16
24
  const hasChildren = Array.isArray(node.children) && node.children.length > 0;
17
25
 
@@ -78,7 +86,11 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
78
86
  const [childElList, setChildElList] = useState([]);
79
87
 
80
88
  const [connectorTick, setConnectorTick] = useState(0);
81
- const notifyDrag = () => setConnectorTick((t) => t + 1);
89
+
90
+ const handleDrag = (newOffset) => {
91
+ setConnectorTick((t) => t + 1);
92
+ if (onDrag) onDrag(newOffset);
93
+ };
82
94
 
83
95
  useLayoutEffect(() => {
84
96
  const els = (node.children || [])
@@ -213,9 +225,15 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
213
225
  position: "relative",
214
226
  }}
215
227
  >
216
- <Box ref={parentRef} sx={{ position: "relative" }}>
228
+ <DraggableNode
229
+ registerRef={(el) => {
230
+ parentRef.current = el;
231
+ if (registerRef) registerRef(el);
232
+ }}
233
+ onDrag={handleDrag}
234
+ >
217
235
  {renderContent()}
218
- </Box>
236
+ </DraggableNode>
219
237
 
220
238
  {hasChildren && (
221
239
  <>
@@ -242,19 +260,17 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
242
260
  }}
243
261
  >
244
262
  {node.children.map((child) => (
245
- <DraggableNode
263
+ <FlowNode
246
264
  key={child.id}
265
+ node={child}
266
+ type={type}
267
+ variant={variant}
268
+ style={style}
269
+ plugin={plugin}
247
270
  registerRef={(el) => (childRefs.current[child.id] = el)}
248
- onDrag={notifyDrag}
249
- >
250
- <FlowNode
251
- node={child}
252
- type={type}
253
- variant={variant}
254
- style={style}
255
- plugin={plugin}
256
- />
257
- </DraggableNode>
271
+ onDrag={() => setConnectorTick((t) => t + 1)}
272
+ isRoot={false}
273
+ />
258
274
  ))}
259
275
  </Box>
260
276
  </>
@@ -263,4 +279,93 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
263
279
  );
264
280
  };
265
281
 
282
+ const FlowNode = ({ isRoot = false, ...props }) => {
283
+ if (!isRoot) {
284
+ return <NodeContent {...props} />;
285
+ }
286
+
287
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
288
+ const [isDragging, setIsDragging] = useState(false);
289
+
290
+ const [zoom, setZoom] = useState(1);
291
+
292
+ const clampZoom = (z) => Math.min(2.5, Math.max(0.25, z));
293
+
294
+ useEffect(() => {
295
+ const onWheel = (e) => {
296
+ const wantsZoom = e.ctrlKey || e.metaKey;
297
+ if (!wantsZoom) return;
298
+
299
+ e.preventDefault();
300
+
301
+ const direction = e.deltaY > 0 ? -1 : 1;
302
+ const factor = direction > 0 ? 1.1 : 1 / 1.1;
303
+
304
+ setZoom((z) => clampZoom(z * factor));
305
+ };
306
+
307
+ window.addEventListener("wheel", onWheel, { passive: false });
308
+ return () => window.removeEventListener("wheel", onWheel);
309
+ }, []);
310
+
311
+ const handleCanvasMouseDown = (e) => {
312
+ if (e.target?.closest?.('[data-flow-zoom="true"]')) return;
313
+
314
+ if (e.button !== 0) return;
315
+
316
+ setIsDragging(true);
317
+
318
+ const startX = e.clientX;
319
+ const startY = e.clientY;
320
+ const startOffset = { ...offset };
321
+
322
+ const onMove = (ev) => {
323
+ setOffset({
324
+ x: startOffset.x + (ev.clientX - startX),
325
+ y: startOffset.y + (ev.clientY - startY),
326
+ });
327
+ };
328
+
329
+ const onUp = () => {
330
+ setIsDragging(false);
331
+ window.removeEventListener("mousemove", onMove);
332
+ window.removeEventListener("mouseup", onUp);
333
+ };
334
+
335
+ window.addEventListener("mousemove", onMove);
336
+ window.addEventListener("mouseup", onUp);
337
+ };
338
+
339
+ return (
340
+ <Box
341
+ onMouseDown={handleCanvasMouseDown}
342
+ sx={{
343
+ width: "100vw",
344
+ height: "100vh",
345
+ overflow: "hidden",
346
+ bgcolor: "none",
347
+ cursor: isDragging ? "grabbing" : "default",
348
+ userSelect: "none",
349
+ position: "relative",
350
+ }}
351
+ >
352
+ <Box
353
+ sx={{
354
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
355
+ transformOrigin: "center center",
356
+ width: "100%",
357
+ height: "100%",
358
+ display: "flex",
359
+ alignItems: "center",
360
+ justifyContent: "center",
361
+ transition: isDragging ? "none" : "transform 0.1s ease-out",
362
+ pointerEvents: "auto",
363
+ }}
364
+ >
365
+ <NodeContent {...props} />
366
+ </Box>
367
+ </Box>
368
+ );
369
+ };
370
+
266
371
  export default FlowNode;
@@ -1,12 +1,27 @@
1
1
  import React from "react";
2
2
 
3
- import { Box, Card, Stack } from "@mui/material";
3
+ import { Box, Card, Stack, Typography } from "@mui/material";
4
4
  import { alpha, useTheme } from "@mui/material/styles";
5
5
 
6
+ const STYLES = {
7
+ width: "100%",
8
+ display: "-webkit-box",
9
+ WebkitLineClamp: 2,
10
+ WebkitBoxOrient: "vertical",
11
+ overflow: "hidden",
12
+ textOverflow: "ellipsis",
13
+ wordBreak: "break-word",
14
+ lineHeight: 1.3,
15
+ opacity: 0.85,
16
+ };
17
+
6
18
  export function MediaAvatarCard({
7
19
  sx,
8
20
  leftContent,
9
21
  rightContent,
22
+ title,
23
+ description,
24
+ footer,
10
25
  background,
11
26
  overlayColor,
12
27
  backgroundClassName = "animated-background-image",
@@ -25,6 +40,9 @@ export function MediaAvatarCard({
25
40
  position: "relative",
26
41
  display: "flex",
27
42
  flexDirection: "row",
43
+ fontSize: 12,
44
+ color: "rgba(255,255,255,0.82)",
45
+ lineHeight: 1.3,
28
46
  boxShadow: (theme) => theme.shadows[3],
29
47
  "&:hover": {
30
48
  boxShadow: (theme) => theme.shadows[6],
@@ -82,75 +100,44 @@ export function MediaAvatarCard({
82
100
 
83
101
  <Stack
84
102
  direction="column"
85
- spacing={1}
86
103
  sx={{
87
104
  flex: 1,
88
105
  padding: 2,
89
106
  zIndex: 1,
90
107
  position: "relative",
108
+ height: "100%",
109
+ justifyContent: "space-between",
91
110
  }}
92
111
  >
93
- {rightContent}
94
- </Stack>
95
- </Card>
96
- );
97
- }
112
+ {rightContent ? (
113
+ rightContent
114
+ ) : (
115
+ <>
116
+ <Stack spacing={1} sx={{ width: "100%", minWidth: 0 }}>
117
+ <Box>{title}</Box>
98
118
 
99
- export function HeaderCard({
100
- sx,
101
- header,
102
- children,
103
- height = 140,
104
- padding = 2,
105
- }) {
106
- const theme = useTheme();
119
+ {description && (
120
+ <Typography variant="inherit" sx={STYLES}>
121
+ {description}
122
+ </Typography>
123
+ )}
124
+ </Stack>
107
125
 
108
- return (
109
- <Card
110
- sx={{
111
- p: padding,
112
- minWidth: 200,
113
- maxWidth: 420,
114
- height,
115
- borderRadius: 3,
116
- position: "relative",
117
- overflow: "hidden",
118
- backgroundColor: theme.palette.background.paper,
119
- transition: "all 0.3s ease-in-out",
120
- "&:hover": {
121
- transform: "translateY(-4px)",
122
- boxShadow: theme.shadows[8],
123
- "&:before": {
124
- height: "100%",
125
- },
126
- },
127
- "&:before": {
128
- content: '""',
129
- width: "100%",
130
- height: "40%",
131
- position: "absolute",
132
- top: 0,
133
- left: 0,
134
- background: `linear-gradient(135deg, ${alpha(
135
- theme.palette.secondary.light,
136
- 0.2
137
- )}, ${alpha(theme.palette.primary.main, 0.3)})`,
138
- borderRadius: "8px 8px 0 0",
139
- transition: "height 0.3s ease-in-out",
140
- },
141
- ...sx,
142
- }}
143
- >
144
- <Stack
145
- spacing={2}
146
- sx={{
147
- height: "100%",
148
- position: "relative",
149
- zIndex: 1,
150
- }}
151
- >
152
- {header}
153
- {children}
126
+ {footer && (
127
+ <Box
128
+ sx={{
129
+ display: "flex",
130
+ justifyContent: "flex-end",
131
+ alignItems: "center",
132
+ gap: 0.5,
133
+ mt: 0.5,
134
+ }}
135
+ >
136
+ {footer}
137
+ </Box>
138
+ )}
139
+ </>
140
+ )}
154
141
  </Stack>
155
142
  </Card>
156
143
  );
@@ -160,6 +147,9 @@ export function SideStripeCard({
160
147
  sx,
161
148
  stripeContent,
162
149
  mainContent,
150
+ title,
151
+ description,
152
+ footer,
163
153
  minWidth = 200,
164
154
  maxWidth = 290,
165
155
  height = 100,
@@ -168,11 +158,7 @@ export function SideStripeCard({
168
158
  const theme = useTheme();
169
159
 
170
160
  return (
171
- <Box
172
- sx={{
173
- cursor: "pointer",
174
- }}
175
- >
161
+ <Box sx={{ cursor: "pointer" }}>
176
162
  <Card
177
163
  sx={{
178
164
  display: "flex",
@@ -185,6 +171,9 @@ export function SideStripeCard({
185
171
  boxShadow: 3,
186
172
  position: "relative",
187
173
  overflow: "hidden",
174
+ fontSize: 12,
175
+ lineHeight: 1.3,
176
+ color: "rgba(255,255,255,0.82)",
188
177
  "&:hover": {
189
178
  boxShadow: 6,
190
179
  "& .animated-background-selector": {
@@ -233,20 +222,118 @@ export function SideStripeCard({
233
222
 
234
223
  <Stack
235
224
  direction="column"
236
- spacing={1}
237
225
  sx={{
238
226
  padding: 2,
239
227
  position: "relative",
240
228
  zIndex: 1,
229
+ flex: 1,
230
+ height: "100%",
231
+ justifyContent: "space-between",
241
232
  }}
242
233
  >
243
- {mainContent}
234
+ {mainContent ? (
235
+ mainContent
236
+ ) : (
237
+ <>
238
+ <Box sx={{ minWidth: 0 }}>
239
+ {typeof title === "string" ? (
240
+ <Typography
241
+ variant="inherit"
242
+ sx={{
243
+ color: "text.primary",
244
+ wordBreak: "break-word",
245
+ hyphens: "auto",
246
+ fontWeight: 600,
247
+ }}
248
+ >
249
+ {title}
250
+ </Typography>
251
+ ) : (
252
+ title
253
+ )}
254
+
255
+ {description && (
256
+ <Typography
257
+ variant="inherit"
258
+ sx={{
259
+ ...STYLES,
260
+ mt: 0.5,
261
+ color: "text.primary",
262
+ opacity: 0.8,
263
+ }}
264
+ >
265
+ {description}
266
+ </Typography>
267
+ )}
268
+ </Box>
269
+
270
+ {footer && (
271
+ <Box sx={{ alignSelf: "flex-end", mt: 0.5 }}>{footer}</Box>
272
+ )}
273
+ </>
274
+ )}
244
275
  </Stack>
245
276
  </Card>
246
277
  </Box>
247
278
  );
248
279
  }
249
280
 
281
+ export function HeaderCard({
282
+ sx,
283
+ header,
284
+ children,
285
+ height = 140,
286
+ padding = 2,
287
+ }) {
288
+ const theme = useTheme();
289
+ return (
290
+ <Card
291
+ sx={{
292
+ p: padding,
293
+ minWidth: 200,
294
+ maxWidth: 420,
295
+ height,
296
+ borderRadius: 3,
297
+ position: "relative",
298
+ overflow: "hidden",
299
+ backgroundColor: theme.palette.background.paper,
300
+ fontSize: 12,
301
+ lineHeight: 1.3,
302
+ color: "rgba(255,255,255,0.82)",
303
+ transition: "all 0.3s ease-in-out",
304
+ "&:hover": {
305
+ transform: "translateY(-4px)",
306
+ boxShadow: theme.shadows[8],
307
+ "&:before": { height: "100%" },
308
+ },
309
+ "&:before": {
310
+ content: '""',
311
+ width: "100%",
312
+ height: "40%",
313
+ position: "absolute",
314
+ top: 0,
315
+ left: 0,
316
+ background: `linear-gradient(135deg, ${alpha(
317
+ theme.palette.secondary.light,
318
+ 0.2
319
+ )}, ${alpha(theme.palette.primary.main, 0.3)})`,
320
+ borderRadius: "8px 8px 0 0",
321
+ transition: "height 0.3s ease-in-out",
322
+ },
323
+ ...sx,
324
+ }}
325
+ >
326
+ <Stack
327
+ spacing={2}
328
+ sx={{ height: "100%", position: "relative", zIndex: 1 }}
329
+ >
330
+ {header}
331
+ {children}
332
+ </Stack>
333
+ </Card>
334
+ );
335
+ }
336
+
250
337
  export function AvatarRoleCard({
251
338
  sx,
252
339
  leftContent,
@@ -264,13 +351,14 @@ export function AvatarRoleCard({
264
351
  borderRadius: 2,
265
352
  position: "relative",
266
353
  backgroundColor: theme.palette.background.paper,
354
+ fontSize: 12,
355
+ lineHeight: 1.3,
356
+ color: "rgba(255,255,255,0.82)",
267
357
  transition: "all 0.3s ease-in-out",
268
358
  "&:hover": {
269
359
  transform: "translateY(-4px)",
270
360
  boxShadow: theme.shadows[8],
271
- "&:before": {
272
- height: "100%",
273
- },
361
+ "&:before": { height: "100%" },
274
362
  },
275
363
  "&:before": {
276
364
  content: '""',
@@ -291,11 +379,7 @@ export function AvatarRoleCard({
291
379
  >
292
380
  <Stack
293
381
  spacing={1}
294
- sx={{
295
- height: "100%",
296
- position: "relative",
297
- zIndex: 1,
298
- }}
382
+ sx={{ height: "100%", position: "relative", zIndex: 1 }}
299
383
  >
300
384
  <Stack
301
385
  direction="row"