@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,7 +1,7 @@
1
1
  import { type ThemeColor } from '@likec4/core'
2
2
  import { ActionIcon, Box, Text, 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 { assignInlineVars } from '@vanilla-extract/dynamic'
6
6
  import { Handle, type NodeProps, Position } from '@xyflow/react'
7
7
  import clsx from 'clsx'
@@ -12,6 +12,7 @@ import { clamp, isNumber } from 'remeda'
12
12
  import { useDiagramState } from '../../../hooks/useDiagramState'
13
13
  import type { CompoundXYFlowNode } from '../../types'
14
14
  import { stopPropagation } from '../../utils'
15
+ import { ElementIcon } from '../shared/ElementIcon'
15
16
  import { CompoundToolbar } from '../shared/Toolbar'
16
17
  import { useFramerAnimateVariants } from '../use-animate-variants'
17
18
  import * as css from './CompoundNode.css'
@@ -29,30 +30,14 @@ const isEqualProps = (prev: CompoundNodeProps, next: CompoundNodeProps) => (
29
30
  )
30
31
 
31
32
  const VariantsRoot = {
32
- idle: (_, { translateY }) => ({
33
- translateX: 0,
34
- translateY: 0,
35
- transition: isNumber(translateY) && translateY < 0
36
- ? {
37
- delay: 0.09,
38
- delayChildren: 0.1,
39
- staggerChildren: 0.08,
40
- staggerDirection: -1
41
- }
42
- : {}
43
- }),
33
+ idle: {
34
+ transition: {
35
+ delayChildren: .1
36
+ // stagger
37
+ }
38
+ },
44
39
  selected: {},
45
- hovered: (_, { translateY }) => ({
46
- translateX: -1,
47
- translateY: -1,
48
- transition: !isNumber(translateY) || translateY === 0
49
- ? {
50
- delay: 0.08,
51
- delayChildren: 0.09,
52
- staggerChildren: 0.15
53
- }
54
- : {}
55
- }),
40
+ hovered: {},
56
41
  tap: {}
57
42
  } satisfies Variants
58
43
 
@@ -61,76 +46,60 @@ const VariantsNavigate = {
61
46
  '--ai-bg': 'var(--ai-bg-idle)',
62
47
  scale: 1,
63
48
  opacity: 0.8,
49
+ originX: 1,
50
+ originY: 0.25,
64
51
  translateX: 0,
65
52
  translateY: 0
66
53
  },
54
+ selected: {},
67
55
  hovered: {
68
- display: 'block',
69
56
  '--ai-bg': 'var(--ai-bg-hover)',
70
- scale: 1.35,
57
+ scale: 1.25,
71
58
  opacity: 1,
72
- translateX: -6,
73
- translateY: 2
59
+ translateX: -1
74
60
  },
75
61
  'hovered:navigate': {
76
- scale: 1.4
62
+ scale: 1.42
77
63
  },
78
64
  'hovered:relations': {},
79
65
  'tap:navigate': {
80
66
  scale: 1.15
81
67
  }
82
68
  } satisfies Variants
69
+ VariantsNavigate['selected'] = VariantsNavigate['hovered']
83
70
 
84
- const VariantsRelationsBtn = {
71
+ const VariantsDetailsBtn = {
85
72
  idle: {
86
73
  '--ai-bg': 'var(--ai-bg-idle)',
87
74
  scale: 1,
88
- opacity: 0,
89
- translateX: 2,
90
- translateY: -8,
91
- transitionEnd: {
92
- display: 'none'
93
- }
75
+ opacity: 0.3
94
76
  },
77
+ selected: {},
95
78
  hovered: {
96
- display: 'block',
97
- '--ai-bg': 'var(--ai-bg-hover)',
98
79
  scale: 1.2,
99
- opacity: 1,
100
- translateX: -5,
101
- translateY: 2
80
+ opacity: 0.6
102
81
  },
103
- 'hovered:relations': {
104
- scale: 1.42
82
+ 'hovered:details': {
83
+ scale: 1.42,
84
+ opacity: 1
105
85
  },
106
- 'tap:relations': {
86
+ 'tap:details': {
107
87
  scale: 1.15
108
88
  }
109
89
  } satisfies Variants
90
+ VariantsDetailsBtn['selected'] = VariantsDetailsBtn['hovered']
110
91
 
111
- /**
112
- * Variants for the relationships button (when Navigate button is not visible)
113
- */
114
- const VariantsRelationsBtnSingle = {
115
- ...VariantsNavigate,
116
- idle: {
117
- ...VariantsNavigate.idle,
118
- opacity: 0,
119
- transitionEnd: {
120
- display: 'none'
92
+ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>((
93
+ {
94
+ id,
95
+ selected = false,
96
+ dragging = false,
97
+ data: {
98
+ isViewGroup,
99
+ element
121
100
  }
122
101
  }
123
- }
124
-
125
- export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function CompoundNode({
126
- id,
127
- selected = false,
128
- dragging = false,
129
- data: {
130
- isViewGroup,
131
- element
132
- }
133
- }) {
102
+ ) => {
134
103
  const { depth, style, color } = element
135
104
  const isNotViewGroup = !isViewGroup
136
105
  const opacity = clamp((style.opacity ?? 100) / 100, {
@@ -143,29 +112,30 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
143
112
  })
144
113
 
145
114
  const {
115
+ viewId,
146
116
  triggerOnNavigateTo,
147
117
  openOverlay,
148
118
  isEditable,
149
119
  isHovered,
150
120
  isDimmed,
151
121
  isInteractive,
152
- hasOnNavigateTo,
153
- enableRelationshipsMode
122
+ isNavigable,
123
+ renderIcon,
124
+ enableElementDetails
154
125
  } = useDiagramState(s => ({
126
+ viewId: s.view.id,
155
127
  triggerOnNavigateTo: s.triggerOnNavigateTo,
156
128
  openOverlay: s.openOverlay,
157
129
  isEditable: s.readonly !== true,
158
130
  isHovered: s.hoveredNodeId === id,
159
131
  isDimmed: s.dimmed.has(id),
160
- isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableRelationshipsBrowser
132
+ isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableElementDetails
161
133
  || (!!s.onNavigateTo && !!element.navigateTo),
162
- hasOnNavigateTo: !!s.onNavigateTo,
163
- enableRelationshipsMode: s.enableRelationshipsBrowser
134
+ // If this is a view group, we don't want to show the navigate button
135
+ isNavigable: isNotViewGroup && !!s.onNavigateTo && !!element.navigateTo,
136
+ renderIcon: s.renderIcon,
137
+ enableElementDetails: isNotViewGroup && s.enableElementDetails
164
138
  }))
165
- // If this is a view group, we don't want to show the navigate button
166
- const isNavigable = isNotViewGroup && !!element.navigateTo && hasOnNavigateTo
167
- const hasRelationshipsBrowser = isNotViewGroup && enableRelationshipsMode
168
-
169
139
  const _isToolbarVisible = isNotViewGroup && isEditable && (isHovered || (import.meta.env.DEV && selected))
170
140
  const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
171
141
 
@@ -196,125 +166,139 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
196
166
  triggerOnNavigateTo(id, e)
197
167
  }, [triggerOnNavigateTo, id])
198
168
 
199
- const onBrowseRelations = useCallback((e: React.MouseEvent) => {
169
+ const onOpenDetails = useCallback((e: React.MouseEvent) => {
200
170
  e.stopPropagation()
201
- openOverlay({ relationshipsOf: element.id })
171
+ openOverlay({ elementDetails: element.id })
202
172
  }, [openOverlay, element.id])
203
173
 
174
+ const elementIcon = ElementIcon({
175
+ element,
176
+ viewId,
177
+ className: css.elementIcon,
178
+ renderIcon
179
+ })
180
+
204
181
  return (
205
182
  <>
206
183
  {isToolbarVisible && (
207
184
  <CompoundToolbar
208
- isVisible={isToolbarVisible}
185
+ isVisible
209
186
  element={element}
210
187
  align="start"
211
188
  onColorPreview={setPreviewColor} />
212
189
  )}
213
190
  <Box
214
191
  component={m.div}
215
- variants={VariantsRoot}
216
- initial={false}
217
- animate={(isHovered && !dragging) ? (animateVariants ?? animate) : animate}
218
- className={clsx(
219
- css.container,
220
- 'likec4-compound-node',
221
- opacity < 1 && 'likec4-compound-transparent',
222
- isDimmed && css.dimmed
223
- )}
224
- mod={{
225
- 'animate-target': '',
226
- 'compound-depth': depth,
227
- 'likec4-color': previewColor ?? color,
228
- hovered: isHovered
229
- }}
230
- tabIndex={-1}
231
- {...(isInteractive && {
232
- onTapStart: animateHandlers.onTapStart,
233
- onTap: animateHandlers.onTap,
234
- onTapCancel: animateHandlers.onTapCancel
235
- })}
236
- >
237
- <svg className={css.indicator}>
238
- <rect
239
- x={0}
240
- y={0}
241
- width={'100%'}
242
- height={'100%'}
243
- rx={6}
244
- />
245
- </svg>
246
- <div
192
+ key={`${viewId}:element:${id}`}
193
+ layoutId={`${viewId}:element:${id}`}
194
+ className={css.containerForFramer}>
195
+ <Box
196
+ component={m.div}
197
+ variants={VariantsRoot}
198
+ initial={false}
199
+ animate={(isHovered && !dragging) ? (animateVariants ?? animate) : animate}
247
200
  className={clsx(
248
- css.compoundBody,
249
- opacity < 1 && css.transparent,
250
- 'likec4-compound'
201
+ css.container,
202
+ 'likec4-compound-node',
203
+ opacity < 1 && 'likec4-compound-transparent',
204
+ isDimmed && css.dimmed
251
205
  )}
252
- style={{
253
- ...(opacity < 1 && {
254
- ...assignInlineVars({
255
- [css.varBorderTransparency]: `${borderTransparency}%`,
256
- [css.varOpacity]: opacity.toFixed(2)
257
- }),
258
- borderStyle: style.border ?? 'dashed'
259
- })
206
+ mod={{
207
+ 'animate-target': '',
208
+ 'compound-depth': depth,
209
+ 'likec4-color': previewColor ?? color,
210
+ hovered: isHovered
260
211
  }}
212
+ tabIndex={-1}
213
+ {...(isInteractive && {
214
+ onTapStart: animateHandlers.onTapStart,
215
+ onTap: animateHandlers.onTap,
216
+ onTapCancel: animateHandlers.onTapCancel
217
+ })}
261
218
  >
262
- <Text
263
- component="div"
219
+ <svg className={css.indicator}>
220
+ <rect
221
+ x={0}
222
+ y={0}
223
+ width={'100%'}
224
+ height={'100%'}
225
+ rx={6}
226
+ />
227
+ </svg>
228
+ <Box
264
229
  className={clsx(
265
- css.title,
266
- (isNavigable || hasRelationshipsBrowser) && css.titleWithNavigation,
267
- 'likec4-compound-title'
268
- )}>
269
- {element.title}
270
- </Text>
271
- </div>
272
- {isNavigable && (
273
- <ActionIcon
274
- key={'navigate'}
275
- component={m.div}
276
- variants={VariantsNavigate}
277
- data-animate-target="navigate"
278
- className={clsx('nodrag nopan', css.navigateBtn)}
279
- radius="md"
280
- style={{ zIndex: 100 }}
281
- onClick={onNavigateTo}
282
- role="button"
283
- onDoubleClick={stopPropagation}
284
- {...isInteractive && animateHandlers}
230
+ css.compoundBody,
231
+ opacity < 1 && css.transparent,
232
+ 'likec4-compound'
233
+ )}
234
+ style={{
235
+ ...(opacity < 1 && {
236
+ ...assignInlineVars({
237
+ [css.varBorderTransparency]: `${borderTransparency}%`,
238
+ [css.varOpacity]: opacity.toFixed(2)
239
+ }),
240
+ borderStyle: style.border ?? 'dashed'
241
+ })
242
+ }}
285
243
  >
286
- <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
287
- </ActionIcon>
288
- )}
289
- {hasRelationshipsBrowser && (
290
- <Tooltip
291
- fz="xs"
292
- color="dark"
293
- label="Browse relationships"
294
- withinPortal={false}
295
- openDelay={600}
296
- position="right">
244
+ <Box
245
+ className={clsx(
246
+ css.compoundTitle,
247
+ isNavigable && css.withNavigation,
248
+ 'likec4-compound-title'
249
+ )}>
250
+ {elementIcon}
251
+ <Text
252
+ component={m.div}
253
+ key={`${viewId}:element:title:${id}`}
254
+ layoutId={`${viewId}:element:title:${id}`}
255
+ className={css.title}>
256
+ {element.title}
257
+ </Text>
258
+ {enableElementDetails && (
259
+ <Tooltip
260
+ fz="xs"
261
+ color="dark"
262
+ label="Open details"
263
+ withinPortal={false}
264
+ offset={2}
265
+ openDelay={600}>
266
+ <ActionIcon
267
+ component={m.div}
268
+ variants={VariantsDetailsBtn}
269
+ data-animate-target="details"
270
+ className={clsx('nodrag nopan', css.detailsBtn)}
271
+ radius="md"
272
+ style={{ zIndex: 100 }}
273
+ role="button"
274
+ onClick={onOpenDetails}
275
+ onDoubleClick={stopPropagation}
276
+ {...isInteractive && animateHandlers}
277
+ >
278
+ <IconId stroke={1.8} style={{ width: '75%' }} />
279
+ </ActionIcon>
280
+ </Tooltip>
281
+ )}
282
+ </Box>
283
+ </Box>
284
+ {isNavigable && (
297
285
  <ActionIcon
286
+ key={'navigate'}
298
287
  component={m.div}
299
- // Is is a second button, so we need to use a different variant
300
- key={isNavigable ? 'relations' : 'relations-as-navigate'}
301
- variants={isNavigable ? VariantsRelationsBtn : VariantsRelationsBtnSingle}
302
- data-animate-target={isNavigable ? 'relations' : 'navigate'}
288
+ variants={VariantsNavigate}
289
+ data-animate-target="navigate"
303
290
  className={clsx('nodrag nopan', css.navigateBtn)}
304
291
  radius="md"
305
- style={{
306
- top: isNavigable ? 50 : undefined,
307
- zIndex: 99
308
- }}
292
+ style={{ zIndex: 100 }}
293
+ onClick={onNavigateTo}
309
294
  role="button"
310
- onClick={onBrowseRelations}
311
295
  onDoubleClick={stopPropagation}
312
296
  {...isInteractive && animateHandlers}
313
297
  >
314
- <IconTransform stroke={1.8} style={{ width: '66%' }} />
298
+ <IconZoomScan style={{ width: '75%' }} />
315
299
  </ActionIcon>
316
- </Tooltip>
317
- )}
300
+ )}
301
+ </Box>
318
302
  </Box>
319
303
  <Handle type="target" position={Position.Top} className={css.nodeHandlerInCenter} />
320
304
  <Handle type="source" position={Position.Top} className={css.nodeHandlerInCenter} />
@@ -0,0 +1,101 @@
1
+ import { createVar, globalStyle, style } from '@vanilla-extract/css'
2
+ import { mantine, vars, whereDark, whereLight } from '../../../theme-vars'
3
+
4
+ const bg = createVar('bg')
5
+
6
+ export const container = style({
7
+ backgroundColor: mantine.colors.body,
8
+ pointerEvents: 'all',
9
+ position: 'absolute',
10
+ top: 0,
11
+ left: 0,
12
+ zIndex: 100,
13
+ vars: {
14
+ [bg]: `linear-gradient(180deg, ${vars.element.fill}, ${vars.element.fill} 3px, transparent 3px)`
15
+ },
16
+ backgroundImage: bg,
17
+ selectors: {
18
+ [`${whereDark} &`]: {
19
+ backgroundColor: mantine.colors.dark[6],
20
+ vars: {
21
+ [bg]: `
22
+ linear-gradient(180deg, color-mix(in srgb, ${vars.element.fill} 10%, transparent), transparent 60px),
23
+ linear-gradient(180deg, ${vars.element.fill}, ${vars.element.fill} 3px, transparent 3px)
24
+ `
25
+ }
26
+ }
27
+ }
28
+ })
29
+
30
+ export const noPointerEvents = style({
31
+ pointerEvents: 'none'
32
+ })
33
+
34
+ export const fqn = style({
35
+ display: 'inline-block',
36
+ fontSize: mantine.fontSizes.xs,
37
+ fontWeight: 500,
38
+ whiteSpace: 'nowrap',
39
+ padding: '3px 5px',
40
+ borderRadius: 2,
41
+ background: `color-mix(in srgb , ${vars.element.fill}, transparent 25%)`,
42
+ lineHeight: 1.2,
43
+ color: vars.element.hiContrast,
44
+ selectors: {
45
+ [`${whereDark} &`]: {
46
+ // background: `color-mix(in srgb , ${vars.element.fill}, transparent 45%)`,
47
+ // color: vars.element.loContrast
48
+ }
49
+ }
50
+ })
51
+
52
+ export const edgeNum = style({
53
+ display: 'inline-block',
54
+ fontSize: mantine.fontSizes.sm,
55
+ fontWeight: 600,
56
+ padding: '1px 5px',
57
+ minWidth: 24,
58
+ textAlign: 'center',
59
+ borderRadius: mantine.radius.sm,
60
+ background: mantine.colors.dark[7],
61
+ color: mantine.colors.defaultColor,
62
+ selectors: {
63
+ [`${whereLight} &`]: {
64
+ background: mantine.colors.gray[4],
65
+ color: mantine.colors.dark[6]
66
+ },
67
+ [`&[data-zero]`]: {
68
+ color: mantine.colors.dimmed
69
+ },
70
+ [`${whereLight} &[data-zero]`]: {
71
+ background: mantine.colors.gray[3]
72
+ },
73
+ [`&[data-missing]`]: {
74
+ color: mantine.colors.orange[4],
75
+ background: `color-mix(in srgb, ${mantine.colors.orange[8]} 20%, transparent)`
76
+ },
77
+ [`${whereLight} &[data-missing]`]: {
78
+ color: mantine.colors.orange[8]
79
+ }
80
+ }
81
+ })
82
+
83
+ export const viewButton = style({
84
+ paddingLeft: 2,
85
+ height: 'auto'
86
+ })
87
+
88
+ globalStyle(`${viewButton} .mantine-Button-label`, {
89
+ color: mantine.colors.text,
90
+ fontWeight: 500,
91
+ flexDirection: 'column',
92
+ alignItems: 'flex-start'
93
+ })
94
+ globalStyle(`${viewButton} .tabler-icon`, {
95
+ width: '75%',
96
+ opacity: 0.75
97
+ })
98
+
99
+ globalStyle(`${whereLight} ${viewButton} .mantine-Button-label`, {
100
+ color: mantine.colors.dark[3]
101
+ })