@likec4/diagram 1.13.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.
Files changed (43) hide show
  1. package/package.json +9 -9
  2. package/src/LikeC4CustomColors.tsx +6 -3
  3. package/src/LikeC4Diagram.css.ts +4 -0
  4. package/src/LikeC4Diagram.tsx +9 -7
  5. package/src/overlays/Overlays.css.ts +124 -0
  6. package/src/overlays/Overlays.tsx +210 -0
  7. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +247 -0
  8. package/src/overlays/edge-details/SelectEdge.css.ts +99 -0
  9. package/src/overlays/edge-details/SelectEdge.tsx +153 -0
  10. package/src/overlays/edge-details/_types.ts +38 -0
  11. package/src/overlays/edge-details/use-layouted-edge-details.ts +356 -0
  12. package/src/overlays/edge-details/xyflow/CompoundNode.tsx +80 -0
  13. package/src/overlays/edge-details/xyflow/ElementNode.tsx +154 -0
  14. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +81 -0
  15. package/src/overlays/edge-details/xyflow/styles.css.ts +196 -0
  16. package/src/overlays/index.tsx +1 -33
  17. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +124 -63
  18. package/src/overlays/relationships-of/_types.ts +13 -2
  19. package/src/overlays/relationships-of/use-layouted-relationships.ts +37 -37
  20. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +2 -2
  21. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +47 -26
  22. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +58 -22
  23. package/src/overlays/relationships-of/xyflow/styles.css.ts +53 -16
  24. package/src/state/DiagramContext.tsx +23 -5
  25. package/src/state/diagram-to-xyflow.ts +31 -20
  26. package/src/state/diagramStore.ts +30 -23
  27. package/src/xyflow/XYFlow.tsx +2 -1
  28. package/src/xyflow/XYFlowEvents.tsx +13 -1
  29. package/src/xyflow/edges/RelationshipEdge.tsx +10 -4
  30. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +24 -21
  31. package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -0
  32. package/src/xyflow/nodes/compound/CompoundNode.tsx +11 -11
  33. package/src/xyflow/nodes/shared/Toolbar.tsx +1 -1
  34. package/src/xyflow/types.ts +1 -0
  35. package/src/xyflow/useLayoutConstraints.ts +38 -19
  36. package/src/components/Overlay.css.ts +0 -78
  37. package/src/components/Overlay.tsx +0 -102
  38. package/src/components/index.ts +0 -2
  39. package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +0 -13
  40. package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +0 -37
  41. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +0 -33
  42. package/src/overlays/relationships-of/styles.css.ts +0 -34
  43. /package/src/{components → overlays}/OverlayContext.tsx +0 -0
@@ -0,0 +1,356 @@
1
+ import dagre, { type GraphLabel, type Label } from '@dagrejs/dagre'
2
+ import {
3
+ compareFqnHierarchically,
4
+ compareRelations,
5
+ type DiagramEdge,
6
+ type DiagramNode,
7
+ type DiagramView,
8
+ type EdgeId,
9
+ type Fqn,
10
+ invariant,
11
+ isAncestor,
12
+ type LikeC4Model
13
+ } from '@likec4/core'
14
+ import { MarkerType } from '@xyflow/system'
15
+ import { useMemo } from 'react'
16
+ import { filter, first, forEach, isTruthy, map, pipe, prop, reverse, sort, sortBy, takeWhile } from 'remeda'
17
+ import { useDiagramState } from '../../hooks/useDiagramState'
18
+ import { useLikeC4Model } from '../../likec4model'
19
+ import type { XYFlowTypes } from './_types'
20
+
21
+ /**
22
+ * All constants related to the layout
23
+ */
24
+ const Sizes = {
25
+ dagre: {
26
+ ranksep: 40,
27
+ nodesep: 25,
28
+ edgesep: 15
29
+ } satisfies GraphLabel,
30
+ edgeLabel: {
31
+ width: 120
32
+ } satisfies Label,
33
+
34
+ nodeWidth: 270,
35
+ hodeHeight: 160,
36
+
37
+ // Spacer between elements in a compound node
38
+ // 0 means no spacer
39
+ spacerHeight: 0,
40
+
41
+ compoundLabelHeight: 5
42
+ }
43
+
44
+ export const ZIndexes = {
45
+ compound: 2,
46
+ edge: 3,
47
+ element: 4,
48
+
49
+ max: 5
50
+ }
51
+
52
+ function createGraph() {
53
+ const g = new dagre.graphlib.Graph({
54
+ directed: true,
55
+ compound: true
56
+ })
57
+ g.setGraph({
58
+ ...Sizes.dagre,
59
+ rankdir: 'LR'
60
+ })
61
+ g.setDefaultEdgeLabel(() => ({ ...Sizes.edgeLabel }))
62
+ g.setDefaultNodeLabel(() => ({}))
63
+
64
+ return g
65
+ }
66
+
67
+ type Context = {
68
+ g: dagre.graphlib.Graph
69
+ diagramNodes: Map<Fqn, DiagramNode>
70
+ xynodes: Map<Fqn, XYFlowTypes.Node>
71
+ edge: DiagramEdge
72
+ edges: XYFlowTypes.Edge[]
73
+ }
74
+ const sized = (height: number = Sizes.hodeHeight) => ({
75
+ width: Sizes.nodeWidth,
76
+ height
77
+ })
78
+
79
+ const graphId = (node: XYFlowTypes.Node) => ({
80
+ id: node.id,
81
+ port: node.type === 'compound' ? `${node.id}::port` : node.id,
82
+ body: `${node.id}`,
83
+ spacer: `${node.id}:spacer`
84
+ })
85
+
86
+ function nodeData(
87
+ element: LikeC4Model.ElementModel,
88
+ ctx: Context
89
+ ): Omit<XYFlowTypes.Node['data'], 'column'> {
90
+ // We try to inherit style from existing diagram node
91
+ let diagramNode = ctx.diagramNodes.get(element.id)
92
+
93
+ // Ansector separetely, because we want to inherit
94
+ // color from it if there is no diagram node
95
+ const ancestor = diagramNode ?? pipe(
96
+ element.ancestors(),
97
+ map(ancestor => ctx.diagramNodes.get(ancestor.id)),
98
+ filter(isTruthy),
99
+ first()
100
+ )
101
+
102
+ return {
103
+ fqn: element.id,
104
+ element: {
105
+ kind: element.kind,
106
+ title: diagramNode?.title ?? element.title,
107
+ description: diagramNode?.description ?? element.element.description,
108
+ color: diagramNode?.color ?? ancestor?.color ?? element.color,
109
+ shape: diagramNode?.shape ?? element.shape
110
+ },
111
+ navigateTo: diagramNode?.navigateTo ?? first(element.viewsOf())?.id ?? null,
112
+ ports: {
113
+ in: [],
114
+ out: []
115
+ }
116
+ }
117
+ }
118
+
119
+ function createNode(
120
+ nodeType: XYFlowTypes.Node['type'],
121
+ element: LikeC4Model.ElementModel,
122
+ ctx: Context
123
+ ): XYFlowTypes.Node {
124
+ let node = ctx.xynodes.get(element.id)
125
+ if (node) {
126
+ return node
127
+ }
128
+ const g = ctx.g
129
+
130
+ // Create parent node
131
+ const parent = pipe(
132
+ element.ancestors(),
133
+ takeWhile(ancestor => !isAncestor(ancestor.id, ctx.edge.source) && !isAncestor(ancestor.id, ctx.edge.target)),
134
+ first(),
135
+ found => found ? createNode('compound', found, ctx) : null
136
+ )
137
+
138
+ const xynode: XYFlowTypes.Node = {
139
+ type: nodeType,
140
+ id: element.id,
141
+ position: { x: 0, y: 0 },
142
+ data: {
143
+ ...nodeData(element, ctx)
144
+ },
145
+ zIndex: ZIndexes[nodeType],
146
+ ...(!!parent && { parentId: parent.id })
147
+ }
148
+ ctx.xynodes.set(element.id, xynode)
149
+ const k = graphId(xynode)
150
+ g.setNode(k.id, sized())
151
+
152
+ // There is one-to-one mapping between XYFlow node and dagre node
153
+ // Compound node has two nodes: body and port (for edges)
154
+ if (xynode.type === 'compound') {
155
+ g.setNode(k.port, {
156
+ width: Sizes.nodeWidth - Sizes.dagre.ranksep,
157
+ height: Sizes.compoundLabelHeight
158
+ })
159
+ g.setParent(k.port, k.id)
160
+ }
161
+
162
+ if (parent) {
163
+ const parentGraphId = graphId(parent).body
164
+ g.setParent(k.id, parentGraphId)
165
+
166
+ // Add spacer after the last element
167
+ if (Sizes.spacerHeight > 0) {
168
+ g.setNode(k.spacer, {
169
+ width: Sizes.nodeWidth - Sizes.dagre.ranksep,
170
+ height: Sizes.compoundLabelHeight
171
+ })
172
+ g.setParent(k.spacer, parentGraphId)
173
+ }
174
+ }
175
+
176
+ return xynode
177
+ }
178
+
179
+ /**
180
+ * Apply dagre layout to the graph
181
+ * And return a function to get node bounds for xyflow
182
+ */
183
+ function applyDagreLayout(g: dagre.graphlib.Graph) {
184
+ type NodeBounds = Required<Pick<XYFlowTypes.Node, 'position' | 'width' | 'height'>>
185
+ dagre.layout(g)
186
+ return function nodeBounds(nodeId: string, relativeTo?: string): NodeBounds {
187
+ const { x, y, width, height } = g.node(nodeId)
188
+ const pos = {
189
+ position: {
190
+ x: x - Math.round(width / 2),
191
+ y: y - Math.round(height / 2)
192
+ },
193
+ width,
194
+ height
195
+ }
196
+ if (!relativeTo) {
197
+ return pos
198
+ }
199
+ const offset = nodeBounds(relativeTo).position
200
+ return {
201
+ position: {
202
+ x: pos.position.x - offset.x,
203
+ y: pos.position.y - offset.y
204
+ },
205
+ width: pos.width,
206
+ height: pos.height
207
+ }
208
+ }
209
+ }
210
+
211
+ function layout(
212
+ edgeId: EdgeId,
213
+ view: DiagramView,
214
+ likec4model: LikeC4Model
215
+ ): {
216
+ view: DiagramView
217
+ edge: DiagramEdge
218
+ nodes: XYFlowTypes.Node[]
219
+ edges: XYFlowTypes.Edge[]
220
+ bounds: { x: number; y: number; width: number; height: number }
221
+ } {
222
+ const edge = view.edges.find(e => e.id === edgeId)
223
+ invariant(edge, 'edge not found')
224
+ const all = new Set([edge.source, edge.target])
225
+
226
+ const relations = edge.relations
227
+ .map(r => {
228
+ const relation = likec4model.relationship(r).relationship
229
+ all.add(relation.source)
230
+ all.add(relation.target)
231
+ return relation
232
+ })
233
+ .sort(compareRelations)
234
+ .reverse()
235
+
236
+ const diagramNodes = new Map(view.nodes.map(n => [n.id, n]))
237
+ const g = createGraph()
238
+
239
+ const ctx: Context = {
240
+ g,
241
+ xynodes: new Map(),
242
+ edge,
243
+ diagramNodes,
244
+ edges: []
245
+ }
246
+
247
+ pipe(
248
+ [...all],
249
+ sort(compareFqnHierarchically),
250
+ reverse(),
251
+ forEach(id => {
252
+ const element = likec4model.element(id)
253
+ createNode('element', element, ctx)
254
+ })
255
+ )
256
+
257
+ if (relations.length === 1 && all.size < 4) {
258
+ g.setDefaultEdgeLabel(() => ({ width: 250 }))
259
+ }
260
+
261
+ for (const relation of relations) {
262
+ const source = ctx.xynodes.get(relation.source)
263
+ invariant(source, 'source node not found')
264
+ const target = ctx.xynodes.get(relation.target)
265
+ invariant(target, 'target node not found')
266
+
267
+ source.data.ports.out.push(target.id)
268
+ target.data.ports.in.push(source.id)
269
+
270
+ g.setEdge(graphId(source).port, graphId(target).port)
271
+ const edge: XYFlowTypes.Edge = {
272
+ id: relation.id,
273
+ type: 'relation',
274
+ source: source.id,
275
+ target: target.id,
276
+ sourceHandle: target.id,
277
+ targetHandle: source.id,
278
+ data: {
279
+ relation
280
+ },
281
+ label: relation.title,
282
+ zIndex: ZIndexes.edge,
283
+ markerEnd: {
284
+ type: MarkerType.ArrowClosed,
285
+ width: 10
286
+ },
287
+ style: {
288
+ strokeWidth: 2.2,
289
+ strokeDasharray: '5, 5'
290
+ }
291
+ }
292
+ ctx.edges.push(edge)
293
+ }
294
+
295
+ const nodebounds = applyDagreLayout(ctx.g)
296
+
297
+ // Sort ports
298
+ const sortedPorts = (ports: string[]) => {
299
+ if (ports.length < 2) {
300
+ return ports
301
+ }
302
+ return pipe(
303
+ ports,
304
+ map(port => {
305
+ return {
306
+ port,
307
+ topY: nodebounds(port).position.y
308
+ }
309
+ }),
310
+ sortBy(prop('topY')),
311
+ map(prop('port'))
312
+ )
313
+ }
314
+
315
+ const xynodes = [...ctx.xynodes.values()].map((node) => {
316
+ if (node.data.ports.in.length > 1) {
317
+ node.data.ports.in = sortedPorts(node.data.ports.in)
318
+ }
319
+ if (node.data.ports.out.length > 1) {
320
+ node.data.ports.out = sortedPorts(node.data.ports.out)
321
+ }
322
+ return {
323
+ ...node,
324
+ ...nodebounds(node.id, node.parentId)
325
+ }
326
+ })
327
+
328
+ return {
329
+ view,
330
+ edge,
331
+ edges: ctx.edges,
332
+ nodes: xynodes,
333
+ bounds: {
334
+ x: 0,
335
+ y: 0,
336
+ width: g.graph().width ?? 0,
337
+ height: g.graph().height ?? 0
338
+ }
339
+ }
340
+ }
341
+
342
+ export function useLayoutedEdgeDetails(edgeId: EdgeId) {
343
+ const view = useDiagramState(s => s.view)
344
+ const likec4model = useLikeC4Model(true)
345
+ return useMemo(() =>
346
+ layout(
347
+ edgeId,
348
+ view,
349
+ likec4model
350
+ ), [
351
+ edgeId,
352
+ view,
353
+ likec4model,
354
+ layout
355
+ ])
356
+ }
@@ -0,0 +1,80 @@
1
+ import { Text as MantineText } from '@mantine/core'
2
+ import { Handle, type NodeProps, Position } from '@xyflow/react'
3
+ import clsx from 'clsx'
4
+ import { m } from 'framer-motion'
5
+ import type { XYFlowTypes } from '../_types'
6
+ import * as css from './styles.css'
7
+
8
+ const Text = MantineText.withProps({
9
+ component: 'div'
10
+ })
11
+
12
+ type CompoundNodeProps = NodeProps<XYFlowTypes.CompoundNode>
13
+
14
+ export function CompoundNode({
15
+ data: {
16
+ element,
17
+ ports,
18
+ ...data
19
+ },
20
+ width = 200,
21
+ selectable = true
22
+ }: CompoundNodeProps) {
23
+ return (
24
+ <>
25
+ <m.div
26
+ className={clsx([
27
+ css.compoundNodeBody,
28
+ 'likec4-compound-node'
29
+ ])}
30
+ data-compound-depth={3}
31
+ data-likec4-color={element.color}
32
+ animate={{
33
+ opacity: data.dimmed ? 0.15 : 1,
34
+ transition: {
35
+ delay: data.dimmed === true ? .4 : 0
36
+ }
37
+ }}
38
+ {...(selectable && {
39
+ whileHover: {
40
+ scale: 1.04,
41
+ transition: {
42
+ delay: 0.15
43
+ }
44
+ },
45
+ whileTap: {
46
+ scale: 1
47
+ }
48
+ })}
49
+ >
50
+ <Text className={css.compoundNodeTitle} maw={width - 20}>{element.title}</Text>
51
+ </m.div>
52
+ {ports.out.map((id, i) => (
53
+ <Handle
54
+ key={id}
55
+ id={id}
56
+ type={'source'}
57
+ position={Position.Right}
58
+ style={{
59
+ visibility: 'hidden',
60
+ top: `${16 + 20 * i}px`
61
+ }} />
62
+ ))}
63
+ {ports.in.map((id, i) => (
64
+ <Handle
65
+ key={id}
66
+ id={id}
67
+ type={'target'}
68
+ position={Position.Left}
69
+ style={{
70
+ visibility: 'hidden',
71
+ top: `${16 + 20 * i}px`
72
+ }} />
73
+ ))}
74
+ {
75
+ /* <Handle type="target" position={Position.Left}/>
76
+ <Handle type="source" position={Position.Right}/> */
77
+ }
78
+ </>
79
+ )
80
+ }
@@ -0,0 +1,154 @@
1
+ import { ActionIcon, Box, Group, Text as MantineText } from '@mantine/core'
2
+ import { IconFileSymlink, IconTransform, IconZoomScan } from '@tabler/icons-react'
3
+ import { Handle, type NodeProps, Position } from '@xyflow/react'
4
+ import clsx from 'clsx'
5
+ import { m } from 'framer-motion'
6
+ import { type DiagramState, useDiagramState } from '../../../hooks'
7
+ import { ElementShapeSvg } from '../../../xyflow/nodes/element/ElementShapeSvg'
8
+ import { stopPropagation } from '../../../xyflow/utils'
9
+ import { useOverlayDialog } from '../../OverlayContext'
10
+ import type { XYFlowTypes } from '../_types'
11
+ import * as css from './styles.css'
12
+
13
+ const Action = ActionIcon.withProps({
14
+ className: 'nodrag nopan ' + css.navigateBtn,
15
+ radius: 'md',
16
+ role: 'button',
17
+ onDoubleClick: stopPropagation,
18
+ onPointerDownCapture: stopPropagation
19
+ })
20
+
21
+ const Text = MantineText.withProps({
22
+ component: 'div'
23
+ })
24
+
25
+ type ElementNodeProps = NodeProps<XYFlowTypes.ElementNode>
26
+
27
+ function selector(s: DiagramState) {
28
+ return {
29
+ currentViewId: s.view.id,
30
+ onNavigateTo: s.onNavigateTo,
31
+ onOpenSource: s.onOpenSourceElement
32
+ }
33
+ }
34
+
35
+ export function ElementNode({
36
+ data: {
37
+ element,
38
+ ports,
39
+ navigateTo,
40
+ ...data
41
+ },
42
+ selectable = true,
43
+ width: w = 100,
44
+ height: h = 100
45
+ }: ElementNodeProps) {
46
+ const overlay = useOverlayDialog()
47
+ const {
48
+ currentViewId,
49
+ onNavigateTo,
50
+ onOpenSource
51
+ } = useDiagramState(selector)
52
+ return (
53
+ <>
54
+ <m.div
55
+ className={clsx([
56
+ css.elementNode,
57
+ 'likec4-element-node'
58
+ ])}
59
+ data-likec4-color={element.color}
60
+ data-likec4-shape={element.shape}
61
+ animate={{
62
+ opacity: data.dimmed ? 0.15 : 1,
63
+ transition: {
64
+ delay: data.dimmed === true ? .4 : 0
65
+ }
66
+ }}
67
+ {...(selectable && {
68
+ whileHover: {
69
+ scale: 1.045,
70
+ transition: {
71
+ delay: 0.15
72
+ }
73
+ },
74
+ whileTap: {
75
+ scale: 0.97
76
+ }
77
+ })}
78
+ >
79
+ <svg
80
+ className={clsx(
81
+ css.cssShapeSvg
82
+ )}
83
+ viewBox={`0 0 ${w} ${h}`}
84
+ width={w}
85
+ height={h}
86
+ >
87
+ <ElementShapeSvg shape={element.shape} w={w} h={h} />
88
+ </svg>
89
+ <Box className={css.elementNodeContent}>
90
+ <Text className={css.elementNodeTitle} lineClamp={2}>{element.title}</Text>
91
+ {element.description && (
92
+ <Text className={css.elementNodeDescription} lineClamp={4}>{element.description}</Text>
93
+ )}
94
+ </Box>
95
+ <Group className={css.navigateBtnBox}>
96
+ {navigateTo && onNavigateTo && navigateTo !== currentViewId && (
97
+ <Action
98
+ onClick={(event) => {
99
+ event.stopPropagation()
100
+ setTimeout(() => onNavigateTo(navigateTo), 100)
101
+ overlay.close()
102
+ }}>
103
+ <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
104
+ </Action>
105
+ )}
106
+ <Action
107
+ onClick={(event) => {
108
+ event.stopPropagation()
109
+ overlay.openOverlay({
110
+ relationshipsOf: data.fqn
111
+ })
112
+ }}>
113
+ <IconTransform stroke={1.8} style={{ width: '72%' }} />
114
+ </Action>
115
+ {onOpenSource && (
116
+ <Action
117
+ onClick={(event) => {
118
+ event.stopPropagation()
119
+ onOpenSource(data.fqn)
120
+ }}>
121
+ <IconFileSymlink stroke={1.8} style={{ width: '72%' }} />
122
+ </Action>
123
+ )}
124
+ </Group>
125
+ </m.div>
126
+ {ports.out.map((id, i) => (
127
+ <Handle
128
+ key={id}
129
+ id={id}
130
+ type="source"
131
+ position={Position.Right}
132
+ style={{
133
+ visibility: 'hidden',
134
+ top: `${15 + (i + 1) * ((h - 30) / (ports.out.length + 1))}px`
135
+ }} />
136
+ ))}
137
+ {ports.in.map((id, i) => (
138
+ <Handle
139
+ key={id}
140
+ id={id}
141
+ type="target"
142
+ position={Position.Left}
143
+ style={{
144
+ visibility: 'hidden',
145
+ top: `${15 + (i + 1) * ((h - 30) / (ports.in.length + 1))}px`
146
+ }} />
147
+ ))}
148
+ {
149
+ /* <Handle type="target" position={Position.Left} />
150
+ <Handle type="source" position={Position.Right} /> */
151
+ }
152
+ </>
153
+ )
154
+ }
@@ -0,0 +1,81 @@
1
+ import { ActionIcon, Box, Stack, Text } from '@mantine/core'
2
+ import { IconZoomScan } from '@tabler/icons-react'
3
+ import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getBezierPath } from '@xyflow/react'
4
+ import clsx from 'clsx'
5
+ import { useDiagramState } from '../../../hooks/useDiagramState'
6
+ import { stopPropagation } from '../../../xyflow/utils'
7
+ import type { XYFlowTypes } from '../_types'
8
+ import { ZIndexes } from '../use-layouted-edge-details'
9
+ import * as css from './styles.css'
10
+
11
+ export function RelationshipEdge({
12
+ data,
13
+ label,
14
+ ...props
15
+ }: EdgeProps<XYFlowTypes.Edge>) {
16
+ const onNavigateTo = useDiagramState(s => s.onNavigateTo)
17
+ const [edgePath, labelX, labelY] = getBezierPath(props)
18
+ const navigateTo = data.relation.navigateTo
19
+ return (
20
+ <>
21
+ <g
22
+ className={css.edgePath}
23
+ data-edge-dimmed={data.dimmed}
24
+ data-edge-hovered={data.hovered}
25
+ >
26
+ <BaseEdge
27
+ path={edgePath}
28
+ {...props}
29
+ />
30
+ </g>
31
+ <EdgeLabelRenderer>
32
+ <Stack
33
+ gap={2}
34
+ style={{
35
+ position: 'absolute',
36
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
37
+ maxWidth: Math.abs(props.targetX - props.sourceX - 40),
38
+ zIndex: ZIndexes.max
39
+ }}
40
+ className={clsx([
41
+ css.edgeLabel,
42
+ 'nodrag nopan'
43
+ ])}
44
+ data-edge-dimmed={data.dimmed}
45
+ data-edge-hovered={data.hovered}
46
+ >
47
+ {label && (
48
+ <Text component={'div'} className={css.edgeLabelText} lineClamp={3}>
49
+ {label}
50
+ </Text>
51
+ )}
52
+ {data.relation.technology && (
53
+ <Text component={'div'} className={css.edgeLabelTechnology}>
54
+ {'[ '}
55
+ {data.relation.technology}
56
+ {' ]'}
57
+ </Text>
58
+ )}
59
+ {navigateTo && onNavigateTo && (
60
+ <Box ta={'center'} mt={4}>
61
+ <ActionIcon
62
+ variant="default"
63
+ size={'sm'}
64
+ radius="sm"
65
+ onPointerDownCapture={stopPropagation}
66
+ onClick={event => {
67
+ event.stopPropagation()
68
+ onNavigateTo(navigateTo, event)
69
+ }}
70
+ role="button"
71
+ onDoubleClick={stopPropagation}
72
+ >
73
+ <IconZoomScan />
74
+ </ActionIcon>
75
+ </Box>
76
+ )}
77
+ </Stack>
78
+ </EdgeLabelRenderer>
79
+ </>
80
+ )
81
+ }