html-overlay-node 0.1.6 → 0.1.10

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 CHANGED
@@ -3,419 +3,113 @@
3
3
  [![npm version](https://img.shields.io/npm/v/html-overlay-node.svg)](https://www.npmjs.com/package/html-overlay-node)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- **HTML-overlay-Node** is a customizable, LiteGraph-style node editor library for building visual programming interfaces. It uses **Canvas rendering** for fast performance and supports node type registration, execution cycle control, custom drawing, HTML overlays, and group management.
6
+ **HTML-overlay-Node**는 Canvas의 정밀함과 HTML의 유연함을 결합한 전문가용 노드 에디터 라이브러리입니다. 장난감 같은 디자인에서 벗어나 실제 프로덕션 환경에 적합한 날카롭고 세련된 UI/UX를 지향합니다.
7
7
 
8
8
  ---
9
9
 
10
- ## Features
11
-
12
- - 🎨 **Production-quality design** - Professional dark theme with refined aesthetics
13
- - 🔧 **Type registration system** - Easily create custom node types
14
- - 🔌 **Dual port system** - Exec ports (flow control) and data ports (values)
15
- - ⚡ **Flexible execution** - Automatic or manual execution modes with trigger nodes
16
- - 🔗 **Multiple edge styles** - Curved, orthogonal, or straight connections
17
- - 💾 **Serialization** - Save and load graphs with `toJSON`/`fromJSON`
18
- - 🖱️ **Rich interactions** - Zoom, pan, drag, box select, and snap-to-grid
19
- - ⌨️ **Keyboard shortcuts** - Undo/redo, align, group, and more
20
- - 🎯 **Custom drawing** - Per-node custom rendering with `onDraw`
21
- - 🌐 **HTML overlays** - Embed interactive HTML UIs with proper port layering
22
- - 📦 **Group nodes** - Organize nodes in hierarchical groups
23
- - 🪝 **Event hooks** - Subscribe to graph events for extensibility
24
- - 🎥 **Visual feedback** - Smooth animations and hover states
10
+ ## 주요 특징
25
11
 
26
- ---
12
+ - **전문가급 디자인**: 2px의 날카로운 라운딩 처리와 깊이감 있는 다크 테마로 도구의 전문성을 극대화했습니다.
13
+ - **카테고리별 컬러 시스템**: Math, Logic, Data 등 노드 성격에 따른 정교한 컬러 코딩으로 복잡한 그래프의 가독성을 높였습니다.
14
+ - **강력한 듀얼 포트**: 실행 제어(Flow)를 위한 Exec 포트와 데이터 전송(Data) 포트를 분리하여 복잡한 로직 설계가 가능합니다.
15
+ - **인터랙티브 HTML 오버레이**: 노드 내부의 복잡한 UI는 HTML로, 전체 연결은 Canvas로 처리하여 성능과 확장성을 모두 잡았습니다.
16
+ - **파워 유저 생산성**: 정렬(Align), 그룹화(Group), Undo/Redo 등 실제 작업 시간을 단축해주는 풍부한 단축키를 지원합니다.
17
+ - **정밀한 시각 릴레이**: 노드 실행 상태를 보여주는 완벽하게 밀착된 테두리 애니메이션을 통해 실시간 피드백을 제공합니다.
27
18
 
28
- ## Demo
19
+ ---
29
20
 
30
- - 🏠 [Demo](https://cheonghakim.github.io/HTML-overlay-node/)
21
+ ## 🚀 시작하기
31
22
 
32
- ## 🚀 Quick Start
23
+ Vite나 Webpack 환경에서 바로 시작할 수 있습니다.
33
24
 
34
25
  ```javascript
35
26
  import { createGraphEditor } from "html-overlay-node";
27
+ import "html-overlay-node/index.css";
36
28
 
37
- // One-liner Initialization!
38
- // Pass a selector or HTMLElement. Canvas and overlays are created automatically.
29
+ // 한 줄로 에디터 초기화
39
30
  const editor = createGraphEditor("#editor-container", {
40
31
  autorun: true,
41
- enablePropertyPanel: true, // Integrated property panel (default: true)
42
- });
43
-
44
- const { graph, registry, addGroup, start } = editor;
45
- ```
46
-
47
- ### 💅 Styles (Required)
48
-
49
- Make sure to import the necessary CSS files for the editor and Property Panel to look correctly:
50
-
51
- ```javascript
52
- import "html-overlay-node/index.css";
53
- import "html-overlay-node/src/ui/PropertyPanel.css";
54
- ```
55
-
56
- // Register and add nodes
57
- registry.register("math/Add", {
58
- title: "Add",
59
- size: { w: 180, h: 80 },
60
- inputs: [
61
- { name: "a", datatype: "number" },
62
- { name: "b", datatype: "number" },
63
- ],
64
- outputs: [{ name: "result", datatype: "number" }],
65
- onCreate(node) {
66
- node.state.a = 0;
67
- node.state.b = 0;
68
- },
69
- onExecute(node, { getInput, setOutput }) {
70
- const a = getInput("a") ?? node.state.a;
71
- const b = getInput("b") ?? node.state.b;
72
- setOutput("result", a + b);
73
- },
74
- });
75
-
76
- // Add nodes
77
- const node1 = graph.addNode("math/Add", { x: 100, y: 100 });
78
- const node2 = graph.addNode("math/Add", { x: 100, y: 200 });
79
-
80
- // Create a group
81
- addGroup({
82
- title: "Math Operations",
83
- x: 50,
84
- y: 50,
85
- width: 300,
86
- height: 300,
87
- color: "#4a5568",
88
- members: [node1.id, node2.id],
89
- });
90
-
91
- start();
92
-
93
- ````
94
-
95
- ---
96
-
97
- ## 📦 Group Management
98
-
99
- HTML-overlay-Node supports organizing nodes into hierarchical groups for better organization.
100
-
101
- ### Creating Groups
102
-
103
- ```javascript
104
- const { addGroup } = editor;
105
-
106
- // Create a group
107
- const group = addGroup({
108
- title: "My Group", // Group name
109
- x: 0, // X position
110
- y: 0, // Y position
111
- width: 400, // Width (min: 100)
112
- height: 300, // Height (min: 60)
113
- color: "#2d3748", // Background color
114
- members: [node1.id, node2.id], // Nodes to include
115
- });
116
- ````
117
-
118
- ### Group Features
119
-
120
- - **Hierarchical Structure**: Groups can contain multiple nodes
121
- - **Automatic Movement**: Nodes inside the group move with the group
122
- - **Resizable**: Resize groups using the handle in the bottom-right corner
123
- - **Custom Colors**: Set group colors for visual organization
124
- - **Local Coordinates**: World and local coordinates automatically managed
125
-
126
- ### Advanced Group Operations
127
-
128
- ```javascript
129
- // Access GroupManager
130
- const groupManager = graph.groupManager;
131
-
132
- // Reparent a node to a group
133
- graph.reparent(node, group);
134
-
135
- // Remove node from group (reparent to root)
136
- graph.reparent(node, null);
137
-
138
- // Resize a group
139
- groupManager.resizeGroup(group.id, 50, 50); // add 50px to width and height
140
-
141
- // Remove a group (children are un-grouped)
142
- groupManager.removeGroup(group.id);
143
-
144
- // Listen to group events
145
- hooks.on("group:change", () => {
146
- console.log("Group structure changed");
147
- });
148
- ```
149
-
150
- ---
151
-
152
- ## 🌐 HTML Overlays
153
-
154
- Create interactive HTML UIs inside nodes.
155
-
156
- ### Basic Example
157
-
158
- ```javascript
159
- registry.register("ui/TextInput", {
160
- title: "Text Input",
161
- size: { w: 220, h: 100 },
162
- outputs: [{ name: "text", datatype: "string" }],
163
-
164
- html: {
165
- init(node, el, { header, body }) {
166
- el.style.backgroundColor = "#1a1a1a";
167
- el.style.borderRadius = "8px";
168
-
169
- const input = document.createElement("input");
170
- input.type = "text";
171
- input.placeholder = "Enter text...";
172
- Object.assign(input.style, {
173
- width: "100%",
174
- padding: "8px",
175
- background: "#111",
176
- border: "1px solid #444",
177
- color: "#fff",
178
- pointerEvents: "auto", // IMPORTANT: Enable interaction
179
- });
180
-
181
- input.addEventListener("input", (e) => {
182
- node.state.text = e.target.value;
183
- hooks.emit("node:updated", node);
184
- });
185
-
186
- input.addEventListener("mousedown", (e) => e.stopPropagation()); // Prevent drag
187
-
188
- body.appendChild(input);
189
- el._input = input;
190
- },
191
-
192
- update(node, el, { selected }) {
193
- el.style.borderColor = selected ? "#3b82f6" : "#333";
194
- if (el._input.value !== (node.state.text || "")) {
195
- el._input.value = node.state.text || "";
196
- }
197
- },
198
- },
199
-
200
- onCreate(node) {
201
- node.state.text = "";
202
- },
203
-
204
- onExecute(node, { setOutput }) {
205
- setOutput("text", node.state.text || "");
206
- },
32
+ enablePropertyPanel: true
207
33
  });
208
- ```
209
-
210
- ### Best Practices
211
-
212
- 1. **Enable Interaction**: Set `pointerEvents: "auto"` on interactive elements
213
- 2. **Stop Propagation**: Prevent canvas drag with `e.stopPropagation()` on `mousedown`
214
- 3. **Update State**: Emit `"node:updated"` when state changes
215
- 4. **Store References**: Cache DOM elements in `el._refs` for performance
216
- 5. **Port Visibility**: HTML overlays are rendered below ports for proper visibility
217
-
218
- ---
219
-
220
- ## 🔌 Port Types
221
-
222
- HTML-overlay-Node supports two types of ports for different purposes:
223
-
224
- ### Exec Ports (Flow Control)
225
-
226
- Exec ports control the execution flow between nodes.
227
-
228
- ```javascript
229
- registry.register("util/Print", {
230
- title: "Print",
231
- inputs: [
232
- { name: "exec", portType: "exec" }, // Execution input
233
- { name: "value", portType: "data", datatype: "any" },
234
- ],
235
- outputs: [
236
- { name: "exec", portType: "exec" }, // Execution output
237
- ],
238
- onExecute(node, { getInput }) {
239
- console.log("[Print]", getInput("value"));
240
- },
241
- });
242
- ```
243
34
 
244
- ### Data Ports (Values)
35
+ const { graph, registry, start } = editor;
245
36
 
246
- Data ports transfer values between nodes.
247
-
248
- ```javascript
37
+ // 노드 등록 예시 (컬러 코드 포함)
249
38
  registry.register("math/Add", {
250
39
  title: "Add",
40
+ color: "#f43f5e", // 연산 노드는 로즈 핑크
41
+ size: { w: 180, h: 80 },
251
42
  inputs: [
252
- { name: "exec", portType: "exec" },
253
- { name: "a", portType: "data", datatype: "number" },
254
- { name: "b", portType: "data", datatype: "number" },
255
- ],
256
- outputs: [
257
- { name: "exec", portType: "exec" },
258
- { name: "result", portType: "data", datatype: "number" },
43
+ { name: "a", datatype: "number" },
44
+ { name: "b", datatype: "number" },
259
45
  ],
46
+ outputs: [{ name: "result", datatype: "number" }],
260
47
  onExecute(node, { getInput, setOutput }) {
261
- const result = (getInput("a") ?? 0) + (getInput("b") ?? 0);
262
- setOutput("result", result);
48
+ const a = getInput("a") ?? 0;
49
+ const b = getInput("b") ?? 0;
50
+ setOutput("result", a + b);
263
51
  },
264
52
  });
265
- ```
266
-
267
- ### Visual Style
268
-
269
- - **Exec ports**: Emerald green rounded squares (8×8px)
270
- - **Data ports**: Indigo blue circles (10px diameter)
271
- - Both have subtle outlines for depth
272
-
273
- ---
274
-
275
- ## ⌨️ Keyboard Shortcuts
276
-
277
- | Shortcut | Action |
278
- | ------------------------------- | --------------------------- |
279
- | **Selection** | |
280
- | `Click` | Select node |
281
- | `Shift + Click` | Add to selection |
282
- | `Ctrl + Drag` | Box select |
283
- | **Editing** | |
284
- | `Delete` | Delete selected nodes |
285
- | `Ctrl + Z` | Undo |
286
- | `Ctrl + Y` / `Ctrl + Shift + Z` | Redo |
287
- | **Grouping** | |
288
- | `Ctrl + G` | Create group from selection |
289
- | **Alignment** | |
290
- | `A` | Align nodes horizontally |
291
- | `Shift + A` | Align nodes vertically |
292
- | **Tools** | |
293
- | `G` | Toggle snap-to-grid |
294
- | `?` | Toggle shortcuts help |
295
- | **Navigation** | |
296
- | `Middle Click + Drag` | Pan canvas |
297
- | `Mouse Wheel` | Zoom in/out |
298
- | `Right Click` | Context menu |
299
53
 
300
- ---
301
-
302
- ## 📚 Complete API
303
-
304
- For full API documentation, see the comments in [src/index.js](src/index.js).
305
-
306
- ### Editor API
307
-
308
- | Property | Description |
309
- | ------------------- | --------------------- |
310
- | `graph` | Graph instance |
311
- | `registry` | Node type registry |
312
- | `hooks` | Event system |
313
- | `render()` | Trigger manual render |
314
- | `start()` | Start execution loop |
315
- | `stop()` | Stop execution loop |
316
- | `addGroup(options)` | Create a group |
317
- | `destroy()` | Cleanup |
318
-
319
- ### Key Methods
320
-
321
- - `registry.register(type, definition)` - Register node type
322
- - `graph.addNode(type, options)` - Create node
323
- - `graph.addEdge(from, fromPort, to, toPort)` - Connect nodes
324
- - `graph.toJSON()` / `graph.fromJSON(json)` - Serialize/deserialize
325
- - `hooks.on(event, callback)` - Subscribe to events
326
-
327
- ### Available Events
328
-
329
- - `node:create` | `node:move` | `node:resize` | `node:updated`
330
- - `edge:create` | `edge:delete`
331
- - `group:change`
332
- - `runner:start` | `runner:stop` | `runner:tick`
333
- - `error`
54
+ start();
55
+ ```
334
56
 
335
57
  ---
336
58
 
337
- ## 🎨 Customization
59
+ ## 🎨 디자인 커스터마이징
338
60
 
339
- ### Theme Colors
61
+ 다크 모드를 기본으로 하지만, 프로젝트 테마에 맞춰 모든 색상을 조정할 수 있습니다.
340
62
 
341
63
  ```javascript
342
- const editor = createGraphEditor(canvas, {
64
+ const editor = createGraphEditor("#container", {
343
65
  theme: {
344
- bg: "#0d0d0f", // Canvas background
345
- grid: "#1a1a1d", // Grid lines
346
- node: "#16161a", // Node background
347
- nodeBorder: "#2a2a2f", // Node border
348
- title: "#1f1f24", // Node header
349
- text: "#e4e4e7", // Primary text
350
- textMuted: "#a1a1aa", // Secondary text
351
- port: "#6366f1", // Data port color (indigo)
352
- portExec: "#10b981", // Exec port color (emerald)
353
- edge: "#52525b", // Edge color
354
- edgeActive: "#8b5cf6", // Active edge (purple)
355
- accent: "#6366f1", // Accent color
356
- accentBright: "#818cf8", // Bright accent
357
- },
66
+ bg: "#0d0d0f",
67
+ accent: "#6366f1", // 메인 포인트 컬러
68
+ node: "#16161a", // 노드 배경
69
+ title: "#1f1f24" // 노드 헤더
70
+ }
358
71
  });
359
72
  ```
360
73
 
361
- ### Edge Styles
362
-
363
- ```javascript
364
- // Set edge style
365
- editor.renderer.setEdgeStyle("orthogonal"); // or "curved", "line"
366
- ```
74
+ ---
367
75
 
368
- ### Custom Node Drawing
76
+ ## ⌨️ 생산성을 높여주는 단축키
369
77
 
370
- ```javascript
371
- registry.register("visual/Circle", {
372
- title: "Circle",
373
- size: { w: 120, h: 120 },
374
- onDraw(node, { ctx, theme }) {
375
- const { x, y, width, height } = node.computed;
376
- const centerX = x + width / 2;
377
- const centerY = y + height / 2;
378
- const radius = Math.min(width, height) / 3;
379
-
380
- ctx.beginPath();
381
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
382
- ctx.fillStyle = theme.wire;
383
- ctx.fill();
384
- },
385
- });
386
- ```
78
+ | 기능 | 단축키 |
79
+ | :--- | :--- |
80
+ | **노드 삭제** | `Delete` |
81
+ | **수평 정렬** | `A` |
82
+ | **수직 정렬** | `Shift + A` |
83
+ | **그룹 생성** | `Ctrl + G` |
84
+ | **그리드 스냅**| `G` |
85
+ | **되돌리기** | `Ctrl + Z` / `Ctrl + Y` |
86
+ | **영역 선택** | `Ctrl + Drag` |
387
87
 
388
88
  ---
389
89
 
390
- ## 💾 Serialization
90
+ ## 💾 데이터 다루기
91
+
92
+ 그래프 통째로 JSON으로 뽑거나, 저장된 데이터를 불러오는 것도 간단합니다.
391
93
 
392
94
  ```javascript
393
- // Save
394
- const json = graph.toJSON();
395
- localStorage.setItem("myGraph", JSON.stringify(json));
95
+ // 현재 그래프 저장
96
+ const data = graph.toJSON();
396
97
 
397
- // Load
398
- const saved = JSON.parse(localStorage.getItem("myGraph"));
399
- graph.fromJSON(saved);
98
+ // 데이터 불러오기
99
+ graph.fromJSON(data);
400
100
  ```
401
101
 
402
102
  ---
403
103
 
404
- ## 🐛 Troubleshooting
405
-
406
- | Issue | Solution |
407
- | ---------------------------- | --------------------------------------------------- |
408
- | Canvas not rendering | Ensure canvas has explicit width/height |
409
- | Nodes not executing | Call `start()` or set `autorun: true` |
410
- | Type errors | Register node types before using them |
411
- | HTML overlay not interactive | Set `pointerEvents: "auto"` on elements |
412
- | Performance issues | Limit to <1000 nodes, optimize `onExecute`/`onDraw` |
104
+ ## 🔗 관련 링크
105
+ - [GitHub Repository](https://github.com/cheonghakim/html-overlay-node)
106
+ - [Issue Tracker](https://github.com/cheonghakim/html-overlay-node/issues)
413
107
 
414
108
  ---
415
109
 
416
- ## 🤝 Contributing
417
-
418
- Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
110
+ ## 📄 라이선스
111
+ [MIT](LICENSE) © cheonghakim
112
+ G.md).
419
113
 
420
114
  ```bash
421
115
  npm install # Install dependencies
package/src/core/Edge.js CHANGED
@@ -16,8 +16,10 @@ export class Edge {
16
16
  * @param {string} options.toPort - Target port ID
17
17
  */
18
18
  constructor({ id, fromNode, fromPort, toNode, toPort }) {
19
- if (!fromNode || !fromPort || !toNode || !toPort) {
20
- throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
19
+ // Allow empty strings for port names (exec ports use empty names)
20
+ // Only check for null/undefined
21
+ if (fromNode == null || fromPort == null || toNode == null || toPort == null) {
22
+ throw new Error("Edge requires fromNode, fromPort, toNode, and toPort (null/undefined not allowed)");
21
23
  }
22
24
  this.id = id ?? randomUUID();
23
25
  this.fromNode = fromNode;
package/src/core/Graph.js CHANGED
@@ -51,11 +51,13 @@ export class Graph {
51
51
  const available = Array.from(this.registry.types.keys()).join(", ") || "none";
52
52
  throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
53
53
  }
54
+ const height = opts.height || def.size?.h || this._calculateDefaultNodeHeight(def);
55
+
54
56
  const node = new Node({
55
57
  type,
56
58
  title: def.title,
57
- width: def.size?.w,
58
- height: def.size?.h,
59
+ width: opts.width || def.size?.w || 140,
60
+ height,
59
61
  ...opts,
60
62
  });
61
63
  for (const i of def.inputs || []) node.addInput(i.name, i.datatype, i.portType || "data");
@@ -212,11 +214,17 @@ export class Graph {
212
214
  this.hooks?.emit("graph:serialize", json);
213
215
  return json;
214
216
  }
217
+
215
218
  fromJSON(json) {
216
- this.clear();
219
+ this.nodes.clear();
220
+ this.edges.clear();
217
221
 
218
222
  // Restore nodes first
219
223
  for (const nd of json.nodes) {
224
+ const def = this.registry?.types?.get(nd.type);
225
+ const minH = def ? this._calculateDefaultNodeHeight(def) : 60;
226
+ const height = nd.h !== undefined ? nd.h : minH;
227
+
220
228
  const node = new Node({
221
229
  id: nd.id,
222
230
  type: nd.type,
@@ -224,10 +232,10 @@ export class Graph {
224
232
  x: nd.x,
225
233
  y: nd.y,
226
234
  width: nd.w,
227
- height: nd.h,
235
+ height: height,
228
236
  });
237
+
229
238
  // Call onCreate to initialize node with defaults first
230
- const def = this.registry?.types?.get(nd.type);
231
239
  if (def?.onCreate) {
232
240
  def.onCreate(node);
233
241
  }
@@ -264,4 +272,20 @@ export class Graph {
264
272
 
265
273
  return this;
266
274
  }
275
+
276
+ _calculateDefaultNodeHeight(def) {
277
+ const inCount = def.inputs?.length || 0;
278
+ const outCount = def.outputs?.length || 0;
279
+ const maxPorts = Math.max(inCount, outCount);
280
+ const headerHeight = 26;
281
+ const padding = 8; // tighter buffer
282
+ const portSpacing = 20; // compact spacing
283
+
284
+ let h = headerHeight + padding + (maxPorts * portSpacing) + padding;
285
+
286
+ // Add extra space if it has HTML overlay to prevent overlap with ports
287
+ if (def.html) h += 16;
288
+
289
+ return Math.max(h, 40); // Minimum height 40
290
+ }
267
291
  }
package/src/core/Node.js CHANGED
@@ -43,28 +43,44 @@ export class Node {
43
43
  * @param {string} [portType="data"] - Port type: "exec" or "data"
44
44
  * @returns {Object} The created port
45
45
  */
46
+ /**
47
+ * Recalculate minimum size based on ports
48
+ */
49
+ _updateMinSize() {
50
+ const HEADER_HEIGHT = 28;
51
+ const PORT_SPACING = 24;
52
+ const BOTTOM_PADDING = 10;
53
+
54
+ // Calculate required height for inputs and outputs
55
+ const inHeight = HEADER_HEIGHT + 10 + this.inputs.length * PORT_SPACING + BOTTOM_PADDING;
56
+ const outHeight = HEADER_HEIGHT + 10 + this.outputs.length * PORT_SPACING + BOTTOM_PADDING;
57
+
58
+ const minHeight = Math.max(inHeight, outHeight, 60); // Minimum 60px base
59
+
60
+ if (this.size.height < minHeight) {
61
+ this.size.height = minHeight;
62
+ }
63
+ }
64
+
46
65
  addInput(name, datatype = "any", portType = "data") {
47
- if (!name || typeof name !== "string") {
48
- throw new Error("Input port name must be a non-empty string");
66
+ // ... existing validation ...
67
+ if (typeof name !== "string" || (portType === "data" && !name)) {
68
+ throw new Error("Input port name must be a string (non-empty for data ports)");
49
69
  }
50
70
  const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
51
71
  this.inputs.push(port);
72
+ this._updateMinSize();
52
73
  return port;
53
74
  }
54
75
 
55
- /**
56
- * Add an output port to this node
57
- * @param {string} name - Port name
58
- * @param {string} [datatype="any"] - Data type for the port
59
- * @param {string} [portType="data"] - Port type: "exec" or "data"
60
- * @returns {Object} The created port
61
- */
62
76
  addOutput(name, datatype = "any", portType = "data") {
63
- if (!name || typeof name !== "string") {
64
- throw new Error("Output port name must be a non-empty string");
77
+ // ... existing validation ...
78
+ if (typeof name !== "string" || (portType === "data" && !name)) {
79
+ throw new Error("Output port name must be a string (non-empty for data ports)");
65
80
  }
66
81
  const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
67
82
  this.outputs.push(port);
83
+ this._updateMinSize();
68
84
  return port;
69
85
  }
70
86
  }