@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
@@ -16,11 +16,12 @@ import {
16
16
  Tooltip as MantineTooltip,
17
17
  TooltipGroup
18
18
  } from '@mantine/core'
19
- import { IconArrowRight, IconArrowsMaximize, IconFileSymlink, IconInfoCircle, IconZoomScan } from '@tabler/icons-react'
19
+ import { IconArrowRight, IconFileSymlink, IconInfoCircle, IconZoomScan } from '@tabler/icons-react'
20
20
  import clsx from 'clsx'
21
21
  import { forwardRef, Fragment, type MouseEventHandler, type PropsWithChildren, useCallback } from 'react'
22
22
  import { filter, isTruthy, map, partition, pick, pipe } from 'remeda'
23
23
  import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useXYNodesData } from '../../hooks'
24
+ import { Link } from '../../ui/Link'
24
25
  import type { RelationshipData } from '../types'
25
26
  import * as css from './RelationshipsDropdownMenu.css'
26
27
 
@@ -72,7 +73,7 @@ export function RelationshipsDropdownMenu({
72
73
  }
73
74
  }),
74
75
  filter(isTruthy),
75
- partition(r => r.relationship.source === edge.source && r.relationship.target === edge.target)
76
+ partition(r => r.source.id === edge.source && r.target.id === edge.target)
76
77
  )
77
78
 
78
79
  const onClickOpenOverlay = useCallback((e: React.MouseEvent): void => {
@@ -84,7 +85,7 @@ export function RelationshipsDropdownMenu({
84
85
  }
85
86
  }, [edge.id, openOverlay, enableRelationshipBrowser])
86
87
 
87
- const renderRelationship = (relationship: LikeC4Model.ViewModel.Relationship, index: number) => (
88
+ const renderRelationship = (relationship: LikeC4Model.AnyRelation, index: number) => (
88
89
  <Fragment key={relationship.id}>
89
90
  {index > 0 && <MenuDivider opacity={0.65} />}
90
91
  <MenuItem
@@ -155,7 +156,7 @@ export function RelationshipsDropdownMenu({
155
156
  const Relationship = forwardRef<
156
157
  HTMLDivElement,
157
158
  StackProps & {
158
- relationship: LikeC4Model.ViewModel.Relationship
159
+ relationship: LikeC4Model.AnyRelation
159
160
  edge: RelationshipData['edge']
160
161
  sourceNode: DiagramNode
161
162
  targetNode: DiagramNode
@@ -175,12 +176,13 @@ const Relationship = forwardRef<
175
176
  hasOnNavigateTo
176
177
  } = useDiagramState(s => ({
177
178
  viewId: s.view.id,
178
- hasOnOpenSourceRelation: !!s.onOpenSourceRelation,
179
+ hasOnOpenSourceRelation: !!s.onOpenSource,
179
180
  hasOnNavigateTo: !!s.onNavigateTo
180
181
  }))
181
182
  const sourceId = nameFromFqn(edge.source) + r.source.id.slice(edge.source.length)
182
183
  const targetId = nameFromFqn(edge.target) + r.target.id.slice(edge.target.length)
183
- const navigateTo = hasOnNavigateTo && r.relationship.navigateTo !== viewId ? r.relationship.navigateTo : undefined
184
+ const navigateTo = hasOnNavigateTo && r.navigateTo?.id !== viewId ? r.navigateTo?.id : undefined
185
+ const links = r.links
184
186
 
185
187
  return (
186
188
  <Stack ref={ref} className={clsx(css.menuItemRelationship, className)} {...props}>
@@ -223,7 +225,9 @@ const Relationship = forwardRef<
223
225
  onPointerDownCapture={stopPropagation}
224
226
  onClick={event => {
225
227
  event.stopPropagation()
226
- diagramApi.getState().onOpenSourceRelation?.(r.id)
228
+ diagramApi.getState().onOpenSource?.({
229
+ relation: r.id
230
+ })
227
231
  }}
228
232
  role="button"
229
233
  >
@@ -235,6 +239,15 @@ const Relationship = forwardRef<
235
239
  )}
236
240
  </Group>
237
241
  <Box className={css.title}>{r.title || 'untitled'}</Box>
242
+ {r.description && <Text size="xs" c="dimmed">{r.description}</Text>}
243
+ {links.length > 0 && (
244
+ <Stack
245
+ gap={3}
246
+ justify="stretch"
247
+ align="stretch">
248
+ {links.map((link) => <Link key={link.url} link={link} />)}
249
+ </Stack>
250
+ )}
238
251
  </Stack>
239
252
  )
240
253
  })
@@ -49,7 +49,20 @@ globalStyle(`.react-flow__node-compound:has(${dimmed})`, {
49
49
  willChange: 'opacity, filter'
50
50
  })
51
51
 
52
+ globalStyle(`:where([data-mantine-color-scheme='dark'] .likec4-compound-transparent)`, {
53
+ vars: {
54
+ ['--_compound-border-color']: `color-mix(in srgb, ${vars.compound.titleColor} 25%, ${vars.element.stroke})`
55
+ }
56
+ })
57
+ globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
58
+ vars: {
59
+ ['--_compound-title-color']: vars.element.stroke
60
+ }
61
+ })
62
+
52
63
  export const varOpacity = createVar('opacity')
64
+ export const varBorderColor = createVar('border-color')
65
+ export const varBorderRadius = createVar('border-transparency')
53
66
  export const varBorderTransparency = createVar('border-transparency')
54
67
 
55
68
  const outlineColor = fallbackVar(
@@ -62,14 +75,17 @@ export const compoundBody = style({
62
75
  width: '100%',
63
76
  height: '100%',
64
77
  position: 'relative',
65
- borderRadius: 6,
78
+ borderRadius: varBorderRadius,
66
79
  boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
67
80
  padding: 0,
68
81
  margin: 0,
69
82
  transition: 'all 200ms ease-out',
70
- backgroundClip: 'padding-box',
83
+ // backgroundClip: 'padding-box',
71
84
  // overflow: 'hidden',
72
85
  cursor: 'default',
86
+ vars: {
87
+ [varBorderRadius]: '6px'
88
+ },
73
89
  selectors: {
74
90
  [`:where(.react-flow__node.selected) &`]: {
75
91
  boxShadow: 'none'
@@ -88,8 +104,11 @@ export const compoundBody = style({
88
104
  width: '100%',
89
105
  height: '100%',
90
106
  pointerEvents: 'none',
107
+ border: '1px solid transparent',
108
+ borderRadius: varBorderRadius,
91
109
  transition: 'background 175ms ease-out, opacity 175ms ease-out',
92
- background: vars.element.fill
110
+ background: vars.element.fill,
111
+ backgroundClip: 'border-box'
93
112
  }
94
113
  })
95
114
  export const compoundTitle = style({
@@ -97,19 +116,20 @@ export const compoundTitle = style({
97
116
  alignItems: 'center',
98
117
  gap: 6,
99
118
  paddingLeft: 14,
100
- paddingTop: 9,
119
+ paddingTop: 6,
101
120
  minHeight: 30,
121
+ paddingRight: 2,
102
122
  selectors: {
103
123
  [`:where(.react-flow__node.draggable) &`]: {
104
124
  cursor: 'grab'
105
125
  },
106
126
  [`:where(.likec4-compound-transparent) &`]: {
107
- paddingTop: 4
127
+ paddingTop: 3
108
128
  }
109
129
  }
110
130
  })
111
131
  export const withNavigation = style({
112
- paddingLeft: 30
132
+ paddingLeft: 32
113
133
  })
114
134
 
115
135
  const opacityDeltaOnHover = createVar('opacityDeltaOnHover')
@@ -117,8 +137,9 @@ export const transparent = style({
117
137
  borderStyle: 'dashed',
118
138
  borderWidth: 3,
119
139
  boxShadow: 'none',
120
- borderColor: `color-mix(in srgb , ${vars.element.stroke}, transparent ${fallbackVar(varBorderTransparency, '10%')})`,
140
+ borderColor: `color-mix(in srgb , ${varBorderColor}, transparent ${fallbackVar(varBorderTransparency, '5%')})`,
121
141
  vars: {
142
+ [varBorderColor]: `var(--_compound-border-color,${vars.element.stroke})`,
122
143
  [opacityDeltaOnHover]: '0'
123
144
  },
124
145
  ':before': {
@@ -156,12 +177,6 @@ export const title = style({
156
177
  mixBlendMode: 'screen'
157
178
  })
158
179
 
159
- globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
160
- vars: {
161
- ['--_compound-title-color']: vars.element.stroke
162
- }
163
- })
164
-
165
180
  const indicatorKeyframes = keyframes({
166
181
  'from': {
167
182
  opacity: 0.6
@@ -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"