@flowdrop/flowdrop 1.2.1 → 1.3.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.
@@ -2,9 +2,11 @@
2
2
  Simple Node Component
3
3
  A simple node with optional input and output ports
4
4
  Styled with BEM syntax
5
-
5
+
6
6
  UI Extensions Support:
7
- - hideUnconnectedHandles: Hides trigger ports that are not connected to reduce visual clutter
7
+ - hideUnconnectedHandles: Hides ports that are not connected to reduce visual clutter
8
+ - hiddenPorts: Manually hide individual ports (visual-only, no effect on execution)
9
+ - portOrder: Reorder ports by ID array (unspecified ports appear at end in original order)
8
10
  -->
9
11
 
10
12
  <script lang="ts">
@@ -21,6 +23,11 @@
21
23
  getCategoryColorToken,
22
24
  } from "../../utils/colors.js";
23
25
  import { getConnectedHandles } from "../../stores/workflowStore.svelte.js";
26
+ import {
27
+ applyPortOrder,
28
+ getPortTop,
29
+ isPortVisible,
30
+ } from "../../utils/portUtils.js";
24
31
  import CogIcon from "../icons/CogIcon.svelte";
25
32
  import AlertCircleIcon from "../icons/AlertCircleIcon.svelte";
26
33
 
@@ -43,15 +50,25 @@
43
50
  }>();
44
51
 
45
52
  /**
46
- * Get the hideUnconnectedHandles setting from extensions
47
- * Merges node type defaults with instance overrides
53
+ * Get UI extension settings from extensions, merging node type defaults with instance overrides.
48
54
  */
49
- const hideUnconnectedHandles = $derived(() => {
50
- const typeDefault =
51
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
52
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
53
- return instanceOverride ?? typeDefault;
54
- });
55
+ const hideUnconnectedHandles = $derived(
56
+ props.data.extensions?.ui?.hideUnconnectedHandles ??
57
+ props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
58
+ false,
59
+ );
60
+
61
+ const hiddenPorts = $derived(
62
+ props.data.extensions?.ui?.hiddenPorts ??
63
+ props.data.metadata?.extensions?.ui?.hiddenPorts ??
64
+ {},
65
+ );
66
+
67
+ const portOrder = $derived(
68
+ props.data.extensions?.ui?.portOrder ??
69
+ props.data.metadata?.extensions?.ui?.portOrder ??
70
+ {},
71
+ );
55
72
 
56
73
  // Prioritize metadata icon over config icon for simple nodes (metadata is the node definition)
57
74
  let nodeIcon = $derived(
@@ -117,110 +134,67 @@
117
134
  }
118
135
 
119
136
  /**
120
- * Check if a port is connected
121
- * @param portId - The port ID to check
122
- * @param type - Whether this is an 'input' or 'output' port
123
- * @returns true if the port is connected
137
+ * All visible input ports in user-defined order.
124
138
  */
125
- function isPortConnected(portId: string, type: "input" | "output"): boolean {
126
- const handleId = `${props.data.nodeId}-${type}-${portId}`;
127
- return getConnectedHandles().has(handleId);
128
- }
129
-
130
- /**
131
- * Check if a trigger port should be visible
132
- * Always shows if hideUnconnectedHandles is disabled or if port is connected
133
- */
134
- function shouldShowTriggerPort(
135
- portId: string,
136
- type: "input" | "output",
137
- ): boolean {
138
- if (!hideUnconnectedHandles()) {
139
- return true;
140
- }
141
- return isPortConnected(portId, type);
142
- }
143
-
144
- // Get first input/output ports for simple node representation
145
- // Special handling for trigger ports - they should always be shown if present
146
- let triggerInputPort = $derived(
147
- props.data.metadata?.inputs?.find(
148
- (port: NodePort) => port.dataType === "trigger",
149
- ),
150
- );
151
- let triggerOutputPort = $derived(
152
- props.data.metadata?.outputs?.find(
153
- (port: NodePort) => port.dataType === "trigger",
139
+ const visibleInputPorts = $derived(
140
+ applyPortOrder(props.data.metadata?.inputs ?? [], portOrder.inputs).filter(
141
+ (p: NodePort) =>
142
+ isPortVisible(
143
+ p,
144
+ "input",
145
+ hiddenPorts,
146
+ hideUnconnectedHandles,
147
+ getConnectedHandles(),
148
+ props.data.nodeId,
149
+ ),
154
150
  ),
155
151
  );
156
152
 
157
- // Get first non-trigger ports for data connections
158
- let firstConnectedDataInputPort = $derived(
159
- props.data.metadata?.inputs?.find(
160
- (port: NodePort) =>
161
- port.dataType !== "trigger" && isPortConnected(port.id, "input"),
162
- ),
163
- );
164
-
165
- let firstDataInputPort = $derived(
166
- props.data.metadata?.inputs?.find(
167
- (port: NodePort) => port.dataType !== "trigger",
153
+ /**
154
+ * All visible output ports in user-defined order.
155
+ */
156
+ const visibleOutputPorts = $derived(
157
+ applyPortOrder(
158
+ props.data.metadata?.outputs ?? [],
159
+ portOrder.outputs,
160
+ ).filter((p: NodePort) =>
161
+ isPortVisible(
162
+ p,
163
+ "output",
164
+ hiddenPorts,
165
+ hideUnconnectedHandles,
166
+ getConnectedHandles(),
167
+ props.data.nodeId,
168
+ ),
168
169
  ),
169
170
  );
170
171
 
171
- let firstConnectedDataOutputPort = $derived(
172
- props.data.metadata?.outputs?.find(
173
- (port: NodePort) =>
174
- port.dataType !== "trigger" && isPortConnected(port.id, "output"),
175
- ),
176
- );
177
- let firstDataOutputPort = $derived(
178
- props.data.metadata?.outputs?.find(
179
- (port: NodePort) => port.dataType !== "trigger",
180
- ),
172
+ /**
173
+ * Dynamic node min-height so handles never render outside the node body.
174
+ */
175
+ const nodeMinHeight = $derived(
176
+ (() => {
177
+ const maxPorts = Math.max(
178
+ visibleInputPorts.length,
179
+ visibleOutputPorts.length,
180
+ 1,
181
+ );
182
+ return maxPorts <= 1 ? 80 : 20 + maxPorts * 40;
183
+ })(),
181
184
  );
182
-
183
- let inputPorts = $derived.by(() => {
184
- return [
185
- ...(firstConnectedDataInputPort
186
- ? [firstConnectedDataInputPort]
187
- : firstDataInputPort
188
- ? [firstDataInputPort]
189
- : []),
190
- ...(triggerInputPort &&
191
- shouldShowTriggerPort(triggerInputPort.id, "input")
192
- ? [triggerInputPort]
193
- : []),
194
- ];
195
- });
196
- let outputPorts = $derived.by(() => {
197
- return [
198
- ...(firstConnectedDataOutputPort
199
- ? [firstConnectedDataOutputPort]
200
- : firstDataOutputPort
201
- ? [firstDataOutputPort]
202
- : []),
203
- ...(triggerOutputPort &&
204
- shouldShowTriggerPort(triggerOutputPort.id, "output")
205
- ? [triggerOutputPort]
206
- : []),
207
- ];
208
- });
209
185
  </script>
210
186
 
211
- <!-- Input Handles: center at 20/40/60px (multiple of 10), 20px connection area -->
212
- {#each inputPorts as port, index}
187
+ <!-- Input Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
188
+ {#each visibleInputPorts as port, index}
213
189
  <Handle
214
190
  type="target"
215
191
  position={Position.Left}
216
192
  style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
217
193
  port.dataType,
218
- )}); --fd-handle-border-color: var(--fd-handle-border); top: {inputPorts.length >
219
- 1
220
- ? index === 0
221
- ? 20
222
- : 60
223
- : 40}px; transform: translateY(-50%); z-index: 30;"
194
+ )}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
195
+ index,
196
+ visibleInputPorts.length,
197
+ )}px; transform: translateY(-50%); z-index: 30;"
224
198
  id={`${props.data.nodeId}-input-${port.id}`}
225
199
  />
226
200
  {/each}
@@ -231,6 +205,7 @@
231
205
  class:flowdrop-simple-node--selected={props.selected}
232
206
  class:flowdrop-simple-node--processing={props.isProcessing}
233
207
  class:flowdrop-simple-node--error={props.isError}
208
+ style="min-height: {nodeMinHeight}px"
234
209
  onclick={handleClick}
235
210
  ondblclick={handleDoubleClick}
236
211
  onkeydown={handleKeydown}
@@ -290,19 +265,17 @@
290
265
  </button>
291
266
  </div>
292
267
 
293
- <!-- Output Handles: center at 20/40/60px (multiple of 10), 20px connection area -->
294
- {#each outputPorts as port, index}
268
+ <!-- Output Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
269
+ {#each visibleOutputPorts as port, index}
295
270
  <Handle
296
271
  type="source"
297
272
  position={Position.Right}
298
273
  style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
299
274
  port.dataType,
300
- )}); --fd-handle-border-color: var(--fd-handle-border); top: {outputPorts.length >
301
- 1
302
- ? index === 0
303
- ? 20
304
- : 60
305
- : 40}px; transform: translateY(-50%); z-index: 30;"
275
+ )}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
276
+ index,
277
+ visibleOutputPorts.length,
278
+ )}px; transform: translateY(-50%); z-index: 30;"
306
279
  id={`${props.data.nodeId}-output-${port.id}`}
307
280
  />
308
281
  {/each}
@@ -2,9 +2,11 @@
2
2
  Square Node Component
3
3
  A simple square node with optional input and output ports
4
4
  Styled with BEM syntax
5
-
5
+
6
6
  UI Extensions Support:
7
- - hideUnconnectedHandles: Hides trigger ports that are not connected to reduce visual clutter
7
+ - hideUnconnectedHandles: Hides ports that are not connected to reduce visual clutter
8
+ - hiddenPorts: Manually hide individual ports (visual-only, no effect on execution)
9
+ - portOrder: Reorder ports by ID array (unspecified ports appear at end in original order)
8
10
  -->
9
11
 
10
12
  <script lang="ts">
@@ -22,6 +24,11 @@
22
24
  } from "../../utils/colors.js";
23
25
  import { getNodeIcon } from "../../utils/icons.js";
24
26
  import { getConnectedHandles } from "../../stores/workflowStore.svelte.js";
27
+ import {
28
+ applyPortOrder,
29
+ getPortTop,
30
+ isPortVisible,
31
+ } from "../../utils/portUtils.js";
25
32
  import CogIcon from "../icons/CogIcon.svelte";
26
33
  import AlertCircleIcon from "../icons/AlertCircleIcon.svelte";
27
34
 
@@ -44,15 +51,25 @@
44
51
  }>();
45
52
 
46
53
  /**
47
- * Get the hideUnconnectedHandles setting from extensions
48
- * Merges node type defaults with instance overrides
54
+ * Get UI extension settings from extensions, merging node type defaults with instance overrides.
49
55
  */
50
- const hideUnconnectedHandles = $derived(() => {
51
- const typeDefault =
52
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
53
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
54
- return instanceOverride ?? typeDefault;
55
- });
56
+ const hideUnconnectedHandles = $derived(
57
+ props.data.extensions?.ui?.hideUnconnectedHandles ??
58
+ props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
59
+ false,
60
+ );
61
+
62
+ const hiddenPorts = $derived(
63
+ props.data.extensions?.ui?.hiddenPorts ??
64
+ props.data.metadata?.extensions?.ui?.hiddenPorts ??
65
+ {},
66
+ );
67
+
68
+ const portOrder = $derived(
69
+ props.data.extensions?.ui?.portOrder ??
70
+ props.data.metadata?.extensions?.ui?.portOrder ??
71
+ {},
72
+ );
56
73
 
57
74
  /**
58
75
  * Get icon using the same resolution as WorkflowNode
@@ -101,110 +118,68 @@
101
118
  }
102
119
  }
103
120
  /**
104
- * Check if a port is connected
105
- * @param portId - The port ID to check
106
- * @param type - Whether this is an 'input' or 'output' port
107
- * @returns true if the port is connected
121
+ * All visible input ports in user-defined order.
108
122
  */
109
- function isPortConnected(portId: string, type: "input" | "output"): boolean {
110
- const handleId = `${props.data.nodeId}-${type}-${portId}`;
111
- return getConnectedHandles().has(handleId);
112
- }
113
-
114
- /**
115
- * Check if a trigger port should be visible
116
- * Always shows if hideUnconnectedHandles is disabled or if port is connected
117
- */
118
- function shouldShowTriggerPort(
119
- portId: string,
120
- type: "input" | "output",
121
- ): boolean {
122
- if (!hideUnconnectedHandles()) {
123
- return true;
124
- }
125
- return isPortConnected(portId, type);
126
- }
127
-
128
- // Get first input/output ports for square node representation
129
- // Special handling for trigger ports - they should always be shown if present
130
- let triggerInputPort = $derived(
131
- props.data.metadata?.inputs?.find(
132
- (port: NodePort) => port.dataType === "trigger",
133
- ),
134
- );
135
- let triggerOutputPort = $derived(
136
- props.data.metadata?.outputs?.find(
137
- (port: NodePort) => port.dataType === "trigger",
123
+ const visibleInputPorts = $derived(
124
+ applyPortOrder(props.data.metadata?.inputs ?? [], portOrder.inputs).filter(
125
+ (p: NodePort) =>
126
+ isPortVisible(
127
+ p,
128
+ "input",
129
+ hiddenPorts,
130
+ hideUnconnectedHandles,
131
+ getConnectedHandles(),
132
+ props.data.nodeId,
133
+ ),
138
134
  ),
139
135
  );
140
136
 
141
- // Get first non-trigger ports for data connections
142
- let firstConnectedDataInputPort = $derived(
143
- props.data.metadata?.inputs?.find(
144
- (port: NodePort) =>
145
- port.dataType !== "trigger" && isPortConnected(port.id, "input"),
146
- ),
147
- );
148
-
149
- let firstDataInputPort = $derived(
150
- props.data.metadata?.inputs?.find(
151
- (port: NodePort) => port.dataType !== "trigger",
137
+ /**
138
+ * All visible output ports in user-defined order.
139
+ */
140
+ const visibleOutputPorts = $derived(
141
+ applyPortOrder(
142
+ props.data.metadata?.outputs ?? [],
143
+ portOrder.outputs,
144
+ ).filter((p: NodePort) =>
145
+ isPortVisible(
146
+ p,
147
+ "output",
148
+ hiddenPorts,
149
+ hideUnconnectedHandles,
150
+ getConnectedHandles(),
151
+ props.data.nodeId,
152
+ ),
152
153
  ),
153
154
  );
154
155
 
155
- let firstConnectedDataOutputPort = $derived(
156
- props.data.metadata?.outputs?.find(
157
- (port: NodePort) =>
158
- port.dataType !== "trigger" && isPortConnected(port.id, "output"),
159
- ),
160
- );
161
- let firstDataOutputPort = $derived(
162
- props.data.metadata?.outputs?.find(
163
- (port: NodePort) => port.dataType !== "trigger",
164
- ),
156
+ /**
157
+ * Dynamic node size so handles never render outside the node body.
158
+ * Overrides the fixed CSS height/width when more than 2 ports are visible on either side.
159
+ */
160
+ const nodeSize = $derived(
161
+ (() => {
162
+ const maxPorts = Math.max(
163
+ visibleInputPorts.length,
164
+ visibleOutputPorts.length,
165
+ 1,
166
+ );
167
+ return maxPorts <= 1 ? 80 : 20 + maxPorts * 40;
168
+ })(),
165
169
  );
166
-
167
- let inputPorts = $derived.by(() => {
168
- return [
169
- ...(firstConnectedDataInputPort
170
- ? [firstConnectedDataInputPort]
171
- : firstDataInputPort
172
- ? [firstDataInputPort]
173
- : []),
174
- ...(triggerInputPort &&
175
- shouldShowTriggerPort(triggerInputPort.id, "input")
176
- ? [triggerInputPort]
177
- : []),
178
- ];
179
- });
180
- let outputPorts = $derived.by(() => {
181
- return [
182
- ...(firstConnectedDataOutputPort
183
- ? [firstConnectedDataOutputPort]
184
- : firstDataOutputPort
185
- ? [firstDataOutputPort]
186
- : []),
187
- ...(triggerOutputPort &&
188
- shouldShowTriggerPort(triggerOutputPort.id, "output")
189
- ? [triggerOutputPort]
190
- : []),
191
- ];
192
- });
193
170
  </script>
194
171
 
195
- <!-- Input Handles: center at 20/40/60px (multiple of 10), 20px connection area -->
196
- {#each inputPorts as port, index}
172
+ <!-- Input Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
173
+ {#each visibleInputPorts as port, index}
197
174
  <Handle
198
175
  type="target"
199
176
  position={Position.Left}
200
177
  style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
201
178
  port.dataType,
202
- )}); --fd-handle-border-color: var(--fd-handle-border); top: {inputPorts.length >
203
- 1
204
- ? index === 0
205
- ? 20
206
- : 60
207
- : 40}px; transform: translateY(-50%); z-index: 30;"
179
+ )}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
180
+ index,
181
+ visibleInputPorts.length,
182
+ )}px; transform: translateY(-50%); z-index: 30;"
208
183
  id={`${props.data.nodeId}-input-${port.id}`}
209
184
  />
210
185
  {/each}
@@ -215,6 +190,7 @@
215
190
  class:flowdrop-square-node--selected={props.selected}
216
191
  class:flowdrop-square-node--processing={props.isProcessing}
217
192
  class:flowdrop-square-node--error={props.isError}
193
+ style="height: {nodeSize}px; width: {nodeSize}px"
218
194
  onclick={handleClick}
219
195
  ondblclick={handleDoubleClick}
220
196
  onkeydown={handleKeydown}
@@ -261,19 +237,17 @@
261
237
  </button>
262
238
  </div>
263
239
 
264
- <!-- Output Handles: center at 20/40/60px (multiple of 10), 20px connection area -->
265
- {#each outputPorts as port, index}
240
+ <!-- Output Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
241
+ {#each visibleOutputPorts as port, index}
266
242
  <Handle
267
243
  type="source"
268
244
  position={Position.Right}
269
245
  style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
270
246
  port.dataType,
271
- )}); --fd-handle-border-color: var(--fd-handle-border); top: {outputPorts.length >
272
- 1
273
- ? index === 0
274
- ? 20
275
- : 60
276
- : 40}px; transform: translateY(-50%); z-index: 30;"
247
+ )}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
248
+ index,
249
+ visibleOutputPorts.length,
250
+ )}px; transform: translateY(-50%); z-index: 30;"
277
251
  id={`${props.data.nodeId}-output-${port.id}`}
278
252
  />
279
253
  {/each}
@@ -155,18 +155,17 @@
155
155
  * Get the hideUnconnectedHandles setting from extensions
156
156
  * Merges node type defaults with instance overrides
157
157
  */
158
- const hideUnconnectedHandles = $derived(() => {
159
- const typeDefault =
160
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
161
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
162
- return instanceOverride ?? typeDefault;
163
- });
158
+ const hideUnconnectedHandles = $derived(
159
+ props.data.extensions?.ui?.hideUnconnectedHandles ??
160
+ props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
161
+ false,
162
+ );
164
163
 
165
164
  /**
166
165
  * Check if a port should be visible based on connection state and settings
167
166
  */
168
167
  function isPortVisible(port: NodePort, type: "input" | "output"): boolean {
169
- if (!hideUnconnectedHandles()) {
168
+ if (!hideUnconnectedHandles) {
170
169
  return true;
171
170
  }
172
171
  if (port.required) {
@@ -6,6 +6,8 @@
6
6
 
7
7
  UI Extensions Support:
8
8
  - hideUnconnectedHandles: Hides ports that are not connected to reduce visual clutter
9
+ - portOrder: Visual-only reordering of input/output ports (no effect on execution)
10
+ - hiddenPorts: Manually hidden ports per direction (required ports cannot be hidden)
9
11
  -->
10
12
 
11
13
  <script lang="ts">
@@ -25,6 +27,7 @@
25
27
  getPortBackgroundColor,
26
28
  } from "../../utils/colors.js";
27
29
  import { getConnectedHandles } from "../../stores/workflowStore.svelte.js";
30
+ import { applyPortOrder } from "../../utils/portUtils.js";
28
31
 
29
32
  interface Props {
30
33
  data: WorkflowNode["data"] & {
@@ -64,12 +67,31 @@
64
67
  * Get the hideUnconnectedHandles setting from extensions
65
68
  * Merges node type defaults with instance overrides
66
69
  */
67
- const hideUnconnectedHandles = $derived(() => {
68
- const typeDefault =
69
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
70
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
71
- return instanceOverride ?? typeDefault;
72
- });
70
+ const hideUnconnectedHandles = $derived(
71
+ props.data.extensions?.ui?.hideUnconnectedHandles ??
72
+ props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
73
+ false,
74
+ );
75
+
76
+ /**
77
+ * Get the portOrder setting from extensions (visual-only, no effect on execution)
78
+ * Merges node type defaults with instance overrides
79
+ */
80
+ const portOrder = $derived(
81
+ props.data.extensions?.ui?.portOrder ??
82
+ props.data.metadata?.extensions?.ui?.portOrder ??
83
+ {},
84
+ );
85
+
86
+ /**
87
+ * Get the hiddenPorts setting from extensions (visual-only, no effect on execution)
88
+ * Merges node type defaults with instance overrides
89
+ */
90
+ const hiddenPorts = $derived(
91
+ props.data.extensions?.ui?.hiddenPorts ??
92
+ props.data.metadata?.extensions?.ui?.hiddenPorts ??
93
+ {},
94
+ );
73
95
 
74
96
  /**
75
97
  * Dynamic inputs from config - user-defined input ports
@@ -92,20 +114,26 @@
92
114
  );
93
115
 
94
116
  /**
95
- * Combined input ports: static metadata inputs + dynamic config inputs
117
+ * Combined input ports: static metadata inputs + dynamic config inputs,
118
+ * sorted by portOrder if set (visual-only)
96
119
  */
97
- const allInputPorts = $derived([
98
- ...props.data.metadata.inputs,
99
- ...dynamicInputs,
100
- ]);
120
+ const allInputPorts = $derived(
121
+ applyPortOrder(
122
+ [...props.data.metadata.inputs, ...dynamicInputs],
123
+ portOrder.inputs,
124
+ ),
125
+ );
101
126
 
102
127
  /**
103
- * Combined output ports: static metadata outputs + dynamic config outputs
128
+ * Combined output ports: static metadata outputs + dynamic config outputs,
129
+ * sorted by portOrder if set (visual-only)
104
130
  */
105
- const allOutputPorts = $derived([
106
- ...props.data.metadata.outputs,
107
- ...dynamicOutputs,
108
- ]);
131
+ const allOutputPorts = $derived(
132
+ applyPortOrder(
133
+ [...props.data.metadata.outputs, ...dynamicOutputs],
134
+ portOrder.outputs,
135
+ ),
136
+ );
109
137
 
110
138
  /**
111
139
  * Check if a port should be visible based on connection state and settings
@@ -114,8 +142,15 @@
114
142
  * @returns true if the port should be visible
115
143
  */
116
144
  function isPortVisible(port: NodePort, type: "input" | "output"): boolean {
145
+ // Manual hide takes precedence (required ports are prevented from being hidden in ConfigForm)
146
+ const manuallyHidden =
147
+ type === "input"
148
+ ? hiddenPorts.inputs?.includes(port.id)
149
+ : hiddenPorts.outputs?.includes(port.id);
150
+ if (manuallyHidden) return false;
151
+
117
152
  // Always show if hideUnconnectedHandles is disabled
118
- if (!hideUnconnectedHandles()) {
153
+ if (!hideUnconnectedHandles) {
119
154
  return true;
120
155
  }
121
156
 
@@ -245,7 +280,7 @@
245
280
  {#if visibleInputPorts.length > 0}
246
281
  <div class="flowdrop-workflow-node__ports">
247
282
  <div class="flowdrop-workflow-node__ports-list">
248
- {#each visibleInputPorts as port, inputIndex (port.id)}
283
+ {#each visibleInputPorts as port (port.id)}
249
284
  <div class="flowdrop-workflow-node__port">
250
285
  <!-- Input Handle: centered in row, at node edge (ports have no padding) -->
251
286
  <Handle
@@ -308,7 +343,7 @@
308
343
  {#if visibleOutputPorts.length > 0}
309
344
  <div class="flowdrop-workflow-node__ports">
310
345
  <div class="flowdrop-workflow-node__ports-list">
311
- {#each visibleOutputPorts as port, outputIndex (port.id)}
346
+ {#each visibleOutputPorts as port (port.id)}
312
347
  <div class="flowdrop-workflow-node__port">
313
348
  <!-- Port Info: padding lives here so handle position is simple -->
314
349
  <div