@likec4/diagram 1.6.1 → 1.7.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 (49) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +10 -11
  3. package/dist/hooks/useUpdateEffect.d.ts +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.mjs +572 -464
  6. package/dist/state/DiagramContext.d.ts +746 -181
  7. package/dist/state/diagram-to-xyflow.d.ts +10 -0
  8. package/dist/state/diagramStore.d.ts +782 -202
  9. package/dist/style.css +273 -275
  10. package/dist/types.d.ts +20 -0
  11. package/dist/xyflow/EdgeMarkers.d.ts +1 -0
  12. package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -1
  13. package/dist/xyflow/XYFlow.d.ts +1 -4
  14. package/dist/xyflow/XYFlowEvents.d.ts +41 -12
  15. package/dist/xyflow/XYFlowInner.d.ts +0 -1
  16. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -3
  17. package/dist/xyflow/hooks/index.d.ts +0 -1
  18. package/dist/xyflow/hooks/useXYFlow.d.ts +27 -27
  19. package/dist/xyflow/types.d.ts +11 -19
  20. package/dist/xyflow/utils.d.ts +1 -0
  21. package/package.json +6 -6
  22. package/src/LikeC4Diagram.props.ts +17 -13
  23. package/src/LikeC4Diagram.tsx +62 -96
  24. package/src/globals.css.ts +10 -6
  25. package/src/hooks/useUpdateEffect.ts +16 -1
  26. package/src/index.ts +1 -0
  27. package/src/state/DiagramContext.tsx +19 -27
  28. package/src/{xyflow → state}/diagram-to-xyflow.ts +19 -18
  29. package/src/state/diagramStore.ts +404 -192
  30. package/src/types.ts +22 -0
  31. package/src/ui/BackwardForwardPanel.tsx +24 -7
  32. package/src/xyflow/EdgeMarkers.tsx +2 -0
  33. package/src/xyflow/FitviewOnDiagramChange.tsx +42 -75
  34. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +1 -1
  35. package/src/xyflow/XYFlow.tsx +57 -32
  36. package/src/xyflow/XYFlowEvents.tsx +58 -10
  37. package/src/xyflow/XYFlowInner.tsx +8 -8
  38. package/src/xyflow/edges/RelationshipEdge.tsx +43 -29
  39. package/src/xyflow/edges/edges.css.ts +3 -3
  40. package/src/xyflow/edges/utils.ts +0 -1
  41. package/src/xyflow/hooks/index.ts +0 -1
  42. package/src/xyflow/hooks/useLayoutConstraints.ts +42 -65
  43. package/src/xyflow/nodes/element/ElementNode.tsx +0 -4
  44. package/src/xyflow/nodes/element/element.css.ts +8 -6
  45. package/src/xyflow/types.ts +14 -32
  46. package/src/xyflow/utils.ts +4 -0
  47. package/dist/xyflow/SyncWithDiagram.d.ts +0 -4
  48. package/dist/xyflow/diagram-to-xyflow.d.ts +0 -7
  49. package/src/xyflow/SyncWithDiagram.tsx +0 -85
@@ -7,18 +7,18 @@ import {
7
7
  type RelationshipArrowType
8
8
  } from '@likec4/core'
9
9
  import { Box } from '@mantine/core'
10
- import { useIsomorphicLayoutEffect } from '@react-hookz/web'
10
+ import { useDebouncedEffect } from '@react-hookz/web'
11
11
  import { assignInlineVars } from '@vanilla-extract/dynamic'
12
12
  import type { EdgeProps, XYPosition } from '@xyflow/react'
13
13
  import { EdgeLabelRenderer } from '@xyflow/react'
14
14
  import clsx from 'clsx'
15
15
  import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
16
16
  import { deepEqual, deepEqual as eq } from 'fast-equals'
17
- import { memo, type PointerEvent as ReactPointerEvent, useRef, useState } from 'react'
17
+ import { memo, type PointerEvent as ReactPointerEvent, useEffect, useRef, useState } from 'react'
18
18
  import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
19
19
  import { useDiagramState, useDiagramStoreApi } from '../../state/useDiagramStore'
20
20
  import { ZIndexes } from '../const'
21
- import { EdgeMarkers } from '../EdgeMarkers'
21
+ import { EdgeMarkers, type EdgeMarkerType } from '../EdgeMarkers'
22
22
  import { useXYStoreApi } from '../hooks/useXYFlow'
23
23
  import { type RelationshipData, type XYFlowEdge } from '../types'
24
24
  import { bezierControlPoints } from '../utils'
@@ -56,7 +56,7 @@ import { getNodeIntersectionFromCenterToPoint } from './utils'
56
56
  // return path + segment
57
57
  // }
58
58
 
59
- const toMarker = (arrowType?: RelationshipArrowType) => {
59
+ const toMarker = (arrowType?: RelationshipArrowType): EdgeMarkerType | undefined => {
60
60
  if (!arrowType || arrowType === 'none') {
61
61
  return undefined
62
62
  }
@@ -124,6 +124,7 @@ const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>)
124
124
  && isSame(prev.targetX, next.targetX)
125
125
  && isSame(prev.targetY, next.targetY)
126
126
  && eq(prev.data.stepNum, next.data.stepNum)
127
+ && eq(prev.data.label, next.data.label)
127
128
  && sameControlPoints(prev.data.controlPoints, next.data.controlPoints)
128
129
  && eq(prev.data.edge, next.data.edge)
129
130
  )
@@ -149,8 +150,7 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
149
150
  const [isControlPointDragging, setIsControlPointDragging] = useState(false)
150
151
  const diagramStore = useDiagramStoreApi()
151
152
  const xyflowStore = useXYStoreApi()
152
- const { isActive, isEditable, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
153
- isEditable: s.readonly !== true,
153
+ const { isActive, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
154
154
  isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true,
155
155
  isActive: s.focusedNodeId === source || s.focusedNodeId === target
156
156
  || (s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum),
@@ -161,10 +161,9 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
161
161
  const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
162
162
  const targetNode = nonNullable(nodeLookup.get(target)!, `target node ${target} not found`)
163
163
 
164
- const isModified = isEditable && (0
165
- || isTruthy(data.controlPoints)
164
+ const isModified = isTruthy(data.controlPoints)
166
165
  || !isSamePoint(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
167
- || !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position))
166
+ || !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position)
168
167
 
169
168
  const {
170
169
  label,
@@ -172,6 +171,7 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
172
171
  points: diagramEdgePoints,
173
172
  line = 'dashed',
174
173
  color = 'gray',
174
+ labelBBox,
175
175
  ...diagramEdge
176
176
  }
177
177
  } = data
@@ -189,12 +189,12 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
189
189
  strokeDasharray = '8,10'
190
190
  }
191
191
 
192
- let labelX = label?.bbox.x ?? 0,
193
- labelY = label?.bbox.y ?? 0
192
+ let labelX = labelBBox?.x ?? 0,
193
+ labelY = labelBBox?.y ?? 0
194
194
 
195
195
  const [labelPos, setLabelPosition] = useState<XYPosition>({
196
- x: labelX,
197
- y: labelY
196
+ x: label?.bbox.x ?? labelX,
197
+ y: label?.bbox.y ?? labelY
198
198
  })
199
199
 
200
200
  let edgePath: string
@@ -224,16 +224,10 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
224
224
  edgePath = nonNullable(curve(points))
225
225
  } else {
226
226
  edgePath = bezierPath(diagramEdgePoints)
227
- // if (diagramEdge.tailArrowPoint) {
228
- // edgePath = `M ${diagramEdge.tailArrowPoint[0]},${diagramEdge.tailArrowPoint[1]} L ${edgePath.substring(1)}`
229
- // }
230
- // if (diagramEdge.headArrowPoint) {
231
- // edgePath += ` L ${diagramEdge.headArrowPoint[0]},${diagramEdge.headArrowPoint[1]}`
232
- // }
233
227
  }
234
228
 
235
229
  const svgPathRef = useRef<SVGPathElement>(null)
236
- useIsomorphicLayoutEffect(() => {
230
+ useEffect(() => {
237
231
  const path = svgPathRef.current
238
232
  if (!path) return
239
233
  const dompoint = path.getPointAtLength(path.getTotalLength() * 0.5)
@@ -244,6 +238,26 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
244
238
  setLabelPosition(current => isSamePoint(current, point) ? current : point)
245
239
  }, [edgePath])
246
240
 
241
+ useDebouncedEffect(
242
+ () => {
243
+ if (!label || label.bbox.x === labelPos.x && label.bbox.y === labelPos.y) {
244
+ return
245
+ }
246
+ diagramStore.getState().xyflow.updateEdgeData(id, {
247
+ label: {
248
+ ...label,
249
+ bbox: {
250
+ ...label.bbox,
251
+ ...labelPos
252
+ }
253
+ }
254
+ })
255
+ },
256
+ [labelPos],
257
+ 100,
258
+ 300
259
+ )
260
+
247
261
  const onControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>) => {
248
262
  const { domNode } = xyflowStore.getState()
249
263
  if (!domNode || e.pointerType !== 'mouse') {
@@ -263,15 +277,13 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
263
277
  if (e.button !== 0) {
264
278
  return
265
279
  }
280
+ const wasCanceled = diagramStore.getState().cancelSaveManualLayout()
266
281
  e.stopPropagation()
267
282
  let hasMoved = false
268
283
  let pointer = { x: e.clientX, y: e.clientY }
269
284
  const onPointerMove = (e: PointerEvent) => {
270
285
  if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
271
286
  setIsControlPointDragging(true)
272
- if (!hasMoved) {
273
- diagramStore.getState().cancelSaveManualLayout()
274
- }
275
287
  hasMoved = true
276
288
  pointer = { x: e.clientX, y: e.clientY }
277
289
  const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
@@ -284,12 +296,12 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
284
296
  }
285
297
  }
286
298
  const onPointerUp = () => {
287
- setIsControlPointDragging(false)
288
299
  domNode.removeEventListener('pointermove', onPointerMove)
289
300
  domNode.removeEventListener('pointerup', onPointerUp)
290
- if (hasMoved) {
291
- diagramStore.getState().triggerSaveManualLayout()
301
+ if (hasMoved || wasCanceled) {
302
+ diagramStore.getState().scheduleSaveManualLayout()
292
303
  }
304
+ setIsControlPointDragging(false)
293
305
  }
294
306
  domNode.addEventListener('pointermove', onPointerMove)
295
307
  domNode.addEventListener('pointerup', onPointerUp, { once: true })
@@ -304,13 +316,15 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
304
316
  const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null
305
317
  const MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null
306
318
 
319
+ const labelZIndex = 1 + (isHovered ? ZIndexes.Element : (edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge))
320
+
307
321
  return (
308
322
  <g
309
- className={clsx([
323
+ className={clsx(
310
324
  edgesCss.container,
311
325
  isDimmed && edgesCss.dimmed,
312
326
  isControlPointDragging && edgesCss.controlDragging
313
- ])}
327
+ )}
314
328
  data-likec4-color={color}
315
329
  data-edge-dir={diagramEdge.dir}
316
330
  data-edge-active={isActive}
@@ -375,7 +389,7 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
375
389
  selected: selected ?? false,
376
390
  stepNum: data.stepNum,
377
391
  label: data.label,
378
- zIndex: (edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge) + 1,
392
+ zIndex: labelZIndex,
379
393
  isHovered,
380
394
  isActive,
381
395
  isStepEdge
@@ -97,7 +97,7 @@ export const controlPoint = style({
97
97
  stroke: xyvars.edge.stroke,
98
98
  fillOpacity: 0.75,
99
99
  strokeWidth: 1,
100
- cursor: 'default',
100
+ cursor: 'grab',
101
101
  pointerEvents: 'auto',
102
102
  visibility: 'hidden',
103
103
  ':hover': {
@@ -117,11 +117,11 @@ export const controlPoint = style({
117
117
  })
118
118
 
119
119
  export const controlDragging = style({
120
- cursor: 'grab'
120
+ cursor: 'grabbing'
121
121
  })
122
122
 
123
123
  globalStyle(`${controlDragging} *`, {
124
- cursor: 'grab'
124
+ cursor: 'grabbing !important'
125
125
  })
126
126
 
127
127
  const strokeKeyframes = keyframes({
@@ -1,4 +1,3 @@
1
- import type { Point } from '@likec4/core'
2
1
  import { Position, type XYPosition } from '@xyflow/react'
3
2
  import { getNodeDimensions } from '@xyflow/system'
4
3
  import type { InternalXYFlowNode } from '../types'
@@ -1,4 +1,3 @@
1
- export { useLayoutConstraints } from './useLayoutConstraints'
2
1
  export { useXYFlow, useXYNodesData, useXYStore, useXYStoreApi } from './useXYFlow'
3
2
 
4
3
  export type * from './useXYFlow'
@@ -1,13 +1,12 @@
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 type { InternalNode, ReactFlowProps, XYPosition } from '@xyflow/react'
3
+ import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyflow/react'
4
4
  import { getNodeDimensions } from '@xyflow/system'
5
5
  import { useMemo, useRef } from 'react'
6
6
  import { isNullish } from 'remeda'
7
7
  import { useDiagramStoreApi } from '../../state/useDiagramStore'
8
- import type { XYFlowInstance, XYFlowNode } from '../types'
9
- import { isSamePoint } from '../utils'
10
- import { useXYStoreApi, type XYStoreApi } from './useXYFlow'
8
+ import type { XYFlowNode } from '../types'
9
+ import { type XYStoreApi } from './useXYFlow'
11
10
 
12
11
  abstract class Rect {
13
12
  id!: string
@@ -81,28 +80,28 @@ class Compound extends Rect {
81
80
  )
82
81
  const { weak } = Strength
83
82
  this.solver.createConstraint(leftPadding, Operator.Ge, 40)
84
- this.solver.createConstraint(leftPadding, Operator.Eq, 40, weak)
83
+ this.solver.createConstraint(leftPadding, Operator.Eq, 41, weak)
85
84
 
86
85
  const topPadding = new Expr(
87
86
  rect.minY,
88
87
  [-1, this.minY]
89
88
  )
90
- this.solver.createConstraint(topPadding, Operator.Ge, 60)
91
- this.solver.createConstraint(topPadding, Operator.Eq, 60, weak)
89
+ this.solver.createConstraint(topPadding, Operator.Ge, 54)
90
+ this.solver.createConstraint(topPadding, Operator.Eq, 55, weak)
92
91
 
93
92
  const rightPadding = new Expr(
94
93
  this.maxX,
95
94
  [-1, rect.maxX]
96
95
  )
97
96
  this.solver.createConstraint(rightPadding, Operator.Ge, 40)
98
- this.solver.createConstraint(rightPadding, Operator.Eq, 40, weak)
97
+ this.solver.createConstraint(rightPadding, Operator.Eq, 41, weak)
99
98
 
100
99
  const bottomPadding = new Expr(
101
100
  this.maxY,
102
101
  [-1, rect.maxY]
103
102
  )
104
103
  this.solver.createConstraint(bottomPadding, Operator.Ge, 40)
105
- this.solver.createConstraint(bottomPadding, Operator.Eq, 40, weak)
104
+ this.solver.createConstraint(bottomPadding, Operator.Eq, 41, weak)
106
105
  }
107
106
  }
108
107
 
@@ -164,7 +163,6 @@ class Leaf extends Rect {
164
163
  }
165
164
 
166
165
  function createLayoutConstraints(
167
- xyflow: XYFlowInstance,
168
166
  xyflowApi: XYStoreApi,
169
167
  draggingNodeId: string
170
168
  ) {
@@ -206,40 +204,29 @@ function createLayoutConstraints(
206
204
  solver.updateVariables()
207
205
  solver.maxIterations = 1000
208
206
 
207
+ const rectsToUpdate = [...rects.values()].filter(r => r.id !== draggingNodeId)
208
+
209
209
  function updateXYFlowNodes() {
210
210
  solver.updateVariables()
211
-
212
- xyflow.setNodes(nodes =>
213
- nodes.map((n) => {
214
- if (n.id === draggingNodeId || n.dragging === true) {
215
- return n
216
- }
217
- const rect = rects.get(n.id)
218
- if (!rect) {
219
- return n
220
- }
221
- const dimensions = rect.dimensions
222
- const newXY = rect.position
223
-
224
- if (newXY.x !== n.position.x || newXY.y !== n.position.y) {
225
- return {
226
- ...n,
227
- position: rect.position,
228
- width: dimensions.width,
229
- height: dimensions.height,
230
- measured: dimensions
231
- }
232
- }
233
- if (dimensions.width !== n.width || dimensions.height !== n.height) {
234
- return {
235
- ...n,
236
- width: dimensions.width,
237
- height: dimensions.height,
238
- measured: dimensions
239
- }
211
+ xyflowApi.getState().triggerNodeChanges(
212
+ rectsToUpdate.reduce((acc, r) => {
213
+ acc.push({
214
+ id: r.id,
215
+ type: 'position',
216
+ dragging: false,
217
+ position: r.position,
218
+ positionAbsolute: r.positionAbsolute
219
+ })
220
+ if (r instanceof Compound) {
221
+ acc.push({
222
+ id: r.id,
223
+ type: 'dimensions',
224
+ setAttributes: true,
225
+ dimensions: r.dimensions
226
+ })
240
227
  }
241
- return n
242
- })
228
+ return acc
229
+ }, [] as NodeChange<XYFlowNode>[])
243
230
  )
244
231
  }
245
232
 
@@ -249,13 +236,16 @@ function createLayoutConstraints(
249
236
  * Move the editing node to the given position.
250
237
  */
251
238
  function onNodeDrag(xynode: XYFlowNode) {
252
- const pos = nodeLookup.get(xynode.id)!.internals.positionAbsolute
253
- const rect = nonNullable(rects.get(xynode.id))
254
- solver.suggestValue(rect.minX, Math.ceil(pos.x))
255
- solver.suggestValue(rect.minY, Math.ceil(pos.y))
239
+ if (rectsToUpdate.length === 0) {
240
+ return
241
+ }
256
242
  animationFrameId ??= requestAnimationFrame(() => {
257
- updateXYFlowNodes()
258
243
  animationFrameId = null
244
+ const pos = nodeLookup.get(xynode.id)!.internals.positionAbsolute
245
+ const rect = nonNullable(rects.get(xynode.id))
246
+ solver.suggestValue(rect.minX, Math.ceil(pos.x))
247
+ solver.suggestValue(rect.minY, Math.ceil(pos.y))
248
+ updateXYFlowNodes()
259
249
  })
260
250
  }
261
251
 
@@ -271,33 +261,20 @@ type LayoutConstraints = Required<Pick<ReactFlowProps<XYFlowNode>, 'onNodeDragSt
271
261
  */
272
262
  export function useLayoutConstraints(): LayoutConstraints {
273
263
  const diagramApi = useDiagramStoreApi()
274
- const xyflowApi = useXYStoreApi()
275
264
  const solverRef = useRef<ReturnType<typeof createLayoutConstraints>>()
276
-
277
- const dragStartedAt = useRef({
278
- x: 0,
279
- y: 0
280
- })
281
-
282
265
  return useMemo((): LayoutConstraints => ({
283
- onNodeDragStart: (event, xynode) => {
284
- dragStartedAt.current = {
285
- x: event.clientX,
286
- y: event.clientY
287
- }
288
- const { xyflow, cancelSaveManualLayout } = diagramApi.getState()
266
+ onNodeDragStart: (_event, xynode) => {
267
+ const { cancelSaveManualLayout, xystore } = diagramApi.getState()
289
268
  cancelSaveManualLayout()
290
- solverRef.current = createLayoutConstraints(xyflow, xyflowApi, xynode.id)
269
+ solverRef.current = createLayoutConstraints(xystore, xynode.id)
291
270
  },
292
271
  onNodeDrag: (_event, xynode) => {
293
272
  invariant(solverRef.current, 'solverRef.current should be defined')
294
273
  solverRef.current?.onNodeDrag(xynode)
295
274
  },
296
- onNodeDragStop: (event, _xynode) => {
297
- if (!isSamePoint(dragStartedAt.current, { x: event.clientX, y: event.clientY })) {
298
- diagramApi.getState().triggerSaveManualLayout()
299
- }
275
+ onNodeDragStop: (_event, _xynode) => {
276
+ diagramApi.getState().scheduleSaveManualLayout()
300
277
  solverRef.current = undefined
301
278
  }
302
- }), [xyflowApi, diagramApi])
279
+ }), [diagramApi])
303
280
  }
@@ -96,10 +96,6 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
96
96
  isHovercards: s.showElementLinks,
97
97
  hasOnNavigateTo: !!s.onNavigateTo
98
98
  }))
99
- // const diagramState = useDiagramState()
100
- // const isNodeInteractive = diagramState.isNodeInteractive
101
- // const isHovercards = diagramState.disableHovercards !== true
102
- // const isNavigable = diagramState.hasOnNavigateTo && !!element.navigateTo
103
99
  const isNavigable = hasOnNavigateTo && !!element.navigateTo
104
100
 
105
101
  const w = toDomPrecision(width ?? element.width)
@@ -197,14 +197,15 @@ export const elementDataContainer = style({
197
197
  paddingTop: 32,
198
198
  paddingBottom: 28
199
199
  },
200
- [`&:is(${hasIcon})`]: {
201
- paddingRight: 20
202
- },
200
+ // [`&:is(${hasIcon})`]: {
201
+ // paddingLeft: 40,
202
+ // paddingRight: 20
203
+ // },
203
204
  [`${container}:not(:is([data-likec4-shape="queue"],[data-likec4-shape="mobile"])) &:is(${hasIcon})`]: {
204
- paddingLeft: 20
205
+ paddingLeft: 24,
206
+ paddingRight: 18
205
207
  },
206
208
  [`&:has(${description}, ${technology})`]: {
207
- alignItems: 'flex-start',
208
209
  gap: rem(16),
209
210
  vars: {
210
211
  [iconSize]: '60px'
@@ -247,7 +248,8 @@ export const elementIcon = style({
247
248
  display: 'flex',
248
249
  alignItems: 'center',
249
250
  justifyContent: 'center',
250
- mixBlendMode: 'hard-light'
251
+ mixBlendMode: 'hard-light',
252
+ alignSelf: 'flex-start'
251
253
  })
252
254
  globalStyle(`${elementIcon} svg, ${elementIcon} img`, {
253
255
  width: '100%',
@@ -1,4 +1,4 @@
1
- import type { BBox, DiagramEdge, DiagramNode, Fqn, XYPosition } from '@likec4/core'
1
+ import type { BBox, DiagramEdge, DiagramNode, Fqn, XYPoint } from '@likec4/core'
2
2
  import type {
3
3
  Edge,
4
4
  EdgeMouseHandler,
@@ -13,38 +13,23 @@ import type {
13
13
  import { isNode } from '@xyflow/react'
14
14
  import type { MouseEvent as ReactMouseEvent } from 'react'
15
15
  import { isTruthy } from 'remeda'
16
- import type { SetReadonly, SetRequired } from 'type-fest'
16
+ import type { Simplify } from 'type-fest'
17
17
 
18
- export type ElementNodeData = {
18
+ export type ElementXYFlowNode = Node<{
19
19
  fqn: Fqn
20
20
  element: DiagramNode
21
- }
22
-
23
- type TypedXYFlowNode<D extends Record<string, unknown>, T extends string> = SetReadonly<
24
- SetRequired<Node<D, T>, 'type'>,
25
- 'id' | 'type'
26
- >
27
- type TypedXYFlowEdge<D extends Record<string, unknown>, T extends string> = SetReadonly<
28
- SetRequired<Edge<D, T>, 'type' | 'data'>,
29
- 'id' | 'type'
30
- >
31
-
32
- export type ElementXYFlowNode = TypedXYFlowNode<ElementNodeData, 'element'>
21
+ }, 'element'>
33
22
 
34
- export type CompoundNodeData = {
23
+ export type CompoundXYFlowNode = Node<{
35
24
  fqn: Fqn
36
25
  element: DiagramNode
37
- }
38
-
39
- export type CompoundXYFlowNode = TypedXYFlowNode<CompoundNodeData, 'compound'>
26
+ }, 'compound'>
40
27
 
41
28
  export type XYFlowNode = ElementXYFlowNode | CompoundXYFlowNode
42
29
 
43
30
  export type InternalXYFlowNode = InternalNode<XYFlowNode>
44
31
 
45
32
  export namespace XYFlowNode {
46
- export type Data = ElementNodeData | CompoundNodeData
47
-
48
33
  export function isCompound(node: unknown): node is CompoundXYFlowNode {
49
34
  return isNode(node) && node.type === 'compound'
50
35
  }
@@ -57,12 +42,8 @@ export namespace XYFlowNode {
57
42
 
58
43
  export type RelationshipData = {
59
44
  edge: DiagramEdge
60
- // to replace edges on view change
61
- // viewId: ViewID
62
45
  // if set - edge was changed by user
63
- controlPoints: XYPosition[] | null
64
- type: 'bezier' | 'poly'
65
- // if dynamic view
46
+ controlPoints: XYPoint[] | null
66
47
  stepNum: number | null
67
48
  label: null | {
68
49
  bbox: BBox
@@ -70,7 +51,13 @@ export type RelationshipData = {
70
51
  }
71
52
  }
72
53
 
73
- export type RelationshipEdge = TypedXYFlowEdge<RelationshipData, 'relationship'>
54
+ export type RelationshipEdge = Simplify<
55
+ Edge<RelationshipData, 'relationship'> & {
56
+ type: 'relationship'
57
+ // Make field required
58
+ data: RelationshipData
59
+ }
60
+ >
74
61
 
75
62
  export type XYFlowEdge = RelationshipEdge
76
63
 
@@ -83,11 +70,6 @@ export namespace XYFlowEdge {
83
70
  export type XYFlowInstance = ReactFlowInstance<XYFlowNode, XYFlowEdge>
84
71
  export type XYFlowState = ReactFlowState<XYFlowNode, XYFlowEdge>
85
72
 
86
- export type XYFlowData = {
87
- nodes: XYFlowNode[]
88
- edges: XYFlowEdge[]
89
- }
90
-
91
73
  export type XYFlowEventHandlers = {
92
74
  onPanelClick: (event: ReactMouseEvent) => void
93
75
  onNodeContextMenu: NodeMouseHandler<XYFlowNode>
@@ -55,3 +55,7 @@ export function isSamePoint(a: XYPosition | Point, b: XYPosition | Point) {
55
55
  const [bx, by] = isArray(b) ? b : [b.x, b.y]
56
56
  return isClose(ax, bx) && isClose(ay, by)
57
57
  }
58
+
59
+ export function distanceBetweenPoints(a: XYPosition, b: XYPosition) {
60
+ return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
61
+ }
@@ -1,4 +0,0 @@
1
- /**
2
- * Syncs the diagram state with the XYFlow instance
3
- */
4
- export declare function SyncWithDiagram(): null;
@@ -1,7 +0,0 @@
1
- import { DiagramView } from '@likec4/core/types';
2
- import { XYFlowData } from '../xyflow/types';
3
-
4
- export declare function diagramViewToXYFlowData(view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>, opts: {
5
- draggable: boolean;
6
- selectable: boolean;
7
- }): XYFlowData;
@@ -1,85 +0,0 @@
1
- import { deepEqual as eq } from 'fast-equals'
2
-
3
- import { useEffect } from 'react'
4
- import { omit } from 'remeda'
5
- import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
6
- import { diagramViewToXYFlowData } from './diagram-to-xyflow'
7
-
8
- /**
9
- * Syncs the diagram state with the XYFlow instance
10
- */
11
- export function SyncWithDiagram() {
12
- const xyflowSynced = useDiagramState(s => s.xyflowSynced)
13
- const diagramStoreApi = useDiagramStoreApi()
14
-
15
- useEffect(() => {
16
- if (xyflowSynced) {
17
- return
18
- }
19
- const {
20
- view,
21
- nodesDraggable,
22
- nodesSelectable,
23
- focusedNodeId,
24
- xystore
25
- } = diagramStoreApi.getState()
26
- const updates = diagramViewToXYFlowData(view, {
27
- draggable: nodesDraggable,
28
- selectable: nodesSelectable || focusedNodeId !== null
29
- })
30
-
31
- const {
32
- nodeLookup,
33
- edgeLookup,
34
- setNodes,
35
- setEdges
36
- } = xystore.getState()
37
-
38
- setNodes(updates.nodes.map(update => {
39
- const existing = nodeLookup.get(update.id)?.internals.userNode
40
- if (existing && existing.type === update.type && eq(existing.parentId, update.parentId)) {
41
- if (eq(existing.data.element, update.data.element)) {
42
- return existing
43
- }
44
- return {
45
- ...existing,
46
- ...update
47
- }
48
- }
49
- return update
50
- }))
51
-
52
- setEdges(updates.edges.map(update => {
53
- const existing = edgeLookup.get(update.id)
54
- if (existing) {
55
- if (
56
- eq(existing.data.controlPoints, update.data.controlPoints)
57
- && eq(existing.markerStart, update.markerStart)
58
- && eq(existing.markerEnd, update.markerEnd)
59
- && eq(existing.data.edge, update.data.edge)
60
- ) {
61
- return existing
62
- }
63
- return {
64
- ...omit(existing, ['data', 'markerStart', 'markerEnd']),
65
- ...update,
66
- data: {
67
- ...existing.data,
68
- ...update.data
69
- }
70
- }
71
- }
72
- return update
73
- }))
74
-
75
- diagramStoreApi.setState(
76
- {
77
- xyflowSynced: true
78
- },
79
- false,
80
- 'xyflowSynced'
81
- )
82
- }, [xyflowSynced, diagramStoreApi])
83
-
84
- return null
85
- }