@likec4/diagram 1.7.2 → 1.7.4

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 (70) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +38 -22
  3. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  4. package/dist/hooks/index.d.ts +2 -0
  5. package/dist/{state/useDiagramStore.d.ts → hooks/useDiagramState.d.ts} +2 -3
  6. package/dist/{xyflow/hooks → hooks}/useXYFlow.d.ts +10 -10
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.mjs +818 -448
  9. package/dist/state/DiagramContext.d.ts +74 -30
  10. package/dist/state/diagram-to-xyflow.d.ts +2 -0
  11. package/dist/state/diagramStore.d.ts +71 -22
  12. package/dist/style.css +58 -37
  13. package/dist/ui/top-left/ChangeAutoLayoutButton.d.ts +3 -0
  14. package/dist/ui/top-left/TopLeftPanel.d.ts +4 -0
  15. package/dist/ui/top-left/_shared.d.ts +5 -0
  16. package/dist/ui/top-left/styles.css.d.ts +3 -0
  17. package/dist/xyflow/XYFlowInner.d.ts +0 -1
  18. package/dist/xyflow/const.d.ts +1 -0
  19. package/dist/xyflow/edges/edges.css.d.ts +1 -1
  20. package/dist/xyflow/{hooks/useLayoutConstraints.d.ts → useLayoutConstraints.d.ts} +1 -1
  21. package/dist/xyflow/utils.d.ts +11 -1
  22. package/package.json +6 -6
  23. package/src/LikeC4Diagram.props.ts +66 -32
  24. package/src/LikeC4Diagram.tsx +19 -12
  25. package/src/StaticLikeC4Diagram.tsx +2 -0
  26. package/src/hooks/index.ts +2 -0
  27. package/src/{state/useDiagramStore.ts → hooks/useDiagramState.ts} +3 -4
  28. package/src/{xyflow/hooks → hooks}/useXYFlow.ts +1 -1
  29. package/src/index.ts +2 -2
  30. package/src/state/DiagramContext.tsx +15 -7
  31. package/src/state/diagram-to-xyflow.ts +17 -3
  32. package/src/state/diagramStore.ts +73 -38
  33. package/src/ui/DiagramTitlePanel.css.ts +2 -2
  34. package/src/ui/DiagramTitlePanel.tsx +62 -44
  35. package/src/ui/DynamicViewWalkthrough.tsx +1 -2
  36. package/src/ui/OptionsPanel.tsx +1 -1
  37. package/src/ui/options/EdgeOptions.tsx +1 -16
  38. package/src/ui/options/NodeOptions.tsx +38 -13
  39. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +131 -0
  40. package/src/ui/top-left/TopLeftPanel.tsx +213 -0
  41. package/src/ui/top-left/_shared.ts +19 -0
  42. package/src/ui/top-left/styles.css.ts +38 -0
  43. package/src/xyflow/FitviewOnDiagramChange.tsx +2 -2
  44. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +87 -59
  45. package/src/xyflow/XYFlow.tsx +11 -10
  46. package/src/xyflow/XYFlowEvents.tsx +44 -18
  47. package/src/xyflow/XYFlowInner.tsx +5 -7
  48. package/src/xyflow/const.ts +1 -0
  49. package/src/xyflow/edges/RelationshipEdge.tsx +60 -27
  50. package/src/xyflow/edges/edges.css.ts +29 -25
  51. package/src/xyflow/nodes/compound/CompoundNode.tsx +1 -1
  52. package/src/xyflow/nodes/element/ElementLink.tsx +1 -2
  53. package/src/xyflow/nodes/element/ElementNode.tsx +7 -5
  54. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -1
  55. package/src/xyflow/{hooks/useLayoutConstraints.ts → useLayoutConstraints.ts} +3 -3
  56. package/src/xyflow/utils.ts +25 -1
  57. package/dist/state/hooks.d.ts +0 -4
  58. package/dist/state/index.d.ts +0 -2
  59. package/dist/state/useNavigationHistory.d.ts +0 -5
  60. package/dist/types.d.ts +0 -20
  61. package/dist/ui/BackwardForwardPanel.css.d.ts +0 -1
  62. package/dist/ui/BackwardForwardPanel.d.ts +0 -1
  63. package/dist/xyflow/hooks/index.d.ts +0 -2
  64. package/src/state/hooks.ts +0 -5
  65. package/src/state/index.ts +0 -2
  66. package/src/state/useNavigationHistory.ts +0 -39
  67. package/src/types.ts +0 -22
  68. package/src/ui/BackwardForwardPanel.css.ts +0 -22
  69. package/src/ui/BackwardForwardPanel.tsx +0 -76
  70. package/src/xyflow/hooks/index.ts +0 -3
@@ -3,7 +3,7 @@ import type { ReactFlowProps } from '@xyflow/react'
3
3
  import { useMemo, useRef } from 'react'
4
4
  import { isNonNullish, isTruthy } from 'remeda'
5
5
  import type { Simplify } from 'type-fest'
6
- import { useDiagramStoreApi } from '../state/useDiagramStore'
6
+ import { useDiagramStoreApi } from '../hooks/useDiagramState'
7
7
  import type { XYFlowEdge, XYFlowNode } from './types'
8
8
 
9
9
  export type XYFlowEventHandlers = Simplify<
@@ -57,18 +57,21 @@ export function useXYFlowEvents() {
57
57
  return ({
58
58
  onDoubleClick: (event) => {
59
59
  const {
60
- fitViewEnabled,
61
60
  onCanvasDblClick,
62
- resetLastClicked,
63
- fitDiagram
61
+ zoomable,
62
+ xystore,
63
+ viewportChanged,
64
+ fitDiagram,
65
+ resetLastClicked
64
66
  } = diagramApi.getState()
65
- if (fitViewEnabled) {
67
+ resetLastClicked()
68
+ xystore.getState().resetSelectedElements()
69
+ if (!onCanvasDblClick) {
70
+ event.stopPropagation()
71
+ }
72
+ if (zoomable && viewportChanged) {
66
73
  fitDiagram()
67
- if (!onCanvasDblClick) {
68
- event.stopPropagation()
69
- }
70
74
  }
71
- resetLastClicked()
72
75
  onCanvasDblClick?.(event)
73
76
  },
74
77
  onPaneClick: (event) => {
@@ -80,6 +83,7 @@ export function useXYFlowEvents() {
80
83
  activeDynamicViewStep,
81
84
  fitDiagram,
82
85
  onCanvasClick,
86
+ xystore,
83
87
  resetLastClicked
84
88
  } = diagramApi.getState()
85
89
  if ((focusedNodeId ?? activeDynamicViewStep) !== null) {
@@ -89,6 +93,7 @@ export function useXYFlowEvents() {
89
93
  }
90
94
  }
91
95
  resetLastClicked()
96
+ xystore.getState().resetSelectedElements()
92
97
  onCanvasClick?.(event)
93
98
  },
94
99
  onNodeContextMenu: (event, xynode) => {
@@ -117,27 +122,42 @@ export function useXYFlowEvents() {
117
122
  fitDiagram,
118
123
  focusOnNode,
119
124
  onNodeClick,
125
+ xystore,
120
126
  enableFocusMode,
121
127
  lastClickedNodeId,
122
- setLastClickedNode
128
+ nodesSelectable,
129
+ setLastClickedNode,
130
+ onOpenSourceElement
123
131
  } = diagramApi.getState()
124
132
  setLastClickedNode(xynode.id)
125
133
  // if we focused on a node, and clicked on another node - focus on the clicked node
126
134
  const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
127
135
  // if user clicked on the same node twice in a short time, focus on it
128
136
  const clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent()
129
- if (enableFocusMode) {
130
- let stopPropagation = false
137
+
138
+ if (clickedRecently && !!onOpenSourceElement) {
139
+ onOpenSourceElement(xynode.data.element.id)
140
+ }
141
+
142
+ if (enableFocusMode || nodesSelectable) {
143
+ let stopPropagation = true
131
144
  switch (true) {
132
- case !focusedNodeId && clickedRecently:
133
- case shallChangeFocus:
134
- stopPropagation = true
145
+ case enableFocusMode && !focusedNodeId && clickedRecently:
146
+ case enableFocusMode && shallChangeFocus: {
135
147
  focusOnNode(xynode.id)
136
148
  break
137
- case focusedNodeId === xynode.id && clickedRecently:
138
- stopPropagation = true
149
+ }
150
+ case enableFocusMode && focusedNodeId === xynode.id && clickedRecently: {
139
151
  fitDiagram()
140
152
  break
153
+ }
154
+ case nodesSelectable: {
155
+ xystore.getState().addSelectedNodes([xynode.id])
156
+ break
157
+ }
158
+ default: {
159
+ stopPropagation = false
160
+ }
141
161
  }
142
162
  if (!onNodeClick && stopPropagation) {
143
163
  // user did not provide a custom handler, stop propagation
@@ -179,6 +199,8 @@ export function useXYFlowEvents() {
179
199
  activateDynamicStep,
180
200
  activeDynamicViewStep,
181
201
  focusedNodeId,
202
+ xystore,
203
+ nodesSelectable,
182
204
  focusOnNode,
183
205
  onEdgeClick,
184
206
  setLastClickedEdge
@@ -204,7 +226,11 @@ export function useXYFlowEvents() {
204
226
  if (isEdgeOfFocusedNode) {
205
227
  focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
206
228
  if (!onEdgeClick) {
207
- // user did not provide a custom handler, stop propagation
229
+ event.stopPropagation()
230
+ }
231
+ } else if (nodesSelectable) {
232
+ xystore.getState().addSelectedEdges([xyedge.id])
233
+ if (!onEdgeClick) {
208
234
  event.stopPropagation()
209
235
  }
210
236
  }
@@ -2,27 +2,25 @@ import { Controls } from '@xyflow/react'
2
2
  import { shallowEqual } from 'fast-equals'
3
3
  import { memo, type PropsWithChildren } from 'react'
4
4
  import { isNonNullish } from 'remeda'
5
+ import { useDiagramState } from '../hooks/useDiagramState'
5
6
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
6
- import { useDiagramState } from '../state/useDiagramStore'
7
- import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
8
7
  import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
8
  import { DynamicViewWalkthrough } from '../ui/DynamicViewWalkthrough'
10
9
  import OptionsPanel from '../ui/OptionsPanel'
10
+ import { TopLeftPanel } from '../ui/top-left/TopLeftPanel'
11
11
  import { XYFlowBackground } from './XYFlowBackground'
12
12
 
13
13
  type XYFlowInnerProps = PropsWithChildren<{
14
14
  background: NonNullable<LikeC4DiagramProperties['background']>
15
15
  controls: boolean
16
16
  showDiagramTitle: boolean
17
- showNavigationButtons: boolean
18
17
  }>
19
18
 
20
19
  export const XYFlowInner = memo(function XYFlowInnerR({
21
20
  children,
22
21
  background,
23
22
  controls,
24
- showDiagramTitle,
25
- showNavigationButtons
23
+ showDiagramTitle
26
24
  }: XYFlowInnerProps) {
27
25
  const {
28
26
  isDynamicView,
@@ -44,10 +42,10 @@ export const XYFlowInner = memo(function XYFlowInnerR({
44
42
  <>
45
43
  {isBgWithPattern && <XYFlowBackground background={background} />}
46
44
  {controls && <Controls position={'bottom-right'} />}
47
- {readonly === false && <OptionsPanel />}
45
+ <TopLeftPanel />
46
+ {readonly !== true && <OptionsPanel />}
48
47
  {isDiagramTitleVisible && <DiagramTitlePanel />}
49
48
  {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
50
- {showNavigationButtons && <BackwardForwardPanel />}
51
49
  {children}
52
50
  </>
53
51
  )
@@ -5,3 +5,4 @@ export const ZIndexes = {
5
5
  } as const
6
6
 
7
7
  export const MinZoom = 0.1
8
+ export const MaxZoom = 2
@@ -16,10 +16,10 @@ import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
16
16
  import { deepEqual, deepEqual as eq } from 'fast-equals'
17
17
  import { memo, type PointerEvent as ReactPointerEvent, useEffect, useRef, useState } from 'react'
18
18
  import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
19
- import { useDiagramState, useDiagramStoreApi } from '../../state/useDiagramStore'
19
+ import { useDiagramState, useDiagramStoreApi } from '../../hooks/useDiagramState'
20
+ import { useXYStoreApi } from '../../hooks/useXYFlow'
20
21
  import { ZIndexes } from '../const'
21
22
  import { EdgeMarkers, type EdgeMarkerType } from '../EdgeMarkers'
22
- import { useXYStoreApi } from '../hooks/useXYFlow'
23
23
  import { type RelationshipData, type XYFlowEdge } from '../types'
24
24
  import { bezierControlPoints } from '../utils'
25
25
  import * as edgesCss from './edges.css'
@@ -97,9 +97,9 @@ function bezierPath(bezierSpline: NonEmptyArray<Point>) {
97
97
  return path
98
98
  }
99
99
 
100
- // If points are within 3px, consider them the same
100
+ // If points are within 1px, consider them the same
101
101
  const isSame = (a: number, b: number) => {
102
- return Math.abs(a - b) < 3.1
102
+ return Math.abs(a - b) < 2.5
103
103
  }
104
104
 
105
105
  const isSamePoint = (a: XYPosition | Point, b: XYPosition | Point) => {
@@ -150,12 +150,14 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
150
150
  const [isControlPointDragging, setIsControlPointDragging] = useState(false)
151
151
  const diagramStore = useDiagramStoreApi()
152
152
  const xyflowStore = useXYStoreApi()
153
- const { isActive, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
154
- isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true,
153
+ const { isActive, isEdgePathEditable, isHovered, isDimmed, hasOpenSourceRelation } = useDiagramState(s => ({
154
+ isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true && s.focusedNodeId === null
155
+ && s.activeDynamicViewStep === null,
155
156
  isActive: s.focusedNodeId === source || s.focusedNodeId === target
156
157
  || (s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum),
157
158
  isHovered: s.hoveredEdgeId === id,
158
- isDimmed: s.dimmed.has(id)
159
+ isDimmed: s.dimmed.has(id),
160
+ hasOpenSourceRelation: !!s.onOpenSourceRelation
159
161
  }))
160
162
  const { nodeLookup, edgeLookup } = xyflowStore.getState()
161
163
  const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
@@ -200,8 +202,6 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
200
202
  let edgePath: string
201
203
 
202
204
  if (isModified) {
203
- labelX = labelPos.x
204
- labelY = labelPos.y
205
205
  const sourceCenterPos = { x: sourceX, y: sourceY }
206
206
  const targetCenterPos = { x: targetX, y: targetY }
207
207
 
@@ -254,10 +254,15 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
254
254
  })
255
255
  },
256
256
  [labelPos],
257
- 100,
257
+ 50,
258
258
  300
259
259
  )
260
260
 
261
+ if (isModified || isControlPointDragging) {
262
+ labelX = labelPos.x
263
+ labelY = labelPos.y
264
+ }
265
+
261
266
  const onControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>) => {
262
267
  const { domNode } = xyflowStore.getState()
263
268
  if (!domNode || e.pointerType !== 'mouse') {
@@ -281,30 +286,47 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
281
286
  e.stopPropagation()
282
287
  let hasMoved = false
283
288
  let pointer = { x: e.clientX, y: e.clientY }
289
+
284
290
  const onPointerMove = (e: PointerEvent) => {
285
- if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
291
+ if (hasMoved || !isSamePoint(pointer, [e.clientX, e.clientY])) {
286
292
  setIsControlPointDragging(true)
287
293
  hasMoved = true
288
294
  pointer = { x: e.clientX, y: e.clientY }
289
295
  const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
290
- const newControlPoints = controlPoints.slice()
291
- newControlPoints[index] = {
292
- x: Math.round(x),
293
- y: Math.round(y)
294
- }
295
- xyflow.updateEdgeData(id, { controlPoints: newControlPoints })
296
+ xyflow.updateEdgeData(id, xyedge => {
297
+ const cp = (xyedge.data.controlPoints ?? controlPoints).slice()
298
+ cp[index] = {
299
+ x: Math.round(x),
300
+ y: Math.round(y)
301
+ }
302
+ return {
303
+ controlPoints: cp
304
+ }
305
+ })
296
306
  }
307
+ e.stopPropagation()
297
308
  }
298
- const onPointerUp = () => {
299
- domNode.removeEventListener('pointermove', onPointerMove)
300
- domNode.removeEventListener('pointerup', onPointerUp)
309
+
310
+ const onPointerUp = (e: PointerEvent) => {
311
+ domNode.removeEventListener('pointermove', onPointerMove, {
312
+ capture: true
313
+ })
314
+ if (hasMoved) {
315
+ e.stopPropagation()
316
+ }
301
317
  if (hasMoved || wasCanceled) {
302
318
  diagramStore.getState().scheduleSaveManualLayout()
303
319
  }
304
320
  setIsControlPointDragging(false)
305
321
  }
306
- domNode.addEventListener('pointermove', onPointerMove)
307
- domNode.addEventListener('pointerup', onPointerUp, { once: true })
322
+
323
+ domNode.addEventListener('pointermove', onPointerMove, {
324
+ capture: true
325
+ })
326
+ domNode.addEventListener('pointerup', onPointerUp, {
327
+ once: true,
328
+ capture: true
329
+ })
308
330
  }
309
331
 
310
332
  let markerStartName = toMarker(diagramEdge.tail)
@@ -318,6 +340,9 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
318
340
 
319
341
  const labelZIndex = 1 + (isHovered ? ZIndexes.Element : (edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge))
320
342
 
343
+ const relationId = first(data.edge.relations)
344
+ const showOpenSourceRelation = hasOpenSourceRelation && relationId !== undefined
345
+
321
346
  return (
322
347
  <g
323
348
  className={clsx(
@@ -393,7 +418,12 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
393
418
  isHovered,
394
419
  isActive,
395
420
  isStepEdge
396
- })} />
421
+ })}
422
+ onClick={showOpenSourceRelation && ((e) => {
423
+ e.stopPropagation()
424
+ diagramStore.getState().onOpenSourceRelation?.(relationId)
425
+ })}
426
+ />
397
427
  )}
398
428
  </g>
399
429
  )
@@ -412,9 +442,10 @@ type EdgeLabelProps = {
412
442
  isHovered: boolean
413
443
  isActive: boolean
414
444
  zIndex: number
445
+ onClick: false | ((e: ReactPointerEvent<HTMLDivElement>) => void)
415
446
  }
416
447
 
417
- const EdgeLabel = memo<EdgeLabelProps>(({
448
+ const EdgeLabel = ({
418
449
  isDimmed,
419
450
  color,
420
451
  isModified,
@@ -426,8 +457,9 @@ const EdgeLabel = memo<EdgeLabelProps>(({
426
457
  stepNum,
427
458
  isHovered,
428
459
  isActive,
429
- zIndex
430
- }) => {
460
+ zIndex,
461
+ onClick
462
+ }: EdgeLabelProps) => {
431
463
  return (
432
464
  <EdgeLabelRenderer>
433
465
  <Box
@@ -455,6 +487,7 @@ const EdgeLabel = memo<EdgeLabelProps>(({
455
487
  'data-edge-hovered': isHovered,
456
488
  'data-edge-active': isActive
457
489
  }}
490
+ onClick={onClick || undefined}
458
491
  >
459
492
  {stepNum !== null && (
460
493
  <Box className={edgesCss.stepEdgeNumber}>
@@ -469,4 +502,4 @@ const EdgeLabel = memo<EdgeLabelProps>(({
469
502
  </Box>
470
503
  </EdgeLabelRenderer>
471
504
  )
472
- }, deepEqual)
505
+ }
@@ -10,7 +10,7 @@ export const container = style({
10
10
  [mixColor]: `black`,
11
11
  [xyvars.edge.stroke]: vars.relation.lineColor,
12
12
  [xyvars.edge.strokeSelected]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
13
- [xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 20%)`,
13
+ [xyvars.edge.labelColor]: `color-mix(in srgb, ${vars.relation.labelColor}, rgba(255 255 255 / 0.85) 40%)`,
14
14
  [xyvars.edge.labelBgColor]: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 40%)`,
15
15
  [xyvars.edge.strokeWidth]: '3'
16
16
  }
@@ -159,6 +159,24 @@ export const cssEdgePath = style({
159
159
  }
160
160
  })
161
161
 
162
+ const labelBorderRadius = 2
163
+ export const stepEdgeNumber = style({
164
+ flex: '0 0 auto',
165
+ fontWeight: 600,
166
+ fontSize: rem(14),
167
+ lineHeight: 1,
168
+ padding: '5px 5px',
169
+ textAlign: 'center',
170
+ minWidth: 22,
171
+ borderTopLeftRadius: labelBorderRadius,
172
+ borderBottomLeftRadius: labelBorderRadius,
173
+ backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, ${mixColor} 5%)`,
174
+ fontVariantNumeric: 'tabular-nums'
175
+ })
176
+ globalStyle(`:where([data-mantine-color-scheme="dark"]) :where([data-likec4-color="gray"]) ${stepEdgeNumber}`, {
177
+ backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, ${mixColor} 15%)`
178
+ })
179
+
162
180
  export const varLabelX = createVar('label-x')
163
181
  export const varLabelY = createVar('label-y')
164
182
 
@@ -169,6 +187,7 @@ export const edgeLabel = style({
169
187
  left: 0,
170
188
  padding: '2px 4px 4px 4px',
171
189
  fontFamily: vars.likec4.font,
190
+ display: 'flex',
172
191
  position: 'absolute',
173
192
  pointerEvents: 'all',
174
193
  cursor: 'pointer',
@@ -177,7 +196,7 @@ export const edgeLabel = style({
177
196
  mixBlendMode: 'plus-lighter',
178
197
  color: xyvars.edge.labelColor,
179
198
  backgroundColor: xyvars.edge.labelBgColor,
180
- borderRadius: 3,
199
+ borderRadius: labelBorderRadius,
181
200
  transform: varTranslate,
182
201
  vars: {
183
202
  [varTranslate]: `translate(${fallbackVar(varLabelX, '-50%')}, ${fallbackVar(varLabelY, '-50%')})`
@@ -187,6 +206,10 @@ export const edgeLabel = style({
187
206
  transition: 'all 140ms ease-in-out',
188
207
  transform: `${varTranslate} scale(1.12)`
189
208
  },
209
+ [`&:has(${stepEdgeNumber})`]: {
210
+ padding: 0,
211
+ gap: 2
212
+ },
190
213
  [`&:is(${dimmed})`]: {
191
214
  opacity: 0.3,
192
215
  transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
@@ -197,32 +220,13 @@ export const edgeLabel = style({
197
220
  }
198
221
  })
199
222
 
200
- export const stepEdgeNumber = style({
201
- position: 'absolute',
202
- top: -2,
203
- left: -2,
204
- fontWeight: 500,
205
- fontSize: rem(16),
206
- lineHeight: 0.9,
207
- padding: '4px 6px',
208
- borderRadius: 99999,
209
- textAlign: 'center',
210
- minWidth: 22,
211
- backgroundColor: xyvars.edge.labelBgColor,
212
- transition: 'all 120ms ease-out',
213
- transform: 'translateX(-100%)',
214
- fontVariantNumeric: 'tabular-nums',
215
- selectors: {
216
- ':where([data-edge-hovered="true"],[data-edge-active="true"]) &': {
217
- fontWeight: 600,
218
- backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 5%)`
219
- }
220
- }
221
- })
222
-
223
223
  export const edgeLabelText = style({
224
224
  textAlign: 'left',
225
225
  whiteSpaceCollapse: 'preserve-breaks',
226
226
  fontSize: rem(14),
227
227
  lineHeight: 1.185
228
228
  })
229
+
230
+ globalStyle(`${edgeLabel}:has(${stepEdgeNumber}) ${edgeLabelText}`, {
231
+ padding: '2px 5px 4px 2px'
232
+ })
@@ -5,7 +5,7 @@ import clsx from 'clsx'
5
5
  import { deepEqual as eq } from 'fast-equals'
6
6
  import { memo } from 'react'
7
7
  import { clamp } from 'remeda'
8
- import { useDiagramState } from '../../../state/useDiagramStore'
8
+ import { useDiagramState } from '../../../hooks/useDiagramState'
9
9
  import type { CompoundXYFlowNode } from '../../types'
10
10
  import { NavigateToBtn } from '../shared/NavigateToBtn'
11
11
  import * as css from './CompoundNode.css'
@@ -7,7 +7,6 @@ import {
7
7
  Group,
8
8
  HoverCard,
9
9
  HoverCardDropdown,
10
- type HoverCardProps,
11
10
  HoverCardTarget,
12
11
  Stack,
13
12
  UnstyledButton
@@ -16,8 +15,8 @@ import { IconLink } from '@tabler/icons-react'
16
15
  import clsx from 'clsx'
17
16
  import { useId } from 'react'
18
17
  import { clamp } from 'remeda'
18
+ import { useDiagramState } from '../../../hooks/useDiagramState'
19
19
  import { type DiagramState } from '../../../state/diagramStore'
20
- import { useDiagramState } from '../../../state/useDiagramStore'
21
20
  import type { XYFlowNode } from '../../types'
22
21
  import { elementLink, trigger } from './ElementLink.css'
23
22
 
@@ -1,12 +1,13 @@
1
1
  import type { DiagramNode } from '@likec4/core/types'
2
- import { Text as MantineText } from '@mantine/core'
3
- import { Handle, type NodeProps, Position } from '@xyflow/react'
2
+ import { ActionIcon, Text as MantineText, Tooltip } from '@mantine/core'
3
+ import { IconFileSymlink } from '@tabler/icons-react'
4
+ import { Handle, type NodeProps, NodeToolbar, Position } from '@xyflow/react'
4
5
  import clsx from 'clsx'
5
6
  import { deepEqual as eq } from 'fast-equals'
6
7
  import { m, type Variants } from 'framer-motion'
7
8
  import { memo } from 'react'
8
9
  import { isNumber, isTruthy } from 'remeda'
9
- import { type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../../../state/useDiagramStore'
10
+ import { type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../../../hooks/useDiagramState'
10
11
  import type { ElementXYFlowNode } from '../../types'
11
12
  import { toDomPrecision } from '../../utils'
12
13
  import { NavigateToBtn } from '../shared/NavigateToBtn'
@@ -89,12 +90,13 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
89
90
  height
90
91
  }) {
91
92
  const store = useDiagramStoreApi()
92
- const { isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive } = useDiagramState(s => ({
93
+ const { isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive, hasGoToSource } = useDiagramState(s => ({
93
94
  isHovered: s.hoveredNodeId === id,
94
95
  isDimmed: s.dimmed.has(id),
95
96
  isInteractive: s.nodesDraggable || s.nodesSelectable || !!s.onNavigateTo,
96
97
  isHovercards: s.showElementLinks,
97
- hasOnNavigateTo: !!s.onNavigateTo
98
+ hasOnNavigateTo: !!s.onNavigateTo,
99
+ hasGoToSource: !!s.onOpenSourceElement
98
100
  }))
99
101
  const isNavigable = hasOnNavigateTo && !!element.navigateTo
100
102
 
@@ -1,7 +1,7 @@
1
1
  import { ActionIcon } from '@mantine/core'
2
2
  import { IconZoomScan } from '@tabler/icons-react'
3
3
  import clsx from 'clsx'
4
- import { useDiagramStoreApi } from '../../../state/useDiagramStore'
4
+ import { useDiagramStoreApi } from '../../../hooks/useDiagramState'
5
5
 
6
6
  export type NavigateToBtnProps = {
7
7
  xynodeId: string
@@ -4,9 +4,9 @@ import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyfl
4
4
  import { getNodeDimensions } from '@xyflow/system'
5
5
  import { useMemo, useRef } from 'react'
6
6
  import { isNullish } from 'remeda'
7
- import { useDiagramStoreApi } from '../../state/useDiagramStore'
8
- import type { XYFlowNode } from '../types'
9
- import { type XYStoreApi } from './useXYFlow'
7
+ import { useDiagramStoreApi } from '../hooks'
8
+ import { type XYStoreApi } from '../hooks/useXYFlow'
9
+ import type { XYFlowNode } from './types'
10
10
 
11
11
  abstract class Rect {
12
12
  id!: string
@@ -1,8 +1,9 @@
1
1
  import { hasAtLeast, invariant } from '@likec4/core'
2
2
  import type { DiagramEdge, Point } from '@likec4/core/types'
3
- import type { XYPosition } from '@xyflow/react'
3
+ import type { Rect, XYPosition } from '@xyflow/react'
4
4
  import { Bezier } from 'bezier-js'
5
5
  import { isArray } from 'remeda'
6
+ import type { InternalXYFlowNode } from './types'
6
7
 
7
8
  export function toDomPrecision(v: number | null) {
8
9
  if (v === null) {
@@ -15,6 +16,29 @@ export function distance(a: XYPosition, b: XYPosition) {
15
16
  return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
16
17
  }
17
18
 
19
+ export const nodeToRect = (nd: InternalXYFlowNode): Rect => ({
20
+ x: nd.internals.positionAbsolute.x,
21
+ y: nd.internals.positionAbsolute.y,
22
+ width: nd.width ?? nd.data.element.width,
23
+ height: nd.height ?? nd.data.element.height
24
+ })
25
+
26
+ /**
27
+ * Checks if a rectangle is completely inside another rectangle.
28
+ *
29
+ * @param test - The rectangle to test.
30
+ * @param target - The target rectangle.
31
+ * @returns `true` if the `test` rectangle is completely inside the `target` rectangle, otherwise `false`.
32
+ */
33
+ export const isInside = (test: Rect, target: Rect) => {
34
+ return (
35
+ test.x >= target.x
36
+ && test.y >= target.y
37
+ && test.x + test.width <= target.x + target.width
38
+ && test.y + test.height <= target.y + target.height
39
+ )
40
+ }
41
+
18
42
  export function bezierControlPoints(diagramEdge: DiagramEdge) {
19
43
  let [start, ...bezierPoints] = diagramEdge.points
20
44
  invariant(start, 'start should be defined')
@@ -1,4 +0,0 @@
1
- export { useDiagramState, useDiagramStoreApi } from './useDiagramStore';
2
- export { useNavigationHistory } from './useNavigationHistory';
3
- export type * from './diagramStore';
4
- export type * from './useDiagramStore';
@@ -1,2 +0,0 @@
1
- export type * from './diagramStore';
2
- export type * from './useDiagramStore';
@@ -1,5 +0,0 @@
1
- export declare function useNavigationHistory(): {
2
- hasBack: boolean;
3
- hasForward: boolean;
4
- ops: import('@mantine/hooks').UseStateHistoryHandlers<import('@likec4/core').ViewID>;
5
- };
package/dist/types.d.ts DELETED
@@ -1,20 +0,0 @@
1
- import { Fqn, RelationID, ViewID } from '@likec4/core';
2
- import { Simplify } from 'type-fest';
3
-
4
- type TaggedUnion<TagKey extends string, UnionMembers extends Record<string, Record<string, unknown>>> = {
5
- [Name in keyof UnionMembers]: Simplify<{
6
- [Key in TagKey]: Name;
7
- } & UnionMembers[Name]>;
8
- }[keyof UnionMembers];
9
- export type DiagramEditorCommand = TaggedUnion<'type', {
10
- showElement: {
11
- element: Fqn;
12
- };
13
- showRelation: {
14
- relation: RelationID;
15
- };
16
- showView: {
17
- view: ViewID;
18
- };
19
- }>;
20
- export {};
@@ -1 +0,0 @@
1
- export declare const panel: string;
@@ -1 +0,0 @@
1
- export declare function BackwardForwardPanel(): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +0,0 @@
1
- export { useXYFlow, useXYNodesData, useXYStore, useXYStoreApi } from './useXYFlow';
2
- export type * from './useXYFlow';
@@ -1,5 +0,0 @@
1
- export { useDiagramState, useDiagramStoreApi } from './useDiagramStore'
2
- export { useNavigationHistory } from './useNavigationHistory'
3
-
4
- export type * from './diagramStore'
5
- export type * from './useDiagramStore'
@@ -1,2 +0,0 @@
1
- export type * from './diagramStore'
2
- export type * from './useDiagramStore'