@likec4/diagram 1.1.1 → 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 +7 -2
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2517 -1326
- package/dist/mantine/EnsureMantine.d.ts +1 -1
- package/dist/state/DiagramContext.d.ts +2125 -10
- package/dist/state/WhenInitialized.d.ts +1 -1
- package/dist/state/diagramStore.d.ts +2133 -10
- package/dist/state/useDiagramStore.d.ts +2124 -9
- package/dist/style.css +210 -136
- package/dist/theme.css.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.d.ts +1 -2
- package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
- package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
- package/dist/ui/OptionsPanel.d.ts +1 -2
- 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/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 +57 -63
- 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 +4 -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 +19 -16
- package/src/KeepAspectRatioContainer.tsx +1 -1
- package/src/LikeC4Diagram.css.ts +2 -0
- package/src/LikeC4Diagram.props.ts +9 -4
- package/src/LikeC4Diagram.tsx +17 -11
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +1 -0
- package/src/index.ts +0 -3
- package/src/state/DiagramContext.tsx +21 -12
- package/src/state/diagramStore.ts +177 -38
- package/src/theme.css.ts +1 -0
- package/src/ui/DiagramTitlePanel.css.ts +2 -1
- package/src/ui/DiagramTitlePanel.tsx +3 -4
- package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
- package/src/ui/DynamicViewWalkthrough.tsx +86 -0
- package/src/ui/OptionsPanel.tsx +5 -3
- package/src/ui/index.ts +1 -0
- package/src/ui/options/NodeOptions.tsx +23 -24
- package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
- package/src/xyflow/SyncWithDiagram.tsx +55 -107
- package/src/xyflow/XYFlow.tsx +51 -35
- package/src/xyflow/XYFlowBackground.tsx +7 -5
- package/src/xyflow/XYFlowEvents.tsx +97 -92
- package/src/xyflow/XYFlowInner.tsx +50 -0
- package/src/xyflow/diagram-to-xyflow.ts +16 -21
- package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
- package/src/xyflow/edges/edges.css.ts +68 -34
- package/src/xyflow/edges/utils.ts +31 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
- 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 +9 -2
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
- package/src/xyflow/types.ts +7 -8
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { useMantineColorScheme } from '@mantine/core'
|
|
2
|
-
import { Controls,
|
|
3
|
-
import {
|
|
2
|
+
import { Controls, ReactFlow, useOnViewportChange } from '@xyflow/react'
|
|
3
|
+
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
4
4
|
import { type CSSProperties, memo, type PropsWithChildren } from 'react'
|
|
5
|
+
import { omit, omitBy } from 'remeda'
|
|
5
6
|
import type { SetNonNullable, Simplify } from 'type-fest'
|
|
6
7
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
7
8
|
import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
|
|
8
9
|
import { MinZoom } from './const'
|
|
9
10
|
import { RelationshipEdge } from './edges/RelationshipEdge'
|
|
10
11
|
import { useLayoutConstraints } from './hooks/useLayoutConstraints'
|
|
12
|
+
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
11
13
|
import { CompoundNode } from './nodes/compound'
|
|
12
14
|
import { ElementNode } from './nodes/element'
|
|
13
|
-
import { SelectEdgesOnNodeFocus } from './SelectEdgesOnNodeFocus'
|
|
14
15
|
import { XYFlowEdge, XYFlowNode } from './types'
|
|
15
16
|
import { XYFlowBackground } from './XYFlowBackground'
|
|
16
17
|
import { useXYFlowEvents } from './XYFlowEvents'
|
|
@@ -23,24 +24,22 @@ const edgeTypes = {
|
|
|
23
24
|
relationship: RelationshipEdge
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
type
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
>
|
|
33
|
-
>
|
|
27
|
+
type XYFlowWrapperProps = PropsWithChildren<{
|
|
28
|
+
className?: string | undefined
|
|
29
|
+
defaultNodes: XYFlowNode[]
|
|
30
|
+
defaultEdges: XYFlowEdge[]
|
|
31
|
+
style?: CSSProperties | undefined
|
|
32
|
+
}>
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
// const propsAreEqual = (
|
|
35
|
+
// prev: XYFlowWrapperProps,
|
|
36
|
+
// next: XYFlowWrapperProps
|
|
37
|
+
// ) => {
|
|
38
|
+
// return shallowEqual(
|
|
39
|
+
// omit(prev, ['children']),
|
|
40
|
+
// omit(next, ['children'])
|
|
41
|
+
// )
|
|
42
|
+
// }
|
|
44
43
|
|
|
45
44
|
const selector = (s: DiagramState) => ({
|
|
46
45
|
nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
|
|
@@ -57,15 +56,14 @@ const selector = (s: DiagramState) => ({
|
|
|
57
56
|
pannable: s.pannable
|
|
58
57
|
})
|
|
59
58
|
|
|
60
|
-
function
|
|
59
|
+
export function XYFlow({
|
|
61
60
|
className,
|
|
62
61
|
children,
|
|
63
62
|
defaultNodes,
|
|
64
63
|
defaultEdges,
|
|
65
|
-
controls,
|
|
66
|
-
background,
|
|
67
64
|
style
|
|
68
65
|
}: XYFlowWrapperProps) {
|
|
66
|
+
const xyflowApi = useXYStoreApi()
|
|
69
67
|
const diagramApi = useDiagramStoreApi()
|
|
70
68
|
const {
|
|
71
69
|
nodesSelectable,
|
|
@@ -74,17 +72,39 @@ function XYFlowWrapper({
|
|
|
74
72
|
fitViewPadding,
|
|
75
73
|
pannable,
|
|
76
74
|
zoomable,
|
|
77
|
-
|
|
75
|
+
hasOnNodeClick,
|
|
76
|
+
hasOnNavigateTo,
|
|
77
|
+
hasOnNodeContextMenu,
|
|
78
|
+
hasOnCanvasContextMenu,
|
|
79
|
+
hasOnEdgeContextMenu,
|
|
80
|
+
hasOnEdgeClick
|
|
78
81
|
} = useDiagramState(selector)
|
|
79
82
|
|
|
80
83
|
const layoutConstraints = useLayoutConstraints()
|
|
81
84
|
|
|
82
85
|
const handlers = useXYFlowEvents()
|
|
83
|
-
const isBgWithPattern = background !== 'transparent' && background !== 'solid'
|
|
84
86
|
|
|
85
87
|
const { colorScheme } = useMantineColorScheme()
|
|
86
88
|
const colorMode = colorScheme !== 'auto' ? colorScheme : undefined
|
|
87
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
|
+
|
|
88
108
|
return (
|
|
89
109
|
<ReactFlow<XYFlowNode, XYFlowEdge>
|
|
90
110
|
className={className}
|
|
@@ -115,8 +135,8 @@ function XYFlowWrapper({
|
|
|
115
135
|
panOnScroll={pannable}
|
|
116
136
|
panOnDrag={pannable}
|
|
117
137
|
elementsSelectable={nodesSelectable}
|
|
118
|
-
nodesFocusable={nodesDraggable || nodesSelectable ||
|
|
119
|
-
edgesFocusable={
|
|
138
|
+
nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
|
|
139
|
+
edgesFocusable={hasOnEdgeClick}
|
|
120
140
|
{...(!nodesSelectable && {
|
|
121
141
|
selectionKeyCode: null
|
|
122
142
|
})}
|
|
@@ -139,25 +159,21 @@ function XYFlowWrapper({
|
|
|
139
159
|
onNodeClick={handlers.onNodeClick}
|
|
140
160
|
onNodeDoubleClick={handlers.onNodeDoubleClick}
|
|
141
161
|
onEdgeClick={handlers.onEdgeClick}
|
|
142
|
-
onViewportChange={handlers.onViewportChange}
|
|
143
162
|
onInit={() => {
|
|
144
163
|
diagramApi.setState({ initialized: true }, false, 'initialized')
|
|
145
164
|
}}
|
|
146
|
-
{...(
|
|
165
|
+
{...(hasOnNodeContextMenu && {
|
|
147
166
|
onNodeContextMenu: handlers.onNodeContextMenu
|
|
148
167
|
})}
|
|
149
|
-
{...(
|
|
168
|
+
{...(hasOnEdgeContextMenu && {
|
|
150
169
|
onEdgeContextMenu: handlers.onEdgeContextMenu
|
|
151
170
|
})}
|
|
152
|
-
{...(
|
|
171
|
+
{...(hasOnCanvasContextMenu && {
|
|
153
172
|
onPaneContextMenu: handlers.onPaneContextMenu
|
|
154
173
|
})}>
|
|
155
|
-
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
156
|
-
{controls && <Controls position={'bottom-right'} />}
|
|
157
|
-
{zoomable && !editor.hasOnNodeClick && <SelectEdgesOnNodeFocus />}
|
|
158
174
|
{children}
|
|
159
175
|
</ReactFlow>
|
|
160
176
|
)
|
|
161
177
|
}
|
|
162
178
|
|
|
163
|
-
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
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ReactFlowProps } from '@xyflow/react'
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo, useRef } from 'react'
|
|
3
3
|
import type { Simplify } from 'type-fest'
|
|
4
4
|
import { useDiagramStoreApi } from '../state'
|
|
5
5
|
import { useXYStoreApi } from './hooks'
|
|
6
|
-
import type { XYFlowEdge, XYFlowNode
|
|
6
|
+
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
7
7
|
|
|
8
8
|
type XYFlowEventHandlers = Simplify<
|
|
9
9
|
Required<
|
|
@@ -22,23 +22,17 @@ type XYFlowEventHandlers = Simplify<
|
|
|
22
22
|
| 'onNodeMouseLeave'
|
|
23
23
|
| 'onEdgeMouseEnter'
|
|
24
24
|
| 'onEdgeMouseLeave'
|
|
25
|
-
| 'onViewportChange'
|
|
26
25
|
>
|
|
27
26
|
>
|
|
28
27
|
>
|
|
29
28
|
|
|
30
|
-
const viewportDomNode = (state: XYFlowState) =>
|
|
31
|
-
state.domNode?.querySelector<HTMLDivElement>('.react-flow__viewport') ?? null
|
|
32
|
-
|
|
33
29
|
export function useXYFlowEvents() {
|
|
34
30
|
const diagramApi = useDiagramStoreApi()
|
|
35
31
|
const xyflowApi = useXYStoreApi()
|
|
36
32
|
|
|
37
33
|
const dblclickTimeout = useRef<number>()
|
|
38
34
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const dbclickGuarg = () => {
|
|
35
|
+
const dbclickLock = () => {
|
|
42
36
|
if (dblclickTimeout.current !== undefined) {
|
|
43
37
|
return true
|
|
44
38
|
}
|
|
@@ -48,46 +42,41 @@ export function useXYFlowEvents() {
|
|
|
48
42
|
return false
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
useEffect(() =>
|
|
55
|
-
xyflowApi.subscribe((state, prev) => {
|
|
56
|
-
if (state.domNode !== prev.domNode || (state.domNode && !viewportRef.current)) {
|
|
57
|
-
viewportRef.current = viewportDomNode(state)
|
|
58
|
-
}
|
|
59
|
-
}), [xyflowApi])
|
|
45
|
+
// If we are in focused mode, on edge enter we want to "highlight" the other node
|
|
46
|
+
// This ref contains the id of this node
|
|
47
|
+
const hoveredNodeFromOnEdgeEnterRef = useRef('')
|
|
60
48
|
|
|
61
49
|
return useMemo(() =>
|
|
62
50
|
({
|
|
63
51
|
onDoubleClick: (event) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
52
|
+
const {
|
|
53
|
+
fitViewEnabled,
|
|
54
|
+
onCanvasDblClick,
|
|
55
|
+
resetLastClicked,
|
|
56
|
+
fitDiagram
|
|
57
|
+
} = diagramApi.getState()
|
|
58
|
+
if (fitViewEnabled) {
|
|
59
|
+
fitDiagram(xyflowApi)
|
|
60
|
+
if (!onCanvasDblClick) {
|
|
61
|
+
event.stopPropagation()
|
|
62
|
+
}
|
|
74
63
|
}
|
|
64
|
+
resetLastClicked()
|
|
65
|
+
onCanvasDblClick?.(event)
|
|
75
66
|
},
|
|
76
67
|
onPaneClick: (event) => {
|
|
77
|
-
if (
|
|
68
|
+
if (dbclickLock()) {
|
|
78
69
|
return
|
|
79
70
|
}
|
|
80
71
|
const { focusedNodeId, fitDiagram, onCanvasClick, resetLastClicked } = diagramApi.getState()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
onCanvasClick
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// reset focus if clicked on empty space
|
|
87
|
-
if (focusedNodeId) {
|
|
88
|
-
fitDiagram()
|
|
89
|
-
event.stopPropagation()
|
|
72
|
+
if (!!focusedNodeId) {
|
|
73
|
+
fitDiagram(xyflowApi)
|
|
74
|
+
if (!onCanvasClick) {
|
|
75
|
+
event.stopPropagation()
|
|
76
|
+
}
|
|
90
77
|
}
|
|
78
|
+
resetLastClicked()
|
|
79
|
+
onCanvasClick?.(event)
|
|
91
80
|
},
|
|
92
81
|
onNodeContextMenu: (event, xynode) => {
|
|
93
82
|
diagramApi.getState().setLastClickedNode(xynode.id)
|
|
@@ -110,49 +99,63 @@ export function useXYFlowEvents() {
|
|
|
110
99
|
})
|
|
111
100
|
},
|
|
112
101
|
onNodeClick: (event, xynode) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onNodeClick
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
// if we clicked on a node that is not focused, fit the diagram
|
|
102
|
+
const {
|
|
103
|
+
focusedNodeId,
|
|
104
|
+
focusOnNode,
|
|
105
|
+
onNodeClick,
|
|
106
|
+
setLastClickedNode
|
|
107
|
+
} = diagramApi.getState()
|
|
108
|
+
setLastClickedNode(xynode.id)
|
|
109
|
+
// if we focused on a node, and clicked on another node - focus on the clicked node
|
|
124
110
|
if (!!focusedNodeId && focusedNodeId !== xynode.id) {
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
focusOnNode(xynode.id)
|
|
112
|
+
if (!onNodeClick) {
|
|
113
|
+
// user did not provide a custom handler, stop propagation
|
|
114
|
+
event.stopPropagation()
|
|
115
|
+
}
|
|
127
116
|
}
|
|
117
|
+
onNodeClick?.({
|
|
118
|
+
element: xynode.data.element,
|
|
119
|
+
xynode,
|
|
120
|
+
event
|
|
121
|
+
})
|
|
128
122
|
},
|
|
129
123
|
onNodeDoubleClick: (event, xynode) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
const {
|
|
125
|
+
focusedNodeId,
|
|
126
|
+
zoomable,
|
|
127
|
+
fitViewEnabled,
|
|
128
|
+
fitDiagram,
|
|
129
|
+
focusOnNode,
|
|
130
|
+
setLastClickedNode
|
|
131
|
+
} = diagramApi.getState()
|
|
132
|
+
setLastClickedNode(xynode.id)
|
|
133
|
+
if (!!focusedNodeId || (zoomable && fitViewEnabled)) {
|
|
134
|
+
// if we are already focused on the node, cancel
|
|
135
|
+
if (focusedNodeId === xynode.id) {
|
|
136
|
+
fitDiagram(xyflowApi)
|
|
137
|
+
} else {
|
|
138
|
+
focusOnNode(xynode.id)
|
|
139
|
+
}
|
|
137
140
|
event.stopPropagation()
|
|
138
141
|
}
|
|
139
142
|
},
|
|
140
143
|
onEdgeClick: (event, xyedge) => {
|
|
141
144
|
diagramApi.getState().setLastClickedEdge(xyedge.id)
|
|
142
|
-
const { focusedNodeId,
|
|
143
|
-
if
|
|
144
|
-
onEdgeClick({
|
|
145
|
-
relation: xyedge.data.edge,
|
|
146
|
-
xyedge,
|
|
147
|
-
event
|
|
148
|
-
})
|
|
149
|
-
return
|
|
150
|
-
}
|
|
145
|
+
const { focusedNodeId, focusOnNode, onEdgeClick } = diagramApi.getState()
|
|
146
|
+
// if we focused on a node, and clicked on an edge connected to it - focus on the other node
|
|
151
147
|
if (focusedNodeId && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target)) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
|
|
149
|
+
if (!onEdgeClick) {
|
|
150
|
+
// user did not provide a custom handler, stop propagation
|
|
151
|
+
event.stopPropagation()
|
|
152
|
+
}
|
|
155
153
|
}
|
|
154
|
+
onEdgeClick?.({
|
|
155
|
+
relation: xyedge.data.edge,
|
|
156
|
+
xyedge,
|
|
157
|
+
event
|
|
158
|
+
})
|
|
156
159
|
},
|
|
157
160
|
onMoveEnd: (event, _viewport) => {
|
|
158
161
|
// if event is present, the move was triggered by user
|
|
@@ -162,33 +165,35 @@ export function useXYFlowEvents() {
|
|
|
162
165
|
}
|
|
163
166
|
},
|
|
164
167
|
onNodeMouseEnter: (event, xynode) => {
|
|
165
|
-
|
|
168
|
+
hoveredNodeFromOnEdgeEnterRef.current = ''
|
|
169
|
+
const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
|
|
170
|
+
if (hoveredNodeId !== xynode.id) {
|
|
171
|
+
setHoveredNode(xynode.id)
|
|
172
|
+
}
|
|
166
173
|
},
|
|
167
174
|
onNodeMouseLeave: (event, xynode) => {
|
|
168
|
-
diagramApi.getState()
|
|
175
|
+
const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
|
|
176
|
+
if (hoveredNodeId === xynode.id) {
|
|
177
|
+
setHoveredNode(null)
|
|
178
|
+
}
|
|
169
179
|
},
|
|
170
|
-
onEdgeMouseEnter: (event,
|
|
171
|
-
diagramApi.getState()
|
|
180
|
+
onEdgeMouseEnter: (event, { id, source, target }) => {
|
|
181
|
+
const { hoveredNodeId, focusedNodeId, setHoveredEdge, setHoveredNode } = diagramApi.getState()
|
|
182
|
+
setHoveredEdge(id)
|
|
183
|
+
if ((focusedNodeId === source || focusedNodeId === target) && focusedNodeId !== hoveredNodeId) {
|
|
184
|
+
const next = hoveredNodeFromOnEdgeEnterRef.current = source === focusedNodeId ? target : source
|
|
185
|
+
setHoveredNode(next)
|
|
186
|
+
}
|
|
172
187
|
},
|
|
173
188
|
onEdgeMouseLeave: (event, xyedge) => {
|
|
174
|
-
diagramApi.getState()
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const scale = zoom != 1 ? zoom.toFixed(6) : '1'
|
|
183
|
-
transformRef.current = `translate(${Math.round(x)}px, ${Math.round(y)}px) scale(${scale})`
|
|
184
|
-
cbRef.current ??= requestIdleCallback(() => {
|
|
185
|
-
cbRef.current = undefined
|
|
186
|
-
if (viewportRef.current && viewportRef.current.style.transform !== transformRef.current) {
|
|
187
|
-
viewportRef.current.style.transform = transformRef.current
|
|
188
|
-
}
|
|
189
|
-
}, {
|
|
190
|
-
timeout: 1000
|
|
191
|
-
})
|
|
189
|
+
const { hoveredEdgeId, setHoveredEdge, hoveredNodeId, setHoveredNode } = diagramApi.getState()
|
|
190
|
+
if (hoveredEdgeId === xyedge.id) {
|
|
191
|
+
setHoveredEdge(null)
|
|
192
|
+
}
|
|
193
|
+
if (hoveredNodeId === hoveredNodeFromOnEdgeEnterRef.current) {
|
|
194
|
+
hoveredNodeFromOnEdgeEnterRef.current = ''
|
|
195
|
+
setHoveredNode(null)
|
|
196
|
+
}
|
|
192
197
|
}
|
|
193
198
|
}) satisfies XYFlowEventHandlers, [diagramApi, xyflowApi])
|
|
194
199
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isDynamicView } from '@likec4/core'
|
|
2
|
+
import { Controls } from '@xyflow/react'
|
|
3
|
+
import type { PropsWithChildren } from 'react'
|
|
4
|
+
import { isNonNull, isNonNullish, isNullish } from 'remeda'
|
|
5
|
+
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
6
|
+
import { useDiagramState } from '../state/useDiagramStore'
|
|
7
|
+
import { DynamicViewWalkthrough } from '../ui'
|
|
8
|
+
import DiagramTitlePanel from '../ui/DiagramTitlePanel'
|
|
9
|
+
import OptionsPanel from '../ui/OptionsPanel'
|
|
10
|
+
import { XYFlowBackground } from './XYFlowBackground'
|
|
11
|
+
|
|
12
|
+
type XYFlowInnerProps = PropsWithChildren<{
|
|
13
|
+
background: NonNullable<LikeC4DiagramProperties['background']>
|
|
14
|
+
controls: boolean
|
|
15
|
+
showDiagramTitle: boolean
|
|
16
|
+
enableDynamicViewWalkthrough: boolean
|
|
17
|
+
}>
|
|
18
|
+
|
|
19
|
+
export function XYFlowInner({
|
|
20
|
+
children,
|
|
21
|
+
background,
|
|
22
|
+
controls,
|
|
23
|
+
showDiagramTitle,
|
|
24
|
+
enableDynamicViewWalkthrough
|
|
25
|
+
}: XYFlowInnerProps) {
|
|
26
|
+
const {
|
|
27
|
+
isDynamicView,
|
|
28
|
+
isDynamicViewActive,
|
|
29
|
+
readonly
|
|
30
|
+
} = useDiagramState(s => ({
|
|
31
|
+
isDynamicView: s.isDynamicView(),
|
|
32
|
+
readonly: s.readonly,
|
|
33
|
+
isDynamicViewActive: isNonNull(s.activeDynamicViewStep)
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
const isBgWithPattern = background !== 'transparent' && background !== 'solid'
|
|
37
|
+
|
|
38
|
+
const isDiagramTitleVisible = !isDynamicViewActive && showDiagramTitle === true
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
43
|
+
{controls && <Controls position={'bottom-right'} />}
|
|
44
|
+
{readonly === false && <OptionsPanel />}
|
|
45
|
+
{isDiagramTitleVisible && <DiagramTitlePanel />}
|
|
46
|
+
{isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
|
|
47
|
+
{children}
|
|
48
|
+
</>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type DiagramNode,
|
|
3
3
|
type DiagramView,
|
|
4
|
+
extractStep,
|
|
4
5
|
type Fqn,
|
|
5
6
|
invariant,
|
|
6
7
|
type NonEmptyArray,
|
|
7
8
|
nonNullable,
|
|
8
9
|
type Point
|
|
9
10
|
} from '@likec4/core'
|
|
10
|
-
import {
|
|
11
|
+
import { Bezier } from 'bezier-js'
|
|
11
12
|
import { hasAtLeast } from 'remeda'
|
|
12
13
|
import type { XYFlowData } from '../xyflow/types'
|
|
13
14
|
|
|
@@ -20,24 +21,16 @@ function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
|
|
|
20
21
|
|
|
21
22
|
while (hasAtLeast(bezierPoints, 3)) {
|
|
22
23
|
const [cp1, cp2, end, ...rest] = bezierPoints
|
|
23
|
-
const [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
const bezier = new Bezier(start[0], start[1], cp1[0], cp1[1], cp2[0], cp2[1], end[0], end[1])
|
|
25
|
+
// const { x, y } = bezier.get(0.5)
|
|
26
|
+
const inflections = bezier.inflections()
|
|
27
|
+
if (inflections.length === 0) {
|
|
28
|
+
inflections.push(0.5)
|
|
29
|
+
}
|
|
30
|
+
inflections.forEach(t => {
|
|
31
|
+
const { x, y } = bezier.get(t)
|
|
32
|
+
handles.push([x, y])
|
|
32
33
|
})
|
|
33
|
-
handles.push(
|
|
34
|
-
[centerX, centerY]
|
|
35
|
-
)
|
|
36
|
-
// if (rest.length === 0) {
|
|
37
|
-
// handles.push(
|
|
38
|
-
// end
|
|
39
|
-
// )
|
|
40
|
-
// }
|
|
41
34
|
bezierPoints = rest
|
|
42
35
|
start = end
|
|
43
36
|
}
|
|
@@ -50,12 +43,13 @@ function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
|
|
|
50
43
|
const nodeZIndex = (node: DiagramNode) => node.level + 1
|
|
51
44
|
|
|
52
45
|
export function diagramViewToXYFlowData(
|
|
53
|
-
view: Pick<DiagramView, 'nodes' | 'edges'>,
|
|
46
|
+
view: Pick<DiagramView, 'nodes' | 'edges' | '__'>,
|
|
54
47
|
opts: {
|
|
55
48
|
draggable: boolean
|
|
56
49
|
selectable: boolean
|
|
57
50
|
}
|
|
58
51
|
): XYFlowData {
|
|
52
|
+
const isDynamicView = view.__ === 'dynamic'
|
|
59
53
|
const editor: XYFlowData = {
|
|
60
54
|
nodes: [],
|
|
61
55
|
edges: []
|
|
@@ -178,11 +172,12 @@ export function diagramViewToXYFlowData(
|
|
|
178
172
|
controlPoints,
|
|
179
173
|
headPoint: edge.headArrowPoint ?? null,
|
|
180
174
|
tailPoint: edge.tailArrowPoint ?? null,
|
|
181
|
-
|
|
175
|
+
stepNum: isDynamicView ? extractStep(edge.id) : null,
|
|
176
|
+
label: !!edge.labelBBox
|
|
182
177
|
? {
|
|
183
178
|
bbox: edge.labelBBox,
|
|
184
179
|
// text: edge.labels ? edge.labels.map(l => l.text).join('\n') : ''
|
|
185
|
-
text: edge.label
|
|
180
|
+
text: edge.label ?? ''
|
|
186
181
|
}
|
|
187
182
|
: null
|
|
188
183
|
},
|