@likec4/diagram 1.0.0-next.2 → 1.0.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 -2
- package/dist/LikeC4Diagram.props.d.ts +24 -25
- package/dist/StaticLikeC4Diagram.d.ts +1 -2
- package/dist/globals.css.d.ts +1 -0
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/useSetState.d.ts +3 -0
- package/dist/hooks/useUpdateEffect.d.ts +4 -0
- package/dist/icons/Clipboard.d.ts +0 -1
- package/dist/icons/Link.d.ts +0 -1
- package/dist/icons/ZoomIn.d.ts +0 -1
- package/dist/icons/index.d.ts +0 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.mjs +9388 -0
- package/dist/mantine/EnsureMantine.d.ts +2 -4
- package/dist/mantine/theme.d.ts +3 -0
- package/dist/mantine.css.d.ts +1 -0
- package/dist/state/DiagramContext.d.ts +386 -0
- package/dist/state/WhenInitialized.d.ts +3 -0
- package/dist/state/diagramStore.d.ts +428 -0
- package/dist/state/index.d.ts +4 -2
- package/dist/state/useDiagramStore.d.ts +387 -0
- package/dist/style.css +1212 -1
- package/dist/theme.css.d.ts +16 -2
- package/dist/ui/Camera.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.d.ts +0 -1
- package/dist/ui/OptionsPanel.css.d.ts +0 -1
- package/dist/ui/OptionsPanel.d.ts +0 -1
- package/dist/ui/index.d.ts +0 -1
- package/dist/ui/options/NodeOptions.d.ts +0 -1
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -5
- package/dist/xyflow/SyncWithDiagram.d.ts +5 -0
- package/dist/xyflow/XYFlowBackground.d.ts +0 -1
- package/dist/xyflow/XYFlowEvents.d.ts +5 -15
- package/dist/xyflow/XYFlowWrapper.d.ts +4 -5
- package/dist/xyflow/const.d.ts +1 -2
- package/dist/xyflow/diagram-to-xyflow.d.ts +4 -2
- package/dist/xyflow/edges/RelationshipEdge.d.ts +7 -7
- package/dist/xyflow/edges/edges.css.d.ts +2 -1
- package/dist/xyflow/edges/utils.d.ts +3 -4
- package/dist/xyflow/hooks/index.d.ts +2 -37
- package/dist/xyflow/hooks/{use-layout-/321/201onstraints.d.ts → useLayoutConstraints.d.ts} +2 -4
- package/dist/xyflow/hooks/useXYFlow.d.ts +266 -0
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -1
- package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -1
- package/dist/xyflow/nodes/compound/index.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -3
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -3
- package/dist/xyflow/nodes/element/element.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/index.d.ts +1 -2
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -1
- package/dist/xyflow/types.d.ts +20 -8
- package/dist/xyflow/utils.d.ts +0 -1
- package/package.json +36 -34
- package/src/LikeC4Diagram.css.ts +31 -0
- package/src/LikeC4Diagram.props.ts +154 -0
- package/src/LikeC4Diagram.tsx +119 -0
- package/src/StaticLikeC4Diagram.tsx +39 -0
- package/src/globals.css.ts +128 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useSetState.ts +19 -0
- package/src/hooks/useUpdateEffect.ts +28 -0
- package/src/icons/Clipboard.tsx +24 -0
- package/src/icons/Link.tsx +23 -0
- package/src/icons/ZoomIn.tsx +25 -0
- package/src/icons/index.ts +3 -0
- package/src/index.ts +11 -0
- package/src/mantine/EnsureMantine.tsx +14 -0
- package/src/mantine/theme.ts +19 -0
- package/src/mantine.css.ts +4 -0
- package/src/reset.d.ts +2 -0
- package/src/state/DiagramContext.tsx +42 -0
- package/src/state/WhenInitialized.tsx +24 -0
- package/src/state/diagramStore.ts +358 -0
- package/src/state/index.ts +4 -0
- package/src/state/useDiagramStore.ts +39 -0
- package/src/theme.css.ts +64 -0
- package/src/ui/Camera.tsx +174 -0
- package/src/ui/KeepAspectRatio.css.ts +7 -0
- package/src/ui/KeepAspectRatio.tsx +35 -0
- package/src/ui/OptionsPanel.css.ts +10 -0
- package/src/ui/OptionsPanel.tsx +65 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/options/NodeOptions.tsx +253 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +93 -0
- package/src/xyflow/SyncWithDiagram.tsx +113 -0
- package/src/xyflow/XYFlowBackground.tsx +33 -0
- package/src/xyflow/XYFlowEvents.tsx +100 -0
- package/src/xyflow/XYFlowWrapper.tsx +166 -0
- package/src/xyflow/const.ts +7 -0
- package/src/xyflow/diagram-to-xyflow.ts +197 -0
- package/src/xyflow/edges/RelationshipEdge.tsx +288 -0
- package/src/xyflow/edges/edges.css.ts +124 -0
- package/src/xyflow/edges/utils.ts +110 -0
- package/src/xyflow/hooks/index.ts +2 -0
- package/src/xyflow/hooks/useLayoutConstraints.ts +289 -0
- package/src/xyflow/hooks/useXYFlow.ts +20 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +191 -0
- package/src/xyflow/nodes/compound/CompoundNode.tsx +105 -0
- package/src/xyflow/nodes/compound/index.ts +1 -0
- package/src/xyflow/nodes/element/ElementIcon.tsx +44 -0
- package/src/xyflow/nodes/element/ElementLink.css.ts +67 -0
- package/src/xyflow/nodes/element/ElementLink.tsx +94 -0
- package/src/xyflow/nodes/element/ElementNode.tsx +225 -0
- package/src/xyflow/nodes/element/ElementShapeSvg.tsx +181 -0
- package/src/xyflow/nodes/element/element.css.ts +261 -0
- package/src/xyflow/nodes/element/index.ts +1 -0
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +25 -0
- package/src/xyflow/types.ts +100 -0
- package/src/xyflow/utils.ts +12 -0
- package/dist/EmbeddedLikeC4Diagram.css.d.ts +0 -6
- package/dist/EmbeddedLikeC4Diagram.d.ts +0 -18
- package/dist/bundle.d.ts +0 -3
- package/dist/hooks/use-set-state.d.ts +0 -2
- package/dist/hooks/use-update-effect.d.ts +0 -4
- package/dist/index.css.d.ts +0 -2
- package/dist/index.js +0 -13043
- package/dist/mantine/index.d.ts +0 -3
- package/dist/state/DiagramState.d.ts +0 -162
- package/dist/xyflow/UpdateOnDiagramChange.d.ts +0 -10
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useDebouncedEffect, useDebouncedState, useRafEffect } from '@react-hookz/web'
|
|
2
|
+
import { useNodesInitialized } from '@xyflow/react'
|
|
3
|
+
import { shallowEqual } from 'fast-equals'
|
|
4
|
+
import { memo, useEffect, useRef, useState } from 'react'
|
|
5
|
+
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
|
|
6
|
+
import { useXYStore } from './hooks'
|
|
7
|
+
import type { XYFlowState } from './types'
|
|
8
|
+
|
|
9
|
+
function selectDimensions(state: XYFlowState) {
|
|
10
|
+
return `${Math.round(state.width)}:${Math.round(state.height)}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function FitViewOnViewportResize({ diagramApi }: { diagramApi: DiagramStoreApi }) {
|
|
14
|
+
const dimensions = useXYStore(selectDimensions)
|
|
15
|
+
const prevDimensionsRef = useRef(dimensions)
|
|
16
|
+
|
|
17
|
+
useDebouncedEffect(
|
|
18
|
+
() => {
|
|
19
|
+
if (prevDimensionsRef.current === dimensions) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
diagramApi.getState().fitDiagram()
|
|
23
|
+
prevDimensionsRef.current = dimensions
|
|
24
|
+
},
|
|
25
|
+
[dimensions],
|
|
26
|
+
150
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const diagramChangeId = (s: DiagramState) => s.view.id + '_' + s.view.autoLayout + '_' + s.fitViewPadding
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fits the view when the view changes and nodes are initialized
|
|
36
|
+
*/
|
|
37
|
+
function FitViewOnDiagramChanges() {
|
|
38
|
+
const diagramApi = useDiagramStoreApi()
|
|
39
|
+
const [viewportMoved, setViewportMoved] = useState(diagramApi.getState().viewportChanged)
|
|
40
|
+
const [processedChangeId, setProcessed] = useState(diagramChangeId(diagramApi.getState()))
|
|
41
|
+
const [pendingChangeId, setPending] = useDebouncedState(processedChangeId, 100)
|
|
42
|
+
|
|
43
|
+
const isReady = useNodesInitialized({
|
|
44
|
+
includeHiddenNodes: true
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
useEffect(
|
|
48
|
+
() => {
|
|
49
|
+
return diagramApi.subscribe(
|
|
50
|
+
s => ({
|
|
51
|
+
viewportMoved: s.viewportChanged,
|
|
52
|
+
changeid: diagramChangeId(s)
|
|
53
|
+
}),
|
|
54
|
+
({ viewportMoved, changeid }) => {
|
|
55
|
+
setPending(changeid)
|
|
56
|
+
setViewportMoved(viewportMoved)
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
equalityFn: shallowEqual
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
[diagramApi]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
useRafEffect(
|
|
67
|
+
() => {
|
|
68
|
+
if (!isReady || processedChangeId === pendingChangeId) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
diagramApi.getState().fitDiagram()
|
|
72
|
+
setProcessed(pendingChangeId)
|
|
73
|
+
},
|
|
74
|
+
[isReady, processedChangeId, pendingChangeId]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if (!viewportMoved && isReady) {
|
|
78
|
+
return <FitViewOnViewportResize diagramApi={diagramApi} />
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const selectFitViewFlag = (s: DiagramState) => s.fitViewEnabled
|
|
84
|
+
|
|
85
|
+
export const FitViewOnDiagramChange = memo(() => {
|
|
86
|
+
const fitView = useDiagramState(selectFitViewFlag)
|
|
87
|
+
|
|
88
|
+
if (!fitView) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return <FitViewOnDiagramChanges />
|
|
93
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
2
|
+
import { useXYFlow, useXYStoreApi } from './hooks'
|
|
3
|
+
import type { XYFlowNode } from './types'
|
|
4
|
+
|
|
5
|
+
import { memo, useEffect } from 'react'
|
|
6
|
+
import { useDiagramStoreApi } from '../state'
|
|
7
|
+
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
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Syncs the diagram state with the XYFlow instance
|
|
15
|
+
*/
|
|
16
|
+
export const SyncWithDiagram = memo(() => {
|
|
17
|
+
const xyflow = useXYFlow()
|
|
18
|
+
const xyflowApi = useXYStoreApi()
|
|
19
|
+
const diagramStoreApi = useDiagramStoreApi()
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
return diagramStoreApi.subscribe(
|
|
23
|
+
// selector
|
|
24
|
+
state => ({
|
|
25
|
+
viewId: state.view.id,
|
|
26
|
+
nodes: state.view.nodes,
|
|
27
|
+
edges: state.view.edges,
|
|
28
|
+
draggable: state.nodesDraggable,
|
|
29
|
+
selectable: state.nodesSelectable
|
|
30
|
+
}),
|
|
31
|
+
// listener
|
|
32
|
+
({ viewId, nodes, edges, ...opts }) => {
|
|
33
|
+
const { lastOnNavigate } = diagramStoreApi.getState()
|
|
34
|
+
const updates = diagramViewToXYFlowData({ nodes, edges }, opts)
|
|
35
|
+
|
|
36
|
+
xyflow.setNodes(prev =>
|
|
37
|
+
updates.nodes.map(update => {
|
|
38
|
+
const existing = prev.find(isSameNode(update))
|
|
39
|
+
if (existing) {
|
|
40
|
+
if (eq(existing.data.element, update.data.element)) {
|
|
41
|
+
return existing
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...existing,
|
|
45
|
+
...update
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return update
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
xyflow.setEdges(prev =>
|
|
53
|
+
updates.edges.map(update => {
|
|
54
|
+
const existing = prev.find(e => e.id === update.id)
|
|
55
|
+
if (existing) {
|
|
56
|
+
return eq(existing.data, update.data) ? existing : {
|
|
57
|
+
...existing,
|
|
58
|
+
...update
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return update
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (lastOnNavigate?.toView === viewId && !lastOnNavigate.positionCorrected) {
|
|
66
|
+
const elFrom = lastOnNavigate.element
|
|
67
|
+
const elTo = nodes.find(n => n.id === elFrom.id)
|
|
68
|
+
if (elTo) {
|
|
69
|
+
const centerFrom = lastOnNavigate.elementCenterScreenPosition
|
|
70
|
+
const nextZoom = Math.min(elFrom.width / elTo.width, elFrom.height / elTo.height)
|
|
71
|
+
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
requestAnimationFrame(() => {
|
|
74
|
+
const v = xyflow.getViewport()
|
|
75
|
+
if (nextZoom < v.zoom) {
|
|
76
|
+
xyflow.setViewport({
|
|
77
|
+
x: v.x,
|
|
78
|
+
y: v.y,
|
|
79
|
+
zoom: nextZoom
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
const centerTo = xyflow.flowToScreenPosition({
|
|
83
|
+
x: elTo.position[0] + elTo.width / 2,
|
|
84
|
+
y: elTo.position[1] + elTo.height / 2
|
|
85
|
+
}),
|
|
86
|
+
diff = {
|
|
87
|
+
x: toDomPrecision(centerFrom.x - centerTo.x),
|
|
88
|
+
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
89
|
+
}
|
|
90
|
+
xyflowApi.getState().panBy(diff)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
diagramStoreApi.setState(
|
|
95
|
+
{
|
|
96
|
+
lastOnNavigate: {
|
|
97
|
+
...lastOnNavigate,
|
|
98
|
+
positionCorrected: true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
false,
|
|
102
|
+
'positionCorrected'
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
equalityFn: shallowEqual
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
}, [xyflow, xyflowApi, diagramStoreApi])
|
|
111
|
+
|
|
112
|
+
return null
|
|
113
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { nonexhaustive } from '@likec4/core'
|
|
2
|
+
import { Background, type BackgroundProps, BackgroundVariant } from '@xyflow/react'
|
|
3
|
+
import { deepEqual } from 'fast-equals'
|
|
4
|
+
import { memo } from 'react'
|
|
5
|
+
import type { OverrideProperties } from 'type-fest'
|
|
6
|
+
|
|
7
|
+
export type XYBackgroundVariant = 'dots' | 'lines' | 'cross'
|
|
8
|
+
export type XYBackgroundProps =
|
|
9
|
+
| XYBackgroundVariant
|
|
10
|
+
| OverrideProperties<BackgroundProps, {
|
|
11
|
+
variant: XYBackgroundVariant
|
|
12
|
+
}>
|
|
13
|
+
|
|
14
|
+
function literalToEnum(value: XYBackgroundVariant): BackgroundVariant {
|
|
15
|
+
switch (value) {
|
|
16
|
+
case 'dots':
|
|
17
|
+
return BackgroundVariant.Dots
|
|
18
|
+
case 'lines':
|
|
19
|
+
return BackgroundVariant.Lines
|
|
20
|
+
case 'cross':
|
|
21
|
+
return BackgroundVariant.Cross
|
|
22
|
+
default:
|
|
23
|
+
nonexhaustive(value)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const XYFlowBackground = memo<{ background: XYBackgroundProps }>(({ background }) => {
|
|
28
|
+
if (typeof background === 'string') {
|
|
29
|
+
return <Background variant={literalToEnum(background)} />
|
|
30
|
+
}
|
|
31
|
+
const { variant, ...rest } = background
|
|
32
|
+
return <Background variant={literalToEnum(variant)} {...rest} />
|
|
33
|
+
}, deepEqual)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { EdgeMouseHandler, NodeMouseHandler, OnMoveEnd } from '@xyflow/react'
|
|
2
|
+
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
3
|
+
import { useMemo, useRef } from 'react'
|
|
4
|
+
import { useDiagramStoreApi } from '../state'
|
|
5
|
+
import { useXYStoreApi } from './hooks'
|
|
6
|
+
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
7
|
+
|
|
8
|
+
type XYFlowEventHandlers = {
|
|
9
|
+
onPaneClick: (event: ReactMouseEvent) => void
|
|
10
|
+
onNodeContextMenu: NodeMouseHandler<XYFlowNode>
|
|
11
|
+
onEdgeContextMenu: EdgeMouseHandler<XYFlowEdge>
|
|
12
|
+
onPaneContextMenu: (event: ReactMouseEvent | MouseEvent) => void
|
|
13
|
+
onNodeClick: NodeMouseHandler<XYFlowNode>
|
|
14
|
+
onEdgeClick: EdgeMouseHandler<XYFlowEdge>
|
|
15
|
+
// onMoveStart: OnMoveStart
|
|
16
|
+
onMoveEnd: OnMoveEnd
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useXYFlowEvents() {
|
|
20
|
+
const diagramApi = useDiagramStoreApi()
|
|
21
|
+
const xyflowApi = useXYStoreApi()
|
|
22
|
+
|
|
23
|
+
const dblclickTimeout = useRef<number>()
|
|
24
|
+
|
|
25
|
+
return useMemo<XYFlowEventHandlers>(() => ({
|
|
26
|
+
onPaneClick: (event) => {
|
|
27
|
+
diagramApi.setState(
|
|
28
|
+
{
|
|
29
|
+
lastClickedNodeId: null,
|
|
30
|
+
lastClickedEdgeId: null
|
|
31
|
+
},
|
|
32
|
+
false,
|
|
33
|
+
'onPaneClick'
|
|
34
|
+
)
|
|
35
|
+
const diagramState = diagramApi.getState()
|
|
36
|
+
|
|
37
|
+
if (dblclickTimeout.current) {
|
|
38
|
+
window.clearTimeout(dblclickTimeout.current)
|
|
39
|
+
dblclickTimeout.current = undefined
|
|
40
|
+
if (diagramState.onCanvasDblClick) {
|
|
41
|
+
diagramState.onCanvasDblClick(event)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
diagramState.onCanvasClick?.(event)
|
|
45
|
+
if (diagramState.fitViewEnabled && diagramState.viewportChanged) {
|
|
46
|
+
diagramState.fitDiagram()
|
|
47
|
+
}
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
dblclickTimeout.current = window.setTimeout(() => {
|
|
52
|
+
dblclickTimeout.current = undefined
|
|
53
|
+
diagramApi.getState().onCanvasClick?.(event)
|
|
54
|
+
}, 300)
|
|
55
|
+
},
|
|
56
|
+
onNodeContextMenu: (event, xynode) => {
|
|
57
|
+
diagramApi.getState().setLastClickedNode(xynode.id)
|
|
58
|
+
diagramApi.getState().onNodeContextMenu?.({
|
|
59
|
+
element: xynode.data.element,
|
|
60
|
+
xynode,
|
|
61
|
+
event
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
onPaneContextMenu: (event) => {
|
|
65
|
+
diagramApi.getState().setLastClickedNode(null)
|
|
66
|
+
diagramApi.getState().setLastClickedEdge(null)
|
|
67
|
+
diagramApi.getState().onCanvasContextMenu?.(event as any)
|
|
68
|
+
},
|
|
69
|
+
onEdgeContextMenu: (event, xyedge) => {
|
|
70
|
+
diagramApi.getState().setLastClickedEdge(xyedge.id)
|
|
71
|
+
diagramApi.getState().onEdgeContextMenu?.({
|
|
72
|
+
relation: xyedge.data.edge,
|
|
73
|
+
xyedge,
|
|
74
|
+
event
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
onNodeClick: (event, xynode) => {
|
|
78
|
+
diagramApi.getState().setLastClickedNode(xynode.id)
|
|
79
|
+
diagramApi.getState().onNodeClick?.({
|
|
80
|
+
element: xynode.data.element,
|
|
81
|
+
xynode,
|
|
82
|
+
event
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
onEdgeClick: (event, xyedge) => {
|
|
86
|
+
diagramApi.getState().setLastClickedEdge(xyedge.id)
|
|
87
|
+
diagramApi.getState().onEdgeClick?.({
|
|
88
|
+
relation: xyedge.data.edge,
|
|
89
|
+
xyedge,
|
|
90
|
+
event
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
onMoveEnd: (event, _viewport) => {
|
|
94
|
+
const viewportChanged = !!event
|
|
95
|
+
if (viewportChanged !== diagramApi.getState().viewportChanged) {
|
|
96
|
+
diagramApi.setState({ viewportChanged }, false, `viewport-changed: ${viewportChanged}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}), [diagramApi, xyflowApi])
|
|
100
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useMantineColorScheme } from '@mantine/core'
|
|
2
|
+
import { Controls, ReactFlow } from '@xyflow/react'
|
|
3
|
+
import { type CSSProperties, memo, type PropsWithChildren } from 'react'
|
|
4
|
+
import type { SetNonNullable, Simplify } from 'type-fest'
|
|
5
|
+
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
6
|
+
import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
|
|
7
|
+
import { MinZoom } from './const'
|
|
8
|
+
import { RelationshipEdge } from './edges/RelationshipEdge'
|
|
9
|
+
import { useLayoutConstraints } from './hooks/useLayoutConstraints'
|
|
10
|
+
import { CompoundNode } from './nodes/compound'
|
|
11
|
+
import { ElementNode } from './nodes/element'
|
|
12
|
+
import { XYFlowEdge, XYFlowNode } from './types'
|
|
13
|
+
import { XYFlowBackground } from './XYFlowBackground'
|
|
14
|
+
import { useXYFlowEvents } from './XYFlowEvents'
|
|
15
|
+
|
|
16
|
+
const nodeTypes = {
|
|
17
|
+
element: ElementNode,
|
|
18
|
+
compound: CompoundNode
|
|
19
|
+
}
|
|
20
|
+
const edgeTypes = {
|
|
21
|
+
relationship: RelationshipEdge
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type OnlyExpectedProps = Required<
|
|
25
|
+
Pick<
|
|
26
|
+
LikeC4DiagramProperties,
|
|
27
|
+
| 'className'
|
|
28
|
+
| 'controls'
|
|
29
|
+
| 'pannable'
|
|
30
|
+
| 'zoomable'
|
|
31
|
+
| 'background'
|
|
32
|
+
>
|
|
33
|
+
>
|
|
34
|
+
|
|
35
|
+
type XYFlowWrapperProps = Simplify<
|
|
36
|
+
PropsWithChildren<
|
|
37
|
+
SetNonNullable<OnlyExpectedProps> & {
|
|
38
|
+
defaultNodes: XYFlowNode[]
|
|
39
|
+
defaultEdges: XYFlowEdge[]
|
|
40
|
+
style?: CSSProperties | undefined
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
>
|
|
44
|
+
|
|
45
|
+
const selector = (s: DiagramState) => ({
|
|
46
|
+
nodesSelectable: s.nodesSelectable,
|
|
47
|
+
nodesDraggable: s.nodesDraggable,
|
|
48
|
+
fitView: s.fitViewEnabled,
|
|
49
|
+
fitViewPadding: s.fitViewPadding,
|
|
50
|
+
hasOnNavigateTo: !!s.onNavigateTo,
|
|
51
|
+
hasOnNodeClick: !!s.onNodeClick,
|
|
52
|
+
hasOnNodeContextMenu: !!s.onNodeContextMenu,
|
|
53
|
+
hasOnCanvasContextMenu: !!s.onCanvasContextMenu,
|
|
54
|
+
hasOnEdgeContextMenu: !!s.onEdgeContextMenu,
|
|
55
|
+
hasOnEdgeClick: !!s.onEdgeClick,
|
|
56
|
+
hasOnCanvasClick: !!s.onCanvasClick || !!s.onCanvasDblClick
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
function XYFlowWrapper({
|
|
60
|
+
className,
|
|
61
|
+
children,
|
|
62
|
+
defaultNodes,
|
|
63
|
+
defaultEdges,
|
|
64
|
+
pannable,
|
|
65
|
+
zoomable,
|
|
66
|
+
controls,
|
|
67
|
+
background,
|
|
68
|
+
style
|
|
69
|
+
}: XYFlowWrapperProps) {
|
|
70
|
+
const diagramApi = useDiagramStoreApi()
|
|
71
|
+
const {
|
|
72
|
+
nodesSelectable,
|
|
73
|
+
nodesDraggable,
|
|
74
|
+
fitView,
|
|
75
|
+
fitViewPadding,
|
|
76
|
+
...editor
|
|
77
|
+
} = useDiagramState(selector)
|
|
78
|
+
|
|
79
|
+
const layoutConstraints = useLayoutConstraints()
|
|
80
|
+
|
|
81
|
+
const handlers = useXYFlowEvents()
|
|
82
|
+
const isBgWithPattern = background !== 'transparent' && background !== 'solid'
|
|
83
|
+
|
|
84
|
+
const { colorScheme } = useMantineColorScheme()
|
|
85
|
+
const colorMode = colorScheme !== 'auto' ? colorScheme : undefined
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ReactFlow<XYFlowNode, XYFlowEdge>
|
|
89
|
+
className={className}
|
|
90
|
+
style={style}
|
|
91
|
+
{...colorMode && { colorMode }}
|
|
92
|
+
defaultNodes={defaultNodes}
|
|
93
|
+
defaultEdges={defaultEdges}
|
|
94
|
+
nodeTypes={nodeTypes}
|
|
95
|
+
edgeTypes={edgeTypes}
|
|
96
|
+
zoomOnPinch={zoomable}
|
|
97
|
+
zoomOnScroll={!pannable && zoomable}
|
|
98
|
+
{...(!zoomable && {
|
|
99
|
+
zoomActivationKeyCode: null
|
|
100
|
+
})}
|
|
101
|
+
maxZoom={zoomable ? 1.9 : 1}
|
|
102
|
+
minZoom={zoomable ? MinZoom : 1}
|
|
103
|
+
fitView={fitView}
|
|
104
|
+
fitViewOptions={{
|
|
105
|
+
minZoom: MinZoom,
|
|
106
|
+
maxZoom: 1,
|
|
107
|
+
padding: fitViewPadding,
|
|
108
|
+
includeHiddenNodes: true
|
|
109
|
+
}}
|
|
110
|
+
preventScrolling={zoomable || pannable}
|
|
111
|
+
defaultMarkerColor="var(--xy-edge-stroke)"
|
|
112
|
+
noDragClassName="nodrag"
|
|
113
|
+
noPanClassName="nopan"
|
|
114
|
+
panOnScroll={pannable}
|
|
115
|
+
panOnDrag={pannable}
|
|
116
|
+
elementsSelectable={nodesSelectable}
|
|
117
|
+
nodesFocusable={nodesDraggable || nodesSelectable || editor.hasOnNodeClick || editor.hasOnNavigateTo}
|
|
118
|
+
edgesFocusable={editor.hasOnEdgeClick}
|
|
119
|
+
{...(!nodesSelectable && {
|
|
120
|
+
selectionKeyCode: null
|
|
121
|
+
})}
|
|
122
|
+
nodesDraggable={nodesDraggable}
|
|
123
|
+
{...(nodesDraggable && layoutConstraints)}
|
|
124
|
+
zoomOnDoubleClick={false}
|
|
125
|
+
elevateNodesOnSelect={false} // or edges are not visible after select
|
|
126
|
+
selectNodesOnDrag={false} // or weird camera movement
|
|
127
|
+
onPaneClick={handlers.onPaneClick}
|
|
128
|
+
{...(editor.hasOnNodeClick && {
|
|
129
|
+
onNodeClick: handlers.onNodeClick
|
|
130
|
+
})}
|
|
131
|
+
{...(editor.hasOnEdgeClick && {
|
|
132
|
+
onEdgeClick: handlers.onEdgeClick
|
|
133
|
+
})}
|
|
134
|
+
onMoveEnd={handlers.onMoveEnd}
|
|
135
|
+
onInit={() => {
|
|
136
|
+
diagramApi.setState({ initialized: true }, false, 'initialized')
|
|
137
|
+
}}
|
|
138
|
+
onNodeMouseEnter={(_event, node) => {
|
|
139
|
+
diagramApi.getState().setHoveredNode(node.id)
|
|
140
|
+
}}
|
|
141
|
+
onNodeMouseLeave={() => {
|
|
142
|
+
diagramApi.getState().setHoveredNode(null)
|
|
143
|
+
}}
|
|
144
|
+
onEdgeMouseEnter={(_event, edge) => {
|
|
145
|
+
diagramApi.getState().setHoveredEdge(edge.id)
|
|
146
|
+
}}
|
|
147
|
+
onEdgeMouseLeave={() => {
|
|
148
|
+
diagramApi.getState().setHoveredEdge(null)
|
|
149
|
+
}}
|
|
150
|
+
{...(editor.hasOnNodeContextMenu && {
|
|
151
|
+
onNodeContextMenu: handlers.onNodeContextMenu
|
|
152
|
+
})}
|
|
153
|
+
{...(editor.hasOnEdgeContextMenu && {
|
|
154
|
+
onEdgeContextMenu: handlers.onEdgeContextMenu
|
|
155
|
+
})}
|
|
156
|
+
{...(editor.hasOnCanvasContextMenu && {
|
|
157
|
+
onPaneContextMenu: handlers.onPaneContextMenu
|
|
158
|
+
})}>
|
|
159
|
+
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
160
|
+
{controls && <Controls />}
|
|
161
|
+
{children}
|
|
162
|
+
</ReactFlow>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const XYFlow = memo(XYFlowWrapper) as typeof XYFlowWrapper
|