@likec4/diagram 1.4.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.
- package/dist/LikeC4Diagram.css.d.ts +0 -1
- package/dist/LikeC4Diagram.props.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1398 -1114
- package/dist/state/DiagramContext.d.ts +86 -4159
- package/dist/state/diagramStore.d.ts +88 -4162
- package/dist/state/hooks.d.ts +4 -0
- package/dist/state/index.d.ts +1 -2
- package/dist/style.css +182 -156
- package/dist/ui/options/EdgeOptions.d.ts +19 -0
- package/dist/ui/options/NodeOptions.d.ts +2 -2
- package/dist/xyflow/EdgeMarkers.d.ts +11 -0
- package/dist/xyflow/XYFlowEvents.d.ts +8 -5
- package/dist/xyflow/const.d.ts +3 -3
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -2
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/dist/xyflow/edges/utils.d.ts +1 -0
- package/dist/xyflow/hooks/index.d.ts +3 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +13 -12
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
- package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
- package/dist/xyflow/types.d.ts +2 -32
- package/package.json +19 -19
- package/src/LikeC4Diagram.css.ts +0 -8
- package/src/LikeC4Diagram.props.ts +2 -2
- package/src/LikeC4Diagram.tsx +14 -20
- package/src/globals.css.ts +10 -3
- package/src/index.ts +1 -1
- package/src/mantine.css.ts +2 -2
- package/src/state/DiagramContext.tsx +44 -13
- package/src/state/diagramStore.ts +64 -52
- package/src/state/hooks.ts +5 -0
- package/src/state/index.ts +1 -2
- package/src/ui/BackwardForwardPanel.css.ts +9 -9
- package/src/ui/BackwardForwardPanel.tsx +1 -1
- package/src/ui/DiagramTitlePanel.css.ts +7 -2
- package/src/ui/DiagramTitlePanel.tsx +5 -3
- package/src/ui/DynamicViewWalkthrough.tsx +6 -4
- package/src/ui/OptionsPanel.tsx +37 -27
- package/src/ui/options/EdgeOptions.tsx +157 -0
- package/src/ui/options/NodeOptions.tsx +37 -29
- package/src/xyflow/EdgeMarkers.tsx +148 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
- package/src/xyflow/SyncWithDiagram.tsx +13 -20
- package/src/xyflow/XYFlow.tsx +2 -4
- package/src/xyflow/XYFlowEvents.tsx +48 -33
- package/src/xyflow/XYFlowInner.tsx +1 -1
- package/src/xyflow/const.ts +3 -3
- package/src/xyflow/diagram-to-xyflow.ts +17 -41
- package/src/xyflow/edges/RelationshipEdge.tsx +207 -118
- package/src/xyflow/edges/edges.css.ts +36 -22
- package/src/xyflow/edges/utils.ts +10 -0
- package/src/xyflow/hooks/index.ts +4 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +3 -4
- package/src/xyflow/hooks/useXYFlow.ts +28 -1
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +34 -24
- package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
- package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
- package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
- package/src/xyflow/nodes/element/ElementNode.tsx +64 -108
- package/src/xyflow/nodes/element/element.css.ts +30 -10
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
- package/src/xyflow/types.ts +2 -4
- package/src/xyflow/utils.ts +3 -6
- package/dist/KeepAspectRatioContainer.d.ts +0 -9
- package/dist/icons/Clipboard.d.ts +0 -3
- package/dist/icons/Link.d.ts +0 -3
- package/dist/icons/ZoomIn.d.ts +0 -3
- package/dist/icons/index.d.ts +0 -3
- package/dist/mantine/theme.d.ts +0 -3
- package/dist/state/WhenInitialized.d.ts +0 -3
- package/dist/ui/Camera.d.ts +0 -3
- package/src/KeepAspectRatioContainer.tsx +0 -36
- package/src/icons/Clipboard.tsx +0 -24
- package/src/icons/Link.tsx +0 -23
- package/src/icons/ZoomIn.tsx +0 -25
- package/src/icons/index.ts +0 -3
- package/src/mantine/theme.ts +0 -20
- package/src/state/WhenInitialized.tsx +0 -24
- package/src/ui/Camera.tsx +0 -174
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import {
|
|
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'
|
|
21
|
+
import { EdgeMarkers } from '../EdgeMarkers'
|
|
15
22
|
import { useXYStoreApi } from '../hooks'
|
|
16
|
-
import { type XYFlowEdge } from '../types'
|
|
17
|
-
import { bezierControlPoints
|
|
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
|
|
87
|
-
&& prev.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
|
|
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 =
|
|
168
|
-
const targetCenterPos =
|
|
205
|
+
const sourceCenterPos = { x: sourceX, y: sourceY }
|
|
206
|
+
const targetCenterPos = { x: targetX, y: targetY }
|
|
169
207
|
|
|
170
|
-
const points =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,15 +244,31 @@ 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:
|
|
247
|
+
const onControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>) => {
|
|
202
248
|
const { domNode } = xyflowStore.getState()
|
|
203
|
-
if (!domNode)
|
|
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)
|
|
210
272
|
if (!hasMoved) {
|
|
211
273
|
diagramStore.getState().cancelSaveManualLayout()
|
|
212
274
|
}
|
|
@@ -215,35 +277,39 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
215
277
|
const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
|
|
216
278
|
const newControlPoints = controlPoints.slice()
|
|
217
279
|
newControlPoints[index] = {
|
|
218
|
-
x:
|
|
219
|
-
y:
|
|
280
|
+
x: Math.round(x),
|
|
281
|
+
y: Math.round(y)
|
|
220
282
|
}
|
|
221
283
|
xyflow.updateEdgeData(id, { controlPoints: newControlPoints })
|
|
222
284
|
}
|
|
223
285
|
}
|
|
224
286
|
const onPointerUp = () => {
|
|
287
|
+
setIsControlPointDragging(false)
|
|
225
288
|
domNode.removeEventListener('pointermove', onPointerMove)
|
|
226
289
|
domNode.removeEventListener('pointerup', onPointerUp)
|
|
227
290
|
if (hasMoved) {
|
|
228
|
-
diagramStore.getState().triggerSaveManualLayout(
|
|
291
|
+
diagramStore.getState().triggerSaveManualLayout()
|
|
229
292
|
}
|
|
230
293
|
}
|
|
231
294
|
domNode.addEventListener('pointermove', onPointerMove)
|
|
232
295
|
domNode.addEventListener('pointerup', onPointerUp, { once: true })
|
|
233
296
|
}
|
|
234
297
|
|
|
235
|
-
|
|
236
|
-
let
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
;[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]
|
|
240
302
|
}
|
|
241
303
|
|
|
304
|
+
const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null
|
|
305
|
+
const MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null
|
|
306
|
+
|
|
242
307
|
return (
|
|
243
308
|
<g
|
|
244
309
|
className={clsx([
|
|
245
310
|
edgesCss.container,
|
|
246
|
-
isDimmed && edgesCss.dimmed
|
|
311
|
+
isDimmed && edgesCss.dimmed,
|
|
312
|
+
isControlPointDragging && edgesCss.controlDragging
|
|
247
313
|
])}
|
|
248
314
|
data-likec4-color={color}
|
|
249
315
|
data-edge-dir={diagramEdge.dir}
|
|
@@ -258,19 +324,8 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
258
324
|
/>
|
|
259
325
|
<g className={edgesCss.markerContext}>
|
|
260
326
|
<defs>
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
viewBox="0 0 16 10"
|
|
264
|
-
// TODO: graphviz cut bezier path at arrow, we don't
|
|
265
|
-
refX={isModified ? '15' : '1'}
|
|
266
|
-
refY="5"
|
|
267
|
-
markerWidth="5"
|
|
268
|
-
markerHeight="4"
|
|
269
|
-
markerUnits="strokeWidth"
|
|
270
|
-
preserveAspectRatio="xMaxYMid meet"
|
|
271
|
-
orient="auto-start-reverse">
|
|
272
|
-
<path d="M 0 0 L 16 5 L 0 10 z" stroke="context-stroke" fill="context-stroke" />
|
|
273
|
-
</marker>
|
|
327
|
+
{MarkerStart && <MarkerStart id={'start' + id} />}
|
|
328
|
+
{MarkerEnd && <MarkerEnd id={'end' + id} />}
|
|
274
329
|
</defs>
|
|
275
330
|
<path
|
|
276
331
|
className={clsx('react-flow__edge-path', edgesCss.edgePathBg)}
|
|
@@ -285,85 +340,119 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
285
340
|
style={style}
|
|
286
341
|
strokeLinecap={'round'}
|
|
287
342
|
strokeDasharray={strokeDasharray}
|
|
288
|
-
markerStart={
|
|
289
|
-
markerEnd={
|
|
343
|
+
markerStart={MarkerStart ? `url(#start${id})` : undefined}
|
|
344
|
+
markerEnd={MarkerEnd ? `url(#end${id})` : undefined}
|
|
290
345
|
/>
|
|
291
346
|
</g>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
)}
|
|
302
366
|
{(data.label || isStepEdge) && (
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
...(data.label && {
|
|
320
|
-
maxWidth: data.label.bbox.width + 12
|
|
321
|
-
}),
|
|
322
|
-
zIndex: edgeLookup.get(id)?.zIndex ?? ZIndexes.Edge
|
|
323
|
-
}}
|
|
324
|
-
mod={{
|
|
325
|
-
'data-edge-hovered': isHovered,
|
|
326
|
-
'data-edge-active': isActive
|
|
327
|
-
}}
|
|
328
|
-
>
|
|
329
|
-
{isStepEdge && (
|
|
330
|
-
<Box className={edgesCss.stepEdgeNumber}>
|
|
331
|
-
{data.stepNum}
|
|
332
|
-
</Box>
|
|
333
|
-
)}
|
|
334
|
-
{data.label?.text && (
|
|
335
|
-
<Box className={edgesCss.edgeLabelText}>
|
|
336
|
-
{data.label.text}
|
|
337
|
-
</Box>
|
|
338
|
-
)}
|
|
339
|
-
{
|
|
340
|
-
/* <Popover
|
|
341
|
-
position="bottom"
|
|
342
|
-
floatingStrategy="fixed"
|
|
343
|
-
shadow="lg"
|
|
344
|
-
disabled={!selected}
|
|
345
|
-
transitionProps={{
|
|
346
|
-
transition: 'pop'
|
|
347
|
-
}}>
|
|
348
|
-
<Popover.Target>
|
|
349
|
-
<Box className={clsx('nodrag nopan', edgeLabelBody)}>
|
|
350
|
-
{data.label.text}
|
|
351
|
-
</Box>
|
|
352
|
-
</Popover.Target>
|
|
353
|
-
<Popover.Dropdown>
|
|
354
|
-
{edge.relations.map((relation) => (
|
|
355
|
-
<Box key={relation}>
|
|
356
|
-
<Text size='xs'>{relation}</Text>
|
|
357
|
-
</Box>
|
|
358
|
-
))}
|
|
359
|
-
<TextInput label="Name" placeholder="Name" size="xs" />
|
|
360
|
-
<TextInput label="Email" placeholder="john@doe.com" size="xs" mt="xs" />
|
|
361
|
-
</Popover.Dropdown>
|
|
362
|
-
</Popover> */
|
|
363
|
-
}
|
|
364
|
-
</Box>
|
|
365
|
-
</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
|
+
})} />
|
|
366
383
|
)}
|
|
367
384
|
</g>
|
|
368
385
|
)
|
|
369
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)
|
|
@@ -11,8 +11,8 @@ export const container = style({
|
|
|
11
11
|
[xyvars.edge.stroke]: vars.relation.lineColor,
|
|
12
12
|
[xyvars.edge.strokeSelected]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
|
|
13
13
|
[xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 20%)`,
|
|
14
|
-
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent
|
|
15
|
-
[xyvars.edge.strokeWidth]: '
|
|
14
|
+
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 40%)`,
|
|
15
|
+
[xyvars.edge.strokeWidth]: '3'
|
|
16
16
|
}
|
|
17
17
|
})
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ globalStyle(`:where([data-mantine-color-scheme="dark"]) ${container}`, {
|
|
|
20
20
|
vars: {
|
|
21
21
|
[mixColor]: `white`,
|
|
22
22
|
[xyvars.edge.labelColor]: vars.relation.labelColor,
|
|
23
|
-
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent
|
|
23
|
+
[xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 50%)`
|
|
24
24
|
}
|
|
25
25
|
})
|
|
26
26
|
|
|
@@ -29,7 +29,7 @@ const isSelected = '.react-flow__edge.selected'
|
|
|
29
29
|
globalStyle(`:where(${isSelected}) ${container}`, {
|
|
30
30
|
vars: {
|
|
31
31
|
[xyvars.edge.stroke]: xyvars.edge.strokeSelected,
|
|
32
|
-
[xyvars.edge.strokeWidth]: '
|
|
32
|
+
[xyvars.edge.strokeWidth]: '3'
|
|
33
33
|
}
|
|
34
34
|
})
|
|
35
35
|
|
|
@@ -37,13 +37,13 @@ globalStyle(`${container}:is([data-edge-hovered='true'],[data-edge-active='true'
|
|
|
37
37
|
vars: {
|
|
38
38
|
// [xyvars.edge.stroke]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
|
|
39
39
|
[xyvars.edge.stroke]: xyvars.edge.strokeSelected,
|
|
40
|
-
[xyvars.edge.strokeWidth]: '
|
|
40
|
+
[xyvars.edge.strokeWidth]: '3'
|
|
41
41
|
}
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
globalStyle(`:where(${isSelected}) ${container}[data-edge-hovered='true']`, {
|
|
45
45
|
vars: {
|
|
46
|
-
[xyvars.edge.strokeWidth]: '
|
|
46
|
+
[xyvars.edge.strokeWidth]: '4'
|
|
47
47
|
}
|
|
48
48
|
})
|
|
49
49
|
// globalStyle(`${container}[data-edge-hovered='true']`, {
|
|
@@ -56,6 +56,10 @@ globalStyle(`.react-flow__edges > svg`, {
|
|
|
56
56
|
mixBlendMode: 'screen'
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
+
// globalStyle(`:where([data-mantine-color-scheme="light"]) .react-flow__edges > svg`, {
|
|
60
|
+
// mixBlendMode: 'screen'
|
|
61
|
+
// })
|
|
62
|
+
|
|
59
63
|
export const dimmed = style({})
|
|
60
64
|
|
|
61
65
|
globalStyle(`.react-flow__edges > svg:has(${dimmed})`, {
|
|
@@ -67,7 +71,7 @@ globalStyle(`.react-flow__edges > svg:has(${dimmed})`, {
|
|
|
67
71
|
})
|
|
68
72
|
|
|
69
73
|
export const edgePathBg = style({
|
|
70
|
-
strokeWidth: xyvars.edge.strokeWidth,
|
|
74
|
+
// strokeWidth: xyvars.edge.strokeWidth,
|
|
71
75
|
strokeOpacity: 0.08,
|
|
72
76
|
// transition: 'stroke-width 175ms ease-in-out',
|
|
73
77
|
// transition: 'stroke-width 175ms ease-in-out, stroke-opacity 150ms ease-out',
|
|
@@ -76,7 +80,7 @@ export const edgePathBg = style({
|
|
|
76
80
|
transitionTimingFunction: 'ease-out',
|
|
77
81
|
selectors: {
|
|
78
82
|
[`:where(${isSelected}, [data-edge-active='true'], [data-edge-hovered='true']) &`]: {
|
|
79
|
-
strokeWidth: `calc(${xyvars.edge.strokeWidth} +
|
|
83
|
+
strokeWidth: `calc(${xyvars.edge.strokeWidth} + 8)`,
|
|
80
84
|
strokeOpacity: 0.15
|
|
81
85
|
}
|
|
82
86
|
}
|
|
@@ -93,7 +97,7 @@ export const controlPoint = style({
|
|
|
93
97
|
stroke: xyvars.edge.stroke,
|
|
94
98
|
fillOpacity: 0.75,
|
|
95
99
|
strokeWidth: 1,
|
|
96
|
-
cursor: '
|
|
100
|
+
cursor: 'default',
|
|
97
101
|
pointerEvents: 'auto',
|
|
98
102
|
visibility: 'hidden',
|
|
99
103
|
':hover': {
|
|
@@ -112,12 +116,20 @@ export const controlPoint = style({
|
|
|
112
116
|
}
|
|
113
117
|
})
|
|
114
118
|
|
|
119
|
+
export const controlDragging = style({
|
|
120
|
+
cursor: 'grabbing'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
globalStyle(`${controlDragging} *`, {
|
|
124
|
+
cursor: 'grabbing'
|
|
125
|
+
})
|
|
126
|
+
|
|
115
127
|
const strokeKeyframes = keyframes({
|
|
116
128
|
'from': {
|
|
117
|
-
strokeDashoffset: 18 * 2
|
|
129
|
+
strokeDashoffset: 18 * 2 + 10
|
|
118
130
|
},
|
|
119
131
|
'to': {
|
|
120
|
-
strokeDashoffset:
|
|
132
|
+
strokeDashoffset: 10
|
|
121
133
|
}
|
|
122
134
|
})
|
|
123
135
|
|
|
@@ -125,16 +137,16 @@ export const cssEdgePath = style({
|
|
|
125
137
|
animationDuration: '800ms',
|
|
126
138
|
animationIterationCount: 'infinite',
|
|
127
139
|
animationTimingFunction: 'linear',
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
animationFillMode: 'both',
|
|
141
|
+
strokeDashoffset: 10,
|
|
130
142
|
selectors: {
|
|
131
143
|
[`:where([data-edge-hovered='true']) &`]: {
|
|
132
|
-
|
|
133
|
-
animationDelay: '
|
|
144
|
+
animationName: strokeKeyframes,
|
|
145
|
+
animationDelay: '450ms',
|
|
134
146
|
transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
|
|
135
147
|
},
|
|
136
148
|
[`:where(${isSelected}, [data-edge-active='true']) &`]: {
|
|
137
|
-
|
|
149
|
+
animationName: strokeKeyframes,
|
|
138
150
|
animationDelay: '0ms',
|
|
139
151
|
transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
|
|
140
152
|
},
|
|
@@ -155,23 +167,25 @@ const varTranslate = createVar('translate')
|
|
|
155
167
|
export const edgeLabel = style({
|
|
156
168
|
top: 0,
|
|
157
169
|
left: 0,
|
|
170
|
+
padding: '2px 4px 4px 4px',
|
|
158
171
|
fontFamily: vars.likec4.font,
|
|
159
172
|
position: 'absolute',
|
|
160
173
|
pointerEvents: 'all',
|
|
161
174
|
cursor: 'pointer',
|
|
175
|
+
width: 'fit-content',
|
|
162
176
|
transformOrigin: '50% 50%',
|
|
163
177
|
mixBlendMode: 'screen',
|
|
164
178
|
color: xyvars.edge.labelColor,
|
|
165
179
|
backgroundColor: xyvars.edge.labelBgColor,
|
|
166
|
-
borderRadius:
|
|
180
|
+
borderRadius: 3,
|
|
167
181
|
transform: varTranslate,
|
|
168
182
|
vars: {
|
|
169
183
|
[varTranslate]: `translate(${fallbackVar(varLabelX, '-50%')}, ${fallbackVar(varLabelY, '-50%')})`
|
|
170
184
|
},
|
|
171
185
|
selectors: {
|
|
172
186
|
'&[data-edge-hovered="true"]': {
|
|
173
|
-
transition: 'all 140ms ease-out',
|
|
174
|
-
transform: `${varTranslate} scale(1.
|
|
187
|
+
transition: 'all 140ms ease-in-out',
|
|
188
|
+
transform: `${varTranslate} scale(1.12)`
|
|
175
189
|
},
|
|
176
190
|
[`&:is(${dimmed})`]: {
|
|
177
191
|
opacity: 0.6,
|
|
@@ -207,8 +221,8 @@ export const stepEdgeNumber = style({
|
|
|
207
221
|
})
|
|
208
222
|
|
|
209
223
|
export const edgeLabelText = style({
|
|
210
|
-
padding: '2px 6px 4px 6px',
|
|
211
224
|
textAlign: 'left',
|
|
212
|
-
|
|
213
|
-
|
|
225
|
+
whiteSpaceCollapse: 'preserve-breaks',
|
|
226
|
+
fontSize: rem(14),
|
|
227
|
+
lineHeight: 1.185
|
|
214
228
|
})
|
|
@@ -3,6 +3,16 @@ import { Position, type XYPosition } from '@xyflow/react'
|
|
|
3
3
|
import { getNodeDimensions } from '@xyflow/system'
|
|
4
4
|
import type { InternalXYFlowNode } from '../types'
|
|
5
5
|
|
|
6
|
+
export function getNodeCenter(node: InternalXYFlowNode): XYPosition {
|
|
7
|
+
const { width, height } = getNodeDimensions(node)
|
|
8
|
+
const { x, y } = node.internals.positionAbsolute
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
x: x + width / 2,
|
|
12
|
+
y: y + height / 2
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
// this helper function returns the intersection point
|
|
7
17
|
// of the line between the center of the intersectionNode and the target node
|
|
8
18
|
export function getNodeIntersectionFromCenterToPoint(
|
|
@@ -1,12 +1,11 @@
|
|
|
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'
|
|
4
3
|
import type { InternalNode, ReactFlowProps, XYPosition } from '@xyflow/react'
|
|
5
4
|
import { getNodeDimensions } from '@xyflow/system'
|
|
6
5
|
import { useMemo, useRef } from 'react'
|
|
7
6
|
import { isNullish } from 'remeda'
|
|
8
|
-
import { useDiagramStoreApi } from '../../state'
|
|
9
|
-
import {
|
|
7
|
+
import { useDiagramStoreApi } from '../../state/hooks'
|
|
8
|
+
import type { XYFlowInstance, XYFlowNode } from '../types'
|
|
10
9
|
import { isSamePoint } from '../utils'
|
|
11
10
|
import { useXYStoreApi, type XYStoreApi } from './useXYFlow'
|
|
12
11
|
|
|
@@ -296,7 +295,7 @@ export function useLayoutConstraints(): LayoutConstraints {
|
|
|
296
295
|
},
|
|
297
296
|
onNodeDragStop: (event, _xynode) => {
|
|
298
297
|
if (!isSamePoint(dragStartedAt.current, { x: event.clientX, y: event.clientY })) {
|
|
299
|
-
diagramApi.getState().triggerSaveManualLayout(
|
|
298
|
+
diagramApi.getState().triggerSaveManualLayout()
|
|
300
299
|
}
|
|
301
300
|
solverRef.current = undefined
|
|
302
301
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useNodesData, useReactFlow, useStore, useStoreApi } from '@xyflow/react'
|
|
2
|
-
import { shallowEqual } from 'fast-equals'
|
|
2
|
+
import { deepEqual, shallowEqual } from 'fast-equals'
|
|
3
|
+
import { useCallback } from 'react'
|
|
3
4
|
import type { XYFlowEdge, XYFlowNode, XYFlowState } from '../types'
|
|
4
5
|
|
|
5
6
|
export const useXYFlow = useReactFlow<XYFlowNode, XYFlowEdge>
|
|
@@ -18,3 +19,29 @@ export function useXYStore<StateSlice = unknown>(
|
|
|
18
19
|
}
|
|
19
20
|
export const useXYStoreApi = useStoreApi<XYFlowNode, XYFlowEdge>
|
|
20
21
|
export type XYStoreApi = ReturnType<typeof useXYStoreApi>
|
|
22
|
+
|
|
23
|
+
export function useXYEdgesData(edgeIds: string[]): Pick<XYFlowEdge, 'id' | 'data'>[] {
|
|
24
|
+
const ids = edgeIds.join(',')
|
|
25
|
+
const edgesData = useXYStore(
|
|
26
|
+
useCallback(
|
|
27
|
+
(s) => {
|
|
28
|
+
const data = [] as Pick<XYFlowEdge, 'id' | 'data'>[]
|
|
29
|
+
for (const id of edgeIds) {
|
|
30
|
+
const edge = s.edgeLookup.get(id)
|
|
31
|
+
if (edge) {
|
|
32
|
+
data.push({
|
|
33
|
+
id: edge.id,
|
|
34
|
+
data: edge.data
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return data
|
|
40
|
+
},
|
|
41
|
+
[ids]
|
|
42
|
+
),
|
|
43
|
+
deepEqual
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return edgesData
|
|
47
|
+
}
|