@likec4/diagram 1.0.0-next.2 → 1.0.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/dist/LikeC4Diagram.css.d.ts +0 -1
- package/dist/LikeC4Diagram.d.ts +1 -2
- package/dist/LikeC4Diagram.props.d.ts +24 -25
- package/dist/StaticLikeC4Diagram.d.ts +1 -2
- package/dist/globals.css.d.ts +1 -0
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/useSetState.d.ts +3 -0
- package/dist/hooks/useUpdateEffect.d.ts +4 -0
- package/dist/icons/Clipboard.d.ts +0 -1
- package/dist/icons/Link.d.ts +0 -1
- package/dist/icons/ZoomIn.d.ts +0 -1
- package/dist/icons/index.d.ts +0 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.mjs +9388 -0
- package/dist/mantine/EnsureMantine.d.ts +2 -4
- package/dist/mantine/theme.d.ts +3 -0
- package/dist/mantine.css.d.ts +1 -0
- package/dist/state/DiagramContext.d.ts +386 -0
- package/dist/state/WhenInitialized.d.ts +3 -0
- package/dist/state/diagramStore.d.ts +428 -0
- package/dist/state/index.d.ts +4 -2
- package/dist/state/useDiagramStore.d.ts +387 -0
- package/dist/style.css +1212 -1
- package/dist/theme.css.d.ts +16 -2
- package/dist/ui/Camera.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.d.ts +0 -1
- package/dist/ui/OptionsPanel.css.d.ts +0 -1
- package/dist/ui/OptionsPanel.d.ts +0 -1
- package/dist/ui/index.d.ts +0 -1
- package/dist/ui/options/NodeOptions.d.ts +0 -1
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -5
- package/dist/xyflow/SyncWithDiagram.d.ts +5 -0
- package/dist/xyflow/XYFlowBackground.d.ts +0 -1
- package/dist/xyflow/XYFlowEvents.d.ts +5 -15
- package/dist/xyflow/XYFlowWrapper.d.ts +4 -5
- package/dist/xyflow/const.d.ts +1 -2
- package/dist/xyflow/diagram-to-xyflow.d.ts +4 -2
- package/dist/xyflow/edges/RelationshipEdge.d.ts +7 -7
- package/dist/xyflow/edges/edges.css.d.ts +2 -1
- package/dist/xyflow/edges/utils.d.ts +3 -4
- package/dist/xyflow/hooks/index.d.ts +2 -37
- package/dist/xyflow/hooks/{use-layout-/321/201onstraints.d.ts → useLayoutConstraints.d.ts} +2 -4
- package/dist/xyflow/hooks/useXYFlow.d.ts +266 -0
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -1
- package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -1
- package/dist/xyflow/nodes/compound/index.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -3
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -3
- package/dist/xyflow/nodes/element/element.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/index.d.ts +1 -2
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -1
- package/dist/xyflow/types.d.ts +20 -8
- package/dist/xyflow/utils.d.ts +0 -1
- package/package.json +36 -34
- package/src/LikeC4Diagram.css.ts +31 -0
- package/src/LikeC4Diagram.props.ts +154 -0
- package/src/LikeC4Diagram.tsx +119 -0
- package/src/StaticLikeC4Diagram.tsx +39 -0
- package/src/globals.css.ts +128 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useSetState.ts +19 -0
- package/src/hooks/useUpdateEffect.ts +28 -0
- package/src/icons/Clipboard.tsx +24 -0
- package/src/icons/Link.tsx +23 -0
- package/src/icons/ZoomIn.tsx +25 -0
- package/src/icons/index.ts +3 -0
- package/src/index.ts +11 -0
- package/src/mantine/EnsureMantine.tsx +14 -0
- package/src/mantine/theme.ts +19 -0
- package/src/mantine.css.ts +4 -0
- package/src/reset.d.ts +2 -0
- package/src/state/DiagramContext.tsx +42 -0
- package/src/state/WhenInitialized.tsx +24 -0
- package/src/state/diagramStore.ts +358 -0
- package/src/state/index.ts +4 -0
- package/src/state/useDiagramStore.ts +39 -0
- package/src/theme.css.ts +64 -0
- package/src/ui/Camera.tsx +174 -0
- package/src/ui/KeepAspectRatio.css.ts +7 -0
- package/src/ui/KeepAspectRatio.tsx +35 -0
- package/src/ui/OptionsPanel.css.ts +10 -0
- package/src/ui/OptionsPanel.tsx +65 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/options/NodeOptions.tsx +253 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +93 -0
- package/src/xyflow/SyncWithDiagram.tsx +113 -0
- package/src/xyflow/XYFlowBackground.tsx +33 -0
- package/src/xyflow/XYFlowEvents.tsx +100 -0
- package/src/xyflow/XYFlowWrapper.tsx +166 -0
- package/src/xyflow/const.ts +7 -0
- package/src/xyflow/diagram-to-xyflow.ts +197 -0
- package/src/xyflow/edges/RelationshipEdge.tsx +288 -0
- package/src/xyflow/edges/edges.css.ts +124 -0
- package/src/xyflow/edges/utils.ts +110 -0
- package/src/xyflow/hooks/index.ts +2 -0
- package/src/xyflow/hooks/useLayoutConstraints.ts +289 -0
- package/src/xyflow/hooks/useXYFlow.ts +20 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +191 -0
- package/src/xyflow/nodes/compound/CompoundNode.tsx +105 -0
- package/src/xyflow/nodes/compound/index.ts +1 -0
- package/src/xyflow/nodes/element/ElementIcon.tsx +44 -0
- package/src/xyflow/nodes/element/ElementLink.css.ts +67 -0
- package/src/xyflow/nodes/element/ElementLink.tsx +94 -0
- package/src/xyflow/nodes/element/ElementNode.tsx +225 -0
- package/src/xyflow/nodes/element/ElementShapeSvg.tsx +181 -0
- package/src/xyflow/nodes/element/element.css.ts +261 -0
- package/src/xyflow/nodes/element/index.ts +1 -0
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +25 -0
- package/src/xyflow/types.ts +100 -0
- package/src/xyflow/utils.ts +12 -0
- package/dist/EmbeddedLikeC4Diagram.css.d.ts +0 -6
- package/dist/EmbeddedLikeC4Diagram.d.ts +0 -18
- package/dist/bundle.d.ts +0 -3
- package/dist/hooks/use-set-state.d.ts +0 -2
- package/dist/hooks/use-update-effect.d.ts +0 -4
- package/dist/index.css.d.ts +0 -2
- package/dist/index.js +0 -13043
- package/dist/mantine/index.d.ts +0 -3
- package/dist/state/DiagramState.d.ts +0 -162
- package/dist/xyflow/UpdateOnDiagramChange.d.ts +0 -10
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DiagramNode,
|
|
3
|
+
type DiagramView,
|
|
4
|
+
type Fqn,
|
|
5
|
+
invariant,
|
|
6
|
+
type NonEmptyArray,
|
|
7
|
+
nonNullable,
|
|
8
|
+
type Point
|
|
9
|
+
} from '@likec4/core'
|
|
10
|
+
import { getBezierEdgeCenter } from '@xyflow/react'
|
|
11
|
+
import { hasAtLeast } from 'remeda'
|
|
12
|
+
import type { XYFlowData } from '../xyflow/types'
|
|
13
|
+
|
|
14
|
+
function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
|
|
15
|
+
let [start, ...bezierPoints] = bezierSpline
|
|
16
|
+
invariant(start, 'start should be defined')
|
|
17
|
+
const handles = [
|
|
18
|
+
// start
|
|
19
|
+
] as Point[]
|
|
20
|
+
|
|
21
|
+
while (hasAtLeast(bezierPoints, 3)) {
|
|
22
|
+
const [cp1, cp2, end, ...rest] = bezierPoints
|
|
23
|
+
const [centerX, centerY] = getBezierEdgeCenter({
|
|
24
|
+
sourceX: start[0],
|
|
25
|
+
sourceY: start[1],
|
|
26
|
+
targetX: end[0],
|
|
27
|
+
targetY: end[1],
|
|
28
|
+
sourceControlX: cp1[0],
|
|
29
|
+
sourceControlY: cp1[1],
|
|
30
|
+
targetControlX: cp2[0],
|
|
31
|
+
targetControlY: cp2[1]
|
|
32
|
+
})
|
|
33
|
+
handles.push(
|
|
34
|
+
[centerX, centerY]
|
|
35
|
+
)
|
|
36
|
+
// if (rest.length === 0) {
|
|
37
|
+
// handles.push(
|
|
38
|
+
// end
|
|
39
|
+
// )
|
|
40
|
+
// }
|
|
41
|
+
bezierPoints = rest
|
|
42
|
+
start = end
|
|
43
|
+
}
|
|
44
|
+
invariant(bezierPoints.length === 0, 'all points should be consumed')
|
|
45
|
+
|
|
46
|
+
return handles
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// const nodeZIndex = (node: DiagramNode) => node.level + (hasAtLeast(node.children, 1) ? 2 : 1)
|
|
50
|
+
const nodeZIndex = (node: DiagramNode) => node.level + 1
|
|
51
|
+
|
|
52
|
+
export function diagramViewToXYFlowData(
|
|
53
|
+
view: Pick<DiagramView, 'nodes' | 'edges'>,
|
|
54
|
+
opts: {
|
|
55
|
+
draggable: boolean
|
|
56
|
+
selectable: boolean
|
|
57
|
+
}
|
|
58
|
+
): XYFlowData {
|
|
59
|
+
const editor: XYFlowData = {
|
|
60
|
+
nodes: [],
|
|
61
|
+
edges: []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nodeLookup = new Map<Fqn, DiagramNode>()
|
|
65
|
+
|
|
66
|
+
const traverse = view.nodes.reduce(
|
|
67
|
+
(acc, node) => {
|
|
68
|
+
nodeLookup.set(node.id, node)
|
|
69
|
+
if (!node.parent) {
|
|
70
|
+
acc.push({ node, parent: null })
|
|
71
|
+
}
|
|
72
|
+
return acc
|
|
73
|
+
},
|
|
74
|
+
new Array<{
|
|
75
|
+
node: DiagramNode
|
|
76
|
+
parent: DiagramNode | null
|
|
77
|
+
}>()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// namespace to force unique ids
|
|
81
|
+
const ns = ''
|
|
82
|
+
const nodeById = (id: Fqn) => nonNullable(nodeLookup.get(id), `Node not found: ${id}`)
|
|
83
|
+
|
|
84
|
+
let next: typeof traverse[0] | undefined
|
|
85
|
+
while ((next = traverse.pop())) {
|
|
86
|
+
const { node, parent } = next
|
|
87
|
+
if (node.children.length > 0) {
|
|
88
|
+
traverse.unshift(...node.children.map(child => ({ node: nodeById(child), parent: node })))
|
|
89
|
+
}
|
|
90
|
+
const isCompound = hasAtLeast(node.children, 1)
|
|
91
|
+
const position = {
|
|
92
|
+
x: node.position[0],
|
|
93
|
+
y: node.position[1]
|
|
94
|
+
}
|
|
95
|
+
if (parent) {
|
|
96
|
+
position.x -= parent.position[0]
|
|
97
|
+
position.y -= parent.position[1]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// const outEdges = node.outEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
|
|
101
|
+
// const inEdges = node.inEdges.map(e => view.edges.find(edge => edge.id === e)).filter(Boolean)
|
|
102
|
+
|
|
103
|
+
const id = ns + node.id
|
|
104
|
+
editor.nodes.push({
|
|
105
|
+
id,
|
|
106
|
+
type: isCompound ? 'compound' : 'element',
|
|
107
|
+
data: {
|
|
108
|
+
fqn: node.id,
|
|
109
|
+
element: node
|
|
110
|
+
},
|
|
111
|
+
draggable: opts.draggable && (!parent || parent.children.length > 1),
|
|
112
|
+
selectable: opts.selectable,
|
|
113
|
+
deletable: false,
|
|
114
|
+
position,
|
|
115
|
+
zIndex: nodeZIndex(node),
|
|
116
|
+
hidden: false,
|
|
117
|
+
/* initialWidth: node.width,
|
|
118
|
+
initialHeight: node.height, */
|
|
119
|
+
width: node.width,
|
|
120
|
+
height: node.height,
|
|
121
|
+
measured: {
|
|
122
|
+
width: node.width,
|
|
123
|
+
height: node.height
|
|
124
|
+
},
|
|
125
|
+
...(parent && {
|
|
126
|
+
parentId: ns + parent.id
|
|
127
|
+
})
|
|
128
|
+
// handles: [
|
|
129
|
+
// ...outEdges.map(out => ({
|
|
130
|
+
// id: `${out.id}`,
|
|
131
|
+
// type: 'source' as const,
|
|
132
|
+
// position: Position.Bottom,
|
|
133
|
+
// x: out.points[0][0],
|
|
134
|
+
// y: out.points[0][1],
|
|
135
|
+
// width: 10,
|
|
136
|
+
// height: 10
|
|
137
|
+
// })),
|
|
138
|
+
// ...inEdges.map(out => ({
|
|
139
|
+
// id: out.id,
|
|
140
|
+
// type: 'target' as const,
|
|
141
|
+
// position: Position.Top,
|
|
142
|
+
// x: out.points[0][0],
|
|
143
|
+
// y: out.points[0][1],
|
|
144
|
+
// width: 10,
|
|
145
|
+
// height: 10
|
|
146
|
+
// }))
|
|
147
|
+
// ],
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const edge of view.edges) {
|
|
152
|
+
const source = edge.source
|
|
153
|
+
const target = edge.target
|
|
154
|
+
const id = ns + edge.id
|
|
155
|
+
// const points = isElkEdge(edge) ? edge.points : deriveEdgePoints(edge.points)
|
|
156
|
+
const controlPoints = deriveEdgePoints(edge.points)
|
|
157
|
+
// if (edge.tailArrowPoint) {
|
|
158
|
+
// controlPoints.unshift([...edge.tailArrowPoint])
|
|
159
|
+
// }
|
|
160
|
+
// if (edge.headArrowPoint) {
|
|
161
|
+
// controlPoints.push([...edge.headArrowPoint])
|
|
162
|
+
// }
|
|
163
|
+
invariant(hasAtLeast(edge.points, 2), 'edge should have at least 2 points')
|
|
164
|
+
// invariant(hasAtLeast(controlPoints, 2), 'edge controlPoints should have at least 2 points')
|
|
165
|
+
|
|
166
|
+
const level = Math.max(nodeZIndex(nodeById(source)), nodeZIndex(nodeById(target)))
|
|
167
|
+
|
|
168
|
+
editor.edges.push({
|
|
169
|
+
id,
|
|
170
|
+
type: 'relationship',
|
|
171
|
+
source: ns + source,
|
|
172
|
+
target: ns + target,
|
|
173
|
+
zIndex: level,
|
|
174
|
+
deletable: false,
|
|
175
|
+
data: {
|
|
176
|
+
edge,
|
|
177
|
+
type: 'bezier',
|
|
178
|
+
controlPoints,
|
|
179
|
+
headPoint: edge.headArrowPoint ?? null,
|
|
180
|
+
tailPoint: edge.tailArrowPoint ?? null,
|
|
181
|
+
label: edge.labelBBox && edge.label
|
|
182
|
+
? {
|
|
183
|
+
bbox: edge.labelBBox,
|
|
184
|
+
// text: edge.labels ? edge.labels.map(l => l.text).join('\n') : ''
|
|
185
|
+
text: edge.label
|
|
186
|
+
}
|
|
187
|
+
: null
|
|
188
|
+
},
|
|
189
|
+
// markerEnd: {
|
|
190
|
+
// type: MarkerType.ArrowClosed
|
|
191
|
+
// },
|
|
192
|
+
interactionWidth: 20
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return editor
|
|
197
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { invariant, type NonEmptyArray, nonNullable, type Point } from '@likec4/core'
|
|
2
|
+
import { Box } from '@mantine/core'
|
|
3
|
+
import { assignInlineVars } from '@vanilla-extract/dynamic'
|
|
4
|
+
import type { EdgeProps, XYPosition } from '@xyflow/react'
|
|
5
|
+
import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react'
|
|
6
|
+
import clsx from 'clsx'
|
|
7
|
+
import { deepEqual as eq } from 'fast-equals'
|
|
8
|
+
import { type CSSProperties, memo } from 'react'
|
|
9
|
+
import { hasAtLeast } from 'remeda'
|
|
10
|
+
import { useDiagramState } from '../../state'
|
|
11
|
+
import { ZIndexes } from '../const'
|
|
12
|
+
import { useXYStoreApi } from '../hooks'
|
|
13
|
+
import { type XYFlowEdge } from '../types'
|
|
14
|
+
import { toDomPrecision } from '../utils'
|
|
15
|
+
import {
|
|
16
|
+
container,
|
|
17
|
+
cssEdgePath,
|
|
18
|
+
edgeLabel,
|
|
19
|
+
edgeLabelBody,
|
|
20
|
+
edgePathBg,
|
|
21
|
+
fillStrokeCtx,
|
|
22
|
+
varLabelX,
|
|
23
|
+
varLabelY
|
|
24
|
+
} from './edges.css'
|
|
25
|
+
import { getEdgeParams } from './utils'
|
|
26
|
+
// import { getEdgeParams } from './utils'
|
|
27
|
+
|
|
28
|
+
// function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size = 8): string {
|
|
29
|
+
// const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size)
|
|
30
|
+
// const { x, y } = b
|
|
31
|
+
// // no bend
|
|
32
|
+
// if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
|
|
33
|
+
// return `L${x} ${y}`
|
|
34
|
+
// }
|
|
35
|
+
|
|
36
|
+
// // first segment is horizontal
|
|
37
|
+
// if (a.y === y) {
|
|
38
|
+
// const xDir = a.x < c.x ? -1 : 1
|
|
39
|
+
// const yDir = a.y < c.y ? 1 : -1
|
|
40
|
+
// return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`
|
|
41
|
+
// }
|
|
42
|
+
|
|
43
|
+
// const xDir = a.x < c.x ? 1 : -1
|
|
44
|
+
// const yDir = a.y < c.y ? -1 : 1
|
|
45
|
+
// return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`
|
|
46
|
+
// }
|
|
47
|
+
|
|
48
|
+
// const reduceToPath = (path: string, p: XYPosition, i: number, points: XYPosition[]) => {
|
|
49
|
+
// let segment = ''
|
|
50
|
+
// if (i > 0 && i < points.length - 1) {
|
|
51
|
+
// segment = getBend(points[i - 1]!, p, points[i + 1]!)
|
|
52
|
+
// } else {
|
|
53
|
+
// segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`
|
|
54
|
+
// }
|
|
55
|
+
// return path + segment
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
function bezierPath(bezierSpline: NonEmptyArray<Point>) {
|
|
59
|
+
let [start, ...points] = bezierSpline
|
|
60
|
+
invariant(start, 'start should be defined')
|
|
61
|
+
let path = `M ${start[0]},${start[1]}`
|
|
62
|
+
|
|
63
|
+
while (hasAtLeast(points, 3)) {
|
|
64
|
+
const [cp1, cp2, end, ...rest] = points
|
|
65
|
+
path = path + ` C ${cp1[0]},${cp1[1]} ${cp2[0]},${cp2[1]} ${end[0]},${end[1]}`
|
|
66
|
+
points = rest
|
|
67
|
+
}
|
|
68
|
+
invariant(points.length === 0, 'all points should be consumed')
|
|
69
|
+
|
|
70
|
+
return path
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If points are within 2px, consider them the same
|
|
74
|
+
const isSamePoint = (a: number, b: number) => {
|
|
75
|
+
return Math.abs(a - b) < 2.1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isSamePosition = (a: XYPosition, [bx, by]: Point) => {
|
|
79
|
+
return isSamePoint(a.x, bx) && isSamePoint(a.y, by)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>) => (
|
|
83
|
+
prev.id === next.id
|
|
84
|
+
&& prev.source === next.source
|
|
85
|
+
&& prev.target === next.target
|
|
86
|
+
&& isSamePoint(prev.sourceX, next.sourceX)
|
|
87
|
+
&& isSamePoint(prev.sourceY, next.sourceY)
|
|
88
|
+
&& isSamePoint(prev.targetX, next.targetX)
|
|
89
|
+
&& isSamePoint(prev.targetY, next.targetY)
|
|
90
|
+
&& eq(prev.data, next.data)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(function RelationshipEdgeR({
|
|
94
|
+
id,
|
|
95
|
+
data,
|
|
96
|
+
selected: _selected,
|
|
97
|
+
markerEnd: _markerEnd,
|
|
98
|
+
style,
|
|
99
|
+
source,
|
|
100
|
+
target,
|
|
101
|
+
interactionWidth
|
|
102
|
+
}) {
|
|
103
|
+
const { nodeLookup, edgeLookup } = useXYStoreApi().getState()
|
|
104
|
+
const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
|
|
105
|
+
const targetNode = nonNullable(nodeLookup.get(target)!, `target node ${target} not found`)
|
|
106
|
+
|
|
107
|
+
const isNotModified = isSamePosition(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
|
|
108
|
+
&& isSamePosition(targetNode.internals.positionAbsolute, targetNode.data.element.position)
|
|
109
|
+
|
|
110
|
+
invariant(data, 'data is required')
|
|
111
|
+
const {
|
|
112
|
+
edge
|
|
113
|
+
} = data
|
|
114
|
+
// const edgePath = bezierPath(edge.points)
|
|
115
|
+
|
|
116
|
+
const color = edge.color ?? 'gray'
|
|
117
|
+
const isHovered = useDiagramState(s => s.hoveredEdgeId === id)
|
|
118
|
+
|
|
119
|
+
const line = edge.line ?? 'dashed'
|
|
120
|
+
const isDotted = line === 'dotted'
|
|
121
|
+
const isDashed = isDotted || line === 'dashed'
|
|
122
|
+
|
|
123
|
+
let strokeDasharray: string | undefined
|
|
124
|
+
if (isDotted) {
|
|
125
|
+
strokeDasharray = '1,8'
|
|
126
|
+
} else if (isDashed) {
|
|
127
|
+
strokeDasharray = '8,10'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const marker = `url(#arrow-${id})`
|
|
131
|
+
|
|
132
|
+
let edgePath: string, labelX: number, labelY: number
|
|
133
|
+
|
|
134
|
+
if (isNotModified) {
|
|
135
|
+
edgePath = bezierPath(edge.points)
|
|
136
|
+
labelX = data.label?.bbox.x ?? 0
|
|
137
|
+
labelY = data.label?.bbox.y ?? 0
|
|
138
|
+
} else {
|
|
139
|
+
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode)
|
|
140
|
+
|
|
141
|
+
const [_edgePath, _labelX, _labelY] = getBezierPath({
|
|
142
|
+
sourceX: sx,
|
|
143
|
+
sourceY: sy,
|
|
144
|
+
sourcePosition: sourcePos,
|
|
145
|
+
targetPosition: targetPos,
|
|
146
|
+
targetX: tx,
|
|
147
|
+
targetY: ty
|
|
148
|
+
})
|
|
149
|
+
edgePath = _edgePath
|
|
150
|
+
labelX = _labelX
|
|
151
|
+
labelY = _labelY
|
|
152
|
+
}
|
|
153
|
+
// const deferredEdgePath = edgePath
|
|
154
|
+
// useTilg()`
|
|
155
|
+
// ${id}
|
|
156
|
+
// path=${edgePath}
|
|
157
|
+
// isModified=${isModified}
|
|
158
|
+
// `
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<g className={clsx(container)} data-likec4-color={color} data-edge-hovered={isHovered}>
|
|
162
|
+
<g className={clsx(fillStrokeCtx)}>
|
|
163
|
+
<defs>
|
|
164
|
+
<marker
|
|
165
|
+
id={`arrow-${id}`}
|
|
166
|
+
viewBox="0 0 10 8"
|
|
167
|
+
refX="0"
|
|
168
|
+
refY="4"
|
|
169
|
+
markerWidth="5"
|
|
170
|
+
markerHeight="5"
|
|
171
|
+
markerUnits="strokeWidth"
|
|
172
|
+
preserveAspectRatio="xMaxYMid meet"
|
|
173
|
+
orient="auto-start-reverse">
|
|
174
|
+
<path d="M 0 0 L 10 4 L 0 8 z" stroke="context-stroke" fill="context-stroke" />
|
|
175
|
+
</marker>
|
|
176
|
+
</defs>
|
|
177
|
+
</g>
|
|
178
|
+
<RelationshipPath
|
|
179
|
+
edgePath={edgePath}
|
|
180
|
+
interactionWidth={interactionWidth ?? 10}
|
|
181
|
+
strokeDasharray={strokeDasharray}
|
|
182
|
+
markerStart={edge.tailArrow ? marker : undefined}
|
|
183
|
+
markerEnd={edge.headArrow ? marker : undefined}
|
|
184
|
+
style={style} />
|
|
185
|
+
{
|
|
186
|
+
/*
|
|
187
|
+
{controlPoints.map((p, i) => (
|
|
188
|
+
<circle
|
|
189
|
+
className={styles.controlPoint}
|
|
190
|
+
key={i}
|
|
191
|
+
cx={p[0]}
|
|
192
|
+
cy={p[1]}
|
|
193
|
+
r={5}
|
|
194
|
+
/>
|
|
195
|
+
))} */
|
|
196
|
+
}
|
|
197
|
+
{data.label && (
|
|
198
|
+
<EdgeLabelRenderer>
|
|
199
|
+
<Box
|
|
200
|
+
className={clsx(container, edgeLabel)}
|
|
201
|
+
data-likec4-color={color}
|
|
202
|
+
style={{
|
|
203
|
+
...assignInlineVars({
|
|
204
|
+
[varLabelX]: toDomPrecision(labelX) + 'px',
|
|
205
|
+
[varLabelY]: toDomPrecision(labelY) + 'px'
|
|
206
|
+
}),
|
|
207
|
+
maxWidth: data.label.bbox.width + 10,
|
|
208
|
+
zIndex: edgeLookup.get(id)?.zIndex ?? ZIndexes.Edge
|
|
209
|
+
}}
|
|
210
|
+
mod={{
|
|
211
|
+
'data-edge-hovered': isHovered
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
<Box className={edgeLabelBody}>
|
|
215
|
+
{data.label.text}
|
|
216
|
+
</Box>
|
|
217
|
+
{
|
|
218
|
+
/* <Popover
|
|
219
|
+
position="bottom"
|
|
220
|
+
floatingStrategy="fixed"
|
|
221
|
+
shadow="lg"
|
|
222
|
+
disabled={!selected}
|
|
223
|
+
transitionProps={{
|
|
224
|
+
transition: 'pop'
|
|
225
|
+
}}>
|
|
226
|
+
<Popover.Target>
|
|
227
|
+
<Box className={clsx('nodrag nopan', edgeLabelBody)}>
|
|
228
|
+
{data.label.text}
|
|
229
|
+
</Box>
|
|
230
|
+
</Popover.Target>
|
|
231
|
+
<Popover.Dropdown>
|
|
232
|
+
{edge.relations.map((relation) => (
|
|
233
|
+
<Box key={relation}>
|
|
234
|
+
<Text size='xs'>{relation}</Text>
|
|
235
|
+
</Box>
|
|
236
|
+
))}
|
|
237
|
+
<TextInput label="Name" placeholder="Name" size="xs" />
|
|
238
|
+
<TextInput label="Email" placeholder="john@doe.com" size="xs" mt="xs" />
|
|
239
|
+
</Popover.Dropdown>
|
|
240
|
+
</Popover> */
|
|
241
|
+
}
|
|
242
|
+
</Box>
|
|
243
|
+
</EdgeLabelRenderer>
|
|
244
|
+
)}
|
|
245
|
+
</g>
|
|
246
|
+
)
|
|
247
|
+
}, isEqualProps)
|
|
248
|
+
|
|
249
|
+
const RelationshipPath = memo<{
|
|
250
|
+
edgePath: string
|
|
251
|
+
interactionWidth: number
|
|
252
|
+
strokeDasharray: string | undefined
|
|
253
|
+
markerStart: string | undefined
|
|
254
|
+
markerEnd: string | undefined
|
|
255
|
+
style: CSSProperties | undefined
|
|
256
|
+
}>(({
|
|
257
|
+
edgePath,
|
|
258
|
+
interactionWidth,
|
|
259
|
+
strokeDasharray,
|
|
260
|
+
markerStart,
|
|
261
|
+
markerEnd,
|
|
262
|
+
style
|
|
263
|
+
}) => (
|
|
264
|
+
<>
|
|
265
|
+
<path
|
|
266
|
+
className={clsx('react-flow__edge-path', edgePathBg)}
|
|
267
|
+
d={edgePath}
|
|
268
|
+
style={style}
|
|
269
|
+
strokeLinecap={'round'}
|
|
270
|
+
/>
|
|
271
|
+
<path
|
|
272
|
+
className={clsx('react-flow__edge-path', cssEdgePath)}
|
|
273
|
+
d={edgePath}
|
|
274
|
+
style={style}
|
|
275
|
+
strokeLinecap={'round'}
|
|
276
|
+
strokeDasharray={strokeDasharray}
|
|
277
|
+
markerStart={markerStart}
|
|
278
|
+
markerEnd={markerEnd}
|
|
279
|
+
/>
|
|
280
|
+
<path
|
|
281
|
+
className={clsx('react-flow__edge-interaction')}
|
|
282
|
+
d={edgePath}
|
|
283
|
+
fill="none"
|
|
284
|
+
strokeOpacity={0}
|
|
285
|
+
strokeWidth={interactionWidth}
|
|
286
|
+
/>
|
|
287
|
+
</>
|
|
288
|
+
))
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { rem } from '@mantine/core'
|
|
2
|
+
import { createVar, globalStyle, style } from '@vanilla-extract/css'
|
|
3
|
+
import { mantine } from '../../mantine.css'
|
|
4
|
+
import { vars, xyvars } from '../../theme.css'
|
|
5
|
+
|
|
6
|
+
export const container = style({
|
|
7
|
+
vars: {
|
|
8
|
+
[xyvars.edge.stroke]: vars.relation.lineColor,
|
|
9
|
+
[xyvars.edge.strokeSelected]: `color-mix(in srgb, ${vars.relation.lineColor}, white 30%)`,
|
|
10
|
+
[xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 20%)`,
|
|
11
|
+
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 35%)`,
|
|
12
|
+
[xyvars.edge.strokeWidth]: '2.5px'
|
|
13
|
+
},
|
|
14
|
+
selectors: {
|
|
15
|
+
[mantine.darkSelector]: {
|
|
16
|
+
vars: {
|
|
17
|
+
[xyvars.edge.labelColor]: vars.relation.labelColor,
|
|
18
|
+
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 75%)`
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// [mantine.lightSelector]: {
|
|
22
|
+
// // mixBlendMode: 'luminosity',
|
|
23
|
+
// vars: {
|
|
24
|
+
// // [xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, #000 85%)`
|
|
25
|
+
// }
|
|
26
|
+
// }
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
globalStyle(`${container}[data-edge-hovered='true']`, {
|
|
31
|
+
vars: {
|
|
32
|
+
[xyvars.edge.stroke]: `color-mix(in srgb, ${vars.relation.lineColor}, white 30%)`,
|
|
33
|
+
[xyvars.edge.strokeWidth]: '3px'
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
// globalStyle(`${container}[data-edge-hovered='true']`, {
|
|
37
|
+
// vars: {
|
|
38
|
+
// [xyvars.edge.stroke]: `color-mix(in srgb, ${vars.relation.lineColor}, white 35%)`
|
|
39
|
+
// },
|
|
40
|
+
// })
|
|
41
|
+
|
|
42
|
+
globalStyle(`.react-flow__edge.selected ${container}`, {
|
|
43
|
+
vars: {
|
|
44
|
+
[xyvars.edge.strokeWidth]: '2.5'
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
globalStyle(`.react-flow__edges > svg`, {
|
|
49
|
+
mixBlendMode: 'screen'
|
|
50
|
+
})
|
|
51
|
+
globalStyle(`:where([data-mantine-color-scheme="dark"]) .react-flow__edges > svg`, {
|
|
52
|
+
mixBlendMode: 'lighten'
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const edgePathBg = style({
|
|
56
|
+
strokeWidth: `calc(${xyvars.edge.strokeWidth} + 8)`,
|
|
57
|
+
strokeOpacity: 0.1
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export const fillStrokeCtx = style({
|
|
61
|
+
fill: xyvars.edge.stroke,
|
|
62
|
+
stroke: xyvars.edge.stroke,
|
|
63
|
+
selectors: {
|
|
64
|
+
'.react-flow__edge.selected &': {
|
|
65
|
+
vars: {
|
|
66
|
+
[xyvars.edge.stroke]: xyvars.edge.strokeSelected
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const cssEdgePath = style({
|
|
73
|
+
selectors: {
|
|
74
|
+
[`${container}[data-edge-hovered='true'] &`]: {
|
|
75
|
+
transition: 'all 130ms ease-out'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const varLabelX = createVar('label-x')
|
|
81
|
+
export const varLabelY = createVar('label-y')
|
|
82
|
+
|
|
83
|
+
const varTranslate = createVar('translate')
|
|
84
|
+
|
|
85
|
+
export const edgeLabel = style({
|
|
86
|
+
top: 0,
|
|
87
|
+
left: 0,
|
|
88
|
+
fontFamily: vars.likec4.font,
|
|
89
|
+
position: 'absolute',
|
|
90
|
+
pointerEvents: 'all',
|
|
91
|
+
cursor: 'pointer',
|
|
92
|
+
transformOrigin: '50% 50%',
|
|
93
|
+
textAlign: 'left',
|
|
94
|
+
mixBlendMode: 'screen',
|
|
95
|
+
padding: '2px 6px 4px 6px',
|
|
96
|
+
width: 'max-content',
|
|
97
|
+
backgroundColor: xyvars.edge.labelBgColor,
|
|
98
|
+
borderRadius: '3px',
|
|
99
|
+
transform: varTranslate,
|
|
100
|
+
willChange: 'transform',
|
|
101
|
+
vars: {
|
|
102
|
+
// [varTranslate]: `translate(calc(${varLabelX} - 50%), calc(${varLabelY} - 50%))`,
|
|
103
|
+
[varTranslate]: `translate(${varLabelX}, ${varLabelY})`,
|
|
104
|
+
[varLabelX]: '50%',
|
|
105
|
+
[varLabelY]: '50%'
|
|
106
|
+
},
|
|
107
|
+
selectors: {
|
|
108
|
+
[mantine.darkSelector]: {
|
|
109
|
+
mixBlendMode: 'luminosity'
|
|
110
|
+
},
|
|
111
|
+
'&[data-edge-hovered="true"]': {
|
|
112
|
+
transition: 'all 140ms ease-out',
|
|
113
|
+
transform: `${varTranslate} scale(1.1)`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export const edgeLabelBody = style({
|
|
119
|
+
// display: 'inline',
|
|
120
|
+
color: xyvars.edge.labelColor,
|
|
121
|
+
fontSize: rem(12.5),
|
|
122
|
+
lineHeight: 1.12,
|
|
123
|
+
transition: 'color 120ms ease-out'
|
|
124
|
+
})
|