@likec4/diagram 1.1.1 → 1.2.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 (80) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +1 -1
  2. package/dist/LikeC4Diagram.d.ts +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +10 -5
  4. package/dist/StaticLikeC4Diagram.d.ts +1 -1
  5. package/dist/hooks/useUpdateEffect.d.ts +1 -1
  6. package/dist/icons/Clipboard.d.ts +1 -1
  7. package/dist/icons/Link.d.ts +1 -1
  8. package/dist/icons/ZoomIn.d.ts +1 -1
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.mjs +2604 -1502
  11. package/dist/mantine/EnsureMantine.d.ts +1 -1
  12. package/dist/state/DiagramContext.d.ts +2307 -192
  13. package/dist/state/WhenInitialized.d.ts +1 -1
  14. package/dist/state/diagramStore.d.ts +2220 -97
  15. package/dist/state/useDiagramStore.d.ts +2124 -9
  16. package/dist/style.css +210 -136
  17. package/dist/theme.css.d.ts +1 -0
  18. package/dist/ui/DiagramTitlePanel.d.ts +1 -2
  19. package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
  20. package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
  21. package/dist/ui/OptionsPanel.d.ts +1 -2
  22. package/dist/ui/index.d.ts +1 -0
  23. package/dist/ui/options/NodeOptions.d.ts +1 -2
  24. package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
  25. package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
  26. package/dist/xyflow/XYFlow.d.ts +5 -8
  27. package/dist/xyflow/XYFlowBackground.d.ts +6 -5
  28. package/dist/xyflow/XYFlowEvents.d.ts +60 -66
  29. package/dist/xyflow/XYFlowInner.d.ts +11 -0
  30. package/dist/xyflow/diagram-to-xyflow.d.ts +2 -2
  31. package/dist/xyflow/edges/RelationshipEdge.d.ts +17 -17
  32. package/dist/xyflow/edges/edges.css.d.ts +4 -2
  33. package/dist/xyflow/edges/utils.d.ts +3 -1
  34. package/dist/xyflow/hooks/useLayoutConstraints.d.ts +1 -1
  35. package/dist/xyflow/hooks/useXYFlow.d.ts +162 -162
  36. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
  37. package/dist/xyflow/nodes/compound/CompoundNode.d.ts +1 -1
  38. package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
  39. package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
  40. package/dist/xyflow/nodes/element/ElementNode.d.ts +2 -2
  41. package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
  42. package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
  43. package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
  44. package/dist/xyflow/types.d.ts +23 -23
  45. package/package.json +23 -21
  46. package/src/KeepAspectRatioContainer.tsx +1 -1
  47. package/src/LikeC4Diagram.css.ts +2 -0
  48. package/src/LikeC4Diagram.props.ts +9 -4
  49. package/src/LikeC4Diagram.tsx +17 -11
  50. package/src/StaticLikeC4Diagram.tsx +1 -0
  51. package/src/globals.css.ts +1 -0
  52. package/src/index.ts +0 -3
  53. package/src/state/DiagramContext.tsx +21 -12
  54. package/src/state/diagramStore.ts +177 -38
  55. package/src/theme.css.ts +1 -0
  56. package/src/ui/DiagramTitlePanel.css.ts +2 -1
  57. package/src/ui/DiagramTitlePanel.tsx +3 -4
  58. package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
  59. package/src/ui/DynamicViewWalkthrough.tsx +86 -0
  60. package/src/ui/OptionsPanel.tsx +5 -3
  61. package/src/ui/index.ts +1 -0
  62. package/src/ui/options/NodeOptions.tsx +23 -24
  63. package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
  64. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
  65. package/src/xyflow/SyncWithDiagram.tsx +55 -107
  66. package/src/xyflow/XYFlow.tsx +51 -35
  67. package/src/xyflow/XYFlowBackground.tsx +7 -5
  68. package/src/xyflow/XYFlowEvents.tsx +100 -95
  69. package/src/xyflow/XYFlowInner.tsx +50 -0
  70. package/src/xyflow/diagram-to-xyflow.ts +16 -21
  71. package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
  72. package/src/xyflow/edges/edges.css.ts +68 -34
  73. package/src/xyflow/edges/utils.ts +31 -0
  74. package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
  75. package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
  76. package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
  77. package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
  78. package/src/xyflow/nodes/element/element.css.ts +9 -2
  79. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
  80. package/src/xyflow/types.ts +7 -8
@@ -0,0 +1,50 @@
1
+ import { isDynamicView } from '@likec4/core'
2
+ import { Controls } from '@xyflow/react'
3
+ import type { PropsWithChildren } from 'react'
4
+ import { isNonNull, isNonNullish, isNullish } from 'remeda'
5
+ import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
6
+ import { useDiagramState } from '../state/useDiagramStore'
7
+ import { DynamicViewWalkthrough } from '../ui'
8
+ import DiagramTitlePanel from '../ui/DiagramTitlePanel'
9
+ import OptionsPanel from '../ui/OptionsPanel'
10
+ import { XYFlowBackground } from './XYFlowBackground'
11
+
12
+ type XYFlowInnerProps = PropsWithChildren<{
13
+ background: NonNullable<LikeC4DiagramProperties['background']>
14
+ controls: boolean
15
+ showDiagramTitle: boolean
16
+ enableDynamicViewWalkthrough: boolean
17
+ }>
18
+
19
+ export function XYFlowInner({
20
+ children,
21
+ background,
22
+ controls,
23
+ showDiagramTitle,
24
+ enableDynamicViewWalkthrough
25
+ }: XYFlowInnerProps) {
26
+ const {
27
+ isDynamicView,
28
+ isDynamicViewActive,
29
+ readonly
30
+ } = useDiagramState(s => ({
31
+ isDynamicView: s.isDynamicView(),
32
+ readonly: s.readonly,
33
+ isDynamicViewActive: isNonNull(s.activeDynamicViewStep)
34
+ }))
35
+
36
+ const isBgWithPattern = background !== 'transparent' && background !== 'solid'
37
+
38
+ const isDiagramTitleVisible = !isDynamicViewActive && showDiagramTitle === true
39
+
40
+ return (
41
+ <>
42
+ {isBgWithPattern && <XYFlowBackground background={background} />}
43
+ {controls && <Controls position={'bottom-right'} />}
44
+ {readonly === false && <OptionsPanel />}
45
+ {isDiagramTitleVisible && <DiagramTitlePanel />}
46
+ {isDynamicView && enableDynamicViewWalkthrough && <DynamicViewWalkthrough />}
47
+ {children}
48
+ </>
49
+ )
50
+ }
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  type DiagramNode,
3
3
  type DiagramView,
4
+ extractStep,
4
5
  type Fqn,
5
6
  invariant,
6
7
  type NonEmptyArray,
7
8
  nonNullable,
8
9
  type Point
9
10
  } from '@likec4/core'
10
- import { getBezierEdgeCenter } from '@xyflow/react'
11
+ import { Bezier } from 'bezier-js'
11
12
  import { hasAtLeast } from 'remeda'
12
13
  import type { XYFlowData } from '../xyflow/types'
13
14
 
@@ -20,24 +21,16 @@ function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
20
21
 
21
22
  while (hasAtLeast(bezierPoints, 3)) {
22
23
  const [cp1, cp2, end, ...rest] = bezierPoints
23
- const [centerX, centerY] = getBezierEdgeCenter({
24
- sourceX: start[0],
25
- sourceY: start[1],
26
- targetX: end[0],
27
- targetY: end[1],
28
- sourceControlX: cp1[0],
29
- sourceControlY: cp1[1],
30
- targetControlX: cp2[0],
31
- targetControlY: cp2[1]
24
+ const bezier = new Bezier(start[0], start[1], cp1[0], cp1[1], cp2[0], cp2[1], end[0], end[1])
25
+ // const { x, y } = bezier.get(0.5)
26
+ const inflections = bezier.inflections()
27
+ if (inflections.length === 0) {
28
+ inflections.push(0.5)
29
+ }
30
+ inflections.forEach(t => {
31
+ const { x, y } = bezier.get(t)
32
+ handles.push([x, y])
32
33
  })
33
- handles.push(
34
- [centerX, centerY]
35
- )
36
- // if (rest.length === 0) {
37
- // handles.push(
38
- // end
39
- // )
40
- // }
41
34
  bezierPoints = rest
42
35
  start = end
43
36
  }
@@ -50,12 +43,13 @@ function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
50
43
  const nodeZIndex = (node: DiagramNode) => node.level + 1
51
44
 
52
45
  export function diagramViewToXYFlowData(
53
- view: Pick<DiagramView, 'nodes' | 'edges'>,
46
+ view: Pick<DiagramView, 'nodes' | 'edges' | '__'>,
54
47
  opts: {
55
48
  draggable: boolean
56
49
  selectable: boolean
57
50
  }
58
51
  ): XYFlowData {
52
+ const isDynamicView = view.__ === 'dynamic'
59
53
  const editor: XYFlowData = {
60
54
  nodes: [],
61
55
  edges: []
@@ -178,11 +172,12 @@ export function diagramViewToXYFlowData(
178
172
  controlPoints,
179
173
  headPoint: edge.headArrowPoint ?? null,
180
174
  tailPoint: edge.tailArrowPoint ?? null,
181
- label: edge.labelBBox && edge.label
175
+ stepNum: isDynamicView ? extractStep(edge.id) : null,
176
+ label: !!edge.labelBBox
182
177
  ? {
183
178
  bbox: edge.labelBBox,
184
179
  // text: edge.labels ? edge.labels.map(l => l.text).join('\n') : ''
185
- text: edge.label
180
+ text: edge.label ?? ''
186
181
  }
187
182
  : null
188
183
  },
@@ -8,14 +8,13 @@ import clsx from 'clsx'
8
8
  import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
9
9
  import { deepEqual as eq } from 'fast-equals'
10
10
  import { type CSSProperties, memo } from 'react'
11
- import { hasAtLeast } from 'remeda'
11
+ import { first, hasAtLeast, last, zip } from 'remeda'
12
12
  import { useDiagramState } from '../../state'
13
13
  import { ZIndexes } from '../const'
14
14
  import { useXYStoreApi } from '../hooks'
15
15
  import { type XYFlowEdge } from '../types'
16
- import { toDomPrecision } from '../utils'
17
16
  import * as edgesCss from './edges.css'
18
- import { getEdgeParams } from './utils'
17
+ import { getEdgeParams, getNodeIntersectionFromCenterToPoint } from './utils'
19
18
  // import { getEdgeParams } from './utils'
20
19
 
21
20
  // function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size = 8): string {
@@ -63,9 +62,9 @@ function bezierPath(bezierSpline: NonEmptyArray<Point>) {
63
62
  return path
64
63
  }
65
64
 
66
- // If points are within 2px, consider them the same
65
+ // If points are within 3px, consider them the same
67
66
  const isSamePoint = (a: number, b: number) => {
68
- return Math.abs(a - b) < 2.1
67
+ return Math.abs(a - b) < 3.1
69
68
  }
70
69
 
71
70
  const isSamePosition = (a: XYPosition, [bx, by]: Point) => {
@@ -81,10 +80,15 @@ const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>)
81
80
  && isSamePoint(prev.sourceY, next.sourceY)
82
81
  && isSamePoint(prev.targetX, next.targetX)
83
82
  && isSamePoint(prev.targetY, next.targetY)
84
- && eq(prev.data, next.data)
83
+ && eq(prev.data.stepNum, next.data.stepNum)
84
+ && eq(prev.data.edge, next.data.edge)
85
+ && zip(prev.data.controlPoints, next.data.controlPoints).every(([a, b]) =>
86
+ isSamePoint(a[0], b[0]) && isSamePoint(a[1], b[1])
87
+ )
85
88
  )
86
89
 
87
- const curve = d3line<Point>().curve(curveCatmullRomOpen.alpha(1))
90
+ const curve = d3line<Point>()
91
+ .curve(curveCatmullRomOpen)
88
92
 
89
93
  export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(function RelationshipEdgeR({
90
94
  id,
@@ -99,14 +103,20 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
99
103
  const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
100
104
  const targetNode = nonNullable(nodeLookup.get(target)!, `target node ${target} not found`)
101
105
 
102
- const isNotModified = isSamePosition(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
103
- && isSamePosition(targetNode.internals.positionAbsolute, targetNode.data.element.position)
106
+ const isModified = !isSamePosition(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
107
+ || !isSamePosition(targetNode.internals.positionAbsolute, targetNode.data.element.position)
104
108
 
105
109
  const { edge: diagramEdge, controlPoints } = data
110
+
106
111
  // const edgePath = bezierPath(edge.points)
107
112
 
108
113
  const color = diagramEdge.color ?? 'gray'
109
- const isHovered = useDiagramState(s => s.hoveredEdgeId === id)
114
+ const isStepEdge = data.stepNum !== null
115
+ const { isActive, isHovered, isDimmed } = useDiagramState(s => ({
116
+ isActive: s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum,
117
+ isHovered: s.hoveredEdgeId === id,
118
+ isDimmed: s.dimmed.has(id)
119
+ }))
110
120
 
111
121
  const line = diagramEdge.line ?? 'dashed'
112
122
  const isDotted = line === 'dotted'
@@ -123,8 +133,14 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
123
133
 
124
134
  let edgePath: string, labelX: number, labelY: number
125
135
 
126
- if (isNotModified) {
136
+ if (!isModified) {
127
137
  edgePath = bezierPath(diagramEdge.points)
138
+ // if (diagramEdge.tailArrowPoint) {
139
+ // edgePath = `M ${diagramEdge.tailArrowPoint[0]},${diagramEdge.tailArrowPoint[1]} L ${edgePath.substring(1)}`
140
+ // }
141
+ // if (diagramEdge.headArrowPoint) {
142
+ // edgePath += ` L ${diagramEdge.headArrowPoint[0]},${diagramEdge.headArrowPoint[1]}`
143
+ // }
128
144
  labelX = data.label?.bbox.x ?? 0
129
145
  labelY = data.label?.bbox.y ?? 0
130
146
  } else {
@@ -147,28 +163,36 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
147
163
 
148
164
  edgePath = curve([
149
165
  [sourceCenterPos.positionAbsolute.x, sourceCenterPos.positionAbsolute.y],
150
- [sx, sy],
166
+ getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints)!),
151
167
  ...controlPoints,
152
- [tx, ty],
168
+ getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints)!),
153
169
  [targetCenterPos.positionAbsolute.x, targetCenterPos.positionAbsolute.y]
154
170
  ])!
155
171
  }
156
172
 
157
173
  return (
158
- <g className={clsx(edgesCss.container)} data-likec4-color={color} data-edge-hovered={isHovered}>
159
- <g className={clsx(edgesCss.fillStrokeCtx)}>
174
+ <g
175
+ className={clsx([
176
+ edgesCss.container,
177
+ isDimmed && edgesCss.dimmed
178
+ ])}
179
+ data-likec4-color={color}
180
+ data-edge-dir={diagramEdge.dir}
181
+ data-edge-active={isActive}
182
+ data-edge-hovered={isHovered}>
183
+ <g className={edgesCss.markerContext}>
160
184
  <defs>
161
185
  <marker
162
186
  id={`arrow-${id}`}
163
- viewBox="0 0 10 8"
164
- refX="0"
165
- refY="4"
187
+ viewBox="0 0 16 10"
188
+ refX={isModified ? '12' : '10'}
189
+ refY="5"
166
190
  markerWidth="5"
167
- markerHeight="5"
191
+ markerHeight="4"
168
192
  markerUnits="strokeWidth"
169
193
  preserveAspectRatio="xMaxYMid meet"
170
194
  orient="auto-start-reverse">
171
- <path d="M 0 0 L 10 4 L 0 8 z" stroke="context-stroke" fill="context-stroke" />
195
+ <path d="M 0 0 L 16 5 L 0 10 z" stroke="context-stroke" fill="context-stroke" />
172
196
  </marker>
173
197
  </defs>
174
198
  </g>
@@ -179,37 +203,49 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
179
203
  markerStart={diagramEdge.tailArrow ? marker : undefined}
180
204
  markerEnd={diagramEdge.headArrow ? marker : undefined}
181
205
  style={style} />
182
- {
183
- /* controlPoints.map((p, i) => (
206
+ {isModified && controlPoints.map((p, i) => (
184
207
  <circle
185
208
  className={edgesCss.controlPoint}
186
209
  key={i}
187
210
  cx={p[0]}
188
211
  cy={p[1]}
189
- r={5}
212
+ r={3}
190
213
  />
191
- )) */
192
- }
193
- {data.label && (
214
+ ))}
215
+ {(data.label || isStepEdge) && (
194
216
  <EdgeLabelRenderer>
195
217
  <Box
196
- className={clsx(edgesCss.container, edgesCss.edgeLabel)}
218
+ className={clsx([
219
+ edgesCss.container,
220
+ edgesCss.edgeLabel,
221
+ isDimmed && edgesCss.dimmed
222
+ ])}
197
223
  data-likec4-color={color}
198
224
  style={{
199
225
  ...assignInlineVars({
200
- [edgesCss.varLabelX]: toDomPrecision(labelX) + 'px',
201
- [edgesCss.varLabelY]: toDomPrecision(labelY) + 'px'
226
+ [edgesCss.varLabelX]: Math.floor(labelX) + 'px',
227
+ [edgesCss.varLabelY]: Math.floor(labelY) + 'px'
228
+ }),
229
+ ...(data.label && {
230
+ maxWidth: data.label.bbox.width + 12
202
231
  }),
203
- maxWidth: data.label.bbox.width + 10,
204
232
  zIndex: edgeLookup.get(id)?.zIndex ?? ZIndexes.Edge
205
233
  }}
206
234
  mod={{
207
- 'data-edge-hovered': isHovered
235
+ 'data-edge-hovered': isHovered,
236
+ 'data-edge-active': isActive
208
237
  }}
209
238
  >
210
- <Box className={edgesCss.edgeLabelBody}>
211
- {data.label.text}
212
- </Box>
239
+ {isStepEdge && (
240
+ <Box className={edgesCss.stepEdgeNumber}>
241
+ {data.stepNum}
242
+ </Box>
243
+ )}
244
+ {data.label?.text && (
245
+ <Box className={edgesCss.edgeLabelText}>
246
+ {data.label.text}
247
+ </Box>
248
+ )}
213
249
  {
214
250
  /* <Popover
215
251
  position="bottom"
@@ -1,5 +1,5 @@
1
1
  import { rem } from '@mantine/core'
2
- import { createVar, generateIdentifier, globalKeyframes, globalStyle, style } from '@vanilla-extract/css'
2
+ import { createVar, fallbackVar, generateIdentifier, globalKeyframes, globalStyle, style } from '@vanilla-extract/css'
3
3
  import { vars, xyvars } from '../../theme.css'
4
4
 
5
5
  const mixColor = createVar('mix-color')
@@ -15,6 +15,13 @@ export const container = style({
15
15
  }
16
16
  })
17
17
 
18
+ export const dimmed = style({})
19
+
20
+ globalStyle(`svg:has(${dimmed})`, {
21
+ filter: vars.filterDimmed,
22
+ transition: 'filter 800ms ease-out'
23
+ })
24
+
18
25
  globalStyle(`:where([data-mantine-color-scheme="dark"]) ${container}`, {
19
26
  vars: {
20
27
  [mixColor]: `white`,
@@ -32,7 +39,7 @@ globalStyle(`:where(${isSelected}) ${container}`, {
32
39
  }
33
40
  })
34
41
 
35
- globalStyle(`${container}[data-edge-hovered='true']`, {
42
+ globalStyle(`${container}:is([data-edge-hovered='true'],[data-edge-active='true'])`, {
36
43
  vars: {
37
44
  // [xyvars.edge.stroke]: `color-mix(in srgb, ${vars.relation.lineColor}, ${mixColor} 35%)`,
38
45
  [xyvars.edge.stroke]: xyvars.edge.strokeSelected,
@@ -54,39 +61,46 @@ globalStyle(`:where(${isSelected}) ${container}[data-edge-hovered='true']`, {
54
61
  globalStyle(`.react-flow__edges > svg`, {
55
62
  mixBlendMode: 'screen'
56
63
  })
57
- globalStyle(`:where([data-mantine-color-scheme="dark"]) .react-flow__edges > svg`, {
58
- mixBlendMode: 'lighten'
59
- })
64
+ // globalStyle(`:where([data-mantine-color-scheme="dark"]) .react-flow__edges > svg`, {
65
+ // mixBlendMode: 'lighten'
66
+ // })
60
67
 
61
68
  export const edgePathBg = style({
62
69
  strokeWidth: xyvars.edge.strokeWidth,
63
- strokeOpacity: 0.05,
70
+ strokeOpacity: 0.08,
64
71
  // transition: 'stroke-width 175ms ease-in-out',
65
72
  // transition: 'stroke-width 175ms ease-in-out, stroke-opacity 150ms ease-out',
66
73
  transitionProperty: 'stroke-width, stroke-opacity',
67
74
  transitionDuration: '155ms',
68
75
  transitionTimingFunction: 'ease-out',
69
76
  selectors: {
70
- [`:where(${isSelected}, [data-edge-hovered='true']) &`]: {
77
+ [`:where(${isSelected}, [data-edge-active='true'], [data-edge-hovered='true']) &`]: {
71
78
  strokeWidth: `calc(${xyvars.edge.strokeWidth} + 8px)`,
72
- strokeOpacity: 0.11
79
+ strokeOpacity: 0.15
73
80
  }
74
81
  }
75
82
  })
76
83
 
77
- export const fillStrokeCtx = style({
84
+ // To fix issue with marker not inheriting color from path - we need to create container
85
+ export const markerContext = style({
78
86
  fill: xyvars.edge.stroke,
79
- stroke: xyvars.edge.stroke
87
+ stroke: xyvars.edge.stroke,
88
+ pointerEvents: 'none'
80
89
  })
81
90
 
82
91
  export const controlPoint = style({
83
92
  fill: xyvars.edge.stroke,
84
- stroke: xyvars.edge.stroke
85
- })
86
-
87
- globalStyle(`:where(${isSelected}) ${fillStrokeCtx}`, {
88
- vars: {
89
- [xyvars.edge.stroke]: xyvars.edge.strokeSelected
93
+ stroke: xyvars.edge.stroke,
94
+ fillOpacity: 0.75,
95
+ strokeWidth: 1,
96
+ cursor: 'grab',
97
+ pointerEvents: 'visible',
98
+ selectors: {
99
+ [`:where(${isSelected}, [data-edge-active='true'], [data-edge-hovered='true']) &`]: {
100
+ transition: 'all 150ms ease-out',
101
+ fillOpacity: 1,
102
+ strokeWidth: 3
103
+ }
90
104
  }
91
105
  })
92
106
 
@@ -107,13 +121,16 @@ export const cssEdgePath = style({
107
121
  selectors: {
108
122
  [`:where([data-edge-hovered='true']) &`]: {
109
123
  animationName: strokeKeyframes,
110
- animationDelay: '500ms',
124
+ animationDelay: '350ms',
111
125
  transition: 'all 130ms ease-out'
112
126
  },
113
- [`:where(${isSelected}) &`]: {
127
+ [`:where(${isSelected}, [data-edge-active='true']) &`]: {
114
128
  animationName: strokeKeyframes,
115
129
  animationDelay: '0ms',
116
130
  transition: 'all 130ms ease-out'
131
+ },
132
+ [`:where([data-edge-dir='back']) &`]: {
133
+ animationDirection: 'reverse'
117
134
  }
118
135
  }
119
136
  })
@@ -131,35 +148,52 @@ export const edgeLabel = style({
131
148
  pointerEvents: 'all',
132
149
  cursor: 'pointer',
133
150
  transformOrigin: '50% 50%',
134
- textAlign: 'left',
135
151
  mixBlendMode: 'screen',
136
- padding: '2px 6px 4px 6px',
137
- width: 'max-content',
152
+ color: xyvars.edge.labelColor,
138
153
  backgroundColor: xyvars.edge.labelBgColor,
139
154
  borderRadius: '3px',
140
155
  transform: varTranslate,
141
- backfaceVisibility: 'hidden',
142
156
  vars: {
143
- // [varTranslate]: `translate(calc(${varLabelX} - 50%), calc(${varLabelY} - 50%))`,
144
- [varTranslate]: `translate(${varLabelX}, ${varLabelY})`,
145
- [varLabelX]: '50%',
146
- [varLabelY]: '50%'
157
+ [varTranslate]: `translate(${fallbackVar(varLabelX, '50%')}, ${fallbackVar(varLabelY, '50%')})`
147
158
  },
148
159
  selectors: {
149
- // [mantine.darkSelector]: {
150
- // mixBlendMode: 'luminosity'
151
- // },
152
160
  '&[data-edge-hovered="true"]': {
153
161
  transition: 'all 140ms ease-out',
154
162
  transform: `${varTranslate} scale(1.1)`
163
+ },
164
+ [`&:is(${dimmed})`]: {
165
+ transition: 'filter 800ms ease-out',
166
+ filter: vars.filterDimmed
155
167
  }
156
168
  }
157
169
  })
158
170
 
159
- export const edgeLabelBody = style({
160
- // display: 'inline',
161
- color: xyvars.edge.labelColor,
171
+ export const stepEdgeNumber = style({
172
+ position: 'absolute',
173
+ top: -2,
174
+ left: -2,
175
+ fontWeight: 500,
176
+ fontSize: rem(16),
177
+ lineHeight: 0.9,
178
+ padding: '4px 6px',
179
+ borderRadius: 99999,
180
+ textAlign: 'center',
181
+ minWidth: 22,
182
+ backgroundColor: xyvars.edge.labelBgColor,
183
+ transition: 'all 120ms ease-out',
184
+ transform: 'translateX(-100%)',
185
+ fontVariantNumeric: 'tabular-nums',
186
+ selectors: {
187
+ ':where([data-edge-hovered="true"],[data-edge-active="true"]) &': {
188
+ fontWeight: 600,
189
+ backgroundColor: `color-mix(in srgb, ${vars.relation.labelBgColor}, transparent 5%)`
190
+ }
191
+ }
192
+ })
193
+
194
+ export const edgeLabelText = style({
195
+ padding: '2px 6px 4px 6px',
196
+ textAlign: 'left',
162
197
  fontSize: rem(12.5),
163
- lineHeight: 1.12,
164
- transition: 'color 120ms ease-out'
198
+ lineHeight: 1.2
165
199
  })
@@ -1,7 +1,38 @@
1
+ import type { Point } from '@likec4/core'
1
2
  import { Position, type XYPosition } from '@xyflow/react'
2
3
  import { getNodeDimensions } from '@xyflow/system'
3
4
  import type { InternalXYFlowNode } from '../types'
4
5
 
6
+ // this helper function returns the intersection point
7
+ // of the line between the center of the intersectionNode and the target node
8
+ export function getNodeIntersectionFromCenterToPoint(intersectionNode: InternalXYFlowNode, [x1, y1]: Point) {
9
+ // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
10
+ const {
11
+ width: intersectionNodeWidth,
12
+ height: intersectionNodeHeight
13
+ } = getNodeDimensions(intersectionNode)
14
+
15
+ const intersectionNodePosition = intersectionNode.internals.positionAbsolute
16
+
17
+ const w = intersectionNodeWidth / 2
18
+ const h = intersectionNodeHeight / 2
19
+
20
+ const x2 = intersectionNodePosition.x + w
21
+ const y2 = intersectionNodePosition.y + h
22
+ // const x1 = targetPoint.x
23
+ // const y1 = targetPoint.y
24
+
25
+ const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h)
26
+ const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h)
27
+ const a = 1 / (Math.abs(xx1) + Math.abs(yy1))
28
+ const xx3 = a * xx1
29
+ const yy3 = a * yy1
30
+ const x = w * (xx3 + yy3) + x2
31
+ const y = h * (-xx3 + yy3) + y2
32
+
33
+ return [x, y] as const
34
+ }
35
+
5
36
  // this helper function returns the intersection point
6
37
  // of the line between the center of the intersectionNode and the target node
7
38
  function getNodeIntersection(intersectionNode: InternalXYFlowNode, targetNode: InternalXYFlowNode): XYPosition {
@@ -11,6 +11,11 @@ export const container = style({
11
11
  margin: 0
12
12
  })
13
13
 
14
+ export const dimmed = style({
15
+ filter: vars.filterDimmed,
16
+ transition: 'filter 800ms ease-out'
17
+ })
18
+
14
19
  export const varTransparency = createVar('transparency')
15
20
 
16
21
  const outlineColor = fallbackVar(
@@ -71,8 +76,7 @@ export const transparent = style({
71
76
  vars: {
72
77
  [bgTransparencyDelta]: '15%'
73
78
  }
74
- },
75
-
79
+ }
76
80
  }
77
81
  })
78
82
 
@@ -37,15 +37,17 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
37
37
  // const h = toDomPrecision(height ?? compound.height)
38
38
  const opacity = style.opacity ?? 100
39
39
 
40
- const { isHovered, hasOnNavigateTo } = useDiagramState(s => ({
40
+ const { isHovered, isDimmed, hasOnNavigateTo } = useDiagramState(s => ({
41
41
  isHovered: s.hoveredNodeId === id,
42
+ isDimmed: s.dimmed.has(id),
42
43
  hasOnNavigateTo: !!s.onNavigateTo
43
44
  }))
44
45
  const isnavigable = !!compound.navigateTo && hasOnNavigateTo
45
46
 
47
+ // invert opacity to transparency
46
48
  const transparency = clamp(100 - opacity, {
47
49
  min: 0,
48
- max: 98
50
+ max: 99
49
51
  })
50
52
 
51
53
  return (
@@ -53,7 +55,8 @@ export const CompoundNodeMemo = /* @__PURE__ */ memo<CompoundNodeProps>(function
53
55
  className={clsx(
54
56
  css.container,
55
57
  'likec4-compound-node',
56
- opacity < 100 && 'likec4-compound-transparent'
58
+ opacity < 100 && 'likec4-compound-transparent',
59
+ isDimmed && css.dimmed
57
60
  )}
58
61
  mod={{
59
62
  'compound-depth': depth,
@@ -68,7 +68,10 @@ export function ElementLink({
68
68
  <span>links</span>
69
69
  </UnstyledButton>
70
70
  </HoverCardTarget>
71
- <HoverCardDropdown onClick={e => e.stopPropagation()} p={'xs'}>
71
+ <HoverCardDropdown
72
+ onPointerDownCapture={e => e.stopPropagation()}
73
+ onClick={e => e.stopPropagation()}
74
+ p={'xs'}>
72
75
  <Stack gap={'xs'}>
73
76
  {element.links.map((link) => (
74
77
  <Group key={link} wrap="nowrap" gap={'sm'}>
@@ -79,8 +82,15 @@ export function ElementLink({
79
82
  </Box>
80
83
  <CopyButton value={link}>
81
84
  {({ copied, copy }) => (
82
- <Button size="compact-xs" fz={'12'} onClick={copy} color={copied ? 'teal' : 'gray'}>
83
- {copied ? 'Copied' : 'Copy'}
85
+ <Button
86
+ size="compact-xs"
87
+ fz={'10'}
88
+ variant="light"
89
+ onClick={copy}
90
+ color={copied
91
+ ? 'teal'
92
+ : 'gray'}>
93
+ {copied ? 'copied' : 'copy'}
84
94
  </Button>
85
95
  )}
86
96
  </CopyButton>