@likec4/diagram 1.3.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 (95) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +0 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +7 -2
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.mjs +1580 -1194
  6. package/dist/state/DiagramContext.d.ts +97 -4169
  7. package/dist/state/diagramStore.d.ts +98 -4172
  8. package/dist/state/hooks.d.ts +4 -0
  9. package/dist/state/index.d.ts +1 -3
  10. package/dist/state/useDiagramStore.d.ts +3 -4622
  11. package/dist/state/useNavigationHistory.d.ts +5 -0
  12. package/dist/style.css +217 -159
  13. package/dist/theme.css.d.ts +5 -0
  14. package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
  15. package/dist/ui/BackwardForwardPanel.d.ts +1 -0
  16. package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
  17. package/dist/ui/OptionsPanel.d.ts +1 -3
  18. package/dist/ui/options/EdgeOptions.d.ts +19 -0
  19. package/dist/ui/options/NodeOptions.d.ts +2 -2
  20. package/dist/xyflow/EdgeMarkers.d.ts +11 -0
  21. package/dist/xyflow/XYFlowEvents.d.ts +12 -9
  22. package/dist/xyflow/XYFlowInner.d.ts +1 -0
  23. package/dist/xyflow/const.d.ts +3 -3
  24. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  25. package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -3
  26. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  27. package/dist/xyflow/edges/utils.d.ts +1 -0
  28. package/dist/xyflow/hooks/index.d.ts +3 -2
  29. package/dist/xyflow/hooks/useXYFlow.d.ts +22 -21
  30. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  31. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
  32. package/dist/xyflow/nodes/element/element.css.d.ts +3 -1
  33. package/dist/xyflow/types.d.ts +2 -32
  34. package/package.json +21 -21
  35. package/src/LikeC4Diagram.css.ts +10 -8
  36. package/src/LikeC4Diagram.props.ts +12 -5
  37. package/src/LikeC4Diagram.tsx +43 -26
  38. package/src/globals.css.ts +11 -4
  39. package/src/index.ts +1 -1
  40. package/src/mantine.css.ts +2 -2
  41. package/src/state/DiagramContext.tsx +47 -16
  42. package/src/state/diagramStore.ts +89 -80
  43. package/src/state/hooks.ts +5 -0
  44. package/src/state/index.ts +1 -3
  45. package/src/state/useDiagramStore.ts +6 -13
  46. package/src/state/useNavigationHistory.ts +39 -0
  47. package/src/theme.css.ts +5 -0
  48. package/src/ui/BackwardForwardPanel.css.ts +22 -0
  49. package/src/ui/BackwardForwardPanel.tsx +59 -0
  50. package/src/ui/DiagramTitlePanel.css.ts +17 -6
  51. package/src/ui/DiagramTitlePanel.tsx +108 -94
  52. package/src/ui/DynamicViewWalkthrough.tsx +6 -4
  53. package/src/ui/OptionsPanel.tsx +37 -18
  54. package/src/ui/options/EdgeOptions.tsx +157 -0
  55. package/src/ui/options/NodeOptions.tsx +37 -29
  56. package/src/xyflow/EdgeMarkers.tsx +148 -0
  57. package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
  58. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
  59. package/src/xyflow/SyncWithDiagram.tsx +13 -20
  60. package/src/xyflow/XYFlow.tsx +2 -4
  61. package/src/xyflow/XYFlowEvents.tsx +48 -33
  62. package/src/xyflow/XYFlowInner.tsx +6 -3
  63. package/src/xyflow/const.ts +3 -3
  64. package/src/xyflow/diagram-to-xyflow.ts +18 -53
  65. package/src/xyflow/edges/RelationshipEdge.tsx +218 -126
  66. package/src/xyflow/edges/edges.css.ts +44 -26
  67. package/src/xyflow/edges/utils.ts +10 -0
  68. package/src/xyflow/hooks/index.ts +4 -2
  69. package/src/xyflow/hooks/useLayoutConstraints.ts +5 -5
  70. package/src/xyflow/hooks/useXYFlow.ts +28 -1
  71. package/src/xyflow/nodes/compound/CompoundNode.css.ts +41 -31
  72. package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
  73. package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
  74. package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
  75. package/src/xyflow/nodes/element/ElementNode.tsx +69 -114
  76. package/src/xyflow/nodes/element/element.css.ts +48 -15
  77. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  78. package/src/xyflow/types.ts +2 -4
  79. package/src/xyflow/utils.ts +3 -6
  80. package/dist/KeepAspectRatioContainer.d.ts +0 -9
  81. package/dist/icons/Clipboard.d.ts +0 -3
  82. package/dist/icons/Link.d.ts +0 -3
  83. package/dist/icons/ZoomIn.d.ts +0 -3
  84. package/dist/icons/index.d.ts +0 -3
  85. package/dist/mantine/theme.d.ts +0 -3
  86. package/dist/state/WhenInitialized.d.ts +0 -3
  87. package/dist/ui/Camera.d.ts +0 -3
  88. package/src/KeepAspectRatioContainer.tsx +0 -36
  89. package/src/icons/Clipboard.tsx +0 -24
  90. package/src/icons/Link.tsx +0 -23
  91. package/src/icons/ZoomIn.tsx +0 -25
  92. package/src/icons/index.ts +0 -3
  93. package/src/mantine/theme.ts +0 -20
  94. package/src/state/WhenInitialized.tsx +0 -24
  95. package/src/ui/Camera.tsx +0 -174
@@ -1,20 +1,10 @@
1
- import {
2
- type DiagramNode,
3
- type DiagramView,
4
- extractStep,
5
- type Fqn,
6
- invariant,
7
- type NonEmptyArray,
8
- nonNullable,
9
- type Point
10
- } from '@likec4/core'
11
- import { Bezier } from 'bezier-js'
12
- import { hasAtLeast, reduce } from 'remeda'
1
+ import { extractStep, invariant, nonNullable } from '@likec4/core'
2
+ import type { DiagramNode, DiagramView, Fqn } from '@likec4/core/types'
3
+ import { hasAtLeast } from 'remeda'
13
4
  import type { XYFlowData } from '../xyflow/types'
14
- import { toDomPrecision } from './utils'
5
+ import { ZIndexes } from './const'
15
6
 
16
- // const nodeZIndex = (node: DiagramNode) => node.level + (hasAtLeast(node.children, 1) ? 2 : 1)
17
- const nodeZIndex = (node: DiagramNode) => node.level + 1
7
+ // const nodeZIndex = (node: DiagramNode) => node.level - (node.children.length > 0 ? 1 : 0)
18
8
 
19
9
  export function diagramViewToXYFlowData(
20
10
  view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>,
@@ -50,10 +40,10 @@ export function diagramViewToXYFlowData(
50
40
  const nodeById = (id: Fqn) => nonNullable(nodeLookup.get(id), `Node not found: ${id}`)
51
41
 
52
42
  let next: typeof traverse[0] | undefined
53
- while ((next = traverse.pop())) {
43
+ while ((next = traverse.shift())) {
54
44
  const { node, parent } = next
55
45
  if (node.children.length > 0) {
56
- traverse.unshift(...node.children.map(child => ({ node: nodeById(child), parent: node })))
46
+ traverse.push(...node.children.map(child => ({ node: nodeById(child), parent: node })))
57
47
  }
58
48
  const isCompound = hasAtLeast(node.children, 1)
59
49
  const position = {
@@ -69,6 +59,7 @@ export function diagramViewToXYFlowData(
69
59
  // const inEdges = node.inEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
70
60
 
71
61
  const id = ns + node.id
62
+ const draggable = opts.draggable && (!parent || parent.children.length > 1)
72
63
  editor.nodes.push({
73
64
  id,
74
65
  type: isCompound ? 'compound' : 'element',
@@ -76,43 +67,22 @@ export function diagramViewToXYFlowData(
76
67
  fqn: node.id,
77
68
  element: node
78
69
  },
79
- draggable: opts.draggable && (!parent || parent.children.length > 1),
70
+ draggable,
80
71
  selectable: opts.selectable,
81
72
  deletable: false,
82
73
  position,
83
- zIndex: nodeZIndex(node),
74
+ zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
84
75
  hidden: false,
85
- /* initialWidth: node.width,
86
- initialHeight: node.height, */
76
+ initialWidth: node.width,
77
+ initialHeight: node.height,
87
78
  width: node.width,
88
79
  height: node.height,
89
- // measured: {
90
- // width: node.width,
91
- // height: node.height
92
- // },
93
80
  ...(parent && {
94
81
  parentId: ns + parent.id
82
+ }),
83
+ ...(isCompound && {
84
+ dragHandle: '.likec4-compound-title'
95
85
  })
96
- // handles: [
97
- // ...outEdges.map(out => ({
98
- // id: `${out.id}`,
99
- // type: 'source' as const,
100
- // position: Position.Bottom,
101
- // x: out.points[0][0],
102
- // y: out.points[0][1],
103
- // width: 10,
104
- // height: 10
105
- // })),
106
- // ...inEdges.map(out => ({
107
- // id: out.id,
108
- // type: 'target' as const,
109
- // position: Position.Top,
110
- // x: out.points[0][0],
111
- // y: out.points[0][1],
112
- // width: 10,
113
- // height: 10
114
- // }))
115
- // ],
116
86
  })
117
87
  }
118
88
 
@@ -131,33 +101,28 @@ export function diagramViewToXYFlowData(
131
101
  invariant(hasAtLeast(edge.points, 2), 'edge should have at least 2 points')
132
102
  // invariant(hasAtLeast(controlPoints, 2), 'edge controlPoints should have at least 2 points')
133
103
 
134
- const level = Math.max(nodeZIndex(nodeById(source)), nodeZIndex(nodeById(target)))
104
+ // const level = Math.max(nodeZIndex(nodeById(source)), nodeZIndex(nodeById(target)))
135
105
 
136
106
  editor.edges.push({
137
107
  id,
138
108
  type: 'relationship',
139
109
  source: ns + source,
140
110
  target: ns + target,
141
- zIndex: level,
111
+ zIndex: ZIndexes.Edge,
112
+ selectable: opts.selectable,
142
113
  deletable: false,
143
114
  data: {
144
115
  edge,
145
116
  type: 'bezier',
146
117
  controlPoints: view.manualLayout?.edges[edge.id]?.controlPoints || null,
147
- headPoint: edge.headArrowPoint ?? null,
148
- tailPoint: edge.tailArrowPoint ?? null,
149
118
  stepNum: isDynamicView ? extractStep(edge.id) : null,
150
119
  label: !!edge.labelBBox
151
120
  ? {
152
121
  bbox: edge.labelBBox,
153
- // text: edge.labels ? edge.labels.map(l => l.text).join('\n') : ''
154
122
  text: edge.label ?? ''
155
123
  }
156
124
  : null
157
125
  },
158
- // markerEnd: {
159
- // type: MarkerType.ArrowClosed
160
- // },
161
126
  interactionWidth: 20
162
127
  })
163
128
  }
@@ -1,20 +1,27 @@
1
- import { invariant, type NonEmptyArray, nonNullable, type Point } from '@likec4/core'
1
+ import {
2
+ invariant,
3
+ type NonEmptyArray,
4
+ nonexhaustive,
5
+ nonNullable,
6
+ type Point,
7
+ type RelationshipArrowType
8
+ } from '@likec4/core'
2
9
  import { Box } from '@mantine/core'
3
10
  import { useIsomorphicLayoutEffect } from '@react-hookz/web'
4
11
  import { assignInlineVars } from '@vanilla-extract/dynamic'
5
12
  import type { EdgeProps, XYPosition } from '@xyflow/react'
6
13
  import { EdgeLabelRenderer } from '@xyflow/react'
7
- import { getNodePositionWithOrigin } from '@xyflow/system'
8
14
  import clsx from 'clsx'
9
15
  import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
10
- import { deepEqual as eq } from 'fast-equals'
11
- import { memo, useRef, useState } from 'react'
16
+ import { deepEqual, deepEqual as eq } from 'fast-equals'
17
+ import { memo, type PointerEvent as ReactPointerEvent, useRef, useState } from 'react'
12
18
  import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
13
- import { useDiagramState, useDiagramStoreApi } from '../../state'
19
+ import { useDiagramState, useDiagramStoreApi } from '../../state/hooks'
14
20
  import { ZIndexes } from '../const'
15
- import { useXYFlow, useXYStoreApi } from '../hooks'
16
- import { type XYFlowEdge } from '../types'
17
- import { bezierControlPoints, toDomPrecision } from '../utils'
21
+ import { EdgeMarkers } from '../EdgeMarkers'
22
+ import { useXYStoreApi } from '../hooks'
23
+ import { type RelationshipData, type XYFlowEdge } from '../types'
24
+ import { bezierControlPoints } from '../utils'
18
25
  import * as edgesCss from './edges.css'
19
26
  import { getNodeIntersectionFromCenterToPoint } from './utils'
20
27
  // import { getEdgeParams } from './utils'
@@ -49,6 +56,32 @@ import { getNodeIntersectionFromCenterToPoint } from './utils'
49
56
  // return path + segment
50
57
  // }
51
58
 
59
+ const toMarker = (arrowType?: RelationshipArrowType) => {
60
+ if (!arrowType || arrowType === 'none') {
61
+ return undefined
62
+ }
63
+ switch (arrowType) {
64
+ case 'normal':
65
+ case 'crow':
66
+ return 'Arrow' as const
67
+ case 'onormal':
68
+ return 'OArrow' as const
69
+ case 'diamond':
70
+ return 'Diamond' as const
71
+ case 'odiamond':
72
+ return 'ODiamond' as const
73
+ case 'open':
74
+ case 'vee':
75
+ return 'Open' as const
76
+ case 'dot':
77
+ return 'Dot' as const
78
+ case 'odot':
79
+ return `ODot` as const
80
+ default:
81
+ nonexhaustive(arrowType)
82
+ }
83
+ }
84
+
52
85
  function bezierPath(bezierSpline: NonEmptyArray<Point>) {
53
86
  let [start, ...points] = bezierSpline
54
87
  invariant(start, 'start should be defined')
@@ -83,8 +116,8 @@ const sameControlPoints = (a: XYPosition[] | null, b: XYPosition[] | null) => {
83
116
 
84
117
  const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>) => (
85
118
  prev.id === next.id
86
- && prev.source === next.source
87
- && prev.target === next.target
119
+ && eq(prev.source, next.source)
120
+ && eq(prev.target, next.target)
88
121
  && eq(prev.selected ?? false, next.selected ?? false)
89
122
  && isSame(prev.sourceX, next.sourceX)
90
123
  && isSame(prev.sourceY, next.sourceY)
@@ -103,12 +136,17 @@ const curve = d3line<XYPosition>()
103
136
  export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(function RelationshipEdgeR({
104
137
  id,
105
138
  data,
106
- selected: _selected,
139
+ selected,
140
+ sourceX,
141
+ sourceY,
142
+ targetX,
143
+ targetY,
107
144
  style,
108
145
  source,
109
146
  target,
110
147
  interactionWidth
111
148
  }) {
149
+ const [isControlPointDragging, setIsControlPointDragging] = useState(false)
112
150
  const diagramStore = useDiagramStoreApi()
113
151
  const xyflowStore = useXYStoreApi()
114
152
  const { isActive, isEditable, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
@@ -164,16 +202,24 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
164
202
  if (isModified) {
165
203
  labelX = labelPos.x
166
204
  labelY = labelPos.y
167
- const sourceCenterPos = getNodePositionWithOrigin(sourceNode, [-0.5, -0.5])
168
- const targetCenterPos = getNodePositionWithOrigin(targetNode, [-0.5, -0.5])
205
+ const sourceCenterPos = { x: sourceX, y: sourceY }
206
+ const targetCenterPos = { x: targetX, y: targetY }
169
207
 
170
- const points = [
171
- sourceCenterPos.positionAbsolute,
172
- getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints)!),
173
- ...controlPoints,
174
- getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints)!),
175
- targetCenterPos.positionAbsolute
176
- ]
208
+ const points = diagramEdge.dir === 'back'
209
+ ? [
210
+ targetCenterPos,
211
+ getNodeIntersectionFromCenterToPoint(targetNode, first(controlPoints) ?? sourceCenterPos),
212
+ ...controlPoints,
213
+ getNodeIntersectionFromCenterToPoint(sourceNode, last(controlPoints) ?? targetCenterPos),
214
+ sourceCenterPos
215
+ ]
216
+ : [
217
+ sourceCenterPos,
218
+ getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints) ?? targetCenterPos),
219
+ ...controlPoints,
220
+ getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints) ?? sourceCenterPos),
221
+ targetCenterPos
222
+ ]
177
223
 
178
224
  edgePath = nonNullable(curve(points))
179
225
  } else {
@@ -198,69 +244,88 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
198
244
  setLabelPosition(current => isSamePoint(current, point) ? current : point)
199
245
  }, [edgePath])
200
246
 
201
- const onControlPointerDown = (index: number, e: React.PointerEvent) => {
247
+ const onControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>) => {
202
248
  const { domNode } = xyflowStore.getState()
203
- if (!domNode) return
249
+ if (!domNode || e.pointerType !== 'mouse') {
250
+ return
251
+ }
204
252
  const { xyflow } = diagramStore.getState()
253
+ if (e.button === 2 && controlPoints.length > 1) {
254
+ const newControlPoints = controlPoints.slice()
255
+ newControlPoints.splice(index, 1)
256
+ e.stopPropagation()
257
+ // Defer the update to avoid conflict with the pointerup event
258
+ setTimeout(() => {
259
+ xyflow.updateEdgeData(id, { controlPoints: newControlPoints })
260
+ }, 10)
261
+ return
262
+ }
263
+ if (e.button !== 0) {
264
+ return
265
+ }
205
266
  e.stopPropagation()
206
267
  let hasMoved = false
207
268
  let pointer = { x: e.clientX, y: e.clientY }
208
269
  const onPointerMove = (e: PointerEvent) => {
209
270
  if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
271
+ setIsControlPointDragging(true)
272
+ if (!hasMoved) {
273
+ diagramStore.getState().cancelSaveManualLayout()
274
+ }
210
275
  hasMoved = true
211
276
  pointer = { x: e.clientX, y: e.clientY }
212
277
  const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
213
278
  const newControlPoints = controlPoints.slice()
214
279
  newControlPoints[index] = {
215
- x: toDomPrecision(x),
216
- y: toDomPrecision(y)
280
+ x: Math.round(x),
281
+ y: Math.round(y)
217
282
  }
218
283
  xyflow.updateEdgeData(id, { controlPoints: newControlPoints })
219
284
  }
220
285
  }
221
286
  const onPointerUp = () => {
287
+ setIsControlPointDragging(false)
222
288
  domNode.removeEventListener('pointermove', onPointerMove)
223
289
  domNode.removeEventListener('pointerup', onPointerUp)
224
290
  if (hasMoved) {
225
- diagramStore.getState().triggerSaveManualLayout(xyflowStore)
291
+ diagramStore.getState().triggerSaveManualLayout()
226
292
  }
227
293
  }
228
294
  domNode.addEventListener('pointermove', onPointerMove)
229
295
  domNode.addEventListener('pointerup', onPointerUp, { once: true })
230
296
  }
231
297
 
232
- const marker = `url(#arrow-${id})`
233
- let markerStart = diagramEdge.tailArrow ? marker : undefined
234
- let markerEnd = diagramEdge.headArrow ? marker : undefined
235
- if (isModified && diagramEdge.dir === 'back') {
236
- ;[markerStart, markerEnd] = [markerEnd, markerStart]
298
+ let markerStartName = toMarker(diagramEdge.tail)
299
+ let markerEndName = toMarker(diagramEdge.head ?? 'normal')
300
+ if (diagramEdge.dir === 'back') {
301
+ ;[markerStartName, markerEndName] = [markerEndName, markerStartName]
237
302
  }
238
303
 
304
+ const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null
305
+ const MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null
306
+
239
307
  return (
240
308
  <g
241
309
  className={clsx([
242
310
  edgesCss.container,
243
- isDimmed && edgesCss.dimmed
311
+ isDimmed && edgesCss.dimmed,
312
+ isControlPointDragging && edgesCss.controlDragging
244
313
  ])}
245
314
  data-likec4-color={color}
246
315
  data-edge-dir={diagramEdge.dir}
247
316
  data-edge-active={isActive}
248
317
  data-edge-hovered={isHovered}>
318
+ <path
319
+ className={clsx('react-flow__edge-interaction')}
320
+ d={edgePath}
321
+ fill="none"
322
+ stroke={'transparent'}
323
+ strokeWidth={interactionWidth ?? 10}
324
+ />
249
325
  <g className={edgesCss.markerContext}>
250
326
  <defs>
251
- <marker
252
- id={`arrow-${id}`}
253
- viewBox="0 0 16 10"
254
- // TODO: graphviz cut bezier path at arrow, we don't
255
- refX={isModified ? '15' : '1'}
256
- refY="5"
257
- markerWidth="5"
258
- markerHeight="4"
259
- markerUnits="strokeWidth"
260
- preserveAspectRatio="xMaxYMid meet"
261
- orient="auto-start-reverse">
262
- <path d="M 0 0 L 16 5 L 0 10 z" stroke="context-stroke" fill="context-stroke" />
263
- </marker>
327
+ {MarkerStart && <MarkerStart id={'start' + id} />}
328
+ {MarkerEnd && <MarkerEnd id={'end' + id} />}
264
329
  </defs>
265
330
  <path
266
331
  className={clsx('react-flow__edge-path', edgesCss.edgePathBg)}
@@ -275,92 +340,119 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
275
340
  style={style}
276
341
  strokeLinecap={'round'}
277
342
  strokeDasharray={strokeDasharray}
278
- markerStart={markerStart}
279
- markerEnd={markerEnd}
343
+ markerStart={MarkerStart ? `url(#start${id})` : undefined}
344
+ markerEnd={MarkerEnd ? `url(#end${id})` : undefined}
280
345
  />
281
346
  </g>
282
- <path
283
- className={clsx('react-flow__edge-interaction')}
284
- d={edgePath}
285
- fill="none"
286
- strokeOpacity={0}
287
- strokeWidth={interactionWidth ?? 10}
288
- />
289
- {isEdgePathEditable && controlPoints.map((p, i) => (
290
- <circle
291
- onPointerDown={e => onControlPointerDown(i, e)}
292
- className={edgesCss.controlPoint}
293
- key={i}
294
- cx={p.x}
295
- cy={p.y}
296
- r={3}
297
- />
298
- ))}
347
+
348
+ {isEdgePathEditable && (
349
+ <g
350
+ onContextMenu={e => {
351
+ e.preventDefault()
352
+ e.stopPropagation()
353
+ }}>
354
+ {controlPoints.map((p, i) => (
355
+ <circle
356
+ onPointerDown={e => onControlPointerDown(i, e)}
357
+ className={edgesCss.controlPoint}
358
+ key={i}
359
+ cx={p.x}
360
+ cy={p.y}
361
+ r={3}
362
+ />
363
+ ))}
364
+ </g>
365
+ )}
299
366
  {(data.label || isStepEdge) && (
300
- <EdgeLabelRenderer>
301
- <Box
302
- className={clsx([
303
- edgesCss.container,
304
- edgesCss.edgeLabel,
305
- isDimmed && edgesCss.dimmed
306
- ])}
307
- data-likec4-color={color}
308
- style={{
309
- ...assignInlineVars({
310
- [edgesCss.varLabelX]: isModified ? `calc(${labelX}px - 10%)` : `${labelX}px`,
311
- [edgesCss.varLabelY]: isModified ? `${labelY - 5}px` : `${labelY}px`
312
- }),
313
- ...(isEdgePathEditable && {
314
- pointerEvents: 'none'
315
- }),
316
- ...(data.label && {
317
- maxWidth: data.label.bbox.width + 12
318
- }),
319
- zIndex: edgeLookup.get(id)?.zIndex ?? ZIndexes.Edge
320
- }}
321
- mod={{
322
- 'data-edge-hovered': isHovered,
323
- 'data-edge-active': isActive
324
- }}
325
- >
326
- {isStepEdge && (
327
- <Box className={edgesCss.stepEdgeNumber}>
328
- {data.stepNum}
329
- </Box>
330
- )}
331
- {data.label?.text && (
332
- <Box className={edgesCss.edgeLabelText}>
333
- {data.label.text}
334
- </Box>
335
- )}
336
- {
337
- /* <Popover
338
- position="bottom"
339
- floatingStrategy="fixed"
340
- shadow="lg"
341
- disabled={!selected}
342
- transitionProps={{
343
- transition: 'pop'
344
- }}>
345
- <Popover.Target>
346
- <Box className={clsx('nodrag nopan', edgeLabelBody)}>
347
- {data.label.text}
348
- </Box>
349
- </Popover.Target>
350
- <Popover.Dropdown>
351
- {edge.relations.map((relation) => (
352
- <Box key={relation}>
353
- <Text size='xs'>{relation}</Text>
354
- </Box>
355
- ))}
356
- <TextInput label="Name" placeholder="Name" size="xs" />
357
- <TextInput label="Email" placeholder="john@doe.com" size="xs" mt="xs" />
358
- </Popover.Dropdown>
359
- </Popover> */
360
- }
361
- </Box>
362
- </EdgeLabelRenderer>
367
+ <EdgeLabel
368
+ {...({
369
+ isDimmed,
370
+ color,
371
+ isModified,
372
+ labelX,
373
+ labelY,
374
+ isEdgePathEditable,
375
+ selected: selected ?? false,
376
+ stepNum: data.stepNum,
377
+ label: data.label,
378
+ zIndex: edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge,
379
+ isHovered,
380
+ isActive,
381
+ isStepEdge
382
+ })} />
363
383
  )}
364
384
  </g>
365
385
  )
366
386
  }, isEqualProps)
387
+
388
+ type EdgeLabelProps = {
389
+ isDimmed: boolean
390
+ color: string
391
+ isModified: boolean
392
+ labelX: number
393
+ labelY: number
394
+ isEdgePathEditable: boolean
395
+ selected: boolean
396
+ stepNum: number | null
397
+ label: RelationshipData['label']
398
+ isHovered: boolean
399
+ isActive: boolean
400
+ zIndex: number
401
+ }
402
+
403
+ const EdgeLabel = memo<EdgeLabelProps>(({
404
+ isDimmed,
405
+ color,
406
+ isModified,
407
+ labelX,
408
+ labelY,
409
+ isEdgePathEditable,
410
+ selected,
411
+ label,
412
+ stepNum,
413
+ isHovered,
414
+ isActive,
415
+ zIndex
416
+ }) => {
417
+ return (
418
+ <EdgeLabelRenderer>
419
+ <Box
420
+ className={clsx([
421
+ 'nodrag nopan',
422
+ edgesCss.container,
423
+ edgesCss.edgeLabel,
424
+ isDimmed && edgesCss.dimmed
425
+ ])}
426
+ style={{
427
+ ...assignInlineVars({
428
+ [edgesCss.varLabelX]: isModified ? `calc(${labelX}px - 10%)` : `${labelX}px`,
429
+ [edgesCss.varLabelY]: isModified ? `${labelY - 5}px` : `${labelY}px`
430
+ }),
431
+ ...(isEdgePathEditable && selected && {
432
+ pointerEvents: 'none'
433
+ }),
434
+ ...(label && {
435
+ maxWidth: label.bbox.width + 18
436
+ }),
437
+ zIndex
438
+ }}
439
+ mod={{
440
+ 'data-likec4-color': color,
441
+ 'data-edge-hovered': isHovered,
442
+ 'data-edge-active': isActive
443
+ }}
444
+ >
445
+ {stepNum !== null && (
446
+ <Box className={edgesCss.stepEdgeNumber}>
447
+ {stepNum}
448
+ </Box>
449
+ )}
450
+ {isTruthy(label?.text) && (
451
+ <Box className={edgesCss.edgeLabelText}>
452
+ {label.text}
453
+ </Box>
454
+ )}
455
+ </Box>
456
+ </EdgeLabelRenderer>
457
+ )
458
+ }, deepEqual)