@likec4/diagram 1.0.0-next.2 → 1.0.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/LikeC4Diagram.css.d.ts +0 -1
- package/dist/LikeC4Diagram.d.ts +1 -2
- package/dist/LikeC4Diagram.props.d.ts +24 -25
- package/dist/StaticLikeC4Diagram.d.ts +1 -2
- package/dist/globals.css.d.ts +1 -0
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/useSetState.d.ts +3 -0
- package/dist/hooks/useUpdateEffect.d.ts +4 -0
- package/dist/icons/Clipboard.d.ts +0 -1
- package/dist/icons/Link.d.ts +0 -1
- package/dist/icons/ZoomIn.d.ts +0 -1
- package/dist/icons/index.d.ts +0 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.mjs +9388 -0
- package/dist/mantine/EnsureMantine.d.ts +2 -4
- package/dist/mantine/theme.d.ts +3 -0
- package/dist/mantine.css.d.ts +1 -0
- package/dist/state/DiagramContext.d.ts +386 -0
- package/dist/state/WhenInitialized.d.ts +3 -0
- package/dist/state/diagramStore.d.ts +428 -0
- package/dist/state/index.d.ts +4 -2
- package/dist/state/useDiagramStore.d.ts +387 -0
- package/dist/style.css +1212 -1
- package/dist/theme.css.d.ts +16 -2
- package/dist/ui/Camera.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.d.ts +0 -1
- package/dist/ui/OptionsPanel.css.d.ts +0 -1
- package/dist/ui/OptionsPanel.d.ts +0 -1
- package/dist/ui/index.d.ts +0 -1
- package/dist/ui/options/NodeOptions.d.ts +0 -1
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -5
- package/dist/xyflow/SyncWithDiagram.d.ts +5 -0
- package/dist/xyflow/XYFlowBackground.d.ts +0 -1
- package/dist/xyflow/XYFlowEvents.d.ts +5 -15
- package/dist/xyflow/XYFlowWrapper.d.ts +4 -5
- package/dist/xyflow/const.d.ts +1 -2
- package/dist/xyflow/diagram-to-xyflow.d.ts +4 -2
- package/dist/xyflow/edges/RelationshipEdge.d.ts +7 -7
- package/dist/xyflow/edges/edges.css.d.ts +2 -1
- package/dist/xyflow/edges/utils.d.ts +3 -4
- package/dist/xyflow/hooks/index.d.ts +2 -37
- package/dist/xyflow/hooks/{use-layout-/321/201onstraints.d.ts → useLayoutConstraints.d.ts} +2 -4
- package/dist/xyflow/hooks/useXYFlow.d.ts +266 -0
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +0 -1
- package/dist/xyflow/nodes/compound/CompoundNode.d.ts +0 -1
- package/dist/xyflow/nodes/compound/index.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementLink.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -2
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -3
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -3
- package/dist/xyflow/nodes/element/element.css.d.ts +0 -1
- package/dist/xyflow/nodes/element/index.d.ts +1 -2
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +0 -1
- package/dist/xyflow/types.d.ts +20 -8
- package/dist/xyflow/utils.d.ts +0 -1
- package/package.json +36 -34
- package/src/LikeC4Diagram.css.ts +31 -0
- package/src/LikeC4Diagram.props.ts +154 -0
- package/src/LikeC4Diagram.tsx +119 -0
- package/src/StaticLikeC4Diagram.tsx +39 -0
- package/src/globals.css.ts +128 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useSetState.ts +19 -0
- package/src/hooks/useUpdateEffect.ts +28 -0
- package/src/icons/Clipboard.tsx +24 -0
- package/src/icons/Link.tsx +23 -0
- package/src/icons/ZoomIn.tsx +25 -0
- package/src/icons/index.ts +3 -0
- package/src/index.ts +11 -0
- package/src/mantine/EnsureMantine.tsx +14 -0
- package/src/mantine/theme.ts +19 -0
- package/src/mantine.css.ts +4 -0
- package/src/reset.d.ts +2 -0
- package/src/state/DiagramContext.tsx +42 -0
- package/src/state/WhenInitialized.tsx +24 -0
- package/src/state/diagramStore.ts +358 -0
- package/src/state/index.ts +4 -0
- package/src/state/useDiagramStore.ts +39 -0
- package/src/theme.css.ts +64 -0
- package/src/ui/Camera.tsx +174 -0
- package/src/ui/KeepAspectRatio.css.ts +7 -0
- package/src/ui/KeepAspectRatio.tsx +35 -0
- package/src/ui/OptionsPanel.css.ts +10 -0
- package/src/ui/OptionsPanel.tsx +65 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/options/NodeOptions.tsx +253 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +93 -0
- package/src/xyflow/SyncWithDiagram.tsx +113 -0
- package/src/xyflow/XYFlowBackground.tsx +33 -0
- package/src/xyflow/XYFlowEvents.tsx +100 -0
- package/src/xyflow/XYFlowWrapper.tsx +166 -0
- package/src/xyflow/const.ts +7 -0
- package/src/xyflow/diagram-to-xyflow.ts +197 -0
- package/src/xyflow/edges/RelationshipEdge.tsx +288 -0
- package/src/xyflow/edges/edges.css.ts +124 -0
- package/src/xyflow/edges/utils.ts +110 -0
- package/src/xyflow/hooks/index.ts +2 -0
- package/src/xyflow/hooks/useLayoutConstraints.ts +289 -0
- package/src/xyflow/hooks/useXYFlow.ts +20 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +191 -0
- package/src/xyflow/nodes/compound/CompoundNode.tsx +105 -0
- package/src/xyflow/nodes/compound/index.ts +1 -0
- package/src/xyflow/nodes/element/ElementIcon.tsx +44 -0
- package/src/xyflow/nodes/element/ElementLink.css.ts +67 -0
- package/src/xyflow/nodes/element/ElementLink.tsx +94 -0
- package/src/xyflow/nodes/element/ElementNode.tsx +225 -0
- package/src/xyflow/nodes/element/ElementShapeSvg.tsx +181 -0
- package/src/xyflow/nodes/element/element.css.ts +261 -0
- package/src/xyflow/nodes/element/index.ts +1 -0
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +25 -0
- package/src/xyflow/types.ts +100 -0
- package/src/xyflow/utils.ts +12 -0
- package/dist/EmbeddedLikeC4Diagram.css.d.ts +0 -6
- package/dist/EmbeddedLikeC4Diagram.d.ts +0 -18
- package/dist/bundle.d.ts +0 -3
- package/dist/hooks/use-set-state.d.ts +0 -2
- package/dist/hooks/use-update-effect.d.ts +0 -4
- package/dist/index.css.d.ts +0 -2
- package/dist/index.js +0 -13043
- package/dist/mantine/index.d.ts +0 -3
- package/dist/state/DiagramState.d.ts +0 -162
- package/dist/xyflow/UpdateOnDiagramChange.d.ts +0 -10
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Position, type XYPosition } from '@xyflow/react'
|
|
2
|
+
import { getNodeDimensions } from '@xyflow/system'
|
|
3
|
+
import type { InternalXYFlowNode } from '../types'
|
|
4
|
+
|
|
5
|
+
// this helper function returns the intersection point
|
|
6
|
+
// of the line between the center of the intersectionNode and the target node
|
|
7
|
+
function getNodeIntersection(intersectionNode: InternalXYFlowNode, targetNode: InternalXYFlowNode): XYPosition {
|
|
8
|
+
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
|
9
|
+
const {
|
|
10
|
+
width: intersectionNodeWidth,
|
|
11
|
+
height: intersectionNodeHeight
|
|
12
|
+
} = getNodeDimensions(intersectionNode)
|
|
13
|
+
const {
|
|
14
|
+
width: targetNodeWidth,
|
|
15
|
+
height: targetNodeHeight
|
|
16
|
+
} = getNodeDimensions(targetNode)
|
|
17
|
+
|
|
18
|
+
const intersectionNodePosition = intersectionNode.internals.positionAbsolute
|
|
19
|
+
const targetPosition = targetNode.internals.positionAbsolute
|
|
20
|
+
|
|
21
|
+
const w = intersectionNodeWidth / 2
|
|
22
|
+
const h = intersectionNodeHeight / 2
|
|
23
|
+
|
|
24
|
+
const x2 = intersectionNodePosition.x + w
|
|
25
|
+
const y2 = intersectionNodePosition.y + h
|
|
26
|
+
const x1 = targetPosition.x + targetNodeWidth / 2
|
|
27
|
+
const y1 = targetPosition.y + targetNodeHeight / 2
|
|
28
|
+
|
|
29
|
+
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h)
|
|
30
|
+
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h)
|
|
31
|
+
const a = 1 / (Math.abs(xx1) + Math.abs(yy1))
|
|
32
|
+
const xx3 = a * xx1
|
|
33
|
+
const yy3 = a * yy1
|
|
34
|
+
const x = w * (xx3 + yy3) + x2
|
|
35
|
+
const y = h * (-xx3 + yy3) + y2
|
|
36
|
+
|
|
37
|
+
return { x, y }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
|
41
|
+
export function getPointPosition(node: InternalXYFlowNode, intersectionPoint: XYPosition) {
|
|
42
|
+
const n = {
|
|
43
|
+
// x: node.data.element.position[0],
|
|
44
|
+
// y: node.data.element.position[1],
|
|
45
|
+
...node.internals.positionAbsolute,
|
|
46
|
+
...getNodeDimensions(node)
|
|
47
|
+
}
|
|
48
|
+
const nx = Math.round(n.x)
|
|
49
|
+
const ny = Math.round(n.y)
|
|
50
|
+
const px = Math.round(intersectionPoint.x)
|
|
51
|
+
const py = Math.round(intersectionPoint.y)
|
|
52
|
+
|
|
53
|
+
let handlePosition = Position.Top
|
|
54
|
+
if (px <= nx + 1) {
|
|
55
|
+
handlePosition = Position.Left
|
|
56
|
+
} else if (px >= nx + n.width - 1) {
|
|
57
|
+
handlePosition = Position.Right
|
|
58
|
+
} else if (py <= ny + 1) {
|
|
59
|
+
handlePosition = Position.Top
|
|
60
|
+
} else if (py >= n.y + n.height - 1) {
|
|
61
|
+
handlePosition = Position.Bottom
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let handleWidth = 8, handleHeight = 8
|
|
65
|
+
|
|
66
|
+
let offsetX = 0
|
|
67
|
+
let offsetY = 0
|
|
68
|
+
|
|
69
|
+
// this is a tiny detail to make the markerEnd of an edge visible.
|
|
70
|
+
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
|
71
|
+
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
|
72
|
+
switch (handlePosition) {
|
|
73
|
+
case Position.Left:
|
|
74
|
+
offsetX = -handleWidth
|
|
75
|
+
break
|
|
76
|
+
case Position.Right:
|
|
77
|
+
offsetX = handleWidth
|
|
78
|
+
break
|
|
79
|
+
case Position.Top:
|
|
80
|
+
offsetY = -handleHeight
|
|
81
|
+
break
|
|
82
|
+
case Position.Bottom:
|
|
83
|
+
offsetY = handleHeight
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
intersectionPoint.x + offsetX,
|
|
89
|
+
intersectionPoint.y + offsetY,
|
|
90
|
+
handlePosition
|
|
91
|
+
] as const
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
|
95
|
+
export function getEdgeParams(source: InternalXYFlowNode, target: InternalXYFlowNode) {
|
|
96
|
+
const sourceIntersectionPoint = getNodeIntersection(source, target)
|
|
97
|
+
const targetIntersectionPoint = getNodeIntersection(target, source)
|
|
98
|
+
|
|
99
|
+
const [sx, sy, sourcePos] = getPointPosition(source, sourceIntersectionPoint)
|
|
100
|
+
const [tx, ty, targetPos] = getPointPosition(target, targetIntersectionPoint)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
sx,
|
|
104
|
+
sy,
|
|
105
|
+
tx,
|
|
106
|
+
ty,
|
|
107
|
+
sourcePos,
|
|
108
|
+
targetPos
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { invariant, isAncestor, nonNullable } from '@likec4/core'
|
|
2
|
+
import { Expression, Expression as Expr, Operator, Solver, Strength, Variable } from '@lume/kiwi'
|
|
3
|
+
import type { InternalNode, ReactFlowProps, XYPosition } from '@xyflow/react'
|
|
4
|
+
import { getNodeDimensions } from '@xyflow/system'
|
|
5
|
+
import { useMemo, useRef } from 'react'
|
|
6
|
+
import { isNullish } from 'remeda'
|
|
7
|
+
import { useDiagramStoreApi } from '../../state'
|
|
8
|
+
import { type XYFlowInstance, XYFlowNode } from '../types'
|
|
9
|
+
import { useXYStoreApi, type XYStoreApi } from './useXYFlow'
|
|
10
|
+
|
|
11
|
+
abstract class Rect {
|
|
12
|
+
id!: string
|
|
13
|
+
minX = new Variable()
|
|
14
|
+
minY = new Variable()
|
|
15
|
+
maxX: Expression | Variable = new Variable()
|
|
16
|
+
maxY: Expression | Variable = new Variable()
|
|
17
|
+
|
|
18
|
+
get x() {
|
|
19
|
+
return this.minX
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get y() {
|
|
23
|
+
return this.minY
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get positionAbsolute(): XYPosition {
|
|
27
|
+
return {
|
|
28
|
+
x: this.x.value(),
|
|
29
|
+
y: this.y.value()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get dimensions() {
|
|
34
|
+
return {
|
|
35
|
+
width: this.maxX.value() - this.minX.value(),
|
|
36
|
+
height: this.maxY.value() - this.minY.value()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Position relative to parent
|
|
41
|
+
get position(): XYPosition {
|
|
42
|
+
const positionAbsolute = this.positionAbsolute
|
|
43
|
+
if (!this.parent) {
|
|
44
|
+
return positionAbsolute
|
|
45
|
+
}
|
|
46
|
+
const parentPosition = this.parent.positionAbsolute
|
|
47
|
+
return {
|
|
48
|
+
x: positionAbsolute.x - parentPosition.x,
|
|
49
|
+
y: positionAbsolute.y - parentPosition.y
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
protected abstract parent: Compound | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class Compound extends Rect {
|
|
56
|
+
protected readonly children = [] as Rect[]
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
protected solver: Solver,
|
|
60
|
+
xynode: InternalNode<XYFlowNode>,
|
|
61
|
+
protected readonly parent: Compound | null = null
|
|
62
|
+
) {
|
|
63
|
+
super()
|
|
64
|
+
this.id = xynode.id
|
|
65
|
+
|
|
66
|
+
solver.createConstraint(this.maxX, Operator.Ge, this.minX)
|
|
67
|
+
solver.createConstraint(this.maxY, Operator.Ge, this.minY)
|
|
68
|
+
|
|
69
|
+
if (parent) {
|
|
70
|
+
parent.addChild(this)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
addChild<R extends Rect>(rect: R) {
|
|
75
|
+
this.children.push(rect)
|
|
76
|
+
|
|
77
|
+
const leftPadding = new Expr(
|
|
78
|
+
rect.minX,
|
|
79
|
+
[-1, this.minX]
|
|
80
|
+
)
|
|
81
|
+
const { weak } = Strength
|
|
82
|
+
this.solver.createConstraint(leftPadding, Operator.Ge, 40)
|
|
83
|
+
this.solver.createConstraint(leftPadding, Operator.Eq, 40, weak)
|
|
84
|
+
|
|
85
|
+
const topPadding = new Expr(
|
|
86
|
+
rect.minY,
|
|
87
|
+
[-1, this.minY]
|
|
88
|
+
)
|
|
89
|
+
this.solver.createConstraint(topPadding, Operator.Ge, 60)
|
|
90
|
+
this.solver.createConstraint(topPadding, Operator.Eq, 60, weak)
|
|
91
|
+
|
|
92
|
+
const rightPadding = new Expr(
|
|
93
|
+
this.maxX,
|
|
94
|
+
[-1, rect.maxX]
|
|
95
|
+
)
|
|
96
|
+
this.solver.createConstraint(rightPadding, Operator.Ge, 40)
|
|
97
|
+
this.solver.createConstraint(rightPadding, Operator.Eq, 40, weak)
|
|
98
|
+
|
|
99
|
+
const bottomPadding = new Expr(
|
|
100
|
+
this.maxY,
|
|
101
|
+
[-1, rect.maxY]
|
|
102
|
+
)
|
|
103
|
+
this.solver.createConstraint(bottomPadding, Operator.Ge, 40)
|
|
104
|
+
this.solver.createConstraint(bottomPadding, Operator.Eq, 40, weak)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class Leaf extends Rect {
|
|
109
|
+
constructor(
|
|
110
|
+
solver: Solver,
|
|
111
|
+
xynode: InternalNode<XYFlowNode>,
|
|
112
|
+
public readonly parent: Compound | null = null,
|
|
113
|
+
public readonly isEditing: boolean = false
|
|
114
|
+
) {
|
|
115
|
+
super()
|
|
116
|
+
this.id = xynode.id
|
|
117
|
+
|
|
118
|
+
// const isEditing: boolean = xynode.dragging ?? false
|
|
119
|
+
const x = Math.ceil(xynode.internals.positionAbsolute.x)
|
|
120
|
+
const y = Math.ceil(xynode.internals.positionAbsolute.y)
|
|
121
|
+
|
|
122
|
+
const { width, height } = getNodeDimensions(xynode)
|
|
123
|
+
|
|
124
|
+
this.maxX = new Expression(this.minX, Math.ceil(width))
|
|
125
|
+
this.maxY = new Expression(this.minY, Math.ceil(height))
|
|
126
|
+
|
|
127
|
+
// solver.createConstraint(this.width, Operator.Eq, width, Strength.required)
|
|
128
|
+
// solver.createConstraint(this.height, Operator.Eq, height, Strength.required)
|
|
129
|
+
|
|
130
|
+
if (isEditing) {
|
|
131
|
+
const superStrong = Strength.create(100, 0, 0)
|
|
132
|
+
solver.addEditVariable(this.minX, superStrong)
|
|
133
|
+
solver.addEditVariable(this.minY, superStrong)
|
|
134
|
+
solver.suggestValue(this.minX, x)
|
|
135
|
+
solver.suggestValue(this.minY, y)
|
|
136
|
+
} else {
|
|
137
|
+
const halfMedium = Strength.create(0.0, 1.0, 0.0, 0.5)
|
|
138
|
+
solver.createConstraint(this.minX, Operator.Eq, x, halfMedium)
|
|
139
|
+
solver.createConstraint(this.minY, Operator.Eq, y, halfMedium)
|
|
140
|
+
// const weaker = Strength.create(0.0, 0.0, 1.0, 0.5)
|
|
141
|
+
// const weak = Strength.create(0.0, 0.0, 1.0)
|
|
142
|
+
// const x = new Expr(
|
|
143
|
+
// this.minX,
|
|
144
|
+
// [-1, pos.x]
|
|
145
|
+
// )
|
|
146
|
+
// solver.createConstraint(x, Operator.Eq, 0, weaker)
|
|
147
|
+
// solver.createConstraint(x, Operator.Ge, -30, weak)
|
|
148
|
+
// solver.createConstraint(x, Operator.Le, 30, weak)
|
|
149
|
+
|
|
150
|
+
// const y = new Expr(
|
|
151
|
+
// this.minY,
|
|
152
|
+
// [-1, pos.y]
|
|
153
|
+
// )
|
|
154
|
+
// solver.createConstraint(y, Operator.Eq, 0, weaker)
|
|
155
|
+
// solver.createConstraint(y, Operator.Ge, -30, weak)
|
|
156
|
+
// solver.createConstraint(y, Operator.Le, 30, weak)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (parent) {
|
|
160
|
+
parent.addChild(this)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createLayoutConstraints(
|
|
166
|
+
xyflow: XYFlowInstance,
|
|
167
|
+
xyflowApi: XYStoreApi,
|
|
168
|
+
draggingNodeId: string
|
|
169
|
+
) {
|
|
170
|
+
const { parentLookup, nodeLookup } = xyflowApi.getState()
|
|
171
|
+
const solver = new Solver()
|
|
172
|
+
const rects = new Map<string, Leaf | Compound>()
|
|
173
|
+
|
|
174
|
+
const traverse = new Array<{ xynode: InternalNode<XYFlowNode>; parent: Compound | null }>()
|
|
175
|
+
|
|
176
|
+
for (const [, xynode] of nodeLookup) {
|
|
177
|
+
if (isNullish(xynode.parentId)) {
|
|
178
|
+
traverse.push({
|
|
179
|
+
xynode,
|
|
180
|
+
parent: null
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
while (traverse.length > 0) {
|
|
186
|
+
const { xynode, parent } = traverse.shift()!
|
|
187
|
+
const isDragging = xynode.dragging === true || xynode.id === draggingNodeId
|
|
188
|
+
|
|
189
|
+
// Traverse children if the node is a compound, not dragging, and is an ancestor of the dragging node
|
|
190
|
+
const shouldTraverse = !isDragging && xynode.type === 'compound' && isAncestor(xynode.id, draggingNodeId)
|
|
191
|
+
|
|
192
|
+
const rect = shouldTraverse ? new Compound(solver, xynode, parent) : new Leaf(solver, xynode, parent, isDragging)
|
|
193
|
+
rects.set(xynode.id, rect)
|
|
194
|
+
|
|
195
|
+
if (shouldTraverse) {
|
|
196
|
+
parentLookup.get(xynode.id)?.forEach(child => {
|
|
197
|
+
traverse.push({
|
|
198
|
+
xynode: child,
|
|
199
|
+
parent: rect as Compound
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
solver.updateVariables()
|
|
206
|
+
solver.maxIterations = 1000
|
|
207
|
+
|
|
208
|
+
function updateXYFlowNodes() {
|
|
209
|
+
solver.updateVariables()
|
|
210
|
+
|
|
211
|
+
xyflow.setNodes(nodes =>
|
|
212
|
+
nodes.map((n) => {
|
|
213
|
+
if (n.id === draggingNodeId || n.dragging === true) {
|
|
214
|
+
return n
|
|
215
|
+
}
|
|
216
|
+
const rect = rects.get(n.id)
|
|
217
|
+
if (!rect) {
|
|
218
|
+
return n
|
|
219
|
+
}
|
|
220
|
+
const dimensions = rect.dimensions
|
|
221
|
+
const newXY = rect.position
|
|
222
|
+
|
|
223
|
+
if (newXY.x !== n.position.x || newXY.y !== n.position.y) {
|
|
224
|
+
return {
|
|
225
|
+
...n,
|
|
226
|
+
position: rect.position,
|
|
227
|
+
width: dimensions.width,
|
|
228
|
+
height: dimensions.height,
|
|
229
|
+
measured: dimensions
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (dimensions.width !== n.width || dimensions.height !== n.height) {
|
|
233
|
+
return {
|
|
234
|
+
...n,
|
|
235
|
+
width: dimensions.width,
|
|
236
|
+
height: dimensions.height,
|
|
237
|
+
measured: dimensions
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return n
|
|
241
|
+
})
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let animationFrameId: number | null = null
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Move the editing node to the given position.
|
|
249
|
+
*/
|
|
250
|
+
function onNodeDrag(xynode: XYFlowNode) {
|
|
251
|
+
const pos = nodeLookup.get(xynode.id)!.internals.positionAbsolute
|
|
252
|
+
const rect = nonNullable(rects.get(xynode.id))
|
|
253
|
+
solver.suggestValue(rect.minX, Math.ceil(pos.x))
|
|
254
|
+
solver.suggestValue(rect.minY, Math.ceil(pos.y))
|
|
255
|
+
animationFrameId ??= requestAnimationFrame(() => {
|
|
256
|
+
updateXYFlowNodes()
|
|
257
|
+
animationFrameId = null
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
onNodeDrag,
|
|
263
|
+
updateXYFlowNodes
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type LayoutConstraints = Required<Pick<ReactFlowProps<XYFlowNode>, 'onNodeDragStart' | 'onNodeDrag' | 'onNodeDragStop'>>
|
|
268
|
+
/**
|
|
269
|
+
* Keeps the layout constraints (parent nodes and children) when dragging a node
|
|
270
|
+
*/
|
|
271
|
+
export function useLayoutConstraints(): LayoutConstraints {
|
|
272
|
+
const diagramApi = useDiagramStoreApi()
|
|
273
|
+
const xyflowApi = useXYStoreApi()
|
|
274
|
+
const solverRef = useRef<ReturnType<typeof createLayoutConstraints>>()
|
|
275
|
+
|
|
276
|
+
return useMemo((): LayoutConstraints => ({
|
|
277
|
+
onNodeDragStart: (_event, xynode) => {
|
|
278
|
+
const { xyflow } = diagramApi.getState()
|
|
279
|
+
solverRef.current = createLayoutConstraints(xyflow, xyflowApi, xynode.id)
|
|
280
|
+
},
|
|
281
|
+
onNodeDrag: (_event, xynode) => {
|
|
282
|
+
invariant(solverRef.current, 'solverRef.current should be defined')
|
|
283
|
+
solverRef.current?.onNodeDrag(xynode)
|
|
284
|
+
},
|
|
285
|
+
onNodeDragStop: (_event, _xynode) => {
|
|
286
|
+
solverRef.current = undefined
|
|
287
|
+
}
|
|
288
|
+
}), [xyflowApi])
|
|
289
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useNodesData, useReactFlow, useStore, useStoreApi } from '@xyflow/react'
|
|
2
|
+
import { shallowEqual } from 'fast-equals'
|
|
3
|
+
import type { XYFlowEdge, XYFlowNode, XYFlowState } from '../types'
|
|
4
|
+
|
|
5
|
+
export const useXYFlow = useReactFlow<XYFlowNode, XYFlowEdge>
|
|
6
|
+
|
|
7
|
+
export const useXYNodesData = useNodesData<XYFlowNode>
|
|
8
|
+
|
|
9
|
+
export function useXYStore<StateSlice = unknown>(
|
|
10
|
+
selector: (state: XYFlowState) => StateSlice,
|
|
11
|
+
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
|
|
12
|
+
): StateSlice {
|
|
13
|
+
return useStore(
|
|
14
|
+
// @ts-expect-error - types are not correct
|
|
15
|
+
selector,
|
|
16
|
+
equalityFn ?? shallowEqual
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
export const useXYStoreApi = useStoreApi<XYFlowNode, XYFlowEdge>
|
|
20
|
+
export type XYStoreApi = ReturnType<typeof useXYStoreApi>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { rem } from '@mantine/core'
|
|
2
|
+
import { createVar, fallbackVar, generateIdentifier, globalKeyframes, globalStyle, style } from '@vanilla-extract/css'
|
|
3
|
+
import { mantine } from '../../../mantine.css'
|
|
4
|
+
import { vars } from '../../../theme.css'
|
|
5
|
+
|
|
6
|
+
export const cssContainer = style({
|
|
7
|
+
width: '100%',
|
|
8
|
+
height: '100%',
|
|
9
|
+
position: 'relative',
|
|
10
|
+
padding: 0,
|
|
11
|
+
margin: 0
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const bgTransparency = createVar('bgTransparency')
|
|
15
|
+
|
|
16
|
+
const outlineColor = fallbackVar(
|
|
17
|
+
mantine.colors.primaryColors.outline,
|
|
18
|
+
mantine.colors.primaryColors.filled,
|
|
19
|
+
vars.element.stroke
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
export const cssCompound = style({
|
|
23
|
+
width: '100%',
|
|
24
|
+
height: '100%',
|
|
25
|
+
position: 'relative',
|
|
26
|
+
borderRadius: rem(6),
|
|
27
|
+
boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.1) , 0 2px 2px -1px rgba(0,0,0,0.4)',
|
|
28
|
+
padding: rem(8),
|
|
29
|
+
background: vars.element.fill,
|
|
30
|
+
transition: 'all 300ms ease-out',
|
|
31
|
+
selectors: {
|
|
32
|
+
[`:where(.react-flow__node.selected) &`]: {
|
|
33
|
+
boxShadow: 'none'
|
|
34
|
+
},
|
|
35
|
+
':where(.react-flow__node:focus-visible) &': {
|
|
36
|
+
transitionDuration: '0ms',
|
|
37
|
+
outline: `3px solid ${outlineColor}`,
|
|
38
|
+
outlineOffset: rem(1.5)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
const cssTransparentCompound = style({
|
|
45
|
+
width: '100%',
|
|
46
|
+
height: '100%',
|
|
47
|
+
position: 'relative',
|
|
48
|
+
borderRadius: rem(8),
|
|
49
|
+
borderStyle: 'dashed',
|
|
50
|
+
borderWidth: rem(3.5),
|
|
51
|
+
borderColor: vars.element.fill,
|
|
52
|
+
boxShadow: '0 4px 10px 0.5px rgba(0,0,0,0.05) , 0 2px 2px -1px rgba(0,0,0,0.04)',
|
|
53
|
+
padding: rem(8),
|
|
54
|
+
background: `color-mix(in srgb , ${vars.element.fill}, transparent ${bgTransparency})`,
|
|
55
|
+
transition: 'all 200ms ease-out',
|
|
56
|
+
vars: {
|
|
57
|
+
[bgTransparency]: '90%'
|
|
58
|
+
},
|
|
59
|
+
selectors: {
|
|
60
|
+
[`:where(${cssContainer}[data-hovered]) &`]: {
|
|
61
|
+
transitionDelay: '300ms',
|
|
62
|
+
vars: {
|
|
63
|
+
[bgTransparency]: '65%'
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[`:where(.react-flow__node.selected) &`]: {
|
|
67
|
+
transitionDelay: '50ms',
|
|
68
|
+
boxShadow: 'none',
|
|
69
|
+
borderColor: 'transparent',
|
|
70
|
+
vars: {
|
|
71
|
+
[bgTransparency]: '15%'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export const cssTitle = style({
|
|
78
|
+
fontFamily: vars.compound.font,
|
|
79
|
+
fontOpticalSizing: 'auto',
|
|
80
|
+
fontStyle: 'normal',
|
|
81
|
+
textAlign: 'left',
|
|
82
|
+
display: 'inline-block',
|
|
83
|
+
fontWeight: 600,
|
|
84
|
+
fontSize: '13px',
|
|
85
|
+
textTransform: 'uppercase',
|
|
86
|
+
letterSpacing: '0.2px',
|
|
87
|
+
lineHeight: 1,
|
|
88
|
+
opacity: 0.75,
|
|
89
|
+
color: vars.compound.titleColor,
|
|
90
|
+
paddingLeft: rem(12),
|
|
91
|
+
mixBlendMode: 'lighten',
|
|
92
|
+
selectors: {
|
|
93
|
+
[`:where([data-likec4-navigable='true']) &`]: {
|
|
94
|
+
paddingLeft: rem(26)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const indicatorKeyframes = generateIdentifier('indicator')
|
|
100
|
+
globalKeyframes(indicatorKeyframes, {
|
|
101
|
+
'from': {
|
|
102
|
+
opacity: 0.6
|
|
103
|
+
},
|
|
104
|
+
'to': {
|
|
105
|
+
opacity: 0.3
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
export const cssIndicator = style({
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
top: '0',
|
|
112
|
+
left: '0',
|
|
113
|
+
width: '100%',
|
|
114
|
+
height: '100%',
|
|
115
|
+
pointerEvents: 'none',
|
|
116
|
+
overflow: 'visible',
|
|
117
|
+
visibility: 'hidden',
|
|
118
|
+
selectors: {
|
|
119
|
+
':where(.react-flow__node.selected:not(:focus-visible)) &': {
|
|
120
|
+
visibility: 'visible'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
globalStyle(`${cssIndicator} rect`, {
|
|
126
|
+
stroke: vars.element.loContrast,
|
|
127
|
+
transformOrigin: 'center center',
|
|
128
|
+
strokeWidth: 6,
|
|
129
|
+
animationDuration: '800ms',
|
|
130
|
+
animationName: indicatorKeyframes,
|
|
131
|
+
animationIterationCount: 'infinite',
|
|
132
|
+
animationDirection: 'alternate',
|
|
133
|
+
fill: 'none'
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
globalStyle(`:where(.react-flow__node:focus-within:not(.selected)) ${cssIndicator} rect`, {
|
|
137
|
+
stroke: `color-mix(in srgb, ${vars.element.stroke} 30%, ${vars.element.loContrast})`,
|
|
138
|
+
strokeWidth: 8
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
export const cssNavigateBtn = style({
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
pointerEvents: 'all',
|
|
144
|
+
left: 3,
|
|
145
|
+
top: 7,
|
|
146
|
+
cursor: 'pointer',
|
|
147
|
+
color: vars.element.loContrast,
|
|
148
|
+
transformOrigin: '90% 70%',
|
|
149
|
+
opacity: 0.75,
|
|
150
|
+
transition: 'all 150ms ease-out',
|
|
151
|
+
transitionDelay: '0ms',
|
|
152
|
+
backgroundColor: 'var(--ai-bg)',
|
|
153
|
+
vars: {
|
|
154
|
+
'--ai-bg': `color-mix(in srgb , ${vars.element.fill}, transparent 99%)`,
|
|
155
|
+
'--ai-bg-hover': `color-mix(in srgb , ${vars.element.fill} 65%, ${vars.element.stroke})`,
|
|
156
|
+
'--ai-hover': `color-mix(in srgb , ${vars.element.fill} 50%, ${vars.element.stroke})`
|
|
157
|
+
},
|
|
158
|
+
':hover': {
|
|
159
|
+
boxShadow: mantine.shadows.lg,
|
|
160
|
+
transform: 'scale(1.466)',
|
|
161
|
+
opacity: 1,
|
|
162
|
+
transitionDelay: '0'
|
|
163
|
+
},
|
|
164
|
+
':active': {
|
|
165
|
+
transform: 'scale(1.1)',
|
|
166
|
+
opacity: 1,
|
|
167
|
+
transitionDelay: '0'
|
|
168
|
+
},
|
|
169
|
+
selectors: {
|
|
170
|
+
[`:where(.react-flow__node:not(.dragging) ${cssContainer}:hover) &:not(:hover)`]: {
|
|
171
|
+
boxShadow: mantine.shadows.lg,
|
|
172
|
+
transform: 'scale(1.223)',
|
|
173
|
+
opacity: 1,
|
|
174
|
+
transitionDelay: '250ms',
|
|
175
|
+
vars: {
|
|
176
|
+
'--ai-bg': 'var(--ai-bg-hover)'
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
globalStyle(`${cssNavigateBtn} svg.icon`, {
|
|
182
|
+
width: '70%',
|
|
183
|
+
height: '70%',
|
|
184
|
+
strokeWidth: 1.5
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// globalStyle(`:where(.react-flow__node:not(.dragging)) ${cssIndicator}`, {
|
|
188
|
+
// visibility: 'visible',
|
|
189
|
+
// ":hover": {
|
|
190
|
+
// }
|
|
191
|
+
// })
|