@likec4/diagram 1.7.1 → 1.7.3
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.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +38 -22
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/globals.css.d.ts +0 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/{state/useDiagramStore.d.ts → hooks/useDiagramState.d.ts} +2 -3
- package/dist/{xyflow/hooks → hooks}/useXYFlow.d.ts +10 -10
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +755 -401
- package/dist/state/DiagramContext.d.ts +74 -30
- package/dist/state/diagram-to-xyflow.d.ts +2 -0
- package/dist/state/diagramStore.d.ts +71 -22
- package/dist/style.css +655 -35
- package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +3 -0
- package/dist/ui/top-left/TopLeftPanel.d.ts +4 -0
- package/dist/ui/top-left/_shared.d.ts +5 -0
- package/dist/ui/top-left/styles.css.d.ts +3 -0
- package/dist/xyflow/XYFlowInner.d.ts +0 -1
- package/dist/xyflow/edges/edges.css.d.ts +1 -1
- package/dist/xyflow/{hooks/useLayoutConstraints.d.ts → useLayoutConstraints.d.ts} +1 -1
- package/dist/xyflow/utils.d.ts +11 -1
- package/package.json +6 -6
- package/src/LikeC4Diagram.props.ts +66 -32
- package/src/LikeC4Diagram.tsx +19 -12
- package/src/StaticLikeC4Diagram.tsx +2 -0
- package/src/globals.css.ts +0 -2
- package/src/hooks/index.ts +2 -0
- package/src/{state/useDiagramStore.ts → hooks/useDiagramState.ts} +3 -4
- package/src/{xyflow/hooks → hooks}/useXYFlow.ts +1 -1
- package/src/index.ts +4 -2
- package/src/state/DiagramContext.tsx +15 -7
- package/src/state/diagram-to-xyflow.ts +17 -3
- package/src/state/diagramStore.ts +73 -38
- package/src/ui/DiagramTitlePanel.tsx +1 -1
- package/src/ui/DynamicViewWalkthrough.tsx +1 -2
- package/src/ui/OptionsPanel.tsx +1 -1
- package/src/ui/options/EdgeOptions.tsx +1 -16
- package/src/ui/options/NodeOptions.tsx +38 -13
- package/src/ui/top-left/ChangeAutoLayoutButton.tsx +131 -0
- package/src/ui/top-left/TopLeftPanel.tsx +213 -0
- package/src/ui/top-left/_shared.ts +19 -0
- package/src/ui/top-left/styles.css.ts +38 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +2 -2
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +87 -59
- package/src/xyflow/XYFlow.tsx +7 -8
- package/src/xyflow/XYFlowEvents.tsx +39 -19
- package/src/xyflow/XYFlowInner.tsx +5 -7
- package/src/xyflow/edges/RelationshipEdge.tsx +60 -27
- package/src/xyflow/edges/edges.css.ts +29 -25
- package/src/xyflow/nodes/compound/CompoundNode.tsx +1 -1
- package/src/xyflow/nodes/element/ElementLink.tsx +1 -2
- package/src/xyflow/nodes/element/ElementNode.tsx +7 -5
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
- package/src/xyflow/{hooks/useLayoutConstraints.ts → useLayoutConstraints.ts} +3 -3
- package/src/xyflow/utils.ts +25 -1
- package/dist/state/hooks.d.ts +0 -4
- package/dist/state/index.d.ts +0 -2
- package/dist/state/useNavigationHistory.d.ts +0 -5
- package/dist/types.d.ts +0 -20
- package/dist/ui/BackwardForwardPanel.css.d.ts +0 -1
- package/dist/ui/BackwardForwardPanel.d.ts +0 -1
- package/dist/xyflow/hooks/index.d.ts +0 -2
- package/src/state/hooks.ts +0 -5
- package/src/state/index.ts +0 -2
- package/src/state/useNavigationHistory.ts +0 -39
- package/src/types.ts +0 -22
- package/src/ui/BackwardForwardPanel.css.ts +0 -22
- package/src/ui/BackwardForwardPanel.tsx +0 -76
- package/src/xyflow/hooks/index.ts +0 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Code,
|
|
3
|
+
Group,
|
|
4
|
+
HoverCard,
|
|
5
|
+
HoverCardDropdown,
|
|
6
|
+
HoverCardTarget,
|
|
7
|
+
Notification,
|
|
8
|
+
type PopoverProps,
|
|
9
|
+
Stack,
|
|
10
|
+
Text
|
|
11
|
+
} from '@mantine/core'
|
|
12
|
+
import {
|
|
13
|
+
IconAlertTriangle,
|
|
14
|
+
IconChevronLeft,
|
|
15
|
+
IconChevronRight,
|
|
16
|
+
IconFileSymlink,
|
|
17
|
+
IconFocusCentered,
|
|
18
|
+
IconMenu2
|
|
19
|
+
} from '@tabler/icons-react'
|
|
20
|
+
import clsx from 'clsx'
|
|
21
|
+
import { AnimatePresence, m } from 'framer-motion'
|
|
22
|
+
import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
|
|
23
|
+
import { ActionIcon, Tooltip } from './_shared'
|
|
24
|
+
import { ChangeAutoLayoutButton } from './ChangeAutoLayoutButton'
|
|
25
|
+
import * as css from './styles.css'
|
|
26
|
+
|
|
27
|
+
const historySelector = (s: DiagramState) => ({
|
|
28
|
+
showBurgerMenu: !!s.onBurgerMenuClick,
|
|
29
|
+
hasStepBack: s.navigationHistoryIndex > 0,
|
|
30
|
+
hasStepForward: s.navigationHistoryIndex < s.navigationHistory.length - 1
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const BackwardForwardButtons = () => {
|
|
34
|
+
const store = useDiagramStoreApi()
|
|
35
|
+
const {
|
|
36
|
+
showBurgerMenu,
|
|
37
|
+
hasStepBack,
|
|
38
|
+
hasStepForward
|
|
39
|
+
} = useDiagramState(historySelector)
|
|
40
|
+
return (
|
|
41
|
+
<Group
|
|
42
|
+
className={'likec4-navigation-panel'}
|
|
43
|
+
gap={'xs'}>
|
|
44
|
+
{showBurgerMenu && (
|
|
45
|
+
<ActionIcon
|
|
46
|
+
onClick={e => {
|
|
47
|
+
e.stopPropagation()
|
|
48
|
+
store.getState().onBurgerMenuClick?.()
|
|
49
|
+
}}>
|
|
50
|
+
<IconMenu2 />
|
|
51
|
+
</ActionIcon>
|
|
52
|
+
)}
|
|
53
|
+
<AnimatePresence>
|
|
54
|
+
{hasStepBack && (
|
|
55
|
+
<m.div
|
|
56
|
+
layout
|
|
57
|
+
initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
|
|
58
|
+
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
59
|
+
exit={{
|
|
60
|
+
opacity: 0.05,
|
|
61
|
+
transform: 'translateX(-10px)'
|
|
62
|
+
}}
|
|
63
|
+
key={'back'}>
|
|
64
|
+
<ActionIcon
|
|
65
|
+
onClick={e => {
|
|
66
|
+
e.stopPropagation()
|
|
67
|
+
store.getState().goBack()
|
|
68
|
+
}}>
|
|
69
|
+
<IconChevronLeft />
|
|
70
|
+
</ActionIcon>
|
|
71
|
+
</m.div>
|
|
72
|
+
)}
|
|
73
|
+
{hasStepForward && (
|
|
74
|
+
<m.div
|
|
75
|
+
layout
|
|
76
|
+
initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
|
|
77
|
+
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
78
|
+
exit={{
|
|
79
|
+
opacity: 0,
|
|
80
|
+
transform: 'translateX(10px)'
|
|
81
|
+
}}
|
|
82
|
+
key={'forward'}>
|
|
83
|
+
<ActionIcon
|
|
84
|
+
onClick={e => {
|
|
85
|
+
e.stopPropagation()
|
|
86
|
+
store.getState().goForward()
|
|
87
|
+
}}>
|
|
88
|
+
<IconChevronRight />
|
|
89
|
+
</ActionIcon>
|
|
90
|
+
</m.div>
|
|
91
|
+
)}
|
|
92
|
+
</AnimatePresence>
|
|
93
|
+
</Group>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const LayoutDriftNotification = (props: PopoverProps) => (
|
|
98
|
+
<HoverCard
|
|
99
|
+
position="right-start"
|
|
100
|
+
openDelay={200}
|
|
101
|
+
closeDelay={100}
|
|
102
|
+
{...props}>
|
|
103
|
+
<HoverCardTarget>
|
|
104
|
+
<ActionIcon
|
|
105
|
+
className="action-icon"
|
|
106
|
+
variant="light"
|
|
107
|
+
color="orange">
|
|
108
|
+
<IconAlertTriangle />
|
|
109
|
+
</ActionIcon>
|
|
110
|
+
</HoverCardTarget>
|
|
111
|
+
<HoverCardDropdown p={'0'}>
|
|
112
|
+
<Notification
|
|
113
|
+
color="orange"
|
|
114
|
+
withBorder={false}
|
|
115
|
+
withCloseButton={false}
|
|
116
|
+
title="Manual layout issues">
|
|
117
|
+
<Text mt={2} size="sm" lh="xs">
|
|
118
|
+
View contains new elements or their sizes have changed,<br />
|
|
119
|
+
last manual layout can not be applied.
|
|
120
|
+
</Text>
|
|
121
|
+
<Text mt={'xs'} size="sm" lh="xs">
|
|
122
|
+
Update view predicates or remove <Code>@likec4-generated</Code>
|
|
123
|
+
</Text>
|
|
124
|
+
</Notification>
|
|
125
|
+
</HoverCardDropdown>
|
|
126
|
+
</HoverCard>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
export const TopLeftPanel = () => {
|
|
130
|
+
const store = useDiagramStoreApi()
|
|
131
|
+
const {
|
|
132
|
+
showNavigationButtons,
|
|
133
|
+
showFitDiagram,
|
|
134
|
+
showLayoutDriftWarning,
|
|
135
|
+
showChangeAutoLayout,
|
|
136
|
+
portalProps: _portalProps,
|
|
137
|
+
showGoToSource
|
|
138
|
+
} = useDiagramState(s => {
|
|
139
|
+
const isNotActive = s.activeDynamicViewStep === null && s.focusedNodeId === null
|
|
140
|
+
const target = s.getContainer()
|
|
141
|
+
return ({
|
|
142
|
+
showNavigationButtons: !!s.onBurgerMenuClick || s.showNavigationButtons && !!s.onNavigateTo,
|
|
143
|
+
showFitDiagram: s.fitViewEnabled && s.zoomable && s.viewportChanged,
|
|
144
|
+
showLayoutDriftWarning: s.readonly !== true && s.view.hasLayoutDrift === true && isNotActive,
|
|
145
|
+
showChangeAutoLayout: s.readonly !== true && !!s.onChange && isNotActive,
|
|
146
|
+
showGoToSource: !!s.onOpenSourceView,
|
|
147
|
+
portalProps: target ? { target } : undefined
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
const portalProps = _portalProps ? { portalProps: _portalProps } : undefined
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Stack
|
|
154
|
+
className={clsx(
|
|
155
|
+
'react-flow__panel',
|
|
156
|
+
css.panel,
|
|
157
|
+
'likec4-top-left-panel'
|
|
158
|
+
)}
|
|
159
|
+
onClick={e => e.stopPropagation()}
|
|
160
|
+
gap={'xs'}>
|
|
161
|
+
{showNavigationButtons && <BackwardForwardButtons />}
|
|
162
|
+
{showGoToSource && (
|
|
163
|
+
<Tooltip label="Go to source" {...portalProps}>
|
|
164
|
+
<ActionIcon
|
|
165
|
+
className="action-icon"
|
|
166
|
+
onClick={e => {
|
|
167
|
+
e.stopPropagation()
|
|
168
|
+
store.getState().onOpenSourceView?.()
|
|
169
|
+
}}>
|
|
170
|
+
<IconFileSymlink stroke={1.5} />
|
|
171
|
+
</ActionIcon>
|
|
172
|
+
</Tooltip>
|
|
173
|
+
)}
|
|
174
|
+
{showChangeAutoLayout && <ChangeAutoLayoutButton {...portalProps} />}
|
|
175
|
+
<AnimatePresence>
|
|
176
|
+
{showLayoutDriftWarning && (
|
|
177
|
+
<m.div
|
|
178
|
+
initial={{ opacity: 0.05, transform: 'translateX(-40%)' }}
|
|
179
|
+
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
180
|
+
exit={{
|
|
181
|
+
opacity: 0,
|
|
182
|
+
transform: 'translateX(-40%)'
|
|
183
|
+
}}
|
|
184
|
+
key={'layout'}>
|
|
185
|
+
<LayoutDriftNotification {...portalProps} />
|
|
186
|
+
</m.div>
|
|
187
|
+
)}
|
|
188
|
+
{showFitDiagram && (
|
|
189
|
+
<m.div
|
|
190
|
+
initial={{ opacity: 0.05, transform: 'translateX(-20%)' }}
|
|
191
|
+
animate={{ opacity: 1, transform: 'translateX(0)' }}
|
|
192
|
+
exit={{
|
|
193
|
+
opacity: 0,
|
|
194
|
+
transform: 'translateX(-30%)'
|
|
195
|
+
}}
|
|
196
|
+
key={'fit-view'}>
|
|
197
|
+
<Tooltip label="Center diagram" {...portalProps}>
|
|
198
|
+
<ActionIcon
|
|
199
|
+
className="action-icon"
|
|
200
|
+
onClick={e => {
|
|
201
|
+
e.stopPropagation()
|
|
202
|
+
store.getState().fitDiagram()
|
|
203
|
+
}}>
|
|
204
|
+
<IconFocusCentered />
|
|
205
|
+
</ActionIcon>
|
|
206
|
+
</Tooltip>
|
|
207
|
+
</m.div>
|
|
208
|
+
)}
|
|
209
|
+
</AnimatePresence>
|
|
210
|
+
</Stack>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
TopLeftPanel.displayName = 'TopLeftPanel'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ActionIcon as MantineActionIcon, Tooltip as MantineTooltip } from '@mantine/core'
|
|
2
|
+
|
|
3
|
+
export const Tooltip = MantineTooltip.withProps({
|
|
4
|
+
color: 'gray',
|
|
5
|
+
fz: 'xs',
|
|
6
|
+
position: 'right',
|
|
7
|
+
openDelay: 400,
|
|
8
|
+
closeDelay: 100,
|
|
9
|
+
label: '',
|
|
10
|
+
children: null,
|
|
11
|
+
offset: 8,
|
|
12
|
+
transitionProps: { transition: 'fade-right', duration: 200 }
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const ActionIcon = MantineActionIcon.withProps({
|
|
16
|
+
className: 'action-icon',
|
|
17
|
+
variant: 'light',
|
|
18
|
+
color: 'gray'
|
|
19
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
|
|
2
|
+
import { mantine } from '../../mantine.css'
|
|
3
|
+
import { vars } from '../../theme.css'
|
|
4
|
+
|
|
5
|
+
export const panel = style({
|
|
6
|
+
top: fallbackVar(vars.navigationPanel.top, '1rem'),
|
|
7
|
+
left: fallbackVar(vars.navigationPanel.left, '1rem'),
|
|
8
|
+
margin: 0
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
globalStyle(`.likec4-top-left-panel .action-icon`, {
|
|
12
|
+
vars: {
|
|
13
|
+
['--ai-size']: 'var(--ai-size-lg)'
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
globalStyle(`.likec4-top-left-panel .action-icon .tabler-icon`, {
|
|
17
|
+
width: '70%',
|
|
18
|
+
height: '70%'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const autolayoutIcon = style({})
|
|
22
|
+
|
|
23
|
+
globalStyle(`${autolayoutIcon} .tabler-icon`, {
|
|
24
|
+
width: '75%',
|
|
25
|
+
height: '75%'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const autolayoutIndicator = style({
|
|
29
|
+
background: mantine.colors.gray[2],
|
|
30
|
+
borderRadius: mantine.radius.sm,
|
|
31
|
+
border: `1px solid ${mantine.colors.gray[3]}`,
|
|
32
|
+
selectors: {
|
|
33
|
+
[`:where([data-mantine-color-scheme="dark"]) &`]: {
|
|
34
|
+
background: mantine.colors.dark[5],
|
|
35
|
+
border: `1px solid ${mantine.colors.dark[4]}`
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useDebouncedEffect } from '@react-hookz/web'
|
|
2
2
|
import { shallowEqual } from 'fast-equals'
|
|
3
3
|
import { useRef, useState } from 'react'
|
|
4
|
-
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../
|
|
5
|
-
import { useXYStore } from '
|
|
4
|
+
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../hooks/useDiagramState'
|
|
5
|
+
import { useXYStore } from '../hooks/useXYFlow'
|
|
6
6
|
|
|
7
7
|
function selectXYFlowSize(state: { width: number; height: number }): string {
|
|
8
8
|
return `${Math.round(state.width)}:${Math.round(state.height)}`
|
|
@@ -1,75 +1,103 @@
|
|
|
1
1
|
import { nonNullable } from '@likec4/core'
|
|
2
|
+
import { useHotkeys } from '@mantine/hooks'
|
|
2
3
|
import type { EdgeChange, NodeChange } from '@xyflow/react'
|
|
3
|
-
import { getBoundsOfRects,
|
|
4
|
-
import {
|
|
5
|
-
import { useDiagramStoreApi } from '../
|
|
6
|
-
import { MinZoom } from './const'
|
|
4
|
+
import { getBoundsOfRects, getViewportForBounds } from '@xyflow/system'
|
|
5
|
+
import { useUpdateEffect } from '../hooks'
|
|
6
|
+
import { useDiagramState, useDiagramStoreApi } from '../hooks/useDiagramState'
|
|
7
7
|
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
8
|
+
import { nodeToRect } from './utils'
|
|
8
9
|
|
|
9
10
|
export function SelectEdgesOnNodeFocus() {
|
|
10
11
|
const diagramStore = useDiagramStoreApi()
|
|
12
|
+
const focusedNodeId = useDiagramState(s => s.focusedNodeId)
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
()
|
|
14
|
-
diagramStore.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
useUpdateEffect(() => {
|
|
15
|
+
if (!focusedNodeId) {
|
|
16
|
+
diagramStore.getState().xystore.getState().resetSelectedElements()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
const container = diagramStore.getState().getContainer()
|
|
20
|
+
if (!container) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const { width, height } = container.getBoundingClientRect()
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
triggerEdgeChanges
|
|
30
|
-
} = diagramStore.getState().xystore.getState()
|
|
31
|
-
let focusBounds = nodeToRect(nonNullable(nodeLookup.get(focusedNodeId)))
|
|
25
|
+
const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
|
|
26
|
+
const nodeChanges = [] as NodeChange<XYFlowNode>[]
|
|
27
|
+
const {
|
|
28
|
+
edgeLookup,
|
|
29
|
+
nodeLookup,
|
|
30
|
+
panZoom,
|
|
31
|
+
transform,
|
|
32
|
+
triggerNodeChanges,
|
|
33
|
+
triggerEdgeChanges
|
|
34
|
+
} = diagramStore.getState().xystore.getState()
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
type: 'select',
|
|
52
|
-
selected: false
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
}
|
|
36
|
+
let focusBounds = nodeToRect(nonNullable(nodeLookup.get(focusedNodeId)))
|
|
37
|
+
|
|
38
|
+
for (const [, edge] of edgeLookup) {
|
|
39
|
+
if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
|
|
40
|
+
const anotherNodeId = edge.source === focusedNodeId ? edge.target : edge.source
|
|
41
|
+
const nd = nonNullable(nodeLookup.get(anotherNodeId))
|
|
42
|
+
focusBounds = getBoundsOfRects(focusBounds, nodeToRect(nd))
|
|
43
|
+
}
|
|
44
|
+
if (edge.selected) {
|
|
45
|
+
edgeChanges.push({
|
|
46
|
+
id: edge.id,
|
|
47
|
+
type: 'select',
|
|
48
|
+
selected: false
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const [, node] of nodeLookup) {
|
|
53
|
+
if (node.selected && node.id !== focusedNodeId) {
|
|
56
54
|
nodeChanges.push({
|
|
57
|
-
id:
|
|
55
|
+
id: node.id,
|
|
58
56
|
type: 'select',
|
|
59
|
-
selected:
|
|
57
|
+
selected: false
|
|
60
58
|
})
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
nodeChanges.push({
|
|
62
|
+
id: focusedNodeId,
|
|
63
|
+
type: 'select',
|
|
64
|
+
selected: true
|
|
65
|
+
})
|
|
66
|
+
if (edgeChanges.length > 0) {
|
|
67
|
+
triggerEdgeChanges(edgeChanges)
|
|
68
|
+
}
|
|
69
|
+
triggerNodeChanges(nodeChanges)
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const maxZoom = Math.max(1, transform[2])
|
|
72
|
+
const viewport = getViewportForBounds(
|
|
73
|
+
{
|
|
74
|
+
x: focusBounds.x - 16,
|
|
75
|
+
y: focusBounds.y - 16,
|
|
76
|
+
width: focusBounds.width + 32,
|
|
77
|
+
height: focusBounds.height + 32
|
|
78
|
+
},
|
|
79
|
+
width,
|
|
80
|
+
height,
|
|
81
|
+
0.1,
|
|
82
|
+
maxZoom,
|
|
83
|
+
0
|
|
84
|
+
)
|
|
85
|
+
panZoom?.setViewport(viewport, {
|
|
86
|
+
duration: 350
|
|
87
|
+
})
|
|
88
|
+
}, [focusedNodeId])
|
|
89
|
+
|
|
90
|
+
useHotkeys(
|
|
91
|
+
focusedNodeId !== null
|
|
92
|
+
? [
|
|
93
|
+
['Escape', (e) => {
|
|
94
|
+
e.stopImmediatePropagation()
|
|
95
|
+
const { xystore, fitDiagram } = diagramStore.getState()
|
|
96
|
+
fitDiagram()
|
|
97
|
+
xystore.getState().resetSelectedElements()
|
|
98
|
+
}, { preventDefault: true }]
|
|
99
|
+
]
|
|
100
|
+
: []
|
|
73
101
|
)
|
|
74
102
|
|
|
75
103
|
return null
|
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { ReactFlow, useOnViewportChange } from '@xyflow/react'
|
|
2
|
-
import {
|
|
3
|
-
import { type CSSProperties, type PropsWithChildren,
|
|
2
|
+
import { shallowEqual } from 'fast-equals'
|
|
3
|
+
import { type CSSProperties, type PropsWithChildren, useMemo } from 'react'
|
|
4
|
+
import { useDiagramState } from '../hooks/useDiagramState'
|
|
5
|
+
import { useXYStoreApi } from '../hooks/useXYFlow'
|
|
4
6
|
import type { DiagramState } from '../state/diagramStore'
|
|
5
|
-
import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
|
|
6
7
|
import { MinZoom } from './const'
|
|
7
8
|
import { RelationshipEdge } from './edges/RelationshipEdge'
|
|
8
|
-
import { useLayoutConstraints } from './hooks/useLayoutConstraints'
|
|
9
|
-
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
10
9
|
import { CompoundNode } from './nodes/compound'
|
|
11
10
|
import { ElementNode } from './nodes/element'
|
|
12
11
|
import { XYFlowEdge, XYFlowNode } from './types'
|
|
12
|
+
import { useLayoutConstraints } from './useLayoutConstraints'
|
|
13
13
|
import { useXYFlowEvents } from './XYFlowEvents'
|
|
14
|
-
// import { useLogger } from '@mantine/hooks'
|
|
15
14
|
|
|
16
15
|
const nodeTypes = {
|
|
17
16
|
element: ElementNode,
|
|
@@ -163,9 +162,9 @@ export function XYFlow({
|
|
|
163
162
|
onNodeDragStop: layoutConstraints.onNodeDragStop
|
|
164
163
|
}}
|
|
165
164
|
nodeDragThreshold={3}
|
|
166
|
-
zoomOnDoubleClick={
|
|
165
|
+
zoomOnDoubleClick={zoomable}
|
|
167
166
|
elevateNodesOnSelect={false} // or edges are not visible after select\
|
|
168
|
-
selectNodesOnDrag={false}
|
|
167
|
+
selectNodesOnDrag={false}
|
|
169
168
|
onNodesChange={onNodesChange}
|
|
170
169
|
onEdgesChange={onEdgesChange}
|
|
171
170
|
onDoubleClick={handlers.onDoubleClick}
|
|
@@ -3,7 +3,7 @@ import type { ReactFlowProps } from '@xyflow/react'
|
|
|
3
3
|
import { useMemo, useRef } from 'react'
|
|
4
4
|
import { isNonNullish, isTruthy } from 'remeda'
|
|
5
5
|
import type { Simplify } from 'type-fest'
|
|
6
|
-
import { useDiagramStoreApi } from '../
|
|
6
|
+
import { useDiagramStoreApi } from '../hooks/useDiagramState'
|
|
7
7
|
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
8
8
|
|
|
9
9
|
export type XYFlowEventHandlers = Simplify<
|
|
@@ -57,18 +57,15 @@ export function useXYFlowEvents() {
|
|
|
57
57
|
return ({
|
|
58
58
|
onDoubleClick: (event) => {
|
|
59
59
|
const {
|
|
60
|
-
fitViewEnabled,
|
|
61
60
|
onCanvasDblClick,
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
xystore,
|
|
62
|
+
resetLastClicked
|
|
64
63
|
} = diagramApi.getState()
|
|
65
|
-
if (fitViewEnabled) {
|
|
66
|
-
fitDiagram()
|
|
67
|
-
if (!onCanvasDblClick) {
|
|
68
|
-
event.stopPropagation()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
64
|
resetLastClicked()
|
|
65
|
+
xystore.getState().resetSelectedElements()
|
|
66
|
+
if (!onCanvasDblClick) {
|
|
67
|
+
event.stopPropagation()
|
|
68
|
+
}
|
|
72
69
|
onCanvasDblClick?.(event)
|
|
73
70
|
},
|
|
74
71
|
onPaneClick: (event) => {
|
|
@@ -80,6 +77,7 @@ export function useXYFlowEvents() {
|
|
|
80
77
|
activeDynamicViewStep,
|
|
81
78
|
fitDiagram,
|
|
82
79
|
onCanvasClick,
|
|
80
|
+
xystore,
|
|
83
81
|
resetLastClicked
|
|
84
82
|
} = diagramApi.getState()
|
|
85
83
|
if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
|
|
@@ -89,6 +87,7 @@ export function useXYFlowEvents() {
|
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
89
|
resetLastClicked()
|
|
90
|
+
xystore.getState().resetSelectedElements()
|
|
92
91
|
onCanvasClick?.(event)
|
|
93
92
|
},
|
|
94
93
|
onNodeContextMenu: (event, xynode) => {
|
|
@@ -117,27 +116,42 @@ export function useXYFlowEvents() {
|
|
|
117
116
|
fitDiagram,
|
|
118
117
|
focusOnNode,
|
|
119
118
|
onNodeClick,
|
|
119
|
+
xystore,
|
|
120
120
|
enableFocusMode,
|
|
121
121
|
lastClickedNodeId,
|
|
122
|
-
|
|
122
|
+
nodesSelectable,
|
|
123
|
+
setLastClickedNode,
|
|
124
|
+
onOpenSourceElement
|
|
123
125
|
} = diagramApi.getState()
|
|
124
126
|
setLastClickedNode(xynode.id)
|
|
125
127
|
// if we focused on a node, and clicked on another node - focus on the clicked node
|
|
126
128
|
const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
|
|
127
129
|
// if user clicked on the same node twice in a short time, focus on it
|
|
128
130
|
const clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
|
|
132
|
+
if (clickedRecently && !!onOpenSourceElement) {
|
|
133
|
+
onOpenSourceElement(xynode.data.element.id)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (enableFocusMode || nodesSelectable) {
|
|
137
|
+
let stopPropagation = true
|
|
131
138
|
switch (true) {
|
|
132
|
-
case !focusedNodeId && clickedRecently:
|
|
133
|
-
case shallChangeFocus:
|
|
134
|
-
stopPropagation = true
|
|
139
|
+
case enableFocusMode && !focusedNodeId && clickedRecently:
|
|
140
|
+
case enableFocusMode && shallChangeFocus: {
|
|
135
141
|
focusOnNode(xynode.id)
|
|
136
142
|
break
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
}
|
|
144
|
+
case enableFocusMode && focusedNodeId === xynode.id && clickedRecently: {
|
|
139
145
|
fitDiagram()
|
|
140
146
|
break
|
|
147
|
+
}
|
|
148
|
+
case nodesSelectable: {
|
|
149
|
+
xystore.getState().addSelectedNodes([xynode.id])
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
default: {
|
|
153
|
+
stopPropagation = false
|
|
154
|
+
}
|
|
141
155
|
}
|
|
142
156
|
if (!onNodeClick && stopPropagation) {
|
|
143
157
|
// user did not provide a custom handler, stop propagation
|
|
@@ -179,6 +193,8 @@ export function useXYFlowEvents() {
|
|
|
179
193
|
activateDynamicStep,
|
|
180
194
|
activeDynamicViewStep,
|
|
181
195
|
focusedNodeId,
|
|
196
|
+
xystore,
|
|
197
|
+
nodesSelectable,
|
|
182
198
|
focusOnNode,
|
|
183
199
|
onEdgeClick,
|
|
184
200
|
setLastClickedEdge
|
|
@@ -204,7 +220,11 @@ export function useXYFlowEvents() {
|
|
|
204
220
|
if (isEdgeOfFocusedNode) {
|
|
205
221
|
focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
|
|
206
222
|
if (!onEdgeClick) {
|
|
207
|
-
|
|
223
|
+
event.stopPropagation()
|
|
224
|
+
}
|
|
225
|
+
} else if (nodesSelectable) {
|
|
226
|
+
xystore.getState().addSelectedEdges([xyedge.id])
|
|
227
|
+
if (!onEdgeClick) {
|
|
208
228
|
event.stopPropagation()
|
|
209
229
|
}
|
|
210
230
|
}
|
|
@@ -2,27 +2,25 @@ import { Controls } from '@xyflow/react'
|
|
|
2
2
|
import { shallowEqual } from 'fast-equals'
|
|
3
3
|
import { memo, type PropsWithChildren } from 'react'
|
|
4
4
|
import { isNonNullish } from 'remeda'
|
|
5
|
+
import { useDiagramState } from '../hooks/useDiagramState'
|
|
5
6
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
6
|
-
import { useDiagramState } from '../state/useDiagramStore'
|
|
7
|
-
import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
|
|
8
7
|
import DiagramTitlePanel from '../ui/DiagramTitlePanel'
|
|
9
8
|
import { DynamicViewWalkthrough } from '../ui/DynamicViewWalkthrough'
|
|
10
9
|
import OptionsPanel from '../ui/OptionsPanel'
|
|
10
|
+
import { TopLeftPanel } from '../ui/top-left/TopLeftPanel'
|
|
11
11
|
import { XYFlowBackground } from './XYFlowBackground'
|
|
12
12
|
|
|
13
13
|
type XYFlowInnerProps = PropsWithChildren<{
|
|
14
14
|
background: NonNullable<LikeC4DiagramProperties['background']>
|
|
15
15
|
controls: boolean
|
|
16
16
|
showDiagramTitle: boolean
|
|
17
|
-
showNavigationButtons: boolean
|
|
18
17
|
}>
|
|
19
18
|
|
|
20
19
|
export const XYFlowInner = memo(function XYFlowInnerR({
|
|
21
20
|
children,
|
|
22
21
|
background,
|
|
23
22
|
controls,
|
|
24
|
-
showDiagramTitle
|
|
25
|
-
showNavigationButtons
|
|
23
|
+
showDiagramTitle
|
|
26
24
|
}: XYFlowInnerProps) {
|
|
27
25
|
const {
|
|
28
26
|
isDynamicView,
|
|
@@ -44,10 +42,10 @@ export const XYFlowInner = memo(function XYFlowInnerR({
|
|
|
44
42
|
<>
|
|
45
43
|
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
46
44
|
{controls && <Controls position={'bottom-right'} />}
|
|
47
|
-
|
|
45
|
+
<TopLeftPanel />
|
|
46
|
+
{readonly !== true && <OptionsPanel />}
|
|
48
47
|
{isDiagramTitleVisible && <DiagramTitlePanel />}
|
|
49
48
|
{isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
|
|
50
|
-
{showNavigationButtons && <BackwardForwardPanel />}
|
|
51
49
|
{children}
|
|
52
50
|
</>
|
|
53
51
|
)
|