@likec4/diagram 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +22 -18
  2. package/src/LikeC4CustomColors.tsx +24 -21
  3. package/src/LikeC4Diagram.props.ts +15 -9
  4. package/src/LikeC4Diagram.tsx +33 -27
  5. package/src/LikeC4Search.tsx +146 -0
  6. package/src/StaticLikeC4Diagram.tsx +10 -5
  7. package/src/overlays/OverlayContext.tsx +1 -1
  8. package/src/overlays/Overlays.css.ts +28 -23
  9. package/src/overlays/Overlays.tsx +116 -64
  10. package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
  11. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
  12. package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
  13. package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
  14. package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
  15. package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
  16. package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
  17. package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
  18. package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
  19. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
  20. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
  21. package/src/overlays/relationships-of/_types.ts +15 -4
  22. package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
  23. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
  24. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
  25. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
  26. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
  27. package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
  28. package/src/state/DiagramContext.tsx +15 -8
  29. package/src/state/aligners.ts +473 -0
  30. package/src/state/diagramStore.layout.ts +67 -0
  31. package/src/state/diagramStore.ts +18 -73
  32. package/src/theme-vars.ts +9 -0
  33. package/src/ui/FramerMotionConfig.tsx +18 -0
  34. package/src/ui/notation/NotationPanel.tsx +3 -3
  35. package/src/ui/top-left/ManualLayoutToolsButton.tsx +15 -0
  36. package/src/ui/top-left/TopLeftPanel.tsx +14 -1
  37. package/src/xyflow/XYFlow.tsx +18 -35
  38. package/src/xyflow/XYFlowInner.tsx +2 -2
  39. package/src/xyflow/edges/EdgeLabel.tsx +3 -3
  40. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
  41. package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
  42. package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
  43. package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
  44. package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
  45. package/src/xyflow/nodes/element/ElementNode.tsx +165 -133
  46. package/src/xyflow/nodes/element/element.css.ts +46 -17
  47. package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
  48. package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
  49. package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
  50. package/src/xyflow/utils.ts +12 -3
  51. package/src/LikeC4CustomColors.props.ts +0 -5
  52. /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, Group, 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, 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
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
  }),
@@ -56,21 +54,24 @@ const VariantsRoot = {
56
54
  }
57
55
  } satisfies Variants
58
56
 
59
- const VariantsNavigate = {
57
+ const VariantsNavigateBtn = {
60
58
  idle: {
61
59
  '--ai-bg': 'var(--ai-bg-idle)',
62
60
  scale: 1,
63
- opacity: 0.75,
64
- translateX: '-50%',
65
- originY: 0.1
61
+ opacity: 0.5,
62
+ originY: 0.5
66
63
  },
67
64
  selected: {},
68
65
  hovered: {
69
- display: 'block',
70
66
  '--ai-bg': 'var(--ai-bg-hover)',
71
67
  scale: 1.35,
72
68
  opacity: 1
73
- },
69
+ }
70
+ } satisfies Variants
71
+ VariantsNavigateBtn['selected'] = VariantsNavigateBtn['hovered']
72
+
73
+ const VariantsNavigate = {
74
+ ...VariantsNavigateBtn,
74
75
  'hovered:navigate': {
75
76
  scale: 1.42
76
77
  },
@@ -78,54 +79,37 @@ const VariantsNavigate = {
78
79
  scale: 1.15
79
80
  }
80
81
  } satisfies Variants
81
- VariantsNavigate['selected'] = VariantsNavigate['hovered']
82
82
 
83
- /**
84
- * Variants for the relationships button (when Navigate button is visible)
85
- */
86
- const VariantsRelationsBtn = {
83
+ const VariantRealationships = {
84
+ ...VariantsNavigateBtn,
85
+ 'hovered:relationships': {
86
+ scale: 1.42
87
+ },
88
+ 'tap:relationships': {
89
+ scale: 1.15
90
+ }
91
+ } satisfies Variants
92
+
93
+ const VariantsDetailsBtn = {
87
94
  idle: {
88
95
  '--ai-bg': 'var(--ai-bg-idle)',
89
96
  scale: 1,
90
- opacity: 0,
91
- originX: 0.25,
92
- originY: 0.1,
93
- translateX: -8,
94
- transitionEnd: {
95
- display: 'none'
96
- }
97
+ opacity: 0.5
97
98
  },
98
99
  selected: {},
99
100
  hovered: {
100
- display: 'block',
101
- '--ai-bg': 'var(--ai-bg-hover)',
102
- scale: 1.25,
103
- opacity: 1,
104
- translateX: 0
101
+ scale: 1.2,
102
+ opacity: 0.7
105
103
  },
106
- 'hovered:navigate': {},
107
- 'hovered:relations': {
108
- scale: 1.42
104
+ 'hovered:details': {
105
+ scale: 1.42,
106
+ opacity: 1
109
107
  },
110
- 'tap:relations': {
108
+ 'tap:details': {
111
109
  scale: 1.15
112
110
  }
113
111
  } 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
- }
112
+ VariantsDetailsBtn['selected'] = VariantsDetailsBtn['hovered']
129
113
 
130
114
  type ElementNodeProps = NodeProps<ElementXYFlowNode>
131
115
  const isEqualProps = (prev: ElementNodeProps, next: ElementNodeProps) => (
@@ -148,33 +132,36 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
148
132
  height
149
133
  }) {
150
134
  const {
135
+ viewId,
151
136
  isEditable,
152
137
  isHovered,
153
138
  isDimmed,
154
- hasOnNavigateTo,
155
- isHovercards,
139
+ isNavigable,
156
140
  isInteractive,
157
- enableRelationshipsMode,
141
+ enableElementDetails,
142
+ enableRelationshipBrowser,
158
143
  triggerOnNavigateTo,
159
144
  openOverlay,
145
+ isInActiveOverlay,
160
146
  renderIcon
161
147
  } = useDiagramState(s => ({
148
+ viewId: s.view.id,
162
149
  isEditable: s.readonly !== true,
163
150
  isHovered: s.hoveredNodeId === id,
164
151
  isDimmed: s.dimmed.has(id),
165
- isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableRelationshipsBrowser
152
+ isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableElementDetails || s.enableRelationshipBrowser
166
153
  || (!!s.onNavigateTo && !!element.navigateTo),
167
- isHovercards: s.showElementLinks,
168
- hasOnNavigateTo: !!s.onNavigateTo,
169
- enableRelationshipsMode: s.enableRelationshipsBrowser,
154
+ isNavigable: (!!s.onNavigateTo && !!element.navigateTo),
155
+ enableElementDetails: s.enableElementDetails,
156
+ enableRelationshipBrowser: s.enableRelationshipBrowser,
170
157
  triggerOnNavigateTo: s.triggerOnNavigateTo,
171
158
  openOverlay: s.openOverlay,
159
+ isInActiveOverlay: (s.activeOverlay?.elementDetails ?? s.activeOverlay?.relationshipsOf) === id,
172
160
  renderIcon: s.renderIcon
173
161
  }))
174
-
175
- const isNavigable = hasOnNavigateTo && !!element.navigateTo
176
-
177
- const _isToolbarVisible = isEditable && (isHovered || (import.meta.env.DEV && selected))
162
+ // For development purposes, show the toolbar when the element is selected
163
+ const isHoveredOrSelected = isHovered || (import.meta.env.DEV && selected)
164
+ const _isToolbarVisible = isEditable && isHoveredOrSelected
178
165
  const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
179
166
 
180
167
  const w = toDomPrecision(width ?? element.width)
@@ -184,6 +171,9 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
184
171
 
185
172
  let animate: keyof typeof VariantsRoot
186
173
  switch (true) {
174
+ case isInActiveOverlay:
175
+ animate = 'idle'
176
+ break
187
177
  case dragging && selected:
188
178
  animate = 'selected'
189
179
  break
@@ -202,6 +192,8 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
202
192
 
203
193
  const elementIcon = ElementIcon({
204
194
  element,
195
+ viewId,
196
+ className: css.elementIcon,
205
197
  renderIcon
206
198
  })
207
199
 
@@ -212,7 +204,12 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
212
204
  triggerOnNavigateTo(id, e)
213
205
  }, [triggerOnNavigateTo, id])
214
206
 
215
- const onBrowseRelations = useCallback((e: React.MouseEvent) => {
207
+ const onOpenDetails = useCallback((e: React.MouseEvent) => {
208
+ e.stopPropagation()
209
+ openOverlay({ elementDetails: element.id })
210
+ }, [openOverlay, element.id])
211
+
212
+ const onOpenRelationships = useCallback((e: React.MouseEvent) => {
216
213
  e.stopPropagation()
217
214
  openOverlay({ relationshipsOf: element.id })
218
215
  }, [openOverlay, element.id])
@@ -226,21 +223,23 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
226
223
  onColorPreview={setPreviewColor}
227
224
  />
228
225
  )}
229
- <m.div
230
- id={id}
226
+ <Box
227
+ component={m.div}
231
228
  className={clsx([
232
229
  css.container,
233
230
  isDimmed && css.dimmed,
234
231
  animate !== 'idle' && css.containerAnimated,
235
232
  'likec4-element-node'
236
233
  ])}
234
+ key={`${viewId}:element:${id}`}
235
+ layoutId={`${viewId}:element:${id}`}
237
236
  data-hovered={!dragging && isHovered}
238
237
  data-likec4-color={previewColor ?? element.color}
239
238
  data-likec4-shape={element.shape}
240
239
  data-animate-target=""
241
- variants={VariantsRoot}
242
240
  initial={false}
243
- animate={(isHovered && !dragging) ? (animateVariants ?? animate) : animate}
241
+ variants={VariantsRoot}
242
+ animate={(isHovered && !dragging && !isInActiveOverlay) ? (animateVariants ?? animate) : animate}
244
243
  tabIndex={-1}
245
244
  {...(isInteractive && {
246
245
  onTapStart: animateHandlers.onTapStart,
@@ -248,13 +247,15 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
248
247
  onTapCancel: animateHandlers.onTapCancel
249
248
  })}
250
249
  >
251
- <svg className={clsx(css.cssShapeSvg)} viewBox={`0 0 ${w} ${h}`} width={w} height={h}>
250
+ <svg
251
+ className={clsx(css.shapeSvg)}
252
+ viewBox={`0 0 ${w} ${h}`}>
252
253
  <g className={css.indicator}>
253
254
  <SelectedIndicator shape={element.shape} w={w} h={h} />
254
255
  </g>
255
256
  <ElementShapeSvg shape={element.shape} w={w} h={h} />
256
257
  </svg>
257
- <div
258
+ <Box
258
259
  className={clsx(
259
260
  css.elementDataContainer,
260
261
  isTruthy(elementIcon) && css.hasIcon,
@@ -262,10 +263,15 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
262
263
  )}
263
264
  >
264
265
  {elementIcon}
265
- <div className={clsx(css.elementTextData, 'likec4-element-main-props')}>
266
- <Text className={clsx(css.title, 'likec4-element-title')} lineClamp={3}>
266
+ <Box className={clsx(css.elementTextData, 'likec4-element-main-props')}>
267
+ <Text
268
+ component={m.div}
269
+ key={`${viewId}:element:title:${id}`}
270
+ layoutId={`${viewId}:element:title:${id}`}
271
+ className={clsx(css.title, 'likec4-element-title')}>
267
272
  {element.title}
268
273
  </Text>
274
+
269
275
  {element.technology && (
270
276
  <Text className={clsx(css.technology, 'likec4-element-technology')}>
271
277
  {element.technology}
@@ -276,80 +282,106 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
276
282
  {element.description}
277
283
  </Text>
278
284
  )}
279
- </div>
280
- </div>
281
- {isHovercards && element.links && <ElementLink element={element} />}
282
- {isNavigable && (
283
- <ActionIcon
284
- component={m.div}
285
- variants={VariantsNavigate}
286
- data-animate-target="navigate"
287
- className={clsx('nodrag nopan', css.navigateBtn)}
288
- radius="md"
289
- style={{ zIndex: 100 }}
290
- role="button"
291
- onClick={onNavigateTo}
292
- onDoubleClick={stopPropagation}
293
- {...isInteractive && animateHandlers}
294
- >
295
- <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
296
- </ActionIcon>
285
+ </Box>
286
+ </Box>
287
+ {/* {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>
297
322
  )}
298
- {enableRelationshipsMode && (
299
- <Tooltip fz="xs" color="dark" label="Browse relationships" withinPortal={false} openDelay={600}>
323
+ {enableElementDetails && (
324
+ <Tooltip
325
+ fz="xs"
326
+ color="dark"
327
+ label="Open details"
328
+ withinPortal={false}
329
+ offset={2}
330
+ openDelay={600}>
300
331
  <ActionIcon
301
332
  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)}
333
+ variants={VariantsDetailsBtn}
334
+ data-animate-target="details"
335
+ className={clsx('nodrag nopan', css.detailsBtn)}
306
336
  radius="md"
307
- style={{
308
- left: isNavigable ? 'calc(50% + 28px)' : '50%',
309
- zIndex: 99
310
- }}
337
+ style={{ zIndex: 100 }}
311
338
  role="button"
312
- onClick={onBrowseRelations}
339
+ onClick={onOpenDetails}
313
340
  onDoubleClick={stopPropagation}
314
341
  {...isInteractive && animateHandlers}
315
342
  >
316
- <IconTransform stroke={1.8} style={{ width: '67%' }} />
343
+ <IconId stroke={1.8} style={{ width: '75%' }} />
317
344
  </ActionIcon>
318
345
  </Tooltip>
319
346
  )}
320
- </m.div>
347
+ </Box>
321
348
  <Handle type="target" position={Position.Top} className={css.handleCenter} />
322
349
  <Handle type="source" position={Position.Top} className={css.handleCenter} />
323
350
  </>
324
351
  )
325
352
  }, isEqualProps)
326
353
 
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
- }
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
+ // }
370
+
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
+ // }
@@ -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,45 @@ 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 navigateBtnBox = style({
358
+ zIndex: 100,
359
+ position: 'absolute',
360
+ left: '50%',
361
+ bottom: 2,
362
+ transform: 'translate(-50%, 0%)',
363
+ gap: 0,
356
364
  selectors: {
357
365
  [`:where([data-likec4-shape='browser']) &`]: {
358
366
  bottom: 4
359
367
  }
360
- },
361
- ':hover': {
362
- boxShadow: mantine.shadows.md
363
368
  }
364
369
  })
370
+ globalStyle(`:where(.react-flow__node:hover) ${navigateBtnBox}`, {
371
+ transitionDelay: '20ms',
372
+ gap: 16
373
+ })
374
+
375
+ export const navigateBtn = style([btn, {}])
376
+
377
+ export const detailsBtn = style([btn, {
378
+ position: 'absolute',
379
+ top: 2,
380
+ right: 2,
381
+ selectors: {
382
+ [`:where([data-likec4-shape='browser']) &`]: {
383
+ right: 5
384
+ },
385
+ ':where([data-likec4-shape="cylinder"], [data-likec4-shape="storage"]) &': {
386
+ top: 14
387
+ },
388
+ ':where([data-likec4-shape="queue"]) &': {
389
+ top: 1,
390
+ right: 12
391
+ }
392
+ }
393
+ }])
@@ -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
  }