@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.
Files changed (69) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +27 -4
  3. package/dist/index.mjs +1543 -1346
  4. package/dist/state/DiagramContext.d.ts +2353 -264
  5. package/dist/state/diagramStore.d.ts +2360 -268
  6. package/dist/state/index.d.ts +0 -1
  7. package/dist/state/useDiagramStore.d.ts +3 -2534
  8. package/dist/state/useNavigationHistory.d.ts +5 -0
  9. package/dist/style.css +194 -131
  10. package/dist/theme.css.d.ts +5 -1
  11. package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
  12. package/dist/ui/BackwardForwardPanel.d.ts +1 -0
  13. package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
  14. package/dist/xyflow/XYFlow.d.ts +2 -1
  15. package/dist/xyflow/XYFlowEvents.d.ts +4 -4
  16. package/dist/xyflow/XYFlowInner.d.ts +2 -1
  17. package/dist/xyflow/const.d.ts +1 -1
  18. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  19. package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -5
  20. package/dist/xyflow/edges/utils.d.ts +4 -2
  21. package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
  22. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
  23. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  24. package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
  25. package/dist/xyflow/nodes/element/index.d.ts +1 -1
  26. package/dist/xyflow/types.d.ts +3 -3
  27. package/dist/xyflow/utils.d.ts +3 -0
  28. package/package.json +18 -17
  29. package/src/LikeC4Diagram.css.ts +15 -5
  30. package/src/LikeC4Diagram.props.ts +33 -5
  31. package/src/LikeC4Diagram.tsx +103 -32
  32. package/src/globals.css.ts +1 -2
  33. package/src/mantine/theme.ts +1 -0
  34. package/src/state/DiagramContext.tsx +3 -4
  35. package/src/state/diagramStore.ts +177 -103
  36. package/src/state/index.ts +0 -1
  37. package/src/state/useDiagramStore.ts +6 -13
  38. package/src/state/useNavigationHistory.ts +39 -0
  39. package/src/theme.css.ts +5 -1
  40. package/src/ui/BackwardForwardPanel.css.ts +22 -0
  41. package/src/ui/BackwardForwardPanel.tsx +59 -0
  42. package/src/ui/DiagramTitlePanel.css.ts +11 -5
  43. package/src/ui/DiagramTitlePanel.tsx +108 -96
  44. package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
  45. package/src/ui/DynamicViewWalkthrough.tsx +36 -17
  46. package/src/ui/OptionsPanel.tsx +28 -19
  47. package/src/ui/options/NodeOptions.tsx +16 -19
  48. package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
  49. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
  50. package/src/xyflow/SyncWithDiagram.tsx +21 -2
  51. package/src/xyflow/XYFlow.tsx +20 -23
  52. package/src/xyflow/XYFlowEvents.tsx +26 -8
  53. package/src/xyflow/XYFlowInner.tsx +10 -6
  54. package/src/xyflow/const.ts +1 -1
  55. package/src/xyflow/diagram-to-xyflow.ts +4 -41
  56. package/src/xyflow/edges/RelationshipEdge.tsx +160 -111
  57. package/src/xyflow/edges/edges.css.ts +41 -26
  58. package/src/xyflow/edges/utils.ts +5 -2
  59. package/src/xyflow/hooks/useLayoutConstraints.ts +19 -4
  60. package/src/xyflow/nodes/compound/CompoundNode.css.ts +45 -37
  61. package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
  62. package/src/xyflow/nodes/element/ElementNode.tsx +27 -14
  63. package/src/xyflow/nodes/element/element.css.ts +31 -18
  64. package/src/xyflow/nodes/element/index.ts +1 -1
  65. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
  66. package/src/xyflow/types.ts +5 -2
  67. package/src/xyflow/utils.ts +48 -0
  68. package/dist/ui/index.d.ts +0 -2
  69. 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 paper = style({
12
+ export const card = style({
12
13
  cursor: 'default',
13
- minWidth: 100,
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, Paper, Spoiler, Stack, Text } from '@mantine/core'
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, motion } from 'framer-motion'
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
- <motion.div
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
- <Paper
39
+ <Card
39
40
  radius="sm"
40
- withBorder={!isCollapsed}
41
- className={css.paper}
42
- p={isCollapsed ? 'xs' : 'lg'}
41
+ className={css.card}
42
+ p={'md'}
43
43
  onDoubleClick={e => e.stopPropagation()}>
44
- <Stack gap={'sm'} justify="stretch" align="stretch">
45
- <Group justify="stretch" wrap="nowrap" onClick={toggle} style={{ cursor: 'pointer' }}>
46
- <Text
47
- component={'div'}
48
- flex={'1'}
49
- size={isCollapsed ? 'sm' : 'lg'}
50
- fw={500}
51
- lh={1.1}
52
- className={css.title}
53
- >
54
- {title}
55
- </Text>
56
- <Text
57
- hidden={isCollapsed}
58
- component={'div'}
59
- flex={'0 0 auto'}
60
- inline
61
- size="xs"
62
- fz={9}
63
- fw={500}
64
- c={'dimmed'}
65
- style={{
66
- userSelect: 'all'
67
- }}>
68
- <span style={{ userSelect: 'none' }}>id:{' '}</span>
69
- {id}
70
- </Text>
71
- </Group>
72
- {!isCollapsed && (
73
- <>
74
- {description && (
75
- <Spoiler
76
- maxHeight={42}
77
- showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
78
- hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
79
- <Text
80
- component={'div'}
81
- size="sm"
82
- className={css.description}>
83
- {description || 'no description'}
84
- </Text>
85
- </Spoiler>
86
- )}
87
- {!description && (
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="xs"
91
- c={'dimmed'}>
92
- no description
94
+ size="sm"
95
+ className={css.description}>
96
+ {description || 'no description'}
93
97
  </Text>
94
- )}
95
- {links && (
96
- <Stack
97
- gap={3}
98
- justify="stretch"
99
- align="stretch">
100
- {links.map((link) => (
101
- <Group key={link} wrap="nowrap" align="center" gap={'sm'}>
102
- <Box flex={'1'} style={{ overflow: 'hidden' }}>
103
- <Anchor
104
- href={link}
105
- target="_blank"
106
- fz="xs"
107
- truncate="end"
108
- display={'inline-block'}
109
- w={'100%'}>
110
- {link}
111
- </Anchor>
112
- </Box>
113
- <CopyButton value={link}>
114
- {({ copied, copy }) => (
115
- <Button
116
- size="compact-xs"
117
- fz={'10'}
118
- variant="light"
119
- onClick={copy}
120
- color={copied
121
- ? 'teal'
122
- : 'gray'}>
123
- {copied ? 'copied' : 'copy'}
124
- </Button>
125
- )}
126
- </CopyButton>
127
- </Group>
128
- ))}
129
- </Stack>
130
- )}
131
- </>
132
- )}
133
- </Stack>
134
- </Paper>
135
- </motion.div>
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
  }
@@ -41,6 +41,7 @@ export const btn = style({
41
41
  }
42
42
  })
43
43
 
44
- globalStyle(`${btn} .tabler-icon`, {
45
- width: '80%'
44
+ globalStyle(`:where(${buttons}, ${btn}) .tabler-icon`, {
45
+ width: '0.85em',
46
+ height: '0.85em'
46
47
  })
@@ -1,19 +1,19 @@
1
- import { ActionIcon, Box, Button, rem, useComputedColorScheme } from '@mantine/core'
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-xl',
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={'sm'}
85
+ px={hasNextSteps ? 'md' : 'xl'}
68
86
  onClick={e => {
69
87
  e.stopPropagation()
70
88
  stopDynamicView()
71
89
  }}>
72
- <IconPlayerStopFilled />
73
- </Button>
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>
@@ -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, motion } from 'framer-motion'
6
- import { useState } from 'react'
7
- import { isNullish, isTruthy } from 'remeda'
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 => isNullish(s.focusedNodeId))
14
- const [selectedNodes, setSelectedNodes] = useState([] as string[])
15
-
16
- useOnSelectionChange({
17
- onChange: ({ nodes }) => {
18
- const next = nodes.map(n => n.id).sort()
19
- setSelectedNodes(prev => {
20
- return shallowEqual(prev, next) ? prev : next
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
- {isFocusDisabled && selectedNodes.length > 0 && (
28
- <motion.div
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
- </motion.div>
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 { useEffect, useState } from 'react'
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().triggerOnChange([{
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
- <Box maw={200}>
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
- </Box>
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
- <Box key={firstNode.id}>
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
- </Box>
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
- <Box>
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
- </Box>
194
+ </div>
198
195
  )
199
196
  }
200
197
 
@@ -215,7 +212,7 @@ function ShapeOption({
215
212
  })
216
213
 
217
214
  return (
218
- <Box>
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
- </Box>
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
- <Box>
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
- </Box>
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
- useEffect(() => {
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
- useEffect(() => {
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
- 200,
30
- 800
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 && elTo && elFrom) {
84
- const [x, y, zoom] = xyflowApi.getState().transform
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
- const nextZoom = Math.min(
87
- elFrom.width / elTo.width,
88
- elFrom.height / elTo.height
89
- )
90
- const centerFrom = lastOnNavigate!.elementCenterScreenPosition
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(centerFrom.x - centerTo.x),
105
- y: toDomPrecision(centerFrom.y - centerTo.y)
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
- if (anotherNode) {
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
- includeHiddenNodes: true,
66
- duration: 300,
63
+ duration: 350,
67
64
  padding: 0.1,
68
65
  minZoom: MinZoom,
69
66
  maxZoom: Math.max(1, transform[2]),