@runtypelabs/react-flow 0.1.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/README.md +289 -0
- package/example/.env.example +3 -0
- package/example/index.html +25 -0
- package/example/node_modules/.bin/browserslist +21 -0
- package/example/node_modules/.bin/terser +21 -0
- package/example/node_modules/.bin/tsc +21 -0
- package/example/node_modules/.bin/tsserver +21 -0
- package/example/node_modules/.bin/vite +21 -0
- package/example/package.json +26 -0
- package/example/src/App.tsx +1744 -0
- package/example/src/main.tsx +11 -0
- package/example/tsconfig.json +21 -0
- package/example/vite.config.ts +13 -0
- package/package.json +65 -0
- package/src/components/RuntypeFlowEditor.tsx +528 -0
- package/src/components/nodes/BaseNode.tsx +357 -0
- package/src/components/nodes/CodeNode.tsx +252 -0
- package/src/components/nodes/ConditionalNode.tsx +264 -0
- package/src/components/nodes/FetchUrlNode.tsx +299 -0
- package/src/components/nodes/PromptNode.tsx +270 -0
- package/src/components/nodes/SendEmailNode.tsx +311 -0
- package/src/hooks/useFlowValidation.ts +424 -0
- package/src/hooks/useRuntypeFlow.ts +414 -0
- package/src/index.ts +28 -0
- package/src/types/index.ts +332 -0
- package/src/utils/adapter.ts +544 -0
- package/src/utils/layout.ts +284 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { RuntypeNode, RuntypeEdge } from '../types'
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Layout Constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
const DEFAULT_NODE_WIDTH = 280
|
|
8
|
+
const DEFAULT_NODE_HEIGHT = 150
|
|
9
|
+
const HORIZONTAL_SPACING = 100
|
|
10
|
+
const VERTICAL_SPACING = 80
|
|
11
|
+
const BRANCH_OFFSET = 350
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Auto Layout Algorithm
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export interface LayoutOptions {
|
|
18
|
+
/** Direction of the flow */
|
|
19
|
+
direction?: 'vertical' | 'horizontal'
|
|
20
|
+
/** Starting position */
|
|
21
|
+
startPosition?: { x: number; y: number }
|
|
22
|
+
/** Node dimensions */
|
|
23
|
+
nodeWidth?: number
|
|
24
|
+
nodeHeight?: number
|
|
25
|
+
/** Spacing between nodes */
|
|
26
|
+
horizontalSpacing?: number
|
|
27
|
+
verticalSpacing?: number
|
|
28
|
+
/** Offset for conditional branches */
|
|
29
|
+
branchOffset?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Automatically layout nodes in a flow-like arrangement
|
|
34
|
+
*/
|
|
35
|
+
export function autoLayout(
|
|
36
|
+
nodes: RuntypeNode[],
|
|
37
|
+
edges: RuntypeEdge[],
|
|
38
|
+
options: LayoutOptions = {}
|
|
39
|
+
): RuntypeNode[] {
|
|
40
|
+
const {
|
|
41
|
+
direction = 'vertical',
|
|
42
|
+
startPosition = { x: 400, y: 50 },
|
|
43
|
+
nodeWidth = DEFAULT_NODE_WIDTH,
|
|
44
|
+
nodeHeight = DEFAULT_NODE_HEIGHT,
|
|
45
|
+
horizontalSpacing = HORIZONTAL_SPACING,
|
|
46
|
+
verticalSpacing = VERTICAL_SPACING,
|
|
47
|
+
branchOffset = BRANCH_OFFSET,
|
|
48
|
+
} = options
|
|
49
|
+
|
|
50
|
+
// Build adjacency map from edges
|
|
51
|
+
const adjacencyMap = new Map<string, string[]>()
|
|
52
|
+
const incomingMap = new Map<string, string[]>()
|
|
53
|
+
|
|
54
|
+
for (const edge of edges) {
|
|
55
|
+
const existing = adjacencyMap.get(edge.source) || []
|
|
56
|
+
adjacencyMap.set(edge.source, [...existing, edge.target])
|
|
57
|
+
|
|
58
|
+
const incoming = incomingMap.get(edge.target) || []
|
|
59
|
+
incomingMap.set(edge.target, [...incoming, edge.source])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find root nodes (nodes with no incoming edges)
|
|
63
|
+
const rootNodes = nodes.filter(node => {
|
|
64
|
+
const incoming = incomingMap.get(node.id)
|
|
65
|
+
return !incoming || incoming.length === 0
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// If no root nodes found, use the first node
|
|
69
|
+
if (rootNodes.length === 0 && nodes.length > 0) {
|
|
70
|
+
rootNodes.push(nodes[0])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Track positioned nodes
|
|
74
|
+
const positionedNodes = new Map<string, { x: number; y: number }>()
|
|
75
|
+
const visited = new Set<string>()
|
|
76
|
+
|
|
77
|
+
// Position nodes using BFS
|
|
78
|
+
const queue: Array<{ nodeId: string; x: number; y: number; depth: number }> = []
|
|
79
|
+
|
|
80
|
+
// Start with root nodes
|
|
81
|
+
let startX = startPosition.x
|
|
82
|
+
for (const rootNode of rootNodes) {
|
|
83
|
+
queue.push({ nodeId: rootNode.id, x: startX, y: startPosition.y, depth: 0 })
|
|
84
|
+
startX += nodeWidth + horizontalSpacing
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
while (queue.length > 0) {
|
|
88
|
+
const { nodeId, x, y, depth } = queue.shift()!
|
|
89
|
+
|
|
90
|
+
if (visited.has(nodeId)) continue
|
|
91
|
+
visited.add(nodeId)
|
|
92
|
+
|
|
93
|
+
positionedNodes.set(nodeId, { x, y })
|
|
94
|
+
|
|
95
|
+
// Get children
|
|
96
|
+
const children = adjacencyMap.get(nodeId) || []
|
|
97
|
+
const node = nodes.find(n => n.id === nodeId)
|
|
98
|
+
|
|
99
|
+
// Check if this is a conditional node
|
|
100
|
+
const isConditional = node?.data.step.type === 'conditional'
|
|
101
|
+
|
|
102
|
+
if (isConditional && children.length > 0) {
|
|
103
|
+
// Position conditional branches side by side
|
|
104
|
+
const trueBranch = children.filter(c => c.includes('-true-'))
|
|
105
|
+
const falseBranch = children.filter(c => c.includes('-false-'))
|
|
106
|
+
const normalChildren = children.filter(c => !c.includes('-true-') && !c.includes('-false-'))
|
|
107
|
+
|
|
108
|
+
// Position true branch to the right
|
|
109
|
+
let trueY = y + nodeHeight + verticalSpacing
|
|
110
|
+
for (const childId of trueBranch) {
|
|
111
|
+
if (!visited.has(childId)) {
|
|
112
|
+
queue.push({
|
|
113
|
+
nodeId: childId,
|
|
114
|
+
x: x + branchOffset,
|
|
115
|
+
y: trueY,
|
|
116
|
+
depth: depth + 1,
|
|
117
|
+
})
|
|
118
|
+
trueY += nodeHeight + verticalSpacing
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Position false branch to the left
|
|
123
|
+
let falseY = y + nodeHeight + verticalSpacing
|
|
124
|
+
for (const childId of falseBranch) {
|
|
125
|
+
if (!visited.has(childId)) {
|
|
126
|
+
queue.push({
|
|
127
|
+
nodeId: childId,
|
|
128
|
+
x: x - branchOffset,
|
|
129
|
+
y: falseY,
|
|
130
|
+
depth: depth + 1,
|
|
131
|
+
})
|
|
132
|
+
falseY += nodeHeight + verticalSpacing
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Position normal children below
|
|
137
|
+
const maxBranchY = Math.max(trueY, falseY)
|
|
138
|
+
let childY = maxBranchY
|
|
139
|
+
for (const childId of normalChildren) {
|
|
140
|
+
if (!visited.has(childId)) {
|
|
141
|
+
queue.push({
|
|
142
|
+
nodeId: childId,
|
|
143
|
+
x,
|
|
144
|
+
y: childY,
|
|
145
|
+
depth: depth + 1,
|
|
146
|
+
})
|
|
147
|
+
childY += nodeHeight + verticalSpacing
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// Position children in sequence
|
|
152
|
+
let childY = y + nodeHeight + verticalSpacing
|
|
153
|
+
let childX = x
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < children.length; i++) {
|
|
156
|
+
const childId = children[i]
|
|
157
|
+
if (!visited.has(childId)) {
|
|
158
|
+
if (direction === 'horizontal') {
|
|
159
|
+
queue.push({
|
|
160
|
+
nodeId: childId,
|
|
161
|
+
x: childX + nodeWidth + horizontalSpacing,
|
|
162
|
+
y,
|
|
163
|
+
depth: depth + 1,
|
|
164
|
+
})
|
|
165
|
+
childX += nodeWidth + horizontalSpacing
|
|
166
|
+
} else {
|
|
167
|
+
queue.push({
|
|
168
|
+
nodeId: childId,
|
|
169
|
+
x,
|
|
170
|
+
y: childY,
|
|
171
|
+
depth: depth + 1,
|
|
172
|
+
})
|
|
173
|
+
childY += nodeHeight + verticalSpacing
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Apply positions to nodes
|
|
181
|
+
return nodes.map(node => {
|
|
182
|
+
const position = positionedNodes.get(node.id)
|
|
183
|
+
if (position) {
|
|
184
|
+
return {
|
|
185
|
+
...node,
|
|
186
|
+
position,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return node
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Center nodes within a viewport
|
|
195
|
+
*/
|
|
196
|
+
export function centerNodes(
|
|
197
|
+
nodes: RuntypeNode[],
|
|
198
|
+
viewportWidth: number,
|
|
199
|
+
viewportHeight: number
|
|
200
|
+
): RuntypeNode[] {
|
|
201
|
+
if (nodes.length === 0) return nodes
|
|
202
|
+
|
|
203
|
+
// Calculate bounding box
|
|
204
|
+
let minX = Infinity
|
|
205
|
+
let maxX = -Infinity
|
|
206
|
+
let minY = Infinity
|
|
207
|
+
let maxY = -Infinity
|
|
208
|
+
|
|
209
|
+
for (const node of nodes) {
|
|
210
|
+
minX = Math.min(minX, node.position.x)
|
|
211
|
+
maxX = Math.max(maxX, node.position.x + DEFAULT_NODE_WIDTH)
|
|
212
|
+
minY = Math.min(minY, node.position.y)
|
|
213
|
+
maxY = Math.max(maxY, node.position.y + DEFAULT_NODE_HEIGHT)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Calculate offset to center
|
|
217
|
+
const contentWidth = maxX - minX
|
|
218
|
+
const contentHeight = maxY - minY
|
|
219
|
+
const offsetX = (viewportWidth - contentWidth) / 2 - minX
|
|
220
|
+
const offsetY = (viewportHeight - contentHeight) / 2 - minY
|
|
221
|
+
|
|
222
|
+
// Apply offset
|
|
223
|
+
return nodes.map(node => ({
|
|
224
|
+
...node,
|
|
225
|
+
position: {
|
|
226
|
+
x: node.position.x + offsetX,
|
|
227
|
+
y: node.position.y + offsetY,
|
|
228
|
+
},
|
|
229
|
+
}))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Align nodes to a grid
|
|
234
|
+
*/
|
|
235
|
+
export function snapToGrid(
|
|
236
|
+
nodes: RuntypeNode[],
|
|
237
|
+
gridSize: number = 20
|
|
238
|
+
): RuntypeNode[] {
|
|
239
|
+
return nodes.map(node => ({
|
|
240
|
+
...node,
|
|
241
|
+
position: {
|
|
242
|
+
x: Math.round(node.position.x / gridSize) * gridSize,
|
|
243
|
+
y: Math.round(node.position.y / gridSize) * gridSize,
|
|
244
|
+
},
|
|
245
|
+
}))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get the bounding box of all nodes
|
|
250
|
+
*/
|
|
251
|
+
export function getNodesBoundingBox(nodes: RuntypeNode[]): {
|
|
252
|
+
minX: number
|
|
253
|
+
maxX: number
|
|
254
|
+
minY: number
|
|
255
|
+
maxY: number
|
|
256
|
+
width: number
|
|
257
|
+
height: number
|
|
258
|
+
} {
|
|
259
|
+
if (nodes.length === 0) {
|
|
260
|
+
return { minX: 0, maxX: 0, minY: 0, maxY: 0, width: 0, height: 0 }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let minX = Infinity
|
|
264
|
+
let maxX = -Infinity
|
|
265
|
+
let minY = Infinity
|
|
266
|
+
let maxY = -Infinity
|
|
267
|
+
|
|
268
|
+
for (const node of nodes) {
|
|
269
|
+
minX = Math.min(minX, node.position.x)
|
|
270
|
+
maxX = Math.max(maxX, node.position.x + DEFAULT_NODE_WIDTH)
|
|
271
|
+
minY = Math.min(minY, node.position.y)
|
|
272
|
+
maxY = Math.max(maxY, node.position.y + DEFAULT_NODE_HEIGHT)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
minX,
|
|
277
|
+
maxX,
|
|
278
|
+
minY,
|
|
279
|
+
maxY,
|
|
280
|
+
width: maxX - minX,
|
|
281
|
+
height: maxY - minY,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "ES2020"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"noEmit": false,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./src",
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"module": "esnext",
|
|
15
|
+
"moduleResolution": "bundler",
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"declarationMap": true,
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"jsx": "react-jsx",
|
|
21
|
+
"baseUrl": ".",
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/*": ["./src/*"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"include": ["src/**/*"],
|
|
27
|
+
"exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.test.tsx"]
|
|
28
|
+
}
|
|
29
|
+
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['cjs', 'esm'],
|
|
6
|
+
// DTS generation disabled due to React version conflicts in the monorepo
|
|
7
|
+
// Types are exported from src/types/index.ts and can be imported directly
|
|
8
|
+
dts: false,
|
|
9
|
+
splitting: false,
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
clean: true,
|
|
12
|
+
external: ['react', 'react-dom', '@xyflow/react'],
|
|
13
|
+
treeshake: true,
|
|
14
|
+
})
|
|
15
|
+
|