@likec4/diagram 1.2.2 → 1.4.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.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +27 -4
- package/dist/index.mjs +1543 -1346
- package/dist/state/DiagramContext.d.ts +2353 -264
- package/dist/state/diagramStore.d.ts +2360 -268
- package/dist/state/index.d.ts +0 -1
- package/dist/state/useDiagramStore.d.ts +3 -2534
- package/dist/state/useNavigationHistory.d.ts +5 -0
- package/dist/style.css +194 -131
- package/dist/theme.css.d.ts +5 -1
- package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
- package/dist/ui/BackwardForwardPanel.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
- package/dist/xyflow/XYFlow.d.ts +2 -1
- package/dist/xyflow/XYFlowEvents.d.ts +4 -4
- package/dist/xyflow/XYFlowInner.d.ts +2 -1
- package/dist/xyflow/const.d.ts +1 -1
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -5
- package/dist/xyflow/edges/utils.d.ts +4 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/index.d.ts +1 -1
- package/dist/xyflow/types.d.ts +3 -3
- package/dist/xyflow/utils.d.ts +3 -0
- package/package.json +18 -17
- package/src/LikeC4Diagram.css.ts +15 -5
- package/src/LikeC4Diagram.props.ts +33 -5
- package/src/LikeC4Diagram.tsx +103 -32
- package/src/globals.css.ts +1 -2
- package/src/mantine/theme.ts +1 -0
- package/src/state/DiagramContext.tsx +3 -4
- package/src/state/diagramStore.ts +177 -103
- package/src/state/index.ts +0 -1
- package/src/state/useDiagramStore.ts +6 -13
- package/src/state/useNavigationHistory.ts +39 -0
- package/src/theme.css.ts +5 -1
- package/src/ui/BackwardForwardPanel.css.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +59 -0
- package/src/ui/DiagramTitlePanel.css.ts +11 -5
- package/src/ui/DiagramTitlePanel.tsx +108 -96
- package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
- package/src/ui/DynamicViewWalkthrough.tsx +36 -17
- package/src/ui/OptionsPanel.tsx +28 -19
- package/src/ui/options/NodeOptions.tsx +16 -19
- package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
- package/src/xyflow/SyncWithDiagram.tsx +21 -2
- package/src/xyflow/XYFlow.tsx +20 -23
- package/src/xyflow/XYFlowEvents.tsx +26 -8
- package/src/xyflow/XYFlowInner.tsx +10 -6
- package/src/xyflow/const.ts +1 -1
- package/src/xyflow/diagram-to-xyflow.ts +4 -41
- package/src/xyflow/edges/RelationshipEdge.tsx +160 -111
- package/src/xyflow/edges/edges.css.ts +41 -26
- package/src/xyflow/edges/utils.ts +5 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +19 -4
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +45 -37
- package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
- package/src/xyflow/nodes/element/ElementNode.tsx +27 -14
- package/src/xyflow/nodes/element/element.css.ts +31 -18
- package/src/xyflow/nodes/element/index.ts +1 -1
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
- package/src/xyflow/types.ts +5 -2
- package/src/xyflow/utils.ts +48 -0
- package/dist/ui/index.d.ts +0 -2
- package/src/ui/index.ts +0 -3
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { invariant, type NonEmptyArray, nonNullable, type Point } from '@likec4/core'
|
|
2
2
|
import { Box } from '@mantine/core'
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '@react-hookz/web'
|
|
3
4
|
import { assignInlineVars } from '@vanilla-extract/dynamic'
|
|
4
5
|
import type { EdgeProps, XYPosition } from '@xyflow/react'
|
|
5
|
-
import { EdgeLabelRenderer
|
|
6
|
+
import { EdgeLabelRenderer } from '@xyflow/react'
|
|
6
7
|
import { getNodePositionWithOrigin } from '@xyflow/system'
|
|
7
8
|
import clsx from 'clsx'
|
|
8
9
|
import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
|
|
9
10
|
import { deepEqual as eq } from 'fast-equals'
|
|
10
|
-
import {
|
|
11
|
-
import { first, hasAtLeast,
|
|
12
|
-
import { useDiagramState } from '../../state'
|
|
11
|
+
import { memo, useRef, useState } from 'react'
|
|
12
|
+
import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
|
|
13
|
+
import { useDiagramState, useDiagramStoreApi } from '../../state'
|
|
13
14
|
import { ZIndexes } from '../const'
|
|
14
15
|
import { useXYStoreApi } from '../hooks'
|
|
15
16
|
import { type XYFlowEdge } from '../types'
|
|
17
|
+
import { bezierControlPoints, toDomPrecision } from '../utils'
|
|
16
18
|
import * as edgesCss from './edges.css'
|
|
17
|
-
import {
|
|
19
|
+
import { getNodeIntersectionFromCenterToPoint } from './utils'
|
|
18
20
|
// import { getEdgeParams } from './utils'
|
|
19
21
|
|
|
20
22
|
// function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size = 8): string {
|
|
@@ -63,12 +65,20 @@ function bezierPath(bezierSpline: NonEmptyArray<Point>) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// If points are within 3px, consider them the same
|
|
66
|
-
const
|
|
68
|
+
const isSame = (a: number, b: number) => {
|
|
67
69
|
return Math.abs(a - b) < 3.1
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
+
const isSamePoint = (a: XYPosition | Point, b: XYPosition | Point) => {
|
|
73
|
+
const [ax, ay] = isArray(a) ? a : [a.x, a.y]
|
|
74
|
+
const [bx, by] = isArray(b) ? b : [b.x, b.y]
|
|
75
|
+
return isSame(ax, bx) && isSame(ay, by)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sameControlPoints = (a: XYPosition[] | null, b: XYPosition[] | null) => {
|
|
79
|
+
if (a === b) return true
|
|
80
|
+
if (!a || !b || a.length !== b.length) return false
|
|
81
|
+
return a.every((ap, i) => isSamePoint(ap, b[i]!))
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>) => (
|
|
@@ -76,19 +86,19 @@ const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>)
|
|
|
76
86
|
&& prev.source === next.source
|
|
77
87
|
&& prev.target === next.target
|
|
78
88
|
&& eq(prev.selected ?? false, next.selected ?? false)
|
|
79
|
-
&&
|
|
80
|
-
&&
|
|
81
|
-
&&
|
|
82
|
-
&&
|
|
89
|
+
&& isSame(prev.sourceX, next.sourceX)
|
|
90
|
+
&& isSame(prev.sourceY, next.sourceY)
|
|
91
|
+
&& isSame(prev.targetX, next.targetX)
|
|
92
|
+
&& isSame(prev.targetY, next.targetY)
|
|
83
93
|
&& eq(prev.data.stepNum, next.data.stepNum)
|
|
94
|
+
&& sameControlPoints(prev.data.controlPoints, next.data.controlPoints)
|
|
84
95
|
&& eq(prev.data.edge, next.data.edge)
|
|
85
|
-
&& zip(prev.data.controlPoints, next.data.controlPoints).every(([a, b]) =>
|
|
86
|
-
isSamePoint(a[0], b[0]) && isSamePoint(a[1], b[1])
|
|
87
|
-
)
|
|
88
96
|
)
|
|
89
97
|
|
|
90
|
-
const curve = d3line<
|
|
98
|
+
const curve = d3line<XYPosition>()
|
|
91
99
|
.curve(curveCatmullRomOpen)
|
|
100
|
+
.x(d => d.x)
|
|
101
|
+
.y(d => d.y)
|
|
92
102
|
|
|
93
103
|
export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(function RelationshipEdgeR({
|
|
94
104
|
id,
|
|
@@ -99,26 +109,38 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
99
109
|
target,
|
|
100
110
|
interactionWidth
|
|
101
111
|
}) {
|
|
102
|
-
const
|
|
112
|
+
const diagramStore = useDiagramStoreApi()
|
|
113
|
+
const xyflowStore = useXYStoreApi()
|
|
114
|
+
const { isActive, isEditable, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
|
|
115
|
+
isEditable: s.readonly !== true,
|
|
116
|
+
isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true,
|
|
117
|
+
isActive: s.focusedNodeId === source || s.focusedNodeId === target
|
|
118
|
+
|| (s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum),
|
|
119
|
+
isHovered: s.hoveredEdgeId === id,
|
|
120
|
+
isDimmed: s.dimmed.has(id)
|
|
121
|
+
}))
|
|
122
|
+
const { nodeLookup, edgeLookup } = xyflowStore.getState()
|
|
103
123
|
const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
|
|
104
124
|
const targetNode = nonNullable(nodeLookup.get(target)!, `target node ${target} not found`)
|
|
105
125
|
|
|
106
|
-
const isModified =
|
|
107
|
-
||
|
|
126
|
+
const isModified = isEditable && (0
|
|
127
|
+
|| isTruthy(data.controlPoints)
|
|
128
|
+
|| !isSamePoint(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
|
|
129
|
+
|| !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position))
|
|
108
130
|
|
|
109
|
-
const {
|
|
131
|
+
const {
|
|
132
|
+
label,
|
|
133
|
+
edge: {
|
|
134
|
+
points: diagramEdgePoints,
|
|
135
|
+
line = 'dashed',
|
|
136
|
+
color = 'gray',
|
|
137
|
+
...diagramEdge
|
|
138
|
+
}
|
|
139
|
+
} = data
|
|
110
140
|
|
|
111
|
-
|
|
141
|
+
let controlPoints = data.controlPoints ?? bezierControlPoints(data.edge)
|
|
112
142
|
|
|
113
|
-
const color = diagramEdge.color ?? 'gray'
|
|
114
143
|
const isStepEdge = data.stepNum !== null
|
|
115
|
-
const { isActive, isHovered, isDimmed } = useDiagramState(s => ({
|
|
116
|
-
isActive: s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum,
|
|
117
|
-
isHovered: s.hoveredEdgeId === id,
|
|
118
|
-
isDimmed: s.dimmed.has(id)
|
|
119
|
-
}))
|
|
120
|
-
|
|
121
|
-
const line = diagramEdge.line ?? 'dashed'
|
|
122
144
|
const isDotted = line === 'dotted'
|
|
123
145
|
const isDashed = isDotted || line === 'dashed'
|
|
124
146
|
|
|
@@ -129,45 +151,92 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
129
151
|
strokeDasharray = '8,10'
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
|
|
154
|
+
let labelX = label?.bbox.x ?? 0,
|
|
155
|
+
labelY = label?.bbox.y ?? 0
|
|
156
|
+
|
|
157
|
+
const [labelPos, setLabelPosition] = useState<XYPosition>({
|
|
158
|
+
x: labelX,
|
|
159
|
+
y: labelY
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
let edgePath: string
|
|
133
163
|
|
|
134
|
-
|
|
164
|
+
if (isModified) {
|
|
165
|
+
labelX = labelPos.x
|
|
166
|
+
labelY = labelPos.y
|
|
167
|
+
const sourceCenterPos = getNodePositionWithOrigin(sourceNode, [-0.5, -0.5])
|
|
168
|
+
const targetCenterPos = getNodePositionWithOrigin(targetNode, [-0.5, -0.5])
|
|
169
|
+
|
|
170
|
+
const points = [
|
|
171
|
+
sourceCenterPos.positionAbsolute,
|
|
172
|
+
getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints)!),
|
|
173
|
+
...controlPoints,
|
|
174
|
+
getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints)!),
|
|
175
|
+
targetCenterPos.positionAbsolute
|
|
176
|
+
]
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
|
|
178
|
+
edgePath = nonNullable(curve(points))
|
|
179
|
+
} else {
|
|
180
|
+
edgePath = bezierPath(diagramEdgePoints)
|
|
138
181
|
// if (diagramEdge.tailArrowPoint) {
|
|
139
182
|
// edgePath = `M ${diagramEdge.tailArrowPoint[0]},${diagramEdge.tailArrowPoint[1]} L ${edgePath.substring(1)}`
|
|
140
183
|
// }
|
|
141
184
|
// if (diagramEdge.headArrowPoint) {
|
|
142
185
|
// edgePath += ` L ${diagramEdge.headArrowPoint[0]},${diagramEdge.headArrowPoint[1]}`
|
|
143
186
|
// }
|
|
144
|
-
|
|
145
|
-
labelY = data.label?.bbox.y ?? 0
|
|
146
|
-
} else {
|
|
147
|
-
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode)
|
|
187
|
+
}
|
|
148
188
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
const svgPathRef = useRef<SVGPathElement>(null)
|
|
190
|
+
useIsomorphicLayoutEffect(() => {
|
|
191
|
+
const path = svgPathRef.current
|
|
192
|
+
if (!path) return
|
|
193
|
+
const dompoint = path.getPointAtLength(path.getTotalLength() * 0.5)
|
|
194
|
+
const point = {
|
|
195
|
+
x: Math.round(dompoint.x),
|
|
196
|
+
y: Math.round(dompoint.y)
|
|
197
|
+
}
|
|
198
|
+
setLabelPosition(current => isSamePoint(current, point) ? current : point)
|
|
199
|
+
}, [edgePath])
|
|
160
200
|
|
|
161
|
-
|
|
162
|
-
const
|
|
201
|
+
const onControlPointerDown = (index: number, e: React.PointerEvent) => {
|
|
202
|
+
const { domNode } = xyflowStore.getState()
|
|
203
|
+
if (!domNode) return
|
|
204
|
+
const { xyflow } = diagramStore.getState()
|
|
205
|
+
e.stopPropagation()
|
|
206
|
+
let hasMoved = false
|
|
207
|
+
let pointer = { x: e.clientX, y: e.clientY }
|
|
208
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
209
|
+
if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
|
|
210
|
+
if (!hasMoved) {
|
|
211
|
+
diagramStore.getState().cancelSaveManualLayout()
|
|
212
|
+
}
|
|
213
|
+
hasMoved = true
|
|
214
|
+
pointer = { x: e.clientX, y: e.clientY }
|
|
215
|
+
const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
|
|
216
|
+
const newControlPoints = controlPoints.slice()
|
|
217
|
+
newControlPoints[index] = {
|
|
218
|
+
x: toDomPrecision(x),
|
|
219
|
+
y: toDomPrecision(y)
|
|
220
|
+
}
|
|
221
|
+
xyflow.updateEdgeData(id, { controlPoints: newControlPoints })
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const onPointerUp = () => {
|
|
225
|
+
domNode.removeEventListener('pointermove', onPointerMove)
|
|
226
|
+
domNode.removeEventListener('pointerup', onPointerUp)
|
|
227
|
+
if (hasMoved) {
|
|
228
|
+
diagramStore.getState().triggerSaveManualLayout(xyflowStore)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
domNode.addEventListener('pointermove', onPointerMove)
|
|
232
|
+
domNode.addEventListener('pointerup', onPointerUp, { once: true })
|
|
233
|
+
}
|
|
163
234
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
[targetCenterPos.positionAbsolute.x, targetCenterPos.positionAbsolute.y]
|
|
170
|
-
])!
|
|
235
|
+
const marker = `url(#arrow-${id})`
|
|
236
|
+
let markerStart = diagramEdge.tailArrow ? marker : undefined
|
|
237
|
+
let markerEnd = diagramEdge.headArrow ? marker : undefined
|
|
238
|
+
if (isModified && diagramEdge.dir === 'back') {
|
|
239
|
+
;[markerStart, markerEnd] = [markerEnd, markerStart]
|
|
171
240
|
}
|
|
172
241
|
|
|
173
242
|
return (
|
|
@@ -180,12 +249,20 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
180
249
|
data-edge-dir={diagramEdge.dir}
|
|
181
250
|
data-edge-active={isActive}
|
|
182
251
|
data-edge-hovered={isHovered}>
|
|
252
|
+
<path
|
|
253
|
+
className={clsx('react-flow__edge-interaction')}
|
|
254
|
+
d={edgePath}
|
|
255
|
+
fill="none"
|
|
256
|
+
stroke={'transparent'}
|
|
257
|
+
strokeWidth={interactionWidth ?? 10}
|
|
258
|
+
/>
|
|
183
259
|
<g className={edgesCss.markerContext}>
|
|
184
260
|
<defs>
|
|
185
261
|
<marker
|
|
186
262
|
id={`arrow-${id}`}
|
|
187
263
|
viewBox="0 0 16 10"
|
|
188
|
-
|
|
264
|
+
// TODO: graphviz cut bezier path at arrow, we don't
|
|
265
|
+
refX={isModified ? '15' : '1'}
|
|
189
266
|
refY="5"
|
|
190
267
|
markerWidth="5"
|
|
191
268
|
markerHeight="4"
|
|
@@ -195,20 +272,30 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
195
272
|
<path d="M 0 0 L 16 5 L 0 10 z" stroke="context-stroke" fill="context-stroke" />
|
|
196
273
|
</marker>
|
|
197
274
|
</defs>
|
|
275
|
+
<path
|
|
276
|
+
className={clsx('react-flow__edge-path', edgesCss.edgePathBg)}
|
|
277
|
+
d={edgePath}
|
|
278
|
+
style={style}
|
|
279
|
+
strokeLinecap={'round'}
|
|
280
|
+
/>
|
|
281
|
+
<path
|
|
282
|
+
ref={svgPathRef}
|
|
283
|
+
className={clsx('react-flow__edge-path', edgesCss.cssEdgePath)}
|
|
284
|
+
d={edgePath}
|
|
285
|
+
style={style}
|
|
286
|
+
strokeLinecap={'round'}
|
|
287
|
+
strokeDasharray={strokeDasharray}
|
|
288
|
+
markerStart={markerStart}
|
|
289
|
+
markerEnd={markerEnd}
|
|
290
|
+
/>
|
|
198
291
|
</g>
|
|
199
|
-
|
|
200
|
-
edgePath={edgePath}
|
|
201
|
-
interactionWidth={interactionWidth ?? 10}
|
|
202
|
-
strokeDasharray={strokeDasharray}
|
|
203
|
-
markerStart={diagramEdge.tailArrow ? marker : undefined}
|
|
204
|
-
markerEnd={diagramEdge.headArrow ? marker : undefined}
|
|
205
|
-
style={style} />
|
|
206
|
-
{isModified && controlPoints.map((p, i) => (
|
|
292
|
+
{isEdgePathEditable && controlPoints.map((p, i) => (
|
|
207
293
|
<circle
|
|
294
|
+
onPointerDown={e => onControlPointerDown(i, e)}
|
|
208
295
|
className={edgesCss.controlPoint}
|
|
209
296
|
key={i}
|
|
210
|
-
cx={p
|
|
211
|
-
cy={p
|
|
297
|
+
cx={p.x}
|
|
298
|
+
cy={p.y}
|
|
212
299
|
r={3}
|
|
213
300
|
/>
|
|
214
301
|
))}
|
|
@@ -223,8 +310,11 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
223
310
|
data-likec4-color={color}
|
|
224
311
|
style={{
|
|
225
312
|
...assignInlineVars({
|
|
226
|
-
[edgesCss.varLabelX]:
|
|
227
|
-
[edgesCss.varLabelY]:
|
|
313
|
+
[edgesCss.varLabelX]: isModified ? `calc(${labelX}px - 10%)` : `${labelX}px`,
|
|
314
|
+
[edgesCss.varLabelY]: isModified ? `${labelY - 5}px` : `${labelY}px`
|
|
315
|
+
}),
|
|
316
|
+
...(isEdgePathEditable && {
|
|
317
|
+
pointerEvents: 'none'
|
|
228
318
|
}),
|
|
229
319
|
...(data.label && {
|
|
230
320
|
maxWidth: data.label.bbox.width + 12
|
|
@@ -277,44 +367,3 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
277
367
|
</g>
|
|
278
368
|
)
|
|
279
369
|
}, isEqualProps)
|
|
280
|
-
|
|
281
|
-
const RelationshipPath = ({
|
|
282
|
-
edgePath,
|
|
283
|
-
interactionWidth,
|
|
284
|
-
strokeDasharray,
|
|
285
|
-
markerStart,
|
|
286
|
-
markerEnd,
|
|
287
|
-
style
|
|
288
|
-
}: {
|
|
289
|
-
edgePath: string
|
|
290
|
-
interactionWidth: number
|
|
291
|
-
strokeDasharray: string | undefined
|
|
292
|
-
markerStart: string | undefined
|
|
293
|
-
markerEnd: string | undefined
|
|
294
|
-
style: CSSProperties | undefined
|
|
295
|
-
}) => (
|
|
296
|
-
<>
|
|
297
|
-
<path
|
|
298
|
-
className={clsx('react-flow__edge-path', edgesCss.edgePathBg)}
|
|
299
|
-
d={edgePath}
|
|
300
|
-
style={style}
|
|
301
|
-
strokeLinecap={'round'}
|
|
302
|
-
/>
|
|
303
|
-
<path
|
|
304
|
-
className={clsx('react-flow__edge-path', edgesCss.cssEdgePath)}
|
|
305
|
-
d={edgePath}
|
|
306
|
-
style={style}
|
|
307
|
-
strokeLinecap={'round'}
|
|
308
|
-
strokeDasharray={strokeDasharray}
|
|
309
|
-
markerStart={markerStart}
|
|
310
|
-
markerEnd={markerEnd}
|
|
311
|
-
/>
|
|
312
|
-
<path
|
|
313
|
-
className={clsx('react-flow__edge-interaction')}
|
|
314
|
-
d={edgePath}
|
|
315
|
-
fill="none"
|
|
316
|
-
strokeOpacity={0}
|
|
317
|
-
strokeWidth={interactionWidth}
|
|
318
|
-
/>
|
|
319
|
-
</>
|
|
320
|
-
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { rem } from '@mantine/core'
|
|
2
|
-
import { createVar, fallbackVar,
|
|
2
|
+
import { createVar, fallbackVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
|
|
3
|
+
import { mantine } from '../../mantine.css'
|
|
3
4
|
import { vars, xyvars } from '../../theme.css'
|
|
4
5
|
|
|
5
6
|
const mixColor = createVar('mix-color')
|
|
@@ -15,13 +16,6 @@ export const container = style({
|
|
|
15
16
|
}
|
|
16
17
|
})
|
|
17
18
|
|
|
18
|
-
export const dimmed = style({})
|
|
19
|
-
|
|
20
|
-
globalStyle(`svg:has(${dimmed})`, {
|
|
21
|
-
filter: vars.filterDimmed,
|
|
22
|
-
transition: 'filter 800ms ease-out'
|
|
23
|
-
})
|
|
24
|
-
|
|
25
19
|
globalStyle(`:where([data-mantine-color-scheme="dark"]) ${container}`, {
|
|
26
20
|
vars: {
|
|
27
21
|
[mixColor]: `white`,
|
|
@@ -61,9 +55,16 @@ globalStyle(`:where(${isSelected}) ${container}[data-edge-hovered='true']`, {
|
|
|
61
55
|
globalStyle(`.react-flow__edges > svg`, {
|
|
62
56
|
mixBlendMode: 'screen'
|
|
63
57
|
})
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
|
|
59
|
+
export const dimmed = style({})
|
|
60
|
+
|
|
61
|
+
globalStyle(`.react-flow__edges > svg:has(${dimmed})`, {
|
|
62
|
+
opacity: 0.8,
|
|
63
|
+
transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
|
|
64
|
+
transitionDelay: '200ms',
|
|
65
|
+
filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
|
|
66
|
+
willChange: 'opacity, filter'
|
|
67
|
+
})
|
|
67
68
|
|
|
68
69
|
export const edgePathBg = style({
|
|
69
70
|
strokeWidth: xyvars.edge.strokeWidth,
|
|
@@ -84,8 +85,7 @@ export const edgePathBg = style({
|
|
|
84
85
|
// To fix issue with marker not inheriting color from path - we need to create container
|
|
85
86
|
export const markerContext = style({
|
|
86
87
|
fill: xyvars.edge.stroke,
|
|
87
|
-
stroke: xyvars.edge.stroke
|
|
88
|
-
pointerEvents: 'none'
|
|
88
|
+
stroke: xyvars.edge.stroke
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
export const controlPoint = style({
|
|
@@ -94,18 +94,25 @@ export const controlPoint = style({
|
|
|
94
94
|
fillOpacity: 0.75,
|
|
95
95
|
strokeWidth: 1,
|
|
96
96
|
cursor: 'grab',
|
|
97
|
-
pointerEvents: '
|
|
97
|
+
pointerEvents: 'auto',
|
|
98
|
+
visibility: 'hidden',
|
|
99
|
+
':hover': {
|
|
100
|
+
stroke: mantine.colors.primaryColors.filledHover,
|
|
101
|
+
strokeWidth: 9,
|
|
102
|
+
transition: 'stroke 100ms ease-out, stroke-width 100ms ease-out'
|
|
103
|
+
},
|
|
98
104
|
selectors: {
|
|
99
|
-
[`:where(${isSelected}, [data-edge-
|
|
100
|
-
|
|
105
|
+
[`:where(${isSelected}, [data-edge-hovered='true']) &`]: {
|
|
106
|
+
visibility: 'visible',
|
|
107
|
+
transition: 'fill-opacity 150ms ease-out, stroke 150ms ease-out, stroke-width 150ms ease-out',
|
|
108
|
+
transitionDelay: '50ms',
|
|
101
109
|
fillOpacity: 1,
|
|
102
|
-
strokeWidth:
|
|
110
|
+
strokeWidth: 5
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
})
|
|
106
114
|
|
|
107
|
-
const strokeKeyframes =
|
|
108
|
-
globalKeyframes(strokeKeyframes, {
|
|
115
|
+
const strokeKeyframes = keyframes({
|
|
109
116
|
'from': {
|
|
110
117
|
strokeDashoffset: 18 * 2
|
|
111
118
|
},
|
|
@@ -118,19 +125,24 @@ export const cssEdgePath = style({
|
|
|
118
125
|
animationDuration: '800ms',
|
|
119
126
|
animationIterationCount: 'infinite',
|
|
120
127
|
animationTimingFunction: 'linear',
|
|
128
|
+
animationName: strokeKeyframes,
|
|
129
|
+
animationPlayState: 'paused',
|
|
121
130
|
selectors: {
|
|
122
131
|
[`:where([data-edge-hovered='true']) &`]: {
|
|
123
|
-
|
|
132
|
+
animationPlayState: 'running',
|
|
124
133
|
animationDelay: '350ms',
|
|
125
|
-
transition: '
|
|
134
|
+
transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
|
|
126
135
|
},
|
|
127
136
|
[`:where(${isSelected}, [data-edge-active='true']) &`]: {
|
|
128
|
-
|
|
137
|
+
animationPlayState: 'running',
|
|
129
138
|
animationDelay: '0ms',
|
|
130
|
-
transition: '
|
|
139
|
+
transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
|
|
131
140
|
},
|
|
132
141
|
[`:where([data-edge-dir='back']) &`]: {
|
|
133
142
|
animationDirection: 'reverse'
|
|
143
|
+
},
|
|
144
|
+
[`${dimmed} &`]: {
|
|
145
|
+
animationPlayState: 'paused'
|
|
134
146
|
}
|
|
135
147
|
}
|
|
136
148
|
})
|
|
@@ -154,7 +166,7 @@ export const edgeLabel = style({
|
|
|
154
166
|
borderRadius: '3px',
|
|
155
167
|
transform: varTranslate,
|
|
156
168
|
vars: {
|
|
157
|
-
[varTranslate]: `translate(${fallbackVar(varLabelX, '50%')}, ${fallbackVar(varLabelY, '50%')})`
|
|
169
|
+
[varTranslate]: `translate(${fallbackVar(varLabelX, '-50%')}, ${fallbackVar(varLabelY, '-50%')})`
|
|
158
170
|
},
|
|
159
171
|
selectors: {
|
|
160
172
|
'&[data-edge-hovered="true"]': {
|
|
@@ -162,8 +174,11 @@ export const edgeLabel = style({
|
|
|
162
174
|
transform: `${varTranslate} scale(1.1)`
|
|
163
175
|
},
|
|
164
176
|
[`&:is(${dimmed})`]: {
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
opacity: 0.6,
|
|
178
|
+
transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
|
|
179
|
+
transitionDelay: '200ms',
|
|
180
|
+
filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
|
|
181
|
+
willChange: 'opacity, filter'
|
|
167
182
|
}
|
|
168
183
|
}
|
|
169
184
|
})
|
|
@@ -5,7 +5,10 @@ import type { InternalXYFlowNode } from '../types'
|
|
|
5
5
|
|
|
6
6
|
// this helper function returns the intersection point
|
|
7
7
|
// of the line between the center of the intersectionNode and the target node
|
|
8
|
-
export function getNodeIntersectionFromCenterToPoint(
|
|
8
|
+
export function getNodeIntersectionFromCenterToPoint(
|
|
9
|
+
intersectionNode: InternalXYFlowNode,
|
|
10
|
+
{ x: x1, y: y1 }: XYPosition
|
|
11
|
+
) {
|
|
9
12
|
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
|
10
13
|
const {
|
|
11
14
|
width: intersectionNodeWidth,
|
|
@@ -30,7 +33,7 @@ export function getNodeIntersectionFromCenterToPoint(intersectionNode: InternalX
|
|
|
30
33
|
const x = w * (xx3 + yy3) + x2
|
|
31
34
|
const y = h * (-xx3 + yy3) + y2
|
|
32
35
|
|
|
33
|
-
return
|
|
36
|
+
return { x, y }
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// this helper function returns the intersection point
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { invariant, isAncestor, nonNullable } from '@likec4/core'
|
|
2
2
|
import { Expression, Expression as Expr, Operator, Solver, Strength, Variable } from '@lume/kiwi'
|
|
3
|
+
import { useDebouncedCallback } from '@react-hookz/web'
|
|
3
4
|
import type { InternalNode, ReactFlowProps, XYPosition } from '@xyflow/react'
|
|
4
5
|
import { getNodeDimensions } from '@xyflow/system'
|
|
5
6
|
import { useMemo, useRef } from 'react'
|
|
6
7
|
import { isNullish } from 'remeda'
|
|
7
8
|
import { useDiagramStoreApi } from '../../state'
|
|
8
9
|
import { type XYFlowInstance, XYFlowNode } from '../types'
|
|
10
|
+
import { isSamePoint } from '../utils'
|
|
9
11
|
import { useXYStoreApi, type XYStoreApi } from './useXYFlow'
|
|
10
12
|
|
|
11
13
|
abstract class Rect {
|
|
@@ -273,17 +275,30 @@ export function useLayoutConstraints(): LayoutConstraints {
|
|
|
273
275
|
const xyflowApi = useXYStoreApi()
|
|
274
276
|
const solverRef = useRef<ReturnType<typeof createLayoutConstraints>>()
|
|
275
277
|
|
|
278
|
+
const dragStartedAt = useRef({
|
|
279
|
+
x: 0,
|
|
280
|
+
y: 0
|
|
281
|
+
})
|
|
282
|
+
|
|
276
283
|
return useMemo((): LayoutConstraints => ({
|
|
277
|
-
onNodeDragStart: (
|
|
278
|
-
|
|
284
|
+
onNodeDragStart: (event, xynode) => {
|
|
285
|
+
dragStartedAt.current = {
|
|
286
|
+
x: event.clientX,
|
|
287
|
+
y: event.clientY
|
|
288
|
+
}
|
|
289
|
+
const { xyflow, cancelSaveManualLayout } = diagramApi.getState()
|
|
290
|
+
cancelSaveManualLayout()
|
|
279
291
|
solverRef.current = createLayoutConstraints(xyflow, xyflowApi, xynode.id)
|
|
280
292
|
},
|
|
281
293
|
onNodeDrag: (_event, xynode) => {
|
|
282
294
|
invariant(solverRef.current, 'solverRef.current should be defined')
|
|
283
295
|
solverRef.current?.onNodeDrag(xynode)
|
|
284
296
|
},
|
|
285
|
-
onNodeDragStop: (
|
|
297
|
+
onNodeDragStop: (event, _xynode) => {
|
|
298
|
+
if (!isSamePoint(dragStartedAt.current, { x: event.clientX, y: event.clientY })) {
|
|
299
|
+
diagramApi.getState().triggerSaveManualLayout(xyflowApi)
|
|
300
|
+
}
|
|
286
301
|
solverRef.current = undefined
|
|
287
302
|
}
|
|
288
|
-
}), [xyflowApi])
|
|
303
|
+
}), [xyflowApi, diagramApi])
|
|
289
304
|
}
|