@likec4/diagram 1.14.0 → 1.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/diagram",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -23,7 +23,7 @@
23
23
  "exports": {
24
24
  ".": "./src/index.ts"
25
25
  },
26
- "packageManager": "yarn@4.5.0",
26
+ "packageManager": "yarn@4.5.1",
27
27
  "scripts": {
28
28
  "typecheck": "tsc --noEmit",
29
29
  "lint": "run -T eslint src/ --fix",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@dagrejs/dagre": "^1.1.4",
34
- "@likec4/core": "1.14.0",
35
- "@mantine/vanilla-extract": "7.13.2",
34
+ "@likec4/core": "1.15.0",
35
+ "@mantine/vanilla-extract": "7.13.4",
36
36
  "@react-hookz/web": "^24.0.4",
37
37
  "@tabler/icons-react": "3.17.0",
38
38
  "@vanilla-extract/css": "^1.16.0",
@@ -58,9 +58,9 @@
58
58
  "react-dom": "^18.3.1"
59
59
  },
60
60
  "devDependencies": {
61
- "@likec4/tsconfig": "1.14.0",
62
- "@mantine/core": "7.13.2",
63
- "@mantine/hooks": "7.13.2",
61
+ "@likec4/tsconfig": "1.15.0",
62
+ "@mantine/core": "7.13.4",
63
+ "@mantine/hooks": "7.13.4",
64
64
  "@types/bezier-js": "^4.1.3",
65
65
  "@types/d3-shape": "^3.1.6",
66
66
  "@types/react": "^18.3.11",
@@ -1,12 +1,14 @@
1
1
  import { type ThemeColorValues } from '@likec4/core'
2
2
  import { useMantineStyleNonce } from '@mantine/core'
3
+ import { deepEqual } from 'fast-equals'
4
+ import { memo } from 'react'
3
5
  import { entries } from 'remeda'
4
6
  import type { LikeC4CustomColorsProperties } from './LikeC4CustomColors.props'
5
7
  import { vars } from './theme-vars'
6
8
 
7
9
  type CSSVarFunction = `var(--${string})` | `var(--${string}, ${string | number} )`
8
10
 
9
- export function LikeC4CustomColors({ customColors }: LikeC4CustomColorsProperties) {
11
+ export const LikeC4CustomColors = memo<LikeC4CustomColorsProperties>(({ customColors }) => {
10
12
  function toStyle(name: String, colorValues: ThemeColorValues): String {
11
13
  const rules = new Array<String>(
12
14
  ...entries(colorValues.elements).map(([key, value]) => `${stripCssVarReference(vars.element[key])}: ${value};`),
@@ -34,7 +36,8 @@ export function LikeC4CustomColors({ customColors }: LikeC4CustomColorsPropertie
34
36
 
35
37
  return (
36
38
  <>
37
- <style type="text/css" dangerouslySetInnerHTML={{ __html: styles }} {...(nonce && { nonce })} />
39
+ <style type="text/css" dangerouslySetInnerHTML={{ __html: styles }} nonce={nonce} />
38
40
  </>
39
41
  )
40
- }
42
+ }, deepEqual)
43
+ LikeC4CustomColors.displayName = 'LikeC4CustomColors'
@@ -67,10 +67,10 @@ export function LikeC4Diagram({
67
67
  initialWidth: initialWidth ?? view.bounds.width,
68
68
  initialHeight: initialHeight ?? view.bounds.height
69
69
  })
70
- if (readonly !== true && !!where) {
71
- console.warn('where filter is only supported in readonly mode')
72
- }
73
70
  useEffect(() => {
71
+ if (readonly !== true && where != null) {
72
+ console.warn('Ignore filter, supported in readonly mode only\n', { where })
73
+ }
74
74
  if (hasLikec4model) {
75
75
  return
76
76
  }
@@ -80,7 +80,7 @@ export function LikeC4Diagram({
80
80
  if (enableRelationshipsBrowser) {
81
81
  console.warn('Invalid enableRelationshipsBrowser=true, requires LikeC4ModelProvider')
82
82
  }
83
- }, [])
83
+ })
84
84
 
85
85
  return (
86
86
  <EnsureMantine>
@@ -111,7 +111,8 @@ export function LikeC4Diagram({
111
111
  enableDynamicViewWalkthrough={enableDynamicViewWalkthrough}
112
112
  enableFocusMode={enableFocusMode}
113
113
  enableRelationshipsBrowser={enableRelationshipsBrowser && hasLikec4model}
114
- whereFilter={readonly !== true ? (where ?? null) : null}
114
+ // Apply where filter only in readonly mode
115
+ whereFilter={readonly ? (where ?? null) : null}
115
116
  renderIcon={renderIcon ?? null}
116
117
  onCanvasClick={onCanvasClick ?? null}
117
118
  onCanvasContextMenu={onCanvasContextMenu ?? null}
@@ -346,7 +346,7 @@ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function Relationshi
346
346
  </Button>
347
347
  <Space w={2} />
348
348
  <Group gap={'xs'} pos={'relative'} wrap="nowrap" flex={'1 0 auto'}>
349
- <Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap' }}>Relationships of</Box>
349
+ <Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap', userSelect: 'none' }}>Relationships of</Box>
350
350
  <Box flex={'1 0 auto'}>
351
351
  <SelectElement
352
352
  scope={scope}
@@ -63,7 +63,7 @@ const Sizes = {
63
63
  // 0 means no spacer
64
64
  spacerHeight: 0,
65
65
 
66
- compoundLabelHeight: 5
66
+ compoundLabelHeight: 1
67
67
  }
68
68
 
69
69
  export const ZIndexes = {
@@ -91,12 +91,11 @@ function createGraph() {
91
91
  columns.reduce((prev, column) => {
92
92
  const c = graphColumn(column)
93
93
  g.setNode(c.id, {})
94
- g.setNode(c.anchor, { width: 40, height: 5 })
94
+ g.setNode(c.anchor, { width: 40, height: 2 })
95
95
  g.setParent(c.anchor, c.id)
96
96
  if (prev) {
97
97
  g.setEdge(prev, c.anchor, {
98
- width: 0,
99
- weight: 10
98
+ width: 0
100
99
  })
101
100
  }
102
101
  return c.anchor
@@ -258,9 +257,6 @@ function createNode(
258
257
 
259
258
  g.setNode(k.id, sized())
260
259
  if (xynode.type === 'compound') {
261
- g.setNode(k.id, {
262
- padding: 40
263
- })
264
260
  g.setNode(k.port, {
265
261
  width: Sizes.nodeWidth - Sizes.dagre.ranksep,
266
262
  height: Sizes.compoundLabelHeight
@@ -3,6 +3,7 @@ import { useMergedRef } from '@mantine/hooks'
3
3
  import { deepEqual, shallowEqual } from 'fast-equals'
4
4
  import { useAnimate } from 'framer-motion'
5
5
  import { createContext, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
6
+ import { clone } from 'remeda'
6
7
  import { useUpdateEffect } from '../hooks/useUpdateEffect'
7
8
  import { useXYFlow, useXYStoreApi } from '../hooks/useXYFlow'
8
9
  import { createDiagramStore, type DiagramInitialState, type DiagramZustandStore } from './diagramStore'
@@ -39,7 +40,7 @@ export function DiagramContextProvider({
39
40
  xyflow,
40
41
  view,
41
42
  getContainer,
42
- whereFilter: structuredClone(whereFilter),
43
+ whereFilter: clone(whereFilter),
43
44
  ...props
44
45
  })
45
46
  }
@@ -59,7 +60,7 @@ export function DiagramContextProvider({
59
60
  const current = store.current
60
61
  invariant(current, 'DiagramContext.store.current is not defined')
61
62
  if (!deepEqual(whereFilter, current.getState().whereFilter)) {
62
- current.setState({ whereFilter: structuredClone(whereFilter) }, false, 'update where filter')
63
+ current.setState({ whereFilter: clone(whereFilter) }, false, 'update where filter')
63
64
  }
64
65
  current.getState().updateView(view)
65
66
  },
@@ -1,6 +1,7 @@
1
- import type { DiagramEdge, DiagramNode, DiagramView, Fqn } from '@likec4/core'
1
+ import { type DiagramEdge, DiagramNode, type DiagramView, ElementKind, type Fqn } from '@likec4/core'
2
2
  import { nonNullable, whereOperatorAsPredicate } from '@likec4/core'
3
3
  import { hasAtLeast } from 'remeda'
4
+ import type { UnionToIntersection } from 'type-fest'
4
5
  import type { WhereOperator } from '../LikeC4Diagram.props'
5
6
  import { ZIndexes } from '../xyflow/const'
6
7
  import type { XYFlowEdge, XYFlowNode } from '../xyflow/types'
@@ -55,7 +56,7 @@ export function diagramViewToXYFlowData(
55
56
  let next: typeof traverse[0] | undefined
56
57
  while ((next = traverse.shift())) {
57
58
  const { node, parent } = next
58
- const isCompound = hasAtLeast(node.children, 1)
59
+ const isCompound = hasAtLeast(node.children, 1) || node.kind == ElementKind.Group
59
60
  if (isCompound) {
60
61
  traverse.push(...node.children.map(child => ({ node: nodeById(child), parent: node })))
61
62
  }
@@ -69,35 +70,45 @@ export function diagramViewToXYFlowData(
69
70
  position.y -= parent.position[1]
70
71
  }
71
72
 
72
- // const outEdges = node.outEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
73
- // const inEdges = node.inEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
74
-
75
73
  const id = ns + node.id
76
- const draggable = opts.draggable
77
- xynodes.push({
74
+
75
+ const base: Omit<UnionToIntersection<XYFlowNode>, 'data'> = {
78
76
  id,
79
- type: isCompound ? 'compound' : 'element',
80
- data: {
81
- fqn: node.id,
82
- element: node
83
- },
84
- draggable,
85
- selectable: opts.selectable,
77
+ draggable: opts.draggable,
78
+ selectable: opts.selectable && node.kind !== ElementKind.Group,
86
79
  focusable: opts.selectable && !isCompound,
87
80
  deletable: false,
88
81
  position,
89
82
  zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
90
83
  width: node.width,
91
84
  height: node.height,
92
- hidden: !visiblePredicate(node),
93
- // parentId: parent ? ns + parent.id : null,
85
+ hidden: node.kind !== ElementKind.Group && !visiblePredicate(node),
94
86
  ...(parent && {
95
87
  parentId: ns + parent.id
96
- }),
97
- ...(isCompound && {
98
- dragHandle: '.likec4-compound-title'
99
88
  })
100
- })
89
+ }
90
+
91
+ xynodes.push(
92
+ isCompound
93
+ ? {
94
+ ...base,
95
+ type: 'compound',
96
+ data: {
97
+ fqn: node.id,
98
+ isViewGroup: node.kind === ElementKind.Group,
99
+ element: node
100
+ },
101
+ dragHandle: '.likec4-compound-title'
102
+ }
103
+ : {
104
+ ...base,
105
+ type: 'element',
106
+ data: {
107
+ fqn: node.id,
108
+ element: node
109
+ }
110
+ }
111
+ )
101
112
  }
102
113
 
103
114
  for (const edge of view.edges) {
@@ -395,7 +395,7 @@ export function createDiagramStore(props: DiagramInitialState) {
395
395
  selectable: nodesSelectable
396
396
  })
397
397
 
398
- update.xynodes = update.xynodes.map(update => {
398
+ update.xynodes = update.xynodes.map((update) => {
399
399
  const existing = xynodes.find(n => n.id === update.id)
400
400
  if (
401
401
  existing
@@ -403,18 +403,18 @@ export function createDiagramStore(props: DiagramInitialState) {
403
403
  && eq(existing.parentId ?? null, update.parentId ?? null)
404
404
  ) {
405
405
  if (
406
- existing.hidden === update.hidden
407
- && existing.width === update.width
406
+ existing.width === update.width
408
407
  && existing.height === update.height
408
+ && eq(existing.hidden ?? false, update.hidden ?? false)
409
409
  && eq(existing.position, update.position)
410
- && eq(existing.data.element, update.data.element)
410
+ && eq(existing.data, update.data)
411
411
  ) {
412
412
  return existing
413
413
  }
414
414
  return {
415
415
  ...existing,
416
416
  ...update
417
- }
417
+ } as XYFlowNode
418
418
  }
419
419
  return update
420
420
  })
@@ -425,7 +425,7 @@ export function createDiagramStore(props: DiagramInitialState) {
425
425
  const existing = xyedges.find(n => n.id === update.id)
426
426
  if (existing) {
427
427
  if (
428
- existing.hidden === update.hidden
428
+ eq(existing.hidden ?? false, update.hidden ?? false)
429
429
  && eq(existing.data.label, update.data.label)
430
430
  && eq(existing.data.controlPoints, update.data.controlPoints)
431
431
  && eq(existing.data.edge, update.data.edge)
@@ -1065,7 +1065,12 @@ export function createDiagramStore(props: DiagramInitialState) {
1065
1065
  const { nodeLookup, parentLookup } = xystore.getState()
1066
1066
 
1067
1067
  const selectedNodes = new Set(nodeLookup.values().filter(n => n.selected).map(n => n.id))
1068
- const nodesToAlign = new Set(selectedNodes.difference(new Set(parentLookup.keys())))
1068
+ const nodesToAlign = [...selectedNodes.difference(new Set(parentLookup.keys()))]
1069
+
1070
+ if (!hasAtLeast(nodesToAlign, 2)) {
1071
+ console.warn('At least 2 nodes must be selected to align')
1072
+ return
1073
+ }
1069
1074
  const constraints = createLayoutConstraints(xystore, nodesToAlign)
1070
1075
 
1071
1076
  let getEdgePosition: (nodes: InternalNode<XYFlowNode>[]) => number
@@ -137,7 +137,6 @@ const curve = d3line<XYPosition>()
137
137
  export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function RelationshipEdgeR({
138
138
  id,
139
139
  data,
140
- selected,
141
140
  sourceX,
142
141
  sourceY,
143
142
  targetX,
@@ -438,10 +437,17 @@ export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function Relationshi
438
437
  const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null
439
438
  const MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null
440
439
 
441
- let labelZIndex = 1 + (isHovered ? ZIndexes.Element : (edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge))
442
- if (isEdgePathEditable && selected) {
440
+ const edgeZIndex = edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge
441
+ let labelZIndex
442
+ if (isEdgePathEditable) {
443
443
  // Move label below ControlPoints, otherwise they don't capture events
444
- labelZIndex = (edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge) - 1
444
+ labelZIndex = edgeZIndex - 1
445
+ }
446
+ else if (isHovered) {
447
+ // Move above the elements
448
+ labelZIndex = ZIndexes.Element + 1
449
+ } else {
450
+ labelZIndex = edgeZIndex + 1
445
451
  }
446
452
 
447
453
  return (
@@ -122,6 +122,7 @@ export const title = style({
122
122
  paddingLeft: 14,
123
123
  paddingTop: 13,
124
124
  paddingBottom: 6,
125
+ minHeight: 30,
125
126
  mixBlendMode: 'screen',
126
127
  selectors: {
127
128
  [`:where(.react-flow__node.draggable) &`]: {
@@ -1,4 +1,4 @@
1
- import type { ThemeColor } from '@likec4/core'
1
+ import { type ThemeColor } from '@likec4/core'
2
2
  import { ActionIcon, Box, Text, Tooltip } from '@mantine/core'
3
3
  import { useDebouncedValue } from '@mantine/hooks'
4
4
  import { IconTransform, IconZoomScan } from '@tabler/icons-react'
@@ -127,14 +127,12 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
127
127
  selected = false,
128
128
  dragging = false,
129
129
  data: {
130
- element: {
131
- color,
132
- style,
133
- depth = 1,
134
- ...element
135
- }
130
+ isViewGroup,
131
+ element
136
132
  }
137
133
  }) {
134
+ const { depth, style, color } = element
135
+ const isNotViewGroup = !isViewGroup
138
136
  const opacity = clamp((style.opacity ?? 100) / 100, {
139
137
  min: 0,
140
138
  max: 1
@@ -164,9 +162,11 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
164
162
  hasOnNavigateTo: !!s.onNavigateTo,
165
163
  enableRelationshipsMode: s.enableRelationshipsBrowser
166
164
  }))
167
- const isNavigable = !!element.navigateTo && hasOnNavigateTo
165
+ // If this is a view group, we don't want to show the navigate button
166
+ const isNavigable = isNotViewGroup && !!element.navigateTo && hasOnNavigateTo
167
+ const hasRelationshipsBrowser = isNotViewGroup && enableRelationshipsMode
168
168
 
169
- const _isToolbarVisible = isEditable && (isHovered || (import.meta.env.DEV && selected))
169
+ const _isToolbarVisible = isNotViewGroup && isEditable && (isHovered || (import.meta.env.DEV && selected))
170
170
  const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
171
171
 
172
172
  const [animateVariants, animateHandlers] = useFramerAnimateVariants()
@@ -263,7 +263,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
263
263
  component="div"
264
264
  className={clsx(
265
265
  css.title,
266
- (isNavigable || enableRelationshipsMode) && css.titleWithNavigation,
266
+ (isNavigable || hasRelationshipsBrowser) && css.titleWithNavigation,
267
267
  'likec4-compound-title'
268
268
  )}>
269
269
  {element.title}
@@ -286,7 +286,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
286
286
  <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
287
287
  </ActionIcon>
288
288
  )}
289
- {enableRelationshipsMode && (
289
+ {hasRelationshipsBrowser && (
290
290
  <Tooltip
291
291
  fz="xs"
292
292
  color="dark"
@@ -11,6 +11,7 @@ export type ElementXYFlowNode = Node<{
11
11
 
12
12
  export type CompoundXYFlowNode = Node<{
13
13
  fqn: Fqn
14
+ isViewGroup: boolean
14
15
  element: DiagramNode
15
16
  }, 'compound'>
16
17
 
@@ -1,8 +1,8 @@
1
- import { invariant, isAncestor, nonNullable } from '@likec4/core'
1
+ import { invariant, isAncestor, type NodeId, type NonEmptyArray, nonNullable } from '@likec4/core'
2
2
  import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyflow/react'
3
3
  import { getNodeDimensions } from '@xyflow/system'
4
4
  import { useMemo, useRef } from 'react'
5
- import { isNullish } from 'remeda'
5
+ import { filter, hasAtLeast, isNullish, map, pipe } from 'remeda'
6
6
  import { useDiagramStoreApi } from '../hooks'
7
7
  import { type XYStoreApi } from '../hooks/useXYFlow'
8
8
  import type { XYFlowNode } from './types'
@@ -99,11 +99,27 @@ type NodePositionUpdater = (
99
99
 
100
100
  export function createLayoutConstraints(
101
101
  xyflowApi: XYStoreApi,
102
- editingNodeIds: Set<string>
102
+ editingNodeIds: NonEmptyArray<string>
103
103
  ) {
104
104
  const { parentLookup, nodeLookup } = xyflowApi.getState()
105
105
  const rects = new Map<string, Leaf | Compound>()
106
106
 
107
+ const ancestorsOf = (nodeId: string) => {
108
+ const ancestors = [] as string[]
109
+ const xynode = nodeLookup.get(nodeId)
110
+ let parent = xynode?.parentId
111
+ let parentNode: InternalNode<XYFlowNode> | undefined
112
+ while (parent && (parentNode = nodeLookup.get(parent))) {
113
+ ancestors.push(parentNode.id as NodeId)
114
+ parent = parentNode.parentId
115
+ }
116
+ return ancestors
117
+ }
118
+
119
+ const ancestorsOfDraggingNodes = new Set(
120
+ editingNodeIds.flatMap(ancestorsOf)
121
+ )
122
+
107
123
  const traverse = new Array<{ xynode: InternalNode<XYFlowNode>; parent: Compound | null }>()
108
124
 
109
125
  for (const [, xynode] of nodeLookup) {
@@ -117,11 +133,11 @@ export function createLayoutConstraints(
117
133
 
118
134
  while (traverse.length > 0) {
119
135
  const { xynode, parent } = traverse.shift()!
120
- const isEditing = editingNodeIds.has(xynode.id)
136
+ const isEditing = editingNodeIds.includes(xynode.id)
121
137
 
122
138
  // Traverse children if the node is a compound, not dragging, and is an ancestor of the dragging node
123
139
  const shouldTraverse = !isEditing && xynode.type === 'compound'
124
- && editingNodeIds.values().some(x => isAncestor(xynode.id, x))
140
+ && ancestorsOfDraggingNodes.has(xynode.id)
125
141
 
126
142
  const rect = shouldTraverse ? new Compound(xynode, parent) : new Leaf(xynode, parent)
127
143
  rects.set(xynode.id, rect)
@@ -157,7 +173,6 @@ export function createLayoutConstraints(
157
173
  maxY: -Infinity
158
174
  })
159
175
 
160
-
161
176
  r.minX = childrenBB.minX - Rect.LeftPadding
162
177
  r.minY = childrenBB.minY - Rect.TopPadding
163
178
  r.maxX = childrenBB.maxX + Rect.RightPadding
@@ -200,11 +215,14 @@ export function createLayoutConstraints(
200
215
  animationFrameId ??= requestAnimationFrame(() => {
201
216
  animationFrameId = null
202
217
  updater(
203
- [
204
- ...editingNodeIds.values()
205
- .filter(id => rects.has(id) && nodeLookup.has(id))
206
- .map(id => ({ rect: nonNullable(rects.get(id)), node: nodeLookup.get(id)! }))
207
- ]
218
+ pipe(
219
+ editingNodeIds,
220
+ filter(id => rects.has(id) && nodeLookup.has(id)),
221
+ map(id => ({
222
+ rect: nonNullable(rects.get(id)),
223
+ node: nonNullable(nodeLookup.get(id))
224
+ }))
225
+ )
208
226
  )
209
227
  updateXYFlowNodes()
210
228
  })
@@ -229,17 +247,18 @@ export function useLayoutConstraints(): LayoutConstraints {
229
247
  const { nodeLookup } = xystore.getState()
230
248
  cancelSaveManualLayout()
231
249
 
232
- const draggingNodes = new Set(
233
- nodeLookup.values()
234
- .filter(x => x.dragging === true || x.id === xynode.id || x.selected)
235
- .filter(x => x.draggable === true)
236
- .map(x => x.id)
250
+ const draggingNodes = pipe(
251
+ Array.from(nodeLookup.values()),
252
+ filter(n => n.dragging === true || n.id === xynode.id || n.selected === true),
253
+ filter(n => n.draggable !== false),
254
+ map(x => x.id)
237
255
  )
238
- solverRef.current = createLayoutConstraints(xystore, draggingNodes)
256
+ if (hasAtLeast(draggingNodes, 1)) {
257
+ solverRef.current = createLayoutConstraints(xystore, draggingNodes)
258
+ }
239
259
  },
240
260
  onNodeDrag: () => {
241
- invariant(solverRef.current, 'solverRef.current should be defined')
242
- solverRef.current.onMove((nodes) => {
261
+ solverRef.current?.onMove((nodes) => {
243
262
  nodes.forEach(({ rect, node }) => {
244
263
  rect.positionAbsolute = node.internals.positionAbsolute
245
264
  })