@likec4/diagram 1.7.2 → 1.7.3

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 (67) 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 +755 -401
  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 +56 -35
  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/edges/edges.css.d.ts +1 -1
  19. package/dist/xyflow/{hooks/useLayoutConstraints.d.ts → useLayoutConstraints.d.ts} +1 -1
  20. package/dist/xyflow/utils.d.ts +11 -1
  21. package/package.json +6 -6
  22. package/src/LikeC4Diagram.props.ts +66 -32
  23. package/src/LikeC4Diagram.tsx +19 -12
  24. package/src/StaticLikeC4Diagram.tsx +2 -0
  25. package/src/hooks/index.ts +2 -0
  26. package/src/{state/useDiagramStore.ts → hooks/useDiagramState.ts} +3 -4
  27. package/src/{xyflow/hooks → hooks}/useXYFlow.ts +1 -1
  28. package/src/index.ts +2 -2
  29. package/src/state/DiagramContext.tsx +15 -7
  30. package/src/state/diagram-to-xyflow.ts +17 -3
  31. package/src/state/diagramStore.ts +73 -38
  32. package/src/ui/DiagramTitlePanel.tsx +1 -1
  33. package/src/ui/DynamicViewWalkthrough.tsx +1 -2
  34. package/src/ui/OptionsPanel.tsx +1 -1
  35. package/src/ui/options/EdgeOptions.tsx +1 -16
  36. package/src/ui/options/NodeOptions.tsx +38 -13
  37. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +131 -0
  38. package/src/ui/top-left/TopLeftPanel.tsx +213 -0
  39. package/src/ui/top-left/_shared.ts +19 -0
  40. package/src/ui/top-left/styles.css.ts +38 -0
  41. package/src/xyflow/FitviewOnDiagramChange.tsx +2 -2
  42. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +87 -59
  43. package/src/xyflow/XYFlow.tsx +7 -8
  44. package/src/xyflow/XYFlowEvents.tsx +39 -19
  45. package/src/xyflow/XYFlowInner.tsx +5 -7
  46. package/src/xyflow/edges/RelationshipEdge.tsx +60 -27
  47. package/src/xyflow/edges/edges.css.ts +29 -25
  48. package/src/xyflow/nodes/compound/CompoundNode.tsx +1 -1
  49. package/src/xyflow/nodes/element/ElementLink.tsx +1 -2
  50. package/src/xyflow/nodes/element/ElementNode.tsx +7 -5
  51. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  52. package/src/xyflow/{hooks/useLayoutConstraints.ts → useLayoutConstraints.ts} +3 -3
  53. package/src/xyflow/utils.ts +25 -1
  54. package/dist/state/hooks.d.ts +0 -4
  55. package/dist/state/index.d.ts +0 -2
  56. package/dist/state/useNavigationHistory.d.ts +0 -5
  57. package/dist/types.d.ts +0 -20
  58. package/dist/ui/BackwardForwardPanel.css.d.ts +0 -1
  59. package/dist/ui/BackwardForwardPanel.d.ts +0 -1
  60. package/dist/xyflow/hooks/index.d.ts +0 -2
  61. package/src/state/hooks.ts +0 -5
  62. package/src/state/index.ts +0 -2
  63. package/src/state/useNavigationHistory.ts +0 -39
  64. package/src/types.ts +0 -22
  65. package/src/ui/BackwardForwardPanel.css.ts +0 -22
  66. package/src/ui/BackwardForwardPanel.tsx +0 -76
  67. package/src/xyflow/hooks/index.ts +0 -3
@@ -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 } 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
7
  import { 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,
@@ -163,9 +162,9 @@ export function XYFlow({
163
162
  onNodeDragStop: layoutConstraints.onNodeDragStop
164
163
  }}
165
164
  nodeDragThreshold={3}
166
- zoomOnDoubleClick={false}
165
+ zoomOnDoubleClick={zoomable}
167
166
  elevateNodesOnSelect={false} // or edges are not visible after select\
168
- selectNodesOnDrag={false} // or weird camera movement
167
+ selectNodesOnDrag={false}
169
168
  onNodesChange={onNodesChange}
170
169
  onEdgesChange={onEdgesChange}
171
170
  onDoubleClick={handlers.onDoubleClick}
@@ -3,7 +3,7 @@ import type { ReactFlowProps } from '@xyflow/react'
3
3
  import { useMemo, useRef } from 'react'
4
4
  import { isNonNullish, isTruthy } from 'remeda'
5
5
  import type { Simplify } from 'type-fest'
6
- import { useDiagramStoreApi } from '../state/useDiagramStore'
6
+ import { useDiagramStoreApi } from '../hooks/useDiagramState'
7
7
  import type { XYFlowEdge, XYFlowNode } from './types'
8
8
 
9
9
  export type XYFlowEventHandlers = Simplify<
@@ -57,18 +57,15 @@ export function useXYFlowEvents() {
57
57
  return ({
58
58
  onDoubleClick: (event) => {
59
59
  const {
60
- fitViewEnabled,
61
60
  onCanvasDblClick,
62
- resetLastClicked,
63
- fitDiagram
61
+ xystore,
62
+ resetLastClicked
64
63
  } = diagramApi.getState()
65
- if (fitViewEnabled) {
66
- fitDiagram()
67
- if (!onCanvasDblClick) {
68
- event.stopPropagation()
69
- }
70
- }
71
64
  resetLastClicked()
65
+ xystore.getState().resetSelectedElements()
66
+ if (!onCanvasDblClick) {
67
+ event.stopPropagation()
68
+ }
72
69
  onCanvasDblClick?.(event)
73
70
  },
74
71
  onPaneClick: (event) => {
@@ -80,6 +77,7 @@ export function useXYFlowEvents() {
80
77
  activeDynamicViewStep,
81
78
  fitDiagram,
82
79
  onCanvasClick,
80
+ xystore,
83
81
  resetLastClicked
84
82
  } = diagramApi.getState()
85
83
  if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
@@ -89,6 +87,7 @@ export function useXYFlowEvents() {
89
87
  }
90
88
  }
91
89
  resetLastClicked()
90
+ xystore.getState().resetSelectedElements()
92
91
  onCanvasClick?.(event)
93
92
  },
94
93
  onNodeContextMenu: (event, xynode) => {
@@ -117,27 +116,42 @@ export function useXYFlowEvents() {
117
116
  fitDiagram,
118
117
  focusOnNode,
119
118
  onNodeClick,
119
+ xystore,
120
120
  enableFocusMode,
121
121
  lastClickedNodeId,
122
- setLastClickedNode
122
+ nodesSelectable,
123
+ setLastClickedNode,
124
+ onOpenSourceElement
123
125
  } = diagramApi.getState()
124
126
  setLastClickedNode(xynode.id)
125
127
  // if we focused on a node, and clicked on another node - focus on the clicked node
126
128
  const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
127
129
  // if user clicked on the same node twice in a short time, focus on it
128
130
  const clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent()
129
- if (enableFocusMode) {
130
- let stopPropagation = false
131
+
132
+ if (clickedRecently && !!onOpenSourceElement) {
133
+ onOpenSourceElement(xynode.data.element.id)
134
+ }
135
+
136
+ if (enableFocusMode || nodesSelectable) {
137
+ let stopPropagation = true
131
138
  switch (true) {
132
- case !focusedNodeId && clickedRecently:
133
- case shallChangeFocus:
134
- stopPropagation = true
139
+ case enableFocusMode && !focusedNodeId && clickedRecently:
140
+ case enableFocusMode && shallChangeFocus: {
135
141
  focusOnNode(xynode.id)
136
142
  break
137
- case focusedNodeId === xynode.id && clickedRecently:
138
- stopPropagation = true
143
+ }
144
+ case enableFocusMode && focusedNodeId === xynode.id && clickedRecently: {
139
145
  fitDiagram()
140
146
  break
147
+ }
148
+ case nodesSelectable: {
149
+ xystore.getState().addSelectedNodes([xynode.id])
150
+ break
151
+ }
152
+ default: {
153
+ stopPropagation = false
154
+ }
141
155
  }
142
156
  if (!onNodeClick && stopPropagation) {
143
157
  // user did not provide a custom handler, stop propagation
@@ -179,6 +193,8 @@ export function useXYFlowEvents() {
179
193
  activateDynamicStep,
180
194
  activeDynamicViewStep,
181
195
  focusedNodeId,
196
+ xystore,
197
+ nodesSelectable,
182
198
  focusOnNode,
183
199
  onEdgeClick,
184
200
  setLastClickedEdge
@@ -204,7 +220,11 @@ export function useXYFlowEvents() {
204
220
  if (isEdgeOfFocusedNode) {
205
221
  focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
206
222
  if (!onEdgeClick) {
207
- // user did not provide a custom handler, stop propagation
223
+ event.stopPropagation()
224
+ }
225
+ } else if (nodesSelectable) {
226
+ xystore.getState().addSelectedEdges([xyedge.id])
227
+ if (!onEdgeClick) {
208
228
  event.stopPropagation()
209
229
  }
210
230
  }
@@ -2,27 +2,25 @@ import { Controls } from '@xyflow/react'
2
2
  import { shallowEqual } from 'fast-equals'
3
3
  import { memo, type PropsWithChildren } from 'react'
4
4
  import { isNonNullish } from 'remeda'
5
+ import { useDiagramState } from '../hooks/useDiagramState'
5
6
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
6
- import { useDiagramState } from '../state/useDiagramStore'
7
- import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
8
7
  import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
8
  import { DynamicViewWalkthrough } from '../ui/DynamicViewWalkthrough'
10
9
  import OptionsPanel from '../ui/OptionsPanel'
10
+ import { TopLeftPanel } from '../ui/top-left/TopLeftPanel'
11
11
  import { XYFlowBackground } from './XYFlowBackground'
12
12
 
13
13
  type XYFlowInnerProps = PropsWithChildren<{
14
14
  background: NonNullable<LikeC4DiagramProperties['background']>
15
15
  controls: boolean
16
16
  showDiagramTitle: boolean
17
- showNavigationButtons: boolean
18
17
  }>
19
18
 
20
19
  export const XYFlowInner = memo(function XYFlowInnerR({
21
20
  children,
22
21
  background,
23
22
  controls,
24
- showDiagramTitle,
25
- showNavigationButtons
23
+ showDiagramTitle
26
24
  }: XYFlowInnerProps) {
27
25
  const {
28
26
  isDynamicView,
@@ -44,10 +42,10 @@ export const XYFlowInner = memo(function XYFlowInnerR({
44
42
  <>
45
43
  {isBgWithPattern && <XYFlowBackground background={background} />}
46
44
  {controls && <Controls position={'bottom-right'} />}
47
- {readonly === false && <OptionsPanel />}
45
+ <TopLeftPanel />
46
+ {readonly !== true && <OptionsPanel />}
48
47
  {isDiagramTitleVisible && <DiagramTitlePanel />}
49
48
  {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
50
- {showNavigationButtons && <BackwardForwardPanel />}
51
49
  {children}
52
50
  </>
53
51
  )