panel-reactflow 0.3.1a0__tar.gz → 0.4.0__tar.gz

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.
Files changed (89) hide show
  1. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/PKG-INFO +1 -1
  2. panel_reactflow-0.4.0/docs/how-to/context-menu.md +105 -0
  3. panel_reactflow-0.4.0/docs/how-to/control-handle-connectivity.md +336 -0
  4. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/how-to/declare-types.md +157 -0
  5. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/how-to/define-nodes-edges.md +24 -1
  6. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/how-to/react-to-events.md +1 -0
  7. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/index.md +1 -0
  8. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/releases.md +46 -0
  9. panel_reactflow-0.4.0/examples/context_menu.py +82 -0
  10. panel_reactflow-0.4.0/examples/edge_types_comparison.py +102 -0
  11. panel_reactflow-0.4.0/examples/smart_edges_example.py +99 -0
  12. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/pixi.lock +10738 -9965
  13. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/pixi.toml +1 -1
  14. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/base.py +146 -33
  15. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/dist/css/reactflow.css +36 -0
  16. panel_reactflow-0.4.0/src/panel_reactflow/dist/panel-reactflow.bundle.css +1 -0
  17. panel_reactflow-0.4.0/src/panel_reactflow/dist/panel-reactflow.bundle.js +88 -0
  18. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/models/reactflow.jsx +124 -46
  19. panel_reactflow-0.4.0/tests/test_connectable_handles.py +293 -0
  20. panel_reactflow-0.4.0/tests/test_connectable_integration.py +259 -0
  21. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/tests/test_core.py +4 -3
  22. panel_reactflow-0.4.0/tests/ui/test_context_menu.py +152 -0
  23. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/tests/ui/test_ui.py +267 -0
  24. panel_reactflow-0.3.1a0/src/panel_reactflow/dist/panel-reactflow.bundle.css +0 -1
  25. panel_reactflow-0.3.1a0/src/panel_reactflow/dist/panel-reactflow.bundle.js +0 -88
  26. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.copier-answers.yml +0 -0
  27. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.gitattributes +0 -0
  28. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.github/CODEOWNERS +0 -0
  29. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.github/dependabot.yml +0 -0
  30. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.github/workflows/build.yml +0 -0
  31. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.github/workflows/docs.yml +0 -0
  32. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.github/workflows/test.yml +0 -0
  33. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.gitignore +0 -0
  34. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.pre-commit-config.yaml +0 -0
  35. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/.prettierrc +0 -0
  36. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/LICENSE.txt +0 -0
  37. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/MANIFEST.in +0 -0
  38. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/README.md +0 -0
  39. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/logo.svg +0 -0
  40. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/declare-types.png +0 -0
  41. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/define-editors-edge.png +0 -0
  42. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/define-editors-node.png +0 -0
  43. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/define-nodes-edges.png +0 -0
  44. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/embed-views-in-nodes.png +0 -0
  45. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/advanced.png +0 -0
  46. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/custom_editor.png +0 -0
  47. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/edge_editors.png +0 -0
  48. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/node_edge_instances.png +0 -0
  49. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/schema_types.png +0 -0
  50. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/simple.png +0 -0
  51. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/threejs_viewer.png +0 -0
  52. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/examples/threejs_viewer_instances.png +0 -0
  53. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/quickstart.png +0 -0
  54. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/react-to-events.png +0 -0
  55. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/assets/screenshots/style-nodes-edges.png +0 -0
  56. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/advanced.md +0 -0
  57. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/custom-editor.md +0 -0
  58. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/edge-editors.md +0 -0
  59. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/index.md +0 -0
  60. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/node-edge-instances.md +0 -0
  61. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/schema-types.md +0 -0
  62. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/simple.md +0 -0
  63. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/threejs-viewer-instances.md +0 -0
  64. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/examples/threejs-viewer.md +0 -0
  65. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/how-to/define-editors.md +0 -0
  66. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/how-to/embed-views-in-nodes.md +0 -0
  67. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/how-to/style-nodes-edges.md +0 -0
  68. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/quickstart.md +0 -0
  69. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/docs/reference/panel_reactflow.md +0 -0
  70. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/advanced.py +0 -0
  71. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/custom_editor.py +0 -0
  72. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/edge_editors.py +0 -0
  73. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/node_edge_instances.py +0 -0
  74. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/schema_types.py +0 -0
  75. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/simple.py +0 -0
  76. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/threejs_viewer.py +0 -0
  77. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/examples/threejs_viewer_instances.py +0 -0
  78. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/hatch_build.py +0 -0
  79. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/pyproject.toml +0 -0
  80. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/__init__.py +0 -0
  81. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/__version.py +0 -0
  82. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/dist/icons/gear.svg +0 -0
  83. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/py.typed +0 -0
  84. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/src/panel_reactflow/schema.py +0 -0
  85. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/tests/__init__.py +0 -0
  86. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/tests/conftest.py +0 -0
  87. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/tests/test_api.py +0 -0
  88. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/tests/ui/__init__.py +0 -0
  89. {panel_reactflow-0.3.1a0 → panel_reactflow-0.4.0}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panel-reactflow
3
- Version: 0.3.1a0
3
+ Version: 0.4.0
4
4
  Summary: A Panel wrapper for the Reactflow JS library.
5
5
  Project-URL: Homepage, https://github.com/panel-extensions/panel-reactflow
6
6
  Project-URL: Source, https://github.com/panel-extensions/panel-reactflow
@@ -0,0 +1,105 @@
1
+ # Node Context Menus
2
+
3
+ Panel-ReactFlow supports per-node context menus that appear on right-click.
4
+ Define the menu content by overriding the `context_menu()` method on a `Node`
5
+ subclass. The method returns any Panel component, which is rendered as a
6
+ floating overlay at the click position.
7
+
8
+ ---
9
+
10
+ ## Define a context menu
11
+
12
+ Override `context_menu()` on your `Node` subclass to return a Panel component.
13
+ The menu is dismissed automatically when the user clicks elsewhere on the
14
+ canvas.
15
+
16
+ ```python
17
+ import panel as pn
18
+ import panel_material_ui as pmui
19
+ from panel_reactflow import Node, ReactFlow
20
+
21
+
22
+ class TaskNode(Node):
23
+ def context_menu(self):
24
+ return pn.Column(
25
+ pmui.Button(name="Run", variant="text", size="small"),
26
+ pmui.Button(name="Delete", variant="text", color="error", size="small"),
27
+ )
28
+
29
+
30
+ flow = ReactFlow(nodes=[
31
+ TaskNode(id="t1", position={"x": 0, "y": 0}, label="My Task", data={}),
32
+ ])
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Access node state in the menu
38
+
39
+ The `context_menu()` method runs on the node instance, so you have access to
40
+ all its parameters and the parent flow via `self.flow`.
41
+
42
+ ```python
43
+ class PipelineNode(Node):
44
+ status = param.Selector(
45
+ default="idle", objects=["idle", "running", "done"], precedence=0
46
+ )
47
+
48
+ def context_menu(self):
49
+ def set_status(status):
50
+ self.flow.patch_node_data(self.id, {"status": status})
51
+ # Close the menu after action
52
+ self.flow._handle_msg({"type": "close_context_menu"})
53
+
54
+ return pn.Column(
55
+ pn.pane.Markdown(f"**{self.label}** ({self.status})"),
56
+ pmui.Button(
57
+ name="Start", variant="text", size="small",
58
+ on_click=lambda e: set_status("running"),
59
+ ),
60
+ pmui.Button(
61
+ name="Delete", variant="text", color="error", size="small",
62
+ on_click=lambda e: self.flow.remove_node(self.id),
63
+ ),
64
+ )
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Close the menu programmatically
70
+
71
+ The context menu closes when the user clicks anywhere on the canvas pane.
72
+ To close it from a button callback (e.g. after performing an action), send
73
+ the close message:
74
+
75
+ ```python
76
+ self.flow._handle_msg({"type": "close_context_menu"})
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Listen for context menu events
82
+
83
+ You can also react to the right-click event without rendering a menu by
84
+ subscribing to the `"node_context_menu"` event:
85
+
86
+ ```python
87
+ def on_context(payload, flow):
88
+ print(f"Right-clicked node {payload['node_id']} at {payload['position']}")
89
+
90
+ flow.on("node_context_menu", on_context)
91
+ ```
92
+
93
+ The payload includes `node_id` and `position` (with `x` and `y` screen
94
+ coordinates).
95
+
96
+ ---
97
+
98
+ ## Tips
99
+
100
+ - Return `None` from `context_menu()` to disable the menu for specific nodes
101
+ (this is the default behavior).
102
+ - Only `Node` subclass instances support context menus. Dict-based nodes do
103
+ not trigger a context menu on right-click.
104
+ - The menu overlay uses the `.rf-context-menu` CSS class for styling. Override
105
+ it in a custom stylesheet to change appearance.
@@ -0,0 +1,336 @@
1
+ # Control Handle Connectivity
2
+
3
+ Restrict which connections users can create by configuring the connectable
4
+ flags on `NodeType`. This lets you enforce directional flow (sources, sinks,
5
+ transforms) and prevent invalid edges at the UI level.
6
+
7
+ ![Screenshot: connectable handles demo](../assets/screenshots/connectable-handles.png)
8
+
9
+ ---
10
+
11
+ ## Problem
12
+
13
+ By default, all handles are fully connectable — users can drag edges from or
14
+ to any handle on any node. But many graph types have directional semantics:
15
+
16
+ - **Data sources** should only output data, never accept input
17
+ - **Data sinks** should only accept input, never produce output
18
+ - **Monitor nodes** might accept input but only emit status (one direction)
19
+ - **Read-only nodes** might display data without allowing any new connections
20
+
21
+ Without restrictions, users can create semantically invalid edges that break
22
+ your application's logic.
23
+
24
+ ---
25
+
26
+ ## Solution
27
+
28
+ Use the six `*_connectable*` flags on `NodeType` to control which drag
29
+ operations are allowed for input and output handles.
30
+
31
+ ### Available flags
32
+
33
+ | Flag | Default | Controls |
34
+ |------|---------|----------|
35
+ | `input_connectable` | `True` | Whether input handles are connectable at all |
36
+ | `input_connectable_start` | `True` | Whether edges can **start from** input handles |
37
+ | `input_connectable_end` | `True` | Whether edges can **end at** input handles |
38
+ | `output_connectable` | `True` | Whether output handles are connectable at all |
39
+ | `output_connectable_start` | `True` | Whether edges can **start from** output handles |
40
+ | `output_connectable_end` | `True` | Whether edges can **end at** output handles |
41
+
42
+ ---
43
+
44
+ ## Common patterns
45
+
46
+ ### Data source (output only)
47
+
48
+ Produces data but cannot accept incoming connections:
49
+
50
+ ```python
51
+ from panel_reactflow import NodeType
52
+
53
+ source = NodeType(
54
+ type="source",
55
+ label="Data Source",
56
+ outputs=["data"],
57
+ output_connectable_start=True, # Can drag FROM output
58
+ output_connectable_end=False, # Cannot drag TO output
59
+ )
60
+ ```
61
+
62
+ **Valid**: Drag from source output → another node's input ✓
63
+ **Invalid**: Drag from another node → source output ✗
64
+
65
+ ### Data sink (input only)
66
+
67
+ Consumes data but cannot produce outgoing connections:
68
+
69
+ ```python
70
+ sink = NodeType(
71
+ type="sink",
72
+ label="Data Sink",
73
+ inputs=["data"],
74
+ input_connectable_start=False, # Cannot drag FROM input
75
+ input_connectable_end=True, # Can drag TO input
76
+ )
77
+ ```
78
+
79
+ **Valid**: Drag from another node's output → sink input ✓
80
+ **Invalid**: Drag from sink input → another node ✗
81
+
82
+ ### Transform (bidirectional)
83
+
84
+ Full connectivity in both directions (default):
85
+
86
+ ```python
87
+ transform = NodeType(
88
+ type="transform",
89
+ label="Transform",
90
+ inputs=["in"],
91
+ outputs=["out"],
92
+ # All flags default to True — no need to specify
93
+ )
94
+ ```
95
+
96
+ **Valid**: Any drag operation ✓
97
+
98
+ ### Monitor node
99
+
100
+ Accepts input and emits status, but status cannot receive incoming edges:
101
+
102
+ ```python
103
+ monitor = NodeType(
104
+ type="monitor",
105
+ label="Monitor",
106
+ inputs=["in"],
107
+ outputs=["status"],
108
+ input_connectable_start=False, # Cannot start edges from input
109
+ output_connectable_end=False, # Cannot end edges at output
110
+ )
111
+ ```
112
+
113
+ **Valid**: Drag from another node → monitor input ✓
114
+ **Valid**: Drag from monitor output → another node ✓
115
+ **Invalid**: Drag from monitor input → another node ✗
116
+ **Invalid**: Drag from another node → monitor output ✗
117
+
118
+ ### Read-only node
119
+
120
+ Displays data but allows no new connections:
121
+
122
+ ```python
123
+ readonly = NodeType(
124
+ type="readonly",
125
+ label="Read Only",
126
+ inputs=["in"],
127
+ outputs=["out"],
128
+ input_connectable=False,
129
+ output_connectable=False,
130
+ )
131
+ ```
132
+
133
+ **Invalid**: Any drag operation ✗
134
+
135
+ ---
136
+
137
+ ## Complete working example
138
+
139
+ This example builds a data pipeline with sources, transforms, and sinks:
140
+
141
+ ```python
142
+ import panel as pn
143
+ from panel_reactflow import NodeType, NodeSpec, EdgeSpec, ReactFlow
144
+
145
+ pn.extension("jsoneditor")
146
+
147
+ # Define node types
148
+ node_types = {
149
+ "source": NodeType(
150
+ type="source",
151
+ label="Data Source",
152
+ outputs=["data"],
153
+ output_connectable_start=True,
154
+ output_connectable_end=False,
155
+ ),
156
+ "transform": NodeType(
157
+ type="transform",
158
+ label="Transform",
159
+ inputs=["in"],
160
+ outputs=["out"],
161
+ ),
162
+ "monitor": NodeType(
163
+ type="monitor",
164
+ label="Monitor",
165
+ inputs=["in"],
166
+ outputs=["status"],
167
+ input_connectable_start=False,
168
+ output_connectable_end=False,
169
+ ),
170
+ "sink": NodeType(
171
+ type="sink",
172
+ label="Data Sink",
173
+ inputs=["data"],
174
+ input_connectable_start=False,
175
+ input_connectable_end=True,
176
+ ),
177
+ }
178
+
179
+ # Build pipeline
180
+ flow = ReactFlow(
181
+ nodes=[
182
+ NodeSpec(id="source1", type="source", position={"x": 100, "y": 150}, data={}).to_dict(),
183
+ NodeSpec(id="transform1", type="transform", position={"x": 350, "y": 150}, data={}).to_dict(),
184
+ NodeSpec(id="monitor1", type="monitor", position={"x": 600, "y": 150}, data={}).to_dict(),
185
+ NodeSpec(id="sink1", type="sink", position={"x": 850, "y": 150}, data={}).to_dict(),
186
+ ],
187
+ edges=[
188
+ EdgeSpec(id="e1", source="source1", target="transform1", sourceHandle="data", targetHandle="in").to_dict(),
189
+ EdgeSpec(id="e2", source="transform1", target="monitor1", sourceHandle="out", targetHandle="in").to_dict(),
190
+ EdgeSpec(id="e3", source="monitor1", target="sink1", sourceHandle="status", targetHandle="data").to_dict(),
191
+ ],
192
+ node_types=node_types,
193
+ width=1200,
194
+ height=400,
195
+ )
196
+
197
+ flow.servable()
198
+ ```
199
+
200
+ ### Try these interactions
201
+
202
+ 1. **Valid**: Drag from source output → transform input ✓
203
+ 2. **Valid**: Drag from transform output → monitor input ✓
204
+ 3. **Valid**: Drag from monitor output → sink input ✓
205
+ 4. **Invalid**: Try to drag TO source output ✗ (blocked)
206
+ 5. **Invalid**: Try to drag FROM sink input ✗ (blocked)
207
+ 6. **Invalid**: Try to drag TO monitor output ✗ (blocked)
208
+
209
+ The UI automatically prevents invalid connections — handles show different
210
+ cursor behavior and won't accept or initiate drag operations when restricted.
211
+
212
+ ---
213
+
214
+ ## Multiple handles per side
215
+
216
+ Connectable flags apply to **all handles on a given side** (input or output).
217
+ If a node has multiple input handles, `input_connectable_start=False` affects
218
+ all of them:
219
+
220
+ ```python
221
+ multi_input = NodeType(
222
+ type="multi",
223
+ label="Multi-Input",
224
+ inputs=["in1", "in2", "in3"], # Three input handles
225
+ outputs=["out"],
226
+ input_connectable_start=False, # Applies to all input handles
227
+ )
228
+ ```
229
+
230
+ All three input handles will respect the same connectivity rules.
231
+
232
+ ---
233
+
234
+ ## Programmatic edges vs UI restrictions
235
+
236
+ Connectable flags only affect **user drag interactions** in the UI. You can
237
+ still create edges programmatically regardless of the flags:
238
+
239
+ ```python
240
+ # This edge will be created even if handles are non-connectable
241
+ flow.add_edge(EdgeSpec(id="e1", source="source1", target="sink1"))
242
+ ```
243
+
244
+ Or by passing edges directly to `ReactFlow`:
245
+
246
+ ```python
247
+ edges = [
248
+ EdgeSpec(id="e1", source="source1", target="sink1").to_dict()
249
+ ]
250
+ flow = ReactFlow(nodes=nodes, edges=edges, node_types=node_types)
251
+ ```
252
+
253
+ The flags control what users can do via drag-and-drop, not what your code
254
+ can create.
255
+
256
+ ---
257
+
258
+ ## Flag independence
259
+
260
+ Each flag is independent. Setting one doesn't affect others:
261
+
262
+ ```python
263
+ # Only restrict starting edges from inputs
264
+ node = NodeType(
265
+ type="restricted",
266
+ inputs=["in"],
267
+ outputs=["out"],
268
+ input_connectable_start=False, # Only this flag changes
269
+ # input_connectable=True (default)
270
+ # input_connectable_end=True (default)
271
+ # All output_* flags also default to True
272
+ )
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Backwards compatibility
278
+
279
+ Existing code without connectable flags continues to work — all flags default
280
+ to `True`:
281
+
282
+ ```python
283
+ # Old-style node type definition
284
+ legacy = NodeType(
285
+ type="task",
286
+ label="Task",
287
+ inputs=["in"],
288
+ outputs=["out"],
289
+ )
290
+ # Behaves exactly as before — all handles fully connectable
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Use cases
296
+
297
+ ### ETL pipelines
298
+
299
+ ```python
300
+ extract = NodeType(type="extract", outputs=["data"], output_connectable_end=False)
301
+ transform = NodeType(type="transform", inputs=["in"], outputs=["out"])
302
+ load = NodeType(type="load", inputs=["data"], input_connectable_start=False)
303
+ ```
304
+
305
+ ### State machines
306
+
307
+ ```python
308
+ start = NodeType(type="start", outputs=["next"], output_connectable_end=False)
309
+ state = NodeType(type="state", inputs=["in"], outputs=["out"])
310
+ end = NodeType(type="end", inputs=["in"], input_connectable_start=False)
311
+ ```
312
+
313
+ ### DAG workflows
314
+
315
+ ```python
316
+ trigger = NodeType(type="trigger", outputs=["event"], output_connectable_end=False)
317
+ task = NodeType(type="task", inputs=["trigger"], outputs=["result"])
318
+ logger = NodeType(type="logger", inputs=["log"], input_connectable_start=False)
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Tips
324
+
325
+ - **Start with defaults**: Don't add restrictions until you need them.
326
+ - **Test in UI**: Try dragging to confirm the restrictions work as expected.
327
+ - **Use patterns**: The source/sink/transform/monitor patterns cover most use cases.
328
+ - **Document intent**: Add comments explaining why specific flags are set.
329
+
330
+ ---
331
+
332
+ ## Related
333
+
334
+ - [Declare Node & Edge Types](declare-types.md) — full type declaration guide
335
+ - [Define Nodes & Edges](define-nodes-edges.md) — node and edge structure
336
+ - [API Reference](../reference/panel_reactflow.md) — complete API documentation
@@ -6,6 +6,7 @@ kind of node/edge carries**. A type can provide:
6
6
  - a type name (`type`)
7
7
  - a display label (`label`)
8
8
  - node handles (`inputs` / `outputs`)
9
+ - handle connectivity controls (`input_connectable*` / `output_connectable*`)
9
10
  - a schema for the `data` payload (`schema`)
10
11
 
11
12
  Types are separate from editors. A type defines structure; an editor defines
@@ -237,3 +238,159 @@ flow = ReactFlow(
237
238
 
238
239
  Types without a schema still work; they just do not get schema-driven
239
240
  validation or auto-generated forms.
241
+
242
+ ---
243
+
244
+ ## Handle tooltips
245
+
246
+ By default, handles are plain connection points. You can add a tooltip (shown
247
+ on hover) by passing a dict with `"id"` and `"label"` instead of a plain string:
248
+
249
+ ```python
250
+ from panel_reactflow import NodeType
251
+
252
+ node_types = {
253
+ "transform": NodeType(
254
+ type="transform",
255
+ label="Transform",
256
+ inputs=[{"id": "in", "label": "Data Input"}],
257
+ outputs=[
258
+ {"id": "success", "label": "Successful results"},
259
+ {"id": "error", "label": "Failed records"},
260
+ ],
261
+ ),
262
+ }
263
+ ```
264
+
265
+ Plain strings and dicts can be mixed freely in the same list:
266
+
267
+ ```python
268
+ inputs=["simple_port", {"id": "documented_port", "label": "Hover to see this"}]
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Control handle connectivity
274
+
275
+ By default, all handles (inputs and outputs) are fully connectable — users can
276
+ drag edges from or to any handle. Use the `*_connectable*` flags to restrict
277
+ which connections are allowed.
278
+
279
+ ### Common patterns
280
+
281
+ #### Data source (output only)
282
+
283
+ A node that produces data but cannot accept incoming connections to its output:
284
+
285
+ ```python
286
+ from panel_reactflow import NodeType
287
+
288
+ source_type = NodeType(
289
+ type="data_source",
290
+ label="Data Source",
291
+ outputs=["data"],
292
+ output_connectable_start=True, # Can drag FROM output
293
+ output_connectable_end=False, # Cannot drag TO output
294
+ )
295
+ ```
296
+
297
+ #### Data sink (input only)
298
+
299
+ A node that consumes data but cannot produce outgoing connections from its input:
300
+
301
+ ```python
302
+ sink_type = NodeType(
303
+ type="data_sink",
304
+ label="Data Sink",
305
+ inputs=["data"],
306
+ input_connectable_start=False, # Cannot drag FROM input
307
+ input_connectable_end=True, # Can drag TO input
308
+ )
309
+ ```
310
+
311
+ #### Monitor node
312
+
313
+ A node that accepts input but whose output is status-only (one direction):
314
+
315
+ ```python
316
+ monitor_type = NodeType(
317
+ type="monitor",
318
+ label="Monitor",
319
+ inputs=["in"],
320
+ outputs=["status"],
321
+ input_connectable_start=False, # Cannot start edges from input
322
+ output_connectable_end=False, # Cannot end edges at output
323
+ )
324
+ ```
325
+
326
+ ### All connectivity flags
327
+
328
+ | Flag | Default | Controls |
329
+ |------|---------|----------|
330
+ | `input_connectable` | `True` | Whether input handles are connectable at all |
331
+ | `input_connectable_start` | `True` | Whether edges can start from input handles |
332
+ | `input_connectable_end` | `True` | Whether edges can end at input handles |
333
+ | `output_connectable` | `True` | Whether output handles are connectable at all |
334
+ | `output_connectable_start` | `True` | Whether edges can start from output handles |
335
+ | `output_connectable_end` | `True` | Whether edges can end at output handles |
336
+
337
+ ### Complete example
338
+
339
+ ```python
340
+ import panel as pn
341
+ from panel_reactflow import NodeType, NodeSpec, EdgeSpec, ReactFlow
342
+
343
+ pn.extension("jsoneditor")
344
+
345
+ # Define node types with different connectivity patterns
346
+ node_types = {
347
+ "source": NodeType(
348
+ type="source",
349
+ label="Data Source",
350
+ outputs=["data"],
351
+ output_connectable_start=True,
352
+ output_connectable_end=False,
353
+ ),
354
+ "transform": NodeType(
355
+ type="transform",
356
+ label="Transform",
357
+ inputs=["in"],
358
+ outputs=["out"],
359
+ # All connectable flags default to True
360
+ ),
361
+ "sink": NodeType(
362
+ type="sink",
363
+ label="Data Sink",
364
+ inputs=["data"],
365
+ input_connectable_start=False,
366
+ input_connectable_end=True,
367
+ ),
368
+ }
369
+
370
+ # Create a data pipeline
371
+ flow = ReactFlow(
372
+ nodes=[
373
+ NodeSpec(id="src", type="source", position={"x": 0, "y": 100}, data={}).to_dict(),
374
+ NodeSpec(id="tx", type="transform", position={"x": 250, "y": 100}, data={}).to_dict(),
375
+ NodeSpec(id="snk", type="sink", position={"x": 500, "y": 100}, data={}).to_dict(),
376
+ ],
377
+ edges=[
378
+ EdgeSpec(id="e1", source="src", target="tx").to_dict(),
379
+ EdgeSpec(id="e2", source="tx", target="snk").to_dict(),
380
+ ],
381
+ node_types=node_types,
382
+ sizing_mode="stretch_both",
383
+ )
384
+
385
+ flow.servable()
386
+ ```
387
+
388
+ In this example:
389
+
390
+ - Users can drag from the **source** output to the **transform** input ✓
391
+ - Users cannot drag to the **source** output ✗
392
+ - Users can drag from the **transform** output to the **sink** input ✓
393
+ - Users cannot drag from the **sink** input ✗
394
+
395
+ The UI prevents invalid connections automatically — non-connectable handles
396
+ show different cursor behavior and won't accept drag operations.
@@ -155,11 +155,34 @@ edges = [
155
155
  | `source` | yes | ID of the source node. |
156
156
  | `target` | yes | ID of the target node. |
157
157
  | `label` | no | Text rendered on the edge. |
158
- | `type` | no | Edge type name (for styling / editors). |
158
+ | `type` | no | Edge type (see built-in types below, or a custom type name). |
159
159
  | `data` | no | Arbitrary dict of payload data. |
160
160
  | `sourceHandle` | no | Specific output handle on the source node. |
161
161
  | `targetHandle` | no | Specific input handle on the target node. |
162
162
 
163
+ ### Built-in edge types
164
+
165
+ | Type | Description |
166
+ |------------------|-------------|
167
+ | `"bezier"` | Smooth bezier curve (default). |
168
+ | `"straight"` | Straight line between nodes. |
169
+ | `"step"` | Orthogonal path with right angles. |
170
+ | `"smoothstep"` | Step path with rounded corners. |
171
+ | `"smart_bezier"` | Bezier curve that automatically routes around nodes. |
172
+ | `"smart_straight"`| Straight segments that automatically route around nodes. |
173
+ | `"smart_step"` | Step path that automatically routes around nodes. |
174
+
175
+ Smart edge types use pathfinding to avoid overlapping with other nodes in
176
+ the graph. They are useful when edges would otherwise pass through
177
+ intermediate nodes.
178
+
179
+ ```python
180
+ edges = [
181
+ {"id": "e1", "source": "n1", "target": "n2", "type": "smoothstep"},
182
+ {"id": "e2", "source": "n1", "target": "n3", "type": "smart_bezier"},
183
+ ]
184
+ ```
185
+
163
186
  ---
164
187
 
165
188
  ## Define edges as classes
@@ -23,6 +23,7 @@ the `ReactFlow` instance as a second argument. You can also listen for
23
23
  | `node_deleted` | A node is removed. | `node_id` |
24
24
  | `node_moved` | A node is dragged to a new position. | `node_id`, `position` |
25
25
  | `node_clicked` | A node is clicked (single click). | `node_id` |
26
+ | `node_context_menu` | A node is right-clicked. | `node_id`, `position` |
26
27
  | `node_data_changed` | Node data is patched (via API, editor patch, or parameter-driven sync). | `node_id`, `patch` |
27
28
  | `edge_added` | An edge is created (UI connect or API). | `edge` |
28
29
  | `edge_deleted` | An edge is removed. | `edge_id` |
@@ -61,6 +61,7 @@ flow.servable()
61
61
 
62
62
  - [Define Nodes & Edges](how-to/define-nodes-edges.md)
63
63
  - [Declare Node & Edge Types](how-to/declare-types.md)
64
+ - [Control Handle Connectivity](how-to/control-handle-connectivity.md) — restrict connections
64
65
  - [Define Editors](how-to/define-editors.md) — node *and* edge editors
65
66
  - [Embed Views in Nodes](how-to/embed-views-in-nodes.md)
66
67
  - [Style Nodes & Edges](how-to/style-nodes-edges.md)