@likec4/diagram 1.36.1 → 1.37.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 (173) hide show
  1. package/bundle/{chunk-DHvr-RGh.js → chunk-DSORWspW.js} +1008 -988
  2. package/bundle/custom.js +64 -56
  3. package/bundle/index.js +3539 -3315
  4. package/dist/LikeC4Diagram.props.d.ts +5 -5
  5. package/dist/LikeC4Styles.js +17 -15
  6. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  7. package/dist/StaticLikeC4Diagram.js +3 -0
  8. package/dist/base/BaseXYFlow.d.ts +1 -1
  9. package/dist/base/BaseXYFlow.js +25 -27
  10. package/dist/base/const.d.ts +4 -4
  11. package/dist/base/const.js +5 -4
  12. package/dist/base/primitives/MarkdownBlock.js +4 -1
  13. package/dist/base/primitives/compound/CompoundActionButton.js +27 -42
  14. package/dist/base/primitives/compound/CompoundDetailsButton.js +23 -48
  15. package/dist/base/primitives/compound/CompoundNodeContainer.js +16 -48
  16. package/dist/base/primitives/compound/CompoundTitle.js +5 -80
  17. package/dist/base/primitives/compound/actionbtns.css.js +1 -13
  18. package/dist/base/primitives/customEdge.d.ts +1 -1
  19. package/dist/base/primitives/customNode.d.ts +4 -4
  20. package/dist/base/primitives/customNode.js +7 -7
  21. package/dist/base/primitives/edge/EdgeLabel.d.ts +5 -4
  22. package/dist/base/primitives/edge/EdgeLabel.js +39 -21
  23. package/dist/base/primitives/edge/EdgeLabelContainer.js +11 -23
  24. package/dist/base/primitives/edge/edge.css.js +19 -11
  25. package/dist/base/primitives/element/ElementShape.js +38 -18
  26. package/dist/base/primitives/element/ElementTags.js +7 -4
  27. package/dist/base/primitives/element/ElementTitle.js +10 -2
  28. package/dist/base/types.d.ts +17 -21
  29. package/dist/bundle/LikeC4View.d.ts +2 -2
  30. package/dist/bundle/LikeC4View.js +4 -1
  31. package/dist/bundle/LikeC4View.props.d.ts +2 -2
  32. package/dist/bundle/ReactLikeC4.d.ts +3 -3
  33. package/dist/bundle/custom.js +2 -1
  34. package/dist/bundle/index.js +2 -1
  35. package/dist/components/Link.js +3 -2
  36. package/dist/context/ReduceGraphics.js +2 -5
  37. package/dist/context/RootContainer.js +1 -5
  38. package/dist/custom/customNodes.d.ts +32 -26
  39. package/dist/custom/customNodes.js +12 -8
  40. package/dist/custom/hooks.d.ts +2 -1
  41. package/dist/custom/index.js +2 -1
  42. package/dist/globals.css.js +1 -1
  43. package/dist/hooks/index.d.ts +1 -1
  44. package/dist/hooks/useDiagram.d.ts +18 -1
  45. package/dist/hooks/useDiagram.js +18 -2
  46. package/dist/hooks/useXYFlow.d.ts +40 -21
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js +2 -1
  49. package/dist/likec4diagram/DiagramUI.js +5 -2
  50. package/dist/likec4diagram/DiagramXYFlow.js +9 -9
  51. package/dist/likec4diagram/custom/edges/RelationshipEdge.d.ts +3 -3
  52. package/dist/likec4diagram/custom/edges/RelationshipEdge.js +27 -44
  53. package/dist/likec4diagram/custom/index.js +3 -3
  54. package/dist/likec4diagram/custom/nodes/nodes.d.ts +8 -6
  55. package/dist/likec4diagram/custom/nodes/nodes.js +13 -14
  56. package/dist/likec4diagram/custom/nodes/toolbar/CompoundToolbar.js +3 -2
  57. package/dist/likec4diagram/custom/nodes/toolbar/_shared.d.ts +2 -2
  58. package/dist/likec4diagram/relationship-popover/RelationshipPopover.d.ts +2 -0
  59. package/dist/likec4diagram/relationship-popover/RelationshipPopover.js +441 -0
  60. package/dist/likec4diagram/relationship-popover/actor.d.ts +177 -0
  61. package/dist/likec4diagram/relationship-popover/actor.js +155 -0
  62. package/dist/likec4diagram/relationship-popover/styles.css.d.ts +2 -0
  63. package/dist/likec4diagram/{custom/edges/RelationshipsDropdownMenu.css.js → relationship-popover/styles.css.js} +4 -17
  64. package/dist/likec4diagram/useLayoutConstraints.js +0 -1
  65. package/dist/likec4model/useLikeC4ElementsTree.js +1 -1
  66. package/dist/likec4model/useLikeC4Project.js +15 -1
  67. package/dist/navigationpanel/NavigationPanelDropdown.js +1 -2
  68. package/dist/navigationpanel/actor.js +1 -2
  69. package/dist/navigationpanel/controls/NavigationButtons.js +1 -5
  70. package/dist/navigationpanel/controls/ViewDetailsCard.js +2 -0
  71. package/dist/overlays/element-details/actor.d.ts +564 -8
  72. package/dist/overlays/overlay/Overlay.css.d.ts +1 -1
  73. package/dist/overlays/relationship-details/actor.d.ts +3 -1
  74. package/dist/overlays/relationship-details/hooks.d.ts +19 -1
  75. package/dist/overlays/relationship-details/layout-to-xyflow.js +0 -1
  76. package/dist/overlays/relationship-details/layout.js +1 -2
  77. package/dist/overlays/relationships-browser/actor.d.ts +26 -2
  78. package/dist/overlays/relationships-browser/custom/nodeTypes.d.ts +16 -0
  79. package/dist/overlays/relationships-browser/custom/nodeTypes.js +18 -8
  80. package/dist/overlays/relationships-browser/useViewToNodesEdges.js +0 -1
  81. package/dist/search/components/_shared.css.d.ts +1 -1
  82. package/dist/state/DiagramActorProvider.js +2 -19
  83. package/dist/state/assign.js +24 -10
  84. package/dist/state/diagram-machine.d.ts +149 -474
  85. package/dist/state/diagram-machine.js +94 -54
  86. package/dist/state/types.d.ts +1 -1
  87. package/dist/styles-min.css +1 -1
  88. package/dist/styles-xyflow.css +1 -1
  89. package/dist/styles.css +1 -1
  90. package/dist/styles.css2.js +1 -1
  91. package/dist/utils/index.d.ts +2 -1
  92. package/dist/utils/roundDpr.d.ts +1 -0
  93. package/dist/utils/roundDpr.js +20 -0
  94. package/dist/utils/xyflow.d.ts +0 -1
  95. package/dist/utils/xyflow.js +1 -7
  96. package/package.json +11 -10
  97. package/panda.buildinfo.json +90 -155
  98. package/src/LikeC4Diagram.props.ts +5 -5
  99. package/src/LikeC4Styles.tsx +22 -14
  100. package/src/StaticLikeC4Diagram.tsx +3 -0
  101. package/src/base/BaseXYFlow.tsx +26 -29
  102. package/src/base/const.ts +5 -4
  103. package/src/base/primitives/MarkdownBlock.tsx +6 -1
  104. package/src/base/primitives/compound/CompoundActionButton.tsx +31 -43
  105. package/src/base/primitives/compound/CompoundDetailsButton.tsx +26 -48
  106. package/src/base/primitives/compound/CompoundNodeContainer.tsx +15 -43
  107. package/src/base/primitives/compound/CompoundTitle.tsx +4 -76
  108. package/src/base/primitives/compound/actionbtns.css.ts +0 -12
  109. package/src/base/primitives/customEdge.ts +1 -1
  110. package/src/base/primitives/customNode.tsx +20 -24
  111. package/src/base/primitives/edge/EdgeLabel.tsx +23 -8
  112. package/src/base/primitives/edge/EdgeLabelContainer.tsx +9 -21
  113. package/src/base/primitives/edge/edge.css.ts +19 -11
  114. package/src/base/primitives/element/ElementShape.tsx +40 -18
  115. package/src/base/primitives/element/ElementTags.tsx +7 -4
  116. package/src/base/primitives/element/ElementTitle.tsx +23 -11
  117. package/src/base/types.ts +21 -25
  118. package/src/bundle/LikeC4View.props.ts +2 -2
  119. package/src/bundle/LikeC4View.tsx +8 -5
  120. package/src/bundle/ReactLikeC4.tsx +3 -3
  121. package/src/components/Link.tsx +3 -2
  122. package/src/context/RootContainer.tsx +1 -5
  123. package/src/context/TagStylesContext.tsx +2 -0
  124. package/src/custom/{customNodes.tsx → customNodes.ts} +58 -52
  125. package/src/custom/hooks.ts +7 -0
  126. package/src/globals.css +58 -5
  127. package/src/hooks/index.ts +1 -1
  128. package/src/hooks/useDiagram.ts +39 -5
  129. package/src/hooks/useXYFlow.ts +3 -1
  130. package/src/index.ts +1 -0
  131. package/src/likec4diagram/DiagramUI.tsx +3 -0
  132. package/src/likec4diagram/DiagramXYFlow.tsx +9 -11
  133. package/src/likec4diagram/custom/edges/RelationshipEdge.tsx +25 -49
  134. package/src/likec4diagram/custom/index.ts +5 -4
  135. package/src/likec4diagram/custom/nodes/nodes.tsx +13 -11
  136. package/src/likec4diagram/custom/nodes/toolbar/CompoundToolbar.tsx +3 -2
  137. package/src/likec4diagram/relationship-popover/RelationshipPopover.tsx +535 -0
  138. package/src/likec4diagram/relationship-popover/actor.ts +192 -0
  139. package/src/likec4diagram/{custom/edges/RelationshipsDropdownMenu.css.ts → relationship-popover/styles.css.ts} +3 -19
  140. package/src/likec4diagram/useLayoutConstraints.ts +0 -1
  141. package/src/likec4model/useLikeC4ElementsTree.ts +1 -1
  142. package/src/navigationpanel/NavigationPanelDropdown.tsx +1 -2
  143. package/src/navigationpanel/actor.ts +2 -2
  144. package/src/navigationpanel/controls/NavigationButtons.tsx +1 -5
  145. package/src/navigationpanel/controls/ViewDetailsCard.tsx +2 -0
  146. package/src/overlays/relationship-details/layout-to-xyflow.ts +0 -1
  147. package/src/overlays/relationship-details/layout.ts +4 -3
  148. package/src/overlays/relationships-browser/custom/nodeTypes.tsx +22 -8
  149. package/src/overlays/relationships-browser/useViewToNodesEdges.ts +0 -1
  150. package/src/state/DiagramActorProvider.tsx +2 -20
  151. package/src/state/assign.ts +24 -10
  152. package/src/state/diagram-machine.ts +101 -53
  153. package/src/state/types.ts +4 -1
  154. package/src/utils/index.ts +2 -1
  155. package/src/utils/roundDpr.ts +27 -0
  156. package/src/utils/xyflow.ts +0 -7
  157. package/dist/base/primitives/compound/CompoundNodeContainer.css.d.ts +0 -35
  158. package/dist/base/primitives/compound/CompoundNodeContainer.css.js +0 -132
  159. package/dist/base/primitives/edge/EdgeLabel.css.d.ts +0 -7
  160. package/dist/base/primitives/edge/EdgeLabel.css.js +0 -57
  161. package/dist/base/primitives/element/ElementShape.css.d.ts +0 -6
  162. package/dist/base/primitives/element/ElementShape.css.js +0 -107
  163. package/dist/likec4diagram/custom/edges/NotePopover.css.js +0 -21
  164. package/dist/likec4diagram/custom/edges/NotePopover.js +0 -91
  165. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.css.d.ts +0 -5
  166. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.d.ts +0 -13
  167. package/dist/likec4diagram/custom/edges/RelationshipsDropdownMenu.js +0 -215
  168. package/dist/likec4diagram/custom/index.d.ts +0 -62
  169. package/dist/overlays/relationship-details/custom/nodeTypes.d.ts +0 -10
  170. package/src/base/primitives/compound/CompoundNodeContainer.css.ts +0 -138
  171. package/src/base/primitives/edge/EdgeLabel.css.ts +0 -64
  172. package/src/base/primitives/element/ElementShape.css.ts +0 -112
  173. package/src/likec4diagram/custom/edges/RelationshipsDropdownMenu.tsx +0 -281
@@ -4,7 +4,6 @@ import {
4
4
  CompoundDetailsButton,
5
5
  CompoundNodeContainer,
6
6
  CompoundTitle,
7
- customNode,
8
7
  DefaultHandles,
9
8
  ElementDetailsButton,
10
9
  ElementNodeContainer,
@@ -13,6 +12,7 @@ import {
13
12
  ElementTitle,
14
13
  } from '../../../base/primitives'
15
14
  import { useEnabledFeatures } from '../../../context/DiagramFeatures'
15
+ import type { CustomNodes as CustomNodesType } from '../../../custom/customNodes'
16
16
  import { useDiagram } from '../../../hooks/useDiagram'
17
17
  import type { Types } from '../../types'
18
18
  import { CompoundActions } from './CompoundActions'
@@ -20,6 +20,8 @@ import { DeploymentElementActions, ElementActions } from './ElementActions'
20
20
  import { CompoundDeploymentToolbar, CompoundElementToolbar } from './toolbar/CompoundToolbar'
21
21
  import { DeploymentElementToolbar, ElementToolbar } from './toolbar/ElementToolbar'
22
22
 
23
+ type CustomNodes = Required<CustomNodesType>
24
+
23
25
  export function ElementDetailsButtonWithHandler(
24
26
  props: NodeProps<Types.ElementNodeData | Types.DeploymentElementNodeData>,
25
27
  ) {
@@ -58,7 +60,7 @@ export function CompoundDetailsButtonWithHandler(
58
60
  )
59
61
  }
60
62
 
61
- export const ElementNode = customNode<Types.ElementNodeData, 'element'>(props => {
63
+ export const ElementNode: CustomNodes['element'] = (props) => {
62
64
  const { enableElementTags, enableReadOnly } = useEnabledFeatures()
63
65
  return (
64
66
  <ElementNodeContainer nodeProps={props}>
@@ -71,10 +73,10 @@ export const ElementNode = customNode<Types.ElementNodeData, 'element'>(props =>
71
73
  <DefaultHandles />
72
74
  </ElementNodeContainer>
73
75
  )
74
- })
76
+ }
75
77
  ElementNode.displayName = 'ElementNode'
76
78
 
77
- export const DeploymentNode = customNode<Types.DeploymentElementNodeData, 'deployment'>((props) => {
79
+ export const DeploymentNode: CustomNodes['deployment'] = (props) => {
78
80
  const { enableElementTags, enableReadOnly } = useEnabledFeatures()
79
81
  return (
80
82
  <ElementNodeContainer nodeProps={props}>
@@ -87,10 +89,10 @@ export const DeploymentNode = customNode<Types.DeploymentElementNodeData, 'deplo
87
89
  <DefaultHandles />
88
90
  </ElementNodeContainer>
89
91
  )
90
- })
92
+ }
91
93
  DeploymentNode.displayName = 'DeploymentNode'
92
94
 
93
- export const CompoundElementNode = customNode<Types.CompoundElementNodeData, 'compound-element'>((props) => {
95
+ export const CompoundElementNode: CustomNodes['compoundElement'] = (props) => {
94
96
  const { enableElementDetails, enableReadOnly } = useEnabledFeatures()
95
97
  return (
96
98
  <CompoundNodeContainer nodeProps={props}>
@@ -101,10 +103,10 @@ export const CompoundElementNode = customNode<Types.CompoundElementNodeData, 'co
101
103
  <DefaultHandles />
102
104
  </CompoundNodeContainer>
103
105
  )
104
- })
106
+ }
105
107
  CompoundElementNode.displayName = 'CompoundElementNode'
106
108
 
107
- export const CompoundDeploymentNode = customNode<Types.CompoundDeploymentNodeData, 'compound-deployment'>((props) => {
109
+ export const CompoundDeploymentNode: CustomNodes['compoundDeployment'] = (props) => {
108
110
  const { enableElementDetails, enableReadOnly } = useEnabledFeatures()
109
111
  return (
110
112
  <CompoundNodeContainer nodeProps={props}>
@@ -115,13 +117,13 @@ export const CompoundDeploymentNode = customNode<Types.CompoundDeploymentNodeDat
115
117
  <DefaultHandles />
116
118
  </CompoundNodeContainer>
117
119
  )
118
- })
120
+ }
119
121
  CompoundDeploymentNode.displayName = 'CompoundDeploymentNode'
120
122
 
121
- export const ViewGroupNode = customNode<Types.ViewGroupNodeData, 'view-group'>((props) => (
123
+ export const ViewGroupNode: CustomNodes['viewGroup'] = (props) => (
122
124
  <CompoundNodeContainer nodeProps={props}>
123
125
  <CompoundTitle {...props} />
124
126
  <DefaultHandles />
125
127
  </CompoundNodeContainer>
126
- ))
128
+ )
127
129
  ViewGroupNode.displayName = 'ViewGroupNode'
@@ -20,6 +20,7 @@ export function CompoundElementToolbar(props: CompoundElementToolbarProps) {
20
20
  } = props
21
21
 
22
22
  const { elementColor, onColorPreview, onChange } = useHandlers(modelFqn, props)
23
+ const opacity = style?.opacity ?? 100
23
24
  return (
24
25
  <Toolbar
25
26
  nodeProps={props}
@@ -29,12 +30,12 @@ export function CompoundElementToolbar(props: CompoundElementToolbarProps) {
29
30
  elementColor={elementColor}
30
31
  onColorPreview={onColorPreview}
31
32
  isOpacityEditable
32
- elementOpacity={style?.opacity}
33
+ elementOpacity={opacity}
33
34
  onChange={onChange}
34
35
  position="left-start"
35
36
  />
36
37
  <BorderStyleOption
37
- elementBorderStyle={style?.border}
38
+ elementBorderStyle={style?.border ?? (opacity < 99 ? 'dashed' : 'none')}
38
39
  onChange={onChange}
39
40
  />
40
41
  {enableVscode && <GoToSourceButton elementId={modelFqn} />}
@@ -0,0 +1,535 @@
1
+ import { autoPlacement, autoUpdate, computePosition, hide, offset, size } from '@floating-ui/dom'
2
+ import { nameFromFqn } from '@likec4/core'
3
+ import type { LikeC4Model } from '@likec4/core/model'
4
+ import type { DiagramEdge, DiagramNode, EdgeId, ViewId } from '@likec4/core/types'
5
+ import { css, cx } from '@likec4/styles/css'
6
+ import { Box, HStack, styled, VStack } from '@likec4/styles/jsx'
7
+ import { bleed } from '@likec4/styles/patterns'
8
+ import {
9
+ ActionIcon,
10
+ Button,
11
+ Divider,
12
+ Portal,
13
+ ScrollAreaAutosize,
14
+ Space,
15
+ Text,
16
+ Tooltip as MantineTooltip,
17
+ TooltipGroup,
18
+ } from '@mantine/core'
19
+ import { IconArrowRight, IconFileSymlink, IconZoomScan } from '@tabler/icons-react'
20
+ import { useActorRef, useSelector } from '@xstate/react'
21
+ import { shallowEqual } from 'fast-equals'
22
+ import {
23
+ type MouseEvent,
24
+ type MouseEventHandler,
25
+ forwardRef,
26
+ Fragment,
27
+ memo,
28
+ useCallback,
29
+ useEffect,
30
+ useRef,
31
+ useState,
32
+ } from 'react'
33
+ import { clamp, filter, isTruthy, map, partition, pipe } from 'remeda'
34
+ import { Link } from '../../components/Link'
35
+ import { useDiagramEventHandlers } from '../../context/DiagramEventHandlers'
36
+ import { useEnabledFeatures } from '../../context/DiagramFeatures'
37
+ import { MarkdownBlock } from '../../custom'
38
+ import { useDiagram, useDiagramContext, useOnDiagramEvent } from '../../hooks/useDiagram'
39
+ import { useMantinePortalProps } from '../../hooks/useMantinePortalProps'
40
+ import { useUpdateEffect } from '../../hooks/useUpdateEffect'
41
+ import { type XYStoreState, useXYStore } from '../../hooks/useXYFlow'
42
+ import { useLikeC4Model } from '../../likec4model'
43
+ import type { DiagramContext } from '../../state/types'
44
+ import { findDiagramEdge, findDiagramNode } from '../../state/utils'
45
+ import { roundDpr } from '../../utils'
46
+ import { RelationshipPopoverActorLogic } from './actor'
47
+ import * as styles from './styles.css'
48
+
49
+ function selectDiagramContext(c: DiagramContext) {
50
+ let selected: EdgeId | null = null
51
+ for (const edge of c.xyedges) {
52
+ if (edge.selected) {
53
+ if (selected) {
54
+ selected = null
55
+ break
56
+ }
57
+ selected = edge.data.id
58
+ }
59
+ }
60
+ return {
61
+ viewId: c.view.id,
62
+ selected,
63
+ }
64
+ }
65
+
66
+ export const RelationshipPopover = memo(() => {
67
+ const actorRef = useActorRef(RelationshipPopoverActorLogic)
68
+ const diagram = useDiagram()
69
+ const { viewId, selected } = useDiagramContext(selectDiagramContext)
70
+
71
+ const openedEdgeId = useSelector(actorRef, s => s.hasTag('opened') ? s.context.edgeId : null)
72
+
73
+ useUpdateEffect(() => {
74
+ actorRef.send({ type: 'close' })
75
+ }, [viewId])
76
+
77
+ useOnDiagramEvent('edgeMouseEnter', ({ edge }) => {
78
+ actorRef.send({ type: 'xyedge.mouseEnter', edgeId: edge.data.id })
79
+ })
80
+
81
+ useOnDiagramEvent('edgeMouseLeave', () => {
82
+ actorRef.send({ type: 'xyedge.mouseLeave' })
83
+ })
84
+
85
+ useOnDiagramEvent('edgeEditingStarted', () => {
86
+ actorRef.send({ type: 'close' })
87
+ })
88
+
89
+ useOnDiagramEvent('walkthroughStarted', () => {
90
+ actorRef.send({ type: 'close' })
91
+ })
92
+
93
+ useUpdateEffect(() => {
94
+ if (selected) {
95
+ actorRef.send({ type: 'xyedge.select', edgeId: selected })
96
+ } else {
97
+ actorRef.send({ type: 'xyedge.unselect' })
98
+ }
99
+ }, [selected])
100
+
101
+ const onMouseEnter = useCallback((event: MouseEvent<HTMLDivElement>) => {
102
+ if (!openedEdgeId) {
103
+ return
104
+ }
105
+ actorRef.send({ type: 'dropdown.mouseEnter' })
106
+ const edge = diagram.findEdge(openedEdgeId)
107
+ if (edge && !edge.data.hovered) {
108
+ diagram.send({ type: 'xyflow.edgeMouseEnter', edge, event })
109
+ }
110
+ }, [actorRef, diagram, openedEdgeId])
111
+
112
+ const onMouseLeave = useCallback((event: MouseEvent<HTMLDivElement>) => {
113
+ if (!openedEdgeId) {
114
+ return
115
+ }
116
+ actorRef.send({ type: 'dropdown.mouseLeave' })
117
+ const edge = diagram.findEdge(openedEdgeId)
118
+ if (edge?.data.hovered) {
119
+ diagram.send({ type: 'xyflow.edgeMouseLeave', edge, event })
120
+ }
121
+ }, [actorRef, diagram, openedEdgeId])
122
+
123
+ const { diagramEdge, sourceNode, targetNode } = useDiagramContext(
124
+ ctx => {
125
+ const diagramEdge = openedEdgeId ? findDiagramEdge(ctx, openedEdgeId) : null
126
+ const sourceNode = diagramEdge ? findDiagramNode(ctx, diagramEdge.source) : null
127
+ const targetNode = diagramEdge ? findDiagramNode(ctx, diagramEdge.target) : null
128
+ return ({
129
+ diagramEdge,
130
+ sourceNode,
131
+ targetNode,
132
+ })
133
+ },
134
+ shallowEqual,
135
+ [openedEdgeId],
136
+ )
137
+
138
+ if (!diagramEdge || !sourceNode || !targetNode) {
139
+ return null
140
+ }
141
+
142
+ return (
143
+ <RelationshipPopoverInternal
144
+ viewId={viewId}
145
+ diagramEdge={diagramEdge}
146
+ sourceNode={sourceNode}
147
+ targetNode={targetNode}
148
+ onMouseEnter={onMouseEnter}
149
+ onMouseLeave={onMouseLeave} />
150
+ )
151
+ })
152
+
153
+ const getEdgeLabelElement = (edgeId: string, container: HTMLDivElement | undefined) => {
154
+ return container?.querySelector<HTMLDivElement>(`.likec4-edge-label[data-edge-id="${edgeId}"]`) ?? null
155
+ }
156
+
157
+ const selectTransform = (s: XYStoreState) =>
158
+ roundDpr(s.transform[0]) + ' ' + roundDpr(s.transform[1]) + ' ' + s.transform[2].toFixed(3)
159
+
160
+ type RelationshipPopoverInternalProps = {
161
+ viewId: ViewId
162
+ diagramEdge: DiagramEdge
163
+ sourceNode: DiagramNode
164
+ targetNode: DiagramNode
165
+ onMouseEnter: MouseEventHandler<HTMLDivElement>
166
+ onMouseLeave: MouseEventHandler<HTMLDivElement>
167
+ }
168
+ const RelationshipPopoverInternal = ({
169
+ viewId,
170
+ diagramEdge,
171
+ sourceNode,
172
+ targetNode,
173
+ onMouseEnter,
174
+ onMouseLeave,
175
+ }: RelationshipPopoverInternalProps) => {
176
+ const ref = useRef<HTMLDivElement>(null)
177
+ const { enableNavigateTo, enableVscode } = useEnabledFeatures()
178
+ const { onOpenSource } = useDiagramEventHandlers()
179
+
180
+ const { portalProps } = useMantinePortalProps()
181
+
182
+ const [referenceEl, setReferenceEl] = useState<HTMLDivElement | null>(null)
183
+ const viewport = useXYStore(selectTransform)
184
+
185
+ useEffect(() => {
186
+ setReferenceEl(getEdgeLabelElement(diagramEdge.id, portalProps?.target))
187
+ }, [diagramEdge, viewport])
188
+
189
+ useEffect(() => {
190
+ const reference = referenceEl
191
+ const popper = ref.current
192
+ if (!reference || !popper) {
193
+ return
194
+ }
195
+ let wasCanceled = false
196
+
197
+ const update = () => {
198
+ computePosition(reference, popper, {
199
+ placement: 'bottom',
200
+ middleware: [
201
+ offset(2),
202
+ autoPlacement({
203
+ padding: 16,
204
+ allowedPlacements: [
205
+ 'bottom-start',
206
+ 'bottom-end',
207
+ 'top-start',
208
+ 'top-end',
209
+ 'right-start',
210
+ 'right-end',
211
+ 'left-start',
212
+ 'left-end',
213
+ ],
214
+ }),
215
+ size({
216
+ padding: 16,
217
+ apply({ availableHeight, availableWidth, elements }) {
218
+ if (wasCanceled) {
219
+ return
220
+ }
221
+ Object.assign(elements.floating.style, {
222
+ maxWidth: `${clamp(roundDpr(availableWidth), { min: 150, max: 400 })}px`,
223
+ maxHeight: `${clamp(roundDpr(availableHeight), { min: 0, max: 500 })}px`,
224
+ })
225
+ },
226
+ }),
227
+ hide({
228
+ padding: 16,
229
+ }),
230
+ ],
231
+ }).then(({ x, y, middlewareData }) => {
232
+ if (wasCanceled) {
233
+ return
234
+ }
235
+ popper.style.transform = `translate(${roundDpr(x)}px, ${roundDpr(y)}px)`
236
+ popper.style.visibility = middlewareData.hide?.referenceHidden ? 'hidden' : 'visible'
237
+ })
238
+ }
239
+ const cleanup = autoUpdate(reference, popper, update)
240
+ return () => {
241
+ wasCanceled = true
242
+ cleanup()
243
+ }
244
+ }, [referenceEl])
245
+
246
+ const likec4model = useLikeC4Model()
247
+ const diagram = useDiagram()
248
+
249
+ const [direct, nested] = pipe(
250
+ diagramEdge.relations,
251
+ map(id => {
252
+ try {
253
+ return likec4model.relationship(id)
254
+ } catch (e) {
255
+ // View was cached, but likec4model based on new data
256
+ console.error(
257
+ `View is cached and likec4model missing relationship ${id} from ${sourceNode.id} -> ${targetNode.id}`,
258
+ e,
259
+ )
260
+ return null
261
+ }
262
+ }),
263
+ filter(isTruthy),
264
+ partition(r => r.source.id === sourceNode.id && r.target.id === targetNode.id),
265
+ )
266
+
267
+ if (direct.length === 0 && nested.length === 0) {
268
+ console.error('No relationships found diagram edge', {
269
+ diagramEdge,
270
+ sourceNode,
271
+ targetNode,
272
+ })
273
+ return null
274
+ }
275
+
276
+ const navigateTo = enableNavigateTo
277
+ ? (viewId: ViewId) => {
278
+ diagram.navigateTo(viewId)
279
+ }
280
+ : undefined
281
+
282
+ const renderRelationship = (relationship: LikeC4Model.AnyRelation, index: number) => (
283
+ <Fragment key={relationship.id}>
284
+ {index > 0 && <Divider />}
285
+ <Relationship
286
+ viewId={viewId}
287
+ relationship={relationship}
288
+ sourceNode={sourceNode}
289
+ targetNode={targetNode}
290
+ edge={diagramEdge}
291
+ onNavigateTo={navigateTo}
292
+ {...(onOpenSource && enableVscode && {
293
+ onOpenSource: () => onOpenSource({ relation: relationship.id }),
294
+ })}
295
+ />
296
+ </Fragment>
297
+ )
298
+
299
+ return (
300
+ <Portal {...portalProps}>
301
+ <ScrollAreaAutosize
302
+ ref={ref}
303
+ onMouseEnter={onMouseEnter}
304
+ onMouseLeave={onMouseLeave}
305
+ type="auto"
306
+ scrollbars={'y'}
307
+ scrollbarSize={6}
308
+ styles={{
309
+ viewport: {
310
+ overscrollBehavior: 'contain',
311
+ minWidth: 150,
312
+ },
313
+ }}
314
+ className={cx(
315
+ 'react-flow__panel nowheel nopan nodrag',
316
+ css({
317
+ layerStyle: 'likec4.dropdown',
318
+ p: '0',
319
+ pointerEvents: 'all',
320
+ position: 'absolute',
321
+ top: '0',
322
+ left: '0',
323
+ width: 'max-content',
324
+ cursor: 'default',
325
+ }),
326
+ )}
327
+ >
328
+ <VStack
329
+ css={{
330
+ gap: '3.5',
331
+ padding: '4',
332
+ paddingTop: '2.5',
333
+ }}
334
+ >
335
+ <Button
336
+ variant="default"
337
+ color="gray"
338
+ size="compact-xs"
339
+ style={{
340
+ alignSelf: 'flex-start',
341
+ fontWeight: 500,
342
+ ['--button-fz']: 'var(--font-sizes-xxs)',
343
+ }}
344
+ onClick={(e) => {
345
+ e.stopPropagation()
346
+ diagram.openRelationshipDetails(diagramEdge.id)
347
+ }}
348
+ >
349
+ browse relationships
350
+ </Button>
351
+ {direct.length > 0 && (
352
+ <>
353
+ <Divider label={<Label>direct relationships</Label>} labelPosition="left" />
354
+ {direct.map(renderRelationship)}
355
+ </>
356
+ )}
357
+ {nested.length > 0 && (
358
+ <>
359
+ {direct.length > 0 && <Space />}
360
+ <Divider label={<Label>resolved from nested</Label>} labelPosition="left" />
361
+ {nested.map(renderRelationship)}
362
+ </>
363
+ )}
364
+ </VStack>
365
+ </ScrollAreaAutosize>
366
+ </Portal>
367
+ )
368
+ }
369
+
370
+ const Relationship = forwardRef<
371
+ HTMLDivElement,
372
+ {
373
+ // current view id
374
+ viewId: ViewId
375
+ relationship: LikeC4Model.AnyRelation
376
+ edge: DiagramEdge
377
+ sourceNode: DiagramNode
378
+ targetNode: DiagramNode
379
+ onOpenSource?: () => void
380
+ onNavigateTo?: ((next: ViewId) => void) | undefined
381
+ }
382
+ >(({
383
+ viewId,
384
+ relationship: r,
385
+ edge,
386
+ sourceNode,
387
+ targetNode,
388
+ onNavigateTo,
389
+ onOpenSource,
390
+ }, ref) => {
391
+ const sourceId = getShortId(r, r.source.id, sourceNode)
392
+ const targetId = getShortId(r, r.target.id, targetNode)
393
+ const navigateTo = onNavigateTo && r.navigateTo?.id !== viewId ? r.navigateTo?.id : undefined
394
+ const links = r.links
395
+
396
+ return (
397
+ <VStack
398
+ ref={ref}
399
+ className={bleed({
400
+ block: '2',
401
+ inline: '2',
402
+ paddingBlock: '2',
403
+ paddingInline: '2',
404
+ gap: '1',
405
+ rounded: 'sm',
406
+ backgroundColor: {
407
+ _hover: {
408
+ base: 'mantine.colors.gray[1]',
409
+ _dark: 'mantine.colors.dark[5]/70',
410
+ },
411
+ },
412
+ })}
413
+ >
414
+ <HStack gap={'0.5'}>
415
+ <Text component="div" data-likec4-color={sourceNode.color} className={styles.endpoint}>
416
+ {sourceId}
417
+ </Text>
418
+ <IconArrowRight stroke={2.5} size={'11px'} opacity={0.65} />
419
+ <Text component="div" data-likec4-color={targetNode.color} className={styles.endpoint}>
420
+ {targetId}
421
+ </Text>
422
+ <TooltipGroup openDelay={100}>
423
+ {navigateTo && (
424
+ <Tooltip label={'Open dynamic view'}>
425
+ <ActionIcon
426
+ size={'sm'}
427
+ radius="sm"
428
+ variant="default"
429
+ onClick={event => {
430
+ event.stopPropagation()
431
+ onNavigateTo?.(navigateTo)
432
+ }}
433
+ style={{
434
+ alignSelf: 'flex-end',
435
+ }}
436
+ role="button"
437
+ >
438
+ <IconZoomScan size="80%" stroke={2} />
439
+ </ActionIcon>
440
+ </Tooltip>
441
+ )}
442
+ {onOpenSource && (
443
+ <Tooltip label={'Open source'}>
444
+ <ActionIcon
445
+ size={'sm'}
446
+ radius="sm"
447
+ variant="default"
448
+ onClick={event => {
449
+ event.stopPropagation()
450
+ onOpenSource()
451
+ }}
452
+ role="button"
453
+ >
454
+ <IconFileSymlink size="80%" stroke={2} />
455
+ </ActionIcon>
456
+ </Tooltip>
457
+ )}
458
+ </TooltipGroup>
459
+ </HStack>
460
+ <Box className={styles.title}>{r.title || 'untitled'}</Box>
461
+ {r.kind && (
462
+ <HStack gap="2">
463
+ <Label>kind</Label>
464
+ <Text size="xs" className={css({ userSelect: 'all' })}>{r.kind}</Text>
465
+ </HStack>
466
+ )}
467
+ {r.technology && (
468
+ <HStack gap="2">
469
+ <Label>technology</Label>
470
+ <Text size="xs" className={css({ userSelect: 'all' })}>{r.technology}</Text>
471
+ </HStack>
472
+ )}
473
+ {r.description.nonEmpty && (
474
+ <>
475
+ <Label>description</Label>
476
+ <Box
477
+ css={{
478
+ paddingLeft: '2.5',
479
+ py: '1.5',
480
+ borderLeft: '2px dotted',
481
+ borderLeftColor: {
482
+ base: 'mantine.colors.gray[3]',
483
+ _dark: 'mantine.colors.dark[4]',
484
+ },
485
+ }}
486
+ >
487
+ <MarkdownBlock value={r.description} fontSize={'sm'} />
488
+ </Box>
489
+ </>
490
+ )}
491
+ {links.length > 0 && (
492
+ <>
493
+ <Label>links</Label>
494
+ <HStack gap="1" flexWrap={'wrap'}>
495
+ {links.map((link) => <Link key={link.url} size="sm" value={link} />)}
496
+ </HStack>
497
+ </>
498
+ )}
499
+ </VStack>
500
+ )
501
+ })
502
+
503
+ const Label = styled('span', {
504
+ base: {
505
+ display: 'inline-block',
506
+ fontSize: 'xxs',
507
+ fontWeight: 500,
508
+ userSelect: 'none',
509
+ lineHeight: '[1.11]',
510
+ color: 'mantine.colors.dimmed',
511
+ },
512
+ })
513
+
514
+ const Tooltip = MantineTooltip.withProps({
515
+ color: 'dark',
516
+ fz: 'xs',
517
+ label: '',
518
+ children: null,
519
+ offset: 8,
520
+ withinPortal: false,
521
+ })
522
+
523
+ function getShortId(
524
+ r: LikeC4Model.AnyRelation,
525
+ actualEndpointId: string,
526
+ diagramNode: DiagramNode,
527
+ ) {
528
+ const diagramNodeId = r.isDeploymentRelation()
529
+ // Relation defined in deployment model. Use id of the deployment node as is.
530
+ ? diagramNode.id
531
+ // Relation defined in model. Get id of the model element
532
+ : diagramNode.modelRef || ''
533
+
534
+ return nameFromFqn(diagramNodeId) + actualEndpointId.slice(diagramNodeId.length)
535
+ }