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