@flowdrop/flowdrop 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/README.md +50 -50
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/WorkflowAdapter.js +25 -25
  4. package/dist/adapters/agentspec/AgentSpecAdapter.d.ts +2 -2
  5. package/dist/adapters/agentspec/AgentSpecAdapter.js +133 -122
  6. package/dist/adapters/agentspec/agentAdapter.d.ts +2 -2
  7. package/dist/adapters/agentspec/agentAdapter.js +10 -10
  8. package/dist/adapters/agentspec/autoLayout.d.ts +1 -1
  9. package/dist/adapters/agentspec/autoLayout.js +2 -2
  10. package/dist/adapters/agentspec/componentTypeDefaults.d.ts +1 -1
  11. package/dist/adapters/agentspec/componentTypeDefaults.js +120 -120
  12. package/dist/adapters/agentspec/defaultNodeTypes.d.ts +2 -2
  13. package/dist/adapters/agentspec/defaultNodeTypes.js +307 -307
  14. package/dist/adapters/agentspec/index.d.ts +10 -10
  15. package/dist/adapters/agentspec/index.js +6 -6
  16. package/dist/adapters/agentspec/validator.d.ts +2 -2
  17. package/dist/adapters/agentspec/validator.js +22 -20
  18. package/dist/api/enhanced-client.d.ts +3 -3
  19. package/dist/api/enhanced-client.js +73 -72
  20. package/dist/components/App.svelte +1090 -961
  21. package/dist/components/App.svelte.d.ts +9 -6
  22. package/dist/components/CanvasBanner.stories.svelte +23 -20
  23. package/dist/components/CanvasBanner.stories.svelte.d.ts +1 -1
  24. package/dist/components/CanvasBanner.svelte +52 -46
  25. package/dist/components/ConfigForm.svelte +1164 -1065
  26. package/dist/components/ConfigForm.svelte.d.ts +2 -2
  27. package/dist/components/ConfigModal.svelte +180 -180
  28. package/dist/components/ConfigModal.svelte.d.ts +1 -1
  29. package/dist/components/ConfigPanel.stories.svelte +35 -35
  30. package/dist/components/ConfigPanel.stories.svelte.d.ts +1 -1
  31. package/dist/components/ConfigPanel.svelte +178 -167
  32. package/dist/components/ConfigPanel.svelte.d.ts +1 -1
  33. package/dist/components/ConnectionLine.svelte +25 -25
  34. package/dist/components/EdgeRefresher.svelte +26 -26
  35. package/dist/components/FlowDropEdge.stories.svelte +179 -143
  36. package/dist/components/FlowDropEdge.svelte +147 -147
  37. package/dist/components/FlowDropEdge.svelte.d.ts +1 -1
  38. package/dist/components/FlowDropZone.svelte +63 -60
  39. package/dist/components/FlowDropZone.svelte.d.ts +1 -1
  40. package/dist/components/LoadingSpinner.stories.svelte +19 -19
  41. package/dist/components/LoadingSpinner.stories.svelte.d.ts +1 -1
  42. package/dist/components/LoadingSpinner.svelte +21 -21
  43. package/dist/components/LoadingSpinner.svelte.d.ts +1 -1
  44. package/dist/components/Logo.stories.svelte +13 -13
  45. package/dist/components/Logo.stories.svelte.d.ts +1 -1
  46. package/dist/components/Logo.svelte +101 -95
  47. package/dist/components/LogsSidebar.svelte +553 -546
  48. package/dist/components/LogsSidebar.svelte.d.ts +1 -1
  49. package/dist/components/MarkdownDisplay.stories.svelte +29 -23
  50. package/dist/components/MarkdownDisplay.stories.svelte.d.ts +1 -1
  51. package/dist/components/MarkdownDisplay.svelte +16 -14
  52. package/dist/components/Navbar.stories.svelte +43 -38
  53. package/dist/components/Navbar.stories.svelte.d.ts +1 -1
  54. package/dist/components/Navbar.svelte +760 -706
  55. package/dist/components/Navbar.svelte.d.ts +1 -1
  56. package/dist/components/NodeSidebar.svelte +905 -746
  57. package/dist/components/NodeSidebar.svelte.d.ts +5 -1
  58. package/dist/components/NodeStatusOverlay.stories.svelte +82 -70
  59. package/dist/components/NodeStatusOverlay.stories.svelte.d.ts +1 -1
  60. package/dist/components/NodeStatusOverlay.svelte +295 -280
  61. package/dist/components/NodeStatusOverlay.svelte.d.ts +3 -3
  62. package/dist/components/PipelineStatus.svelte +326 -300
  63. package/dist/components/PipelineStatus.svelte.d.ts +4 -4
  64. package/dist/components/PortCoordinateTracker.svelte +49 -47
  65. package/dist/components/PortCoordinateTracker.svelte.d.ts +1 -1
  66. package/dist/components/ReadOnlyDetails.svelte +156 -156
  67. package/dist/components/SchemaForm.stories.svelte +106 -98
  68. package/dist/components/SchemaForm.stories.svelte.d.ts +1 -1
  69. package/dist/components/SchemaForm.svelte +490 -463
  70. package/dist/components/SchemaForm.svelte.d.ts +2 -2
  71. package/dist/components/SettingsModal.svelte +226 -223
  72. package/dist/components/SettingsModal.svelte.d.ts +1 -1
  73. package/dist/components/SettingsPanel.svelte +637 -601
  74. package/dist/components/SettingsPanel.svelte.d.ts +1 -1
  75. package/dist/components/StatusIcon.stories.svelte +62 -49
  76. package/dist/components/StatusIcon.stories.svelte.d.ts +1 -1
  77. package/dist/components/StatusIcon.svelte +87 -87
  78. package/dist/components/StatusIcon.svelte.d.ts +2 -2
  79. package/dist/components/StatusLabel.stories.svelte +12 -12
  80. package/dist/components/StatusLabel.stories.svelte.d.ts +1 -1
  81. package/dist/components/StatusLabel.svelte +19 -19
  82. package/dist/components/ThemeToggle.stories.svelte +16 -16
  83. package/dist/components/ThemeToggle.stories.svelte.d.ts +1 -1
  84. package/dist/components/ThemeToggle.svelte +180 -169
  85. package/dist/components/ThemeToggle.svelte.d.ts +1 -1
  86. package/dist/components/UniversalNode.svelte +150 -138
  87. package/dist/components/UniversalNode.svelte.d.ts +3 -3
  88. package/dist/components/WorkflowEditor.svelte +1069 -1014
  89. package/dist/components/WorkflowEditor.svelte.d.ts +4 -4
  90. package/dist/components/form/FormArray.svelte +1034 -973
  91. package/dist/components/form/FormArray.svelte.d.ts +1 -1
  92. package/dist/components/form/FormAutocomplete.svelte +1021 -978
  93. package/dist/components/form/FormAutocomplete.svelte.d.ts +1 -1
  94. package/dist/components/form/FormCheckboxGroup.stories.svelte +23 -20
  95. package/dist/components/form/FormCheckboxGroup.stories.svelte.d.ts +1 -1
  96. package/dist/components/form/FormCheckboxGroup.svelte +136 -136
  97. package/dist/components/form/FormCodeEditor.svelte +452 -434
  98. package/dist/components/form/FormField.svelte +366 -355
  99. package/dist/components/form/FormField.svelte.d.ts +2 -2
  100. package/dist/components/form/FormFieldLight.svelte +400 -384
  101. package/dist/components/form/FormFieldLight.svelte.d.ts +1 -1
  102. package/dist/components/form/FormFieldWrapper.stories.svelte +42 -42
  103. package/dist/components/form/FormFieldWrapper.stories.svelte.d.ts +1 -1
  104. package/dist/components/form/FormFieldWrapper.svelte +100 -93
  105. package/dist/components/form/FormFieldWrapper.svelte.d.ts +1 -1
  106. package/dist/components/form/FormFieldset.svelte +108 -108
  107. package/dist/components/form/FormFieldset.svelte.d.ts +2 -2
  108. package/dist/components/form/FormMarkdownEditor.svelte +758 -725
  109. package/dist/components/form/FormNumberField.stories.svelte +25 -25
  110. package/dist/components/form/FormNumberField.stories.svelte.d.ts +1 -1
  111. package/dist/components/form/FormNumberField.svelte +88 -88
  112. package/dist/components/form/FormRangeField.stories.svelte +20 -20
  113. package/dist/components/form/FormRangeField.stories.svelte.d.ts +1 -1
  114. package/dist/components/form/FormRangeField.svelte +234 -226
  115. package/dist/components/form/FormSelect.stories.svelte +38 -38
  116. package/dist/components/form/FormSelect.stories.svelte.d.ts +1 -1
  117. package/dist/components/form/FormSelect.svelte +101 -101
  118. package/dist/components/form/FormSelect.svelte.d.ts +1 -1
  119. package/dist/components/form/FormTemplateEditor.svelte +847 -798
  120. package/dist/components/form/FormTemplateEditor.svelte.d.ts +1 -1
  121. package/dist/components/form/FormTextField.stories.svelte +29 -23
  122. package/dist/components/form/FormTextField.stories.svelte.d.ts +1 -1
  123. package/dist/components/form/FormTextField.svelte +68 -68
  124. package/dist/components/form/FormTextarea.stories.svelte +28 -25
  125. package/dist/components/form/FormTextarea.stories.svelte.d.ts +1 -1
  126. package/dist/components/form/FormTextarea.svelte +74 -74
  127. package/dist/components/form/FormToggle.stories.svelte +23 -20
  128. package/dist/components/form/FormToggle.stories.svelte.d.ts +1 -1
  129. package/dist/components/form/FormToggle.svelte +98 -98
  130. package/dist/components/form/FormUISchemaRenderer.svelte +120 -113
  131. package/dist/components/form/FormUISchemaRenderer.svelte.d.ts +3 -3
  132. package/dist/components/form/index.d.ts +19 -19
  133. package/dist/components/form/index.js +18 -18
  134. package/dist/components/form/templateAutocomplete.d.ts +2 -2
  135. package/dist/components/form/templateAutocomplete.js +64 -55
  136. package/dist/components/form/types.d.ts +6 -6
  137. package/dist/components/form/types.js +9 -4
  138. package/dist/components/icons/AlertCircleIcon.svelte +11 -0
  139. package/dist/components/icons/AlertCircleIcon.svelte.d.ts +26 -0
  140. package/dist/components/icons/CogIcon.svelte +11 -0
  141. package/dist/components/icons/CogIcon.svelte.d.ts +26 -0
  142. package/dist/components/interrupt/ChoicePrompt.stories.svelte +54 -38
  143. package/dist/components/interrupt/ChoicePrompt.stories.svelte.d.ts +1 -1
  144. package/dist/components/interrupt/ChoicePrompt.svelte +407 -383
  145. package/dist/components/interrupt/ChoicePrompt.svelte.d.ts +1 -1
  146. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte +48 -48
  147. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte.d.ts +1 -1
  148. package/dist/components/interrupt/ConfirmationPrompt.svelte +280 -274
  149. package/dist/components/interrupt/ConfirmationPrompt.svelte.d.ts +1 -1
  150. package/dist/components/interrupt/FormPrompt.svelte +223 -218
  151. package/dist/components/interrupt/FormPrompt.svelte.d.ts +1 -1
  152. package/dist/components/interrupt/InterruptBubble.svelte +617 -583
  153. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +2 -2
  154. package/dist/components/interrupt/ReviewPrompt.stories.svelte +66 -56
  155. package/dist/components/interrupt/ReviewPrompt.stories.svelte.d.ts +1 -1
  156. package/dist/components/interrupt/ReviewPrompt.svelte +861 -841
  157. package/dist/components/interrupt/ReviewPrompt.svelte.d.ts +1 -1
  158. package/dist/components/interrupt/TextInputPrompt.stories.svelte +38 -33
  159. package/dist/components/interrupt/TextInputPrompt.stories.svelte.d.ts +1 -1
  160. package/dist/components/interrupt/TextInputPrompt.svelte +333 -328
  161. package/dist/components/interrupt/TextInputPrompt.svelte.d.ts +1 -1
  162. package/dist/components/interrupt/index.d.ts +5 -5
  163. package/dist/components/interrupt/index.js +5 -5
  164. package/dist/components/layouts/MainLayout.svelte +724 -691
  165. package/dist/components/layouts/MainLayout.svelte.d.ts +6 -6
  166. package/dist/components/nodes/GatewayNode.stories.svelte +100 -99
  167. package/dist/components/nodes/GatewayNode.svelte +605 -571
  168. package/dist/components/nodes/GatewayNode.svelte.d.ts +3 -3
  169. package/dist/components/nodes/IdeaNode.stories.svelte +44 -43
  170. package/dist/components/nodes/IdeaNode.svelte +451 -437
  171. package/dist/components/nodes/IdeaNode.svelte.d.ts +1 -1
  172. package/dist/components/nodes/NotesNode.stories.svelte +65 -64
  173. package/dist/components/nodes/NotesNode.svelte +380 -369
  174. package/dist/components/nodes/NotesNode.svelte.d.ts +1 -1
  175. package/dist/components/nodes/SimpleNode.stories.svelte +145 -144
  176. package/dist/components/nodes/SimpleNode.svelte +486 -424
  177. package/dist/components/nodes/SimpleNode.svelte.d.ts +1 -1
  178. package/dist/components/nodes/SquareNode.stories.svelte +73 -73
  179. package/dist/components/nodes/SquareNode.svelte +439 -380
  180. package/dist/components/nodes/SquareNode.svelte.d.ts +1 -1
  181. package/dist/components/nodes/TerminalNode.stories.svelte +13 -13
  182. package/dist/components/nodes/TerminalNode.svelte +709 -670
  183. package/dist/components/nodes/TerminalNode.svelte.d.ts +1 -1
  184. package/dist/components/nodes/ToolNode.stories.svelte +181 -180
  185. package/dist/components/nodes/ToolNode.svelte +505 -447
  186. package/dist/components/nodes/ToolNode.svelte.d.ts +1 -1
  187. package/dist/components/nodes/WorkflowNode.stories.svelte +70 -46
  188. package/dist/components/nodes/WorkflowNode.svelte +621 -551
  189. package/dist/components/nodes/WorkflowNode.svelte.d.ts +3 -3
  190. package/dist/components/playground/ChatPanel.svelte +945 -889
  191. package/dist/components/playground/ExecutionLogs.svelte +495 -472
  192. package/dist/components/playground/InputCollector.svelte +449 -428
  193. package/dist/components/playground/MessageBubble.stories.svelte +47 -47
  194. package/dist/components/playground/MessageBubble.stories.svelte.d.ts +1 -1
  195. package/dist/components/playground/MessageBubble.svelte +626 -610
  196. package/dist/components/playground/MessageBubble.svelte.d.ts +1 -1
  197. package/dist/components/playground/Playground.svelte +1088 -1057
  198. package/dist/components/playground/Playground.svelte.d.ts +3 -3
  199. package/dist/components/playground/PlaygroundModal.svelte +208 -204
  200. package/dist/components/playground/PlaygroundModal.svelte.d.ts +3 -3
  201. package/dist/components/playground/SessionManager.svelte +527 -521
  202. package/dist/components/playground/SessionManager.svelte.d.ts +1 -1
  203. package/dist/config/agentSpecEndpoints.d.ts +1 -1
  204. package/dist/config/agentSpecEndpoints.js +20 -20
  205. package/dist/config/constants.js +2 -2
  206. package/dist/config/defaultCategories.d.ts +1 -1
  207. package/dist/config/defaultCategories.js +86 -86
  208. package/dist/config/defaultPortConfig.d.ts +1 -1
  209. package/dist/config/defaultPortConfig.js +144 -144
  210. package/dist/config/endpoints.d.ts +4 -4
  211. package/dist/config/endpoints.js +65 -65
  212. package/dist/config/runtimeConfig.d.ts +2 -2
  213. package/dist/config/runtimeConfig.js +8 -8
  214. package/dist/core/index.d.ts +63 -59
  215. package/dist/core/index.js +35 -33
  216. package/dist/display/index.d.ts +2 -2
  217. package/dist/display/index.js +2 -2
  218. package/dist/editor/index.d.ts +62 -62
  219. package/dist/editor/index.js +53 -53
  220. package/dist/form/code.d.ts +5 -5
  221. package/dist/form/code.js +14 -14
  222. package/dist/form/fieldRegistry.d.ts +3 -3
  223. package/dist/form/fieldRegistry.js +11 -9
  224. package/dist/form/full.d.ts +8 -8
  225. package/dist/form/full.js +9 -9
  226. package/dist/form/index.d.ts +18 -18
  227. package/dist/form/index.js +16 -16
  228. package/dist/form/markdown.d.ts +4 -4
  229. package/dist/form/markdown.js +8 -8
  230. package/dist/helpers/proximityConnect.d.ts +3 -3
  231. package/dist/helpers/proximityConnect.js +34 -32
  232. package/dist/helpers/workflowEditorHelper.d.ts +5 -5
  233. package/dist/helpers/workflowEditorHelper.js +108 -96
  234. package/dist/index.d.ts +6 -6
  235. package/dist/index.js +6 -6
  236. package/dist/mocks/app-environment.js +2 -2
  237. package/dist/mocks/app-forms.js +9 -9
  238. package/dist/mocks/app-navigation.js +11 -11
  239. package/dist/mocks/app-stores.js +8 -8
  240. package/dist/playground/index.d.ts +19 -19
  241. package/dist/playground/index.js +16 -16
  242. package/dist/playground/mount.d.ts +3 -3
  243. package/dist/playground/mount.js +24 -24
  244. package/dist/registry/builtinFormats.js +13 -13
  245. package/dist/registry/builtinNodes.d.ts +2 -2
  246. package/dist/registry/builtinNodes.js +77 -77
  247. package/dist/registry/index.d.ts +4 -4
  248. package/dist/registry/index.js +4 -4
  249. package/dist/registry/nodeComponentRegistry.d.ts +8 -8
  250. package/dist/registry/nodeComponentRegistry.js +11 -9
  251. package/dist/registry/plugin.d.ts +2 -2
  252. package/dist/registry/plugin.js +11 -11
  253. package/dist/registry/workflowFormatRegistry.d.ts +3 -3
  254. package/dist/registry/workflowFormatRegistry.js +2 -2
  255. package/dist/schema/index.d.ts +1 -1
  256. package/dist/schema/index.js +2 -2
  257. package/dist/services/agentSpecExecutionService.d.ts +3 -3
  258. package/dist/services/agentSpecExecutionService.js +59 -55
  259. package/dist/services/api.d.ts +2 -2
  260. package/dist/services/api.js +37 -37
  261. package/dist/services/apiVariableService.d.ts +1 -1
  262. package/dist/services/apiVariableService.js +41 -34
  263. package/dist/services/autoSaveService.js +8 -8
  264. package/dist/services/categoriesApi.d.ts +2 -2
  265. package/dist/services/categoriesApi.js +8 -8
  266. package/dist/services/draftStorage.d.ts +1 -1
  267. package/dist/services/draftStorage.js +11 -11
  268. package/dist/services/dynamicSchemaService.d.ts +1 -1
  269. package/dist/services/dynamicSchemaService.js +41 -39
  270. package/dist/services/globalSave.d.ts +2 -2
  271. package/dist/services/globalSave.js +41 -38
  272. package/dist/services/historyService.d.ts +1 -1
  273. package/dist/services/historyService.js +8 -8
  274. package/dist/services/interruptService.d.ts +1 -1
  275. package/dist/services/interruptService.js +35 -29
  276. package/dist/services/nodeExecutionService.d.ts +1 -1
  277. package/dist/services/nodeExecutionService.js +45 -44
  278. package/dist/services/playgroundService.d.ts +1 -1
  279. package/dist/services/playgroundService.js +29 -29
  280. package/dist/services/portConfigApi.d.ts +2 -2
  281. package/dist/services/portConfigApi.js +8 -8
  282. package/dist/services/settingsService.d.ts +2 -2
  283. package/dist/services/settingsService.js +25 -19
  284. package/dist/services/toastService.d.ts +4 -4
  285. package/dist/services/toastService.js +33 -33
  286. package/dist/services/variableService.d.ts +1 -1
  287. package/dist/services/variableService.js +36 -36
  288. package/dist/services/workflowStorage.d.ts +2 -2
  289. package/dist/services/workflowStorage.js +13 -13
  290. package/dist/settings/index.d.ts +7 -7
  291. package/dist/settings/index.js +6 -6
  292. package/dist/skins/default.d.ts +2 -0
  293. package/dist/skins/default.js +1 -0
  294. package/dist/skins/index.d.ts +13 -0
  295. package/dist/skins/index.js +30 -0
  296. package/dist/skins/slate.d.ts +2 -0
  297. package/dist/skins/slate.js +78 -0
  298. package/dist/stores/categoriesStore.svelte.d.ts +1 -1
  299. package/dist/stores/categoriesStore.svelte.js +5 -5
  300. package/dist/stores/editorStateMachine.svelte.d.ts +2 -2
  301. package/dist/stores/editorStateMachine.svelte.js +65 -33
  302. package/dist/stores/historyStore.svelte.d.ts +4 -4
  303. package/dist/stores/historyStore.svelte.js +4 -4
  304. package/dist/stores/interruptStore.svelte.d.ts +3 -3
  305. package/dist/stores/interruptStore.svelte.js +21 -21
  306. package/dist/stores/playgroundStore.svelte.d.ts +2 -2
  307. package/dist/stores/playgroundStore.svelte.js +25 -18
  308. package/dist/stores/portCoordinateStore.svelte.d.ts +2 -2
  309. package/dist/stores/portCoordinateStore.svelte.js +15 -8
  310. package/dist/stores/settingsStore.svelte.d.ts +2 -2
  311. package/dist/stores/settingsStore.svelte.js +62 -57
  312. package/dist/stores/workflowStore.svelte.d.ts +3 -3
  313. package/dist/stores/workflowStore.svelte.js +50 -47
  314. package/dist/stories/CanvasDecorator.svelte +35 -32
  315. package/dist/stories/CanvasDecorator.svelte.d.ts +2 -2
  316. package/dist/stories/EdgeDecorator.svelte +102 -99
  317. package/dist/stories/EdgeDecorator.svelte.d.ts +1 -1
  318. package/dist/stories/NodeDecorator.svelte +59 -53
  319. package/dist/stories/NodeDecorator.svelte.d.ts +1 -1
  320. package/dist/stories/utils.d.ts +2 -2
  321. package/dist/stories/utils.js +105 -67
  322. package/dist/styles/base.css +599 -595
  323. package/dist/styles/toast.css +14 -14
  324. package/dist/styles/tokens.css +409 -378
  325. package/dist/svelte-app.d.ts +12 -9
  326. package/dist/svelte-app.js +40 -39
  327. package/dist/themes/default.d.ts +2 -0
  328. package/dist/themes/default.js +9 -0
  329. package/dist/themes/index.d.ts +13 -0
  330. package/dist/themes/index.js +44 -0
  331. package/dist/themes/minimal.d.ts +2 -0
  332. package/dist/themes/minimal.js +11 -0
  333. package/dist/types/agentspec.d.ts +18 -18
  334. package/dist/types/agentspec.js +2 -2
  335. package/dist/types/auth.d.ts +1 -1
  336. package/dist/types/auth.js +6 -6
  337. package/dist/types/config.d.ts +6 -6
  338. package/dist/types/events.d.ts +2 -2
  339. package/dist/types/events.js +2 -2
  340. package/dist/types/index.d.ts +32 -32
  341. package/dist/types/index.js +6 -6
  342. package/dist/types/interrupt.d.ts +6 -6
  343. package/dist/types/interrupt.js +21 -21
  344. package/dist/types/interruptState.d.ts +12 -12
  345. package/dist/types/interruptState.js +66 -66
  346. package/dist/types/playground.d.ts +7 -7
  347. package/dist/types/playground.js +14 -14
  348. package/dist/types/settings.d.ts +5 -3
  349. package/dist/types/settings.js +25 -18
  350. package/dist/types/skin.d.ts +31 -0
  351. package/dist/types/skin.js +1 -0
  352. package/dist/types/theme.d.ts +35 -0
  353. package/dist/types/theme.js +1 -0
  354. package/dist/types/uischema.d.ts +4 -4
  355. package/dist/types/uischema.js +3 -3
  356. package/dist/utils/colors.d.ts +1 -1
  357. package/dist/utils/colors.js +97 -95
  358. package/dist/utils/config.d.ts +2 -2
  359. package/dist/utils/config.js +48 -48
  360. package/dist/utils/connections.d.ts +2 -2
  361. package/dist/utils/connections.js +15 -15
  362. package/dist/utils/errors.js +3 -3
  363. package/dist/utils/fetchWithAuth.d.ts +1 -1
  364. package/dist/utils/fetchWithAuth.js +2 -2
  365. package/dist/utils/handleIds.d.ts +2 -2
  366. package/dist/utils/handleIds.js +8 -8
  367. package/dist/utils/handlePositioning.d.ts +1 -1
  368. package/dist/utils/handlePositioning.js +2 -2
  369. package/dist/utils/icons.d.ts +1 -1
  370. package/dist/utils/icons.js +74 -74
  371. package/dist/utils/logger.d.ts +1 -1
  372. package/dist/utils/logger.js +7 -7
  373. package/dist/utils/nodeStatus.d.ts +1 -1
  374. package/dist/utils/nodeStatus.js +48 -48
  375. package/dist/utils/nodeTypes.d.ts +1 -1
  376. package/dist/utils/nodeTypes.js +21 -20
  377. package/dist/utils/nodeWrapper.d.ts +7 -7
  378. package/dist/utils/nodeWrapper.js +21 -19
  379. package/dist/utils/performanceUtils.d.ts +1 -1
  380. package/dist/utils/performanceUtils.js +2 -1
  381. package/dist/utils/sanitize.js +1 -1
  382. package/dist/utils/uischema.d.ts +2 -2
  383. package/dist/utils/uischema.js +8 -8
  384. package/dist/utils/validation.js +20 -8
  385. package/package.json +1 -1
@@ -5,949 +5,1004 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
- import {
9
- SvelteFlow,
10
- ConnectionLineType,
11
- Controls,
12
- Background,
13
- BackgroundVariant,
14
- MiniMap,
15
- SvelteFlowProvider,
16
- type ColorMode
17
- } from '@xyflow/svelte';
18
- import '@xyflow/svelte/dist/style.css';
19
- import {
20
- getResolvedTheme,
21
- getEditorSettings,
22
- getBehaviorSettings
23
- } from '../stores/settingsStore.svelte.js';
24
- import type {
25
- WorkflowNode as WorkflowNodeType,
26
- NodeMetadata,
27
- Workflow,
28
- WorkflowEdge
29
- } from '../types/index.js';
30
- import CanvasBanner from './CanvasBanner.svelte';
31
- import FlowDropZone from './FlowDropZone.svelte';
32
- import EdgeRefresher from './EdgeRefresher.svelte';
33
- import { tick, untrack } from 'svelte';
34
- import type { EndpointConfig } from '../config/endpoints.js';
35
- import ConnectionLine from './ConnectionLine.svelte';
36
- import FlowDropEdge from './FlowDropEdge.svelte';
37
- import { getWorkflowStore, workflowActions } from '../stores/workflowStore.svelte.js';
38
- import { historyActions, setOnRestoreCallback } from '../stores/historyStore.svelte.js';
39
- import UniversalNode from './UniversalNode.svelte';
40
- import {
41
- EdgeStylingHelper,
42
- NodeOperationsHelper,
43
- WorkflowOperationsHelper,
44
- ConfigurationHelper
45
- } from '../helpers/workflowEditorHelper.js';
46
- import type { NodeExecutionInfo } from '../types/index.js';
47
- import { Toaster } from 'svelte-5-french-toast';
48
- import {
49
- flowdropToastOptions,
50
- FLOWDROP_TOASTER_CLASS,
51
- apiToasts
52
- } from '../services/toastService.js';
53
- import {
54
- ProximityConnectHelper,
55
- type ProximityEdgeCandidate
56
- } from '../helpers/proximityConnect.js';
57
- import PortCoordinateTracker from './PortCoordinateTracker.svelte';
58
- import { getPortCoordinateSnapshot } from '../stores/portCoordinateStore.svelte.js';
59
- import { logger } from '../utils/logger.js';
60
- import { validateWorkflowData } from '../utils/validation.js';
61
- import { createEditorStateMachine } from '../stores/editorStateMachine.svelte.js';
62
-
63
- interface Props {
64
- nodes?: NodeMetadata[];
65
- endpointConfig?: EndpointConfig;
66
- height?: string | number;
67
- width?: string | number;
68
- isConfigSidebarOpen?: boolean;
69
- selectedNodeForConfig?: WorkflowNodeType | null;
70
- openConfigSidebar?: (node: WorkflowNodeType) => void;
71
- closeConfigSidebar?: () => void;
72
- // New configuration options for pipeline status mode
73
- lockWorkflow?: boolean;
74
- readOnly?: boolean;
75
- nodeStatuses?: Record<string, 'pending' | 'running' | 'completed' | 'error'>;
76
- // Pipeline ID for fetching node execution info from jobs
77
- pipelineId?: string;
78
- }
79
-
80
- let props: Props = $props();
81
-
82
- // ---------------------------------------------------------------------------
83
- // Editor State Machine
84
- // Centralizes reactive guards — replaces scattered boolean flags
85
- // (isDraggingNode, lastEditorStoreValue identity checks, etc.)
86
- // ---------------------------------------------------------------------------
87
- const machine = createEditorStateMachine();
88
-
89
- // Dev-mode transition logging
90
- if (import.meta.env?.DEV) {
91
- machine.onTransition((from, event, to) => {
92
- logger.debug(`[EditorFSM] ${from} --${event}--> ${to}`);
93
- });
94
- }
95
-
96
- // Proximity connect state
97
- let currentProximityCandidates = $state<ProximityEdgeCandidate[]>([]);
98
-
99
- // Port coordinate tracker state
100
- let portCoordNodeToUpdate = $state<WorkflowNodeType | null>(null);
101
- let portCoordRebuildTrigger = $state(0);
102
-
103
- // ---------------------------------------------------------------------------
104
- // Flow state — bound to SvelteFlow via bind:nodes / bind:edges
105
- // These are $state.raw to prevent deep proxy leaking (SvelteFlow mutates
106
- // node internals during drag which would cause infinite loops with $state).
107
- // ---------------------------------------------------------------------------
108
- let flowNodes = $state.raw<WorkflowNodeType[]>([]);
109
- let flowEdges = $state.raw<WorkflowEdge[]>([]);
110
-
111
- // Execution info loading state
112
- let loadExecutionInfoTimeout: number | null = null;
113
- let executionInfoAbortController: AbortController | null = null;
114
-
115
- /**
116
- * Key for SvelteFlow component — changes when workflow ID changes.
117
- * Forces SvelteFlow to remount with fresh state, allowing fitView to work correctly.
118
- */
119
- let svelteFlowKey = $derived(getWorkflowStore()?.id ?? 'default');
120
-
121
- /**
122
- * Derive snap grid configuration from editor settings
123
- */
124
- let snapGrid = $derived(
125
- getEditorSettings().snapToGrid
126
- ? ([getEditorSettings().gridSize, getEditorSettings().gridSize] as [number, number])
127
- : undefined
128
- );
129
-
130
- /**
131
- * Derive initial viewport configuration from editor settings
132
- */
133
- let initialViewport = $derived({
134
- zoom: getEditorSettings().defaultZoom,
135
- x: 0,
136
- y: 0
137
- });
138
-
139
- // ---------------------------------------------------------------------------
140
- // Helper: derive flowNodes/flowEdges from a Workflow object
141
- // ---------------------------------------------------------------------------
142
- function buildFlowNodesFromStore(workflow: Workflow): {
143
- nodes: WorkflowNodeType[];
144
- edges: WorkflowEdge[];
145
- } {
146
- const nodesWithCallbacks = workflow.nodes.map((node) => ({
147
- ...node,
148
- data: {
149
- ...node.data,
150
- onConfigOpen: props.openConfigSidebar
151
- }
152
- }));
153
- const styledEdges = EdgeStylingHelper.updateEdgeStyles(workflow.edges, nodesWithCallbacks);
154
- return { nodes: nodesWithCallbacks, edges: styledEdges };
155
- }
156
-
157
- // ---------------------------------------------------------------------------
158
- // Helper: sync current flowNodes/flowEdges back to the global store
159
- // ---------------------------------------------------------------------------
160
- function syncFlowToStore(): void {
161
- const storeValue = untrack(() => getWorkflowStore());
162
- if (!storeValue) return;
163
- const updatedWorkflow = WorkflowOperationsHelper.updateWorkflow(
164
- storeValue,
165
- flowNodes,
166
- flowEdges
167
- );
168
- workflowActions.updateWorkflow(updatedWorkflow);
169
- }
170
-
171
- // ---------------------------------------------------------------------------
172
- // Single sync effect: workflowStore → flowNodes / flowEdges
173
- // Replaces the old Effect A (store→currentWorkflow) + Effect B (currentWorkflow→flow).
174
- // Suppressed during operations via state machine; handlers update flowNodes directly.
175
- // ---------------------------------------------------------------------------
176
- let previousSyncedWorkflowId: string | null = null;
177
-
178
- $effect(() => {
179
- const storeValue = getWorkflowStore();
180
-
181
- // Suppressed during operations — handlers write to flowNodes directly
182
- if (untrack(() => machine.permissions.suppressEffect)) return;
183
-
184
- if (!storeValue) {
185
- if (flowNodes.length > 0 || flowEdges.length > 0) {
186
- flowNodes = [];
187
- flowEdges = [];
188
- previousSyncedWorkflowId = null;
189
- untrack(() => machine.send('WORKFLOW_CLEARED'));
190
- }
191
- return;
192
- }
193
-
194
- const isNewWorkflow = storeValue.id !== previousSyncedWorkflowId;
195
-
196
- if (isNewWorkflow) {
197
- untrack(() =>
198
- machine.send(previousSyncedWorkflowId ? 'WORKFLOW_SWITCHED' : 'WORKFLOW_LOADED')
199
- );
200
- }
201
-
202
- // Derive flowNodes/flowEdges from store
203
- const derived = buildFlowNodesFromStore(storeValue);
204
- flowNodes = derived.nodes;
205
- flowEdges = derived.edges;
206
- previousSyncedWorkflowId = storeValue.id;
207
-
208
- // Trigger port coordinate rebuild after workflow load
209
- if (getEditorSettings().proximityConnect) {
210
- portCoordRebuildTrigger = Date.now();
211
- }
212
-
213
- if (isNewWorkflow) {
214
- untrack(() => machine.send('LOAD_COMPLETE'));
215
- }
216
- });
217
-
218
- // ---------------------------------------------------------------------------
219
- // Execution info effect (separate — async, depends on workflow + pipeline ID)
220
- // ---------------------------------------------------------------------------
221
- let previousExecWorkflowId: string | null = null;
222
- let previousExecPipelineId: string | undefined = undefined;
223
-
224
- $effect(() => {
225
- const storeValue = getWorkflowStore();
226
- const pipelineId = props.pipelineId;
227
-
228
- if (!storeValue || !pipelineId) return;
229
-
230
- const workflowChanged = storeValue.id !== previousExecWorkflowId;
231
- const pipelineChanged = pipelineId !== previousExecPipelineId;
232
-
233
- if (!workflowChanged && !pipelineChanged) return;
234
-
235
- previousExecWorkflowId = storeValue.id;
236
- previousExecPipelineId = pipelineId;
237
-
238
- // Cancel any pending timeout / in-flight request
239
- if (loadExecutionInfoTimeout) {
240
- clearTimeout(loadExecutionInfoTimeout);
241
- loadExecutionInfoTimeout = null;
242
- }
243
- if (executionInfoAbortController) {
244
- executionInfoAbortController.abort();
245
- executionInfoAbortController = null;
246
- }
247
-
248
- // Schedule loading with requestIdleCallback (falls back to setTimeout)
249
- if (typeof requestIdleCallback !== 'undefined') {
250
- loadExecutionInfoTimeout = requestIdleCallback(
251
- () => {
252
- loadNodeExecutionInfo();
253
- },
254
- { timeout: 500 }
255
- ) as unknown as number;
256
- } else {
257
- loadExecutionInfoTimeout = setTimeout(() => {
258
- loadNodeExecutionInfo();
259
- }, 300) as unknown as number;
260
- }
261
- });
262
-
263
- // ---------------------------------------------------------------------------
264
- // History restore callback
265
- // ---------------------------------------------------------------------------
266
- $effect(() => {
267
- setOnRestoreCallback((restoredWorkflow: Workflow) => {
268
- machine.send('START_RESTORE');
269
- // Update the store (effect is suppressed during 'restoring')
270
- workflowActions.restoreFromHistory(restoredWorkflow);
271
- // Derive flowNodes/flowEdges directly for immediate visual update
272
- const derived = buildFlowNodesFromStore(restoredWorkflow);
273
- flowNodes = derived.nodes;
274
- flowEdges = derived.edges;
275
- machine.send('RESTORE_COMPLETE');
276
- // After RESTORE_COMPLETE → idle, the sync effect runs but produces
277
- // the same data (no-op re-derive).
278
- });
279
-
280
- return () => {
281
- setOnRestoreCallback(null);
282
- };
283
- });
284
-
285
- /**
286
- * Load node execution information for all nodes in the workflow
287
- */
288
- async function loadNodeExecutionInfo(): Promise<void> {
289
- const workflow = untrack(() => getWorkflowStore());
290
- if (!workflow?.nodes || !props.pipelineId) return;
291
-
292
- try {
293
- executionInfoAbortController = new AbortController();
294
-
295
- const executionInfo = await NodeOperationsHelper.loadNodeExecutionInfo(
296
- workflow,
297
- props.pipelineId
298
- );
299
-
300
- if (executionInfoAbortController?.signal.aborted) return;
301
-
302
- const defaultExecutionInfo: NodeExecutionInfo = {
303
- status: 'idle' as const,
304
- executionCount: 0,
305
- isExecuting: false
306
- };
307
-
308
- // Update flowNodes with execution info (visual-only, no store sync needed)
309
- flowNodes = flowNodes.map((node) => ({
310
- ...node,
311
- data: {
312
- ...node.data,
313
- executionInfo: executionInfo[node.id] || defaultExecutionInfo
314
- }
315
- }));
316
-
317
- executionInfoAbortController = null;
318
- } catch (error) {
319
- if (error instanceof Error && error.name !== 'AbortError') {
320
- logger.error('Failed to load node execution info:', error);
321
- }
322
- }
323
- }
324
-
325
- // The global store should be initialized by the parent App component
326
-
327
- // Sidebar is now always visible - removed toggle functionality
328
-
329
- // Node types for Svelte Flow - using UniversalNode for all node types
330
- // All nodes use 'universalNode' type, and UniversalNode handles internal switching
331
- const nodeTypes = {
332
- universalNode: UniversalNode
333
- };
334
-
335
- // Use custom edge that shortens the path so the stroke ends at the arrow base
336
- const edgeTypes = {
337
- default: FlowDropEdge
338
- };
339
-
340
- // Handle arrows in our custom connection handler
341
- const defaultEdgeOptions = {};
342
-
343
- /**
344
- * Handle node drag start
345
- *
346
- * Transitions the state machine to 'dragging', which suppresses
347
- * the sync effect to prevent reactive loops during high-frequency
348
- * position updates. SvelteFlow mutates flowNodes directly via bind:nodes.
349
- */
350
- function handleNodeDragStart(): void {
351
- machine.send('START_DRAG');
352
- // Clear any leftover proximity previews
353
- currentProximityCandidates = [];
354
- }
355
-
356
- /**
357
- * Handle node drag - compute proximity connect preview edges
358
- * Called continuously during drag if proximity connect is enabled.
359
- * Uses port-to-port distance via the port coordinate store.
360
- */
361
- function handleNodeDrag({
362
- targetNode
363
- }: {
364
- targetNode: WorkflowNodeType | null;
365
- nodes: WorkflowNodeType[];
366
- event: MouseEvent | TouchEvent;
367
- }): void {
368
- if (
369
- !getEditorSettings().proximityConnect ||
370
- !targetNode ||
371
- props.readOnly ||
372
- props.lockWorkflow
373
- ) {
374
- if (currentProximityCandidates.length > 0) {
375
- flowEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
376
- currentProximityCandidates = [];
377
- }
378
- portCoordNodeToUpdate = null;
379
- return;
380
- }
381
-
382
- // Update the dragged node's port coordinates (position changed during drag)
383
- portCoordNodeToUpdate = targetNode;
384
-
385
- // Remove previous preview edges
386
- const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
387
-
388
- // Find the best compatible edge using port-to-port distance
389
- const portCoordinates = getPortCoordinateSnapshot();
390
- const candidates =
391
- portCoordinates.size > 0
392
- ? ProximityConnectHelper.findCompatibleEdgesByPortCoordinates(
393
- targetNode.id,
394
- portCoordinates,
395
- baseEdges,
396
- getEditorSettings().proximityConnectDistance
397
- )
398
- : ProximityConnectHelper.findCompatibleEdges(
399
- targetNode,
400
- flowNodes,
401
- baseEdges,
402
- getEditorSettings().proximityConnectDistance
403
- );
404
-
405
- // Create preview edges
406
- const previews = ProximityConnectHelper.createPreviewEdges(candidates);
407
-
408
- // Update state
409
- currentProximityCandidates = candidates;
410
- flowEdges = [...baseEdges, ...previews];
411
- }
412
-
413
- /**
414
- * Handle node drag stop
415
- *
416
- * Still in 'dragging' state — sync effect suppressed.
417
- * Syncs final positions to store, pushes history, then transitions to idle.
418
- */
419
- function handleNodeDragStop(): void {
420
- portCoordNodeToUpdate = null;
421
-
422
- // Finalize proximity connect if there are candidates
423
- if (getEditorSettings().proximityConnect && currentProximityCandidates.length > 0) {
424
- const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
425
- const permanentEdges = ProximityConnectHelper.createPermanentEdges(
426
- currentProximityCandidates
427
- );
428
-
429
- for (const edge of permanentEdges) {
430
- const sourceNode = flowNodes.find((n) => n.id === edge.source);
431
- const targetNode = flowNodes.find((n) => n.id === edge.target);
432
- if (sourceNode && targetNode) {
433
- EdgeStylingHelper.applyConnectionStyling(edge, sourceNode, targetNode);
434
- }
435
- }
436
-
437
- flowEdges = [...baseEdges, ...permanentEdges];
438
- currentProximityCandidates = [];
439
- }
440
-
441
- // Sync flowNodes/flowEdges → store
442
- syncFlowToStore();
443
-
444
- // Push history AFTER the drag completed
445
- const storeValue = getWorkflowStore();
446
- if (storeValue) {
447
- workflowActions.pushHistory('Move node', storeValue);
448
- }
449
-
450
- // Transition to idle sync effect is now unblocked
451
- machine.send('STOP_DRAG');
452
- }
453
-
454
- /**
455
- * Handle new connections between nodes
456
- */
457
- async function handleConnect(connection: {
458
- source: string;
459
- target: string;
460
- sourceHandle?: string;
461
- targetHandle?: string;
462
- }): Promise<void> {
463
- machine.send('START_CONNECT');
464
-
465
- // SvelteFlow auto-creates the edge via bind:edges — wait for DOM update
466
- await tick();
467
-
468
- // Apply styling to all edges (including the new one)
469
- flowEdges = EdgeStylingHelper.updateEdgeStyles(flowEdges, flowNodes);
470
-
471
- // Sync to store
472
- syncFlowToStore();
473
-
474
- const storeValue = getWorkflowStore();
475
- if (storeValue) {
476
- workflowActions.pushHistory('Add connection', storeValue);
477
- }
478
-
479
- machine.send('CONNECTION_MADE');
480
- }
481
-
482
- /**
483
- * Handle before delete - show confirmation dialog if enabled in settings
484
- *
485
- * This callback is called before nodes/edges are deleted.
486
- * Return true to proceed with deletion, false to cancel.
487
- *
488
- * @param params - Object containing nodes and edges to be deleted
489
- * @returns Promise resolving to true if deletion should proceed, false to cancel
490
- */
491
- async function handleBeforeDelete(params: {
492
- nodes: WorkflowNodeType[];
493
- edges: WorkflowEdge[];
494
- }): Promise<boolean> {
495
- // If confirmDelete setting is enabled, show confirmation dialog
496
- if (getBehaviorSettings().confirmDelete) {
497
- const nodeCount = params.nodes.length;
498
- const edgeCount = params.edges.length;
499
-
500
- // Build a descriptive message
501
- let message = 'Are you sure you want to delete ';
502
- const parts: string[] = [];
503
-
504
- if (nodeCount > 0) {
505
- parts.push(`${nodeCount} node${nodeCount > 1 ? 's' : ''}`);
506
- }
507
- if (edgeCount > 0) {
508
- parts.push(`${edgeCount} connection${edgeCount > 1 ? 's' : ''}`);
509
- }
510
-
511
- message += parts.join(' and ') + '?';
512
-
513
- // Show native confirmation dialog
514
- const confirmed = window.confirm(message);
515
- if (!confirmed) {
516
- return false;
517
- }
518
- }
519
-
520
- // Don't push to history here - we'll push AFTER deletion in handleNodesDelete
521
- // This ensures undo will restore the state before deletion
522
- return true;
523
- }
524
-
525
- /**
526
- * Handle node deletion - automatically remove connected edges and push to history
527
- */
528
- function handleNodesDelete(params: { nodes: WorkflowNodeType[]; edges: WorkflowEdge[] }): void {
529
- machine.send('START_DELETE');
530
-
531
- const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
532
-
533
- // Filter out edges connected to deleted nodes
534
- flowEdges = flowEdges.filter(
535
- (edge) => !deletedNodeIds.has(edge.source) && !deletedNodeIds.has(edge.target)
536
- );
537
-
538
- // Sync to store
539
- syncFlowToStore();
540
-
541
- // Push to history AFTER the deletion so undo restores the previous state
542
- const nodeCount = params.nodes.length;
543
- const edgeCount = params.edges.length;
544
- let description = 'Delete';
545
- if (nodeCount > 0 && edgeCount > 0) {
546
- description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''} and ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
547
- } else if (nodeCount > 0) {
548
- description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''}`;
549
- } else if (edgeCount > 0) {
550
- description = `Delete ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
551
- }
552
- const storeValue = getWorkflowStore();
553
- if (storeValue) {
554
- workflowActions.pushHistory(description, storeValue);
555
- }
556
-
557
- machine.send('DELETE_COMPLETE');
558
- }
559
-
560
- // Edge styling will be handled when edges are first created or manually updated
561
-
562
- // Configure endpoints when props change
563
- $effect(() => {
564
- if (props.endpointConfig) {
565
- ConfigurationHelper.configureEndpoints(props.endpointConfig);
566
- }
567
- });
568
-
569
- /**
570
- * Check if workflow has cycles
571
- */
572
- function checkWorkflowCycles(): boolean {
573
- return WorkflowOperationsHelper.checkWorkflowCycles(flowNodes, flowEdges);
574
- }
575
-
576
- /**
577
- * Handle drop event and add new node to canvas
578
- */
579
- async function handleNodeDrop(
580
- nodeTypeData: string,
581
- position: { x: number; y: number }
582
- ): Promise<void> {
583
- machine.send('START_DROP');
584
-
585
- const newNode = NodeOperationsHelper.createNodeFromDrop(nodeTypeData, position, flowNodes);
586
-
587
- if (newNode) {
588
- // Add onConfigOpen callback and append to flowNodes for immediate visual feedback
589
- const nodeWithCallback = {
590
- ...newNode,
591
- data: { ...newNode.data, onConfigOpen: props.openConfigSidebar }
592
- };
593
- flowNodes = [...flowNodes, nodeWithCallback];
594
-
595
- // Sync to store
596
- syncFlowToStore();
597
-
598
- await tick();
599
-
600
- const storeValue = getWorkflowStore();
601
- if (storeValue) {
602
- workflowActions.pushHistory('Add node', storeValue);
603
- }
604
- } else {
605
- logger.warn('Failed to create node from drop data');
606
- }
607
-
608
- machine.send('DROP_COMPLETE');
609
- }
610
-
611
- /**
612
- * Handle a workflow JSON file dropped directly onto the canvas.
613
- *
614
- * Validates the JSON against the minimum required Workflow fields and, if valid,
615
- * loads it into the workflow store. Shows a toast on validation failure or read error.
616
- */
617
- function handleWorkflowFileDrop(file: File): void {
618
- const reader = new FileReader();
619
- reader.onload = (event) => {
620
- try {
621
- const text = event.target?.result;
622
- if (typeof text !== 'string') {
623
- throw new Error('Could not read file contents.');
624
- }
625
- const data = JSON.parse(text);
626
- const validation = validateWorkflowData(data);
627
- if (!validation.valid) {
628
- apiToasts.error('Import workflow', validation.error ?? 'Invalid workflow JSON');
629
- logger.warn('Workflow file drop validation failed:', validation.error);
630
- return;
631
- }
632
- workflowActions.initialize(data as Workflow);
633
- } catch (error) {
634
- const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
635
- logger.error('Workflow file drop import failed:', errorObj);
636
- apiToasts.error('Import workflow', errorObj.message);
637
- }
638
- };
639
- reader.onerror = () => {
640
- const message = 'Failed to read the dropped file.';
641
- logger.error(message);
642
- apiToasts.error('Import workflow', message);
643
- };
644
- reader.readAsText(file);
645
- }
646
-
647
- /**
648
- * Node ID that needs edge refresh - used to trigger EdgeRefresher component
649
- */
650
- let nodeIdToRefresh = $state<string | null>(null);
651
-
652
- /**
653
- * Update a node's data in the local editor state.
654
- * Called by App.svelte AFTER it has already updated the global store via
655
- * workflowActions.updateNode(). We only need to update flowNodes for
656
- * immediate visual feedback — no store sync needed.
657
- *
658
- * @param nodeId - The ID of the node to update
659
- * @param dataUpdates - Partial data updates to merge into the node's data
660
- */
661
- export function updateNodeData(
662
- nodeId: string,
663
- dataUpdates: Partial<WorkflowNodeType['data']>
664
- ): void {
665
- machine.send('START_NODE_UPDATE');
666
-
667
- flowNodes = flowNodes.map((node) => {
668
- if (node.id === nodeId) {
669
- return {
670
- ...node,
671
- data: {
672
- ...node.data,
673
- ...dataUpdates
674
- }
675
- };
676
- }
677
- return node;
678
- });
679
-
680
- machine.send('UPDATE_COMPLETE');
681
- }
682
-
683
- /**
684
- * Force edge position recalculation after node config changes
685
- * This should be called after saving gateway/switch node configs where branches are reordered
686
- * Svelte Flow doesn't automatically recalculate edge paths when handle positions change
687
- * @param nodeId - The ID of the node whose handles have changed position
688
- */
689
- export async function refreshEdgePositions(nodeId: string): Promise<void> {
690
- // Wait for DOM to update with new handle positions
691
- await tick();
692
-
693
- // Trigger the EdgeRefresher component to call updateNodeInternals
694
- nodeIdToRefresh = nodeId;
695
- }
696
-
697
- /**
698
- * Callback when edge refresh is complete
699
- */
700
- function handleEdgeRefreshComplete(): void {
701
- nodeIdToRefresh = null;
702
- }
703
-
704
- /**
705
- * Handle keyboard shortcuts for undo/redo
706
- *
707
- * - Ctrl+Z (or Cmd+Z on Mac): Undo
708
- * - Ctrl+Shift+Z (or Cmd+Shift+Z): Redo
709
- * - Ctrl+Y (or Cmd+Y): Redo (Windows convention)
710
- */
711
- function handleKeydown(event: KeyboardEvent): void {
712
- // Check for Ctrl (Windows/Linux) or Cmd (Mac)
713
- const isModifierPressed = event.ctrlKey || event.metaKey;
714
-
715
- if (!isModifierPressed) {
716
- return;
717
- }
718
-
719
- // Don't handle shortcuts if user is typing in an input, textarea, or contenteditable
720
- const target = event.target as HTMLElement;
721
- const isInputElement =
722
- target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
723
-
724
- if (isInputElement) {
725
- return;
726
- }
727
-
728
- // Undo: Ctrl+Z (without Shift)
729
- if (event.key === 'z' && !event.shiftKey) {
730
- event.preventDefault();
731
- historyActions.undo();
732
- return;
733
- }
734
-
735
- // Redo: Ctrl+Shift+Z or Ctrl+Y
736
- if ((event.key === 'z' && event.shiftKey) || event.key === 'y') {
737
- event.preventDefault();
738
- historyActions.redo();
739
- return;
740
- }
741
- }
8
+ import {
9
+ SvelteFlow,
10
+ ConnectionLineType,
11
+ Controls,
12
+ Background,
13
+ BackgroundVariant,
14
+ MiniMap,
15
+ SvelteFlowProvider,
16
+ type ColorMode,
17
+ } from "@xyflow/svelte";
18
+ import "@xyflow/svelte/dist/style.css";
19
+ import {
20
+ getResolvedTheme,
21
+ getEditorSettings,
22
+ getBehaviorSettings,
23
+ } from "../stores/settingsStore.svelte.js";
24
+ import type {
25
+ WorkflowNode as WorkflowNodeType,
26
+ NodeMetadata,
27
+ Workflow,
28
+ WorkflowEdge,
29
+ } from "../types/index.js";
30
+ import CanvasBanner from "./CanvasBanner.svelte";
31
+ import FlowDropZone from "./FlowDropZone.svelte";
32
+ import EdgeRefresher from "./EdgeRefresher.svelte";
33
+ import { tick, untrack } from "svelte";
34
+ import type { EndpointConfig } from "../config/endpoints.js";
35
+ import ConnectionLine from "./ConnectionLine.svelte";
36
+ import FlowDropEdge from "./FlowDropEdge.svelte";
37
+ import {
38
+ getWorkflowStore,
39
+ workflowActions,
40
+ } from "../stores/workflowStore.svelte.js";
41
+ import {
42
+ historyActions,
43
+ setOnRestoreCallback,
44
+ } from "../stores/historyStore.svelte.js";
45
+ import UniversalNode from "./UniversalNode.svelte";
46
+ import {
47
+ EdgeStylingHelper,
48
+ NodeOperationsHelper,
49
+ WorkflowOperationsHelper,
50
+ ConfigurationHelper,
51
+ } from "../helpers/workflowEditorHelper.js";
52
+ import type { NodeExecutionInfo } from "../types/index.js";
53
+ import { Toaster } from "svelte-5-french-toast";
54
+ import {
55
+ flowdropToastOptions,
56
+ FLOWDROP_TOASTER_CLASS,
57
+ apiToasts,
58
+ } from "../services/toastService.js";
59
+ import {
60
+ ProximityConnectHelper,
61
+ type ProximityEdgeCandidate,
62
+ } from "../helpers/proximityConnect.js";
63
+ import PortCoordinateTracker from "./PortCoordinateTracker.svelte";
64
+ import { getPortCoordinateSnapshot } from "../stores/portCoordinateStore.svelte.js";
65
+ import { logger } from "../utils/logger.js";
66
+ import { validateWorkflowData } from "../utils/validation.js";
67
+ import { createEditorStateMachine } from "../stores/editorStateMachine.svelte.js";
68
+
69
+ interface Props {
70
+ nodes?: NodeMetadata[];
71
+ endpointConfig?: EndpointConfig;
72
+ height?: string | number;
73
+ width?: string | number;
74
+ isConfigSidebarOpen?: boolean;
75
+ selectedNodeForConfig?: WorkflowNodeType | null;
76
+ openConfigSidebar?: (node: WorkflowNodeType) => void;
77
+ closeConfigSidebar?: () => void;
78
+ // New configuration options for pipeline status mode
79
+ lockWorkflow?: boolean;
80
+ readOnly?: boolean;
81
+ nodeStatuses?: Record<
82
+ string,
83
+ "pending" | "running" | "completed" | "error"
84
+ >;
85
+ // Pipeline ID for fetching node execution info from jobs
86
+ pipelineId?: string;
87
+ }
88
+
89
+ let props: Props = $props();
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Editor State Machine
93
+ // Centralizes reactive guards — replaces scattered boolean flags
94
+ // (isDraggingNode, lastEditorStoreValue identity checks, etc.)
95
+ // ---------------------------------------------------------------------------
96
+ const machine = createEditorStateMachine();
97
+
98
+ // Dev-mode transition logging
99
+ if (import.meta.env?.DEV) {
100
+ machine.onTransition((from, event, to) => {
101
+ logger.debug(`[EditorFSM] ${from} --${event}--> ${to}`);
102
+ });
103
+ }
104
+
105
+ // Proximity connect state
106
+ let currentProximityCandidates = $state<ProximityEdgeCandidate[]>([]);
107
+
108
+ // Port coordinate tracker state
109
+ let portCoordNodeToUpdate = $state<WorkflowNodeType | null>(null);
110
+ let portCoordRebuildTrigger = $state(0);
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Flow state bound to SvelteFlow via bind:nodes / bind:edges
114
+ // These are $state.raw to prevent deep proxy leaking (SvelteFlow mutates
115
+ // node internals during drag which would cause infinite loops with $state).
116
+ // ---------------------------------------------------------------------------
117
+ let flowNodes = $state.raw<WorkflowNodeType[]>([]);
118
+ let flowEdges = $state.raw<WorkflowEdge[]>([]);
119
+
120
+ // Execution info loading state
121
+ let loadExecutionInfoTimeout: number | null = null;
122
+ let executionInfoAbortController: AbortController | null = null;
123
+
124
+ /**
125
+ * Key for SvelteFlow component — changes when workflow ID changes.
126
+ * Forces SvelteFlow to remount with fresh state, allowing fitView to work correctly.
127
+ */
128
+ let svelteFlowKey = $derived(getWorkflowStore()?.id ?? "default");
129
+
130
+ /**
131
+ * Derive snap grid configuration from editor settings
132
+ */
133
+ let snapGrid = $derived(
134
+ getEditorSettings().snapToGrid
135
+ ? ([getEditorSettings().gridSize, getEditorSettings().gridSize] as [
136
+ number,
137
+ number,
138
+ ])
139
+ : undefined,
140
+ );
141
+
142
+ /**
143
+ * Derive initial viewport configuration from editor settings
144
+ */
145
+ let initialViewport = $derived({
146
+ zoom: getEditorSettings().defaultZoom,
147
+ x: 0,
148
+ y: 0,
149
+ });
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Helper: derive flowNodes/flowEdges from a Workflow object
153
+ // ---------------------------------------------------------------------------
154
+ function buildFlowNodesFromStore(workflow: Workflow): {
155
+ nodes: WorkflowNodeType[];
156
+ edges: WorkflowEdge[];
157
+ } {
158
+ const nodesWithCallbacks = workflow.nodes.map((node) => ({
159
+ ...node,
160
+ data: {
161
+ ...node.data,
162
+ onConfigOpen: props.openConfigSidebar,
163
+ },
164
+ }));
165
+ const styledEdges = EdgeStylingHelper.updateEdgeStyles(
166
+ workflow.edges,
167
+ nodesWithCallbacks,
168
+ );
169
+ return { nodes: nodesWithCallbacks, edges: styledEdges };
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Helper: sync current flowNodes/flowEdges back to the global store
174
+ // ---------------------------------------------------------------------------
175
+ function syncFlowToStore(): void {
176
+ const storeValue = untrack(() => getWorkflowStore());
177
+ if (!storeValue) return;
178
+ const updatedWorkflow = WorkflowOperationsHelper.updateWorkflow(
179
+ storeValue,
180
+ flowNodes,
181
+ flowEdges,
182
+ );
183
+ workflowActions.updateWorkflow(updatedWorkflow);
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Single sync effect: workflowStore → flowNodes / flowEdges
188
+ // Replaces the old Effect A (store→currentWorkflow) + Effect B (currentWorkflow→flow).
189
+ // Suppressed during operations via state machine; handlers update flowNodes directly.
190
+ // ---------------------------------------------------------------------------
191
+ let previousSyncedWorkflowId: string | null = null;
192
+
193
+ $effect(() => {
194
+ const storeValue = getWorkflowStore();
195
+
196
+ // Suppressed during operations — handlers write to flowNodes directly
197
+ if (untrack(() => machine.permissions.suppressEffect)) return;
198
+
199
+ if (!storeValue) {
200
+ if (flowNodes.length > 0 || flowEdges.length > 0) {
201
+ flowNodes = [];
202
+ flowEdges = [];
203
+ previousSyncedWorkflowId = null;
204
+ untrack(() => machine.send("WORKFLOW_CLEARED"));
205
+ }
206
+ return;
207
+ }
208
+
209
+ const isNewWorkflow = storeValue.id !== previousSyncedWorkflowId;
210
+
211
+ if (isNewWorkflow) {
212
+ untrack(() =>
213
+ machine.send(
214
+ previousSyncedWorkflowId ? "WORKFLOW_SWITCHED" : "WORKFLOW_LOADED",
215
+ ),
216
+ );
217
+ }
218
+
219
+ // Derive flowNodes/flowEdges from store
220
+ const derived = buildFlowNodesFromStore(storeValue);
221
+ flowNodes = derived.nodes;
222
+ flowEdges = derived.edges;
223
+ previousSyncedWorkflowId = storeValue.id;
224
+
225
+ // Trigger port coordinate rebuild after workflow load
226
+ if (getEditorSettings().proximityConnect) {
227
+ portCoordRebuildTrigger = Date.now();
228
+ }
229
+
230
+ if (isNewWorkflow) {
231
+ untrack(() => machine.send("LOAD_COMPLETE"));
232
+ }
233
+ });
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Execution info effect (separate — async, depends on workflow + pipeline ID)
237
+ // ---------------------------------------------------------------------------
238
+ let previousExecWorkflowId: string | null = null;
239
+ let previousExecPipelineId: string | undefined = undefined;
240
+
241
+ $effect(() => {
242
+ const storeValue = getWorkflowStore();
243
+ const pipelineId = props.pipelineId;
244
+
245
+ if (!storeValue || !pipelineId) return;
246
+
247
+ const workflowChanged = storeValue.id !== previousExecWorkflowId;
248
+ const pipelineChanged = pipelineId !== previousExecPipelineId;
249
+
250
+ if (!workflowChanged && !pipelineChanged) return;
251
+
252
+ previousExecWorkflowId = storeValue.id;
253
+ previousExecPipelineId = pipelineId;
254
+
255
+ // Cancel any pending timeout / in-flight request
256
+ if (loadExecutionInfoTimeout) {
257
+ clearTimeout(loadExecutionInfoTimeout);
258
+ loadExecutionInfoTimeout = null;
259
+ }
260
+ if (executionInfoAbortController) {
261
+ executionInfoAbortController.abort();
262
+ executionInfoAbortController = null;
263
+ }
264
+
265
+ // Schedule loading with requestIdleCallback (falls back to setTimeout)
266
+ if (typeof requestIdleCallback !== "undefined") {
267
+ loadExecutionInfoTimeout = requestIdleCallback(
268
+ () => {
269
+ loadNodeExecutionInfo();
270
+ },
271
+ { timeout: 500 },
272
+ ) as unknown as number;
273
+ } else {
274
+ loadExecutionInfoTimeout = setTimeout(() => {
275
+ loadNodeExecutionInfo();
276
+ }, 300) as unknown as number;
277
+ }
278
+ });
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // History restore callback
282
+ // ---------------------------------------------------------------------------
283
+ $effect(() => {
284
+ setOnRestoreCallback((restoredWorkflow: Workflow) => {
285
+ machine.send("START_RESTORE");
286
+ // Update the store (effect is suppressed during 'restoring')
287
+ workflowActions.restoreFromHistory(restoredWorkflow);
288
+ // Derive flowNodes/flowEdges directly for immediate visual update
289
+ const derived = buildFlowNodesFromStore(restoredWorkflow);
290
+ flowNodes = derived.nodes;
291
+ flowEdges = derived.edges;
292
+ machine.send("RESTORE_COMPLETE");
293
+ // After RESTORE_COMPLETE → idle, the sync effect runs but produces
294
+ // the same data (no-op re-derive).
295
+ });
296
+
297
+ return () => {
298
+ setOnRestoreCallback(null);
299
+ };
300
+ });
301
+
302
+ /**
303
+ * Load node execution information for all nodes in the workflow
304
+ */
305
+ async function loadNodeExecutionInfo(): Promise<void> {
306
+ const workflow = untrack(() => getWorkflowStore());
307
+ if (!workflow?.nodes || !props.pipelineId) return;
308
+
309
+ try {
310
+ executionInfoAbortController = new AbortController();
311
+
312
+ const executionInfo = await NodeOperationsHelper.loadNodeExecutionInfo(
313
+ workflow,
314
+ props.pipelineId,
315
+ );
316
+
317
+ if (executionInfoAbortController?.signal.aborted) return;
318
+
319
+ const defaultExecutionInfo: NodeExecutionInfo = {
320
+ status: "idle" as const,
321
+ executionCount: 0,
322
+ isExecuting: false,
323
+ };
324
+
325
+ // Update flowNodes with execution info (visual-only, no store sync needed)
326
+ flowNodes = flowNodes.map((node) => ({
327
+ ...node,
328
+ data: {
329
+ ...node.data,
330
+ executionInfo: executionInfo[node.id] || defaultExecutionInfo,
331
+ },
332
+ }));
333
+
334
+ executionInfoAbortController = null;
335
+ } catch (error) {
336
+ if (error instanceof Error && error.name !== "AbortError") {
337
+ logger.error("Failed to load node execution info:", error);
338
+ }
339
+ }
340
+ }
341
+
342
+ // The global store should be initialized by the parent App component
343
+
344
+ // Sidebar is now always visible - removed toggle functionality
345
+
346
+ // Node types for Svelte Flow - using UniversalNode for all node types
347
+ // All nodes use 'universalNode' type, and UniversalNode handles internal switching
348
+ const nodeTypes = {
349
+ universalNode: UniversalNode,
350
+ };
351
+
352
+ // Use custom edge that shortens the path so the stroke ends at the arrow base
353
+ const edgeTypes = {
354
+ default: FlowDropEdge,
355
+ };
356
+
357
+ // Handle arrows in our custom connection handler
358
+ const defaultEdgeOptions = {};
359
+
360
+ /**
361
+ * Handle node drag start
362
+ *
363
+ * Transitions the state machine to 'dragging', which suppresses
364
+ * the sync effect to prevent reactive loops during high-frequency
365
+ * position updates. SvelteFlow mutates flowNodes directly via bind:nodes.
366
+ */
367
+ function handleNodeDragStart(): void {
368
+ machine.send("START_DRAG");
369
+ // Clear any leftover proximity previews
370
+ currentProximityCandidates = [];
371
+ }
372
+
373
+ /**
374
+ * Handle node drag - compute proximity connect preview edges
375
+ * Called continuously during drag if proximity connect is enabled.
376
+ * Uses port-to-port distance via the port coordinate store.
377
+ */
378
+ function handleNodeDrag({
379
+ targetNode,
380
+ }: {
381
+ targetNode: WorkflowNodeType | null;
382
+ nodes: WorkflowNodeType[];
383
+ event: MouseEvent | TouchEvent;
384
+ }): void {
385
+ if (
386
+ !getEditorSettings().proximityConnect ||
387
+ !targetNode ||
388
+ props.readOnly ||
389
+ props.lockWorkflow
390
+ ) {
391
+ if (currentProximityCandidates.length > 0) {
392
+ flowEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
393
+ currentProximityCandidates = [];
394
+ }
395
+ portCoordNodeToUpdate = null;
396
+ return;
397
+ }
398
+
399
+ // Update the dragged node's port coordinates (position changed during drag)
400
+ portCoordNodeToUpdate = targetNode;
401
+
402
+ // Remove previous preview edges
403
+ const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
404
+
405
+ // Find the best compatible edge using port-to-port distance
406
+ const portCoordinates = getPortCoordinateSnapshot();
407
+ const candidates =
408
+ portCoordinates.size > 0
409
+ ? ProximityConnectHelper.findCompatibleEdgesByPortCoordinates(
410
+ targetNode.id,
411
+ portCoordinates,
412
+ baseEdges,
413
+ getEditorSettings().proximityConnectDistance,
414
+ )
415
+ : ProximityConnectHelper.findCompatibleEdges(
416
+ targetNode,
417
+ flowNodes,
418
+ baseEdges,
419
+ getEditorSettings().proximityConnectDistance,
420
+ );
421
+
422
+ // Create preview edges
423
+ const previews = ProximityConnectHelper.createPreviewEdges(candidates);
424
+
425
+ // Update state
426
+ currentProximityCandidates = candidates;
427
+ flowEdges = [...baseEdges, ...previews];
428
+ }
429
+
430
+ /**
431
+ * Handle node drag stop
432
+ *
433
+ * Still in 'dragging' state — sync effect suppressed.
434
+ * Syncs final positions to store, pushes history, then transitions to idle.
435
+ */
436
+ function handleNodeDragStop(): void {
437
+ portCoordNodeToUpdate = null;
438
+
439
+ // Finalize proximity connect if there are candidates
440
+ if (
441
+ getEditorSettings().proximityConnect &&
442
+ currentProximityCandidates.length > 0
443
+ ) {
444
+ const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
445
+ const permanentEdges = ProximityConnectHelper.createPermanentEdges(
446
+ currentProximityCandidates,
447
+ );
448
+
449
+ for (const edge of permanentEdges) {
450
+ const sourceNode = flowNodes.find((n) => n.id === edge.source);
451
+ const targetNode = flowNodes.find((n) => n.id === edge.target);
452
+ if (sourceNode && targetNode) {
453
+ EdgeStylingHelper.applyConnectionStyling(
454
+ edge,
455
+ sourceNode,
456
+ targetNode,
457
+ );
458
+ }
459
+ }
460
+
461
+ flowEdges = [...baseEdges, ...permanentEdges];
462
+ currentProximityCandidates = [];
463
+ }
464
+
465
+ // Sync flowNodes/flowEdges store
466
+ syncFlowToStore();
467
+
468
+ // Push history AFTER the drag completed
469
+ const storeValue = getWorkflowStore();
470
+ if (storeValue) {
471
+ workflowActions.pushHistory("Move node", storeValue);
472
+ }
473
+
474
+ // Transition to idle — sync effect is now unblocked
475
+ machine.send("STOP_DRAG");
476
+ }
477
+
478
+ /**
479
+ * Handle new connections between nodes
480
+ */
481
+ async function handleConnect(connection: {
482
+ source: string;
483
+ target: string;
484
+ sourceHandle?: string;
485
+ targetHandle?: string;
486
+ }): Promise<void> {
487
+ machine.send("START_CONNECT");
488
+
489
+ // SvelteFlow auto-creates the edge via bind:edges wait for DOM update
490
+ await tick();
491
+
492
+ // Apply styling to all edges (including the new one)
493
+ flowEdges = EdgeStylingHelper.updateEdgeStyles(flowEdges, flowNodes);
494
+
495
+ // Sync to store
496
+ syncFlowToStore();
497
+
498
+ const storeValue = getWorkflowStore();
499
+ if (storeValue) {
500
+ workflowActions.pushHistory("Add connection", storeValue);
501
+ }
502
+
503
+ machine.send("CONNECTION_MADE");
504
+ }
505
+
506
+ /**
507
+ * Handle before delete - show confirmation dialog if enabled in settings
508
+ *
509
+ * This callback is called before nodes/edges are deleted.
510
+ * Return true to proceed with deletion, false to cancel.
511
+ *
512
+ * @param params - Object containing nodes and edges to be deleted
513
+ * @returns Promise resolving to true if deletion should proceed, false to cancel
514
+ */
515
+ async function handleBeforeDelete(params: {
516
+ nodes: WorkflowNodeType[];
517
+ edges: WorkflowEdge[];
518
+ }): Promise<boolean> {
519
+ // If confirmDelete setting is enabled, show confirmation dialog
520
+ if (getBehaviorSettings().confirmDelete) {
521
+ const nodeCount = params.nodes.length;
522
+ const edgeCount = params.edges.length;
523
+
524
+ // Build a descriptive message
525
+ let message = "Are you sure you want to delete ";
526
+ const parts: string[] = [];
527
+
528
+ if (nodeCount > 0) {
529
+ parts.push(`${nodeCount} node${nodeCount > 1 ? "s" : ""}`);
530
+ }
531
+ if (edgeCount > 0) {
532
+ parts.push(`${edgeCount} connection${edgeCount > 1 ? "s" : ""}`);
533
+ }
534
+
535
+ message += parts.join(" and ") + "?";
536
+
537
+ // Show native confirmation dialog
538
+ const confirmed = window.confirm(message);
539
+ if (!confirmed) {
540
+ return false;
541
+ }
542
+ }
543
+
544
+ // Don't push to history here - we'll push AFTER deletion in handleNodesDelete
545
+ // This ensures undo will restore the state before deletion
546
+ return true;
547
+ }
548
+
549
+ /**
550
+ * Handle node deletion - automatically remove connected edges and push to history
551
+ */
552
+ function handleNodesDelete(params: {
553
+ nodes: WorkflowNodeType[];
554
+ edges: WorkflowEdge[];
555
+ }): void {
556
+ machine.send("START_DELETE");
557
+
558
+ const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
559
+
560
+ // Filter out edges connected to deleted nodes
561
+ flowEdges = flowEdges.filter(
562
+ (edge) =>
563
+ !deletedNodeIds.has(edge.source) && !deletedNodeIds.has(edge.target),
564
+ );
565
+
566
+ // Sync to store
567
+ syncFlowToStore();
568
+
569
+ // Push to history AFTER the deletion so undo restores the previous state
570
+ const nodeCount = params.nodes.length;
571
+ const edgeCount = params.edges.length;
572
+ let description = "Delete";
573
+ if (nodeCount > 0 && edgeCount > 0) {
574
+ description = `Delete ${nodeCount} node${nodeCount > 1 ? "s" : ""} and ${edgeCount} connection${edgeCount > 1 ? "s" : ""}`;
575
+ } else if (nodeCount > 0) {
576
+ description = `Delete ${nodeCount} node${nodeCount > 1 ? "s" : ""}`;
577
+ } else if (edgeCount > 0) {
578
+ description = `Delete ${edgeCount} connection${edgeCount > 1 ? "s" : ""}`;
579
+ }
580
+ const storeValue = getWorkflowStore();
581
+ if (storeValue) {
582
+ workflowActions.pushHistory(description, storeValue);
583
+ }
584
+
585
+ machine.send("DELETE_COMPLETE");
586
+ }
587
+
588
+ // Edge styling will be handled when edges are first created or manually updated
589
+
590
+ // Configure endpoints when props change
591
+ $effect(() => {
592
+ if (props.endpointConfig) {
593
+ ConfigurationHelper.configureEndpoints(props.endpointConfig);
594
+ }
595
+ });
596
+
597
+ /**
598
+ * Check if workflow has cycles
599
+ */
600
+ function checkWorkflowCycles(): boolean {
601
+ return WorkflowOperationsHelper.checkWorkflowCycles(flowNodes, flowEdges);
602
+ }
603
+
604
+ /**
605
+ * Handle drop event and add new node to canvas
606
+ */
607
+ async function handleNodeDrop(
608
+ nodeTypeData: string,
609
+ position: { x: number; y: number },
610
+ ): Promise<void> {
611
+ machine.send("START_DROP");
612
+
613
+ const newNode = NodeOperationsHelper.createNodeFromDrop(
614
+ nodeTypeData,
615
+ position,
616
+ flowNodes,
617
+ );
618
+
619
+ if (newNode) {
620
+ // Add onConfigOpen callback and append to flowNodes for immediate visual feedback
621
+ const nodeWithCallback = {
622
+ ...newNode,
623
+ data: { ...newNode.data, onConfigOpen: props.openConfigSidebar },
624
+ };
625
+ flowNodes = [...flowNodes, nodeWithCallback];
626
+
627
+ // Sync to store
628
+ syncFlowToStore();
629
+
630
+ await tick();
631
+
632
+ const storeValue = getWorkflowStore();
633
+ if (storeValue) {
634
+ workflowActions.pushHistory("Add node", storeValue);
635
+ }
636
+ } else {
637
+ logger.warn("Failed to create node from drop data");
638
+ }
639
+
640
+ machine.send("DROP_COMPLETE");
641
+ }
642
+
643
+ /**
644
+ * Handle a workflow JSON file dropped directly onto the canvas.
645
+ *
646
+ * Validates the JSON against the minimum required Workflow fields and, if valid,
647
+ * loads it into the workflow store. Shows a toast on validation failure or read error.
648
+ */
649
+ function handleWorkflowFileDrop(file: File): void {
650
+ const reader = new FileReader();
651
+ reader.onload = (event) => {
652
+ try {
653
+ const text = event.target?.result;
654
+ if (typeof text !== "string") {
655
+ throw new Error("Could not read file contents.");
656
+ }
657
+ const data = JSON.parse(text);
658
+ const validation = validateWorkflowData(data);
659
+ if (!validation.valid) {
660
+ apiToasts.error(
661
+ "Import workflow",
662
+ validation.error ?? "Invalid workflow JSON",
663
+ );
664
+ logger.warn(
665
+ "Workflow file drop validation failed:",
666
+ validation.error,
667
+ );
668
+ return;
669
+ }
670
+ workflowActions.initialize(data as Workflow);
671
+ } catch (error) {
672
+ const errorObj =
673
+ error instanceof Error ? error : new Error("Unknown error occurred");
674
+ logger.error("Workflow file drop import failed:", errorObj);
675
+ apiToasts.error("Import workflow", errorObj.message);
676
+ }
677
+ };
678
+ reader.onerror = () => {
679
+ const message = "Failed to read the dropped file.";
680
+ logger.error(message);
681
+ apiToasts.error("Import workflow", message);
682
+ };
683
+ reader.readAsText(file);
684
+ }
685
+
686
+ /**
687
+ * Node ID that needs edge refresh - used to trigger EdgeRefresher component
688
+ */
689
+ let nodeIdToRefresh = $state<string | null>(null);
690
+
691
+ /**
692
+ * Update a node's data in the local editor state.
693
+ * Called by App.svelte AFTER it has already updated the global store via
694
+ * workflowActions.updateNode(). We only need to update flowNodes for
695
+ * immediate visual feedback — no store sync needed.
696
+ *
697
+ * @param nodeId - The ID of the node to update
698
+ * @param dataUpdates - Partial data updates to merge into the node's data
699
+ */
700
+ export function updateNodeData(
701
+ nodeId: string,
702
+ dataUpdates: Partial<WorkflowNodeType["data"]>,
703
+ ): void {
704
+ machine.send("START_NODE_UPDATE");
705
+
706
+ flowNodes = flowNodes.map((node) => {
707
+ if (node.id === nodeId) {
708
+ return {
709
+ ...node,
710
+ data: {
711
+ ...node.data,
712
+ ...dataUpdates,
713
+ },
714
+ };
715
+ }
716
+ return node;
717
+ });
718
+
719
+ machine.send("UPDATE_COMPLETE");
720
+ }
721
+
722
+ /**
723
+ * Force edge position recalculation after node config changes
724
+ * This should be called after saving gateway/switch node configs where branches are reordered
725
+ * Svelte Flow doesn't automatically recalculate edge paths when handle positions change
726
+ * @param nodeId - The ID of the node whose handles have changed position
727
+ */
728
+ export async function refreshEdgePositions(nodeId: string): Promise<void> {
729
+ // Wait for DOM to update with new handle positions
730
+ await tick();
731
+
732
+ // Trigger the EdgeRefresher component to call updateNodeInternals
733
+ nodeIdToRefresh = nodeId;
734
+ }
735
+
736
+ /**
737
+ * Callback when edge refresh is complete
738
+ */
739
+ function handleEdgeRefreshComplete(): void {
740
+ nodeIdToRefresh = null;
741
+ }
742
+
743
+ /**
744
+ * Handle keyboard shortcuts for undo/redo
745
+ *
746
+ * - Ctrl+Z (or Cmd+Z on Mac): Undo
747
+ * - Ctrl+Shift+Z (or Cmd+Shift+Z): Redo
748
+ * - Ctrl+Y (or Cmd+Y): Redo (Windows convention)
749
+ */
750
+ function handleKeydown(event: KeyboardEvent): void {
751
+ // Check for Ctrl (Windows/Linux) or Cmd (Mac)
752
+ const isModifierPressed = event.ctrlKey || event.metaKey;
753
+
754
+ if (!isModifierPressed) {
755
+ return;
756
+ }
757
+
758
+ // Don't handle shortcuts if user is typing in an input, textarea, or contenteditable
759
+ const target = event.target as HTMLElement;
760
+ const isInputElement =
761
+ target.tagName === "INPUT" ||
762
+ target.tagName === "TEXTAREA" ||
763
+ target.isContentEditable;
764
+
765
+ if (isInputElement) {
766
+ return;
767
+ }
768
+
769
+ // Undo: Ctrl+Z (without Shift)
770
+ if (event.key === "z" && !event.shiftKey) {
771
+ event.preventDefault();
772
+ historyActions.undo();
773
+ return;
774
+ }
775
+
776
+ // Redo: Ctrl+Shift+Z or Ctrl+Y
777
+ if ((event.key === "z" && event.shiftKey) || event.key === "y") {
778
+ event.preventDefault();
779
+ historyActions.redo();
780
+ return;
781
+ }
782
+ }
742
783
  </script>
743
784
 
744
785
  <svelte:window onkeydown={handleKeydown} />
745
786
 
746
787
  <SvelteFlowProvider>
747
- <!-- EdgeRefresher component - handles updateNodeInternals calls -->
748
- <EdgeRefresher {nodeIdToRefresh} onRefreshComplete={handleEdgeRefreshComplete} />
749
-
750
- <!-- Port Coordinate Tracker - maintains port positions for proximity connect -->
751
- <PortCoordinateTracker
752
- nodeToUpdate={portCoordNodeToUpdate}
753
- rebuildTrigger={portCoordRebuildTrigger}
754
- nodes={flowNodes}
755
- />
756
-
757
- <div class="flowdrop-workflow-editor">
758
- <!-- Main Editor Area -->
759
- <div class="flowdrop-workflow-editor__main">
760
- <!-- Flow Canvas -->
761
- <div class="flowdrop-canvas">
762
- <FlowDropZone ondrop={handleNodeDrop} onfiledrop={handleWorkflowFileDrop}>
763
- {#key svelteFlowKey}
764
- <SvelteFlow
765
- bind:nodes={flowNodes}
766
- bind:edges={flowEdges}
767
- {nodeTypes}
768
- {edgeTypes}
769
- {defaultEdgeOptions}
770
- onconnect={(connection) =>
771
- void handleConnect({
772
- source: connection.source,
773
- target: connection.target,
774
- sourceHandle: connection.sourceHandle ?? undefined,
775
- targetHandle: connection.targetHandle ?? undefined
776
- })}
777
- onbeforedelete={handleBeforeDelete}
778
- ondelete={handleNodesDelete}
779
- onnodedragstart={handleNodeDragStart}
780
- onnodedrag={handleNodeDrag}
781
- onnodedragstop={handleNodeDragStop}
782
- minZoom={0.2}
783
- maxZoom={3}
784
- clickConnect={true}
785
- elevateEdgesOnSelect={true}
786
- connectionLineType={ConnectionLineType.Bezier}
787
- connectionLineComponent={ConnectionLine}
788
- {snapGrid}
789
- {initialViewport}
790
- colorMode={getResolvedTheme() as ColorMode}
791
- fitView={getEditorSettings().fitViewOnLoad}
792
- >
793
- <Controls />
794
- <!-- Always render Background for consistent bg color in dark/light mode -->
795
- <Background
796
- gap={getEditorSettings().gridSize}
797
- bgColor="var(--fd-background)"
798
- variant={BackgroundVariant.Dots}
799
- patternColor={getEditorSettings().showGrid ? undefined : 'transparent'}
800
- />
801
- {#if getEditorSettings().showMinimap}
802
- <MiniMap />
803
- {/if}
804
- </SvelteFlow>
805
- {/key}
806
- <!-- Drop Zone Indicator -->
807
- {#if flowNodes.length === 0}
808
- <CanvasBanner
809
- title="Drag components here to start building"
810
- description="Use the sidebar to add components to your workflow"
811
- iconName="mdi:graph"
812
- />
813
- {/if}
814
- </FlowDropZone>
815
- </div>
816
-
817
- <!-- Status Bar: aria-live announces dynamic changes (node/edge counts, cycle warnings) -->
818
- <div class="flowdrop-status-bar" aria-live="polite" aria-atomic="true">
819
- <div class="flowdrop-status-bar__content">
820
- <div class="flowdrop-flex flowdrop-gap--4">
821
- <span class="flowdrop-text--xs flowdrop-text--gray">{flowNodes.length} nodes</span>
822
- <span class="flowdrop-text--xs flowdrop-text--gray">•</span>
823
- <span class="flowdrop-text--xs flowdrop-text--gray">{flowEdges.length} connections</span
824
- >
825
-
826
- {#if checkWorkflowCycles()}
827
- <span class="flowdrop-text--xs flowdrop-text--gray">•</span>
828
- <span class="flowdrop-text--xs flowdrop-font--medium flowdrop-text--error"
829
- >⚠️ Cycles detected</span
830
- >
831
- {/if}
832
- </div>
833
- </div>
834
- </div>
835
- </div>
836
- </div>
788
+ <!-- EdgeRefresher component - handles updateNodeInternals calls -->
789
+ <EdgeRefresher
790
+ {nodeIdToRefresh}
791
+ onRefreshComplete={handleEdgeRefreshComplete}
792
+ />
793
+
794
+ <!-- Port Coordinate Tracker - maintains port positions for proximity connect -->
795
+ <PortCoordinateTracker
796
+ nodeToUpdate={portCoordNodeToUpdate}
797
+ rebuildTrigger={portCoordRebuildTrigger}
798
+ nodes={flowNodes}
799
+ />
800
+
801
+ <div class="flowdrop-workflow-editor">
802
+ <!-- Main Editor Area -->
803
+ <div class="flowdrop-workflow-editor__main">
804
+ <!-- Flow Canvas -->
805
+ <div class="flowdrop-canvas">
806
+ <FlowDropZone
807
+ ondrop={handleNodeDrop}
808
+ onfiledrop={handleWorkflowFileDrop}
809
+ >
810
+ {#key svelteFlowKey}
811
+ <SvelteFlow
812
+ bind:nodes={flowNodes}
813
+ bind:edges={flowEdges}
814
+ {nodeTypes}
815
+ {edgeTypes}
816
+ {defaultEdgeOptions}
817
+ onconnect={(connection) =>
818
+ void handleConnect({
819
+ source: connection.source,
820
+ target: connection.target,
821
+ sourceHandle: connection.sourceHandle ?? undefined,
822
+ targetHandle: connection.targetHandle ?? undefined,
823
+ })}
824
+ onbeforedelete={handleBeforeDelete}
825
+ ondelete={handleNodesDelete}
826
+ onnodedragstart={handleNodeDragStart}
827
+ onnodedrag={handleNodeDrag}
828
+ onnodedragstop={handleNodeDragStop}
829
+ minZoom={0.2}
830
+ maxZoom={3}
831
+ clickConnect={true}
832
+ elevateEdgesOnSelect={true}
833
+ connectionLineType={ConnectionLineType.Bezier}
834
+ connectionLineComponent={ConnectionLine}
835
+ {snapGrid}
836
+ {initialViewport}
837
+ colorMode={getResolvedTheme() as ColorMode}
838
+ fitView={getEditorSettings().fitViewOnLoad}
839
+ >
840
+ <Controls />
841
+ <!-- Always render Background for consistent bg color in dark/light mode -->
842
+ <Background
843
+ gap={getEditorSettings().gridSize}
844
+ bgColor="var(--fd-background)"
845
+ variant={BackgroundVariant.Dots}
846
+ patternColor={getEditorSettings().showGrid
847
+ ? undefined
848
+ : "transparent"}
849
+ />
850
+ {#if getEditorSettings().showMinimap}
851
+ <MiniMap />
852
+ {/if}
853
+ </SvelteFlow>
854
+ {/key}
855
+ <!-- Drop Zone Indicator -->
856
+ {#if flowNodes.length === 0}
857
+ <CanvasBanner
858
+ title="Drag components here to start building"
859
+ description="Use the sidebar to add components to your workflow"
860
+ iconName="mdi:graph"
861
+ />
862
+ {/if}
863
+ </FlowDropZone>
864
+ </div>
865
+
866
+ <!-- Status Bar: aria-live announces dynamic changes (node/edge counts, cycle warnings) -->
867
+ <div class="flowdrop-status-bar" aria-live="polite" aria-atomic="true">
868
+ <div class="flowdrop-status-bar__content">
869
+ <div class="flowdrop-flex flowdrop-gap--4">
870
+ <span class="flowdrop-text--xs flowdrop-text--gray"
871
+ >{flowNodes.length} nodes</span
872
+ >
873
+ <span class="flowdrop-text--xs flowdrop-text--gray">•</span>
874
+ <span class="flowdrop-text--xs flowdrop-text--gray"
875
+ >{flowEdges.length} connections</span
876
+ >
877
+
878
+ {#if checkWorkflowCycles()}
879
+ <span class="flowdrop-text--xs flowdrop-text--gray">•</span>
880
+ <span
881
+ class="flowdrop-text--xs flowdrop-font--medium flowdrop-text--error"
882
+ >⚠️ Cycles detected</span
883
+ >
884
+ {/if}
885
+ </div>
886
+ </div>
887
+ </div>
888
+ </div>
889
+ </div>
837
890
  </SvelteFlowProvider>
838
891
 
839
892
  <!-- Toast notifications container -->
840
893
  <!-- aria-live="polite" ensures screen readers announce toast messages without interrupting -->
841
894
  <div aria-live="polite" aria-atomic="true">
842
- <Toaster
843
- position="bottom-center"
844
- containerClassName={FLOWDROP_TOASTER_CLASS}
845
- toastOptions={flowdropToastOptions}
846
- />
895
+ <Toaster
896
+ position="bottom-center"
897
+ containerClassName={FLOWDROP_TOASTER_CLASS}
898
+ toastOptions={flowdropToastOptions}
899
+ />
847
900
  </div>
848
901
 
849
902
  <style>
850
- .flowdrop-workflow-editor {
851
- display: flex;
852
- flex-direction: row; /* Side by side layout */
853
- height: 100%;
854
- position: relative;
855
- }
856
-
857
- .flowdrop-workflow-editor__main {
858
- flex: 1;
859
- display: flex;
860
- flex-direction: column;
861
- min-height: 0;
862
- transition: margin-left 0.3s ease-in-out;
863
- }
864
-
865
- .flowdrop-text--error {
866
- color: var(--fd-error);
867
- }
868
-
869
- .flowdrop-canvas {
870
- flex: 1;
871
- min-height: 0;
872
- position: relative;
873
- background: transparent;
874
- }
875
-
876
- .flowdrop-status-bar {
877
- background-color: var(--fd-backdrop);
878
- backdrop-filter: var(--fd-backdrop-blur);
879
- border-top: 1px solid var(--fd-border);
880
- padding: 0.75rem;
881
- height: 40px;
882
- min-height: 40px;
883
- max-height: 40px;
884
- display: flex;
885
- align-items: center;
886
- flex-shrink: 0;
887
- }
888
-
889
- .flowdrop-status-bar__content {
890
- display: flex;
891
- align-items: center;
892
- justify-content: space-between;
893
- }
894
-
895
- :global(.flowdrop-workflow-editor .svelte-flow__node:hover) {
896
- transform: translateY(-2px);
897
- }
898
-
899
- :global(.flowdrop-workflow-editor .svelte-flow__edge) {
900
- stroke-width: 2 !important;
901
- cursor: pointer;
902
- pointer-events: all;
903
- }
904
-
905
- :global(.flowdrop-workflow-editor .svelte-flow__edge path) {
906
- stroke-width: 2 !important;
907
- }
908
-
909
- :global(.flowdrop-workflow-editor .svelte-flow__edge:hover) {
910
- stroke: var(--fd-primary) !important;
911
- stroke-width: 3 !important;
912
- }
913
-
914
- :global(.flowdrop-workflow-editor .svelte-flow__edge:hover path) {
915
- stroke-width: 3 !important;
916
- }
917
-
918
- :global(.flowdrop-workflow-editor .svelte-flow__edge.selected) {
919
- stroke: var(--fd-primary) !important;
920
- stroke-width: 3 !important;
921
- filter: drop-shadow(0 0 4px color-mix(in srgb, var(--fd-primary) 50%, transparent));
922
- }
923
-
924
- :global(.flowdrop-workflow-editor .svelte-flow__edge.selected path) {
925
- stroke-width: 3 !important;
926
- }
927
-
928
- /* Ensure edge paths are clickable */
929
- :global(.flowdrop-workflow-editor .svelte-flow__edge path) {
930
- pointer-events: all;
931
- cursor: pointer;
932
- }
933
-
934
- /* Handle size/position only; colors come from inline --fd-handle-fill and base.css ::before */
935
- :global(.flowdrop-workflow-editor .svelte-flow__handle) {
936
- width: var(--fd-handle-size);
937
- height: var(--fd-handle-size);
938
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
939
- z-index: 20;
940
- }
941
-
942
- /* Ensure our custom handles are clickable */
943
- :global(.flowdrop-workflow-editor .svelte-flow__handle) {
944
- pointer-events: all;
945
- cursor: crosshair;
946
- background-color: var(--fd-handle-fill);
947
- border-color: var(--fd-handle-border-color);
948
- }
949
-
950
- /**
903
+ .flowdrop-workflow-editor {
904
+ display: flex;
905
+ flex-direction: row; /* Side by side layout */
906
+ height: 100%;
907
+ position: relative;
908
+ }
909
+
910
+ .flowdrop-workflow-editor__main {
911
+ flex: 1;
912
+ display: flex;
913
+ flex-direction: column;
914
+ min-height: 0;
915
+ transition: margin-left 0.3s ease-in-out;
916
+ }
917
+
918
+ .flowdrop-text--error {
919
+ color: var(--fd-error);
920
+ }
921
+
922
+ .flowdrop-canvas {
923
+ flex: 1;
924
+ min-height: 0;
925
+ position: relative;
926
+ background: transparent;
927
+ }
928
+
929
+ .flowdrop-status-bar {
930
+ background-color: var(--fd-backdrop);
931
+ backdrop-filter: var(--fd-backdrop-blur);
932
+ border-top: 1px solid var(--fd-border);
933
+ padding: 0.75rem;
934
+ height: 40px;
935
+ min-height: 40px;
936
+ max-height: 40px;
937
+ display: flex;
938
+ align-items: center;
939
+ flex-shrink: 0;
940
+ }
941
+
942
+ .flowdrop-status-bar__content {
943
+ display: flex;
944
+ align-items: center;
945
+ justify-content: space-between;
946
+ }
947
+
948
+ :global(.flowdrop-workflow-editor .svelte-flow__node:hover) {
949
+ transform: translateY(-2px);
950
+ }
951
+
952
+ :global(.flowdrop-workflow-editor .svelte-flow__edge) {
953
+ stroke-width: 2 !important;
954
+ cursor: pointer;
955
+ pointer-events: all;
956
+ }
957
+
958
+ :global(.flowdrop-workflow-editor .svelte-flow__edge path) {
959
+ stroke-width: 2 !important;
960
+ }
961
+
962
+ :global(.flowdrop-workflow-editor .svelte-flow__edge:hover) {
963
+ stroke: var(--fd-primary) !important;
964
+ stroke-width: 3 !important;
965
+ }
966
+
967
+ :global(.flowdrop-workflow-editor .svelte-flow__edge:hover path) {
968
+ stroke-width: 3 !important;
969
+ }
970
+
971
+ :global(.flowdrop-workflow-editor .svelte-flow__edge.selected) {
972
+ stroke: var(--fd-primary) !important;
973
+ stroke-width: 3 !important;
974
+ filter: drop-shadow(
975
+ 0 0 4px color-mix(in srgb, var(--fd-primary) 50%, transparent)
976
+ );
977
+ }
978
+
979
+ :global(.flowdrop-workflow-editor .svelte-flow__edge.selected path) {
980
+ stroke-width: 3 !important;
981
+ }
982
+
983
+ /* Ensure edge paths are clickable */
984
+ :global(.flowdrop-workflow-editor .svelte-flow__edge path) {
985
+ pointer-events: all;
986
+ cursor: pointer;
987
+ }
988
+
989
+ /* Handle size/position only; colors come from inline --fd-handle-fill and base.css ::before */
990
+ :global(.flowdrop-workflow-editor .svelte-flow__handle) {
991
+ width: var(--fd-handle-size);
992
+ height: var(--fd-handle-size);
993
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
994
+ z-index: 20;
995
+ }
996
+
997
+ /* Ensure our custom handles are clickable */
998
+ :global(.flowdrop-workflow-editor .svelte-flow__handle) {
999
+ pointer-events: all;
1000
+ cursor: crosshair;
1001
+ background-color: var(--fd-handle-fill);
1002
+ border-color: var(--fd-handle-border-color);
1003
+ }
1004
+
1005
+ /**
951
1006
  * Edge Styling Based on Source Port Data Type
952
1007
  * Uses CSS tokens from base.css for consistent theming
953
1008
  * - Trigger edges: solid dark line (control flow)
@@ -955,88 +1010,88 @@
955
1010
  * - Data edges: normal gray line (data flow)
956
1011
  */
957
1012
 
958
- /* Trigger Edge: Solid dark line for control flow */
959
- :global(.flowdrop--edge--trigger path.svelte-flow__edge-path) {
960
- stroke: var(--fd-edge-trigger);
961
- stroke-width: var(--fd-edge-trigger-width);
962
- }
963
-
964
- :global(.flowdrop--edge--trigger:hover path.svelte-flow__edge-path) {
965
- stroke: var(--fd-edge-trigger-hover);
966
- stroke-width: var(--fd-edge-trigger-width-hover);
967
- }
968
-
969
- :global(.flowdrop--edge--trigger.selected path.svelte-flow__edge-path) {
970
- stroke: var(--fd-edge-trigger-selected);
971
- stroke-width: var(--fd-edge-trigger-width-hover);
972
- }
973
-
974
- /* Tool Edge: Dashed amber line for tool connections */
975
- :global(.flowdrop--edge--tool path.svelte-flow__edge-path) {
976
- stroke: var(--fd-edge-tool);
977
- stroke-dasharray: 5 3;
978
- }
979
-
980
- :global(.flowdrop--edge--tool:hover path.svelte-flow__edge-path) {
981
- stroke: var(--fd-edge-tool-hover);
982
- stroke-width: 2;
983
- }
984
-
985
- :global(.flowdrop--edge--tool.selected path.svelte-flow__edge-path) {
986
- stroke: var(--fd-edge-tool-selected);
987
- stroke-dasharray: 5 3;
988
- stroke-width: 2;
989
- }
990
-
991
- /* Data Edge: Normal gray line for data flow (default) */
992
- :global(.flowdrop--edge--data path.svelte-flow__edge-path) {
993
- stroke: var(--fd-edge-data);
994
- }
995
-
996
- :global(.flowdrop--edge--data:hover path.svelte-flow__edge-path) {
997
- stroke: var(--fd-edge-data-hover);
998
- stroke-width: 2;
999
- }
1000
-
1001
- :global(.flowdrop--edge--data.selected path.svelte-flow__edge-path) {
1002
- stroke: var(--fd-edge-data-selected);
1003
- stroke-width: 2;
1004
- }
1005
-
1006
- /* Loopback Edge: Dashed gray line for loop iteration connections */
1007
- :global(.flowdrop--edge--loopback path.svelte-flow__edge-path) {
1008
- stroke: var(--fd-edge-loopback);
1009
- stroke-width: var(--fd-edge-loopback-width);
1010
- stroke-dasharray: var(--fd-edge-loopback-dasharray);
1011
- opacity: var(--fd-edge-loopback-opacity);
1012
- }
1013
-
1014
- :global(.flowdrop--edge--loopback:hover path.svelte-flow__edge-path) {
1015
- stroke: var(--fd-edge-loopback-hover);
1016
- stroke-width: var(--fd-edge-loopback-width-hover);
1017
- opacity: 1;
1018
- }
1019
-
1020
- :global(.flowdrop--edge--loopback.selected path.svelte-flow__edge-path) {
1021
- stroke: var(--fd-edge-loopback-selected);
1022
- stroke-width: var(--fd-edge-loopback-width-hover);
1023
- stroke-dasharray: var(--fd-edge-loopback-dasharray);
1024
- filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
1025
- opacity: 1;
1026
- }
1027
-
1028
- /* Proximity Connect Preview Edge: animated dashed line */
1029
- :global(.flowdrop--edge--proximity-preview path.svelte-flow__edge-path) {
1030
- stroke: var(--fd-primary);
1031
- stroke-width: 2;
1032
- stroke-dasharray: 5 5;
1033
- opacity: 0.6;
1034
- animation: flowdrop-proximity-dash 0.5s linear infinite;
1035
- }
1036
-
1037
- @keyframes flowdrop-proximity-dash {
1038
- to {
1039
- stroke-dashoffset: -10;
1040
- }
1041
- }
1013
+ /* Trigger Edge: Solid dark line for control flow */
1014
+ :global(.flowdrop--edge--trigger path.svelte-flow__edge-path) {
1015
+ stroke: var(--fd-edge-trigger);
1016
+ stroke-width: var(--fd-edge-trigger-width);
1017
+ }
1018
+
1019
+ :global(.flowdrop--edge--trigger:hover path.svelte-flow__edge-path) {
1020
+ stroke: var(--fd-edge-trigger-hover);
1021
+ stroke-width: var(--fd-edge-trigger-width-hover);
1022
+ }
1023
+
1024
+ :global(.flowdrop--edge--trigger.selected path.svelte-flow__edge-path) {
1025
+ stroke: var(--fd-edge-trigger-selected);
1026
+ stroke-width: var(--fd-edge-trigger-width-hover);
1027
+ }
1028
+
1029
+ /* Tool Edge: Dashed amber line for tool connections */
1030
+ :global(.flowdrop--edge--tool path.svelte-flow__edge-path) {
1031
+ stroke: var(--fd-edge-tool);
1032
+ stroke-dasharray: 5 3;
1033
+ }
1034
+
1035
+ :global(.flowdrop--edge--tool:hover path.svelte-flow__edge-path) {
1036
+ stroke: var(--fd-edge-tool-hover);
1037
+ stroke-width: 2;
1038
+ }
1039
+
1040
+ :global(.flowdrop--edge--tool.selected path.svelte-flow__edge-path) {
1041
+ stroke: var(--fd-edge-tool-selected);
1042
+ stroke-dasharray: 5 3;
1043
+ stroke-width: 2;
1044
+ }
1045
+
1046
+ /* Data Edge: Normal gray line for data flow (default) */
1047
+ :global(.flowdrop--edge--data path.svelte-flow__edge-path) {
1048
+ stroke: var(--fd-edge-data);
1049
+ }
1050
+
1051
+ :global(.flowdrop--edge--data:hover path.svelte-flow__edge-path) {
1052
+ stroke: var(--fd-edge-data-hover);
1053
+ stroke-width: 2;
1054
+ }
1055
+
1056
+ :global(.flowdrop--edge--data.selected path.svelte-flow__edge-path) {
1057
+ stroke: var(--fd-edge-data-selected);
1058
+ stroke-width: 2;
1059
+ }
1060
+
1061
+ /* Loopback Edge: Dashed gray line for loop iteration connections */
1062
+ :global(.flowdrop--edge--loopback path.svelte-flow__edge-path) {
1063
+ stroke: var(--fd-edge-loopback);
1064
+ stroke-width: var(--fd-edge-loopback-width);
1065
+ stroke-dasharray: var(--fd-edge-loopback-dasharray);
1066
+ opacity: var(--fd-edge-loopback-opacity);
1067
+ }
1068
+
1069
+ :global(.flowdrop--edge--loopback:hover path.svelte-flow__edge-path) {
1070
+ stroke: var(--fd-edge-loopback-hover);
1071
+ stroke-width: var(--fd-edge-loopback-width-hover);
1072
+ opacity: 1;
1073
+ }
1074
+
1075
+ :global(.flowdrop--edge--loopback.selected path.svelte-flow__edge-path) {
1076
+ stroke: var(--fd-edge-loopback-selected);
1077
+ stroke-width: var(--fd-edge-loopback-width-hover);
1078
+ stroke-dasharray: var(--fd-edge-loopback-dasharray);
1079
+ filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
1080
+ opacity: 1;
1081
+ }
1082
+
1083
+ /* Proximity Connect Preview Edge: animated dashed line */
1084
+ :global(.flowdrop--edge--proximity-preview path.svelte-flow__edge-path) {
1085
+ stroke: var(--fd-primary);
1086
+ stroke-width: 2;
1087
+ stroke-dasharray: 5 5;
1088
+ opacity: 0.6;
1089
+ animation: flowdrop-proximity-dash 0.5s linear infinite;
1090
+ }
1091
+
1092
+ @keyframes flowdrop-proximity-dash {
1093
+ to {
1094
+ stroke-dashoffset: -10;
1095
+ }
1096
+ }
1042
1097
  </style>