@likec4/diagram 1.2.2 → 1.3.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 (56) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +21 -3
  3. package/dist/index.mjs +1335 -1240
  4. package/dist/state/DiagramContext.d.ts +2263 -175
  5. package/dist/state/diagramStore.d.ts +2272 -180
  6. package/dist/state/useDiagramStore.d.ts +2409 -321
  7. package/dist/style.css +123 -92
  8. package/dist/theme.css.d.ts +0 -1
  9. package/dist/ui/OptionsPanel.d.ts +3 -1
  10. package/dist/xyflow/XYFlow.d.ts +2 -1
  11. package/dist/xyflow/XYFlowInner.d.ts +1 -1
  12. package/dist/xyflow/const.d.ts +1 -1
  13. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  14. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -4
  15. package/dist/xyflow/edges/utils.d.ts +4 -2
  16. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
  17. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  18. package/dist/xyflow/nodes/element/index.d.ts +1 -1
  19. package/dist/xyflow/types.d.ts +2 -2
  20. package/dist/xyflow/utils.d.ts +3 -0
  21. package/package.json +15 -14
  22. package/src/LikeC4Diagram.css.ts +4 -4
  23. package/src/LikeC4Diagram.props.ts +23 -2
  24. package/src/LikeC4Diagram.tsx +79 -31
  25. package/src/globals.css.ts +0 -1
  26. package/src/mantine/theme.ts +1 -0
  27. package/src/state/DiagramContext.tsx +0 -1
  28. package/src/state/diagramStore.ts +177 -100
  29. package/src/theme.css.ts +0 -1
  30. package/src/ui/DiagramTitlePanel.tsx +3 -3
  31. package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
  32. package/src/ui/DynamicViewWalkthrough.tsx +36 -17
  33. package/src/ui/OptionsPanel.tsx +12 -12
  34. package/src/ui/options/NodeOptions.tsx +16 -19
  35. package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
  36. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
  37. package/src/xyflow/SyncWithDiagram.tsx +21 -2
  38. package/src/xyflow/XYFlow.tsx +20 -23
  39. package/src/xyflow/XYFlowEvents.tsx +26 -8
  40. package/src/xyflow/XYFlowInner.tsx +5 -4
  41. package/src/xyflow/const.ts +1 -1
  42. package/src/xyflow/diagram-to-xyflow.ts +5 -31
  43. package/src/xyflow/edges/RelationshipEdge.tsx +158 -112
  44. package/src/xyflow/edges/edges.css.ts +32 -21
  45. package/src/xyflow/edges/utils.ts +5 -2
  46. package/src/xyflow/hooks/useLayoutConstraints.ts +17 -3
  47. package/src/xyflow/nodes/compound/CompoundNode.css.ts +40 -32
  48. package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
  49. package/src/xyflow/nodes/element/ElementNode.tsx +20 -6
  50. package/src/xyflow/nodes/element/element.css.ts +15 -15
  51. package/src/xyflow/nodes/element/index.ts +1 -1
  52. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
  53. package/src/xyflow/types.ts +5 -2
  54. package/src/xyflow/utils.ts +48 -0
  55. package/dist/ui/index.d.ts +0 -2
  56. package/src/ui/index.ts +0 -3
@@ -9,41 +9,15 @@ import {
9
9
  type Point
10
10
  } from '@likec4/core'
11
11
  import { Bezier } from 'bezier-js'
12
- import { hasAtLeast } from 'remeda'
12
+ import { hasAtLeast, reduce } from 'remeda'
13
13
  import type { XYFlowData } from '../xyflow/types'
14
-
15
- function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
16
- let [start, ...bezierPoints] = bezierSpline
17
- invariant(start, 'start should be defined')
18
- const handles = [
19
- // start
20
- ] as Point[]
21
-
22
- while (hasAtLeast(bezierPoints, 3)) {
23
- const [cp1, cp2, end, ...rest] = bezierPoints
24
- const bezier = new Bezier(start[0], start[1], cp1[0], cp1[1], cp2[0], cp2[1], end[0], end[1])
25
- // const { x, y } = bezier.get(0.5)
26
- const inflections = bezier.inflections()
27
- if (inflections.length === 0) {
28
- inflections.push(0.5)
29
- }
30
- inflections.forEach(t => {
31
- const { x, y } = bezier.get(t)
32
- handles.push([x, y])
33
- })
34
- bezierPoints = rest
35
- start = end
36
- }
37
- invariant(bezierPoints.length === 0, 'all points should be consumed')
38
-
39
- return handles
40
- }
14
+ import { toDomPrecision } from './utils'
41
15
 
42
16
  // const nodeZIndex = (node: DiagramNode) => node.level + (hasAtLeast(node.children, 1) ? 2 : 1)
43
17
  const nodeZIndex = (node: DiagramNode) => node.level + 1
44
18
 
45
19
  export function diagramViewToXYFlowData(
46
- view: Pick<DiagramView, 'nodes' | 'edges' | '__'>,
20
+ view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>,
47
21
  opts: {
48
22
  draggable: boolean
49
23
  selectable: boolean
@@ -147,7 +121,7 @@ export function diagramViewToXYFlowData(
147
121
  const target = edge.target
148
122
  const id = ns + edge.id
149
123
  // const points = isElkEdge(edge) ? edge.points : deriveEdgePoints(edge.points)
150
- const controlPoints = deriveEdgePoints(edge.points)
124
+ // const controlPoints = deriveEdgePoints(edge.points)
151
125
  // if (edge.tailArrowPoint) {
152
126
  // controlPoints.unshift([...edge.tailArrowPoint])
153
127
  // }
@@ -169,7 +143,7 @@ export function diagramViewToXYFlowData(
169
143
  data: {
170
144
  edge,
171
145
  type: 'bezier',
172
- controlPoints,
146
+ controlPoints: view.manualLayout?.edges[edge.id]?.controlPoints || null,
173
147
  headPoint: edge.headArrowPoint ?? null,
174
148
  tailPoint: edge.tailArrowPoint ?? null,
175
149
  stepNum: isDynamicView ? extractStep(edge.id) : null,
@@ -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, getBezierPath } from '@xyflow/react'
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 { type CSSProperties, memo } from 'react'
11
- import { first, hasAtLeast, last, zip } from 'remeda'
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
- import { useXYStoreApi } from '../hooks'
15
+ import { useXYFlow, 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 { getEdgeParams, getNodeIntersectionFromCenterToPoint } from './utils'
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 isSamePoint = (a: number, b: number) => {
68
+ const isSame = (a: number, b: number) => {
67
69
  return Math.abs(a - b) < 3.1
68
70
  }
69
71
 
70
- const isSamePosition = (a: XYPosition, [bx, by]: Point) => {
71
- return isSamePoint(a.x, bx) && isSamePoint(a.y, by)
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
- && isSamePoint(prev.sourceX, next.sourceX)
80
- && isSamePoint(prev.sourceY, next.sourceY)
81
- && isSamePoint(prev.targetX, next.targetX)
82
- && isSamePoint(prev.targetY, next.targetY)
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<Point>()
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 { nodeLookup, edgeLookup } = useXYStoreApi().getState()
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 = !isSamePosition(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
107
- || !isSamePosition(targetNode.internals.positionAbsolute, targetNode.data.element.position)
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 { edge: diagramEdge, controlPoints } = data
131
+ const {
132
+ label,
133
+ edge: {
134
+ points: diagramEdgePoints,
135
+ line = 'dashed',
136
+ color = 'gray',
137
+ ...diagramEdge
138
+ }
139
+ } = data
110
140
 
111
- // const edgePath = bezierPath(edge.points)
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,89 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
129
151
  strokeDasharray = '8,10'
130
152
  }
131
153
 
132
- const marker = `url(#arrow-${id})`
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
- let edgePath: string, labelX: number, labelY: number
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
- if (!isModified) {
137
- edgePath = bezierPath(diagramEdge.points)
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
- labelX = data.label?.bbox.x ?? 0
145
- labelY = data.label?.bbox.y ?? 0
146
- } else {
147
- const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode)
187
+ }
148
188
 
149
- const [_edgePath, _labelX, _labelY] = getBezierPath({
150
- sourceX: sx,
151
- sourceY: sy,
152
- sourcePosition: sourcePos,
153
- targetPosition: targetPos,
154
- targetX: tx,
155
- targetY: ty
156
- })
157
- edgePath = _edgePath
158
- labelX = _labelX
159
- labelY = _labelY
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
- const sourceCenterPos = getNodePositionWithOrigin(sourceNode, [-0.5, -0.5])
162
- const targetCenterPos = getNodePositionWithOrigin(targetNode, [-0.5, -0.5])
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
+ hasMoved = true
211
+ pointer = { x: e.clientX, y: e.clientY }
212
+ const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
213
+ const newControlPoints = controlPoints.slice()
214
+ newControlPoints[index] = {
215
+ x: toDomPrecision(x),
216
+ y: toDomPrecision(y)
217
+ }
218
+ xyflow.updateEdgeData(id, { controlPoints: newControlPoints })
219
+ }
220
+ }
221
+ const onPointerUp = () => {
222
+ domNode.removeEventListener('pointermove', onPointerMove)
223
+ domNode.removeEventListener('pointerup', onPointerUp)
224
+ if (hasMoved) {
225
+ diagramStore.getState().triggerSaveManualLayout(xyflowStore)
226
+ }
227
+ }
228
+ domNode.addEventListener('pointermove', onPointerMove)
229
+ domNode.addEventListener('pointerup', onPointerUp, { once: true })
230
+ }
163
231
 
164
- edgePath = curve([
165
- [sourceCenterPos.positionAbsolute.x, sourceCenterPos.positionAbsolute.y],
166
- getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints)!),
167
- ...controlPoints,
168
- getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints)!),
169
- [targetCenterPos.positionAbsolute.x, targetCenterPos.positionAbsolute.y]
170
- ])!
232
+ const marker = `url(#arrow-${id})`
233
+ let markerStart = diagramEdge.tailArrow ? marker : undefined
234
+ let markerEnd = diagramEdge.headArrow ? marker : undefined
235
+ if (isModified && diagramEdge.dir === 'back') {
236
+ ;[markerStart, markerEnd] = [markerEnd, markerStart]
171
237
  }
172
238
 
173
239
  return (
@@ -185,7 +251,8 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
185
251
  <marker
186
252
  id={`arrow-${id}`}
187
253
  viewBox="0 0 16 10"
188
- refX={isModified ? '12' : '10'}
254
+ // TODO: graphviz cut bezier path at arrow, we don't
255
+ refX={isModified ? '15' : '1'}
189
256
  refY="5"
190
257
  markerWidth="5"
191
258
  markerHeight="4"
@@ -195,20 +262,37 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
195
262
  <path d="M 0 0 L 16 5 L 0 10 z" stroke="context-stroke" fill="context-stroke" />
196
263
  </marker>
197
264
  </defs>
265
+ <path
266
+ className={clsx('react-flow__edge-path', edgesCss.edgePathBg)}
267
+ d={edgePath}
268
+ style={style}
269
+ strokeLinecap={'round'}
270
+ />
271
+ <path
272
+ ref={svgPathRef}
273
+ className={clsx('react-flow__edge-path', edgesCss.cssEdgePath)}
274
+ d={edgePath}
275
+ style={style}
276
+ strokeLinecap={'round'}
277
+ strokeDasharray={strokeDasharray}
278
+ markerStart={markerStart}
279
+ markerEnd={markerEnd}
280
+ />
198
281
  </g>
199
- <RelationshipPath
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) => (
282
+ <path
283
+ className={clsx('react-flow__edge-interaction')}
284
+ d={edgePath}
285
+ fill="none"
286
+ strokeOpacity={0}
287
+ strokeWidth={interactionWidth ?? 10}
288
+ />
289
+ {isEdgePathEditable && controlPoints.map((p, i) => (
207
290
  <circle
291
+ onPointerDown={e => onControlPointerDown(i, e)}
208
292
  className={edgesCss.controlPoint}
209
293
  key={i}
210
- cx={p[0]}
211
- cy={p[1]}
294
+ cx={p.x}
295
+ cy={p.y}
212
296
  r={3}
213
297
  />
214
298
  ))}
@@ -223,8 +307,11 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
223
307
  data-likec4-color={color}
224
308
  style={{
225
309
  ...assignInlineVars({
226
- [edgesCss.varLabelX]: Math.floor(labelX) + 'px',
227
- [edgesCss.varLabelY]: Math.floor(labelY) + 'px'
310
+ [edgesCss.varLabelX]: isModified ? `calc(${labelX}px - 10%)` : `${labelX}px`,
311
+ [edgesCss.varLabelY]: isModified ? `${labelY - 5}px` : `${labelY}px`
312
+ }),
313
+ ...(isEdgePathEditable && {
314
+ pointerEvents: 'none'
228
315
  }),
229
316
  ...(data.label && {
230
317
  maxWidth: data.label.bbox.width + 12
@@ -277,44 +364,3 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
277
364
  </g>
278
365
  )
279
366
  }, 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
2
  import { createVar, fallbackVar, generateIdentifier, globalKeyframes, globalStyle, 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
- // globalStyle(`:where([data-mantine-color-scheme="dark"]) .react-flow__edges > svg`, {
65
- // mixBlendMode: 'lighten'
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) 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,12 +94,20 @@ export const controlPoint = style({
94
94
  fillOpacity: 0.75,
95
95
  strokeWidth: 1,
96
96
  cursor: 'grab',
97
- pointerEvents: 'visible',
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-active='true'], [data-edge-hovered='true']) &`]: {
100
- transition: 'all 150ms ease-out',
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: 3
110
+ strokeWidth: 5
103
111
  }
104
112
  }
105
113
  })
@@ -122,12 +130,12 @@ export const cssEdgePath = style({
122
130
  [`:where([data-edge-hovered='true']) &`]: {
123
131
  animationName: strokeKeyframes,
124
132
  animationDelay: '350ms',
125
- transition: 'all 130ms ease-out'
133
+ transition: 'stroke,stroke-width 130ms ease-out'
126
134
  },
127
135
  [`:where(${isSelected}, [data-edge-active='true']) &`]: {
128
136
  animationName: strokeKeyframes,
129
137
  animationDelay: '0ms',
130
- transition: 'all 130ms ease-out'
138
+ transition: 'stroke,stroke-width 130ms ease-out'
131
139
  },
132
140
  [`:where([data-edge-dir='back']) &`]: {
133
141
  animationDirection: 'reverse'
@@ -154,7 +162,7 @@ export const edgeLabel = style({
154
162
  borderRadius: '3px',
155
163
  transform: varTranslate,
156
164
  vars: {
157
- [varTranslate]: `translate(${fallbackVar(varLabelX, '50%')}, ${fallbackVar(varLabelY, '50%')})`
165
+ [varTranslate]: `translate(${fallbackVar(varLabelX, '-50%')}, ${fallbackVar(varLabelY, '-50%')})`
158
166
  },
159
167
  selectors: {
160
168
  '&[data-edge-hovered="true"]': {
@@ -162,8 +170,11 @@ export const edgeLabel = style({
162
170
  transform: `${varTranslate} scale(1.1)`
163
171
  },
164
172
  [`&:is(${dimmed})`]: {
165
- transition: 'filter 800ms ease-out',
166
- filter: vars.filterDimmed
173
+ opacity: 0.8,
174
+ transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
175
+ transitionDelay: '200ms',
176
+ filter: 'grayscale(0.85) blur(2px)',
177
+ willChange: 'opacity, filter'
167
178
  }
168
179
  }
169
180
  })
@@ -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(intersectionNode: InternalXYFlowNode, [x1, y1]: Point) {
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 [x, y] as const
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,8 +275,17 @@ 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: (_event, xynode) => {
284
+ onNodeDragStart: (event, xynode) => {
285
+ dragStartedAt.current = {
286
+ x: event.clientX,
287
+ y: event.clientY
288
+ }
278
289
  const { xyflow } = diagramApi.getState()
279
290
  solverRef.current = createLayoutConstraints(xyflow, xyflowApi, xynode.id)
280
291
  },
@@ -282,8 +293,11 @@ export function useLayoutConstraints(): LayoutConstraints {
282
293
  invariant(solverRef.current, 'solverRef.current should be defined')
283
294
  solverRef.current?.onNodeDrag(xynode)
284
295
  },
285
- onNodeDragStop: (_event, _xynode) => {
296
+ onNodeDragStop: (event, _xynode) => {
297
+ if (!isSamePoint(dragStartedAt.current, { x: event.clientX, y: event.clientY })) {
298
+ diagramApi.getState().triggerSaveManualLayout(xyflowApi)
299
+ }
286
300
  solverRef.current = undefined
287
301
  }
288
- }), [xyflowApi])
302
+ }), [xyflowApi, diagramApi])
289
303
  }