@d34dman/flowdrop 0.0.1 → 0.0.3

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 (120) hide show
  1. package/README.md +307 -215
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/WorkflowAdapter.js +30 -30
  4. package/dist/api/client.d.ts +24 -1
  5. package/dist/api/client.js +55 -38
  6. package/dist/api/enhanced-client.d.ts +46 -0
  7. package/dist/api/enhanced-client.js +211 -0
  8. package/dist/clients/ApiClient.d.ts +19 -23
  9. package/dist/clients/ApiClient.js +36 -34
  10. package/dist/components/App.svelte +1299 -230
  11. package/dist/components/App.svelte.d.ts +21 -1
  12. package/dist/components/CanvasBanner.svelte +50 -44
  13. package/dist/components/CanvasBanner.svelte.d.ts +5 -19
  14. package/dist/components/ConfigForm.svelte +555 -0
  15. package/dist/components/ConfigForm.svelte.d.ts +32 -0
  16. package/dist/components/ConfigModal.svelte +261 -0
  17. package/dist/components/ConfigModal.svelte.d.ts +31 -0
  18. package/dist/components/ConfigSidebar.svelte +934 -0
  19. package/dist/components/ConfigSidebar.svelte.d.ts +51 -0
  20. package/dist/components/ConnectionLine.svelte +32 -0
  21. package/dist/components/ConnectionLine.svelte.d.ts +3 -0
  22. package/dist/components/GatewayNode.svelte +471 -0
  23. package/dist/components/GatewayNode.svelte.d.ts +15 -0
  24. package/dist/components/LoadingSpinner.svelte +23 -23
  25. package/dist/components/LoadingSpinner.svelte.d.ts +1 -1
  26. package/dist/components/Logo.svelte +82 -0
  27. package/dist/components/Logo.svelte.d.ts +26 -0
  28. package/dist/components/LogsSidebar.svelte +565 -0
  29. package/dist/components/LogsSidebar.svelte.d.ts +34 -0
  30. package/dist/components/MarkdownDisplay.svelte +28 -0
  31. package/dist/components/MarkdownDisplay.svelte.d.ts +7 -0
  32. package/dist/components/Navbar.svelte +663 -0
  33. package/dist/components/Navbar.svelte.d.ts +21 -0
  34. package/dist/components/NodeSidebar.svelte +629 -488
  35. package/dist/components/NodeSidebar.svelte.d.ts +1 -2
  36. package/dist/components/NodeStatusOverlay.svelte +327 -0
  37. package/dist/components/NodeStatusOverlay.svelte.d.ts +11 -0
  38. package/dist/components/NotesNode.svelte +566 -0
  39. package/dist/components/NotesNode.svelte.d.ts +43 -0
  40. package/dist/components/PipelineStatus.svelte +331 -0
  41. package/dist/components/PipelineStatus.svelte.d.ts +18 -0
  42. package/dist/components/SimpleNode.svelte +447 -0
  43. package/dist/components/SimpleNode.svelte.d.ts +24 -0
  44. package/dist/components/SquareNode.svelte +346 -0
  45. package/dist/components/SquareNode.svelte.d.ts +24 -0
  46. package/dist/components/StatusIcon.svelte +112 -0
  47. package/dist/components/StatusIcon.svelte.d.ts +10 -0
  48. package/dist/components/StatusLabel.svelte +33 -0
  49. package/dist/components/StatusLabel.svelte.d.ts +7 -0
  50. package/dist/components/ToolNode.svelte +385 -0
  51. package/dist/components/ToolNode.svelte.d.ts +36 -0
  52. package/dist/components/UniversalNode.svelte +126 -0
  53. package/dist/components/UniversalNode.svelte.d.ts +15 -0
  54. package/dist/components/WorkflowEditor.svelte +871 -528
  55. package/dist/components/WorkflowEditor.svelte.d.ts +15 -5
  56. package/dist/components/WorkflowNode.svelte +428 -542
  57. package/dist/components/WorkflowNode.svelte.d.ts +7 -3
  58. package/dist/config/apiConfig.d.ts +33 -0
  59. package/dist/config/apiConfig.js +39 -0
  60. package/dist/config/defaultPortConfig.d.ts +6 -0
  61. package/dist/config/defaultPortConfig.js +192 -0
  62. package/dist/config/demo.d.ts +58 -0
  63. package/dist/config/demo.js +142 -0
  64. package/dist/config/endpoints.d.ts +106 -0
  65. package/dist/config/endpoints.js +128 -0
  66. package/dist/data/samples.d.ts +38 -4
  67. package/dist/data/samples.js +2789 -737
  68. package/dist/examples/adapter-usage.d.ts +4 -4
  69. package/dist/examples/adapter-usage.js +21 -26
  70. package/dist/examples/api-client-usage.d.ts +6 -6
  71. package/dist/examples/api-client-usage.js +55 -54
  72. package/dist/index.d.ts +23 -15
  73. package/dist/index.js +23 -15
  74. package/dist/mocks/app-environment.d.ts +8 -0
  75. package/dist/mocks/app-environment.js +16 -0
  76. package/dist/mocks/app-forms.d.ts +2 -0
  77. package/dist/mocks/app-forms.js +21 -0
  78. package/dist/mocks/app-navigation.d.ts +5 -0
  79. package/dist/mocks/app-navigation.js +34 -0
  80. package/dist/mocks/app-stores.d.ts +14 -0
  81. package/dist/mocks/app-stores.js +26 -0
  82. package/dist/services/api.d.ts +13 -3
  83. package/dist/services/api.js +91 -36
  84. package/dist/services/globalSave.d.ts +20 -0
  85. package/dist/services/globalSave.js +165 -0
  86. package/dist/services/nodeExecutionService.d.ts +63 -0
  87. package/dist/services/nodeExecutionService.js +261 -0
  88. package/dist/services/portConfigApi.d.ts +14 -0
  89. package/dist/services/portConfigApi.js +69 -0
  90. package/dist/services/toastService.d.ts +147 -0
  91. package/dist/services/toastService.js +235 -0
  92. package/dist/services/workflowStorage.d.ts +2 -2
  93. package/dist/services/workflowStorage.js +10 -10
  94. package/dist/stores/workflowStore.d.ts +53 -0
  95. package/dist/stores/workflowStore.js +264 -0
  96. package/dist/styles/base.css +896 -363
  97. package/dist/svelte-app.d.ts +52 -5
  98. package/dist/svelte-app.js +128 -6
  99. package/dist/types/config.d.ts +291 -0
  100. package/dist/types/config.js +4 -0
  101. package/dist/types/index.d.ts +231 -19
  102. package/dist/types/index.js +1 -1
  103. package/dist/utils/colors.d.ts +67 -33
  104. package/dist/utils/colors.js +183 -118
  105. package/dist/utils/config.d.ts +41 -0
  106. package/dist/utils/config.js +248 -0
  107. package/dist/utils/connections.d.ts +40 -3
  108. package/dist/utils/connections.js +115 -44
  109. package/dist/utils/icons.d.ts +1 -1
  110. package/dist/utils/icons.js +71 -70
  111. package/dist/utils/nodeStatus.d.ts +53 -0
  112. package/dist/utils/nodeStatus.js +183 -0
  113. package/dist/utils/nodeTypes.d.ts +57 -0
  114. package/dist/utils/nodeTypes.js +109 -0
  115. package/dist/utils/nodeWrapper.d.ts +39 -0
  116. package/dist/utils/nodeWrapper.js +62 -0
  117. package/package.json +132 -97
  118. package/dist/app.css +0 -0
  119. package/dist/components/Node.svelte +0 -38
  120. package/dist/components/Node.svelte.d.ts +0 -4
@@ -5,233 +5,1302 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
- import { onMount } from "svelte";
9
- import WorkflowEditor from "./WorkflowEditor.svelte";
10
- import { api } from "../services/api.js";
11
- import type { NodeMetadata, Workflow } from "../types/index.js";
12
- import { getDefaultIcon } from '../utils/icons.js';
13
-
14
- let nodes = $state<NodeMetadata[]>([]);
15
- let workflow = $state<Workflow | undefined>(undefined);
16
- let error = $state<string | null>(null);
17
- let loading = $state(true);
18
-
19
- /**
20
- * Fetch node types from the server
21
- */
22
- async function fetchNodeTypes(): Promise<void> {
23
- try {
24
- loading = true;
25
- error = null;
26
-
27
- console.log("Fetching node types from server...");
28
- const fetchedNodes = await api.nodes.getNodes();
29
-
30
- console.log("✅ Fetched", fetchedNodes.length, "node types from server");
31
- nodes = fetchedNodes;
32
-
33
- } catch (err) {
34
- console.error("❌ Failed to fetch node types:", err);
35
- error = err instanceof Error ? err.message : "Failed to load node types";
36
- } finally {
37
- loading = false;
38
- }
39
- }
40
-
41
- /**
42
- * Retry loading node types
43
- */
44
- function retryLoad(): void {
45
- fetchNodeTypes();
46
- }
47
-
48
- // Load node types on mount
49
- onMount(() => {
50
- fetchNodeTypes();
51
- });
52
-
53
- /**
54
- * Handle workflow export
55
- */
56
- function handleWorkflowExport(): void {
57
- if (!workflow) return;
58
-
59
- const dataStr = JSON.stringify(workflow, null, 2);
60
- const dataBlob = new Blob([dataStr], { type: "application/json" });
61
- const url = URL.createObjectURL(dataBlob);
62
- const link = document.createElement("a");
63
- link.href = url;
64
- link.download = `${workflow.name}.json`;
65
- link.click();
66
- URL.revokeObjectURL(url);
67
- }
68
- </script>
69
-
70
- <svelte:head>
71
- <title>FlowDrop - LLM Workflow Editor</title>
72
- <meta name="description" content="A modern drag-and-drop workflow editor for LLM applications" />
73
- </svelte:head>
74
-
75
- <div class="flowdrop-app" style="min-height: 1200px;">
76
- <!-- Header -->
77
- <div class="flowdrop-navbar">
78
- <div class="flowdrop-navbar__start">
79
- <!-- Logo and Title -->
80
- <div class="flowdrop-flex flowdrop-gap--3">
81
- <div class="flowdrop-logo flowdrop-logo--header">
82
- FD
83
- </div>
84
- <div>
85
- <h1 class="flowdrop-text--lg flowdrop-font--bold">FlowDrop</h1>
86
- <p class="flowdrop-text--xs flowdrop-text--gray">LLM Workflow Editor</p>
87
- </div>
88
- </div>
89
- </div>
90
-
91
- <div class="flowdrop-navbar__center">
92
- <!-- TODO: Add navigation -->
93
- </div>
94
-
95
- <div class="flowdrop-navbar__end">
96
- <!-- TODO: Add user menu -->
97
- </div>
98
- </div>
99
-
100
- <!-- Main Content -->
101
- <main class="flowdrop-main" style="height: calc(100vh - 60px);">
102
- <!-- Status Display -->
103
- {#if loading}
104
- <div class="flowdrop-status flowdrop-status--loading">
105
- <div class="flowdrop-status__content">
106
- <div class="flowdrop-flex flowdrop-gap--3">
107
- <div class="flowdrop-spinner"></div>
108
- <span class="flowdrop-text--sm flowdrop-font--medium">Loading node types...</span>
109
- </div>
110
- </div>
111
- </div>
112
- {:else if error}
113
- <div class="flowdrop-status flowdrop-status--error">
114
- <div class="flowdrop-status__content">
115
- <div class="flowdrop-flex flowdrop-gap--3">
116
- <div class="flowdrop-status__indicator flowdrop-status__indicator--error"></div>
117
- <span class="flowdrop-text--sm flowdrop-font--medium">Error: {error}</span>
118
- </div>
119
- <div class="flowdrop-flex flowdrop-gap--2">
120
- <button
121
- class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline"
122
- onclick={retryLoad}
123
- type="button"
124
- >
125
- Retry
126
- </button>
127
- <button
128
- class="flowdrop-btn flowdrop-btn--ghost flowdrop-btn--sm"
129
- onclick={() => error = null}
130
- type="button"
131
- >
132
-
133
- </button>
134
- </div>
135
- </div>
136
- </div>
137
- {/if}
138
-
139
- <!-- Workflow Editor -->
140
- <div class="flowdrop-editor-container" style="height: {(loading || error) ? 'calc(100% - 60px)' : '100%'};">
141
- <WorkflowEditor
142
- nodes={nodes}
143
- workflow={workflow}
144
- />
145
- </div>
146
- </main>
147
- </div>
148
-
149
- <style>
150
- .flowdrop-app {
151
- background: linear-gradient(135deg, #f9fafb 0%, #e0e7ff 50%, #c7d2fe 100%);
152
- }
153
-
154
- .flowdrop-navbar {
155
- height: 60px;
156
- display: flex;
157
- align-items: center;
158
- justify-content: space-between;
159
- padding: 0 1rem;
160
- background-color: #ffffff;
161
- border-bottom: 1px solid #e5e7eb;
162
- }
163
-
164
- .flowdrop-navbar__start {
165
- display: flex;
166
- align-items: center;
167
- }
168
-
169
- .flowdrop-logo {
170
- width: 2rem;
171
- height: 2rem;
172
- background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
173
- border-radius: 0.5rem;
174
- display: flex;
175
- align-items: center;
176
- justify-content: center;
177
- color: #ffffff;
178
- font-weight: 700;
179
- font-size: 0.875rem;
180
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
181
- }
182
-
183
- .flowdrop-logo--header {
184
- width: 40px;
185
- height: 40px;
186
- font-size: 1.25rem;
187
- margin-top: 15px;
188
- }
189
-
190
- .flowdrop-main {
191
- position: relative;
192
- }
193
-
194
- .flowdrop-status {
195
- background-color: #eff6ff;
196
- border-bottom: 1px solid #bfdbfe;
197
- padding: 1rem;
198
- }
199
-
200
- .flowdrop-status--error {
201
- background-color: #fef2f2;
202
- border-bottom: 1px solid #fecaca;
203
- }
204
-
205
- .flowdrop-status__content {
206
- max-width: 80rem;
207
- margin: 0 auto;
208
- display: flex;
209
- align-items: center;
210
- justify-content: space-between;
211
- }
212
-
213
- .flowdrop-status__indicator {
214
- width: 0.5rem;
215
- height: 0.5rem;
216
- border-radius: 50%;
217
- }
218
-
219
- .flowdrop-status__indicator--error {
220
- background-color: #ef4444;
221
- }
222
-
223
- .flowdrop-editor-container {
224
- position: relative;
225
- }
226
-
227
- /* Ensure full height for screens larger than 1200px */
228
- @media (min-height: 1200px) {
229
- :global(html), :global(body) {
230
- height: 100%;
231
- }
232
-
233
- :global(#svelte) {
234
- height: 100%;
235
- }
236
- }
237
- </style>
8
+ import { onMount, tick } from 'svelte';
9
+ import { page } from '$app/stores';
10
+ import WorkflowEditor from './WorkflowEditor.svelte';
11
+ import NodeSidebar from './NodeSidebar.svelte';
12
+ import ConfigSidebar from './ConfigSidebar.svelte';
13
+ import Navbar from './Navbar.svelte';
14
+ import { api, setEndpointConfig } from '../services/api.js';
15
+ import type { NodeMetadata, Workflow, WorkflowNode, ConfigSchema } from '../types/index.js';
16
+ import { sampleNodes } from '../data/samples.js';
17
+ import { createEndpointConfig } from '../config/endpoints.js';
18
+ import type { EndpointConfig } from '../config/endpoints.js';
19
+ import { workflowStore, workflowActions, workflowName } from '../stores/workflowStore.js';
20
+ import { resolveComponentName } from '../utils/nodeTypes.js';
21
+ import { apiToasts, dismissToast } from '../services/toastService.js';
22
+
23
+ // Configuration props for runtime customization
24
+ interface Props {
25
+ workflow?: Workflow;
26
+ height?: string | number;
27
+ width?: string | number;
28
+ showNavbar?: boolean;
29
+ // New configuration options for pipeline status mode
30
+ disableSidebar?: boolean;
31
+ lockWorkflow?: boolean;
32
+ readOnly?: boolean;
33
+ nodeStatuses?: Record<string, 'pending' | 'running' | 'completed' | 'error'>;
34
+ // Pipeline ID for fetching node execution info from jobs
35
+ pipelineId?: string;
36
+ // Navbar customization
37
+ navbarTitle?: string;
38
+ navbarActions?: Array<{
39
+ label: string;
40
+ href: string;
41
+ icon?: string;
42
+ variant?: 'primary' | 'secondary' | 'outline';
43
+ onclick?: (event: Event) => void;
44
+ }>;
45
+ }
46
+
47
+ let {
48
+ workflow: initialWorkflow,
49
+ height = '100vh',
50
+ width = '100%',
51
+ showNavbar = false,
52
+ disableSidebar = false,
53
+ lockWorkflow = false,
54
+ readOnly = false,
55
+ nodeStatuses = {},
56
+ pipelineId,
57
+ navbarTitle,
58
+ navbarActions = []
59
+ }: Props = $props();
60
+
61
+ // Create breadcrumb-style title - at top level to avoid store subscription issues
62
+ let breadcrumbTitle = $derived(() => {
63
+ // Use custom navbar title if provided
64
+ if (navbarTitle) {
65
+ return navbarTitle;
66
+ }
67
+ // Default workflow title logic
68
+ if (!$workflowName || $workflowName === 'Untitled Workflow') {
69
+ return 'Workflow / New Workflow';
70
+ }
71
+ return `Workflow / ${$workflowName}`;
72
+ });
73
+
74
+ let nodes = $state<NodeMetadata[]>([]);
75
+ // Remove workflow prop - use global store directly
76
+ // let workflow = $derived($workflowStore || initialWorkflow);
77
+ let error = $state<string | null>(null);
78
+ let loading = $state(true);
79
+ let endpointConfig = $state<EndpointConfig | null>(null);
80
+
81
+ // ConfigSidebar state
82
+ let isConfigSidebarOpen = $state(false);
83
+ let selectedNodeId = $state<string | null>(null);
84
+
85
+ // Workflow settings sidebar state
86
+ let isWorkflowSettingsOpen = $state(false);
87
+
88
+ // Workflow configuration schema
89
+ const workflowConfigSchema: ConfigSchema = {
90
+ type: 'object',
91
+ properties: {
92
+ name: {
93
+ type: 'string',
94
+ title: 'Workflow Name',
95
+ description: 'The name of the workflow',
96
+ default: ''
97
+ },
98
+ description: {
99
+ type: 'string',
100
+ title: 'Description',
101
+ description: 'A description of the workflow',
102
+ default: ''
103
+ }
104
+ },
105
+ required: ['name']
106
+ };
107
+
108
+ // Workflow configuration values
109
+ let workflowConfigValues = $derived({
110
+ name: $workflowName || '',
111
+ description: $workflowStore?.description || ''
112
+ });
113
+
114
+ // Get the current node from the workflow store
115
+ let selectedNodeForConfig = $derived(() => {
116
+ if (!selectedNodeId || !$workflowStore) return null;
117
+ return $workflowStore.nodes.find((node) => node.id === selectedNodeId) || null;
118
+ });
119
+
120
+ // WorkflowEditor reference for save functionality
121
+ let workflowEditorRef: WorkflowEditor | null = null;
122
+
123
+ // Removed currentWorkflowState - no longer needed
124
+ // The global store ($workflowStore) serves as the single source of truth
125
+
126
+ /**
127
+ * Fetch node types from the server
128
+ */
129
+ async function fetchNodeTypes(): Promise<void> {
130
+ // Show loading toast
131
+ const loadingToast = apiToasts.loading('Loading node types');
132
+ try {
133
+ loading = true;
134
+ error = null;
135
+
136
+ const fetchedNodes = await api.nodes.getNodes();
137
+
138
+ nodes = fetchedNodes;
139
+ error = null;
140
+
141
+ // Dismiss loading toast
142
+ dismissToast(loadingToast);
143
+ } catch (err) {
144
+ // Dismiss loading toast and show error toast
145
+ dismissToast(loadingToast);
146
+ // Show error but don't block the UI
147
+ error = `API Error: ${err instanceof Error ? err.message : 'Unknown error'}. Using sample data.`;
148
+ apiToasts.error('Load node types', err instanceof Error ? err.message : 'Unknown error');
149
+
150
+ // Fallback to sample data
151
+ nodes = sampleNodes;
152
+ } finally {
153
+ loading = false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Retry loading node types
159
+ */
160
+ function retryLoad(): void {
161
+ fetchNodeTypes();
162
+ }
163
+
164
+ /**
165
+ * Test API connection
166
+ */
167
+ async function testApiConnection(): Promise<void> {
168
+ try {
169
+ const testUrl = '/api/flowdrop/nodes';
170
+
171
+ const response = await fetch(testUrl);
172
+ const data = await response.json();
173
+
174
+ if (response.ok && data.success) {
175
+ apiToasts.success('API connection test', 'Connection successful');
176
+ } else {
177
+ apiToasts.error('API connection test', 'Connection failed');
178
+ }
179
+ } catch (err) {
180
+ apiToasts.error('API connection test', err instanceof Error ? err.message : 'Unknown error');
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Initialize API endpoints
186
+ */
187
+ async function initializeApiEndpoints(): Promise<void> {
188
+ // Use the same environment variable priority as the global save function
189
+ // Prioritize VITE_API_BASE_URL since it's configured correctly
190
+ const apiBaseUrl =
191
+ import.meta.env.VITE_API_BASE_URL || import.meta.env.VITE_DRUPAL_API_URL || '/api/flowdrop';
192
+
193
+ const config = createEndpointConfig(apiBaseUrl, {
194
+ auth: {
195
+ type: 'none' // No authentication for now
196
+ },
197
+ timeout: 10000, // 10 second timeout
198
+ retry: {
199
+ enabled: true,
200
+ maxAttempts: 2,
201
+ delay: 1000,
202
+ backoff: 'exponential'
203
+ }
204
+ });
205
+
206
+ setEndpointConfig(config);
207
+ // Store the configuration for passing to WorkflowEditor
208
+ endpointConfig = config;
209
+ }
210
+
211
+ /**
212
+ * ConfigSidebar functions
213
+ */
214
+ function openConfigSidebar(node: WorkflowNode): void {
215
+ // Close if already open for the same node
216
+ if (isConfigSidebarOpen && selectedNodeId === node.id) {
217
+ closeConfigSidebar();
218
+ return;
219
+ }
220
+ selectedNodeId = node.id;
221
+ isConfigSidebarOpen = true;
222
+ }
223
+
224
+ function closeConfigSidebar(): void {
225
+ isConfigSidebarOpen = false;
226
+ selectedNodeId = null;
227
+ }
228
+
229
+ /**
230
+ * Toggle workflow settings sidebar
231
+ */
232
+ function toggleWorkflowSettings(): void {
233
+ isWorkflowSettingsOpen = !isWorkflowSettingsOpen;
234
+ // Close config sidebar if opening workflow settings
235
+ if (isWorkflowSettingsOpen) {
236
+ closeConfigSidebar();
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Handle workflow configuration save
242
+ */
243
+ async function handleWorkflowSave(config: any): Promise<void> {
244
+ console.log('Workflow configuration saved:', config);
245
+
246
+ // Update the workflow store
247
+ if ($workflowStore) {
248
+ $workflowStore.name = config.name;
249
+ $workflowStore.description = config.description;
250
+ }
251
+
252
+ // Close the sidebar
253
+ isWorkflowSettingsOpen = false;
254
+
255
+ // Also save the workflow to the backend
256
+ try {
257
+ await saveWorkflow();
258
+ console.log('Workflow saved to backend successfully');
259
+ } catch (error) {
260
+ console.error('Failed to save workflow to backend:', error);
261
+ // Note: We don't throw the error here to avoid breaking the UI flow
262
+ // The user can still manually save via the main Save button if needed
263
+ }
264
+ }
265
+
266
+ // Removed handleWorkflowChange function - no longer needed
267
+ // The global store serves as the single source of truth and is already reactive
268
+
269
+ /**
270
+ * Save workflow - exposed API function
271
+ */
272
+ async function saveWorkflow(): Promise<void> {
273
+ try {
274
+ // Wait for any pending DOM updates before saving
275
+ await tick();
276
+
277
+ // Import necessary modules
278
+ const { workflowApi } = await import('../services/api.js');
279
+ const { v4: uuidv4 } = await import('uuid');
280
+
281
+ // Use current workflow from global store
282
+ const workflowToSave = $workflowStore;
283
+
284
+ if (!workflowToSave) {
285
+ return;
286
+ }
287
+
288
+ // Determine the workflow ID
289
+ let workflowId: string;
290
+ if (workflowToSave.id) {
291
+ workflowId = workflowToSave.id;
292
+ } else {
293
+ workflowId = uuidv4();
294
+ }
295
+
296
+ // Create workflow object for saving
297
+ const finalWorkflow = {
298
+ id: workflowId,
299
+ name: workflowToSave.name || 'Untitled Workflow',
300
+ description: workflowToSave.description || '',
301
+ nodes: workflowToSave.nodes || [],
302
+ edges: workflowToSave.edges || [],
303
+ metadata: {
304
+ version: '1.0.0',
305
+ createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
306
+ updatedAt: new Date().toISOString()
307
+ }
308
+ };
309
+
310
+ const savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
311
+
312
+ // Update the workflow ID if it changed (new workflow)
313
+ // Keep our current workflow state, only update ID and metadata from Drupal
314
+ if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
315
+ workflowActions.batchUpdate({
316
+ nodes: finalWorkflow.nodes,
317
+ edges: finalWorkflow.edges,
318
+ name: finalWorkflow.name,
319
+ metadata: {
320
+ ...finalWorkflow.metadata,
321
+ ...savedWorkflow.metadata
322
+ }
323
+ });
324
+ }
325
+ } catch (error) {
326
+ throw error; // Re-throw so caller can handle
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Export workflow - exposed API function
332
+ */
333
+ async function exportWorkflow(): Promise<void> {
334
+ try {
335
+ // Wait for any pending DOM updates before exporting
336
+ await tick();
337
+
338
+ // Use current workflow from global store
339
+ const workflowToExport = $workflowStore;
340
+
341
+ if (!workflowToExport) {
342
+ return;
343
+ }
344
+
345
+ // Create workflow object for export
346
+ const finalWorkflow = {
347
+ id: workflowToExport.id || 'untitled-workflow',
348
+ name: workflowToExport.name || 'Untitled Workflow',
349
+ nodes: workflowToExport.nodes || [],
350
+ edges: workflowToExport.edges || [],
351
+ metadata: {
352
+ version: '1.0.0',
353
+ createdAt: workflowToExport.metadata?.createdAt || new Date().toISOString(),
354
+ updatedAt: new Date().toISOString()
355
+ }
356
+ };
357
+
358
+ // Create and download the file
359
+ const dataStr = JSON.stringify(finalWorkflow, null, 2);
360
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
361
+ const url = URL.createObjectURL(dataBlob);
362
+ const link = document.createElement('a');
363
+ link.href = url;
364
+ link.download = `${finalWorkflow.name}.json`;
365
+ link.click();
366
+ URL.revokeObjectURL(url);
367
+ } catch (error) {
368
+ // Export failed
369
+ }
370
+ }
371
+
372
+ // Expose save and export functions globally for external access
373
+ if (typeof window !== 'undefined') {
374
+ // @ts-ignore - Adding to window for external access
375
+ window.flowdropSave = saveWorkflow;
376
+ // @ts-ignore - Adding to window for external access
377
+ window.flowdropExport = exportWorkflow;
378
+ }
379
+
380
+ // Function to handle clicks outside the sidebar
381
+ function handleCanvasClick(event: MouseEvent): void {
382
+ // Check if the click is outside the right sidebar
383
+ const rightSidebar = document.querySelector('.flowdrop-sidebar--right');
384
+ if (rightSidebar && !rightSidebar.contains(event.target as Node)) {
385
+ // Close sidebar when clicking outside of it
386
+ if (isConfigSidebarOpen) {
387
+ closeConfigSidebar();
388
+ }
389
+ }
390
+ }
391
+
392
+ // Load node types on mount
393
+ onMount(() => {
394
+ (async () => {
395
+ await initializeApiEndpoints();
396
+ await fetchNodeTypes();
397
+
398
+ // Initialize the workflow store if we have an initial workflow
399
+ if (initialWorkflow) {
400
+ workflowActions.initialize(initialWorkflow);
401
+ }
402
+ })();
403
+
404
+ // Listen for workflow settings toggle from main navbar
405
+ const handleWorkflowSettingsToggle = () => {
406
+ toggleWorkflowSettings();
407
+ };
408
+
409
+ window.addEventListener('workflow-settings-toggle', handleWorkflowSettingsToggle);
410
+
411
+ return () => {
412
+ window.removeEventListener('workflow-settings-toggle', handleWorkflowSettingsToggle);
413
+ };
414
+ });
415
+
416
+ // Monitor workflow store changes for testing node drag updates
417
+ $effect(() => {
418
+ const currentWorkflow = $workflowStore;
419
+ if (currentWorkflow) {
420
+ // Workflow store updated
421
+ }
422
+ });
423
+ </script>
424
+
425
+ <svelte:head>
426
+ <title>FlowDrop - Visual Workflow Manager</title>
427
+ <meta name="description" content="A modern drag-and-drop workflow editor for LLM applications" />
428
+ </svelte:head>
429
+
430
+ <div
431
+ class="flowdrop-app"
432
+ style="height: {typeof height === 'number' ? `${height}px` : height}; width: {typeof width ===
433
+ 'number'
434
+ ? `${width}px`
435
+ : width};"
436
+ >
437
+ <!-- Navbar (conditionally rendered) - hide on workflow edit pages -->
438
+ {#if showNavbar && !$page.url.pathname.includes('/edit')}
439
+ <Navbar
440
+ title={breadcrumbTitle()}
441
+ primaryActions={navbarActions.length > 0
442
+ ? navbarActions
443
+ : [
444
+ {
445
+ label: 'Save',
446
+ href: '#save',
447
+ icon: 'heroicons:document-arrow-down',
448
+ variant: 'primary',
449
+ onclick: (e) => {
450
+ e.preventDefault();
451
+ saveWorkflow();
452
+ }
453
+ },
454
+ {
455
+ label: 'Export',
456
+ href: '#export',
457
+ icon: 'heroicons:arrow-down-tray',
458
+ variant: 'outline',
459
+ onclick: (e) => {
460
+ e.preventDefault();
461
+ exportWorkflow();
462
+ }
463
+ },
464
+ {
465
+ label: 'Workflow Settings',
466
+ href: '#settings',
467
+ icon: 'heroicons:cog-6-tooth',
468
+ variant: 'outline',
469
+ onclick: (e) => {
470
+ e.preventDefault();
471
+ toggleWorkflowSettings();
472
+ }
473
+ }
474
+ ]}
475
+ showStatus={true}
476
+ />
477
+ {/if}
478
+
479
+ <!-- Main Content -->
480
+ <main class="flowdrop-main">
481
+ <!-- Status Display -->
482
+ {#if error}
483
+ <div class="flowdrop-status flowdrop-status--error">
484
+ <div class="flowdrop-status__content">
485
+ <div class="flowdrop-flex flowdrop-gap--3">
486
+ <div class="flowdrop-status__indicator flowdrop-status__indicator--error"></div>
487
+ <span class="flowdrop-text--sm flowdrop-font--medium">Error: {error}</span>
488
+ </div>
489
+ <div class="flowdrop-flex flowdrop-gap--2">
490
+ <button
491
+ class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline"
492
+ onclick={retryLoad}
493
+ type="button"
494
+ >
495
+ Retry
496
+ </button>
497
+ <button
498
+ class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--primary"
499
+ onclick={() => {
500
+ nodes = sampleNodes;
501
+ error = null;
502
+ }}
503
+ type="button"
504
+ >
505
+ Use Sample Data
506
+ </button>
507
+ <button
508
+ class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline"
509
+ onclick={() => {
510
+ const defaultUrl = '/api/flowdrop';
511
+ const newUrl = prompt('Enter Drupal API URL:', defaultUrl);
512
+ if (newUrl) {
513
+ const endpointConfig = createEndpointConfig(newUrl);
514
+ setEndpointConfig(endpointConfig);
515
+ fetchNodeTypes();
516
+ }
517
+ }}
518
+ type="button"
519
+ >
520
+ Set API URL
521
+ </button>
522
+ <button
523
+ class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline"
524
+ onclick={testApiConnection}
525
+ type="button"
526
+ >
527
+ Test API
528
+ </button>
529
+ <button
530
+ class="flowdrop-btn flowdrop-btn--ghost flowdrop-btn--sm"
531
+ onclick={() => (error = null)}
532
+ type="button"
533
+ >
534
+
535
+ </button>
536
+ </div>
537
+ </div>
538
+ </div>
539
+ {/if}
540
+
541
+ <!-- Workflow Editor with Sidebars -->
542
+ <div class="flowdrop-editor-container">
543
+ <!-- Left Sidebar - Node Components (conditionally rendered) -->
544
+ {#if !disableSidebar}
545
+ <div class="flowdrop-sidebar flowdrop-sidebar--left">
546
+ <NodeSidebar {nodes} />
547
+ </div>
548
+ {/if}
549
+
550
+ <!-- Main Editor Area -->
551
+ <div
552
+ class="flowdrop-editor-main"
553
+ class:pipeline-view={!!pipelineId}
554
+ onclick={handleCanvasClick}
555
+ onkeydown={(e) => e.key === 'Escape' && closeConfigSidebar()}
556
+ role="button"
557
+ tabindex="0"
558
+ aria-label="Workflow canvas - click to close sidebar"
559
+ >
560
+ <WorkflowEditor
561
+ bind:this={workflowEditorRef}
562
+ {nodes}
563
+ {height}
564
+ {width}
565
+ {endpointConfig}
566
+ {isConfigSidebarOpen}
567
+ selectedNodeForConfig={selectedNodeForConfig()}
568
+ {openConfigSidebar}
569
+ {closeConfigSidebar}
570
+ {lockWorkflow}
571
+ {readOnly}
572
+ {nodeStatuses}
573
+ {pipelineId}
574
+ />
575
+ </div>
576
+
577
+ <!-- Right Sidebar - Configuration or Workflow Settings (conditionally rendered) -->
578
+ {#if !disableSidebar && isWorkflowSettingsOpen}
579
+ <ConfigSidebar
580
+ isOpen={isWorkflowSettingsOpen}
581
+ title="Workflow Settings"
582
+ configSchema={workflowConfigSchema}
583
+ configValues={workflowConfigValues}
584
+ onSave={handleWorkflowSave}
585
+ onClose={() => (isWorkflowSettingsOpen = false)}
586
+ />
587
+ {:else if !disableSidebar && selectedNodeForConfig()}
588
+ <div class="flowdrop-sidebar flowdrop-sidebar--right">
589
+ <div class="flowdrop-config-sidebar">
590
+ <!-- Header -->
591
+ <div class="flowdrop-config-sidebar__header">
592
+ <h2 class="flowdrop-config-sidebar__title">{selectedNodeForConfig().data.label}</h2>
593
+ <button
594
+ class="flowdrop-config-sidebar__close"
595
+ onclick={closeConfigSidebar}
596
+ aria-label="Close configuration sidebar"
597
+ >
598
+ ×
599
+ </button>
600
+ </div>
601
+
602
+ <!-- Content -->
603
+ <div class="flowdrop-config-sidebar__content">
604
+ <!-- Node Details -->
605
+ <div class="flowdrop-config-sidebar__section">
606
+ <h3 class="flowdrop-config-sidebar__section-title">Node Details</h3>
607
+ <div class="flowdrop-config-sidebar__details">
608
+ <div class="flowdrop-config-sidebar__detail">
609
+ <span class="flowdrop-config-sidebar__detail-label">Type:</span>
610
+ <span class="flowdrop-config-sidebar__detail-value"
611
+ >{selectedNodeForConfig().data.metadata?.type ||
612
+ selectedNodeForConfig().type}</span
613
+ >
614
+ </div>
615
+ <div class="flowdrop-config-sidebar__detail">
616
+ <span class="flowdrop-config-sidebar__detail-label">Category:</span>
617
+ <span class="flowdrop-config-sidebar__detail-value"
618
+ >{selectedNodeForConfig().data.metadata?.category || 'general'}</span
619
+ >
620
+ </div>
621
+ <div class="flowdrop-config-sidebar__detail">
622
+ <span class="flowdrop-config-sidebar__detail-label">Description:</span>
623
+ <p class="flowdrop-config-sidebar__detail-description">
624
+ {selectedNodeForConfig().data.metadata?.description || 'Node configuration'}
625
+ </p>
626
+ </div>
627
+ </div>
628
+ </div>
629
+
630
+ <!-- Configuration Form -->
631
+ <div class="flowdrop-config-sidebar__section">
632
+ <h3 class="flowdrop-config-sidebar__section-title">Configuration</h3>
633
+ <div class="flowdrop-config-sidebar__form">
634
+ {#if selectedNodeForConfig().data.metadata?.configSchema}
635
+ <!-- Debug: Log the config schema -->
636
+ {@const configSchema = selectedNodeForConfig().data.metadata.configSchema}
637
+ {@const nodeConfig = selectedNodeForConfig().data.config || {}}
638
+ {@const configValues = (() => {
639
+ // Create a config object that merges defaults with existing values
640
+ const mergedConfig = {};
641
+ if (configSchema.properties) {
642
+ Object.entries(configSchema.properties).forEach(([key, field]) => {
643
+ const fieldConfig = field as any;
644
+ // Use existing value if available, otherwise use default
645
+ mergedConfig[key] =
646
+ nodeConfig[key] !== undefined ? nodeConfig[key] : fieldConfig.default;
647
+ });
648
+ }
649
+ return mergedConfig;
650
+ })()}
651
+
652
+ <!-- Render configuration fields based on schema -->
653
+ {#if configSchema.properties}
654
+ {#each Object.entries(configSchema.properties) as [key, field]}
655
+ {@const fieldConfig = field as any}
656
+ {#if fieldConfig.format !== 'hidden'}
657
+ <div class="flowdrop-config-sidebar__field">
658
+ <label class="flowdrop-config-sidebar__field-label" for={key}>
659
+ {fieldConfig.title || fieldConfig.description || key}
660
+ </label>
661
+ {#if fieldConfig.enum && fieldConfig.multiple}
662
+ <!-- Checkboxes for enum with multiple selection -->
663
+ <div class="flowdrop-config-sidebar__checkbox-group">
664
+ {#each fieldConfig.enum as option}
665
+ <label class="flowdrop-config-sidebar__checkbox-item">
666
+ <input
667
+ type="checkbox"
668
+ class="flowdrop-config-sidebar__checkbox"
669
+ value={String(option)}
670
+ checked={Array.isArray(configValues[key]) &&
671
+ configValues[key].includes(String(option))}
672
+ onchange={(e) => {
673
+ const checked = e.currentTarget.checked;
674
+ const currentValues = Array.isArray(configValues[key])
675
+ ? [...configValues[key]]
676
+ : [];
677
+ if (checked) {
678
+ if (!currentValues.includes(String(option))) {
679
+ configValues[key] = [...currentValues, String(option)];
680
+ }
681
+ } else {
682
+ configValues[key] = currentValues.filter(
683
+ (v) => v !== String(option)
684
+ );
685
+ }
686
+ }}
687
+ />
688
+ <span class="flowdrop-config-sidebar__checkbox-label">
689
+ {String(option)}
690
+ </span>
691
+ </label>
692
+ {/each}
693
+ </div>
694
+ {:else if fieldConfig.enum}
695
+ <!-- Select for enum with single selection -->
696
+ <select
697
+ id={key}
698
+ class="flowdrop-config-sidebar__select"
699
+ bind:value={configValues[key]}
700
+ >
701
+ {#each fieldConfig.enum as option}
702
+ <option value={String(option)}>{String(option)}</option>
703
+ {/each}
704
+ </select>
705
+ {:else if fieldConfig.type === 'string' && fieldConfig.format === 'multiline'}
706
+ <!-- Textarea for multiline strings -->
707
+ <textarea
708
+ id={key}
709
+ class="flowdrop-config-sidebar__textarea"
710
+ bind:value={configValues[key]}
711
+ placeholder={String(fieldConfig.placeholder || '')}
712
+ rows="4"
713
+ ></textarea>
714
+ {:else if fieldConfig.type === 'string'}
715
+ <input
716
+ id={key}
717
+ type="text"
718
+ class="flowdrop-config-sidebar__input"
719
+ bind:value={configValues[key]}
720
+ placeholder={String(fieldConfig.placeholder || '')}
721
+ />
722
+ {:else if fieldConfig.type === 'number'}
723
+ <input
724
+ id={key}
725
+ type="number"
726
+ class="flowdrop-config-sidebar__input"
727
+ bind:value={configValues[key]}
728
+ placeholder={String(fieldConfig.placeholder || '')}
729
+ />
730
+ {:else if fieldConfig.type === 'boolean'}
731
+ <input
732
+ id={key}
733
+ type="checkbox"
734
+ class="flowdrop-config-sidebar__checkbox"
735
+ checked={Boolean(configValues[key] || fieldConfig.default || false)}
736
+ onchange={(e) => {
737
+ configValues[key] = e.currentTarget.checked;
738
+ }}
739
+ />
740
+ {:else if fieldConfig.type === 'select' || fieldConfig.options}
741
+ <select
742
+ id={key}
743
+ class="flowdrop-config-sidebar__select"
744
+ bind:value={configValues[key]}
745
+ >
746
+ {#if fieldConfig.options}
747
+ {#each fieldConfig.options as option}
748
+ {@const optionConfig = option as any}
749
+ <option value={String(optionConfig.value)}
750
+ >{String(optionConfig.label)}</option
751
+ >
752
+ {/each}
753
+ {/if}
754
+ </select>
755
+ {:else}
756
+ <!-- Fallback for unknown field types -->
757
+ <input
758
+ id={key}
759
+ type="text"
760
+ class="flowdrop-config-sidebar__input"
761
+ bind:value={configValues[key]}
762
+ placeholder={String(fieldConfig.placeholder || '')}
763
+ />
764
+ {/if}
765
+ {#if fieldConfig.description}
766
+ <p class="flowdrop-config-sidebar__field-description">
767
+ {String(fieldConfig.description)}
768
+ </p>
769
+ {/if}
770
+ </div>
771
+ {/if}
772
+ {/each}
773
+ {:else}
774
+ <!-- If no properties, show the raw schema for debugging -->
775
+ <div class="flowdrop-config-sidebar__debug">
776
+ <p><strong>Debug - Config Schema:</strong></p>
777
+ <pre>{JSON.stringify(configSchema, null, 2)}</pre>
778
+ </div>
779
+ {/if}
780
+ {:else}
781
+ <p class="flowdrop-config-sidebar__no-config">
782
+ No configuration options available for this node.
783
+ </p>
784
+ {/if}
785
+ </div>
786
+ </div>
787
+ </div>
788
+
789
+ <!-- Footer -->
790
+ <div class="flowdrop-config-sidebar__footer">
791
+ <button
792
+ class="flowdrop-config-sidebar__button flowdrop-config-sidebar__button--secondary"
793
+ onclick={closeConfigSidebar}
794
+ >
795
+ Cancel
796
+ </button>
797
+ <button
798
+ class="flowdrop-config-sidebar__button flowdrop-config-sidebar__button--primary"
799
+ onclick={() => {
800
+ // Get the current config values from the form
801
+ const currentNode = selectedNodeForConfig();
802
+ if (selectedNodeId && currentNode) {
803
+ // Collect the current form values
804
+ const form = document.querySelector('.flowdrop-config-sidebar__form');
805
+ const updatedConfig: Record<string, unknown> = {};
806
+
807
+ if (form) {
808
+ const inputs = form.querySelectorAll('input, select, textarea');
809
+ inputs.forEach((input: any) => {
810
+ if (input.id) {
811
+ if (input.type === 'checkbox') {
812
+ updatedConfig[input.id] = input.checked;
813
+ } else if (input.type === 'number') {
814
+ updatedConfig[input.id] = input.value
815
+ ? Number(input.value)
816
+ : input.value;
817
+ } else if (input.type === 'hidden') {
818
+ // Parse hidden field values that might be JSON
819
+ try {
820
+ const parsed = JSON.parse(input.value);
821
+ updatedConfig[input.id] = parsed;
822
+ } catch {
823
+ // If not JSON, use raw value
824
+ updatedConfig[input.id] = input.value;
825
+ }
826
+ } else {
827
+ updatedConfig[input.id] = input.value;
828
+ }
829
+ }
830
+ });
831
+ }
832
+
833
+ // Preserve hidden field values from original config if not collected from form
834
+ if (
835
+ currentNode.data.config &&
836
+ currentNode.data.metadata?.configSchema?.properties
837
+ ) {
838
+ Object.entries(currentNode.data.metadata.configSchema.properties).forEach(
839
+ ([key, property]: [string, any]) => {
840
+ if (
841
+ property.format === 'hidden' &&
842
+ !(key in updatedConfig) &&
843
+ key in currentNode.data.config
844
+ ) {
845
+ updatedConfig[key] = currentNode.data.config[key];
846
+ }
847
+ }
848
+ );
849
+ }
850
+
851
+ // Handle nodeType switching if nodeType is in the config
852
+ let nodeUpdates: Record<string, unknown> = {
853
+ data: {
854
+ ...currentNode.data,
855
+ config: updatedConfig
856
+ }
857
+ };
858
+
859
+ // NOTE: We do NOT change the node's type field anymore
860
+ // All nodes use 'universalNode' and UniversalNode handles internal switching
861
+ workflowActions.updateNode(selectedNodeId, nodeUpdates);
862
+ }
863
+
864
+ closeConfigSidebar();
865
+ }}
866
+ >
867
+ Save Changes
868
+ </button>
869
+ </div>
870
+ </div>
871
+ </div>
872
+ {/if}
873
+ </div>
874
+ </main>
875
+ </div>
876
+
877
+ <style>
878
+ .flowdrop-app {
879
+ background: linear-gradient(135deg, #f9fafb 0%, #e0e7ff 50%, #c7d2fe 100%);
880
+ display: flex;
881
+ flex-direction: column;
882
+ overflow: hidden;
883
+ }
884
+
885
+ .flowdrop-main {
886
+ flex: 1;
887
+ position: relative;
888
+ display: flex;
889
+ flex-direction: column;
890
+ min-height: 0;
891
+ overflow: hidden;
892
+ }
893
+
894
+ .flowdrop-status {
895
+ background-color: #eff6ff;
896
+ border-bottom: 1px solid #bfdbfe;
897
+ padding: 1rem;
898
+ }
899
+
900
+ .flowdrop-status--error {
901
+ background-color: #fef2f2;
902
+ border-bottom: 1px solid #fecaca;
903
+ }
904
+
905
+ .flowdrop-status__content {
906
+ max-width: 80rem;
907
+ margin: 0 auto;
908
+ display: flex;
909
+ align-items: center;
910
+ justify-content: space-between;
911
+ }
912
+
913
+ .flowdrop-status__indicator {
914
+ width: 0.5rem;
915
+ height: 0.5rem;
916
+ border-radius: 50%;
917
+ }
918
+
919
+ .flowdrop-status__indicator--error {
920
+ background-color: #ef4444;
921
+ }
922
+
923
+ .flowdrop-btn {
924
+ padding: 0.375rem 0.75rem;
925
+ border-radius: 0.375rem;
926
+ font-size: 0.75rem;
927
+ font-weight: 500;
928
+ cursor: pointer;
929
+ border: 1px solid transparent;
930
+ transition: all 0.2s ease-in-out;
931
+ }
932
+
933
+ .flowdrop-btn--sm {
934
+ padding: 0.25rem 0.5rem;
935
+ font-size: 0.625rem;
936
+ }
937
+
938
+ .flowdrop-btn--outline {
939
+ background-color: transparent;
940
+ border-color: #d1d5db;
941
+ color: #374151;
942
+ }
943
+
944
+ .flowdrop-btn--outline:hover {
945
+ background-color: #f9fafb;
946
+ border-color: #9ca3af;
947
+ }
948
+
949
+ .flowdrop-btn--primary {
950
+ background-color: #3b82f6;
951
+ border-color: #3b82f6;
952
+ color: #ffffff;
953
+ }
954
+
955
+ .flowdrop-btn--primary:hover {
956
+ background-color: #2563eb;
957
+ border-color: #2563eb;
958
+ }
959
+
960
+ .flowdrop-btn--ghost {
961
+ background-color: transparent;
962
+ border-color: transparent;
963
+ color: #6b7280;
964
+ }
965
+
966
+ .flowdrop-btn--ghost:hover {
967
+ background-color: #f3f4f6;
968
+ color: #374151;
969
+ }
970
+
971
+ .flowdrop-flex {
972
+ display: flex;
973
+ }
974
+
975
+ .flowdrop-gap--2 {
976
+ gap: 0.5rem;
977
+ }
978
+
979
+ .flowdrop-gap--3 {
980
+ gap: 0.75rem;
981
+ }
982
+
983
+ .flowdrop-text--sm {
984
+ font-size: 0.875rem;
985
+ line-height: 1.25rem;
986
+ }
987
+
988
+ .flowdrop-font--medium {
989
+ font-weight: 500;
990
+ }
991
+
992
+ @keyframes spin {
993
+ 0% {
994
+ transform: rotate(0deg);
995
+ }
996
+ 100% {
997
+ transform: rotate(360deg);
998
+ }
999
+ }
1000
+
1001
+ .flowdrop-editor-container {
1002
+ flex: 1;
1003
+ position: relative;
1004
+ min-height: 0;
1005
+ overflow: hidden;
1006
+ display: flex;
1007
+ }
1008
+
1009
+ .flowdrop-sidebar {
1010
+ background-color: #ffffff;
1011
+ border: 1px solid #e5e7eb;
1012
+ overflow-y: auto;
1013
+ overflow-x: hidden;
1014
+ height: 100%;
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ /* Custom scrollbar styling */
1018
+ scrollbar-width: thin;
1019
+ scrollbar-color: #cbd5e1 #f1f5f9;
1020
+ }
1021
+
1022
+ .flowdrop-sidebar::-webkit-scrollbar {
1023
+ width: 8px;
1024
+ }
1025
+
1026
+ .flowdrop-sidebar::-webkit-scrollbar-track {
1027
+ background: #f1f5f9;
1028
+ border-radius: 4px;
1029
+ }
1030
+
1031
+ .flowdrop-sidebar::-webkit-scrollbar-thumb {
1032
+ background: #cbd5e1;
1033
+ border-radius: 4px;
1034
+ }
1035
+
1036
+ .flowdrop-sidebar::-webkit-scrollbar-thumb:hover {
1037
+ background: #94a3b8;
1038
+ }
1039
+
1040
+ .flowdrop-sidebar--left {
1041
+ width: 320px;
1042
+ min-width: 320px;
1043
+ border-right: 1px solid #e5e7eb;
1044
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
1045
+ display: flex;
1046
+ flex-direction: column;
1047
+ }
1048
+
1049
+ .flowdrop-sidebar--right {
1050
+ border-left: 1px solid #e5e7eb;
1051
+ box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1);
1052
+ }
1053
+
1054
+ .flowdrop-editor-main {
1055
+ flex: 1;
1056
+ position: relative;
1057
+ min-width: 0;
1058
+ overflow: hidden;
1059
+ background-color: #1f2937;
1060
+ }
1061
+
1062
+ /* Configuration Sidebar Styles */
1063
+ .flowdrop-config-sidebar {
1064
+ height: 100%;
1065
+ display: flex;
1066
+ flex-direction: column;
1067
+ background-color: #ffffff;
1068
+ }
1069
+
1070
+ .flowdrop-config-sidebar__header {
1071
+ display: flex;
1072
+ justify-content: space-between;
1073
+ align-items: center;
1074
+ padding: 1rem;
1075
+ border-bottom: 1px solid #e5e7eb;
1076
+ background-color: #f9fafb;
1077
+ }
1078
+
1079
+ .flowdrop-config-sidebar__title {
1080
+ margin: 0;
1081
+ font-size: 1.125rem;
1082
+ font-weight: 600;
1083
+ color: #111827;
1084
+ }
1085
+
1086
+ .flowdrop-config-sidebar__close {
1087
+ background: none;
1088
+ border: none;
1089
+ font-size: 1.5rem;
1090
+ cursor: pointer;
1091
+ color: #6b7280;
1092
+ padding: 0.25rem;
1093
+ border-radius: 0.25rem;
1094
+ transition: color 0.2s;
1095
+ }
1096
+
1097
+ .flowdrop-config-sidebar__close:hover {
1098
+ color: #374151;
1099
+ background-color: #f3f4f6;
1100
+ }
1101
+
1102
+ .flowdrop-config-sidebar__content {
1103
+ flex: 1;
1104
+ overflow-y: auto;
1105
+ padding: 1rem;
1106
+ }
1107
+
1108
+ .flowdrop-config-sidebar__section {
1109
+ margin-bottom: 1.5rem;
1110
+ }
1111
+
1112
+ .flowdrop-config-sidebar__section-title {
1113
+ margin: 0 0 0.75rem 0;
1114
+ font-size: 0.875rem;
1115
+ font-weight: 600;
1116
+ color: #374151;
1117
+ text-transform: uppercase;
1118
+ letter-spacing: 0.05em;
1119
+ }
1120
+
1121
+ .flowdrop-config-sidebar__details {
1122
+ display: flex;
1123
+ flex-direction: column;
1124
+ gap: 0.5rem;
1125
+ }
1126
+
1127
+ .flowdrop-config-sidebar__detail {
1128
+ display: flex;
1129
+ flex-direction: column;
1130
+ gap: 0.25rem;
1131
+ }
1132
+
1133
+ .flowdrop-config-sidebar__detail-label {
1134
+ font-size: 0.75rem;
1135
+ font-weight: 500;
1136
+ color: #6b7280;
1137
+ text-transform: uppercase;
1138
+ letter-spacing: 0.05em;
1139
+ }
1140
+
1141
+ .flowdrop-config-sidebar__detail-value {
1142
+ font-size: 0.875rem;
1143
+ color: #111827;
1144
+ font-weight: 500;
1145
+ }
1146
+
1147
+ .flowdrop-config-sidebar__detail-description {
1148
+ margin: 0;
1149
+ font-size: 0.875rem;
1150
+ color: #6b7280;
1151
+ line-height: 1.4;
1152
+ }
1153
+
1154
+ .flowdrop-config-sidebar__form {
1155
+ display: flex;
1156
+ flex-direction: column;
1157
+ gap: 1rem;
1158
+ }
1159
+
1160
+ .flowdrop-config-sidebar__field {
1161
+ display: flex;
1162
+ flex-direction: column;
1163
+ gap: 0.5rem;
1164
+ }
1165
+
1166
+ .flowdrop-config-sidebar__field-label {
1167
+ font-size: 0.875rem;
1168
+ font-weight: 500;
1169
+ color: #374151;
1170
+ }
1171
+
1172
+ .flowdrop-config-sidebar__input,
1173
+ .flowdrop-config-sidebar__select {
1174
+ padding: 0.5rem;
1175
+ border: 1px solid #d1d5db;
1176
+ border-radius: 0.375rem;
1177
+ font-size: 0.875rem;
1178
+ transition:
1179
+ border-color 0.2s,
1180
+ box-shadow 0.2s;
1181
+ }
1182
+
1183
+ .flowdrop-config-sidebar__input:focus,
1184
+ .flowdrop-config-sidebar__select:focus {
1185
+ outline: none;
1186
+ border-color: #3b82f6;
1187
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
1188
+ }
1189
+
1190
+ .flowdrop-config-sidebar__checkbox-group {
1191
+ display: flex;
1192
+ flex-direction: column;
1193
+ gap: 0.5rem;
1194
+ }
1195
+
1196
+ .flowdrop-config-sidebar__checkbox-item {
1197
+ display: flex;
1198
+ align-items: center;
1199
+ gap: 0.5rem;
1200
+ cursor: pointer;
1201
+ }
1202
+
1203
+ .flowdrop-config-sidebar__checkbox {
1204
+ width: 1rem;
1205
+ height: 1rem;
1206
+ accent-color: #3b82f6;
1207
+ cursor: pointer;
1208
+ }
1209
+
1210
+ .flowdrop-config-sidebar__checkbox-label {
1211
+ font-size: 0.875rem;
1212
+ color: #374151;
1213
+ cursor: pointer;
1214
+ }
1215
+
1216
+ .flowdrop-config-sidebar__textarea {
1217
+ width: 100%;
1218
+ padding: 0.5rem 0.75rem;
1219
+ border: 1px solid #d1d5db;
1220
+ border-radius: 0.375rem;
1221
+ font-size: 0.875rem;
1222
+ background-color: #ffffff;
1223
+ transition: all 0.2s ease-in-out;
1224
+ resize: vertical;
1225
+ min-height: 4rem;
1226
+ }
1227
+
1228
+ .flowdrop-config-sidebar__textarea:focus {
1229
+ outline: none;
1230
+ border-color: #3b82f6;
1231
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
1232
+ }
1233
+
1234
+ .flowdrop-config-sidebar__field-description {
1235
+ margin: 0;
1236
+ font-size: 0.75rem;
1237
+ color: #6b7280;
1238
+ line-height: 1.4;
1239
+ }
1240
+
1241
+ .flowdrop-config-sidebar__no-config {
1242
+ text-align: center;
1243
+ color: #6b7280;
1244
+ font-style: italic;
1245
+ padding: 2rem 1rem;
1246
+ }
1247
+
1248
+ .flowdrop-config-sidebar__footer {
1249
+ padding: 1rem;
1250
+ border-top: 1px solid #e5e7eb;
1251
+ background-color: #f9fafb;
1252
+ display: flex;
1253
+ gap: 0.75rem;
1254
+ justify-content: flex-end;
1255
+ height: 40px;
1256
+ align-items: center;
1257
+ }
1258
+
1259
+ .flowdrop-config-sidebar__button {
1260
+ padding: 0.5rem 1rem;
1261
+ border-radius: 0.375rem;
1262
+ font-size: 0.875rem;
1263
+ font-weight: 500;
1264
+ cursor: pointer;
1265
+ transition: all 0.2s;
1266
+ border: 1px solid transparent;
1267
+ }
1268
+
1269
+ .flowdrop-config-sidebar__button--secondary {
1270
+ background-color: #ffffff;
1271
+ border-color: #d1d5db;
1272
+ color: #374151;
1273
+ }
1274
+
1275
+ .flowdrop-config-sidebar__button--secondary:hover {
1276
+ background-color: #f9fafb;
1277
+ border-color: #9ca3af;
1278
+ }
1279
+
1280
+ .flowdrop-config-sidebar__button--primary {
1281
+ background-color: #3b82f6;
1282
+ color: #ffffff;
1283
+ }
1284
+
1285
+ .flowdrop-config-sidebar__button--primary:hover {
1286
+ background-color: #2563eb;
1287
+ }
1288
+
1289
+ .flowdrop-config-sidebar__debug {
1290
+ background-color: #f3f4f6;
1291
+ border: 1px solid #d1d5db;
1292
+ border-radius: 0.375rem;
1293
+ padding: 1rem;
1294
+ margin: 1rem 0;
1295
+ }
1296
+
1297
+ .flowdrop-config-sidebar__debug pre {
1298
+ background-color: #ffffff;
1299
+ border: 1px solid #e5e7eb;
1300
+ border-radius: 0.25rem;
1301
+ padding: 0.75rem;
1302
+ font-size: 0.75rem;
1303
+ overflow-x: auto;
1304
+ margin: 0.5rem 0 0 0;
1305
+ }
1306
+ </style>