@pyreon/flow 0.5.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/LICENSE +21 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/chunk-C8JhGJ3N.js +33 -0
- package/lib/elk.bundled-B9dPTHTZ.js +88591 -0
- package/lib/elk.bundled-B9dPTHTZ.js.map +1 -0
- package/lib/index.js +2526 -0
- package/lib/index.js.map +1 -0
- package/lib/types/chunk.d.ts +2 -0
- package/lib/types/elk.bundled.d.ts +7 -0
- package/lib/types/elk.bundled.d.ts.map +1 -0
- package/lib/types/index.d.ts +2342 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +708 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/components/background.tsx +128 -0
- package/src/components/controls.tsx +158 -0
- package/src/components/flow-component.tsx +918 -0
- package/src/components/handle.tsx +42 -0
- package/src/components/minimap.tsx +136 -0
- package/src/components/node-resizer.tsx +169 -0
- package/src/components/node-toolbar.tsx +74 -0
- package/src/components/panel.tsx +33 -0
- package/src/edges.ts +369 -0
- package/src/flow.ts +1141 -0
- package/src/index.ts +83 -0
- package/src/layout.ts +152 -0
- package/src/styles.ts +115 -0
- package/src/tests/flow-advanced.test.ts +802 -0
- package/src/tests/flow.test.ts +1284 -0
- package/src/types.ts +483 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pyreon/flow — Reactive flow diagrams for Pyreon.
|
|
3
|
+
*
|
|
4
|
+
* Signal-native nodes, edges, pan/zoom, auto-layout. No D3 dependency.
|
|
5
|
+
* Per-node signal reactivity — O(1) updates instead of O(n) array diffing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { createFlow, Flow, Background, MiniMap, Controls } from '@pyreon/flow'
|
|
10
|
+
*
|
|
11
|
+
* const flow = createFlow({
|
|
12
|
+
* nodes: [
|
|
13
|
+
* { id: '1', position: { x: 0, y: 0 }, data: { label: 'Start' } },
|
|
14
|
+
* { id: '2', position: { x: 200, y: 100 }, data: { label: 'End' } },
|
|
15
|
+
* ],
|
|
16
|
+
* edges: [{ source: '1', target: '2' }],
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* <Flow instance={flow}>
|
|
20
|
+
* <Background />
|
|
21
|
+
* <MiniMap />
|
|
22
|
+
* <Controls />
|
|
23
|
+
* </Flow>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export { Background } from './components/background'
|
|
28
|
+
export { Controls } from './components/controls'
|
|
29
|
+
export type { FlowComponentProps } from './components/flow-component'
|
|
30
|
+
// Components
|
|
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
|
+
// Edge path utilities
|
|
40
|
+
export {
|
|
41
|
+
getBezierPath,
|
|
42
|
+
getEdgePath,
|
|
43
|
+
getHandlePosition,
|
|
44
|
+
getSmartHandlePositions,
|
|
45
|
+
getSmoothStepPath,
|
|
46
|
+
getStepPath,
|
|
47
|
+
getStraightPath,
|
|
48
|
+
getWaypointPath,
|
|
49
|
+
} from './edges'
|
|
50
|
+
// Core
|
|
51
|
+
export { createFlow } from './flow'
|
|
52
|
+
// Layout
|
|
53
|
+
export { computeLayout } from './layout'
|
|
54
|
+
// Styles
|
|
55
|
+
export { flowStyles } from './styles'
|
|
56
|
+
export type {
|
|
57
|
+
BackgroundProps,
|
|
58
|
+
Connection,
|
|
59
|
+
ConnectionRule,
|
|
60
|
+
ControlsProps,
|
|
61
|
+
Dimensions,
|
|
62
|
+
EdgePathResult,
|
|
63
|
+
EdgeType,
|
|
64
|
+
FlowConfig,
|
|
65
|
+
FlowEdge,
|
|
66
|
+
FlowInstance,
|
|
67
|
+
FlowNode,
|
|
68
|
+
FlowProps,
|
|
69
|
+
HandleConfig,
|
|
70
|
+
HandleProps,
|
|
71
|
+
HandleType,
|
|
72
|
+
LayoutAlgorithm,
|
|
73
|
+
LayoutOptions,
|
|
74
|
+
MiniMapProps,
|
|
75
|
+
NodeChange,
|
|
76
|
+
NodeComponentProps,
|
|
77
|
+
PanelProps,
|
|
78
|
+
Rect,
|
|
79
|
+
Viewport,
|
|
80
|
+
XYPosition,
|
|
81
|
+
} from './types'
|
|
82
|
+
// Types
|
|
83
|
+
export { Position } from './types'
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FlowEdge,
|
|
3
|
+
FlowNode,
|
|
4
|
+
LayoutAlgorithm,
|
|
5
|
+
LayoutOptions,
|
|
6
|
+
} from './types'
|
|
7
|
+
|
|
8
|
+
// ─── ELK algorithm mapping ───────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const ELK_ALGORITHMS: Record<LayoutAlgorithm, string> = {
|
|
11
|
+
layered: 'org.eclipse.elk.layered',
|
|
12
|
+
force: 'org.eclipse.elk.force',
|
|
13
|
+
stress: 'org.eclipse.elk.stress',
|
|
14
|
+
tree: 'org.eclipse.elk.mrtree',
|
|
15
|
+
radial: 'org.eclipse.elk.radial',
|
|
16
|
+
box: 'org.eclipse.elk.box',
|
|
17
|
+
rectpacking: 'org.eclipse.elk.rectpacking',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ELK_DIRECTIONS: Record<string, string> = {
|
|
21
|
+
UP: 'UP',
|
|
22
|
+
DOWN: 'DOWN',
|
|
23
|
+
LEFT: 'LEFT',
|
|
24
|
+
RIGHT: 'RIGHT',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Lazy-loaded ELK instance ────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
let elkInstance: any = null
|
|
30
|
+
let elkPromise: Promise<any> | null = null
|
|
31
|
+
|
|
32
|
+
async function getELK(): Promise<any> {
|
|
33
|
+
if (elkInstance) return elkInstance
|
|
34
|
+
if (elkPromise) return elkPromise
|
|
35
|
+
|
|
36
|
+
elkPromise = import('elkjs/lib/elk.bundled.js').then((mod) => {
|
|
37
|
+
const ELK = mod.default || mod
|
|
38
|
+
elkInstance = new ELK()
|
|
39
|
+
return elkInstance
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return elkPromise
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Convert flow graph to ELK format ────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
interface ElkNode {
|
|
48
|
+
id: string
|
|
49
|
+
width: number
|
|
50
|
+
height: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ElkEdge {
|
|
54
|
+
id: string
|
|
55
|
+
sources: string[]
|
|
56
|
+
targets: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ElkGraph {
|
|
60
|
+
id: string
|
|
61
|
+
layoutOptions: Record<string, string>
|
|
62
|
+
children: ElkNode[]
|
|
63
|
+
edges: ElkEdge[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ElkResult {
|
|
67
|
+
children: Array<{ id: string; x: number; y: number }>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toElkGraph(
|
|
71
|
+
nodes: FlowNode[],
|
|
72
|
+
edges: FlowEdge[],
|
|
73
|
+
algorithm: LayoutAlgorithm,
|
|
74
|
+
options: LayoutOptions,
|
|
75
|
+
): ElkGraph {
|
|
76
|
+
const layoutOptions: Record<string, string> = {
|
|
77
|
+
'elk.algorithm': ELK_ALGORITHMS[algorithm] ?? ELK_ALGORITHMS.layered,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (options.direction) {
|
|
81
|
+
layoutOptions['elk.direction'] = ELK_DIRECTIONS[options.direction] ?? 'DOWN'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (options.nodeSpacing !== undefined) {
|
|
85
|
+
layoutOptions['elk.spacing.nodeNode'] = String(options.nodeSpacing)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (options.layerSpacing !== undefined) {
|
|
89
|
+
layoutOptions['elk.layered.spacing.nodeNodeBetweenLayers'] = String(
|
|
90
|
+
options.layerSpacing,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.edgeRouting) {
|
|
95
|
+
const routingMap: Record<string, string> = {
|
|
96
|
+
orthogonal: 'ORTHOGONAL',
|
|
97
|
+
splines: 'SPLINES',
|
|
98
|
+
polyline: 'POLYLINE',
|
|
99
|
+
}
|
|
100
|
+
layoutOptions['elk.edgeRouting'] =
|
|
101
|
+
routingMap[options.edgeRouting] ?? 'ORTHOGONAL'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
id: 'root',
|
|
106
|
+
layoutOptions,
|
|
107
|
+
children: nodes.map((node) => ({
|
|
108
|
+
id: node.id,
|
|
109
|
+
width: node.width ?? 150,
|
|
110
|
+
height: node.height ?? 40,
|
|
111
|
+
})),
|
|
112
|
+
edges: edges.map((edge, i) => ({
|
|
113
|
+
id: edge.id ?? `e-${i}`,
|
|
114
|
+
sources: [edge.source],
|
|
115
|
+
targets: [edge.target],
|
|
116
|
+
})),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compute a layout for the given nodes and edges using elkjs.
|
|
124
|
+
* Returns an array of { id, position } for each node.
|
|
125
|
+
*
|
|
126
|
+
* elkjs is lazy-loaded — zero bundle cost until this function is called.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const positions = await computeLayout(nodes, edges, 'layered', {
|
|
131
|
+
* direction: 'RIGHT',
|
|
132
|
+
* nodeSpacing: 50,
|
|
133
|
+
* layerSpacing: 100,
|
|
134
|
+
* })
|
|
135
|
+
* // positions: [{ id: '1', position: { x: 0, y: 0 } }, ...]
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export async function computeLayout(
|
|
139
|
+
nodes: FlowNode[],
|
|
140
|
+
edges: FlowEdge[],
|
|
141
|
+
algorithm: LayoutAlgorithm = 'layered',
|
|
142
|
+
options: LayoutOptions = {},
|
|
143
|
+
): Promise<Array<{ id: string; position: { x: number; y: number } }>> {
|
|
144
|
+
const elk = await getELK()
|
|
145
|
+
const graph = toElkGraph(nodes, edges, algorithm, options)
|
|
146
|
+
const result: ElkResult = await elk.layout(graph)
|
|
147
|
+
|
|
148
|
+
return (result.children ?? []).map((child) => ({
|
|
149
|
+
id: child.id,
|
|
150
|
+
position: { x: child.x ?? 0, y: child.y ?? 0 },
|
|
151
|
+
}))
|
|
152
|
+
}
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default CSS styles for the flow diagram.
|
|
3
|
+
* Inject via `<style>` tag or import in your CSS.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```tsx
|
|
7
|
+
* import { flowStyles } from '@pyreon/flow'
|
|
8
|
+
*
|
|
9
|
+
* // Inject once at app root
|
|
10
|
+
* const style = document.createElement('style')
|
|
11
|
+
* style.textContent = flowStyles
|
|
12
|
+
* document.head.appendChild(style)
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const flowStyles = `
|
|
16
|
+
/* ── Animated edges ────────────────────────────────────────────────────────── */
|
|
17
|
+
|
|
18
|
+
.pyreon-flow-edge-animated {
|
|
19
|
+
stroke-dasharray: 5;
|
|
20
|
+
animation: pyreon-flow-edge-dash 0.5s linear infinite;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@keyframes pyreon-flow-edge-dash {
|
|
24
|
+
to {
|
|
25
|
+
stroke-dashoffset: -10;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ── Node states ──────────────────────────────────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
.pyreon-flow-node {
|
|
32
|
+
transition: box-shadow 0.15s ease;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.pyreon-flow-node.dragging {
|
|
36
|
+
opacity: 0.9;
|
|
37
|
+
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));
|
|
38
|
+
cursor: grabbing;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.pyreon-flow-node.selected {
|
|
42
|
+
filter: drop-shadow(0 0 0 2px rgba(59, 130, 246, 0.3));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── Handles ──────────────────────────────────────────────────────────────── */
|
|
46
|
+
|
|
47
|
+
.pyreon-flow-handle {
|
|
48
|
+
transition: transform 0.1s ease, background 0.1s ease;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.pyreon-flow-handle:hover {
|
|
52
|
+
transform: scale(1.4);
|
|
53
|
+
background: #3b82f6 !important;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.pyreon-flow-handle-target:hover {
|
|
57
|
+
background: #22c55e !important;
|
|
58
|
+
border-color: #22c55e !important;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Resizer ──────────────────────────────────────────────────────────────── */
|
|
62
|
+
|
|
63
|
+
.pyreon-flow-resizer {
|
|
64
|
+
transition: background 0.1s ease, transform 0.1s ease;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.pyreon-flow-resizer:hover {
|
|
68
|
+
background: #3b82f6 !important;
|
|
69
|
+
transform: scale(1.2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── Selection box ────────────────────────────────────────────────────────── */
|
|
73
|
+
|
|
74
|
+
.pyreon-flow-selection-box {
|
|
75
|
+
pointer-events: none;
|
|
76
|
+
border-radius: 2px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ── MiniMap ──────────────────────────────────────────────────────────────── */
|
|
80
|
+
|
|
81
|
+
.pyreon-flow-minimap {
|
|
82
|
+
transition: opacity 0.2s ease;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.pyreon-flow-minimap:hover {
|
|
86
|
+
opacity: 1 !important;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Node toolbar ─────────────────────────────────────────────────────────── */
|
|
90
|
+
|
|
91
|
+
.pyreon-flow-node-toolbar {
|
|
92
|
+
animation: pyreon-flow-toolbar-enter 0.15s ease;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes pyreon-flow-toolbar-enter {
|
|
96
|
+
from {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
transform: translateX(-50%) translateY(4px);
|
|
99
|
+
}
|
|
100
|
+
to {
|
|
101
|
+
opacity: 1;
|
|
102
|
+
transform: translateX(-50%) translateY(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ── Controls ─────────────────────────────────────────────────────────────── */
|
|
107
|
+
|
|
108
|
+
.pyreon-flow-controls button:hover {
|
|
109
|
+
background: #f3f4f6 !important;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.pyreon-flow-controls button:active {
|
|
113
|
+
background: #e5e7eb !important;
|
|
114
|
+
}
|
|
115
|
+
`
|