@likec4/diagram 1.1.0 → 1.2.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/KeepAspectRatioContainer.d.ts +1 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +22 -12
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2209 -706
- package/dist/mantine/EnsureMantine.d.ts +1 -1
- package/dist/state/DiagramContext.d.ts +2143 -19
- package/dist/state/WhenInitialized.d.ts +1 -1
- package/dist/state/diagramStore.d.ts +2152 -19
- package/dist/state/useDiagramStore.d.ts +2142 -18
- package/dist/style.css +256 -118
- package/dist/theme.css.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
- package/dist/ui/DiagramTitlePanel.d.ts +1 -0
- package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
- package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
- package/dist/ui/OptionsPanel.d.ts +1 -3
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/options/NodeOptions.d.ts +1 -2
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
- package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
- package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
- package/dist/xyflow/XYFlow.d.ts +4 -7
- package/dist/xyflow/XYFlowBackground.d.ts +5 -4
- package/dist/xyflow/XYFlowEvents.d.ts +84 -84
- package/dist/xyflow/XYFlowInner.d.ts +11 -0
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +14 -14
- package/dist/xyflow/edges/edges.css.d.ts +5 -2
- package/dist/xyflow/edges/utils.d.ts +2 -0
- package/dist/xyflow/hooks/useXYFlow.d.ts +126 -126
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
- package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
- package/dist/xyflow/types.d.ts +18 -18
- package/package.json +21 -16
- package/src/KeepAspectRatioContainer.tsx +1 -1
- package/src/LikeC4Diagram.css.ts +4 -1
- package/src/LikeC4Diagram.props.ts +24 -14
- package/src/LikeC4Diagram.tsx +18 -9
- package/src/StaticLikeC4Diagram.tsx +2 -0
- package/src/globals.css.ts +7 -1
- package/src/index.ts +0 -3
- package/src/state/DiagramContext.tsx +21 -12
- package/src/state/diagramStore.ts +275 -98
- package/src/theme.css.ts +1 -0
- package/src/ui/DiagramTitlePanel.css.ts +33 -0
- package/src/ui/DiagramTitlePanel.tsx +138 -0
- package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
- package/src/ui/DynamicViewWalkthrough.tsx +86 -0
- package/src/ui/OptionsPanel.tsx +7 -24
- package/src/ui/index.ts +1 -0
- package/src/ui/options/NodeOptions.tsx +103 -27
- package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +75 -0
- package/src/xyflow/SyncWithDiagram.tsx +55 -101
- package/src/xyflow/XYFlow.tsx +52 -32
- package/src/xyflow/XYFlowBackground.tsx +7 -5
- package/src/xyflow/XYFlowEvents.tsx +150 -104
- package/src/xyflow/XYFlowInner.tsx +50 -0
- package/src/xyflow/diagram-to-xyflow.ts +16 -21
- package/src/xyflow/edges/RelationshipEdge.tsx +102 -70
- package/src/xyflow/edges/edges.css.ts +128 -53
- package/src/xyflow/edges/utils.ts +31 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +20 -12
- package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
- package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
- package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
- package/src/xyflow/nodes/element/element.css.ts +10 -2
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
- package/src/xyflow/types.ts +7 -8
|
@@ -1,99 +1,119 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { memo, useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useDebouncedEffect } from '@react-hookz/web'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
4
3
|
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
|
|
5
|
-
import { useXYStore } from './hooks'
|
|
4
|
+
import { useXYStore, useXYStoreApi, type XYStoreApi } from './hooks'
|
|
6
5
|
import type { XYFlowState } from './types'
|
|
6
|
+
import { toDomPrecision } from './utils'
|
|
7
7
|
|
|
8
8
|
function selectDimensions(state: XYFlowState) {
|
|
9
9
|
return `${Math.round(state.width)}:${Math.round(state.height)}`
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function FitViewOnViewportResize({ diagramApi }: {
|
|
12
|
+
function FitViewOnViewportResize({ diagramApi, xyflowApi }: {
|
|
13
|
+
xyflowApi: XYStoreApi
|
|
14
|
+
diagramApi: DiagramStoreApi
|
|
15
|
+
}) {
|
|
13
16
|
const dimensions = useXYStore(selectDimensions)
|
|
14
17
|
const prevDimensionsRef = useRef(dimensions)
|
|
15
18
|
|
|
16
19
|
useDebouncedEffect(
|
|
17
20
|
() => {
|
|
18
|
-
|
|
21
|
+
const { focusedNodeId, fitDiagram } = diagramApi.getState()
|
|
22
|
+
if (focusedNodeId || prevDimensionsRef.current === dimensions) {
|
|
19
23
|
return
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
fitDiagram(xyflowApi)
|
|
22
26
|
prevDimensionsRef.current = dimensions
|
|
23
27
|
},
|
|
24
|
-
[dimensions],
|
|
25
|
-
|
|
28
|
+
[dimensions, diagramApi, xyflowApi],
|
|
29
|
+
200,
|
|
30
|
+
800
|
|
26
31
|
)
|
|
27
32
|
|
|
28
33
|
return null
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
function selector({ view, xyflowSynced, viewportChanged, lastOnNavigate, ...s }: DiagramState) {
|
|
37
|
+
const viewId = view.id + '_' + view.autoLayout + '_' + s.fitViewPadding
|
|
38
|
+
|
|
39
|
+
const waitCorrection = lastOnNavigate && lastOnNavigate.toView === view.id
|
|
40
|
+
&& lastOnNavigate.positionCorrected === false
|
|
41
|
+
const elTo = waitCorrection ? view.nodes.find(n => n.id === lastOnNavigate.element.id) : undefined
|
|
42
|
+
if (waitCorrection && elTo) {
|
|
43
|
+
return {
|
|
44
|
+
viewId,
|
|
45
|
+
xyflowSynced,
|
|
46
|
+
viewportChanged,
|
|
47
|
+
waitCorrection: true,
|
|
48
|
+
elFrom: lastOnNavigate.element,
|
|
49
|
+
elTo
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
viewId,
|
|
55
|
+
xyflowSynced,
|
|
56
|
+
viewportChanged,
|
|
57
|
+
waitCorrection: false,
|
|
58
|
+
fromElement: null,
|
|
59
|
+
toElement: null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
32
62
|
|
|
33
63
|
/**
|
|
34
64
|
* Fits the view when the view changes and nodes are initialized
|
|
35
65
|
*/
|
|
36
|
-
function
|
|
66
|
+
export function FitViewOnDiagramChange() {
|
|
67
|
+
const xyflowApi = useXYStoreApi()
|
|
68
|
+
const {
|
|
69
|
+
viewId: pendingViewId,
|
|
70
|
+
xyflowSynced,
|
|
71
|
+
viewportChanged,
|
|
72
|
+
waitCorrection,
|
|
73
|
+
elFrom,
|
|
74
|
+
elTo
|
|
75
|
+
} = useDiagramState(selector)
|
|
37
76
|
const diagramApi = useDiagramStoreApi()
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
),
|
|
60
|
-
[diagramApi, schedulePending]
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
useEffect(
|
|
64
|
-
() =>
|
|
65
|
-
diagramApi.subscribe(
|
|
66
|
-
s => s.viewportChanged,
|
|
67
|
-
setViewportMoved
|
|
68
|
-
),
|
|
69
|
-
[diagramApi]
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
useRafEffect(
|
|
73
|
-
() => {
|
|
74
|
-
if (!isReady || processedChangeId === pendingChangeId) {
|
|
75
|
-
return
|
|
77
|
+
const processedRef = useRef(pendingViewId)
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!xyflowSynced || pendingViewId === processedRef.current) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (waitCorrection && elTo && elFrom) {
|
|
84
|
+
const [x, y, zoom] = xyflowApi.getState().transform
|
|
85
|
+
const { xyflow, lastOnNavigate } = diagramApi.getState()
|
|
86
|
+
const nextZoom = Math.min(
|
|
87
|
+
elFrom.width / elTo.width,
|
|
88
|
+
elFrom.height / elTo.height
|
|
89
|
+
)
|
|
90
|
+
const centerFrom = lastOnNavigate!.elementCenterScreenPosition
|
|
91
|
+
if (nextZoom !== 1 && nextZoom < zoom) {
|
|
92
|
+
xyflowApi.setState({ transform: [x, y, nextZoom] })
|
|
93
|
+
xyflow.setViewport({
|
|
94
|
+
x,
|
|
95
|
+
y,
|
|
96
|
+
zoom: nextZoom
|
|
97
|
+
})
|
|
76
98
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
const centerTo = xyflow.flowToScreenPosition({
|
|
100
|
+
x: elTo.position[0] + elTo.width / 2,
|
|
101
|
+
y: elTo.position[1] + elTo.height / 2
|
|
102
|
+
}),
|
|
103
|
+
diff = {
|
|
104
|
+
x: toDomPrecision(centerFrom.x - centerTo.x),
|
|
105
|
+
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
106
|
+
}
|
|
107
|
+
xyflowApi.getState().panBy(diff)
|
|
108
|
+
diagramApi.setState({ lastOnNavigate: null })
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
processedRef.current = pendingViewId
|
|
112
|
+
diagramApi.getState().fitDiagram(xyflowApi)
|
|
113
|
+
}, [pendingViewId, xyflowSynced, waitCorrection])
|
|
114
|
+
|
|
115
|
+
if (!viewportChanged && xyflowSynced) {
|
|
116
|
+
return <FitViewOnViewportResize xyflowApi={xyflowApi} diagramApi={diagramApi} />
|
|
85
117
|
}
|
|
86
118
|
return null
|
|
87
119
|
}
|
|
88
|
-
|
|
89
|
-
const selectFitViewFlag = (s: DiagramState) => s.fitViewEnabled
|
|
90
|
-
|
|
91
|
-
export const FitViewOnDiagramChange = memo(() => {
|
|
92
|
-
const fitView = useDiagramState(selectFitViewFlag)
|
|
93
|
-
|
|
94
|
-
if (!fitView) {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return <FitViewOnDiagramChanges />
|
|
99
|
-
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { nonNullable } from '@likec4/core'
|
|
2
|
+
import type { EdgeChange, NodeChange } from '@xyflow/react'
|
|
3
|
+
import { useUpdateEffect } from '../hooks/useUpdateEffect'
|
|
4
|
+
import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
|
|
5
|
+
import { MinZoom } from './const'
|
|
6
|
+
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
7
|
+
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
8
|
+
|
|
9
|
+
export function SelectEdgesOnNodeFocus() {
|
|
10
|
+
const focusedNodeId = useDiagramState(s => s.focusedNodeId)
|
|
11
|
+
const diagramStore = useDiagramStoreApi()
|
|
12
|
+
const xyflowApi = useXYStoreApi()
|
|
13
|
+
|
|
14
|
+
useUpdateEffect(() => {
|
|
15
|
+
if (!focusedNodeId) {
|
|
16
|
+
xyflowApi.getState().resetSelectedElements()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
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)))
|
|
25
|
+
|
|
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 = nodeLookup.get(edge.source === focusedNodeId ? edge.target : edge.source)
|
|
34
|
+
if (anotherNode) {
|
|
35
|
+
fitViewNodes.add(anotherNode)
|
|
36
|
+
}
|
|
37
|
+
} else if (edge.selected) {
|
|
38
|
+
edgeChanges.push({
|
|
39
|
+
id: edge.id,
|
|
40
|
+
type: 'select',
|
|
41
|
+
selected: false
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const [, node] of nodeLookup) {
|
|
46
|
+
if (node.selected && node.id !== focusedNodeId) {
|
|
47
|
+
nodeChanges.push({
|
|
48
|
+
id: node.id,
|
|
49
|
+
type: 'select',
|
|
50
|
+
selected: false
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
nodeChanges.push({
|
|
55
|
+
id: focusedNodeId,
|
|
56
|
+
type: 'select',
|
|
57
|
+
selected: true
|
|
58
|
+
})
|
|
59
|
+
if (edgeChanges.length > 0) {
|
|
60
|
+
triggerEdgeChanges(edgeChanges)
|
|
61
|
+
}
|
|
62
|
+
triggerNodeChanges(nodeChanges)
|
|
63
|
+
|
|
64
|
+
fitView({
|
|
65
|
+
includeHiddenNodes: true,
|
|
66
|
+
duration: 300,
|
|
67
|
+
padding: 0.1,
|
|
68
|
+
minZoom: MinZoom,
|
|
69
|
+
maxZoom: Math.max(1, transform[2]),
|
|
70
|
+
nodes: Array.from(fitViewNodes)
|
|
71
|
+
})
|
|
72
|
+
}, [diagramStore, xyflowApi, focusedNodeId])
|
|
73
|
+
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
@@ -1,119 +1,73 @@
|
|
|
1
|
-
import { deepEqual as eq
|
|
2
|
-
import {
|
|
3
|
-
import type { XYFlowNode } from './types'
|
|
1
|
+
import { deepEqual as eq } from 'fast-equals'
|
|
2
|
+
import { useXYStoreApi } from './hooks'
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import { useDiagramStoreApi } from '../state'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { useDiagramState, useDiagramStoreApi } from '../state'
|
|
7
6
|
import { diagramViewToXYFlowData } from './diagram-to-xyflow'
|
|
8
|
-
import { toDomPrecision } from './utils'
|
|
9
|
-
|
|
10
|
-
const isSameNode = (a: XYFlowNode) => (b: XYFlowNode) =>
|
|
11
|
-
a.id === b.id && a.type === b.type && Object.is(a.parentId, b.parentId)
|
|
12
7
|
|
|
13
8
|
/**
|
|
14
9
|
* Syncs the diagram state with the XYFlow instance
|
|
15
10
|
*/
|
|
16
|
-
export
|
|
17
|
-
const
|
|
18
|
-
const xyflowRef = useRef(_xyflow)
|
|
19
|
-
xyflowRef.current = _xyflow
|
|
20
|
-
|
|
11
|
+
export function SyncWithDiagram() {
|
|
12
|
+
const xyflowSynced = useDiagramState(s => s.xyflowSynced)
|
|
21
13
|
const xyflowApi = useXYStoreApi()
|
|
22
14
|
const diagramStoreApi = useDiagramStoreApi()
|
|
23
15
|
|
|
24
16
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
xyflowRef.current.setNodes(prev =>
|
|
40
|
-
updates.nodes.map(update => {
|
|
41
|
-
const existing = prev.find(isSameNode(update))
|
|
42
|
-
if (existing) {
|
|
43
|
-
if (eq(existing.data.element, update.data.element)) {
|
|
44
|
-
return existing
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
...existing,
|
|
48
|
-
...update
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return update
|
|
52
|
-
})
|
|
53
|
-
)
|
|
17
|
+
if (xyflowSynced) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const {
|
|
21
|
+
view,
|
|
22
|
+
nodesDraggable,
|
|
23
|
+
nodesSelectable,
|
|
24
|
+
focusedNodeId
|
|
25
|
+
} = diagramStoreApi.getState()
|
|
26
|
+
const updates = diagramViewToXYFlowData(view, {
|
|
27
|
+
draggable: nodesDraggable,
|
|
28
|
+
selectable: nodesSelectable || focusedNodeId !== null
|
|
29
|
+
})
|
|
54
30
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
...update
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return update
|
|
65
|
-
})
|
|
66
|
-
)
|
|
31
|
+
const {
|
|
32
|
+
nodeLookup,
|
|
33
|
+
edgeLookup,
|
|
34
|
+
setNodes,
|
|
35
|
+
setEdges
|
|
36
|
+
} = xyflowApi.getState()
|
|
67
37
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
xyflowRef.current.setViewport({
|
|
82
|
-
x: v.x,
|
|
83
|
-
y: v.y,
|
|
84
|
-
zoom: nextZoom
|
|
85
|
-
})
|
|
86
|
-
}
|
|
38
|
+
setNodes(updates.nodes.map(update => {
|
|
39
|
+
const existing = nodeLookup.get(update.id)?.internals.userNode
|
|
40
|
+
if (existing && existing.type === update.type && eq(existing.parentId, update.parentId)) {
|
|
41
|
+
if (eq(existing.data.element, update.data.element)) {
|
|
42
|
+
return existing
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...existing,
|
|
46
|
+
...update
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return update
|
|
50
|
+
}))
|
|
87
51
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
95
|
-
}
|
|
96
|
-
xyflowApi.getState().panBy(diff)
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
diagramStoreApi.setState(
|
|
101
|
-
{
|
|
102
|
-
lastOnNavigate: {
|
|
103
|
-
...lastOnNavigate,
|
|
104
|
-
positionCorrected: true
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
false,
|
|
108
|
-
'positionCorrected'
|
|
109
|
-
)
|
|
52
|
+
setEdges(updates.edges.map(update => {
|
|
53
|
+
const existing = edgeLookup.get(update.id)
|
|
54
|
+
if (existing) {
|
|
55
|
+
return eq(existing.data.edge, update.data.edge) ? existing : {
|
|
56
|
+
...existing,
|
|
57
|
+
...update
|
|
110
58
|
}
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
equalityFn: shallowEqual
|
|
114
59
|
}
|
|
60
|
+
return update
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
diagramStoreApi.setState(
|
|
64
|
+
{
|
|
65
|
+
xyflowSynced: true
|
|
66
|
+
},
|
|
67
|
+
false,
|
|
68
|
+
'xyflowSynced'
|
|
115
69
|
)
|
|
116
|
-
}, [xyflowApi, diagramStoreApi])
|
|
70
|
+
}, [xyflowSynced, xyflowApi, diagramStoreApi])
|
|
117
71
|
|
|
118
72
|
return null
|
|
119
|
-
}
|
|
73
|
+
}
|
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { useMantineColorScheme } from '@mantine/core'
|
|
2
|
-
import { Controls, ReactFlow } from '@xyflow/react'
|
|
2
|
+
import { Controls, ReactFlow, useOnViewportChange } from '@xyflow/react'
|
|
3
|
+
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
3
4
|
import { type CSSProperties, memo, type PropsWithChildren } from 'react'
|
|
5
|
+
import { omit, omitBy } from 'remeda'
|
|
4
6
|
import type { SetNonNullable, Simplify } from 'type-fest'
|
|
5
7
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
6
8
|
import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
|
|
7
9
|
import { MinZoom } from './const'
|
|
8
10
|
import { RelationshipEdge } from './edges/RelationshipEdge'
|
|
9
11
|
import { useLayoutConstraints } from './hooks/useLayoutConstraints'
|
|
12
|
+
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
10
13
|
import { CompoundNode } from './nodes/compound'
|
|
11
14
|
import { ElementNode } from './nodes/element'
|
|
12
15
|
import { XYFlowEdge, XYFlowNode } from './types'
|
|
@@ -21,27 +24,25 @@ const edgeTypes = {
|
|
|
21
24
|
relationship: RelationshipEdge
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
type
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
>
|
|
31
|
-
>
|
|
27
|
+
type XYFlowWrapperProps = PropsWithChildren<{
|
|
28
|
+
className?: string | undefined
|
|
29
|
+
defaultNodes: XYFlowNode[]
|
|
30
|
+
defaultEdges: XYFlowEdge[]
|
|
31
|
+
style?: CSSProperties | undefined
|
|
32
|
+
}>
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
// const propsAreEqual = (
|
|
35
|
+
// prev: XYFlowWrapperProps,
|
|
36
|
+
// next: XYFlowWrapperProps
|
|
37
|
+
// ) => {
|
|
38
|
+
// return shallowEqual(
|
|
39
|
+
// omit(prev, ['children']),
|
|
40
|
+
// omit(next, ['children'])
|
|
41
|
+
// )
|
|
42
|
+
// }
|
|
42
43
|
|
|
43
44
|
const selector = (s: DiagramState) => ({
|
|
44
|
-
nodesSelectable: s.nodesSelectable,
|
|
45
|
+
nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
|
|
45
46
|
nodesDraggable: s.nodesDraggable,
|
|
46
47
|
fitView: s.fitViewEnabled,
|
|
47
48
|
fitViewPadding: s.fitViewPadding,
|
|
@@ -55,15 +56,14 @@ const selector = (s: DiagramState) => ({
|
|
|
55
56
|
pannable: s.pannable
|
|
56
57
|
})
|
|
57
58
|
|
|
58
|
-
function
|
|
59
|
+
export function XYFlow({
|
|
59
60
|
className,
|
|
60
61
|
children,
|
|
61
62
|
defaultNodes,
|
|
62
63
|
defaultEdges,
|
|
63
|
-
controls,
|
|
64
|
-
background,
|
|
65
64
|
style
|
|
66
65
|
}: XYFlowWrapperProps) {
|
|
66
|
+
const xyflowApi = useXYStoreApi()
|
|
67
67
|
const diagramApi = useDiagramStoreApi()
|
|
68
68
|
const {
|
|
69
69
|
nodesSelectable,
|
|
@@ -72,17 +72,39 @@ function XYFlowWrapper({
|
|
|
72
72
|
fitViewPadding,
|
|
73
73
|
pannable,
|
|
74
74
|
zoomable,
|
|
75
|
-
|
|
75
|
+
hasOnNodeClick,
|
|
76
|
+
hasOnNavigateTo,
|
|
77
|
+
hasOnNodeContextMenu,
|
|
78
|
+
hasOnCanvasContextMenu,
|
|
79
|
+
hasOnEdgeContextMenu,
|
|
80
|
+
hasOnEdgeClick
|
|
76
81
|
} = useDiagramState(selector)
|
|
77
82
|
|
|
78
83
|
const layoutConstraints = useLayoutConstraints()
|
|
79
84
|
|
|
80
85
|
const handlers = useXYFlowEvents()
|
|
81
|
-
const isBgWithPattern = background !== 'transparent' && background !== 'solid'
|
|
82
86
|
|
|
83
87
|
const { colorScheme } = useMantineColorScheme()
|
|
84
88
|
const colorMode = colorScheme !== 'auto' ? colorScheme : undefined
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* WORKAROUND - Called on viewport change
|
|
92
|
+
* Viewport transform is not rounded to integers which results in blurry nodes on some resolution
|
|
93
|
+
* https://github.com/xyflow/xyflow/issues/3282
|
|
94
|
+
* https://github.com/likec4/likec4/issues/734
|
|
95
|
+
*/
|
|
96
|
+
useOnViewportChange({
|
|
97
|
+
onEnd: ({ x, y, zoom }) => {
|
|
98
|
+
const rounded = {
|
|
99
|
+
x: Math.round(x),
|
|
100
|
+
y: Math.round(y)
|
|
101
|
+
}
|
|
102
|
+
if (x !== rounded.x || y !== rounded.y) {
|
|
103
|
+
xyflowApi.setState({ transform: [rounded.x, rounded.y, zoom] })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
86
108
|
return (
|
|
87
109
|
<ReactFlow<XYFlowNode, XYFlowEdge>
|
|
88
110
|
className={className}
|
|
@@ -113,8 +135,8 @@ function XYFlowWrapper({
|
|
|
113
135
|
panOnScroll={pannable}
|
|
114
136
|
panOnDrag={pannable}
|
|
115
137
|
elementsSelectable={nodesSelectable}
|
|
116
|
-
nodesFocusable={nodesDraggable || nodesSelectable ||
|
|
117
|
-
edgesFocusable={
|
|
138
|
+
nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
|
|
139
|
+
edgesFocusable={hasOnEdgeClick}
|
|
118
140
|
{...(!nodesSelectable && {
|
|
119
141
|
selectionKeyCode: null
|
|
120
142
|
})}
|
|
@@ -140,20 +162,18 @@ function XYFlowWrapper({
|
|
|
140
162
|
onInit={() => {
|
|
141
163
|
diagramApi.setState({ initialized: true }, false, 'initialized')
|
|
142
164
|
}}
|
|
143
|
-
{...(
|
|
165
|
+
{...(hasOnNodeContextMenu && {
|
|
144
166
|
onNodeContextMenu: handlers.onNodeContextMenu
|
|
145
167
|
})}
|
|
146
|
-
{...(
|
|
168
|
+
{...(hasOnEdgeContextMenu && {
|
|
147
169
|
onEdgeContextMenu: handlers.onEdgeContextMenu
|
|
148
170
|
})}
|
|
149
|
-
{...(
|
|
171
|
+
{...(hasOnCanvasContextMenu && {
|
|
150
172
|
onPaneContextMenu: handlers.onPaneContextMenu
|
|
151
173
|
})}>
|
|
152
|
-
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
153
|
-
{controls && <Controls />}
|
|
154
174
|
{children}
|
|
155
175
|
</ReactFlow>
|
|
156
176
|
)
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
export const XYFlow = memo(XYFlowWrapper) as typeof XYFlowWrapper
|
|
179
|
+
// export const XYFlow = memo(XYFlowWrapper, propsAreEqual) as typeof XYFlowWrapper
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { nonexhaustive } from '@likec4/core'
|
|
2
2
|
import { Background, type BackgroundProps, BackgroundVariant } from '@xyflow/react'
|
|
3
|
-
import { deepEqual } from 'fast-equals'
|
|
4
|
-
import { memo } from 'react'
|
|
5
3
|
import type { OverrideProperties } from 'type-fest'
|
|
6
4
|
|
|
7
5
|
export type XYBackgroundVariant = 'dots' | 'lines' | 'cross'
|
|
8
|
-
export type
|
|
6
|
+
export type XYBackground =
|
|
9
7
|
| XYBackgroundVariant
|
|
10
8
|
| OverrideProperties<BackgroundProps, {
|
|
11
9
|
variant: XYBackgroundVariant
|
|
12
10
|
}>
|
|
13
11
|
|
|
12
|
+
export type XYBackgroundProps = {
|
|
13
|
+
background: XYBackground
|
|
14
|
+
}
|
|
15
|
+
|
|
14
16
|
function literalToEnum(value: XYBackgroundVariant): BackgroundVariant {
|
|
15
17
|
switch (value) {
|
|
16
18
|
case 'dots':
|
|
@@ -24,10 +26,10 @@ function literalToEnum(value: XYBackgroundVariant): BackgroundVariant {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export
|
|
29
|
+
export function XYFlowBackground({ background }: XYBackgroundProps) {
|
|
28
30
|
if (typeof background === 'string') {
|
|
29
31
|
return <Background variant={literalToEnum(background)} />
|
|
30
32
|
}
|
|
31
33
|
const { variant, ...rest } = background
|
|
32
34
|
return <Background variant={literalToEnum(variant)} {...rest} />
|
|
33
|
-
}
|
|
35
|
+
}
|