@likec4/diagram 1.17.1 → 1.18.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 (49) hide show
  1. package/package.json +17 -15
  2. package/src/LikeC4Diagram.props.ts +21 -12
  3. package/src/LikeC4Diagram.tsx +5 -7
  4. package/src/LikeC4Search.tsx +54 -51
  5. package/src/hooks/useDiagramState.ts +8 -3
  6. package/src/hooks/useXYFlow.ts +2 -1
  7. package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
  8. package/src/likec4model/useLikeC4Model.ts +21 -11
  9. package/src/overlays/Overlays.tsx +96 -63
  10. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
  11. package/src/overlays/edge-details/_types.ts +7 -3
  12. package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
  13. package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
  14. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
  15. package/src/overlays/element-details/ElementDetailsCard.tsx +91 -78
  16. package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
  17. package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
  18. package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
  19. package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
  20. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
  21. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
  22. package/src/overlays/relationships-of/SelectElement.tsx +2 -2
  23. package/src/overlays/relationships-of/_types.ts +2 -2
  24. package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
  25. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
  26. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
  27. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
  28. package/src/state/diagramStore.ts +28 -11
  29. package/src/ui/DiagramTitlePanel.tsx +2 -29
  30. package/src/ui/Link.tsx +40 -0
  31. package/src/ui/notation/NotationPanel.tsx +4 -2
  32. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
  33. package/src/ui/top-left/TopLeftPanel.tsx +5 -5
  34. package/src/xyflow/edges/EdgeLabel.tsx +4 -4
  35. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
  36. package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
  37. package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
  38. package/src/xyflow/nodes/element/ElementNode.tsx +147 -124
  39. package/src/xyflow/nodes/element/element.css.ts +52 -19
  40. package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
  41. package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
  42. package/src/xyflow/useLayoutConstraints.ts +1 -1
  43. package/src/xyflow/useXYFlowEvents.ts +18 -5
  44. package/src/ui/OptionsPanel.css.ts +0 -9
  45. package/src/ui/OptionsPanel.tsx +0 -70
  46. package/src/ui/options/EdgeOptions.tsx +0 -142
  47. package/src/ui/options/NodeOptions.tsx +0 -362
  48. package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
  49. package/src/xyflow/nodes/element/ElementDetails.tsx +0 -393
@@ -1,12 +1,12 @@
1
- import { type ThemeColor } from '@likec4/core'
2
- import { ActionIcon, Box, Group, Text as MantineText, Tooltip } from '@mantine/core'
1
+ import { DiagramNode, type ThemeColor } from '@likec4/core'
2
+ import { ActionIcon, type ActionIconProps, Box, Text as MantineText, Tooltip } from '@mantine/core'
3
3
  import { useDebouncedValue } from '@mantine/hooks'
4
4
  import { IconId, IconTransform, IconZoomScan } from '@tabler/icons-react'
5
5
  import { Handle, type NodeProps, Position } from '@xyflow/react'
6
6
  import clsx from 'clsx'
7
7
  import { deepEqual as eq } from 'fast-equals'
8
- import { m, type Variants } from 'framer-motion'
9
- import React, { memo, useCallback, useState } from 'react'
8
+ import { type HTMLMotionProps, m, type Variants } from 'framer-motion'
9
+ import React, { memo, type PropsWithoutRef, useCallback, useState } from 'react'
10
10
  import { isNumber, isTruthy } from 'remeda'
11
11
  import { useDiagramState } from '../../../hooks/useDiagramState'
12
12
  import type { ElementXYFlowNode } from '../../types'
@@ -54,47 +54,60 @@ const VariantsRoot = {
54
54
  }
55
55
  } satisfies Variants
56
56
 
57
- const VariantsNavigateBtn = {
58
- idle: {
59
- '--ai-bg': 'var(--ai-bg-idle)',
60
- scale: 1,
61
- opacity: 0.5,
62
- originY: 0.5
63
- },
64
- selected: {},
65
- hovered: {
66
- '--ai-bg': 'var(--ai-bg-hover)',
67
- scale: 1.35,
68
- opacity: 1
69
- }
70
- } satisfies Variants
71
- VariantsNavigateBtn['selected'] = VariantsNavigateBtn['hovered']
72
-
73
- const VariantsNavigate = {
74
- ...VariantsNavigateBtn,
75
- 'hovered:navigate': {
76
- scale: 1.42
77
- },
78
- 'tap:navigate': {
79
- scale: 1.15
80
- }
81
- } satisfies Variants
82
-
83
- const VariantRealationships = {
84
- ...VariantsNavigateBtn,
85
- 'hovered:relationships': {
86
- scale: 1.42
87
- },
88
- 'tap:relationships': {
89
- scale: 1.15
90
- }
91
- } satisfies Variants
57
+ const variantsBottomButton = (target: 'navigate' | 'relationships', align: 'left' | 'right' | false) => {
58
+ const variants = {
59
+ idle: {
60
+ '--icon-scale': 'scale(1)',
61
+ '--ai-bg': 'var(--ai-bg-idle)',
62
+ scale: 1,
63
+ opacity: 0.5,
64
+ originX: 0.5,
65
+ originY: 0.35,
66
+ translateY: 0,
67
+ ...align === 'left' && {
68
+ originX: 0.75,
69
+ translateX: -1
70
+ },
71
+ ...align === 'right' && {
72
+ originX: 0.25,
73
+ translateX: 1
74
+ }
75
+ },
76
+ selected: {},
77
+ hovered: {
78
+ '--icon-scale': 'scale(1)',
79
+ '--ai-bg': 'var(--ai-bg-hover)',
80
+ translateY: 3,
81
+ scale: 1.32,
82
+ opacity: 1,
83
+ ...align === 'left' && {
84
+ translateX: -4
85
+ },
86
+ ...align === 'right' && {
87
+ translateX: 4
88
+ }
89
+ },
90
+ 'hovered:details': {},
91
+ [`hovered:${target}`]: {
92
+ '--icon-scale': 'scale(1.08)',
93
+ scale: 1.45
94
+ },
95
+ [`tap:${target}`]: {
96
+ scale: 1.15
97
+ }
98
+ } satisfies Variants
99
+ variants['selected'] = variants['hovered']
100
+ variants['hovered:details'] = variants.idle
101
+ return variants
102
+ }
92
103
 
93
104
  const VariantsDetailsBtn = {
94
105
  idle: {
95
106
  '--ai-bg': 'var(--ai-bg-idle)',
96
107
  scale: 1,
97
- opacity: 0.5
108
+ opacity: 0.5,
109
+ originX: 0.45,
110
+ originY: 0.55
98
111
  },
99
112
  selected: {},
100
113
  hovered: {
@@ -102,7 +115,7 @@ const VariantsDetailsBtn = {
102
115
  opacity: 0.7
103
116
  },
104
117
  'hovered:details': {
105
- scale: 1.42,
118
+ scale: 1.44,
106
119
  opacity: 1
107
120
  },
108
121
  'tap:details': {
@@ -131,6 +144,8 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
131
144
  width,
132
145
  height
133
146
  }) {
147
+ const modelRef = DiagramNode.modelRef(element)
148
+ // const deploymentRef = DiagramNode.deploymentRef(element)
134
149
  const {
135
150
  viewId,
136
151
  isEditable,
@@ -167,27 +182,29 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
167
182
  const w = toDomPrecision(width ?? element.width)
168
183
  const h = toDomPrecision(height ?? element.height)
169
184
 
170
- const [animateVariants, animateHandlers] = useFramerAnimateVariants()
171
-
172
- let animate: keyof typeof VariantsRoot
185
+ let animateVariant: keyof typeof VariantsRoot | string[]
173
186
  switch (true) {
174
187
  case isInActiveOverlay:
175
- animate = 'idle'
188
+ animateVariant = 'idle'
176
189
  break
177
190
  case dragging && selected:
178
- animate = 'selected'
191
+ animateVariant = 'selected'
179
192
  break
180
193
  case dragging:
181
- animate = 'idle'
194
+ animateVariant = 'idle'
182
195
  break
183
196
  case isInteractive && isHovered:
184
- animate = 'hovered'
197
+ animateVariant = 'hovered'
185
198
  break
186
199
  case selected:
187
- animate = 'selected'
200
+ animateVariant = 'selected'
188
201
  break
189
202
  default:
190
- animate = 'idle'
203
+ animateVariant = 'idle'
204
+ }
205
+ const [animateVariants, animateHandlers] = useFramerAnimateVariants()
206
+ if (isHovered && !dragging && !isInActiveOverlay) {
207
+ animateVariant = animateVariants ?? animateVariant
191
208
  }
192
209
 
193
210
  const elementIcon = ElementIcon({
@@ -210,9 +227,11 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
210
227
  }, [openOverlay, element.id])
211
228
 
212
229
  const onOpenRelationships = useCallback((e: React.MouseEvent) => {
213
- e.stopPropagation()
214
- openOverlay({ relationshipsOf: element.id })
215
- }, [openOverlay, element.id])
230
+ if (modelRef) {
231
+ e.stopPropagation()
232
+ openOverlay({ relationshipsOf: modelRef })
233
+ }
234
+ }, [openOverlay, modelRef])
216
235
 
217
236
  return (
218
237
  <>
@@ -228,7 +247,7 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
228
247
  className={clsx([
229
248
  css.container,
230
249
  isDimmed && css.dimmed,
231
- animate !== 'idle' && css.containerAnimated,
250
+ animateVariant !== 'idle' && css.containerAnimated,
232
251
  'likec4-element-node'
233
252
  ])}
234
253
  key={`${viewId}:element:${id}`}
@@ -239,7 +258,7 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
239
258
  data-animate-target=""
240
259
  initial={false}
241
260
  variants={VariantsRoot}
242
- animate={(isHovered && !dragging && !isInActiveOverlay) ? (animateVariants ?? animate) : animate}
261
+ animate={animateVariant}
243
262
  tabIndex={-1}
244
263
  {...(isInteractive && {
245
264
  onTapStart: animateHandlers.onTapStart,
@@ -285,42 +304,13 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
285
304
  </Box>
286
305
  </Box>
287
306
  {/* {isHovercards && element.links && <ElementLink element={element} />} */}
288
-
289
- {(enableRelationshipBrowser || isNavigable) && (
290
- <Group className={css.navigateBtnBox} align="center">
291
- {enableRelationshipBrowser && (
292
- <ActionIcon
293
- component={m.div}
294
- variants={VariantRealationships}
295
- data-animate-target="relationships"
296
- className={clsx('nodrag nopan', css.navigateBtn)}
297
- radius="md"
298
- role="button"
299
- onClick={onOpenRelationships}
300
- onDoubleClick={stopPropagation}
301
- {...isInteractive && animateHandlers}
302
- >
303
- <IconTransform style={{ width: '75%' }} />
304
- </ActionIcon>
305
- )}
306
- {isNavigable && (
307
- <ActionIcon
308
- component={m.div}
309
- variants={VariantsNavigate}
310
- data-animate-target="navigate"
311
- className={clsx('nodrag nopan', css.navigateBtn)}
312
- radius="md"
313
- role="button"
314
- onClick={onNavigateTo}
315
- onDoubleClick={stopPropagation}
316
- {...isInteractive && animateHandlers}
317
- >
318
- <IconZoomScan style={{ width: '75%' }} />
319
- </ActionIcon>
320
- )}
321
- </Group>
322
- )}
323
- {enableElementDetails && (
307
+ <BottomButtons
308
+ keyPrefix={`${viewId}:element:${id}:`}
309
+ onNavigateTo={isNavigable && onNavigateTo}
310
+ onOpenRelationships={enableRelationshipBrowser && !!modelRef && onOpenRelationships}
311
+ {...isInteractive && animateHandlers}
312
+ />
313
+ {enableElementDetails && !!modelRef && (
324
314
  <Tooltip
325
315
  fz="xs"
326
316
  color="dark"
@@ -329,6 +319,7 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
329
319
  offset={2}
330
320
  openDelay={600}>
331
321
  <ActionIcon
322
+ key="details"
332
323
  component={m.div}
333
324
  variants={VariantsDetailsBtn}
334
325
  data-animate-target="details"
@@ -351,37 +342,69 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
351
342
  )
352
343
  }, isEqualProps)
353
344
 
354
- // const ElementIcon = (
355
- // { element, viewId, renderIcon: RenderIcon }: {
356
- // element: DiagramNode
357
- // viewId: string
358
- // renderIcon: ElementIconRenderer | null
359
- // }
360
- // ) => {
361
- // if (!element.icon) {
362
- // return null
363
- // }
364
- // let icon = null as React.ReactNode
365
- // if (element.icon.startsWith('http://') || element.icon.startsWith('https://')) {
366
- // icon = <img src={element.icon} alt={element.title} />
367
- // } else if (RenderIcon) {
368
- // icon = <RenderIcon node={element} />
369
- // }
345
+ type BottomButtonsProps = PropsWithoutRef<
346
+ ActionIconProps & HTMLMotionProps<'div'> & {
347
+ keyPrefix: string
348
+ onNavigateTo: ((e: React.MouseEvent) => void) | false
349
+ onOpenRelationships: ((e: React.MouseEvent) => void) | false
350
+ }
351
+ >
352
+ const BottomButtons = ({
353
+ keyPrefix,
354
+ onNavigateTo,
355
+ onOpenRelationships,
356
+ ...props
357
+ }: BottomButtonsProps) => {
358
+ const enableRelationships = !!onOpenRelationships
359
+ const enableNavigate = !!onNavigateTo
370
360
 
371
- // if (!icon) {
372
- // return null
373
- // }
374
- // return (
375
- // <m.div
376
- // // key={`${viewId}:element:icon:${element.id}`}
377
- // layoutId={`${viewId}:element:icon:${element.id}`}
378
- // className={clsx(
379
- // css.elementIcon,
380
- // 'likec4-element-icon'
381
- // )}
382
- // data-likec4-icon={element.icon}
383
- // >
384
- // {icon}
385
- // </m.div>
386
- // )
387
- // }
361
+ if (!enableRelationships && !enableNavigate) {
362
+ return null
363
+ }
364
+
365
+ return (
366
+ <Box className={css.bottomButtonsContainer}>
367
+ {enableNavigate && (
368
+ <ActionIcon
369
+ {...props}
370
+ key={`${keyPrefix}navigate`}
371
+ data-animate-target="navigate"
372
+ component={m.div}
373
+ // Weird, but dts-bundle-generator fails on "enableRelationships && 'left'"
374
+ variants={variantsBottomButton('navigate', enableRelationships ? 'left' : false)}
375
+ className={clsx('nodrag nopan', css.navigateBtn)}
376
+ radius="md"
377
+ role="button"
378
+ onClick={onNavigateTo}
379
+ onDoubleClick={stopPropagation}
380
+ >
381
+ <IconZoomScan
382
+ style={{
383
+ width: '70%',
384
+ transform: 'var(--icon-scale)'
385
+ }} />
386
+ </ActionIcon>
387
+ )}
388
+ {enableRelationships && (
389
+ <ActionIcon
390
+ {...props}
391
+ key={`${keyPrefix}relationships`}
392
+ data-animate-target="relationships"
393
+ component={m.div}
394
+ variants={variantsBottomButton('relationships', enableNavigate ? 'right' : false)}
395
+ className={clsx('nodrag nopan', css.navigateBtn)}
396
+ radius="md"
397
+ role="button"
398
+ onClick={onOpenRelationships}
399
+ onDoubleClick={stopPropagation}
400
+ >
401
+ <IconTransform
402
+ style={{
403
+ width: '70%',
404
+ transform: 'var(--icon-scale)'
405
+ }} />
406
+ </ActionIcon>
407
+ )}
408
+ </Box>
409
+ )
410
+ }
@@ -141,7 +141,8 @@ export const title = style({
141
141
  fontSize: 19,
142
142
  lineHeight: 1.15,
143
143
  textWrap: 'balance',
144
- color: vars.element.hiContrast
144
+ color: vars.element.hiContrast,
145
+ whiteSpaceCollapse: 'preserve-breaks'
145
146
  })
146
147
 
147
148
  export const description = style({
@@ -338,6 +339,56 @@ export const shapeSvg = style({
338
339
  }
339
340
  })
340
341
 
342
+ export const bottomButtonsContainer = style({
343
+ zIndex: 100,
344
+ position: 'absolute',
345
+ left: 0,
346
+ width: '100%',
347
+ bottom: 2,
348
+ display: 'flex',
349
+ flexDirection: 'row',
350
+ flexWrap: 'nowrap',
351
+ alignItems: 'center',
352
+ gap: 2,
353
+ justifyContent: 'center',
354
+ pointerEvents: 'none',
355
+ selectors: {
356
+ [`:where([data-likec4-shape='browser']) &`]: {
357
+ bottom: 4
358
+ }
359
+ }
360
+ })
361
+
362
+ // --group-gap: var(--mantine-spacing-md);
363
+ // --group-align: center;
364
+ // --group-justify: flex-start;
365
+ // --group-wrap: wrap;
366
+ // gap: 0px;
367
+ // }
368
+ // <style>
369
+ // .element_bottomButtonsContainer__14yllojn {
370
+ // z-index: 100;
371
+ // position: absolute;
372
+ // left: 50%;
373
+ // bottom: 2px;
374
+ // transform: translate(-50%, 0%);
375
+ // }
376
+
377
+ // <style>
378
+ // .m_4081bf90 {
379
+ // display: flex
380
+ // ;
381
+ // flex-direction: row;
382
+ // flex-wrap: var(--group-wrap, wrap);
383
+ // justify-content: var(--group-justify, flex-start);
384
+ // align-items: var(--group-align, center);
385
+ // gap: var(--group-gap, var(--mantine-spacing-md));
386
+ // }
387
+ // globalStyle(`:where(.react-flow__node:hover) ${bottomButtonsContainer}`, {
388
+ // transitionDelay: '20ms',
389
+ // gap: 16
390
+ // })
391
+
341
392
  const btn = style({
342
393
  pointerEvents: 'all',
343
394
  color: vars.element.loContrast,
@@ -354,24 +405,6 @@ const btn = style({
354
405
  }
355
406
  })
356
407
 
357
- export const navigateBtnBox = style({
358
- zIndex: 100,
359
- position: 'absolute',
360
- left: '50%',
361
- bottom: 2,
362
- transform: 'translate(-50%, 0%)',
363
- gap: 0,
364
- selectors: {
365
- [`:where([data-likec4-shape='browser']) &`]: {
366
- bottom: 4
367
- }
368
- }
369
- })
370
- globalStyle(`:where(.react-flow__node:hover) ${navigateBtnBox}`, {
371
- transitionDelay: '20ms',
372
- gap: 16
373
- })
374
-
375
408
  export const navigateBtn = style([btn, {}])
376
409
 
377
410
  export const detailsBtn = style([btn, {
@@ -14,7 +14,7 @@ export const ElementIcon = ({
14
14
  className: string
15
15
  renderIcon: ElementIconRenderer | null
16
16
  }) => {
17
- if (!element.icon) {
17
+ if (!element.icon || element.icon === 'none') {
18
18
  return null
19
19
  }
20
20
  let icon = null as React.ReactNode
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type BorderStyle,
3
3
  defaultTheme,
4
+ DiagramNode,
4
5
  type Element,
5
6
  ElementShapes,
6
7
  type Fqn,
@@ -40,6 +41,7 @@ import { IconCheck, IconFileSymlink, IconSelector, IconTransform } from '@tabler
40
41
  import { NodeToolbar, type NodeToolbarProps } from '@xyflow/react'
41
42
  import clsx from 'clsx'
42
43
  import { useEffect, useState } from 'react'
44
+ import type { MergeExclusive } from 'type-fest'
43
45
  import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useUpdateEffect } from '../../../hooks'
44
46
  import { stopPropagation } from '../../utils'
45
47
 
@@ -75,7 +77,7 @@ const Tooltip = MantineTooltip.withProps({
75
77
  })
76
78
 
77
79
  type ToolbarProps = NodeToolbarProps & {
78
- element: Element
80
+ element: DiagramNode
79
81
  onColorPreview: (color: ThemeColor | null) => void
80
82
  }
81
83
 
@@ -86,13 +88,15 @@ export function CompoundToolbar({
86
88
  onColorPreview,
87
89
  ...props
88
90
  }: ToolbarProps) {
91
+ const modelRef = DiagramNode.modelRef(element)
92
+ const deploymentRef = DiagramNode.deploymentRef(element)
89
93
  const targets = [element.id] as NonEmptyArray<Fqn>
90
94
  const diagramApi = useDiagramStoreApi()
91
95
  const {
92
96
  hasGoToSource,
93
97
  enableRelationshipBrowser
94
98
  } = useDiagramState(s => ({
95
- hasGoToSource: !!s.onOpenSourceElement,
99
+ hasGoToSource: !!s.onOpenSource,
96
100
  enableRelationshipBrowser: s.enableRelationshipBrowser
97
101
  }))
98
102
 
@@ -117,7 +121,8 @@ export function CompoundToolbar({
117
121
  elementBorderStyle={element.style?.border}
118
122
  onChange={onChange}
119
123
  />
120
- {hasGoToSource && <GoToSourceButton elementId={element.id} />}
124
+ {hasGoToSource && modelRef && <GoToSourceButton elementId={modelRef} />}
125
+ {hasGoToSource && !modelRef && deploymentRef && <GoToSourceButton deploymentId={deploymentRef} />}
121
126
  {enableRelationshipBrowser && <BrowseRelationshipsButton elementId={element.id} />}
122
127
  </Toolbar>
123
128
  )
@@ -156,13 +161,15 @@ export function ElementToolbar({
156
161
  onColorPreview,
157
162
  ...props
158
163
  }: ToolbarProps) {
164
+ const modelRef = DiagramNode.modelRef(element)
165
+ const deploymentRef = DiagramNode.deploymentRef(element)
159
166
  const targets = [element.id] as NonEmptyArray<Fqn>
160
167
  const diagramApi = useDiagramStoreApi()
161
168
  const {
162
169
  hasGoToSource,
163
170
  enableRelationshipBrowser
164
171
  } = useDiagramState(s => ({
165
- hasGoToSource: !!s.onOpenSourceElement,
172
+ hasGoToSource: !!s.onOpenSource,
166
173
  enableRelationshipBrowser: s.enableRelationshipBrowser
167
174
  }))
168
175
  const portalProps = useMantinePortalProps()
@@ -234,20 +241,21 @@ export function ElementToolbar({
234
241
  onChange={onChange}
235
242
  position="right-end"
236
243
  />
237
- {hasGoToSource && <GoToSourceButton elementId={element.id} />}
244
+ {hasGoToSource && modelRef && <GoToSourceButton elementId={modelRef} />}
245
+ {hasGoToSource && !modelRef && deploymentRef && <GoToSourceButton deploymentId={deploymentRef} />}
238
246
  {enableRelationshipBrowser && <BrowseRelationshipsButton elementId={element.id} />}
239
247
  </Toolbar>
240
248
  )
241
249
  }
242
250
 
243
251
  type ColorButtonProps = Omit<PopoverProps, 'onChange'> & {
244
- element: Element
252
+ element: DiagramNode
245
253
  isOpacityEditable?: boolean
246
254
  onColorPreview: (color: ThemeColor | null) => void
247
255
  onChange: OnStyleChange
248
256
  }
249
257
 
250
- function GoToSourceButton({ elementId }: { elementId: Fqn }) {
258
+ function GoToSourceButton(props: MergeExclusive<{ elementId: Fqn }, { deploymentId: Fqn }>) {
251
259
  const diagramApi = useDiagramStoreApi()
252
260
  const portalProps = useMantinePortalProps()
253
261
  return (
@@ -258,7 +266,15 @@ function GoToSourceButton({ elementId }: { elementId: Fqn }) {
258
266
  color="gray"
259
267
  onClick={e => {
260
268
  e.stopPropagation()
261
- diagramApi.getState().onOpenSourceElement?.(elementId)
269
+ if (props.elementId) {
270
+ diagramApi.getState().onOpenSource?.({
271
+ element: props.elementId
272
+ })
273
+ } else if (props.deploymentId) {
274
+ diagramApi.getState().onOpenSource?.({
275
+ deployment: props.deploymentId
276
+ })
277
+ }
262
278
  }}>
263
279
  <IconFileSymlink stroke={1.8} style={{ width: '70%' }} />
264
280
  </ActionIcon>
@@ -1,4 +1,4 @@
1
- import { invariant, isAncestor, type NodeId, type NonEmptyArray, nonNullable } from '@likec4/core'
1
+ import { type NodeId, type NonEmptyArray, nonNullable } from '@likec4/core'
2
2
  import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyflow/react'
3
3
  import { getNodeDimensions } from '@xyflow/system'
4
4
  import { useMemo, useRef } from 'react'
@@ -1,3 +1,4 @@
1
+ import { DiagramNode } from '@likec4/core'
1
2
  import type { ReactFlowProps } from '@xyflow/react'
2
3
  import { useMemo, useRef } from 'react'
3
4
  import { isNonNullish, isTruthy } from 'remeda'
@@ -123,9 +124,11 @@ export function useXYFlowEvents() {
123
124
  nodesSelectable,
124
125
  enableElementDetails,
125
126
  setLastClickedNode,
126
- onOpenSourceElement,
127
+ onOpenSource,
127
128
  openOverlay
128
129
  } = diagramApi.getState()
130
+ const modelRef = DiagramNode.modelRef(xynode.data.element)
131
+ const deploymentRef = DiagramNode.deploymentRef(xynode.data.element)
129
132
  setLastClickedNode(xynode.id)
130
133
  // if we focused on a node, and clicked on another node - focus on the clicked node
131
134
  const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
@@ -134,8 +137,16 @@ export function useXYFlowEvents() {
134
137
 
135
138
  let stopPropagation = false
136
139
 
137
- if (clickedRecently && !!onOpenSourceElement) {
138
- onOpenSourceElement(xynode.data.element.id)
140
+ if (clickedRecently && !!onOpenSource && (modelRef || deploymentRef)) {
141
+ if (modelRef) {
142
+ onOpenSource({
143
+ element: modelRef
144
+ })
145
+ } else if (deploymentRef) {
146
+ onOpenSource({
147
+ deployment: deploymentRef
148
+ })
149
+ }
139
150
  stopPropagation = true
140
151
  }
141
152
 
@@ -152,7 +163,7 @@ export function useXYFlowEvents() {
152
163
  stopPropagation = true
153
164
  break
154
165
  }
155
- case !clickedRecently && focusedNodeId === xynode.id && enableElementDetails: {
166
+ case !clickedRecently && modelRef && focusedNodeId === xynode.id && enableElementDetails: {
156
167
  openOverlay({
157
168
  elementDetails: xynode.data.element.id
158
169
  })
@@ -160,7 +171,9 @@ export function useXYFlowEvents() {
160
171
  break
161
172
  }
162
173
  }
163
- } else if (enableElementDetails && (clickedRecently || focusedNodeId === xynode.id) && !onNodeClick) {
174
+ } else if (
175
+ enableElementDetails && modelRef && (clickedRecently || focusedNodeId === xynode.id) && !onNodeClick
176
+ ) {
164
177
  openOverlay({
165
178
  elementDetails: xynode.data.element.id
166
179
  })
@@ -1,9 +0,0 @@
1
- import { fallbackVar, style } from '@vanilla-extract/css'
2
- import { mantine, vars } from '../theme-vars'
3
-
4
- export const panel = style({
5
- top: fallbackVar(vars.optionsPanel.top, '1rem'),
6
- right: fallbackVar(vars.optionsPanel.right, '1rem'),
7
- margin: 0,
8
- boxShadow: mantine.shadows.xl
9
- })