@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/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
+ `