@d34dman/flowdrop 0.0.49 → 0.0.51

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.
@@ -140,6 +140,21 @@
140
140
  title: 'Fit View on Load',
141
141
  description: 'Automatically fit workflow to view when loading',
142
142
  default: true
143
+ },
144
+ proximityConnect: {
145
+ type: 'boolean',
146
+ title: 'Proximity Connect',
147
+ description:
148
+ 'Auto-connect compatible ports when dragging nodes near each other',
149
+ default: false
150
+ },
151
+ proximityConnectDistance: {
152
+ type: 'number',
153
+ title: 'Proximity Distance',
154
+ description: 'Distance threshold in pixels for proximity connect',
155
+ minimum: 50,
156
+ maximum: 500,
157
+ default: 150
143
158
  }
144
159
  }
145
160
  },
@@ -7,8 +7,7 @@
7
7
 
8
8
  <script lang="ts">
9
9
  import Icon from '@iconify/svelte';
10
- // Use settingsStore for theme (themeStore is deprecated)
11
- import { theme, resolvedTheme, cycleTheme } from '../stores/settingsStore.js';
10
+ import { theme, resolvedTheme, cycleTheme } from '../stores/themeStore.js';
12
11
  import type { ThemePreference } from '../types/settings.js';
13
12
 
14
13
  /**
@@ -16,7 +16,8 @@
16
16
  type ColorMode
17
17
  } from '@xyflow/svelte';
18
18
  import '@xyflow/svelte/dist/style.css';
19
- import { resolvedTheme, editorSettings, behaviorSettings } from '../stores/settingsStore.js';
19
+ import { resolvedTheme } from '../stores/themeStore.js';
20
+ import { editorSettings, behaviorSettings } from '../stores/settingsStore.js';
20
21
  import type {
21
22
  WorkflowNode as WorkflowNodeType,
22
23
  NodeMetadata,
@@ -42,6 +43,10 @@
42
43
  import { areNodeArraysEqual, areEdgeArraysEqual, throttle } from '../utils/performanceUtils.js';
43
44
  import { Toaster } from 'svelte-5-french-toast';
44
45
  import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS } from '../services/toastService.js';
46
+ import {
47
+ ProximityConnectHelper,
48
+ type ProximityEdgeCandidate
49
+ } from '../helpers/proximityConnect.js';
45
50
 
46
51
  interface Props {
47
52
  nodes?: NodeMetadata[];
@@ -69,19 +74,31 @@
69
74
  // Track if we're currently dragging a node (for history debouncing)
70
75
  let isDraggingNode = $state(false);
71
76
 
77
+ // Proximity connect state
78
+ let currentProximityCandidates = $state<ProximityEdgeCandidate[]>([]);
79
+
72
80
  // Track the workflow ID we're currently editing to detect workflow switches
73
81
  let currentWorkflowId: string | null = null;
74
82
 
83
+ // Track the last store value written by this editor to distinguish
84
+ // external programmatic changes from our own echoed writes
85
+ let lastEditorStoreValue: Workflow | null = null;
86
+
75
87
  // Initialize currentWorkflow from global store
76
- // Only sync when workflow ID changes (new workflow loaded) or on initial load
88
+ // Sync on workflow ID change (new workflow loaded) or external programmatic changes
77
89
  $effect(() => {
78
90
  if ($workflowStore) {
79
91
  const storeWorkflowId = $workflowStore.id;
80
92
 
81
- // Sync on initial load or when a different workflow is loaded
82
93
  if (currentWorkflowId !== storeWorkflowId) {
94
+ // New workflow loaded
83
95
  currentWorkflow = $workflowStore;
84
96
  currentWorkflowId = storeWorkflowId;
97
+ lastEditorStoreValue = null;
98
+ } else if ($workflowStore !== lastEditorStoreValue) {
99
+ // External programmatic change (e.g. addEdge, updateNode, updateEdges)
100
+ // The store value differs from what this editor last wrote, so sync it
101
+ currentWorkflow = $workflowStore;
85
102
  }
86
103
  } else if (currentWorkflow !== null) {
87
104
  // Store was cleared
@@ -95,6 +112,8 @@
95
112
  setOnRestoreCallback((restoredWorkflow: Workflow) => {
96
113
  // Directly update local state (bypass store sync effect)
97
114
  currentWorkflow = restoredWorkflow;
115
+ // Mark as our own write so sync effect doesn't re-process it
116
+ lastEditorStoreValue = restoredWorkflow;
98
117
  // Also update the store without triggering history
99
118
  workflowActions.restoreFromHistory(restoredWorkflow);
100
119
  });
@@ -207,6 +226,7 @@
207
226
  */
208
227
  const updateGlobalStore = throttle((): void => {
209
228
  if (currentWorkflow) {
229
+ lastEditorStoreValue = currentWorkflow;
210
230
  workflowActions.updateWorkflow(currentWorkflow);
211
231
  }
212
232
  }, 16);
@@ -322,6 +342,46 @@
322
342
  */
323
343
  function handleNodeDragStart(): void {
324
344
  isDraggingNode = true;
345
+ // Clear any leftover proximity previews
346
+ currentProximityCandidates = [];
347
+ }
348
+
349
+ /**
350
+ * Handle node drag - compute proximity connect preview edges
351
+ * Called continuously during drag if proximity connect is enabled
352
+ */
353
+ function handleNodeDrag({
354
+ targetNode
355
+ }: {
356
+ targetNode: WorkflowNodeType | null;
357
+ nodes: WorkflowNodeType[];
358
+ event: MouseEvent | TouchEvent;
359
+ }): void {
360
+ if (!$editorSettings.proximityConnect || !targetNode || props.readOnly || props.lockWorkflow) {
361
+ if (currentProximityCandidates.length > 0) {
362
+ flowEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
363
+ currentProximityCandidates = [];
364
+ }
365
+ return;
366
+ }
367
+
368
+ // Remove previous preview edges
369
+ const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
370
+
371
+ // Find the best compatible edge with nearby nodes
372
+ const candidates = ProximityConnectHelper.findCompatibleEdges(
373
+ targetNode,
374
+ flowNodes,
375
+ baseEdges,
376
+ $editorSettings.proximityConnectDistance
377
+ );
378
+
379
+ // Create preview edges
380
+ const previews = ProximityConnectHelper.createPreviewEdges(candidates);
381
+
382
+ // Update state
383
+ currentProximityCandidates = candidates;
384
+ flowEdges = [...baseEdges, ...previews];
325
385
  }
326
386
 
327
387
  /**
@@ -332,6 +392,38 @@
332
392
  */
333
393
  function handleNodeDragStop(): void {
334
394
  isDraggingNode = false;
395
+
396
+ // Finalize proximity connect if there are candidates
397
+ if ($editorSettings.proximityConnect && currentProximityCandidates.length > 0) {
398
+ // Remove all preview edges
399
+ const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
400
+
401
+ // Create permanent edges from candidates
402
+ const permanentEdges = ProximityConnectHelper.createPermanentEdges(
403
+ currentProximityCandidates
404
+ );
405
+
406
+ // Apply proper styling to each new permanent edge
407
+ for (const edge of permanentEdges) {
408
+ const sourceNode = flowNodes.find((n) => n.id === edge.source);
409
+ const targetNode = flowNodes.find((n) => n.id === edge.target);
410
+ if (sourceNode && targetNode) {
411
+ EdgeStylingHelper.applyConnectionStyling(edge, sourceNode, targetNode);
412
+ }
413
+ }
414
+
415
+ // Set final edges
416
+ flowEdges = [...baseEdges, ...permanentEdges];
417
+
418
+ // Clear proximity state
419
+ currentProximityCandidates = [];
420
+
421
+ // Update workflow
422
+ if (currentWorkflow) {
423
+ updateCurrentWorkflowFromSvelteFlow();
424
+ }
425
+ }
426
+
335
427
  // Push the current state AFTER the drag completed
336
428
  if (currentWorkflow) {
337
429
  workflowActions.pushHistory('Move node', currentWorkflow);
@@ -624,6 +716,7 @@
624
716
  onbeforedelete={handleBeforeDelete}
625
717
  ondelete={handleNodesDelete}
626
718
  onnodedragstart={handleNodeDragStart}
719
+ onnodedrag={handleNodeDrag}
627
720
  onnodedragstop={handleNodeDragStop}
628
721
  minZoom={0.2}
629
722
  maxZoom={3}
@@ -872,4 +965,19 @@
872
965
  filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
873
966
  opacity: 1;
874
967
  }
968
+
969
+ /* Proximity Connect Preview Edge: animated dashed line */
970
+ :global(.flowdrop--edge--proximity-preview path.svelte-flow__edge-path) {
971
+ stroke: var(--fd-primary);
972
+ stroke-width: 2;
973
+ stroke-dasharray: 5 5;
974
+ opacity: 0.6;
975
+ animation: flowdrop-proximity-dash 0.5s linear infinite;
976
+ }
977
+
978
+ @keyframes flowdrop-proximity-dash {
979
+ to {
980
+ stroke-dashoffset: -10;
981
+ }
982
+ }
875
983
  </style>
@@ -44,7 +44,7 @@
44
44
  import type { FieldSchema } from './types.js';
45
45
  import { getSchemaOptions } from './types.js';
46
46
  import type { WorkflowNode, WorkflowEdge, AuthProvider } from '../../types/index.js';
47
- import { resolvedTheme } from '../../stores/settingsStore.js';
47
+ import { resolvedTheme } from '../../stores/themeStore.js';
48
48
 
49
49
  interface Props {
50
50
  /** Unique key/id for the field */
@@ -43,7 +43,7 @@
43
43
  import FormCheckboxGroup from './FormCheckboxGroup.svelte';
44
44
  import FormArray from './FormArray.svelte';
45
45
  import { resolveFieldComponent } from '../../form/fieldRegistry.js';
46
- import { resolvedTheme } from '../../stores/settingsStore.js';
46
+ import { resolvedTheme } from '../../stores/themeStore.js';
47
47
  import type { FieldSchema } from './types.js';
48
48
  import { getSchemaOptions } from './types.js';
49
49
 
@@ -61,6 +61,17 @@
61
61
  'Tool'
62
62
  );
63
63
 
64
+ /**
65
+ * Instance-specific badge label override from config.
66
+ * Falls back to metadata badge or default 'TOOL' if not set.
67
+ * This allows users to customize the badge text per-instance via config.
68
+ */
69
+ const displayBadge = $derived(
70
+ (props.data.config?.instanceBadge as string) ||
71
+ (props.data.metadata?.badge as string) ||
72
+ 'TOOL'
73
+ );
74
+
64
75
  /**
65
76
  * Instance-specific description override from config.
66
77
  * Falls back to metadata description or toolDescription config if not set.
@@ -185,7 +196,7 @@
185
196
  </div>
186
197
 
187
198
  <!-- Tool Badge - tinted style matching icon wrappers -->
188
- <div class="flowdrop-tool-node__badge">TOOL</div>
199
+ <div class="flowdrop-tool-node__badge">{displayBadge}</div>
189
200
  </div>
190
201
 
191
202
  <!-- Tool Description - uses instanceDescription override if set -->
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Proximity Connect Helper
3
+ *
4
+ * Provides type-aware proximity connect logic for the workflow editor.
5
+ * When a node is dragged near another node, this helper finds the best
6
+ * compatible port pair and creates a preview/permanent edge.
7
+ */
8
+ import type { WorkflowNode as WorkflowNodeType, WorkflowEdge, NodePort } from '../types/index.js';
9
+ /** A candidate proximity edge before it is finalized */
10
+ export interface ProximityEdgeCandidate {
11
+ id: string;
12
+ source: string;
13
+ target: string;
14
+ sourceHandle: string;
15
+ targetHandle: string;
16
+ sourcePortDataType: string;
17
+ targetPortDataType: string;
18
+ }
19
+ export declare class ProximityConnectHelper {
20
+ /**
21
+ * Get ALL ports (static + dynamic + gateway branches) for a node.
22
+ */
23
+ static getAllPorts(node: WorkflowNodeType, direction: 'input' | 'output'): NodePort[];
24
+ /**
25
+ * Build handle ID in the standard format.
26
+ */
27
+ static buildHandleId(nodeId: string, direction: 'input' | 'output', portId: string): string;
28
+ /**
29
+ * Calculate center-to-center Euclidean distance between two nodes.
30
+ */
31
+ static getNodeDistance(nodeA: {
32
+ position: {
33
+ x: number;
34
+ y: number;
35
+ };
36
+ measured?: {
37
+ width?: number;
38
+ height?: number;
39
+ };
40
+ }, nodeB: {
41
+ position: {
42
+ x: number;
43
+ y: number;
44
+ };
45
+ measured?: {
46
+ width?: number;
47
+ height?: number;
48
+ };
49
+ }): number;
50
+ /**
51
+ * Find the single best compatible edge between a dragged node and nearby nodes.
52
+ *
53
+ * Algorithm:
54
+ * 1. Find the closest node within minDistance
55
+ * 2. Check both directions (dragged->nearby and nearby->dragged)
56
+ * 3. Return the first exact-type match, or first compatible match
57
+ * 4. Skip pairs where an edge already exists or input handle is already connected
58
+ *
59
+ * @returns Array with at most ONE ProximityEdgeCandidate
60
+ */
61
+ static findCompatibleEdges(draggedNode: WorkflowNodeType, allNodes: WorkflowNodeType[], existingEdges: WorkflowEdge[], minDistance: number): ProximityEdgeCandidate[];
62
+ /**
63
+ * Convert candidates to temporary (preview) WorkflowEdge objects with dashed styling.
64
+ */
65
+ static createPreviewEdges(candidates: ProximityEdgeCandidate[]): WorkflowEdge[];
66
+ /**
67
+ * Convert candidates to permanent WorkflowEdge objects.
68
+ */
69
+ static createPermanentEdges(candidates: ProximityEdgeCandidate[]): WorkflowEdge[];
70
+ /**
71
+ * Check if an edge is a temporary proximity preview edge.
72
+ */
73
+ static isProximityPreviewEdge(edge: WorkflowEdge): boolean;
74
+ /**
75
+ * Remove all proximity preview edges from an edge array.
76
+ */
77
+ static removePreviewEdges(edges: WorkflowEdge[]): WorkflowEdge[];
78
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Proximity Connect Helper
3
+ *
4
+ * Provides type-aware proximity connect logic for the workflow editor.
5
+ * When a node is dragged near another node, this helper finds the best
6
+ * compatible port pair and creates a preview/permanent edge.
7
+ */
8
+ import { dynamicPortToNodePort } from '../types/index.js';
9
+ import { getPortCompatibilityChecker } from '../utils/connections.js';
10
+ import { v4 as uuidv4 } from 'uuid';
11
+ /** CSS class applied to proximity preview edges */
12
+ const PROXIMITY_EDGE_CLASS = 'flowdrop--edge--proximity-preview';
13
+ export class ProximityConnectHelper {
14
+ /**
15
+ * Get ALL ports (static + dynamic + gateway branches) for a node.
16
+ */
17
+ static getAllPorts(node, direction) {
18
+ // Static ports from metadata
19
+ const staticPorts = direction === 'output'
20
+ ? (node.data?.metadata?.outputs ?? [])
21
+ : (node.data?.metadata?.inputs ?? []);
22
+ // Dynamic ports from config
23
+ const dynamicKey = direction === 'output' ? 'dynamicOutputs' : 'dynamicInputs';
24
+ const rawDynamic = node.data?.config?.[dynamicKey] ?? [];
25
+ const dynamicPorts = rawDynamic.map((p) => dynamicPortToNodePort(p, direction));
26
+ // Gateway branches (output only, dataType = 'trigger')
27
+ if (direction === 'output') {
28
+ const branches = node.data?.config?.branches;
29
+ const nodeType = node.data?.metadata?.type || node.type;
30
+ if (nodeType === 'gateway' && branches?.length) {
31
+ const branchPorts = branches
32
+ .filter((b) => !staticPorts.some((sp) => sp.id === b.name))
33
+ .map((b) => ({
34
+ id: b.name,
35
+ name: b.name,
36
+ type: 'output',
37
+ dataType: 'trigger'
38
+ }));
39
+ return [...staticPorts, ...dynamicPorts, ...branchPorts];
40
+ }
41
+ }
42
+ return [...staticPorts, ...dynamicPorts];
43
+ }
44
+ /**
45
+ * Build handle ID in the standard format.
46
+ */
47
+ static buildHandleId(nodeId, direction, portId) {
48
+ return `${nodeId}-${direction}-${portId}`;
49
+ }
50
+ /**
51
+ * Calculate center-to-center Euclidean distance between two nodes.
52
+ */
53
+ static getNodeDistance(nodeA, nodeB) {
54
+ const ax = nodeA.position.x + (nodeA.measured?.width ?? 0) / 2;
55
+ const ay = nodeA.position.y + (nodeA.measured?.height ?? 0) / 2;
56
+ const bx = nodeB.position.x + (nodeB.measured?.width ?? 0) / 2;
57
+ const by = nodeB.position.y + (nodeB.measured?.height ?? 0) / 2;
58
+ return Math.sqrt((ax - bx) ** 2 + (ay - by) ** 2);
59
+ }
60
+ /**
61
+ * Find the single best compatible edge between a dragged node and nearby nodes.
62
+ *
63
+ * Algorithm:
64
+ * 1. Find the closest node within minDistance
65
+ * 2. Check both directions (dragged->nearby and nearby->dragged)
66
+ * 3. Return the first exact-type match, or first compatible match
67
+ * 4. Skip pairs where an edge already exists or input handle is already connected
68
+ *
69
+ * @returns Array with at most ONE ProximityEdgeCandidate
70
+ */
71
+ static findCompatibleEdges(draggedNode, allNodes, existingEdges, minDistance) {
72
+ const checker = getPortCompatibilityChecker();
73
+ // Build lookup sets for O(1) duplicate/connected checks
74
+ const existingEdgeSet = new Set(existingEdges.map((e) => `${e.source}:${e.sourceHandle}->${e.target}:${e.targetHandle}`));
75
+ const connectedTargetHandles = new Set(existingEdges.map((e) => `${e.target}:${e.targetHandle}`));
76
+ // Find the closest node within distance
77
+ let closestNode = null;
78
+ let closestDistance = Infinity;
79
+ for (const node of allNodes) {
80
+ if (node.id === draggedNode.id)
81
+ continue;
82
+ const dist = this.getNodeDistance(draggedNode, node);
83
+ if (dist < minDistance && dist < closestDistance) {
84
+ closestDistance = dist;
85
+ closestNode = node;
86
+ }
87
+ }
88
+ if (!closestNode)
89
+ return [];
90
+ const draggedOutputs = this.getAllPorts(draggedNode, 'output');
91
+ const draggedInputs = this.getAllPorts(draggedNode, 'input');
92
+ const nearbyInputs = this.getAllPorts(closestNode, 'input');
93
+ const nearbyOutputs = this.getAllPorts(closestNode, 'output');
94
+ // Collect all compatible pairs, then pick the best one
95
+ let exactMatch = null;
96
+ let compatibleMatch = null;
97
+ // Direction A: dragged (source) -> nearby (target)
98
+ for (const outPort of draggedOutputs) {
99
+ for (const inPort of nearbyInputs) {
100
+ if (!checker.areDataTypesCompatible(outPort.dataType, inPort.dataType))
101
+ continue;
102
+ const sourceHandle = this.buildHandleId(draggedNode.id, 'output', outPort.id);
103
+ const targetHandle = this.buildHandleId(closestNode.id, 'input', inPort.id);
104
+ const edgeKey = `${draggedNode.id}:${sourceHandle}->${closestNode.id}:${targetHandle}`;
105
+ const targetHandleKey = `${closestNode.id}:${targetHandle}`;
106
+ if (existingEdgeSet.has(edgeKey))
107
+ continue;
108
+ if (connectedTargetHandles.has(targetHandleKey))
109
+ continue;
110
+ const candidate = {
111
+ id: `proximity-${uuidv4()}`,
112
+ source: draggedNode.id,
113
+ target: closestNode.id,
114
+ sourceHandle,
115
+ targetHandle,
116
+ sourcePortDataType: outPort.dataType,
117
+ targetPortDataType: inPort.dataType
118
+ };
119
+ if (outPort.dataType === inPort.dataType) {
120
+ if (!exactMatch)
121
+ exactMatch = candidate;
122
+ }
123
+ else {
124
+ if (!compatibleMatch)
125
+ compatibleMatch = candidate;
126
+ }
127
+ // Early exit if we found an exact match
128
+ if (exactMatch)
129
+ break;
130
+ }
131
+ if (exactMatch)
132
+ break;
133
+ }
134
+ // Direction B: nearby (source) -> dragged (target)
135
+ if (!exactMatch) {
136
+ for (const outPort of nearbyOutputs) {
137
+ for (const inPort of draggedInputs) {
138
+ if (!checker.areDataTypesCompatible(outPort.dataType, inPort.dataType))
139
+ continue;
140
+ const sourceHandle = this.buildHandleId(closestNode.id, 'output', outPort.id);
141
+ const targetHandle = this.buildHandleId(draggedNode.id, 'input', inPort.id);
142
+ const edgeKey = `${closestNode.id}:${sourceHandle}->${draggedNode.id}:${targetHandle}`;
143
+ const targetHandleKey = `${draggedNode.id}:${targetHandle}`;
144
+ if (existingEdgeSet.has(edgeKey))
145
+ continue;
146
+ if (connectedTargetHandles.has(targetHandleKey))
147
+ continue;
148
+ const candidate = {
149
+ id: `proximity-${uuidv4()}`,
150
+ source: closestNode.id,
151
+ target: draggedNode.id,
152
+ sourceHandle,
153
+ targetHandle,
154
+ sourcePortDataType: outPort.dataType,
155
+ targetPortDataType: inPort.dataType
156
+ };
157
+ if (outPort.dataType === inPort.dataType) {
158
+ if (!exactMatch)
159
+ exactMatch = candidate;
160
+ }
161
+ else {
162
+ if (!compatibleMatch)
163
+ compatibleMatch = candidate;
164
+ }
165
+ if (exactMatch)
166
+ break;
167
+ }
168
+ if (exactMatch)
169
+ break;
170
+ }
171
+ }
172
+ const best = exactMatch ?? compatibleMatch;
173
+ return best ? [best] : [];
174
+ }
175
+ /**
176
+ * Convert candidates to temporary (preview) WorkflowEdge objects with dashed styling.
177
+ */
178
+ static createPreviewEdges(candidates) {
179
+ return candidates.map((c) => ({
180
+ id: c.id,
181
+ source: c.source,
182
+ target: c.target,
183
+ sourceHandle: c.sourceHandle,
184
+ targetHandle: c.targetHandle,
185
+ class: PROXIMITY_EDGE_CLASS,
186
+ style: 'stroke-dasharray: 5 5; opacity: 0.6;',
187
+ animated: true,
188
+ selectable: false,
189
+ deletable: false,
190
+ data: {
191
+ metadata: {
192
+ edgeType: 'data',
193
+ sourcePortDataType: c.sourcePortDataType,
194
+ isProximityPreview: true
195
+ }
196
+ }
197
+ }));
198
+ }
199
+ /**
200
+ * Convert candidates to permanent WorkflowEdge objects.
201
+ */
202
+ static createPermanentEdges(candidates) {
203
+ return candidates.map((c) => ({
204
+ id: uuidv4(),
205
+ source: c.source,
206
+ target: c.target,
207
+ sourceHandle: c.sourceHandle,
208
+ targetHandle: c.targetHandle
209
+ }));
210
+ }
211
+ /**
212
+ * Check if an edge is a temporary proximity preview edge.
213
+ */
214
+ static isProximityPreviewEdge(edge) {
215
+ return (edge.id.startsWith('proximity-') ||
216
+ edge.data?.metadata?.isProximityPreview === true);
217
+ }
218
+ /**
219
+ * Remove all proximity preview edges from an edge array.
220
+ */
221
+ static removePreviewEdges(edges) {
222
+ return edges.filter((e) => !this.isProximityPreviewEdge(e));
223
+ }
224
+ }
@@ -95,6 +95,15 @@ export class EdgeStylingHelper {
95
95
  return port.dataType;
96
96
  }
97
97
  }
98
+ // Check dynamic ports from config (dynamicInputs/dynamicOutputs)
99
+ const dynamicKey = portType === 'output' ? 'dynamicOutputs' : 'dynamicInputs';
100
+ const dynamicPorts = node.data?.config?.[dynamicKey];
101
+ if (dynamicPorts && Array.isArray(dynamicPorts)) {
102
+ const dynamicPort = dynamicPorts.find((p) => p.name === portId);
103
+ if (dynamicPort?.dataType) {
104
+ return dynamicPort.dataType;
105
+ }
106
+ }
98
107
  // For output ports, also check dynamic Gateway branches
99
108
  // Gateway branches are always trigger type (control flow)
100
109
  if (portType === 'output' && this.isGatewayBranch(node, portId)) {
package/dist/index.d.ts CHANGED
@@ -34,8 +34,4 @@ export * from './form/index.js';
34
34
  export * from './display/index.js';
35
35
  export * from './playground/index.js';
36
36
  export * from './editor/index.js';
37
- export { default as ThemeToggle } from './components/ThemeToggle.svelte';
38
- export { default as SettingsPanel } from './components/SettingsPanel.svelte';
39
- export { default as SettingsModal } from './components/SettingsModal.svelte';
40
- export { settingsStore, themeSettings, editorSettings, uiSettings, behaviorSettings, apiSettings, syncStatusStore, theme, resolvedTheme, updateSettings, resetSettings, getSettings, setTheme, toggleTheme, cycleTheme, initializeTheme, initializeSettings, setSettingsService, syncSettingsToApi, loadSettingsFromApi, onSettingsChange } from './stores/settingsStore.js';
41
- export { settingsApi, SettingsService, createSettingsService, setSettingsEndpointConfig, getSettingsEndpointConfig } from './services/settingsService.js';
37
+ export * from './settings/index.js';
package/dist/index.js CHANGED
@@ -56,15 +56,6 @@ export * from './playground/index.js';
56
56
  // ============================================================================
57
57
  export * from './editor/index.js';
58
58
  // ============================================================================
59
- // Theme Component Export
59
+ // Settings Exports (stores, services, components, types)
60
60
  // ============================================================================
61
- export { default as ThemeToggle } from './components/ThemeToggle.svelte';
62
- // ============================================================================
63
- // Settings Component & Store Exports
64
- // ============================================================================
65
- export { default as SettingsPanel } from './components/SettingsPanel.svelte';
66
- export { default as SettingsModal } from './components/SettingsModal.svelte';
67
- // Settings store exports
68
- export { settingsStore, themeSettings, editorSettings, uiSettings, behaviorSettings, apiSettings, syncStatusStore, theme, resolvedTheme, updateSettings, resetSettings, getSettings, setTheme, toggleTheme, cycleTheme, initializeTheme, initializeSettings, setSettingsService, syncSettingsToApi, loadSettingsFromApi, onSettingsChange } from './stores/settingsStore.js';
69
- // Settings service exports
70
- export { settingsApi, SettingsService, createSettingsService, setSettingsEndpointConfig, getSettingsEndpointConfig } from './services/settingsService.js';
61
+ export * from './settings/index.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * FlowDrop Settings Module
3
+ *
4
+ * Provides settings stores, services, and components for user-configurable
5
+ * preferences with hybrid persistence (localStorage + optional API sync).
6
+ *
7
+ * Theme stores and functions (theme, resolvedTheme, setTheme, toggleTheme,
8
+ * cycleTheme, initializeTheme) are exported from `@d34dman/flowdrop/core`.
9
+ *
10
+ * @module settings
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { settingsStore, updateSettings, SettingsPanel } from "@d34dman/flowdrop/settings";
15
+ * import { theme, setTheme } from "@d34dman/flowdrop/core";
16
+ * ```
17
+ */
18
+ export { default as ThemeToggle } from '../components/ThemeToggle.svelte';
19
+ export { default as SettingsPanel } from '../components/SettingsPanel.svelte';
20
+ export { default as SettingsModal } from '../components/SettingsModal.svelte';
21
+ export type { ThemeSettings, EditorSettings, UISettings, BehaviorSettings, ApiSettings, FlowDropSettings, SettingsCategory, PartialSettings, DeepPartial, SettingsChangeEvent, SettingsChangeCallback, SyncStatus, SettingsStoreState } from '../types/settings.js';
22
+ export { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_LABELS, SETTINGS_CATEGORY_ICONS, DEFAULT_THEME_SETTINGS, DEFAULT_EDITOR_SETTINGS, DEFAULT_UI_SETTINGS, DEFAULT_BEHAVIOR_SETTINGS, DEFAULT_API_SETTINGS, DEFAULT_SETTINGS, SETTINGS_STORAGE_KEY } from '../types/settings.js';
23
+ export { settingsStore, themeSettings, editorSettings, uiSettings, behaviorSettings, apiSettings, syncStatusStore, updateSettings, resetSettings, getSettings, initializeSettings, setSettingsService, syncSettingsToApi, loadSettingsFromApi, onSettingsChange } from '../stores/settingsStore.js';
24
+ export { settingsApi, SettingsService, createSettingsService, setSettingsEndpointConfig, getSettingsEndpointConfig } from '../services/settingsService.js';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * FlowDrop Settings Module
3
+ *
4
+ * Provides settings stores, services, and components for user-configurable
5
+ * preferences with hybrid persistence (localStorage + optional API sync).
6
+ *
7
+ * Theme stores and functions (theme, resolvedTheme, setTheme, toggleTheme,
8
+ * cycleTheme, initializeTheme) are exported from `@d34dman/flowdrop/core`.
9
+ *
10
+ * @module settings
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { settingsStore, updateSettings, SettingsPanel } from "@d34dman/flowdrop/settings";
15
+ * import { theme, setTheme } from "@d34dman/flowdrop/core";
16
+ * ```
17
+ */
18
+ // ============================================================================
19
+ // Components
20
+ // ============================================================================
21
+ export { default as ThemeToggle } from '../components/ThemeToggle.svelte';
22
+ export { default as SettingsPanel } from '../components/SettingsPanel.svelte';
23
+ export { default as SettingsModal } from '../components/SettingsModal.svelte';
24
+ export { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_LABELS, SETTINGS_CATEGORY_ICONS, DEFAULT_THEME_SETTINGS, DEFAULT_EDITOR_SETTINGS, DEFAULT_UI_SETTINGS, DEFAULT_BEHAVIOR_SETTINGS, DEFAULT_API_SETTINGS, DEFAULT_SETTINGS, SETTINGS_STORAGE_KEY } from '../types/settings.js';
25
+ // ============================================================================
26
+ // Settings Stores
27
+ // ============================================================================
28
+ export { settingsStore, themeSettings, editorSettings, uiSettings, behaviorSettings, apiSettings, syncStatusStore, updateSettings, resetSettings, getSettings, initializeSettings, setSettingsService, syncSettingsToApi, loadSettingsFromApi, onSettingsChange } from '../stores/settingsStore.js';
29
+ // ============================================================================
30
+ // Settings Service
31
+ // ============================================================================
32
+ export { settingsApi, SettingsService, createSettingsService, setSettingsEndpointConfig, getSettingsEndpointConfig } from '../services/settingsService.js';
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module stores/settingsStore
11
11
  */
12
- import type { FlowDropSettings, ThemeSettings, EditorSettings, UISettings, BehaviorSettings, ApiSettings, PartialSettings, SyncStatus, ResolvedTheme, ThemePreference, SettingsChangeCallback, SettingsCategory } from '../types/settings.js';
12
+ import type { FlowDropSettings, ThemeSettings, EditorSettings, UISettings, BehaviorSettings, ApiSettings, PartialSettings, SyncStatus, SettingsChangeCallback, SettingsCategory } from '../types/settings.js';
13
13
  /**
14
14
  * Main settings store (read-only access to current settings)
15
15
  */
@@ -42,15 +42,6 @@ export declare const behaviorSettings: import("svelte/store").Readable<BehaviorS
42
42
  * API settings store
43
43
  */
44
44
  export declare const apiSettings: import("svelte/store").Readable<ApiSettings>;
45
- /**
46
- * Theme preference store (backward compatible with themeStore)
47
- */
48
- export declare const theme: import("svelte/store").Readable<ThemePreference>;
49
- /**
50
- * Resolved theme store
51
- * Always returns the actual theme being applied ('light' or 'dark')
52
- */
53
- export declare const resolvedTheme: import("svelte/store").Readable<ResolvedTheme>;
54
45
  /**
55
46
  * Update settings with partial values
56
47
  *
@@ -67,26 +58,6 @@ export declare function resetSettings(categories?: SettingsCategory[]): void;
67
58
  * Get current settings synchronously
68
59
  */
69
60
  export declare function getSettings(): FlowDropSettings;
70
- /**
71
- * Set the theme preference
72
- *
73
- * @param newTheme - The new theme preference ('light', 'dark', or 'auto')
74
- */
75
- export declare function setTheme(newTheme: ThemePreference): void;
76
- /**
77
- * Toggle between light and dark themes
78
- * If currently 'auto', switches to the opposite of system preference
79
- */
80
- export declare function toggleTheme(): void;
81
- /**
82
- * Cycle through theme options: light -> dark -> auto -> light
83
- */
84
- export declare function cycleTheme(): void;
85
- /**
86
- * Initialize the theme system
87
- * Should be called once on app startup
88
- */
89
- export declare function initializeTheme(): void;
90
61
  /**
91
62
  * Set the settings service for API operations
92
63
  *
@@ -188,14 +188,13 @@ if (typeof window !== 'undefined') {
188
188
  }
189
189
  }
190
190
  /**
191
- * Theme preference store (backward compatible with themeStore)
191
+ * Theme preference (internal - exported via core/themeStore)
192
192
  */
193
- export const theme = derived(themeSettings, ($theme) => $theme.preference);
193
+ const theme = derived(themeSettings, ($theme) => $theme.preference);
194
194
  /**
195
- * Resolved theme store
196
- * Always returns the actual theme being applied ('light' or 'dark')
195
+ * Resolved theme (internal - exported via core/themeStore)
197
196
  */
198
- export const resolvedTheme = derived([themeSettings, systemTheme], ([$themeSettings, $systemTheme]) => {
197
+ const resolvedTheme = derived([themeSettings, systemTheme], ([$themeSettings, $systemTheme]) => {
199
198
  if ($themeSettings.preference === 'auto') {
200
199
  return $systemTheme;
201
200
  }
@@ -292,18 +291,15 @@ export function getSettings() {
292
291
  // Theme-Specific Functions (backward compatible with themeStore)
293
292
  // =========================================================================
294
293
  /**
295
- * Set the theme preference
296
- *
297
- * @param newTheme - The new theme preference ('light', 'dark', or 'auto')
294
+ * Set the theme preference (internal - exported via core/themeStore)
298
295
  */
299
- export function setTheme(newTheme) {
296
+ function setTheme(newTheme) {
300
297
  updateSettings({ theme: { preference: newTheme } });
301
298
  }
302
299
  /**
303
- * Toggle between light and dark themes
304
- * If currently 'auto', switches to the opposite of system preference
300
+ * Toggle between light and dark themes (internal - exported via core/themeStore)
305
301
  */
306
- export function toggleTheme() {
302
+ function toggleTheme() {
307
303
  const currentTheme = get(theme);
308
304
  const currentResolved = get(resolvedTheme);
309
305
  if (currentTheme === 'auto') {
@@ -314,9 +310,9 @@ export function toggleTheme() {
314
310
  }
315
311
  }
316
312
  /**
317
- * Cycle through theme options: light -> dark -> auto -> light
313
+ * Cycle through theme options (internal - exported via core/themeStore)
318
314
  */
319
- export function cycleTheme() {
315
+ function cycleTheme() {
320
316
  const currentTheme = get(theme);
321
317
  switch (currentTheme) {
322
318
  case 'light':
@@ -342,10 +338,9 @@ function applyTheme(resolved) {
342
338
  document.documentElement.setAttribute('data-theme', resolved);
343
339
  }
344
340
  /**
345
- * Initialize the theme system
346
- * Should be called once on app startup
341
+ * Initialize the theme system (internal - exported via core/themeStore)
347
342
  */
348
- export function initializeTheme() {
343
+ function initializeTheme() {
349
344
  const resolved = get(resolvedTheme);
350
345
  applyTheme(resolved);
351
346
  // Subscribe to resolved theme changes and apply them
@@ -476,6 +476,8 @@ export interface NodeMetadata {
476
476
  version: string;
477
477
  icon?: string;
478
478
  color?: string;
479
+ /** Badge label displayed in the node header (e.g., "TOOL", "API", "LLM"). Overridable per-instance via config.instanceBadge. */
480
+ badge?: string;
479
481
  inputs: NodePort[];
480
482
  outputs: NodePort[];
481
483
  configSchema?: ConfigSchema;
@@ -40,6 +40,10 @@ export interface EditorSettings {
40
40
  defaultZoom: number;
41
41
  /** Automatically fit workflow to view on load */
42
42
  fitViewOnLoad: boolean;
43
+ /** Enable proximity connect when dragging nodes near other nodes */
44
+ proximityConnect: boolean;
45
+ /** Distance threshold in pixels for proximity connect */
46
+ proximityConnectDistance: number;
43
47
  }
44
48
  /**
45
49
  * UI layout and display settings
@@ -48,7 +48,9 @@ export const DEFAULT_EDITOR_SETTINGS = {
48
48
  gridSize: 20,
49
49
  showMinimap: true,
50
50
  defaultZoom: 1,
51
- fitViewOnLoad: true
51
+ fitViewOnLoad: true,
52
+ proximityConnect: false,
53
+ proximityConnectDistance: 150
52
54
  };
53
55
  /**
54
56
  * Default UI settings
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@d34dman/flowdrop",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "0.0.49",
5
+ "version": "0.0.51",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "build": "vite build && npm run prepack",
@@ -128,6 +128,11 @@
128
128
  "svelte": "./dist/playground/index.js",
129
129
  "default": "./dist/playground/index.js"
130
130
  },
131
+ "./settings": {
132
+ "types": "./dist/settings/index.d.ts",
133
+ "svelte": "./dist/settings/index.js",
134
+ "default": "./dist/settings/index.js"
135
+ },
131
136
  "./styles": "./dist/styles/base.css",
132
137
  "./styles/*": "./dist/styles/*"
133
138
  },