@pyreon/flow 0.9.0 → 0.11.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/lib/analysis/index.js.html +1 -1
- package/lib/{elk.bundled-B9dPTHTZ.js → elk.bundled-wgxWJMYd.js} +2 -2
- package/lib/elk.bundled-wgxWJMYd.js.map +1 -0
- package/lib/index.js +13 -13
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +13 -13
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +14 -7
- package/src/components/background.tsx +9 -14
- package/src/components/controls.tsx +11 -18
- package/src/components/flow-component.tsx +67 -117
- package/src/components/handle.tsx +8 -8
- package/src/components/minimap.tsx +10 -27
- package/src/components/node-resizer.tsx +28 -28
- package/src/components/node-toolbar.tsx +12 -21
- package/src/components/panel.tsx +9 -9
- package/src/edges.ts +16 -47
- package/src/flow.ts +32 -88
- package/src/index.ts +17 -17
- package/src/layout.ts +23 -31
- package/src/tests/flow-advanced.test.ts +191 -200
- package/src/tests/flow.test.ts +418 -468
- package/src/types.ts +28 -42
- package/lib/elk.bundled-B9dPTHTZ.js.map +0 -1
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import { signal } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
getSmartHandlePositions,
|
|
7
|
-
getWaypointPath,
|
|
8
|
-
} from '../edges'
|
|
9
|
-
import type {
|
|
10
|
-
Connection,
|
|
11
|
-
FlowInstance,
|
|
12
|
-
FlowNode,
|
|
13
|
-
NodeComponentProps,
|
|
14
|
-
} from '../types'
|
|
15
|
-
import { Position } from '../types'
|
|
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"
|
|
16
6
|
|
|
17
7
|
// ─── Node type registry ──────────────────────────────────────────────────────
|
|
18
8
|
|
|
19
|
-
type NodeTypeMap = Record<
|
|
20
|
-
string,
|
|
21
|
-
(props: NodeComponentProps<any>) => VNodeChild
|
|
22
|
-
>
|
|
9
|
+
type NodeTypeMap = Record<string, (props: NodeComponentProps<any>) => VNodeChild>
|
|
23
10
|
|
|
24
11
|
/**
|
|
25
12
|
* Default node renderer — simple labeled box.
|
|
26
13
|
*/
|
|
27
14
|
function DefaultNode(props: NodeComponentProps) {
|
|
28
|
-
const borderColor = props.selected ?
|
|
29
|
-
const cursor = props.dragging ?
|
|
15
|
+
const borderColor = props.selected ? "#3b82f6" : "#ddd"
|
|
16
|
+
const cursor = props.dragging ? "grabbing" : "grab"
|
|
30
17
|
return (
|
|
31
18
|
<div
|
|
32
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;`}
|
|
@@ -51,8 +38,8 @@ interface ConnectionState {
|
|
|
51
38
|
|
|
52
39
|
const emptyConnection: ConnectionState = {
|
|
53
40
|
active: false,
|
|
54
|
-
sourceNodeId:
|
|
55
|
-
sourceHandleId:
|
|
41
|
+
sourceNodeId: "",
|
|
42
|
+
sourceHandleId: "",
|
|
56
43
|
sourcePosition: Position.Right,
|
|
57
44
|
sourceX: 0,
|
|
58
45
|
sourceY: 0,
|
|
@@ -91,7 +78,7 @@ interface DragState {
|
|
|
91
78
|
|
|
92
79
|
const emptyDrag: DragState = {
|
|
93
80
|
active: false,
|
|
94
|
-
nodeId:
|
|
81
|
+
nodeId: "",
|
|
95
82
|
startX: 0,
|
|
96
83
|
startY: 0,
|
|
97
84
|
startPositions: new Map(),
|
|
@@ -122,11 +109,13 @@ function EdgeLayer(props: {
|
|
|
122
109
|
<defs>
|
|
123
110
|
<marker
|
|
124
111
|
id="flow-arrowhead"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
112
|
+
{...{
|
|
113
|
+
markerWidth: "10",
|
|
114
|
+
markerHeight: "7",
|
|
115
|
+
refX: "10",
|
|
116
|
+
refY: "3.5",
|
|
117
|
+
orient: "auto",
|
|
118
|
+
}}
|
|
130
119
|
>
|
|
131
120
|
<polygon points="0 0, 10 3.5, 0 7" fill="#999" />
|
|
132
121
|
</marker>
|
|
@@ -141,10 +130,7 @@ function EdgeLayer(props: {
|
|
|
141
130
|
const targetW = targetNode.width ?? 150
|
|
142
131
|
const targetH = targetNode.height ?? 40
|
|
143
132
|
|
|
144
|
-
const { sourcePosition, targetPosition } = getSmartHandlePositions(
|
|
145
|
-
sourceNode,
|
|
146
|
-
targetNode,
|
|
147
|
-
)
|
|
133
|
+
const { sourcePosition, targetPosition } = getSmartHandlePositions(sourceNode, targetNode)
|
|
148
134
|
|
|
149
135
|
const sourcePos = getHandlePosition(
|
|
150
136
|
sourcePosition,
|
|
@@ -170,7 +156,7 @@ function EdgeLayer(props: {
|
|
|
170
156
|
waypoints: edge.waypoints,
|
|
171
157
|
})
|
|
172
158
|
: getEdgePath(
|
|
173
|
-
edge.type ??
|
|
159
|
+
edge.type ?? "bezier",
|
|
174
160
|
sourcePos.x,
|
|
175
161
|
sourcePos.y,
|
|
176
162
|
sourcePosition,
|
|
@@ -186,10 +172,7 @@ function EdgeLayer(props: {
|
|
|
186
172
|
const CustomEdge = edge.type && edgeTypes?.[edge.type]
|
|
187
173
|
if (CustomEdge) {
|
|
188
174
|
return (
|
|
189
|
-
<g
|
|
190
|
-
key={edge.id}
|
|
191
|
-
onClick={() => edge.id && instance.selectEdge(edge.id)}
|
|
192
|
-
>
|
|
175
|
+
<g key={edge.id} onClick={() => edge.id && instance.selectEdge(edge.id)}>
|
|
193
176
|
<CustomEdge
|
|
194
177
|
edge={edge}
|
|
195
178
|
sourceX={sourcePos.x}
|
|
@@ -207,11 +190,11 @@ function EdgeLayer(props: {
|
|
|
207
190
|
<path
|
|
208
191
|
d={path}
|
|
209
192
|
fill="none"
|
|
210
|
-
stroke={isSelected ?
|
|
211
|
-
stroke-width={isSelected ?
|
|
193
|
+
stroke={isSelected ? "#3b82f6" : "#999"}
|
|
194
|
+
stroke-width={isSelected ? "2" : "1.5"}
|
|
212
195
|
marker-end="url(#flow-arrowhead)"
|
|
213
|
-
class={edge.animated ?
|
|
214
|
-
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 ?? ""}`}
|
|
215
198
|
onClick={() => {
|
|
216
199
|
if (edge.id) instance.selectEdge(edge.id)
|
|
217
200
|
instance._emit.edgeClick(edge)
|
|
@@ -235,7 +218,7 @@ function EdgeLayer(props: {
|
|
|
235
218
|
<path
|
|
236
219
|
d={
|
|
237
220
|
getEdgePath(
|
|
238
|
-
|
|
221
|
+
"bezier",
|
|
239
222
|
conn.sourceX,
|
|
240
223
|
conn.sourceY,
|
|
241
224
|
conn.sourcePosition,
|
|
@@ -270,13 +253,7 @@ function NodeLayer(props: {
|
|
|
270
253
|
position: Position,
|
|
271
254
|
) => void
|
|
272
255
|
}): VNodeChild {
|
|
273
|
-
const {
|
|
274
|
-
instance,
|
|
275
|
-
nodeTypes,
|
|
276
|
-
draggingNodeId,
|
|
277
|
-
onNodePointerDown,
|
|
278
|
-
onHandlePointerDown,
|
|
279
|
-
} = props
|
|
256
|
+
const { instance, nodeTypes, draggingNodeId, onNodePointerDown, onHandlePointerDown } = props
|
|
280
257
|
|
|
281
258
|
return () => {
|
|
282
259
|
const nodes = instance.nodes()
|
|
@@ -288,43 +265,37 @@ function NodeLayer(props: {
|
|
|
288
265
|
{nodes.map((node) => {
|
|
289
266
|
const isSelected = selectedIds.includes(node.id)
|
|
290
267
|
const isDragging = dragId === node.id
|
|
291
|
-
const NodeComponent =
|
|
292
|
-
(node.type && nodeTypes[node.type]) || nodeTypes.default!
|
|
268
|
+
const NodeComponent = (node.type && nodeTypes[node.type]) || nodeTypes.default!
|
|
293
269
|
|
|
294
270
|
return (
|
|
295
271
|
<div
|
|
296
272
|
key={node.id}
|
|
297
|
-
class={`pyreon-flow-node ${node.class ??
|
|
298
|
-
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 ?? ""}`}
|
|
299
275
|
data-nodeid={node.id}
|
|
300
276
|
onClick={(e: MouseEvent) => {
|
|
301
277
|
e.stopPropagation()
|
|
302
278
|
instance.selectNode(node.id, e.shiftKey)
|
|
303
279
|
instance._emit.nodeClick(node)
|
|
304
280
|
}}
|
|
305
|
-
|
|
281
|
+
onDblClick={(e: MouseEvent) => {
|
|
306
282
|
e.stopPropagation()
|
|
307
283
|
instance._emit.nodeDoubleClick(node)
|
|
308
284
|
}}
|
|
309
|
-
|
|
285
|
+
onPointerDown={(e: PointerEvent) => {
|
|
310
286
|
// Check if clicking a handle
|
|
311
287
|
const target = e.target as HTMLElement
|
|
312
|
-
const handle = target.closest(
|
|
288
|
+
const handle = target.closest(".pyreon-flow-handle")
|
|
313
289
|
if (handle) {
|
|
314
|
-
const hType =
|
|
315
|
-
|
|
316
|
-
const hId = handle.getAttribute('data-handleid') ?? 'source'
|
|
290
|
+
const hType = handle.getAttribute("data-handletype") ?? "source"
|
|
291
|
+
const hId = handle.getAttribute("data-handleid") ?? "source"
|
|
317
292
|
const hPos =
|
|
318
|
-
(handle.getAttribute(
|
|
319
|
-
Position.Right
|
|
293
|
+
(handle.getAttribute("data-handleposition") as Position) ?? Position.Right
|
|
320
294
|
onHandlePointerDown(e, node.id, hType, hId, hPos)
|
|
321
295
|
return
|
|
322
296
|
}
|
|
323
297
|
// Otherwise start dragging node
|
|
324
|
-
if (
|
|
325
|
-
node.draggable !== false &&
|
|
326
|
-
instance.config.nodesDraggable !== false
|
|
327
|
-
) {
|
|
298
|
+
if (node.draggable !== false && instance.config.nodesDraggable !== false) {
|
|
328
299
|
onNodePointerDown(e, node)
|
|
329
300
|
}
|
|
330
301
|
}}
|
|
@@ -348,7 +319,7 @@ function NodeLayer(props: {
|
|
|
348
319
|
type EdgeTypeMap = Record<
|
|
349
320
|
string,
|
|
350
321
|
(props: {
|
|
351
|
-
edge: import(
|
|
322
|
+
edge: import("../types").FlowEdge
|
|
352
323
|
sourceX: number
|
|
353
324
|
sourceY: number
|
|
354
325
|
targetX: number
|
|
@@ -407,7 +378,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
407
378
|
y: null,
|
|
408
379
|
})
|
|
409
380
|
|
|
410
|
-
const draggingNodeId = () => (dragState().active ? dragState().nodeId :
|
|
381
|
+
const draggingNodeId = () => (dragState().active ? dragState().nodeId : "")
|
|
411
382
|
|
|
412
383
|
// ── Node dragging ──────────────────────────────────────────────────────
|
|
413
384
|
|
|
@@ -445,9 +416,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
445
416
|
|
|
446
417
|
instance._emit.nodeDragStart(node)
|
|
447
418
|
|
|
448
|
-
const container = (e.currentTarget as HTMLElement).closest(
|
|
449
|
-
'.pyreon-flow',
|
|
450
|
-
) as HTMLElement
|
|
419
|
+
const container = (e.currentTarget as HTMLElement).closest(".pyreon-flow") as HTMLElement
|
|
451
420
|
if (container) container.setPointerCapture(e.pointerId)
|
|
452
421
|
}
|
|
453
422
|
|
|
@@ -468,13 +437,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
468
437
|
|
|
469
438
|
const w = node.width ?? 150
|
|
470
439
|
const h = node.height ?? 40
|
|
471
|
-
const handlePos = getHandlePosition(
|
|
472
|
-
position,
|
|
473
|
-
node.position.x,
|
|
474
|
-
node.position.y,
|
|
475
|
-
w,
|
|
476
|
-
h,
|
|
477
|
-
)
|
|
440
|
+
const handlePos = getHandlePosition(position, node.position.x, node.position.y, w, h)
|
|
478
441
|
|
|
479
442
|
connectionState.set({
|
|
480
443
|
active: true,
|
|
@@ -487,9 +450,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
487
450
|
currentY: handlePos.y,
|
|
488
451
|
})
|
|
489
452
|
|
|
490
|
-
const container = (e.target as HTMLElement).closest(
|
|
491
|
-
'.pyreon-flow',
|
|
492
|
-
) as HTMLElement
|
|
453
|
+
const container = (e.target as HTMLElement).closest(".pyreon-flow") as HTMLElement
|
|
493
454
|
if (container) container.setPointerCapture(e.pointerId)
|
|
494
455
|
}
|
|
495
456
|
|
|
@@ -501,10 +462,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
501
462
|
|
|
502
463
|
const delta = -e.deltaY * 0.001
|
|
503
464
|
const newZoom = Math.min(
|
|
504
|
-
Math.max(
|
|
505
|
-
instance.viewport.peek().zoom * (1 + delta),
|
|
506
|
-
instance.config.minZoom ?? 0.1,
|
|
507
|
-
),
|
|
465
|
+
Math.max(instance.viewport.peek().zoom * (1 + delta), instance.config.minZoom ?? 0.1),
|
|
508
466
|
instance.config.maxZoom ?? 4,
|
|
509
467
|
)
|
|
510
468
|
|
|
@@ -533,8 +491,8 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
533
491
|
if (instance.config.pannable === false) return
|
|
534
492
|
|
|
535
493
|
const target = e.target as HTMLElement
|
|
536
|
-
if (target.closest(
|
|
537
|
-
if (target.closest(
|
|
494
|
+
if (target.closest(".pyreon-flow-node")) return
|
|
495
|
+
if (target.closest(".pyreon-flow-handle")) return
|
|
538
496
|
|
|
539
497
|
// Shift+drag on empty space → selection box
|
|
540
498
|
if (e.shiftKey && instance.config.multiSelect !== false) {
|
|
@@ -679,11 +637,10 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
679
637
|
if (conn.active) {
|
|
680
638
|
// Check if we released over a handle target
|
|
681
639
|
const target = e.target as HTMLElement
|
|
682
|
-
const handle = target.closest(
|
|
640
|
+
const handle = target.closest(".pyreon-flow-handle")
|
|
683
641
|
if (handle) {
|
|
684
|
-
const targetNodeId =
|
|
685
|
-
|
|
686
|
-
const targetHandleId = handle.getAttribute('data-handleid') ?? 'target'
|
|
642
|
+
const targetNodeId = handle.closest(".pyreon-flow-node")?.getAttribute("data-nodeid") ?? ""
|
|
643
|
+
const targetHandleId = handle.getAttribute("data-handleid") ?? "target"
|
|
687
644
|
|
|
688
645
|
if (targetNodeId && targetNodeId !== conn.sourceNodeId) {
|
|
689
646
|
const connection: Connection = {
|
|
@@ -697,12 +654,8 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
697
654
|
instance.addEdge({
|
|
698
655
|
source: connection.source,
|
|
699
656
|
target: connection.target,
|
|
700
|
-
...(connection.sourceHandle != null
|
|
701
|
-
|
|
702
|
-
: {}),
|
|
703
|
-
...(connection.targetHandle != null
|
|
704
|
-
? { targetHandle: connection.targetHandle }
|
|
705
|
-
: {}),
|
|
657
|
+
...(connection.sourceHandle != null ? { sourceHandle: connection.sourceHandle } : {}),
|
|
658
|
+
...(connection.targetHandle != null ? { targetHandle: connection.targetHandle } : {}),
|
|
706
659
|
})
|
|
707
660
|
}
|
|
708
661
|
}
|
|
@@ -717,31 +670,31 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
717
670
|
// ── Keyboard ───────────────────────────────────────────────────────────
|
|
718
671
|
|
|
719
672
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
720
|
-
if (e.key ===
|
|
673
|
+
if (e.key === "Delete" || e.key === "Backspace") {
|
|
721
674
|
const target = e.target as HTMLElement
|
|
722
|
-
if (target.tagName ===
|
|
675
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return
|
|
723
676
|
instance.pushHistory()
|
|
724
677
|
instance.deleteSelected()
|
|
725
678
|
}
|
|
726
|
-
if (e.key ===
|
|
679
|
+
if (e.key === "Escape") {
|
|
727
680
|
instance.clearSelection()
|
|
728
681
|
connectionState.set({ ...emptyConnection })
|
|
729
682
|
}
|
|
730
|
-
if (e.key ===
|
|
683
|
+
if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
|
|
731
684
|
e.preventDefault()
|
|
732
685
|
instance.selectAll()
|
|
733
686
|
}
|
|
734
|
-
if (e.key ===
|
|
687
|
+
if (e.key === "c" && (e.metaKey || e.ctrlKey)) {
|
|
735
688
|
instance.copySelected()
|
|
736
689
|
}
|
|
737
|
-
if (e.key ===
|
|
690
|
+
if (e.key === "v" && (e.metaKey || e.ctrlKey)) {
|
|
738
691
|
instance.paste()
|
|
739
692
|
}
|
|
740
|
-
if (e.key ===
|
|
693
|
+
if (e.key === "z" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
741
694
|
e.preventDefault()
|
|
742
695
|
instance.undo()
|
|
743
696
|
}
|
|
744
|
-
if (e.key ===
|
|
697
|
+
if (e.key === "z" && (e.metaKey || e.ctrlKey) && e.shiftKey) {
|
|
745
698
|
e.preventDefault()
|
|
746
699
|
instance.redo()
|
|
747
700
|
}
|
|
@@ -757,10 +710,7 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
757
710
|
e.preventDefault()
|
|
758
711
|
const t1 = e.touches[0]!
|
|
759
712
|
const t2 = e.touches[1]!
|
|
760
|
-
lastTouchDist = Math.hypot(
|
|
761
|
-
t2.clientX - t1.clientX,
|
|
762
|
-
t2.clientY - t1.clientY,
|
|
763
|
-
)
|
|
713
|
+
lastTouchDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
|
|
764
714
|
lastTouchCenter = {
|
|
765
715
|
x: (t1.clientX + t2.clientX) / 2,
|
|
766
716
|
y: (t1.clientY + t2.clientY) / 2,
|
|
@@ -830,21 +780,21 @@ export function Flow(props: FlowComponentProps): VNodeChild {
|
|
|
830
780
|
resizeObserver.observe(el)
|
|
831
781
|
}
|
|
832
782
|
|
|
833
|
-
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 ?? ""}`
|
|
834
784
|
|
|
835
785
|
return (
|
|
836
786
|
<div
|
|
837
787
|
ref={containerRef}
|
|
838
|
-
class={`pyreon-flow ${props.class ??
|
|
788
|
+
class={`pyreon-flow ${props.class ?? ""}`}
|
|
839
789
|
style={containerStyle}
|
|
840
790
|
tabIndex={0}
|
|
841
791
|
onWheel={handleWheel}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
792
|
+
onPointerDown={handlePointerDown}
|
|
793
|
+
onPointerMove={handlePointerMove}
|
|
794
|
+
onPointerUp={handlePointerUp}
|
|
795
|
+
onTouchStart={handleTouchStart}
|
|
796
|
+
onTouchMove={handleTouchMove}
|
|
797
|
+
onKeyDown={handleKeyDown}
|
|
848
798
|
>
|
|
849
799
|
{children}
|
|
850
800
|
{() => {
|
|
@@ -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
|
|
@@ -12,14 +12,12 @@ import type { FlowInstance, MiniMapProps } from '../types'
|
|
|
12
12
|
* </Flow>
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
|
-
export function MiniMap(
|
|
16
|
-
props: MiniMapProps & { instance?: FlowInstance },
|
|
17
|
-
): VNodeChild {
|
|
15
|
+
export function MiniMap(props: MiniMapProps & { instance?: FlowInstance }): VNodeChild {
|
|
18
16
|
const {
|
|
19
17
|
width = 200,
|
|
20
18
|
height = 150,
|
|
21
|
-
nodeColor =
|
|
22
|
-
maskColor =
|
|
19
|
+
nodeColor = "#e2e8f0",
|
|
20
|
+
maskColor = "rgba(0, 0, 0, 0.08)",
|
|
23
21
|
instance,
|
|
24
22
|
} = props
|
|
25
23
|
|
|
@@ -29,8 +27,7 @@ export function MiniMap(
|
|
|
29
27
|
|
|
30
28
|
return () => {
|
|
31
29
|
const nodes = instance.nodes()
|
|
32
|
-
if (nodes.length === 0)
|
|
33
|
-
return <div class="pyreon-flow-minimap" style={containerStyle} />
|
|
30
|
+
if (nodes.length === 0) return <div class="pyreon-flow-minimap" style={containerStyle} />
|
|
34
31
|
|
|
35
32
|
// Calculate graph bounds
|
|
36
33
|
let minX = Number.POSITIVE_INFINITY
|
|
@@ -78,23 +75,10 @@ export function MiniMap(
|
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
return (
|
|
81
|
-
<div
|
|
82
|
-
|
|
83
|
-
style={containerStyle}
|
|
84
|
-
onClick={handleClick}
|
|
85
|
-
>
|
|
86
|
-
<svg
|
|
87
|
-
role="img"
|
|
88
|
-
aria-label="minimap"
|
|
89
|
-
width={String(width)}
|
|
90
|
-
height={String(height)}
|
|
91
|
-
>
|
|
78
|
+
<div class="pyreon-flow-minimap" style={containerStyle} onClick={handleClick}>
|
|
79
|
+
<svg role="img" aria-label="minimap" width={String(width)} height={String(height)}>
|
|
92
80
|
{/* Mask outside viewport */}
|
|
93
|
-
<rect
|
|
94
|
-
width={String(width)}
|
|
95
|
-
height={String(height)}
|
|
96
|
-
fill={maskColor}
|
|
97
|
-
/>
|
|
81
|
+
<rect width={String(width)} height={String(height)} fill={maskColor} />
|
|
98
82
|
|
|
99
83
|
{/* Nodes */}
|
|
100
84
|
{nodes.map((node) => {
|
|
@@ -102,8 +86,7 @@ export function MiniMap(
|
|
|
102
86
|
const h = (node.height ?? 40) * scale
|
|
103
87
|
const x = (node.position.x - minX + padding) * scale
|
|
104
88
|
const y = (node.position.y - minY + padding) * scale
|
|
105
|
-
const color =
|
|
106
|
-
typeof nodeColor === 'function' ? nodeColor(node) : nodeColor
|
|
89
|
+
const color = typeof nodeColor === "function" ? nodeColor(node) : nodeColor
|
|
107
90
|
|
|
108
91
|
return (
|
|
109
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
|
}
|
|
@@ -158,9 +158,9 @@ export function NodeResizer(props: NodeResizerProps): VNodeChild {
|
|
|
158
158
|
key={dir}
|
|
159
159
|
class={`pyreon-flow-resizer pyreon-flow-resizer-${dir}`}
|
|
160
160
|
style={`${baseStyle} ${directionPositions[dir]} cursor: ${directionCursors[dir]};`}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
onPointerDown={handler.onPointerDown}
|
|
162
|
+
onPointerMove={handler.onPointerMove}
|
|
163
|
+
onPointerUp={handler.onPointerUp}
|
|
164
164
|
/>
|
|
165
165
|
)
|
|
166
166
|
})}
|
|
@@ -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,33 +41,24 @@ const positionStyles: Record<string, string> = {
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
export function NodeToolbar(props: NodeToolbarProps): VNodeChild {
|
|
44
|
-
const {
|
|
45
|
-
position = 'top',
|
|
46
|
-
offset = 8,
|
|
47
|
-
showOnSelect = true,
|
|
48
|
-
selected = false,
|
|
49
|
-
children,
|
|
50
|
-
} = props
|
|
44
|
+
const { position = "top", offset = 8, showOnSelect = true, selected = false, children } = props
|
|
51
45
|
|
|
52
46
|
if (showOnSelect && !selected) return null
|
|
53
47
|
|
|
54
48
|
const posStyle = positionStyles[position] ?? positionStyles.top
|
|
55
49
|
const marginProp =
|
|
56
|
-
position ===
|
|
50
|
+
position === "top"
|
|
57
51
|
? `margin-bottom: ${offset}px;`
|
|
58
|
-
: position ===
|
|
52
|
+
: position === "bottom"
|
|
59
53
|
? `margin-top: ${offset}px;`
|
|
60
|
-
: position ===
|
|
54
|
+
: position === "left"
|
|
61
55
|
? `margin-right: ${offset}px;`
|
|
62
56
|
: `margin-left: ${offset}px;`
|
|
63
57
|
|
|
64
|
-
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 ?? ""}`
|
|
65
59
|
|
|
66
60
|
return (
|
|
67
|
-
<div
|
|
68
|
-
class={`pyreon-flow-node-toolbar ${props.class ?? ''}`}
|
|
69
|
-
style={baseStyle}
|
|
70
|
-
>
|
|
61
|
+
<div class={`pyreon-flow-node-toolbar ${props.class ?? ""}`} style={baseStyle}>
|
|
71
62
|
{children}
|
|
72
63
|
</div>
|
|
73
64
|
)
|