@likec4/diagram 1.1.0 → 1.2.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 (76) 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 +22 -12
  4. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.mjs +2209 -706
  7. package/dist/mantine/EnsureMantine.d.ts +1 -1
  8. package/dist/state/DiagramContext.d.ts +2143 -19
  9. package/dist/state/WhenInitialized.d.ts +1 -1
  10. package/dist/state/diagramStore.d.ts +2152 -19
  11. package/dist/state/useDiagramStore.d.ts +2142 -18
  12. package/dist/style.css +256 -118
  13. package/dist/theme.css.d.ts +1 -0
  14. package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
  15. package/dist/ui/DiagramTitlePanel.d.ts +1 -0
  16. package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
  17. package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
  18. package/dist/ui/OptionsPanel.d.ts +1 -3
  19. package/dist/ui/index.d.ts +1 -0
  20. package/dist/ui/options/NodeOptions.d.ts +1 -2
  21. package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
  22. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
  23. package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
  24. package/dist/xyflow/XYFlow.d.ts +4 -7
  25. package/dist/xyflow/XYFlowBackground.d.ts +5 -4
  26. package/dist/xyflow/XYFlowEvents.d.ts +84 -84
  27. package/dist/xyflow/XYFlowInner.d.ts +11 -0
  28. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  29. package/dist/xyflow/edges/RelationshipEdge.d.ts +14 -14
  30. package/dist/xyflow/edges/edges.css.d.ts +5 -2
  31. package/dist/xyflow/edges/utils.d.ts +2 -0
  32. package/dist/xyflow/hooks/useXYFlow.d.ts +126 -126
  33. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  34. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
  35. package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
  36. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  37. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
  38. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  39. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
  40. package/dist/xyflow/types.d.ts +18 -18
  41. package/package.json +21 -16
  42. package/src/KeepAspectRatioContainer.tsx +1 -1
  43. package/src/LikeC4Diagram.css.ts +4 -1
  44. package/src/LikeC4Diagram.props.ts +24 -14
  45. package/src/LikeC4Diagram.tsx +18 -9
  46. package/src/StaticLikeC4Diagram.tsx +2 -0
  47. package/src/globals.css.ts +7 -1
  48. package/src/index.ts +0 -3
  49. package/src/state/DiagramContext.tsx +21 -12
  50. package/src/state/diagramStore.ts +275 -98
  51. package/src/theme.css.ts +1 -0
  52. package/src/ui/DiagramTitlePanel.css.ts +33 -0
  53. package/src/ui/DiagramTitlePanel.tsx +138 -0
  54. package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
  55. package/src/ui/DynamicViewWalkthrough.tsx +86 -0
  56. package/src/ui/OptionsPanel.tsx +7 -24
  57. package/src/ui/index.ts +1 -0
  58. package/src/ui/options/NodeOptions.tsx +103 -27
  59. package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
  60. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +75 -0
  61. package/src/xyflow/SyncWithDiagram.tsx +55 -101
  62. package/src/xyflow/XYFlow.tsx +52 -32
  63. package/src/xyflow/XYFlowBackground.tsx +7 -5
  64. package/src/xyflow/XYFlowEvents.tsx +150 -104
  65. package/src/xyflow/XYFlowInner.tsx +50 -0
  66. package/src/xyflow/diagram-to-xyflow.ts +16 -21
  67. package/src/xyflow/edges/RelationshipEdge.tsx +102 -70
  68. package/src/xyflow/edges/edges.css.ts +128 -53
  69. package/src/xyflow/edges/utils.ts +31 -0
  70. package/src/xyflow/nodes/compound/CompoundNode.css.ts +20 -12
  71. package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
  72. package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
  73. package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
  74. package/src/xyflow/nodes/element/element.css.ts +10 -2
  75. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
  76. package/src/xyflow/types.ts +7 -8
@@ -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]
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]
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
- })
@@ -0,0 +1,75 @@
1
+ import { nonNullable } from '@likec4/core'
2
+ import type { EdgeChange, NodeChange } from '@xyflow/react'
3
+ import { useUpdateEffect } from '../hooks/useUpdateEffect'
4
+ import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
5
+ import { MinZoom } from './const'
6
+ import { useXYStoreApi } from './hooks/useXYFlow'
7
+ import type { XYFlowEdge, XYFlowNode } from './types'
8
+
9
+ export function SelectEdgesOnNodeFocus() {
10
+ const focusedNodeId = useDiagramState(s => s.focusedNodeId)
11
+ const diagramStore = useDiagramStoreApi()
12
+ const xyflowApi = useXYStoreApi()
13
+
14
+ useUpdateEffect(() => {
15
+ if (!focusedNodeId) {
16
+ xyflowApi.getState().resetSelectedElements()
17
+ return
18
+ }
19
+
20
+ const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
21
+ const nodeChanges = [] as NodeChange<XYFlowNode>[]
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)
36
+ }
37
+ } else if (edge.selected) {
38
+ edgeChanges.push({
39
+ id: edge.id,
40
+ type: 'select',
41
+ selected: false
42
+ })
43
+ }
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
+ })
52
+ }
53
+ }
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])
73
+
74
+ return null
75
+ }
@@ -1,119 +1,73 @@
1
- import { deepEqual as eq, shallowEqual } from 'fast-equals'
2
- import { useXYFlow, useXYStoreApi } from './hooks'
3
- import type { XYFlowNode } from './types'
1
+ import { deepEqual as eq } from 'fast-equals'
2
+ import { useXYStoreApi } from './hooks'
4
3
 
5
- import { memo, useEffect, useRef } from 'react'
6
- import { useDiagramStoreApi } from '../state'
4
+ import { useEffect } from 'react'
5
+ import { useDiagramState, useDiagramStoreApi } from '../state'
7
6
  import { diagramViewToXYFlowData } from './diagram-to-xyflow'
8
- import { toDomPrecision } from './utils'
9
-
10
- const isSameNode = (a: XYFlowNode) => (b: XYFlowNode) =>
11
- a.id === b.id && a.type === b.type && Object.is(a.parentId, b.parentId)
12
7
 
13
8
  /**
14
9
  * Syncs the diagram state with the XYFlow instance
15
10
  */
16
- export const SyncWithDiagram = memo(() => {
17
- const _xyflow = useXYFlow()
18
- const xyflowRef = useRef(_xyflow)
19
- xyflowRef.current = _xyflow
20
-
11
+ export function SyncWithDiagram() {
12
+ const xyflowSynced = useDiagramState(s => s.xyflowSynced)
21
13
  const xyflowApi = useXYStoreApi()
22
14
  const diagramStoreApi = useDiagramStoreApi()
23
15
 
24
16
  useEffect(() => {
25
- return diagramStoreApi.subscribe(
26
- // selector
27
- state => ({
28
- viewId: state.view.id,
29
- nodes: state.view.nodes,
30
- edges: state.view.edges,
31
- draggable: state.nodesDraggable,
32
- selectable: state.nodesSelectable
33
- }),
34
- // listener
35
- ({ viewId, nodes, edges, ...opts }) => {
36
- const { lastOnNavigate } = diagramStoreApi.getState()
37
- const updates = diagramViewToXYFlowData({ nodes, edges }, opts)
38
-
39
- xyflowRef.current.setNodes(prev =>
40
- updates.nodes.map(update => {
41
- const existing = prev.find(isSameNode(update))
42
- if (existing) {
43
- if (eq(existing.data.element, update.data.element)) {
44
- return existing
45
- }
46
- return {
47
- ...existing,
48
- ...update
49
- }
50
- }
51
- return update
52
- })
53
- )
17
+ if (xyflowSynced) {
18
+ return
19
+ }
20
+ const {
21
+ view,
22
+ nodesDraggable,
23
+ nodesSelectable,
24
+ focusedNodeId
25
+ } = diagramStoreApi.getState()
26
+ const updates = diagramViewToXYFlowData(view, {
27
+ draggable: nodesDraggable,
28
+ selectable: nodesSelectable || focusedNodeId !== null
29
+ })
54
30
 
55
- xyflowRef.current.setEdges(prev =>
56
- updates.edges.map(update => {
57
- const existing = prev.find(e => e.id === update.id)
58
- if (existing) {
59
- return eq(existing.data, update.data) ? existing : {
60
- ...existing,
61
- ...update
62
- }
63
- }
64
- return update
65
- })
66
- )
31
+ const {
32
+ nodeLookup,
33
+ edgeLookup,
34
+ setNodes,
35
+ setEdges
36
+ } = xyflowApi.getState()
67
37
 
68
- if (lastOnNavigate?.toView === viewId && lastOnNavigate.positionCorrected !== true) {
69
- const elFrom = lastOnNavigate.element
70
- const elTo = nodes.find(n => n.id === elFrom.id)
71
- if (elTo) {
72
- const centerFrom = lastOnNavigate.elementCenterScreenPosition
73
- let nextZoom = Math.min(elFrom.width / elTo.width, elFrom.height / elTo.height)
74
-
75
- // TODO: investigate why 2 animation frames are needed
76
- requestAnimationFrame(() => {
77
- requestAnimationFrame(() => {
78
- const v = xyflowRef.current.getViewport()
79
- if (nextZoom < v.zoom) {
80
- nextZoom = Math.max(nextZoom, v.zoom * 0.5)
81
- xyflowRef.current.setViewport({
82
- x: v.x,
83
- y: v.y,
84
- zoom: nextZoom
85
- })
86
- }
38
+ setNodes(updates.nodes.map(update => {
39
+ const existing = nodeLookup.get(update.id)?.internals.userNode
40
+ if (existing && existing.type === update.type && eq(existing.parentId, update.parentId)) {
41
+ if (eq(existing.data.element, update.data.element)) {
42
+ return existing
43
+ }
44
+ return {
45
+ ...existing,
46
+ ...update
47
+ }
48
+ }
49
+ return update
50
+ }))
87
51
 
88
- const centerTo = xyflowRef.current.flowToScreenPosition({
89
- x: elTo.position[0] + elTo.width / 2,
90
- y: elTo.position[1] + elTo.height / 2
91
- }),
92
- diff = {
93
- x: toDomPrecision(centerFrom.x - centerTo.x),
94
- y: toDomPrecision(centerFrom.y - centerTo.y)
95
- }
96
- xyflowApi.getState().panBy(diff)
97
- })
98
- })
99
- }
100
- diagramStoreApi.setState(
101
- {
102
- lastOnNavigate: {
103
- ...lastOnNavigate,
104
- positionCorrected: true
105
- }
106
- },
107
- false,
108
- 'positionCorrected'
109
- )
52
+ setEdges(updates.edges.map(update => {
53
+ const existing = edgeLookup.get(update.id)
54
+ if (existing) {
55
+ return eq(existing.data.edge, update.data.edge) ? existing : {
56
+ ...existing,
57
+ ...update
110
58
  }
111
- },
112
- {
113
- equalityFn: shallowEqual
114
59
  }
60
+ return update
61
+ }))
62
+
63
+ diagramStoreApi.setState(
64
+ {
65
+ xyflowSynced: true
66
+ },
67
+ false,
68
+ 'xyflowSynced'
115
69
  )
116
- }, [xyflowApi, diagramStoreApi])
70
+ }, [xyflowSynced, xyflowApi, diagramStoreApi])
117
71
 
118
72
  return null
119
- })
73
+ }
@@ -1,12 +1,15 @@
1
1
  import { useMantineColorScheme } from '@mantine/core'
2
- import { Controls, ReactFlow } from '@xyflow/react'
2
+ import { Controls, ReactFlow, useOnViewportChange } from '@xyflow/react'
3
+ import { deepEqual as eq, shallowEqual } from 'fast-equals'
3
4
  import { type CSSProperties, memo, type PropsWithChildren } from 'react'
5
+ import { omit, omitBy } from 'remeda'
4
6
  import type { SetNonNullable, Simplify } from 'type-fest'
5
7
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
6
8
  import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
7
9
  import { MinZoom } from './const'
8
10
  import { RelationshipEdge } from './edges/RelationshipEdge'
9
11
  import { useLayoutConstraints } from './hooks/useLayoutConstraints'
12
+ import { useXYStoreApi } from './hooks/useXYFlow'
10
13
  import { CompoundNode } from './nodes/compound'
11
14
  import { ElementNode } from './nodes/element'
12
15
  import { XYFlowEdge, XYFlowNode } from './types'
@@ -21,27 +24,25 @@ const edgeTypes = {
21
24
  relationship: RelationshipEdge
22
25
  }
23
26
 
24
- type OnlyExpectedProps = Required<
25
- Pick<
26
- LikeC4DiagramProperties,
27
- | 'className'
28
- | 'controls'
29
- | 'background'
30
- >
31
- >
27
+ type XYFlowWrapperProps = PropsWithChildren<{
28
+ className?: string | undefined
29
+ defaultNodes: XYFlowNode[]
30
+ defaultEdges: XYFlowEdge[]
31
+ style?: CSSProperties | undefined
32
+ }>
32
33
 
33
- type XYFlowWrapperProps = Simplify<
34
- PropsWithChildren<
35
- SetNonNullable<OnlyExpectedProps> & {
36
- defaultNodes: XYFlowNode[]
37
- defaultEdges: XYFlowEdge[]
38
- style?: CSSProperties | undefined
39
- }
40
- >
41
- >
34
+ // const propsAreEqual = (
35
+ // prev: XYFlowWrapperProps,
36
+ // next: XYFlowWrapperProps
37
+ // ) => {
38
+ // return shallowEqual(
39
+ // omit(prev, ['children']),
40
+ // omit(next, ['children'])
41
+ // )
42
+ // }
42
43
 
43
44
  const selector = (s: DiagramState) => ({
44
- nodesSelectable: s.nodesSelectable,
45
+ nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
45
46
  nodesDraggable: s.nodesDraggable,
46
47
  fitView: s.fitViewEnabled,
47
48
  fitViewPadding: s.fitViewPadding,
@@ -55,15 +56,14 @@ const selector = (s: DiagramState) => ({
55
56
  pannable: s.pannable
56
57
  })
57
58
 
58
- function XYFlowWrapper({
59
+ export function XYFlow({
59
60
  className,
60
61
  children,
61
62
  defaultNodes,
62
63
  defaultEdges,
63
- controls,
64
- background,
65
64
  style
66
65
  }: XYFlowWrapperProps) {
66
+ const xyflowApi = useXYStoreApi()
67
67
  const diagramApi = useDiagramStoreApi()
68
68
  const {
69
69
  nodesSelectable,
@@ -72,17 +72,39 @@ function XYFlowWrapper({
72
72
  fitViewPadding,
73
73
  pannable,
74
74
  zoomable,
75
- ...editor
75
+ hasOnNodeClick,
76
+ hasOnNavigateTo,
77
+ hasOnNodeContextMenu,
78
+ hasOnCanvasContextMenu,
79
+ hasOnEdgeContextMenu,
80
+ hasOnEdgeClick
76
81
  } = useDiagramState(selector)
77
82
 
78
83
  const layoutConstraints = useLayoutConstraints()
79
84
 
80
85
  const handlers = useXYFlowEvents()
81
- const isBgWithPattern = background !== 'transparent' && background !== 'solid'
82
86
 
83
87
  const { colorScheme } = useMantineColorScheme()
84
88
  const colorMode = colorScheme !== 'auto' ? colorScheme : undefined
85
89
 
90
+ /**
91
+ * WORKAROUND - Called on viewport change
92
+ * Viewport transform is not rounded to integers which results in blurry nodes on some resolution
93
+ * https://github.com/xyflow/xyflow/issues/3282
94
+ * https://github.com/likec4/likec4/issues/734
95
+ */
96
+ useOnViewportChange({
97
+ onEnd: ({ x, y, zoom }) => {
98
+ const rounded = {
99
+ x: Math.round(x),
100
+ y: Math.round(y)
101
+ }
102
+ if (x !== rounded.x || y !== rounded.y) {
103
+ xyflowApi.setState({ transform: [rounded.x, rounded.y, zoom] })
104
+ }
105
+ }
106
+ })
107
+
86
108
  return (
87
109
  <ReactFlow<XYFlowNode, XYFlowEdge>
88
110
  className={className}
@@ -113,8 +135,8 @@ function XYFlowWrapper({
113
135
  panOnScroll={pannable}
114
136
  panOnDrag={pannable}
115
137
  elementsSelectable={nodesSelectable}
116
- nodesFocusable={nodesDraggable || nodesSelectable || editor.hasOnNodeClick || editor.hasOnNavigateTo}
117
- edgesFocusable={editor.hasOnEdgeClick}
138
+ nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
139
+ edgesFocusable={hasOnEdgeClick}
118
140
  {...(!nodesSelectable && {
119
141
  selectionKeyCode: null
120
142
  })}
@@ -140,20 +162,18 @@ function XYFlowWrapper({
140
162
  onInit={() => {
141
163
  diagramApi.setState({ initialized: true }, false, 'initialized')
142
164
  }}
143
- {...(editor.hasOnNodeContextMenu && {
165
+ {...(hasOnNodeContextMenu && {
144
166
  onNodeContextMenu: handlers.onNodeContextMenu
145
167
  })}
146
- {...(editor.hasOnEdgeContextMenu && {
168
+ {...(hasOnEdgeContextMenu && {
147
169
  onEdgeContextMenu: handlers.onEdgeContextMenu
148
170
  })}
149
- {...(editor.hasOnCanvasContextMenu && {
171
+ {...(hasOnCanvasContextMenu && {
150
172
  onPaneContextMenu: handlers.onPaneContextMenu
151
173
  })}>
152
- {isBgWithPattern && <XYFlowBackground background={background} />}
153
- {controls && <Controls />}
154
174
  {children}
155
175
  </ReactFlow>
156
176
  )
157
177
  }
158
178
 
159
- export const XYFlow = memo(XYFlowWrapper) as typeof XYFlowWrapper
179
+ // export const XYFlow = memo(XYFlowWrapper, propsAreEqual) as typeof XYFlowWrapper
@@ -1,16 +1,18 @@
1
1
  import { nonexhaustive } from '@likec4/core'
2
2
  import { Background, type BackgroundProps, BackgroundVariant } from '@xyflow/react'
3
- import { deepEqual } from 'fast-equals'
4
- import { memo } from 'react'
5
3
  import type { OverrideProperties } from 'type-fest'
6
4
 
7
5
  export type XYBackgroundVariant = 'dots' | 'lines' | 'cross'
8
- export type XYBackgroundProps =
6
+ export type XYBackground =
9
7
  | XYBackgroundVariant
10
8
  | OverrideProperties<BackgroundProps, {
11
9
  variant: XYBackgroundVariant
12
10
  }>
13
11
 
12
+ export type XYBackgroundProps = {
13
+ background: XYBackground
14
+ }
15
+
14
16
  function literalToEnum(value: XYBackgroundVariant): BackgroundVariant {
15
17
  switch (value) {
16
18
  case 'dots':
@@ -24,10 +26,10 @@ function literalToEnum(value: XYBackgroundVariant): BackgroundVariant {
24
26
  }
25
27
  }
26
28
 
27
- export const XYFlowBackground = memo<{ background: XYBackgroundProps }>(({ background }) => {
29
+ export function XYFlowBackground({ background }: XYBackgroundProps) {
28
30
  if (typeof background === 'string') {
29
31
  return <Background variant={literalToEnum(background)} />
30
32
  }
31
33
  const { variant, ...rest } = background
32
34
  return <Background variant={literalToEnum(variant)} {...rest} />
33
- }, deepEqual)
35
+ }