@d34dman/flowdrop 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +52 -62
  2. package/dist/components/App.svelte +15 -2
  3. package/dist/components/ConfigForm.svelte +4 -1
  4. package/dist/components/ConfigModal.svelte +4 -70
  5. package/dist/components/ConfigPanel.svelte +4 -9
  6. package/dist/components/EdgeRefresher.svelte +42 -0
  7. package/dist/components/EdgeRefresher.svelte.d.ts +9 -0
  8. package/dist/components/ReadOnlyDetails.svelte +3 -1
  9. package/dist/components/UniversalNode.svelte +6 -3
  10. package/dist/components/WorkflowEditor.svelte +33 -0
  11. package/dist/components/WorkflowEditor.svelte.d.ts +3 -1
  12. package/dist/components/form/FormCheckboxGroup.svelte +2 -9
  13. package/dist/components/form/FormCodeEditor.svelte +416 -0
  14. package/dist/components/form/FormCodeEditor.svelte.d.ts +23 -0
  15. package/dist/components/form/FormField.svelte +125 -85
  16. package/dist/components/form/FormField.svelte.d.ts +1 -1
  17. package/dist/components/form/FormFieldWrapper.svelte +2 -10
  18. package/dist/components/form/FormFieldWrapper.svelte.d.ts +1 -1
  19. package/dist/components/form/FormMarkdownEditor.svelte +553 -0
  20. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +29 -0
  21. package/dist/components/form/FormNumberField.svelte +5 -6
  22. package/dist/components/form/FormRangeField.svelte +3 -13
  23. package/dist/components/form/FormSelect.svelte +4 -5
  24. package/dist/components/form/FormSelect.svelte.d.ts +1 -1
  25. package/dist/components/form/FormTemplateEditor.svelte +463 -0
  26. package/dist/components/form/FormTemplateEditor.svelte.d.ts +25 -0
  27. package/dist/components/form/FormTextField.svelte +3 -4
  28. package/dist/components/form/FormTextarea.svelte +3 -4
  29. package/dist/components/form/FormToggle.svelte +2 -3
  30. package/dist/components/form/index.d.ts +14 -11
  31. package/dist/components/form/index.js +14 -11
  32. package/dist/components/form/types.d.ts +55 -2
  33. package/dist/components/form/types.js +1 -1
  34. package/dist/components/nodes/NotesNode.svelte +39 -45
  35. package/dist/components/nodes/NotesNode.svelte.d.ts +1 -1
  36. package/dist/components/nodes/WorkflowNode.svelte +1 -3
  37. package/dist/styles/base.css +1 -1
  38. package/package.json +162 -148
package/README.md CHANGED
@@ -51,13 +51,12 @@ npm install @d34dman/flowdrop
51
51
 
52
52
  You get a production-ready workflow UI. You keep full control of everything else.
53
53
 
54
-
55
54
  ## Quickstart
56
55
 
57
56
  ```svelte
58
57
  <script lang="ts">
59
- import { WorkflowEditor } from "@d34dman/flowdrop";
60
- import "@d34dman/flowdrop/styles/base.css";
58
+ import { WorkflowEditor } from '@d34dman/flowdrop';
59
+ import '@d34dman/flowdrop/styles/base.css';
61
60
  </script>
62
61
 
63
62
  <WorkflowEditor />
@@ -65,11 +64,10 @@ You get a production-ready workflow UI. You keep full control of everything else
65
64
 
66
65
  **5 lines. One fully-functional workflow editor.**
67
66
 
68
-
69
67
  ## Features
70
68
 
71
- | | |
72
- | --------------------------- | ------------------------------------------------------------------------- |
69
+ | | |
70
+ | ---------------------------- | ------------------------------------------------------------------------- |
73
71
  | 🎨 **Visual Editor Only** | Pure UI component. No hidden backend, no external dependencies |
74
72
  | 🔐 **You Own Everything** | Your data, your servers, your orchestration logic, your security policies |
75
73
  | 🔌 **Backend Agnostic** | Connect to any API: Drupal, Laravel, Express, FastAPI, or your own |
@@ -77,7 +75,6 @@ You get a production-ready workflow UI. You keep full control of everything else
77
75
  | 🎭 **Framework Flexible** | Use as Svelte component or mount into React, Vue, Angular, or vanilla JS |
78
76
  | 🐳 **Deploy Anywhere** | Runtime config means build once, deploy everywhere |
79
77
 
80
-
81
78
  ## Node Types
82
79
 
83
80
  FlowDrop ships with 7 beautifully designed node types:
@@ -105,27 +102,27 @@ FlowDrop ships with 7 beautifully designed node types:
105
102
 
106
103
  ```svelte
107
104
  <script>
108
- import { WorkflowEditor, NodeSidebar } from "@d34dman/flowdrop";
105
+ import { WorkflowEditor, NodeSidebar } from '@d34dman/flowdrop';
109
106
  </script>
110
107
 
111
108
  <div class="flex h-screen">
112
- <NodeSidebar {nodes} />
113
- <WorkflowEditor {nodes} />
109
+ <NodeSidebar {nodes} />
110
+ <WorkflowEditor {nodes} />
114
111
  </div>
115
112
  ```
116
113
 
117
114
  ### Vanilla JS / React / Vue / Angular
118
115
 
119
116
  ```javascript
120
- import { mountFlowDropApp, createEndpointConfig } from "@d34dman/flowdrop";
121
-
122
- const app = await mountFlowDropApp(document.getElementById("editor"), {
123
- workflow: myWorkflow,
124
- endpointConfig: createEndpointConfig("/api/flowdrop"),
125
- eventHandlers: {
126
- onDirtyStateChange: (isDirty) => console.log("Unsaved changes:", isDirty),
127
- onAfterSave: (workflow) => console.log("Saved!", workflow)
128
- }
117
+ import { mountFlowDropApp, createEndpointConfig } from '@d34dman/flowdrop';
118
+
119
+ const app = await mountFlowDropApp(document.getElementById('editor'), {
120
+ workflow: myWorkflow,
121
+ endpointConfig: createEndpointConfig('/api/flowdrop'),
122
+ eventHandlers: {
123
+ onDirtyStateChange: (isDirty) => console.log('Unsaved changes:', isDirty),
124
+ onAfterSave: (workflow) => console.log('Saved!', workflow)
125
+ }
129
126
  });
130
127
 
131
128
  // Full control over the editor
@@ -137,69 +134,66 @@ app.destroy();
137
134
  ### Enterprise Integration
138
135
 
139
136
  ```javascript
140
- import { mountFlowDropApp, CallbackAuthProvider } from "@d34dman/flowdrop";
137
+ import { mountFlowDropApp, CallbackAuthProvider } from '@d34dman/flowdrop';
141
138
 
142
139
  const app = await mountFlowDropApp(container, {
143
- // Dynamic token refresh
144
- authProvider: new CallbackAuthProvider({
145
- getToken: () => authService.getAccessToken(),
146
- onUnauthorized: () => authService.refreshToken()
147
- }),
148
-
149
- // Lifecycle hooks
150
- eventHandlers: {
151
- onBeforeUnmount: (workflow, isDirty) => {
152
- if (isDirty) saveDraft(workflow);
153
- }
154
- },
155
-
156
- // Auto-save, toasts, and more
157
- features: {
158
- autoSaveDraft: true,
159
- autoSaveDraftInterval: 30000
160
- }
140
+ // Dynamic token refresh
141
+ authProvider: new CallbackAuthProvider({
142
+ getToken: () => authService.getAccessToken(),
143
+ onUnauthorized: () => authService.refreshToken()
144
+ }),
145
+
146
+ // Lifecycle hooks
147
+ eventHandlers: {
148
+ onBeforeUnmount: (workflow, isDirty) => {
149
+ if (isDirty) saveDraft(workflow);
150
+ }
151
+ },
152
+
153
+ // Auto-save, toasts, and more
154
+ features: {
155
+ autoSaveDraft: true,
156
+ autoSaveDraftInterval: 30000
157
+ }
161
158
  });
162
159
  ```
163
160
 
164
-
165
161
  ## API Configuration
166
162
 
167
163
  Connect to any backend in seconds:
168
164
 
169
165
  ```typescript
170
- import { createEndpointConfig } from "@d34dman/flowdrop";
166
+ import { createEndpointConfig } from '@d34dman/flowdrop';
171
167
 
172
168
  const config = createEndpointConfig({
173
- baseUrl: "https://api.example.com",
174
- endpoints: {
175
- nodes: { list: "/nodes", get: "/nodes/{id}" },
176
- workflows: {
177
- list: "/workflows",
178
- get: "/workflows/{id}",
179
- create: "/workflows",
180
- update: "/workflows/{id}",
181
- execute: "/workflows/{id}/execute"
182
- }
183
- },
184
- auth: { type: "bearer", token: "your-token" }
169
+ baseUrl: 'https://api.example.com',
170
+ endpoints: {
171
+ nodes: { list: '/nodes', get: '/nodes/{id}' },
172
+ workflows: {
173
+ list: '/workflows',
174
+ get: '/workflows/{id}',
175
+ create: '/workflows',
176
+ update: '/workflows/{id}',
177
+ execute: '/workflows/{id}/execute'
178
+ }
179
+ },
180
+ auth: { type: 'bearer', token: 'your-token' }
185
181
  });
186
182
  ```
187
183
 
188
-
189
184
  ## Customization
190
185
 
191
186
  Make it yours with CSS custom properties:
192
187
 
193
188
  ```css
194
189
  :root {
195
- --flowdrop-background-color: #0a0a0a;
196
- --flowdrop-primary-color: #6366f1;
197
- --flowdrop-border-color: #27272a;
198
- --flowdrop-text-color: #fafafa;
190
+ --flowdrop-background-color: #0a0a0a;
191
+ --flowdrop-primary-color: #6366f1;
192
+ --flowdrop-border-color: #27272a;
193
+ --flowdrop-text-color: #fafafa;
199
194
  }
200
195
  ```
201
196
 
202
-
203
197
  ## Deploy
204
198
 
205
199
  ### Docker (Recommended)
@@ -218,8 +212,6 @@ FLOWDROP_API_BASE_URL=http://your-backend/api node build
218
212
 
219
213
  Runtime configuration means you build once and deploy to staging, production, or anywhere else with just environment variables.
220
214
 
221
-
222
-
223
215
  ## Documentation
224
216
 
225
217
  | Resource | Description |
@@ -238,12 +230,10 @@ npm run build # Build library
238
230
  npm test # Run all tests
239
231
  ```
240
232
 
241
-
242
233
  ## Contributing
243
234
 
244
235
  FlowDrop is stabilizing. Contributions will open soon. Star the repo to stay updated.
245
236
 
246
-
247
237
  <p align="center">
248
238
  <strong>FlowDrop</strong> - The visual workflow editor you own completely
249
239
  </p>
@@ -14,7 +14,13 @@
14
14
  import ConfigPanel from './ConfigPanel.svelte';
15
15
  import Navbar from './Navbar.svelte';
16
16
  import { api, setEndpointConfig } from '../services/api.js';
17
- import type { NodeMetadata, Workflow, WorkflowNode, ConfigSchema, NodeUIExtensions } from '../types/index.js';
17
+ import type {
18
+ NodeMetadata,
19
+ Workflow,
20
+ WorkflowNode,
21
+ ConfigSchema,
22
+ NodeUIExtensions
23
+ } from '../types/index.js';
18
24
  import { createEndpointConfig } from '../config/endpoints.js';
19
25
  import type { EndpointConfig } from '../config/endpoints.js';
20
26
  import type { AuthProvider } from '../types/auth.js';
@@ -663,7 +669,7 @@
663
669
  >
664
670
  <ConfigForm
665
671
  node={currentNode}
666
- onSave={(updatedConfig, uiExtensions?: NodeUIExtensions) => {
672
+ onSave={async (updatedConfig, uiExtensions?: NodeUIExtensions) => {
667
673
  if (selectedNodeId && currentNode) {
668
674
  // Build the updated node data
669
675
  const updatedData = {
@@ -687,6 +693,13 @@
687
693
  // NOTE: We do NOT change the node's type field anymore
688
694
  // All nodes use 'universalNode' and UniversalNode handles internal switching
689
695
  workflowActions.updateNode(selectedNodeId, nodeUpdates);
696
+
697
+ // For gateway nodes (which have branches), refresh edge positions
698
+ // This fixes the bug where reordering branches doesn't update connections visually
699
+ const nodeType = currentNode.data?.metadata?.type;
700
+ if (nodeType === "gateway" && workflowEditorRef && selectedNodeId) {
701
+ await workflowEditorRef.refreshEdgePositions(selectedNodeId);
702
+ }
690
703
  }
691
704
 
692
705
  closeConfigSidebar();
@@ -131,7 +131,10 @@
131
131
  if (inputEl.id && !inputEl.id.startsWith('ext-')) {
132
132
  if (inputEl instanceof HTMLInputElement && inputEl.type === 'checkbox') {
133
133
  updatedConfig[inputEl.id] = inputEl.checked;
134
- } else if (inputEl instanceof HTMLInputElement && (inputEl.type === 'number' || inputEl.type === 'range')) {
134
+ } else if (
135
+ inputEl instanceof HTMLInputElement &&
136
+ (inputEl.type === 'number' || inputEl.type === 'range')
137
+ ) {
135
138
  updatedConfig[inputEl.id] = inputEl.value ? Number(inputEl.value) : inputEl.value;
136
139
  } else if (inputEl instanceof HTMLInputElement && inputEl.type === 'hidden') {
137
140
  // Parse hidden field values that might be JSON
@@ -19,10 +19,6 @@
19
19
  cancel: void;
20
20
  }>();
21
21
 
22
- function handleSave() {
23
- dispatch('save', { values: localConfigValues });
24
- }
25
-
26
22
  function handleCancel() {
27
23
  dispatch('cancel');
28
24
  }
@@ -79,29 +75,13 @@
79
75
  <ConfigForm
80
76
  schema={props.configSchema}
81
77
  values={localConfigValues}
82
- on:change={({ detail }) => {
83
- localConfigValues = detail.values;
78
+ showUIExtensions={false}
79
+ onSave={(config) => {
80
+ dispatch('save', { values: config });
84
81
  }}
82
+ onCancel={handleCancel}
85
83
  />
86
84
  </div>
87
-
88
- <!-- Modal Footer -->
89
- <div class="config-modal__footer">
90
- <button
91
- type="button"
92
- class="config-modal__btn config-modal__btn--secondary"
93
- onclick={handleCancel}
94
- >
95
- Cancel
96
- </button>
97
- <button
98
- type="button"
99
- class="config-modal__btn config-modal__btn--primary"
100
- onclick={handleSave}
101
- >
102
- Save Configuration
103
- </button>
104
- </div>
105
85
  </div>
106
86
  </div>
107
87
  {/if}
@@ -179,48 +159,6 @@
179
159
  min-height: 0;
180
160
  }
181
161
 
182
- .config-modal__footer {
183
- display: flex;
184
- align-items: center;
185
- justify-content: flex-end;
186
- gap: 0.75rem;
187
- padding: 1rem 1.5rem 1.5rem 1.5rem;
188
- border-top: 1px solid #e5e7eb;
189
- background-color: #f9fafb;
190
- }
191
-
192
- .config-modal__btn {
193
- padding: 0.5rem 1rem;
194
- border-radius: 0.375rem;
195
- font-size: 0.875rem;
196
- font-weight: 500;
197
- cursor: pointer;
198
- transition: all 0.2s;
199
- border: 1px solid transparent;
200
- }
201
-
202
- .config-modal__btn--primary {
203
- background-color: #3b82f6;
204
- color: white;
205
- border-color: #3b82f6;
206
- }
207
-
208
- .config-modal__btn--primary:hover {
209
- background-color: #2563eb;
210
- border-color: #2563eb;
211
- }
212
-
213
- .config-modal__btn--secondary {
214
- background-color: white;
215
- color: #374151;
216
- border-color: #d1d5db;
217
- }
218
-
219
- .config-modal__btn--secondary:hover {
220
- background-color: #f9fafb;
221
- border-color: #9ca3af;
222
- }
223
-
224
162
  /* Responsive adjustments */
225
163
  @media (max-width: 1024px) {
226
164
  .config-modal {
@@ -242,10 +180,6 @@
242
180
  .config-modal__content {
243
181
  padding: 1rem;
244
182
  }
245
-
246
- .config-modal__footer {
247
- padding: 0.75rem 1rem 1rem 1rem;
248
- }
249
183
  }
250
184
 
251
185
  @media (max-width: 640px) {
@@ -60,13 +60,7 @@
60
60
  <!-- Header -->
61
61
  <div class="config-panel__header">
62
62
  <h2 class="config-panel__title">{title}</h2>
63
- <button
64
- class="config-panel__close"
65
- onclick={onClose}
66
- aria-label="Close panel"
67
- >
68
- ×
69
- </button>
63
+ <button class="config-panel__close" onclick={onClose} aria-label="Close panel"> × </button>
70
64
  </div>
71
65
 
72
66
  <!-- Details Section (between header and content) -->
@@ -121,7 +115,9 @@
121
115
  color: #6b7280;
122
116
  padding: 0.25rem;
123
117
  border-radius: 0.25rem;
124
- transition: color 0.15s, background-color 0.15s;
118
+ transition:
119
+ color 0.15s,
120
+ background-color 0.15s;
125
121
  }
126
122
 
127
123
  .config-panel__close:hover {
@@ -157,4 +153,3 @@
157
153
  letter-spacing: 0.05em;
158
154
  }
159
155
  </style>
160
-
@@ -0,0 +1,42 @@
1
+ <!--
2
+ EdgeRefresher Component
3
+ Helper component that uses useUpdateNodeInternals to force edge recalculation
4
+ Must be rendered inside SvelteFlowProvider context
5
+ -->
6
+
7
+ <script lang="ts">
8
+ import { useUpdateNodeInternals } from '@xyflow/svelte';
9
+
10
+ interface Props {
11
+ /** Node ID to refresh - when this changes, edges are recalculated */
12
+ nodeIdToRefresh: string | null;
13
+ /** Callback when refresh is complete */
14
+ onRefreshComplete?: () => void;
15
+ }
16
+
17
+ let { nodeIdToRefresh, onRefreshComplete }: Props = $props();
18
+
19
+ /**
20
+ * Get the updateNodeInternals function from Svelte Flow context
21
+ * This recalculates handle positions and forces edge path updates
22
+ */
23
+ const updateNodeInternals = useUpdateNodeInternals();
24
+
25
+ /**
26
+ * Watch for nodeIdToRefresh changes and trigger edge recalculation
27
+ */
28
+ $effect(() => {
29
+ if (nodeIdToRefresh) {
30
+ // Tell Svelte Flow to recalculate node internals (handle positions)
31
+ updateNodeInternals(nodeIdToRefresh);
32
+
33
+ // Notify parent that refresh is complete
34
+ if (onRefreshComplete) {
35
+ onRefreshComplete();
36
+ }
37
+ }
38
+ });
39
+ </script>
40
+
41
+ <!-- This component renders nothing - it's just for the hook logic -->
42
+
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ /** Node ID to refresh - when this changes, edges are recalculated */
3
+ nodeIdToRefresh: string | null;
4
+ /** Callback when refresh is complete */
5
+ onRefreshComplete?: () => void;
6
+ }
7
+ declare const EdgeRefresher: import("svelte").Component<Props, {}, "">;
8
+ type EdgeRefresher = ReturnType<typeof EdgeRefresher>;
9
+ export default EdgeRefresher;
@@ -147,7 +147,9 @@
147
147
  display: flex;
148
148
  align-items: center;
149
149
  justify-content: center;
150
- transition: color 0.15s, background-color 0.15s;
150
+ transition:
151
+ color 0.15s,
152
+ background-color 0.15s;
151
153
  }
152
154
 
153
155
  .readonly-details__copy-btn:hover {
@@ -128,9 +128,12 @@
128
128
  </script>
129
129
 
130
130
  <div class="universal-node">
131
- <!-- Render the node component dynamically -->
132
- <!-- svelte-ignore binding_property_non_reactive -->
133
- <svelte:component this={nodeComponent} {data} {selected} />
131
+ <!-- Render the node component dynamically (Svelte 5 dynamic component syntax) -->
132
+ {#if nodeComponent}
133
+ <!-- svelte-ignore binding_property_non_reactive -->
134
+ {@const NodeComponent = nodeComponent}
135
+ <NodeComponent {data} {selected} />
136
+ {/if}
134
137
 
135
138
  <!-- Status overlay - only show if there's meaningful status information -->
136
139
  {#if shouldShowStatus}
@@ -23,6 +23,7 @@
23
23
  } from '../types/index.js';
24
24
  import CanvasBanner from './CanvasBanner.svelte';
25
25
  import FlowDropZone from './FlowDropZone.svelte';
26
+ import EdgeRefresher from './EdgeRefresher.svelte';
26
27
  import { tick } from 'svelte';
27
28
  import type { EndpointConfig } from '../config/endpoints.js';
28
29
  import ConnectionLine from './ConnectionLine.svelte';
@@ -359,9 +360,41 @@
359
360
  console.warn('No currentWorkflow available for new node');
360
361
  }
361
362
  }
363
+
364
+ /**
365
+ * Node ID that needs edge refresh - used to trigger EdgeRefresher component
366
+ */
367
+ let nodeIdToRefresh = $state<string | null>(null);
368
+
369
+ /**
370
+ * Force edge position recalculation after node config changes
371
+ * This should be called after saving gateway/switch node configs where branches are reordered
372
+ * Svelte Flow doesn't automatically recalculate edge paths when handle positions change
373
+ * @param nodeId - The ID of the node whose handles have changed position
374
+ */
375
+ export async function refreshEdgePositions(nodeId: string): Promise<void> {
376
+ // Wait for DOM to update with new handle positions
377
+ await tick();
378
+
379
+ // Trigger the EdgeRefresher component to call updateNodeInternals
380
+ nodeIdToRefresh = nodeId;
381
+ }
382
+
383
+ /**
384
+ * Callback when edge refresh is complete
385
+ */
386
+ function handleEdgeRefreshComplete(): void {
387
+ nodeIdToRefresh = null;
388
+ }
362
389
  </script>
363
390
 
364
391
  <SvelteFlowProvider>
392
+ <!-- EdgeRefresher component - handles updateNodeInternals calls -->
393
+ <EdgeRefresher
394
+ {nodeIdToRefresh}
395
+ onRefreshComplete={handleEdgeRefreshComplete}
396
+ />
397
+
365
398
  <div class="flowdrop-workflow-editor">
366
399
  <!-- Main Editor Area -->
367
400
  <div class="flowdrop-workflow-editor__main">
@@ -15,6 +15,8 @@ interface Props {
15
15
  nodeStatuses?: Record<string, 'pending' | 'running' | 'completed' | 'error'>;
16
16
  pipelineId?: string;
17
17
  }
18
- declare const WorkflowEditor: import("svelte").Component<Props, {}, "">;
18
+ declare const WorkflowEditor: import("svelte").Component<Props, {
19
+ refreshEdgePositions: (nodeId: string) => Promise<void>;
20
+ }, "">;
19
21
  type WorkflowEditor = ReturnType<typeof WorkflowEditor>;
20
22
  export default WorkflowEditor;
@@ -9,7 +9,7 @@
9
9
  -->
10
10
 
11
11
  <script lang="ts">
12
- import Icon from "@iconify/svelte";
12
+ import Icon from '@iconify/svelte';
13
13
 
14
14
  interface Props {
15
15
  /** Field identifier (used for ARIA) */
@@ -24,13 +24,7 @@
24
24
  onChange: (value: string[]) => void;
25
25
  }
26
26
 
27
- let {
28
- id,
29
- value = [],
30
- options = [],
31
- ariaDescribedBy,
32
- onChange
33
- }: Props = $props();
27
+ let { id, value = [], options = [], ariaDescribedBy, onChange }: Props = $props();
34
28
 
35
29
  /**
36
30
  * Handle checkbox toggle
@@ -149,4 +143,3 @@
149
143
  line-height: 1.4;
150
144
  }
151
145
  </style>
152
-