@likec4/diagram 1.9.0 → 1.12.1

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 (113) hide show
  1. package/package.json +20 -43
  2. package/src/LikeC4CustomColors.tsx +5 -2
  3. package/src/LikeC4Diagram.css.ts +1 -1
  4. package/src/LikeC4Diagram.props.ts +40 -94
  5. package/src/LikeC4Diagram.tsx +14 -4
  6. package/src/LikeC4ModelProvider.tsx +21 -0
  7. package/src/StaticLikeC4Diagram.tsx +1 -0
  8. package/src/globals.css.ts +2 -3
  9. package/src/hooks/index.ts +1 -0
  10. package/src/hooks/useMantinePortalProps.ts +8 -0
  11. package/src/hooks/useXYFlow.ts +1 -2
  12. package/src/index.ts +9 -0
  13. package/src/likec4model/LikeC4ModelContext.ts +4 -0
  14. package/src/likec4model/index.ts +1 -0
  15. package/src/likec4model/useLikeC4Model.ts +58 -0
  16. package/src/state/diagram-to-xyflow.ts +1 -2
  17. package/src/state/diagramStore.ts +185 -66
  18. package/src/{theme.css.ts → theme-vars.ts} +11 -4
  19. package/src/ui/DiagramTitlePanel.css.ts +2 -3
  20. package/src/ui/DiagramTitlePanel.tsx +25 -3
  21. package/src/ui/DynamicViewWalkthrough.css.ts +28 -2
  22. package/src/ui/DynamicViewWalkthrough.tsx +97 -52
  23. package/src/ui/OptionsPanel.css.ts +1 -2
  24. package/src/ui/OptionsPanel.tsx +5 -2
  25. package/src/ui/notation/NotationPanel.css.ts +2 -3
  26. package/src/ui/notation/NotationPanel.tsx +10 -11
  27. package/src/ui/options/NodeOptions.tsx +4 -1
  28. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +122 -71
  29. package/src/ui/top-left/TopLeftPanel.tsx +128 -117
  30. package/src/ui/top-left/_shared.ts +7 -6
  31. package/src/ui/top-left/styles.css.ts +48 -8
  32. package/src/utils/vector.ts +57 -0
  33. package/src/xyflow/FitviewOnDiagramChange.tsx +5 -4
  34. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +7 -5
  35. package/src/xyflow/XYFlow.tsx +13 -17
  36. package/src/xyflow/XYFlowEvents.tsx +58 -58
  37. package/src/xyflow/XYFlowInner.tsx +4 -8
  38. package/src/xyflow/edges/EdgeLabel.tsx +185 -0
  39. package/src/xyflow/edges/RelationshipEdge.tsx +170 -137
  40. package/src/xyflow/edges/RelationshipsDropdownMenu.css.ts +60 -0
  41. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +214 -0
  42. package/src/xyflow/edges/edges.css.ts +75 -7
  43. package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -2
  44. package/src/xyflow/nodes/compound/CompoundNode.tsx +25 -8
  45. package/src/xyflow/nodes/element/ElementLink.css.ts +1 -1
  46. package/src/xyflow/nodes/element/ElementLink.tsx +4 -8
  47. package/src/xyflow/nodes/element/ElementNode.tsx +47 -8
  48. package/src/xyflow/nodes/element/element.css.ts +1 -2
  49. package/src/xyflow/nodes/shared/Toolbar.tsx +426 -0
  50. package/src/xyflow/types.ts +1 -24
  51. package/src/xyflow/useLayoutConstraints.ts +10 -6
  52. package/dist/LikeC4CustomColors.d.ts +0 -3
  53. package/dist/LikeC4CustomColors.props.d.ts +0 -5
  54. package/dist/LikeC4Diagram.css.d.ts +0 -4
  55. package/dist/LikeC4Diagram.d.ts +0 -7
  56. package/dist/LikeC4Diagram.props.d.ts +0 -154
  57. package/dist/StaticLikeC4Diagram.d.ts +0 -4
  58. package/dist/globals.css.d.ts +0 -1
  59. package/dist/hooks/index.d.ts +0 -4
  60. package/dist/hooks/useDiagramState.d.ts +0 -6
  61. package/dist/hooks/useSetState.d.ts +0 -3
  62. package/dist/hooks/useUpdateEffect.d.ts +0 -5
  63. package/dist/hooks/useXYFlow.d.ts +0 -267
  64. package/dist/index.d.ts +0 -9
  65. package/dist/index.mjs +0 -3684
  66. package/dist/mantine/EnsureMantine.d.ts +0 -8
  67. package/dist/mantine.css.d.ts +0 -3
  68. package/dist/state/DiagramContext.d.ts +0 -1192
  69. package/dist/state/diagram-to-xyflow.d.ts +0 -12
  70. package/dist/state/diagramStore.d.ts +0 -1268
  71. package/dist/style.css +0 -2139
  72. package/dist/theme.css.d.ts +0 -63
  73. package/dist/ui/DiagramTitlePanel.css.d.ts +0 -4
  74. package/dist/ui/DiagramTitlePanel.d.ts +0 -1
  75. package/dist/ui/DynamicViewWalkthrough.css.d.ts +0 -3
  76. package/dist/ui/DynamicViewWalkthrough.d.ts +0 -1
  77. package/dist/ui/OptionsPanel.css.d.ts +0 -1
  78. package/dist/ui/OptionsPanel.d.ts +0 -1
  79. package/dist/ui/notation/NotationPanel.css.d.ts +0 -6
  80. package/dist/ui/notation/NotationPanel.d.ts +0 -1
  81. package/dist/ui/notation/index.d.ts +0 -1
  82. package/dist/ui/options/EdgeOptions.d.ts +0 -19
  83. package/dist/ui/options/NodeOptions.d.ts +0 -19
  84. package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +0 -3
  85. package/dist/ui/top-left/TopLeftPanel.d.ts +0 -4
  86. package/dist/ui/top-left/_shared.d.ts +0 -5
  87. package/dist/ui/top-left/styles.css.d.ts +0 -3
  88. package/dist/xyflow/EdgeMarkers.d.ts +0 -12
  89. package/dist/xyflow/FitviewOnDiagramChange.d.ts +0 -4
  90. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +0 -1
  91. package/dist/xyflow/XYFlow.d.ts +0 -12
  92. package/dist/xyflow/XYFlowBackground.d.ts +0 -11
  93. package/dist/xyflow/XYFlowEvents.d.ts +0 -161
  94. package/dist/xyflow/XYFlowInner.d.ts +0 -10
  95. package/dist/xyflow/const.d.ts +0 -7
  96. package/dist/xyflow/edges/RelationshipEdge.d.ts +0 -32
  97. package/dist/xyflow/edges/edges.css.d.ts +0 -12
  98. package/dist/xyflow/edges/utils.d.ts +0 -17
  99. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -11
  100. package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -6
  101. package/dist/xyflow/nodes/compound/index.d.ts +0 -1
  102. package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -3
  103. package/dist/xyflow/nodes/element/ElementLink.d.ts +0 -7
  104. package/dist/xyflow/nodes/element/ElementNode.d.ts +0 -6
  105. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +0 -18
  106. package/dist/xyflow/nodes/element/element.css.d.ts +0 -19
  107. package/dist/xyflow/nodes/element/index.d.ts +0 -1
  108. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -5
  109. package/dist/xyflow/types.d.ts +0 -50
  110. package/dist/xyflow/useLayoutConstraints.d.ts +0 -9
  111. package/dist/xyflow/utils.d.ts +0 -18
  112. package/src/mantine.css.ts +0 -7
  113. package/src/reset.d.ts +0 -2
@@ -1,14 +1,24 @@
1
1
  import type {
2
2
  DiagramNode,
3
3
  DiagramView,
4
+ EdgeId,
4
5
  ElementKind,
5
6
  ElementNotation,
6
7
  Fqn,
7
8
  NodeId,
8
9
  ViewChange,
9
- ViewID
10
+ ViewID,
11
+ XYPoint
12
+ } from '@likec4/core'
13
+ import {
14
+ getBBoxCenter,
15
+ getParallelStepsPrefix,
16
+ invariant,
17
+ isStepEdgeId,
18
+ nonexhaustive,
19
+ nonNullable,
20
+ StepEdgeId
10
21
  } from '@likec4/core'
11
- import { getBBoxCenter, invariant, nonexhaustive, nonNullable, StepEdgeId } from '@likec4/core'
12
22
  import {
13
23
  applyEdgeChanges,
14
24
  applyNodeChanges,
@@ -20,7 +30,7 @@ import { boxToRect, getBoundsOfRects, getNodeDimensions } from '@xyflow/system'
20
30
  import { DEV } from 'esm-env'
21
31
  import { deepEqual as eq, shallowEqual } from 'fast-equals'
22
32
  import type { MouseEvent as ReactMouseEvent } from 'react'
23
- import { entries, hasAtLeast, isNullish, map, prop, reduce } from 'remeda'
33
+ import { entries, first, hasAtLeast, isNullish, map, prop, reduce } from 'remeda'
24
34
  import type { ConditionalKeys, Except, RequiredKeysOf, Simplify } from 'type-fest'
25
35
  import { devtools, subscribeWithSelector } from 'zustand/middleware'
26
36
  import { shallow } from 'zustand/shallow'
@@ -32,6 +42,7 @@ import type {
32
42
  LikeC4DiagramEventHandlers,
33
43
  WhereOperator
34
44
  } from '../LikeC4Diagram.props'
45
+ import { type Vector, vector } from '../utils/vector'
35
46
  import { MinZoom } from '../xyflow/const'
36
47
  import type { XYFlowEdge, XYFlowInstance, XYFlowNode } from '../xyflow/types'
37
48
  import { bezierControlPoints, isInside, isSamePoint, toDomPrecision } from '../xyflow/utils'
@@ -47,6 +58,7 @@ export type DiagramInitialState = {
47
58
  showElementLinks: boolean
48
59
  showNavigationButtons: boolean
49
60
  showNotations: boolean
61
+ showRelationshipDetails: boolean
50
62
  fitViewEnabled: boolean
51
63
  fitViewPadding: number
52
64
  zoomable: boolean
@@ -81,9 +93,17 @@ export type DiagramState = Simplify<
81
93
 
82
94
  initialized: boolean
83
95
 
96
+ // readonly=false and onChange is not null
97
+ isEditable: () => boolean
98
+
84
99
  // If Dynamic View
85
100
  isDynamicView: boolean
86
- activeDynamicViewStep: number | null
101
+ activeWalkthrough: null | {
102
+ stepId: StepEdgeId
103
+ hasPrevious: boolean
104
+ hasNext: boolean
105
+ parallelPrefix: string | null
106
+ }
87
107
 
88
108
  // This is XYFlow id's
89
109
  lastClickedNodeId: string | null
@@ -109,7 +129,11 @@ export type DiagramState = Simplify<
109
129
  // Actions
110
130
  updateView: (view: DiagramView) => void
111
131
 
112
- focusOnNode: (nodeId: string) => void
132
+ /**
133
+ * A function to set the focus on a specific node given its identifier.
134
+ * It can also be used to remove focus if the input is `false`.
135
+ */
136
+ focusOnNode: (nodeId: string | false) => void
113
137
 
114
138
  setHoveredNode: (nodeId: string | null) => void
115
139
  setHoveredEdge: (edgeId: string | null) => void
@@ -117,7 +141,7 @@ export type DiagramState = Simplify<
117
141
  setLastClickedNode: (nodeId: string | null) => void
118
142
  setLastClickedEdge: (edgeId: string | null) => void
119
143
 
120
- resetLastClicked: () => void
144
+ resetFocusAndLastClicked: () => void
121
145
 
122
146
  getElement(id: Fqn): DiagramNode | null
123
147
  triggerChangeElementStyle: (change: ViewChange.ChangeElementStyle) => void
@@ -136,14 +160,16 @@ export type DiagramState = Simplify<
136
160
  goForward: () => void
137
161
 
138
162
  nextDynamicStep: (increment?: number) => void
139
- activateDynamicStep: (step: number) => void
140
- stopDynamicView: () => void
163
+ activateWalkthrough: (step: EdgeId | XYFlowEdge) => void
164
+ stopWalkthrough: () => void
141
165
 
142
166
  onInit: (xyflow: XYFlowInstance) => void
143
167
  onNodesChange: OnNodesChange<XYFlowNode>
144
168
  onEdgesChange: OnEdgesChange<XYFlowEdge>
145
169
 
146
170
  highlightByElementNotation: (notation: ElementNotation, onlyOfKind?: ElementKind) => void
171
+
172
+ resetEdgeControlPoints: () => void
147
173
  }
148
174
  >
149
175
 
@@ -158,7 +184,7 @@ const DEFAULT_PROPS: Except<
158
184
  initialized: false,
159
185
  navigationHistoryIndex: 0,
160
186
  viewportChanged: false,
161
- activeDynamicViewStep: null,
187
+ activeWalkthrough: null,
162
188
  focusedNodeId: null,
163
189
  hoveredNodeId: null,
164
190
  hoveredEdgeId: null,
@@ -231,7 +257,7 @@ export function createDiagramStore(props: DiagramInitialState) {
231
257
  focusedNodeId,
232
258
  lastClickedNodeId,
233
259
  lastClickedEdgeId,
234
- activeDynamicViewStep,
260
+ activeWalkthrough,
235
261
  nodesDraggable,
236
262
  nodesSelectable,
237
263
  hoveredEdgeId,
@@ -259,6 +285,7 @@ export function createDiagramStore(props: DiagramInitialState) {
259
285
  }
260
286
  if (focusedNodeId && !nodeIds.has(focusedNodeId)) {
261
287
  focusedNodeId = null
288
+ dimmed = EmptyStringSet
262
289
  }
263
290
  if (lastClickedEdgeId && !edgeIds.has(lastClickedEdgeId)) {
264
291
  lastClickedEdgeId = null
@@ -266,8 +293,9 @@ export function createDiagramStore(props: DiagramInitialState) {
266
293
  if (hoveredEdgeId && !edgeIds.has(hoveredEdgeId)) {
267
294
  hoveredEdgeId = null
268
295
  }
269
- if (activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep))) {
270
- activeDynamicViewStep = null
296
+ if (activeWalkthrough && !edgeIds.has(activeWalkthrough.stepId)) {
297
+ activeWalkthrough = null
298
+ dimmed = EmptyStringSet
271
299
  }
272
300
  if (dimmed.size > 0) {
273
301
  let nextDimmed = new StringSet([...dimmed].filter(id => nodeIds.has(id) || edgeIds.has(id)))
@@ -335,7 +363,7 @@ export function createDiagramStore(props: DiagramInitialState) {
335
363
  hoveredEdgeId = null
336
364
  hoveredNodeId = null
337
365
  focusedNodeId = null
338
- activeDynamicViewStep = null
366
+ activeWalkthrough = null
339
367
  dimmed = EmptyStringSet
340
368
  }
341
369
 
@@ -400,7 +428,7 @@ export function createDiagramStore(props: DiagramInitialState) {
400
428
  isDynamicView: nextView.__ === 'dynamic',
401
429
  viewSyncDebounceTimeout,
402
430
  view: nextView,
403
- activeDynamicViewStep,
431
+ activeWalkthrough,
404
432
  lastOnNavigate,
405
433
  lastClickedNodeId,
406
434
  lastClickedEdgeId,
@@ -421,6 +449,18 @@ export function createDiagramStore(props: DiagramInitialState) {
421
449
  focusOnNode: (nodeId) => {
422
450
  const { focusedNodeId, view, enableFocusMode } = get()
423
451
  invariant(enableFocusMode, 'focus mode is not enabled')
452
+ if (nodeId === false) {
453
+ set(
454
+ {
455
+ activeWalkthrough: null,
456
+ focusedNodeId: null,
457
+ dimmed: EmptyStringSet
458
+ },
459
+ noReplace,
460
+ `unfocus`
461
+ )
462
+ return
463
+ }
424
464
  if (nodeId !== focusedNodeId) {
425
465
  const notDimmed = new StringSet([nodeId])
426
466
  const dimmed = new StringSet()
@@ -440,7 +480,7 @@ export function createDiagramStore(props: DiagramInitialState) {
440
480
  }
441
481
  set(
442
482
  {
443
- activeDynamicViewStep: null,
483
+ activeWalkthrough: null,
444
484
  focusedNodeId: nodeId,
445
485
  dimmed
446
486
  },
@@ -474,17 +514,18 @@ export function createDiagramStore(props: DiagramInitialState) {
474
514
  }
475
515
  },
476
516
 
477
- resetLastClicked: () => {
517
+ resetFocusAndLastClicked: () => {
478
518
  let {
479
- activeDynamicViewStep,
519
+ activeWalkthrough,
480
520
  focusedNodeId,
481
521
  lastClickedNodeId,
482
- lastClickedEdgeId
522
+ lastClickedEdgeId,
523
+ xystore
483
524
  } = get()
484
- if (activeDynamicViewStep || focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
525
+ if (activeWalkthrough || focusedNodeId || lastClickedNodeId || lastClickedEdgeId) {
485
526
  set(
486
527
  {
487
- activeDynamicViewStep: null,
528
+ activeWalkthrough: null,
488
529
  focusedNodeId: null,
489
530
  lastClickedNodeId: null,
490
531
  lastClickedEdgeId: null,
@@ -494,6 +535,7 @@ export function createDiagramStore(props: DiagramInitialState) {
494
535
  'resetLastClicked'
495
536
  )
496
537
  }
538
+ xystore.getState().resetSelectedElements()
497
539
  },
498
540
 
499
541
  getElement: (fqn) => {
@@ -501,6 +543,11 @@ export function createDiagramStore(props: DiagramInitialState) {
501
543
  return view.nodes.find(({ id }) => id === fqn) ?? null
502
544
  },
503
545
 
546
+ isEditable: () => {
547
+ const { readonly, onChange } = get()
548
+ return !readonly && !!onChange
549
+ },
550
+
504
551
  triggerChangeElementStyle: (change) => {
505
552
  if (DEV) {
506
553
  console.debug('triggerChangeElementStyle', change)
@@ -617,25 +664,25 @@ export function createDiagramStore(props: DiagramInitialState) {
617
664
  }, {} as ViewChange.SaveManualLayout['layout']['nodes'])
618
665
 
619
666
  const edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
620
- let controlPoints = data.controlPoints
667
+ let controlPoints = data.controlPoints ?? []
621
668
  const sourceOrTargetMoved = movedNodes.has(source) || movedNodes.has(target)
622
669
  // If edge control points are not set, but the source or target node was moved
623
- if ((!controlPoints || controlPoints.length === 0) && sourceOrTargetMoved) {
670
+ if (controlPoints.length === 0 && sourceOrTargetMoved) {
624
671
  controlPoints = bezierControlPoints(data.edge)
625
672
  }
626
- if (data.edge.points.length === 0 && (!controlPoints || controlPoints.length === 0)) {
673
+ if (data.edge.points.length === 0 && controlPoints.length === 0) {
627
674
  return acc
628
675
  }
629
676
  const _updated: ViewChange.SaveManualLayout['layout']['edges'][string] = acc[data.edge.id] = {
630
677
  points: data.edge.points
631
678
  }
632
679
  if (data.label?.bbox) {
633
- _updated.labelBBox = data.label?.bbox
680
+ _updated.labelBBox = data.label.bbox
634
681
  }
635
682
  if (data.edge.labelBBox) {
636
683
  _updated.labelBBox ??= data.edge.labelBBox
637
684
  }
638
- if (controlPoints && hasAtLeast(controlPoints, 1)) {
685
+ if (hasAtLeast(controlPoints, 1)) {
639
686
  _updated.controlPoints = controlPoints
640
687
  }
641
688
  if (!sourceOrTargetMoved && data.edge.dotpos) {
@@ -643,12 +690,12 @@ export function createDiagramStore(props: DiagramInitialState) {
643
690
  }
644
691
  const allX = [
645
692
  ...data.edge.points.map(p => p[0]),
646
- ...(controlPoints ?? []).map(p => p.x),
693
+ ...controlPoints.map(p => p.x),
647
694
  ...(_updated.labelBBox ? [_updated.labelBBox.x, _updated.labelBBox.x + _updated.labelBBox.width] : [])
648
695
  ]
649
696
  const allY = [
650
697
  ...data.edge.points.map(p => p[1]),
651
- ...(controlPoints ?? []).map(p => p.y),
698
+ ...controlPoints.map(p => p.y),
652
699
  ...(_updated.labelBBox ? [_updated.labelBBox.y, _updated.labelBBox.y + _updated.labelBBox.height] : [])
653
700
  ]
654
701
  const rect = boxToRect({
@@ -733,8 +780,7 @@ export function createDiagramStore(props: DiagramInitialState) {
733
780
  onNavigateTo(
734
781
  element.navigateTo,
735
782
  event,
736
- element as DiagramNodeWithNavigate,
737
- xynode
783
+ element as DiagramNodeWithNavigate
738
784
  )
739
785
  },
740
786
 
@@ -790,51 +836,62 @@ export function createDiagramStore(props: DiagramInitialState) {
790
836
  },
791
837
 
792
838
  fitDiagram: (duration = 500) => {
793
- const { fitViewPadding, view, focusedNodeId, activeDynamicViewStep, xystore } = get()
839
+ const { fitViewPadding, view, xystore } = get()
794
840
  const { width, height, panZoom, transform } = xystore.getState()
795
841
 
796
842
  const bounds = view.bounds
797
843
  const maxZoom = Math.max(1, transform[2])
798
844
  const viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding)
799
845
  panZoom?.setViewport(viewport, { duration })
800
- if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
801
- set(
802
- {
803
- activeDynamicViewStep: null,
804
- focusedNodeId: null,
805
- dimmed: EmptyStringSet
806
- },
807
- noReplace,
808
- 'unfocus'
809
- )
810
- }
811
846
  },
812
847
 
813
848
  nextDynamicStep: (increment = 1) => {
814
- const { activeDynamicViewStep, activateDynamicStep } = get()
815
- const nextStep = (activeDynamicViewStep ?? 0) + increment
816
- if (nextStep <= 0) {
817
- return
849
+ const { activeWalkthrough, xyedges, activateWalkthrough } = get()
850
+ const stepId = activeWalkthrough?.stepId
851
+ let nextStep
852
+ if (stepId) {
853
+ const currentIndex = xyedges.findIndex(({ id }) => id === stepId)
854
+ const nextIndex = currentIndex + increment
855
+ nextStep = xyedges[nextIndex]
856
+ } else {
857
+ nextStep = first(xyedges)
818
858
  }
819
- if (nextStep !== activeDynamicViewStep) {
820
- activateDynamicStep(nextStep)
859
+ if (nextStep) {
860
+ activateWalkthrough(nextStep.data.edge.id)
861
+ return
821
862
  }
822
863
  },
823
864
 
824
- activateDynamicStep: (nextStep: number) => {
825
- const { isDynamicView, xyflow, xystore, fitViewPadding } = get()
865
+ activateWalkthrough: (step: EdgeId | XYFlowEdge) => {
866
+ const stepId = typeof step === 'string' ? step : step.data.edge.id
867
+ invariant(isStepEdgeId(stepId), `stepId ${stepId} is not a step edge id`)
868
+ let {
869
+ isDynamicView,
870
+ xyflow,
871
+ xyedges,
872
+ xystore,
873
+ fitViewPadding,
874
+ activeWalkthrough
875
+ } = get()
826
876
  invariant(isDynamicView, 'view is not dynamic')
827
- const edgeId = StepEdgeId(nextStep)
828
- const dimmed = new StringSet()
829
- let edge: XYFlowEdge | null = null
830
- for (const e of xyflow.getEdges()) {
831
- if (e.id === edgeId) {
832
- edge = e
833
- continue
834
- }
835
- dimmed.add(e.id)
877
+ const edge = typeof step === 'string' ? xyedges.find(({ id }) => id === stepId) : step
878
+ invariant(edge, `edge not found: ${stepId}`)
879
+ const currentIndex = xyedges.findIndex(({ id }) => id === stepId)
880
+ activeWalkthrough = {
881
+ stepId,
882
+ hasPrevious: currentIndex > 0,
883
+ hasNext: currentIndex < xyedges.length - 1,
884
+ parallelPrefix: getParallelStepsPrefix(stepId)
836
885
  }
837
- invariant(!!edge, `edge not found: ${edgeId}`)
886
+
887
+ const dimmed = new StringSet(
888
+ xyedges
889
+ .filter(({ id }) =>
890
+ id !== stepId
891
+ && !(activeWalkthrough.parallelPrefix && id.startsWith(activeWalkthrough.parallelPrefix))
892
+ )
893
+ .map(({ id }) => id)
894
+ )
838
895
  const selected = [] as XYFlowNode[]
839
896
  for (const n of xyflow.getNodes()) {
840
897
  if (n.id === edge.source || n.id === edge.target) {
@@ -855,24 +912,24 @@ export function createDiagramStore(props: DiagramInitialState) {
855
912
  set(
856
913
  {
857
914
  focusedNodeId: null,
858
- activeDynamicViewStep: nextStep,
915
+ activeWalkthrough,
859
916
  dimmed
860
917
  },
861
918
  noReplace,
862
- 'activateDynamicStep'
919
+ 'activateWalkthrough'
863
920
  )
864
921
  },
865
922
 
866
- stopDynamicView: () => {
867
- if (get().activeDynamicViewStep !== null) {
923
+ stopWalkthrough: () => {
924
+ if (get().activeWalkthrough !== null) {
868
925
  set(
869
926
  {
870
- activeDynamicViewStep: null,
927
+ activeWalkthrough: null,
871
928
  focusedNodeId: null,
872
929
  dimmed: EmptyStringSet
873
930
  },
874
931
  noReplace,
875
- 'stopDynamicView'
932
+ 'stopWalkthrough'
876
933
  )
877
934
  get().fitDiagram()
878
935
  }
@@ -905,8 +962,70 @@ export function createDiagramStore(props: DiagramInitialState) {
905
962
  }
906
963
  })
907
964
  set({ dimmed }, noReplace, 'highlightByElementNotation')
965
+ },
966
+
967
+ resetEdgeControlPoints: () => {
968
+ const { xyflow, scheduleSaveManualLayout, xynodes, xyedges } = get()
969
+
970
+ xyedges.forEach(edge => {
971
+ xyflow.updateEdgeData(edge.id, {
972
+ controlPoints: getControlPointForEdge(edge)
973
+ })
974
+ })
975
+
976
+ scheduleSaveManualLayout()
977
+
978
+ function getNodeCenter(node: XYFlowNode, nodes: XYFlowNode[]) {
979
+ const dimensions = vector({ x: node.width || 0, y: node.height || 0 })
980
+ let position = vector(node.position)
981
+ .add(dimensions.mul(0.5))
982
+
983
+ let currentNode = node
984
+ do {
985
+ const parent = currentNode.parentId && nodes.find(x => x.id == currentNode.parentId)
986
+
987
+ if (!parent) {
988
+ break
989
+ }
990
+
991
+ currentNode = parent
992
+ position = position.add(parent.position)
993
+ } while (true)
994
+
995
+ return position
996
+ }
997
+
998
+ function getControlPointForEdge(edge: XYFlowEdge): XYPoint[] {
999
+ const source = xynodes.find(x => x.id == edge.source)
1000
+ const target = xynodes.find(x => x.id == edge.target)
1001
+ if (!source || !target) {
1002
+ return []
1003
+ }
1004
+
1005
+ const sourceCenter = getNodeCenter(source, xynodes)
1006
+ const targetCenter = getNodeCenter(target, xynodes)
1007
+
1008
+ if (sourceCenter && targetCenter) {
1009
+ const sourceToTargetVector = targetCenter.sub(sourceCenter)
1010
+ const sourceBorderPoint = getBorderPointOnVector(source, sourceCenter, sourceToTargetVector)
1011
+ const targetBorderPoint = getBorderPointOnVector(target, targetCenter, sourceToTargetVector.mul(-1))
1012
+
1013
+ return [sourceBorderPoint.add(targetBorderPoint.sub(sourceBorderPoint).mul(0.3))]
1014
+ }
1015
+
1016
+ return []
1017
+ }
1018
+
1019
+ function getBorderPointOnVector(node: XYFlowNode, nodeCenter: Vector, v: Vector) {
1020
+ const xScale = (node.width || 0) / 2 / v.x
1021
+ const yScale = (node.height || 0) / 2 / v.y
1022
+
1023
+ const scale = Math.min(Math.abs(xScale), Math.abs(yScale))
1024
+
1025
+ return vector(v).mul(scale).add(nodeCenter)
1026
+ }
908
1027
  }
909
- }),
1028
+ } satisfies DiagramState),
910
1029
  {
911
1030
  name: `${storeDevId} - ${props.view.id}`,
912
1031
  enabled: DEV
@@ -1,3 +1,4 @@
1
+ import { themeToVars } from '@mantine/vanilla-extract'
1
2
  import { createGlobalThemeContract } from '@vanilla-extract/css'
2
3
 
3
4
  export const vars = createGlobalThemeContract({
@@ -31,12 +32,12 @@ export const vars = createGlobalThemeContract({
31
32
  labelBgColor: 'relation-labelBg'
32
33
  },
33
34
  optionsPanel: {
34
- top: 'options-panel-top',
35
- right: 'options-panel-right'
35
+ top: 'options-webview-top',
36
+ right: 'options-webview-right'
36
37
  },
37
38
  navigationPanel: {
38
- top: 'navigation-panel-top',
39
- left: 'navigation-panel-left'
39
+ top: 'navigation-webview-top',
40
+ left: 'navigation-webview-left'
40
41
  }
41
42
  }, (value) => `likec4-${value}`)
42
43
 
@@ -67,3 +68,9 @@ export const xyvars = createGlobalThemeContract({
67
68
  borderRadius: 'node-border-radius'
68
69
  }
69
70
  }, (value) => `xy-${value}`)
71
+
72
+ export const whereLight = ':where([data-mantine-color-scheme="light"])'
73
+ export const whereDark = ':where([data-mantine-color-scheme="dark"])'
74
+
75
+ // we need only variables names
76
+ export const mantine = themeToVars({})
@@ -1,6 +1,5 @@
1
- import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
- import { mantine, whereDark } from '../mantine.css'
3
- import { vars } from '../theme.css'
1
+ import { fallbackVar, style } from '@vanilla-extract/css'
2
+ import { mantine, vars, whereDark } from '../theme-vars'
4
3
 
5
4
  export const container = style({
6
5
  bottom: 0,
@@ -16,13 +16,29 @@ const selector = (s: DiagramState) => ({
16
16
  export default function DiagramTitlePanel() {
17
17
  const { id, title, description, links } = useDiagramState(selector)
18
18
  const [isCollapsed, setCollapsed] = useLocalStorage({
19
- key: 'diagram-title-panel-collapsed',
19
+ key: 'diagram-title-webview-collapsed',
20
20
  defaultValue: false
21
21
  })
22
22
  const toggle = () => setCollapsed(v => !v)
23
23
 
24
24
  return (
25
25
  <AnimatePresence mode="wait">
26
+ {
27
+ /* <ViewportPortal>
28
+ <div className={css.inlineTitle} style={{ transform: `translate(0px,${height}px)` }}>
29
+ <m.div
30
+ key={id}
31
+ animate={{ opacity: 0.7, scale: .92 }}
32
+ whileHover={{ opacity: 1, scale: 1 }}
33
+ style={{
34
+ transformOrigin: 'left bottom'
35
+ }}
36
+ >
37
+ <Title order={1}>{title}</Title>
38
+ </m.div>
39
+ </div>
40
+ </ViewportPortal> */
41
+ }
26
42
  <m.div
27
43
  key={id}
28
44
  initial={{ opacity: 0.05, scale: 0.7 }}
@@ -54,6 +70,7 @@ export default function DiagramTitlePanel() {
54
70
  onClick={toggle}
55
71
  fw={500}
56
72
  fz={'sm'}
73
+ tabIndex={-1}
57
74
  >
58
75
  {title}
59
76
  </Button>
@@ -71,6 +88,7 @@ export default function DiagramTitlePanel() {
71
88
  variant={'subtle'}
72
89
  color="gray"
73
90
  onClick={toggle}
91
+ tabIndex={-1}
74
92
  >
75
93
  <IconMenu size={11} opacity={0.7} />
76
94
  </Button>
@@ -106,8 +124,12 @@ export default function DiagramTitlePanel() {
106
124
  {description && (
107
125
  <Spoiler
108
126
  maxHeight={42}
109
- showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
110
- hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
127
+ showLabel={
128
+ <Button color="gray" variant="light" fz={'10'} size="compact-xs" tabIndex={-1}>show more</Button>
129
+ }
130
+ hideLabel={
131
+ <Button color="gray" variant="light" fz={'10'} size="compact-xs" tabIndex={-1}>hide</Button>
132
+ }>
111
133
  <Text
112
134
  component={'div'}
113
135
  size="sm"
@@ -1,5 +1,5 @@
1
1
  import { createVar, globalStyle, style } from '@vanilla-extract/css'
2
- import { mantine } from '../mantine.css'
2
+ import { mantine } from '../theme-vars'
3
3
 
4
4
  export const container = style({
5
5
  position: 'absolute',
@@ -7,7 +7,16 @@ export const container = style({
7
7
  left: '50%',
8
8
  transform: 'translateX(-50%)',
9
9
  padding: 0,
10
- margin: 0
10
+ margin: 0,
11
+ display: 'flex',
12
+ flexDirection: 'column',
13
+ gap: '0.4rem',
14
+ alignItems: 'center',
15
+ '@media': {
16
+ [mantine.largerThan('md')]: {
17
+ gap: '0.6rem'
18
+ }
19
+ }
11
20
  })
12
21
 
13
22
  export const buttons = style({
@@ -45,3 +54,20 @@ globalStyle(`:where(${buttons}, ${btn}) .tabler-icon`, {
45
54
  width: '0.85em',
46
55
  height: '0.85em'
47
56
  })
57
+
58
+ export const parallelStateFrame = style({
59
+ position: 'absolute',
60
+ margin: 0,
61
+ padding: 0,
62
+ top: 0,
63
+ left: 0,
64
+ width: '100%',
65
+ height: '100%',
66
+ border: `2px solid ${mantine.colors.orange[6]}`,
67
+ pointerEvents: 'none',
68
+ '@media': {
69
+ [mantine.largerThan('md')]: {
70
+ borderWidth: 4
71
+ }
72
+ }
73
+ })