@likec4/diagram 1.2.2 → 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 (69) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +27 -4
  3. package/dist/index.mjs +1543 -1346
  4. package/dist/state/DiagramContext.d.ts +2353 -264
  5. package/dist/state/diagramStore.d.ts +2360 -268
  6. package/dist/state/index.d.ts +0 -1
  7. package/dist/state/useDiagramStore.d.ts +3 -2534
  8. package/dist/state/useNavigationHistory.d.ts +5 -0
  9. package/dist/style.css +194 -131
  10. package/dist/theme.css.d.ts +5 -1
  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/xyflow/XYFlow.d.ts +2 -1
  15. package/dist/xyflow/XYFlowEvents.d.ts +4 -4
  16. package/dist/xyflow/XYFlowInner.d.ts +2 -1
  17. package/dist/xyflow/const.d.ts +1 -1
  18. package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
  19. package/dist/xyflow/edges/RelationshipEdge.d.ts +4 -5
  20. package/dist/xyflow/edges/utils.d.ts +4 -2
  21. package/dist/xyflow/hooks/useXYFlow.d.ts +9 -9
  22. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
  23. package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
  24. package/dist/xyflow/nodes/element/element.css.d.ts +1 -0
  25. package/dist/xyflow/nodes/element/index.d.ts +1 -1
  26. package/dist/xyflow/types.d.ts +3 -3
  27. package/dist/xyflow/utils.d.ts +3 -0
  28. package/package.json +18 -17
  29. package/src/LikeC4Diagram.css.ts +15 -5
  30. package/src/LikeC4Diagram.props.ts +33 -5
  31. package/src/LikeC4Diagram.tsx +103 -32
  32. package/src/globals.css.ts +1 -2
  33. package/src/mantine/theme.ts +1 -0
  34. package/src/state/DiagramContext.tsx +3 -4
  35. package/src/state/diagramStore.ts +177 -103
  36. package/src/state/index.ts +0 -1
  37. package/src/state/useDiagramStore.ts +6 -13
  38. package/src/state/useNavigationHistory.ts +39 -0
  39. package/src/theme.css.ts +5 -1
  40. package/src/ui/BackwardForwardPanel.css.ts +22 -0
  41. package/src/ui/BackwardForwardPanel.tsx +59 -0
  42. package/src/ui/DiagramTitlePanel.css.ts +11 -5
  43. package/src/ui/DiagramTitlePanel.tsx +108 -96
  44. package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
  45. package/src/ui/DynamicViewWalkthrough.tsx +36 -17
  46. package/src/ui/OptionsPanel.tsx +28 -19
  47. package/src/ui/options/NodeOptions.tsx +16 -19
  48. package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
  49. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
  50. package/src/xyflow/SyncWithDiagram.tsx +21 -2
  51. package/src/xyflow/XYFlow.tsx +20 -23
  52. package/src/xyflow/XYFlowEvents.tsx +26 -8
  53. package/src/xyflow/XYFlowInner.tsx +10 -6
  54. package/src/xyflow/const.ts +1 -1
  55. package/src/xyflow/diagram-to-xyflow.ts +4 -41
  56. package/src/xyflow/edges/RelationshipEdge.tsx +160 -111
  57. package/src/xyflow/edges/edges.css.ts +41 -26
  58. package/src/xyflow/edges/utils.ts +5 -2
  59. package/src/xyflow/hooks/useLayoutConstraints.ts +19 -4
  60. package/src/xyflow/nodes/compound/CompoundNode.css.ts +45 -37
  61. package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
  62. package/src/xyflow/nodes/element/ElementNode.tsx +27 -14
  63. package/src/xyflow/nodes/element/element.css.ts +31 -18
  64. package/src/xyflow/nodes/element/index.ts +1 -1
  65. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
  66. package/src/xyflow/types.ts +5 -2
  67. package/src/xyflow/utils.ts +48 -0
  68. package/dist/ui/index.d.ts +0 -2
  69. package/src/ui/index.ts +0 -3
@@ -1,5 +1,6 @@
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
+ import { calc } from '@vanilla-extract/css-utils'
3
4
  import { mantine } from '../../../mantine.css'
4
5
  import { vars } from '../../../theme.css'
5
6
 
@@ -11,12 +12,18 @@ export const container = style({
11
12
  margin: 0
12
13
  })
13
14
 
14
- export const dimmed = style({
15
- filter: vars.filterDimmed,
16
- transition: 'filter 800ms ease-out'
15
+ export const dimmed = style({})
16
+
17
+ globalStyle(`.react-flow__node-compound:has(${dimmed})`, {
18
+ opacity: 0.25,
19
+ transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
20
+ transitionDelay: '200ms',
21
+ filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(1px)')}`,
22
+ willChange: 'opacity, filter'
17
23
  })
18
24
 
19
- export const varTransparency = createVar('transparency')
25
+ export const varOpacity = createVar('opacity')
26
+ export const varBorderTransparency = createVar('border-transparency')
20
27
 
21
28
  const outlineColor = fallbackVar(
22
29
  mantine.colors.primaryColors.outline,
@@ -31,7 +38,6 @@ export const compoundBody = style({
31
38
  borderRadius: 6,
32
39
  boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
33
40
  padding: 8,
34
- background: vars.element.fill,
35
41
  transition: 'all 200ms ease-out',
36
42
  selectors: {
37
43
  [`:where(.react-flow__node.selected) &`]: {
@@ -42,39 +48,47 @@ export const compoundBody = style({
42
48
  outline: `3px solid ${outlineColor}`,
43
49
  outlineOffset: rem(1.5)
44
50
  }
51
+ },
52
+ ':before': {
53
+ content: '" "',
54
+ position: 'absolute',
55
+ top: 0,
56
+ left: 0,
57
+ width: '100%',
58
+ height: '100%',
59
+ borderRadius: 6,
60
+ pointerEvents: 'none',
61
+ transition: 'background 175ms ease-out, opacity 175ms ease-out',
62
+ background: vars.element.fill,
63
+ backgroundClip: 'padding-box'
45
64
  }
46
65
  })
47
66
 
48
- const bgTransparency = createVar('bgTransparency')
49
- const bgTransparencyDelta = createVar('bgTransparencyDelta')
67
+ const opacityDeltaOnHover = createVar('opacityDeltaOnHover')
50
68
  export const transparent = style({
51
69
  borderStyle: 'dashed',
52
70
  borderWidth: 3,
53
71
  padding: 6,
54
- borderColor: `color-mix(in srgb , ${vars.element.stroke}, transparent min(${bgTransparency}, 30%))`,
55
- background: `color-mix(in srgb , ${vars.element.fill}, transparent ${bgTransparency})`,
72
+ boxShadow: 'none',
73
+ borderColor: `color-mix(in srgb , ${vars.element.stroke}, transparent ${fallbackVar(varBorderTransparency, '10%')})`,
56
74
  backgroundClip: 'padding-box',
57
- // transitionTimingFunction: 'ease-in',
58
- // transition: 'all 150ms ease-in-out',
59
75
  vars: {
60
- [bgTransparency]: `calc(${fallbackVar(varTransparency, '80%')} - ${bgTransparencyDelta})`,
61
- [bgTransparencyDelta]: '0%',
76
+ [opacityDeltaOnHover]: '0',
62
77
  '--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`
63
78
  },
79
+ ':before': {
80
+ transitionDelay: '200ms',
81
+ opacity: calc.add(fallbackVar(varOpacity, '1'), opacityDeltaOnHover)
82
+ },
64
83
  selectors: {
65
- [`:where(.react-flow__node.selected) &`]: {
66
- transitionDelay: '50ms',
67
- // borderColor: 'transparent',
68
- // transitionTimingFunction: 'ease-out',
69
- vars: {
70
- [bgTransparencyDelta]: '5%'
71
- }
72
- },
84
+ // [`:where(.react-flow__node.selected) &`]: {
85
+ // vars: {
86
+ // [opacityDeltaOnHover]: '.05'
87
+ // }
88
+ // },
73
89
  [`:where([data-hovered]) &`]: {
74
- transitionDelay: '175ms',
75
- // transitionTimingFunction: 'ease-out',
76
90
  vars: {
77
- [bgTransparencyDelta]: '15%'
91
+ [opacityDeltaOnHover]: '.07'
78
92
  }
79
93
  }
80
94
  }
@@ -93,15 +107,10 @@ export const title = style({
93
107
  lineHeight: 1,
94
108
  opacity: 0.75,
95
109
  color: `var(--_compound-title-color,${vars.compound.titleColor})`,
96
- paddingLeft: 12,
97
- selectors: {
98
- [`:where([data-likec4-navigable='true']) &`]: {
99
- paddingLeft: 26
100
- }
101
- // [`:where([data-mantine-color-scheme='light']) &`]: {
102
- // opacity: 1
103
- // }
104
- }
110
+ paddingLeft: 12
111
+ })
112
+ export const titleWithNavigation = style({
113
+ paddingLeft: 26
105
114
  })
106
115
 
107
116
  globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transparent)`, {
@@ -114,8 +123,7 @@ globalStyle(`:where([data-mantine-color-scheme='light'] .likec4-compound-transpa
114
123
  opacity: 1
115
124
  })
116
125
 
117
- const indicatorKeyframes = generateIdentifier('indicator')
118
- globalKeyframes(indicatorKeyframes, {
126
+ const indicatorKeyframes = keyframes({
119
127
  'from': {
120
128
  opacity: 0.6
121
129
  },
@@ -204,7 +212,7 @@ export const navigateBtn = style({
204
212
  },
205
213
  ':hover': {
206
214
  boxShadow: mantine.shadows.lg,
207
- transform: 'scale(1.466)',
215
+ transform: 'scale(1.42)',
208
216
  opacity: 1,
209
217
  transitionDelay: '0'
210
218
  },
@@ -2,13 +2,14 @@ import { Box, Text } from '@mantine/core'
2
2
  import { assignInlineVars } from '@vanilla-extract/dynamic'
3
3
  import { Handle, type NodeProps, Position } from '@xyflow/react'
4
4
  import clsx from 'clsx'
5
+ import { DEV } from 'esm-env'
5
6
  import { deepEqual as eq } from 'fast-equals'
6
7
  import { memo } from 'react'
7
8
  import { clamp } from 'remeda'
9
+ import useTilg from 'tilg'
8
10
  import { useDiagramState } from '../../../state'
9
11
  import type { CompoundXYFlowNode } from '../../types'
10
12
  import { NavigateToBtn } from '../shared/NavigateToBtn'
11
- import { compoundBody, container, indicator, navigateBtn, title } from './CompoundNode.css'
12
13
  import * as css from './CompoundNode.css'
13
14
 
14
15
  type CompoundNodeProps = Pick<
@@ -16,13 +17,12 @@ type CompoundNodeProps = Pick<
16
17
  'id' | 'data' | 'width' | 'height' | 'selected'
17
18
  >
18
19
 
19
- // @ts-ignore
20
20
  const isEqualProps = (prev: CompoundNodeProps, next: CompoundNodeProps) => (
21
21
  prev.id === next.id
22
- && prev.selected === next.selected
23
- && prev.width === next.width
24
- && prev.height === next.height
25
- && eq(prev.data, next.data)
22
+ // && prev.selected === next.selected
23
+ // && eq(prev.width, next.width)
24
+ // && eq(prev.height, next.height)
25
+ && eq(prev.data.element, next.data.element)
26
26
  )
27
27
 
28
28
  export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function CompoundNode({
@@ -31,11 +31,18 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
31
31
  element
32
32
  }
33
33
  }) {
34
- // useTilg()
34
+ DEV && useTilg()
35
35
  const { color, style, depth = 1, ...compound } = element
36
36
  // const w = toDomPrecision(width ?? compound.width)
37
37
  // const h = toDomPrecision(height ?? compound.height)
38
- const opacity = style.opacity ?? 100
38
+ const opacity = clamp((style.opacity ?? 100) / 100, {
39
+ min: 0,
40
+ max: 1
41
+ })
42
+ const borderTransparency = clamp(50 - opacity * 50, {
43
+ min: 0,
44
+ max: 50
45
+ })
39
46
 
40
47
  const { isHovered, isDimmed, hasOnNavigateTo } = useDiagramState(s => ({
41
48
  isHovered: s.hoveredNodeId === id,
@@ -44,30 +51,19 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
44
51
  }))
45
52
  const isnavigable = !!compound.navigateTo && hasOnNavigateTo
46
53
 
47
- // invert opacity to transparency
48
- const transparency = clamp(100 - opacity, {
49
- min: 0,
50
- max: 99
51
- })
52
-
53
54
  return (
54
55
  <Box
55
56
  className={clsx(
56
57
  css.container,
57
58
  'likec4-compound-node',
58
- opacity < 100 && 'likec4-compound-transparent',
59
+ opacity < 1 && 'likec4-compound-transparent',
59
60
  isDimmed && css.dimmed
60
61
  )}
61
62
  mod={{
62
63
  'compound-depth': depth,
63
64
  'likec4-color': color,
64
- 'likec4-shape': compound.shape,
65
- 'likec4-navigable': isnavigable,
66
65
  hovered: isHovered
67
66
  }}
68
- style={assignInlineVars({
69
- [css.varTransparency]: transparency + '%'
70
- })}
71
67
  >
72
68
  <Handle
73
69
  type="target"
@@ -84,14 +80,28 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
84
80
  />
85
81
  </svg>
86
82
  <div
87
- className={clsx(css.compoundBody, opacity < 100 && css.transparent, 'likec4-compound')}
88
- style={{
89
- borderStyle: opacity < 100 ? style.border : undefined
90
- }}
83
+ className={clsx(
84
+ css.compoundBody,
85
+ opacity < 1 && css.transparent,
86
+ 'likec4-compound'
87
+ )}
88
+ style={opacity < 1
89
+ ? {
90
+ ...assignInlineVars({
91
+ [css.varBorderTransparency]: `${borderTransparency}%`,
92
+ [css.varOpacity]: opacity.toFixed(2)
93
+ }),
94
+ borderStyle: style.border ?? 'dashed'
95
+ }
96
+ : undefined}
91
97
  >
92
98
  <Text
93
99
  component="div"
94
- className={clsx(css.title, 'likec4-compound-title')}>
100
+ className={clsx(
101
+ css.title,
102
+ isnavigable && css.titleWithNavigation,
103
+ 'likec4-compound-title'
104
+ )}>
95
105
  {compound.title}
96
106
  </Text>
97
107
  </div>
@@ -103,4 +113,4 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
103
113
  />
104
114
  </Box>
105
115
  )
106
- })
116
+ }, isEqualProps)
@@ -1,8 +1,12 @@
1
1
  import { Text } from '@mantine/core'
2
2
  import { Handle, type NodeProps, Position } from '@xyflow/react'
3
3
  import clsx from 'clsx'
4
- import { motion, type Variants } from 'framer-motion'
4
+ import { DEV } from 'esm-env'
5
+ import { deepEqual as eq } from 'fast-equals'
6
+ import { m, type Variants } from 'framer-motion'
7
+ import { memo } from 'react'
5
8
  import { isNumber } from 'remeda'
9
+ import useTilg from 'tilg'
6
10
  import { useDiagramState } from '../../../state'
7
11
  import type { ElementXYFlowNode } from '../../types'
8
12
  import { toDomPrecision } from '../../utils'
@@ -20,13 +24,13 @@ const variants = {
20
24
  idle: (_, { scale }) => ({
21
25
  scale: 1,
22
26
  transition: {
23
- delay: isNumber(scale) && scale > selectedScale ? 0.075 : 0
27
+ delay: isNumber(scale) && scale > selectedScale ? 0.09 : 0
24
28
  }
25
29
  }),
26
30
  selected: (_, { scale }) => ({
27
31
  scale: selectedScale,
28
32
  transition: {
29
- delay: isNumber(scale) && scale > selectedScale ? 0.075 : 0
33
+ delay: isNumber(scale) && scale > selectedScale ? 0.09 : 0
30
34
  }
31
35
  }),
32
36
  // dimmed: {
@@ -50,12 +54,9 @@ const variants = {
50
54
  // delay: 1
51
55
  // }
52
56
  // }),
53
- hovered: (_, { scale }) => ({
54
- scale: 1.06,
55
- transition: {
56
- delay: isNumber(scale) && scale !== 1 && scale !== selectedScale ? 0 : 0.15
57
- }
58
- }),
57
+ hovered: {
58
+ scale: 1.06
59
+ },
59
60
  tap: {
60
61
  scale: 0.975
61
62
  }
@@ -64,7 +65,16 @@ const variants = {
64
65
  // }
65
66
  } satisfies Variants
66
67
 
67
- export function ElementNode({
68
+ const isEqualProps = (prev: ElementNodeProps, next: ElementNodeProps) => (
69
+ prev.id === next.id
70
+ && eq(prev.selected ?? false, next.selected ?? false)
71
+ && eq(prev.dragging ?? false, next.dragging ?? false)
72
+ && eq(prev.width ?? 0, next.width ?? 0)
73
+ && eq(prev.height ?? 0, next.height ?? 0)
74
+ && eq(prev.data.element, next.data.element)
75
+ )
76
+
77
+ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
68
78
  id,
69
79
  data: {
70
80
  element
@@ -73,7 +83,8 @@ export function ElementNode({
73
83
  selected = false,
74
84
  width,
75
85
  height
76
- }: ElementNodeProps) {
86
+ }) {
87
+ DEV && useTilg()
77
88
  const { isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive } = useDiagramState(s => ({
78
89
  isHovered: s.hoveredNodeId === id,
79
90
  isDimmed: s.dimmed.has(id),
@@ -109,13 +120,15 @@ export function ElementNode({
109
120
  // useTilg(animate, isHovered, isInteractive)
110
121
 
111
122
  return (
112
- <motion.div
123
+ <m.div
113
124
  id={id}
114
125
  className={clsx([
115
126
  css.container,
116
127
  isDimmed && css.dimmed,
128
+ animate !== 'idle' && css.containerAnimated,
117
129
  'likec4-element-node'
118
130
  ])}
131
+ data-hovered={!dragging && isHovered}
119
132
  data-likec4-color={element.color}
120
133
  data-likec4-shape={element.shape}
121
134
  variants={variants}
@@ -225,6 +238,6 @@ export function ElementNode({
225
238
  }
226
239
  {isHovercards && element.links && <ElementLink element={element} />}
227
240
  {isNavigable && <NavigateToBtn xynodeId={id} className={css.cssNavigateBtn} />}
228
- </motion.div>
241
+ </m.div>
229
242
  )
230
- }
243
+ }, isEqualProps)
@@ -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 } from '../../../theme.css'
5
5
 
@@ -8,26 +8,40 @@ const elPadding = createVar('el-padding')
8
8
  // assignVars
9
9
 
10
10
  export const container = style({
11
- position: 'relative',
12
11
  width: '100%',
13
12
  height: '100%',
14
13
  padding: 0,
15
14
  margin: 0,
16
- backfaceVisibility: 'hidden',
15
+ display: 'flex',
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
- globalStyle(`.react-flow__node:has(${dimmed})`, {
25
- filter: vars.filterDimmed,
26
- transition: 'filter 800ms ease-out'
36
+ globalStyle(`.react-flow__node-element:has(${dimmed})`, {
37
+ opacity: 0.25,
38
+ transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
39
+ transitionDelay: '200ms',
40
+ filter: `grayscale(0.85) ${fallbackVar(vars.safariAnimationHook, 'blur(2px)')}`,
41
+ willChange: 'opacity, filter'
27
42
  })
28
43
 
29
- const indicatorKeyframes = generateIdentifier('indicator')
30
- globalKeyframes(indicatorKeyframes, {
44
+ const indicatorKeyframes = keyframes({
31
45
  'from': {
32
46
  opacity: 0.6
33
47
  },
@@ -47,22 +61,26 @@ export const indicator = style({
47
61
  transformOrigin: 'center center',
48
62
  strokeWidth: 6,
49
63
  animationDuration: '800ms',
50
- animationName: indicatorKeyframes,
51
64
  animationIterationCount: 'infinite',
52
65
  animationDirection: 'alternate',
66
+ opacity: 0.6,
53
67
  visibility: 'hidden',
54
68
  selectors: {
55
69
  ':where(.react-flow__node.selected) &': {
56
- visibility: 'visible'
70
+ visibility: 'visible',
71
+ animationName: fallbackVar(vars.safariAnimationHook, indicatorKeyframes)
57
72
  },
58
73
  [`:where(.react-flow__node:focus-visible, ${container}:focus-visible) &`]: {
59
- animationName: 'none',
60
74
  strokeWidth: 10,
61
75
  stroke: outlineColor,
62
76
  visibility: 'visible'
63
77
  },
64
78
  ':where([data-likec4-shape="queue"], [data-likec4-shape="cylinder"], [data-likec4-shape="storage"]) &': {
65
79
  strokeWidth: 10
80
+ },
81
+ [`${dimmed} &`]: {
82
+ visibility: 'hidden',
83
+ display: 'none'
66
84
  }
67
85
  }
68
86
  })
@@ -128,9 +146,7 @@ export const technology = style({
128
146
  })
129
147
 
130
148
  export const element = style({
131
- position: 'relative',
132
- width: '100%',
133
- height: '100%',
149
+ flex: '1',
134
150
  padding: elPadding,
135
151
  display: 'flex',
136
152
  flexDirection: 'column',
@@ -165,10 +181,6 @@ export const elementIcon = style({
165
181
  flex: `0 1 ${iconMaxH}`,
166
182
  maxHeight: iconMaxH,
167
183
  marginTop: -8,
168
- // display: 'flex',
169
- // justifyContent: 'flex-end',
170
- // flexDirection: 'column',
171
- // alignItems: 'center',
172
184
  position: 'relative',
173
185
  userSelect: 'none',
174
186
  pointerEvents: 'none',
@@ -203,6 +215,7 @@ export const cssShapeSvg = style({
203
215
  filter: fallbackVar(filterShadowHovered, filterShadow),
204
216
  transition: 'filter 300ms ease-out',
205
217
  transitionDelay: '0ms',
218
+ zIndex: -1,
206
219
  vars: {
207
220
  [filterShadow]: `
208
221
  drop-shadow(0 2px 1px rgba(0, 0, 0, 0.21))
@@ -1 +1 @@
1
- export { ElementNode } from './ElementNode'
1
+ export { ElementNodeMemo as ElementNode } from './ElementNode'
@@ -1,6 +1,6 @@
1
1
  import { ActionIcon } from '@mantine/core'
2
+ import { IconZoomScan } from '@tabler/icons-react'
2
3
  import clsx from 'clsx'
3
- import { ZoomIn } from '../../../icons'
4
4
  import { useDiagramStoreApi } from '../../../state'
5
5
 
6
6
  export type NavigateToBtnProps = {
@@ -13,7 +13,7 @@ export function NavigateToBtn({ xynodeId, className }: NavigateToBtnProps) {
13
13
  return (
14
14
  <ActionIcon
15
15
  className={clsx('nodrag nopan', className)}
16
- radius="xl"
16
+ radius="md"
17
17
  onPointerDownCapture={e => e.stopPropagation()}
18
18
  onClick={event => {
19
19
  event.stopPropagation()
@@ -21,7 +21,11 @@ export function NavigateToBtn({ xynodeId, className }: NavigateToBtnProps) {
21
21
  }}
22
22
  onDoubleClick={event => event.stopPropagation()}
23
23
  >
24
- <ZoomIn />
24
+ <IconZoomScan
25
+ style={{
26
+ width: '75%',
27
+ height: '75%'
28
+ }} />
25
29
  </ActionIcon>
26
30
  )
27
31
  }
@@ -1,4 +1,4 @@
1
- import type { BBox, DiagramEdge, DiagramNode, Fqn, Point } from '@likec4/core'
1
+ import type { BBox, DiagramEdge, DiagramNode, Fqn, Point, ViewID, XYPosition } from '@likec4/core'
2
2
  import type {
3
3
  Edge,
4
4
  EdgeMouseHandler,
@@ -57,7 +57,10 @@ export namespace XYFlowNode {
57
57
 
58
58
  export type RelationshipData = {
59
59
  edge: DiagramEdge
60
- controlPoints: Point[]
60
+ // to replace edges on view change
61
+ // viewId: ViewID
62
+ // if set - edge was changed by user
63
+ controlPoints: XYPosition[] | null
61
64
  headPoint: Point | null
62
65
  tailPoint: Point | null
63
66
  type: 'bezier' | 'poly'
@@ -1,4 +1,8 @@
1
+ import { hasAtLeast, invariant } from '@likec4/core'
2
+ import type { DiagramEdge, NonEmptyArray, Point } from '@likec4/core/types'
1
3
  import type { XYPosition } from '@xyflow/react'
4
+ import { Bezier } from 'bezier-js'
5
+ import { isArray } from 'remeda'
2
6
 
3
7
  export function toDomPrecision(v: number | null) {
4
8
  if (v === null) {
@@ -10,3 +14,47 @@ export function toDomPrecision(v: number | null) {
10
14
  export function distance(a: XYPosition, b: XYPosition) {
11
15
  return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
12
16
  }
17
+
18
+ export function bezierControlPoints(diagramEdge: DiagramEdge) {
19
+ let [start, ...bezierPoints] = diagramEdge.points
20
+ invariant(start, 'start should be defined')
21
+ const handles = [
22
+ // start
23
+ ] as XYPosition[]
24
+
25
+ while (hasAtLeast(bezierPoints, 3)) {
26
+ const [cp1, cp2, end, ...rest] = bezierPoints
27
+ const bezier = new Bezier(start[0], start[1], cp1[0], cp1[1], cp2[0], cp2[1], end[0], end[1])
28
+ // TODO: temporary, we need correcly derive catmull-rom from bezier. Actually, from poly-bezier
29
+ const inflections = bezier.inflections()
30
+ if (inflections.length === 0) {
31
+ inflections.push(0.5)
32
+ }
33
+ inflections.forEach(t => {
34
+ const { x, y } = bezier.get(t)
35
+ handles.push({
36
+ x: toDomPrecision(x),
37
+ y: toDomPrecision(y)
38
+ })
39
+ })
40
+ bezierPoints = rest
41
+ start = end
42
+ }
43
+ invariant(bezierPoints.length === 0, 'all points should be consumed')
44
+ if (diagramEdge.dir === 'back' && handles.length > 0) {
45
+ handles.reverse()
46
+ }
47
+
48
+ return handles
49
+ }
50
+
51
+ // If points are within 3px, consider them the same
52
+ const isClose = (a: number, b: number) => {
53
+ return Math.abs(a - b) < 3.1
54
+ }
55
+
56
+ export function isSamePoint(a: XYPosition | Point, b: XYPosition | Point) {
57
+ const [ax, ay] = isArray(a) ? a : [a.x, a.y]
58
+ const [bx, by] = isArray(b) ? b : [b.x, b.y]
59
+ return isClose(ax, bx) && isClose(ay, by)
60
+ }
@@ -1,2 +0,0 @@
1
- export * from './DynamicViewWalkthrough';
2
- export { default as OptionsPanel } from './OptionsPanel';
package/src/ui/index.ts DELETED
@@ -1,3 +0,0 @@
1
- // export { default as Camera } from './Camera'
2
- export * from './DynamicViewWalkthrough'
3
- export { default as OptionsPanel } from './OptionsPanel'