@likec4/diagram 1.1.1 → 1.2.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.
- package/dist/KeepAspectRatioContainer.d.ts +1 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +7 -2
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2517 -1326
- package/dist/mantine/EnsureMantine.d.ts +1 -1
- package/dist/state/DiagramContext.d.ts +2125 -10
- package/dist/state/WhenInitialized.d.ts +1 -1
- package/dist/state/diagramStore.d.ts +2133 -10
- 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 +4 -7
- package/dist/xyflow/XYFlowBackground.d.ts +5 -4
- package/dist/xyflow/XYFlowEvents.d.ts +57 -63
- package/dist/xyflow/XYFlowInner.d.ts +11 -0
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +14 -14
- package/dist/xyflow/edges/edges.css.d.ts +4 -2
- package/dist/xyflow/edges/utils.d.ts +2 -0
- package/dist/xyflow/hooks/useXYFlow.d.ts +126 -126
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- 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 +1 -1
- 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 +18 -18
- package/package.json +19 -16
- 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 +97 -92
- 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
|
@@ -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>
|
|
@@ -7,16 +7,7 @@ import { useDiagramState } from '../../../state'
|
|
|
7
7
|
import type { ElementXYFlowNode } from '../../types'
|
|
8
8
|
import { toDomPrecision } from '../../utils'
|
|
9
9
|
import { NavigateToBtn } from '../shared/NavigateToBtn'
|
|
10
|
-
import
|
|
11
|
-
container,
|
|
12
|
-
cssElement,
|
|
13
|
-
cssNavigateBtn,
|
|
14
|
-
cssShapeSvg,
|
|
15
|
-
description as cssdescription,
|
|
16
|
-
indicator,
|
|
17
|
-
technology as csstechnology,
|
|
18
|
-
title as cssTitle
|
|
19
|
-
} from './element.css'
|
|
10
|
+
import * as css from './element.css'
|
|
20
11
|
import { ElementIcon } from './ElementIcon'
|
|
21
12
|
import { ElementLink } from './ElementLink'
|
|
22
13
|
import { ElementShapeSvg, SelectedIndicator } from './ElementShapeSvg'
|
|
@@ -38,6 +29,13 @@ const variants = {
|
|
|
38
29
|
delay: isNumber(scale) && scale > selectedScale ? 0.075 : 0
|
|
39
30
|
}
|
|
40
31
|
}),
|
|
32
|
+
// dimmed: {
|
|
33
|
+
// filter: 'brightness(0.5)',
|
|
34
|
+
// transition: {
|
|
35
|
+
// duration: 0.8,
|
|
36
|
+
// ease: 'easeInOut',
|
|
37
|
+
// }
|
|
38
|
+
// },
|
|
41
39
|
// dragging: {
|
|
42
40
|
// scale: selectedScale
|
|
43
41
|
// },
|
|
@@ -76,8 +74,9 @@ export function ElementNode({
|
|
|
76
74
|
width,
|
|
77
75
|
height
|
|
78
76
|
}: ElementNodeProps) {
|
|
79
|
-
const { isHovered, hasOnNavigateTo, isHovercards, isInteractive } = useDiagramState(s => ({
|
|
77
|
+
const { isHovered, isDimmed, hasOnNavigateTo, isHovercards, isInteractive } = useDiagramState(s => ({
|
|
80
78
|
isHovered: s.hoveredNodeId === id,
|
|
79
|
+
isDimmed: s.dimmed.has(id),
|
|
81
80
|
isInteractive: s.nodesDraggable || s.nodesSelectable || !!s.onNavigateTo,
|
|
82
81
|
isHovercards: s.showElementLinks,
|
|
83
82
|
hasOnNavigateTo: !!s.onNavigateTo
|
|
@@ -112,7 +111,11 @@ export function ElementNode({
|
|
|
112
111
|
return (
|
|
113
112
|
<motion.div
|
|
114
113
|
id={id}
|
|
115
|
-
className={clsx(
|
|
114
|
+
className={clsx([
|
|
115
|
+
css.container,
|
|
116
|
+
isDimmed && css.dimmed,
|
|
117
|
+
'likec4-element-node'
|
|
118
|
+
])}
|
|
116
119
|
data-likec4-color={element.color}
|
|
117
120
|
data-likec4-shape={element.shape}
|
|
118
121
|
variants={variants}
|
|
@@ -162,12 +165,14 @@ export function ElementNode({
|
|
|
162
165
|
style={{ visibility: 'hidden' }}
|
|
163
166
|
/>
|
|
164
167
|
<svg
|
|
165
|
-
className={
|
|
168
|
+
className={clsx(
|
|
169
|
+
css.cssShapeSvg
|
|
170
|
+
)}
|
|
166
171
|
viewBox={`0 0 ${w} ${h}`}
|
|
167
172
|
width={w}
|
|
168
173
|
height={h}
|
|
169
174
|
>
|
|
170
|
-
<g className={indicator}>
|
|
175
|
+
<g className={css.indicator}>
|
|
171
176
|
<SelectedIndicator
|
|
172
177
|
shape={element.shape}
|
|
173
178
|
w={w}
|
|
@@ -180,24 +185,24 @@ export function ElementNode({
|
|
|
180
185
|
h={h}
|
|
181
186
|
/>
|
|
182
187
|
</svg>
|
|
183
|
-
<div className={clsx(
|
|
188
|
+
<div className={clsx(css.element, 'likec4-element')}>
|
|
184
189
|
{element.icon && <ElementIcon node={element} />}
|
|
185
190
|
<Text
|
|
186
191
|
component="div"
|
|
187
|
-
className={clsx(
|
|
192
|
+
className={clsx(css.title, 'likec4-element-title')}>
|
|
188
193
|
{element.title}
|
|
189
194
|
</Text>
|
|
190
195
|
{element.technology && (
|
|
191
196
|
<Text
|
|
192
197
|
component="div"
|
|
193
|
-
className={clsx(
|
|
198
|
+
className={clsx(css.technology, 'likec4-element-technology')}>
|
|
194
199
|
{element.technology}
|
|
195
200
|
</Text>
|
|
196
201
|
)}
|
|
197
202
|
{element.description && (
|
|
198
203
|
<Text
|
|
199
204
|
component="div"
|
|
200
|
-
className={clsx(
|
|
205
|
+
className={clsx(css.description, 'likec4-element-description')}>
|
|
201
206
|
{element.description}
|
|
202
207
|
</Text>
|
|
203
208
|
)}
|
|
@@ -219,7 +224,7 @@ export function ElementNode({
|
|
|
219
224
|
))} */
|
|
220
225
|
}
|
|
221
226
|
{isHovercards && element.links && <ElementLink element={element} />}
|
|
222
|
-
{isNavigable && <NavigateToBtn xynodeId={id} className={cssNavigateBtn} />}
|
|
227
|
+
{isNavigable && <NavigateToBtn xynodeId={id} className={css.cssNavigateBtn} />}
|
|
223
228
|
</motion.div>
|
|
224
229
|
)
|
|
225
230
|
}
|
|
@@ -19,6 +19,13 @@ export const container = style({
|
|
|
19
19
|
}
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
+
export const dimmed = style({})
|
|
23
|
+
|
|
24
|
+
globalStyle(`.react-flow__node:has(${dimmed})`, {
|
|
25
|
+
filter: vars.filterDimmed,
|
|
26
|
+
transition: 'filter 800ms ease-out'
|
|
27
|
+
})
|
|
28
|
+
|
|
22
29
|
const indicatorKeyframes = generateIdentifier('indicator')
|
|
23
30
|
globalKeyframes(indicatorKeyframes, {
|
|
24
31
|
'from': {
|
|
@@ -92,7 +99,7 @@ export const description = style({
|
|
|
92
99
|
fontStyle: 'normal',
|
|
93
100
|
fontWeight: 400,
|
|
94
101
|
fontSize: rem(14),
|
|
95
|
-
lineHeight: 1.
|
|
102
|
+
lineHeight: 1.25,
|
|
96
103
|
textAlign: 'center',
|
|
97
104
|
textWrap: 'pretty',
|
|
98
105
|
color: vars.element.loContrast,
|
|
@@ -120,7 +127,7 @@ export const technology = style({
|
|
|
120
127
|
}
|
|
121
128
|
})
|
|
122
129
|
|
|
123
|
-
export const
|
|
130
|
+
export const element = style({
|
|
124
131
|
position: 'relative',
|
|
125
132
|
width: '100%',
|
|
126
133
|
height: '100%',
|
|
@@ -14,6 +14,7 @@ export function NavigateToBtn({ xynodeId, className }: NavigateToBtnProps) {
|
|
|
14
14
|
<ActionIcon
|
|
15
15
|
className={clsx('nodrag nopan', className)}
|
|
16
16
|
radius="xl"
|
|
17
|
+
onPointerDownCapture={e => e.stopPropagation()}
|
|
17
18
|
onClick={event => {
|
|
18
19
|
event.stopPropagation()
|
|
19
20
|
diagramApi.getState().triggerOnNavigateTo(xynodeId, event)
|
package/src/xyflow/types.ts
CHANGED
|
@@ -24,6 +24,10 @@ type TypedXYFlowNode<D extends Record<string, unknown>, T extends string> = SetR
|
|
|
24
24
|
SetRequired<Node<D, T>, 'type'>,
|
|
25
25
|
'id' | 'type'
|
|
26
26
|
>
|
|
27
|
+
type TypedXYFlowEdge<D extends Record<string, unknown>, T extends string> = SetReadonly<
|
|
28
|
+
SetRequired<Edge<D, T>, 'type' | 'data'>,
|
|
29
|
+
'id' | 'type'
|
|
30
|
+
>
|
|
27
31
|
|
|
28
32
|
export type ElementXYFlowNode = TypedXYFlowNode<ElementNodeData, 'element'>
|
|
29
33
|
|
|
@@ -57,20 +61,15 @@ export type RelationshipData = {
|
|
|
57
61
|
headPoint: Point | null
|
|
58
62
|
tailPoint: Point | null
|
|
59
63
|
type: 'bezier' | 'poly'
|
|
64
|
+
// if dynamic view
|
|
65
|
+
stepNum: number | null
|
|
60
66
|
label: null | {
|
|
61
67
|
bbox: BBox
|
|
62
68
|
text: string
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
export type RelationshipEdge =
|
|
67
|
-
SetRequired<
|
|
68
|
-
Omit<Edge<RelationshipData>, 'type'>,
|
|
69
|
-
'data'
|
|
70
|
-
> & {
|
|
71
|
-
type: 'relationship'
|
|
72
|
-
}
|
|
73
|
-
>
|
|
72
|
+
export type RelationshipEdge = TypedXYFlowEdge<RelationshipData, 'relationship'>
|
|
74
73
|
|
|
75
74
|
export type XYFlowEdge = RelationshipEdge
|
|
76
75
|
|