@likec4/diagram 1.20.0 → 1.20.2

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 (133) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +1 -0
  2. package/dist/LikeC4Diagram.css.js +3 -1
  3. package/dist/base/BaseXYFlow.js +15 -14
  4. package/dist/base/Link.css.d.ts +3 -0
  5. package/dist/base/Link.css.js +9 -0
  6. package/dist/base/Link.d.ts +5 -0
  7. package/dist/base/Link.js +58 -0
  8. package/dist/base/const.d.ts +1 -0
  9. package/dist/base/const.js +2 -1
  10. package/dist/base/example/custom.d.ts +1 -1
  11. package/dist/base/index.d.ts +1 -0
  12. package/dist/base/primitives/compound/CompoundActionButton.js +2 -1
  13. package/dist/base/primitives/compound/CompoundDetailsButton.js +2 -1
  14. package/dist/base/primitives/compound/CompoundNodeContainer.css.d.ts +0 -1
  15. package/dist/base/primitives/compound/CompoundNodeContainer.css.js +4 -6
  16. package/dist/base/primitives/compound/CompoundNodeContainer.js +2 -2
  17. package/dist/base/primitives/customEdge.d.ts +1 -1
  18. package/dist/base/primitives/customEdge.js +1 -1
  19. package/dist/base/primitives/customNode.js +1 -1
  20. package/dist/base/primitives/edge/EdgeLabel.js +3 -1
  21. package/dist/base/primitives/edge/EdgePath.d.ts +2 -1
  22. package/dist/base/primitives/edge/EdgePath.js +2 -0
  23. package/dist/base/primitives/element/ElementActionButtons.js +2 -1
  24. package/dist/base/primitives/element/ElementDetailsButton.js +2 -1
  25. package/dist/base/primitives/element/ElementNodeContainer.css.d.ts +0 -1
  26. package/dist/base/primitives/element/ElementNodeContainer.css.js +1 -3
  27. package/dist/base/primitives/element/ElementNodeContainer.js +2 -3
  28. package/dist/base/primitives/element/ElementShape.js +4 -2
  29. package/dist/base/primitives/element/ElementTitle.js +2 -1
  30. package/dist/base/updateEdges.js +3 -3
  31. package/dist/base/updateNodes.js +2 -2
  32. package/dist/context/FramerMotionConfig.js +1 -1
  33. package/dist/hooks/useDiagram.d.ts +313 -90
  34. package/dist/hooks/useDiagramActor.d.ts +2 -2
  35. package/dist/hooks/useDiagramContext.d.ts +1 -1
  36. package/dist/hooks/useXYFlow.d.ts +2 -1
  37. package/dist/hooks/useXYFlow.js +5 -0
  38. package/dist/likec4diagram/XYFlow.js +12 -10
  39. package/dist/likec4diagram/custom/edges/RelationshipEdge.d.ts +1 -1
  40. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.js +2 -2
  41. package/dist/likec4diagram/custom/edges/utils.js +0 -1
  42. package/dist/likec4diagram/custom/index.d.ts +1 -1
  43. package/dist/likec4diagram/custom/nodes/nodeTypes.js +2 -2
  44. package/dist/likec4diagram/state/actorContext.d.ts +1438 -364
  45. package/dist/likec4diagram/state/assign.js +2 -2
  46. package/dist/likec4diagram/state/machine.d.ts +321 -93
  47. package/dist/likec4diagram/state/syncManualLayoutActor.d.ts +1 -1
  48. package/dist/likec4diagram/types.d.ts +1 -0
  49. package/dist/likec4diagram/ui/diagram-title/DiagramTitlePanel.js +4 -3
  50. package/dist/likec4diagram/useViewToNodesEdges.js +3 -1
  51. package/dist/overlays/element-details/ElementDetailsCard.css.d.ts +0 -1
  52. package/dist/overlays/element-details/ElementDetailsCard.css.js +1 -3
  53. package/dist/overlays/element-details/ElementDetailsCard.js +5 -30
  54. package/dist/overlays/relationship-details/RelationshipDetails.js +4 -4
  55. package/dist/overlays/relationship-details/custom/ElementActions.js +1 -3
  56. package/dist/overlays/relationship-details/custom/edgeTypes.d.ts +1 -1
  57. package/dist/overlays/relationship-details/hooks.d.ts +2 -2
  58. package/dist/overlays/relationship-details/hooks.js +7 -7
  59. package/dist/overlays/relationship-details/useLayoutedDetails.js +7 -10
  60. package/dist/overlays/relationships-browser/-useRelationshipsView.d.ts +4 -1
  61. package/dist/overlays/relationships-browser/-useRelationshipsView.js +12 -11
  62. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +81 -66
  63. package/dist/overlays/relationships-browser/SelectElement.js +0 -1
  64. package/dist/overlays/relationships-browser/_types.d.ts +2 -1
  65. package/dist/overlays/relationships-browser/actor.d.ts +15 -2
  66. package/dist/overlays/relationships-browser/actor.js +262 -65
  67. package/dist/overlays/relationships-browser/const.d.ts +1 -0
  68. package/dist/overlays/relationships-browser/const.js +4 -0
  69. package/dist/overlays/relationships-browser/custom/ElementActions.js +1 -1
  70. package/dist/overlays/relationships-browser/custom/edgeTypes.d.ts +3 -2
  71. package/dist/overlays/relationships-browser/custom/edgeTypes.js +24 -6
  72. package/dist/overlays/relationships-browser/custom/nodeTypes.js +3 -2
  73. package/dist/overlays/relationships-browser/hooks.d.ts +6 -3
  74. package/dist/overlays/relationships-browser/hooks.js +4 -2
  75. package/dist/overlays/relationships-browser/useViewToNodesEdges.js +7 -2
  76. package/dist/style.css +1 -1
  77. package/dist/utils/xyflow.js +9 -1
  78. package/package.json +8 -8
  79. package/src/LikeC4Diagram.css.ts +8 -0
  80. package/src/base/BaseXYFlow.tsx +23 -14
  81. package/src/base/Link.css.ts +49 -0
  82. package/src/base/Link.tsx +66 -0
  83. package/src/base/const.ts +1 -0
  84. package/src/base/index.ts +2 -0
  85. package/src/base/primitives/compound/CompoundActionButton.tsx +2 -1
  86. package/src/base/primitives/compound/CompoundDetailsButton.tsx +2 -1
  87. package/src/base/primitives/compound/CompoundNodeContainer.css.ts +16 -10
  88. package/src/base/primitives/compound/CompoundNodeContainer.tsx +1 -1
  89. package/src/base/primitives/customEdge.ts +2 -0
  90. package/src/base/primitives/customNode.ts +3 -0
  91. package/src/base/primitives/edge/EdgeLabel.tsx +3 -1
  92. package/src/base/primitives/edge/EdgePath.tsx +3 -0
  93. package/src/base/primitives/element/ElementActionButtons.tsx +2 -1
  94. package/src/base/primitives/element/ElementDetailsButton.tsx +2 -1
  95. package/src/base/primitives/element/ElementNodeContainer.css.ts +9 -4
  96. package/src/base/primitives/element/ElementNodeContainer.tsx +1 -2
  97. package/src/base/primitives/element/ElementShape.tsx +4 -3
  98. package/src/base/primitives/element/ElementTitle.tsx +2 -1
  99. package/src/base/updateEdges.ts +3 -2
  100. package/src/base/updateNodes.ts +3 -2
  101. package/src/context/DiagramFeatures.tsx +1 -0
  102. package/src/context/FramerMotionConfig.tsx +1 -1
  103. package/src/hooks/useDiagramActor.ts +7 -5
  104. package/src/hooks/useDiagramContext.ts +4 -1
  105. package/src/hooks/useXYFlow.ts +6 -1
  106. package/src/likec4diagram/XYFlow.tsx +20 -9
  107. package/src/likec4diagram/custom/edges/RelationshipsDropdownMenu.tsx +2 -2
  108. package/src/likec4diagram/custom/edges/utils.ts +0 -1
  109. package/src/likec4diagram/custom/nodes/nodeTypes.tsx +2 -2
  110. package/src/likec4diagram/types.ts +1 -0
  111. package/src/likec4diagram/ui/diagram-title/DiagramTitlePanel.tsx +4 -3
  112. package/src/likec4diagram/useViewToNodesEdges.ts +1 -0
  113. package/src/overlays/element-details/ElementDetailsCard.css.ts +0 -28
  114. package/src/overlays/element-details/ElementDetailsCard.tsx +2 -50
  115. package/src/overlays/relationship-details/RelationshipDetails.tsx +4 -4
  116. package/src/overlays/relationship-details/custom/ElementActions.tsx +1 -2
  117. package/src/overlays/relationship-details/custom/edgeTypes.tsx +0 -5
  118. package/src/overlays/relationship-details/hooks.ts +6 -6
  119. package/src/overlays/relationship-details/useLayoutedDetails.ts +7 -10
  120. package/src/overlays/relationships-browser/-useRelationshipsView.ts +16 -12
  121. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +105 -68
  122. package/src/overlays/relationships-browser/SelectElement.tsx +0 -1
  123. package/src/overlays/relationships-browser/_types.ts +2 -1
  124. package/src/overlays/relationships-browser/actor.ts +321 -62
  125. package/src/overlays/relationships-browser/const.ts +1 -0
  126. package/src/overlays/relationships-browser/custom/ElementActions.tsx +1 -1
  127. package/src/overlays/relationships-browser/custom/edgeTypes.tsx +25 -6
  128. package/src/overlays/relationships-browser/custom/nodeTypes.tsx +3 -2
  129. package/src/overlays/relationships-browser/hooks.ts +3 -1
  130. package/src/overlays/relationships-browser/useViewToNodesEdges.ts +8 -2
  131. package/dist/likec4diagram/ui/diagram-title/Link.js +0 -36
  132. package/dist/ui/Link.d.ts +0 -4
  133. package/src/ui/Link.tsx +0 -40
@@ -1,32 +1,49 @@
1
- import { type BBox, type DiagramView, type Fqn, invariant } from '@likec4/core'
1
+ import { type BBox, type DiagramView, type Fqn, delay, invariant, nonNullable } from '@likec4/core'
2
2
  import {
3
3
  type EdgeChange,
4
4
  type NodeChange,
5
5
  type ReactFlowInstance,
6
6
  applyEdgeChanges,
7
7
  applyNodeChanges,
8
+ useStoreApi,
8
9
  } from '@xyflow/react'
9
- import { prop } from 'remeda'
10
+ import { type InternalNodeUpdate, getNodeDimensions, getViewportForBounds } from '@xyflow/system'
11
+ import { isNullish, omit, prop } from 'remeda'
10
12
  import {
11
13
  type ActorLogic,
12
14
  type ActorRefFromLogic,
15
+ type BaseActorRef,
13
16
  type MachineSnapshot,
14
17
  type SnapshotFrom,
15
18
  assign,
16
19
  cancel,
17
20
  enqueueActions,
21
+ fromPromise,
18
22
  raise,
19
23
  setup,
20
24
  } from 'xstate'
21
- import { MinZoom } from '../../base/const'
25
+ import { Base } from '../../base'
26
+ import { MinZoom, ZIndexes } from '../../base/const'
22
27
  import { updateEdges } from '../../base/updateEdges'
23
28
  import { updateNodes } from '../../base/updateNodes'
29
+ import { centerXYInternalNode } from '../../utils'
24
30
  import type { LayoutRelationshipsViewResult } from './-useRelationshipsView'
25
31
  import type { RelationshipsBrowserTypes } from './_types'
32
+ import { ViewPadding } from './const'
26
33
  import { viewToNodesEdge } from './useViewToNodesEdges'
27
34
 
28
35
  type XYFLowInstance = ReactFlowInstance<RelationshipsBrowserTypes.Node, RelationshipsBrowserTypes.Edge>
29
36
 
37
+ /**
38
+ * Root node in 'subjects' column
39
+ */
40
+ const findRootSubject = (nodes: RelationshipsBrowserTypes.Node[]) =>
41
+ nodes.find((n): n is RelationshipsBrowserTypes.ElementNode | RelationshipsBrowserTypes.CompoundNode =>
42
+ n.data.column === 'subjects' && isNullish(n.parentId)
43
+ )
44
+
45
+ export type XYStoreApi = ReturnType<typeof useStoreApi<RelationshipsBrowserTypes.Node, RelationshipsBrowserTypes.Edge>>
46
+
30
47
  export type Input = {
31
48
  subject: Fqn
32
49
  scope?: DiagramView | null
@@ -41,15 +58,17 @@ export type Context = Readonly<
41
58
  enableNavigationMenu: boolean
42
59
  // parentRef: AnyActorRef | null
43
60
  xyflow: XYFLowInstance | null
61
+ xystore: XYStoreApi | null
44
62
  initialized: boolean
45
63
  layouted: LayoutRelationshipsViewResult | null
64
+ navigateFromNode: string | null
46
65
  xynodes: RelationshipsBrowserTypes.Node[]
47
66
  xyedges: RelationshipsBrowserTypes.Edge[]
48
67
  }
49
68
  >
50
69
 
51
70
  export type Events =
52
- | { type: 'xyflow.init'; instance: XYFLowInstance }
71
+ | { type: 'xyflow.init'; instance: XYFLowInstance; store: XYStoreApi }
53
72
  | { type: 'xyflow.nodeClick'; node: RelationshipsBrowserTypes.Node }
54
73
  | { type: 'xyflow.edgeClick'; edge: RelationshipsBrowserTypes.Edge }
55
74
  | { type: 'xyflow.applyNodeChages'; changes: NodeChange<RelationshipsBrowserTypes.Node>[] }
@@ -59,85 +78,310 @@ export type Events =
59
78
  | { type: 'xyflow.resized' }
60
79
  | { type: 'xyflow.edgeMouseEnter'; edge: RelationshipsBrowserTypes.Edge }
61
80
  | { type: 'xyflow.edgeMouseLeave'; edge: RelationshipsBrowserTypes.Edge }
81
+ | { type: 'xyflow.updateNodeInternals' }
62
82
  | { type: 'fitDiagram'; duration?: number; bounds?: BBox }
63
- | { type: 'navigate.to'; subject: Fqn }
83
+ | { type: 'navigate.to'; subject: Fqn; fromNode?: string | undefined }
84
+ | {
85
+ type: 'update.xydata'
86
+ xynodes: RelationshipsBrowserTypes.Node[]
87
+ xyedges: RelationshipsBrowserTypes.Edge[]
88
+ }
64
89
  | { type: 'update.view'; layouted: LayoutRelationshipsViewResult }
65
90
  | { type: 'close' }
66
91
 
67
92
  export const relationshipsBrowserActor = setup({
68
93
  types: {
69
94
  context: {} as Context,
95
+ tags: '' as 'active',
70
96
  input: {} as Input,
71
97
  events: {} as Events,
72
98
  },
73
99
  actions: {
100
+ 'xyflow:updateNodeInternals': ({ context }) => {
101
+ invariant(context.xystore, 'xystore is not initialized')
102
+ const { domNode, updateNodeInternals } = context.xystore.getState()
103
+ const nodeIds = new Set(context.xyedges.flatMap((e) => [e.source, e.target]))
104
+
105
+ if (nodeIds.size === 0 || !domNode) {
106
+ return
107
+ }
108
+
109
+ const updates = new Map<string, InternalNodeUpdate>()
110
+
111
+ for (const updateId of nodeIds) {
112
+ const nodeElement = domNode.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement
113
+ if (nodeElement) {
114
+ updates.set(updateId, { id: updateId, nodeElement, force: true })
115
+ }
116
+ }
117
+
118
+ requestAnimationFrame(() => updateNodeInternals(updates, { triggerFitView: false }))
119
+ },
74
120
  'xyflow:fitDiagram': ({ context }, params?: { duration?: number; bounds?: BBox }) => {
75
- const {
121
+ let {
76
122
  duration = 450,
77
123
  bounds,
78
124
  } = params ?? {}
79
- const { xyflow } = context
125
+ const { xyflow, xystore } = context
80
126
  invariant(xyflow, 'xyflow is not initialized')
127
+ invariant(xystore, 'xystore is not initialized')
128
+ bounds ??= context.layouted?.bounds
129
+ const maxZoom = Math.max(xyflow.getZoom(), 1)
81
130
  if (bounds) {
82
- xyflow.fitBounds(bounds, duration > 0 ? { duration } : undefined)
131
+ const { width, height } = xystore.getState()
132
+ const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, ViewPadding)
133
+ requestAnimationFrame(() => xyflow.setViewport(viewport, duration > 0 ? { duration } : undefined))
83
134
  } else {
84
- const maxZoom = Math.max(xyflow.getZoom(), 1)
85
- xyflow.fitView({
86
- minZoom: MinZoom,
87
- maxZoom,
88
- padding: 0.1,
89
- ...(duration > 0 && { duration }),
90
- })
135
+ requestAnimationFrame(() =>
136
+ xyflow.fitView({
137
+ minZoom: MinZoom,
138
+ maxZoom,
139
+ padding: ViewPadding,
140
+ ...(duration > 0 && { duration }),
141
+ })
142
+ )
91
143
  }
92
144
  },
93
145
  },
94
146
  guards: {
95
147
  'enable: navigate.to': () => true,
148
+ 'closeable': ({ context }) => context.closeable,
149
+ },
150
+ actors: {
151
+ layouter: fromPromise<{
152
+ xyedges: RelationshipsBrowserTypes.Edge[]
153
+ xynodes: RelationshipsBrowserTypes.Node[]
154
+ }, {
155
+ subjectId: Fqn
156
+ navigateFromNode: string | null
157
+ xyflow: XYFLowInstance
158
+ xystore: XYStoreApi
159
+ update: LayoutRelationshipsViewResult
160
+ }>(async ({ input, self, signal }) => {
161
+ const {
162
+ subjectId,
163
+ navigateFromNode,
164
+ xyflow,
165
+ xystore,
166
+ update,
167
+ } = input
168
+ let {
169
+ nodes: currentNodes,
170
+ setNodes,
171
+ setEdges,
172
+ width,
173
+ height,
174
+ } = xystore.getState()
175
+ const next = viewToNodesEdge(update)
176
+
177
+ const updateXYData = () => {
178
+ const { nodes, edges } = xystore.getState()
179
+ return {
180
+ xynodes: updateNodes(nodes, next.xynodes),
181
+ xyedges: updateEdges(edges, next.xyedges),
182
+ }
183
+ }
184
+ const parent = nonNullable(self._parent) as BaseActorRef<Events>
185
+
186
+ let zoom = xyflow.getZoom()
187
+ const maxZoom = Math.max(zoom, 1)
188
+ const nextviewport = getViewportForBounds(update.bounds, width, height, MinZoom, maxZoom, ViewPadding)
189
+
190
+ const nextSubjectNode = next.xynodes.find(n =>
191
+ n.type !== 'empty' && n.data.column === 'subjects' && n.data.fqn === subjectId
192
+ ) ?? findRootSubject(next.xynodes)
193
+ const currentSubjectNode = findRootSubject(currentNodes)
194
+
195
+ const existingNode = navigateFromNode
196
+ ? currentNodes.find(n => n.id === navigateFromNode)
197
+ : currentNodes.find(n => n.type !== 'empty' && n.data.column !== 'subjects' && n.data.fqn === subjectId)
198
+
199
+ if (
200
+ !nextSubjectNode || !existingNode || nextSubjectNode.type === 'empty' || !currentSubjectNode ||
201
+ nextSubjectNode.data.fqn === currentSubjectNode.data.fqn
202
+ ) {
203
+ xyflow.setViewport(nextviewport)
204
+ return updateXYData()
205
+ }
206
+
207
+ const nextSubjectCenter = {
208
+ x: nextSubjectNode.position.x + (nextSubjectNode.initialWidth ?? 0) / 2,
209
+ y: nextSubjectNode.position.y + (nextSubjectNode.initialHeight ?? 0) / 2,
210
+ }
211
+
212
+ // Center of current subject
213
+ const currentSubjectInternalNode = xyflow.getInternalNode(currentSubjectNode.id)!
214
+ const currentSubjectCenter = centerXYInternalNode(currentSubjectInternalNode)
215
+
216
+ // Move to center of existing node
217
+ const existingInternalNode = xyflow.getInternalNode(existingNode.id)!
218
+ const existingDimensions = getNodeDimensions(existingInternalNode)
219
+
220
+ // Dim all nodes except the existing node
221
+ // Hide nested nodes
222
+ const nested = new Set<string>()
223
+ currentNodes.forEach(n => {
224
+ if (n.id === existingNode.id) {
225
+ return
226
+ }
227
+ if (n.data.column === 'subjects') {
228
+ nested.add(n.id)
229
+ return
230
+ }
231
+ if (n.parentId && (n.parentId === existingNode.id || nested.has(n.parentId))) {
232
+ nested.add(n.id)
233
+ }
234
+ })
235
+ currentNodes = updateNodes(
236
+ currentNodes,
237
+ currentNodes.flatMap(n => {
238
+ if (nested.has(n.id)) {
239
+ return []
240
+ }
241
+ if (n.id !== existingNode.id) {
242
+ return {
243
+ ...n,
244
+ data: {
245
+ ...n.data,
246
+ dimmed: n.data.column === 'subjects' ? 'immediate' : true,
247
+ },
248
+ } as RelationshipsBrowserTypes.Node
249
+ }
250
+ // Move existing node
251
+ return {
252
+ ...omit(n, ['parentId']),
253
+ position: {
254
+ x: currentSubjectCenter.x - existingDimensions.width / 2,
255
+ y: currentSubjectCenter.y - existingDimensions.height / 2,
256
+ },
257
+ zIndex: ZIndexes.Max,
258
+ hidden: false,
259
+ data: {
260
+ ...n.data,
261
+ dimmed: false,
262
+ },
263
+ } as RelationshipsBrowserTypes.Node
264
+ }),
265
+ )
266
+ parent.send({
267
+ type: 'update.xydata',
268
+ xynodes: currentNodes,
269
+ xyedges: [],
270
+ })
271
+
272
+ // Pick the smaller zoom level
273
+ zoom = Math.min(
274
+ zoom,
275
+ nextviewport.zoom,
276
+ )
277
+ // allow framer to render
278
+ await delay(180)
279
+ next.xynodes = next.xynodes.map(Base.setDimmed(false))
280
+
281
+ if (signal.aborted) {
282
+ return updateXYData()
283
+ }
284
+ await xyflow.setCenter(currentSubjectCenter.x, currentSubjectCenter.y, { zoom, duration: 350 })
285
+ xyflow.setCenter(nextSubjectCenter.x, nextSubjectCenter.y, { zoom })
286
+ return updateXYData()
287
+ }),
96
288
  },
97
289
  }).createMachine({
98
- initial: 'waiting-data',
290
+ initial: 'initializing',
99
291
  context: ({ input }) => ({
100
292
  ...input,
101
293
  closeable: input.closeable ?? true,
102
294
  enableNavigationMenu: input.enableNavigationMenu ?? true,
103
295
  initialized: false,
104
296
  xyflow: null,
297
+ xystore: null,
105
298
  layouted: null,
299
+ navigateFromNode: null,
106
300
  xynodes: [],
107
301
  xyedges: [],
108
302
  }),
109
303
  states: {
110
- 'waiting-data': {
304
+ 'initializing': {
111
305
  on: {
112
- 'update.view': {
113
- actions: assign(({ event }) => {
114
- return {
115
- layouted: event.layouted,
116
- ...viewToNodesEdge(event.layouted),
117
- }
118
- }),
119
- target: 'opening',
306
+ 'xyflow.init': {
307
+ actions: [
308
+ assign({
309
+ initialized: true,
310
+ xyflow: ({ event }) => event.instance,
311
+ xystore: ({ event }) => event.store,
312
+ }),
313
+ ],
314
+ target: 'waiting-data',
315
+ },
316
+ 'close': {
317
+ target: 'closed',
120
318
  },
121
319
  },
122
320
  },
123
- 'opening': {
321
+ 'waiting-data': {
124
322
  on: {
125
- 'xyflow.init': {
126
- actions: assign({
127
- initialized: true,
128
- xyflow: ({ event }) => event.instance,
129
- }),
323
+ 'update.view': {
324
+ actions: [
325
+ assign(({ event }) => {
326
+ return {
327
+ layouted: event.layouted,
328
+ ...viewToNodesEdge(event.layouted),
329
+ }
330
+ }),
331
+ raise({ type: 'fitDiagram', duration: 0 }),
332
+ ],
130
333
  target: 'active',
131
334
  },
132
- 'close': {
133
- target: 'closed',
134
- },
135
335
  },
136
336
  },
137
337
  'active': {
138
- entry: {
139
- type: 'xyflow:fitDiagram',
140
- params: { duration: 0 },
338
+ initial: 'idle',
339
+ tags: ['active'],
340
+ states: {
341
+ 'idle': {},
342
+ 'layouting': {
343
+ invoke: {
344
+ id: 'layouter',
345
+ src: 'layouter',
346
+ input: ({ context }) => {
347
+ return {
348
+ subjectId: context.subject,
349
+ navigateFromNode: context.navigateFromNode,
350
+ xyflow: nonNullable(context.xyflow),
351
+ xystore: nonNullable(context.xystore),
352
+ update: nonNullable(context.layouted),
353
+ }
354
+ },
355
+ onDone: {
356
+ target: 'idle',
357
+ actions: [
358
+ assign({
359
+ xynodes: ({ event }) => event.output.xynodes,
360
+ xyedges: ({ event }) => event.output.xyedges,
361
+ navigateFromNode: null,
362
+ }),
363
+ raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 80 }),
364
+ raise({ type: 'xyflow.updateNodeInternals' }, { delay: 100 }),
365
+ raise({ type: 'xyflow.updateNodeInternals' }, { delay: 250 }),
366
+ raise({ type: 'xyflow.updateNodeInternals' }, { delay: 500 }),
367
+ ],
368
+ },
369
+ },
370
+ on: {
371
+ 'update.xydata': {
372
+ actions: assign({
373
+ xynodes: ({ event }) => event.xynodes,
374
+ xyedges: ({ event }) => event.xyedges,
375
+ }),
376
+ },
377
+ 'xyflow.applyEdgeChages': {
378
+ // actions: log('layouting: ignore xyflow.applyEdgeChages'),
379
+ },
380
+ 'xyflow.applyNodeChages': {
381
+ // actions: log('layouting: ignore xyflow.applyNodeChages'),
382
+ },
383
+ },
384
+ },
141
385
  },
142
386
  on: {
143
387
  'xyflow.nodeClick': {
@@ -145,8 +389,10 @@ export const relationshipsBrowserActor = setup({
145
389
  actions: enqueueActions(({ event, enqueue }) => {
146
390
  if ('fqn' in event.node.data) {
147
391
  const fqn = event.node.data.fqn
148
- enqueue.assign({
392
+ enqueue.raise({
393
+ type: 'navigate.to',
149
394
  subject: fqn,
395
+ fromNode: event.node.id,
150
396
  })
151
397
  }
152
398
  }),
@@ -156,6 +402,7 @@ export const relationshipsBrowserActor = setup({
156
402
  actions: [
157
403
  assign({
158
404
  subject: ({ event }) => event.subject,
405
+ navigateFromNode: ({ event }) => event.fromNode ?? null,
159
406
  }),
160
407
  ],
161
408
  },
@@ -163,24 +410,35 @@ export const relationshipsBrowserActor = setup({
163
410
  actions: 'xyflow:fitDiagram',
164
411
  },
165
412
  'update.view': {
166
- actions: enqueueActions(({ context, event, enqueue }) => {
167
- const updated = viewToNodesEdge(event.layouted)
168
- enqueue.assign({
169
- layouted: event.layouted,
170
- xynodes: updateNodes(context.xynodes, updated.xynodes),
171
- xyedges: updateEdges(context.xyedges, updated.xyedges),
172
- })
173
- enqueue.raise({ type: 'fitDiagram' }, { delay: 50 })
413
+ actions: assign({
414
+ layouted: ({ event }) => event.layouted,
174
415
  }),
416
+ target: '.layouting',
417
+ },
418
+ 'xyflow.updateNodeInternals': {
419
+ actions: 'xyflow:updateNodeInternals',
420
+ },
421
+ 'fitDiagram': {
422
+ actions: {
423
+ type: 'xyflow:fitDiagram',
424
+ params: prop('event'),
425
+ },
426
+ },
427
+ 'xyflow.resized': {
428
+ actions: [
429
+ cancel('fitDiagram'),
430
+ raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 300 }),
431
+ ],
175
432
  },
176
433
  'close': {
177
- guard: ({ context }) => context.closeable,
434
+ guard: 'closeable',
178
435
  target: 'closed',
179
436
  },
180
437
  },
181
438
  exit: assign({
182
439
  initialized: false,
183
440
  xyflow: null,
441
+ xystore: null,
184
442
  }),
185
443
  },
186
444
  'closed': {
@@ -188,31 +446,32 @@ export const relationshipsBrowserActor = setup({
188
446
  },
189
447
  },
190
448
  on: {
191
- 'fitDiagram': {
192
- actions: {
193
- type: 'xyflow:fitDiagram',
194
- params: prop('event'),
195
- },
196
- },
197
- 'xyflow.resized': {
198
- actions: [
199
- cancel('fitDiagram'),
200
- raise({ type: 'fitDiagram' }, { id: 'fitDiagram', delay: 200 }),
201
- ],
202
- },
203
449
  'xyflow.applyNodeChages': {
204
450
  actions: assign({
205
- xynodes: ({ context, event }) => applyNodeChanges(event.changes, context.xynodes),
451
+ xynodes: ({ context, event }) => {
452
+ return applyNodeChanges(event.changes, context.xynodes)
453
+ },
206
454
  }),
207
455
  },
208
456
  'xyflow.applyEdgeChages': {
209
457
  actions: assign({
210
- xyedges: ({ context, event }) => applyEdgeChanges(event.changes, context.xyedges),
458
+ xyedges: ({ context, event }) => {
459
+ return applyEdgeChanges(event.changes, context.xyedges)
460
+ },
211
461
  }),
212
462
  },
213
463
  },
214
464
  }) as unknown as ActorLogic<
215
- MachineSnapshot<Context, Events, any, 'opening' | 'active' | 'closed', any, any, any, any>,
465
+ MachineSnapshot<
466
+ Context,
467
+ Events,
468
+ any,
469
+ 'initializing' | 'waiting-data' | 'closed' | { active: 'idle' | 'layouting' },
470
+ 'active',
471
+ any,
472
+ any,
473
+ any
474
+ >,
216
475
  Events,
217
476
  Input,
218
477
  any,
@@ -0,0 +1 @@
1
+ export const ViewPadding = 0.08
@@ -31,7 +31,7 @@ export const ElementActions = (props: ElementActionsProps) => {
31
31
  icon: <IconTransform />,
32
32
  onClick: (e) => {
33
33
  e.stopPropagation()
34
- browser.navigateTo(fqn)
34
+ browser.navigateTo(fqn, props.id)
35
35
  },
36
36
  })
37
37
  }
@@ -8,17 +8,36 @@ export const edgeTypes = {
8
8
  relationships: customEdge<RelationshipsBrowserTypes.EdgeData>((props) => {
9
9
  const { enableNavigateTo } = useEnabledFeature('NavigateTo')
10
10
  const {
11
- sourceX,
12
- targetY,
13
- data: { navigateTo },
11
+ data: {
12
+ navigateTo,
13
+ relations,
14
+ },
14
15
  } = props
15
16
  const [svgPath, labelX, labelY] = getBezierPath(props)
16
17
  const diagram = useDiagram()
18
+
19
+ const edgeProps = relations.length > 1
20
+ ? {
21
+ ...props,
22
+ data: {
23
+ ...props.data,
24
+ line: 'solid',
25
+ color: 'amber',
26
+ } satisfies RelationshipsBrowserTypes.EdgeData,
27
+ }
28
+ : props
29
+
17
30
  return (
18
- <EdgeContainer {...props}>
19
- <EdgePath {...props} svgPath={svgPath} />
31
+ <EdgeContainer {...edgeProps}>
32
+ <EdgePath
33
+ {...edgeProps}
34
+ svgPath={svgPath}
35
+ {...relations.length > 1 && {
36
+ strokeWidth: 5,
37
+ }}
38
+ />
20
39
  <EdgeLabel
21
- edgeProps={props}
40
+ edgeProps={edgeProps}
22
41
  labelPosition={{
23
42
  x: labelX,
24
43
  y: labelY,
@@ -1,5 +1,6 @@
1
1
  import { Handle } from '@xyflow/react'
2
2
  import { Position } from '@xyflow/system'
3
+ import { m } from 'framer-motion'
3
4
  import type { NodeProps } from '../../../base'
4
5
  import {
5
6
  CompoundNodeContainer,
@@ -16,7 +17,7 @@ import { EmptyNode } from './EmptyNode'
16
17
  export const nodeTypes = {
17
18
  element: customNode<RelationshipsBrowserTypes.ElementNodeData>((props) => {
18
19
  return (
19
- <ElementNodeContainer nodeProps={props}>
20
+ <ElementNodeContainer component={m.div} layoutId={props.id} nodeProps={props}>
20
21
  <ElementShape {...props} />
21
22
  <ElementTitle {...props} iconSize={40} />
22
23
  <ElementActions {...props} />
@@ -27,7 +28,7 @@ export const nodeTypes = {
27
28
 
28
29
  compound: customNode<RelationshipsBrowserTypes.CompoundNodeData>((props) => {
29
30
  return (
30
- <CompoundNodeContainer nodeProps={props}>
31
+ <CompoundNodeContainer component={m.div} layoutId={props.id} nodeProps={props}>
31
32
  <CompoundTitle {...props} />
32
33
  <CompoundPorts {...props} />
33
34
  </CompoundNodeContainer>
@@ -1,6 +1,7 @@
1
1
  import { type Fqn, nonNullable } from '@likec4/core'
2
2
  import { useCallbackRef } from '@mantine/hooks'
3
3
  import { useSelector } from '@xstate/react'
4
+ import { useStoreApi } from '@xyflow/react'
4
5
  import { shallowEqual } from 'fast-equals'
5
6
  import { createContext, useContext, useMemo, useTransition } from 'react'
6
7
  import type { RelationshipsBrowserActorRef, RelationshipsBrowserSnapshot } from './actor'
@@ -26,11 +27,12 @@ export function useRelationshipsBrowser() {
26
27
  return useMemo(() => ({
27
28
  getState: () => actor.getSnapshot().context,
28
29
  send: actor.send,
29
- navigateTo: (subject: Fqn) => {
30
+ navigateTo: (subject: Fqn, fromNode?: string) => {
30
31
  startTransition(() => {
31
32
  actor.send({
32
33
  type: 'navigate.to',
33
34
  subject,
35
+ fromNode,
34
36
  })
35
37
  })
36
38
  },
@@ -163,18 +163,24 @@ export function viewToNodesEdge(
163
163
  continue
164
164
  }
165
165
 
166
+ if (!hasAtLeast(edge.relations, 1)) {
167
+ console.error('edge should have at least 1 relation', edge)
168
+ continue
169
+ }
170
+
166
171
  xyedges.push({
167
172
  id,
168
173
  type: 'relationships',
169
174
  source: ns + source,
170
175
  target: ns + target,
171
- sourceHandle: ns + target,
172
- targetHandle: ns + source,
176
+ sourceHandle: edge.sourceHandle,
177
+ targetHandle: edge.targetHandle,
173
178
  zIndex: ZIndexes.Edge,
174
179
  // selectable: selectable,
175
180
  // hidden: !visiblePredicate(edge),
176
181
  deletable: false,
177
182
  data: {
183
+ relations: edge.relations,
178
184
  color: edge.color ?? 'gray',
179
185
  label: edge.label,
180
186
  navigateTo: edge.navigateTo ?? null,