@likec4/diagram 1.12.2 → 1.13.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 +8 -8
  2. package/src/LikeC4CustomColors.tsx +2 -2
  3. package/src/LikeC4Diagram.props.ts +15 -7
  4. package/src/LikeC4Diagram.tsx +23 -14
  5. package/src/StaticLikeC4Diagram.tsx +2 -0
  6. package/src/components/Overlay.css.ts +78 -0
  7. package/src/components/Overlay.tsx +102 -0
  8. package/src/components/OverlayContext.tsx +11 -0
  9. package/src/components/index.ts +2 -0
  10. package/src/likec4model/index.ts +1 -0
  11. package/src/likec4model/useLikeC4ElementsTree.ts +44 -0
  12. package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +13 -0
  13. package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +37 -0
  14. package/src/overlays/index.tsx +33 -0
  15. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +33 -0
  16. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +359 -0
  17. package/src/overlays/relationships-of/SelectElement.css.ts +18 -0
  18. package/src/overlays/relationships-of/SelectElement.tsx +133 -0
  19. package/src/overlays/relationships-of/_types.ts +56 -0
  20. package/src/overlays/relationships-of/styles.css.ts +34 -0
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +607 -0
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +76 -0
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +130 -0
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +22 -0
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +59 -0
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +170 -0
  27. package/src/state/diagram-to-xyflow.ts +1 -1
  28. package/src/state/diagramStore.ts +119 -27
  29. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +1 -1
  30. package/src/ui/top-left/ManualLayoutToolsButton.tsx +110 -0
  31. package/src/ui/top-left/TopLeftPanel.tsx +15 -31
  32. package/src/ui/top-left/_shared.ts +2 -2
  33. package/src/xyflow/EdgeMarkers.tsx +19 -0
  34. package/src/xyflow/XYFlowEvents.tsx +21 -24
  35. package/src/xyflow/XYFlowInner.tsx +6 -10
  36. package/src/xyflow/edges/EdgeLabel.tsx +6 -5
  37. package/src/xyflow/edges/RelationshipEdge.tsx +4 -3
  38. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +16 -0
  39. package/src/xyflow/edges/edges.css.ts +5 -11
  40. package/src/xyflow/nodes/compound/CompoundNode.css.ts +17 -40
  41. package/src/xyflow/nodes/compound/CompoundNode.tsx +219 -28
  42. package/src/xyflow/nodes/element/ElementLink.tsx +11 -8
  43. package/src/xyflow/nodes/element/ElementNode.tsx +211 -140
  44. package/src/xyflow/nodes/element/element.css.ts +22 -33
  45. package/src/xyflow/nodes/shared/Toolbar.tsx +81 -27
  46. package/src/xyflow/nodes/use-animate-variants.tsx +64 -0
  47. package/src/xyflow/useLayoutConstraints.ts +95 -125
  48. package/src/xyflow/utils.ts +4 -0
  49. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +0 -32
@@ -1,41 +1,140 @@
1
1
  import type { ThemeColor } from '@likec4/core'
2
- import { Box, Text } from '@mantine/core'
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
5
  import { assignInlineVars } from '@vanilla-extract/dynamic'
5
6
  import { Handle, type NodeProps, Position } from '@xyflow/react'
6
7
  import clsx from 'clsx'
7
8
  import { deepEqual as eq } from 'fast-equals'
8
- import { memo, useState } from 'react'
9
- import { clamp } from 'remeda'
9
+ import { m, type Variants } from 'framer-motion'
10
+ import { memo, useCallback, useState } from 'react'
11
+ import { clamp, isNumber } from 'remeda'
10
12
  import { useDiagramState } from '../../../hooks/useDiagramState'
11
13
  import type { CompoundXYFlowNode } from '../../types'
12
- import { NavigateToBtn } from '../shared/NavigateToBtn'
14
+ import { stopPropagation } from '../../utils'
13
15
  import { CompoundToolbar } from '../shared/Toolbar'
16
+ import { useFramerAnimateVariants } from '../use-animate-variants'
14
17
  import * as css from './CompoundNode.css'
15
18
 
16
19
  type CompoundNodeProps = Pick<
17
20
  NodeProps<CompoundXYFlowNode>,
18
- 'id' | 'data' | 'width' | 'height' | 'selected'
21
+ 'id' | 'data' | 'selected' | 'dragging'
19
22
  >
20
23
 
21
24
  const isEqualProps = (prev: CompoundNodeProps, next: CompoundNodeProps) => (
22
25
  prev.id === next.id
23
26
  && eq(prev.selected ?? false, next.selected ?? false)
24
- // && eq(prev.width, next.width)
25
- // && eq(prev.height, next.height)
26
- && eq(prev.data.element, next.data.element)
27
+ && eq(prev.dragging ?? false, next.dragging ?? false)
28
+ && eq(prev.data, next.data)
27
29
  )
28
30
 
31
+ 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
+ }),
44
+ 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
+ }),
56
+ tap: {}
57
+ } satisfies Variants
58
+
59
+ const VariantsNavigate = {
60
+ idle: {
61
+ '--ai-bg': 'var(--ai-bg-idle)',
62
+ scale: 1,
63
+ opacity: 0.8,
64
+ translateX: 0,
65
+ translateY: 0
66
+ },
67
+ hovered: {
68
+ display: 'block',
69
+ '--ai-bg': 'var(--ai-bg-hover)',
70
+ scale: 1.35,
71
+ opacity: 1,
72
+ translateX: -6,
73
+ translateY: 2
74
+ },
75
+ 'hovered:navigate': {
76
+ scale: 1.4
77
+ },
78
+ 'hovered:relations': {},
79
+ 'tap:navigate': {
80
+ scale: 1.15
81
+ }
82
+ } satisfies Variants
83
+
84
+ const VariantsRelationsBtn = {
85
+ idle: {
86
+ '--ai-bg': 'var(--ai-bg-idle)',
87
+ scale: 1,
88
+ opacity: 0,
89
+ translateX: 2,
90
+ translateY: -8,
91
+ transitionEnd: {
92
+ display: 'none'
93
+ }
94
+ },
95
+ hovered: {
96
+ display: 'block',
97
+ '--ai-bg': 'var(--ai-bg-hover)',
98
+ scale: 1.2,
99
+ opacity: 1,
100
+ translateX: -5,
101
+ translateY: 2
102
+ },
103
+ 'hovered:relations': {
104
+ scale: 1.42
105
+ },
106
+ 'tap:relations': {
107
+ scale: 1.15
108
+ }
109
+ } satisfies Variants
110
+
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'
121
+ }
122
+ }
123
+ }
124
+
29
125
  export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function CompoundNode({
30
126
  id,
31
127
  selected = false,
128
+ dragging = false,
32
129
  data: {
33
- element
130
+ element: {
131
+ color,
132
+ style,
133
+ depth = 1,
134
+ ...element
135
+ }
34
136
  }
35
137
  }) {
36
- const { color, style, depth = 1, ...compound } = element
37
- // const w = toDomPrecision(width ?? compound.width)
38
- // const h = toDomPrecision(height ?? compound.height)
39
138
  const opacity = clamp((style.opacity ?? 100) / 100, {
40
139
  min: 0,
41
140
  max: 1
@@ -45,19 +144,63 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
45
144
  max: 50
46
145
  })
47
146
 
48
- const { isEditable, isHovered, isDimmed, hasOnNavigateTo } = useDiagramState(s => ({
147
+ const {
148
+ triggerOnNavigateTo,
149
+ openOverlay,
150
+ isEditable,
151
+ isHovered,
152
+ isDimmed,
153
+ isInteractive,
154
+ hasOnNavigateTo,
155
+ enableRelationshipsMode
156
+ } = useDiagramState(s => ({
157
+ triggerOnNavigateTo: s.triggerOnNavigateTo,
158
+ openOverlay: s.openOverlay,
49
159
  isEditable: s.readonly !== true,
50
160
  isHovered: s.hoveredNodeId === id,
51
161
  isDimmed: s.dimmed.has(id),
52
- hasOnNavigateTo: !!s.onNavigateTo
162
+ isInteractive: s.nodesDraggable || s.nodesSelectable || s.enableRelationshipsBrowser
163
+ || (!!s.onNavigateTo && !!element.navigateTo),
164
+ hasOnNavigateTo: !!s.onNavigateTo,
165
+ enableRelationshipsMode: s.enableRelationshipsBrowser
53
166
  }))
54
- const isnavigable = !!compound.navigateTo && hasOnNavigateTo
167
+ const isNavigable = !!element.navigateTo && hasOnNavigateTo
55
168
 
56
169
  const _isToolbarVisible = isEditable && (isHovered || (import.meta.env.DEV && selected))
57
170
  const [isToolbarVisible] = useDebouncedValue(_isToolbarVisible, _isToolbarVisible ? 500 : 300)
58
171
 
172
+ const [animateVariants, animateHandlers] = useFramerAnimateVariants()
173
+
174
+ let animate: keyof typeof VariantsRoot
175
+ switch (true) {
176
+ case dragging && selected:
177
+ animate = 'selected'
178
+ break
179
+ case dragging:
180
+ animate = 'idle'
181
+ break
182
+ case isInteractive && isHovered:
183
+ animate = 'hovered'
184
+ break
185
+ case selected:
186
+ animate = 'selected'
187
+ break
188
+ default:
189
+ animate = 'idle'
190
+ }
191
+
59
192
  const [previewColor, setPreviewColor] = useState<ThemeColor | null>(null)
60
193
 
194
+ const onNavigateTo = useCallback((e: React.MouseEvent) => {
195
+ e.stopPropagation()
196
+ triggerOnNavigateTo(id, e)
197
+ }, [triggerOnNavigateTo, id])
198
+
199
+ const onBrowseRelations = useCallback((e: React.MouseEvent) => {
200
+ e.stopPropagation()
201
+ openOverlay({ relationshipsOf: element.id })
202
+ }, [openOverlay, element.id])
203
+
61
204
  return (
62
205
  <>
63
206
  {isToolbarVisible && (
@@ -67,12 +210,11 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
67
210
  align="start"
68
211
  onColorPreview={setPreviewColor} />
69
212
  )}
70
- <Handle
71
- type="target"
72
- position={Position.Top}
73
- className={css.nodeHandlerInCenter}
74
- />
75
213
  <Box
214
+ component={m.div}
215
+ variants={VariantsRoot}
216
+ initial={false}
217
+ animate={(isHovered && !dragging) ? (animateVariants ?? animate) : animate}
76
218
  className={clsx(
77
219
  css.container,
78
220
  'likec4-compound-node',
@@ -80,10 +222,17 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
80
222
  isDimmed && css.dimmed
81
223
  )}
82
224
  mod={{
225
+ 'animate-target': '',
83
226
  'compound-depth': depth,
84
227
  'likec4-color': previewColor ?? color,
85
228
  hovered: isHovered
86
229
  }}
230
+ tabIndex={-1}
231
+ {...(isInteractive && {
232
+ onTapStart: animateHandlers.onTapStart,
233
+ onTap: animateHandlers.onTap,
234
+ onTapCancel: animateHandlers.onTapCancel
235
+ })}
87
236
  >
88
237
  <svg className={css.indicator}>
89
238
  <rect
@@ -114,19 +263,61 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
114
263
  component="div"
115
264
  className={clsx(
116
265
  css.title,
117
- isnavigable && css.titleWithNavigation,
266
+ (isNavigable || enableRelationshipsMode) && css.titleWithNavigation,
118
267
  'likec4-compound-title'
119
268
  )}>
120
- {compound.title}
269
+ {element.title}
121
270
  </Text>
122
271
  </div>
123
- {isnavigable && <NavigateToBtn xynodeId={id} className={css.navigateBtn} />}
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}
285
+ >
286
+ <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
287
+ </ActionIcon>
288
+ )}
289
+ {enableRelationshipsMode && (
290
+ <Tooltip
291
+ fz="xs"
292
+ color="dark"
293
+ label="Browse relationships"
294
+ withinPortal={false}
295
+ openDelay={600}
296
+ position="right">
297
+ <ActionIcon
298
+ 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'}
303
+ className={clsx('nodrag nopan', css.navigateBtn)}
304
+ radius="md"
305
+ style={{
306
+ top: isNavigable ? 50 : undefined,
307
+ zIndex: 99
308
+ }}
309
+ role="button"
310
+ onClick={onBrowseRelations}
311
+ onDoubleClick={stopPropagation}
312
+ {...isInteractive && animateHandlers}
313
+ >
314
+ <IconTransform stroke={1.8} style={{ width: '66%' }} />
315
+ </ActionIcon>
316
+ </Tooltip>
317
+ )}
124
318
  </Box>
125
- <Handle
126
- type="source"
127
- position={Position.Top}
128
- className={css.nodeHandlerInCenter}
129
- />
319
+ <Handle type="target" position={Position.Top} className={css.nodeHandlerInCenter} />
320
+ <Handle type="source" position={Position.Top} className={css.nodeHandlerInCenter} />
130
321
  </>
131
322
  )
132
323
  }, isEqualProps)
@@ -24,6 +24,8 @@ type ElementLinkProps = {
24
24
  element: XYFlowNode['data']['element']
25
25
  }
26
26
 
27
+ const stopEventPropagation = (e: React.MouseEvent) => e.stopPropagation()
28
+
27
29
  export function ElementLink({
28
30
  element
29
31
  }: ElementLinkProps) {
@@ -31,7 +33,12 @@ export function ElementLink({
31
33
  const id = useId()
32
34
  const portalProps = useMantinePortalProps()
33
35
  return (
34
- <div className={elementLink} data-likec4-linkid={id}>
36
+ <div
37
+ className={elementLink}
38
+ data-likec4-linkid={id}
39
+ onPointerDownCapture={stopEventPropagation}
40
+ onDoubleClick={stopEventPropagation}
41
+ onClick={stopEventPropagation}>
35
42
  <HoverCard
36
43
  position="bottom-start"
37
44
  shadow="lg"
@@ -59,10 +66,7 @@ export function ElementLink({
59
66
  <span>links</span>
60
67
  </UnstyledButton>
61
68
  </HoverCardTarget>
62
- <HoverCardDropdown
63
- onPointerDownCapture={e => e.stopPropagation()}
64
- onClick={e => e.stopPropagation()}
65
- p={'xs'}>
69
+ <HoverCardDropdown p={'xs'}>
66
70
  <Stack gap={'xs'}>
67
71
  {element.links.map((link) => (
68
72
  <Group key={link.url} wrap="nowrap" gap={'sm'}>
@@ -78,9 +82,8 @@ export function ElementLink({
78
82
  fz={'10'}
79
83
  variant="light"
80
84
  onClick={copy}
81
- color={copied
82
- ? 'teal'
83
- : 'gray'}>
85
+ color={copied ? 'teal' : 'gray'}
86
+ >
84
87
  {copied ? 'copied' : 'copy'}
85
88
  </Button>
86
89
  )}