@likec4/diagram 1.4.0 → 1.6.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 (87) hide show
  1. package/LICENSE +1 -1
  2. package/dist/LikeC4Diagram.css.d.ts +0 -1
  3. package/dist/LikeC4Diagram.d.ts +1 -1
  4. package/dist/LikeC4Diagram.props.d.ts +19 -4
  5. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.mjs +3937 -3646
  8. package/dist/state/DiagramContext.d.ts +95 -4159
  9. package/dist/state/diagramStore.d.ts +100 -4164
  10. package/dist/state/hooks.d.ts +4 -0
  11. package/dist/state/index.d.ts +1 -2
  12. package/dist/style.css +281 -191
  13. package/dist/ui/options/EdgeOptions.d.ts +19 -0
  14. package/dist/ui/options/NodeOptions.d.ts +2 -2
  15. package/dist/xyflow/EdgeMarkers.d.ts +11 -0
  16. package/dist/xyflow/XYFlowEvents.d.ts +8 -5
  17. package/dist/xyflow/const.d.ts +3 -3
  18. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  19. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -2
  20. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  21. package/dist/xyflow/edges/utils.d.ts +1 -0
  22. package/dist/xyflow/hooks/index.d.ts +3 -2
  23. package/dist/xyflow/hooks/useXYFlow.d.ts +13 -12
  24. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  25. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +5 -0
  26. package/dist/xyflow/nodes/element/element.css.d.ts +5 -2
  27. package/dist/xyflow/types.d.ts +2 -32
  28. package/package.json +19 -19
  29. package/src/LikeC4Diagram.css.ts +0 -8
  30. package/src/LikeC4Diagram.props.ts +23 -5
  31. package/src/LikeC4Diagram.tsx +18 -21
  32. package/src/StaticLikeC4Diagram.tsx +1 -0
  33. package/src/globals.css.ts +10 -3
  34. package/src/index.ts +1 -1
  35. package/src/mantine.css.ts +2 -2
  36. package/src/state/DiagramContext.tsx +44 -13
  37. package/src/state/diagramStore.ts +72 -53
  38. package/src/state/hooks.ts +5 -0
  39. package/src/state/index.ts +1 -2
  40. package/src/ui/BackwardForwardPanel.css.ts +9 -9
  41. package/src/ui/DiagramTitlePanel.css.ts +7 -2
  42. package/src/ui/DiagramTitlePanel.tsx +5 -3
  43. package/src/ui/DynamicViewWalkthrough.tsx +6 -4
  44. package/src/ui/OptionsPanel.tsx +37 -27
  45. package/src/ui/options/EdgeOptions.tsx +157 -0
  46. package/src/ui/options/NodeOptions.tsx +37 -29
  47. package/src/xyflow/EdgeMarkers.tsx +148 -0
  48. package/src/xyflow/FitviewOnDiagramChange.tsx +9 -5
  49. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
  50. package/src/xyflow/SyncWithDiagram.tsx +13 -20
  51. package/src/xyflow/XYFlow.tsx +3 -4
  52. package/src/xyflow/XYFlowEvents.tsx +48 -33
  53. package/src/xyflow/const.ts +3 -3
  54. package/src/xyflow/diagram-to-xyflow.ts +17 -41
  55. package/src/xyflow/edges/RelationshipEdge.tsx +208 -119
  56. package/src/xyflow/edges/edges.css.ts +40 -26
  57. package/src/xyflow/edges/utils.ts +10 -0
  58. package/src/xyflow/hooks/index.ts +4 -2
  59. package/src/xyflow/hooks/useLayoutConstraints.ts +3 -4
  60. package/src/xyflow/hooks/useXYFlow.ts +28 -1
  61. package/src/xyflow/nodes/compound/CompoundNode.css.ts +34 -24
  62. package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
  63. package/src/xyflow/nodes/element/ElementLink.tsx +12 -16
  64. package/src/xyflow/nodes/element/ElementNode.tsx +118 -114
  65. package/src/xyflow/nodes/element/ElementShapeSvg.tsx +2 -2
  66. package/src/xyflow/nodes/element/element.css.ts +147 -55
  67. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  68. package/src/xyflow/types.ts +2 -4
  69. package/src/xyflow/utils.ts +3 -6
  70. package/dist/KeepAspectRatioContainer.d.ts +0 -9
  71. package/dist/icons/Clipboard.d.ts +0 -3
  72. package/dist/icons/Link.d.ts +0 -3
  73. package/dist/icons/ZoomIn.d.ts +0 -3
  74. package/dist/icons/index.d.ts +0 -3
  75. package/dist/mantine/theme.d.ts +0 -3
  76. package/dist/state/WhenInitialized.d.ts +0 -3
  77. package/dist/ui/Camera.d.ts +0 -3
  78. package/dist/xyflow/nodes/element/ElementIcon.d.ts +0 -10
  79. package/src/KeepAspectRatioContainer.tsx +0 -36
  80. package/src/icons/Clipboard.tsx +0 -24
  81. package/src/icons/Link.tsx +0 -23
  82. package/src/icons/ZoomIn.tsx +0 -25
  83. package/src/icons/index.ts +0 -3
  84. package/src/mantine/theme.ts +0 -20
  85. package/src/state/WhenInitialized.tsx +0 -24
  86. package/src/ui/Camera.tsx +0 -174
  87. package/src/xyflow/nodes/element/ElementIcon.tsx +0 -44
@@ -1,20 +1,27 @@
1
- import { invariant, type NonEmptyArray, nonNullable, type Point } from '@likec4/core'
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/useDiagramStore'
14
20
  import { ZIndexes } from '../const'
15
- import { useXYStoreApi } from '../hooks'
16
- import { type XYFlowEdge } from '../types'
17
- import { bezierControlPoints, toDomPrecision } from '../utils'
21
+ import { EdgeMarkers } from '../EdgeMarkers'
22
+ import { useXYStoreApi } from '../hooks/useXYFlow'
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 === next.source
87
- && prev.target === next.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: _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 = getNodePositionWithOrigin(sourceNode, [-0.5, -0.5])
168
- const targetCenterPos = getNodePositionWithOrigin(targetNode, [-0.5, -0.5])
205
+ const sourceCenterPos = { x: sourceX, y: sourceY }
206
+ const targetCenterPos = { x: targetX, y: targetY }
169
207
 
170
- const points = [
171
- sourceCenterPos.positionAbsolute,
172
- getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints)!),
173
- ...controlPoints,
174
- getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints)!),
175
- targetCenterPos.positionAbsolute
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: React.PointerEvent) => {
247
+ const onControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>) => {
202
248
  const { domNode } = xyflowStore.getState()
203
- if (!domNode) return
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: toDomPrecision(x),
219
- y: toDomPrecision(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(xyflowStore)
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
- 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]
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
- <marker
262
- id={`arrow-${id}`}
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={markerStart}
289
- markerEnd={markerEnd}
343
+ markerStart={MarkerStart ? `url(#start${id})` : undefined}
344
+ markerEnd={MarkerEnd ? `url(#end${id})` : undefined}
290
345
  />
291
346
  </g>
292
- {isEdgePathEditable && controlPoints.map((p, i) => (
293
- <circle
294
- onPointerDown={e => onControlPointerDown(i, e)}
295
- className={edgesCss.controlPoint}
296
- key={i}
297
- cx={p.x}
298
- cy={p.y}
299
- r={3}
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
- <EdgeLabelRenderer>
304
- <Box
305
- className={clsx([
306
- edgesCss.container,
307
- edgesCss.edgeLabel,
308
- isDimmed && edgesCss.dimmed
309
- ])}
310
- data-likec4-color={color}
311
- style={{
312
- ...assignInlineVars({
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'
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) + 1,
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 35%)`,
15
- [xyvars.edge.strokeWidth]: '2.2px'
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 55%)`
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]: '3px'
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]: '3px'
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]: '3.6px'
46
+ [xyvars.edge.strokeWidth]: '4'
47
47
  }
48
48
  })
49
49
  // globalStyle(`${container}[data-edge-hovered='true']`, {
@@ -53,13 +53,17 @@ globalStyle(`:where(${isSelected}) ${container}[data-edge-hovered='true']`, {
53
53
  // })
54
54
 
55
55
  globalStyle(`.react-flow__edges > svg`, {
56
- mixBlendMode: 'screen'
56
+ mixBlendMode: 'plus-lighter'
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})`, {
62
- opacity: 0.8,
66
+ opacity: 0.6,
63
67
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
64
68
  transitionDelay: '200ms',
65
69
  filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
@@ -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} + 8px)`,
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: 'grab',
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: 'grab'
121
+ })
122
+
123
+ globalStyle(`${controlDragging} *`, {
124
+ cursor: 'grab'
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: 1
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
- animationName: strokeKeyframes,
129
- animationPlayState: 'paused',
140
+ animationFillMode: 'both',
141
+ strokeDashoffset: 10,
130
142
  selectors: {
131
143
  [`:where([data-edge-hovered='true']) &`]: {
132
- animationPlayState: 'running',
133
- animationDelay: '350ms',
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
- animationPlayState: 'running',
149
+ animationName: strokeKeyframes,
138
150
  animationDelay: '0ms',
139
151
  transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
140
152
  },
@@ -155,26 +167,28 @@ 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
- mixBlendMode: 'screen',
177
+ mixBlendMode: 'plus-lighter',
164
178
  color: xyvars.edge.labelColor,
165
179
  backgroundColor: xyvars.edge.labelBgColor,
166
- borderRadius: '3px',
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.1)`
187
+ transition: 'all 140ms ease-in-out',
188
+ transform: `${varTranslate} scale(1.12)`
175
189
  },
176
190
  [`&:is(${dimmed})`]: {
177
- opacity: 0.6,
191
+ opacity: 0.3,
178
192
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
179
193
  transitionDelay: '200ms',
180
194
  filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
@@ -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
- fontSize: rem(12.5),
213
- lineHeight: 1.2
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,2 +1,4 @@
1
- export * from './useLayoutConstraints'
2
- export * from './useXYFlow'
1
+ export { useLayoutConstraints } from './useLayoutConstraints'
2
+ export { useXYFlow, useXYNodesData, useXYStore, useXYStoreApi } from './useXYFlow'
3
+
4
+ export type * from './useXYFlow'
@@ -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 { type XYFlowInstance, XYFlowNode } from '../types'
7
+ import { useDiagramStoreApi } from '../../state/useDiagramStore'
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(xyflowApi)
298
+ diagramApi.getState().triggerSaveManualLayout()
300
299
  }
301
300
  solverRef.current = undefined
302
301
  }