@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
package/lib/types/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface Viewport {
|
|
|
16
16
|
y: number;
|
|
17
17
|
zoom: number;
|
|
18
18
|
}
|
|
19
|
-
type HandleType =
|
|
19
|
+
type HandleType = 'source' | 'target';
|
|
20
20
|
declare enum Position {
|
|
21
21
|
Top = "top",
|
|
22
22
|
Right = "right",
|
|
@@ -54,7 +54,7 @@ interface FlowNode<TData = Record<string, unknown>> {
|
|
|
54
54
|
/** Whether this node is a group */
|
|
55
55
|
group?: boolean;
|
|
56
56
|
}
|
|
57
|
-
type EdgeType =
|
|
57
|
+
type EdgeType = 'bezier' | 'smoothstep' | 'straight' | 'step';
|
|
58
58
|
interface FlowEdge {
|
|
59
59
|
id?: string;
|
|
60
60
|
source: string;
|
|
@@ -81,19 +81,19 @@ type ConnectionRule = Record<string, {
|
|
|
81
81
|
outputs: string[];
|
|
82
82
|
}>;
|
|
83
83
|
type NodeChange = {
|
|
84
|
-
type:
|
|
84
|
+
type: 'position';
|
|
85
85
|
id: string;
|
|
86
86
|
position: XYPosition;
|
|
87
87
|
} | {
|
|
88
|
-
type:
|
|
88
|
+
type: 'dimensions';
|
|
89
89
|
id: string;
|
|
90
90
|
dimensions: Dimensions;
|
|
91
91
|
} | {
|
|
92
|
-
type:
|
|
92
|
+
type: 'select';
|
|
93
93
|
id: string;
|
|
94
94
|
selected: boolean;
|
|
95
95
|
} | {
|
|
96
|
-
type:
|
|
96
|
+
type: 'remove';
|
|
97
97
|
id: string;
|
|
98
98
|
};
|
|
99
99
|
interface EdgePathResult {
|
|
@@ -297,16 +297,16 @@ interface FlowInstance {
|
|
|
297
297
|
/** Dispose all listeners and clean up */
|
|
298
298
|
dispose: () => void;
|
|
299
299
|
}
|
|
300
|
-
type LayoutAlgorithm =
|
|
300
|
+
type LayoutAlgorithm = 'layered' | 'force' | 'stress' | 'tree' | 'radial' | 'box' | 'rectpacking';
|
|
301
301
|
interface LayoutOptions {
|
|
302
302
|
/** Layout direction — default: 'DOWN' */
|
|
303
|
-
direction?:
|
|
303
|
+
direction?: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
|
|
304
304
|
/** Spacing between nodes — default: 50 */
|
|
305
305
|
nodeSpacing?: number;
|
|
306
306
|
/** Spacing between layers — default: 80 */
|
|
307
307
|
layerSpacing?: number;
|
|
308
308
|
/** Edge routing — default: 'orthogonal' */
|
|
309
|
-
edgeRouting?:
|
|
309
|
+
edgeRouting?: 'orthogonal' | 'splines' | 'polyline';
|
|
310
310
|
/** Whether to animate the layout transition — default: true */
|
|
311
311
|
animate?: boolean;
|
|
312
312
|
/** Animation duration in ms — default: 300 */
|
|
@@ -319,7 +319,7 @@ interface FlowProps {
|
|
|
319
319
|
children?: VNodeChild;
|
|
320
320
|
}
|
|
321
321
|
interface BackgroundProps {
|
|
322
|
-
variant?:
|
|
322
|
+
variant?: 'dots' | 'lines' | 'cross';
|
|
323
323
|
gap?: number;
|
|
324
324
|
size?: number;
|
|
325
325
|
color?: string;
|
|
@@ -337,10 +337,10 @@ interface ControlsProps {
|
|
|
337
337
|
showZoomOut?: boolean;
|
|
338
338
|
showFitView?: boolean;
|
|
339
339
|
showLock?: boolean;
|
|
340
|
-
position?:
|
|
340
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
341
341
|
}
|
|
342
342
|
interface PanelProps {
|
|
343
|
-
position?:
|
|
343
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
344
344
|
style?: string;
|
|
345
345
|
class?: string;
|
|
346
346
|
children?: VNodeChild;
|
|
@@ -503,7 +503,7 @@ declare function NodeResizer(props: NodeResizerProps): VNodeChild;
|
|
|
503
503
|
//#region src/components/node-toolbar.d.ts
|
|
504
504
|
interface NodeToolbarProps {
|
|
505
505
|
/** Position relative to node — default: 'top' */
|
|
506
|
-
position?:
|
|
506
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
507
507
|
/** Offset from node in px — default: 8 */
|
|
508
508
|
offset?: number;
|
|
509
509
|
/** Only show when node is selected — default: true */
|
package/package.json
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/flow",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Reactive flow diagrams for Pyreon — signal-native nodes, edges, pan/zoom, auto-layout",
|
|
5
|
+
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/flow#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
13
|
"directory": "packages/fundamentals/flow"
|
|
10
14
|
},
|
|
11
|
-
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/flow#readme",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
|
-
},
|
|
15
|
-
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
-
},
|
|
18
15
|
"files": [
|
|
19
16
|
"lib",
|
|
20
17
|
"src",
|
|
@@ -22,6 +19,7 @@
|
|
|
22
19
|
"LICENSE"
|
|
23
20
|
],
|
|
24
21
|
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
25
23
|
"main": "./lib/index.js",
|
|
26
24
|
"module": "./lib/index.js",
|
|
27
25
|
"types": "./lib/types/index.d.ts",
|
|
@@ -32,25 +30,27 @@
|
|
|
32
30
|
"types": "./lib/types/index.d.ts"
|
|
33
31
|
}
|
|
34
32
|
},
|
|
35
|
-
"
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "vl_rolldown_build",
|
|
38
38
|
"dev": "vl_rolldown_build-watch",
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"lint": "
|
|
41
|
+
"lint": "oxlint ."
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"elkjs": "^0.9.3"
|
|
45
45
|
},
|
|
46
|
-
"peerDependencies": {
|
|
47
|
-
"@pyreon/core": "^0.11.4",
|
|
48
|
-
"@pyreon/reactivity": "^0.11.4"
|
|
49
|
-
},
|
|
50
46
|
"devDependencies": {
|
|
51
47
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
52
|
-
"@pyreon/core": "^0.11.
|
|
53
|
-
"@pyreon/reactivity": "^0.11.
|
|
48
|
+
"@pyreon/core": "^0.11.6",
|
|
49
|
+
"@pyreon/reactivity": "^0.11.6",
|
|
54
50
|
"@vitus-labs/tools-lint": "^1.11.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@pyreon/core": "^0.11.6",
|
|
54
|
+
"@pyreon/reactivity": "^0.11.6"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import type { BackgroundProps } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { BackgroundProps } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Background pattern for the flow canvas.
|
|
@@ -13,11 +13,11 @@ import type { BackgroundProps } from "../types"
|
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
15
|
export function Background(props: BackgroundProps): VNodeChild {
|
|
16
|
-
const { variant =
|
|
16
|
+
const { variant = 'dots', gap = 20, size = 1, color = '#ddd' } = props
|
|
17
17
|
|
|
18
18
|
const patternId = `flow-bg-${variant}`
|
|
19
19
|
|
|
20
|
-
if (variant ===
|
|
20
|
+
if (variant === 'dots') {
|
|
21
21
|
return (
|
|
22
22
|
<svg
|
|
23
23
|
role="img"
|
|
@@ -32,7 +32,7 @@ export function Background(props: BackgroundProps): VNodeChild {
|
|
|
32
32
|
y="0"
|
|
33
33
|
width={String(gap)}
|
|
34
34
|
height={String(gap)}
|
|
35
|
-
{...{ patternUnits:
|
|
35
|
+
{...{ patternUnits: 'userSpaceOnUse' }}
|
|
36
36
|
>
|
|
37
37
|
<circle cx={String(size)} cy={String(size)} r={String(size)} fill={color} />
|
|
38
38
|
</pattern>
|
|
@@ -42,7 +42,7 @@ export function Background(props: BackgroundProps): VNodeChild {
|
|
|
42
42
|
)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
if (variant ===
|
|
45
|
+
if (variant === 'lines') {
|
|
46
46
|
return (
|
|
47
47
|
<svg
|
|
48
48
|
role="img"
|
|
@@ -57,7 +57,7 @@ export function Background(props: BackgroundProps): VNodeChild {
|
|
|
57
57
|
y="0"
|
|
58
58
|
width={String(gap)}
|
|
59
59
|
height={String(gap)}
|
|
60
|
-
{...{ patternUnits:
|
|
60
|
+
{...{ patternUnits: 'userSpaceOnUse' }}
|
|
61
61
|
>
|
|
62
62
|
<line
|
|
63
63
|
x1="0"
|
|
@@ -97,7 +97,7 @@ export function Background(props: BackgroundProps): VNodeChild {
|
|
|
97
97
|
y="0"
|
|
98
98
|
width={String(gap)}
|
|
99
99
|
height={String(gap)}
|
|
100
|
-
{...{ patternUnits:
|
|
100
|
+
{...{ patternUnits: 'userSpaceOnUse' }}
|
|
101
101
|
>
|
|
102
102
|
<line
|
|
103
103
|
x1={String(gap / 2 - size * 2)}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import type { ControlsProps, FlowInstance } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { ControlsProps, FlowInstance } 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
|
// Simple SVG icons
|
|
@@ -86,15 +86,15 @@ export function Controls(props: ControlsProps & { instance?: FlowInstance }): VN
|
|
|
86
86
|
showZoomOut = true,
|
|
87
87
|
showFitView = true,
|
|
88
88
|
showLock = false,
|
|
89
|
-
position =
|
|
89
|
+
position = 'bottom-left',
|
|
90
90
|
instance,
|
|
91
91
|
} = props
|
|
92
92
|
|
|
93
93
|
if (!instance) return null
|
|
94
94
|
|
|
95
|
-
const baseStyle = `position: absolute; ${positionStyles[position] ?? positionStyles[
|
|
95
|
+
const baseStyle = `position: absolute; ${positionStyles[position] ?? positionStyles['bottom-left']} display: flex; flex-direction: column; gap: 2px; z-index: 5; background: white; border: 1px solid #ddd; border-radius: 6px; padding: 2px; box-shadow: 0 1px 4px rgba(0,0,0,0.08);`
|
|
96
96
|
const btnStyle =
|
|
97
|
-
|
|
97
|
+
'width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; border-radius: 4px; cursor: pointer; color: #555; padding: 0;'
|
|
98
98
|
|
|
99
99
|
return () => {
|
|
100
100
|
const zoomPercent = Math.round(instance.zoom() * 100)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import { signal } from
|
|
3
|
-
import { getEdgePath, getHandlePosition, getSmartHandlePositions, getWaypointPath } from
|
|
4
|
-
import type { Connection, FlowInstance, FlowNode, NodeComponentProps } from
|
|
5
|
-
import { Position } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import { signal } from '@pyreon/reactivity'
|
|
3
|
+
import { getEdgePath, getHandlePosition, getSmartHandlePositions, getWaypointPath } from '../edges'
|
|
4
|
+
import type { Connection, FlowInstance, FlowNode, NodeComponentProps } from '../types'
|
|
5
|
+
import { Position } from '../types'
|
|
6
6
|
|
|
7
7
|
// ─── Node type registry ──────────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -12,8 +12,8 @@ type NodeTypeMap = Record<string, (props: NodeComponentProps<any>) => VNodeChild
|
|
|
12
12
|
* Default node renderer — simple labeled box.
|
|
13
13
|
*/
|
|
14
14
|
function DefaultNode(props: NodeComponentProps) {
|
|
15
|
-
const borderColor = props.selected ?
|
|
16
|
-
const cursor = props.dragging ?
|
|
15
|
+
const borderColor = props.selected ? '#3b82f6' : '#ddd'
|
|
16
|
+
const cursor = props.dragging ? 'grabbing' : 'grab'
|
|
17
17
|
return (
|
|
18
18
|
<div
|
|
19
19
|
style={`padding: 8px 16px; background: white; border: 2px solid ${borderColor}; border-radius: 6px; font-size: 13px; min-width: 80px; text-align: center; cursor: ${cursor}; user-select: none;`}
|
|
@@ -38,8 +38,8 @@ interface ConnectionState {
|
|
|
38
38
|
|
|
39
39
|
const emptyConnection: ConnectionState = {
|
|
40
40
|
active: false,
|
|
41
|
-
sourceNodeId:
|
|
42
|
-
sourceHandleId:
|
|
41
|
+
sourceNodeId: '',
|
|
42
|
+
sourceHandleId: '',
|
|
43
43
|
sourcePosition: Position.Right,
|
|
44
44
|
sourceX: 0,
|
|
45
45
|
sourceY: 0,
|
|
@@ -78,7 +78,7 @@ interface DragState {
|
|
|
78
78
|
|
|
79
79
|
const emptyDrag: DragState = {
|
|
80
80
|
active: false,
|
|
81
|
-
nodeId:
|
|
81
|
+
nodeId: '',
|
|
82
82
|
startX: 0,
|
|
83
83
|
startY: 0,
|
|
84
84
|
startPositions: new Map(),
|
|
@@ -110,11 +110,11 @@ function EdgeLayer(props: {
|
|
|
110
110
|
<marker
|
|
111
111
|
id="flow-arrowhead"
|
|
112
112
|
{...{
|
|
113
|
-
markerWidth:
|
|
114
|
-
markerHeight:
|
|
115
|
-
refX:
|
|
116
|
-
refY:
|
|
117
|
-
orient:
|
|
113
|
+
markerWidth: '10',
|
|
114
|
+
markerHeight: '7',
|
|
115
|
+
refX: '10',
|
|
116
|
+
refY: '3.5',
|
|
117
|
+
orient: 'auto',
|
|
118
118
|
}}
|
|
119
119
|
>
|
|
120
120
|
<polygon points="0 0, 10 3.5, 0 7" fill="#999" />
|
|
@@ -156,7 +156,7 @@ function EdgeLayer(props: {
|
|
|
156
156
|
waypoints: edge.waypoints,
|
|
157
157
|
})
|
|
158
158
|
: getEdgePath(
|
|
159
|
-
edge.type ??
|
|
159
|
+
edge.type ?? 'bezier',
|
|
160
160
|
sourcePos.x,
|
|
161
161
|
sourcePos.y,
|
|
162
162
|
sourcePosition,
|
|
@@ -190,11 +190,11 @@ function EdgeLayer(props: {
|
|
|
190
190
|
<path
|
|
191
191
|
d={path}
|
|
192
192
|
fill="none"
|
|
193
|
-
stroke={isSelected ?
|
|
194
|
-
stroke-width={isSelected ?
|
|
193
|
+
stroke={isSelected ? '#3b82f6' : '#999'}
|
|
194
|
+
stroke-width={isSelected ? '2' : '1.5'}
|
|
195
195
|
marker-end="url(#flow-arrowhead)"
|
|
196
|
-
class={edge.animated ?
|
|
197
|
-
style={`pointer-events: stroke; cursor: pointer; ${edge.style ??
|
|
196
|
+
class={edge.animated ? 'pyreon-flow-edge-animated' : ''}
|
|
197
|
+
style={`pointer-events: stroke; cursor: pointer; ${edge.style ?? ''}`}
|
|
198
198
|
onClick={() => {
|
|
199
199
|
if (edge.id) instance.selectEdge(edge.id)
|
|
200
200
|
instance._emit.edgeClick(edge)
|
|
@@ -218,7 +218,7 @@ function EdgeLayer(props: {
|
|
|
218
218
|
<path
|
|
219
219
|
d={
|
|
220
220
|
getEdgePath(
|
|
221
|
-
|
|
221
|
+
'bezier',
|
|
222
222
|
conn.sourceX,
|
|
223
223
|
conn.sourceY,
|
|
224
224
|
conn.sourcePosition,
|
|
@@ -270,8 +270,8 @@ function NodeLayer(props: {
|
|
|
270
270
|
return (
|
|
271
271
|
<div
|
|
272
272
|
key={node.id}
|
|
273
|
-
class={`pyreon-flow-node ${node.class ??
|
|
274
|
-
style={`position: absolute; transform: translate(${node.position.x}px, ${node.position.y}px); z-index: ${isDragging ? 1000 : isSelected ? 100 : 0}; ${node.style ??
|
|
273
|
+
class={`pyreon-flow-node ${node.class ?? ''} ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''}`}
|
|
274
|
+
style={`position: absolute; transform: translate(${node.position.x}px, ${node.position.y}px); z-index: ${isDragging ? 1000 : isSelected ? 100 : 0}; ${node.style ?? ''}`}
|
|
275
275
|
data-nodeid={node.id}
|
|
276
276
|
onClick={(e: MouseEvent) => {
|
|
277
277
|
e.stopPropagation()
|
|
@@ -285,12 +285,12 @@ function NodeLayer(props: {
|
|
|
285
285
|
onPointerDown={(e: PointerEvent) => {
|
|
286
286
|
// Check if clicking a handle
|
|
287
287
|
const target = e.target as HTMLElement
|
|
288
|
-
const handle = target.closest(
|
|
288
|
+
const handle = target.closest('.pyreon-flow-handle')
|
|
289
289
|
if (handle) {
|
|
290
|
-
const hType = handle.getAttribute(
|
|
291
|
-
const hId = handle.getAttribute(
|
|
290
|
+
const hType = handle.getAttribute('data-handletype') ?? 'source'
|
|
291
|
+
const hId = handle.getAttribute('data-handleid') ?? 'source'
|
|
292
292
|
const hPos =
|
|
293
|
-
(handle.getAttribute(
|
|
293
|
+
(handle.getAttribute('data-handleposition') as Position) ?? Position.Right
|
|
294
294
|
onHandlePointerDown(e, node.id, hType, hId, hPos)
|
|
295
295
|
return
|
|
296
296
|
}
|
|
@@ -319,7 +319,7 @@ function NodeLayer(props: {
|
|
|
319
319
|
type EdgeTypeMap = Record<
|
|
320
320
|
string,
|
|
321
321
|
(props: {
|
|
322
|
-
edge: import(
|
|
322
|
+
edge: import('../types').FlowEdge
|
|
323
323
|
sourceX: number
|
|
324
324
|
sourceY: number
|
|
325
325
|
targetX: number
|
|
@@ -378,7 +378,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
378
378
|
y: null,
|
|
379
379
|
})
|
|
380
380
|
|
|
381
|
-
const draggingNodeId = () => (dragState().active ? dragState().nodeId :
|
|
381
|
+
const draggingNodeId = () => (dragState().active ? dragState().nodeId : '')
|
|
382
382
|
|
|
383
383
|
// ── Node dragging ──────────────────────────────────────────────────────
|
|
384
384
|
|
|
@@ -416,7 +416,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
416
416
|
|
|
417
417
|
instance._emit.nodeDragStart(node)
|
|
418
418
|
|
|
419
|
-
const container = (e.currentTarget as HTMLElement).closest(
|
|
419
|
+
const container = (e.currentTarget as HTMLElement).closest('.pyreon-flow') as HTMLElement
|
|
420
420
|
if (container) container.setPointerCapture(e.pointerId)
|
|
421
421
|
}
|
|
422
422
|
|
|
@@ -450,7 +450,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
450
450
|
currentY: handlePos.y,
|
|
451
451
|
})
|
|
452
452
|
|
|
453
|
-
const container = (e.target as HTMLElement).closest(
|
|
453
|
+
const container = (e.target as HTMLElement).closest('.pyreon-flow') as HTMLElement
|
|
454
454
|
if (container) container.setPointerCapture(e.pointerId)
|
|
455
455
|
}
|
|
456
456
|
|
|
@@ -491,8 +491,8 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
491
491
|
if (instance.config.pannable === false) return
|
|
492
492
|
|
|
493
493
|
const target = e.target as HTMLElement
|
|
494
|
-
if (target.closest(
|
|
495
|
-
if (target.closest(
|
|
494
|
+
if (target.closest('.pyreon-flow-node')) return
|
|
495
|
+
if (target.closest('.pyreon-flow-handle')) return
|
|
496
496
|
|
|
497
497
|
// Shift+drag on empty space → selection box
|
|
498
498
|
if (e.shiftKey && instance.config.multiSelect !== false) {
|
|
@@ -637,10 +637,10 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
637
637
|
if (conn.active) {
|
|
638
638
|
// Check if we released over a handle target
|
|
639
639
|
const target = e.target as HTMLElement
|
|
640
|
-
const handle = target.closest(
|
|
640
|
+
const handle = target.closest('.pyreon-flow-handle')
|
|
641
641
|
if (handle) {
|
|
642
|
-
const targetNodeId = handle.closest(
|
|
643
|
-
const targetHandleId = handle.getAttribute(
|
|
642
|
+
const targetNodeId = handle.closest('.pyreon-flow-node')?.getAttribute('data-nodeid') ?? ''
|
|
643
|
+
const targetHandleId = handle.getAttribute('data-handleid') ?? 'target'
|
|
644
644
|
|
|
645
645
|
if (targetNodeId && targetNodeId !== conn.sourceNodeId) {
|
|
646
646
|
const connection: Connection = {
|
|
@@ -670,31 +670,31 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
670
670
|
// ── Keyboard ───────────────────────────────────────────────────────────
|
|
671
671
|
|
|
672
672
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
673
|
-
if (e.key ===
|
|
673
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
674
674
|
const target = e.target as HTMLElement
|
|
675
|
-
if (target.tagName ===
|
|
675
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return
|
|
676
676
|
instance.pushHistory()
|
|
677
677
|
instance.deleteSelected()
|
|
678
678
|
}
|
|
679
|
-
if (e.key ===
|
|
679
|
+
if (e.key === 'Escape') {
|
|
680
680
|
instance.clearSelection()
|
|
681
681
|
connectionState.set({ ...emptyConnection })
|
|
682
682
|
}
|
|
683
|
-
if (e.key ===
|
|
683
|
+
if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
|
684
684
|
e.preventDefault()
|
|
685
685
|
instance.selectAll()
|
|
686
686
|
}
|
|
687
|
-
if (e.key ===
|
|
687
|
+
if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
|
688
688
|
instance.copySelected()
|
|
689
689
|
}
|
|
690
|
-
if (e.key ===
|
|
690
|
+
if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
|
|
691
691
|
instance.paste()
|
|
692
692
|
}
|
|
693
|
-
if (e.key ===
|
|
693
|
+
if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
694
694
|
e.preventDefault()
|
|
695
695
|
instance.undo()
|
|
696
696
|
}
|
|
697
|
-
if (e.key ===
|
|
697
|
+
if (e.key === 'z' && (e.metaKey || e.ctrlKey) && e.shiftKey) {
|
|
698
698
|
e.preventDefault()
|
|
699
699
|
instance.redo()
|
|
700
700
|
}
|
|
@@ -780,12 +780,12 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
780
780
|
resizeObserver.observe(el)
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
-
const containerStyle = `position: relative; width: 100%; height: 100%; overflow: hidden; outline: none; touch-action: none; ${props.style ??
|
|
783
|
+
const containerStyle = `position: relative; width: 100%; height: 100%; overflow: hidden; outline: none; touch-action: none; ${props.style ?? ''}`
|
|
784
784
|
|
|
785
785
|
return (
|
|
786
786
|
<div
|
|
787
787
|
ref={containerRef}
|
|
788
|
-
class={`pyreon-flow ${props.class ??
|
|
788
|
+
class={`pyreon-flow ${props.class ?? ''}`}
|
|
789
789
|
style={containerStyle}
|
|
790
790
|
tabIndex={0}
|
|
791
791
|
onWheel={handleWheel}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import type { HandleProps } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { HandleProps } from '../types'
|
|
3
3
|
|
|
4
4
|
const positionOffset: Record<string, string> = {
|
|
5
|
-
top:
|
|
6
|
-
right:
|
|
7
|
-
bottom:
|
|
8
|
-
left:
|
|
5
|
+
top: 'top: -4px; left: 50%; transform: translateX(-50%);',
|
|
6
|
+
right: 'right: -4px; top: 50%; transform: translateY(-50%);',
|
|
7
|
+
bottom: 'bottom: -4px; left: 50%; transform: translateX(-50%);',
|
|
8
|
+
left: 'left: -4px; top: 50%; transform: translateY(-50%);',
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -26,13 +26,13 @@ const positionOffset: Record<string, string> = {
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
export function Handle(props: HandleProps): VNodeChild {
|
|
29
|
-
const { type, position, id, style =
|
|
29
|
+
const { type, position, id, style = '' } = props
|
|
30
30
|
const posStyle = positionOffset[position] ?? positionOffset.bottom
|
|
31
31
|
const baseStyle = `position: absolute; ${posStyle} width: 8px; height: 8px; background: #555; border: 2px solid white; border-radius: 50%; cursor: crosshair; z-index: 1; ${style}`
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
34
|
<div
|
|
35
|
-
class={`pyreon-flow-handle pyreon-flow-handle-${type} ${props.class ??
|
|
35
|
+
class={`pyreon-flow-handle pyreon-flow-handle-${type} ${props.class ?? ''}`}
|
|
36
36
|
style={baseStyle}
|
|
37
37
|
data-handletype={type}
|
|
38
38
|
data-handleid={id ?? type}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import type { FlowInstance, MiniMapProps } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { FlowInstance, MiniMapProps } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Miniature overview of the flow diagram showing all nodes
|
|
@@ -16,8 +16,8 @@ export function MiniMap(props: MiniMapProps & { instance?: FlowInstance }): VNod
|
|
|
16
16
|
const {
|
|
17
17
|
width = 200,
|
|
18
18
|
height = 150,
|
|
19
|
-
nodeColor =
|
|
20
|
-
maskColor =
|
|
19
|
+
nodeColor = '#e2e8f0',
|
|
20
|
+
maskColor = 'rgba(0, 0, 0, 0.08)',
|
|
21
21
|
instance,
|
|
22
22
|
} = props
|
|
23
23
|
|
|
@@ -86,7 +86,7 @@ export function MiniMap(props: MiniMapProps & { instance?: FlowInstance }): VNod
|
|
|
86
86
|
const h = (node.height ?? 40) * scale
|
|
87
87
|
const x = (node.position.x - minX + padding) * scale
|
|
88
88
|
const y = (node.position.y - minY + padding) * scale
|
|
89
|
-
const color = typeof nodeColor ===
|
|
89
|
+
const color = typeof nodeColor === 'function' ? nodeColor(node) : nodeColor
|
|
90
90
|
|
|
91
91
|
return (
|
|
92
92
|
<rect
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import type { FlowInstance } from
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { FlowInstance } from '../types'
|
|
3
3
|
|
|
4
4
|
export interface NodeResizerProps {
|
|
5
5
|
nodeId: string
|
|
@@ -14,28 +14,28 @@ export interface NodeResizerProps {
|
|
|
14
14
|
showEdgeHandles?: boolean
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
type ResizeDirection =
|
|
17
|
+
type ResizeDirection = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 's' | 'e' | 'w'
|
|
18
18
|
|
|
19
19
|
const directionCursors: Record<ResizeDirection, string> = {
|
|
20
|
-
nw:
|
|
21
|
-
ne:
|
|
22
|
-
sw:
|
|
23
|
-
se:
|
|
24
|
-
n:
|
|
25
|
-
s:
|
|
26
|
-
e:
|
|
27
|
-
w:
|
|
20
|
+
nw: 'nw-resize',
|
|
21
|
+
ne: 'ne-resize',
|
|
22
|
+
sw: 'sw-resize',
|
|
23
|
+
se: 'se-resize',
|
|
24
|
+
n: 'n-resize',
|
|
25
|
+
s: 's-resize',
|
|
26
|
+
e: 'e-resize',
|
|
27
|
+
w: 'w-resize',
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const directionPositions: Record<ResizeDirection, string> = {
|
|
31
|
-
nw:
|
|
32
|
-
ne:
|
|
33
|
-
sw:
|
|
34
|
-
se:
|
|
35
|
-
n:
|
|
36
|
-
s:
|
|
37
|
-
e:
|
|
38
|
-
w:
|
|
31
|
+
nw: 'top: -4px; left: -4px;',
|
|
32
|
+
ne: 'top: -4px; right: -4px;',
|
|
33
|
+
sw: 'bottom: -4px; left: -4px;',
|
|
34
|
+
se: 'bottom: -4px; right: -4px;',
|
|
35
|
+
n: 'top: -4px; left: 50%; transform: translateX(-50%);',
|
|
36
|
+
s: 'bottom: -4px; left: 50%; transform: translateX(-50%);',
|
|
37
|
+
e: 'right: -4px; top: 50%; transform: translateY(-50%);',
|
|
38
|
+
w: 'left: -4px; top: 50%; transform: translateY(-50%);',
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -67,8 +67,8 @@ export function NodeResizer(props: NodeResizerProps): VNodeChild {
|
|
|
67
67
|
} = props
|
|
68
68
|
|
|
69
69
|
const directions: ResizeDirection[] = showEdgeHandles
|
|
70
|
-
? [
|
|
71
|
-
: [
|
|
70
|
+
? ['nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w']
|
|
71
|
+
: ['nw', 'ne', 'sw', 'se']
|
|
72
72
|
|
|
73
73
|
const createHandler = (dir: ResizeDirection) => {
|
|
74
74
|
let startX = 0
|
|
@@ -112,19 +112,19 @@ export function NodeResizer(props: NodeResizerProps): VNodeChild {
|
|
|
112
112
|
let newY = startNodeY
|
|
113
113
|
|
|
114
114
|
// Horizontal
|
|
115
|
-
if (dir ===
|
|
115
|
+
if (dir === 'e' || dir === 'se' || dir === 'ne') {
|
|
116
116
|
newW = Math.max(minWidth, startWidth + dx)
|
|
117
117
|
}
|
|
118
|
-
if (dir ===
|
|
118
|
+
if (dir === 'w' || dir === 'sw' || dir === 'nw') {
|
|
119
119
|
newW = Math.max(minWidth, startWidth - dx)
|
|
120
120
|
newX = startNodeX + startWidth - newW
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// Vertical
|
|
124
|
-
if (dir ===
|
|
124
|
+
if (dir === 's' || dir === 'se' || dir === 'sw') {
|
|
125
125
|
newH = Math.max(minHeight, startHeight + dy)
|
|
126
126
|
}
|
|
127
|
-
if (dir ===
|
|
127
|
+
if (dir === 'n' || dir === 'ne' || dir === 'nw') {
|
|
128
128
|
newH = Math.max(minHeight, startHeight - dy)
|
|
129
129
|
newY = startNodeY + startHeight - newH
|
|
130
130
|
}
|