@likec4/diagram 1.13.0 → 1.15.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 (43) hide show
  1. package/package.json +9 -9
  2. package/src/LikeC4CustomColors.tsx +6 -3
  3. package/src/LikeC4Diagram.css.ts +4 -0
  4. package/src/LikeC4Diagram.tsx +9 -7
  5. package/src/overlays/Overlays.css.ts +124 -0
  6. package/src/overlays/Overlays.tsx +210 -0
  7. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +247 -0
  8. package/src/overlays/edge-details/SelectEdge.css.ts +99 -0
  9. package/src/overlays/edge-details/SelectEdge.tsx +153 -0
  10. package/src/overlays/edge-details/_types.ts +38 -0
  11. package/src/overlays/edge-details/use-layouted-edge-details.ts +356 -0
  12. package/src/overlays/edge-details/xyflow/CompoundNode.tsx +80 -0
  13. package/src/overlays/edge-details/xyflow/ElementNode.tsx +154 -0
  14. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +81 -0
  15. package/src/overlays/edge-details/xyflow/styles.css.ts +196 -0
  16. package/src/overlays/index.tsx +1 -33
  17. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +124 -63
  18. package/src/overlays/relationships-of/_types.ts +13 -2
  19. package/src/overlays/relationships-of/use-layouted-relationships.ts +37 -37
  20. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +2 -2
  21. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +47 -26
  22. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +58 -22
  23. package/src/overlays/relationships-of/xyflow/styles.css.ts +53 -16
  24. package/src/state/DiagramContext.tsx +23 -5
  25. package/src/state/diagram-to-xyflow.ts +31 -20
  26. package/src/state/diagramStore.ts +30 -23
  27. package/src/xyflow/XYFlow.tsx +2 -1
  28. package/src/xyflow/XYFlowEvents.tsx +13 -1
  29. package/src/xyflow/edges/RelationshipEdge.tsx +10 -4
  30. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +24 -21
  31. package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -0
  32. package/src/xyflow/nodes/compound/CompoundNode.tsx +11 -11
  33. package/src/xyflow/nodes/shared/Toolbar.tsx +1 -1
  34. package/src/xyflow/types.ts +1 -0
  35. package/src/xyflow/useLayoutConstraints.ts +38 -19
  36. package/src/components/Overlay.css.ts +0 -78
  37. package/src/components/Overlay.tsx +0 -102
  38. package/src/components/index.ts +0 -2
  39. package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +0 -13
  40. package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +0 -37
  41. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +0 -33
  42. package/src/overlays/relationships-of/styles.css.ts +0 -34
  43. /package/src/{components → overlays}/OverlayContext.tsx +0 -0
@@ -0,0 +1,196 @@
1
+ import { globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine, vars, whereLight, xyvars } from '../../../theme-vars'
3
+
4
+ export const elementNode = style({
5
+ width: '100%',
6
+ height: '100%'
7
+ })
8
+
9
+ export const elementNodeContent = style({
10
+ width: '100%',
11
+ height: '100%',
12
+ display: 'flex',
13
+ flexDirection: 'column',
14
+ justifyContent: 'center',
15
+ alignItems: 'center',
16
+ overflow: 'hidden',
17
+ gap: 6,
18
+ padding: 16
19
+ })
20
+
21
+ export const elementNodeTitle = style({
22
+ flex: '0 0 auto',
23
+ width: 'fit-content',
24
+ textAlign: 'center',
25
+ fontFamily: vars.element.font,
26
+ fontOpticalSizing: 'auto',
27
+ fontStyle: 'normal',
28
+ fontWeight: 500,
29
+ fontSize: 17,
30
+ lineHeight: 1.25,
31
+ textWrap: 'balance',
32
+ color: vars.element.hiContrast
33
+ })
34
+
35
+ export const elementNodeDescription = style({
36
+ flex: '0 0 auto',
37
+ width: 'fit-content',
38
+ textAlign: 'center',
39
+ fontFamily: vars.element.font,
40
+ fontOpticalSizing: 'auto',
41
+ fontStyle: 'normal',
42
+ fontSize: mantine.fontSizes.xs,
43
+ lineHeight: 1.25,
44
+ textWrap: 'balance',
45
+ color: vars.element.loContrast
46
+ })
47
+
48
+ export const compoundNodeBody = style({
49
+ width: '100%',
50
+ height: '100%',
51
+ boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
52
+ background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
53
+ borderRadius: 4
54
+ })
55
+
56
+ export const compoundNodeTitle = style({
57
+ fontFamily: vars.compound.font,
58
+ fontOpticalSizing: 'auto',
59
+ fontStyle: 'normal',
60
+ fontWeight: 600,
61
+ fontSize: 14,
62
+ lineHeight: 1,
63
+ textTransform: 'uppercase',
64
+ paddingTop: 12,
65
+ paddingLeft: 10,
66
+ mixBlendMode: 'screen',
67
+ color: vars.compound.titleColor
68
+ })
69
+
70
+ export const cssShapeSvg = style({
71
+ top: 0,
72
+ left: 0,
73
+ position: 'absolute',
74
+ pointerEvents: 'none',
75
+ fill: vars.element.fill,
76
+ stroke: vars.element.stroke,
77
+ overflow: 'visible',
78
+ filter: `
79
+ drop-shadow(0 2px 1px rgba(0, 0, 0, 0.21))
80
+ drop-shadow(0 1px 1px color-mix(in srgb, ${vars.element.stroke} 40%, transparent))
81
+ drop-shadow(0 5px 3px rgba(0, 0, 0, 0.1))
82
+ `,
83
+ zIndex: -1
84
+ })
85
+
86
+ const DimmedTransitionDelay = '400ms'
87
+
88
+ export const edgePath = style({
89
+ opacity: 1,
90
+ transition: 'all 0.2s ease-in-out',
91
+
92
+ selectors: {
93
+ [`&[data-edge-dimmed='true']`]: {
94
+ opacity: 0.1,
95
+ transitionDelay: DimmedTransitionDelay
96
+ }
97
+ }
98
+ })
99
+
100
+ export const edgeLabel = style({
101
+ padding: '2px 5px',
102
+ fontFamily: vars.likec4.font,
103
+ position: 'absolute',
104
+ cursor: 'pointer',
105
+ width: 'fit-content',
106
+ color: xyvars.edge.labelColor,
107
+ backgroundColor: xyvars.edge.labelBgColor,
108
+ borderRadius: 4,
109
+ // everything inside EdgeLabelRenderer has no pointer events by default
110
+ // if you have an interactive element, set pointer-events: all
111
+ pointerEvents: 'all',
112
+ textWrap: 'pretty',
113
+ whiteSpace: 'preserve-breaks',
114
+ opacity: 1,
115
+ transition: 'all 0.2s ease-in-out',
116
+ selectors: {
117
+ [`&[data-edge-dimmed='true']`]: {
118
+ opacity: 0.1,
119
+ transitionDelay: DimmedTransitionDelay
120
+ }
121
+ }
122
+ })
123
+
124
+ export const edgeLabelText = style({
125
+ textAlign: 'center',
126
+ whiteSpaceCollapse: 'preserve-breaks',
127
+ fontSize: mantine.fontSizes.md,
128
+ lineHeight: mantine.lineHeights.xs
129
+ })
130
+
131
+ export const edgeLabelTechnology = style({
132
+ textAlign: 'center',
133
+ whiteSpaceCollapse: 'preserve-breaks',
134
+ fontSize: mantine.fontSizes.sm,
135
+ lineHeight: 1
136
+ })
137
+
138
+ export const navigateBtnBox = style({
139
+ zIndex: 100,
140
+ position: 'absolute',
141
+ left: '50%',
142
+ bottom: 2,
143
+ transform: 'translate(-50%, 0%)',
144
+ gap: 0,
145
+ transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
146
+ selectors: {
147
+ [`:where([data-likec4-shape='browser'],[data-likec4-shape='mobile']) &`]: {
148
+ bottom: 6
149
+ }
150
+ }
151
+ })
152
+ globalStyle(`:where(${elementNode}:hover) ${navigateBtnBox}`, {
153
+ transitionDelay: '20ms',
154
+ gap: 8,
155
+ transform: 'translate(-50%, 5px)'
156
+ })
157
+
158
+ export const navigateBtn = style({
159
+ opacity: 0.7,
160
+ pointerEvents: 'all',
161
+ cursor: 'pointer',
162
+ transform: 'scale(0.9)',
163
+ transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
164
+ 'vars': {
165
+ '--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`,
166
+ '--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill} 65%, ${vars.element.stroke})`,
167
+ '--ai-hover': `color-mix(in srgb , ${vars.element.fill} 50%, ${vars.element.stroke})`
168
+ },
169
+ ':hover': {
170
+ transitionDelay: '0ms',
171
+ transform: 'scale(1.25)',
172
+ boxShadow: mantine.shadows.lg
173
+ },
174
+ ':active': {
175
+ transform: 'scale(0.975)'
176
+ }
177
+ })
178
+
179
+ globalStyle(`:where(${elementNode}:hover) ${navigateBtn}`, {
180
+ transitionDelay: '40ms',
181
+ transitionTimingFunction: 'cubic-bezier(0, 0, 0.40, 1)',
182
+ opacity: 1,
183
+ transform: 'scale(1.07)',
184
+ boxShadow: mantine.shadows.md,
185
+ vars: {
186
+ '--ai-bg': `var(--ai-bg-hover)`
187
+ }
188
+ })
189
+ // globalStyle(`${elementNode}:hover ${navigateBtn}`, {
190
+ // opacity: 1,
191
+ // transform: 'scale(1.05)',
192
+ // boxShadow: mantine.shadows.md,
193
+ // vars: {
194
+ // '--ai-bg': `var(--ai-bg-hover)`
195
+ // }
196
+ // })
@@ -1,33 +1 @@
1
- import { memo } from 'react'
2
- import { useDiagramState } from '../hooks/useDiagramState'
3
- import { EdgeDetailsOverlay } from './edge-details/EdgeDetailsOverlay'
4
- import { RelationshipsOfOverlay } from './relationships-of/RelationshipsOverlay'
5
-
6
- export const Overlays = memo(() => {
7
- const {
8
- hasLikeC4Model,
9
- activeOverlay
10
- } = useDiagramState(s => ({
11
- hasLikeC4Model: s.hasLikeC4Model,
12
- activeOverlay: s.activeOverlay
13
- }))
14
-
15
- if (!activeOverlay) {
16
- return null
17
- }
18
-
19
- if (!hasLikeC4Model) {
20
- console.warn('LikeC4Model is not available, but overlays are active')
21
- return null
22
- }
23
-
24
- if (activeOverlay.relationshipsOf) {
25
- return <RelationshipsOfOverlay />
26
- }
27
-
28
- if (activeOverlay.edgeDetails) {
29
- return <EdgeDetailsOverlay edgeId={activeOverlay.edgeDetails} />
30
- }
31
-
32
- return null
33
- })
1
+ export { Overlays } from './Overlays'
@@ -1,16 +1,23 @@
1
- import { isAncestor, type Relation } from '@likec4/core'
1
+ import { type Fqn, isAncestor, type Relation } from '@likec4/core'
2
2
  import { Box, Button, Group, SegmentedControl, Space, Text } from '@mantine/core'
3
3
  import { useLocalStorage, useStateHistory } from '@mantine/hooks'
4
- import { useDebouncedEffect } from '@react-hookz/web'
4
+ import { useDebouncedCallback, useSyncedRef } from '@react-hookz/web'
5
5
  import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'
6
- import { Panel, ReactFlow, type ReactFlowInstance, useReactFlow, useStoreApi } from '@xyflow/react'
6
+ import {
7
+ getViewportForBounds,
8
+ Panel,
9
+ ReactFlow,
10
+ type ReactFlowInstance,
11
+ useReactFlow,
12
+ useStoreApi
13
+ } from '@xyflow/react'
14
+ import { getNodeDimensions } from '@xyflow/system'
7
15
  import { memo, useEffect, useRef } from 'react'
8
16
  import { map, only, prop, unique } from 'remeda'
9
- import { useOverlayDialog } from '../../components'
10
17
  import { useDiagramStoreApi } from '../../hooks/useDiagramState'
18
+ import { useOverlayDialog } from '../OverlayContext'
11
19
  import type { XYFlowTypes } from './_types'
12
20
  import { SelectElement } from './SelectElement'
13
- import * as css from './styles.css'
14
21
  import { useLayoutedRelationships, ZIndexes } from './use-layouted-relationships'
15
22
  import { CompoundNode } from './xyflow/CompoundNode'
16
23
  import { ElementNode } from './xyflow/ElementNode'
@@ -26,6 +33,9 @@ const edgeTypes = {
26
33
  relation: RelationshipEdge
27
34
  }
28
35
 
36
+ const findSubjectNode = (nodes: XYFlowTypes.Node[]) =>
37
+ nodes.find((n): n is XYFlowTypes.ElementNode => n.type === 'element' && n.data.column === 'subject')
38
+
29
39
  const resetDimmedAndHovered = (xyflow: ReactFlowInstance<XYFlowTypes.Node, XYFlowTypes.Edge>) => {
30
40
  xyflow.setEdges(edges =>
31
41
  edges.map(edge => ({
@@ -70,7 +80,7 @@ const onlyOneUnique = <T extends keyof Relation>(
70
80
  return only(unique(map(data.relations, prop(property))))
71
81
  }
72
82
 
73
- export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
83
+ export const RelationshipsXYFlow = memo<{ subjectId: Fqn }>(function RelationshipsXYFlow({ subjectId }) {
74
84
  const diagramStore = useDiagramStoreApi()
75
85
  const lastClickedNodeRef = useRef<XYFlowTypes.NonEmptyNode | null>(null)
76
86
  const [_scope, setScope] = useLocalStorage<'global' | 'view'>({
@@ -83,15 +93,16 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
83
93
  viewIncludesSubject,
84
94
  edges,
85
95
  nodes,
86
- subject
87
- } = useLayoutedRelationships(_scope)
96
+ subject,
97
+ bounds
98
+ } = useLayoutedRelationships(subjectId, _scope)
88
99
  const scope = viewIncludesSubject ? _scope : 'global'
89
100
  const showSubjectWarning = !viewIncludesSubject && _scope === 'view'
90
101
 
91
102
  const [historySubjectId, historyOps, { history, current }] = useStateHistory(subject.id)
92
103
 
93
104
  const xyflow = useReactFlow<XYFlowTypes.Node, XYFlowTypes.Edge>()
94
- const xystore = useStoreApi()
105
+ const xystore = useStoreApi<XYFlowTypes.Node, XYFlowTypes.Edge>()
95
106
  const overlay = useOverlayDialog()
96
107
 
97
108
  useEffect(() => {
@@ -108,66 +119,119 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
108
119
  }
109
120
  }, [historySubjectId])
110
121
 
122
+ const fitview = useDebouncedCallback(
123
+ () => {
124
+ xyflow.fitView({
125
+ padding: 0.2,
126
+ includeHiddenNodes: true,
127
+ maxZoom: 1,
128
+ duration: 500
129
+ })
130
+ },
131
+ [xyflow],
132
+ 150
133
+ )
134
+
111
135
  useEffect(() => {
112
- const nextSubjectNode = nodes.find((n): n is XYFlowTypes.NonEmptyNode =>
113
- n.type !== 'empty' && n.data.column === 'subject'
114
- )
136
+ const {
137
+ nodes: _nodes,
138
+ edges: _edges,
139
+ setNodes,
140
+ setEdges,
141
+ width,
142
+ height
143
+ } = xystore.getState()
144
+
145
+ const nextSubjectNode = findSubjectNode(nodes)
146
+ const currentSubjectNode = findSubjectNode(_nodes)
147
+
148
+ // If subject node is the same, don't animate
149
+ if (nextSubjectNode?.data.fqn === currentSubjectNode?.data.fqn) {
150
+ setNodes(nodes)
151
+ setEdges(edges)
152
+ return
153
+ }
154
+
115
155
  if (!nextSubjectNode) {
116
156
  console.error('Subject node not found')
117
157
  } else if (nextSubjectNode.data.fqn !== subject.id) {
118
158
  console.error(`Subject node mismatch, expected: ${subject.id} got: ${nextSubjectNode.data.fqn}`)
119
159
  }
160
+
161
+ const nextzoom = getViewportForBounds(bounds, width, height, 0.2, 1, 0.2).zoom
162
+
163
+ const nextSubjectCenter = nextSubjectNode && {
164
+ x: nextSubjectNode.position.x + (nextSubjectNode.width ?? 0) / 2,
165
+ y: nextSubjectNode.position.y + (nextSubjectNode.height ?? 0) / 2
166
+ }
167
+
120
168
  const existingNode = lastClickedNodeRef.current
121
169
  ?? xyflow.getNodes().find(n => n.type !== 'empty' && n.data.column !== 'subject' && n.data.fqn === subject.id)
122
170
  lastClickedNodeRef.current = null
123
- // Correct position if we have subject in existing nodes
124
- if (nextSubjectNode && existingNode && existingNode.data.column !== 'subject') {
125
- const xynodeFrom = xyflow.getInternalNode(existingNode.id)!
126
- const fromPos = xyflow.flowToScreenPosition({
127
- x: xynodeFrom.internals.positionAbsolute.x,
128
- y: xynodeFrom.internals.positionAbsolute.y
129
- }),
130
- toPos = xyflow.flowToScreenPosition({
131
- x: nextSubjectNode.position.x,
132
- y: nextSubjectNode.position.y
133
- }),
134
- diff = {
135
- x: fromPos.x - toPos.x,
136
- y: fromPos.y - toPos.y
171
+ // Animate from existing node to next subject node
172
+ if (nextSubjectCenter && existingNode) {
173
+ // Dim all nodes except the existing node
174
+ setNodes(_nodes.map(n => {
175
+ return {
176
+ ...n,
177
+ data: {
178
+ ...n.data,
179
+ dimmed: n.id !== existingNode.id ? 'immediate' : false
180
+ }
181
+ } as XYFlowTypes.Node
182
+ }))
183
+ setEdges(_edges.map(e => ({
184
+ ...e,
185
+ data: {
186
+ ...e.data,
187
+ dimmed: true
137
188
  }
138
- xystore.getState().panBy(diff)
139
- xystore.getState().setNodes(nodes)
140
- xystore.getState().setEdges(edges)
141
- return
142
- }
143
- xyflow.setNodes(nodes)
144
- xyflow.setEdges(edges)
145
- }, [nodes, edges])
189
+ })))
146
190
 
147
- const zoomable = true
191
+ // Move to center of existing node
192
+ const existingInternalNode = xyflow.getInternalNode(existingNode.id)!
193
+ const existingDimensions = getNodeDimensions(existingInternalNode)
194
+ const existingCenter = {
195
+ x: existingInternalNode.internals.positionAbsolute.x + existingDimensions.width / 2,
196
+ y: existingInternalNode.internals.positionAbsolute.y + existingDimensions.height / 2
197
+ }
148
198
 
149
- useDebouncedEffect(
150
- () => {
151
- xyflow.fitView({
152
- padding: 0.2,
153
- includeHiddenNodes: true,
154
- maxZoom: 1,
155
- duration: 450
199
+ // Pick the smaller zoom level
200
+ const zoom = Math.min(
201
+ xyflow.getViewport().zoom,
202
+ nextzoom,
203
+ nextSubjectNode.width! / existingDimensions.width,
204
+ nextSubjectNode.height! / existingDimensions.height
205
+ )
206
+
207
+ let isCancelled = false
208
+
209
+ xyflow.setCenter(existingCenter.x, existingCenter.y, { zoom, duration: 350 }).then(() => {
210
+ // If the component is unmounted, we don't want to update the state
211
+ if (isCancelled) return
212
+ setNodes(nodes)
213
+ setEdges(edges)
214
+ xyflow.setCenter(nextSubjectCenter.x, nextSubjectCenter.y, { zoom: nextzoom })
215
+ fitview()
156
216
  })
157
- },
158
- [subject.id, scope],
159
- 200
160
- )
217
+ return () => {
218
+ isCancelled = true
219
+ }
220
+ }
221
+ setNodes(nodes)
222
+ setEdges(edges)
223
+ fitview()
224
+ return undefined
225
+ }, [nodes, edges])
161
226
 
162
227
  return (
163
228
  <ReactFlow
164
- className={css.root}
165
229
  defaultEdges={[] as XYFlowTypes.Edge[]}
166
230
  defaultNodes={[] as XYFlowTypes.Node[]}
167
231
  nodeTypes={nodeTypes}
168
232
  edgeTypes={edgeTypes}
169
233
  defaultMarkerColor="var(--xy-edge-stroke)"
170
- zoomOnPinch={zoomable}
234
+ zoomOnPinch={true}
171
235
  zoomOnScroll={false}
172
236
  zoomOnDoubleClick={false}
173
237
  maxZoom={1.5}
@@ -224,30 +288,27 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
224
288
  onNodeClick={(e, node) => {
225
289
  e.stopPropagation()
226
290
  if (node.type === 'empty') return
227
- if (subject.id !== node.data.fqn) {
228
- lastClickedNodeRef.current = node
229
- }
291
+ lastClickedNodeRef.current = node
230
292
  overlay.openOverlay({
231
293
  relationshipsOf: node.data.fqn
232
294
  })
233
295
  }}
296
+ onNodeDoubleClick={(e) => {
297
+ e.stopPropagation()
298
+ }}
234
299
  onDoubleClick={e => {
235
300
  e.stopPropagation()
236
- xyflow.fitView({
237
- includeHiddenNodes: true,
238
- maxZoom: 1,
239
- duration: 450
240
- })
301
+ fitview()
241
302
  }}
242
303
  onEdgeClick={(e, edge) => {
243
304
  e.stopPropagation()
244
- if (edge.data.relations.length <= 1) {
245
- const relationId = only(edge.data.relations)?.id
246
- if (relationId) {
247
- diagramStore.getState().onOpenSourceRelation?.(relationId)
248
- }
305
+ // One relation in edge
306
+ const relationId = only(edge.data.relations)?.id
307
+ if (relationId) {
308
+ diagramStore.getState().onOpenSourceRelation?.(relationId)
249
309
  return
250
310
  }
311
+
251
312
  const nodeId = onlyOneUnique(edge.data, 'source') ? edge.source : edge.target
252
313
  const next = xyflow.getNode(nodeId)
253
314
  if (next && next.type !== 'empty') {
@@ -285,7 +346,7 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
285
346
  </Button>
286
347
  <Space w={2} />
287
348
  <Group gap={'xs'} pos={'relative'} wrap="nowrap" flex={'1 0 auto'}>
288
- <Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap' }}>Relationships of</Box>
349
+ <Box fz={'sm'} fw={'400'} style={{ whiteSpace: 'nowrap', userSelect: 'none' }}>Relationships of</Box>
289
350
  <Box flex={'1 0 auto'}>
290
351
  <SelectElement
291
352
  scope={scope}
@@ -324,7 +385,7 @@ export const RelationshipsXYFlow = memo(function RelationshipsXYFlow() {
324
385
  }}
325
386
  >
326
387
  <Text fw={500} size="xs" c="orange" component="div">
327
- Current view doesn't include selected element, switched to Global
388
+ Current view doesn't include this element, switched to Global
328
389
  </Text>
329
390
  </Box>
330
391
  )}
@@ -1,5 +1,10 @@
1
1
  import type { ComputedNode, Fqn, Relation, ViewID } from '@likec4/core'
2
- import type { Edge as ReactFlowEdge, Node as ReactFlowNode, ReactFlowInstance } from '@xyflow/react'
2
+ import type {
3
+ Edge as ReactFlowEdge,
4
+ InternalNode as ReactFlowInternalNode,
5
+ Node as ReactFlowNode,
6
+ ReactFlowInstance
7
+ } from '@xyflow/react'
3
8
  import type { SetRequired } from 'type-fest'
4
9
 
5
10
  export namespace XYFlowTypes {
@@ -23,7 +28,11 @@ export namespace XYFlowTypes {
23
28
  }
24
29
  navigateTo: ViewID | null
25
30
  hovered?: boolean
26
- dimmed?: boolean
31
+ /**
32
+ * Whether the node is dimmed
33
+ * 'immediate' means that the node is dimmed without delay
34
+ */
35
+ dimmed?: 'immediate' | boolean
27
36
  }
28
37
 
29
38
  export type ElementNode = SetRequired<ReactFlowNode<NodeProps, 'element'>, 'type'>
@@ -53,4 +62,6 @@ export namespace XYFlowTypes {
53
62
  }
54
63
 
55
64
  export type Instance = ReactFlowInstance<Node, Edge>
65
+
66
+ export type InternalNode = ReactFlowInternalNode<Node>
56
67
  }