@likec4/diagram 1.13.0 → 1.15.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 (43) hide show
  1. package/package.json +9 -9
  2. package/src/LikeC4CustomColors.tsx +6 -3
  3. package/src/LikeC4Diagram.css.ts +4 -0
  4. package/src/LikeC4Diagram.tsx +9 -7
  5. package/src/overlays/Overlays.css.ts +124 -0
  6. package/src/overlays/Overlays.tsx +210 -0
  7. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +247 -0
  8. package/src/overlays/edge-details/SelectEdge.css.ts +99 -0
  9. package/src/overlays/edge-details/SelectEdge.tsx +153 -0
  10. package/src/overlays/edge-details/_types.ts +38 -0
  11. package/src/overlays/edge-details/use-layouted-edge-details.ts +356 -0
  12. package/src/overlays/edge-details/xyflow/CompoundNode.tsx +80 -0
  13. package/src/overlays/edge-details/xyflow/ElementNode.tsx +154 -0
  14. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +81 -0
  15. package/src/overlays/edge-details/xyflow/styles.css.ts +196 -0
  16. package/src/overlays/index.tsx +1 -33
  17. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +124 -63
  18. package/src/overlays/relationships-of/_types.ts +13 -2
  19. package/src/overlays/relationships-of/use-layouted-relationships.ts +37 -37
  20. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +2 -2
  21. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +47 -26
  22. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +58 -22
  23. package/src/overlays/relationships-of/xyflow/styles.css.ts +53 -16
  24. package/src/state/DiagramContext.tsx +23 -5
  25. package/src/state/diagram-to-xyflow.ts +31 -20
  26. package/src/state/diagramStore.ts +30 -23
  27. package/src/xyflow/XYFlow.tsx +2 -1
  28. package/src/xyflow/XYFlowEvents.tsx +13 -1
  29. package/src/xyflow/edges/RelationshipEdge.tsx +10 -4
  30. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +24 -21
  31. package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -0
  32. package/src/xyflow/nodes/compound/CompoundNode.tsx +11 -11
  33. package/src/xyflow/nodes/shared/Toolbar.tsx +1 -1
  34. package/src/xyflow/types.ts +1 -0
  35. package/src/xyflow/useLayoutConstraints.ts +38 -19
  36. package/src/components/Overlay.css.ts +0 -78
  37. package/src/components/Overlay.tsx +0 -102
  38. package/src/components/index.ts +0 -2
  39. package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +0 -13
  40. package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +0 -37
  41. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +0 -33
  42. package/src/overlays/relationships-of/styles.css.ts +0 -34
  43. /package/src/{components → overlays}/OverlayContext.tsx +0 -0
@@ -28,7 +28,8 @@ import {
28
28
  reverse,
29
29
  sort,
30
30
  sortBy,
31
- takeWhile
31
+ takeWhile,
32
+ tap
32
33
  } from 'remeda'
33
34
  import { useDiagramState } from '../../hooks/useDiagramState'
34
35
  import { useLikeC4Model } from '../../likec4model'
@@ -50,10 +51,10 @@ const Sizes = {
50
51
  edgesep: 20
51
52
  } satisfies GraphLabel,
52
53
  edgeLabel: {
53
- width: 50
54
+ width: 60
54
55
  } satisfies Label,
55
56
 
56
- emptyNodeOffset: 100,
57
+ emptyNodeOffset: 150,
57
58
 
58
59
  nodeWidth: 270,
59
60
  hodeHeight: 160,
@@ -62,7 +63,7 @@ const Sizes = {
62
63
  // 0 means no spacer
63
64
  spacerHeight: 0,
64
65
 
65
- compoundLabelHeight: 5
66
+ compoundLabelHeight: 1
66
67
  }
67
68
 
68
69
  export const ZIndexes = {
@@ -90,10 +91,12 @@ function createGraph() {
90
91
  columns.reduce((prev, column) => {
91
92
  const c = graphColumn(column)
92
93
  g.setNode(c.id, {})
93
- g.setNode(c.anchor, { width: 50, height: 5 })
94
+ g.setNode(c.anchor, { width: 40, height: 2 })
94
95
  g.setParent(c.anchor, c.id)
95
96
  if (prev) {
96
- g.setEdge(prev, c.anchor)
97
+ g.setEdge(prev, c.anchor, {
98
+ width: 0
99
+ })
97
100
  }
98
101
  return c.anchor
99
102
  }, null as string | null)
@@ -254,9 +257,6 @@ function createNode(
254
257
 
255
258
  g.setNode(k.id, sized())
256
259
  if (xynode.type === 'compound') {
257
- g.setNode(k.id, {
258
- padding: 40
259
- })
260
260
  g.setNode(k.port, {
261
261
  width: Sizes.nodeWidth - Sizes.dagre.ranksep,
262
262
  height: Sizes.compoundLabelHeight
@@ -397,6 +397,12 @@ function layout(
397
397
  subject: LikeC4ModelElement
398
398
  nodes: XYFlowTypes.Node[]
399
399
  edges: XYFlowTypes.Edge[]
400
+ bounds: {
401
+ x: number
402
+ y: number
403
+ width: number
404
+ height: number
405
+ }
400
406
  } {
401
407
  const diagramNodes = new Map(view.nodes.map(n => [n.id, n]))
402
408
  const subjectElement = likec4model.element(subjectId)
@@ -459,13 +465,11 @@ function layout(
459
465
 
460
466
  forEachIncoming(sourceId, relations) {
461
467
  const source = createNode('incomers', 'element', likec4model.element(sourceId), ctx)
462
- g.setEdge(graphId(source).port, subject.id, {
463
- weight: source.type === 'compound' ? 0 : 1
464
- })
468
+ g.setEdge(graphId(source).port, subject.id)
465
469
 
466
- const op = source.type === 'compound' ? 'unshift' : 'push'
470
+ // const op = source.type === 'compound' ? 'unshift' : 'push'
467
471
 
468
- subject.data.ports.left[op]({
472
+ subject.data.ports.left.push({
469
473
  id: source.id,
470
474
  type: 'in'
471
475
  })
@@ -482,12 +486,10 @@ function layout(
482
486
 
483
487
  forEachOutgoing(targetId, relations) {
484
488
  const target = createNode('outgoers', 'element', likec4model.element(targetId), ctx)
485
- g.setEdge(subjectElement.id, graphId(target).port, {
486
- weight: target.type === 'compound' ? 2 : 1
487
- })
489
+ g.setEdge(subjectElement.id, graphId(target).port)
488
490
 
489
- const op = target.type === 'compound' ? 'unshift' : 'push'
490
- subject.data.ports.right[op]({
491
+ // const op = target.type === 'compound' ? 'unshift' : 'push'
492
+ subject.data.ports.right.push({
491
493
  id: target.id,
492
494
  type: 'out'
493
495
  })
@@ -548,24 +550,25 @@ function layout(
548
550
  )
549
551
  }
550
552
 
551
- // Sort ports by their position
552
553
  for (const node of xynodes) {
554
+ // Grow empty nodes to fill the space
553
555
  if (node.type === 'empty') {
554
556
  const subjectPosition = nodebounds(subject.id)
555
557
  node.position.y = subjectPosition.position.y + subjectPosition.height / 2 - node.height / 2
556
558
  if (node.data.column === 'incomers') {
557
- // Place on the left
558
- node.position.x = subjectPosition.position.x - node.width - Sizes.emptyNodeOffset
559
+ node.width = subjectPosition.position.x - Sizes.emptyNodeOffset - node.position.x
559
560
  } else {
560
- // Place on the right
561
+ const rightX = node.position.x + node.width
561
562
  node.position.x = subjectPosition.position.x + subjectPosition.width + Sizes.emptyNodeOffset
563
+ node.width = rightX - node.position.x
562
564
  }
563
565
  continue
564
566
  }
565
- if (node.data.ports.left.length > 0) {
567
+ // Sort ports by their position
568
+ if (node.data.ports.left.length > 1) {
566
569
  node.data.ports.left = sortedPorts(node.data.ports.left)
567
570
  }
568
- if (node.data.ports.right.length > 0) {
571
+ if (node.data.ports.right.length > 1) {
569
572
  node.data.ports.right = sortedPorts(node.data.ports.right)
570
573
  }
571
574
  }
@@ -575,21 +578,18 @@ function layout(
575
578
  viewIncludesSubject: diagramNodes.has(subjectId),
576
579
  subject: subjectElement,
577
580
  edges: ctx.edges,
578
- nodes: xynodes
581
+ nodes: xynodes,
582
+ bounds: {
583
+ x: 0,
584
+ y: 0,
585
+ width: g.graph().width ?? 0,
586
+ height: g.graph().height ?? 0
587
+ }
579
588
  }
580
589
  }
581
590
 
582
- export function useLayoutedRelationships(scope: 'global' | 'view') {
583
- const {
584
- subjectId,
585
- view
586
- } = useDiagramState(s => {
587
- return {
588
- subjectId: s.activeOverlay?.relationshipsOf,
589
- view: s.view
590
- }
591
- })
592
- invariant(subjectId, 'subject not found')
591
+ export function useLayoutedRelationships(subjectId: Fqn, scope: 'global' | 'view') {
592
+ const view = useDiagramState(s => s.view)
593
593
  const likec4model = useLikeC4Model(true)
594
594
  return useMemo(() =>
595
595
  layout(
@@ -27,12 +27,12 @@ export function CompoundNode({
27
27
  css.compoundNodeBody,
28
28
  'likec4-compound-node'
29
29
  ])}
30
- data-compound-depth={2}
30
+ data-compound-depth={3}
31
31
  data-likec4-color={element.color}
32
32
  animate={{
33
33
  opacity: data.dimmed ? 0.15 : 1,
34
34
  transition: {
35
- delay: data.dimmed ? .8 : 0
35
+ delay: data.dimmed === true ? .4 : 0
36
36
  }
37
37
  }}
38
38
  {...(selectable && {
@@ -1,15 +1,23 @@
1
- import { ActionIcon, Box, Text as MantineText } from '@mantine/core'
2
- import { IconZoomScan } from '@tabler/icons-react'
1
+ import { ActionIcon, Box, Group, Text as MantineText } from '@mantine/core'
2
+ import { IconFileSymlink, IconTransform, IconZoomScan } from '@tabler/icons-react'
3
3
  import { Handle, type NodeProps, Position } from '@xyflow/react'
4
4
  import clsx from 'clsx'
5
5
  import { m } from 'framer-motion'
6
- import { useOverlayDialog } from '../../../components'
7
6
  import { type DiagramState, useDiagramState } from '../../../hooks'
8
7
  import { ElementShapeSvg } from '../../../xyflow/nodes/element/ElementShapeSvg'
9
8
  import { stopPropagation } from '../../../xyflow/utils'
9
+ import { useOverlayDialog } from '../../OverlayContext'
10
10
  import type { XYFlowTypes } from '../_types'
11
11
  import * as css from './styles.css'
12
12
 
13
+ const Action = ActionIcon.withProps({
14
+ className: 'nodrag nopan ' + css.navigateBtn,
15
+ radius: 'md',
16
+ role: 'button',
17
+ onDoubleClick: stopPropagation,
18
+ onPointerDownCapture: stopPropagation
19
+ })
20
+
13
21
  const Text = MantineText.withProps({
14
22
  component: 'div'
15
23
  })
@@ -18,8 +26,9 @@ type ElementNodeProps = NodeProps<XYFlowTypes.ElementNode>
18
26
 
19
27
  function selector(s: DiagramState) {
20
28
  return {
21
- viewId: s.view.id,
22
- onNavigateTo: s.onNavigateTo
29
+ currentViewId: s.view.id,
30
+ onNavigateTo: s.onNavigateTo,
31
+ onOpenSource: s.onOpenSourceElement
23
32
  }
24
33
  }
25
34
 
@@ -36,8 +45,9 @@ export function ElementNode({
36
45
  }: ElementNodeProps) {
37
46
  const overlay = useOverlayDialog()
38
47
  const {
39
- viewId,
40
- onNavigateTo
48
+ currentViewId,
49
+ onNavigateTo,
50
+ onOpenSource
41
51
  } = useDiagramState(selector)
42
52
  return (
43
53
  <>
@@ -50,7 +60,7 @@ export function ElementNode({
50
60
  animate={{
51
61
  opacity: data.dimmed ? 0.15 : 1,
52
62
  transition: {
53
- delay: data.dimmed ? .8 : 0
63
+ delay: data.dimmed === true ? .4 : 0
54
64
  }
55
65
  }}
56
66
  {...(selectable && {
@@ -73,10 +83,7 @@ export function ElementNode({
73
83
  width={w}
74
84
  height={h}
75
85
  >
76
- <ElementShapeSvg
77
- shape={element.shape}
78
- w={w}
79
- h={h} />
86
+ <ElementShapeSvg shape={element.shape} w={w} h={h} />
80
87
  </svg>
81
88
  <Box className={css.elementNodeContent}>
82
89
  <Text className={css.elementNodeTitle} lineClamp={2}>{element.title}</Text>
@@ -84,24 +91,38 @@ export function ElementNode({
84
91
  <Text className={css.elementNodeDescription} lineClamp={4}>{element.description}</Text>
85
92
  )}
86
93
  </Box>
87
- {navigateTo && onNavigateTo && navigateTo !== viewId && (
88
- <Box className={css.navigateBtnBox}>
89
- <ActionIcon
90
- className={clsx('nodrag nopan', css.navigateBtn)}
91
- radius="md"
94
+ <Group className={css.navigateBtnBox}>
95
+ {navigateTo && onNavigateTo && navigateTo !== currentViewId && (
96
+ <Action
92
97
  onClick={(event) => {
93
98
  event.stopPropagation()
94
- setTimeout(() => onNavigateTo(navigateTo), 200)
99
+ setTimeout(() => onNavigateTo(navigateTo), 100)
95
100
  overlay.close()
96
- }}
97
- role="button"
98
- onDoubleClick={stopPropagation}
99
- onPointerDownCapture={stopPropagation}
100
- >
101
+ }}>
101
102
  <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
102
- </ActionIcon>
103
- </Box>
104
- )}
103
+ </Action>
104
+ )}
105
+ {data.column !== 'subject' && (
106
+ <Action
107
+ onClick={(event) => {
108
+ event.stopPropagation()
109
+ overlay.openOverlay({
110
+ relationshipsOf: data.fqn
111
+ })
112
+ }}>
113
+ <IconTransform stroke={1.8} style={{ width: '72%' }} />
114
+ </Action>
115
+ )}
116
+ {onOpenSource && (
117
+ <Action
118
+ onClick={(event) => {
119
+ event.stopPropagation()
120
+ onOpenSource(data.fqn)
121
+ }}>
122
+ <IconFileSymlink stroke={1.8} style={{ width: '72%' }} />
123
+ </Action>
124
+ )}
125
+ </Group>
105
126
  </m.div>
106
127
  {ports.left.map(({ id, type }, i) => (
107
128
  <Handle
@@ -1,7 +1,10 @@
1
- import { Group, Text, ThemeIcon } from '@mantine/core'
2
- import { IconBoxMultipleFilled } from '@tabler/icons-react'
1
+ import { ActionIcon, Box, Group, Stack, Text, ThemeIcon } from '@mantine/core'
2
+ import { IconBoxMultipleFilled, IconZoomScan } from '@tabler/icons-react'
3
3
  import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getBezierPath } from '@xyflow/react'
4
4
  import clsx from 'clsx'
5
+ import { only } from 'remeda'
6
+ import { useDiagramState } from '../../../hooks/useDiagramState'
7
+ import { stopPropagation } from '../../../xyflow/utils'
5
8
  import type { XYFlowTypes } from '../_types'
6
9
  import { ZIndexes } from '../use-layouted-relationships'
7
10
  import * as css from './styles.css'
@@ -11,8 +14,11 @@ export function RelationshipEdge({
11
14
  label,
12
15
  ...props
13
16
  }: EdgeProps<XYFlowTypes.Edge>) {
17
+ const onNavigateTo = useDiagramState(s => s.onNavigateTo)
14
18
  const [edgePath, labelX, labelY] = getBezierPath(props)
19
+ const navigateTo = onNavigateTo ? only(data.relations)?.navigateTo : undefined
15
20
  const isMultiRelation = data.relations.length > 1
21
+ const technology = only(data.relations)?.technology
16
22
  return (
17
23
  <>
18
24
  <g
@@ -25,35 +31,65 @@ export function RelationshipEdge({
25
31
  {...props}
26
32
  />
27
33
  </g>
28
- {label && (
29
- <EdgeLabelRenderer>
30
- <div
31
- style={{
32
- position: 'absolute',
33
- transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
34
- maxWidth: Math.abs(props.targetX - props.sourceX - 40),
35
- zIndex: ZIndexes.max
36
- }}
37
- className={clsx([
38
- css.edgeLabel,
39
- 'nodrag nopan'
40
- ])}
41
- data-edge-dimmed={data.dimmed}
42
- data-edge-hovered={data.hovered}
43
- >
34
+ <EdgeLabelRenderer>
35
+ <Stack
36
+ gap={2}
37
+ style={{
38
+ position: 'absolute',
39
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
40
+ maxWidth: Math.abs(props.targetX - props.sourceX - 40),
41
+ zIndex: ZIndexes.max
42
+ }}
43
+ className={clsx([
44
+ css.edgeLabel,
45
+ 'nodrag nopan'
46
+ ])}
47
+ data-edge-dimmed={data.dimmed}
48
+ data-edge-hovered={data.hovered}
49
+ >
50
+ {label && (
44
51
  <Group gap={6} wrap="nowrap">
45
52
  {isMultiRelation && (
46
53
  <ThemeIcon size={'sm'} variant="transparent" color="orange">
47
54
  <IconBoxMultipleFilled style={{ width: '80%' }} />
48
55
  </ThemeIcon>
49
56
  )}
50
- <Text fw={isMultiRelation ? '500' : '400'} component={'div'} className={css.edgeLabelText} lineClamp={3}>
57
+ <Text
58
+ fw={isMultiRelation ? '500' : '400'}
59
+ component={'div'}
60
+ className={css.edgeLabelText}
61
+ lineClamp={3}>
51
62
  {label}
52
63
  </Text>
53
64
  </Group>
54
- </div>
55
- </EdgeLabelRenderer>
56
- )}
65
+ )}
66
+ {technology && (
67
+ <Text component={'div'} className={css.edgeLabelTechnology}>
68
+ {'[ '}
69
+ {technology}
70
+ {' ]'}
71
+ </Text>
72
+ )}
73
+ {navigateTo && (
74
+ <Box ta={'center'} mt={4}>
75
+ <ActionIcon
76
+ variant="default"
77
+ size={'sm'}
78
+ radius="sm"
79
+ onPointerDownCapture={stopPropagation}
80
+ onClick={event => {
81
+ event.stopPropagation()
82
+ onNavigateTo?.(navigateTo, event)
83
+ }}
84
+ role="button"
85
+ onDoubleClick={stopPropagation}
86
+ >
87
+ <IconZoomScan />
88
+ </ActionIcon>
89
+ </Box>
90
+ )}
91
+ </Stack>
92
+ </EdgeLabelRenderer>
57
93
  </>
58
94
  )
59
95
  }
@@ -1,4 +1,5 @@
1
- import { style } from '@vanilla-extract/css'
1
+ import { rem } from '@mantine/core'
2
+ import { globalStyle, style } from '@vanilla-extract/css'
2
3
  import { mantine, vars, whereLight, xyvars } from '../../../theme-vars'
3
4
 
4
5
  export const elementNode = style({
@@ -13,6 +14,7 @@ export const elementNodeContent = style({
13
14
  flexDirection: 'column',
14
15
  justifyContent: 'center',
15
16
  alignItems: 'center',
17
+ overflow: 'hidden',
16
18
  gap: 6,
17
19
  padding: 16
18
20
  })
@@ -48,7 +50,7 @@ export const compoundNodeBody = style({
48
50
  width: '100%',
49
51
  height: '100%',
50
52
  boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
51
- background: `color-mix(in srgb , ${vars.element.fill}, transparent 45%)`,
53
+ background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
52
54
  borderRadius: 4
53
55
  })
54
56
 
@@ -63,13 +65,7 @@ export const compoundNodeTitle = style({
63
65
  paddingTop: 12,
64
66
  paddingLeft: 10,
65
67
  mixBlendMode: 'screen',
66
- color: vars.compound.titleColor,
67
- selectors: {
68
- [`${whereLight} &`]: {
69
- mixBlendMode: 'darken',
70
- color: vars.element.stroke
71
- }
72
- }
68
+ color: vars.compound.titleColor
73
69
  })
74
70
 
75
71
  export const cssShapeSvg = style({
@@ -88,6 +84,8 @@ export const cssShapeSvg = style({
88
84
  zIndex: -1
89
85
  })
90
86
 
87
+ const DimmedTransitionDelay = '400ms'
88
+
91
89
  export const edgePath = style({
92
90
  opacity: 1,
93
91
  transition: 'all 0.2s ease-in-out',
@@ -95,7 +93,7 @@ export const edgePath = style({
95
93
  selectors: {
96
94
  [`&[data-edge-dimmed='true']`]: {
97
95
  opacity: 0.1,
98
- transitionDelay: '800ms'
96
+ transitionDelay: DimmedTransitionDelay
99
97
  }
100
98
  }
101
99
  })
@@ -119,8 +117,7 @@ export const edgeLabel = style({
119
117
  selectors: {
120
118
  [`&[data-edge-dimmed='true']`]: {
121
119
  opacity: 0.1,
122
- // transitionDuration: '400ms',
123
- transitionDelay: '800ms'
120
+ transitionDelay: DimmedTransitionDelay
124
121
  }
125
122
  }
126
123
  })
@@ -128,10 +125,17 @@ export const edgeLabel = style({
128
125
  export const edgeLabelText = style({
129
126
  textAlign: 'center',
130
127
  whiteSpaceCollapse: 'preserve-breaks',
131
- fontSize: mantine.fontSizes.sm,
128
+ fontSize: rem(15),
132
129
  lineHeight: mantine.lineHeights.xs
133
130
  })
134
131
 
132
+ export const edgeLabelTechnology = style({
133
+ textAlign: 'center',
134
+ whiteSpaceCollapse: 'preserve-breaks',
135
+ fontSize: mantine.fontSizes.sm,
136
+ lineHeight: 1
137
+ })
138
+
135
139
  export const emptyNode = style({
136
140
  width: '100%',
137
141
  height: '100%',
@@ -148,23 +152,56 @@ export const navigateBtnBox = style({
148
152
  left: '50%',
149
153
  bottom: 2,
150
154
  transform: 'translate(-50%, 0%)',
155
+ gap: 0,
156
+ transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
151
157
  selectors: {
152
158
  [`:where([data-likec4-shape='browser']) &`]: {
153
159
  bottom: 4
154
160
  }
155
161
  }
156
162
  })
163
+ globalStyle(`:where(${elementNode}:hover) ${navigateBtnBox}`, {
164
+ transitionDelay: '20ms',
165
+ gap: 8,
166
+ transform: 'translate(-50%, 5px)'
167
+ })
168
+
157
169
  export const navigateBtn = style({
170
+ opacity: 0.7,
158
171
  pointerEvents: 'all',
159
- color: vars.element.loContrast,
160
172
  cursor: 'pointer',
161
- backgroundColor: 'var(--ai-bg)',
173
+ transform: 'scale(0.9)',
174
+ transition: 'all 190ms cubic-bezier(0.5, 0, 0.4, 1)',
162
175
  'vars': {
163
176
  '--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`,
164
177
  '--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill} 65%, ${vars.element.stroke})`,
165
178
  '--ai-hover': `color-mix(in srgb , ${vars.element.fill} 50%, ${vars.element.stroke})`
166
179
  },
167
180
  ':hover': {
168
- boxShadow: mantine.shadows.md
181
+ transitionDelay: '0ms',
182
+ transform: 'scale(1.25)',
183
+ boxShadow: mantine.shadows.lg
184
+ },
185
+ ':active': {
186
+ transform: 'scale(0.975)'
187
+ }
188
+ })
189
+
190
+ globalStyle(`:where(${elementNode}:hover) ${navigateBtn}`, {
191
+ transitionDelay: '40ms',
192
+ transitionTimingFunction: 'cubic-bezier(0, 0, 0.40, 1)',
193
+ opacity: 1,
194
+ transform: 'scale(1.07)',
195
+ boxShadow: mantine.shadows.md,
196
+ vars: {
197
+ '--ai-bg': `var(--ai-bg-hover)`
169
198
  }
170
199
  })
200
+ // globalStyle(`${elementNode}:hover ${navigateBtn}`, {
201
+ // opacity: 1,
202
+ // transform: 'scale(1.05)',
203
+ // boxShadow: mantine.shadows.md,
204
+ // vars: {
205
+ // '--ai-bg': `var(--ai-bg-hover)`
206
+ // }
207
+ // })
@@ -1,6 +1,9 @@
1
1
  import { invariant } from '@likec4/core'
2
+ import { useMergedRef } from '@mantine/hooks'
2
3
  import { deepEqual, shallowEqual } from 'fast-equals'
3
- import { createContext, type PropsWithChildren, useCallback, useRef } from 'react'
4
+ import { useAnimate } from 'framer-motion'
5
+ import { createContext, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
6
+ import { clone } from 'remeda'
4
7
  import { useUpdateEffect } from '../hooks/useUpdateEffect'
5
8
  import { useXYFlow, useXYStoreApi } from '../hooks/useXYFlow'
6
9
  import { createDiagramStore, type DiagramInitialState, type DiagramZustandStore } from './diagramStore'
@@ -22,7 +25,9 @@ export function DiagramContextProvider({
22
25
  whereFilter,
23
26
  ...props
24
27
  }: DiagramContextProviderProps) {
28
+ const [scope, animate] = useAnimate()
25
29
  const containerRef = useRef<HTMLDivElement>(null)
30
+ const ref = useMergedRef(scope, containerRef)
26
31
  const xystore = useXYStoreApi()
27
32
  const xyflow = useXYFlow()
28
33
  const store = useRef<DiagramZustandStore>()
@@ -35,7 +40,7 @@ export function DiagramContextProvider({
35
40
  xyflow,
36
41
  view,
37
42
  getContainer,
38
- whereFilter: structuredClone(whereFilter),
43
+ whereFilter: clone(whereFilter),
39
44
  ...props
40
45
  })
41
46
  }
@@ -55,7 +60,7 @@ export function DiagramContextProvider({
55
60
  const current = store.current
56
61
  invariant(current, 'DiagramContext.store.current is not defined')
57
62
  if (!deepEqual(whereFilter, current.getState().whereFilter)) {
58
- current.setState({ whereFilter: structuredClone(whereFilter) }, false, 'update where filter')
63
+ current.setState({ whereFilter: clone(whereFilter) }, false, 'update where filter')
59
64
  }
60
65
  current.getState().updateView(view)
61
66
  },
@@ -63,9 +68,22 @@ export function DiagramContextProvider({
63
68
  (a, b) => shallowEqual(a[0], b[0]) && deepEqual(a[1], b[1])
64
69
  )
65
70
 
71
+ const api = store.current
72
+ useEffect(() => {
73
+ return api.subscribe(s => !!s.activeOverlay, (isActiveOverlay) => {
74
+ animate('.likec4-diagram .react-flow__renderer', {
75
+ filter: isActiveOverlay ? 'grayscale(0.85) brightness(0.5)' : 'grayscale(0) brightness()',
76
+ scale: isActiveOverlay ? 0.97 : 1,
77
+ transform: isActiveOverlay ? `translateY(8px)` : `translateY(0)`
78
+ }, {
79
+ duration: isActiveOverlay ? 0.5 : 0.16
80
+ })
81
+ })
82
+ }, [api])
83
+
66
84
  return (
67
85
  <div
68
- ref={containerRef}
86
+ ref={ref}
69
87
  className={className}
70
88
  {...(keepAspectRatio && {
71
89
  style: {
@@ -73,7 +91,7 @@ export function DiagramContextProvider({
73
91
  maxHeight: Math.ceil(view.bounds.height)
74
92
  }
75
93
  })}>
76
- <DiagramContext.Provider value={store.current}>
94
+ <DiagramContext.Provider value={api}>
77
95
  {children}
78
96
  </DiagramContext.Provider>
79
97
  </div>