likec4 0.52.0 → 0.54.0

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,18 +1,15 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { nonexhaustive } from "@likec4/core";
3
3
  import { isEqualSimple } from "@react-hookz/deep-equal/esnext";
4
- import { useToggle } from "@react-hookz/web/esm";
5
- import { useSpring, useTransition } from "@react-spring/konva";
6
- import { lighten, mix, toHex } from "khroma";
4
+ import { useTransition } from "@react-spring/konva";
7
5
  import { memo, useRef } from "react";
8
- import { Group } from "react-konva";
9
- import { AnimatedCircle, AnimatedGroup, Rect } from "../konva.js";
6
+ import { AnimatedGroup, Rect } from "../konva.js";
10
7
  import { Portal } from "../konva-portal.js";
11
- import { ZoomInIcon } from "./icons/index.js";
8
+ import { CompoundZoomBtn, NodeLinkBtn, NodeZoomBtn } from "./nodes/index.js";
12
9
  import { CylinderShape, MobileShape, PersonShape, QueueShape, RectangleShape } from "./shapes/index.js";
13
10
  import { BrowserShape } from "./shapes/Browser.js";
14
11
  import { CompoundShape } from "./shapes/Compound.js";
15
- import { mouseDefault, mousePointer } from "./shapes/utils.js";
12
+ import { mouseDefault } from "./shapes/utils.js";
16
13
  import { isCompound, useNodeSpringsFn } from "./springs.js";
17
14
  import { DiagramGesture, useHoveredEdge, useHoveredNodeId, useSetHoveredNode } from "./state/index.js";
18
15
  function nodeShape({ shape }) {
@@ -59,65 +56,71 @@ export function Nodes({ animate, theme, diagram, onNodeClick }) {
59
56
  const hoveredNodeId = useHoveredNodeId();
60
57
  const [hoveredEdge] = useHoveredEdge();
61
58
  const nodeSprings = useNodeSpringsFn(theme);
62
- const nodeTransitions = useTransition(diagram.nodes, {
63
- initial: nodeSprings,
64
- from: (node) => {
65
- const prevNode = prevNodes.get(node.id);
66
- if (prevNode) {
67
- return nodeSprings(prevNode);
68
- }
69
- return {
70
- ...nodeSprings(node),
71
- opacity: 0,
72
- scaleX: isCompound(node) ? 0.85 : 0.6,
73
- scaleY: isCompound(node) ? 0.85 : 0.6
74
- };
75
- },
76
- enter: (node) => {
77
- const isReplacing = prevNodes.has(node.id);
78
- return {
79
- ...nodeSprings(node),
80
- delay: isReplacing ? 50 : 70
81
- };
82
- },
83
- // update: nodeSprings(),
84
- update: (node) => {
85
- const isInactive = animate && hoveredEdge && hoveredEdge.source !== node.id && hoveredEdge.target !== node.id;
86
- const scale = animate && !isCompound(node) && hoveredNodeId === node.id ? 1.08 : 1;
87
- return {
88
- ...nodeSprings(node),
89
- opacity: isInactive ? 0.3 : 1,
90
- scaleX: scale,
91
- scaleY: scale
92
- };
93
- },
94
- leave: (node) => {
95
- const replacedWith = diagram.nodes.find((n) => n.id === node.id);
96
- if (replacedWith && keyOf(node) !== keyOf(replacedWith)) {
59
+ const nodeTransitions = useTransition(
60
+ diagram.nodes,
61
+ {
62
+ initial: nodeSprings,
63
+ from: (node) => {
64
+ const prevNode = prevNodes.get(node.id);
65
+ if (prevNode) {
66
+ return nodeSprings(prevNode);
67
+ }
68
+ const scale = isCompound(node) ? 0.85 : 0.6;
97
69
  return {
70
+ ...nodeSprings(node),
98
71
  opacity: 0,
99
- immediate: true
72
+ scaleX: scale,
73
+ scaleY: scale
100
74
  };
101
- }
102
- return {
103
- opacity: 0,
104
- scaleX: isCompound(node) ? 0.7 : 0.5,
105
- scaleY: isCompound(node) ? 0.7 : 0.5,
106
- config: {
107
- duration: 120
75
+ },
76
+ enter: (node) => {
77
+ const isReplacing = prevNodes.has(node.id);
78
+ return {
79
+ ...nodeSprings(node),
80
+ delay: isReplacing ? 50 : 70
81
+ };
82
+ },
83
+ // update: nodeSprings(),
84
+ update: (node) => {
85
+ const isInactive = animate && hoveredEdge && hoveredEdge.source !== node.id && hoveredEdge.target !== node.id;
86
+ const isHovered = animate && !isInactive && !isCompound(node) && hoveredNodeId === node.id;
87
+ const scale = isHovered ? 1.08 : 1;
88
+ return {
89
+ ...nodeSprings(node),
90
+ opacity: isInactive ? 0.3 : 1,
91
+ scaleX: scale,
92
+ scaleY: scale
93
+ };
94
+ },
95
+ leave: (node) => {
96
+ const replacedWith = diagram.nodes.find((n) => n.id === node.id);
97
+ if (replacedWith && keyOf(node) !== keyOf(replacedWith)) {
98
+ return {
99
+ opacity: 0,
100
+ immediate: true
101
+ };
108
102
  }
109
- };
110
- },
111
- sort: (a, b) => {
112
- if (isCompound(a) === isCompound(b)) {
113
- return a.level - b.level;
114
- }
115
- return isCompound(a) ? -1 : 1;
116
- },
117
- expires: true,
118
- immediate: !animate,
119
- keys: keyOf
120
- });
103
+ const scale = isCompound(node) ? 0.7 : 0.5;
104
+ return {
105
+ opacity: 0,
106
+ scaleX: scale,
107
+ scaleY: scale,
108
+ config: {
109
+ duration: 120
110
+ }
111
+ };
112
+ },
113
+ sort: (a, b) => {
114
+ if (isCompound(a) === isCompound(b)) {
115
+ return a.level - b.level;
116
+ }
117
+ return isCompound(a) ? -1 : 1;
118
+ },
119
+ expires: true,
120
+ immediate: !animate,
121
+ keys: keyOf
122
+ }
123
+ );
121
124
  return nodeTransitions((_, node, { key, ctrl, expired }) => /* @__PURE__ */ jsx(
122
125
  NodeShape,
123
126
  {
@@ -220,6 +223,16 @@ const NodeShape = memo(
220
223
  isHovered,
221
224
  onNodeClick
222
225
  }
226
+ ),
227
+ node.links && /* @__PURE__ */ jsx(
228
+ NodeLinkBtn,
229
+ {
230
+ animate,
231
+ node,
232
+ ctrl,
233
+ theme,
234
+ isHovered
235
+ }
223
236
  )
224
237
  ] })
225
238
  ]
@@ -229,165 +242,3 @@ const NodeShape = memo(
229
242
  isEqualSimple
230
243
  );
231
244
  NodeShape.displayName = "NodeShape";
232
- const NodeZoomBtn = ({ animate, node, theme, isHovered: _isHovered, onNodeClick }) => {
233
- const size = 30;
234
- const halfSize = size / 2;
235
- const colors = theme.elements[node.color];
236
- let zoomInIconY;
237
- switch (node.shape) {
238
- case "browser":
239
- case "mobile":
240
- zoomInIconY = node.size.height - 20;
241
- break;
242
- default:
243
- zoomInIconY = node.size.height - 16;
244
- }
245
- const fill = toHex(mix(colors.fill, colors.stroke, 65));
246
- const onOver = toHex(mix(colors.fill, colors.stroke, 75));
247
- const [isOver, toggleOver] = useToggle(false);
248
- const isHovered = _isHovered || isOver;
249
- const props = useSpring({
250
- to: {
251
- fill: isOver ? onOver : fill,
252
- opacity: isOver ? 1 : 0,
253
- y: zoomInIconY + (isOver ? 2 : 0),
254
- scale: isOver ? 1.38 : 1,
255
- // shadowBlur: isOver ? 6 : 4,
256
- shadowOpacity: isOver ? 0.3 : 0.15
257
- // shadowOffsetY: isOver ? 8 : 6
258
- },
259
- delay: isHovered && !isOver ? 100 : 0,
260
- immediate: !animate
261
- });
262
- return /* @__PURE__ */ jsxs(
263
- AnimatedGroup,
264
- {
265
- x: node.size.width / 2,
266
- y: props.y,
267
- offsetX: halfSize,
268
- offsetY: halfSize,
269
- scaleX: props.scale,
270
- scaleY: props.scale,
271
- width: size,
272
- height: size,
273
- onPointerEnter: (e) => {
274
- toggleOver(true);
275
- mousePointer(e);
276
- },
277
- onPointerLeave: (e) => {
278
- toggleOver(false);
279
- mouseDefault(e);
280
- },
281
- onPointerClick: (e) => {
282
- if (DiagramGesture.isDragging || e.evt.button !== 0) {
283
- return;
284
- }
285
- e.cancelBubble = true;
286
- onNodeClick(node, e);
287
- },
288
- children: [
289
- /* @__PURE__ */ jsx(
290
- AnimatedCircle,
291
- {
292
- x: halfSize,
293
- y: halfSize,
294
- radius: halfSize,
295
- fill: props.fill,
296
- shadowBlur: 4,
297
- shadowOpacity: props.shadowOpacity,
298
- shadowOffsetX: 2,
299
- shadowOffsetY: 6,
300
- shadowColor: theme.shadow,
301
- shadowEnabled: isHovered,
302
- perfectDrawEnabled: false,
303
- opacity: props.opacity,
304
- hitStrokeWidth: halfSize
305
- }
306
- ),
307
- /* @__PURE__ */ jsx(ZoomInIcon, { size: 16, x: halfSize, y: halfSize })
308
- ]
309
- }
310
- );
311
- };
312
- const CompoundZoomBtn = ({
313
- animate,
314
- node,
315
- theme,
316
- ctrl,
317
- isHovered: _isHovered,
318
- onNodeClick
319
- }) => {
320
- const size = 28;
321
- const [isOver, toggleOver] = useToggle(false);
322
- const halfSize = size / 2;
323
- const fill = toHex(lighten(ctrl.springs.fill.get(), 10));
324
- const isHovered = _isHovered || isOver;
325
- const props = useSpring({
326
- to: {
327
- opacity: isOver ? 1 : 0,
328
- x: halfSize + 4 - (isOver ? 4 : 0),
329
- y: halfSize + 6 - (isOver ? 4 : 0),
330
- scale: isOver ? 1.35 : 1,
331
- // shadowBlur: isOver ? 6 : 4,
332
- shadowOpacity: isOver ? 0.3 : 0.15
333
- // shadowOffsetY: isOver ? 8 : 6
334
- },
335
- delay: isHovered && !isOver ? 100 : 0,
336
- // delay: isOver ? 150 : (isHovered ? 70 : 0),
337
- immediate: !animate
338
- });
339
- return /* @__PURE__ */ jsx(
340
- Group,
341
- {
342
- onPointerEnter: (e) => {
343
- toggleOver(true);
344
- mousePointer(e);
345
- },
346
- onPointerLeave: (e) => {
347
- toggleOver(false);
348
- mouseDefault(e);
349
- },
350
- onPointerClick: (e) => {
351
- if (DiagramGesture.isDragging || e.evt.button !== 0) {
352
- return;
353
- }
354
- e.cancelBubble = true;
355
- onNodeClick(node, e);
356
- },
357
- children: /* @__PURE__ */ jsxs(
358
- AnimatedGroup,
359
- {
360
- x: props.x,
361
- y: props.y,
362
- offsetX: halfSize,
363
- offsetY: halfSize,
364
- scaleX: props.scale,
365
- scaleY: props.scale,
366
- width: size,
367
- height: size,
368
- children: [
369
- /* @__PURE__ */ jsx(
370
- AnimatedCircle,
371
- {
372
- x: halfSize,
373
- y: halfSize,
374
- radius: halfSize,
375
- fill,
376
- shadowBlur: 4,
377
- shadowOpacity: props.shadowOpacity,
378
- shadowOffsetX: 2,
379
- shadowOffsetY: 6,
380
- shadowColor: theme.shadow,
381
- shadowEnabled: isHovered,
382
- perfectDrawEnabled: false,
383
- opacity: props.opacity,
384
- hitStrokeWidth: halfSize
385
- }
386
- ),
387
- /* @__PURE__ */ jsx(ZoomInIcon, { size: 16, x: halfSize, y: halfSize })
388
- ]
389
- }
390
- )
391
- }
392
- );
393
- };
@@ -0,0 +1,48 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { isEqualSimple } from "@react-hookz/deep-equal/esnext";
3
+ import { memo } from "react";
4
+ import { Group, Path } from "../../konva.js";
5
+ export const LinkIcon = memo(({ color = "#BABABA", opacity = 1, size = 24, x, y }) => {
6
+ const originalSize = 24;
7
+ const scale = size / originalSize;
8
+ const offsetIcon = originalSize / 2;
9
+ return /* @__PURE__ */ jsxs(
10
+ Group,
11
+ {
12
+ x,
13
+ y,
14
+ offsetX: offsetIcon,
15
+ offsetY: offsetIcon,
16
+ scaleX: scale,
17
+ scaleY: scale,
18
+ opacity,
19
+ globalCompositeOperation: "luminosity",
20
+ children: [
21
+ /* @__PURE__ */ jsx(
22
+ Path,
23
+ {
24
+ data: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
25
+ stroke: color,
26
+ strokeWidth: 2,
27
+ lineCap: "round",
28
+ lineJoin: "round",
29
+ listening: false,
30
+ perfectDrawEnabled: false
31
+ }
32
+ ),
33
+ /* @__PURE__ */ jsx(
34
+ Path,
35
+ {
36
+ data: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
37
+ stroke: color,
38
+ strokeWidth: 2,
39
+ lineCap: "round",
40
+ lineJoin: "round",
41
+ listening: false,
42
+ perfectDrawEnabled: false
43
+ }
44
+ )
45
+ ]
46
+ }
47
+ );
48
+ }, isEqualSimple);
@@ -0,0 +1,74 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { isEqualSimple } from "@react-hookz/deep-equal/esnext";
3
+ import { memo } from "react";
4
+ import { Circle, Group, Line } from "../../konva.js";
5
+ export const ZoomInIcon = memo(({ fill = "#BABABA", opacity = 1, size = 20, x, y }) => {
6
+ const originalSize = 24;
7
+ const scale = size / originalSize;
8
+ const offsetIcon = originalSize / 2;
9
+ return /* @__PURE__ */ jsxs(
10
+ Group,
11
+ {
12
+ x,
13
+ y,
14
+ offsetX: offsetIcon,
15
+ offsetY: offsetIcon,
16
+ scaleX: scale,
17
+ scaleY: scale,
18
+ width: originalSize,
19
+ height: originalSize,
20
+ opacity,
21
+ globalCompositeOperation: "luminosity",
22
+ children: [
23
+ /* @__PURE__ */ jsx(
24
+ Circle,
25
+ {
26
+ x: 11,
27
+ y: 11,
28
+ radius: 8,
29
+ stroke: fill,
30
+ strokeWidth: 2,
31
+ perfectDrawEnabled: false,
32
+ listening: false
33
+ }
34
+ ),
35
+ /* @__PURE__ */ jsx(
36
+ Line,
37
+ {
38
+ points: [22, 22, 16.65, 16.65],
39
+ stroke: fill,
40
+ strokeWidth: 2,
41
+ perfectDrawEnabled: false,
42
+ listening: false,
43
+ lineCap: "round",
44
+ lineJoin: "round"
45
+ }
46
+ ),
47
+ /* @__PURE__ */ jsx(
48
+ Line,
49
+ {
50
+ points: [11, 8, 11, 14],
51
+ stroke: fill,
52
+ strokeWidth: 2,
53
+ perfectDrawEnabled: false,
54
+ listening: false,
55
+ lineCap: "round",
56
+ lineJoin: "round"
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsx(
60
+ Line,
61
+ {
62
+ points: [8, 11, 14, 11],
63
+ stroke: fill,
64
+ strokeWidth: 2,
65
+ perfectDrawEnabled: false,
66
+ listening: false,
67
+ lineCap: "round",
68
+ lineJoin: "round"
69
+ }
70
+ )
71
+ ]
72
+ }
73
+ );
74
+ }, isEqualSimple);
@@ -1,3 +1,2 @@
1
- export * from "./ExternalLink.js";
2
- export * from "./BrainIcon.js";
3
- export * from "./ZoomIn.js";
1
+ export * from "./LinkIcon.js";
2
+ export * from "./ZoomInIcon.js";
@@ -0,0 +1,139 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { invariant } from "@likec4/core";
3
+ import { isEqualSimple } from "@react-hookz/deep-equal/esnext";
4
+ import { useToggle } from "@react-hookz/web/esm";
5
+ import { useSpring } from "@react-spring/konva";
6
+ import { mix, toHex } from "khroma";
7
+ import { memo } from "react";
8
+ import { AnimatedCircle, AnimatedGroup } from "../../konva.js";
9
+ import { LinkIcon } from "../icons/index.js";
10
+ import { DiagramGesture } from "../state/index.js";
11
+ import { mouseDefault, mousePointer } from "../utils.js";
12
+ export const NodeLinkBtn = memo(
13
+ ({ animate, node, theme, isHovered: _isHovered }) => {
14
+ const links = node.links;
15
+ invariant(links, "NodeLinkBtn: node.links is undefined");
16
+ const size = 30;
17
+ const halfSize = size / 2;
18
+ const colors = theme.elements[node.color];
19
+ let iconX;
20
+ switch (node.shape) {
21
+ case "browser": {
22
+ iconX = 21;
23
+ break;
24
+ }
25
+ case "mobile": {
26
+ iconX = 16;
27
+ break;
28
+ }
29
+ case "queue": {
30
+ iconX = 26;
31
+ break;
32
+ }
33
+ case "cylinder":
34
+ case "storage": {
35
+ iconX = 14;
36
+ break;
37
+ }
38
+ default:
39
+ iconX = 16;
40
+ }
41
+ let iconY;
42
+ switch (node.shape) {
43
+ case "browser": {
44
+ iconY = node.size.height - 20;
45
+ break;
46
+ }
47
+ case "queue": {
48
+ iconY = node.size.height - 14;
49
+ break;
50
+ }
51
+ case "cylinder":
52
+ case "storage": {
53
+ iconY = node.size.height - 22;
54
+ break;
55
+ }
56
+ default:
57
+ iconY = node.size.height - 16;
58
+ }
59
+ const fill = toHex(mix(colors.fill, colors.stroke, 65));
60
+ const onOver = toHex(mix(colors.fill, colors.stroke, 75));
61
+ const [isOver, toggleOver] = useToggle(false);
62
+ const isHovered = _isHovered || isOver;
63
+ const props = useSpring({
64
+ to: {
65
+ fill: isOver ? onOver : fill,
66
+ opacity: isOver ? 1 : 0,
67
+ // y: zoomInIconY + (isOver ? 2 : 0),
68
+ scale: isOver ? 1.38 : 1,
69
+ // shadowBlur: isOver ? 6 : 4,
70
+ shadowOpacity: isOver ? 0.3 : 0.15
71
+ // shadowOffsetY: isOver ? 8 : 6
72
+ },
73
+ delay: isHovered && !isOver ? 100 : 0,
74
+ immediate: !animate
75
+ });
76
+ return /* @__PURE__ */ jsxs(
77
+ AnimatedGroup,
78
+ {
79
+ x: iconX,
80
+ y: iconY,
81
+ offsetX: halfSize,
82
+ offsetY: halfSize,
83
+ scaleX: props.scale,
84
+ scaleY: props.scale,
85
+ width: size,
86
+ height: size,
87
+ onPointerEnter: (e) => {
88
+ toggleOver(true);
89
+ mousePointer(e);
90
+ },
91
+ onPointerLeave: (e) => {
92
+ toggleOver(false);
93
+ mouseDefault(e);
94
+ },
95
+ onPointerClick: (e) => {
96
+ if (DiagramGesture.isDragging || e.evt.button !== 0) {
97
+ return;
98
+ }
99
+ e.cancelBubble = true;
100
+ e.evt.stopPropagation();
101
+ if (!window.open(links[0], "_blank")) {
102
+ window.alert("Please allow popups for this website");
103
+ }
104
+ },
105
+ children: [
106
+ /* @__PURE__ */ jsx(
107
+ AnimatedCircle,
108
+ {
109
+ x: halfSize,
110
+ y: halfSize,
111
+ radius: halfSize,
112
+ fill: props.fill,
113
+ shadowBlur: 4,
114
+ shadowOpacity: props.shadowOpacity,
115
+ shadowOffsetX: 2,
116
+ shadowOffsetY: 6,
117
+ shadowColor: theme.shadow,
118
+ shadowEnabled: isHovered,
119
+ perfectDrawEnabled: false,
120
+ opacity: props.opacity,
121
+ hitStrokeWidth: halfSize
122
+ }
123
+ ),
124
+ /* @__PURE__ */ jsx(
125
+ LinkIcon,
126
+ {
127
+ size: isHovered ? 16 : 14,
128
+ opacity: isHovered ? 1 : 0.9,
129
+ x: halfSize,
130
+ y: halfSize
131
+ }
132
+ )
133
+ ]
134
+ }
135
+ );
136
+ },
137
+ isEqualSimple
138
+ );
139
+ NodeLinkBtn.displayName = "NodeLinkBtn";