@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,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'