@likec4/diagram 1.7.2 → 1.7.4

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 (70) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +38 -22
  3. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  4. package/dist/hooks/index.d.ts +2 -0
  5. package/dist/{state/useDiagramStore.d.ts → hooks/useDiagramState.d.ts} +2 -3
  6. package/dist/{xyflow/hooks → hooks}/useXYFlow.d.ts +10 -10
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.mjs +818 -448
  9. package/dist/state/DiagramContext.d.ts +74 -30
  10. package/dist/state/diagram-to-xyflow.d.ts +2 -0
  11. package/dist/state/diagramStore.d.ts +71 -22
  12. package/dist/style.css +58 -37
  13. package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +3 -0
  14. package/dist/ui/top-left/TopLeftPanel.d.ts +4 -0
  15. package/dist/ui/top-left/_shared.d.ts +5 -0
  16. package/dist/ui/top-left/styles.css.d.ts +3 -0
  17. package/dist/xyflow/XYFlowInner.d.ts +0 -1
  18. package/dist/xyflow/const.d.ts +1 -0
  19. package/dist/xyflow/edges/edges.css.d.ts +1 -1
  20. package/dist/xyflow/{hooks/useLayoutConstraints.d.ts → useLayoutConstraints.d.ts} +1 -1
  21. package/dist/xyflow/utils.d.ts +11 -1
  22. package/package.json +6 -6
  23. package/src/LikeC4Diagram.props.ts +66 -32
  24. package/src/LikeC4Diagram.tsx +19 -12
  25. package/src/StaticLikeC4Diagram.tsx +2 -0
  26. package/src/hooks/index.ts +2 -0
  27. package/src/{state/useDiagramStore.ts → hooks/useDiagramState.ts} +3 -4
  28. package/src/{xyflow/hooks → hooks}/useXYFlow.ts +1 -1
  29. package/src/index.ts +2 -2
  30. package/src/state/DiagramContext.tsx +15 -7
  31. package/src/state/diagram-to-xyflow.ts +17 -3
  32. package/src/state/diagramStore.ts +73 -38
  33. package/src/ui/DiagramTitlePanel.css.ts +2 -2
  34. package/src/ui/DiagramTitlePanel.tsx +62 -44
  35. package/src/ui/DynamicViewWalkthrough.tsx +1 -2
  36. package/src/ui/OptionsPanel.tsx +1 -1
  37. package/src/ui/options/EdgeOptions.tsx +1 -16
  38. package/src/ui/options/NodeOptions.tsx +38 -13
  39. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +131 -0
  40. package/src/ui/top-left/TopLeftPanel.tsx +213 -0
  41. package/src/ui/top-left/_shared.ts +19 -0
  42. package/src/ui/top-left/styles.css.ts +38 -0
  43. package/src/xyflow/FitviewOnDiagramChange.tsx +2 -2
  44. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +87 -59
  45. package/src/xyflow/XYFlow.tsx +11 -10
  46. package/src/xyflow/XYFlowEvents.tsx +44 -18
  47. package/src/xyflow/XYFlowInner.tsx +5 -7
  48. package/src/xyflow/const.ts +1 -0
  49. package/src/xyflow/edges/RelationshipEdge.tsx +60 -27
  50. package/src/xyflow/edges/edges.css.ts +29 -25
  51. package/src/xyflow/nodes/compound/CompoundNode.tsx +1 -1
  52. package/src/xyflow/nodes/element/ElementLink.tsx +1 -2
  53. package/src/xyflow/nodes/element/ElementNode.tsx +7 -5
  54. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  55. package/src/xyflow/{hooks/useLayoutConstraints.ts → useLayoutConstraints.ts} +3 -3
  56. package/src/xyflow/utils.ts +25 -1
  57. package/dist/state/hooks.d.ts +0 -4
  58. package/dist/state/index.d.ts +0 -2
  59. package/dist/state/useNavigationHistory.d.ts +0 -5
  60. package/dist/types.d.ts +0 -20
  61. package/dist/ui/BackwardForwardPanel.css.d.ts +0 -1
  62. package/dist/ui/BackwardForwardPanel.d.ts +0 -1
  63. package/dist/xyflow/hooks/index.d.ts +0 -2
  64. package/src/state/hooks.ts +0 -5
  65. package/src/state/index.ts +0 -2
  66. package/src/state/useNavigationHistory.ts +0 -39
  67. package/src/types.ts +0 -22
  68. package/src/ui/BackwardForwardPanel.css.ts +0 -22
  69. package/src/ui/BackwardForwardPanel.tsx +0 -76
  70. package/src/xyflow/hooks/index.ts +0 -3
@@ -0,0 +1,131 @@
1
+ import type { AutoLayoutDirection } from '@likec4/core'
2
+ import {
3
+ ActionIcon as MantineActionIcon,
4
+ Box,
5
+ FloatingIndicator,
6
+ Group,
7
+ Popover,
8
+ PopoverDropdown,
9
+ type PopoverProps,
10
+ PopoverTarget,
11
+ Stack,
12
+ Text,
13
+ Tooltip as MantineTooltip,
14
+ TooltipGroup
15
+ } from '@mantine/core'
16
+ import { IconArrowBigDownLines, IconLayoutDashboard } from '@tabler/icons-react'
17
+ import { useState } from 'react'
18
+ import { useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
19
+ import * as css from './styles.css'
20
+
21
+ const ActionIcon = MantineActionIcon.withProps({
22
+ size: 'md',
23
+ variant: 'subtle',
24
+ color: 'gray'
25
+ })
26
+
27
+ const Tooltip = MantineTooltip.withProps({
28
+ color: 'gray',
29
+ fz: 'xs',
30
+ position: 'right',
31
+ openDelay: 300,
32
+ label: '',
33
+ children: null,
34
+ offset: 5,
35
+ transitionProps: { transition: 'fade', duration: 200 }
36
+ })
37
+
38
+ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
39
+ const store = useDiagramStoreApi()
40
+ const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null)
41
+ const [controlsRefs, setControlsRefs] = useState<Record<AutoLayoutDirection, HTMLButtonElement | null>>({} as any)
42
+ const autoLayout = useDiagramState(s => s.view.autoLayout)
43
+
44
+ const setControlRef = (name: AutoLayoutDirection) => (node: HTMLButtonElement) => {
45
+ controlsRefs[name] = node
46
+ setControlsRefs(controlsRefs)
47
+ }
48
+
49
+ const setAutoLayout = (layout: AutoLayoutDirection) => (event: React.MouseEvent<HTMLButtonElement>) => {
50
+ event.stopPropagation()
51
+ store.getState().onChange?.({
52
+ change: {
53
+ op: 'change-autolayout',
54
+ layout
55
+ }
56
+ })
57
+ }
58
+
59
+ return (
60
+ <Popover
61
+ position="right-start"
62
+ clickOutsideEvents={[
63
+ 'pointerdown'
64
+ ]}
65
+ radius="xs"
66
+ shadow="lg"
67
+ offset={{
68
+ mainAxis: 10
69
+ }}
70
+ {...props}>
71
+ <PopoverTarget>
72
+ <MantineActionIcon
73
+ className="action-icon"
74
+ variant="light"
75
+ color="gray">
76
+ <IconLayoutDashboard stroke={1.3} />
77
+ </MantineActionIcon>
78
+ </PopoverTarget>
79
+ <PopoverDropdown className="likec4-top-left-panel" p={8} pt={4}>
80
+ <Box pos={'relative'} ref={setRootRef}>
81
+ <FloatingIndicator
82
+ target={controlsRefs[autoLayout]}
83
+ parent={rootRef}
84
+ className={css.autolayoutIndicator}
85
+ />
86
+ <Box mb={10}>
87
+ <Text inline fz={'xs'} c={'dimmed'} fw={500}>Auto layout:</Text>
88
+ </Box>
89
+ <TooltipGroup openDelay={100}>
90
+ <Stack align="center" gap={1}>
91
+ <Tooltip label="Top to Bottom">
92
+ <ActionIcon
93
+ className={css.autolayoutIcon}
94
+ ref={setControlRef('TB')}
95
+ onClick={setAutoLayout('TB')}>
96
+ <IconArrowBigDownLines stroke={1.3} />
97
+ </ActionIcon>
98
+ </Tooltip>
99
+ <Group gap={30}>
100
+ <Tooltip label="Left to Right">
101
+ <ActionIcon
102
+ className={css.autolayoutIcon}
103
+ ref={setControlRef('LR')}
104
+ onClick={setAutoLayout('LR')}>
105
+ <IconArrowBigDownLines stroke={1.3} style={{ rotate: '270deg' }} />
106
+ </ActionIcon>
107
+ </Tooltip>
108
+ <Tooltip label="Right to Left">
109
+ <ActionIcon
110
+ className={css.autolayoutIcon}
111
+ ref={setControlRef('RL')}
112
+ onClick={setAutoLayout('RL')}>
113
+ <IconArrowBigDownLines stroke={1.3} style={{ rotate: '90deg' }} />
114
+ </ActionIcon>
115
+ </Tooltip>
116
+ </Group>
117
+ <Tooltip label="Bottom to Top">
118
+ <ActionIcon
119
+ className={css.autolayoutIcon}
120
+ ref={setControlRef('BT')}
121
+ onClick={setAutoLayout('BT')}>
122
+ <IconArrowBigDownLines stroke={1.3} style={{ rotate: '180deg' }} />
123
+ </ActionIcon>
124
+ </Tooltip>
125
+ </Stack>
126
+ </TooltipGroup>
127
+ </Box>
128
+ </PopoverDropdown>
129
+ </Popover>
130
+ )
131
+ }
@@ -0,0 +1,213 @@
1
+ import {
2
+ Code,
3
+ Group,
4
+ HoverCard,
5
+ HoverCardDropdown,
6
+ HoverCardTarget,
7
+ Notification,
8
+ type PopoverProps,
9
+ Stack,
10
+ Text
11
+ } from '@mantine/core'
12
+ import {
13
+ IconAlertTriangle,
14
+ IconChevronLeft,
15
+ IconChevronRight,
16
+ IconFileSymlink,
17
+ IconFocusCentered,
18
+ IconMenu2
19
+ } from '@tabler/icons-react'
20
+ import clsx from 'clsx'
21
+ import { AnimatePresence, m } from 'framer-motion'
22
+ import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
23
+ import { ActionIcon, Tooltip } from './_shared'
24
+ import { ChangeAutoLayoutButton } from './ChangeAutoLayoutButton'
25
+ import * as css from './styles.css'
26
+
27
+ const historySelector = (s: DiagramState) => ({
28
+ showBurgerMenu: !!s.onBurgerMenuClick,
29
+ hasStepBack: s.navigationHistoryIndex > 0,
30
+ hasStepForward: s.navigationHistoryIndex < s.navigationHistory.length - 1
31
+ })
32
+
33
+ const BackwardForwardButtons = () => {
34
+ const store = useDiagramStoreApi()
35
+ const {
36
+ showBurgerMenu,
37
+ hasStepBack,
38
+ hasStepForward
39
+ } = useDiagramState(historySelector)
40
+ return (
41
+ <Group
42
+ className={'likec4-navigation-panel'}
43
+ gap={'xs'}>
44
+ {showBurgerMenu && (
45
+ <ActionIcon
46
+ onClick={e => {
47
+ e.stopPropagation()
48
+ store.getState().onBurgerMenuClick?.()
49
+ }}>
50
+ <IconMenu2 />
51
+ </ActionIcon>
52
+ )}
53
+ <AnimatePresence>
54
+ {hasStepBack && (
55
+ <m.div
56
+ layout
57
+ initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
58
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
59
+ exit={{
60
+ opacity: 0.05,
61
+ transform: 'translateX(-10px)'
62
+ }}
63
+ key={'back'}>
64
+ <ActionIcon
65
+ onClick={e => {
66
+ e.stopPropagation()
67
+ store.getState().goBack()
68
+ }}>
69
+ <IconChevronLeft />
70
+ </ActionIcon>
71
+ </m.div>
72
+ )}
73
+ {hasStepForward && (
74
+ <m.div
75
+ layout
76
+ initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
77
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
78
+ exit={{
79
+ opacity: 0,
80
+ transform: 'translateX(10px)'
81
+ }}
82
+ key={'forward'}>
83
+ <ActionIcon
84
+ onClick={e => {
85
+ e.stopPropagation()
86
+ store.getState().goForward()
87
+ }}>
88
+ <IconChevronRight />
89
+ </ActionIcon>
90
+ </m.div>
91
+ )}
92
+ </AnimatePresence>
93
+ </Group>
94
+ )
95
+ }
96
+
97
+ const LayoutDriftNotification = (props: PopoverProps) => (
98
+ <HoverCard
99
+ position="right-start"
100
+ openDelay={200}
101
+ closeDelay={100}
102
+ {...props}>
103
+ <HoverCardTarget>
104
+ <ActionIcon
105
+ className="action-icon"
106
+ variant="light"
107
+ color="orange">
108
+ <IconAlertTriangle />
109
+ </ActionIcon>
110
+ </HoverCardTarget>
111
+ <HoverCardDropdown p={'0'}>
112
+ <Notification
113
+ color="orange"
114
+ withBorder={false}
115
+ withCloseButton={false}
116
+ title="Manual layout issues">
117
+ <Text mt={2} size="sm" lh="xs">
118
+ View contains new elements or their sizes have changed,<br />
119
+ last manual layout can not be applied.
120
+ </Text>
121
+ <Text mt={'xs'} size="sm" lh="xs">
122
+ Update view predicates or remove <Code>@likec4-generated</Code>
123
+ </Text>
124
+ </Notification>
125
+ </HoverCardDropdown>
126
+ </HoverCard>
127
+ )
128
+
129
+ export const TopLeftPanel = () => {
130
+ const store = useDiagramStoreApi()
131
+ const {
132
+ showNavigationButtons,
133
+ showFitDiagram,
134
+ showLayoutDriftWarning,
135
+ showChangeAutoLayout,
136
+ portalProps: _portalProps,
137
+ showGoToSource
138
+ } = useDiagramState(s => {
139
+ const isNotActive = s.activeDynamicViewStep === null && s.focusedNodeId === null
140
+ const target = s.getContainer()
141
+ return ({
142
+ showNavigationButtons: !!s.onBurgerMenuClick || s.showNavigationButtons && !!s.onNavigateTo,
143
+ showFitDiagram: s.fitViewEnabled && s.zoomable && s.viewportChanged,
144
+ showLayoutDriftWarning: s.readonly !== true && s.view.hasLayoutDrift === true && isNotActive,
145
+ showChangeAutoLayout: s.readonly !== true && !!s.onChange && isNotActive,
146
+ showGoToSource: !!s.onOpenSourceView,
147
+ portalProps: target ? { target } : undefined
148
+ })
149
+ })
150
+ const portalProps = _portalProps ? { portalProps: _portalProps } : undefined
151
+
152
+ return (
153
+ <Stack
154
+ className={clsx(
155
+ 'react-flow__panel',
156
+ css.panel,
157
+ 'likec4-top-left-panel'
158
+ )}
159
+ onClick={e => e.stopPropagation()}
160
+ gap={'xs'}>
161
+ {showNavigationButtons && <BackwardForwardButtons />}
162
+ {showGoToSource && (
163
+ <Tooltip label="Go to source" {...portalProps}>
164
+ <ActionIcon
165
+ className="action-icon"
166
+ onClick={e => {
167
+ e.stopPropagation()
168
+ store.getState().onOpenSourceView?.()
169
+ }}>
170
+ <IconFileSymlink stroke={1.5} />
171
+ </ActionIcon>
172
+ </Tooltip>
173
+ )}
174
+ {showChangeAutoLayout && <ChangeAutoLayoutButton {...portalProps} />}
175
+ <AnimatePresence>
176
+ {showLayoutDriftWarning && (
177
+ <m.div
178
+ initial={{ opacity: 0.05, transform: 'translateX(-40%)' }}
179
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
180
+ exit={{
181
+ opacity: 0,
182
+ transform: 'translateX(-40%)'
183
+ }}
184
+ key={'layout'}>
185
+ <LayoutDriftNotification {...portalProps} />
186
+ </m.div>
187
+ )}
188
+ {showFitDiagram && (
189
+ <m.div
190
+ initial={{ opacity: 0.05, transform: 'translateX(-20%)' }}
191
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
192
+ exit={{
193
+ opacity: 0,
194
+ transform: 'translateX(-30%)'
195
+ }}
196
+ key={'fit-view'}>
197
+ <Tooltip label="Center diagram" {...portalProps}>
198
+ <ActionIcon
199
+ className="action-icon"
200
+ onClick={e => {
201
+ e.stopPropagation()
202
+ store.getState().fitDiagram()
203
+ }}>
204
+ <IconFocusCentered />
205
+ </ActionIcon>
206
+ </Tooltip>
207
+ </m.div>
208
+ )}
209
+ </AnimatePresence>
210
+ </Stack>
211
+ )
212
+ }
213
+ TopLeftPanel.displayName = 'TopLeftPanel'
@@ -0,0 +1,19 @@
1
+ import { ActionIcon as MantineActionIcon, Tooltip as MantineTooltip } from '@mantine/core'
2
+
3
+ export const Tooltip = MantineTooltip.withProps({
4
+ color: 'gray',
5
+ fz: 'xs',
6
+ position: 'right',
7
+ openDelay: 400,
8
+ closeDelay: 100,
9
+ label: '',
10
+ children: null,
11
+ offset: 8,
12
+ transitionProps: { transition: 'fade-right', duration: 200 }
13
+ })
14
+
15
+ export const ActionIcon = MantineActionIcon.withProps({
16
+ className: 'action-icon',
17
+ variant: 'light',
18
+ color: 'gray'
19
+ })
@@ -0,0 +1,38 @@
1
+ import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine } from '../../mantine.css'
3
+ import { vars } from '../../theme.css'
4
+
5
+ export const panel = style({
6
+ top: fallbackVar(vars.navigationPanel.top, '1rem'),
7
+ left: fallbackVar(vars.navigationPanel.left, '1rem'),
8
+ margin: 0
9
+ })
10
+
11
+ globalStyle(`.likec4-top-left-panel .action-icon`, {
12
+ vars: {
13
+ ['--ai-size']: 'var(--ai-size-lg)'
14
+ }
15
+ })
16
+ globalStyle(`.likec4-top-left-panel .action-icon .tabler-icon`, {
17
+ width: '70%',
18
+ height: '70%'
19
+ })
20
+
21
+ export const autolayoutIcon = style({})
22
+
23
+ globalStyle(`${autolayoutIcon} .tabler-icon`, {
24
+ width: '75%',
25
+ height: '75%'
26
+ })
27
+
28
+ export const autolayoutIndicator = style({
29
+ background: mantine.colors.gray[2],
30
+ borderRadius: mantine.radius.sm,
31
+ border: `1px solid ${mantine.colors.gray[3]}`,
32
+ selectors: {
33
+ [`:where([data-mantine-color-scheme="dark"]) &`]: {
34
+ background: mantine.colors.dark[5],
35
+ border: `1px solid ${mantine.colors.dark[4]}`
36
+ }
37
+ }
38
+ })
@@ -1,8 +1,8 @@
1
1
  import { useDebouncedEffect } from '@react-hookz/web'
2
2
  import { shallowEqual } from 'fast-equals'
3
3
  import { useRef, useState } from 'react'
4
- import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state/hooks'
5
- import { useXYStore } from './hooks/useXYFlow'
4
+ import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../hooks/useDiagramState'
5
+ import { useXYStore } from '../hooks/useXYFlow'
6
6
 
7
7
  function selectXYFlowSize(state: { width: number; height: number }): string {
8
8
  return `${Math.round(state.width)}:${Math.round(state.height)}`
@@ -1,75 +1,103 @@
1
1
  import { nonNullable } from '@likec4/core'
2
+ import { useHotkeys } from '@mantine/hooks'
2
3
  import type { EdgeChange, NodeChange } from '@xyflow/react'
3
- import { getBoundsOfRects, getNodesBounds, getViewportForBounds, nodeToRect } from '@xyflow/system'
4
- import { useEffect } from 'react'
5
- import { useDiagramStoreApi } from '../state/useDiagramStore'
6
- import { MinZoom } from './const'
4
+ import { getBoundsOfRects, getViewportForBounds } from '@xyflow/system'
5
+ import { useUpdateEffect } from '../hooks'
6
+ import { useDiagramState, useDiagramStoreApi } from '../hooks/useDiagramState'
7
7
  import type { XYFlowEdge, XYFlowNode } from './types'
8
+ import { nodeToRect } from './utils'
8
9
 
9
10
  export function SelectEdgesOnNodeFocus() {
10
11
  const diagramStore = useDiagramStoreApi()
12
+ const focusedNodeId = useDiagramState(s => s.focusedNodeId)
11
13
 
12
- useEffect(
13
- () =>
14
- diagramStore.subscribe(s => s.focusedNodeId, focusedNodeId => {
15
- if (!focusedNodeId) {
16
- return
17
- }
14
+ useUpdateEffect(() => {
15
+ if (!focusedNodeId) {
16
+ diagramStore.getState().xystore.getState().resetSelectedElements()
17
+ return
18
+ }
19
+ const container = diagramStore.getState().getContainer()
20
+ if (!container) {
21
+ return
22
+ }
23
+ const { width, height } = container.getBoundingClientRect()
18
24
 
19
- 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
+ const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
26
+ const nodeChanges = [] as NodeChange<XYFlowNode>[]
27
+ const {
28
+ edgeLookup,
29
+ nodeLookup,
30
+ panZoom,
31
+ transform,
32
+ triggerNodeChanges,
33
+ triggerEdgeChanges
34
+ } = diagramStore.getState().xystore.getState()
32
35
 
33
- 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
- }
36
+ let focusBounds = nodeToRect(nonNullable(nodeLookup.get(focusedNodeId)))
37
+
38
+ for (const [, edge] of edgeLookup) {
39
+ if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
40
+ const anotherNodeId = edge.source === focusedNodeId ? edge.target : edge.source
41
+ const nd = nonNullable(nodeLookup.get(anotherNodeId))
42
+ focusBounds = getBoundsOfRects(focusBounds, nodeToRect(nd))
43
+ }
44
+ if (edge.selected) {
45
+ edgeChanges.push({
46
+ id: edge.id,
47
+ type: 'select',
48
+ selected: false
49
+ })
50
+ }
51
+ }
52
+ for (const [, node] of nodeLookup) {
53
+ if (node.selected && node.id !== focusedNodeId) {
56
54
  nodeChanges.push({
57
- id: focusedNodeId,
55
+ id: node.id,
58
56
  type: 'select',
59
- selected: true
57
+ selected: false
60
58
  })
61
- if (edgeChanges.length > 0) {
62
- triggerEdgeChanges(edgeChanges)
63
- }
64
- triggerNodeChanges(nodeChanges)
59
+ }
60
+ }
61
+ nodeChanges.push({
62
+ id: focusedNodeId,
63
+ type: 'select',
64
+ selected: true
65
+ })
66
+ if (edgeChanges.length > 0) {
67
+ triggerEdgeChanges(edgeChanges)
68
+ }
69
+ triggerNodeChanges(nodeChanges)
65
70
 
66
- 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]
71
+ const maxZoom = Math.max(1, transform[2])
72
+ const viewport = getViewportForBounds(
73
+ {
74
+ x: focusBounds.x - 16,
75
+ y: focusBounds.y - 16,
76
+ width: focusBounds.width + 32,
77
+ height: focusBounds.height + 32
78
+ },
79
+ width,
80
+ height,
81
+ 0.1,
82
+ maxZoom,
83
+ 0
84
+ )
85
+ panZoom?.setViewport(viewport, {
86
+ duration: 350
87
+ })
88
+ }, [focusedNodeId])
89
+
90
+ useHotkeys(
91
+ focusedNodeId !== null
92
+ ? [
93
+ ['Escape', (e) => {
94
+ e.stopImmediatePropagation()
95
+ const { xystore, fitDiagram } = diagramStore.getState()
96
+ fitDiagram()
97
+ xystore.getState().resetSelectedElements()
98
+ }, { preventDefault: true }]
99
+ ]
100
+ : []
73
101
  )
74
102
 
75
103
  return null
@@ -1,17 +1,16 @@
1
1
  import { ReactFlow, useOnViewportChange } from '@xyflow/react'
2
- import { deepEqual as eq, shallowEqual } from 'fast-equals'
3
- import { type CSSProperties, type PropsWithChildren, useCallback, useMemo } from 'react'
2
+ import { shallowEqual } from 'fast-equals'
3
+ import { type CSSProperties, type PropsWithChildren, useMemo, useState } from 'react'
4
+ import { useDiagramState } from '../hooks/useDiagramState'
5
+ import { useXYStoreApi } from '../hooks/useXYFlow'
4
6
  import type { DiagramState } from '../state/diagramStore'
5
- import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
6
- import { MinZoom } from './const'
7
+ import { MaxZoom, MinZoom } from './const'
7
8
  import { RelationshipEdge } from './edges/RelationshipEdge'
8
- import { useLayoutConstraints } from './hooks/useLayoutConstraints'
9
- import { useXYStoreApi } from './hooks/useXYFlow'
10
9
  import { CompoundNode } from './nodes/compound'
11
10
  import { ElementNode } from './nodes/element'
12
11
  import { XYFlowEdge, XYFlowNode } from './types'
12
+ import { useLayoutConstraints } from './useLayoutConstraints'
13
13
  import { useXYFlowEvents } from './XYFlowEvents'
14
- // import { useLogger } from '@mantine/hooks'
15
14
 
16
15
  const nodeTypes = {
17
16
  element: ElementNode,
@@ -88,6 +87,7 @@ export function XYFlow({
88
87
  translateX,
89
88
  translateY
90
89
  } = useDiagramState(selector, shallowEqual)
90
+ const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(zoomable)
91
91
 
92
92
  // useLogger('XYFlow',[
93
93
  // nodes,
@@ -111,6 +111,7 @@ export function XYFlow({
111
111
  if (x !== roundedX || y !== roundedY) {
112
112
  xyflowApi.setState({ transform: [roundedX, roundedY, zoom] })
113
113
  }
114
+ setZoomOnDoubleClick(zoomable && zoom < 1.1)
114
115
  }
115
116
  })
116
117
 
@@ -128,7 +129,7 @@ export function XYFlow({
128
129
  {...(!zoomable && {
129
130
  zoomActivationKeyCode: null
130
131
  })}
131
- maxZoom={zoomable ? 1.9 : 1}
132
+ maxZoom={zoomable ? MaxZoom : 1}
132
133
  minZoom={zoomable ? MinZoom : 1}
133
134
  fitView={fitView}
134
135
  fitViewOptions={useMemo(() => ({
@@ -163,9 +164,9 @@ export function XYFlow({
163
164
  onNodeDragStop: layoutConstraints.onNodeDragStop
164
165
  }}
165
166
  nodeDragThreshold={3}
166
- zoomOnDoubleClick={false}
167
+ zoomOnDoubleClick={zoomOnDoubleClick}
167
168
  elevateNodesOnSelect={false} // or edges are not visible after select\
168
- selectNodesOnDrag={false} // or weird camera movement
169
+ selectNodesOnDrag={false}
169
170
  onNodesChange={onNodesChange}
170
171
  onEdgesChange={onEdgesChange}
171
172
  onDoubleClick={handlers.onDoubleClick}