@likec4/diagram 1.15.1 → 1.17.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 (47) hide show
  1. package/package.json +7 -7
  2. package/src/LikeC4CustomColors.tsx +24 -21
  3. package/src/LikeC4Diagram.props.ts +9 -9
  4. package/src/LikeC4Diagram.tsx +25 -26
  5. package/src/StaticLikeC4Diagram.tsx +9 -3
  6. package/src/overlays/OverlayContext.tsx +1 -1
  7. package/src/overlays/Overlays.css.ts +28 -23
  8. package/src/overlays/Overlays.tsx +116 -64
  9. package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
  10. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
  11. package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
  12. package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
  13. package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
  14. package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
  15. package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
  16. package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
  17. package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
  18. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
  19. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
  20. package/src/overlays/relationships-of/_types.ts +15 -4
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
  27. package/src/state/DiagramContext.tsx +15 -8
  28. package/src/state/diagramStore.ts +14 -6
  29. package/src/theme-vars.ts +9 -0
  30. package/src/ui/FramerMotionConfig.tsx +18 -0
  31. package/src/ui/notation/NotationPanel.tsx +3 -3
  32. package/src/xyflow/XYFlow.tsx +18 -35
  33. package/src/xyflow/XYFlowInner.tsx +2 -2
  34. package/src/xyflow/edges/EdgeLabel.tsx +3 -3
  35. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
  36. package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
  37. package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
  38. package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
  39. package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
  40. package/src/xyflow/nodes/element/ElementNode.tsx +110 -116
  41. package/src/xyflow/nodes/element/element.css.ts +38 -18
  42. package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
  43. package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
  44. package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
  45. package/src/xyflow/utils.ts +12 -3
  46. package/src/LikeC4CustomColors.props.ts +0 -5
  47. /package/src/{mantine → ui}/EnsureMantine.tsx +0 -0
@@ -1,21 +1,20 @@
1
- import { type DiagramNode, type ThemeColor } from '@likec4/core'
2
- import { ActionIcon, Text as MantineText, Tooltip } from '@mantine/core'
1
+ import { type ThemeColor } from '@likec4/core'
2
+ import { ActionIcon, Box, Text as MantineText, Tooltip } from '@mantine/core'
3
3
  import { useDebouncedValue } from '@mantine/hooks'
4
- import { IconTransform, IconZoomScan } from '@tabler/icons-react'
4
+ import { IconId, 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
8
  import { m, type Variants } from 'framer-motion'
9
- import { memo, useCallback, useState } from 'react'
9
+ import React, { memo, useCallback, useState } from 'react'
10
10
  import { isNumber, isTruthy } from 'remeda'
11
11
  import { useDiagramState } from '../../../hooks/useDiagramState'
12
- import type { ElementIconRenderer } from '../../../LikeC4Diagram.props'
13
12
  import type { ElementXYFlowNode } from '../../types'
14
13
  import { stopPropagation, toDomPrecision } from '../../utils'
14
+ import { ElementIcon } from '../shared/ElementIcon'
15
15
  import { ElementToolbar } from '../shared/Toolbar'
16
16
  import { useFramerAnimateVariants } from '../use-animate-variants'
17
17
  import * as css from './element.css'
18
- import { ElementLink } from './ElementLink'
19
18
  import { ElementShapeSvg, SelectedIndicator } from './ElementShapeSvg'
20
19
 
21
20
  const Text = MantineText.withProps({
@@ -32,9 +31,9 @@ const VariantsRoot = {
32
31
  transition: isNumber(scale) && scale > selectedScale
33
32
  ? {
34
33
  delay: 0.1,
35
- delayChildren: 0.06,
36
- staggerChildren: 0.07,
37
- staggerDirection: -1
34
+ delayChildren: 0.06
35
+ // staggerChildren: 0.07,
36
+ // staggerDirection: -1
38
37
  }
39
38
  : {}
40
39
  }),
@@ -46,8 +45,7 @@ const VariantsRoot = {
46
45
  transition: !isNumber(scale) || (scale >= 1 && scale < 1.06)
47
46
  ? {
48
47
  // delay: 0.09,
49
- delayChildren: 0.07,
50
- staggerChildren: 0.1
48
+ delayChildren: 0.08
51
49
  }
52
50
  : {}
53
51
  }),
@@ -60,14 +58,14 @@ const VariantsNavigate = {
60
58
  idle: {
61
59
  '--ai-bg': 'var(--ai-bg-idle)',
62
60
  scale: 1,
63
- opacity: 0.75,
61
+ opacity: 0.5,
64
62
  translateX: '-50%',
65
- originY: 0.1
63
+ originY: 0.2
66
64
  },
67
65
  selected: {},
68
66
  hovered: {
69
- display: 'block',
70
67
  '--ai-bg': 'var(--ai-bg-hover)',
68
+ translateX: '-50%',
71
69
  scale: 1.35,
72
70
  opacity: 1
73
71
  },
@@ -80,52 +78,26 @@ const VariantsNavigate = {
80
78
  } satisfies Variants
81
79
  VariantsNavigate['selected'] = VariantsNavigate['hovered']
82
80
 
83
- /**
84
- * Variants for the relationships button (when Navigate button is visible)
85
- */
86
- const VariantsRelationsBtn = {
81
+ const VariantsDetailsBtn = {
87
82
  idle: {
88
83
  '--ai-bg': 'var(--ai-bg-idle)',
89
84
  scale: 1,
90
- opacity: 0,
91
- originX: 0.25,
92
- originY: 0.1,
93
- translateX: -8,
94
- transitionEnd: {
95
- display: 'none'
96
- }
85
+ opacity: 0.5
97
86
  },
98
87
  selected: {},
99
88
  hovered: {
100
- display: 'block',
101
- '--ai-bg': 'var(--ai-bg-hover)',
102
- scale: 1.25,
103
- opacity: 1,
104
- translateX: 0
89
+ scale: 1.2,
90
+ opacity: 0.7
105
91
  },
106
- 'hovered:navigate': {},
107
- 'hovered:relations': {
108
- scale: 1.42
92
+ 'hovered:details': {
93
+ scale: 1.42,
94
+ opacity: 1
109
95
  },
110
- 'tap:relations': {
96
+ 'tap:details': {
111
97
  scale: 1.15
112
98
  }
113
99
  } satisfies Variants
114
- VariantsRelationsBtn['selected'] = VariantsRelationsBtn['hovered']
115
-
116
- /**
117
- * Variants for the relationships button (when Navigate button is not visible)
118
- */
119
- const VariantsRelationsBtnSingle = {
120
- ...VariantsNavigate,
121
- idle: {
122
- ...VariantsNavigate.idle,
123
- opacity: 0,
124
- transitionEnd: {
125
- display: 'none'
126
- }
127
- }
128
- }
100
+ VariantsDetailsBtn['selected'] = VariantsDetailsBtn['hovered']
129
101
 
130
102
  type ElementNodeProps = NodeProps<ElementXYFlowNode>
131
103
  const isEqualProps = (prev: ElementNodeProps, next: ElementNodeProps) => (
@@ -148,33 +120,34 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
148
120
  height
149
121
  }) {
150
122
  const {
123
+ viewId,
151
124
  isEditable,
152
125
  isHovered,
153
126
  isDimmed,
154
- hasOnNavigateTo,
155
- isHovercards,
127
+ isNavigable,
156
128
  isInteractive,
157
- enableRelationshipsMode,
129
+ enableElementDetails,
158
130
  triggerOnNavigateTo,
159
131
  openOverlay,
132
+ isInActiveOverlay,
160
133
  renderIcon
161
134
  } = useDiagramState(s => ({
135
+ viewId: s.view.id,
162
136
  isEditable: s.readonly !== true,
163
137
  isHovered: s.hoveredNodeId === id,
164
138
  isDimmed: s.dimmed.has(id),
165
- isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableRelationshipsBrowser
139
+ isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableElementDetails
166
140
  || (!!s.onNavigateTo && !!element.navigateTo),
167
- isHovercards: s.showElementLinks,
168
- hasOnNavigateTo: !!s.onNavigateTo,
169
- enableRelationshipsMode: s.enableRelationshipsBrowser,
141
+ isNavigable: (!!s.onNavigateTo && !!element.navigateTo),
142
+ enableElementDetails: s.enableElementDetails,
170
143
  triggerOnNavigateTo: s.triggerOnNavigateTo,
171
144
  openOverlay: s.openOverlay,
145
+ isInActiveOverlay: (s.activeOverlay?.elementDetails ?? s.activeOverlay?.relationshipsOf) === id,
172
146
  renderIcon: s.renderIcon
173
147
  }))
174
-
175
- const isNavigable = hasOnNavigateTo && !!element.navigateTo
176
-
177
- const _isToolbarVisible = isEditable && (isHovered || (import.meta.env.DEV && selected))
148
+ // For development purposes, show the toolbar when the element is selected
149
+ const isHoveredOrSelected = isHovered || (import.meta.env.DEV && selected)
150
+ const _isToolbarVisible = isEditable && isHoveredOrSelected
178
151
  const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
179
152
 
180
153
  const w = toDomPrecision(width ?? element.width)
@@ -184,6 +157,9 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
184
157
 
185
158
  let animate: keyof typeof VariantsRoot
186
159
  switch (true) {
160
+ case isInActiveOverlay:
161
+ animate = 'idle'
162
+ break
187
163
  case dragging && selected:
188
164
  animate = 'selected'
189
165
  break
@@ -202,6 +178,8 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
202
178
 
203
179
  const elementIcon = ElementIcon({
204
180
  element,
181
+ viewId,
182
+ className: css.elementIcon,
205
183
  renderIcon
206
184
  })
207
185
 
@@ -212,9 +190,9 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
212
190
  triggerOnNavigateTo(id, e)
213
191
  }, [triggerOnNavigateTo, id])
214
192
 
215
- const onBrowseRelations = useCallback((e: React.MouseEvent) => {
193
+ const onOpenDetails = useCallback((e: React.MouseEvent) => {
216
194
  e.stopPropagation()
217
- openOverlay({ relationshipsOf: element.id })
195
+ openOverlay({ elementDetails: element.id })
218
196
  }, [openOverlay, element.id])
219
197
 
220
198
  return (
@@ -226,21 +204,23 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
226
204
  onColorPreview={setPreviewColor}
227
205
  />
228
206
  )}
229
- <m.div
230
- id={id}
207
+ <Box
208
+ component={m.div}
231
209
  className={clsx([
232
210
  css.container,
233
211
  isDimmed && css.dimmed,
234
212
  animate !== 'idle' && css.containerAnimated,
235
213
  'likec4-element-node'
236
214
  ])}
215
+ key={`${viewId}:element:${id}`}
216
+ layoutId={`${viewId}:element:${id}`}
237
217
  data-hovered={!dragging && isHovered}
238
218
  data-likec4-color={previewColor ?? element.color}
239
219
  data-likec4-shape={element.shape}
240
220
  data-animate-target=""
241
- variants={VariantsRoot}
242
221
  initial={false}
243
- animate={(isHovered && !dragging) ? (animateVariants ?? animate) : animate}
222
+ variants={VariantsRoot}
223
+ animate={(isHovered && !dragging && !isInActiveOverlay) ? (animateVariants ?? animate) : animate}
244
224
  tabIndex={-1}
245
225
  {...(isInteractive && {
246
226
  onTapStart: animateHandlers.onTapStart,
@@ -248,13 +228,15 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
248
228
  onTapCancel: animateHandlers.onTapCancel
249
229
  })}
250
230
  >
251
- <svg className={clsx(css.cssShapeSvg)} viewBox={`0 0 ${w} ${h}`} width={w} height={h}>
231
+ <svg
232
+ className={clsx(css.shapeSvg)}
233
+ viewBox={`0 0 ${w} ${h}`}>
252
234
  <g className={css.indicator}>
253
235
  <SelectedIndicator shape={element.shape} w={w} h={h} />
254
236
  </g>
255
237
  <ElementShapeSvg shape={element.shape} w={w} h={h} />
256
238
  </svg>
257
- <div
239
+ <Box
258
240
  className={clsx(
259
241
  css.elementDataContainer,
260
242
  isTruthy(elementIcon) && css.hasIcon,
@@ -262,10 +244,15 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
262
244
  )}
263
245
  >
264
246
  {elementIcon}
265
- <div className={clsx(css.elementTextData, 'likec4-element-main-props')}>
266
- <Text className={clsx(css.title, 'likec4-element-title')} lineClamp={3}>
247
+ <Box className={clsx(css.elementTextData, 'likec4-element-main-props')}>
248
+ <Text
249
+ component={m.div}
250
+ key={`${viewId}:element:title:${id}`}
251
+ layoutId={`${viewId}:element:title:${id}`}
252
+ className={clsx(css.title, 'likec4-element-title')}>
267
253
  {element.title}
268
254
  </Text>
255
+
269
256
  {element.technology && (
270
257
  <Text className={clsx(css.technology, 'likec4-element-technology')}>
271
258
  {element.technology}
@@ -276,9 +263,9 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
276
263
  {element.description}
277
264
  </Text>
278
265
  )}
279
- </div>
280
- </div>
281
- {isHovercards && element.links && <ElementLink element={element} />}
266
+ </Box>
267
+ </Box>
268
+ {/* {isHovercards && element.links && <ElementLink element={element} />} */}
282
269
  {isNavigable && (
283
270
  <ActionIcon
284
271
  component={m.div}
@@ -292,64 +279,71 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
292
279
  onDoubleClick={stopPropagation}
293
280
  {...isInteractive && animateHandlers}
294
281
  >
295
- <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
282
+ <IconZoomScan style={{ width: '75%' }} />
296
283
  </ActionIcon>
297
284
  )}
298
- {enableRelationshipsMode && (
299
- <Tooltip fz="xs" color="dark" label="Browse relationships" withinPortal={false} openDelay={600}>
285
+ {enableElementDetails && (
286
+ <Tooltip
287
+ fz="xs"
288
+ color="dark"
289
+ label="Open details"
290
+ withinPortal={false}
291
+ offset={2}
292
+ openDelay={600}>
300
293
  <ActionIcon
301
294
  component={m.div}
302
- // Is is a second button, so we need to use a different variant
303
- variants={isNavigable ? VariantsRelationsBtn : VariantsRelationsBtnSingle}
304
- data-animate-target={isNavigable ? 'relations' : 'navigate'}
305
- className={clsx('nodrag nopan', css.navigateBtn)}
295
+ variants={VariantsDetailsBtn}
296
+ data-animate-target="details"
297
+ className={clsx('nodrag nopan', css.detailsBtn)}
306
298
  radius="md"
307
- style={{
308
- left: isNavigable ? 'calc(50% + 28px)' : '50%',
309
- zIndex: 99
310
- }}
299
+ style={{ zIndex: 100 }}
311
300
  role="button"
312
- onClick={onBrowseRelations}
301
+ onClick={onOpenDetails}
313
302
  onDoubleClick={stopPropagation}
314
303
  {...isInteractive && animateHandlers}
315
304
  >
316
- <IconTransform stroke={1.8} style={{ width: '67%' }} />
305
+ <IconId stroke={1.8} style={{ width: '75%' }} />
317
306
  </ActionIcon>
318
307
  </Tooltip>
319
308
  )}
320
- </m.div>
309
+ </Box>
321
310
  <Handle type="target" position={Position.Top} className={css.handleCenter} />
322
311
  <Handle type="source" position={Position.Top} className={css.handleCenter} />
323
312
  </>
324
313
  )
325
314
  }, isEqualProps)
326
315
 
327
- const ElementIcon = (
328
- { element, renderIcon: RenderIcon }: { element: DiagramNode; renderIcon: ElementIconRenderer | null }
329
- ) => {
330
- if (!element.icon) {
331
- return null
332
- }
333
- if (element.icon.startsWith('http://') || element.icon.startsWith('https://')) {
334
- return (
335
- <div className={clsx(css.elementIcon, 'likec4-element-icon')}>
336
- <img src={element.icon} alt={element.title} />
337
- </div>
338
- )
339
- }
340
- const icon = RenderIcon ? <RenderIcon node={element} /> : null
341
- if (!icon) {
342
- return null
343
- }
344
- return (
345
- <div
346
- className={clsx(
347
- css.elementIcon,
348
- 'likec4-element-icon'
349
- )}
350
- data-likec4-icon={element.icon}
351
- >
352
- {icon}
353
- </div>
354
- )
355
- }
316
+ // const ElementIcon = (
317
+ // { element, viewId, renderIcon: RenderIcon }: {
318
+ // element: DiagramNode
319
+ // viewId: string
320
+ // renderIcon: ElementIconRenderer | null
321
+ // }
322
+ // ) => {
323
+ // if (!element.icon) {
324
+ // return null
325
+ // }
326
+ // let icon = null as React.ReactNode
327
+ // if (element.icon.startsWith('http://') || element.icon.startsWith('https://')) {
328
+ // icon = <img src={element.icon} alt={element.title} />
329
+ // } else if (RenderIcon) {
330
+ // icon = <RenderIcon node={element} />
331
+ // }
332
+
333
+ // if (!icon) {
334
+ // return null
335
+ // }
336
+ // return (
337
+ // <m.div
338
+ // // key={`${viewId}:element:icon:${element.id}`}
339
+ // layoutId={`${viewId}:element:icon:${element.id}`}
340
+ // className={clsx(
341
+ // css.elementIcon,
342
+ // 'likec4-element-icon'
343
+ // )}
344
+ // data-likec4-icon={element.icon}
345
+ // >
346
+ // {icon}
347
+ // </m.div>
348
+ // )
349
+ // }
@@ -5,11 +5,13 @@ import { mantine, vars } from '../../../theme-vars'
5
5
  export const stokeFillMix = createVar('stroke-fill-mix')
6
6
 
7
7
  export const container = style({
8
+ position: 'absolute',
9
+ top: 0,
10
+ left: 0,
8
11
  width: '100%',
9
12
  height: '100%',
10
13
  padding: 0,
11
14
  margin: 0,
12
- flex: '1',
13
15
  display: 'flex',
14
16
  alignItems: 'center',
15
17
  justifyContent: 'center',
@@ -24,6 +26,7 @@ export const container = style({
24
26
  willChange: 'transform'
25
27
  }
26
28
  },
29
+ // Catch pointer below the element
27
30
  ':after': {
28
31
  content: ' ',
29
32
  position: 'absolute',
@@ -258,12 +261,7 @@ export const elementIcon = style({
258
261
  alignItems: 'center',
259
262
  justifyContent: 'center',
260
263
  mixBlendMode: 'hard-light',
261
- alignSelf: 'flex-start',
262
- selectors: {
263
- '&[data-likec4-icon^="azure:"]': {
264
- mixBlendMode: 'normal'
265
- }
266
- }
264
+ alignSelf: 'flex-start'
267
265
  })
268
266
  globalStyle(`${elementIcon} svg, ${elementIcon} img`, {
269
267
  width: '100%',
@@ -282,7 +280,7 @@ globalStyle(`${elementIcon} img`, {
282
280
 
283
281
  const filterShadow = createVar('filter-shadow')
284
282
 
285
- export const cssShapeSvgMultiple = style({
283
+ export const shapeSvgMultiple = style({
286
284
  top: 0,
287
285
  left: 0,
288
286
  position: 'absolute',
@@ -310,10 +308,12 @@ export const cssShapeSvgMultiple = style({
310
308
  }
311
309
  }
312
310
  })
313
- export const cssShapeSvg = style({
311
+ export const shapeSvg = style({
314
312
  top: 0,
315
313
  left: 0,
316
314
  position: 'absolute',
315
+ width: '100%',
316
+ height: '100%',
317
317
  pointerEvents: 'none',
318
318
  fill: vars.element.fill,
319
319
  stroke: vars.element.stroke,
@@ -338,12 +338,8 @@ export const cssShapeSvg = style({
338
338
  }
339
339
  })
340
340
 
341
- export const navigateBtn = style({
342
- zIndex: 'calc(var(--layer-overlays, 1) + 1)',
343
- position: 'absolute',
341
+ const btn = style({
344
342
  pointerEvents: 'all',
345
- left: '50%',
346
- bottom: 0,
347
343
  color: vars.element.loContrast,
348
344
  cursor: 'pointer',
349
345
  backgroundColor: 'var(--ai-bg)',
@@ -353,12 +349,36 @@ export const navigateBtn = style({
353
349
  '--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill} 65%, ${vars.element.stroke})`,
354
350
  '--ai-hover': `color-mix(in srgb , ${vars.element.fill} 50%, ${vars.element.stroke})`
355
351
  },
352
+ ':hover': {
353
+ boxShadow: mantine.shadows.md
354
+ }
355
+ })
356
+
357
+ export const navigateBtn = style([btn, {
358
+ position: 'absolute',
359
+ left: '50%',
360
+ bottom: 0,
356
361
  selectors: {
357
362
  [`:where([data-likec4-shape='browser']) &`]: {
358
363
  bottom: 4
359
364
  }
360
- },
361
- ':hover': {
362
- boxShadow: mantine.shadows.md
363
365
  }
364
- })
366
+ }])
367
+
368
+ export const detailsBtn = style([btn, {
369
+ position: 'absolute',
370
+ top: 2,
371
+ right: 2,
372
+ selectors: {
373
+ [`:where([data-likec4-shape='browser']) &`]: {
374
+ right: 5
375
+ },
376
+ ':where([data-likec4-shape="cylinder"], [data-likec4-shape="storage"]) &': {
377
+ top: 14
378
+ },
379
+ ':where([data-likec4-shape="queue"]) &': {
380
+ top: 1,
381
+ right: 12
382
+ }
383
+ }
384
+ }])
@@ -0,0 +1,43 @@
1
+ import type { DiagramNode } from '@likec4/core'
2
+ import clsx from 'clsx'
3
+ import { m } from 'framer-motion'
4
+ import type { ElementIconRenderer } from '../../../LikeC4Diagram.props'
5
+
6
+ export const ElementIcon = ({
7
+ element,
8
+ viewId,
9
+ className,
10
+ renderIcon: RenderIcon
11
+ }: {
12
+ element: DiagramNode
13
+ viewId: string
14
+ className: string
15
+ renderIcon: ElementIconRenderer | null
16
+ }) => {
17
+ if (!element.icon) {
18
+ return null
19
+ }
20
+ let icon = null as React.ReactNode
21
+ if (element.icon.startsWith('http://') || element.icon.startsWith('https://')) {
22
+ icon = <img src={element.icon} alt={element.title} />
23
+ } else if (RenderIcon) {
24
+ icon = <RenderIcon node={element} />
25
+ }
26
+
27
+ if (!icon) {
28
+ return null
29
+ }
30
+ return (
31
+ <m.div
32
+ key={`${viewId}:element:icon:${element.id}`}
33
+ layoutId={`${viewId}:element:icon:${element.id}`}
34
+ className={clsx(
35
+ className,
36
+ 'likec4-element-icon'
37
+ )}
38
+ data-likec4-icon={element.icon}
39
+ >
40
+ {icon}
41
+ </m.div>
42
+ )
43
+ }
@@ -90,10 +90,10 @@ export function CompoundToolbar({
90
90
  const diagramApi = useDiagramStoreApi()
91
91
  const {
92
92
  hasGoToSource,
93
- enableRelationshipsBrowser
93
+ enableRelationshipBrowser
94
94
  } = useDiagramState(s => ({
95
95
  hasGoToSource: !!s.onOpenSourceElement,
96
- enableRelationshipsBrowser: s.enableRelationshipsBrowser
96
+ enableRelationshipBrowser: s.enableRelationshipBrowser
97
97
  }))
98
98
 
99
99
  const onChange: OnStyleChange = (style) => {
@@ -118,7 +118,7 @@ export function CompoundToolbar({
118
118
  onChange={onChange}
119
119
  />
120
120
  {hasGoToSource && <GoToSourceButton elementId={element.id} />}
121
- {enableRelationshipsBrowser && <BrowseRelationshipsButton elementId={element.id} />}
121
+ {enableRelationshipBrowser && <BrowseRelationshipsButton elementId={element.id} />}
122
122
  </Toolbar>
123
123
  )
124
124
  }
@@ -160,10 +160,10 @@ export function ElementToolbar({
160
160
  const diagramApi = useDiagramStoreApi()
161
161
  const {
162
162
  hasGoToSource,
163
- enableRelationshipsBrowser
163
+ enableRelationshipBrowser
164
164
  } = useDiagramState(s => ({
165
165
  hasGoToSource: !!s.onOpenSourceElement,
166
- enableRelationshipsBrowser: s.enableRelationshipsBrowser
166
+ enableRelationshipBrowser: s.enableRelationshipBrowser
167
167
  }))
168
168
  const portalProps = useMantinePortalProps()
169
169
 
@@ -235,7 +235,7 @@ export function ElementToolbar({
235
235
  position="right-end"
236
236
  />
237
237
  {hasGoToSource && <GoToSourceButton elementId={element.id} />}
238
- {enableRelationshipsBrowser && <BrowseRelationshipsButton elementId={element.id} />}
238
+ {enableRelationshipBrowser && <BrowseRelationshipsButton elementId={element.id} />}
239
239
  </Toolbar>
240
240
  )
241
241
  }