@likec4/diagram 1.9.0 → 1.12.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 (113) hide show
  1. package/package.json +20 -43
  2. package/src/LikeC4CustomColors.tsx +5 -2
  3. package/src/LikeC4Diagram.css.ts +1 -1
  4. package/src/LikeC4Diagram.props.ts +40 -94
  5. package/src/LikeC4Diagram.tsx +14 -4
  6. package/src/LikeC4ModelProvider.tsx +21 -0
  7. package/src/StaticLikeC4Diagram.tsx +1 -0
  8. package/src/globals.css.ts +2 -3
  9. package/src/hooks/index.ts +1 -0
  10. package/src/hooks/useMantinePortalProps.ts +8 -0
  11. package/src/hooks/useXYFlow.ts +1 -2
  12. package/src/index.ts +9 -0
  13. package/src/likec4model/LikeC4ModelContext.ts +4 -0
  14. package/src/likec4model/index.ts +1 -0
  15. package/src/likec4model/useLikeC4Model.ts +58 -0
  16. package/src/state/diagram-to-xyflow.ts +1 -2
  17. package/src/state/diagramStore.ts +185 -66
  18. package/src/{theme.css.ts → theme-vars.ts} +11 -4
  19. package/src/ui/DiagramTitlePanel.css.ts +2 -3
  20. package/src/ui/DiagramTitlePanel.tsx +25 -3
  21. package/src/ui/DynamicViewWalkthrough.css.ts +28 -2
  22. package/src/ui/DynamicViewWalkthrough.tsx +97 -52
  23. package/src/ui/OptionsPanel.css.ts +1 -2
  24. package/src/ui/OptionsPanel.tsx +5 -2
  25. package/src/ui/notation/NotationPanel.css.ts +2 -3
  26. package/src/ui/notation/NotationPanel.tsx +10 -11
  27. package/src/ui/options/NodeOptions.tsx +4 -1
  28. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +122 -71
  29. package/src/ui/top-left/TopLeftPanel.tsx +128 -117
  30. package/src/ui/top-left/_shared.ts +7 -6
  31. package/src/ui/top-left/styles.css.ts +48 -8
  32. package/src/utils/vector.ts +57 -0
  33. package/src/xyflow/FitviewOnDiagramChange.tsx +5 -4
  34. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +7 -5
  35. package/src/xyflow/XYFlow.tsx +13 -17
  36. package/src/xyflow/XYFlowEvents.tsx +58 -58
  37. package/src/xyflow/XYFlowInner.tsx +4 -8
  38. package/src/xyflow/edges/EdgeLabel.tsx +185 -0
  39. package/src/xyflow/edges/RelationshipEdge.tsx +170 -137
  40. package/src/xyflow/edges/RelationshipsDropdownMenu.css.ts +60 -0
  41. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +214 -0
  42. package/src/xyflow/edges/edges.css.ts +75 -7
  43. package/src/xyflow/nodes/compound/CompoundNode.css.ts +1 -2
  44. package/src/xyflow/nodes/compound/CompoundNode.tsx +25 -8
  45. package/src/xyflow/nodes/element/ElementLink.css.ts +1 -1
  46. package/src/xyflow/nodes/element/ElementLink.tsx +4 -8
  47. package/src/xyflow/nodes/element/ElementNode.tsx +47 -8
  48. package/src/xyflow/nodes/element/element.css.ts +1 -2
  49. package/src/xyflow/nodes/shared/Toolbar.tsx +426 -0
  50. package/src/xyflow/types.ts +1 -24
  51. package/src/xyflow/useLayoutConstraints.ts +10 -6
  52. package/dist/LikeC4CustomColors.d.ts +0 -3
  53. package/dist/LikeC4CustomColors.props.d.ts +0 -5
  54. package/dist/LikeC4Diagram.css.d.ts +0 -4
  55. package/dist/LikeC4Diagram.d.ts +0 -7
  56. package/dist/LikeC4Diagram.props.d.ts +0 -154
  57. package/dist/StaticLikeC4Diagram.d.ts +0 -4
  58. package/dist/globals.css.d.ts +0 -1
  59. package/dist/hooks/index.d.ts +0 -4
  60. package/dist/hooks/useDiagramState.d.ts +0 -6
  61. package/dist/hooks/useSetState.d.ts +0 -3
  62. package/dist/hooks/useUpdateEffect.d.ts +0 -5
  63. package/dist/hooks/useXYFlow.d.ts +0 -267
  64. package/dist/index.d.ts +0 -9
  65. package/dist/index.mjs +0 -3684
  66. package/dist/mantine/EnsureMantine.d.ts +0 -8
  67. package/dist/mantine.css.d.ts +0 -3
  68. package/dist/state/DiagramContext.d.ts +0 -1192
  69. package/dist/state/diagram-to-xyflow.d.ts +0 -12
  70. package/dist/state/diagramStore.d.ts +0 -1268
  71. package/dist/style.css +0 -2139
  72. package/dist/theme.css.d.ts +0 -63
  73. package/dist/ui/DiagramTitlePanel.css.d.ts +0 -4
  74. package/dist/ui/DiagramTitlePanel.d.ts +0 -1
  75. package/dist/ui/DynamicViewWalkthrough.css.d.ts +0 -3
  76. package/dist/ui/DynamicViewWalkthrough.d.ts +0 -1
  77. package/dist/ui/OptionsPanel.css.d.ts +0 -1
  78. package/dist/ui/OptionsPanel.d.ts +0 -1
  79. package/dist/ui/notation/NotationPanel.css.d.ts +0 -6
  80. package/dist/ui/notation/NotationPanel.d.ts +0 -1
  81. package/dist/ui/notation/index.d.ts +0 -1
  82. package/dist/ui/options/EdgeOptions.d.ts +0 -19
  83. package/dist/ui/options/NodeOptions.d.ts +0 -19
  84. package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +0 -3
  85. package/dist/ui/top-left/TopLeftPanel.d.ts +0 -4
  86. package/dist/ui/top-left/_shared.d.ts +0 -5
  87. package/dist/ui/top-left/styles.css.d.ts +0 -3
  88. package/dist/xyflow/EdgeMarkers.d.ts +0 -12
  89. package/dist/xyflow/FitviewOnDiagramChange.d.ts +0 -4
  90. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +0 -1
  91. package/dist/xyflow/XYFlow.d.ts +0 -12
  92. package/dist/xyflow/XYFlowBackground.d.ts +0 -11
  93. package/dist/xyflow/XYFlowEvents.d.ts +0 -161
  94. package/dist/xyflow/XYFlowInner.d.ts +0 -10
  95. package/dist/xyflow/const.d.ts +0 -7
  96. package/dist/xyflow/edges/RelationshipEdge.d.ts +0 -32
  97. package/dist/xyflow/edges/edges.css.d.ts +0 -12
  98. package/dist/xyflow/edges/utils.d.ts +0 -17
  99. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -11
  100. package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -6
  101. package/dist/xyflow/nodes/compound/index.d.ts +0 -1
  102. package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -3
  103. package/dist/xyflow/nodes/element/ElementLink.d.ts +0 -7
  104. package/dist/xyflow/nodes/element/ElementNode.d.ts +0 -6
  105. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +0 -18
  106. package/dist/xyflow/nodes/element/element.css.d.ts +0 -19
  107. package/dist/xyflow/nodes/element/index.d.ts +0 -1
  108. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -5
  109. package/dist/xyflow/types.d.ts +0 -50
  110. package/dist/xyflow/useLayoutConstraints.d.ts +0 -9
  111. package/dist/xyflow/utils.d.ts +0 -18
  112. package/src/mantine.css.ts +0 -7
  113. package/src/reset.d.ts +0 -2
@@ -1,4 +1,5 @@
1
- import { Box, Button, ButtonGroup } from '@mantine/core'
1
+ import { nonNullable } from '@likec4/core'
2
+ import { Badge, Box, Button, ButtonGroup } from '@mantine/core'
2
3
  import { useHotkeys } from '@mantine/hooks'
3
4
  import {
4
5
  IconPlayerPlayFilled,
@@ -7,8 +8,9 @@ import {
7
8
  IconPlayerStopFilled
8
9
  } from '@tabler/icons-react'
9
10
  import clsx from 'clsx'
10
- import { isNullish, isNumber } from 'remeda'
11
- import { useDiagramState, useXYStore } from '../hooks'
11
+ import { AnimatePresence, m } from 'framer-motion'
12
+ import { first, isNonNull, isTruthy } from 'remeda'
13
+ import { useDiagramState, useDiagramStoreApi, useXYStore } from '../hooks'
12
14
  import * as css from './DynamicViewWalkthrough.css'
13
15
 
14
16
  export function DynamicViewWalkthrough() {
@@ -16,16 +18,19 @@ export function DynamicViewWalkthrough() {
16
18
  const {
17
19
  nextDynamicStep,
18
20
  stopDynamicView,
19
- activeDynamicViewStep,
20
- hasNextSteps
21
+ isActive,
22
+ isParallel,
23
+ hasNext,
24
+ hasPrevious
21
25
  } = useDiagramState(s => ({
22
26
  nextDynamicStep: s.nextDynamicStep,
23
- stopDynamicView: s.stopDynamicView,
24
- activeDynamicViewStep: s.activeDynamicViewStep,
25
- hasNextSteps: (s.activeDynamicViewStep ?? 1) < s.view.edges.length
27
+ stopDynamicView: s.stopWalkthrough,
28
+ isActive: isNonNull(s.activeWalkthrough),
29
+ isParallel: isTruthy(s.activeWalkthrough?.parallelPrefix),
30
+ hasNext: s.activeWalkthrough?.hasNext ?? false,
31
+ hasPrevious: s.activeWalkthrough?.hasPrevious ?? false
26
32
  }))
27
-
28
- const isActive = isNumber(activeDynamicViewStep)
33
+ const diagramApi = useDiagramStoreApi()
29
34
 
30
35
  useHotkeys(
31
36
  isActive
@@ -49,58 +54,98 @@ export function DynamicViewWalkthrough() {
49
54
  radius: isMobile ? 'lg' : 'xl'
50
55
  }
51
56
 
57
+ const startWalkthrough = (e: React.MouseEvent) => {
58
+ e.stopPropagation()
59
+ const {
60
+ xyedges,
61
+ activateWalkthrough
62
+ } = diagramApi.getState()
63
+ const firstEdge = nonNullable(first(xyedges), 'expected at least one edge')
64
+ activateWalkthrough(firstEdge.data.edge.id)
65
+ }
66
+
52
67
  const nextStep = (increment = 1) => (e: React.MouseEvent) => {
53
68
  e.stopPropagation()
54
69
  nextDynamicStep(increment)
55
70
  }
56
71
 
57
72
  return (
58
- <Box
59
- className={clsx('react-flow__panel', css.container)}
60
- onClick={(e) => e.stopPropagation()}
61
- onDoubleClick={(e) => e.stopPropagation()}
62
- >
63
- {isNullish(activeDynamicViewStep)
64
- && (
65
- <Button
66
- {...buttonProps}
67
- className={clsx(css.buttons, css.btn)}
68
- rightSection={<IconPlayerPlayFilled />}
69
- onClick={nextStep()}
70
- px={'xl'}>
71
- Start
72
- </Button>
73
+ <AnimatePresence>
74
+ {isParallel && (
75
+ <Box
76
+ layout
77
+ key={'parallel-frame'}
78
+ component={m.div}
79
+ initial={{ opacity: 0 }}
80
+ animate={{ opacity: 1 }}
81
+ exit={{ opacity: 0 }}
82
+ className={clsx('react-flow__panel', css.parallelStateFrame)}
83
+ />
84
+ )}
85
+ <Box
86
+ className={clsx('react-flow__panel', css.container)}
87
+ onClick={(e) => e.stopPropagation()}
88
+ onDoubleClick={(e) => e.stopPropagation()}
89
+ >
90
+ {!isActive
91
+ && (
92
+ <Button
93
+ {...buttonProps}
94
+ className={clsx(css.buttons, css.btn)}
95
+ rightSection={<IconPlayerPlayFilled />}
96
+ onClick={startWalkthrough}
97
+ px={'xl'}>
98
+ Start
99
+ </Button>
100
+ )}
101
+ {isParallel && (
102
+ <Badge
103
+ key={'parallel-badge'}
104
+ component={m.div}
105
+ initial={{ opacity: 0.05, transform: 'translateY(20px)' }}
106
+ animate={{ opacity: 1, transform: 'translateY(0)' }}
107
+ exit={{
108
+ opacity: 0,
109
+ transform: 'translateY(20px)'
110
+ }}
111
+ variant="gradient"
112
+ size={isMobile ? 'xs' : 'md'}
113
+ gradient={{ from: 'red', to: 'orange', deg: 90 }}
114
+ radius={'sm'}>
115
+ parallel
116
+ </Badge>
73
117
  )}
74
- {isNumber(activeDynamicViewStep) && (
75
- <ButtonGroup className={css.buttons}>
76
- <Button
77
- {...buttonProps}
78
- pl={'lg'}
79
- disabled={activeDynamicViewStep === 1}
80
- onClick={nextStep(-1)}>
81
- <IconPlayerSkipBackFilled />
82
- </Button>
118
+ {isActive && (
119
+ <ButtonGroup className={css.buttons}>
120
+ <Button
121
+ {...buttonProps}
122
+ pl={'lg'}
123
+ disabled={!hasPrevious}
124
+ onClick={nextStep(-1)}>
125
+ <IconPlayerSkipBackFilled />
126
+ </Button>
83
127
 
84
- <Button
85
- {...buttonProps}
86
- px={hasNextSteps ? 'md' : 'xl'}
87
- onClick={e => {
88
- e.stopPropagation()
89
- stopDynamicView()
90
- }}>
91
- {hasNextSteps && <IconPlayerStopFilled />}
92
- {!hasNextSteps && 'End'}
93
- </Button>
94
- {hasNextSteps && (
95
128
  <Button
96
129
  {...buttonProps}
97
- pr={'lg'}
98
- onClick={nextStep()}>
99
- <IconPlayerSkipForwardFilled />
130
+ px={hasNext ? 'md' : 'xl'}
131
+ onClick={e => {
132
+ e.stopPropagation()
133
+ stopDynamicView()
134
+ }}>
135
+ {hasNext && <IconPlayerStopFilled />}
136
+ {!hasNext && 'End'}
100
137
  </Button>
101
- )}
102
- </ButtonGroup>
103
- )}
104
- </Box>
138
+ {hasNext && (
139
+ <Button
140
+ {...buttonProps}
141
+ pr={'lg'}
142
+ onClick={nextStep()}>
143
+ <IconPlayerSkipForwardFilled />
144
+ </Button>
145
+ )}
146
+ </ButtonGroup>
147
+ )}
148
+ </Box>
149
+ </AnimatePresence>
105
150
  )
106
151
  }
@@ -1,6 +1,5 @@
1
1
  import { fallbackVar, style } from '@vanilla-extract/css'
2
- import { mantine } from '../mantine.css'
3
- import { vars } from '../theme.css'
2
+ import { mantine, vars } from '../theme-vars'
4
3
 
5
4
  export const panel = style({
6
5
  top: fallbackVar(vars.optionsPanel.top, '1rem'),
@@ -8,10 +8,13 @@ import { useDiagramState } from '../hooks/useDiagramState'
8
8
  import { NodeOptions } from './options/NodeOptions'
9
9
  import * as styles from './OptionsPanel.css'
10
10
 
11
- export default function OptionsPanel() {
11
+ /**
12
+ * @deprecated
13
+ */
14
+ export function OptionsPanel() {
12
15
  const { isFocused, viewId } = useDiagramState(s => ({
13
16
  viewId: s.view.id,
14
- isFocused: s.focusedNodeId !== null || s.activeDynamicViewStep !== null
17
+ isFocused: s.focusedNodeId !== null || s.activeWalkthrough !== null
15
18
  }))
16
19
  const [selected, setSelected] = useState<{
17
20
  nodes: string[]
@@ -1,7 +1,6 @@
1
- import { fallbackVar, globalStyle, style } from '@vanilla-extract/css'
1
+ import { fallbackVar, style } from '@vanilla-extract/css'
2
2
  import { calc } from '@vanilla-extract/css-utils'
3
- import { mantine, whereDark } from '../../mantine.css'
4
- import { vars } from '../../theme.css'
3
+ import { mantine, vars, whereDark } from '../../theme-vars'
5
4
  import { stokeFillMix } from '../../xyflow/nodes/element/element.css'
6
5
 
7
6
  export const container = style({
@@ -23,7 +23,8 @@ import { AnimatePresence, m } from 'framer-motion'
23
23
  import { useState } from 'react'
24
24
  import { ceil, isNonNullish, isNullish } from 'remeda'
25
25
  import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
26
- import { vars } from '../../theme.css'
26
+ import { useMantinePortalProps } from '../../hooks/useMantinePortalProps'
27
+ import { vars } from '../../theme-vars'
27
28
  import { ElementShapeSvg } from '../../xyflow/nodes/element/ElementShapeSvg'
28
29
  import * as css from './NotationPanel.css'
29
30
 
@@ -120,8 +121,7 @@ const selector = (s: DiagramState) => ({
120
121
  id: s.view.id,
121
122
  notations: s.view.notation?.elements ?? [],
122
123
  height: s.xystore.getState().height,
123
- isVisible: isNullish(s.focusedNodeId ?? s.activeDynamicViewStep),
124
- target: s.getContainer()
124
+ isVisible: isNullish(s.focusedNodeId ?? s.activeWalkthrough)
125
125
  })
126
126
 
127
127
  export function NotationPanel() {
@@ -129,15 +129,14 @@ export function NotationPanel() {
129
129
  id,
130
130
  notations,
131
131
  isVisible,
132
- height,
133
- target
132
+ height
134
133
  } = useDiagramState(selector)
135
134
  const [isCollapsed, setCollapsed] = useLocalStorage({
136
- key: 'notation-panel-collapsed',
135
+ key: 'notation-webview-collapsed',
137
136
  defaultValue: true
138
137
  })
139
138
  const hasNotations = notations.length > 0
140
- const portalProps = target ? { portalProps: { target } } : { withinPortal: false }
139
+ const portalProps = useMantinePortalProps()
141
140
 
142
141
  return (
143
142
  <AnimatePresence>
@@ -153,7 +152,7 @@ export function NotationPanel() {
153
152
  className={clsx('react-flow__panel', css.container)}>
154
153
  <Tooltip label="View has no notations" color="orange" {...portalProps}>
155
154
  <ThemeIcon
156
- size={'xl'}
155
+ size={'lg'}
157
156
  variant="light"
158
157
  color="orange"
159
158
  radius={'md'}
@@ -176,8 +175,8 @@ export function NotationPanel() {
176
175
  >
177
176
  <Tooltip label="Show notation" color="dark" fz={'xs'} {...portalProps}>
178
177
  <ActionIcon
179
- size={'xl'}
180
- variant="light"
178
+ size={'lg'}
179
+ variant="default"
181
180
  color="gray"
182
181
  radius={'md'}
183
182
  onClick={() => setCollapsed(false)}
@@ -236,7 +235,7 @@ export function NotationPanel() {
236
235
  <ScrollAreaAutosize
237
236
  viewportProps={{
238
237
  style: {
239
- maxHeight: height > 50 ? `min(40vh, ${height - 20}px)` : '40vh'
238
+ maxHeight: `min(40vh, ${Math.max(height - 60, 50)}px)`
240
239
  }
241
240
  }}>
242
241
  <Stack gap={0}>
@@ -57,6 +57,9 @@ export type ColorKey = typeof colors[0]['key']
57
57
 
58
58
  type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
59
59
 
60
+ /**
61
+ * @deprecated
62
+ */
60
63
  export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeIds }) => {
61
64
  // export function NodeOptions(props: { nodes: XYFlowNode[] }) {
62
65
  const diagramApi = useDiagramStoreApi()
@@ -92,7 +95,7 @@ export const NodeOptions = memo<{ selectedNodeIds: string[] }>(({ selectedNodeId
92
95
  return (
93
96
  <Stack gap={'xs'}>
94
97
  <Group wrap="nowrap" align="flex-start">
95
- <Box flex={1}>
98
+ <Box flex={1} miw={0}>
96
99
  <Text fz={rem(9)} fw={'500'} c={'dimmed'}>
97
100
  ELEMENT{rest.length > 0 ? 'S' : ``}
98
101
  </Text>
@@ -1,57 +1,64 @@
1
1
  import type { AutoLayoutDirection } from '@likec4/core'
2
2
  import {
3
- ActionIcon as MantineActionIcon,
4
3
  Box,
4
+ Flex,
5
5
  FloatingIndicator,
6
- Group,
7
6
  Popover,
8
7
  PopoverDropdown,
9
8
  type PopoverProps,
10
9
  PopoverTarget,
11
- Stack,
12
10
  Text,
13
- Tooltip as MantineTooltip,
14
- TooltipGroup
11
+ UnstyledButton
15
12
  } from '@mantine/core'
16
- import { IconArrowBigDownLines, IconLayoutDashboard } from '@tabler/icons-react'
13
+ import { clampUseMovePosition, useMove, type UseMovePosition, useUncontrolled } from '@mantine/hooks'
14
+ import { useDebouncedCallback } from '@react-hookz/web'
15
+ import { IconLayoutDashboard } from '@tabler/icons-react'
17
16
  import { useState } from 'react'
18
- import { useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
17
+ import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
18
+ import { ActionIcon, Tooltip } from './_shared'
19
19
  import * as css from './styles.css'
20
20
 
21
- const ActionIcon = MantineActionIcon.withProps({
22
- size: 'md',
23
- variant: 'subtle',
24
- color: 'gray'
25
- })
26
-
27
- const Tooltip = MantineTooltip.withProps({
28
- color: 'gray',
29
- fz: 'xs',
30
- position: 'right',
31
- openDelay: 300,
32
- label: '',
33
- children: null,
34
- offset: 5,
35
- transitionProps: { transition: 'fade', duration: 200 }
21
+ const selector = (state: DiagramState) => ({
22
+ viewId: state.view.id,
23
+ autoLayout: state.view.autoLayout
36
24
  })
37
25
 
38
26
  export const ChangeAutoLayoutButton = (props: PopoverProps) => {
39
27
  const store = useDiagramStoreApi()
40
28
  const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null)
41
29
  const [controlsRefs, setControlsRefs] = useState<Record<AutoLayoutDirection, HTMLButtonElement | null>>({} as any)
42
- const autoLayout = useDiagramState(s => s.view.autoLayout)
30
+ const {
31
+ autoLayout,
32
+ viewId
33
+ } = useDiagramState(selector)
43
34
 
44
35
  const setControlRef = (name: AutoLayoutDirection) => (node: HTMLButtonElement) => {
45
36
  controlsRefs[name] = node
46
37
  setControlsRefs(controlsRefs)
47
38
  }
48
39
 
49
- const setAutoLayout = (layout: AutoLayoutDirection) => (event: React.MouseEvent<HTMLButtonElement>) => {
40
+ const setAutoLayout = (direction: AutoLayoutDirection) => (event: React.MouseEvent<HTMLButtonElement>) => {
50
41
  event.stopPropagation()
51
42
  store.getState().onChange?.({
52
43
  change: {
53
44
  op: 'change-autolayout',
54
- layout
45
+ layout: {
46
+ ...autoLayout,
47
+ direction
48
+ }
49
+ }
50
+ })
51
+ }
52
+
53
+ const setSpacing = (nodeSep: number, rankSep: number) => {
54
+ store.getState().onChange?.({
55
+ change: {
56
+ op: 'change-autolayout',
57
+ layout: {
58
+ ...autoLayout,
59
+ nodeSep,
60
+ rankSep
61
+ }
55
62
  }
56
63
  })
57
64
  }
@@ -69,63 +76,107 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
69
76
  }}
70
77
  {...props}>
71
78
  <PopoverTarget>
72
- <MantineActionIcon
73
- className="action-icon"
74
- variant="light"
75
- color="gray">
76
- <IconLayoutDashboard stroke={1.3} />
77
- </MantineActionIcon>
79
+ <Tooltip label="Change Auto Layout">
80
+ <ActionIcon>
81
+ <IconLayoutDashboard />
82
+ </ActionIcon>
83
+ </Tooltip>
78
84
  </PopoverTarget>
79
- <PopoverDropdown className="likec4-top-left-panel" p={8} pt={4}>
85
+ <PopoverDropdown className="likec4-top-left-panel" p={8} pt={6}>
80
86
  <Box pos={'relative'} ref={setRootRef}>
81
87
  <FloatingIndicator
82
- target={controlsRefs[autoLayout]}
88
+ target={controlsRefs[autoLayout.direction]}
83
89
  parent={rootRef}
84
90
  className={css.autolayoutIndicator}
85
91
  />
86
92
  <Box mb={10}>
87
93
  <Text inline fz={'xs'} c={'dimmed'} fw={500}>Auto layout:</Text>
88
94
  </Box>
89
- <TooltipGroup openDelay={100}>
90
- <Stack align="center" gap={1}>
91
- <Tooltip label="Top to Bottom">
92
- <ActionIcon
93
- className={css.autolayoutIcon}
94
- ref={setControlRef('TB')}
95
- onClick={setAutoLayout('TB')}>
96
- <IconArrowBigDownLines stroke={1.3} />
97
- </ActionIcon>
98
- </Tooltip>
99
- <Group gap={30}>
100
- <Tooltip label="Left to Right">
101
- <ActionIcon
102
- className={css.autolayoutIcon}
103
- ref={setControlRef('LR')}
104
- onClick={setAutoLayout('LR')}>
105
- <IconArrowBigDownLines stroke={1.3} style={{ rotate: '270deg' }} />
106
- </ActionIcon>
107
- </Tooltip>
108
- <Tooltip label="Right to Left">
109
- <ActionIcon
110
- className={css.autolayoutIcon}
111
- ref={setControlRef('RL')}
112
- onClick={setAutoLayout('RL')}>
113
- <IconArrowBigDownLines stroke={1.3} style={{ rotate: '90deg' }} />
114
- </ActionIcon>
115
- </Tooltip>
116
- </Group>
117
- <Tooltip label="Bottom to Top">
118
- <ActionIcon
119
- className={css.autolayoutIcon}
120
- ref={setControlRef('BT')}
121
- onClick={setAutoLayout('BT')}>
122
- <IconArrowBigDownLines stroke={1.3} style={{ rotate: '180deg' }} />
123
- </ActionIcon>
124
- </Tooltip>
125
- </Stack>
126
- </TooltipGroup>
95
+ <Flex gap={2} wrap={'wrap'} justify={'stretch'} maw={160}>
96
+ <UnstyledButton className={css.autolayoutButton} ref={setControlRef('TB')} onClick={setAutoLayout('TB')}>
97
+ Top-Bottom
98
+ </UnstyledButton>
99
+ <UnstyledButton className={css.autolayoutButton} ref={setControlRef('BT')} onClick={setAutoLayout('BT')}>
100
+ Bottom-Top
101
+ </UnstyledButton>
102
+ <UnstyledButton className={css.autolayoutButton} ref={setControlRef('LR')} onClick={setAutoLayout('LR')}>
103
+ Left-Right
104
+ </UnstyledButton>
105
+ <UnstyledButton className={css.autolayoutButton} ref={setControlRef('RL')} onClick={setAutoLayout('RL')}>
106
+ Right-Left
107
+ </UnstyledButton>
108
+ </Flex>
109
+ <Box my={10}>
110
+ <Text inline fz={'xs'} c={'dimmed'} fw={500}>Spacing:</Text>
111
+ </Box>
112
+ <SpacingSliders
113
+ isVertical={autoLayout.direction === 'TB' || autoLayout.direction === 'BT'}
114
+ key={viewId}
115
+ nodeSep={autoLayout.nodeSep}
116
+ rankSep={autoLayout.rankSep}
117
+ onChange={setSpacing}
118
+ />
127
119
  </Box>
128
120
  </PopoverDropdown>
129
121
  </Popover>
130
122
  )
131
123
  }
124
+
125
+ const SpacingSliders = ({
126
+ isVertical,
127
+ nodeSep,
128
+ rankSep,
129
+ onChange
130
+ }: {
131
+ isVertical: boolean
132
+ nodeSep: number | undefined
133
+ rankSep: number | undefined
134
+ onChange: (nodeSep: number, rankSep: number) => void
135
+ }) => {
136
+ if (!isVertical) {
137
+ ;[nodeSep, rankSep] = [rankSep, nodeSep]
138
+ }
139
+
140
+ const propagateChange = useDebouncedCallback(
141
+ ({ x, y }: UseMovePosition) => {
142
+ if (!isVertical) {
143
+ ;[x, y] = [y, x]
144
+ }
145
+ onChange(Math.round(x * 500), Math.round(y * 500))
146
+ },
147
+ [onChange, isVertical],
148
+ 150,
149
+ 2000
150
+ )
151
+
152
+ const [value, setValue] = useUncontrolled({
153
+ defaultValue: clampUseMovePosition({
154
+ x: (nodeSep ?? 250) / 500,
155
+ y: (rankSep ?? 250) / 500
156
+ }),
157
+ onChange: propagateChange
158
+ })
159
+
160
+ const { ref } = useMove(setValue)
161
+
162
+ let nodeSepValue = Math.round(value.x * 500)
163
+ let rankSepValue = Math.round(value.y * 500)
164
+ if (!isVertical) {
165
+ ;[nodeSepValue, rankSepValue] = [rankSepValue, nodeSepValue]
166
+ }
167
+
168
+ return (
169
+ <Box ref={ref} className={css.spacingSliderBody} pt={'100%'}>
170
+ <Box
171
+ className={css.spacingSliderThumb}
172
+ style={{
173
+ left: `${value.x * 100}%`,
174
+ top: `${value.y * 100}%`
175
+ }}
176
+ />
177
+ <Box pos={'absolute'} left={2} bottom={2}>
178
+ <Text component="div" fz={8} c={'dimmed'} fw={500}>{rankSepValue}, {nodeSepValue}</Text>
179
+ </Box>
180
+ </Box>
181
+ )
182
+ }