@d34dman/flowdrop 0.0.50 → 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.
- package/dist/components/SettingsPanel.svelte +15 -0
- package/dist/components/ThemeToggle.svelte +1 -2
- package/dist/components/WorkflowEditor.svelte +97 -1
- package/dist/components/form/FormField.svelte +1 -1
- package/dist/components/form/FormFieldLight.svelte +1 -1
- package/dist/components/nodes/ToolNode.svelte +12 -1
- package/dist/helpers/proximityConnect.d.ts +78 -0
- package/dist/helpers/proximityConnect.js +224 -0
- package/dist/helpers/workflowEditorHelper.js +9 -0
- package/dist/index.d.ts +1 -5
- package/dist/index.js +2 -11
- package/dist/settings/index.d.ts +24 -0
- package/dist/settings/index.js +32 -0
- package/dist/stores/settingsStore.d.ts +1 -30
- package/dist/stores/settingsStore.js +12 -17
- package/dist/types/index.d.ts +2 -0
- package/dist/types/settings.d.ts +4 -0
- package/dist/types/settings.js +3 -1
- package/package.json +6 -1
|
@@ -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
|
-
|
|
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
|
|
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,6 +74,9 @@
|
|
|
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
|
|
|
@@ -334,6 +342,46 @@
|
|
|
334
342
|
*/
|
|
335
343
|
function handleNodeDragStart(): void {
|
|
336
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];
|
|
337
385
|
}
|
|
338
386
|
|
|
339
387
|
/**
|
|
@@ -344,6 +392,38 @@
|
|
|
344
392
|
*/
|
|
345
393
|
function handleNodeDragStop(): void {
|
|
346
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
|
+
|
|
347
427
|
// Push the current state AFTER the drag completed
|
|
348
428
|
if (currentWorkflow) {
|
|
349
429
|
workflowActions.pushHistory('Move node', currentWorkflow);
|
|
@@ -636,6 +716,7 @@
|
|
|
636
716
|
onbeforedelete={handleBeforeDelete}
|
|
637
717
|
ondelete={handleNodesDelete}
|
|
638
718
|
onnodedragstart={handleNodeDragStart}
|
|
719
|
+
onnodedrag={handleNodeDrag}
|
|
639
720
|
onnodedragstop={handleNodeDragStop}
|
|
640
721
|
minZoom={0.2}
|
|
641
722
|
maxZoom={3}
|
|
@@ -884,4 +965,19 @@
|
|
|
884
965
|
filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
|
|
885
966
|
opacity: 1;
|
|
886
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
|
+
}
|
|
887
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/
|
|
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/
|
|
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">
|
|
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
|
|
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
|
-
//
|
|
59
|
+
// Settings Exports (stores, services, components, types)
|
|
60
60
|
// ============================================================================
|
|
61
|
-
export
|
|
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,
|
|
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
|
|
191
|
+
* Theme preference (internal - exported via core/themeStore)
|
|
192
192
|
*/
|
|
193
|
-
|
|
193
|
+
const theme = derived(themeSettings, ($theme) => $theme.preference);
|
|
194
194
|
/**
|
|
195
|
-
* Resolved theme
|
|
196
|
-
* Always returns the actual theme being applied ('light' or 'dark')
|
|
195
|
+
* Resolved theme (internal - exported via core/themeStore)
|
|
197
196
|
*/
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
313
|
+
* Cycle through theme options (internal - exported via core/themeStore)
|
|
318
314
|
*/
|
|
319
|
-
|
|
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
|
-
|
|
343
|
+
function initializeTheme() {
|
|
349
344
|
const resolved = get(resolvedTheme);
|
|
350
345
|
applyTheme(resolved);
|
|
351
346
|
// Subscribe to resolved theme changes and apply them
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/settings.d.ts
CHANGED
|
@@ -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
|
package/dist/types/settings.js
CHANGED
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.
|
|
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
|
},
|