@likec4/diagram 1.3.0 → 1.5.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 (95) hide show
  1. package/dist/LikeC4Diagram.css.d.ts +0 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +7 -2
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.mjs +1580 -1194
  6. package/dist/state/DiagramContext.d.ts +97 -4169
  7. package/dist/state/diagramStore.d.ts +98 -4172
  8. package/dist/state/hooks.d.ts +4 -0
  9. package/dist/state/index.d.ts +1 -3
  10. package/dist/state/useDiagramStore.d.ts +3 -4622
  11. package/dist/state/useNavigationHistory.d.ts +5 -0
  12. package/dist/style.css +217 -159
  13. package/dist/theme.css.d.ts +5 -0
  14. package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
  15. package/dist/ui/BackwardForwardPanel.d.ts +1 -0
  16. package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
  17. package/dist/ui/OptionsPanel.d.ts +1 -3
  18. package/dist/ui/options/EdgeOptions.d.ts +19 -0
  19. package/dist/ui/options/NodeOptions.d.ts +2 -2
  20. package/dist/xyflow/EdgeMarkers.d.ts +11 -0
  21. package/dist/xyflow/XYFlowEvents.d.ts +12 -9
  22. package/dist/xyflow/XYFlowInner.d.ts +1 -0
  23. package/dist/xyflow/const.d.ts +3 -3
  24. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  25. package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -3
  26. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  27. package/dist/xyflow/edges/utils.d.ts +1 -0
  28. package/dist/xyflow/hooks/index.d.ts +3 -2
  29. package/dist/xyflow/hooks/useXYFlow.d.ts +22 -21
  30. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  31. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
  32. package/dist/xyflow/nodes/element/element.css.d.ts +3 -1
  33. package/dist/xyflow/types.d.ts +2 -32
  34. package/package.json +21 -21
  35. package/src/LikeC4Diagram.css.ts +10 -8
  36. package/src/LikeC4Diagram.props.ts +12 -5
  37. package/src/LikeC4Diagram.tsx +43 -26
  38. package/src/globals.css.ts +11 -4
  39. package/src/index.ts +1 -1
  40. package/src/mantine.css.ts +2 -2
  41. package/src/state/DiagramContext.tsx +47 -16
  42. package/src/state/diagramStore.ts +89 -80
  43. package/src/state/hooks.ts +5 -0
  44. package/src/state/index.ts +1 -3
  45. package/src/state/useDiagramStore.ts +6 -13
  46. package/src/state/useNavigationHistory.ts +39 -0
  47. package/src/theme.css.ts +5 -0
  48. package/src/ui/BackwardForwardPanel.css.ts +22 -0
  49. package/src/ui/BackwardForwardPanel.tsx +59 -0
  50. package/src/ui/DiagramTitlePanel.css.ts +17 -6
  51. package/src/ui/DiagramTitlePanel.tsx +108 -94
  52. package/src/ui/DynamicViewWalkthrough.tsx +6 -4
  53. package/src/ui/OptionsPanel.tsx +37 -18
  54. package/src/ui/options/EdgeOptions.tsx +157 -0
  55. package/src/ui/options/NodeOptions.tsx +37 -29
  56. package/src/xyflow/EdgeMarkers.tsx +148 -0
  57. package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
  58. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
  59. package/src/xyflow/SyncWithDiagram.tsx +13 -20
  60. package/src/xyflow/XYFlow.tsx +2 -4
  61. package/src/xyflow/XYFlowEvents.tsx +48 -33
  62. package/src/xyflow/XYFlowInner.tsx +6 -3
  63. package/src/xyflow/const.ts +3 -3
  64. package/src/xyflow/diagram-to-xyflow.ts +18 -53
  65. package/src/xyflow/edges/RelationshipEdge.tsx +218 -126
  66. package/src/xyflow/edges/edges.css.ts +44 -26
  67. package/src/xyflow/edges/utils.ts +10 -0
  68. package/src/xyflow/hooks/index.ts +4 -2
  69. package/src/xyflow/hooks/useLayoutConstraints.ts +5 -5
  70. package/src/xyflow/hooks/useXYFlow.ts +28 -1
  71. package/src/xyflow/nodes/compound/CompoundNode.css.ts +41 -31
  72. package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
  73. package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
  74. package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
  75. package/src/xyflow/nodes/element/ElementNode.tsx +69 -114
  76. package/src/xyflow/nodes/element/element.css.ts +48 -15
  77. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  78. package/src/xyflow/types.ts +2 -4
  79. package/src/xyflow/utils.ts +3 -6
  80. package/dist/KeepAspectRatioContainer.d.ts +0 -9
  81. package/dist/icons/Clipboard.d.ts +0 -3
  82. package/dist/icons/Link.d.ts +0 -3
  83. package/dist/icons/ZoomIn.d.ts +0 -3
  84. package/dist/icons/index.d.ts +0 -3
  85. package/dist/mantine/theme.d.ts +0 -3
  86. package/dist/state/WhenInitialized.d.ts +0 -3
  87. package/dist/ui/Camera.d.ts +0 -3
  88. package/src/KeepAspectRatioContainer.tsx +0 -36
  89. package/src/icons/Clipboard.tsx +0 -24
  90. package/src/icons/Link.tsx +0 -23
  91. package/src/icons/ZoomIn.tsx +0 -25
  92. package/src/icons/index.ts +0 -3
  93. package/src/mantine/theme.ts +0 -20
  94. package/src/state/WhenInitialized.tsx +0 -24
  95. package/src/ui/Camera.tsx +0 -174
@@ -0,0 +1,148 @@
1
+ import type { SVGProps } from 'react'
2
+
3
+ const Open = (props: SVGProps<SVGMarkerElement>) => (
4
+ <marker
5
+ viewBox="-4 -4 14 16"
6
+ refX={5}
7
+ refY={4}
8
+ markerWidth="7"
9
+ markerHeight="8"
10
+ preserveAspectRatio="xMaxYMid meet"
11
+ orient="auto-start-reverse"
12
+ {...props}
13
+ >
14
+ <path
15
+ d="M0,0 L7,4 L0,8 L4,4 Z"
16
+ stroke="context-stroke"
17
+ fill="context-stroke"
18
+ strokeDasharray={0}
19
+ strokeWidth={1}
20
+ strokeLinecap={'round'} />
21
+ </marker>
22
+ )
23
+
24
+ const Arrow = (props: SVGProps<SVGMarkerElement>) => (
25
+ <marker
26
+ viewBox="-1 -1 12 10"
27
+ refX={4}
28
+ refY={3}
29
+ markerWidth="8"
30
+ markerHeight="6"
31
+ preserveAspectRatio="xMaxYMid meet"
32
+ orient="auto-start-reverse"
33
+ {...props}>
34
+ <path
35
+ d="M 0 0 L 8 3 L 0 6 L 1 3 z"
36
+ fill="context-stroke"
37
+ strokeWidth={0}
38
+ />
39
+ </marker>
40
+ )
41
+
42
+ const OArrow = (props: SVGProps<SVGMarkerElement>) => (
43
+ <marker
44
+ viewBox="-1 -1 12 10"
45
+ refX={4}
46
+ refY={3}
47
+ markerWidth="8"
48
+ markerHeight="6"
49
+ preserveAspectRatio="xMaxYMid meet"
50
+ orient="auto-start-reverse"
51
+ {...props}>
52
+ <path
53
+ d="M 0 0 L 8 3 L 0 6 L 1 3 z"
54
+ stroke="context-stroke"
55
+ fill="var(--likec4-background-color)"
56
+ strokeWidth={1.25}
57
+ strokeLinejoin="miter"
58
+ strokeLinecap={'square'}
59
+ />
60
+ </marker>
61
+ )
62
+
63
+ const Diamond = (props: SVGProps<SVGMarkerElement>) => (
64
+ <marker
65
+ viewBox="-4 -4 16 14"
66
+ refX={5}
67
+ refY={4}
68
+ markerWidth="10"
69
+ markerHeight="8"
70
+ preserveAspectRatio="xMaxYMid meet"
71
+ orient="auto-start-reverse"
72
+ {...props}
73
+ >
74
+ <path
75
+ d="M5,0 L10,4 L5,8 L0,4 Z"
76
+ fill="context-stroke"
77
+ strokeWidth={0}
78
+ strokeLinecap={'round'}
79
+ />
80
+ </marker>
81
+ )
82
+
83
+ const ODiamond = (props: SVGProps<SVGMarkerElement>) => (
84
+ <marker
85
+ viewBox="-4 -4 16 14"
86
+ refX={6}
87
+ refY={4}
88
+ markerWidth="10"
89
+ markerHeight="8"
90
+ preserveAspectRatio="xMaxYMid meet"
91
+ orient="auto-start-reverse"
92
+ {...props}>
93
+ <path
94
+ d="M5,0 L10,4 L5,8 L0,4 Z"
95
+ stroke="context-stroke"
96
+ fill="var(--likec4-background-color)"
97
+ strokeWidth={1.25}
98
+ strokeLinecap={'round'}
99
+ />
100
+ </marker>
101
+ )
102
+
103
+ const Dot = (props: SVGProps<SVGMarkerElement>) => (
104
+ <marker
105
+ viewBox="0 0 10 10"
106
+ refX={4}
107
+ refY={4}
108
+ markerWidth="6"
109
+ markerHeight="6"
110
+ {...props}>
111
+ <circle
112
+ strokeWidth={0}
113
+ fill="context-stroke"
114
+ cx={4}
115
+ cy={4}
116
+ r={3}
117
+ />
118
+ </marker>
119
+ )
120
+
121
+ const ODot = (props: SVGProps<SVGMarkerElement>) => (
122
+ <marker
123
+ viewBox="0 0 10 10"
124
+ refX={4}
125
+ refY={4}
126
+ markerWidth="6"
127
+ markerHeight="6"
128
+ {...props}>
129
+ <circle
130
+ strokeWidth={1.25}
131
+ stroke="context-stroke"
132
+ fill="var(--likec4-background-color)"
133
+ cx={4}
134
+ cy={4}
135
+ r={3}
136
+ />
137
+ </marker>
138
+ )
139
+
140
+ export const EdgeMarkers = {
141
+ Arrow,
142
+ OArrow,
143
+ Open,
144
+ Diamond,
145
+ ODiamond,
146
+ Dot,
147
+ ODot
148
+ }
@@ -2,7 +2,7 @@ import { invariant } from '@likec4/core'
2
2
  import { useDebouncedEffect } from '@react-hookz/web'
3
3
  import { shallowEqual } from 'fast-equals'
4
4
  import { useEffect, useRef } from 'react'
5
- import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
5
+ import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state/hooks'
6
6
  import { useXYStore, useXYStoreApi, type XYStoreApi } from './hooks'
7
7
  import type { XYFlowState } from './types'
8
8
  import { toDomPrecision } from './utils'
@@ -24,11 +24,11 @@ function FitViewOnViewportResize({ diagramApi, xyflowApi }: {
24
24
  if (focusedNodeId || prevDimensionsRef.current === dimensions) {
25
25
  return
26
26
  }
27
- fitDiagram(xyflowApi)
27
+ fitDiagram(300)
28
28
  prevDimensionsRef.current = dimensions
29
29
  },
30
30
  [dimensions, diagramApi, xyflowApi],
31
- 250,
31
+ 300,
32
32
  1000
33
33
  )
34
34
 
@@ -77,6 +77,7 @@ export function FitViewOnDiagramChange() {
77
77
  } = useDiagramState(selector, shallowEqual)
78
78
  const diagramApi = useDiagramStoreApi()
79
79
  const processedRef = useRef(pendingViewId)
80
+ const durationRef = useRef(300)
80
81
 
81
82
  useEffect(() => {
82
83
  if (!xyflowSynced || pendingViewId === processedRef.current) {
@@ -97,11 +98,14 @@ export function FitViewOnDiagramChange() {
97
98
  y: toDomPrecision(fromPos.y - toPos.y)
98
99
  }
99
100
  xyflowApi.getState().panBy(diff)
101
+ durationRef.current = 500
100
102
  diagramApi.setState({ lastOnNavigate: null })
101
103
  return
102
104
  }
103
105
  processedRef.current = pendingViewId
104
- diagramApi.getState().fitDiagram(xyflowApi)
106
+ diagramApi.getState().fitDiagram(durationRef.current)
107
+ // Reset duration
108
+ durationRef.current = 300
105
109
  }, [pendingViewId, xyflowSynced, waitCorrection])
106
110
 
107
111
  if (!viewportChanged && xyflowSynced) {
@@ -1,72 +1,76 @@
1
1
  import { nonNullable } from '@likec4/core'
2
2
  import type { EdgeChange, NodeChange } from '@xyflow/react'
3
- import { useUpdateEffect } from '../hooks/useUpdateEffect'
4
- import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
3
+ import { getBoundsOfRects, getViewportForBounds, nodeToRect } from '@xyflow/system'
4
+ import { useEffect } from 'react'
5
+ import { useDiagramStoreApi } from '../state/hooks'
5
6
  import { MinZoom } from './const'
6
- import { useXYStoreApi } from './hooks/useXYFlow'
7
7
  import type { XYFlowEdge, XYFlowNode } from './types'
8
8
 
9
9
  export function SelectEdgesOnNodeFocus() {
10
- const focusedNodeId = useDiagramState(s => s.focusedNodeId)
11
10
  const diagramStore = useDiagramStoreApi()
12
- const xyflowApi = useXYStoreApi()
13
11
 
14
- useUpdateEffect(() => {
15
- if (!focusedNodeId) {
16
- xyflowApi.getState().resetSelectedElements()
17
- return
18
- }
12
+ useEffect(
13
+ () =>
14
+ diagramStore.subscribe(s => s.focusedNodeId, focusedNodeId => {
15
+ if (!focusedNodeId) {
16
+ return
17
+ }
19
18
 
20
- const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
21
- const nodeChanges = [] as NodeChange<XYFlowNode>[]
22
- const { edgeLookup, nodeLookup, transform, triggerNodeChanges, fitView, triggerEdgeChanges } = xyflowApi.getState()
23
- const fitViewNodes = new Set<XYFlowNode>()
24
- fitViewNodes.add(nonNullable(nodeLookup.get(focusedNodeId)))
19
+ const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
20
+ const nodeChanges = [] as NodeChange<XYFlowNode>[]
21
+ const {
22
+ edgeLookup,
23
+ nodeLookup,
24
+ width,
25
+ height,
26
+ panZoom,
27
+ transform,
28
+ triggerNodeChanges,
29
+ triggerEdgeChanges
30
+ } = diagramStore.getState().xystore.getState()
31
+ let focusBounds = nodeToRect(nonNullable(nodeLookup.get(focusedNodeId)))
25
32
 
26
- for (const [, edge] of edgeLookup) {
27
- if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
28
- edgeChanges.push({
29
- id: edge.id,
30
- type: 'select',
31
- selected: true
32
- })
33
- const anotherNode = nonNullable(nodeLookup.get(edge.source === focusedNodeId ? edge.target : edge.source))
34
- fitViewNodes.add(anotherNode)
35
- } else if (edge.selected) {
36
- edgeChanges.push({
37
- id: edge.id,
38
- type: 'select',
39
- selected: false
40
- })
41
- }
42
- }
43
- for (const [, node] of nodeLookup) {
44
- if (node.selected && node.id !== focusedNodeId) {
33
+ for (const [, edge] of edgeLookup) {
34
+ if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
35
+ const anotherNodeId = edge.source === focusedNodeId ? edge.target : edge.source
36
+ const nd = nodeToRect(nonNullable(nodeLookup.get(anotherNodeId)))
37
+ focusBounds = getBoundsOfRects(focusBounds, nd)
38
+ }
39
+ if (edge.selected) {
40
+ edgeChanges.push({
41
+ id: edge.id,
42
+ type: 'select',
43
+ selected: false
44
+ })
45
+ }
46
+ }
47
+ for (const [, node] of nodeLookup) {
48
+ if (node.selected && node.id !== focusedNodeId) {
49
+ nodeChanges.push({
50
+ id: node.id,
51
+ type: 'select',
52
+ selected: false
53
+ })
54
+ }
55
+ }
45
56
  nodeChanges.push({
46
- id: node.id,
57
+ id: focusedNodeId,
47
58
  type: 'select',
48
- selected: false
59
+ selected: true
49
60
  })
50
- }
51
- }
52
- nodeChanges.push({
53
- id: focusedNodeId,
54
- type: 'select',
55
- selected: true
56
- })
57
- if (edgeChanges.length > 0) {
58
- triggerEdgeChanges(edgeChanges)
59
- }
60
- triggerNodeChanges(nodeChanges)
61
+ if (edgeChanges.length > 0) {
62
+ triggerEdgeChanges(edgeChanges)
63
+ }
64
+ triggerNodeChanges(nodeChanges)
61
65
 
62
- fitView({
63
- duration: 350,
64
- padding: 0.1,
65
- minZoom: MinZoom,
66
- maxZoom: Math.max(1, transform[2]),
67
- nodes: Array.from(fitViewNodes)
68
- })
69
- }, [diagramStore, xyflowApi, focusedNodeId])
66
+ const maxZoom = Math.max(1, transform[2])
67
+ const viewport = getViewportForBounds(focusBounds, width, height, MinZoom, maxZoom, 0.2)
68
+ panZoom?.setViewport(viewport, {
69
+ duration: 350
70
+ })
71
+ }),
72
+ [diagramStore]
73
+ )
70
74
 
71
75
  return null
72
76
  }
@@ -1,8 +1,8 @@
1
1
  import { deepEqual as eq } from 'fast-equals'
2
- import { useXYStoreApi } from './hooks'
3
2
 
4
3
  import { useEffect } from 'react'
5
- import { useDiagramState, useDiagramStoreApi } from '../state'
4
+ import { omit } from 'remeda'
5
+ import { useDiagramState, useDiagramStoreApi } from '../state/hooks'
6
6
  import { diagramViewToXYFlowData } from './diagram-to-xyflow'
7
7
 
8
8
  /**
@@ -10,7 +10,6 @@ import { diagramViewToXYFlowData } from './diagram-to-xyflow'
10
10
  */
11
11
  export function SyncWithDiagram() {
12
12
  const xyflowSynced = useDiagramState(s => s.xyflowSynced)
13
- const xyflowApi = useXYStoreApi()
14
13
  const diagramStoreApi = useDiagramStoreApi()
15
14
 
16
15
  useEffect(() => {
@@ -21,7 +20,8 @@ export function SyncWithDiagram() {
21
20
  view,
22
21
  nodesDraggable,
23
22
  nodesSelectable,
24
- focusedNodeId
23
+ focusedNodeId,
24
+ xystore
25
25
  } = diagramStoreApi.getState()
26
26
  const updates = diagramViewToXYFlowData(view, {
27
27
  draggable: nodesDraggable,
@@ -33,7 +33,7 @@ export function SyncWithDiagram() {
33
33
  edgeLookup,
34
34
  setNodes,
35
35
  setEdges
36
- } = xyflowApi.getState()
36
+ } = xystore.getState()
37
37
 
38
38
  setNodes(updates.nodes.map(update => {
39
39
  const existing = nodeLookup.get(update.id)?.internals.userNode
@@ -52,29 +52,22 @@ export function SyncWithDiagram() {
52
52
  setEdges(updates.edges.map(update => {
53
53
  const existing = edgeLookup.get(update.id)
54
54
  if (existing) {
55
- if (eq(existing.data.controlPoints, update.data.controlPoints) && eq(existing.data.edge, update.data.edge)) {
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
+ ) {
56
61
  return existing
57
62
  }
58
63
  return {
59
- ...existing,
64
+ ...omit(existing, ['data', 'markerStart', 'markerEnd']),
60
65
  ...update,
61
66
  data: {
62
67
  ...existing.data,
63
68
  ...update.data
64
69
  }
65
70
  }
66
- // if (update.data.controlPoints === null && existing.data.controlPoints !== null) {
67
- // // we don't want accidentally
68
- // // overwrite existing control points
69
- // return {
70
- // ...existing,
71
- // ...update,
72
- // data: {
73
- // ...update.data,
74
- // controlPoints: existing.data.controlPoints
75
- // }
76
- // }
77
- // }
78
71
  }
79
72
  return update
80
73
  }))
@@ -86,7 +79,7 @@ export function SyncWithDiagram() {
86
79
  false,
87
80
  'xyflowSynced'
88
81
  )
89
- }, [xyflowSynced, xyflowApi, diagramStoreApi])
82
+ }, [xyflowSynced, diagramStoreApi])
90
83
 
91
84
  return null
92
85
  }
@@ -3,7 +3,7 @@ import { DEV } from 'esm-env'
3
3
  import { deepEqual as eq } from 'fast-equals'
4
4
  import { type CSSProperties, type PropsWithChildren, useCallback } from 'react'
5
5
  import useTilg from 'tilg'
6
- import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
6
+ import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state/hooks'
7
7
  import { MinZoom } from './const'
8
8
  import { RelationshipEdge } from './edges/RelationshipEdge'
9
9
  import { useLayoutConstraints } from './hooks/useLayoutConstraints'
@@ -134,9 +134,6 @@ export function XYFlow({
134
134
  elementsSelectable={nodesSelectable}
135
135
  nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
136
136
  edgesFocusable={hasOnEdgeClick}
137
- {...(!nodesSelectable && {
138
- selectionKeyCode: null
139
- })}
140
137
  nodesDraggable={nodesDraggable}
141
138
  {...nodesDraggable && {
142
139
  onNodeDragStart: layoutConstraints.onNodeDragStart,
@@ -146,6 +143,7 @@ export function XYFlow({
146
143
  zoomOnDoubleClick={false}
147
144
  elevateNodesOnSelect={false} // or edges are not visible after select
148
145
  selectNodesOnDrag={false} // or weird camera movement
146
+ selectionKeyCode={null}
149
147
  onDoubleClick={handlers.onDoubleClick}
150
148
  onPaneClick={handlers.onPaneClick}
151
149
  onMoveEnd={handlers.onMoveEnd}
@@ -1,12 +1,10 @@
1
1
  import type { ReactFlowProps } from '@xyflow/react'
2
2
  import { useMemo, useRef } from 'react'
3
- import { isNullish } from 'remeda'
4
3
  import type { Simplify } from 'type-fest'
5
- import { useDiagramStoreApi } from '../state'
6
- import { useXYStoreApi } from './hooks'
4
+ import { useDiagramStoreApi } from '../state/hooks'
7
5
  import type { XYFlowEdge, XYFlowNode } from './types'
8
6
 
9
- type XYFlowEventHandlers = Simplify<
7
+ export type XYFlowEventHandlers = Simplify<
10
8
  Required<
11
9
  Pick<
12
10
  ReactFlowProps<XYFlowNode, XYFlowEdge>,
@@ -29,32 +27,31 @@ type XYFlowEventHandlers = Simplify<
29
27
 
30
28
  export function useXYFlowEvents() {
31
29
  const diagramApi = useDiagramStoreApi()
32
- const xyflowApi = useXYStoreApi()
33
30
 
34
31
  const lastClickTimestamp = useRef<number>()
35
32
 
36
33
  const dblclickTimeout = useRef<number>()
37
34
 
38
- const dbclickLock = () => {
39
- if (dblclickTimeout.current !== undefined) {
40
- return true
41
- }
42
- dblclickTimeout.current = window.setTimeout(() => {
43
- dblclickTimeout.current = undefined
44
- }, 250)
45
- return false
46
- }
47
-
48
- const lastClickWasRecent = (ms = 1500) => {
49
- return lastClickTimestamp.current && (Date.now() - lastClickTimestamp.current) < ms
50
- }
51
-
52
35
  // If we are in focused mode, on edge enter we want to "highlight" the other node
53
36
  // This ref contains the id of this node
54
37
  const hoveredNodeFromOnEdgeEnterRef = useRef('')
55
38
 
56
- return useMemo(() =>
57
- ({
39
+ return useMemo(() => {
40
+ const dbclickLock = () => {
41
+ if (dblclickTimeout.current !== undefined) {
42
+ return true
43
+ }
44
+ dblclickTimeout.current = window.setTimeout(() => {
45
+ dblclickTimeout.current = undefined
46
+ }, 250)
47
+ return false
48
+ }
49
+
50
+ const lastClickWasRecent = (ms = 1500) => {
51
+ return lastClickTimestamp.current && (Date.now() - lastClickTimestamp.current) < ms
52
+ }
53
+
54
+ return ({
58
55
  onDoubleClick: (event) => {
59
56
  const {
60
57
  fitViewEnabled,
@@ -63,7 +60,7 @@ export function useXYFlowEvents() {
63
60
  fitDiagram
64
61
  } = diagramApi.getState()
65
62
  if (fitViewEnabled) {
66
- fitDiagram(xyflowApi)
63
+ fitDiagram()
67
64
  if (!onCanvasDblClick) {
68
65
  event.stopPropagation()
69
66
  }
@@ -82,8 +79,8 @@ export function useXYFlowEvents() {
82
79
  onCanvasClick,
83
80
  resetLastClicked
84
81
  } = diagramApi.getState()
85
- if (focusedNodeId || activeDynamicViewStep) {
86
- fitDiagram(xyflowApi)
82
+ if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
83
+ fitDiagram()
87
84
  if (!onCanvasClick) {
88
85
  event.stopPropagation()
89
86
  }
@@ -106,7 +103,7 @@ export function useXYFlowEvents() {
106
103
  onEdgeContextMenu: (event, xyedge) => {
107
104
  diagramApi.getState().setLastClickedEdge(xyedge.id)
108
105
  diagramApi.getState().onEdgeContextMenu?.({
109
- relation: xyedge.data.edge,
106
+ edge: xyedge.data.edge,
110
107
  xyedge,
111
108
  event
112
109
  })
@@ -116,6 +113,7 @@ export function useXYFlowEvents() {
116
113
  zoomable,
117
114
  fitViewEnabled,
118
115
  focusedNodeId,
116
+ fitDiagram,
119
117
  focusOnNode,
120
118
  onNodeClick,
121
119
  lastClickedNodeId,
@@ -126,10 +124,21 @@ export function useXYFlowEvents() {
126
124
  // if we focused on a node, and clicked on another node - focus on the clicked node
127
125
  const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
128
126
  // if user clicked on the same node twice in a short time, focus on it
129
- const clickedRecently = isNullish(focusedNodeId) && lastClickedNodeId === xynode.id && lastClickWasRecent()
130
- if (focusPossible && (shallChangeFocus || clickedRecently)) {
131
- focusOnNode(xynode.id)
132
- if (!onNodeClick) {
127
+ const clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent()
128
+ if (focusPossible) {
129
+ let stopPropagation = false
130
+ switch (true) {
131
+ case !focusedNodeId && clickedRecently:
132
+ case shallChangeFocus:
133
+ stopPropagation = true
134
+ focusOnNode(xynode.id)
135
+ break
136
+ case focusedNodeId === xynode.id && clickedRecently:
137
+ stopPropagation = true
138
+ fitDiagram()
139
+ break
140
+ }
141
+ if (!onNodeClick && stopPropagation) {
133
142
  // user did not provide a custom handler, stop propagation
134
143
  event.stopPropagation()
135
144
  }
@@ -151,10 +160,11 @@ export function useXYFlowEvents() {
151
160
  setLastClickedNode
152
161
  } = diagramApi.getState()
153
162
  setLastClickedNode(xynode.id)
163
+ lastClickTimestamp.current = Date.now()
154
164
  if (!!focusedNodeId || (zoomable && fitViewEnabled)) {
155
165
  // if we are already focused on the node, cancel
156
166
  if (focusedNodeId === xynode.id) {
157
- fitDiagram(xyflowApi)
167
+ fitDiagram()
158
168
  } else {
159
169
  focusOnNode(xynode.id)
160
170
  }
@@ -163,7 +173,11 @@ export function useXYFlowEvents() {
163
173
  },
164
174
  onEdgeClick: (event, xyedge) => {
165
175
  diagramApi.getState().setLastClickedEdge(xyedge.id)
166
- const { focusedNodeId, focusOnNode, onEdgeClick } = diagramApi.getState()
176
+ const {
177
+ focusedNodeId,
178
+ focusOnNode,
179
+ onEdgeClick
180
+ } = diagramApi.getState()
167
181
  // if we focused on a node, and clicked on an edge connected to it - focus on the other node
168
182
  if (focusedNodeId && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target)) {
169
183
  focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
@@ -173,7 +187,7 @@ export function useXYFlowEvents() {
173
187
  }
174
188
  }
175
189
  onEdgeClick?.({
176
- relation: xyedge.data.edge,
190
+ edge: xyedge.data.edge,
177
191
  xyedge,
178
192
  event
179
193
  })
@@ -213,5 +227,6 @@ export function useXYFlowEvents() {
213
227
  }
214
228
  hoveredNodeFromOnEdgeEnterRef.current = ''
215
229
  }
216
- }) satisfies XYFlowEventHandlers, [diagramApi, xyflowApi])
230
+ }) satisfies XYFlowEventHandlers
231
+ }, [diagramApi])
217
232
  }
@@ -1,10 +1,10 @@
1
- import { isDynamicView } from '@likec4/core'
2
1
  import { Controls } from '@xyflow/react'
3
2
  import { shallowEqual } from 'fast-equals'
4
3
  import { memo, type PropsWithChildren } from 'react'
5
- import { isNonNull, isNonNullish, isNullish } from 'remeda'
4
+ import { isNonNull } from 'remeda'
6
5
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
7
- import { useDiagramState } from '../state/useDiagramStore'
6
+ import { useDiagramState } from '../state/hooks'
7
+ import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
8
8
  import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
9
  import { DynamicViewWalkthrough } from '../ui/DynamicViewWalkthrough'
10
10
  import OptionsPanel from '../ui/OptionsPanel'
@@ -14,6 +14,7 @@ type XYFlowInnerProps = PropsWithChildren<{
14
14
  background: NonNullable<LikeC4DiagramProperties['background']>
15
15
  controls: boolean
16
16
  showDiagramTitle: boolean
17
+ showNavigationButtons: boolean
17
18
  enableDynamicViewWalkthrough: boolean
18
19
  }>
19
20
 
@@ -22,6 +23,7 @@ export const XYFlowInner = memo(function XYFlowInnerR({
22
23
  background,
23
24
  controls,
24
25
  showDiagramTitle,
26
+ showNavigationButtons,
25
27
  enableDynamicViewWalkthrough
26
28
  }: XYFlowInnerProps) {
27
29
  const {
@@ -45,6 +47,7 @@ export const XYFlowInner = memo(function XYFlowInnerR({
45
47
  {readonly === false && <OptionsPanel />}
46
48
  {isDiagramTitleVisible && <DiagramTitlePanel />}
47
49
  {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
50
+ {showNavigationButtons && <BackwardForwardPanel />}
48
51
  {children}
49
52
  </>
50
53
  )
@@ -1,7 +1,7 @@
1
1
  export const ZIndexes = {
2
- Compound: 1,
3
- Edge: 2,
4
- Element: 3
2
+ Compound: 2,
3
+ Edge: 4,
4
+ Element: 6
5
5
  } as const
6
6
 
7
7
  export const MinZoom = 0.1