@likec4/diagram 1.3.0 → 1.4.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 (44) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +6 -1
  3. package/dist/index.mjs +270 -168
  4. package/dist/state/DiagramContext.d.ts +155 -154
  5. package/dist/state/diagramStore.d.ts +154 -154
  6. package/dist/state/index.d.ts +0 -1
  7. package/dist/state/useDiagramStore.d.ts +3 -4622
  8. package/dist/state/useNavigationHistory.d.ts +5 -0
  9. package/dist/style.css +86 -54
  10. package/dist/theme.css.d.ts +5 -0
  11. package/dist/ui/BackwardForwardPanel.css.d.ts +1 -0
  12. package/dist/ui/BackwardForwardPanel.d.ts +1 -0
  13. package/dist/ui/DiagramTitlePanel.css.d.ts +1 -1
  14. package/dist/ui/OptionsPanel.d.ts +1 -3
  15. package/dist/xyflow/XYFlowEvents.d.ts +4 -4
  16. package/dist/xyflow/XYFlowInner.d.ts +1 -0
  17. package/dist/xyflow/edges/RelationshipEdge.d.ts +1 -1
  18. package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
  19. package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
  20. package/dist/xyflow/types.d.ts +1 -1
  21. package/package.json +10 -10
  22. package/src/LikeC4Diagram.css.ts +11 -1
  23. package/src/LikeC4Diagram.props.ts +10 -3
  24. package/src/LikeC4Diagram.tsx +30 -7
  25. package/src/globals.css.ts +1 -1
  26. package/src/state/DiagramContext.tsx +3 -3
  27. package/src/state/diagramStore.ts +27 -30
  28. package/src/state/index.ts +0 -1
  29. package/src/state/useDiagramStore.ts +6 -13
  30. package/src/state/useNavigationHistory.ts +39 -0
  31. package/src/theme.css.ts +5 -0
  32. package/src/ui/BackwardForwardPanel.css.ts +22 -0
  33. package/src/ui/BackwardForwardPanel.tsx +59 -0
  34. package/src/ui/DiagramTitlePanel.css.ts +11 -5
  35. package/src/ui/DiagramTitlePanel.tsx +105 -93
  36. package/src/ui/OptionsPanel.tsx +28 -19
  37. package/src/xyflow/XYFlowInner.tsx +5 -2
  38. package/src/xyflow/diagram-to-xyflow.ts +2 -13
  39. package/src/xyflow/edges/RelationshipEdge.tsx +11 -8
  40. package/src/xyflow/edges/edges.css.ts +14 -10
  41. package/src/xyflow/hooks/useLayoutConstraints.ts +2 -1
  42. package/src/xyflow/nodes/compound/CompoundNode.css.ts +8 -8
  43. package/src/xyflow/nodes/element/ElementNode.tsx +7 -8
  44. package/src/xyflow/nodes/element/element.css.ts +18 -5
@@ -1,5 +1,6 @@
1
- import { Anchor, Box, Button, CopyButton, Group, Paper, Spoiler, Stack, Text } from '@mantine/core'
1
+ import { Anchor, Box, Button, Card, CardSection, CopyButton, Group, Spoiler, Stack, Text } from '@mantine/core'
2
2
  import { useLocalStorage } from '@mantine/hooks'
3
+ import { IconMenu } from '@tabler/icons-react'
3
4
  import clsx from 'clsx'
4
5
  import { AnimatePresence, m } from 'framer-motion'
5
6
  import { type DiagramState, useDiagramState } from '../state'
@@ -35,103 +36,114 @@ export default function DiagramTitlePanel() {
35
36
  transformOrigin: 'left center'
36
37
  }}
37
38
  >
38
- <Paper
39
+ <Card
39
40
  radius="sm"
40
- withBorder={!isCollapsed}
41
- className={css.paper}
42
- p={isCollapsed ? 'xs' : 'lg'}
41
+ className={css.card}
42
+ p={'md'}
43
43
  onDoubleClick={e => e.stopPropagation()}>
44
- <Stack gap={'sm'} justify="stretch" align="stretch">
45
- <Group justify="stretch" wrap="nowrap" onClick={toggle} style={{ cursor: 'pointer' }}>
46
- <Text
47
- component={'div'}
48
- flex={'1'}
49
- size={isCollapsed ? 'sm' : 'lg'}
50
- fw={500}
51
- lh={1.1}
52
- className={css.title}
53
- >
54
- {title}
55
- </Text>
56
- <Text
57
- hidden={isCollapsed}
58
- component={'div'}
59
- flex={'0 0 auto'}
60
- inline
61
- size="xs"
62
- fz={9}
63
- fw={500}
64
- c={'dimmed'}
65
- style={{
66
- userSelect: 'all'
67
- }}>
68
- <span style={{ userSelect: 'none' }}>id:{' '}</span>
69
- {id}
70
- </Text>
71
- </Group>
72
- {!isCollapsed && (
73
- <>
74
- {description && (
75
- <Spoiler
76
- maxHeight={42}
77
- showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
78
- hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
79
- <Text
80
- component={'div'}
81
- size="sm"
82
- className={css.description}>
83
- {description || 'no description'}
84
- </Text>
85
- </Spoiler>
86
- )}
87
- {!description && (
44
+ <CardSection mb={4}>
45
+ <Button
46
+ fullWidth
47
+ size="xs"
48
+ h={'sm'}
49
+ py={2}
50
+ radius={'0'}
51
+ variant="subtle"
52
+ color="gray"
53
+ onClick={toggle}
54
+ >
55
+ <IconMenu size={11} opacity={0.7} />
56
+ </Button>
57
+ </CardSection>
58
+ <Group justify="stretch" wrap="nowrap" mb={isCollapsed ? 0 : 'sm'}>
59
+ <Text
60
+ component={'div'}
61
+ flex={'1'}
62
+ size={isCollapsed ? 'sm' : 'lg'}
63
+ fw={500}
64
+ lh={1.1}
65
+ className={css.title}
66
+ >
67
+ {title}
68
+ </Text>
69
+ <Text
70
+ hidden={isCollapsed}
71
+ component={'div'}
72
+ flex={'0 0 auto'}
73
+ inline
74
+ size="xs"
75
+ fz={9}
76
+ fw={500}
77
+ c={'dimmed'}
78
+ style={{
79
+ userSelect: 'all'
80
+ }}>
81
+ <span style={{ userSelect: 'none' }}>id:{' '}</span>
82
+ {id}
83
+ </Text>
84
+ </Group>
85
+ {!isCollapsed && (
86
+ <>
87
+ {description && (
88
+ <Spoiler
89
+ maxHeight={42}
90
+ showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
91
+ hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
88
92
  <Text
89
93
  component={'div'}
90
- size="xs"
91
- c={'dimmed'}>
92
- no description
94
+ size="sm"
95
+ className={css.description}>
96
+ {description || 'no description'}
93
97
  </Text>
94
- )}
95
- {links && (
96
- <Stack
97
- gap={3}
98
- justify="stretch"
99
- align="stretch">
100
- {links.map((link) => (
101
- <Group key={link} wrap="nowrap" align="center" gap={'sm'}>
102
- <Box flex={'1'} style={{ overflow: 'hidden' }}>
103
- <Anchor
104
- href={link}
105
- target="_blank"
106
- fz="xs"
107
- truncate="end"
108
- display={'inline-block'}
109
- w={'100%'}>
110
- {link}
111
- </Anchor>
112
- </Box>
113
- <CopyButton value={link}>
114
- {({ copied, copy }) => (
115
- <Button
116
- size="compact-xs"
117
- fz={'10'}
118
- variant="light"
119
- onClick={copy}
120
- color={copied
121
- ? 'teal'
122
- : 'gray'}>
123
- {copied ? 'copied' : 'copy'}
124
- </Button>
125
- )}
126
- </CopyButton>
127
- </Group>
128
- ))}
129
- </Stack>
130
- )}
131
- </>
132
- )}
133
- </Stack>
134
- </Paper>
98
+ </Spoiler>
99
+ )}
100
+ {!description && (
101
+ <Text
102
+ component={'div'}
103
+ size="xs"
104
+ c={'dimmed'}>
105
+ no description
106
+ </Text>
107
+ )}
108
+ {links && (
109
+ <Stack
110
+ gap={3}
111
+ justify="stretch"
112
+ align="stretch">
113
+ {links.map((link) => (
114
+ <Group key={link} wrap="nowrap" align="center" gap={'sm'}>
115
+ <Box flex={'1'} style={{ overflow: 'hidden' }}>
116
+ <Anchor
117
+ href={link}
118
+ target="_blank"
119
+ fz="xs"
120
+ truncate="end"
121
+ display={'inline-block'}
122
+ w={'100%'}>
123
+ {link}
124
+ </Anchor>
125
+ </Box>
126
+ <CopyButton value={link}>
127
+ {({ copied, copy }) => (
128
+ <Button
129
+ size="compact-xs"
130
+ fz={'10'}
131
+ variant="light"
132
+ onClick={copy}
133
+ color={copied
134
+ ? 'teal'
135
+ : 'gray'}>
136
+ {copied ? 'copied' : 'copy'}
137
+ </Button>
138
+ )}
139
+ </CopyButton>
140
+ </Group>
141
+ ))}
142
+ </Stack>
143
+ )}
144
+ </>
145
+ )}
146
+ </Card>
135
147
  </m.div>
136
148
  </AnimatePresence>
137
149
  )
@@ -1,29 +1,40 @@
1
1
  import { Card } from '@mantine/core'
2
- import { useOnSelectionChange } from '@xyflow/react'
3
2
  import clsx from 'clsx'
4
- import { deepEqual as eq } from 'fast-equals'
3
+ import { DEV } from 'esm-env'
4
+ import { shallowEqual } from 'fast-equals'
5
5
  import { AnimatePresence, m } from 'framer-motion'
6
- import { memo, useState } from 'react'
7
- import { isNullish } from 'remeda'
6
+ import { useCallback } from 'react'
7
+ import { reduce } from 'remeda'
8
+ import useTilg from 'tilg'
8
9
  import { useDiagramState } from '../state'
10
+ import { useXYStore } from '../xyflow/hooks'
9
11
  import { NodeOptions } from './options/NodeOptions'
10
12
  import * as styles from './OptionsPanel.css'
11
13
 
12
- const OptionsPanelMemo = memo(function OptionsPanel() {
13
- const isFocusDisabled = useDiagramState(s => isNullish(s.focusedNodeId))
14
- const [selectedNodes, setSelectedNodes] = useState([] as string[])
15
-
16
- useOnSelectionChange({
17
- onChange: ({ nodes }) => {
18
- const next = nodes.map(n => n.id)
19
- setSelectedNodes(prev => eq(prev, next) ? prev : next)
20
- }
21
- })
22
-
14
+ const Empty = [] as string[]
15
+ export default function OptionsPanel() {
16
+ const { isFocusDisabled, viewId } = useDiagramState(s => ({
17
+ viewId: s.view.id,
18
+ isFocusDisabled: s.focusedNodeId === null && s.activeDynamicViewStep === null
19
+ }))
20
+ const selectedNodes = useXYStore(
21
+ useCallback(s =>
22
+ s.elementsSelectable && isFocusDisabled
23
+ ? reduce(s.nodes, (acc, n) => {
24
+ if (n.selected) {
25
+ acc.push(n.id)
26
+ }
27
+ return acc
28
+ }, [] as string[])
29
+ : Empty, [isFocusDisabled]),
30
+ shallowEqual
31
+ )
32
+ DEV && useTilg()
23
33
  return (
24
34
  <AnimatePresence mode="wait">
25
- {isFocusDisabled && selectedNodes.length > 0 && (
35
+ {selectedNodes.length > 0 && (
26
36
  <m.div
37
+ key={viewId}
27
38
  initial={{ opacity: 0, scale: 0.85 }}
28
39
  animate={{ opacity: 1, scale: 1 }}
29
40
  exit={{
@@ -43,6 +54,4 @@ const OptionsPanelMemo = memo(function OptionsPanel() {
43
54
  )}
44
55
  </AnimatePresence>
45
56
  )
46
- })
47
-
48
- export default OptionsPanelMemo
57
+ }
@@ -1,10 +1,10 @@
1
- import { isDynamicView } from '@likec4/core'
2
1
  import { Controls } from '@xyflow/react'
3
2
  import { shallowEqual } from 'fast-equals'
4
3
  import { memo, type PropsWithChildren } from 'react'
5
- import { isNonNull, isNonNullish, isNullish } from 'remeda'
4
+ import { isNonNull } from 'remeda'
6
5
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
7
6
  import { useDiagramState } from '../state/useDiagramStore'
7
+ import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
8
8
  import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
9
  import { DynamicViewWalkthrough } from '../ui/DynamicViewWalkthrough'
10
10
  import OptionsPanel from '../ui/OptionsPanel'
@@ -14,6 +14,7 @@ type XYFlowInnerProps = PropsWithChildren<{
14
14
  background: NonNullable<LikeC4DiagramProperties['background']>
15
15
  controls: boolean
16
16
  showDiagramTitle: boolean
17
+ showNavigationButtons: boolean
17
18
  enableDynamicViewWalkthrough: boolean
18
19
  }>
19
20
 
@@ -22,6 +23,7 @@ export const XYFlowInner = memo(function XYFlowInnerR({
22
23
  background,
23
24
  controls,
24
25
  showDiagramTitle,
26
+ showNavigationButtons,
25
27
  enableDynamicViewWalkthrough
26
28
  }: XYFlowInnerProps) {
27
29
  const {
@@ -45,6 +47,7 @@ export const XYFlowInner = memo(function XYFlowInnerR({
45
47
  {readonly === false && <OptionsPanel />}
46
48
  {isDiagramTitleVisible && <DiagramTitlePanel />}
47
49
  {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
50
+ {showNavigationButtons && <BackwardForwardPanel />}
48
51
  {children}
49
52
  </>
50
53
  )
@@ -1,17 +1,6 @@
1
- import {
2
- type DiagramNode,
3
- type DiagramView,
4
- extractStep,
5
- type Fqn,
6
- invariant,
7
- type NonEmptyArray,
8
- nonNullable,
9
- type Point
10
- } from '@likec4/core'
11
- import { Bezier } from 'bezier-js'
12
- import { hasAtLeast, reduce } from 'remeda'
1
+ import { type DiagramNode, type DiagramView, extractStep, type Fqn, invariant, nonNullable } from '@likec4/core'
2
+ import { hasAtLeast } from 'remeda'
13
3
  import type { XYFlowData } from '../xyflow/types'
14
- import { toDomPrecision } from './utils'
15
4
 
16
5
  // const nodeZIndex = (node: DiagramNode) => node.level + (hasAtLeast(node.children, 1) ? 2 : 1)
17
6
  const nodeZIndex = (node: DiagramNode) => node.level + 1
@@ -12,7 +12,7 @@ import { memo, useRef, useState } from 'react'
12
12
  import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
13
13
  import { useDiagramState, useDiagramStoreApi } from '../../state'
14
14
  import { ZIndexes } from '../const'
15
- import { useXYFlow, useXYStoreApi } from '../hooks'
15
+ import { useXYStoreApi } from '../hooks'
16
16
  import { type XYFlowEdge } from '../types'
17
17
  import { bezierControlPoints, toDomPrecision } from '../utils'
18
18
  import * as edgesCss from './edges.css'
@@ -207,6 +207,9 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
207
207
  let pointer = { x: e.clientX, y: e.clientY }
208
208
  const onPointerMove = (e: PointerEvent) => {
209
209
  if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
210
+ if (!hasMoved) {
211
+ diagramStore.getState().cancelSaveManualLayout()
212
+ }
210
213
  hasMoved = true
211
214
  pointer = { x: e.clientX, y: e.clientY }
212
215
  const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
@@ -246,6 +249,13 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
246
249
  data-edge-dir={diagramEdge.dir}
247
250
  data-edge-active={isActive}
248
251
  data-edge-hovered={isHovered}>
252
+ <path
253
+ className={clsx('react-flow__edge-interaction')}
254
+ d={edgePath}
255
+ fill="none"
256
+ stroke={'transparent'}
257
+ strokeWidth={interactionWidth ?? 10}
258
+ />
249
259
  <g className={edgesCss.markerContext}>
250
260
  <defs>
251
261
  <marker
@@ -279,13 +289,6 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
279
289
  markerEnd={markerEnd}
280
290
  />
281
291
  </g>
282
- <path
283
- className={clsx('react-flow__edge-interaction')}
284
- d={edgePath}
285
- fill="none"
286
- strokeOpacity={0}
287
- strokeWidth={interactionWidth ?? 10}
288
- />
289
292
  {isEdgePathEditable && controlPoints.map((p, i) => (
290
293
  <circle
291
294
  onPointerDown={e => onControlPointerDown(i, e)}
@@ -1,5 +1,5 @@
1
1
  import { rem } from '@mantine/core'
2
- import { createVar, fallbackVar, generateIdentifier, globalKeyframes, globalStyle, style } from '@vanilla-extract/css'
2
+ import { createVar, fallbackVar, globalStyle, keyframes, style } from '@vanilla-extract/css'
3
3
  import { mantine } from '../../mantine.css'
4
4
  import { vars, xyvars } from '../../theme.css'
5
5
 
@@ -62,7 +62,7 @@ globalStyle(`.react-flow__edges > svg:has(${dimmed})`, {
62
62
  opacity: 0.8,
63
63
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
64
64
  transitionDelay: '200ms',
65
- filter: 'grayscale(0.85) blur(1px)',
65
+ filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
66
66
  willChange: 'opacity, filter'
67
67
  })
68
68
 
@@ -112,8 +112,7 @@ export const controlPoint = style({
112
112
  }
113
113
  })
114
114
 
115
- const strokeKeyframes = generateIdentifier('strokeKeyframes')
116
- globalKeyframes(strokeKeyframes, {
115
+ const strokeKeyframes = keyframes({
117
116
  'from': {
118
117
  strokeDashoffset: 18 * 2
119
118
  },
@@ -126,19 +125,24 @@ export const cssEdgePath = style({
126
125
  animationDuration: '800ms',
127
126
  animationIterationCount: 'infinite',
128
127
  animationTimingFunction: 'linear',
128
+ animationName: strokeKeyframes,
129
+ animationPlayState: 'paused',
129
130
  selectors: {
130
131
  [`:where([data-edge-hovered='true']) &`]: {
131
- animationName: strokeKeyframes,
132
+ animationPlayState: 'running',
132
133
  animationDelay: '350ms',
133
- transition: 'stroke,stroke-width 130ms ease-out'
134
+ transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
134
135
  },
135
136
  [`:where(${isSelected}, [data-edge-active='true']) &`]: {
136
- animationName: strokeKeyframes,
137
+ animationPlayState: 'running',
137
138
  animationDelay: '0ms',
138
- transition: 'stroke,stroke-width 130ms ease-out'
139
+ transition: 'stroke 130ms ease-out,stroke-width 130ms ease-out'
139
140
  },
140
141
  [`:where([data-edge-dir='back']) &`]: {
141
142
  animationDirection: 'reverse'
143
+ },
144
+ [`${dimmed} &`]: {
145
+ animationPlayState: 'paused'
142
146
  }
143
147
  }
144
148
  })
@@ -170,10 +174,10 @@ export const edgeLabel = style({
170
174
  transform: `${varTranslate} scale(1.1)`
171
175
  },
172
176
  [`&:is(${dimmed})`]: {
173
- opacity: 0.8,
177
+ opacity: 0.6,
174
178
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
175
179
  transitionDelay: '200ms',
176
- filter: 'grayscale(0.85) blur(2px)',
180
+ filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
177
181
  willChange: 'opacity, filter'
178
182
  }
179
183
  }
@@ -286,7 +286,8 @@ export function useLayoutConstraints(): LayoutConstraints {
286
286
  x: event.clientX,
287
287
  y: event.clientY
288
288
  }
289
- const { xyflow } = diagramApi.getState()
289
+ const { xyflow, cancelSaveManualLayout } = diagramApi.getState()
290
+ cancelSaveManualLayout()
290
291
  solverRef.current = createLayoutConstraints(xyflow, xyflowApi, xynode.id)
291
292
  },
292
293
  onNodeDrag: (_event, xynode) => {
@@ -18,7 +18,7 @@ globalStyle(`.react-flow__node-compound:has(${dimmed})`, {
18
18
  opacity: 0.25,
19
19
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
20
20
  transitionDelay: '200ms',
21
- filter: 'grayscale(0.85) blur(1px)',
21
+ filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
22
22
  willChange: 'opacity, filter'
23
23
  })
24
24
 
@@ -81,14 +81,14 @@ export const transparent = style({
81
81
  opacity: calc.add(fallbackVar(varOpacity, '1'), opacityDeltaOnHover)
82
82
  },
83
83
  selectors: {
84
- [`:where(.react-flow__node.selected) &`]: {
85
- vars: {
86
- [opacityDeltaOnHover]: '.05'
87
- }
88
- },
84
+ // [`:where(.react-flow__node.selected) &`]: {
85
+ // vars: {
86
+ // [opacityDeltaOnHover]: '.05'
87
+ // }
88
+ // },
89
89
  [`:where([data-hovered]) &`]: {
90
90
  vars: {
91
- [opacityDeltaOnHover]: '.1'
91
+ [opacityDeltaOnHover]: '.07'
92
92
  }
93
93
  }
94
94
  }
@@ -212,7 +212,7 @@ export const navigateBtn = style({
212
212
  },
213
213
  ':hover': {
214
214
  boxShadow: mantine.shadows.lg,
215
- transform: 'scale(1.466)',
215
+ transform: 'scale(1.42)',
216
216
  opacity: 1,
217
217
  transitionDelay: '0'
218
218
  },
@@ -24,13 +24,13 @@ const variants = {
24
24
  idle: (_, { scale }) => ({
25
25
  scale: 1,
26
26
  transition: {
27
- delay: isNumber(scale) && scale > selectedScale ? 0.075 : 0
27
+ delay: isNumber(scale) && scale > selectedScale ? 0.09 : 0
28
28
  }
29
29
  }),
30
30
  selected: (_, { scale }) => ({
31
31
  scale: selectedScale,
32
32
  transition: {
33
- delay: isNumber(scale) && scale > selectedScale ? 0.075 : 0
33
+ delay: isNumber(scale) && scale > selectedScale ? 0.09 : 0
34
34
  }
35
35
  }),
36
36
  // dimmed: {
@@ -54,12 +54,9 @@ const variants = {
54
54
  // delay: 1
55
55
  // }
56
56
  // }),
57
- hovered: (_, { scale }) => ({
58
- scale: 1.06,
59
- transition: {
60
- delay: isNumber(scale) && scale !== 1 && scale !== selectedScale ? 0 : 0.15
61
- }
62
- }),
57
+ hovered: {
58
+ scale: 1.06
59
+ },
63
60
  tap: {
64
61
  scale: 0.975
65
62
  }
@@ -128,8 +125,10 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
128
125
  className={clsx([
129
126
  css.container,
130
127
  isDimmed && css.dimmed,
128
+ animate !== 'idle' && css.containerAnimated,
131
129
  'likec4-element-node'
132
130
  ])}
131
+ data-hovered={!dragging && isHovered}
133
132
  data-likec4-color={element.color}
134
133
  data-likec4-shape={element.shape}
135
134
  variants={variants}
@@ -16,16 +16,28 @@ export const container = style({
16
16
  alignItems: 'stretch',
17
17
  vars: {
18
18
  [stokeFillMix]: `color-mix(in srgb, ${vars.element.stroke} 90%, ${vars.element.fill})`
19
+ },
20
+ selectors: {
21
+ ':where(.react-flow__node.selected) &': {
22
+ willChange: 'transform'
23
+ },
24
+ '&[data-hovered="true"]': {
25
+ willChange: 'transform'
26
+ }
19
27
  }
20
28
  })
21
29
 
30
+ export const containerAnimated = style({
31
+ willChange: 'transform'
32
+ })
33
+
22
34
  export const dimmed = style({})
23
35
 
24
36
  globalStyle(`.react-flow__node-element:has(${dimmed})`, {
25
37
  opacity: 0.25,
26
38
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
27
39
  transitionDelay: '200ms',
28
- filter: 'grayscale(0.85) blur(2px)',
40
+ filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(2px)')}`,
29
41
  willChange: 'opacity, filter'
30
42
  })
31
43
 
@@ -49,16 +61,16 @@ export const indicator = style({
49
61
  transformOrigin: 'center center',
50
62
  strokeWidth: 6,
51
63
  animationDuration: '800ms',
52
- animationName: indicatorKeyframes,
53
64
  animationIterationCount: 'infinite',
54
65
  animationDirection: 'alternate',
66
+ opacity: 0.6,
55
67
  visibility: 'hidden',
56
68
  selectors: {
57
69
  ':where(.react-flow__node.selected) &': {
58
- visibility: 'visible'
70
+ visibility: 'visible',
71
+ animationName: fallbackVar(vars.safariAnimationHook, indicatorKeyframes)
59
72
  },
60
73
  [`:where(.react-flow__node:focus-visible, ${container}:focus-visible) &`]: {
61
- animationName: 'none',
62
74
  strokeWidth: 10,
63
75
  stroke: outlineColor,
64
76
  visibility: 'visible'
@@ -67,7 +79,8 @@ export const indicator = style({
67
79
  strokeWidth: 10
68
80
  },
69
81
  [`${dimmed} &`]: {
70
- visibility: 'hidden'
82
+ visibility: 'hidden',
83
+ display: 'none'
71
84
  }
72
85
  }
73
86
  })