@likec4/diagram 1.4.0 → 1.5.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.props.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1398 -1114
- package/dist/state/DiagramContext.d.ts +86 -4159
- package/dist/state/diagramStore.d.ts +88 -4162
- package/dist/state/hooks.d.ts +4 -0
- package/dist/state/index.d.ts +1 -2
- package/dist/style.css +182 -156
- package/dist/ui/options/EdgeOptions.d.ts +19 -0
- package/dist/ui/options/NodeOptions.d.ts +2 -2
- package/dist/xyflow/EdgeMarkers.d.ts +11 -0
- package/dist/xyflow/XYFlowEvents.d.ts +8 -5
- package/dist/xyflow/const.d.ts +3 -3
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -2
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/dist/xyflow/edges/utils.d.ts +1 -0
- package/dist/xyflow/hooks/index.d.ts +3 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +13 -12
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -4
- package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
- package/dist/xyflow/types.d.ts +2 -32
- package/package.json +19 -19
- package/src/LikeC4Diagram.css.ts +0 -8
- package/src/LikeC4Diagram.props.ts +2 -2
- package/src/LikeC4Diagram.tsx +14 -20
- package/src/globals.css.ts +10 -3
- package/src/index.ts +1 -1
- package/src/mantine.css.ts +2 -2
- package/src/state/DiagramContext.tsx +44 -13
- package/src/state/diagramStore.ts +64 -52
- package/src/state/hooks.ts +5 -0
- package/src/state/index.ts +1 -2
- package/src/ui/BackwardForwardPanel.css.ts +9 -9
- package/src/ui/BackwardForwardPanel.tsx +1 -1
- package/src/ui/DiagramTitlePanel.css.ts +7 -2
- package/src/ui/DiagramTitlePanel.tsx +5 -3
- package/src/ui/DynamicViewWalkthrough.tsx +6 -4
- package/src/ui/OptionsPanel.tsx +37 -27
- package/src/ui/options/EdgeOptions.tsx +157 -0
- package/src/ui/options/NodeOptions.tsx +37 -29
- package/src/xyflow/EdgeMarkers.tsx +148 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +8 -4
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +59 -55
- package/src/xyflow/SyncWithDiagram.tsx +13 -20
- package/src/xyflow/XYFlow.tsx +2 -4
- package/src/xyflow/XYFlowEvents.tsx +48 -33
- package/src/xyflow/XYFlowInner.tsx +1 -1
- package/src/xyflow/const.ts +3 -3
- package/src/xyflow/diagram-to-xyflow.ts +17 -41
- package/src/xyflow/edges/RelationshipEdge.tsx +207 -118
- package/src/xyflow/edges/edges.css.ts +36 -22
- package/src/xyflow/edges/utils.ts +10 -0
- package/src/xyflow/hooks/index.ts +4 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +3 -4
- package/src/xyflow/hooks/useXYFlow.ts +28 -1
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +34 -24
- package/src/xyflow/nodes/compound/CompoundNode.tsx +52 -53
- package/src/xyflow/nodes/element/ElementIcon.tsx +11 -22
- package/src/xyflow/nodes/element/ElementLink.tsx +11 -16
- package/src/xyflow/nodes/element/ElementNode.tsx +64 -108
- package/src/xyflow/nodes/element/element.css.ts +30 -10
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
- package/src/xyflow/types.ts +2 -4
- package/src/xyflow/utils.ts +3 -6
- package/dist/KeepAspectRatioContainer.d.ts +0 -9
- package/dist/icons/Clipboard.d.ts +0 -3
- package/dist/icons/Link.d.ts +0 -3
- package/dist/icons/ZoomIn.d.ts +0 -3
- package/dist/icons/index.d.ts +0 -3
- package/dist/mantine/theme.d.ts +0 -3
- package/dist/state/WhenInitialized.d.ts +0 -3
- package/dist/ui/Camera.d.ts +0 -3
- package/src/KeepAspectRatioContainer.tsx +0 -36
- package/src/icons/Clipboard.tsx +0 -24
- package/src/icons/Link.tsx +0 -23
- package/src/icons/ZoomIn.tsx +0 -25
- package/src/icons/index.ts +0 -3
- package/src/mantine/theme.ts +0 -20
- package/src/state/WhenInitialized.tsx +0 -24
- package/src/ui/Camera.tsx +0 -174
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { DefaultRelationshipColor, defaultTheme, type NonEmptyArray, type ThemeColor } from '@likec4/core'
|
|
2
|
+
import { CheckIcon, ColorSwatch, Divider, Flex, rem, Stack, Text, Tooltip, TooltipGroup } from '@mantine/core'
|
|
3
|
+
import { shallowEqual } from 'fast-equals'
|
|
4
|
+
import { memo } from 'react'
|
|
5
|
+
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
6
|
+
import { useDiagramStoreApi } from '../../state/hooks'
|
|
7
|
+
import { useXYEdgesData } from '../../xyflow/hooks/useXYFlow'
|
|
8
|
+
import { type XYFlowEdge } from '../../xyflow/types'
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
primary,
|
|
12
|
+
secondary,
|
|
13
|
+
muted,
|
|
14
|
+
...otherColors
|
|
15
|
+
} = defaultTheme.elements
|
|
16
|
+
|
|
17
|
+
export const themedColors = [
|
|
18
|
+
{ key: 'primary', value: primary.fill },
|
|
19
|
+
{ key: 'secondary', value: secondary.fill },
|
|
20
|
+
{ key: 'muted', value: muted.fill }
|
|
21
|
+
] satisfies Array<{ key: ThemeColor; value: string }>
|
|
22
|
+
|
|
23
|
+
export const colors = keys(otherColors).map(key => ({
|
|
24
|
+
key,
|
|
25
|
+
value: defaultTheme.elements[key].fill
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
export type ThemeColorKey = typeof themedColors[0]['key']
|
|
29
|
+
export type ColorKey = typeof colors[0]['key']
|
|
30
|
+
|
|
31
|
+
type XYEdgeData = Pick<XYFlowEdge, 'id' | 'data'>
|
|
32
|
+
|
|
33
|
+
export const EdgeOptions = memo<{ selectedEdgeIds: string[] }>(({ selectedEdgeIds }) => {
|
|
34
|
+
const diagramApi = useDiagramStoreApi()
|
|
35
|
+
const edges = useXYEdgesData(selectedEdgeIds)
|
|
36
|
+
if (!hasAtLeast(edges, 1)) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
if (edges.length !== selectedEdgeIds.length) {
|
|
40
|
+
throw new Error('EdgeOptions: edges and props.edges should have the same length')
|
|
41
|
+
}
|
|
42
|
+
const [firstEdge, ...rest] = edges
|
|
43
|
+
|
|
44
|
+
// // Makes sense to show opacity option only if there is at least one compound node
|
|
45
|
+
// const showOpacityOption = firstNode.type === 'compound'
|
|
46
|
+
// && (rest.length === 0 || rest.every(node => node.type === 'compound'))
|
|
47
|
+
|
|
48
|
+
// const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
|
|
49
|
+
// const targets = edges.map(node => node.data.element.id)
|
|
50
|
+
// invariant(hasAtLeast(targets, 1), 'At least one target is required')
|
|
51
|
+
// diagramApi.getState().triggerChangeElementStyle({
|
|
52
|
+
// op: 'change-element-style',
|
|
53
|
+
// style,
|
|
54
|
+
// targets
|
|
55
|
+
// })
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Stack gap={'xs'}>
|
|
60
|
+
<div>
|
|
61
|
+
<Text fz={rem(9)} fw={'500'} c={'dimmed'}>
|
|
62
|
+
RELATIONSHIP{rest.length > 0 ? 'S' : ''}
|
|
63
|
+
</Text>
|
|
64
|
+
<Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
|
|
65
|
+
{rest.length === 0 ? firstEdge.data.edge.label : `[ multiple ]`}
|
|
66
|
+
</Text>
|
|
67
|
+
</div>
|
|
68
|
+
{
|
|
69
|
+
/* {showShapeOption && (
|
|
70
|
+
<ShapeOption
|
|
71
|
+
nodes={edges}
|
|
72
|
+
onShapeChange={(shape: ElementShape) => {
|
|
73
|
+
triggerChange({ shape })
|
|
74
|
+
}} />
|
|
75
|
+
)} */
|
|
76
|
+
}
|
|
77
|
+
<Colors
|
|
78
|
+
edges={edges}
|
|
79
|
+
onColorChange={(_color: ColorKey | ThemeColorKey) => {
|
|
80
|
+
// triggerChange({ color })
|
|
81
|
+
}} />
|
|
82
|
+
</Stack>
|
|
83
|
+
)
|
|
84
|
+
}, (prevProps, nextProps) => shallowEqual(prevProps.selectedEdgeIds, nextProps.selectedEdgeIds))
|
|
85
|
+
|
|
86
|
+
function Colors({
|
|
87
|
+
edges: [firstEdge, ...edges],
|
|
88
|
+
onColorChange
|
|
89
|
+
}: {
|
|
90
|
+
edges: NonEmptyArray<XYEdgeData>
|
|
91
|
+
onColorChange: (color: ColorKey | ThemeColorKey) => void
|
|
92
|
+
}) {
|
|
93
|
+
let selectedColor = (firstEdge.data.edge.color ?? DefaultRelationshipColor) as ThemeColor | null
|
|
94
|
+
takeWhile(edges, edge => {
|
|
95
|
+
const color = edge.data.edge.color ?? DefaultRelationshipColor
|
|
96
|
+
if (color !== selectedColor) {
|
|
97
|
+
selectedColor = null
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
return true
|
|
101
|
+
})
|
|
102
|
+
const changeColor = (color: ColorKey | ThemeColorKey) => () => {
|
|
103
|
+
if (selectedColor === color) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
onColorChange(color)
|
|
107
|
+
}
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<TooltipGroup openDelay={400} closeDelay={300}>
|
|
111
|
+
<Divider label="color" labelPosition="left" />
|
|
112
|
+
<Flex mt={'xs'} maw={150} gap="xs" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
|
|
113
|
+
{themedColors.map(({ key, value }) => (
|
|
114
|
+
<Tooltip
|
|
115
|
+
key={key}
|
|
116
|
+
label={key}
|
|
117
|
+
fz={'xs'}
|
|
118
|
+
color="dark"
|
|
119
|
+
offset={2}
|
|
120
|
+
transitionProps={{ duration: 140, transition: 'slide-up' }}>
|
|
121
|
+
<ColorSwatch
|
|
122
|
+
color={value}
|
|
123
|
+
size={20}
|
|
124
|
+
withShadow
|
|
125
|
+
onClick={changeColor(key)}
|
|
126
|
+
style={{ color: '#fff', cursor: 'pointer' }}
|
|
127
|
+
>
|
|
128
|
+
{selectedColor === key && <CheckIcon style={{ width: rem(12), height: rem(12) }} />}
|
|
129
|
+
</ColorSwatch>
|
|
130
|
+
</Tooltip>
|
|
131
|
+
))}
|
|
132
|
+
</Flex>
|
|
133
|
+
|
|
134
|
+
<Flex mt="sm" maw={150} gap="xs" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
|
|
135
|
+
{colors.map(({ key, value }) => (
|
|
136
|
+
<Tooltip
|
|
137
|
+
key={key}
|
|
138
|
+
label={key}
|
|
139
|
+
fz={'xs'}
|
|
140
|
+
color="dark"
|
|
141
|
+
offset={2}
|
|
142
|
+
transitionProps={{ duration: 140, transition: 'slide-up' }}>
|
|
143
|
+
<ColorSwatch
|
|
144
|
+
color={value}
|
|
145
|
+
size={20}
|
|
146
|
+
onClick={changeColor(key)}
|
|
147
|
+
style={{ color: '#fff', cursor: 'pointer' }}
|
|
148
|
+
>
|
|
149
|
+
{selectedColor === key && <CheckIcon style={{ width: rem(12), height: rem(12) }} />}
|
|
150
|
+
</ColorSwatch>
|
|
151
|
+
</Tooltip>
|
|
152
|
+
))}
|
|
153
|
+
</Flex>
|
|
154
|
+
</TooltipGroup>
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -17,16 +17,18 @@ import {
|
|
|
17
17
|
SegmentedControl,
|
|
18
18
|
Select,
|
|
19
19
|
Slider,
|
|
20
|
+
Space,
|
|
20
21
|
Stack,
|
|
21
22
|
Text,
|
|
22
23
|
Tooltip,
|
|
23
24
|
TooltipGroup
|
|
24
25
|
} from '@mantine/core'
|
|
25
|
-
import {
|
|
26
|
+
import { shallowEqual } from 'fast-equals'
|
|
27
|
+
import { memo, useState } from 'react'
|
|
26
28
|
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
27
29
|
import { useUpdateEffect } from '../../hooks/useUpdateEffect'
|
|
28
30
|
import type { Changes } from '../../LikeC4Diagram.props'
|
|
29
|
-
import { useDiagramStoreApi } from '../../state'
|
|
31
|
+
import { useDiagramStoreApi } from '../../state/hooks'
|
|
30
32
|
import { useXYNodesData } from '../../xyflow/hooks'
|
|
31
33
|
import { XYFlowNode } from '../../xyflow/types'
|
|
32
34
|
|
|
@@ -43,7 +45,7 @@ export const themedColors = [
|
|
|
43
45
|
{ key: 'muted', value: muted.fill }
|
|
44
46
|
] satisfies Array<{ key: ThemeColor; value: string }>
|
|
45
47
|
|
|
46
|
-
export const colors = keys
|
|
48
|
+
export const colors = keys(otherColors).map(key => ({
|
|
47
49
|
key,
|
|
48
50
|
value: defaultTheme.elements[key].fill
|
|
49
51
|
}))
|
|
@@ -53,7 +55,7 @@ export type ColorKey = typeof colors[0]['key']
|
|
|
53
55
|
|
|
54
56
|
type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
|
|
55
57
|
|
|
56
|
-
export
|
|
58
|
+
export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeIds }) => {
|
|
57
59
|
// export function NodeOptions(props: { nodes: XYFlowNode[] }) {
|
|
58
60
|
const diagramApi = useDiagramStoreApi()
|
|
59
61
|
// const nodes = useXYNodesData(props.nodes.map(node => node.id))
|
|
@@ -70,7 +72,8 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
70
72
|
const [firstNode, ...rest] = nodes
|
|
71
73
|
|
|
72
74
|
// Makes sense to show opacity option only if there is at least one compound node
|
|
73
|
-
const showOpacityOption =
|
|
75
|
+
const showOpacityOption = firstNode.type === 'compound'
|
|
76
|
+
&& (rest.length === 0 || rest.every(node => node.type === 'compound'))
|
|
74
77
|
|
|
75
78
|
const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
|
|
76
79
|
const targets = nodes.map(node => node.data.element.id)
|
|
@@ -83,8 +86,11 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
return (
|
|
86
|
-
<Stack>
|
|
89
|
+
<Stack gap={'xs'}>
|
|
87
90
|
<div>
|
|
91
|
+
<Text fz={rem(9)} fw={'500'} c={'dimmed'}>
|
|
92
|
+
ELEMENT{rest.length > 0 ? 'S' : ``}
|
|
93
|
+
</Text>
|
|
88
94
|
<Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
|
|
89
95
|
{rest.length === 0 ? firstNode.data.element.title : `[ multiple ]`}
|
|
90
96
|
</Text>
|
|
@@ -105,11 +111,13 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
105
111
|
{showOpacityOption && (
|
|
106
112
|
<div key={firstNode.id}>
|
|
107
113
|
<Divider label="opacity and border" labelPosition="left" />
|
|
114
|
+
<Space h={'xs'} />
|
|
108
115
|
<OpacityOption
|
|
109
116
|
node={firstNode}
|
|
110
117
|
onOpacityChange={(opacity: number) => {
|
|
111
118
|
triggerChange({ opacity })
|
|
112
119
|
}} />
|
|
120
|
+
<Space h={'sm'} />
|
|
113
121
|
<BorderStyleOption
|
|
114
122
|
node={firstNode}
|
|
115
123
|
onChange={(border: BorderStyle) => {
|
|
@@ -121,7 +129,7 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
121
129
|
<NavigateToOption nodes={nodes} />
|
|
122
130
|
</Stack>
|
|
123
131
|
)
|
|
124
|
-
}
|
|
132
|
+
}, (prevProps, nextProps) => shallowEqual(prevProps.selectedNodeIds, nextProps.selectedNodeIds))
|
|
125
133
|
|
|
126
134
|
function Colors({
|
|
127
135
|
nodes: [firstNode, ...nodes],
|
|
@@ -242,26 +250,27 @@ function NavigateToOption({
|
|
|
242
250
|
return (
|
|
243
251
|
<div>
|
|
244
252
|
<Divider label="navigate to" labelPosition="left" />
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
253
|
+
{isMultiple && (
|
|
254
|
+
<Text size="xs" c={'dimmed'}>
|
|
255
|
+
{'[ multiple ]'}
|
|
256
|
+
</Text>
|
|
257
|
+
)}
|
|
258
|
+
{!isMultiple && (
|
|
259
|
+
<>
|
|
260
|
+
<Space h={'xs'} />
|
|
261
|
+
<Select
|
|
262
|
+
size="xs"
|
|
263
|
+
variant="filled"
|
|
264
|
+
value={node.data.element.navigateTo ?? null}
|
|
265
|
+
placeholder={'select'}
|
|
266
|
+
data={node.data.element.navigateTo
|
|
267
|
+
? [{ value: node.data.element.navigateTo, label: node.data.element.navigateTo }]
|
|
268
|
+
: []}
|
|
269
|
+
allowDeselect={true}
|
|
270
|
+
checkIconPosition="right"
|
|
271
|
+
/>
|
|
272
|
+
</>
|
|
273
|
+
)}
|
|
265
274
|
</div>
|
|
266
275
|
)
|
|
267
276
|
}
|
|
@@ -281,7 +290,6 @@ function OpacityOption({
|
|
|
281
290
|
|
|
282
291
|
return (
|
|
283
292
|
<Slider
|
|
284
|
-
mt={'xs'}
|
|
285
293
|
size={'sm'}
|
|
286
294
|
color={'dark'}
|
|
287
295
|
value={value}
|
|
@@ -304,7 +312,7 @@ function BorderStyleOption({
|
|
|
304
312
|
}, [selecteBorderStyle])
|
|
305
313
|
|
|
306
314
|
return (
|
|
307
|
-
<Box
|
|
315
|
+
<Box>
|
|
308
316
|
<SegmentedControl
|
|
309
317
|
size="xs"
|
|
310
318
|
fullWidth
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
const Open = (props: SVGProps<SVGMarkerElement>) => (
|
|
4
|
+
<marker
|
|
5
|
+
viewBox="-4 -4 14 16"
|
|
6
|
+
refX={5}
|
|
7
|
+
refY={4}
|
|
8
|
+
markerWidth="7"
|
|
9
|
+
markerHeight="8"
|
|
10
|
+
preserveAspectRatio="xMaxYMid meet"
|
|
11
|
+
orient="auto-start-reverse"
|
|
12
|
+
{...props}
|
|
13
|
+
>
|
|
14
|
+
<path
|
|
15
|
+
d="M0,0 L7,4 L0,8 L4,4 Z"
|
|
16
|
+
stroke="context-stroke"
|
|
17
|
+
fill="context-stroke"
|
|
18
|
+
strokeDasharray={0}
|
|
19
|
+
strokeWidth={1}
|
|
20
|
+
strokeLinecap={'round'} />
|
|
21
|
+
</marker>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const Arrow = (props: SVGProps<SVGMarkerElement>) => (
|
|
25
|
+
<marker
|
|
26
|
+
viewBox="-1 -1 12 10"
|
|
27
|
+
refX={4}
|
|
28
|
+
refY={3}
|
|
29
|
+
markerWidth="8"
|
|
30
|
+
markerHeight="6"
|
|
31
|
+
preserveAspectRatio="xMaxYMid meet"
|
|
32
|
+
orient="auto-start-reverse"
|
|
33
|
+
{...props}>
|
|
34
|
+
<path
|
|
35
|
+
d="M 0 0 L 8 3 L 0 6 L 1 3 z"
|
|
36
|
+
fill="context-stroke"
|
|
37
|
+
strokeWidth={0}
|
|
38
|
+
/>
|
|
39
|
+
</marker>
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const OArrow = (props: SVGProps<SVGMarkerElement>) => (
|
|
43
|
+
<marker
|
|
44
|
+
viewBox="-1 -1 12 10"
|
|
45
|
+
refX={4}
|
|
46
|
+
refY={3}
|
|
47
|
+
markerWidth="8"
|
|
48
|
+
markerHeight="6"
|
|
49
|
+
preserveAspectRatio="xMaxYMid meet"
|
|
50
|
+
orient="auto-start-reverse"
|
|
51
|
+
{...props}>
|
|
52
|
+
<path
|
|
53
|
+
d="M 0 0 L 8 3 L 0 6 L 1 3 z"
|
|
54
|
+
stroke="context-stroke"
|
|
55
|
+
fill="var(--likec4-background-color)"
|
|
56
|
+
strokeWidth={1.25}
|
|
57
|
+
strokeLinejoin="miter"
|
|
58
|
+
strokeLinecap={'square'}
|
|
59
|
+
/>
|
|
60
|
+
</marker>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const Diamond = (props: SVGProps<SVGMarkerElement>) => (
|
|
64
|
+
<marker
|
|
65
|
+
viewBox="-4 -4 16 14"
|
|
66
|
+
refX={5}
|
|
67
|
+
refY={4}
|
|
68
|
+
markerWidth="10"
|
|
69
|
+
markerHeight="8"
|
|
70
|
+
preserveAspectRatio="xMaxYMid meet"
|
|
71
|
+
orient="auto-start-reverse"
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<path
|
|
75
|
+
d="M5,0 L10,4 L5,8 L0,4 Z"
|
|
76
|
+
fill="context-stroke"
|
|
77
|
+
strokeWidth={0}
|
|
78
|
+
strokeLinecap={'round'}
|
|
79
|
+
/>
|
|
80
|
+
</marker>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const ODiamond = (props: SVGProps<SVGMarkerElement>) => (
|
|
84
|
+
<marker
|
|
85
|
+
viewBox="-4 -4 16 14"
|
|
86
|
+
refX={6}
|
|
87
|
+
refY={4}
|
|
88
|
+
markerWidth="10"
|
|
89
|
+
markerHeight="8"
|
|
90
|
+
preserveAspectRatio="xMaxYMid meet"
|
|
91
|
+
orient="auto-start-reverse"
|
|
92
|
+
{...props}>
|
|
93
|
+
<path
|
|
94
|
+
d="M5,0 L10,4 L5,8 L0,4 Z"
|
|
95
|
+
stroke="context-stroke"
|
|
96
|
+
fill="var(--likec4-background-color)"
|
|
97
|
+
strokeWidth={1.25}
|
|
98
|
+
strokeLinecap={'round'}
|
|
99
|
+
/>
|
|
100
|
+
</marker>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const Dot = (props: SVGProps<SVGMarkerElement>) => (
|
|
104
|
+
<marker
|
|
105
|
+
viewBox="0 0 10 10"
|
|
106
|
+
refX={4}
|
|
107
|
+
refY={4}
|
|
108
|
+
markerWidth="6"
|
|
109
|
+
markerHeight="6"
|
|
110
|
+
{...props}>
|
|
111
|
+
<circle
|
|
112
|
+
strokeWidth={0}
|
|
113
|
+
fill="context-stroke"
|
|
114
|
+
cx={4}
|
|
115
|
+
cy={4}
|
|
116
|
+
r={3}
|
|
117
|
+
/>
|
|
118
|
+
</marker>
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const ODot = (props: SVGProps<SVGMarkerElement>) => (
|
|
122
|
+
<marker
|
|
123
|
+
viewBox="0 0 10 10"
|
|
124
|
+
refX={4}
|
|
125
|
+
refY={4}
|
|
126
|
+
markerWidth="6"
|
|
127
|
+
markerHeight="6"
|
|
128
|
+
{...props}>
|
|
129
|
+
<circle
|
|
130
|
+
strokeWidth={1.25}
|
|
131
|
+
stroke="context-stroke"
|
|
132
|
+
fill="var(--likec4-background-color)"
|
|
133
|
+
cx={4}
|
|
134
|
+
cy={4}
|
|
135
|
+
r={3}
|
|
136
|
+
/>
|
|
137
|
+
</marker>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
export const EdgeMarkers = {
|
|
141
|
+
Arrow,
|
|
142
|
+
OArrow,
|
|
143
|
+
Open,
|
|
144
|
+
Diamond,
|
|
145
|
+
ODiamond,
|
|
146
|
+
Dot,
|
|
147
|
+
ODot
|
|
148
|
+
}
|
|
@@ -2,7 +2,7 @@ import { invariant } from '@likec4/core'
|
|
|
2
2
|
import { useDebouncedEffect } from '@react-hookz/web'
|
|
3
3
|
import { shallowEqual } from 'fast-equals'
|
|
4
4
|
import { useEffect, useRef } from 'react'
|
|
5
|
-
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
|
|
5
|
+
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state/hooks'
|
|
6
6
|
import { useXYStore, useXYStoreApi, type XYStoreApi } from './hooks'
|
|
7
7
|
import type { XYFlowState } from './types'
|
|
8
8
|
import { toDomPrecision } from './utils'
|
|
@@ -24,11 +24,11 @@ function FitViewOnViewportResize({ diagramApi, xyflowApi }: {
|
|
|
24
24
|
if (focusedNodeId || prevDimensionsRef.current === dimensions) {
|
|
25
25
|
return
|
|
26
26
|
}
|
|
27
|
-
fitDiagram(
|
|
27
|
+
fitDiagram(300)
|
|
28
28
|
prevDimensionsRef.current = dimensions
|
|
29
29
|
},
|
|
30
30
|
[dimensions, diagramApi, xyflowApi],
|
|
31
|
-
|
|
31
|
+
300,
|
|
32
32
|
1000
|
|
33
33
|
)
|
|
34
34
|
|
|
@@ -77,6 +77,7 @@ export function FitViewOnDiagramChange() {
|
|
|
77
77
|
} = useDiagramState(selector, shallowEqual)
|
|
78
78
|
const diagramApi = useDiagramStoreApi()
|
|
79
79
|
const processedRef = useRef(pendingViewId)
|
|
80
|
+
const durationRef = useRef(300)
|
|
80
81
|
|
|
81
82
|
useEffect(() => {
|
|
82
83
|
if (!xyflowSynced || pendingViewId === processedRef.current) {
|
|
@@ -97,11 +98,14 @@ export function FitViewOnDiagramChange() {
|
|
|
97
98
|
y: toDomPrecision(fromPos.y - toPos.y)
|
|
98
99
|
}
|
|
99
100
|
xyflowApi.getState().panBy(diff)
|
|
101
|
+
durationRef.current = 500
|
|
100
102
|
diagramApi.setState({ lastOnNavigate: null })
|
|
101
103
|
return
|
|
102
104
|
}
|
|
103
105
|
processedRef.current = pendingViewId
|
|
104
|
-
diagramApi.getState().fitDiagram(
|
|
106
|
+
diagramApi.getState().fitDiagram(durationRef.current)
|
|
107
|
+
// Reset duration
|
|
108
|
+
durationRef.current = 300
|
|
105
109
|
}, [pendingViewId, xyflowSynced, waitCorrection])
|
|
106
110
|
|
|
107
111
|
if (!viewportChanged && xyflowSynced) {
|
|
@@ -1,72 +1,76 @@
|
|
|
1
1
|
import { nonNullable } from '@likec4/core'
|
|
2
2
|
import type { EdgeChange, NodeChange } from '@xyflow/react'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getBoundsOfRects, getViewportForBounds, nodeToRect } from '@xyflow/system'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { useDiagramStoreApi } from '../state/hooks'
|
|
5
6
|
import { MinZoom } from './const'
|
|
6
|
-
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
7
7
|
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
8
8
|
|
|
9
9
|
export function SelectEdgesOnNodeFocus() {
|
|
10
|
-
const focusedNodeId = useDiagramState(s => s.focusedNodeId)
|
|
11
10
|
const diagramStore = useDiagramStoreApi()
|
|
12
|
-
const xyflowApi = useXYStoreApi()
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
useEffect(
|
|
13
|
+
() =>
|
|
14
|
+
diagramStore.subscribe(s => s.focusedNodeId, focusedNodeId => {
|
|
15
|
+
if (!focusedNodeId) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
|
|
20
|
+
const nodeChanges = [] as NodeChange<XYFlowNode>[]
|
|
21
|
+
const {
|
|
22
|
+
edgeLookup,
|
|
23
|
+
nodeLookup,
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
panZoom,
|
|
27
|
+
transform,
|
|
28
|
+
triggerNodeChanges,
|
|
29
|
+
triggerEdgeChanges
|
|
30
|
+
} = diagramStore.getState().xystore.getState()
|
|
31
|
+
let focusBounds = nodeToRect(nonNullable(nodeLookup.get(focusedNodeId)))
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
for (const [, edge] of edgeLookup) {
|
|
34
|
+
if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
|
|
35
|
+
const anotherNodeId = edge.source === focusedNodeId ? edge.target : edge.source
|
|
36
|
+
const nd = nodeToRect(nonNullable(nodeLookup.get(anotherNodeId)))
|
|
37
|
+
focusBounds = getBoundsOfRects(focusBounds, nd)
|
|
38
|
+
}
|
|
39
|
+
if (edge.selected) {
|
|
40
|
+
edgeChanges.push({
|
|
41
|
+
id: edge.id,
|
|
42
|
+
type: 'select',
|
|
43
|
+
selected: false
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [, node] of nodeLookup) {
|
|
48
|
+
if (node.selected && node.id !== focusedNodeId) {
|
|
49
|
+
nodeChanges.push({
|
|
50
|
+
id: node.id,
|
|
51
|
+
type: 'select',
|
|
52
|
+
selected: false
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
nodeChanges.push({
|
|
46
|
-
id:
|
|
57
|
+
id: focusedNodeId,
|
|
47
58
|
type: 'select',
|
|
48
|
-
selected:
|
|
59
|
+
selected: true
|
|
49
60
|
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
type: 'select',
|
|
55
|
-
selected: true
|
|
56
|
-
})
|
|
57
|
-
if (edgeChanges.length > 0) {
|
|
58
|
-
triggerEdgeChanges(edgeChanges)
|
|
59
|
-
}
|
|
60
|
-
triggerNodeChanges(nodeChanges)
|
|
61
|
+
if (edgeChanges.length > 0) {
|
|
62
|
+
triggerEdgeChanges(edgeChanges)
|
|
63
|
+
}
|
|
64
|
+
triggerNodeChanges(nodeChanges)
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
const maxZoom = Math.max(1, transform[2])
|
|
67
|
+
const viewport = getViewportForBounds(focusBounds, width, height, MinZoom, maxZoom, 0.2)
|
|
68
|
+
panZoom?.setViewport(viewport, {
|
|
69
|
+
duration: 350
|
|
70
|
+
})
|
|
71
|
+
}),
|
|
72
|
+
[diagramStore]
|
|
73
|
+
)
|
|
70
74
|
|
|
71
75
|
return null
|
|
72
76
|
}
|