@likec4/diagram 1.15.1 → 1.17.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 (47) hide show
  1. package/package.json +7 -7
  2. package/src/LikeC4CustomColors.tsx +24 -21
  3. package/src/LikeC4Diagram.props.ts +9 -9
  4. package/src/LikeC4Diagram.tsx +25 -26
  5. package/src/StaticLikeC4Diagram.tsx +9 -3
  6. package/src/overlays/OverlayContext.tsx +1 -1
  7. package/src/overlays/Overlays.css.ts +28 -23
  8. package/src/overlays/Overlays.tsx +116 -64
  9. package/src/overlays/edge-details/xyflow/ElementNode.tsx +15 -12
  10. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +7 -3
  11. package/src/overlays/edge-details/xyflow/styles.css.ts +22 -16
  12. package/src/overlays/element-details/ElementDetailsCard.css.ts +322 -0
  13. package/src/overlays/element-details/ElementDetailsCard.tsx +639 -0
  14. package/src/overlays/element-details/TabPanelRelationships.css.ts +106 -0
  15. package/src/overlays/element-details/TabPanelRelationships.tsx +201 -0
  16. package/src/overlays/element-details/TabPanelStructure.css.ts +36 -0
  17. package/src/overlays/element-details/TabPanelStructure.tsx +120 -0
  18. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +185 -0
  19. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +179 -213
  20. package/src/overlays/relationships-of/_types.ts +15 -4
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +301 -207
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +68 -14
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +54 -14
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +1 -1
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +51 -23
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +34 -27
  27. package/src/state/DiagramContext.tsx +15 -8
  28. package/src/state/diagramStore.ts +14 -6
  29. package/src/theme-vars.ts +9 -0
  30. package/src/ui/FramerMotionConfig.tsx +18 -0
  31. package/src/ui/notation/NotationPanel.tsx +3 -3
  32. package/src/xyflow/XYFlow.tsx +18 -35
  33. package/src/xyflow/XYFlowInner.tsx +2 -2
  34. package/src/xyflow/edges/EdgeLabel.tsx +3 -3
  35. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +25 -18
  36. package/src/xyflow/nodes/compound/CompoundNode.css.ts +96 -30
  37. package/src/xyflow/nodes/compound/CompoundNode.tsx +149 -165
  38. package/src/xyflow/nodes/element/ElementDetails.css.ts +101 -0
  39. package/src/xyflow/nodes/element/ElementDetails.tsx +393 -0
  40. package/src/xyflow/nodes/element/ElementNode.tsx +110 -116
  41. package/src/xyflow/nodes/element/element.css.ts +38 -18
  42. package/src/xyflow/nodes/shared/ElementIcon.tsx +43 -0
  43. package/src/xyflow/nodes/shared/Toolbar.tsx +6 -6
  44. package/src/xyflow/{XYFlowEvents.tsx → useXYFlowEvents.ts} +26 -22
  45. package/src/xyflow/utils.ts +12 -3
  46. package/src/LikeC4CustomColors.props.ts +0 -5
  47. /package/src/{mantine → ui}/EnsureMantine.tsx +0 -0
@@ -1,7 +1,9 @@
1
1
  import { Text as MantineText } from '@mantine/core'
2
- import { Handle, type NodeProps, Position } from '@xyflow/react'
2
+ import { Handle, type NodeProps, Position, useStore } from '@xyflow/react'
3
3
  import clsx from 'clsx'
4
+ import { deepEqual, shallowEqual } from 'fast-equals'
4
5
  import { m } from 'framer-motion'
6
+ import { memo, useCallback } from 'react'
5
7
  import type { XYFlowTypes } from '../_types'
6
8
  import * as css from './styles.css'
7
9
 
@@ -11,15 +13,44 @@ const Text = MantineText.withProps({
11
13
 
12
14
  type CompoundNodeProps = NodeProps<XYFlowTypes.CompoundNode>
13
15
 
14
- export function CompoundNode({
16
+ export const CompoundNode = memo<CompoundNodeProps>(({
17
+ id,
15
18
  data: {
16
19
  element,
17
20
  ports,
21
+ layoutId = id,
22
+ leaving = false,
23
+ initialAnimation = true,
18
24
  ...data
19
25
  },
20
26
  width = 200,
21
- selectable = true
22
- }: CompoundNodeProps) {
27
+ height = 100,
28
+ ...props
29
+ }) => {
30
+ const scale = (diff: number) => ({
31
+ scaleX: (width + diff) / width,
32
+ scaleY: (height + diff) / height
33
+ })
34
+
35
+ let opacity = 1
36
+ if (data.dimmed) {
37
+ opacity = data.dimmed === 'immediate' ? 0.05 : 0.15
38
+ }
39
+ if (leaving) {
40
+ opacity = 0
41
+ }
42
+
43
+ const {
44
+ elementsSelectable
45
+ } = useStore(
46
+ useCallback((s) => ({
47
+ elementsSelectable: s.elementsSelectable
48
+ }), []),
49
+ shallowEqual
50
+ )
51
+
52
+ const selectable = props.selectable ?? elementsSelectable
53
+
23
54
  return (
24
55
  <>
25
56
  <m.div
@@ -27,27 +58,48 @@ export function CompoundNode({
27
58
  css.compoundNodeBody,
28
59
  'likec4-compound-node'
29
60
  ])}
30
- data-compound-depth={3}
61
+ layoutId={layoutId}
62
+ data-compound-depth={data.depth ?? 1}
31
63
  data-likec4-color={element.color}
64
+ initial={(layoutId === id && initialAnimation)
65
+ ? {
66
+ ...scale(-20),
67
+ opacity: 0,
68
+ width,
69
+ height
70
+ }
71
+ : false}
32
72
  animate={{
33
- opacity: data.dimmed ? 0.15 : 1,
73
+ ...scale(0),
74
+ opacity,
75
+ width,
76
+ height,
34
77
  transition: {
35
- delay: data.dimmed === true ? .4 : 0
78
+ opacity: {
79
+ delay: !leaving && data.dimmed === true ? .4 : 0,
80
+ ...((leaving || data.dimmed === 'immediate') && {
81
+ duration: 0.09
82
+ })
83
+ }
36
84
  }
37
85
  }}
38
86
  {...(selectable && {
39
87
  whileHover: {
40
- scale: 1.04,
88
+ ...scale(12),
89
+ scaleX: (width + 12) / width,
90
+ scaleY: (height + 12) / height,
41
91
  transition: {
42
- delay: 0.15
92
+ delay: 0.1
43
93
  }
44
94
  },
45
95
  whileTap: {
46
- scale: 1
96
+ ...scale(-12)
47
97
  }
48
98
  })}
49
99
  >
50
- <Text className={css.compoundNodeTitle} maw={width - 20}>{element.title}</Text>
100
+ <Text className={css.compoundNodeTitle} maw={width - 20}>
101
+ {element.title}
102
+ </Text>
51
103
  </m.div>
52
104
  {ports.left.map(({ id, type }, i) => (
53
105
  <Handle
@@ -57,7 +109,7 @@ export function CompoundNode({
57
109
  position={Position.Left}
58
110
  style={{
59
111
  visibility: 'hidden',
60
- top: `${16 + 20 * i}px`
112
+ top: `${20 * (i + 1)}px`
61
113
  }} />
62
114
  ))}
63
115
  {ports.right.map(({ id, type }, i) => (
@@ -68,9 +120,11 @@ export function CompoundNode({
68
120
  position={Position.Right}
69
121
  style={{
70
122
  visibility: 'hidden',
71
- top: `${16 + 20 * i}px`
123
+ top: `${20 * (i + 1)}px`
72
124
  }} />
73
125
  ))}
74
126
  </>
75
127
  )
76
- }
128
+ }, (prev, next) => {
129
+ return deepEqual(prev.data, next.data)
130
+ })
@@ -2,7 +2,9 @@ import { ActionIcon, Box, Group, Text as MantineText } from '@mantine/core'
2
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
+ import { deepEqual } from 'fast-equals'
5
6
  import { m } from 'framer-motion'
7
+ import { memo } from 'react'
6
8
  import { type DiagramState, useDiagramState } from '../../../hooks'
7
9
  import { ElementShapeSvg } from '../../../xyflow/nodes/element/ElementShapeSvg'
8
10
  import { stopPropagation } from '../../../xyflow/utils'
@@ -32,23 +34,41 @@ function selector(s: DiagramState) {
32
34
  }
33
35
  }
34
36
 
35
- export function ElementNode({
37
+ export const ElementNode = memo<ElementNodeProps>(({
38
+ id,
36
39
  data: {
37
40
  element,
38
41
  ports,
39
42
  navigateTo,
43
+ layoutId = id,
44
+ leaving = false,
45
+ initialAnimation = true,
40
46
  ...data
41
47
  },
42
48
  selectable = true,
43
49
  width: w = 100,
44
50
  height: h = 100
45
- }: ElementNodeProps) {
51
+ }) => {
46
52
  const overlay = useOverlayDialog()
47
53
  const {
48
54
  currentViewId,
49
55
  onNavigateTo,
50
56
  onOpenSource
51
57
  } = useDiagramState(selector)
58
+
59
+ const scale = (diff: number) => ({
60
+ scaleX: (w + diff) / w,
61
+ scaleY: (h + diff) / h
62
+ })
63
+
64
+ let opacity = 1
65
+ if (data.dimmed) {
66
+ opacity = data.dimmed === 'immediate' ? 0.05 : 0.15
67
+ }
68
+ if (leaving) {
69
+ opacity = 0
70
+ }
71
+
52
72
  return (
53
73
  <>
54
74
  <m.div
@@ -56,22 +76,39 @@ export function ElementNode({
56
76
  css.elementNode,
57
77
  'likec4-element-node'
58
78
  ])}
79
+ layoutId={layoutId}
59
80
  data-likec4-color={element.color}
81
+ initial={(layoutId === id && initialAnimation)
82
+ ? {
83
+ ...scale(-20),
84
+ opacity: 0,
85
+ width: w,
86
+ height: h
87
+ }
88
+ : false}
60
89
  animate={{
61
- opacity: data.dimmed ? 0.15 : 1,
90
+ ...scale(0),
91
+ opacity,
92
+ width: w,
93
+ height: h,
62
94
  transition: {
63
- delay: data.dimmed === true ? .4 : 0
95
+ opacity: {
96
+ delay: !leaving && data.dimmed === true ? .4 : 0,
97
+ ...((leaving || data.dimmed === 'immediate') && {
98
+ duration: 0.09
99
+ })
100
+ }
64
101
  }
65
102
  }}
66
103
  {...(selectable && {
67
104
  whileHover: {
68
- scale: 1.045,
69
- transition: {
70
- delay: 0.15
71
- }
105
+ ...scale(16)
106
+ // transition: {
107
+ // delay: 0.1
108
+ // }
72
109
  },
73
110
  whileTap: {
74
- scale: 0.97
111
+ ...scale(-8)
75
112
  }
76
113
  })}
77
114
  >
@@ -86,7 +123,9 @@ export function ElementNode({
86
123
  <ElementShapeSvg shape={element.shape} w={w} h={h} />
87
124
  </svg>
88
125
  <Box className={css.elementNodeContent}>
89
- <Text className={css.elementNodeTitle} lineClamp={2}>{element.title}</Text>
126
+ <Text className={css.elementNodeTitle} lineClamp={2}>
127
+ {element.title}
128
+ </Text>
90
129
  {element.description && (
91
130
  <Text className={css.elementNodeDescription} lineClamp={4}>{element.description}</Text>
92
131
  )}
@@ -96,13 +135,12 @@ export function ElementNode({
96
135
  <Action
97
136
  onClick={(event) => {
98
137
  event.stopPropagation()
99
- setTimeout(() => onNavigateTo(navigateTo), 100)
100
- overlay.close()
138
+ overlay.close(() => onNavigateTo(navigateTo))
101
139
  }}>
102
140
  <IconZoomScan stroke={1.8} style={{ width: '75%' }} />
103
141
  </Action>
104
142
  )}
105
- {data.column !== 'subject' && (
143
+ {data.column !== 'subjects' && (
106
144
  <Action
107
145
  onClick={(event) => {
108
146
  event.stopPropagation()
@@ -148,4 +186,6 @@ export function ElementNode({
148
186
  ))}
149
187
  </>
150
188
  )
151
- }
189
+ }, (prev, next) => {
190
+ return deepEqual(prev.data, next.data)
191
+ })
@@ -16,7 +16,7 @@ export function EmptyNode({
16
16
  }: EmptyNodeProps) {
17
17
  return (
18
18
  <Box className={css.emptyNode}>
19
- <Text c={'dimmed'} fw={500}>No {column === 'incomers' ? 'incoming' : 'outgoing'}</Text>
19
+ <Text c={'dimmed'} fz={'lg'} fw={500}>No {column === 'incomers' ? 'incoming' : 'outgoing'}</Text>
20
20
  </Box>
21
21
  )
22
22
  }
@@ -1,30 +1,52 @@
1
- import { ActionIcon, Box, Group, Stack, Text, ThemeIcon } from '@mantine/core'
1
+ import { ActionIcon, Box, Group, Stack, Text, ThemeIcon, Tooltip as MantineTooltip } from '@mantine/core'
2
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 { useEffect, useRef } from 'react'
5
6
  import { only } from 'remeda'
6
7
  import { useDiagramState } from '../../../hooks/useDiagramState'
7
8
  import { stopPropagation } from '../../../xyflow/utils'
9
+ import { useOverlayDialog } from '../../OverlayContext'
8
10
  import type { XYFlowTypes } from '../_types'
9
11
  import { ZIndexes } from '../use-layouted-relationships'
10
12
  import * as css from './styles.css'
11
13
 
14
+ const Tooltip = MantineTooltip.withProps({
15
+ color: 'dark',
16
+ fz: 'sm',
17
+ openDelay: 400,
18
+ closeDelay: 150,
19
+ withinPortal: false,
20
+ label: '',
21
+ children: null,
22
+ offset: 4
23
+ })
24
+
12
25
  export function RelationshipEdge({
13
26
  data,
14
27
  label,
15
28
  ...props
16
29
  }: EdgeProps<XYFlowTypes.Edge>) {
17
- const onNavigateTo = useDiagramState(s => s.onNavigateTo)
30
+ const {
31
+ viewId,
32
+ onNavigateTo
33
+ } = useDiagramState(s => ({
34
+ viewId: s.view.id,
35
+ onNavigateTo: s.onNavigateTo
36
+ }))
37
+ const overlay = useOverlayDialog()
18
38
  const [edgePath, labelX, labelY] = getBezierPath(props)
19
39
  const navigateTo = onNavigateTo ? only(data.relations)?.navigateTo : undefined
20
40
  const isMultiRelation = data.relations.length > 1
21
41
  const technology = only(data.relations)?.technology
42
+
22
43
  return (
23
44
  <>
24
45
  <g
25
- className={css.edgePath}
46
+ className={css.edgeContainer}
26
47
  data-edge-dimmed={data.dimmed}
27
48
  data-edge-hovered={data.hovered}
49
+ data-likec4-color={data.includedInCurrentView ? 'gray' : 'amber'}
28
50
  >
29
51
  <BaseEdge
30
52
  path={edgePath}
@@ -37,7 +59,7 @@ export function RelationshipEdge({
37
59
  style={{
38
60
  position: 'absolute',
39
61
  transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
40
- maxWidth: Math.abs(props.targetX - props.sourceX - 40),
62
+ maxWidth: Math.abs(props.targetX - props.sourceX - 70),
41
63
  zIndex: ZIndexes.max
42
64
  }}
43
65
  className={clsx([
@@ -46,25 +68,29 @@ export function RelationshipEdge({
46
68
  ])}
47
69
  data-edge-dimmed={data.dimmed}
48
70
  data-edge-hovered={data.hovered}
71
+ data-likec4-color={data.includedInCurrentView ? 'gray' : 'amber'}
72
+ // {...data.includedInCurrentView === false && { 'data-likec4-color': 'amber' }}
49
73
  >
50
74
  {label && (
51
- <Group gap={6} wrap="nowrap">
52
- {isMultiRelation && (
53
- <ThemeIcon size={'sm'} variant="transparent" color="orange">
54
- <IconBoxMultipleFilled style={{ width: '80%' }} />
55
- </ThemeIcon>
56
- )}
57
- <Text
58
- fw={isMultiRelation ? '500' : '400'}
59
- style={{
60
- whiteSpace: isMultiRelation ? 'nowrap' : undefined
61
- }}
62
- component={'div'}
63
- className={css.edgeLabelText}
64
- lineClamp={3}>
65
- {label}
66
- </Text>
67
- </Group>
75
+ <Tooltip label="Not included in current view" disabled={data.includedInCurrentView} color={'orange'}>
76
+ <Group gap={6} wrap="nowrap">
77
+ {isMultiRelation && (
78
+ <ThemeIcon size={'sm'} variant="transparent" color="orange">
79
+ <IconBoxMultipleFilled style={{ width: '80%' }} />
80
+ </ThemeIcon>
81
+ )}
82
+ <Text
83
+ fw={isMultiRelation ? '500' : '400'}
84
+ style={{
85
+ whiteSpace: isMultiRelation ? 'nowrap' : undefined
86
+ }}
87
+ component={'div'}
88
+ className={css.edgeLabelText}
89
+ lineClamp={3}>
90
+ {label}
91
+ </Text>
92
+ </Group>
93
+ </Tooltip>
68
94
  )}
69
95
  {technology && (
70
96
  <Text component={'div'} className={css.edgeLabelTechnology}>
@@ -73,7 +99,7 @@ export function RelationshipEdge({
73
99
  {' ]'}
74
100
  </Text>
75
101
  )}
76
- {navigateTo && (
102
+ {navigateTo && viewId !== navigateTo && (
77
103
  <Box ta={'center'} mt={4}>
78
104
  <ActionIcon
79
105
  variant="default"
@@ -82,7 +108,9 @@ export function RelationshipEdge({
82
108
  onPointerDownCapture={stopPropagation}
83
109
  onClick={event => {
84
110
  event.stopPropagation()
85
- onNavigateTo?.(navigateTo, event)
111
+ overlay.close(() => {
112
+ onNavigateTo?.(navigateTo, event)
113
+ })
86
114
  }}
87
115
  role="button"
88
116
  onDoubleClick={stopPropagation}
@@ -1,8 +1,10 @@
1
1
  import { rem } from '@mantine/core'
2
2
  import { globalStyle, style } from '@vanilla-extract/css'
3
- import { mantine, vars, whereLight, xyvars } from '../../../theme-vars'
3
+ import { mantine, transitions, vars, whereDark, xyvars } from '../../../theme-vars'
4
+ import { mixColor } from '../../Overlays.css'
4
5
 
5
6
  export const elementNode = style({
7
+ position: 'relative',
6
8
  width: '100%',
7
9
  height: '100%'
8
10
  })
@@ -27,7 +29,7 @@ export const elementNodeTitle = style({
27
29
  fontOpticalSizing: 'auto',
28
30
  fontStyle: 'normal',
29
31
  fontWeight: 500,
30
- fontSize: 17,
32
+ fontSize: 18,
31
33
  lineHeight: 1.25,
32
34
  textWrap: 'balance',
33
35
  color: vars.element.hiContrast
@@ -40,30 +42,30 @@ export const elementNodeDescription = style({
40
42
  fontFamily: vars.element.font,
41
43
  fontOpticalSizing: 'auto',
42
44
  fontStyle: 'normal',
43
- fontSize: mantine.fontSizes.xs,
45
+ fontSize: mantine.fontSizes.sm,
44
46
  lineHeight: 1.25,
45
47
  textWrap: 'balance',
46
48
  color: vars.element.loContrast
47
49
  })
48
50
 
49
51
  export const compoundNodeBody = style({
52
+ position: 'relative',
50
53
  width: '100%',
51
54
  height: '100%',
52
- boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
53
- background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
54
- borderRadius: 4
55
+ boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 1px 1px 4px -1px rgba(0,0,0,0.3)',
56
+ // background: `color-mix(in srgb , ${vars.element.fill}, transparent 10%)`,
57
+ background: vars.element.fill,
58
+ borderRadius: 6
55
59
  })
56
60
 
57
61
  export const compoundNodeTitle = style({
58
62
  fontFamily: vars.compound.font,
59
- fontOpticalSizing: 'auto',
60
- fontStyle: 'normal',
61
63
  fontWeight: 600,
62
- fontSize: 14,
64
+ fontSize: 15,
63
65
  lineHeight: 1,
64
66
  textTransform: 'uppercase',
65
- paddingTop: 12,
66
- paddingLeft: 10,
67
+ paddingTop: 14,
68
+ paddingLeft: 12,
67
69
  mixBlendMode: 'screen',
68
70
  color: vars.compound.titleColor
69
71
  })
@@ -86,19 +88,32 @@ export const cssShapeSvg = style({
86
88
 
87
89
  const DimmedTransitionDelay = '400ms'
88
90
 
89
- export const edgePath = style({
91
+ export const edgeContainer = style({
90
92
  opacity: 1,
91
- transition: 'all 0.2s ease-in-out',
92
-
93
+ transition: transitions.fast,
94
+ vars: {
95
+ [xyvars.edge.stroke]: vars.relation.lineColor,
96
+ [xyvars.edge.strokeSelected]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
97
+ [xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 50%)`,
98
+ [xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 20%)`
99
+ },
93
100
  selectors: {
94
101
  [`&[data-edge-dimmed='true']`]: {
95
- opacity: 0.1,
102
+ opacity: 0.2,
96
103
  transitionDelay: DimmedTransitionDelay
104
+ },
105
+ [`&[data-edge-dimmed='immediate']`]: {
106
+ opacity: 0.2
107
+ },
108
+ [`${whereDark} &`]: {
109
+ vars: {
110
+ [xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 50%)`
111
+ }
97
112
  }
98
113
  }
99
114
  })
100
115
 
101
- export const edgeLabel = style({
116
+ export const edgeLabel = style([edgeContainer, {
102
117
  padding: '2px 5px',
103
118
  fontFamily: vars.likec4.font,
104
119
  position: 'absolute',
@@ -111,21 +126,13 @@ export const edgeLabel = style({
111
126
  // if you have an interactive element, set pointer-events: all
112
127
  pointerEvents: 'all',
113
128
  textWrap: 'pretty',
114
- whiteSpace: 'preserve-breaks',
115
- opacity: 1,
116
- transition: 'all 0.2s ease-in-out',
117
- selectors: {
118
- [`&[data-edge-dimmed='true']`]: {
119
- opacity: 0.1,
120
- transitionDelay: DimmedTransitionDelay
121
- }
122
- }
123
- })
129
+ whiteSpace: 'preserve-breaks'
130
+ }])
124
131
 
125
132
  export const edgeLabelText = style({
126
133
  textAlign: 'center',
127
134
  whiteSpaceCollapse: 'preserve-breaks',
128
- fontSize: rem(15),
135
+ fontSize: mantine.fontSizes.md,
129
136
  lineHeight: mantine.lineHeights.xs
130
137
  })
131
138
 
@@ -1,5 +1,6 @@
1
1
  import { invariant } from '@likec4/core'
2
- import { useMergedRef } from '@mantine/hooks'
2
+ import { Box } from '@mantine/core'
3
+ import { useMergedRef, useReducedMotion } from '@mantine/hooks'
3
4
  import { deepEqual, shallowEqual } from 'fast-equals'
4
5
  import { useAnimate } from 'framer-motion'
5
6
  import { createContext, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
@@ -25,6 +26,7 @@ export function DiagramContextProvider({
25
26
  whereFilter,
26
27
  ...props
27
28
  }: DiagramContextProviderProps) {
29
+ const isMotionReduced = useReducedMotion() ?? false
28
30
  const [scope, animate] = useAnimate()
29
31
  const containerRef = useRef<HTMLDivElement>(null)
30
32
  const ref = useMergedRef(scope, containerRef)
@@ -70,19 +72,24 @@ export function DiagramContextProvider({
70
72
 
71
73
  const api = store.current
72
74
  useEffect(() => {
75
+ if (isMotionReduced) {
76
+ return
77
+ }
73
78
  return api.subscribe(s => !!s.activeOverlay, (isActiveOverlay) => {
74
79
  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)`
80
+ opacity: isActiveOverlay ? 0.7 : 1,
81
+ filter: isActiveOverlay ? 'grayscale(1)' : 'grayscale(0)',
82
+ transform: isActiveOverlay ? `perspective(300px) translateZ(-10px) translateY(2px)` : `translateY(0)`
78
83
  }, {
79
- duration: isActiveOverlay ? 0.5 : 0.16
84
+ duration: isActiveOverlay ? 0.4 : 0.2
80
85
  })
86
+ }, {
87
+ fireImmediately: true
81
88
  })
82
- }, [api])
89
+ }, [api, isMotionReduced])
83
90
 
84
91
  return (
85
- <div
92
+ <Box
86
93
  ref={ref}
87
94
  className={className}
88
95
  {...(keepAspectRatio && {
@@ -94,7 +101,7 @@ export function DiagramContextProvider({
94
101
  <DiagramContext.Provider value={api}>
95
102
  {children}
96
103
  </DiagramContext.Provider>
97
- </div>
104
+ </Box>
98
105
  )
99
106
  }
100
107
  DiagramContextProvider.displayName = 'DiagramContextProvider'
@@ -60,10 +60,8 @@ export type DiagramInitialState = {
60
60
  // If LikeC4Model provided
61
61
  hasLikeC4Model: boolean
62
62
  controls: boolean
63
- showElementLinks: boolean
64
63
  showNavigationButtons: boolean
65
64
  showNotations: boolean
66
- showRelationshipDetails: boolean
67
65
  fitViewEnabled: boolean
68
66
  fitViewPadding: number
69
67
  zoomable: boolean
@@ -72,7 +70,9 @@ export type DiagramInitialState = {
72
70
  nodesSelectable: boolean
73
71
  experimentalEdgeEditing: boolean
74
72
  enableFocusMode: boolean
75
- enableRelationshipsBrowser: boolean
73
+ enableElementDetails: boolean
74
+ enableRelationshipBrowser: boolean
75
+ enableRelationshipDetails: boolean
76
76
  renderIcon: ElementIconRenderer | null
77
77
  whereFilter: WhereOperator<string, string> | null
78
78
  // If Dynamic View
@@ -104,6 +104,7 @@ export type DiagramState = Simplify<
104
104
  activeOverlay:
105
105
  | null
106
106
  | RequireExactlyOne<{
107
+ elementDetails: Fqn
107
108
  relationshipsOf: Fqn
108
109
  edgeDetails: EdgeId
109
110
  }>
@@ -776,7 +777,7 @@ export function createDiagramStore(props: DiagramInitialState) {
776
777
  }
777
778
  const xynode = xynodes.find(({ id }) => id === xynodeId)
778
779
  invariant(xynode, `xynode not found: ${xynodeId}`)
779
- const element = view.nodes.find(({ id }) => id === xynodeId)
780
+ const element = xynode.data.element
780
781
  invariant(element?.navigateTo, `node is not navigable: ${xynodeId}`)
781
782
  cancelSaveManualLayout()
782
783
  set(
@@ -853,7 +854,14 @@ export function createDiagramStore(props: DiagramInitialState) {
853
854
  if (eq(overlay, get().activeOverlay)) {
854
855
  return
855
856
  }
856
- set({ activeOverlay: overlay }, noReplace, 'openOverlay')
857
+ set(
858
+ {
859
+ activeWalkthrough: null,
860
+ activeOverlay: overlay
861
+ },
862
+ noReplace,
863
+ 'openOverlay'
864
+ )
857
865
  },
858
866
  closeOverlay: () => {
859
867
  if (get().activeOverlay !== null) {
@@ -862,7 +870,7 @@ export function createDiagramStore(props: DiagramInitialState) {
862
870
  activeOverlay: null
863
871
  },
864
872
  noReplace,
865
- 'openOverlay'
873
+ 'closeOverlay'
866
874
  )
867
875
  }
868
876
  },
package/src/theme-vars.ts CHANGED
@@ -72,5 +72,14 @@ export const xyvars = createGlobalThemeContract({
72
72
  export const whereLight = ':where([data-mantine-color-scheme="light"])'
73
73
  export const whereDark = ':where([data-mantine-color-scheme="dark"])'
74
74
 
75
+ export const easings = {
76
+ out: 'cubic-bezier(0, 0, 0.40, 1)',
77
+ inOut: 'cubic-bezier(0.50, 0, 0.2, 1)'
78
+ }
79
+
80
+ export const transitions = {
81
+ fast: `all 130ms ${easings.inOut}`
82
+ }
83
+
75
84
  // we need only variables names
76
85
  export const mantine = themeToVars({})
@@ -0,0 +1,18 @@
1
+ import { useMantineStyleNonce } from '@mantine/core'
2
+ import { useId } from '@mantine/hooks'
3
+ import { domMax, LayoutGroup, LazyMotion, MotionConfig } from 'framer-motion'
4
+ import { type PropsWithChildren } from 'react'
5
+
6
+ export const FramerMotionConfig = ({ children }: PropsWithChildren) => {
7
+ const layoutId = useId()
8
+ const nonce = useMantineStyleNonce()?.()
9
+ return (
10
+ <LazyMotion features={domMax} strict>
11
+ <MotionConfig reducedMotion="user" {...nonce && { nonce }}>
12
+ <LayoutGroup id={layoutId}>
13
+ {children}
14
+ </LayoutGroup>
15
+ </MotionConfig>
16
+ </LazyMotion>
17
+ )
18
+ }