@likec4/diagram 1.12.1 → 1.13.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/package.json +9 -9
- package/src/LikeC4CustomColors.tsx +2 -2
- package/src/LikeC4Diagram.props.ts +15 -7
- package/src/LikeC4Diagram.tsx +23 -14
- package/src/StaticLikeC4Diagram.tsx +2 -0
- package/src/components/Overlay.css.ts +78 -0
- package/src/components/Overlay.tsx +102 -0
- package/src/components/OverlayContext.tsx +11 -0
- package/src/components/index.ts +2 -0
- package/src/likec4model/index.ts +1 -0
- package/src/likec4model/useLikeC4ElementsTree.ts +44 -0
- package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +13 -0
- package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +37 -0
- package/src/overlays/index.tsx +33 -0
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +33 -0
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +359 -0
- package/src/overlays/relationships-of/SelectElement.css.ts +18 -0
- package/src/overlays/relationships-of/SelectElement.tsx +133 -0
- package/src/overlays/relationships-of/_types.ts +56 -0
- package/src/overlays/relationships-of/styles.css.ts +34 -0
- package/src/overlays/relationships-of/use-layouted-relationships.ts +607 -0
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +76 -0
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +130 -0
- package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +22 -0
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +59 -0
- package/src/overlays/relationships-of/xyflow/styles.css.ts +170 -0
- package/src/state/diagram-to-xyflow.ts +1 -1
- package/src/state/diagramStore.ts +119 -27
- package/src/ui/top-left/ChangeAutoLayoutButton.tsx +35 -18
- package/src/ui/top-left/ManualLayoutToolsButton.tsx +110 -0
- package/src/ui/top-left/TopLeftPanel.tsx +15 -31
- package/src/ui/top-left/_shared.ts +2 -2
- package/src/xyflow/EdgeMarkers.tsx +19 -0
- package/src/xyflow/XYFlowEvents.tsx +21 -24
- package/src/xyflow/XYFlowInner.tsx +6 -10
- package/src/xyflow/edges/EdgeLabel.tsx +6 -5
- package/src/xyflow/edges/RelationshipEdge.tsx +4 -3
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +16 -0
- package/src/xyflow/edges/edges.css.ts +5 -11
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +17 -40
- package/src/xyflow/nodes/compound/CompoundNode.tsx +219 -28
- package/src/xyflow/nodes/element/ElementLink.tsx +11 -8
- package/src/xyflow/nodes/element/ElementNode.tsx +211 -140
- package/src/xyflow/nodes/element/element.css.ts +22 -33
- package/src/xyflow/nodes/shared/Toolbar.tsx +81 -27
- package/src/xyflow/nodes/use-animate-variants.tsx +64 -0
- package/src/xyflow/useLayoutConstraints.ts +95 -125
- package/src/xyflow/utils.ts +4 -0
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +0 -32
|
@@ -10,10 +10,17 @@ import {
|
|
|
10
10
|
Text,
|
|
11
11
|
UnstyledButton
|
|
12
12
|
} from '@mantine/core'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
clampUseMovePosition,
|
|
15
|
+
useHover,
|
|
16
|
+
useMergedRef,
|
|
17
|
+
useMove,
|
|
18
|
+
type UseMovePosition,
|
|
19
|
+
useUncontrolled
|
|
20
|
+
} from '@mantine/hooks'
|
|
14
21
|
import { useDebouncedCallback } from '@react-hookz/web'
|
|
15
22
|
import { IconLayoutDashboard } from '@tabler/icons-react'
|
|
16
|
-
import { useState } from 'react'
|
|
23
|
+
import { forwardRef, useState } from 'react'
|
|
17
24
|
import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
|
|
18
25
|
import { ActionIcon, Tooltip } from './_shared'
|
|
19
26
|
import * as css from './styles.css'
|
|
@@ -32,6 +39,8 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
|
|
|
32
39
|
viewId
|
|
33
40
|
} = useDiagramState(selector)
|
|
34
41
|
|
|
42
|
+
const { ref, hovered: isSpacingHovered } = useHover()
|
|
43
|
+
|
|
35
44
|
const setControlRef = (name: AutoLayoutDirection) => (node: HTMLButtonElement) => {
|
|
36
45
|
controlsRefs[name] = node
|
|
37
46
|
setControlsRefs(controlsRefs)
|
|
@@ -50,7 +59,11 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
|
|
|
50
59
|
})
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
const setSpacing = (nodeSep: number, rankSep: number) => {
|
|
62
|
+
const setSpacing = (nodeSep: number | null, rankSep: number | null) => {
|
|
63
|
+
// Force fitDiagram
|
|
64
|
+
store.setState({
|
|
65
|
+
viewportChanged: false
|
|
66
|
+
})
|
|
54
67
|
store.getState().onChange?.({
|
|
55
68
|
change: {
|
|
56
69
|
op: 'change-autolayout',
|
|
@@ -82,7 +95,7 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
|
|
|
82
95
|
</ActionIcon>
|
|
83
96
|
</Tooltip>
|
|
84
97
|
</PopoverTarget>
|
|
85
|
-
<PopoverDropdown className="likec4-top-left-panel" p={8} pt={6}>
|
|
98
|
+
<PopoverDropdown className="likec4-top-left-panel" p={8} pt={6} opacity={isSpacingHovered ? 0.6 : 1}>
|
|
86
99
|
<Box pos={'relative'} ref={setRootRef}>
|
|
87
100
|
<FloatingIndicator
|
|
88
101
|
target={controlsRefs[autoLayout.direction]}
|
|
@@ -110,6 +123,7 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
|
|
|
110
123
|
<Text inline fz={'xs'} c={'dimmed'} fw={500}>Spacing:</Text>
|
|
111
124
|
</Box>
|
|
112
125
|
<SpacingSliders
|
|
126
|
+
ref={ref}
|
|
113
127
|
isVertical={autoLayout.direction === 'TB' || autoLayout.direction === 'BT'}
|
|
114
128
|
key={viewId}
|
|
115
129
|
nodeSep={autoLayout.nodeSep}
|
|
@@ -122,17 +136,18 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
|
|
|
122
136
|
)
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
nodeSep,
|
|
128
|
-
rankSep,
|
|
129
|
-
onChange
|
|
130
|
-
}: {
|
|
139
|
+
const MAX_SPACING = 400
|
|
140
|
+
const SpacingSliders = forwardRef<HTMLDivElement, {
|
|
131
141
|
isVertical: boolean
|
|
132
142
|
nodeSep: number | undefined
|
|
133
143
|
rankSep: number | undefined
|
|
134
144
|
onChange: (nodeSep: number, rankSep: number) => void
|
|
135
|
-
}
|
|
145
|
+
}>(({
|
|
146
|
+
isVertical,
|
|
147
|
+
nodeSep,
|
|
148
|
+
rankSep,
|
|
149
|
+
onChange
|
|
150
|
+
}, _ref) => {
|
|
136
151
|
if (!isVertical) {
|
|
137
152
|
;[nodeSep, rankSep] = [rankSep, nodeSep]
|
|
138
153
|
}
|
|
@@ -142,7 +157,7 @@ const SpacingSliders = ({
|
|
|
142
157
|
if (!isVertical) {
|
|
143
158
|
;[x, y] = [y, x]
|
|
144
159
|
}
|
|
145
|
-
onChange(Math.round(x *
|
|
160
|
+
onChange(Math.round(x * MAX_SPACING), Math.round(y * MAX_SPACING))
|
|
146
161
|
},
|
|
147
162
|
[onChange, isVertical],
|
|
148
163
|
150,
|
|
@@ -151,22 +166,24 @@ const SpacingSliders = ({
|
|
|
151
166
|
|
|
152
167
|
const [value, setValue] = useUncontrolled({
|
|
153
168
|
defaultValue: clampUseMovePosition({
|
|
154
|
-
x: (nodeSep ??
|
|
155
|
-
y: (rankSep ??
|
|
169
|
+
x: (nodeSep ?? 100) / MAX_SPACING,
|
|
170
|
+
y: (rankSep ?? 120) / MAX_SPACING
|
|
156
171
|
}),
|
|
157
172
|
onChange: propagateChange
|
|
158
173
|
})
|
|
159
174
|
|
|
160
175
|
const { ref } = useMove(setValue)
|
|
161
176
|
|
|
162
|
-
let nodeSepValue = Math.round(value.x *
|
|
163
|
-
let rankSepValue = Math.round(value.y *
|
|
177
|
+
let nodeSepValue = Math.round(value.x * MAX_SPACING)
|
|
178
|
+
let rankSepValue = Math.round(value.y * MAX_SPACING)
|
|
164
179
|
if (!isVertical) {
|
|
165
180
|
;[nodeSepValue, rankSepValue] = [rankSepValue, nodeSepValue]
|
|
166
181
|
}
|
|
167
182
|
|
|
183
|
+
const mergedRef = useMergedRef(ref, _ref)
|
|
184
|
+
|
|
168
185
|
return (
|
|
169
|
-
<Box ref={
|
|
186
|
+
<Box ref={mergedRef} className={css.spacingSliderBody} pt={'100%'}>
|
|
170
187
|
<Box
|
|
171
188
|
className={css.spacingSliderThumb}
|
|
172
189
|
style={{
|
|
@@ -179,4 +196,4 @@ const SpacingSliders = ({
|
|
|
179
196
|
</Box>
|
|
180
197
|
</Box>
|
|
181
198
|
)
|
|
182
|
-
}
|
|
199
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ActionIconGroup, Group, Popover, PopoverDropdown, type PopoverProps, PopoverTarget } from '@mantine/core'
|
|
2
|
+
import {
|
|
3
|
+
IconLayoutAlignBottom,
|
|
4
|
+
IconLayoutAlignCenter,
|
|
5
|
+
IconLayoutAlignLeft,
|
|
6
|
+
IconLayoutAlignMiddle,
|
|
7
|
+
IconLayoutAlignRight,
|
|
8
|
+
IconLayoutAlignTop,
|
|
9
|
+
IconLayoutCollage,
|
|
10
|
+
IconRouteOff
|
|
11
|
+
} from '@tabler/icons-react'
|
|
12
|
+
import { useDiagramStoreApi } from '../../hooks/useDiagramState'
|
|
13
|
+
import { ActionIcon, Tooltip } from './_shared'
|
|
14
|
+
|
|
15
|
+
const Action = ({
|
|
16
|
+
label,
|
|
17
|
+
icon,
|
|
18
|
+
onClick
|
|
19
|
+
}: {
|
|
20
|
+
label: string
|
|
21
|
+
icon: React.ReactNode
|
|
22
|
+
onClick: React.MouseEventHandler
|
|
23
|
+
}) => (
|
|
24
|
+
<Tooltip label={label} withinPortal={false} position="top">
|
|
25
|
+
<ActionIcon onClick={onClick}>
|
|
26
|
+
{icon}
|
|
27
|
+
</ActionIcon>
|
|
28
|
+
</Tooltip>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export const ManualLayoutToolsButton = (props: PopoverProps) => {
|
|
32
|
+
const store = useDiagramStoreApi()
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Popover
|
|
36
|
+
position="right-start"
|
|
37
|
+
clickOutsideEvents={[
|
|
38
|
+
'pointerdown'
|
|
39
|
+
]}
|
|
40
|
+
radius="xs"
|
|
41
|
+
shadow="lg"
|
|
42
|
+
offset={{
|
|
43
|
+
mainAxis: 10
|
|
44
|
+
}}
|
|
45
|
+
{...props}>
|
|
46
|
+
<PopoverTarget>
|
|
47
|
+
<Tooltip label="Manual layouting tools" withinPortal={false} position="top-end">
|
|
48
|
+
<ActionIcon>
|
|
49
|
+
<IconLayoutCollage />
|
|
50
|
+
</ActionIcon>
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</PopoverTarget>
|
|
53
|
+
<PopoverDropdown p={0}>
|
|
54
|
+
<Group gap={'xs'}>
|
|
55
|
+
<ActionIconGroup pos={'relative'}>
|
|
56
|
+
<Action
|
|
57
|
+
label="Align left"
|
|
58
|
+
icon={<IconLayoutAlignLeft />}
|
|
59
|
+
onClick={e => {
|
|
60
|
+
e.stopPropagation()
|
|
61
|
+
store.getState().align('Left')
|
|
62
|
+
}} />
|
|
63
|
+
<Action
|
|
64
|
+
label="Align center"
|
|
65
|
+
icon={<IconLayoutAlignCenter />}
|
|
66
|
+
onClick={e => {
|
|
67
|
+
e.stopPropagation()
|
|
68
|
+
store.getState().align('Center')
|
|
69
|
+
}} />
|
|
70
|
+
<Action
|
|
71
|
+
label="Align right"
|
|
72
|
+
icon={<IconLayoutAlignRight />}
|
|
73
|
+
onClick={e => {
|
|
74
|
+
e.stopPropagation()
|
|
75
|
+
store.getState().align('Right')
|
|
76
|
+
}} />
|
|
77
|
+
<Action
|
|
78
|
+
label="Align top"
|
|
79
|
+
icon={<IconLayoutAlignTop />}
|
|
80
|
+
onClick={e => {
|
|
81
|
+
e.stopPropagation()
|
|
82
|
+
store.getState().align('Top')
|
|
83
|
+
}} />
|
|
84
|
+
<Action
|
|
85
|
+
label="Align middle"
|
|
86
|
+
icon={<IconLayoutAlignMiddle />}
|
|
87
|
+
onClick={e => {
|
|
88
|
+
e.stopPropagation()
|
|
89
|
+
store.getState().align('Middle')
|
|
90
|
+
}} />
|
|
91
|
+
<Action
|
|
92
|
+
label="Align bottom"
|
|
93
|
+
icon={<IconLayoutAlignBottom />}
|
|
94
|
+
onClick={e => {
|
|
95
|
+
e.stopPropagation()
|
|
96
|
+
store.getState().align('Bottom')
|
|
97
|
+
}} />
|
|
98
|
+
</ActionIconGroup>
|
|
99
|
+
<Action
|
|
100
|
+
label="Reset all control points"
|
|
101
|
+
icon={<IconRouteOff />}
|
|
102
|
+
onClick={e => {
|
|
103
|
+
e.stopPropagation()
|
|
104
|
+
store.getState().resetEdgeControlPoints()
|
|
105
|
+
}} />
|
|
106
|
+
</Group>
|
|
107
|
+
</PopoverDropdown>
|
|
108
|
+
</Popover>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
type PopoverProps,
|
|
11
11
|
Stack,
|
|
12
12
|
Text,
|
|
13
|
-
TooltipGroup
|
|
14
|
-
type TooltipProps
|
|
13
|
+
TooltipGroup
|
|
15
14
|
} from '@mantine/core'
|
|
16
15
|
import {
|
|
17
16
|
IconAlertTriangle,
|
|
@@ -19,8 +18,7 @@ import {
|
|
|
19
18
|
IconChevronRight,
|
|
20
19
|
IconFileSymlink,
|
|
21
20
|
IconFocusCentered,
|
|
22
|
-
IconMenu2
|
|
23
|
-
IconRouteOff
|
|
21
|
+
IconMenu2
|
|
24
22
|
} from '@tabler/icons-react'
|
|
25
23
|
import clsx from 'clsx'
|
|
26
24
|
import { AnimatePresence, LayoutGroup, m } from 'framer-motion'
|
|
@@ -30,12 +28,14 @@ import { useMantinePortalProps } from '../../hooks/useMantinePortalProps'
|
|
|
30
28
|
import { mantine } from '../../theme-vars'
|
|
31
29
|
import { ActionIcon, Tooltip } from './_shared'
|
|
32
30
|
import { ChangeAutoLayoutButton } from './ChangeAutoLayoutButton'
|
|
31
|
+
import { ManualLayoutToolsButton } from './ManualLayoutToolsButton'
|
|
33
32
|
import * as css from './styles.css'
|
|
34
33
|
|
|
35
34
|
const historySelector = (s: DiagramState) => ({
|
|
36
35
|
showBurgerMenu: !!s.onBurgerMenuClick,
|
|
37
|
-
hasStepBack: s.navigationHistoryIndex > 0,
|
|
38
|
-
hasStepForward: s.
|
|
36
|
+
hasStepBack: s.showNavigationButtons && !!s.onNavigateTo && s.navigationHistoryIndex > 0,
|
|
37
|
+
hasStepForward: s.showNavigationButtons && !!s.onNavigateTo
|
|
38
|
+
&& s.navigationHistoryIndex < s.navigationHistory.length - 1
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
const BackwardForwardButtons = () => {
|
|
@@ -135,22 +135,6 @@ const LayoutDriftNotification = (props: PopoverProps) => (
|
|
|
135
135
|
</HoverCard>
|
|
136
136
|
)
|
|
137
137
|
|
|
138
|
-
const ResetControlPointsButton = (props: Omit<TooltipProps, 'label' | 'children'>) => {
|
|
139
|
-
const store = useDiagramStoreApi()
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<Tooltip label="Reset all control points" {...props}>
|
|
143
|
-
<ActionIcon
|
|
144
|
-
onClick={e => {
|
|
145
|
-
e.stopPropagation()
|
|
146
|
-
store.getState().resetEdgeControlPoints()
|
|
147
|
-
}}>
|
|
148
|
-
<IconRouteOff />
|
|
149
|
-
</ActionIcon>
|
|
150
|
-
</Tooltip>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
138
|
export const TopLeftPanel = () => {
|
|
155
139
|
const store = useDiagramStoreApi()
|
|
156
140
|
const {
|
|
@@ -160,25 +144,25 @@ export const TopLeftPanel = () => {
|
|
|
160
144
|
showChangeAutoLayout,
|
|
161
145
|
showGoToSource,
|
|
162
146
|
viewportChanged,
|
|
163
|
-
|
|
147
|
+
showManualLayoutTools
|
|
164
148
|
} = useDiagramState(s => {
|
|
165
149
|
const isNotWalkthrough = isNullish(s.activeWalkthrough)
|
|
166
150
|
const isNotFocused = isNullish(s.focusedNodeId)
|
|
167
151
|
const isNotActive = isNotWalkthrough && isNotFocused
|
|
168
152
|
return ({
|
|
169
|
-
showNavigationButtons: !!s.onBurgerMenuClick || s.showNavigationButtons && !!s.onNavigateTo,
|
|
170
|
-
showFitDiagram: s.fitViewEnabled && s.zoomable && isNotWalkthrough,
|
|
171
|
-
showLayoutDriftWarning: !s.readonly && s.view.hasLayoutDrift === true && isNotActive,
|
|
172
|
-
showChangeAutoLayout: s.isEditable() && isNotActive,
|
|
173
|
-
showGoToSource: !!s.onOpenSourceView && isNotWalkthrough,
|
|
153
|
+
showNavigationButtons: !!s.onBurgerMenuClick || (s.showNavigationButtons && !!s.onNavigateTo),
|
|
154
|
+
showFitDiagram: s.controls && s.fitViewEnabled && s.zoomable && isNotWalkthrough,
|
|
155
|
+
showLayoutDriftWarning: s.controls && !s.readonly && s.view.hasLayoutDrift === true && isNotActive,
|
|
156
|
+
showChangeAutoLayout: s.controls && s.isEditable() && isNotActive,
|
|
157
|
+
showGoToSource: s.controls && !!s.onOpenSourceView && isNotWalkthrough,
|
|
174
158
|
viewportChanged: s.viewportChanged,
|
|
175
|
-
|
|
159
|
+
showManualLayoutTools: s.controls && s.readonly !== true && s.experimentalEdgeEditing === true
|
|
176
160
|
})
|
|
177
161
|
})
|
|
178
162
|
const portalProps = useMantinePortalProps()
|
|
179
163
|
|
|
180
164
|
return (
|
|
181
|
-
<TooltipGroup openDelay={
|
|
165
|
+
<TooltipGroup openDelay={600} closeDelay={150}>
|
|
182
166
|
<Stack
|
|
183
167
|
className={clsx(
|
|
184
168
|
'react-flow__panel',
|
|
@@ -203,7 +187,7 @@ export const TopLeftPanel = () => {
|
|
|
203
187
|
)}
|
|
204
188
|
{showChangeAutoLayout && <ChangeAutoLayoutButton {...portalProps} />}
|
|
205
189
|
{showLayoutDriftWarning && <LayoutDriftNotification {...portalProps} />}
|
|
206
|
-
{
|
|
190
|
+
{showManualLayoutTools && <ManualLayoutToolsButton {...portalProps} />}
|
|
207
191
|
{showFitDiagram && (
|
|
208
192
|
<Tooltip label={viewportChanged ? 'Center camera' : 'Camera is centered'} {...portalProps}>
|
|
209
193
|
<ActionIcon
|
|
@@ -39,6 +39,24 @@ const Arrow = (props: SVGProps<SVGMarkerElement>) => (
|
|
|
39
39
|
</marker>
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
const Crow = (props: SVGProps<SVGMarkerElement>) => (
|
|
43
|
+
<marker
|
|
44
|
+
viewBox="-1 -1 12 12"
|
|
45
|
+
refX={8}
|
|
46
|
+
refY={4}
|
|
47
|
+
markerWidth="8"
|
|
48
|
+
markerHeight="8"
|
|
49
|
+
preserveAspectRatio="xMaxYMid meet"
|
|
50
|
+
orient="auto-start-reverse"
|
|
51
|
+
{...props}>
|
|
52
|
+
<path
|
|
53
|
+
d="M 8 0 L 0 4 L 8 8 M 8 4 L 0 4"
|
|
54
|
+
fill="none"
|
|
55
|
+
strokeWidth={1}
|
|
56
|
+
/>
|
|
57
|
+
</marker>
|
|
58
|
+
)
|
|
59
|
+
|
|
42
60
|
const OArrow = (props: SVGProps<SVGMarkerElement>) => (
|
|
43
61
|
<marker
|
|
44
62
|
viewBox="-1 -1 12 10"
|
|
@@ -139,6 +157,7 @@ const ODot = (props: SVGProps<SVGMarkerElement>) => (
|
|
|
139
157
|
|
|
140
158
|
export const EdgeMarkers = {
|
|
141
159
|
Arrow,
|
|
160
|
+
Crow,
|
|
142
161
|
OArrow,
|
|
143
162
|
Open,
|
|
144
163
|
Diamond,
|
|
@@ -50,12 +50,9 @@ export function useXYFlowEvents() {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const lastClickWasRecent = (ms = 2000) => {
|
|
53
|
-
const prevTimestamp = lastClickTimestamp.current
|
|
54
|
-
lastClickTimestamp.current = Date.now()
|
|
55
|
-
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
return prevTimestamp + ms > Date.now()
|
|
53
|
+
const prevTimestamp = lastClickTimestamp.current ?? 0
|
|
54
|
+
const now = lastClickTimestamp.current = Date.now()
|
|
55
|
+
return prevTimestamp + ms > now
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
return ({
|
|
@@ -68,16 +65,15 @@ export function useXYFlowEvents() {
|
|
|
68
65
|
resetFocusAndLastClicked
|
|
69
66
|
} = diagramApi.getState()
|
|
70
67
|
resetFocusAndLastClicked()
|
|
71
|
-
if (!onCanvasDblClick) {
|
|
72
|
-
event.stopPropagation()
|
|
73
|
-
}
|
|
74
68
|
if (zoomable && viewportChanged) {
|
|
75
69
|
fitDiagram()
|
|
76
70
|
}
|
|
71
|
+
if (!onCanvasDblClick) {
|
|
72
|
+
event.stopPropagation()
|
|
73
|
+
}
|
|
77
74
|
onCanvasDblClick?.(event)
|
|
78
75
|
},
|
|
79
76
|
onPaneClick: (event) => {
|
|
80
|
-
console.log('onPaneClick')
|
|
81
77
|
if (dbclickLock()) {
|
|
82
78
|
return
|
|
83
79
|
}
|
|
@@ -132,7 +128,7 @@ export function useXYFlowEvents() {
|
|
|
132
128
|
// if we focused on a node, and clicked on another node - focus on the clicked node
|
|
133
129
|
const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
|
|
134
130
|
// if user clicked on the same node twice in a short time, focus on it
|
|
135
|
-
const clickedRecently = lastClickedNodeId === xynode.id
|
|
131
|
+
const clickedRecently = lastClickWasRecent() && lastClickedNodeId === xynode.id
|
|
136
132
|
|
|
137
133
|
if (clickedRecently && !!onOpenSourceElement) {
|
|
138
134
|
onOpenSourceElement(xynode.data.element.id)
|
|
@@ -141,27 +137,27 @@ export function useXYFlowEvents() {
|
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
139
|
|
|
144
|
-
if (enableFocusMode
|
|
140
|
+
if (enableFocusMode) {
|
|
145
141
|
switch (true) {
|
|
146
|
-
case
|
|
142
|
+
case clickedRecently && focusedNodeId === xynode.id: {
|
|
147
143
|
focusOnNode(false)
|
|
148
144
|
fitDiagram()
|
|
149
145
|
break
|
|
150
146
|
}
|
|
151
|
-
case
|
|
152
|
-
case enableFocusMode && clickedRecently && focusedNodeId !== xynode.id: {
|
|
147
|
+
case (shallChangeFocus || clickedRecently): {
|
|
153
148
|
focusOnNode(xynode.id)
|
|
154
149
|
break
|
|
155
150
|
}
|
|
156
|
-
case nodesSelectable: {
|
|
157
|
-
xystore.getState().addSelectedNodes([xynode.id])
|
|
158
|
-
break
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (!onNodeClick) {
|
|
162
|
-
event.stopPropagation()
|
|
163
151
|
}
|
|
152
|
+
} else if (nodesSelectable) {
|
|
153
|
+
xystore.getState().addSelectedNodes([xynode.id])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!onNodeClick && (enableFocusMode || nodesSelectable)) {
|
|
157
|
+
event.stopPropagation()
|
|
158
|
+
return
|
|
164
159
|
}
|
|
160
|
+
|
|
165
161
|
onNodeClick?.(
|
|
166
162
|
xynode.data.element,
|
|
167
163
|
event
|
|
@@ -176,16 +172,17 @@ export function useXYFlowEvents() {
|
|
|
176
172
|
setLastClickedNode
|
|
177
173
|
} = diagramApi.getState()
|
|
178
174
|
setLastClickedNode(xynode.id)
|
|
179
|
-
|
|
175
|
+
lastClickWasRecent()
|
|
180
176
|
if (isNonNullish(focusedNodeId) || enableFocusMode) {
|
|
181
177
|
// if we are already focused on the node, cancel
|
|
182
178
|
if (focusedNodeId === xynode.id) {
|
|
179
|
+
focusOnNode(false)
|
|
183
180
|
fitDiagram()
|
|
184
181
|
} else {
|
|
185
182
|
focusOnNode(xynode.id)
|
|
186
183
|
}
|
|
187
|
-
event.stopPropagation()
|
|
188
184
|
}
|
|
185
|
+
event.stopPropagation()
|
|
189
186
|
},
|
|
190
187
|
onEdgeClick: (event, xyedge) => {
|
|
191
188
|
const {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Controls } from '@xyflow/react'
|
|
2
1
|
import { shallowEqual } from 'fast-equals'
|
|
3
|
-
import { memo
|
|
2
|
+
import { memo } from 'react'
|
|
4
3
|
import { isNonNullish } from 'remeda'
|
|
5
4
|
import { useDiagramState } from '../hooks/useDiagramState'
|
|
6
5
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
@@ -10,26 +9,25 @@ import NotationPanel from '../ui/notation'
|
|
|
10
9
|
import { TopLeftPanel } from '../ui/top-left/TopLeftPanel'
|
|
11
10
|
import { XYFlowBackground } from './XYFlowBackground'
|
|
12
11
|
|
|
13
|
-
type XYFlowInnerProps =
|
|
12
|
+
type XYFlowInnerProps = {
|
|
14
13
|
background: NonNullable<LikeC4DiagramProperties['background']>
|
|
15
|
-
controls: boolean
|
|
16
14
|
showDiagramTitle: boolean
|
|
17
|
-
}
|
|
15
|
+
}
|
|
18
16
|
|
|
19
17
|
export const XYFlowInner = /* @__PURE__ */ memo(function XYFlowInnerR({
|
|
20
|
-
children,
|
|
21
18
|
background,
|
|
22
|
-
controls,
|
|
23
19
|
showDiagramTitle
|
|
24
20
|
}: XYFlowInnerProps) {
|
|
25
21
|
const {
|
|
26
22
|
showNotations,
|
|
27
23
|
isDynamicView,
|
|
24
|
+
showTopLeft,
|
|
28
25
|
isDynamicViewActive,
|
|
29
26
|
enableDynamicViewWalkthrough
|
|
30
27
|
} = useDiagramState(s => ({
|
|
31
28
|
showNotations: s.showNotations,
|
|
32
29
|
isDynamicView: s.isDynamicView,
|
|
30
|
+
showTopLeft: s.controls || (s.showNavigationButtons && !!s.onNavigateTo) || !!s.onBurgerMenuClick,
|
|
33
31
|
enableDynamicViewWalkthrough: s.enableDynamicViewWalkthrough,
|
|
34
32
|
isDynamicViewActive: isNonNullish(s.activeWalkthrough)
|
|
35
33
|
}))
|
|
@@ -40,12 +38,10 @@ export const XYFlowInner = /* @__PURE__ */ memo(function XYFlowInnerR({
|
|
|
40
38
|
return (
|
|
41
39
|
<>
|
|
42
40
|
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
43
|
-
{
|
|
44
|
-
<TopLeftPanel />
|
|
41
|
+
{showTopLeft && <TopLeftPanel />}
|
|
45
42
|
{isDiagramTitleVisible && <DiagramTitlePanel />}
|
|
46
43
|
{isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
|
|
47
44
|
{showNotations && <NotationPanel />}
|
|
48
|
-
{children}
|
|
49
45
|
</>
|
|
50
46
|
)
|
|
51
47
|
}, shallowEqual)
|
|
@@ -22,6 +22,7 @@ import { isTruthy } from 'remeda'
|
|
|
22
22
|
import { useDiagramState, useDiagramStoreApi, useMantinePortalProps } from '../../hooks'
|
|
23
23
|
import { useLikeC4Model } from '../../likec4model/useLikeC4Model'
|
|
24
24
|
import type { RelationshipData } from '../types'
|
|
25
|
+
import { stopPropagation } from '../utils'
|
|
25
26
|
import * as edgesCss from './edges.css'
|
|
26
27
|
import { RelationshipsDropdownMenu } from './RelationshipsDropdownMenu'
|
|
27
28
|
|
|
@@ -125,9 +126,9 @@ const NotePopover = ({ notes, children }: PropsWithChildren<{ notes: string }>)
|
|
|
125
126
|
<PopoverDropdown
|
|
126
127
|
component={Stack}
|
|
127
128
|
p={'xs'}
|
|
128
|
-
onPointerDownCapture={
|
|
129
|
-
onClick={
|
|
130
|
-
onDoubleClick={
|
|
129
|
+
onPointerDownCapture={stopPropagation}
|
|
130
|
+
onClick={stopPropagation}
|
|
131
|
+
onDoubleClick={stopPropagation}
|
|
131
132
|
>
|
|
132
133
|
<ScrollAreaAutosize maw={450} mah={350} type="scroll" mx={'auto'} mt={2}>
|
|
133
134
|
<Text component="div" className={edgesCss.edgeNoteText} p={4}>{notes}</Text>
|
|
@@ -171,13 +172,13 @@ function NavigateToBtn({ viewId }: NavigateToBtnProps) {
|
|
|
171
172
|
className={clsx('nodrag nopan', edgesCss.cssNavigateBtn)}
|
|
172
173
|
size={'sm'}
|
|
173
174
|
radius="sm"
|
|
174
|
-
onPointerDownCapture={
|
|
175
|
+
onPointerDownCapture={stopPropagation}
|
|
175
176
|
onClick={event => {
|
|
176
177
|
event.stopPropagation()
|
|
177
178
|
diagramApi.getState().onNavigateTo?.(viewId, event)
|
|
178
179
|
}}
|
|
179
180
|
role="button"
|
|
180
|
-
onDoubleClick={
|
|
181
|
+
onDoubleClick={stopPropagation}
|
|
181
182
|
>
|
|
182
183
|
<IconZoomScan className={edgesCss.cssNavigateBtnIcon} />
|
|
183
184
|
</ActionIcon>
|
|
@@ -63,8 +63,9 @@ const toMarker = (arrowType?: RelationshipArrowType): EdgeMarkerType | undefined
|
|
|
63
63
|
}
|
|
64
64
|
switch (arrowType) {
|
|
65
65
|
case 'normal':
|
|
66
|
-
case 'crow':
|
|
67
66
|
return 'Arrow' as const
|
|
67
|
+
case 'crow':
|
|
68
|
+
return 'Crow' as const
|
|
68
69
|
case 'onormal':
|
|
69
70
|
return 'OArrow' as const
|
|
70
71
|
case 'diamond':
|
|
@@ -332,7 +333,7 @@ export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function Relationshi
|
|
|
332
333
|
const onRmbControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>, domNode: HTMLDivElement) => {
|
|
333
334
|
const { xyflow } = diagramStore.getState()
|
|
334
335
|
|
|
335
|
-
if(controlPoints.length <= 1) {
|
|
336
|
+
if (controlPoints.length <= 1) {
|
|
336
337
|
return
|
|
337
338
|
}
|
|
338
339
|
|
|
@@ -349,7 +350,7 @@ export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function Relationshi
|
|
|
349
350
|
domNode.removeEventListener('pointerup', onPointerUp, {
|
|
350
351
|
capture: true
|
|
351
352
|
})
|
|
352
|
-
e.stopPropagation()
|
|
353
|
+
e.stopPropagation()
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
domNode.addEventListener('pointerup', onPointerUp, {
|
|
@@ -122,6 +122,22 @@ export function RelationshipsDropdownMenu({
|
|
|
122
122
|
{nested.map(renderRelationship)}
|
|
123
123
|
</>
|
|
124
124
|
)}
|
|
125
|
+
{
|
|
126
|
+
/* <Box pos={'absolute'} top={5} right={6}>
|
|
127
|
+
<ActionIcon
|
|
128
|
+
size={24}
|
|
129
|
+
variant="light"
|
|
130
|
+
onClick={e => {
|
|
131
|
+
e.stopPropagation()
|
|
132
|
+
diagramApi.getState().openOverlay({
|
|
133
|
+
edgeDetails: edge.id
|
|
134
|
+
})
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<IconArrowsMaximize style={{ width: '70%' }} />
|
|
138
|
+
</ActionIcon>
|
|
139
|
+
</Box> */
|
|
140
|
+
}
|
|
125
141
|
</MenuDropdown>
|
|
126
142
|
</Menu>
|
|
127
143
|
)
|
|
@@ -45,23 +45,17 @@ globalStyle(`:where(${isSelected}) ${container}[data-edge-hovered='true']`, {
|
|
|
45
45
|
[xyvars.edge.strokeWidth]: '4'
|
|
46
46
|
}
|
|
47
47
|
})
|
|
48
|
-
// globalStyle(`${container}[data-edge-hovered='true']`, {
|
|
49
|
-
// vars: {
|
|
50
|
-
// [xyvars.edge.stroke]: `color-mix(in srgb, ${vars.relation.lineColor}, white 35%)`
|
|
51
|
-
// },
|
|
52
|
-
// })
|
|
53
48
|
|
|
54
|
-
globalStyle(`.react-flow__edges > svg`, {
|
|
49
|
+
globalStyle(`.likec4-diagram .react-flow__edges > svg`, {
|
|
55
50
|
mixBlendMode: 'plus-lighter'
|
|
56
51
|
})
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// })
|
|
52
|
+
globalStyle(`:where([data-mantine-color-scheme="light"]) .likec4-diagram .react-flow__edges > svg`, {
|
|
53
|
+
mixBlendMode: 'screen'
|
|
54
|
+
})
|
|
61
55
|
|
|
62
56
|
export const dimmed = style({})
|
|
63
57
|
|
|
64
|
-
globalStyle(`.react-flow__edges > svg:has(${dimmed})`, {
|
|
58
|
+
globalStyle(`.likec4-diagram .react-flow__edges > svg:has(${dimmed})`, {
|
|
65
59
|
opacity: 0.6,
|
|
66
60
|
transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
|
|
67
61
|
transitionDelay: '200ms',
|