@likec4/diagram 1.8.1 → 1.12.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 (114) hide show
  1. package/package.json +21 -45
  2. package/src/LikeC4CustomColors.props.ts +5 -0
  3. package/src/LikeC4CustomColors.tsx +37 -0
  4. package/src/LikeC4Diagram.css.ts +1 -1
  5. package/src/LikeC4Diagram.props.ts +43 -95
  6. package/src/LikeC4Diagram.tsx +21 -2
  7. package/src/LikeC4ModelProvider.tsx +49 -0
  8. package/src/StaticLikeC4Diagram.tsx +1 -0
  9. package/src/globals.css.ts +2 -3
  10. package/src/hooks/index.ts +1 -0
  11. package/src/hooks/useMantinePortalProps.ts +8 -0
  12. package/src/hooks/useXYFlow.ts +1 -2
  13. package/src/index.ts +3 -0
  14. package/src/likec4model/LikeC4ModelContext.ts +4 -0
  15. package/src/likec4model/index.ts +1 -0
  16. package/src/likec4model/useLikeC4Model.ts +55 -0
  17. package/src/mantine/EnsureMantine.tsx +1 -0
  18. package/src/state/DiagramContext.tsx +1 -0
  19. package/src/state/diagram-to-xyflow.ts +2 -3
  20. package/src/state/diagramStore.ts +185 -66
  21. package/src/{theme.css.ts → theme-vars.ts} +11 -4
  22. package/src/ui/DiagramTitlePanel.css.ts +2 -3
  23. package/src/ui/DiagramTitlePanel.tsx +17 -1
  24. package/src/ui/DynamicViewWalkthrough.css.ts +28 -2
  25. package/src/ui/DynamicViewWalkthrough.tsx +97 -52
  26. package/src/ui/OptionsPanel.css.ts +1 -2
  27. package/src/ui/OptionsPanel.tsx +1 -1
  28. package/src/ui/notation/NotationPanel.css.ts +2 -3
  29. package/src/ui/notation/NotationPanel.tsx +11 -12
  30. package/src/ui/options/NodeOptions.tsx +1 -1
  31. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +20 -32
  32. package/src/ui/top-left/TopLeftPanel.tsx +128 -117
  33. package/src/ui/top-left/_shared.ts +7 -6
  34. package/src/ui/top-left/styles.css.ts +17 -8
  35. package/src/utils/vector.ts +57 -0
  36. package/src/xyflow/FitviewOnDiagramChange.tsx +5 -4
  37. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +7 -5
  38. package/src/xyflow/XYFlow.tsx +10 -11
  39. package/src/xyflow/XYFlowEvents.tsx +59 -60
  40. package/src/xyflow/XYFlowInner.tsx +5 -5
  41. package/src/xyflow/edges/EdgeLabel.tsx +185 -0
  42. package/src/xyflow/edges/RelationshipEdge.tsx +170 -137
  43. package/src/xyflow/edges/RelationshipsDropdownMenu.css.ts +60 -0
  44. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +214 -0
  45. package/src/xyflow/edges/edges.css.ts +75 -7
  46. package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -2
  47. package/src/xyflow/nodes/compound/CompoundNode.tsx +4 -4
  48. package/src/xyflow/nodes/element/ElementLink.css.ts +1 -1
  49. package/src/xyflow/nodes/element/ElementLink.tsx +3 -8
  50. package/src/xyflow/nodes/element/ElementNode.tsx +47 -4
  51. package/src/xyflow/nodes/element/element.css.ts +1 -2
  52. package/src/xyflow/types.ts +1 -24
  53. package/src/xyflow/useLayoutConstraints.ts +10 -6
  54. package/src/xyflow/utils.ts +1 -1
  55. package/dist/LikeC4Diagram.css.d.ts +0 -4
  56. package/dist/LikeC4Diagram.d.ts +0 -4
  57. package/dist/LikeC4Diagram.props.d.ts +0 -185
  58. package/dist/StaticLikeC4Diagram.d.ts +0 -4
  59. package/dist/globals.css.d.ts +0 -1
  60. package/dist/hooks/index.d.ts +0 -4
  61. package/dist/hooks/useDiagramState.d.ts +0 -6
  62. package/dist/hooks/useSetState.d.ts +0 -3
  63. package/dist/hooks/useUpdateEffect.d.ts +0 -5
  64. package/dist/hooks/useXYFlow.d.ts +0 -267
  65. package/dist/index.d.ts +0 -9
  66. package/dist/index.mjs +0 -3651
  67. package/dist/mantine/EnsureMantine.d.ts +0 -5
  68. package/dist/mantine.css.d.ts +0 -3
  69. package/dist/state/DiagramContext.d.ts +0 -1189
  70. package/dist/state/diagram-to-xyflow.d.ts +0 -12
  71. package/dist/state/diagramStore.d.ts +0 -1268
  72. package/dist/style.css +0 -2139
  73. package/dist/theme.css.d.ts +0 -63
  74. package/dist/ui/DiagramTitlePanel.css.d.ts +0 -4
  75. package/dist/ui/DiagramTitlePanel.d.ts +0 -1
  76. package/dist/ui/DynamicViewWalkthrough.css.d.ts +0 -3
  77. package/dist/ui/DynamicViewWalkthrough.d.ts +0 -1
  78. package/dist/ui/OptionsPanel.css.d.ts +0 -1
  79. package/dist/ui/OptionsPanel.d.ts +0 -1
  80. package/dist/ui/notation/NotationPanel.css.d.ts +0 -6
  81. package/dist/ui/notation/NotationPanel.d.ts +0 -1
  82. package/dist/ui/notation/index.d.ts +0 -1
  83. package/dist/ui/options/EdgeOptions.d.ts +0 -19
  84. package/dist/ui/options/NodeOptions.d.ts +0 -19
  85. package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +0 -3
  86. package/dist/ui/top-left/TopLeftPanel.d.ts +0 -4
  87. package/dist/ui/top-left/_shared.d.ts +0 -5
  88. package/dist/ui/top-left/styles.css.d.ts +0 -3
  89. package/dist/xyflow/EdgeMarkers.d.ts +0 -12
  90. package/dist/xyflow/FitviewOnDiagramChange.d.ts +0 -4
  91. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +0 -1
  92. package/dist/xyflow/XYFlow.d.ts +0 -9
  93. package/dist/xyflow/XYFlowBackground.d.ts +0 -11
  94. package/dist/xyflow/XYFlowEvents.d.ts +0 -161
  95. package/dist/xyflow/XYFlowInner.d.ts +0 -10
  96. package/dist/xyflow/const.d.ts +0 -7
  97. package/dist/xyflow/edges/RelationshipEdge.d.ts +0 -32
  98. package/dist/xyflow/edges/edges.css.d.ts +0 -12
  99. package/dist/xyflow/edges/utils.d.ts +0 -17
  100. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -11
  101. package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -6
  102. package/dist/xyflow/nodes/compound/index.d.ts +0 -1
  103. package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -3
  104. package/dist/xyflow/nodes/element/ElementLink.d.ts +0 -7
  105. package/dist/xyflow/nodes/element/ElementNode.d.ts +0 -6
  106. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +0 -18
  107. package/dist/xyflow/nodes/element/element.css.d.ts +0 -19
  108. package/dist/xyflow/nodes/element/index.d.ts +0 -1
  109. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -5
  110. package/dist/xyflow/types.d.ts +0 -50
  111. package/dist/xyflow/useLayoutConstraints.d.ts +0 -9
  112. package/dist/xyflow/utils.d.ts +0 -18
  113. package/src/mantine.css.ts +0 -7
  114. package/src/reset.d.ts +0 -2
@@ -1,13 +1,17 @@
1
1
  import {
2
+ ActionIconGroup,
2
3
  Code,
3
4
  Group,
4
5
  HoverCard,
5
6
  HoverCardDropdown,
6
7
  HoverCardTarget,
7
8
  Notification,
9
+ Overlay,
8
10
  type PopoverProps,
9
11
  Stack,
10
- Text
12
+ Text,
13
+ TooltipGroup,
14
+ type TooltipProps
11
15
  } from '@mantine/core'
12
16
  import {
13
17
  IconAlertTriangle,
@@ -15,11 +19,15 @@ import {
15
19
  IconChevronRight,
16
20
  IconFileSymlink,
17
21
  IconFocusCentered,
18
- IconMenu2
22
+ IconMenu2,
23
+ IconRouteOff
19
24
  } from '@tabler/icons-react'
20
25
  import clsx from 'clsx'
21
- import { AnimatePresence, m } from 'framer-motion'
26
+ import { AnimatePresence, LayoutGroup, m } from 'framer-motion'
27
+ import { isNullish } from 'remeda'
22
28
  import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
29
+ import { useMantinePortalProps } from '../../hooks/useMantinePortalProps'
30
+ import { mantine } from '../../theme-vars'
23
31
  import { ActionIcon, Tooltip } from './_shared'
24
32
  import { ChangeAutoLayoutButton } from './ChangeAutoLayoutButton'
25
33
  import * as css from './styles.css'
@@ -38,59 +46,62 @@ const BackwardForwardButtons = () => {
38
46
  hasStepForward
39
47
  } = useDiagramState(historySelector)
40
48
  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>
49
+ <LayoutGroup>
50
+ <Group
51
+ className={clsx(css.backwardForwardButtons, 'likec4-navigation-webview')}
52
+ align="flex-start"
53
+ gap={'xs'}>
54
+ {showBurgerMenu && (
55
+ <ActionIcon
56
+ onClick={e => {
57
+ e.stopPropagation()
58
+ store.getState().onBurgerMenuClick?.()
59
+ }}>
60
+ <IconMenu2 />
61
+ </ActionIcon>
91
62
  )}
92
- </AnimatePresence>
93
- </Group>
63
+ <AnimatePresence>
64
+ {hasStepBack && (
65
+ <m.div
66
+ layout
67
+ initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
68
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
69
+ exit={{
70
+ opacity: 0.05,
71
+ transform: 'translateX(-10px)'
72
+ }}
73
+ key={'back'}>
74
+ <ActionIcon
75
+ onClick={e => {
76
+ e.stopPropagation()
77
+ store.getState().goBack()
78
+ }}>
79
+ <IconChevronLeft />
80
+ </ActionIcon>
81
+ </m.div>
82
+ )}
83
+ {hasStepForward && (
84
+ <m.div
85
+ layout
86
+ initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
87
+ animate={{ opacity: 1, transform: 'translateX(0)' }}
88
+ exit={{
89
+ opacity: 0,
90
+ transform: 'translateX(10px)'
91
+ }}
92
+ key={'forward'}>
93
+ <ActionIcon
94
+ onClick={e => {
95
+ e.stopPropagation()
96
+ store.getState().goForward()
97
+ }}>
98
+ <IconChevronRight />
99
+ </ActionIcon>
100
+ </m.div>
101
+ )}
102
+ </AnimatePresence>
103
+ </Group>
104
+ </LayoutGroup>
94
105
  )
95
106
  }
96
107
 
@@ -101,10 +112,8 @@ const LayoutDriftNotification = (props: PopoverProps) => (
101
112
  closeDelay={100}
102
113
  {...props}>
103
114
  <HoverCardTarget>
104
- <ActionIcon
105
- className="action-icon"
106
- variant="light"
107
- color="orange">
115
+ <ActionIcon c="orange">
116
+ <Overlay color={mantine.colors.orange.lightHover} backgroundOpacity={1} />
108
117
  <IconAlertTriangle />
109
118
  </ActionIcon>
110
119
  </HoverCardTarget>
@@ -126,6 +135,22 @@ const LayoutDriftNotification = (props: PopoverProps) => (
126
135
  </HoverCard>
127
136
  )
128
137
 
138
+ const ResetControlPointsButton = (props: Omit<TooltipProps, 'label' | 'children'>) => {
139
+ const store = useDiagramStoreApi()
140
+
141
+ return (
142
+ <Tooltip label="Reset all control points" {...props}>
143
+ <ActionIcon
144
+ onClick={e => {
145
+ e.stopPropagation()
146
+ store.getState().resetEdgeControlPoints()
147
+ }}>
148
+ <IconRouteOff />
149
+ </ActionIcon>
150
+ </Tooltip>
151
+ )
152
+ }
153
+
129
154
  export const TopLeftPanel = () => {
130
155
  const store = useDiagramStoreApi()
131
156
  const {
@@ -133,69 +158,55 @@ export const TopLeftPanel = () => {
133
158
  showFitDiagram,
134
159
  showLayoutDriftWarning,
135
160
  showChangeAutoLayout,
136
- target,
137
- showGoToSource
161
+ showGoToSource,
162
+ viewportChanged,
163
+ showResetControlPoints
138
164
  } = useDiagramState(s => {
139
- const isNotActive = s.activeDynamicViewStep === null && s.focusedNodeId === null
165
+ const isNotWalkthrough = isNullish(s.activeWalkthrough)
166
+ const isNotFocused = isNullish(s.focusedNodeId)
167
+ const isNotActive = isNotWalkthrough && isNotFocused
140
168
  return ({
141
169
  showNavigationButtons: !!s.onBurgerMenuClick || s.showNavigationButtons && !!s.onNavigateTo,
142
- showFitDiagram: s.fitViewEnabled && s.zoomable && s.viewportChanged,
143
- showLayoutDriftWarning: s.readonly !== true && s.view.hasLayoutDrift === true && isNotActive,
144
- showChangeAutoLayout: s.readonly !== true && !!s.onChange && isNotActive,
145
- showGoToSource: !!s.onOpenSourceView,
146
- target: s.getContainer()
170
+ showFitDiagram: s.fitViewEnabled && s.zoomable && isNotWalkthrough,
171
+ showLayoutDriftWarning: !s.readonly && s.view.hasLayoutDrift === true && isNotActive,
172
+ showChangeAutoLayout: s.isEditable() && isNotActive,
173
+ showGoToSource: !!s.onOpenSourceView && isNotWalkthrough,
174
+ viewportChanged: s.viewportChanged,
175
+ showResetControlPoints: s.readonly !== true && s.experimentalEdgeEditing === true
147
176
  })
148
177
  })
149
- const portalProps = target ? { portalProps: { target } } : undefined
178
+ const portalProps = useMantinePortalProps()
150
179
 
151
180
  return (
152
- <Stack
153
- className={clsx(
154
- 'react-flow__panel',
155
- css.panel,
156
- 'likec4-top-left-panel'
157
- )}
158
- onClick={e => e.stopPropagation()}
159
- gap={'xs'}>
160
- {showNavigationButtons && <BackwardForwardButtons />}
161
- {showGoToSource && (
162
- <Tooltip label="Go to source" {...portalProps}>
163
- <ActionIcon
164
- className="action-icon"
165
- onClick={e => {
166
- e.stopPropagation()
167
- store.getState().onOpenSourceView?.()
168
- }}>
169
- <IconFileSymlink stroke={1.5} />
170
- </ActionIcon>
171
- </Tooltip>
172
- )}
173
- {showChangeAutoLayout && <ChangeAutoLayoutButton {...portalProps} />}
174
- <AnimatePresence>
175
- {showLayoutDriftWarning && (
176
- <m.div
177
- initial={{ opacity: 0.05, transform: 'translateX(-40%)' }}
178
- animate={{ opacity: 1, transform: 'translateX(0)' }}
179
- exit={{
180
- opacity: 0,
181
- transform: 'translateX(-40%)'
182
- }}
183
- key={'layout'}>
184
- <LayoutDriftNotification {...portalProps} />
185
- </m.div>
181
+ <TooltipGroup openDelay={500} closeDelay={150}>
182
+ <Stack
183
+ className={clsx(
184
+ 'react-flow__panel',
185
+ css.panel,
186
+ 'likec4-top-left-panel'
186
187
  )}
187
- {showFitDiagram && (
188
- <m.div
189
- initial={{ opacity: 0.05, transform: 'translateX(-20%)' }}
190
- animate={{ opacity: 1, transform: 'translateX(0)' }}
191
- exit={{
192
- opacity: 0,
193
- transform: 'translateX(-30%)'
194
- }}
195
- key={'fit-view'}>
196
- <Tooltip label="Center diagram" {...portalProps}>
188
+ align="flex-start"
189
+ onClick={e => e.stopPropagation()}
190
+ gap={'xs'}>
191
+ {showNavigationButtons && <BackwardForwardButtons />}
192
+ <ActionIconGroup className={css.actionIconGroup} orientation="vertical">
193
+ {showGoToSource && (
194
+ <Tooltip label="Open source" {...portalProps}>
195
+ <ActionIcon
196
+ onClick={e => {
197
+ e.stopPropagation()
198
+ store.getState().onOpenSourceView?.()
199
+ }}>
200
+ <IconFileSymlink stroke={1.5} />
201
+ </ActionIcon>
202
+ </Tooltip>
203
+ )}
204
+ {showChangeAutoLayout && <ChangeAutoLayoutButton {...portalProps} />}
205
+ {showLayoutDriftWarning && <LayoutDriftNotification {...portalProps} />}
206
+ {showResetControlPoints && <ResetControlPointsButton {...portalProps} />}
207
+ {showFitDiagram && (
208
+ <Tooltip label={viewportChanged ? 'Center camera' : 'Camera is centered'} {...portalProps}>
197
209
  <ActionIcon
198
- className="action-icon"
199
210
  onClick={e => {
200
211
  e.stopPropagation()
201
212
  store.getState().fitDiagram()
@@ -203,10 +214,10 @@ export const TopLeftPanel = () => {
203
214
  <IconFocusCentered />
204
215
  </ActionIcon>
205
216
  </Tooltip>
206
- </m.div>
207
- )}
208
- </AnimatePresence>
209
- </Stack>
217
+ )}
218
+ </ActionIconGroup>
219
+ </Stack>
220
+ </TooltipGroup>
210
221
  )
211
222
  }
212
223
  TopLeftPanel.displayName = 'TopLeftPanel'
@@ -1,19 +1,20 @@
1
1
  import { ActionIcon as MantineActionIcon, Tooltip as MantineTooltip } from '@mantine/core'
2
2
 
3
3
  export const Tooltip = MantineTooltip.withProps({
4
- color: 'gray',
4
+ color: 'dark',
5
5
  fz: 'xs',
6
6
  position: 'right',
7
7
  openDelay: 400,
8
- closeDelay: 100,
8
+ closeDelay: 150,
9
9
  label: '',
10
10
  children: null,
11
- offset: 8,
12
- transitionProps: { transition: 'fade-right', duration: 200 }
11
+ offset: 8
13
12
  })
14
13
 
15
14
  export const ActionIcon = MantineActionIcon.withProps({
16
- className: 'action-icon',
17
- variant: 'light',
15
+ classNames: {
16
+ root: 'action-icon'
17
+ },
18
+ variant: 'default',
18
19
  color: 'gray'
19
20
  })
@@ -1,28 +1,37 @@
1
1
  import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2
- import { mantine } from '../../mantine.css'
3
- import { vars } from '../../theme.css'
2
+ import { calc } from '@vanilla-extract/css-utils'
3
+ import { mantine, vars } from '../../theme-vars'
4
+
5
+ export const backwardForwardButtons = style({
6
+ gap: calc(mantine.spacing.xs).divide(1.5).toString()
7
+ })
4
8
 
5
9
  export const panel = style({
6
10
  top: fallbackVar(vars.navigationPanel.top, '1rem'),
7
11
  left: fallbackVar(vars.navigationPanel.left, '1rem'),
12
+ gap: calc(mantine.spacing.xs).divide(1.5).toString(),
8
13
  margin: 0
9
14
  })
10
15
 
11
16
  globalStyle(`.likec4-top-left-panel .action-icon`, {
12
17
  vars: {
13
- ['--ai-size']: 'var(--ai-size-lg)'
18
+ ['--ai-size']: '2rem'
14
19
  }
15
20
  })
16
21
  globalStyle(`.likec4-top-left-panel .action-icon .tabler-icon`, {
17
- width: '70%',
18
- height: '70%'
22
+ width: '65%',
23
+ height: '65%'
24
+ })
25
+
26
+ export const actionIconGroup = style({
27
+ boxShadow: mantine.shadows.md
19
28
  })
20
29
 
21
30
  export const autolayoutIcon = style({})
22
31
 
23
32
  globalStyle(`${autolayoutIcon} .tabler-icon`, {
24
- width: '75%',
25
- height: '75%'
33
+ width: '65%',
34
+ height: '65%'
26
35
  })
27
36
 
28
37
  export const autolayoutIndicator = style({
@@ -32,7 +41,7 @@ export const autolayoutIndicator = style({
32
41
  selectors: {
33
42
  [`:where([data-mantine-color-scheme="dark"]) &`]: {
34
43
  background: mantine.colors.dark[5],
35
- border: `1px solid ${mantine.colors.dark[4]}`
44
+ borderColor: mantine.colors.dark[4]
36
45
  }
37
46
  }
38
47
  })
@@ -0,0 +1,57 @@
1
+ export interface Vector {
2
+ x: number
3
+ y: number
4
+ }
5
+
6
+ export class VectorImpl implements Vector {
7
+ constructor(public x: number, public y: number) {}
8
+
9
+ static create(position: Vector): VectorImpl {
10
+ return new VectorImpl(position.x, position.y)
11
+ }
12
+
13
+ static add(a: Vector, b: Vector): Vector {
14
+ return { x: a.x + b.x, y: a.y + b.y }
15
+ }
16
+ static sub(a: Vector, b: Vector): Vector {
17
+ return { x: a.x - b.x, y: a.y - b.y }
18
+ }
19
+ static mul(a: Vector, b: number): Vector {
20
+ return { x: a.x * b, y: a.y * b }
21
+ }
22
+ static dot(a: Vector, b: Vector): number {
23
+ return a.x * b.x + a.y * b.y
24
+ }
25
+ static cross(a: Vector, b: Vector): VectorImpl {
26
+ return new VectorImpl(a.y * b.x - a.x * b.y, a.x * b.y - a.y * b.x)
27
+ }
28
+ static setLength(a: Vector, length: number): Vector {
29
+ return vector(a).setLength(length)
30
+ }
31
+
32
+ add(b: Vector): VectorImpl {
33
+ return new VectorImpl(this.x + b.x, this.y + b.y)
34
+ }
35
+ sub(b: Vector): VectorImpl {
36
+ return new VectorImpl(this.x - b.x, this.y - b.y)
37
+ }
38
+ mul(b: number): VectorImpl {
39
+ return new VectorImpl(this.x * b, this.y * b)
40
+ }
41
+ dot(b: Vector): number {
42
+ return this.x * b.x + this.y * b.y
43
+ }
44
+ cross(b: Vector): VectorImpl {
45
+ return new VectorImpl(this.y * b.x - this.x * b.y, this.x * b.y - this.y * b.x)
46
+ }
47
+ abs(): number {
48
+ return Math.sqrt(this.x * this.x + this.y * this.y)
49
+ }
50
+ setLength(length: number): VectorImpl {
51
+ return this.mul(length / this.abs())
52
+ }
53
+ }
54
+
55
+ export function vector(source: Vector): VectorImpl {
56
+ return VectorImpl.create(source)
57
+ }
@@ -36,16 +36,16 @@ function FitViewOnViewportResize({ diagramApi }: {
36
36
  fitDiagram(250)
37
37
  },
38
38
  [dimensions, diagramApi],
39
- 250,
40
- 1000
39
+ 250
41
40
  )
42
41
 
43
42
  return null
44
43
  }
45
44
 
46
- function selector({ view, viewportChanged, fitViewPadding }: DiagramState) {
45
+ function selector({ view, activeWalkthrough, viewportChanged, fitViewPadding }: DiagramState) {
47
46
  return {
48
47
  layoutId: view.id + '_' + view.autoLayout + '_' + fitViewPadding,
48
+ isActiveWalkthrough: !!activeWalkthrough,
49
49
  viewportNotMoved: !viewportChanged
50
50
  }
51
51
  }
@@ -56,6 +56,7 @@ function selector({ view, viewportChanged, fitViewPadding }: DiagramState) {
56
56
  export function FitViewOnDiagramChange() {
57
57
  const {
58
58
  layoutId,
59
+ isActiveWalkthrough,
59
60
  viewportNotMoved
60
61
  } = useDiagramState(selector, shallowEqual)
61
62
  const diagramApi = useDiagramStoreApi()
@@ -75,7 +76,7 @@ export function FitViewOnDiagramChange() {
75
76
  50
76
77
  )
77
78
 
78
- if (viewportNotMoved && !requiresFit) {
79
+ if (viewportNotMoved && !isActiveWalkthrough && !requiresFit) {
79
80
  return <FitViewOnViewportResize diagramApi={diagramApi} />
80
81
  }
81
82
  return null
@@ -4,6 +4,7 @@ import type { EdgeChange, NodeChange } from '@xyflow/react'
4
4
  import { getBoundsOfRects, getViewportForBounds } from '@xyflow/system'
5
5
  import { useUpdateEffect } from '../hooks'
6
6
  import { useDiagramState, useDiagramStoreApi } from '../hooks/useDiagramState'
7
+ import { MinZoom } from './const'
7
8
  import type { XYFlowEdge, XYFlowNode } from './types'
8
9
  import { nodeToRect } from './utils'
9
10
 
@@ -20,7 +21,6 @@ export function SelectEdgesOnNodeFocus() {
20
21
  if (!container) {
21
22
  return
22
23
  }
23
- const { width, height } = container.getBoundingClientRect()
24
24
 
25
25
  const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
26
26
  const nodeChanges = [] as NodeChange<XYFlowNode>[]
@@ -68,6 +68,8 @@ export function SelectEdgesOnNodeFocus() {
68
68
  }
69
69
  triggerNodeChanges(nodeChanges)
70
70
 
71
+ const { width, height } = container.getBoundingClientRect()
72
+
71
73
  const maxZoom = Math.max(1, transform[2])
72
74
  const viewport = getViewportForBounds(
73
75
  {
@@ -78,7 +80,7 @@ export function SelectEdgesOnNodeFocus() {
78
80
  },
79
81
  width,
80
82
  height,
81
- 0.1,
83
+ MinZoom,
82
84
  maxZoom,
83
85
  0
84
86
  )
@@ -91,10 +93,10 @@ export function SelectEdgesOnNodeFocus() {
91
93
  focusedNodeId !== null
92
94
  ? [
93
95
  ['Escape', (e) => {
94
- e.stopImmediatePropagation()
95
- const { xystore, fitDiagram } = diagramStore.getState()
96
+ e.stopPropagation()
97
+ const { resetFocusAndLastClicked, fitDiagram } = diagramStore.getState()
98
+ resetFocusAndLastClicked()
96
99
  fitDiagram()
97
- xystore.getState().resetSelectedElements()
98
100
  }, { preventDefault: true }]
99
101
  ]
100
102
  : []
@@ -1,6 +1,6 @@
1
1
  import { ReactFlow, useOnViewportChange } from '@xyflow/react'
2
2
  import { shallowEqual } from 'fast-equals'
3
- import { type CSSProperties, type PropsWithChildren, useMemo, useState } from 'react'
3
+ import { type CSSProperties, type PropsWithChildren, useEffect, useMemo, useState } from 'react'
4
4
  import { useDiagramState } from '../hooks/useDiagramState'
5
5
  import { useXYStoreApi } from '../hooks/useXYFlow'
6
6
  import type { DiagramState } from '../state/diagramStore'
@@ -88,14 +88,7 @@ export function XYFlow({
88
88
  translateY
89
89
  } = useDiagramState(selector, shallowEqual)
90
90
  const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(zoomable)
91
-
92
- // useLogger('XYFlow',[
93
- // nodes,
94
- // edges,
95
- // ])
96
-
97
91
  const layoutConstraints = useLayoutConstraints()
98
-
99
92
  const handlers = useXYFlowEvents()
100
93
 
101
94
  /**
@@ -111,10 +104,15 @@ export function XYFlow({
111
104
  if (x !== roundedX || y !== roundedY) {
112
105
  xyflowApi.setState({ transform: [roundedX, roundedY, zoom] })
113
106
  }
114
- setZoomOnDoubleClick(zoomable && zoom < 1.1)
107
+ setZoomOnDoubleClick(zoomable && zoom < 0.75)
115
108
  }
116
109
  })
117
110
 
111
+ useEffect(() => {
112
+ const zoom = xyflowApi.getState().transform[2]
113
+ setZoomOnDoubleClick(zoomable && zoom < 0.75)
114
+ }, [])
115
+
118
116
  return (
119
117
  <ReactFlow<XYFlowNode, XYFlowEdge>
120
118
  className={className}
@@ -129,6 +127,7 @@ export function XYFlow({
129
127
  {...(!zoomable && {
130
128
  zoomActivationKeyCode: null
131
129
  })}
130
+ zoomOnDoubleClick={zoomOnDoubleClick}
132
131
  maxZoom={zoomable ? MaxZoom : 1}
133
132
  minZoom={zoomable ? MinZoom : 1}
134
133
  fitView={fitView}
@@ -163,8 +162,7 @@ export function XYFlow({
163
162
  onNodeDrag: layoutConstraints.onNodeDrag,
164
163
  onNodeDragStop: layoutConstraints.onNodeDragStop
165
164
  }}
166
- nodeDragThreshold={3}
167
- zoomOnDoubleClick={zoomOnDoubleClick}
165
+ nodeDragThreshold={4}
168
166
  elevateNodesOnSelect={false} // or edges are not visible after select\
169
167
  selectNodesOnDrag={false}
170
168
  onNodesChange={onNodesChange}
@@ -194,5 +192,6 @@ export function XYFlow({
194
192
  </ReactFlow>
195
193
  )
196
194
  }
195
+ XYFlow.displayName = 'XYFlow'
197
196
 
198
197
  // export const XYFlow = memo(XYFlowWrapper) as typeof XYFlowWrapper