@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.
- package/dist/KeepAspectRatioContainer.d.ts +1 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +10 -5
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/hooks/useUpdateEffect.d.ts +1 -1
- package/dist/icons/Clipboard.d.ts +1 -1
- package/dist/icons/Link.d.ts +1 -1
- package/dist/icons/ZoomIn.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2604 -1502
- package/dist/mantine/EnsureMantine.d.ts +1 -1
- package/dist/state/DiagramContext.d.ts +2307 -192
- package/dist/state/WhenInitialized.d.ts +1 -1
- package/dist/state/diagramStore.d.ts +2220 -97
- package/dist/state/useDiagramStore.d.ts +2124 -9
- package/dist/style.css +210 -136
- package/dist/theme.css.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.d.ts +1 -2
- package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
- package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
- package/dist/ui/OptionsPanel.d.ts +1 -2
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/options/NodeOptions.d.ts +1 -2
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
- package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
- package/dist/xyflow/XYFlow.d.ts +5 -8
- package/dist/xyflow/XYFlowBackground.d.ts +6 -5
- package/dist/xyflow/XYFlowEvents.d.ts +60 -66
- package/dist/xyflow/XYFlowInner.d.ts +11 -0
- package/dist/xyflow/diagram-to-xyflow.d.ts +2 -2
- package/dist/xyflow/edges/RelationshipEdge.d.ts +17 -17
- package/dist/xyflow/edges/edges.css.d.ts +4 -2
- package/dist/xyflow/edges/utils.d.ts +3 -1
- package/dist/xyflow/hooks/useLayoutConstraints.d.ts +1 -1
- package/dist/xyflow/hooks/useXYFlow.d.ts +162 -162
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/compound/CompoundNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +2 -2
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
- package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
- package/dist/xyflow/types.d.ts +23 -23
- package/package.json +23 -21
- package/src/KeepAspectRatioContainer.tsx +1 -1
- package/src/LikeC4Diagram.css.ts +2 -0
- package/src/LikeC4Diagram.props.ts +9 -4
- package/src/LikeC4Diagram.tsx +17 -11
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +1 -0
- package/src/index.ts +0 -3
- package/src/state/DiagramContext.tsx +21 -12
- package/src/state/diagramStore.ts +177 -38
- package/src/theme.css.ts +1 -0
- package/src/ui/DiagramTitlePanel.css.ts +2 -1
- package/src/ui/DiagramTitlePanel.tsx +3 -4
- package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
- package/src/ui/DynamicViewWalkthrough.tsx +86 -0
- package/src/ui/OptionsPanel.tsx +5 -3
- package/src/ui/index.ts +1 -0
- package/src/ui/options/NodeOptions.tsx +23 -24
- package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
- package/src/xyflow/SyncWithDiagram.tsx +55 -107
- package/src/xyflow/XYFlow.tsx +51 -35
- package/src/xyflow/XYFlowBackground.tsx +7 -5
- package/src/xyflow/XYFlowEvents.tsx +100 -95
- package/src/xyflow/XYFlowInner.tsx +50 -0
- package/src/xyflow/diagram-to-xyflow.ts +16 -21
- package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
- package/src/xyflow/edges/edges.css.ts +68 -34
- package/src/xyflow/edges/utils.ts +31 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
- package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
- package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
- package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
- package/src/xyflow/nodes/element/element.css.ts +9 -2
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
- 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 {
|
|
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 [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
65
|
+
// If points are within 3px, consider them the same
|
|
67
66
|
const isSamePoint = (a: number, b: number) => {
|
|
68
|
-
return Math.abs(a - b) <
|
|
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>()
|
|
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
|
|
103
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
166
|
+
getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints)!),
|
|
151
167
|
...controlPoints,
|
|
152
|
-
|
|
168
|
+
getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints)!),
|
|
153
169
|
[targetCenterPos.positionAbsolute.x, targetCenterPos.positionAbsolute.y]
|
|
154
170
|
])!
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
return (
|
|
158
|
-
<g
|
|
159
|
-
|
|
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
|
|
164
|
-
refX=
|
|
165
|
-
refY="
|
|
187
|
+
viewBox="0 0 16 10"
|
|
188
|
+
refX={isModified ? '12' : '10'}
|
|
189
|
+
refY="5"
|
|
166
190
|
markerWidth="5"
|
|
167
|
-
markerHeight="
|
|
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
|
|
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={
|
|
212
|
+
r={3}
|
|
190
213
|
/>
|
|
191
|
-
))
|
|
192
|
-
|
|
193
|
-
{data.label && (
|
|
214
|
+
))}
|
|
215
|
+
{(data.label || isStepEdge) && (
|
|
194
216
|
<EdgeLabelRenderer>
|
|
195
217
|
<Box
|
|
196
|
-
className={clsx(
|
|
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]:
|
|
201
|
-
[edgesCss.varLabelY]:
|
|
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
|
-
|
|
211
|
-
{
|
|
212
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
79
|
+
strokeOpacity: 0.15
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
})
|
|
76
83
|
|
|
77
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
83
|
-
|
|
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>
|