@likec4/diagram 1.17.0 → 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 (53) hide show
  1. package/package.json +24 -18
  2. package/src/LikeC4Diagram.props.ts +27 -12
  3. package/src/LikeC4Diagram.tsx +14 -9
  4. package/src/LikeC4Search.tsx +149 -0
  5. package/src/StaticLikeC4Diagram.tsx +1 -0
  6. package/src/hooks/useDiagramState.ts +8 -3
  7. package/src/hooks/useXYFlow.ts +2 -1
  8. package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
  9. package/src/likec4model/useLikeC4Model.ts +21 -11
  10. package/src/overlays/Overlays.tsx +96 -63
  11. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
  12. package/src/overlays/edge-details/_types.ts +7 -3
  13. package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
  14. package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
  15. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
  16. package/src/overlays/element-details/ElementDetailsCard.tsx +94 -81
  17. package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
  18. package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
  19. package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
  20. package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
  21. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
  22. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
  23. package/src/overlays/relationships-of/SelectElement.tsx +2 -2
  24. package/src/overlays/relationships-of/_types.ts +2 -2
  25. package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
  26. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
  27. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
  28. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
  29. package/src/state/aligners.ts +473 -0
  30. package/src/state/diagramStore.layout.ts +67 -0
  31. package/src/state/diagramStore.ts +31 -77
  32. package/src/ui/DiagramTitlePanel.tsx +2 -29
  33. package/src/ui/Link.tsx +40 -0
  34. package/src/ui/notation/NotationPanel.tsx +4 -2
  35. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
  36. package/src/ui/top-left/ManualLayoutToolsButton.tsx +15 -0
  37. package/src/ui/top-left/TopLeftPanel.tsx +16 -3
  38. package/src/xyflow/edges/EdgeLabel.tsx +4 -4
  39. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
  40. package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
  41. package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
  42. package/src/xyflow/nodes/element/ElementNode.tsx +153 -92
  43. package/src/xyflow/nodes/element/element.css.ts +53 -11
  44. package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
  45. package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
  46. package/src/xyflow/useLayoutConstraints.ts +1 -1
  47. package/src/xyflow/useXYFlowEvents.ts +18 -5
  48. package/src/ui/OptionsPanel.css.ts +0 -9
  49. package/src/ui/OptionsPanel.tsx +0 -70
  50. package/src/ui/options/EdgeOptions.tsx +0 -142
  51. package/src/ui/options/NodeOptions.tsx +0 -362
  52. package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
  53. package/src/xyflow/nodes/element/ElementDetails.tsx +0 -393
@@ -1,14 +1,14 @@
1
- import { type ThemeColor } from '@likec4/core'
1
+ import { DiagramNode, type ThemeColor } from '@likec4/core'
2
2
  import { ActionIcon, Box, Text, Tooltip } from '@mantine/core'
3
3
  import { useDebouncedValue } from '@mantine/hooks'
4
- import { IconId, IconTransform, IconZoomScan } from '@tabler/icons-react'
4
+ import { IconId, IconZoomScan } from '@tabler/icons-react'
5
5
  import { assignInlineVars } from '@vanilla-extract/dynamic'
6
6
  import { Handle, type NodeProps, Position } from '@xyflow/react'
7
7
  import clsx from 'clsx'
8
8
  import { deepEqual as eq } from 'fast-equals'
9
9
  import { m, type Variants } from 'framer-motion'
10
10
  import { memo, useCallback, useState } from 'react'
11
- import { clamp, isNumber } from 'remeda'
11
+ import { clamp } from 'remeda'
12
12
  import { useDiagramState } from '../../../hooks/useDiagramState'
13
13
  import type { CompoundXYFlowNode } from '../../types'
14
14
  import { stopPropagation } from '../../utils'
@@ -30,14 +30,28 @@ const isEqualProps = (prev: CompoundNodeProps, next: CompoundNodeProps) => (
30
30
  )
31
31
 
32
32
  const VariantsRoot = {
33
- idle: {
34
- transition: {
35
- delayChildren: .1
36
- // stagger
33
+ idle: (_, { translateZ }) => ({
34
+ // Why? translateZ is used to determine state
35
+ ...translateZ !== 0 && {
36
+ transition: {
37
+ delayChildren: .08
38
+ }
39
+ },
40
+ transitionEnd: {
41
+ translateZ: 0
37
42
  }
38
- },
43
+ }),
39
44
  selected: {},
40
- hovered: {},
45
+ hovered: (_, { translateZ }) => ({
46
+ ...translateZ !== 1 && {
47
+ transition: {
48
+ delayChildren: .08
49
+ }
50
+ },
51
+ transitionEnd: {
52
+ translateZ: 1
53
+ }
54
+ }),
41
55
  tap: {}
42
56
  } satisfies Variants
43
57
 
@@ -61,18 +75,21 @@ const VariantsNavigate = {
61
75
  'hovered:navigate': {
62
76
  scale: 1.42
63
77
  },
64
- 'hovered:relations': {},
78
+ 'hovered:details': {},
65
79
  'tap:navigate': {
66
80
  scale: 1.15
67
81
  }
68
82
  } satisfies Variants
69
- VariantsNavigate['selected'] = VariantsNavigate['hovered']
83
+ VariantsNavigate['selected'] = VariantsNavigate.hovered
84
+ VariantsNavigate['hovered:details'] = VariantsNavigate.idle
70
85
 
71
86
  const VariantsDetailsBtn = {
72
87
  idle: {
73
88
  '--ai-bg': 'var(--ai-bg-idle)',
74
89
  scale: 1,
75
- opacity: 0.3
90
+ opacity: 0.3,
91
+ originX: 0.45,
92
+ originY: 0.55
76
93
  },
77
94
  selected: {},
78
95
  hovered: {
@@ -100,15 +117,17 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
100
117
  }
101
118
  }
102
119
  ) => {
120
+ const modelRef = DiagramNode.modelRef(element)
103
121
  const { depth, style, color } = element
104
122
  const isNotViewGroup = !isViewGroup
105
123
  const opacity = clamp((style.opacity ?? 100) / 100, {
106
124
  min: 0,
107
125
  max: 1
108
126
  })
109
- const borderTransparency = clamp(50 - opacity * 50, {
127
+ const MAX_TRANSPARENCY = 40
128
+ const borderTransparency = clamp(MAX_TRANSPARENCY - opacity * MAX_TRANSPARENCY, {
110
129
  min: 0,
111
- max: 50
130
+ max: MAX_TRANSPARENCY
112
131
  })
113
132
 
114
133
  const {
@@ -121,6 +140,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
121
140
  isInteractive,
122
141
  isNavigable,
123
142
  renderIcon,
143
+ isInActiveOverlay,
124
144
  enableElementDetails
125
145
  } = useDiagramState(s => ({
126
146
  viewId: s.view.id,
@@ -134,6 +154,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
134
154
  // If this is a view group, we don't want to show the navigate button
135
155
  isNavigable: isNotViewGroup && !!s.onNavigateTo && !!element.navigateTo,
136
156
  renderIcon: s.renderIcon,
157
+ isInActiveOverlay: (s.activeOverlay?.elementDetails ?? s.activeOverlay?.relationshipsOf) === id,
137
158
  enableElementDetails: isNotViewGroup && s.enableElementDetails
138
159
  }))
139
160
  const _isToolbarVisible = isNotViewGroup && isEditable && (isHovered || (import.meta.env.DEV && selected))
@@ -143,6 +164,9 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
143
164
 
144
165
  let animate: keyof typeof VariantsRoot
145
166
  switch (true) {
167
+ case isInActiveOverlay:
168
+ animate = 'idle'
169
+ break
146
170
  case dragging && selected:
147
171
  animate = 'selected'
148
172
  break
@@ -169,7 +193,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
169
193
  const onOpenDetails = useCallback((e: React.MouseEvent) => {
170
194
  e.stopPropagation()
171
195
  openOverlay({ elementDetails: element.id })
172
- }, [openOverlay, element.id])
196
+ }, [openOverlay, element])
173
197
 
174
198
  const elementIcon = ElementIcon({
175
199
  element,
@@ -189,6 +213,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
189
213
  )}
190
214
  <Box
191
215
  component={m.div}
216
+ variants={VariantsRoot}
192
217
  key={`${viewId}:element:${id}`}
193
218
  layoutId={`${viewId}:element:${id}`}
194
219
  className={css.containerForFramer}>
@@ -196,7 +221,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
196
221
  component={m.div}
197
222
  variants={VariantsRoot}
198
223
  initial={false}
199
- animate={(isHovered && !dragging) ? (animateVariants ?? animate) : animate}
224
+ animate={(isHovered && !dragging && !isInActiveOverlay) ? (animateVariants ?? animate) : animate}
200
225
  className={clsx(
201
226
  css.container,
202
227
  'likec4-compound-node',
@@ -237,7 +262,13 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
237
262
  [css.varBorderTransparency]: `${borderTransparency}%`,
238
263
  [css.varOpacity]: opacity.toFixed(2)
239
264
  }),
240
- borderStyle: style.border ?? 'dashed'
265
+ ...style.border === 'none'
266
+ ? {
267
+ borderColor: 'transparent'
268
+ }
269
+ : {
270
+ borderStyle: style.border ?? 'dashed'
271
+ }
241
272
  })
242
273
  }}
243
274
  >
@@ -255,7 +286,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
255
286
  className={css.title}>
256
287
  {element.title}
257
288
  </Text>
258
- {enableElementDetails && (
289
+ {enableElementDetails && !!modelRef && (
259
290
  <Tooltip
260
291
  fz="xs"
261
292
  color="dark"
@@ -264,6 +295,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
264
295
  offset={2}
265
296
  openDelay={600}>
266
297
  <ActionIcon
298
+ key={`${id}details`}
267
299
  component={m.div}
268
300
  variants={VariantsDetailsBtn}
269
301
  data-animate-target="details"
@@ -283,7 +315,7 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
283
315
  </Box>
284
316
  {isNavigable && (
285
317
  <ActionIcon
286
- key={'navigate'}
318
+ key={`${id}navigate`}
287
319
  component={m.div}
288
320
  variants={VariantsNavigate}
289
321
  data-animate-target="navigate"
@@ -1,12 +1,12 @@
1
- import { type ThemeColor } from '@likec4/core'
2
- import { ActionIcon, Box, 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
- import { IconId, 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
- 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,35 +54,60 @@ const VariantsRoot = {
54
54
  }
55
55
  } satisfies Variants
56
56
 
57
- const VariantsNavigate = {
58
- idle: {
59
- '--ai-bg': 'var(--ai-bg-idle)',
60
- scale: 1,
61
- opacity: 0.5,
62
- translateX: '-50%',
63
- originY: 0.2
64
- },
65
- selected: {},
66
- hovered: {
67
- '--ai-bg': 'var(--ai-bg-hover)',
68
- translateX: '-50%',
69
- scale: 1.35,
70
- opacity: 1
71
- },
72
- 'hovered:navigate': {
73
- scale: 1.42
74
- },
75
- 'tap:navigate': {
76
- scale: 1.15
77
- }
78
- } satisfies Variants
79
- VariantsNavigate['selected'] = VariantsNavigate['hovered']
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
+ }
80
103
 
81
104
  const VariantsDetailsBtn = {
82
105
  idle: {
83
106
  '--ai-bg': 'var(--ai-bg-idle)',
84
107
  scale: 1,
85
- opacity: 0.5
108
+ opacity: 0.5,
109
+ originX: 0.45,
110
+ originY: 0.55
86
111
  },
87
112
  selected: {},
88
113
  hovered: {
@@ -90,7 +115,7 @@ const VariantsDetailsBtn = {
90
115
  opacity: 0.7
91
116
  },
92
117
  'hovered:details': {
93
- scale: 1.42,
118
+ scale: 1.44,
94
119
  opacity: 1
95
120
  },
96
121
  'tap:details': {
@@ -119,6 +144,8 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
119
144
  width,
120
145
  height
121
146
  }) {
147
+ const modelRef = DiagramNode.modelRef(element)
148
+ // const deploymentRef = DiagramNode.deploymentRef(element)
122
149
  const {
123
150
  viewId,
124
151
  isEditable,
@@ -127,6 +154,7 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
127
154
  isNavigable,
128
155
  isInteractive,
129
156
  enableElementDetails,
157
+ enableRelationshipBrowser,
130
158
  triggerOnNavigateTo,
131
159
  openOverlay,
132
160
  isInActiveOverlay,
@@ -136,10 +164,11 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
136
164
  isEditable: s.readonly !== true,
137
165
  isHovered: s.hoveredNodeId === id,
138
166
  isDimmed: s.dimmed.has(id),
139
- isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableElementDetails
167
+ isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableElementDetails || s.enableRelationshipBrowser
140
168
  || (!!s.onNavigateTo && !!element.navigateTo),
141
169
  isNavigable: (!!s.onNavigateTo && !!element.navigateTo),
142
170
  enableElementDetails: s.enableElementDetails,
171
+ enableRelationshipBrowser: s.enableRelationshipBrowser,
143
172
  triggerOnNavigateTo: s.triggerOnNavigateTo,
144
173
  openOverlay: s.openOverlay,
145
174
  isInActiveOverlay: (s.activeOverlay?.elementDetails ?? s.activeOverlay?.relationshipsOf) === id,
@@ -153,27 +182,29 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
153
182
  const w = toDomPrecision(width ?? element.width)
154
183
  const h = toDomPrecision(height ?? element.height)
155
184
 
156
- const [animateVariants, animateHandlers] = useFramerAnimateVariants()
157
-
158
- let animate: keyof typeof VariantsRoot
185
+ let animateVariant: keyof typeof VariantsRoot | string[]
159
186
  switch (true) {
160
187
  case isInActiveOverlay:
161
- animate = 'idle'
188
+ animateVariant = 'idle'
162
189
  break
163
190
  case dragging && selected:
164
- animate = 'selected'
191
+ animateVariant = 'selected'
165
192
  break
166
193
  case dragging:
167
- animate = 'idle'
194
+ animateVariant = 'idle'
168
195
  break
169
196
  case isInteractive && isHovered:
170
- animate = 'hovered'
197
+ animateVariant = 'hovered'
171
198
  break
172
199
  case selected:
173
- animate = 'selected'
200
+ animateVariant = 'selected'
174
201
  break
175
202
  default:
176
- animate = 'idle'
203
+ animateVariant = 'idle'
204
+ }
205
+ const [animateVariants, animateHandlers] = useFramerAnimateVariants()
206
+ if (isHovered && !dragging && !isInActiveOverlay) {
207
+ animateVariant = animateVariants ?? animateVariant
177
208
  }
178
209
 
179
210
  const elementIcon = ElementIcon({
@@ -195,6 +226,13 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
195
226
  openOverlay({ elementDetails: element.id })
196
227
  }, [openOverlay, element.id])
197
228
 
229
+ const onOpenRelationships = useCallback((e: React.MouseEvent) => {
230
+ if (modelRef) {
231
+ e.stopPropagation()
232
+ openOverlay({ relationshipsOf: modelRef })
233
+ }
234
+ }, [openOverlay, modelRef])
235
+
198
236
  return (
199
237
  <>
200
238
  {isToolbarVisible && (
@@ -209,7 +247,7 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
209
247
  className={clsx([
210
248
  css.container,
211
249
  isDimmed && css.dimmed,
212
- animate !== 'idle' && css.containerAnimated,
250
+ animateVariant !== 'idle' && css.containerAnimated,
213
251
  'likec4-element-node'
214
252
  ])}
215
253
  key={`${viewId}:element:${id}`}
@@ -220,7 +258,7 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
220
258
  data-animate-target=""
221
259
  initial={false}
222
260
  variants={VariantsRoot}
223
- animate={(isHovered && !dragging && !isInActiveOverlay) ? (animateVariants ?? animate) : animate}
261
+ animate={animateVariant}
224
262
  tabIndex={-1}
225
263
  {...(isInteractive && {
226
264
  onTapStart: animateHandlers.onTapStart,
@@ -266,23 +304,13 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
266
304
  </Box>
267
305
  </Box>
268
306
  {/* {isHovercards && element.links && <ElementLink element={element} />} */}
269
- {isNavigable && (
270
- <ActionIcon
271
- component={m.div}
272
- variants={VariantsNavigate}
273
- data-animate-target="navigate"
274
- className={clsx('nodrag nopan', css.navigateBtn)}
275
- radius="md"
276
- style={{ zIndex: 100 }}
277
- role="button"
278
- onClick={onNavigateTo}
279
- onDoubleClick={stopPropagation}
280
- {...isInteractive && animateHandlers}
281
- >
282
- <IconZoomScan style={{ width: '75%' }} />
283
- </ActionIcon>
284
- )}
285
- {enableElementDetails && (
307
+ <BottomButtons
308
+ keyPrefix={`${viewId}:element:${id}:`}
309
+ onNavigateTo={isNavigable && onNavigateTo}
310
+ onOpenRelationships={enableRelationshipBrowser && !!modelRef && onOpenRelationships}
311
+ {...isInteractive && animateHandlers}
312
+ />
313
+ {enableElementDetails && !!modelRef && (
286
314
  <Tooltip
287
315
  fz="xs"
288
316
  color="dark"
@@ -291,6 +319,7 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
291
319
  offset={2}
292
320
  openDelay={600}>
293
321
  <ActionIcon
322
+ key="details"
294
323
  component={m.div}
295
324
  variants={VariantsDetailsBtn}
296
325
  data-animate-target="details"
@@ -313,37 +342,69 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
313
342
  )
314
343
  }, isEqualProps)
315
344
 
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
- // }
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
360
+
361
+ if (!enableRelationships && !enableNavigate) {
362
+ return null
363
+ }
332
364
 
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
- // }
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,16 +405,7 @@ const btn = style({
354
405
  }
355
406
  })
356
407
 
357
- export const navigateBtn = style([btn, {
358
- position: 'absolute',
359
- left: '50%',
360
- bottom: 0,
361
- selectors: {
362
- [`:where([data-likec4-shape='browser']) &`]: {
363
- bottom: 4
364
- }
365
- }
366
- }])
408
+ export const navigateBtn = style([btn, {}])
367
409
 
368
410
  export const detailsBtn = style([btn, {
369
411
  position: 'absolute',
@@ -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