@likec4/diagram 1.12.1 → 1.13.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 (49) hide show
  1. package/package.json +9 -9
  2. package/src/LikeC4CustomColors.tsx +2 -2
  3. package/src/LikeC4Diagram.props.ts +15 -7
  4. package/src/LikeC4Diagram.tsx +23 -14
  5. package/src/StaticLikeC4Diagram.tsx +2 -0
  6. package/src/components/Overlay.css.ts +78 -0
  7. package/src/components/Overlay.tsx +102 -0
  8. package/src/components/OverlayContext.tsx +11 -0
  9. package/src/components/index.ts +2 -0
  10. package/src/likec4model/index.ts +1 -0
  11. package/src/likec4model/useLikeC4ElementsTree.ts +44 -0
  12. package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +13 -0
  13. package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +37 -0
  14. package/src/overlays/index.tsx +33 -0
  15. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +33 -0
  16. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +359 -0
  17. package/src/overlays/relationships-of/SelectElement.css.ts +18 -0
  18. package/src/overlays/relationships-of/SelectElement.tsx +133 -0
  19. package/src/overlays/relationships-of/_types.ts +56 -0
  20. package/src/overlays/relationships-of/styles.css.ts +34 -0
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +607 -0
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +76 -0
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +130 -0
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +22 -0
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +59 -0
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +170 -0
  27. package/src/state/diagram-to-xyflow.ts +1 -1
  28. package/src/state/diagramStore.ts +119 -27
  29. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +35 -18
  30. package/src/ui/top-left/ManualLayoutToolsButton.tsx +110 -0
  31. package/src/ui/top-left/TopLeftPanel.tsx +15 -31
  32. package/src/ui/top-left/_shared.ts +2 -2
  33. package/src/xyflow/EdgeMarkers.tsx +19 -0
  34. package/src/xyflow/XYFlowEvents.tsx +21 -24
  35. package/src/xyflow/XYFlowInner.tsx +6 -10
  36. package/src/xyflow/edges/EdgeLabel.tsx +6 -5
  37. package/src/xyflow/edges/RelationshipEdge.tsx +4 -3
  38. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +16 -0
  39. package/src/xyflow/edges/edges.css.ts +5 -11
  40. package/src/xyflow/nodes/compound/CompoundNode.css.ts +17 -40
  41. package/src/xyflow/nodes/compound/CompoundNode.tsx +219 -28
  42. package/src/xyflow/nodes/element/ElementLink.tsx +11 -8
  43. package/src/xyflow/nodes/element/ElementNode.tsx +211 -140
  44. package/src/xyflow/nodes/element/element.css.ts +22 -33
  45. package/src/xyflow/nodes/shared/Toolbar.tsx +81 -27
  46. package/src/xyflow/nodes/use-animate-variants.tsx +64 -0
  47. package/src/xyflow/useLayoutConstraints.ts +95 -125
  48. package/src/xyflow/utils.ts +4 -0
  49. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +0 -32
@@ -10,10 +10,17 @@ import {
10
10
  Text,
11
11
  UnstyledButton
12
12
  } from '@mantine/core'
13
- import { clampUseMovePosition, useMove, type UseMovePosition, useUncontrolled } from '@mantine/hooks'
13
+ import {
14
+ clampUseMovePosition,
15
+ useHover,
16
+ useMergedRef,
17
+ useMove,
18
+ type UseMovePosition,
19
+ useUncontrolled
20
+ } from '@mantine/hooks'
14
21
  import { useDebouncedCallback } from '@react-hookz/web'
15
22
  import { IconLayoutDashboard } from '@tabler/icons-react'
16
- import { useState } from 'react'
23
+ import { forwardRef, useState } from 'react'
17
24
  import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
18
25
  import { ActionIcon, Tooltip } from './_shared'
19
26
  import * as css from './styles.css'
@@ -32,6 +39,8 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
32
39
  viewId
33
40
  } = useDiagramState(selector)
34
41
 
42
+ const { ref, hovered: isSpacingHovered } = useHover()
43
+
35
44
  const setControlRef = (name: AutoLayoutDirection) => (node: HTMLButtonElement) => {
36
45
  controlsRefs[name] = node
37
46
  setControlsRefs(controlsRefs)
@@ -50,7 +59,11 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
50
59
  })
51
60
  }
52
61
 
53
- const setSpacing = (nodeSep: number, rankSep: number) => {
62
+ const setSpacing = (nodeSep: number | null, rankSep: number | null) => {
63
+ // Force fitDiagram
64
+ store.setState({
65
+ viewportChanged: false
66
+ })
54
67
  store.getState().onChange?.({
55
68
  change: {
56
69
  op: 'change-autolayout',
@@ -82,7 +95,7 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
82
95
  </ActionIcon>
83
96
  </Tooltip>
84
97
  </PopoverTarget>
85
- <PopoverDropdown className="likec4-top-left-panel" p={8} pt={6}>
98
+ <PopoverDropdown className="likec4-top-left-panel" p={8} pt={6} opacity={isSpacingHovered ? 0.6 : 1}>
86
99
  <Box pos={'relative'} ref={setRootRef}>
87
100
  <FloatingIndicator
88
101
  target={controlsRefs[autoLayout.direction]}
@@ -110,6 +123,7 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
110
123
  <Text inline fz={'xs'} c={'dimmed'} fw={500}>Spacing:</Text>
111
124
  </Box>
112
125
  <SpacingSliders
126
+ ref={ref}
113
127
  isVertical={autoLayout.direction === 'TB' || autoLayout.direction === 'BT'}
114
128
  key={viewId}
115
129
  nodeSep={autoLayout.nodeSep}
@@ -122,17 +136,18 @@ export const ChangeAutoLayoutButton = (props: PopoverProps) => {
122
136
  )
123
137
  }
124
138
 
125
- const SpacingSliders = ({
126
- isVertical,
127
- nodeSep,
128
- rankSep,
129
- onChange
130
- }: {
139
+ const MAX_SPACING = 400
140
+ const SpacingSliders = forwardRef<HTMLDivElement, {
131
141
  isVertical: boolean
132
142
  nodeSep: number | undefined
133
143
  rankSep: number | undefined
134
144
  onChange: (nodeSep: number, rankSep: number) => void
135
- }) => {
145
+ }>(({
146
+ isVertical,
147
+ nodeSep,
148
+ rankSep,
149
+ onChange
150
+ }, _ref) => {
136
151
  if (!isVertical) {
137
152
  ;[nodeSep, rankSep] = [rankSep, nodeSep]
138
153
  }
@@ -142,7 +157,7 @@ const SpacingSliders = ({
142
157
  if (!isVertical) {
143
158
  ;[x, y] = [y, x]
144
159
  }
145
- onChange(Math.round(x * 500), Math.round(y * 500))
160
+ onChange(Math.round(x * MAX_SPACING), Math.round(y * MAX_SPACING))
146
161
  },
147
162
  [onChange, isVertical],
148
163
  150,
@@ -151,22 +166,24 @@ const SpacingSliders = ({
151
166
 
152
167
  const [value, setValue] = useUncontrolled({
153
168
  defaultValue: clampUseMovePosition({
154
- x: (nodeSep ?? 250) / 500,
155
- y: (rankSep ?? 250) / 500
169
+ x: (nodeSep ?? 100) / MAX_SPACING,
170
+ y: (rankSep ?? 120) / MAX_SPACING
156
171
  }),
157
172
  onChange: propagateChange
158
173
  })
159
174
 
160
175
  const { ref } = useMove(setValue)
161
176
 
162
- let nodeSepValue = Math.round(value.x * 500)
163
- let rankSepValue = Math.round(value.y * 500)
177
+ let nodeSepValue = Math.round(value.x * MAX_SPACING)
178
+ let rankSepValue = Math.round(value.y * MAX_SPACING)
164
179
  if (!isVertical) {
165
180
  ;[nodeSepValue, rankSepValue] = [rankSepValue, nodeSepValue]
166
181
  }
167
182
 
183
+ const mergedRef = useMergedRef(ref, _ref)
184
+
168
185
  return (
169
- <Box ref={ref} className={css.spacingSliderBody} pt={'100%'}>
186
+ <Box ref={mergedRef} className={css.spacingSliderBody} pt={'100%'}>
170
187
  <Box
171
188
  className={css.spacingSliderThumb}
172
189
  style={{
@@ -179,4 +196,4 @@ const SpacingSliders = ({
179
196
  </Box>
180
197
  </Box>
181
198
  )
182
- }
199
+ })
@@ -0,0 +1,110 @@
1
+ import { ActionIconGroup, Group, Popover, PopoverDropdown, type PopoverProps, PopoverTarget } from '@mantine/core'
2
+ import {
3
+ IconLayoutAlignBottom,
4
+ IconLayoutAlignCenter,
5
+ IconLayoutAlignLeft,
6
+ IconLayoutAlignMiddle,
7
+ IconLayoutAlignRight,
8
+ IconLayoutAlignTop,
9
+ IconLayoutCollage,
10
+ IconRouteOff
11
+ } from '@tabler/icons-react'
12
+ import { useDiagramStoreApi } from '../../hooks/useDiagramState'
13
+ import { ActionIcon, Tooltip } from './_shared'
14
+
15
+ const Action = ({
16
+ label,
17
+ icon,
18
+ onClick
19
+ }: {
20
+ label: string
21
+ icon: React.ReactNode
22
+ onClick: React.MouseEventHandler
23
+ }) => (
24
+ <Tooltip label={label} withinPortal={false} position="top">
25
+ <ActionIcon onClick={onClick}>
26
+ {icon}
27
+ </ActionIcon>
28
+ </Tooltip>
29
+ )
30
+
31
+ export const ManualLayoutToolsButton = (props: PopoverProps) => {
32
+ const store = useDiagramStoreApi()
33
+
34
+ return (
35
+ <Popover
36
+ position="right-start"
37
+ clickOutsideEvents={[
38
+ 'pointerdown'
39
+ ]}
40
+ radius="xs"
41
+ shadow="lg"
42
+ offset={{
43
+ mainAxis: 10
44
+ }}
45
+ {...props}>
46
+ <PopoverTarget>
47
+ <Tooltip label="Manual layouting tools" withinPortal={false} position="top-end">
48
+ <ActionIcon>
49
+ <IconLayoutCollage />
50
+ </ActionIcon>
51
+ </Tooltip>
52
+ </PopoverTarget>
53
+ <PopoverDropdown p={0}>
54
+ <Group gap={'xs'}>
55
+ <ActionIconGroup pos={'relative'}>
56
+ <Action
57
+ label="Align left"
58
+ icon={<IconLayoutAlignLeft />}
59
+ onClick={e => {
60
+ e.stopPropagation()
61
+ store.getState().align('Left')
62
+ }} />
63
+ <Action
64
+ label="Align center"
65
+ icon={<IconLayoutAlignCenter />}
66
+ onClick={e => {
67
+ e.stopPropagation()
68
+ store.getState().align('Center')
69
+ }} />
70
+ <Action
71
+ label="Align right"
72
+ icon={<IconLayoutAlignRight />}
73
+ onClick={e => {
74
+ e.stopPropagation()
75
+ store.getState().align('Right')
76
+ }} />
77
+ <Action
78
+ label="Align top"
79
+ icon={<IconLayoutAlignTop />}
80
+ onClick={e => {
81
+ e.stopPropagation()
82
+ store.getState().align('Top')
83
+ }} />
84
+ <Action
85
+ label="Align middle"
86
+ icon={<IconLayoutAlignMiddle />}
87
+ onClick={e => {
88
+ e.stopPropagation()
89
+ store.getState().align('Middle')
90
+ }} />
91
+ <Action
92
+ label="Align bottom"
93
+ icon={<IconLayoutAlignBottom />}
94
+ onClick={e => {
95
+ e.stopPropagation()
96
+ store.getState().align('Bottom')
97
+ }} />
98
+ </ActionIconGroup>
99
+ <Action
100
+ label="Reset all control points"
101
+ icon={<IconRouteOff />}
102
+ onClick={e => {
103
+ e.stopPropagation()
104
+ store.getState().resetEdgeControlPoints()
105
+ }} />
106
+ </Group>
107
+ </PopoverDropdown>
108
+ </Popover>
109
+ )
110
+ }
@@ -10,8 +10,7 @@ import {
10
10
  type PopoverProps,
11
11
  Stack,
12
12
  Text,
13
- TooltipGroup,
14
- type TooltipProps
13
+ TooltipGroup
15
14
  } from '@mantine/core'
16
15
  import {
17
16
  IconAlertTriangle,
@@ -19,8 +18,7 @@ import {
19
18
  IconChevronRight,
20
19
  IconFileSymlink,
21
20
  IconFocusCentered,
22
- IconMenu2,
23
- IconRouteOff
21
+ IconMenu2
24
22
  } from '@tabler/icons-react'
25
23
  import clsx from 'clsx'
26
24
  import { AnimatePresence, LayoutGroup, m } from 'framer-motion'
@@ -30,12 +28,14 @@ import { useMantinePortalProps } from '../../hooks/useMantinePortalProps'
30
28
  import { mantine } from '../../theme-vars'
31
29
  import { ActionIcon, Tooltip } from './_shared'
32
30
  import { ChangeAutoLayoutButton } from './ChangeAutoLayoutButton'
31
+ import { ManualLayoutToolsButton } from './ManualLayoutToolsButton'
33
32
  import * as css from './styles.css'
34
33
 
35
34
  const historySelector = (s: DiagramState) => ({
36
35
  showBurgerMenu: !!s.onBurgerMenuClick,
37
- hasStepBack: s.navigationHistoryIndex > 0,
38
- hasStepForward: s.navigationHistoryIndex < s.navigationHistory.length - 1
36
+ hasStepBack: s.showNavigationButtons && !!s.onNavigateTo && s.navigationHistoryIndex > 0,
37
+ hasStepForward: s.showNavigationButtons && !!s.onNavigateTo
38
+ && s.navigationHistoryIndex < s.navigationHistory.length - 1
39
39
  })
40
40
 
41
41
  const BackwardForwardButtons = () => {
@@ -135,22 +135,6 @@ const LayoutDriftNotification = (props: PopoverProps) => (
135
135
  </HoverCard>
136
136
  )
137
137
 
138
- const ResetControlPointsButton = (props: Omit<TooltipProps, 'label' | 'children'>) => {
139
- const store = useDiagramStoreApi()
140
-
141
- return (
142
- <Tooltip label="Reset all control points" {...props}>
143
- <ActionIcon
144
- onClick={e => {
145
- e.stopPropagation()
146
- store.getState().resetEdgeControlPoints()
147
- }}>
148
- <IconRouteOff />
149
- </ActionIcon>
150
- </Tooltip>
151
- )
152
- }
153
-
154
138
  export const TopLeftPanel = () => {
155
139
  const store = useDiagramStoreApi()
156
140
  const {
@@ -160,25 +144,25 @@ export const TopLeftPanel = () => {
160
144
  showChangeAutoLayout,
161
145
  showGoToSource,
162
146
  viewportChanged,
163
- showResetControlPoints
147
+ showManualLayoutTools
164
148
  } = useDiagramState(s => {
165
149
  const isNotWalkthrough = isNullish(s.activeWalkthrough)
166
150
  const isNotFocused = isNullish(s.focusedNodeId)
167
151
  const isNotActive = isNotWalkthrough && isNotFocused
168
152
  return ({
169
- showNavigationButtons: !!s.onBurgerMenuClick || s.showNavigationButtons && !!s.onNavigateTo,
170
- showFitDiagram: s.fitViewEnabled && s.zoomable && isNotWalkthrough,
171
- showLayoutDriftWarning: !s.readonly && s.view.hasLayoutDrift === true && isNotActive,
172
- showChangeAutoLayout: s.isEditable() && isNotActive,
173
- showGoToSource: !!s.onOpenSourceView && isNotWalkthrough,
153
+ showNavigationButtons: !!s.onBurgerMenuClick || (s.showNavigationButtons && !!s.onNavigateTo),
154
+ showFitDiagram: s.controls && s.fitViewEnabled && s.zoomable && isNotWalkthrough,
155
+ showLayoutDriftWarning: s.controls && !s.readonly && s.view.hasLayoutDrift === true && isNotActive,
156
+ showChangeAutoLayout: s.controls && s.isEditable() && isNotActive,
157
+ showGoToSource: s.controls && !!s.onOpenSourceView && isNotWalkthrough,
174
158
  viewportChanged: s.viewportChanged,
175
- showResetControlPoints: s.readonly !== true && s.experimentalEdgeEditing === true
159
+ showManualLayoutTools: s.controls && s.readonly !== true && s.experimentalEdgeEditing === true
176
160
  })
177
161
  })
178
162
  const portalProps = useMantinePortalProps()
179
163
 
180
164
  return (
181
- <TooltipGroup openDelay={500} closeDelay={150}>
165
+ <TooltipGroup openDelay={600} closeDelay={150}>
182
166
  <Stack
183
167
  className={clsx(
184
168
  'react-flow__panel',
@@ -203,7 +187,7 @@ export const TopLeftPanel = () => {
203
187
  )}
204
188
  {showChangeAutoLayout && <ChangeAutoLayoutButton {...portalProps} />}
205
189
  {showLayoutDriftWarning && <LayoutDriftNotification {...portalProps} />}
206
- {showResetControlPoints && <ResetControlPointsButton {...portalProps} />}
190
+ {showManualLayoutTools && <ManualLayoutToolsButton {...portalProps} />}
207
191
  {showFitDiagram && (
208
192
  <Tooltip label={viewportChanged ? 'Center camera' : 'Camera is centered'} {...portalProps}>
209
193
  <ActionIcon
@@ -4,8 +4,8 @@ export const Tooltip = MantineTooltip.withProps({
4
4
  color: 'dark',
5
5
  fz: 'xs',
6
6
  position: 'right',
7
- openDelay: 400,
8
- closeDelay: 150,
7
+ openDelay: 600,
8
+ closeDelay: 120,
9
9
  label: '',
10
10
  children: null,
11
11
  offset: 8
@@ -39,6 +39,24 @@ const Arrow = (props: SVGProps<SVGMarkerElement>) => (
39
39
  </marker>
40
40
  )
41
41
 
42
+ const Crow = (props: SVGProps<SVGMarkerElement>) => (
43
+ <marker
44
+ viewBox="-1 -1 12 12"
45
+ refX={8}
46
+ refY={4}
47
+ markerWidth="8"
48
+ markerHeight="8"
49
+ preserveAspectRatio="xMaxYMid meet"
50
+ orient="auto-start-reverse"
51
+ {...props}>
52
+ <path
53
+ d="M 8 0 L 0 4 L 8 8 M 8 4 L 0 4"
54
+ fill="none"
55
+ strokeWidth={1}
56
+ />
57
+ </marker>
58
+ )
59
+
42
60
  const OArrow = (props: SVGProps<SVGMarkerElement>) => (
43
61
  <marker
44
62
  viewBox="-1 -1 12 10"
@@ -139,6 +157,7 @@ const ODot = (props: SVGProps<SVGMarkerElement>) => (
139
157
 
140
158
  export const EdgeMarkers = {
141
159
  Arrow,
160
+ Crow,
142
161
  OArrow,
143
162
  Open,
144
163
  Diamond,
@@ -50,12 +50,9 @@ export function useXYFlowEvents() {
50
50
  }
51
51
 
52
52
  const lastClickWasRecent = (ms = 2000) => {
53
- const prevTimestamp = lastClickTimestamp.current
54
- lastClickTimestamp.current = Date.now()
55
- if (prevTimestamp === undefined) {
56
- return false
57
- }
58
- return prevTimestamp + ms > Date.now()
53
+ const prevTimestamp = lastClickTimestamp.current ?? 0
54
+ const now = lastClickTimestamp.current = Date.now()
55
+ return prevTimestamp + ms > now
59
56
  }
60
57
 
61
58
  return ({
@@ -68,16 +65,15 @@ export function useXYFlowEvents() {
68
65
  resetFocusAndLastClicked
69
66
  } = diagramApi.getState()
70
67
  resetFocusAndLastClicked()
71
- if (!onCanvasDblClick) {
72
- event.stopPropagation()
73
- }
74
68
  if (zoomable && viewportChanged) {
75
69
  fitDiagram()
76
70
  }
71
+ if (!onCanvasDblClick) {
72
+ event.stopPropagation()
73
+ }
77
74
  onCanvasDblClick?.(event)
78
75
  },
79
76
  onPaneClick: (event) => {
80
- console.log('onPaneClick')
81
77
  if (dbclickLock()) {
82
78
  return
83
79
  }
@@ -132,7 +128,7 @@ export function useXYFlowEvents() {
132
128
  // if we focused on a node, and clicked on another node - focus on the clicked node
133
129
  const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
134
130
  // if user clicked on the same node twice in a short time, focus on it
135
- const clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent()
131
+ const clickedRecently = lastClickWasRecent() && lastClickedNodeId === xynode.id
136
132
 
137
133
  if (clickedRecently && !!onOpenSourceElement) {
138
134
  onOpenSourceElement(xynode.data.element.id)
@@ -141,27 +137,27 @@ export function useXYFlowEvents() {
141
137
  }
142
138
  }
143
139
 
144
- if (enableFocusMode || nodesSelectable) {
140
+ if (enableFocusMode) {
145
141
  switch (true) {
146
- case enableFocusMode && clickedRecently && focusedNodeId === xynode.id: {
142
+ case clickedRecently && focusedNodeId === xynode.id: {
147
143
  focusOnNode(false)
148
144
  fitDiagram()
149
145
  break
150
146
  }
151
- case enableFocusMode && shallChangeFocus:
152
- case enableFocusMode && clickedRecently && focusedNodeId !== xynode.id: {
147
+ case (shallChangeFocus || clickedRecently): {
153
148
  focusOnNode(xynode.id)
154
149
  break
155
150
  }
156
- case nodesSelectable: {
157
- xystore.getState().addSelectedNodes([xynode.id])
158
- break
159
- }
160
- }
161
- if (!onNodeClick) {
162
- event.stopPropagation()
163
151
  }
152
+ } else if (nodesSelectable) {
153
+ xystore.getState().addSelectedNodes([xynode.id])
154
+ }
155
+
156
+ if (!onNodeClick && (enableFocusMode || nodesSelectable)) {
157
+ event.stopPropagation()
158
+ return
164
159
  }
160
+
165
161
  onNodeClick?.(
166
162
  xynode.data.element,
167
163
  event
@@ -176,16 +172,17 @@ export function useXYFlowEvents() {
176
172
  setLastClickedNode
177
173
  } = diagramApi.getState()
178
174
  setLastClickedNode(xynode.id)
179
- lastClickTimestamp.current = Date.now()
175
+ lastClickWasRecent()
180
176
  if (isNonNullish(focusedNodeId) || enableFocusMode) {
181
177
  // if we are already focused on the node, cancel
182
178
  if (focusedNodeId === xynode.id) {
179
+ focusOnNode(false)
183
180
  fitDiagram()
184
181
  } else {
185
182
  focusOnNode(xynode.id)
186
183
  }
187
- event.stopPropagation()
188
184
  }
185
+ event.stopPropagation()
189
186
  },
190
187
  onEdgeClick: (event, xyedge) => {
191
188
  const {
@@ -1,6 +1,5 @@
1
- import { Controls } from '@xyflow/react'
2
1
  import { shallowEqual } from 'fast-equals'
3
- import { memo, type PropsWithChildren } from 'react'
2
+ import { memo } from 'react'
4
3
  import { isNonNullish } from 'remeda'
5
4
  import { useDiagramState } from '../hooks/useDiagramState'
6
5
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
@@ -10,26 +9,25 @@ import NotationPanel from '../ui/notation'
10
9
  import { TopLeftPanel } from '../ui/top-left/TopLeftPanel'
11
10
  import { XYFlowBackground } from './XYFlowBackground'
12
11
 
13
- type XYFlowInnerProps = PropsWithChildren<{
12
+ type XYFlowInnerProps = {
14
13
  background: NonNullable<LikeC4DiagramProperties['background']>
15
- controls: boolean
16
14
  showDiagramTitle: boolean
17
- }>
15
+ }
18
16
 
19
17
  export const XYFlowInner = /* @__PURE__ */ memo(function XYFlowInnerR({
20
- children,
21
18
  background,
22
- controls,
23
19
  showDiagramTitle
24
20
  }: XYFlowInnerProps) {
25
21
  const {
26
22
  showNotations,
27
23
  isDynamicView,
24
+ showTopLeft,
28
25
  isDynamicViewActive,
29
26
  enableDynamicViewWalkthrough
30
27
  } = useDiagramState(s => ({
31
28
  showNotations: s.showNotations,
32
29
  isDynamicView: s.isDynamicView,
30
+ showTopLeft: s.controls || (s.showNavigationButtons && !!s.onNavigateTo) || !!s.onBurgerMenuClick,
33
31
  enableDynamicViewWalkthrough: s.enableDynamicViewWalkthrough,
34
32
  isDynamicViewActive: isNonNullish(s.activeWalkthrough)
35
33
  }))
@@ -40,12 +38,10 @@ export const XYFlowInner = /* @__PURE__ */ memo(function XYFlowInnerR({
40
38
  return (
41
39
  <>
42
40
  {isBgWithPattern && <XYFlowBackground background={background} />}
43
- {controls && <Controls showInteractive={false} position={'top-center'} />}
44
- <TopLeftPanel />
41
+ {showTopLeft && <TopLeftPanel />}
45
42
  {isDiagramTitleVisible && <DiagramTitlePanel />}
46
43
  {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
47
44
  {showNotations && <NotationPanel />}
48
- {children}
49
45
  </>
50
46
  )
51
47
  }, shallowEqual)
@@ -22,6 +22,7 @@ import { isTruthy } from 'remeda'
22
22
  import { useDiagramState, useDiagramStoreApi, useMantinePortalProps } from '../../hooks'
23
23
  import { useLikeC4Model } from '../../likec4model/useLikeC4Model'
24
24
  import type { RelationshipData } from '../types'
25
+ import { stopPropagation } from '../utils'
25
26
  import * as edgesCss from './edges.css'
26
27
  import { RelationshipsDropdownMenu } from './RelationshipsDropdownMenu'
27
28
 
@@ -125,9 +126,9 @@ const NotePopover = ({ notes, children }: PropsWithChildren<{ notes: string }>)
125
126
  <PopoverDropdown
126
127
  component={Stack}
127
128
  p={'xs'}
128
- onPointerDownCapture={e => e.stopPropagation()}
129
- onClick={e => e.stopPropagation()}
130
- onDoubleClick={e => e.stopPropagation()}
129
+ onPointerDownCapture={stopPropagation}
130
+ onClick={stopPropagation}
131
+ onDoubleClick={stopPropagation}
131
132
  >
132
133
  <ScrollAreaAutosize maw={450} mah={350} type="scroll" mx={'auto'} mt={2}>
133
134
  <Text component="div" className={edgesCss.edgeNoteText} p={4}>{notes}</Text>
@@ -171,13 +172,13 @@ function NavigateToBtn({ viewId }: NavigateToBtnProps) {
171
172
  className={clsx('nodrag nopan', edgesCss.cssNavigateBtn)}
172
173
  size={'sm'}
173
174
  radius="sm"
174
- onPointerDownCapture={e => e.stopPropagation()}
175
+ onPointerDownCapture={stopPropagation}
175
176
  onClick={event => {
176
177
  event.stopPropagation()
177
178
  diagramApi.getState().onNavigateTo?.(viewId, event)
178
179
  }}
179
180
  role="button"
180
- onDoubleClick={event => event.stopPropagation()}
181
+ onDoubleClick={stopPropagation}
181
182
  >
182
183
  <IconZoomScan className={edgesCss.cssNavigateBtnIcon} />
183
184
  </ActionIcon>
@@ -63,8 +63,9 @@ const toMarker = (arrowType?: RelationshipArrowType): EdgeMarkerType | undefined
63
63
  }
64
64
  switch (arrowType) {
65
65
  case 'normal':
66
- case 'crow':
67
66
  return 'Arrow' as const
67
+ case 'crow':
68
+ return 'Crow' as const
68
69
  case 'onormal':
69
70
  return 'OArrow' as const
70
71
  case 'diamond':
@@ -332,7 +333,7 @@ export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function Relationshi
332
333
  const onRmbControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>, domNode: HTMLDivElement) => {
333
334
  const { xyflow } = diagramStore.getState()
334
335
 
335
- if(controlPoints.length <= 1) {
336
+ if (controlPoints.length <= 1) {
336
337
  return
337
338
  }
338
339
 
@@ -349,7 +350,7 @@ export const RelationshipEdge = memo<EdgeProps<XYFlowEdge>>(function Relationshi
349
350
  domNode.removeEventListener('pointerup', onPointerUp, {
350
351
  capture: true
351
352
  })
352
- e.stopPropagation()
353
+ e.stopPropagation()
353
354
  }
354
355
 
355
356
  domNode.addEventListener('pointerup', onPointerUp, {
@@ -122,6 +122,22 @@ export function RelationshipsDropdownMenu({
122
122
  {nested.map(renderRelationship)}
123
123
  </>
124
124
  )}
125
+ {
126
+ /* <Box pos={'absolute'} top={5} right={6}>
127
+ <ActionIcon
128
+ size={24}
129
+ variant="light"
130
+ onClick={e => {
131
+ e.stopPropagation()
132
+ diagramApi.getState().openOverlay({
133
+ edgeDetails: edge.id
134
+ })
135
+ }}
136
+ >
137
+ <IconArrowsMaximize style={{ width: '70%' }} />
138
+ </ActionIcon>
139
+ </Box> */
140
+ }
125
141
  </MenuDropdown>
126
142
  </Menu>
127
143
  )
@@ -45,23 +45,17 @@ globalStyle(`:where(${isSelected}) ${container}[data-edge-hovered='true']`, {
45
45
  [xyvars.edge.strokeWidth]: '4'
46
46
  }
47
47
  })
48
- // globalStyle(`${container}[data-edge-hovered='true']`, {
49
- // vars: {
50
- // [xyvars.edge.stroke]: `color-mix(in srgb, ${vars.relation.lineColor}, white 35%)`
51
- // },
52
- // })
53
48
 
54
- globalStyle(`.react-flow__edges > svg`, {
49
+ globalStyle(`.likec4-diagram .react-flow__edges > svg`, {
55
50
  mixBlendMode: 'plus-lighter'
56
51
  })
57
-
58
- // globalStyle(`:where([data-mantine-color-scheme="light"]) .react-flow__edges > svg`, {
59
- // mixBlendMode: 'screen'
60
- // })
52
+ globalStyle(`:where([data-mantine-color-scheme="light"]) .likec4-diagram .react-flow__edges > svg`, {
53
+ mixBlendMode: 'screen'
54
+ })
61
55
 
62
56
  export const dimmed = style({})
63
57
 
64
- globalStyle(`.react-flow__edges > svg:has(${dimmed})`, {
58
+ globalStyle(`.likec4-diagram .react-flow__edges > svg:has(${dimmed})`, {
65
59
  opacity: 0.6,
66
60
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
67
61
  transitionDelay: '200ms',