@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.
- package/dist/LikeC4Diagram.css.d.ts +0 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1580 -1194
- package/dist/state/DiagramContext.d.ts +97 -4169
- package/dist/state/diagramStore.d.ts +98 -4172
- package/dist/state/hooks.d.ts +4 -0
- package/dist/state/index.d.ts +1 -3
- package/dist/state/useDiagramStore.d.ts +3 -4622
- package/dist/state/useNavigationHistory.d.ts +5 -0
- package/dist/style.css +217 -159
- package/dist/theme.css.d.ts +5 -0
- package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
- package/dist/ui/BackwardForwardPanel.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
- package/dist/ui/OptionsPanel.d.ts +1 -3
- package/dist/ui/options/EdgeOptions.d.ts +19 -0
- package/dist/ui/options/NodeOptions.d.ts +2 -2
- package/dist/xyflow/EdgeMarkers.d.ts +11 -0
- package/dist/xyflow/XYFlowEvents.d.ts +12 -9
- package/dist/xyflow/XYFlowInner.d.ts +1 -0
- package/dist/xyflow/const.d.ts +3 -3
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -3
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/dist/xyflow/edges/utils.d.ts +1 -0
- package/dist/xyflow/hooks/index.d.ts +3 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +22 -21
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
- package/dist/xyflow/nodes/element/element.css.d.ts +3 -1
- package/dist/xyflow/types.d.ts +2 -32
- package/package.json +21 -21
- package/src/LikeC4Diagram.css.ts +10 -8
- package/src/LikeC4Diagram.props.ts +12 -5
- package/src/LikeC4Diagram.tsx +43 -26
- package/src/globals.css.ts +11 -4
- package/src/index.ts +1 -1
- package/src/mantine.css.ts +2 -2
- package/src/state/DiagramContext.tsx +47 -16
- package/src/state/diagramStore.ts +89 -80
- package/src/state/hooks.ts +5 -0
- package/src/state/index.ts +1 -3
- package/src/state/useDiagramStore.ts +6 -13
- package/src/state/useNavigationHistory.ts +39 -0
- package/src/theme.css.ts +5 -0
- package/src/ui/BackwardForwardPanel.css.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +59 -0
- package/src/ui/DiagramTitlePanel.css.ts +17 -6
- package/src/ui/DiagramTitlePanel.tsx +108 -94
- package/src/ui/DynamicViewWalkthrough.tsx +6 -4
- package/src/ui/OptionsPanel.tsx +37 -18
- package/src/ui/options/EdgeOptions.tsx +157 -0
- package/src/ui/options/NodeOptions.tsx +37 -29
- package/src/xyflow/EdgeMarkers.tsx +148 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
- package/src/xyflow/SyncWithDiagram.tsx +13 -20
- package/src/xyflow/XYFlow.tsx +2 -4
- package/src/xyflow/XYFlowEvents.tsx +48 -33
- package/src/xyflow/XYFlowInner.tsx +6 -3
- package/src/xyflow/const.ts +3 -3
- package/src/xyflow/diagram-to-xyflow.ts +18 -53
- package/src/xyflow/edges/RelationshipEdge.tsx +218 -126
- package/src/xyflow/edges/edges.css.ts +44 -26
- package/src/xyflow/edges/utils.ts +10 -0
- package/src/xyflow/hooks/index.ts +4 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +5 -5
- package/src/xyflow/hooks/useXYFlow.ts +28 -1
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +41 -31
- package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
- package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
- package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
- package/src/xyflow/nodes/element/ElementNode.tsx +69 -114
- package/src/xyflow/nodes/element/element.css.ts +48 -15
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
- package/src/xyflow/types.ts +2 -4
- package/src/xyflow/utils.ts +3 -6
- package/dist/KeepAspectRatioContainer.d.ts +0 -9
- package/dist/icons/Clipboard.d.ts +0 -3
- package/dist/icons/Link.d.ts +0 -3
- package/dist/icons/ZoomIn.d.ts +0 -3
- package/dist/icons/index.d.ts +0 -3
- package/dist/mantine/theme.d.ts +0 -3
- package/dist/state/WhenInitialized.d.ts +0 -3
- package/dist/ui/Camera.d.ts +0 -3
- package/src/KeepAspectRatioContainer.tsx +0 -36
- package/src/icons/Clipboard.tsx +0 -24
- package/src/icons/Link.tsx +0 -23
- package/src/icons/ZoomIn.tsx +0 -25
- package/src/icons/index.ts +0 -3
- package/src/mantine/theme.ts +0 -20
- package/src/state/WhenInitialized.tsx +0 -24
- 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(
|
|
27
|
+
fitDiagram(300)
|
|
28
28
|
prevDimensionsRef.current = dimensions
|
|
29
29
|
},
|
|
30
30
|
[dimensions, diagramApi, xyflowApi],
|
|
31
|
-
|
|
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(
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
useEffect(
|
|
13
|
+
() =>
|
|
14
|
+
diagramStore.subscribe(s => s.focusedNodeId, focusedNodeId => {
|
|
15
|
+
if (!focusedNodeId) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
57
|
+
id: focusedNodeId,
|
|
47
58
|
type: 'select',
|
|
48
|
-
selected:
|
|
59
|
+
selected: true
|
|
49
60
|
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 {
|
|
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
|
-
} =
|
|
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 (
|
|
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,
|
|
82
|
+
}, [xyflowSynced, diagramStoreApi])
|
|
90
83
|
|
|
91
84
|
return null
|
|
92
85
|
}
|
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -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(
|
|
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
|
|
86
|
-
fitDiagram(
|
|
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
|
-
|
|
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 =
|
|
130
|
-
if (focusPossible
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
4
|
+
import { isNonNull } from 'remeda'
|
|
6
5
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
7
|
-
import { useDiagramState } from '../state/
|
|
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
|
)
|
package/src/xyflow/const.ts
CHANGED