@likec4/diagram 1.1.1 → 1.2.1

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 (80) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +1 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +10 -5
  4. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  5. package/dist/hooks/useUpdateEffect.d.ts +1 -1
  6. package/dist/icons/Clipboard.d.ts +1 -1
  7. package/dist/icons/Link.d.ts +1 -1
  8. package/dist/icons/ZoomIn.d.ts +1 -1
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.mjs +2604 -1502
  11. package/dist/mantine/EnsureMantine.d.ts +1 -1
  12. package/dist/state/DiagramContext.d.ts +2307 -192
  13. package/dist/state/WhenInitialized.d.ts +1 -1
  14. package/dist/state/diagramStore.d.ts +2220 -97
  15. package/dist/state/useDiagramStore.d.ts +2124 -9
  16. package/dist/style.css +210 -136
  17. package/dist/theme.css.d.ts +1 -0
  18. package/dist/ui/DiagramTitlePanel.d.ts +1 -2
  19. package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
  20. package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
  21. package/dist/ui/OptionsPanel.d.ts +1 -2
  22. package/dist/ui/index.d.ts +1 -0
  23. package/dist/ui/options/NodeOptions.d.ts +1 -2
  24. package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
  25. package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
  26. package/dist/xyflow/XYFlow.d.ts +5 -8
  27. package/dist/xyflow/XYFlowBackground.d.ts +6 -5
  28. package/dist/xyflow/XYFlowEvents.d.ts +60 -66
  29. package/dist/xyflow/XYFlowInner.d.ts +11 -0
  30. package/dist/xyflow/diagram-to-xyflow.d.ts +2 -2
  31. package/dist/xyflow/edges/RelationshipEdge.d.ts +17 -17
  32. package/dist/xyflow/edges/edges.css.d.ts +4 -2
  33. package/dist/xyflow/edges/utils.d.ts +3 -1
  34. package/dist/xyflow/hooks/useLayoutConstraints.d.ts +1 -1
  35. package/dist/xyflow/hooks/useXYFlow.d.ts +162 -162
  36. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  37. package/dist/xyflow/nodes/compound/CompoundNode.d.ts +1 -1
  38. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
  39. package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
  40. package/dist/xyflow/nodes/element/ElementNode.d.ts +2 -2
  41. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
  42. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  43. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
  44. package/dist/xyflow/types.d.ts +23 -23
  45. package/package.json +23 -21
  46. package/src/KeepAspectRatioContainer.tsx +1 -1
  47. package/src/LikeC4Diagram.css.ts +2 -0
  48. package/src/LikeC4Diagram.props.ts +9 -4
  49. package/src/LikeC4Diagram.tsx +17 -11
  50. package/src/StaticLikeC4Diagram.tsx +1 -0
  51. package/src/globals.css.ts +1 -0
  52. package/src/index.ts +0 -3
  53. package/src/state/DiagramContext.tsx +21 -12
  54. package/src/state/diagramStore.ts +177 -38
  55. package/src/theme.css.ts +1 -0
  56. package/src/ui/DiagramTitlePanel.css.ts +2 -1
  57. package/src/ui/DiagramTitlePanel.tsx +3 -4
  58. package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
  59. package/src/ui/DynamicViewWalkthrough.tsx +86 -0
  60. package/src/ui/OptionsPanel.tsx +5 -3
  61. package/src/ui/index.ts +1 -0
  62. package/src/ui/options/NodeOptions.tsx +23 -24
  63. package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
  64. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
  65. package/src/xyflow/SyncWithDiagram.tsx +55 -107
  66. package/src/xyflow/XYFlow.tsx +51 -35
  67. package/src/xyflow/XYFlowBackground.tsx +7 -5
  68. package/src/xyflow/XYFlowEvents.tsx +100 -95
  69. package/src/xyflow/XYFlowInner.tsx +50 -0
  70. package/src/xyflow/diagram-to-xyflow.ts +16 -21
  71. package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
  72. package/src/xyflow/edges/edges.css.ts +68 -34
  73. package/src/xyflow/edges/utils.ts +31 -0
  74. package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
  75. package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
  76. package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
  77. package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
  78. package/src/xyflow/nodes/element/element.css.ts +9 -2
  79. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
  80. package/src/xyflow/types.ts +7 -8
@@ -10,9 +10,10 @@ export const container = style({
10
10
 
11
11
  export const paper = style({
12
12
  cursor: 'default',
13
- minWidth: 200,
13
+ minWidth: 100,
14
14
  maxWidth: 'calc(100vw - 16px)',
15
15
  backgroundColor: `color-mix(in srgb, ${mantine.colors.body}, transparent 20%)`,
16
+ WebkitBackdropFilter: 'blur(8px)',
16
17
  backdropFilter: 'blur(8px)',
17
18
  '@media': {
18
19
  [mantine.largerThan('md')]: {
@@ -1,10 +1,9 @@
1
1
  import { Anchor, Box, Button, CopyButton, Group, Paper, Spoiler, Stack, Text } from '@mantine/core'
2
- import { useToggle } from '@react-hookz/web'
2
+ import { useLocalStorage } from '@mantine/hooks'
3
3
  import clsx from 'clsx'
4
4
  import { AnimatePresence, motion } from 'framer-motion'
5
5
  import { type DiagramState, useDiagramState } from '../state'
6
6
  import * as css from './DiagramTitlePanel.css'
7
- import { useLocalStorage } from '@mantine/hooks'
8
7
 
9
8
  const selector = (s: DiagramState) => ({
10
9
  id: s.view.id,
@@ -40,7 +39,7 @@ export default function DiagramTitlePanel() {
40
39
  radius="sm"
41
40
  withBorder={!isCollapsed}
42
41
  className={css.paper}
43
- p={isCollapsed ? 'sm' : 'md'}
42
+ p={isCollapsed ? 'xs' : 'lg'}
44
43
  onDoubleClick={e => e.stopPropagation()}>
45
44
  <Stack gap={'sm'} justify="stretch" align="stretch">
46
45
  <Group justify="stretch" wrap="nowrap" onClick={toggle} style={{ cursor: 'pointer' }}>
@@ -99,7 +98,7 @@ export default function DiagramTitlePanel() {
99
98
  justify="stretch"
100
99
  align="stretch">
101
100
  {links.map((link) => (
102
- <Group key={link} wrap="nowrap" align='center' gap={'sm'}>
101
+ <Group key={link} wrap="nowrap" align="center" gap={'sm'}>
103
102
  <Box flex={'1'} style={{ overflow: 'hidden' }}>
104
103
  <Anchor
105
104
  href={link}
@@ -0,0 +1,46 @@
1
+ import { createVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine } from '../mantine.css'
3
+
4
+ export const container = style({
5
+ position: 'absolute',
6
+ bottom: '0.5rem',
7
+ left: '50%',
8
+ transform: 'translateX(-50%)',
9
+ padding: 0,
10
+ margin: 0
11
+ })
12
+
13
+ export const buttons = style({
14
+ backdropFilter: 'blur(8px)',
15
+ transition: 'all 175ms ease-in',
16
+ ':hover': {
17
+ transitionTimingFunction: 'ease-out',
18
+ transform: 'scale(1.1)'
19
+ },
20
+ ':active': {
21
+ transitionDuration: '100ms',
22
+ transform: 'scale(0.98) translateY(3px)'
23
+ }
24
+ })
25
+
26
+ var transparency = createVar()
27
+ export const btn = style({
28
+ transition: 'all 175ms ease-in',
29
+ backgroundColor: 'var(--button-bg)',
30
+ vars: {
31
+ [transparency]: '35%',
32
+ ['--button-bg']: `color-mix(in srgb, ${mantine.colors.primaryColors.filled}, transparent ${transparency})`,
33
+ ['--button-hover']: `color-mix(in srgb, ${mantine.colors.primaryColors.filledHover}, transparent ${transparency})`
34
+ },
35
+ selectors: {
36
+ [mantine.lightSelector]: {
37
+ vars: {
38
+ [transparency]: '15%'
39
+ }
40
+ }
41
+ }
42
+ })
43
+
44
+ globalStyle(`${btn} .tabler-icon`, {
45
+ width: '80%'
46
+ })
@@ -0,0 +1,86 @@
1
+ import { ActionIcon, Box, Button, rem, useComputedColorScheme } from '@mantine/core'
2
+ import {
3
+ IconHeart,
4
+ IconPhoto,
5
+ IconPlayerPlayFilled,
6
+ IconPlayerSkipBackFilled,
7
+ IconPlayerSkipForwardFilled,
8
+ IconPlayerStopFilled,
9
+ IconSettings
10
+ } from '@tabler/icons-react'
11
+ import clsx from 'clsx'
12
+ import { isNullish, isNumber } from 'remeda'
13
+ import { useDiagramState } from '../state/useDiagramStore'
14
+ import * as css from './DynamicViewWalkthrough.css'
15
+
16
+ export function DynamicViewWalkthrough() {
17
+ const {
18
+ nextDynamicStep,
19
+ stopDynamicView,
20
+ activeDynamicViewStep,
21
+ hasNextSteps
22
+ } = useDiagramState(s => ({
23
+ nextDynamicStep: s.nextDynamicStep,
24
+ stopDynamicView: s.stopDynamicView,
25
+ activeDynamicViewStep: s.activeDynamicViewStep,
26
+ hasNextSteps: (s.activeDynamicViewStep ?? 1) < s.view.edges.length
27
+ }))
28
+
29
+ const buttonProps = {
30
+ className: css.btn,
31
+ size: 'compact-xl',
32
+ radius: 'xl'
33
+ }
34
+
35
+ const nextStep = (increment = 1) => (e: React.MouseEvent) => {
36
+ e.stopPropagation()
37
+ nextDynamicStep(increment)
38
+ }
39
+
40
+ return (
41
+ <Box
42
+ className={clsx('react-flow__panel', css.container)}
43
+ >
44
+ {isNullish(activeDynamicViewStep)
45
+ && (
46
+ <Button
47
+ {...buttonProps}
48
+ className={clsx(css.buttons, css.btn)}
49
+ rightSection={<IconPlayerPlayFilled />}
50
+ onClick={nextStep()}
51
+ px={'xl'}>
52
+ Start
53
+ </Button>
54
+ )}
55
+ {isNumber(activeDynamicViewStep) && (
56
+ <Button.Group className={css.buttons}>
57
+ <Button
58
+ {...buttonProps}
59
+ pl={'lg'}
60
+ disabled={activeDynamicViewStep === 1}
61
+ onClick={nextStep(-1)}>
62
+ <IconPlayerSkipBackFilled />
63
+ </Button>
64
+
65
+ <Button
66
+ {...buttonProps}
67
+ px={'sm'}
68
+ onClick={e => {
69
+ e.stopPropagation()
70
+ stopDynamicView()
71
+ }}>
72
+ <IconPlayerStopFilled />
73
+ </Button>
74
+
75
+ <Button
76
+ {...buttonProps}
77
+ pr={'lg'}
78
+ disabled={!hasNextSteps}
79
+ onClick={nextStep()}>
80
+ <IconPlayerSkipForwardFilled />
81
+ </Button>
82
+ </Button.Group>
83
+ )}
84
+ </Box>
85
+ )
86
+ }
@@ -4,10 +4,13 @@ import clsx from 'clsx'
4
4
  import { shallowEqual } from 'fast-equals'
5
5
  import { AnimatePresence, motion } from 'framer-motion'
6
6
  import { useState } from 'react'
7
- import * as styles from './OptionsPanel.css'
7
+ import { isNullish, isTruthy } from 'remeda'
8
+ import { useDiagramState } from '../state'
8
9
  import { NodeOptions } from './options/NodeOptions'
10
+ import * as styles from './OptionsPanel.css'
9
11
 
10
12
  export default function OptionsPanel() {
13
+ const isFocusDisabled = useDiagramState(s => isNullish(s.focusedNodeId))
11
14
  const [selectedNodes, setSelectedNodes] = useState([] as string[])
12
15
 
13
16
  useOnSelectionChange({
@@ -19,10 +22,9 @@ export default function OptionsPanel() {
19
22
  }
20
23
  })
21
24
 
22
-
23
25
  return (
24
26
  <AnimatePresence mode="wait">
25
- {selectedNodes.length > 0 && (
27
+ {isFocusDisabled && selectedNodes.length > 0 && (
26
28
  <motion.div
27
29
  initial={{ opacity: 0, scale: 0.85 }}
28
30
  animate={{ opacity: 1, scale: 1 }}
package/src/ui/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  // export { default as Camera } from './Camera'
2
+ export * from './DynamicViewWalkthrough'
2
3
  export { default as OptionsPanel } from './OptionsPanel'
@@ -1,9 +1,9 @@
1
1
  import {
2
- ElementShapes,
3
- defaultTheme,
4
- invariant,
5
2
  type BorderStyle,
3
+ defaultTheme,
6
4
  type ElementShape,
5
+ ElementShapes,
6
+ invariant,
7
7
  type NonEmptyArray,
8
8
  type ThemeColor
9
9
  } from '@likec4/core'
@@ -13,14 +13,14 @@ import {
13
13
  ColorSwatch,
14
14
  Divider,
15
15
  Flex,
16
+ rem,
16
17
  SegmentedControl,
17
18
  Select,
18
19
  Slider,
19
20
  Stack,
20
21
  Text,
21
22
  Tooltip,
22
- TooltipGroup,
23
- rem
23
+ TooltipGroup
24
24
  } from '@mantine/core'
25
25
  import { useEffect, useState } from 'react'
26
26
  import { hasAtLeast, keys, takeWhile } from 'remeda'
@@ -57,7 +57,7 @@ export type ColorKey = typeof colors[0]['key']
57
57
  type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
58
58
 
59
59
  export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] }) {
60
- // export function NodeOptions(props: { nodes: XYFlowNode[] }) {
60
+ // export function NodeOptions(props: { nodes: XYFlowNode[] }) {
61
61
  const diagramApi = useDiagramStoreApi()
62
62
  // const nodes = useXYNodesData(props.nodes.map(node => node.id))
63
63
  const nodes = useXYNodesData(selectedNodeIds)
@@ -104,25 +104,24 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
104
104
  onColorChange={(color: ColorKey | ThemeColorKey) => {
105
105
  triggerChange({ color })
106
106
  }} />
107
+
108
+ {showOpacityOption && (
109
+ <Box key={firstNode.id}>
110
+ <Divider label="opacity and border" labelPosition="left" />
111
+ <OpacityOption
112
+ node={firstNode}
113
+ onOpacityChange={(opacity: number) => {
114
+ triggerChange({ opacity })
115
+ }} />
116
+ <BorderStyleOption
117
+ node={firstNode}
118
+ onChange={(border: BorderStyle) => {
119
+ triggerChange({ border })
120
+ }}
121
+ />
122
+ </Box>
123
+ )}
107
124
  <NavigateToOption nodes={nodes} />
108
- <Box key={firstNode.id}>
109
- {showOpacityOption && (
110
- <>
111
- <Divider label="opacity and border" labelPosition="left" />
112
- <OpacityOption
113
- node={firstNode}
114
- onOpacityChange={(opacity: number) => {
115
- triggerChange({ opacity })
116
- }} />
117
- <BorderStyleOption
118
- node={firstNode}
119
- onChange={(border: BorderStyle) => {
120
- triggerChange({ border })
121
- }}
122
- />
123
- </>
124
- )}
125
- </Box>
126
125
  </Stack>
127
126
  )
128
127
  }
@@ -1,99 +1,119 @@
1
- import { useDebouncedCallback, useDebouncedEffect, useRafEffect } from '@react-hookz/web'
2
- import { useNodesInitialized } from '@xyflow/react'
3
- import { memo, useEffect, useRef, useState } from 'react'
1
+ import { useDebouncedEffect } from '@react-hookz/web'
2
+ import { useEffect, useRef } from 'react'
4
3
  import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
5
- import { useXYStore } from './hooks'
4
+ import { useXYStore, useXYStoreApi, type XYStoreApi } from './hooks'
6
5
  import type { XYFlowState } from './types'
6
+ import { toDomPrecision } from './utils'
7
7
 
8
8
  function selectDimensions(state: XYFlowState) {
9
9
  return `${Math.round(state.width)}:${Math.round(state.height)}`
10
10
  }
11
11
 
12
- function FitViewOnViewportResize({ diagramApi }: { diagramApi: DiagramStoreApi }) {
12
+ function FitViewOnViewportResize({ diagramApi, xyflowApi }: {
13
+ xyflowApi: XYStoreApi
14
+ diagramApi: DiagramStoreApi
15
+ }) {
13
16
  const dimensions = useXYStore(selectDimensions)
14
17
  const prevDimensionsRef = useRef(dimensions)
15
18
 
16
19
  useDebouncedEffect(
17
20
  () => {
18
- if (prevDimensionsRef.current === dimensions) {
21
+ const { focusedNodeId, fitDiagram } = diagramApi.getState()
22
+ if (focusedNodeId || prevDimensionsRef.current === dimensions) {
19
23
  return
20
24
  }
21
- diagramApi.getState().fitDiagram()
25
+ fitDiagram(xyflowApi)
22
26
  prevDimensionsRef.current = dimensions
23
27
  },
24
- [dimensions],
25
- 150
28
+ [dimensions, diagramApi, xyflowApi],
29
+ 200,
30
+ 800
26
31
  )
27
32
 
28
33
  return null
29
34
  }
30
35
 
31
- const diagramChangeId = (s: DiagramState) => s.view.id + '_' + s.view.autoLayout + '_' + s.fitViewPadding
36
+ function selector({ view, xyflowSynced, viewportChanged, lastOnNavigate, ...s }: DiagramState) {
37
+ const viewId = view.id + '_' + view.autoLayout + '_' + s.fitViewPadding
38
+
39
+ const waitCorrection = lastOnNavigate && lastOnNavigate.toView === view.id
40
+ && lastOnNavigate.positionCorrected === false
41
+ const elTo = waitCorrection ? view.nodes.find(n => n.id === lastOnNavigate.element.id) : undefined
42
+ if (waitCorrection && elTo) {
43
+ return {
44
+ viewId,
45
+ xyflowSynced,
46
+ viewportChanged,
47
+ waitCorrection: true,
48
+ elFrom: lastOnNavigate.element,
49
+ elTo
50
+ }
51
+ }
52
+
53
+ return {
54
+ viewId,
55
+ xyflowSynced,
56
+ viewportChanged,
57
+ waitCorrection: false,
58
+ fromElement: null,
59
+ toElement: null
60
+ }
61
+ }
32
62
 
33
63
  /**
34
64
  * Fits the view when the view changes and nodes are initialized
35
65
  */
36
- function FitViewOnDiagramChanges() {
66
+ export function FitViewOnDiagramChange() {
67
+ const xyflowApi = useXYStoreApi()
68
+ const {
69
+ viewId: pendingViewId,
70
+ xyflowSynced,
71
+ viewportChanged,
72
+ waitCorrection,
73
+ elFrom,
74
+ elTo
75
+ } = useDiagramState(selector)
37
76
  const diagramApi = useDiagramStoreApi()
38
- const [viewportMoved, setViewportMoved] = useState(diagramApi.getState().viewportChanged)
39
- const [processedChangeId, setProcessed] = useState(diagramChangeId(diagramApi.getState()))
40
- const [pendingChangeId, setPending] = useState(processedChangeId)
41
-
42
- const isReady = useNodesInitialized({
43
- includeHiddenNodes: true
44
- })
45
-
46
- const schedulePending = useDebouncedCallback(
47
- (pendingChangeId: string) => {
48
- setPending(pendingChangeId)
49
- },
50
- [setPending],
51
- 75
52
- )
53
-
54
- useEffect(
55
- () =>
56
- diagramApi.subscribe(
57
- s => diagramChangeId(s),
58
- schedulePending
59
- ),
60
- [diagramApi, schedulePending]
61
- )
62
-
63
- useEffect(
64
- () =>
65
- diagramApi.subscribe(
66
- s => s.viewportChanged,
67
- setViewportMoved
68
- ),
69
- [diagramApi, setViewportMoved]
70
- )
71
-
72
- useRafEffect(
73
- () => {
74
- if (!isReady || processedChangeId === pendingChangeId) {
75
- return
77
+ const processedRef = useRef(pendingViewId)
78
+
79
+ useEffect(() => {
80
+ if (!xyflowSynced || pendingViewId === processedRef.current) {
81
+ return
82
+ }
83
+ if (waitCorrection && elTo && elFrom) {
84
+ const [x, y, zoom] = xyflowApi.getState().transform
85
+ 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
+ })
76
98
  }
77
- diagramApi.getState().fitDiagram()
78
- setProcessed(pendingChangeId)
79
- },
80
- [isReady, processedChangeId, pendingChangeId, diagramApi]
81
- )
82
-
83
- if (!viewportMoved && isReady) {
84
- return <FitViewOnViewportResize diagramApi={diagramApi} />
99
+ const centerTo = xyflow.flowToScreenPosition({
100
+ x: elTo.position[0] + elTo.width / 2,
101
+ y: elTo.position[1] + elTo.height / 2
102
+ }),
103
+ diff = {
104
+ x: toDomPrecision(centerFrom.x - centerTo.x),
105
+ y: toDomPrecision(centerFrom.y - centerTo.y)
106
+ }
107
+ xyflowApi.getState().panBy(diff)
108
+ diagramApi.setState({ lastOnNavigate: null })
109
+ return
110
+ }
111
+ processedRef.current = pendingViewId
112
+ diagramApi.getState().fitDiagram(xyflowApi)
113
+ }, [pendingViewId, xyflowSynced, waitCorrection])
114
+
115
+ if (!viewportChanged && xyflowSynced) {
116
+ return <FitViewOnViewportResize xyflowApi={xyflowApi} diagramApi={diagramApi} />
85
117
  }
86
118
  return null
87
119
  }
88
-
89
- const selectFitViewFlag = (s: DiagramState) => s.fitViewEnabled
90
-
91
- export const FitViewOnDiagramChange = memo(() => {
92
- const fitView = useDiagramState(selectFitViewFlag)
93
-
94
- if (!fitView) {
95
- return null
96
- }
97
-
98
- return <FitViewOnDiagramChanges />
99
- })
@@ -1,57 +1,75 @@
1
+ import { nonNullable } from '@likec4/core'
1
2
  import type { EdgeChange, NodeChange } from '@xyflow/react'
2
- import { useUpdateEffect } from '../hooks'
3
- import { useDiagramState } from '../state/useDiagramStore'
3
+ import { useUpdateEffect } from '../hooks/useUpdateEffect'
4
+ import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
5
+ import { MinZoom } from './const'
4
6
  import { useXYStoreApi } from './hooks/useXYFlow'
5
7
  import type { XYFlowEdge, XYFlowNode } from './types'
6
8
 
7
9
  export function SelectEdgesOnNodeFocus() {
8
10
  const focusedNodeId = useDiagramState(s => s.focusedNodeId)
11
+ const diagramStore = useDiagramStoreApi()
9
12
  const xyflowApi = useXYStoreApi()
10
13
 
11
14
  useUpdateEffect(() => {
15
+ if (!focusedNodeId) {
16
+ xyflowApi.getState().resetSelectedElements()
17
+ return
18
+ }
19
+
12
20
  const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
13
21
  const nodeChanges = [] as NodeChange<XYFlowNode>[]
14
- const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = xyflowApi.getState()
15
- if (focusedNodeId) {
16
- for (const edge of edges) {
17
- if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
18
- edgeChanges.push({
19
- id: edge.id,
20
- type: 'select',
21
- selected: true
22
- })
23
- } else if (edge.selected) {
24
- edgeChanges.push({
25
- id: edge.id,
26
- type: 'select',
27
- selected: false
28
- })
29
- }
30
- }
31
- for (const node of nodes) {
32
- if (node.selected && node.id !== focusedNodeId) {
33
- nodeChanges.push({
34
- id: node.id,
35
- type: 'select',
36
- selected: false
37
- })
22
+ const { edgeLookup, nodeLookup, transform, triggerNodeChanges, fitView, triggerEdgeChanges } = xyflowApi.getState()
23
+ const fitViewNodes = new Set<XYFlowNode>()
24
+ fitViewNodes.add(nonNullable(nodeLookup.get(focusedNodeId)))
25
+
26
+ for (const [, edge] of edgeLookup) {
27
+ if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
28
+ edgeChanges.push({
29
+ id: edge.id,
30
+ type: 'select',
31
+ selected: true
32
+ })
33
+ const anotherNode = nodeLookup.get(edge.source === focusedNodeId ? edge.target : edge.source)
34
+ if (anotherNode) {
35
+ fitViewNodes.add(anotherNode)
38
36
  }
37
+ } else if (edge.selected) {
38
+ edgeChanges.push({
39
+ id: edge.id,
40
+ type: 'select',
41
+ selected: false
42
+ })
39
43
  }
40
- nodeChanges.push({
41
- id: focusedNodeId,
42
- type: 'select',
43
- selected: true
44
- })
45
- if (edgeChanges.length > 0) {
46
- triggerEdgeChanges(edgeChanges)
47
- }
48
- if (nodeChanges.length > 0) {
49
- triggerNodeChanges(nodeChanges)
44
+ }
45
+ for (const [, node] of nodeLookup) {
46
+ if (node.selected && node.id !== focusedNodeId) {
47
+ nodeChanges.push({
48
+ id: node.id,
49
+ type: 'select',
50
+ selected: false
51
+ })
50
52
  }
51
- } else {
52
- xyflowApi.getState().resetSelectedElements()
53
53
  }
54
- }, [focusedNodeId])
54
+ nodeChanges.push({
55
+ id: focusedNodeId,
56
+ type: 'select',
57
+ selected: true
58
+ })
59
+ if (edgeChanges.length > 0) {
60
+ triggerEdgeChanges(edgeChanges)
61
+ }
62
+ triggerNodeChanges(nodeChanges)
63
+
64
+ fitView({
65
+ includeHiddenNodes: true,
66
+ duration: 300,
67
+ padding: 0.1,
68
+ minZoom: MinZoom,
69
+ maxZoom: Math.max(1, transform[2]),
70
+ nodes: Array.from(fitViewNodes)
71
+ })
72
+ }, [diagramStore, xyflowApi, focusedNodeId])
55
73
 
56
74
  return null
57
75
  }