@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { HandleProps } from '../types'
|
|
3
|
+
|
|
4
|
+
const positionOffset: Record<string, string> = {
|
|
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
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Connection handle — attachment point on a node where edges connect.
|
|
13
|
+
* Place inside custom node components.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function CustomNode({ data }: NodeComponentProps) {
|
|
18
|
+
* return (
|
|
19
|
+
* <div class="custom-node">
|
|
20
|
+
* <Handle type="target" position={Position.Left} />
|
|
21
|
+
* <span>{data.label}</span>
|
|
22
|
+
* <Handle type="source" position={Position.Right} />
|
|
23
|
+
* </div>
|
|
24
|
+
* )
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function Handle(props: HandleProps): VNodeChild {
|
|
29
|
+
const { type, position, id, style = '' } = props
|
|
30
|
+
const posStyle = positionOffset[position] ?? positionOffset.bottom
|
|
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
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
class={`pyreon-flow-handle pyreon-flow-handle-${type} ${props.class ?? ''}`}
|
|
36
|
+
style={baseStyle}
|
|
37
|
+
data-handletype={type}
|
|
38
|
+
data-handleid={id ?? type}
|
|
39
|
+
data-handleposition={position}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { FlowInstance, MiniMapProps } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Miniature overview of the flow diagram showing all nodes
|
|
6
|
+
* and the current viewport position. Click to navigate.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <Flow instance={flow}>
|
|
11
|
+
* <MiniMap nodeColor={(n) => n.type === 'error' ? 'red' : '#ddd'} />
|
|
12
|
+
* </Flow>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function MiniMap(
|
|
16
|
+
props: MiniMapProps & { instance?: FlowInstance },
|
|
17
|
+
): VNodeChild {
|
|
18
|
+
const {
|
|
19
|
+
width = 200,
|
|
20
|
+
height = 150,
|
|
21
|
+
nodeColor = '#e2e8f0',
|
|
22
|
+
maskColor = 'rgba(0, 0, 0, 0.08)',
|
|
23
|
+
instance,
|
|
24
|
+
} = props
|
|
25
|
+
|
|
26
|
+
if (!instance) return null
|
|
27
|
+
|
|
28
|
+
const containerStyle = `position: absolute; bottom: 10px; right: 10px; width: ${width}px; height: ${height}px; border: 1px solid #ddd; background: white; border-radius: 4px; overflow: hidden; z-index: 5; cursor: pointer;`
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
const nodes = instance.nodes()
|
|
32
|
+
if (nodes.length === 0)
|
|
33
|
+
return <div class="pyreon-flow-minimap" style={containerStyle} />
|
|
34
|
+
|
|
35
|
+
// Calculate graph bounds
|
|
36
|
+
let minX = Number.POSITIVE_INFINITY
|
|
37
|
+
let minY = Number.POSITIVE_INFINITY
|
|
38
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
39
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
40
|
+
|
|
41
|
+
for (const node of nodes) {
|
|
42
|
+
const w = node.width ?? 150
|
|
43
|
+
const h = node.height ?? 40
|
|
44
|
+
minX = Math.min(minX, node.position.x)
|
|
45
|
+
minY = Math.min(minY, node.position.y)
|
|
46
|
+
maxX = Math.max(maxX, node.position.x + w)
|
|
47
|
+
maxY = Math.max(maxY, node.position.y + h)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const padding = 40
|
|
51
|
+
const graphW = maxX - minX + padding * 2
|
|
52
|
+
const graphH = maxY - minY + padding * 2
|
|
53
|
+
const scale = Math.min(width / graphW, height / graphH)
|
|
54
|
+
|
|
55
|
+
// Viewport rectangle in minimap coordinates
|
|
56
|
+
const vp = instance.viewport()
|
|
57
|
+
const cs = instance.containerSize()
|
|
58
|
+
const vpLeft = (-vp.x / vp.zoom - minX + padding) * scale
|
|
59
|
+
const vpTop = (-vp.y / vp.zoom - minY + padding) * scale
|
|
60
|
+
const vpWidth = (cs.width / vp.zoom) * scale
|
|
61
|
+
const vpHeight = (cs.height / vp.zoom) * scale
|
|
62
|
+
|
|
63
|
+
const handleClick = (e: MouseEvent) => {
|
|
64
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
65
|
+
const clickX = e.clientX - rect.left
|
|
66
|
+
const clickY = e.clientY - rect.top
|
|
67
|
+
|
|
68
|
+
// Convert minimap click to flow coordinates
|
|
69
|
+
const flowX = clickX / scale + minX - padding
|
|
70
|
+
const flowY = clickY / scale + minY - padding
|
|
71
|
+
|
|
72
|
+
// Center viewport on clicked point
|
|
73
|
+
instance.viewport.set({
|
|
74
|
+
...vp,
|
|
75
|
+
x: -(flowX * vp.zoom) + cs.width / 2,
|
|
76
|
+
y: -(flowY * vp.zoom) + cs.height / 2,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
class="pyreon-flow-minimap"
|
|
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
|
+
>
|
|
92
|
+
{/* Mask outside viewport */}
|
|
93
|
+
<rect
|
|
94
|
+
width={String(width)}
|
|
95
|
+
height={String(height)}
|
|
96
|
+
fill={maskColor}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{/* Nodes */}
|
|
100
|
+
{nodes.map((node) => {
|
|
101
|
+
const w = (node.width ?? 150) * scale
|
|
102
|
+
const h = (node.height ?? 40) * scale
|
|
103
|
+
const x = (node.position.x - minX + padding) * scale
|
|
104
|
+
const y = (node.position.y - minY + padding) * scale
|
|
105
|
+
const color =
|
|
106
|
+
typeof nodeColor === 'function' ? nodeColor(node) : nodeColor
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<rect
|
|
110
|
+
key={node.id}
|
|
111
|
+
x={String(x)}
|
|
112
|
+
y={String(y)}
|
|
113
|
+
width={String(w)}
|
|
114
|
+
height={String(h)}
|
|
115
|
+
fill={color}
|
|
116
|
+
rx="2"
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
})}
|
|
120
|
+
|
|
121
|
+
{/* Viewport indicator */}
|
|
122
|
+
<rect
|
|
123
|
+
x={String(Math.max(0, vpLeft))}
|
|
124
|
+
y={String(Math.max(0, vpTop))}
|
|
125
|
+
width={String(Math.min(vpWidth, width))}
|
|
126
|
+
height={String(Math.min(vpHeight, height))}
|
|
127
|
+
fill="none"
|
|
128
|
+
stroke="#3b82f6"
|
|
129
|
+
stroke-width="1.5"
|
|
130
|
+
rx="2"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { FlowInstance } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface NodeResizerProps {
|
|
5
|
+
nodeId: string
|
|
6
|
+
instance: FlowInstance
|
|
7
|
+
/** Minimum width — default: 50 */
|
|
8
|
+
minWidth?: number
|
|
9
|
+
/** Minimum height — default: 30 */
|
|
10
|
+
minHeight?: number
|
|
11
|
+
/** Handle size in px — default: 8 */
|
|
12
|
+
handleSize?: number
|
|
13
|
+
/** Also show edge (non-corner) resize handles — default: false */
|
|
14
|
+
showEdgeHandles?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ResizeDirection = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 's' | 'e' | 'w'
|
|
18
|
+
|
|
19
|
+
const directionCursors: Record<ResizeDirection, string> = {
|
|
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
|
+
}
|
|
29
|
+
|
|
30
|
+
const directionPositions: Record<ResizeDirection, string> = {
|
|
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
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Node resize handles. Place inside a custom node component
|
|
43
|
+
* to allow users to resize the node by dragging corners or edges.
|
|
44
|
+
*
|
|
45
|
+
* Uses pointer capture for clean event handling — no document listener leaks.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* function ResizableNode({ id, data, selected }: NodeComponentProps) {
|
|
50
|
+
* return (
|
|
51
|
+
* <div style="min-width: 100px; min-height: 50px; position: relative;">
|
|
52
|
+
* {data.label}
|
|
53
|
+
* <NodeResizer nodeId={id} instance={flow} />
|
|
54
|
+
* </div>
|
|
55
|
+
* )
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function NodeResizer(props: NodeResizerProps): VNodeChild {
|
|
60
|
+
const {
|
|
61
|
+
nodeId,
|
|
62
|
+
instance,
|
|
63
|
+
minWidth = 50,
|
|
64
|
+
minHeight = 30,
|
|
65
|
+
handleSize = 8,
|
|
66
|
+
showEdgeHandles = false,
|
|
67
|
+
} = props
|
|
68
|
+
|
|
69
|
+
const directions: ResizeDirection[] = showEdgeHandles
|
|
70
|
+
? ['nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w']
|
|
71
|
+
: ['nw', 'ne', 'sw', 'se']
|
|
72
|
+
|
|
73
|
+
const createHandler = (dir: ResizeDirection) => {
|
|
74
|
+
let startX = 0
|
|
75
|
+
let startY = 0
|
|
76
|
+
let startWidth = 0
|
|
77
|
+
let startHeight = 0
|
|
78
|
+
let startNodeX = 0
|
|
79
|
+
let startNodeY = 0
|
|
80
|
+
let zoomAtStart = 1
|
|
81
|
+
|
|
82
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
83
|
+
e.stopPropagation()
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
|
|
86
|
+
const node = instance.getNode(nodeId)
|
|
87
|
+
if (!node) return
|
|
88
|
+
|
|
89
|
+
startX = e.clientX
|
|
90
|
+
startY = e.clientY
|
|
91
|
+
startWidth = node.width ?? 150
|
|
92
|
+
startHeight = node.height ?? 40
|
|
93
|
+
startNodeX = node.position.x
|
|
94
|
+
startNodeY = node.position.y
|
|
95
|
+
zoomAtStart = instance.viewport.peek().zoom
|
|
96
|
+
|
|
97
|
+
// Use pointer capture — clean, no leaks
|
|
98
|
+
const el = e.currentTarget as HTMLElement
|
|
99
|
+
el.setPointerCapture(e.pointerId)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
103
|
+
const el = e.currentTarget as HTMLElement
|
|
104
|
+
if (!el.hasPointerCapture(e.pointerId)) return
|
|
105
|
+
|
|
106
|
+
const dx = (e.clientX - startX) / zoomAtStart
|
|
107
|
+
const dy = (e.clientY - startY) / zoomAtStart
|
|
108
|
+
|
|
109
|
+
let newW = startWidth
|
|
110
|
+
let newH = startHeight
|
|
111
|
+
let newX = startNodeX
|
|
112
|
+
let newY = startNodeY
|
|
113
|
+
|
|
114
|
+
// Horizontal
|
|
115
|
+
if (dir === 'e' || dir === 'se' || dir === 'ne') {
|
|
116
|
+
newW = Math.max(minWidth, startWidth + dx)
|
|
117
|
+
}
|
|
118
|
+
if (dir === 'w' || dir === 'sw' || dir === 'nw') {
|
|
119
|
+
newW = Math.max(minWidth, startWidth - dx)
|
|
120
|
+
newX = startNodeX + startWidth - newW
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Vertical
|
|
124
|
+
if (dir === 's' || dir === 'se' || dir === 'sw') {
|
|
125
|
+
newH = Math.max(minHeight, startHeight + dy)
|
|
126
|
+
}
|
|
127
|
+
if (dir === 'n' || dir === 'ne' || dir === 'nw') {
|
|
128
|
+
newH = Math.max(minHeight, startHeight - dy)
|
|
129
|
+
newY = startNodeY + startHeight - newH
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
instance.updateNode(nodeId, {
|
|
133
|
+
width: newW,
|
|
134
|
+
height: newH,
|
|
135
|
+
position: { x: newX, y: newY },
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
140
|
+
const el = e.currentTarget as HTMLElement
|
|
141
|
+
if (el.hasPointerCapture(e.pointerId)) {
|
|
142
|
+
el.releasePointerCapture(e.pointerId)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { onPointerDown, onPointerMove, onPointerUp }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const size = `${handleSize}px`
|
|
150
|
+
const baseStyle = `position: absolute; width: ${size}; height: ${size}; background: white; border: 1.5px solid #3b82f6; border-radius: 2px; z-index: 2;`
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
{directions.map((dir) => {
|
|
155
|
+
const handler = createHandler(dir)
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
key={dir}
|
|
159
|
+
class={`pyreon-flow-resizer pyreon-flow-resizer-${dir}`}
|
|
160
|
+
style={`${baseStyle} ${directionPositions[dir]} cursor: ${directionCursors[dir]};`}
|
|
161
|
+
onPointerdown={handler.onPointerDown}
|
|
162
|
+
onPointermove={handler.onPointerMove}
|
|
163
|
+
onPointerup={handler.onPointerUp}
|
|
164
|
+
/>
|
|
165
|
+
)
|
|
166
|
+
})}
|
|
167
|
+
</>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
|
|
3
|
+
export interface NodeToolbarProps {
|
|
4
|
+
/** Position relative to node — default: 'top' */
|
|
5
|
+
position?: 'top' | 'bottom' | 'left' | 'right'
|
|
6
|
+
/** Offset from node in px — default: 8 */
|
|
7
|
+
offset?: number
|
|
8
|
+
/** Only show when node is selected — default: true */
|
|
9
|
+
showOnSelect?: boolean
|
|
10
|
+
/** Whether the node is currently selected */
|
|
11
|
+
selected?: boolean
|
|
12
|
+
style?: string
|
|
13
|
+
class?: string
|
|
14
|
+
children?: VNodeChild
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const positionStyles: Record<string, string> = {
|
|
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
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Floating toolbar that appears near a node, typically when selected.
|
|
26
|
+
* Place inside a custom node component.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* function EditableNode({ id, data, selected }: NodeComponentProps) {
|
|
31
|
+
* return (
|
|
32
|
+
* <div class="node">
|
|
33
|
+
* {data.label}
|
|
34
|
+
* <NodeToolbar selected={selected}>
|
|
35
|
+
* <button onClick={() => duplicate(id)}>Duplicate</button>
|
|
36
|
+
* <button onClick={() => remove(id)}>Delete</button>
|
|
37
|
+
* </NodeToolbar>
|
|
38
|
+
* </div>
|
|
39
|
+
* )
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
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
|
|
51
|
+
|
|
52
|
+
if (showOnSelect && !selected) return null
|
|
53
|
+
|
|
54
|
+
const posStyle = positionStyles[position] ?? positionStyles.top
|
|
55
|
+
const marginProp =
|
|
56
|
+
position === 'top'
|
|
57
|
+
? `margin-bottom: ${offset}px;`
|
|
58
|
+
: position === 'bottom'
|
|
59
|
+
? `margin-top: ${offset}px;`
|
|
60
|
+
: position === 'left'
|
|
61
|
+
? `margin-right: ${offset}px;`
|
|
62
|
+
: `margin-left: ${offset}px;`
|
|
63
|
+
|
|
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 ?? ''}`
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
class={`pyreon-flow-node-toolbar ${props.class ?? ''}`}
|
|
69
|
+
style={baseStyle}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import type { PanelProps } from '../types'
|
|
3
|
+
|
|
4
|
+
const positionStyles: Record<string, string> = {
|
|
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
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Positioned overlay panel for custom content inside the flow canvas.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Flow instance={flow}>
|
|
17
|
+
* <Panel position="top-right">
|
|
18
|
+
* <SearchBar />
|
|
19
|
+
* </Panel>
|
|
20
|
+
* </Flow>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function Panel(props: PanelProps): VNodeChild {
|
|
24
|
+
const { position = 'top-left', style = '', children } = props
|
|
25
|
+
const posStyle = positionStyles[position] ?? positionStyles['top-left']
|
|
26
|
+
const baseStyle = `position: absolute; ${posStyle} z-index: 5; ${style}`
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div class={`pyreon-flow-panel ${props.class ?? ''}`} style={baseStyle}>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|