@pyreon/flow 0.11.4 → 0.11.6
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 +10 -10
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +13 -13
- package/package.json +16 -16
- package/src/components/background.tsx +8 -8
- package/src/components/controls.tsx +9 -9
- package/src/components/flow-component.tsx +46 -46
- package/src/components/handle.tsx +8 -8
- package/src/components/minimap.tsx +5 -5
- package/src/components/node-resizer.tsx +25 -25
- package/src/components/node-toolbar.tsx +12 -12
- package/src/components/panel.tsx +9 -9
- package/src/edges.ts +6 -6
- package/src/flow.ts +11 -11
- package/src/index.ts +17 -17
- package/src/layout.ts +23 -23
- package/src/tests/flow-advanced.test.ts +190 -190
- package/src/tests/flow.test.ts +417 -417
- package/src/types.ts +24 -24
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
2
|
|
|
3
3
|
export interface NodeToolbarProps {
|
|
4
4
|
/** Position relative to node — default: 'top' */
|
|
5
|
-
position?:
|
|
5
|
+
position?: 'top' | 'bottom' | 'left' | 'right'
|
|
6
6
|
/** Offset from node in px — default: 8 */
|
|
7
7
|
offset?: number
|
|
8
8
|
/** Only show when node is selected — default: true */
|
|
@@ -15,10 +15,10 @@ export interface NodeToolbarProps {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const positionStyles: Record<string, string> = {
|
|
18
|
-
top:
|
|
19
|
-
bottom:
|
|
20
|
-
left:
|
|
21
|
-
right:
|
|
18
|
+
top: 'bottom: 100%; left: 50%; transform: translateX(-50%);',
|
|
19
|
+
bottom: 'top: 100%; left: 50%; transform: translateX(-50%);',
|
|
20
|
+
left: 'right: 100%; top: 50%; transform: translateY(-50%);',
|
|
21
|
+
right: 'left: 100%; top: 50%; transform: translateY(-50%);',
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -41,24 +41,24 @@ const positionStyles: Record<string, string> = {
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
export function NodeToolbar(props: NodeToolbarProps): VNodeChild {
|
|
44
|
-
const { position =
|
|
44
|
+
const { position = 'top', offset = 8, showOnSelect = true, selected = false, children } = props
|
|
45
45
|
|
|
46
46
|
if (showOnSelect && !selected) return null
|
|
47
47
|
|
|
48
48
|
const posStyle = positionStyles[position] ?? positionStyles.top
|
|
49
49
|
const marginProp =
|
|
50
|
-
position ===
|
|
50
|
+
position === 'top'
|
|
51
51
|
? `margin-bottom: ${offset}px;`
|
|
52
|
-
: position ===
|
|
52
|
+
: position === 'bottom'
|
|
53
53
|
? `margin-top: ${offset}px;`
|
|
54
|
-
: position ===
|
|
54
|
+
: position === 'left'
|
|
55
55
|
? `margin-right: ${offset}px;`
|
|
56
56
|
: `margin-left: ${offset}px;`
|
|
57
57
|
|
|
58
|
-
const baseStyle = `position: absolute; ${posStyle} ${marginProp} z-index: 10; display: flex; gap: 4px; background: white; border: 1px solid #ddd; border-radius: 6px; padding: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); ${props.style ??
|
|
58
|
+
const baseStyle = `position: absolute; ${posStyle} ${marginProp} z-index: 10; display: flex; gap: 4px; background: white; border: 1px solid #ddd; border-radius: 6px; padding: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); ${props.style ?? ''}`
|
|
59
59
|
|
|
60
60
|
return (
|
|
61
|
-
<div class={`pyreon-flow-node-toolbar ${props.class ??
|
|
61
|
+
<div class={`pyreon-flow-node-toolbar ${props.class ?? ''}`} style={baseStyle}>
|
|
62
62
|
{children}
|
|
63
63
|
</div>
|
|
64
64
|
)
|
package/src/components/panel.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import type { PanelProps } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { PanelProps } from '../types'
|
|
3
3
|
|
|
4
4
|
const positionStyles: Record<string, string> = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
'top-left': 'top: 10px; left: 10px;',
|
|
6
|
+
'top-right': 'top: 10px; right: 10px;',
|
|
7
|
+
'bottom-left': 'bottom: 10px; left: 10px;',
|
|
8
|
+
'bottom-right': 'bottom: 10px; right: 10px;',
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -21,12 +21,12 @@ const positionStyles: Record<string, string> = {
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export function Panel(props: PanelProps): VNodeChild {
|
|
24
|
-
const { position =
|
|
25
|
-
const posStyle = positionStyles[position] ?? positionStyles[
|
|
24
|
+
const { position = 'top-left', style = '', children } = props
|
|
25
|
+
const posStyle = positionStyles[position] ?? positionStyles['top-left']
|
|
26
26
|
const baseStyle = `position: absolute; ${posStyle} z-index: 5; ${style}`
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
|
-
<div class={`pyreon-flow-panel ${props.class ??
|
|
29
|
+
<div class={`pyreon-flow-panel ${props.class ?? ''}`} style={baseStyle}>
|
|
30
30
|
{children}
|
|
31
31
|
</div>
|
|
32
32
|
)
|
package/src/edges.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EdgePathResult, FlowNode, XYPosition } from
|
|
2
|
-
import { Position } from
|
|
1
|
+
import type { EdgePathResult, FlowNode, XYPosition } from './types'
|
|
2
|
+
import { Position } from './types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Auto-detect the best handle position based on relative node positions.
|
|
@@ -280,7 +280,7 @@ export function getWaypointPath(params: {
|
|
|
280
280
|
const allPoints = [{ x: sourceX, y: sourceY }, ...waypoints, { x: targetX, y: targetY }]
|
|
281
281
|
|
|
282
282
|
const segments = allPoints.map((p) => `${p.x},${p.y}`)
|
|
283
|
-
const path = `M${segments.join(
|
|
283
|
+
const path = `M${segments.join(' L')}`
|
|
284
284
|
|
|
285
285
|
// Label at the middle waypoint
|
|
286
286
|
const midIdx = Math.floor(waypoints.length / 2)
|
|
@@ -305,7 +305,7 @@ export function getEdgePath(
|
|
|
305
305
|
targetPosition: Position,
|
|
306
306
|
): EdgePathResult {
|
|
307
307
|
switch (type) {
|
|
308
|
-
case
|
|
308
|
+
case 'smoothstep':
|
|
309
309
|
return getSmoothStepPath({
|
|
310
310
|
sourceX,
|
|
311
311
|
sourceY,
|
|
@@ -314,9 +314,9 @@ export function getEdgePath(
|
|
|
314
314
|
targetY,
|
|
315
315
|
targetPosition,
|
|
316
316
|
})
|
|
317
|
-
case
|
|
317
|
+
case 'straight':
|
|
318
318
|
return getStraightPath({ sourceX, sourceY, targetX, targetY })
|
|
319
|
-
case
|
|
319
|
+
case 'step':
|
|
320
320
|
return getStepPath({
|
|
321
321
|
sourceX,
|
|
322
322
|
sourceY,
|
package/src/flow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { batch, computed, signal } from
|
|
2
|
-
import { computeLayout } from
|
|
1
|
+
import { batch, computed, signal } from '@pyreon/reactivity'
|
|
2
|
+
import { computeLayout } from './layout'
|
|
3
3
|
import type {
|
|
4
4
|
Connection,
|
|
5
5
|
FlowConfig,
|
|
@@ -10,15 +10,15 @@ import type {
|
|
|
10
10
|
LayoutOptions,
|
|
11
11
|
NodeChange,
|
|
12
12
|
XYPosition,
|
|
13
|
-
} from
|
|
13
|
+
} from './types'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Generate a unique edge id from source/target.
|
|
17
17
|
*/
|
|
18
18
|
function edgeId(edge: FlowEdge): string {
|
|
19
19
|
if (edge.id) return edge.id
|
|
20
|
-
const sh = edge.sourceHandle ? `-${edge.sourceHandle}` :
|
|
21
|
-
const th = edge.targetHandle ? `-${edge.targetHandle}` :
|
|
20
|
+
const sh = edge.sourceHandle ? `-${edge.sourceHandle}` : ''
|
|
21
|
+
const th = edge.targetHandle ? `-${edge.targetHandle}` : ''
|
|
22
22
|
return `e-${edge.source}${sh}-${edge.target}${th}`
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -51,7 +51,7 @@ export function createFlow(config: FlowConfig = {}): FlowInstance {
|
|
|
51
51
|
const {
|
|
52
52
|
nodes: initialNodes = [],
|
|
53
53
|
edges: initialEdges = [],
|
|
54
|
-
defaultEdgeType =
|
|
54
|
+
defaultEdgeType = 'bezier',
|
|
55
55
|
minZoom = 0.1,
|
|
56
56
|
maxZoom = 4,
|
|
57
57
|
snapToGrid = false,
|
|
@@ -119,7 +119,7 @@ export function createFlow(config: FlowConfig = {}): FlowInstance {
|
|
|
119
119
|
return next
|
|
120
120
|
})
|
|
121
121
|
})
|
|
122
|
-
emitNodeChanges([{ type:
|
|
122
|
+
emitNodeChanges([{ type: 'remove', id }])
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function updateNode(id: string, update: Partial<FlowNode>): void {
|
|
@@ -139,7 +139,7 @@ export function createFlow(config: FlowConfig = {}): FlowInstance {
|
|
|
139
139
|
pos = clampToExtent(pos, node?.width, node?.height)
|
|
140
140
|
|
|
141
141
|
nodes.update((nds) => nds.map((n) => (n.id === id ? { ...n, position: pos } : n)))
|
|
142
|
-
emitNodeChanges([{ type:
|
|
142
|
+
emitNodeChanges([{ type: 'position', id, position: pos }])
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// ── Edge operations ──────────────────────────────────────────────────────
|
|
@@ -187,7 +187,7 @@ export function createFlow(config: FlowConfig = {}): FlowInstance {
|
|
|
187
187
|
const sourceNode = getNode(connection.source)
|
|
188
188
|
if (!sourceNode) return false
|
|
189
189
|
|
|
190
|
-
const sourceType = sourceNode.type ??
|
|
190
|
+
const sourceType = sourceNode.type ?? 'default'
|
|
191
191
|
const rule = connectionRules[sourceType]
|
|
192
192
|
if (!rule) return true // no rule = allow
|
|
193
193
|
|
|
@@ -195,7 +195,7 @@ export function createFlow(config: FlowConfig = {}): FlowInstance {
|
|
|
195
195
|
const targetNode = getNode(connection.target)
|
|
196
196
|
if (!targetNode) return false
|
|
197
197
|
|
|
198
|
-
const targetType = targetNode.type ??
|
|
198
|
+
const targetType = targetNode.type ?? 'default'
|
|
199
199
|
return rule.outputs.includes(targetType)
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -354,7 +354,7 @@ export function createFlow(config: FlowConfig = {}): FlowInstance {
|
|
|
354
354
|
// ── Layout ───────────────────────────────────────────────────────────────
|
|
355
355
|
|
|
356
356
|
async function layout(
|
|
357
|
-
algorithm: LayoutAlgorithm =
|
|
357
|
+
algorithm: LayoutAlgorithm = 'layered',
|
|
358
358
|
options: LayoutOptions = {},
|
|
359
359
|
): Promise<void> {
|
|
360
360
|
const currentNodes = nodes.peek()
|
package/src/index.ts
CHANGED
|
@@ -24,18 +24,18 @@
|
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
export { Background } from
|
|
28
|
-
export { Controls } from
|
|
29
|
-
export type { FlowComponentProps } from
|
|
27
|
+
export { Background } from './components/background'
|
|
28
|
+
export { Controls } from './components/controls'
|
|
29
|
+
export type { FlowComponentProps } from './components/flow-component'
|
|
30
30
|
// Components
|
|
31
|
-
export { Flow } from
|
|
32
|
-
export { Handle } from
|
|
33
|
-
export { MiniMap } from
|
|
34
|
-
export type { NodeResizerProps } from
|
|
35
|
-
export { NodeResizer } from
|
|
36
|
-
export type { NodeToolbarProps } from
|
|
37
|
-
export { NodeToolbar } from
|
|
38
|
-
export { Panel } from
|
|
31
|
+
export { Flow } from './components/flow-component'
|
|
32
|
+
export { Handle } from './components/handle'
|
|
33
|
+
export { MiniMap } from './components/minimap'
|
|
34
|
+
export type { NodeResizerProps } from './components/node-resizer'
|
|
35
|
+
export { NodeResizer } from './components/node-resizer'
|
|
36
|
+
export type { NodeToolbarProps } from './components/node-toolbar'
|
|
37
|
+
export { NodeToolbar } from './components/node-toolbar'
|
|
38
|
+
export { Panel } from './components/panel'
|
|
39
39
|
// Edge path utilities
|
|
40
40
|
export {
|
|
41
41
|
getBezierPath,
|
|
@@ -46,13 +46,13 @@ export {
|
|
|
46
46
|
getStepPath,
|
|
47
47
|
getStraightPath,
|
|
48
48
|
getWaypointPath,
|
|
49
|
-
} from
|
|
49
|
+
} from './edges'
|
|
50
50
|
// Core
|
|
51
|
-
export { createFlow } from
|
|
51
|
+
export { createFlow } from './flow'
|
|
52
52
|
// Layout
|
|
53
|
-
export { computeLayout } from
|
|
53
|
+
export { computeLayout } from './layout'
|
|
54
54
|
// Styles
|
|
55
|
-
export { flowStyles } from
|
|
55
|
+
export { flowStyles } from './styles'
|
|
56
56
|
export type {
|
|
57
57
|
BackgroundProps,
|
|
58
58
|
Connection,
|
|
@@ -78,6 +78,6 @@ export type {
|
|
|
78
78
|
Rect,
|
|
79
79
|
Viewport,
|
|
80
80
|
XYPosition,
|
|
81
|
-
} from
|
|
81
|
+
} from './types'
|
|
82
82
|
// Types
|
|
83
|
-
export { Position } from
|
|
83
|
+
export { Position } from './types'
|
package/src/layout.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import type { FlowEdge, FlowNode, LayoutAlgorithm, LayoutOptions } from
|
|
1
|
+
import type { FlowEdge, FlowNode, LayoutAlgorithm, LayoutOptions } from './types'
|
|
2
2
|
|
|
3
3
|
// ─── ELK algorithm mapping ───────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
const ELK_ALGORITHMS: Record<LayoutAlgorithm, string> = {
|
|
6
|
-
layered:
|
|
7
|
-
force:
|
|
8
|
-
stress:
|
|
9
|
-
tree:
|
|
10
|
-
radial:
|
|
11
|
-
box:
|
|
12
|
-
rectpacking:
|
|
6
|
+
layered: 'org.eclipse.elk.layered',
|
|
7
|
+
force: 'org.eclipse.elk.force',
|
|
8
|
+
stress: 'org.eclipse.elk.stress',
|
|
9
|
+
tree: 'org.eclipse.elk.mrtree',
|
|
10
|
+
radial: 'org.eclipse.elk.radial',
|
|
11
|
+
box: 'org.eclipse.elk.box',
|
|
12
|
+
rectpacking: 'org.eclipse.elk.rectpacking',
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const ELK_DIRECTIONS: Record<string, string> = {
|
|
16
|
-
UP:
|
|
17
|
-
DOWN:
|
|
18
|
-
LEFT:
|
|
19
|
-
RIGHT:
|
|
16
|
+
UP: 'UP',
|
|
17
|
+
DOWN: 'DOWN',
|
|
18
|
+
LEFT: 'LEFT',
|
|
19
|
+
RIGHT: 'RIGHT',
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// ─── Lazy-loaded ELK instance ────────────────────────────────────────────────
|
|
@@ -28,7 +28,7 @@ async function getELK(): Promise<any> {
|
|
|
28
28
|
if (elkInstance) return elkInstance
|
|
29
29
|
if (elkPromise) return elkPromise
|
|
30
30
|
|
|
31
|
-
elkPromise = import(
|
|
31
|
+
elkPromise = import('elkjs/lib/elk.bundled.js').then((mod) => {
|
|
32
32
|
const ELK = mod.default || mod
|
|
33
33
|
elkInstance = new ELK()
|
|
34
34
|
return elkInstance
|
|
@@ -69,32 +69,32 @@ function toElkGraph(
|
|
|
69
69
|
options: LayoutOptions,
|
|
70
70
|
): ElkGraph {
|
|
71
71
|
const layoutOptions: Record<string, string> = {
|
|
72
|
-
|
|
72
|
+
'elk.algorithm': ELK_ALGORITHMS[algorithm] ?? ELK_ALGORITHMS.layered,
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
if (options.direction) {
|
|
76
|
-
layoutOptions[
|
|
76
|
+
layoutOptions['elk.direction'] = ELK_DIRECTIONS[options.direction] ?? 'DOWN'
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (options.nodeSpacing !== undefined) {
|
|
80
|
-
layoutOptions[
|
|
80
|
+
layoutOptions['elk.spacing.nodeNode'] = String(options.nodeSpacing)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
if (options.layerSpacing !== undefined) {
|
|
84
|
-
layoutOptions[
|
|
84
|
+
layoutOptions['elk.layered.spacing.nodeNodeBetweenLayers'] = String(options.layerSpacing)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (options.edgeRouting) {
|
|
88
88
|
const routingMap: Record<string, string> = {
|
|
89
|
-
orthogonal:
|
|
90
|
-
splines:
|
|
91
|
-
polyline:
|
|
89
|
+
orthogonal: 'ORTHOGONAL',
|
|
90
|
+
splines: 'SPLINES',
|
|
91
|
+
polyline: 'POLYLINE',
|
|
92
92
|
}
|
|
93
|
-
layoutOptions[
|
|
93
|
+
layoutOptions['elk.edgeRouting'] = routingMap[options.edgeRouting] ?? 'ORTHOGONAL'
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
return {
|
|
97
|
-
id:
|
|
97
|
+
id: 'root',
|
|
98
98
|
layoutOptions,
|
|
99
99
|
children: nodes.map((node) => ({
|
|
100
100
|
id: node.id,
|
|
@@ -130,7 +130,7 @@ function toElkGraph(
|
|
|
130
130
|
export async function computeLayout(
|
|
131
131
|
nodes: FlowNode[],
|
|
132
132
|
edges: FlowEdge[],
|
|
133
|
-
algorithm: LayoutAlgorithm =
|
|
133
|
+
algorithm: LayoutAlgorithm = 'layered',
|
|
134
134
|
options: LayoutOptions = {},
|
|
135
135
|
): Promise<Array<{ id: string; position: { x: number; y: number } }>> {
|
|
136
136
|
const elk = await getELK()
|