@likec4/diagram 1.2.2 → 1.4.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.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +27 -4
- package/dist/index.mjs +1543 -1346
- package/dist/state/DiagramContext.d.ts +2353 -264
- package/dist/state/diagramStore.d.ts +2360 -268
- package/dist/state/index.d.ts +0 -1
- package/dist/state/useDiagramStore.d.ts +3 -2534
- package/dist/state/useNavigationHistory.d.ts +5 -0
- package/dist/style.css +194 -131
- package/dist/theme.css.d.ts +5 -1
- package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
- package/dist/ui/BackwardForwardPanel.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
- package/dist/xyflow/XYFlow.d.ts +2 -1
- package/dist/xyflow/XYFlowEvents.d.ts +4 -4
- package/dist/xyflow/XYFlowInner.d.ts +2 -1
- package/dist/xyflow/const.d.ts +1 -1
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -5
- package/dist/xyflow/edges/utils.d.ts +4 -2
- package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/index.d.ts +1 -1
- package/dist/xyflow/types.d.ts +3 -3
- package/dist/xyflow/utils.d.ts +3 -0
- package/package.json +18 -17
- package/src/LikeC4Diagram.css.ts +15 -5
- package/src/LikeC4Diagram.props.ts +33 -5
- package/src/LikeC4Diagram.tsx +103 -32
- package/src/globals.css.ts +1 -2
- package/src/mantine/theme.ts +1 -0
- package/src/state/DiagramContext.tsx +3 -4
- package/src/state/diagramStore.ts +177 -103
- package/src/state/index.ts +0 -1
- package/src/state/useDiagramStore.ts +6 -13
- package/src/state/useNavigationHistory.ts +39 -0
- package/src/theme.css.ts +5 -1
- package/src/ui/BackwardForwardPanel.css.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +59 -0
- package/src/ui/DiagramTitlePanel.css.ts +11 -5
- package/src/ui/DiagramTitlePanel.tsx +108 -96
- package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
- package/src/ui/DynamicViewWalkthrough.tsx +36 -17
- package/src/ui/OptionsPanel.tsx +28 -19
- package/src/ui/options/NodeOptions.tsx +16 -19
- package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
- package/src/xyflow/SyncWithDiagram.tsx +21 -2
- package/src/xyflow/XYFlow.tsx +20 -23
- package/src/xyflow/XYFlowEvents.tsx +26 -8
- package/src/xyflow/XYFlowInner.tsx +10 -6
- package/src/xyflow/const.ts +1 -1
- package/src/xyflow/diagram-to-xyflow.ts +4 -41
- package/src/xyflow/edges/RelationshipEdge.tsx +160 -111
- package/src/xyflow/edges/edges.css.ts +41 -26
- package/src/xyflow/edges/utils.ts +5 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +19 -4
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +45 -37
- package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
- package/src/xyflow/nodes/element/ElementNode.tsx +27 -14
- package/src/xyflow/nodes/element/element.css.ts +31 -18
- package/src/xyflow/nodes/element/index.ts +1 -1
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
- package/src/xyflow/types.ts +5 -2
- package/src/xyflow/utils.ts +48 -0
- package/dist/ui/index.d.ts +0 -2
- package/src/ui/index.ts +0 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { globalStyle, style } from '@vanilla-extract/css'
|
|
1
|
+
import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
|
|
2
2
|
import { mantine } from '../mantine.css'
|
|
3
|
+
import { vars } from '../theme.css'
|
|
3
4
|
|
|
4
5
|
export const container = style({
|
|
5
6
|
bottom: 0,
|
|
@@ -8,17 +9,22 @@ export const container = style({
|
|
|
8
9
|
margin: 0
|
|
9
10
|
})
|
|
10
11
|
|
|
11
|
-
export const
|
|
12
|
+
export const card = style({
|
|
12
13
|
cursor: 'default',
|
|
13
|
-
minWidth:
|
|
14
|
+
minWidth: 150,
|
|
14
15
|
maxWidth: 'calc(100vw - 16px)',
|
|
15
16
|
backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
|
|
16
|
-
WebkitBackdropFilter: 'blur(8px)',
|
|
17
|
-
backdropFilter: 'blur(8px)',
|
|
17
|
+
WebkitBackdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
|
|
18
|
+
backdropFilter: fallbackVar(vars.safariAnimationHook, 'blur(8px)'),
|
|
18
19
|
'@media': {
|
|
19
20
|
[mantine.largerThan('md')]: {
|
|
20
21
|
maxWidth: '45vw'
|
|
21
22
|
}
|
|
23
|
+
},
|
|
24
|
+
selectors: {
|
|
25
|
+
[`:where([data-mantine-color-scheme='dark']) &`]: {
|
|
26
|
+
backgroundColor: `color-mix(in srgb, ${mantine.colors.dark[6]}, transparent 20%)`
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
})
|
|
24
30
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Anchor, Box, Button, CopyButton, Group,
|
|
1
|
+
import { Anchor, Box, Button, Card, CardSection, CopyButton, Group, Spoiler, Stack, Text } from '@mantine/core'
|
|
2
2
|
import { useLocalStorage } from '@mantine/hooks'
|
|
3
|
+
import { IconMenu } from '@tabler/icons-react'
|
|
3
4
|
import clsx from 'clsx'
|
|
4
|
-
import { AnimatePresence,
|
|
5
|
+
import { AnimatePresence, m } from 'framer-motion'
|
|
5
6
|
import { type DiagramState, useDiagramState } from '../state'
|
|
6
7
|
import * as css from './DiagramTitlePanel.css'
|
|
7
8
|
|
|
@@ -22,7 +23,7 @@ export default function DiagramTitlePanel() {
|
|
|
22
23
|
|
|
23
24
|
return (
|
|
24
25
|
<AnimatePresence mode="wait">
|
|
25
|
-
<
|
|
26
|
+
<m.div
|
|
26
27
|
key={id}
|
|
27
28
|
initial={{ opacity: 0.05, scale: 0.7 }}
|
|
28
29
|
animate={{ opacity: 1, scale: 1 }}
|
|
@@ -35,104 +36,115 @@ export default function DiagramTitlePanel() {
|
|
|
35
36
|
transformOrigin: 'left center'
|
|
36
37
|
}}
|
|
37
38
|
>
|
|
38
|
-
<
|
|
39
|
+
<Card
|
|
39
40
|
radius="sm"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
p={isCollapsed ? 'xs' : 'lg'}
|
|
41
|
+
className={css.card}
|
|
42
|
+
p={'md'}
|
|
43
43
|
onDoubleClick={e => e.stopPropagation()}>
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
44
|
+
<CardSection mb={4}>
|
|
45
|
+
<Button
|
|
46
|
+
fullWidth
|
|
47
|
+
size="xs"
|
|
48
|
+
h={'sm'}
|
|
49
|
+
py={2}
|
|
50
|
+
radius={'0'}
|
|
51
|
+
variant="subtle"
|
|
52
|
+
color="gray"
|
|
53
|
+
onClick={toggle}
|
|
54
|
+
>
|
|
55
|
+
<IconMenu size={11} opacity={0.7} />
|
|
56
|
+
</Button>
|
|
57
|
+
</CardSection>
|
|
58
|
+
<Group justify="stretch" wrap="nowrap" mb={isCollapsed ? 0 : 'sm'}>
|
|
59
|
+
<Text
|
|
60
|
+
component={'div'}
|
|
61
|
+
flex={'1'}
|
|
62
|
+
size={isCollapsed ? 'sm' : 'lg'}
|
|
63
|
+
fw={500}
|
|
64
|
+
lh={1.1}
|
|
65
|
+
className={css.title}
|
|
66
|
+
>
|
|
67
|
+
{title}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text
|
|
70
|
+
hidden={isCollapsed}
|
|
71
|
+
component={'div'}
|
|
72
|
+
flex={'0 0 auto'}
|
|
73
|
+
inline
|
|
74
|
+
size="xs"
|
|
75
|
+
fz={9}
|
|
76
|
+
fw={500}
|
|
77
|
+
c={'dimmed'}
|
|
78
|
+
style={{
|
|
79
|
+
userSelect: 'all'
|
|
80
|
+
}}>
|
|
81
|
+
<span style={{ userSelect: 'none' }}>id:{' '}</span>
|
|
82
|
+
{id}
|
|
83
|
+
</Text>
|
|
84
|
+
</Group>
|
|
85
|
+
{!isCollapsed && (
|
|
86
|
+
<>
|
|
87
|
+
{description && (
|
|
88
|
+
<Spoiler
|
|
89
|
+
maxHeight={42}
|
|
90
|
+
showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
|
|
91
|
+
hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
|
|
88
92
|
<Text
|
|
89
93
|
component={'div'}
|
|
90
|
-
size="
|
|
91
|
-
|
|
92
|
-
no description
|
|
94
|
+
size="sm"
|
|
95
|
+
className={css.description}>
|
|
96
|
+
{description || 'no description'}
|
|
93
97
|
</Text>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
98
|
+
</Spoiler>
|
|
99
|
+
)}
|
|
100
|
+
{!description && (
|
|
101
|
+
<Text
|
|
102
|
+
component={'div'}
|
|
103
|
+
size="xs"
|
|
104
|
+
c={'dimmed'}>
|
|
105
|
+
no description
|
|
106
|
+
</Text>
|
|
107
|
+
)}
|
|
108
|
+
{links && (
|
|
109
|
+
<Stack
|
|
110
|
+
gap={3}
|
|
111
|
+
justify="stretch"
|
|
112
|
+
align="stretch">
|
|
113
|
+
{links.map((link) => (
|
|
114
|
+
<Group key={link} wrap="nowrap" align="center" gap={'sm'}>
|
|
115
|
+
<Box flex={'1'} style={{ overflow: 'hidden' }}>
|
|
116
|
+
<Anchor
|
|
117
|
+
href={link}
|
|
118
|
+
target="_blank"
|
|
119
|
+
fz="xs"
|
|
120
|
+
truncate="end"
|
|
121
|
+
display={'inline-block'}
|
|
122
|
+
w={'100%'}>
|
|
123
|
+
{link}
|
|
124
|
+
</Anchor>
|
|
125
|
+
</Box>
|
|
126
|
+
<CopyButton value={link}>
|
|
127
|
+
{({ copied, copy }) => (
|
|
128
|
+
<Button
|
|
129
|
+
size="compact-xs"
|
|
130
|
+
fz={'10'}
|
|
131
|
+
variant="light"
|
|
132
|
+
onClick={copy}
|
|
133
|
+
color={copied
|
|
134
|
+
? 'teal'
|
|
135
|
+
: 'gray'}>
|
|
136
|
+
{copied ? 'copied' : 'copy'}
|
|
137
|
+
</Button>
|
|
138
|
+
)}
|
|
139
|
+
</CopyButton>
|
|
140
|
+
</Group>
|
|
141
|
+
))}
|
|
142
|
+
</Stack>
|
|
143
|
+
)}
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
</Card>
|
|
147
|
+
</m.div>
|
|
136
148
|
</AnimatePresence>
|
|
137
149
|
)
|
|
138
150
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Box, Button } from '@mantine/core'
|
|
2
|
+
import { useHotkeys } from '@mantine/hooks'
|
|
2
3
|
import {
|
|
3
|
-
IconHeart,
|
|
4
|
-
IconPhoto,
|
|
5
4
|
IconPlayerPlayFilled,
|
|
6
5
|
IconPlayerSkipBackFilled,
|
|
7
6
|
IconPlayerSkipForwardFilled,
|
|
8
|
-
IconPlayerStopFilled
|
|
9
|
-
IconSettings
|
|
7
|
+
IconPlayerStopFilled
|
|
10
8
|
} from '@tabler/icons-react'
|
|
11
9
|
import clsx from 'clsx'
|
|
12
10
|
import { isNullish, isNumber } from 'remeda'
|
|
13
11
|
import { useDiagramState } from '../state/useDiagramStore'
|
|
12
|
+
import { useXYStore } from '../xyflow/hooks/useXYFlow'
|
|
14
13
|
import * as css from './DynamicViewWalkthrough.css'
|
|
15
14
|
|
|
16
15
|
export function DynamicViewWalkthrough() {
|
|
16
|
+
const isMobile = useXYStore(s => s.width <= 750)
|
|
17
17
|
const {
|
|
18
18
|
nextDynamicStep,
|
|
19
19
|
stopDynamicView,
|
|
@@ -26,10 +26,28 @@ export function DynamicViewWalkthrough() {
|
|
|
26
26
|
hasNextSteps: (s.activeDynamicViewStep ?? 1) < s.view.edges.length
|
|
27
27
|
}))
|
|
28
28
|
|
|
29
|
+
const isActive = isNumber(activeDynamicViewStep)
|
|
30
|
+
|
|
31
|
+
useHotkeys(
|
|
32
|
+
isActive
|
|
33
|
+
? [
|
|
34
|
+
['ArrowLeft', () => nextDynamicStep(-1)],
|
|
35
|
+
['ArrowRight', () => nextDynamicStep()],
|
|
36
|
+
['Escape', (e) => {
|
|
37
|
+
e.stopImmediatePropagation()
|
|
38
|
+
stopDynamicView()
|
|
39
|
+
}, { preventDefault: true }]
|
|
40
|
+
]
|
|
41
|
+
: [
|
|
42
|
+
['ArrowLeft', () => nextDynamicStep()],
|
|
43
|
+
['ArrowRight', () => nextDynamicStep()]
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
|
|
29
47
|
const buttonProps = {
|
|
30
48
|
className: css.btn,
|
|
31
|
-
size: 'compact-
|
|
32
|
-
radius: 'xl'
|
|
49
|
+
size: isMobile ? 'compact-md' : 'lg',
|
|
50
|
+
radius: isMobile ? 'lg' : 'xl'
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
const nextStep = (increment = 1) => (e: React.MouseEvent) => {
|
|
@@ -64,21 +82,22 @@ export function DynamicViewWalkthrough() {
|
|
|
64
82
|
|
|
65
83
|
<Button
|
|
66
84
|
{...buttonProps}
|
|
67
|
-
px={'
|
|
85
|
+
px={hasNextSteps ? 'md' : 'xl'}
|
|
68
86
|
onClick={e => {
|
|
69
87
|
e.stopPropagation()
|
|
70
88
|
stopDynamicView()
|
|
71
89
|
}}>
|
|
72
|
-
<IconPlayerStopFilled />
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<Button
|
|
76
|
-
{...buttonProps}
|
|
77
|
-
pr={'lg'}
|
|
78
|
-
disabled={!hasNextSteps}
|
|
79
|
-
onClick={nextStep()}>
|
|
80
|
-
<IconPlayerSkipForwardFilled />
|
|
90
|
+
{hasNextSteps && <IconPlayerStopFilled />}
|
|
91
|
+
{!hasNextSteps && 'End'}
|
|
81
92
|
</Button>
|
|
93
|
+
{hasNextSteps && (
|
|
94
|
+
<Button
|
|
95
|
+
{...buttonProps}
|
|
96
|
+
pr={'lg'}
|
|
97
|
+
onClick={nextStep()}>
|
|
98
|
+
<IconPlayerSkipForwardFilled />
|
|
99
|
+
</Button>
|
|
100
|
+
)}
|
|
82
101
|
</Button.Group>
|
|
83
102
|
)}
|
|
84
103
|
</Box>
|
package/src/ui/OptionsPanel.tsx
CHANGED
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
import { Card } from '@mantine/core'
|
|
2
|
-
import { useOnSelectionChange } from '@xyflow/react'
|
|
3
2
|
import clsx from 'clsx'
|
|
3
|
+
import { DEV } from 'esm-env'
|
|
4
4
|
import { shallowEqual } from 'fast-equals'
|
|
5
|
-
import { AnimatePresence,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { AnimatePresence, m } from 'framer-motion'
|
|
6
|
+
import { useCallback } from 'react'
|
|
7
|
+
import { reduce } from 'remeda'
|
|
8
|
+
import useTilg from 'tilg'
|
|
8
9
|
import { useDiagramState } from '../state'
|
|
10
|
+
import { useXYStore } from '../xyflow/hooks'
|
|
9
11
|
import { NodeOptions } from './options/NodeOptions'
|
|
10
12
|
import * as styles from './OptionsPanel.css'
|
|
11
13
|
|
|
14
|
+
const Empty = [] as string[]
|
|
12
15
|
export default function OptionsPanel() {
|
|
13
|
-
const isFocusDisabled = useDiagramState(s =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
const { isFocusDisabled, viewId } = useDiagramState(s => ({
|
|
17
|
+
viewId: s.view.id,
|
|
18
|
+
isFocusDisabled: s.focusedNodeId === null && s.activeDynamicViewStep === null
|
|
19
|
+
}))
|
|
20
|
+
const selectedNodes = useXYStore(
|
|
21
|
+
useCallback(s =>
|
|
22
|
+
s.elementsSelectable && isFocusDisabled
|
|
23
|
+
? reduce(s.nodes, (acc, n) => {
|
|
24
|
+
if (n.selected) {
|
|
25
|
+
acc.push(n.id)
|
|
26
|
+
}
|
|
27
|
+
return acc
|
|
28
|
+
}, [] as string[])
|
|
29
|
+
: Empty, [isFocusDisabled]),
|
|
30
|
+
shallowEqual
|
|
31
|
+
)
|
|
32
|
+
DEV && useTilg()
|
|
25
33
|
return (
|
|
26
34
|
<AnimatePresence mode="wait">
|
|
27
|
-
{
|
|
28
|
-
<
|
|
35
|
+
{selectedNodes.length > 0 && (
|
|
36
|
+
<m.div
|
|
37
|
+
key={viewId}
|
|
29
38
|
initial={{ opacity: 0, scale: 0.85 }}
|
|
30
39
|
animate={{ opacity: 1, scale: 1 }}
|
|
31
40
|
exit={{
|
|
@@ -41,7 +50,7 @@ export default function OptionsPanel() {
|
|
|
41
50
|
<Card shadow="sm">
|
|
42
51
|
<NodeOptions selectedNodeIds={selectedNodes} />
|
|
43
52
|
</Card>
|
|
44
|
-
</
|
|
53
|
+
</m.div>
|
|
45
54
|
)}
|
|
46
55
|
</AnimatePresence>
|
|
47
56
|
)
|
|
@@ -22,17 +22,14 @@ import {
|
|
|
22
22
|
Tooltip,
|
|
23
23
|
TooltipGroup
|
|
24
24
|
} from '@mantine/core'
|
|
25
|
-
import {
|
|
25
|
+
import { useState } from 'react'
|
|
26
26
|
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
27
|
+
import { useUpdateEffect } from '../../hooks/useUpdateEffect'
|
|
27
28
|
import type { Changes } from '../../LikeC4Diagram.props'
|
|
28
29
|
import { useDiagramStoreApi } from '../../state'
|
|
29
30
|
import { useXYNodesData } from '../../xyflow/hooks'
|
|
30
31
|
import { XYFlowNode } from '../../xyflow/types'
|
|
31
32
|
|
|
32
|
-
// const ColorPanel = () => {
|
|
33
|
-
// const selectedNodes = useStore(state => state.nodeInternals
|
|
34
|
-
// }
|
|
35
|
-
|
|
36
33
|
const {
|
|
37
34
|
primary,
|
|
38
35
|
secondary,
|
|
@@ -78,20 +75,20 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
78
75
|
const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
|
|
79
76
|
const targets = nodes.map(node => node.data.element.id)
|
|
80
77
|
invariant(hasAtLeast(targets, 1), 'At least one target is required')
|
|
81
|
-
diagramApi.getState().
|
|
78
|
+
diagramApi.getState().triggerChangeElementStyle({
|
|
82
79
|
op: 'change-element-style',
|
|
83
80
|
style,
|
|
84
81
|
targets
|
|
85
|
-
}
|
|
82
|
+
})
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
return (
|
|
89
86
|
<Stack>
|
|
90
|
-
<
|
|
87
|
+
<div>
|
|
91
88
|
<Text size="xs" c={rest.length > 0 ? 'dimmed' : ''} truncate>
|
|
92
89
|
{rest.length === 0 ? firstNode.data.element.title : `[ multiple ]`}
|
|
93
90
|
</Text>
|
|
94
|
-
</
|
|
91
|
+
</div>
|
|
95
92
|
{showShapeOption && (
|
|
96
93
|
<ShapeOption
|
|
97
94
|
nodes={nodes}
|
|
@@ -106,7 +103,7 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
106
103
|
}} />
|
|
107
104
|
|
|
108
105
|
{showOpacityOption && (
|
|
109
|
-
<
|
|
106
|
+
<div key={firstNode.id}>
|
|
110
107
|
<Divider label="opacity and border" labelPosition="left" />
|
|
111
108
|
<OpacityOption
|
|
112
109
|
node={firstNode}
|
|
@@ -119,7 +116,7 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
119
116
|
triggerChange({ border })
|
|
120
117
|
}}
|
|
121
118
|
/>
|
|
122
|
-
</
|
|
119
|
+
</div>
|
|
123
120
|
)}
|
|
124
121
|
<NavigateToOption nodes={nodes} />
|
|
125
122
|
</Stack>
|
|
@@ -148,7 +145,7 @@ function Colors({
|
|
|
148
145
|
onColorChange(color)
|
|
149
146
|
}
|
|
150
147
|
return (
|
|
151
|
-
<
|
|
148
|
+
<div>
|
|
152
149
|
<TooltipGroup openDelay={400} closeDelay={300}>
|
|
153
150
|
<Divider label="color" labelPosition="left" />
|
|
154
151
|
<Flex mt={'xs'} maw={150} gap="xs" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
|
|
@@ -194,7 +191,7 @@ function Colors({
|
|
|
194
191
|
))}
|
|
195
192
|
</Flex>
|
|
196
193
|
</TooltipGroup>
|
|
197
|
-
</
|
|
194
|
+
</div>
|
|
198
195
|
)
|
|
199
196
|
}
|
|
200
197
|
|
|
@@ -215,7 +212,7 @@ function ShapeOption({
|
|
|
215
212
|
})
|
|
216
213
|
|
|
217
214
|
return (
|
|
218
|
-
<
|
|
215
|
+
<div>
|
|
219
216
|
<Divider label="shape" labelPosition="left" />
|
|
220
217
|
<Select
|
|
221
218
|
mt={'xs'}
|
|
@@ -232,7 +229,7 @@ function ShapeOption({
|
|
|
232
229
|
}
|
|
233
230
|
onShapeChange(value as ElementShape)
|
|
234
231
|
}} />
|
|
235
|
-
</
|
|
232
|
+
</div>
|
|
236
233
|
)
|
|
237
234
|
}
|
|
238
235
|
|
|
@@ -243,7 +240,7 @@ function NavigateToOption({
|
|
|
243
240
|
}) {
|
|
244
241
|
const isMultiple = nodes.length >= 1
|
|
245
242
|
return (
|
|
246
|
-
<
|
|
243
|
+
<div>
|
|
247
244
|
<Divider label="navigate to" labelPosition="left" />
|
|
248
245
|
<Select
|
|
249
246
|
mt={'xs'}
|
|
@@ -265,7 +262,7 @@ function NavigateToOption({
|
|
|
265
262
|
// onShapeChange(value as ElementShape)
|
|
266
263
|
// }} />
|
|
267
264
|
/>
|
|
268
|
-
</
|
|
265
|
+
</div>
|
|
269
266
|
)
|
|
270
267
|
}
|
|
271
268
|
|
|
@@ -278,7 +275,7 @@ function OpacityOption({
|
|
|
278
275
|
}) {
|
|
279
276
|
let selectedOpacity = node.data.element.style.opacity ?? 100
|
|
280
277
|
const [value, setValue] = useState(selectedOpacity)
|
|
281
|
-
|
|
278
|
+
useUpdateEffect(() => {
|
|
282
279
|
setValue(selectedOpacity)
|
|
283
280
|
}, [selectedOpacity])
|
|
284
281
|
|
|
@@ -302,7 +299,7 @@ function BorderStyleOption({
|
|
|
302
299
|
}) {
|
|
303
300
|
let selecteBorderStyle = node.data.element.style.border ?? 'dashed'
|
|
304
301
|
const [value, setValue] = useState(selecteBorderStyle)
|
|
305
|
-
|
|
302
|
+
useUpdateEffect(() => {
|
|
306
303
|
setValue(selecteBorderStyle)
|
|
307
304
|
}, [selecteBorderStyle])
|
|
308
305
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { invariant } from '@likec4/core'
|
|
1
2
|
import { useDebouncedEffect } from '@react-hookz/web'
|
|
3
|
+
import { shallowEqual } from 'fast-equals'
|
|
2
4
|
import { useEffect, useRef } from 'react'
|
|
3
5
|
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
|
|
4
6
|
import { useXYStore, useXYStoreApi, type XYStoreApi } from './hooks'
|
|
@@ -26,8 +28,8 @@ function FitViewOnViewportResize({ diagramApi, xyflowApi }: {
|
|
|
26
28
|
prevDimensionsRef.current = dimensions
|
|
27
29
|
},
|
|
28
30
|
[dimensions, diagramApi, xyflowApi],
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
250,
|
|
32
|
+
1000
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
return null
|
|
@@ -72,7 +74,7 @@ export function FitViewOnDiagramChange() {
|
|
|
72
74
|
waitCorrection,
|
|
73
75
|
elFrom,
|
|
74
76
|
elTo
|
|
75
|
-
} = useDiagramState(selector)
|
|
77
|
+
} = useDiagramState(selector, shallowEqual)
|
|
76
78
|
const diagramApi = useDiagramStoreApi()
|
|
77
79
|
const processedRef = useRef(pendingViewId)
|
|
78
80
|
|
|
@@ -80,29 +82,19 @@ export function FitViewOnDiagramChange() {
|
|
|
80
82
|
if (!xyflowSynced || pendingViewId === processedRef.current) {
|
|
81
83
|
return
|
|
82
84
|
}
|
|
83
|
-
if (waitCorrection
|
|
84
|
-
|
|
85
|
+
if (waitCorrection) {
|
|
86
|
+
invariant(elTo, 'elTo should be defined')
|
|
87
|
+
invariant(elFrom, 'elFrom should be defined')
|
|
85
88
|
const { xyflow, lastOnNavigate } = diagramApi.getState()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (nextZoom !== 1 && nextZoom < zoom) {
|
|
92
|
-
xyflowApi.setState({ transform: [x, y, nextZoom] })
|
|
93
|
-
xyflow.setViewport({
|
|
94
|
-
x,
|
|
95
|
-
y,
|
|
96
|
-
zoom: nextZoom
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
const centerTo = xyflow.flowToScreenPosition({
|
|
100
|
-
x: elTo.position[0] + elTo.width / 2,
|
|
101
|
-
y: elTo.position[1] + elTo.height / 2
|
|
89
|
+
invariant(lastOnNavigate, 'lastOnNavigate should be defined')
|
|
90
|
+
const fromPos = lastOnNavigate.elementScreenPosition
|
|
91
|
+
const toPos = xyflow.flowToScreenPosition({
|
|
92
|
+
x: elTo.position[0], // + elFrom.width / 2,
|
|
93
|
+
y: elTo.position[1] // + elFrom.height / 2
|
|
102
94
|
}),
|
|
103
95
|
diff = {
|
|
104
|
-
x: toDomPrecision(
|
|
105
|
-
y: toDomPrecision(
|
|
96
|
+
x: toDomPrecision(fromPos.x - toPos.x),
|
|
97
|
+
y: toDomPrecision(fromPos.y - toPos.y)
|
|
106
98
|
}
|
|
107
99
|
xyflowApi.getState().panBy(diff)
|
|
108
100
|
diagramApi.setState({ lastOnNavigate: null })
|
|
@@ -30,10 +30,8 @@ export function SelectEdgesOnNodeFocus() {
|
|
|
30
30
|
type: 'select',
|
|
31
31
|
selected: true
|
|
32
32
|
})
|
|
33
|
-
const anotherNode = nodeLookup.get(edge.source === focusedNodeId ? edge.target : edge.source)
|
|
34
|
-
|
|
35
|
-
fitViewNodes.add(anotherNode)
|
|
36
|
-
}
|
|
33
|
+
const anotherNode = nonNullable(nodeLookup.get(edge.source === focusedNodeId ? edge.target : edge.source))
|
|
34
|
+
fitViewNodes.add(anotherNode)
|
|
37
35
|
} else if (edge.selected) {
|
|
38
36
|
edgeChanges.push({
|
|
39
37
|
id: edge.id,
|
|
@@ -62,8 +60,7 @@ export function SelectEdgesOnNodeFocus() {
|
|
|
62
60
|
triggerNodeChanges(nodeChanges)
|
|
63
61
|
|
|
64
62
|
fitView({
|
|
65
|
-
|
|
66
|
-
duration: 300,
|
|
63
|
+
duration: 350,
|
|
67
64
|
padding: 0.1,
|
|
68
65
|
minZoom: MinZoom,
|
|
69
66
|
maxZoom: Math.max(1, transform[2]),
|