@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.
Files changed (69) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +27 -4
  3. package/dist/index.mjs +1543 -1346
  4. package/dist/state/DiagramContext.d.ts +2353 -264
  5. package/dist/state/diagramStore.d.ts +2360 -268
  6. package/dist/state/index.d.ts +0 -1
  7. package/dist/state/useDiagramStore.d.ts +3 -2534
  8. package/dist/state/useNavigationHistory.d.ts +5 -0
  9. package/dist/style.css +194 -131
  10. package/dist/theme.css.d.ts +5 -1
  11. package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
  12. package/dist/ui/BackwardForwardPanel.d.ts +1 -0
  13. package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
  14. package/dist/xyflow/XYFlow.d.ts +2 -1
  15. package/dist/xyflow/XYFlowEvents.d.ts +4 -4
  16. package/dist/xyflow/XYFlowInner.d.ts +2 -1
  17. package/dist/xyflow/const.d.ts +1 -1
  18. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  19. package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -5
  20. package/dist/xyflow/edges/utils.d.ts +4 -2
  21. package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
  22. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
  23. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  24. package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
  25. package/dist/xyflow/nodes/element/index.d.ts +1 -1
  26. package/dist/xyflow/types.d.ts +3 -3
  27. package/dist/xyflow/utils.d.ts +3 -0
  28. package/package.json +18 -17
  29. package/src/LikeC4Diagram.css.ts +15 -5
  30. package/src/LikeC4Diagram.props.ts +33 -5
  31. package/src/LikeC4Diagram.tsx +103 -32
  32. package/src/globals.css.ts +1 -2
  33. package/src/mantine/theme.ts +1 -0
  34. package/src/state/DiagramContext.tsx +3 -4
  35. package/src/state/diagramStore.ts +177 -103
  36. package/src/state/index.ts +0 -1
  37. package/src/state/useDiagramStore.ts +6 -13
  38. package/src/state/useNavigationHistory.ts +39 -0
  39. package/src/theme.css.ts +5 -1
  40. package/src/ui/BackwardForwardPanel.css.ts +22 -0
  41. package/src/ui/BackwardForwardPanel.tsx +59 -0
  42. package/src/ui/DiagramTitlePanel.css.ts +11 -5
  43. package/src/ui/DiagramTitlePanel.tsx +108 -96
  44. package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
  45. package/src/ui/DynamicViewWalkthrough.tsx +36 -17
  46. package/src/ui/OptionsPanel.tsx +28 -19
  47. package/src/ui/options/NodeOptions.tsx +16 -19
  48. package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
  49. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
  50. package/src/xyflow/SyncWithDiagram.tsx +21 -2
  51. package/src/xyflow/XYFlow.tsx +20 -23
  52. package/src/xyflow/XYFlowEvents.tsx +26 -8
  53. package/src/xyflow/XYFlowInner.tsx +10 -6
  54. package/src/xyflow/const.ts +1 -1
  55. package/src/xyflow/diagram-to-xyflow.ts +4 -41
  56. package/src/xyflow/edges/RelationshipEdge.tsx +160 -111
  57. package/src/xyflow/edges/edges.css.ts +41 -26
  58. package/src/xyflow/edges/utils.ts +5 -2
  59. package/src/xyflow/hooks/useLayoutConstraints.ts +19 -4
  60. package/src/xyflow/nodes/compound/CompoundNode.css.ts +45 -37
  61. package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
  62. package/src/xyflow/nodes/element/ElementNode.tsx +27 -14
  63. package/src/xyflow/nodes/element/element.css.ts +31 -18
  64. package/src/xyflow/nodes/element/index.ts +1 -1
  65. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
  66. package/src/xyflow/types.ts +5 -2
  67. package/src/xyflow/utils.ts +48 -0
  68. package/dist/ui/index.d.ts +0 -2
  69. 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, 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
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 { 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,92 @@ 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
+ 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
- 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
- ])!
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
- refX={isModified ? '12' : '10'}
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
- <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) => (
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[0]}
211
- cy={p[1]}
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]: Math.floor(labelX) + 'px',
227
- [edgesCss.varLabelY]: Math.floor(labelY) + 'px'
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, generateIdentifier, globalKeyframes, globalStyle, style } from '@vanilla-extract/css'
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
- // 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) ${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: '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
  })
106
114
 
107
- const strokeKeyframes = generateIdentifier('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
- animationName: strokeKeyframes,
132
+ animationPlayState: 'running',
124
133
  animationDelay: '350ms',
125
- transition: 'all 130ms ease-out'
134
+ transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
126
135
  },
127
136
  [`:where(${isSelected}, [data-edge-active='true']) &`]: {
128
- animationName: strokeKeyframes,
137
+ animationPlayState: 'running',
129
138
  animationDelay: '0ms',
130
- transition: 'all 130ms ease-out'
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
- transition: 'filter 800ms ease-out',
166
- filter: vars.filterDimmed
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(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,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: (_event, xynode) => {
278
- const { xyflow } = diagramApi.getState()
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: (_event, _xynode) => {
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
  }