@likec4/diagram 1.4.0 → 1.5.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 (83) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +0 -1
  2. package/dist/LikeC4Diagram.props.d.ts +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.mjs +1398 -1114
  5. package/dist/state/DiagramContext.d.ts +86 -4159
  6. package/dist/state/diagramStore.d.ts +88 -4162
  7. package/dist/state/hooks.d.ts +4 -0
  8. package/dist/state/index.d.ts +1 -2
  9. package/dist/style.css +182 -156
  10. package/dist/ui/options/EdgeOptions.d.ts +19 -0
  11. package/dist/ui/options/NodeOptions.d.ts +2 -2
  12. package/dist/xyflow/EdgeMarkers.d.ts +11 -0
  13. package/dist/xyflow/XYFlowEvents.d.ts +8 -5
  14. package/dist/xyflow/const.d.ts +3 -3
  15. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  16. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -2
  17. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  18. package/dist/xyflow/edges/utils.d.ts +1 -0
  19. package/dist/xyflow/hooks/index.d.ts +3 -2
  20. package/dist/xyflow/hooks/useXYFlow.d.ts +13 -12
  21. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  22. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
  23. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  24. package/dist/xyflow/types.d.ts +2 -32
  25. package/package.json +19 -19
  26. package/src/LikeC4Diagram.css.ts +0 -8
  27. package/src/LikeC4Diagram.props.ts +2 -2
  28. package/src/LikeC4Diagram.tsx +14 -20
  29. package/src/globals.css.ts +10 -3
  30. package/src/index.ts +1 -1
  31. package/src/mantine.css.ts +2 -2
  32. package/src/state/DiagramContext.tsx +44 -13
  33. package/src/state/diagramStore.ts +64 -52
  34. package/src/state/hooks.ts +5 -0
  35. package/src/state/index.ts +1 -2
  36. package/src/ui/BackwardForwardPanel.css.ts +9 -9
  37. package/src/ui/BackwardForwardPanel.tsx +1 -1
  38. package/src/ui/DiagramTitlePanel.css.ts +7 -2
  39. package/src/ui/DiagramTitlePanel.tsx +5 -3
  40. package/src/ui/DynamicViewWalkthrough.tsx +6 -4
  41. package/src/ui/OptionsPanel.tsx +37 -27
  42. package/src/ui/options/EdgeOptions.tsx +157 -0
  43. package/src/ui/options/NodeOptions.tsx +37 -29
  44. package/src/xyflow/EdgeMarkers.tsx +148 -0
  45. package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
  46. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
  47. package/src/xyflow/SyncWithDiagram.tsx +13 -20
  48. package/src/xyflow/XYFlow.tsx +2 -4
  49. package/src/xyflow/XYFlowEvents.tsx +48 -33
  50. package/src/xyflow/XYFlowInner.tsx +1 -1
  51. package/src/xyflow/const.ts +3 -3
  52. package/src/xyflow/diagram-to-xyflow.ts +17 -41
  53. package/src/xyflow/edges/RelationshipEdge.tsx +207 -118
  54. package/src/xyflow/edges/edges.css.ts +36 -22
  55. package/src/xyflow/edges/utils.ts +10 -0
  56. package/src/xyflow/hooks/index.ts +4 -2
  57. package/src/xyflow/hooks/useLayoutConstraints.ts +3 -4
  58. package/src/xyflow/hooks/useXYFlow.ts +28 -1
  59. package/src/xyflow/nodes/compound/CompoundNode.css.ts +34 -24
  60. package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
  61. package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
  62. package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
  63. package/src/xyflow/nodes/element/ElementNode.tsx +64 -108
  64. package/src/xyflow/nodes/element/element.css.ts +30 -10
  65. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  66. package/src/xyflow/types.ts +2 -4
  67. package/src/xyflow/utils.ts +3 -6
  68. package/dist/KeepAspectRatioContainer.d.ts +0 -9
  69. package/dist/icons/Clipboard.d.ts +0 -3
  70. package/dist/icons/Link.d.ts +0 -3
  71. package/dist/icons/ZoomIn.d.ts +0 -3
  72. package/dist/icons/index.d.ts +0 -3
  73. package/dist/mantine/theme.d.ts +0 -3
  74. package/dist/state/WhenInitialized.d.ts +0 -3
  75. package/dist/ui/Camera.d.ts +0 -3
  76. package/src/KeepAspectRatioContainer.tsx +0 -36
  77. package/src/icons/Clipboard.tsx +0 -24
  78. package/src/icons/Link.tsx +0 -23
  79. package/src/icons/ZoomIn.tsx +0 -25
  80. package/src/icons/index.ts +0 -3
  81. package/src/mantine/theme.ts +0 -20
  82. package/src/state/WhenInitialized.tsx +0 -24
  83. package/src/ui/Camera.tsx +0 -174
@@ -1,5 +1,6 @@
1
1
  import { rem } from '@mantine/core'
2
2
  import { createVar, fallbackVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
3
+ import { calc } from '@vanilla-extract/css-utils'
3
4
  import { mantine } from '../../../mantine.css'
4
5
  import { vars } from '../../../theme.css'
5
6
 
@@ -27,6 +28,12 @@ export const container = style({
27
28
  }
28
29
  })
29
30
 
31
+ export const handleCenter = style({
32
+ top: '50%',
33
+ left: '50%',
34
+ visibility: 'hidden'
35
+ })
36
+
30
37
  export const containerAnimated = style({
31
38
  willChange: 'transform'
32
39
  })
@@ -56,8 +63,11 @@ const outlineColor = fallbackVar(
56
63
  vars.element.stroke
57
64
  )
58
65
 
66
+ const indicatorStroke = createVar('indicator-stroke')
67
+
59
68
  export const indicator = style({
60
- stroke: vars.element.loContrast,
69
+ stroke: indicatorStroke,
70
+ fill: 'none',
61
71
  transformOrigin: 'center center',
62
72
  strokeWidth: 6,
63
73
  animationDuration: '800ms',
@@ -65,6 +75,9 @@ export const indicator = style({
65
75
  animationDirection: 'alternate',
66
76
  opacity: 0.6,
67
77
  visibility: 'hidden',
78
+ vars: {
79
+ [indicatorStroke]: vars.element.loContrast
80
+ },
68
81
  selectors: {
69
82
  ':where(.react-flow__node.selected) &': {
70
83
  visibility: 'visible',
@@ -78,6 +91,11 @@ export const indicator = style({
78
91
  ':where([data-likec4-shape="queue"], [data-likec4-shape="cylinder"], [data-likec4-shape="storage"]) &': {
79
92
  strokeWidth: 10
80
93
  },
94
+ [`:where([data-mantine-color-scheme='light']) &`]: {
95
+ vars: {
96
+ [indicatorStroke]: `color-mix(in srgb, ${vars.element.fill} 60%, ${vars.element.loContrast})`
97
+ }
98
+ },
81
99
  [`${dimmed} &`]: {
82
100
  visibility: 'hidden',
83
101
  display: 'none'
@@ -104,7 +122,7 @@ export const title = style({
104
122
  fontStyle: 'normal',
105
123
  textAlign: 'center',
106
124
  fontWeight: 500,
107
- fontSize: rem(19),
125
+ fontSize: rem(19.5),
108
126
  lineHeight: 1.2,
109
127
  textWrap: 'balance',
110
128
  color: vars.element.hiContrast
@@ -175,34 +193,34 @@ export const element = style({
175
193
  }
176
194
  })
177
195
 
178
- export const iconMaxH = createVar('max-h')
196
+ export const iconOffsetY = createVar('offset-y')
197
+
198
+ const iconMaxH = createVar('max-h')
179
199
 
180
200
  export const elementIcon = style({
181
201
  flex: `0 1 ${iconMaxH}`,
182
202
  maxHeight: iconMaxH,
183
- marginTop: -8,
184
203
  position: 'relative',
185
204
  userSelect: 'none',
186
205
  pointerEvents: 'none',
187
206
  overflow: 'visible',
188
207
  mixBlendMode: 'hard-light',
189
208
  'vars': {
190
- [iconMaxH]: '30px'
209
+ [iconMaxH]: calc(iconOffsetY).subtract('12px').toString()
191
210
  }
192
211
  })
193
212
 
194
213
  globalStyle(`${elementIcon} img`, {
195
214
  position: 'absolute',
196
215
  left: 0,
197
- bottom: 0,
216
+ bottom: 4,
198
217
  width: '100%',
199
218
  height: 'auto',
200
219
  objectFit: 'contain',
201
- maxHeight: `calc(${iconMaxH} + 16px)`
220
+ maxHeight: iconOffsetY
202
221
  })
203
222
 
204
223
  const filterShadow = createVar('filter-shadow')
205
- const filterShadowHovered = createVar('filter-shadow-hovered')
206
224
 
207
225
  export const cssShapeSvg = style({
208
226
  top: '0px',
@@ -212,7 +230,7 @@ export const cssShapeSvg = style({
212
230
  fill: vars.element.fill,
213
231
  stroke: vars.element.stroke,
214
232
  overflow: 'visible',
215
- filter: fallbackVar(filterShadowHovered, filterShadow),
233
+ filter: filterShadow,
216
234
  transition: 'filter 300ms ease-out',
217
235
  transitionDelay: '0ms',
218
236
  zIndex: -1,
@@ -225,7 +243,9 @@ export const cssShapeSvg = style({
225
243
  },
226
244
  selectors: {
227
245
  [`:where(.react-flow__node.selected, .react-flow__node:focus-visible, ${container}:focus-visible) &`]: {
228
- filter: 'none'
246
+ vars: {
247
+ [filterShadow]: `none`
248
+ }
229
249
  }
230
250
  }
231
251
  })
@@ -1,7 +1,7 @@
1
1
  import { ActionIcon } from '@mantine/core'
2
2
  import { IconZoomScan } from '@tabler/icons-react'
3
3
  import clsx from 'clsx'
4
- import { useDiagramStoreApi } from '../../../state'
4
+ import { useDiagramStoreApi } from '../../../state/hooks'
5
5
 
6
6
  export type NavigateToBtnProps = {
7
7
  xynodeId: string
@@ -1,4 +1,4 @@
1
- import type { BBox, DiagramEdge, DiagramNode, Fqn, Point, ViewID, XYPosition } from '@likec4/core'
1
+ import type { BBox, DiagramEdge, DiagramNode, Fqn, XYPosition } from '@likec4/core'
2
2
  import type {
3
3
  Edge,
4
4
  EdgeMouseHandler,
@@ -13,7 +13,7 @@ import type {
13
13
  import { isNode } from '@xyflow/react'
14
14
  import type { MouseEvent as ReactMouseEvent } from 'react'
15
15
  import { isTruthy } from 'remeda'
16
- import type { SetReadonly, SetRequired, Simplify } from 'type-fest'
16
+ import type { SetReadonly, SetRequired } from 'type-fest'
17
17
 
18
18
  export type ElementNodeData = {
19
19
  fqn: Fqn
@@ -61,8 +61,6 @@ export type RelationshipData = {
61
61
  // viewId: ViewID
62
62
  // if set - edge was changed by user
63
63
  controlPoints: XYPosition[] | null
64
- headPoint: Point | null
65
- tailPoint: Point | null
66
64
  type: 'bezier' | 'poly'
67
65
  // if dynamic view
68
66
  stepNum: number | null
@@ -1,5 +1,5 @@
1
1
  import { hasAtLeast, invariant } from '@likec4/core'
2
- import type { DiagramEdge, NonEmptyArray, Point } from '@likec4/core/types'
2
+ import type { DiagramEdge, Point } from '@likec4/core/types'
3
3
  import type { XYPosition } from '@xyflow/react'
4
4
  import { Bezier } from 'bezier-js'
5
5
  import { isArray } from 'remeda'
@@ -33,17 +33,14 @@ export function bezierControlPoints(diagramEdge: DiagramEdge) {
33
33
  inflections.forEach(t => {
34
34
  const { x, y } = bezier.get(t)
35
35
  handles.push({
36
- x: toDomPrecision(x),
37
- y: toDomPrecision(y)
36
+ x: Math.round(x),
37
+ y: Math.round(y)
38
38
  })
39
39
  })
40
40
  bezierPoints = rest
41
41
  start = end
42
42
  }
43
43
  invariant(bezierPoints.length === 0, 'all points should be consumed')
44
- if (diagramEdge.dir === 'back' && handles.length > 0) {
45
- handles.reverse()
46
- }
47
44
 
48
45
  return handles
49
46
  }
@@ -1,9 +0,0 @@
1
- import { HTMLAttributes } from 'react';
2
-
3
- type KeepAspectRatioContainerProps = HTMLAttributes<HTMLDivElement> & {
4
- enabled: boolean;
5
- width: number;
6
- height: number;
7
- };
8
- export declare function KeepAspectRatioContainer({ children, width, height, enabled, className, style, ...rest }: KeepAspectRatioContainerProps): import("react/jsx-runtime").JSX.Element;
9
- export {};
@@ -1,3 +0,0 @@
1
- import { SVGProps } from 'react';
2
-
3
- export declare const ClipboardCopy: import('react').ForwardRefExoticComponent<Omit<SVGProps<SVGSVGElement>, "ref"> & import('react').RefAttributes<SVGSVGElement>>;
@@ -1,3 +0,0 @@
1
- import { SVGProps } from 'react';
2
-
3
- export declare const Link: import('react').ForwardRefExoticComponent<Omit<SVGProps<SVGSVGElement>, "ref"> & import('react').RefAttributes<SVGSVGElement>>;
@@ -1,3 +0,0 @@
1
- import { SVGProps } from 'react';
2
-
3
- export declare const ZoomIn: import('react').ForwardRefExoticComponent<Omit<SVGProps<SVGSVGElement>, "ref"> & import('react').RefAttributes<SVGSVGElement>>;
@@ -1,3 +0,0 @@
1
- export * from './Clipboard';
2
- export * from './Link';
3
- export * from './ZoomIn';
@@ -1,3 +0,0 @@
1
- import { MantineTheme } from '@mantine/core';
2
-
3
- export declare const theme: MantineTheme;
@@ -1,3 +0,0 @@
1
- import { PropsWithChildren } from 'react';
2
-
3
- export declare function WhenInitialized({ children }: PropsWithChildren): import("react/jsx-runtime").JSX.Element | null;
@@ -1,3 +0,0 @@
1
- /// <reference types="react" />
2
- declare const CameraMemo: import('react').NamedExoticComponent<object>;
3
- export default CameraMemo;
@@ -1,36 +0,0 @@
1
- import { Box } from '@mantine/core'
2
- import clsx from 'clsx'
3
- import { type HTMLAttributes, useId } from 'react'
4
- import * as css from './LikeC4Diagram.css'
5
-
6
- type KeepAspectRatioContainerProps = HTMLAttributes<HTMLDivElement> & {
7
- // Whether to keep the aspect ratio
8
- enabled: boolean
9
- width: number
10
- height: number
11
- }
12
-
13
- export function KeepAspectRatioContainer({
14
- children,
15
- width,
16
- height,
17
- enabled,
18
- className,
19
- style,
20
- ...rest
21
- }: KeepAspectRatioContainerProps) {
22
- return (
23
- <Box
24
- {...rest}
25
- className={clsx(css.keepAspectRatioContainer, className)}
26
- style={enabled
27
- ? {
28
- ...style,
29
- aspectRatio: `${Math.ceil(width)}/${Math.ceil(height)}`,
30
- maxHeight: `var(--likec4-view-max-height, ${Math.ceil(height)}px)`
31
- }
32
- : style}>
33
- {children}
34
- </Box>
35
- )
36
- }
@@ -1,24 +0,0 @@
1
- import clsx from 'clsx'
2
- import { forwardRef, type SVGProps } from 'react'
3
-
4
- export const ClipboardCopy = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (
5
- <svg
6
- ref={ref}
7
- xmlns="http://www.w3.org/2000/svg"
8
- className={clsx('icon icon-tabler icon-tabler-zoom-in', className)}
9
- width="24"
10
- height="24"
11
- viewBox="0 0 24 24"
12
- strokeWidth="1.5"
13
- stroke="currentColor"
14
- fill="none"
15
- strokeLinecap="round"
16
- strokeLinejoin="round"
17
- {...props}
18
- >
19
- <path stroke="none" d="M0 0h24v24H0z" fill="none" />
20
- <path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h3m9 -9v-5a2 2 0 0 0 -2 -2h-2" />
21
- <path d="M13 17v-1a1 1 0 0 1 1 -1h1m3 0h1a1 1 0 0 1 1 1v1m0 3v1a1 1 0 0 1 -1 1h-1m-3 0h-1a1 1 0 0 1 -1 -1v-1" />
22
- <path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" />
23
- </svg>
24
- ))
@@ -1,23 +0,0 @@
1
- import clsx from 'clsx'
2
- import { forwardRef, type SVGProps } from 'react'
3
-
4
- export const Link = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (
5
- <svg
6
- ref={ref}
7
- className={clsx('icon icon-tabler', className)}
8
- stroke="currentColor"
9
- fill="currentColor"
10
- strokeWidth="0"
11
- viewBox="0 0 20 20"
12
- height="20"
13
- width="20"
14
- xmlns="http://www.w3.org/2000/svg"
15
- {...props}
16
- >
17
- <path
18
- fillRule="evenodd"
19
- d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z"
20
- clipRule="evenodd">
21
- </path>
22
- </svg>
23
- ))
@@ -1,25 +0,0 @@
1
- import clsx from 'clsx'
2
- import { forwardRef, type SVGProps } from 'react'
3
-
4
- export const ZoomIn = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(({ className, ...props }, ref) => (
5
- <svg
6
- ref={ref}
7
- xmlns="http://www.w3.org/2000/svg"
8
- className={clsx('icon icon-tabler icon-tabler-zoom-in', className)}
9
- width="24"
10
- height="24"
11
- viewBox="0 0 24 24"
12
- strokeWidth="1.5"
13
- stroke="currentColor"
14
- fill="none"
15
- strokeLinecap="round"
16
- strokeLinejoin="round"
17
- {...props}
18
- >
19
- <path stroke="none" d="M0 0h24v24H0z" fill="none" />
20
- <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
21
- <path d="M7 10l6 0" />
22
- <path d="M10 7l0 6" />
23
- <path d="M21 21l-6 -6" />
24
- </svg>
25
- ))
@@ -1,3 +0,0 @@
1
- export * from './Clipboard'
2
- export * from './Link'
3
- export * from './ZoomIn'
@@ -1,20 +0,0 @@
1
- import { createTheme, type MantineTheme } from '@mantine/core'
2
-
3
- export const theme = createTheme({
4
- primaryColor: 'indigo',
5
- cursorType: 'pointer',
6
- defaultRadius: 'sm',
7
- respectReducedMotion: true,
8
- headings: {
9
- fontWeight: '500',
10
- sizes: {
11
- h1: {
12
- // fontSize: '2rem',
13
- fontWeight: '600'
14
- },
15
- h2: {
16
- // fontSize: '1.85rem',
17
- }
18
- }
19
- }
20
- }) as MantineTheme
@@ -1,24 +0,0 @@
1
- import { type PropsWithChildren, useEffect, useState } from 'react'
2
- import { useDiagramStoreApi } from './useDiagramStore'
3
-
4
- export function WhenInitialized({ children }: PropsWithChildren) {
5
- const diagramApi = useDiagramStoreApi()
6
-
7
- const [isInitialized, setIsInitialized] = useState(diagramApi.getState().initialized)
8
-
9
- useEffect(() => {
10
- if (isInitialized) {
11
- return
12
- }
13
- return diagramApi.subscribe(
14
- s => s.initialized,
15
- setIsInitialized
16
- )
17
- }, [isInitialized])
18
-
19
- if (!isInitialized) {
20
- return null
21
- }
22
-
23
- return <>{children}</>
24
- }
package/src/ui/Camera.tsx DELETED
@@ -1,174 +0,0 @@
1
- import { shallowEqual } from '@mantine/hooks'
2
- import { useDebouncedEffect, useIsMounted } from '@react-hookz/web'
3
- import {
4
- type ReactFlowState,
5
- useOnSelectionChange,
6
- useOnViewportChange,
7
- useStore,
8
- useUpdateNodeInternals,
9
- type Viewport
10
- } from '@xyflow/react'
11
- import { memo, useEffect, useRef, useState } from 'react'
12
- import { uniq } from 'remeda'
13
- import { useXYFlow } from '../xyflow/hooks'
14
- import type { XYFlowNode } from '../xyflow/types'
15
- import { distance } from '../xyflow/utils'
16
-
17
- const selector = (state: ReactFlowState) => {
18
- return {
19
- isUserSelectionActive: state.userSelectionActive,
20
- isDragging: state.paneDragging || state.nodes.some(n => n.dragging ?? false)
21
- }
22
- }
23
-
24
- const CameraMemo = /* @__PURE__ */ memo(function Camera() {
25
- const isMounted = useIsMounted()
26
- // const isUserSelectionActive = useStore(selectUserSelectionActive)
27
- const {
28
- isUserSelectionActive,
29
- isDragging
30
- } = useStore(selector, shallowEqual)
31
- const reactflow = useXYFlow()
32
- const updateNd = useUpdateNodeInternals()
33
-
34
- const padding = 0 // editor.fitViewPadding
35
- const viewId = '' // editor.viewId
36
- const fitOnSelect: boolean = false // editor.fitOnSelect
37
-
38
- const previousViewport = useRef<Viewport | null>(null)
39
- const isZoomPendingRef = useRef(false)
40
-
41
- const viewportChangeStart = useRef<Viewport | null>(null)
42
- const prevViewId = useRef(viewId)
43
- const isReady = reactflow.viewportInitialized
44
- const selectedNodesRef = useRef([] as string[])
45
-
46
- const [selectedNodes, setSelectedNodes] = useState([] as XYFlowNode[])
47
-
48
- // WORKAROUND
49
-
50
- useOnViewportChange({
51
- onStart: (viewport) => {
52
- viewportChangeStart.current = { ...viewport }
53
- },
54
- onEnd: (end) => {
55
- if (!previousViewport.current) {
56
- viewportChangeStart.current = null
57
- }
58
- if (!viewportChangeStart.current) {
59
- return
60
- }
61
- const start = {
62
- ...viewportChangeStart.current
63
- }
64
- const d = distance(start, end)
65
- if (d > 15) {
66
- previousViewport.current = null
67
- }
68
- viewportChangeStart.current = null
69
- }
70
- })
71
-
72
- const fixUseOnViewportChange = () => {
73
- // WORKAROUND:
74
- // react-flow triggers useOnViewportChange
75
- // We reset the viewportChangeStart, before useOnViewportChange.onEnd called
76
- setTimeout(() => {
77
- viewportChangeStart.current = null
78
- }, 50)
79
- }
80
-
81
- useOnSelectionChange({
82
- onChange: ({ nodes, edges }) => {
83
- if (!isMounted() || fitOnSelect === false) {
84
- return
85
- }
86
- if (nodes.length === 0 && edges.length === 0) {
87
- setSelectedNodes([])
88
- return
89
- }
90
- const selected = new Set([
91
- ...nodes.map(({ id }) => reactflow.getNode(id)),
92
- ...edges.flatMap((edge) => [
93
- reactflow.getNode(edge.source),
94
- reactflow.getNode(edge.target)
95
- ])
96
- ].filter(Boolean))
97
- setSelectedNodes([...selected])
98
- }
99
- })
100
-
101
- const selectedNodesHash = selectedNodes.map((node) => node.id).sort().join('\n')
102
- useEffect(() => {
103
- isZoomPendingRef.current = selectedNodesHash !== ''
104
- }, [selectedNodesHash])
105
-
106
- useDebouncedEffect(
107
- () => {
108
- if (isUserSelectionActive || isDragging || !isReady) {
109
- return
110
- }
111
- if (selectedNodes.length === 0) {
112
- if (previousViewport.current) {
113
- console.log(`Camera: revert viewport`)
114
- reactflow.setViewport(previousViewport.current, {
115
- duration: 300
116
- })
117
- previousViewport.current = null
118
- }
119
- return
120
- }
121
- if (!isZoomPendingRef.current) {
122
- return
123
- }
124
- isZoomPendingRef.current = false
125
- previousViewport.current ??= { ...reactflow.getViewport() }
126
- const zoom = previousViewport.current.zoom
127
- reactflow.fitView({
128
- duration: 350,
129
- maxZoom: Math.max(1, zoom),
130
- padding,
131
- nodes: selectedNodes
132
- })
133
- fixUseOnViewportChange()
134
- },
135
- [selectedNodesHash, isReady, isDragging, isUserSelectionActive],
136
- previousViewport.current ? 100 : 1500
137
- // previousViewport.current ? 200 : 2500
138
- )
139
-
140
- useEffect(() => {
141
- const ids = selectedNodes.map((node) => node.id)
142
- for (const id of uniq([...selectedNodesRef.current, ...ids])) {
143
- try {
144
- updateNd(id)
145
- } catch (e) {
146
- console.warn(`Camera: failed useUpdateNodeInternals`, e)
147
- }
148
- }
149
- selectedNodesRef.current = ids
150
- }, [selectedNodesHash, updateNd])
151
-
152
- useDebouncedEffect(
153
- () => {
154
- if (!isReady || prevViewId.current === viewId) {
155
- return
156
- }
157
- const zoom = previousViewport.current?.zoom ?? reactflow.getZoom()
158
- reactflow.fitView({
159
- duration: 400,
160
- maxZoom: Math.max(1, zoom),
161
- padding
162
- })
163
- previousViewport.current = null
164
- prevViewId.current = viewId
165
- },
166
- [isReady, viewId],
167
- 50,
168
- 500
169
- )
170
-
171
- return null
172
- })
173
-
174
- export default CameraMemo