likec4 0.49.0 → 0.51.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.
Files changed (50) hide show
  1. package/dist/@likec4/diagrams/diagram/Diagram.js +55 -27
  2. package/dist/@likec4/diagrams/diagram/Edges.js +14 -11
  3. package/dist/@likec4/diagrams/diagram/Nodes.js +212 -23
  4. package/dist/@likec4/diagrams/diagram/icons/ZoomIn.js +2 -1
  5. package/dist/@likec4/diagrams/diagram/shapes/Compound.js +2 -1
  6. package/dist/@likec4/diagrams/diagram/shapes/Edge.js +1 -1
  7. package/dist/@likec4/diagrams/diagram/state/atoms.js +35 -60
  8. package/dist/@likec4/diagrams/diagram/utils.js +14 -0
  9. package/dist/@likec4/diagrams/hooks/useDiagramApi.js +4 -5
  10. package/dist/@likec4/diagrams/hooks/useImageLoader.js +1 -1
  11. package/dist/__app__/likec4.css +8 -5
  12. package/dist/__app__/src/App.jsx +9 -11
  13. package/dist/__app__/src/components/CopyToClipboard.jsx +29 -0
  14. package/dist/__app__/src/components/CopyToClipboard.module.css +16 -0
  15. package/dist/__app__/src/components/DiagramNotFound.jsx +2 -2
  16. package/dist/__app__/src/components/index.js +2 -1
  17. package/dist/__app__/src/components/sidebar/Sidebar.jsx +1 -1
  18. package/dist/__app__/src/components/sidebar/styles.module.css +4 -1
  19. package/dist/__app__/src/components/view-page/DisplayModeSelector.jsx +15 -9
  20. package/dist/__app__/src/components/view-page/Header.jsx +97 -0
  21. package/dist/__app__/src/components/view-page/Header.module.css +24 -0
  22. package/dist/__app__/src/components/view-page/ShareDialog.jsx +23 -15
  23. package/dist/__app__/src/components/view-page/ViewActions.jsx +69 -0
  24. package/dist/__app__/src/data/atoms.js +4 -22
  25. package/dist/__app__/src/data/hooks.js +5 -4
  26. package/dist/__app__/src/data/index-page.js +22 -0
  27. package/dist/__app__/src/likec4-views.js +1 -1
  28. package/dist/__app__/src/pages/index-page/index.jsx +99 -0
  29. package/dist/__app__/src/pages/index-page/index.module.css +20 -0
  30. package/dist/__app__/src/pages/index.js +1 -1
  31. package/dist/__app__/src/pages/useTransparentBackground.js +2 -2
  32. package/dist/__app__/src/pages/view-page/ViewAsReact.jsx +60 -0
  33. package/dist/__app__/src/pages/view-page/index.js +11 -0
  34. package/dist/__app__/src/pages/view-page/other-formats/ViewAsD2.jsx +18 -0
  35. package/dist/__app__/src/pages/view-page/other-formats/ViewAsDot.jsx +33 -0
  36. package/dist/__app__/src/pages/view-page/other-formats/ViewAsMmd.jsx +18 -0
  37. package/dist/__app__/src/pages/view-page/other-formats.jsx +43 -0
  38. package/dist/__app__/src/pages/view-page/view-page.module.css +81 -0
  39. package/dist/__app__/src/pages/view.page.jsx +12 -65
  40. package/dist/__app__/src/router.js +90 -20
  41. package/dist/__app__/src/utils/utils.js +1 -2
  42. package/dist/__app__/tsconfig.json +1 -0
  43. package/dist/cli/index.js +286 -212
  44. package/package.json +19 -20
  45. package/dist/__app__/postcss.config.cjs +0 -11
  46. package/dist/__app__/src/components/view-page/ViewActionsToolbar.jsx +0 -66
  47. package/dist/__app__/src/pages/index.module.css +0 -11
  48. package/dist/__app__/src/pages/index.page.jsx +0 -57
  49. package/dist/__app__/src/pages/view-page.module.css +0 -30
  50. package/dist/__app__/tailwind.config.cjs +0 -17
@@ -2,13 +2,14 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { nonNullable, defaultTheme as theme } from "@likec4/core";
3
3
  import { useHookableRef, useUpdateEffect } from "@react-hookz/web/esm";
4
4
  import { useSpring } from "@react-spring/konva";
5
- import { clamp } from "rambdax";
5
+ import { clamp, isNil } from "rambdax";
6
6
  import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
7
7
  import { AnimatedStage, Layer } from "../konva.js";
8
8
  import { Edges } from "./Edges.js";
9
9
  import { createUseGesture, dragAction, pinchAction } from "@use-gesture/react";
10
10
  import { Nodes } from "./Nodes.js";
11
- import { DiagramGesture, useResetHoveredStates } from "./state/index.js";
11
+ import { DiagramGesture, DiagramStateProvider, useResetHoveredStates } from "./state/index.js";
12
+ import { isNumber } from "./utils.js";
12
13
  const useGesture = createUseGesture([dragAction, pinchAction]);
13
14
  const useSyncedRef = (value) => {
14
15
  const ref = useRef(value);
@@ -27,7 +28,7 @@ function diagramNodeId(konvaNode) {
27
28
  }
28
29
  return null;
29
30
  }
30
- export const Diagram = /* @__PURE__ */ forwardRef(
31
+ const DiagramKonva = /* @__PURE__ */ forwardRef(
31
32
  ({
32
33
  diagram,
33
34
  padding = NoPadding,
@@ -56,15 +57,17 @@ export const Diagram = /* @__PURE__ */ forwardRef(
56
57
  }
57
58
  return value;
58
59
  });
59
- const [paddingTop, paddingRight, paddingBottom, paddingLeft] = Array.isArray(padding) ? padding : [padding, padding, padding, padding];
60
+ const [paddingTop, paddingRight, paddingBottom, paddingLeft] = isNumber(padding) ? [padding, padding, padding, padding] : padding;
60
61
  const width = _width ?? diagram.width + paddingLeft + paddingRight;
61
62
  const height = _height ?? diagram.height + paddingTop + paddingBottom;
62
- const toCenterOnRect = (centerTo) => {
63
+ const toCenterOnRect = (centerTo, opts) => {
64
+ const keepZoom = opts?.keepZoom ?? false;
63
65
  const container = containerRef.current;
66
+ const _maxZoom = keepZoom === true && !isNil(stageRef.current) ? stageRef.current.scaleX() : maxZoom;
64
67
  const viewRect = {
65
68
  width: Math.min(container?.clientWidth ?? width, width) - paddingLeft - paddingRight,
66
69
  height: Math.min(container?.clientHeight ?? height, height) - paddingTop - paddingBottom
67
- }, viewScale = Math.min(viewRect.width / centerTo.width, viewRect.height / centerTo.height), scale = clamp(minZoom, maxZoom, viewScale), centeringAjustment = {
70
+ }, viewScale = Math.min(viewRect.width / centerTo.width, viewRect.height / centerTo.height), scale = clamp(minZoom, _maxZoom, viewScale), centeringAjustment = {
68
71
  x: ((width - centerTo.width) * scale + viewRect.width) / 2,
69
72
  y: ((height - centerTo.height) * scale + viewRect.height) / 2
70
73
  }, finalPosition = {
@@ -88,18 +91,21 @@ export const Diagram = /* @__PURE__ */ forwardRef(
88
91
  },
89
92
  []
90
93
  );
91
- const centerOnRect = (centerTo) => {
94
+ const centerOnRect = (centerTo, opts) => {
92
95
  stageSpringApi.start({
93
- to: toCenterOnRect(centerTo),
94
- immediate
96
+ to: toCenterOnRect(centerTo, {
97
+ keepZoom: opts?.keepZoom ?? true
98
+ }),
99
+ delay: opts?.delay ?? 0,
100
+ immediate: immediate || (opts?.immediate ?? false)
95
101
  });
96
102
  return;
97
103
  };
98
- const centerAndFit = (delay = 0) => {
104
+ const centerAndFit = (opts) => {
99
105
  stageSpringApi.start({
100
106
  to: toFitDiagram(),
101
- delay,
102
- immediate
107
+ delay: opts?.delay ?? 0,
108
+ immediate: immediate || (opts?.immediate ?? false)
103
109
  });
104
110
  return;
105
111
  };
@@ -137,25 +143,31 @@ export const Diagram = /* @__PURE__ */ forwardRef(
137
143
  resetStageZoom: (_immediate) => {
138
144
  refs.current.resetStageZoom(_immediate);
139
145
  },
140
- centerOnNode: (node) => refs.current.centerOnRect({
141
- x: node.position[0],
142
- y: node.position[1],
143
- width: node.size.width,
144
- height: node.size.height
145
- }),
146
- centerAndFit: () => refs.current.centerAndFit()
146
+ centerOnNode: (node, opts) => refs.current.centerOnRect(
147
+ {
148
+ x: node.position[0],
149
+ y: node.position[1],
150
+ width: node.size.width,
151
+ height: node.size.height
152
+ },
153
+ opts
154
+ ),
155
+ centerOnRect: (rect, opts) => refs.current.centerOnRect(rect, opts),
156
+ centerAndFit: (opts) => refs.current.centerAndFit(opts)
147
157
  }),
148
158
  [refs, stageRef]
149
159
  );
150
160
  const resetHoveredStates = useResetHoveredStates();
151
161
  useUpdateEffect(() => {
152
162
  resetHoveredStates();
163
+ refs.current.centerAndFit({
164
+ delay: 200
165
+ });
153
166
  }, [id]);
154
167
  useUpdateEffect(() => {
155
- refs.current.centerAndFit(200);
156
- }, [id]);
157
- useUpdateEffect(() => {
158
- refs.current.centerAndFit(50);
168
+ refs.current.centerAndFit({
169
+ keepZoom: true
170
+ });
159
171
  }, [height, width]);
160
172
  useEffect(() => {
161
173
  if (!zoomable) {
@@ -259,9 +271,12 @@ export const Diagram = /* @__PURE__ */ forwardRef(
259
271
  theme,
260
272
  diagram
261
273
  };
274
+ const layerCenterX = diagram.width / 2;
275
+ const layerCenterY = diagram.height / 2;
262
276
  return /* @__PURE__ */ jsxs(
263
277
  AnimatedStage,
264
278
  {
279
+ _useStrictMode: true,
265
280
  ref: stageRef,
266
281
  width,
267
282
  height,
@@ -316,14 +331,27 @@ export const Diagram = /* @__PURE__ */ forwardRef(
316
331
  },
317
332
  ...props,
318
333
  children: [
319
- /* @__PURE__ */ jsxs(Layer, { children: [
320
- /* @__PURE__ */ jsx(Nodes, { ...sharedProps, onNodeClick }),
321
- /* @__PURE__ */ jsx(Edges, { ...sharedProps, onEdgeClick })
322
- ] }),
334
+ /* @__PURE__ */ jsxs(
335
+ Layer,
336
+ {
337
+ x: layerCenterX,
338
+ offsetX: layerCenterX,
339
+ y: layerCenterY,
340
+ offsetY: layerCenterY,
341
+ scaleX: 1,
342
+ scaleY: 1,
343
+ children: [
344
+ /* @__PURE__ */ jsx(Nodes, { ...sharedProps, onNodeClick }),
345
+ /* @__PURE__ */ jsx(Edges, { ...sharedProps, onEdgeClick })
346
+ ]
347
+ }
348
+ ),
323
349
  /* @__PURE__ */ jsx(Layer, { name: "top" })
324
350
  ]
325
351
  }
326
352
  );
327
353
  }
328
354
  );
355
+ DiagramKonva.displayName = "DiagramKonva";
356
+ export const Diagram = /* @__PURE__ */ forwardRef((props, ref) => /* @__PURE__ */ jsx(DiagramStateProvider, { children: /* @__PURE__ */ jsx(DiagramKonva, { ...props, ref }) }));
329
357
  Diagram.displayName = "Diagram";
@@ -102,23 +102,26 @@ const EdgeShape = memo(({ animate, edge, ctrl, theme, isHovered, onEdgeClick })
102
102
  AnimatedGroup,
103
103
  {
104
104
  opacity: ctrl.springs.opacity,
105
- onPointerClick: (e) => {
106
- if (!onEdgeClick || DiagramGesture.isDragging || e.evt.button !== 0) {
107
- return;
105
+ ...onEdgeClick && {
106
+ onPointerClick: (e) => {
107
+ if (DiagramGesture.isDragging || e.evt.button !== 0) {
108
+ return;
109
+ }
110
+ e.cancelBubble = true;
111
+ onEdgeClick(edge, e);
108
112
  }
109
- e.cancelBubble = true;
110
- onEdgeClick(edge, e);
111
113
  },
112
- onPointerEnter: (e) => {
113
- if (animate) {
114
+ ...animate && {
115
+ onPointerEnter: (e) => {
114
116
  setHoveredEdge(edge);
115
117
  mousePointer(e);
118
+ e.cancelBubble = true;
119
+ },
120
+ onPointerLeave: (e) => {
121
+ setHoveredEdge(null);
122
+ mouseDefault(e);
116
123
  }
117
124
  },
118
- onPointerLeave: (e) => {
119
- setHoveredEdge(null);
120
- mouseDefault(e);
121
- },
122
125
  children: /* @__PURE__ */ jsx(
123
126
  Edge,
124
127
  {
@@ -1,9 +1,11 @@
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 { useTransition } from "@react-spring/konva";
4
+ import { useToggle } from "@react-hookz/web/esm";
5
+ import { useSpring, useTransition } from "@react-spring/konva";
6
+ import { mix, toHex, lighten } from "khroma";
5
7
  import { memo, useRef } from "react";
6
- import { AnimatedGroup } from "../konva.js";
8
+ import { AnimatedCircle, AnimatedGroup, Rect } from "../konva.js";
7
9
  import { Portal } from "../konva-portal.js";
8
10
  import { ZoomInIcon } from "./icons/index.js";
9
11
  import { CylinderShape, MobileShape, PersonShape, QueueShape, RectangleShape } from "./shapes/index.js";
@@ -12,6 +14,7 @@ import { CompoundShape } from "./shapes/Compound.js";
12
14
  import { mouseDefault, mousePointer } from "./shapes/utils.js";
13
15
  import { isCompound, useNodeSpringsFn } from "./springs.js";
14
16
  import { DiagramGesture, useHoveredEdge, useHoveredNodeId, useSetHoveredNode } from "./state/index.js";
17
+ import { Group } from "react-konva";
15
18
  function nodeShape({ shape }) {
16
19
  switch (shape) {
17
20
  case "cylinder":
@@ -136,37 +139,29 @@ const NodeShape = memo(
136
139
  const isNavigatable = animate && !!node.navigateTo && !!onNodeClick;
137
140
  const Shape = nodeShape(node);
138
141
  const springs = ctrl.springs;
139
- let zoomInIconY;
140
- switch (node.shape) {
141
- case "browser":
142
- case "mobile":
143
- zoomInIconY = node.size.height - 20;
144
- break;
145
- default:
146
- zoomInIconY = node.size.height - 16;
147
- }
148
142
  return /* @__PURE__ */ jsx(Portal, { selector: ".top", enabled: isHovered && !_isCompound, children: /* @__PURE__ */ jsxs(
149
143
  AnimatedGroup,
150
144
  {
151
145
  name: node.id,
152
146
  visible: expired !== true,
153
- onPointerEnter: (e) => {
154
- setHoveredNode(node);
155
- if (isNavigatable) {
156
- mousePointer(e);
147
+ ...animate && {
148
+ onPointerEnter: (_) => {
149
+ setHoveredNode(node);
150
+ },
151
+ onPointerLeave: (e) => {
152
+ setHoveredNode(null);
153
+ mouseDefault(e);
157
154
  }
158
155
  },
159
- onPointerLeave: (e) => {
160
- setHoveredNode(null);
161
- mouseDefault(e);
162
- },
163
156
  ...onNodeClick && {
164
157
  onPointerClick: (e) => {
165
158
  if (DiagramGesture.isDragging || e.evt.button !== 0) {
166
159
  return;
167
160
  }
168
161
  e.cancelBubble = true;
169
- onNodeClick(node, e);
162
+ if (!isNavigatable) {
163
+ onNodeClick(node, e);
164
+ }
170
165
  }
171
166
  },
172
167
  x: springs.x,
@@ -186,14 +181,46 @@ const NodeShape = memo(
186
181
  node,
187
182
  theme,
188
183
  springs,
189
- labelOffsetX: isNavigatable ? -20 : 0
184
+ labelOffsetX: isNavigatable ? -22 : 0
190
185
  }
191
186
  ),
192
- isNavigatable && /* @__PURE__ */ jsx(ZoomInIcon, { opacity: 0.9, size: 20, x: 18, y: 21 })
187
+ isNavigatable && /* @__PURE__ */ jsxs(Fragment, { children: [
188
+ /* @__PURE__ */ jsx(
189
+ Rect,
190
+ {
191
+ x: 0,
192
+ y: 0,
193
+ width: node.size.width,
194
+ height: Math.min(node.size.height, 150),
195
+ perfectDrawEnabled: false
196
+ }
197
+ ),
198
+ /* @__PURE__ */ jsx(
199
+ CompoundZoomBtn,
200
+ {
201
+ animate,
202
+ node,
203
+ ctrl,
204
+ theme,
205
+ isHovered,
206
+ onNodeClick
207
+ }
208
+ )
209
+ ] })
193
210
  ] }),
194
211
  !_isCompound && /* @__PURE__ */ jsxs(Fragment, { children: [
195
212
  /* @__PURE__ */ jsx(Shape, { node, theme, springs, isHovered }),
196
- isNavigatable && /* @__PURE__ */ jsx(ZoomInIcon, { size: 16, x: node.size.width / 2, y: zoomInIconY })
213
+ isNavigatable && /* @__PURE__ */ jsx(
214
+ NodeZoomBtn,
215
+ {
216
+ animate,
217
+ node,
218
+ ctrl,
219
+ theme,
220
+ isHovered,
221
+ onNodeClick
222
+ }
223
+ )
197
224
  ] })
198
225
  ]
199
226
  }
@@ -202,3 +229,165 @@ const NodeShape = memo(
202
229
  isEqualSimple
203
230
  );
204
231
  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
+ };
@@ -21,7 +21,8 @@ export const ZoomInIcon = ({ fill = "#BABABA", opacity = 1, size = 20, x, y }) =
21
21
  height: originalSize,
22
22
  opacity,
23
23
  perfectDrawEnabled: false,
24
- globalCompositeOperation: "luminosity"
24
+ globalCompositeOperation: "luminosity",
25
+ listening: false
25
26
  }
26
27
  );
27
28
  };
@@ -36,7 +36,8 @@ export function CompoundShape({ node, theme, springs, labelOffsetX = 0 }) {
36
36
  wrap: "none",
37
37
  ellipsis: true,
38
38
  perfectDrawEnabled: false,
39
- globalCompositeOperation: "luminosity"
39
+ globalCompositeOperation: "luminosity",
40
+ listening: false
40
41
  },
41
42
  i
42
43
  ))
@@ -54,7 +54,7 @@ function EdgeLabelBg({
54
54
  fill: springs.labelBgColor,
55
55
  cornerRadius: 2,
56
56
  globalCompositeOperation: "lighten",
57
- hitStrokeWidth: 5
57
+ hitStrokeWidth: 20
58
58
  }
59
59
  );
60
60
  }
@@ -3,92 +3,67 @@ import { equals } from "rambdax";
3
3
  import { selectAtom } from "jotai/utils";
4
4
  const currentHoveredNodeAtom = atom(null);
5
5
  const nodeTimeoutAtom = atom(void 0);
6
+ const scheduleHoveredNode = (set, node = null, timeout = 175) => {
7
+ if (timeout <= 0) {
8
+ set(currentHoveredNodeAtom, node);
9
+ return;
10
+ }
11
+ set(
12
+ nodeTimeoutAtom,
13
+ setTimeout(() => {
14
+ set(currentHoveredNodeAtom, node);
15
+ }, timeout)
16
+ );
17
+ };
6
18
  export const hoveredNodeAtom = atom(
7
19
  (get) => get(currentHoveredNodeAtom),
8
20
  (get, set, update) => {
9
21
  clearTimeout(get(nodeTimeoutAtom));
10
- clearTimeout(get(edgeTimeoutAtom));
11
22
  const _prev = get(currentHoveredNodeAtom);
12
23
  const _next = typeof update === "function" ? update(_prev) : update;
13
24
  if (equals(_prev, _next)) {
14
25
  return false;
15
26
  }
16
- if (_next != null && _prev == null) {
17
- set(
18
- nodeTimeoutAtom,
19
- setTimeout(() => {
20
- set(currentHoveredNodeAtom, _next);
21
- set(currentHoveredEdgeAtom, null);
22
- }, 200)
23
- );
24
- return true;
25
- }
26
- if (_next != null && _prev != null) {
27
- set(
28
- nodeTimeoutAtom,
29
- setTimeout(() => {
30
- set(currentHoveredNodeAtom, _next);
31
- set(currentHoveredEdgeAtom, null);
32
- }, 150)
33
- );
34
- return true;
35
- }
36
- if (_next == null && _prev != null) {
37
- set(
38
- nodeTimeoutAtom,
39
- setTimeout(() => {
40
- set(currentHoveredNodeAtom, null);
41
- }, 150)
42
- );
43
- return true;
27
+ const timeout = !!_next && !!_prev ? 120 : 175;
28
+ if (_next != null) {
29
+ clearTimeout(get(edgeTimeoutAtom));
30
+ scheduleHoveredEdge(set, null, timeout);
44
31
  }
45
- set(currentHoveredNodeAtom, _next);
32
+ scheduleHoveredNode(set, _next, timeout);
46
33
  return true;
47
34
  }
48
35
  );
49
36
  export const hoveredNodeIdAtom = selectAtom(hoveredNodeAtom, (node) => node?.id ?? null);
50
37
  const currentHoveredEdgeAtom = atom(null);
51
38
  const edgeTimeoutAtom = atom(void 0);
39
+ const scheduleHoveredEdge = (set, edge = null, timeout = 175) => {
40
+ if (timeout <= 0) {
41
+ set(currentHoveredEdgeAtom, edge);
42
+ return;
43
+ }
44
+ set(
45
+ edgeTimeoutAtom,
46
+ setTimeout(() => {
47
+ set(currentHoveredEdgeAtom, edge);
48
+ }, timeout)
49
+ );
50
+ };
52
51
  export const hoveredEdgeAtom = atom(
53
52
  (get) => get(currentHoveredEdgeAtom),
54
53
  (get, set, update) => {
55
- clearTimeout(get(nodeTimeoutAtom));
56
54
  clearTimeout(get(edgeTimeoutAtom));
57
55
  const _prev = get(currentHoveredEdgeAtom);
58
56
  const _next = typeof update === "function" ? update(_prev) : update;
59
57
  if (equals(_prev, _next)) {
60
58
  return false;
61
59
  }
62
- if (_next != null && _prev == null) {
63
- set(
64
- edgeTimeoutAtom,
65
- setTimeout(() => {
66
- set(currentHoveredEdgeAtom, _next);
67
- set(currentHoveredNodeAtom, null);
68
- }, 400)
69
- );
70
- return true;
71
- }
72
- if (_next != null && _prev != null) {
73
- set(
74
- edgeTimeoutAtom,
75
- setTimeout(() => {
76
- set(currentHoveredEdgeAtom, _next);
77
- set(currentHoveredNodeAtom, null);
78
- }, 150)
79
- );
80
- return true;
81
- }
82
- if (_next == null && _prev != null) {
83
- set(
84
- edgeTimeoutAtom,
85
- setTimeout(() => {
86
- set(currentHoveredEdgeAtom, null);
87
- }, 150)
88
- );
89
- return true;
60
+ let timeout = 175;
61
+ if (_next != null) {
62
+ timeout = _prev != null ? 120 : 300;
63
+ clearTimeout(get(nodeTimeoutAtom));
64
+ scheduleHoveredNode(set, null, timeout);
90
65
  }
91
- set(currentHoveredEdgeAtom, null);
66
+ scheduleHoveredEdge(set, _next, timeout);
92
67
  return true;
93
68
  }
94
69
  );
@@ -0,0 +1,14 @@
1
+ import { is } from "rambdax";
2
+ export function mousePointer(e) {
3
+ const container = e.target.getStage()?.container();
4
+ if (container) {
5
+ container.style.cursor = "pointer";
6
+ }
7
+ }
8
+ export function mouseDefault(e) {
9
+ const container = e.target.getStage()?.container();
10
+ if (container) {
11
+ container.style.cursor = "";
12
+ }
13
+ }
14
+ export const isNumber = is(Number);
@@ -13,11 +13,10 @@ export function useDiagramApi() {
13
13
  get container() {
14
14
  return ref.current?.container ?? null;
15
15
  },
16
- resetStageZoom: (_immediate) => {
17
- nonNullable(ref.current, "not mounted, use ref").resetStageZoom(_immediate);
18
- },
19
- centerOnNode: (node) => nonNullable(ref.current, "not mounted, use ref").centerOnNode(node),
20
- centerAndFit: () => nonNullable(ref.current, "not mounted, use ref").centerAndFit()
16
+ resetStageZoom: (_immediate) => nonNullable(ref.current, "not mounted, use ref").resetStageZoom(_immediate),
17
+ centerOnNode: (node, opts) => nonNullable(ref.current, "not mounted, use ref").centerOnNode(node, opts),
18
+ centerOnRect: (rect, opts) => nonNullable(ref.current, "not mounted, use ref").centerOnRect(rect, opts),
19
+ centerAndFit: (opts) => nonNullable(ref.current, "not mounted, use ref").centerAndFit(opts)
21
20
  })
22
21
  );
23
22
  return [ref, api];
@@ -1,5 +1,5 @@
1
1
  import { useLayoutEffect, useRef, useState } from "react";
2
- import { useIsMounted } from "@react-hookz/web/esm/useIsMounted";
2
+ import { useIsMounted } from "@react-hookz/web/esm";
3
3
  import { invariant } from "@likec4/core";
4
4
  const imageElements = /* @__PURE__ */ new Map();
5
5
  export default function useImageLoader(url, crossOrigin, referrerpolicy) {