@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.
@@ -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
+