@flowdrop/flowdrop 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/README.md +50 -50
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/WorkflowAdapter.js +25 -25
  4. package/dist/adapters/agentspec/AgentSpecAdapter.d.ts +2 -2
  5. package/dist/adapters/agentspec/AgentSpecAdapter.js +133 -122
  6. package/dist/adapters/agentspec/agentAdapter.d.ts +2 -2
  7. package/dist/adapters/agentspec/agentAdapter.js +10 -10
  8. package/dist/adapters/agentspec/autoLayout.d.ts +1 -1
  9. package/dist/adapters/agentspec/autoLayout.js +2 -2
  10. package/dist/adapters/agentspec/componentTypeDefaults.d.ts +1 -1
  11. package/dist/adapters/agentspec/componentTypeDefaults.js +120 -120
  12. package/dist/adapters/agentspec/defaultNodeTypes.d.ts +2 -2
  13. package/dist/adapters/agentspec/defaultNodeTypes.js +307 -307
  14. package/dist/adapters/agentspec/index.d.ts +10 -10
  15. package/dist/adapters/agentspec/index.js +6 -6
  16. package/dist/adapters/agentspec/validator.d.ts +2 -2
  17. package/dist/adapters/agentspec/validator.js +22 -20
  18. package/dist/api/enhanced-client.d.ts +3 -3
  19. package/dist/api/enhanced-client.js +73 -72
  20. package/dist/components/App.svelte +1090 -961
  21. package/dist/components/App.svelte.d.ts +9 -6
  22. package/dist/components/CanvasBanner.stories.svelte +23 -20
  23. package/dist/components/CanvasBanner.stories.svelte.d.ts +1 -1
  24. package/dist/components/CanvasBanner.svelte +52 -46
  25. package/dist/components/ConfigForm.svelte +1164 -1065
  26. package/dist/components/ConfigForm.svelte.d.ts +2 -2
  27. package/dist/components/ConfigModal.svelte +180 -180
  28. package/dist/components/ConfigModal.svelte.d.ts +1 -1
  29. package/dist/components/ConfigPanel.stories.svelte +35 -35
  30. package/dist/components/ConfigPanel.stories.svelte.d.ts +1 -1
  31. package/dist/components/ConfigPanel.svelte +178 -167
  32. package/dist/components/ConfigPanel.svelte.d.ts +1 -1
  33. package/dist/components/ConnectionLine.svelte +25 -25
  34. package/dist/components/EdgeRefresher.svelte +26 -26
  35. package/dist/components/FlowDropEdge.stories.svelte +179 -143
  36. package/dist/components/FlowDropEdge.svelte +147 -147
  37. package/dist/components/FlowDropEdge.svelte.d.ts +1 -1
  38. package/dist/components/FlowDropZone.svelte +63 -60
  39. package/dist/components/FlowDropZone.svelte.d.ts +1 -1
  40. package/dist/components/LoadingSpinner.stories.svelte +19 -19
  41. package/dist/components/LoadingSpinner.stories.svelte.d.ts +1 -1
  42. package/dist/components/LoadingSpinner.svelte +21 -21
  43. package/dist/components/LoadingSpinner.svelte.d.ts +1 -1
  44. package/dist/components/Logo.stories.svelte +13 -13
  45. package/dist/components/Logo.stories.svelte.d.ts +1 -1
  46. package/dist/components/Logo.svelte +101 -95
  47. package/dist/components/LogsSidebar.svelte +553 -546
  48. package/dist/components/LogsSidebar.svelte.d.ts +1 -1
  49. package/dist/components/MarkdownDisplay.stories.svelte +29 -23
  50. package/dist/components/MarkdownDisplay.stories.svelte.d.ts +1 -1
  51. package/dist/components/MarkdownDisplay.svelte +16 -14
  52. package/dist/components/Navbar.stories.svelte +43 -38
  53. package/dist/components/Navbar.stories.svelte.d.ts +1 -1
  54. package/dist/components/Navbar.svelte +760 -706
  55. package/dist/components/Navbar.svelte.d.ts +1 -1
  56. package/dist/components/NodeSidebar.svelte +905 -746
  57. package/dist/components/NodeSidebar.svelte.d.ts +5 -1
  58. package/dist/components/NodeStatusOverlay.stories.svelte +82 -70
  59. package/dist/components/NodeStatusOverlay.stories.svelte.d.ts +1 -1
  60. package/dist/components/NodeStatusOverlay.svelte +295 -280
  61. package/dist/components/NodeStatusOverlay.svelte.d.ts +3 -3
  62. package/dist/components/PipelineStatus.svelte +326 -300
  63. package/dist/components/PipelineStatus.svelte.d.ts +4 -4
  64. package/dist/components/PortCoordinateTracker.svelte +49 -47
  65. package/dist/components/PortCoordinateTracker.svelte.d.ts +1 -1
  66. package/dist/components/ReadOnlyDetails.svelte +156 -156
  67. package/dist/components/SchemaForm.stories.svelte +106 -98
  68. package/dist/components/SchemaForm.stories.svelte.d.ts +1 -1
  69. package/dist/components/SchemaForm.svelte +490 -463
  70. package/dist/components/SchemaForm.svelte.d.ts +2 -2
  71. package/dist/components/SettingsModal.svelte +226 -223
  72. package/dist/components/SettingsModal.svelte.d.ts +1 -1
  73. package/dist/components/SettingsPanel.svelte +637 -601
  74. package/dist/components/SettingsPanel.svelte.d.ts +1 -1
  75. package/dist/components/StatusIcon.stories.svelte +62 -49
  76. package/dist/components/StatusIcon.stories.svelte.d.ts +1 -1
  77. package/dist/components/StatusIcon.svelte +87 -87
  78. package/dist/components/StatusIcon.svelte.d.ts +2 -2
  79. package/dist/components/StatusLabel.stories.svelte +12 -12
  80. package/dist/components/StatusLabel.stories.svelte.d.ts +1 -1
  81. package/dist/components/StatusLabel.svelte +19 -19
  82. package/dist/components/ThemeToggle.stories.svelte +16 -16
  83. package/dist/components/ThemeToggle.stories.svelte.d.ts +1 -1
  84. package/dist/components/ThemeToggle.svelte +180 -169
  85. package/dist/components/ThemeToggle.svelte.d.ts +1 -1
  86. package/dist/components/UniversalNode.svelte +150 -138
  87. package/dist/components/UniversalNode.svelte.d.ts +3 -3
  88. package/dist/components/WorkflowEditor.svelte +1069 -1014
  89. package/dist/components/WorkflowEditor.svelte.d.ts +4 -4
  90. package/dist/components/form/FormArray.svelte +1034 -973
  91. package/dist/components/form/FormArray.svelte.d.ts +1 -1
  92. package/dist/components/form/FormAutocomplete.svelte +1021 -978
  93. package/dist/components/form/FormAutocomplete.svelte.d.ts +1 -1
  94. package/dist/components/form/FormCheckboxGroup.stories.svelte +23 -20
  95. package/dist/components/form/FormCheckboxGroup.stories.svelte.d.ts +1 -1
  96. package/dist/components/form/FormCheckboxGroup.svelte +136 -136
  97. package/dist/components/form/FormCodeEditor.svelte +452 -434
  98. package/dist/components/form/FormField.svelte +366 -355
  99. package/dist/components/form/FormField.svelte.d.ts +2 -2
  100. package/dist/components/form/FormFieldLight.svelte +400 -384
  101. package/dist/components/form/FormFieldLight.svelte.d.ts +1 -1
  102. package/dist/components/form/FormFieldWrapper.stories.svelte +42 -42
  103. package/dist/components/form/FormFieldWrapper.stories.svelte.d.ts +1 -1
  104. package/dist/components/form/FormFieldWrapper.svelte +100 -93
  105. package/dist/components/form/FormFieldWrapper.svelte.d.ts +1 -1
  106. package/dist/components/form/FormFieldset.svelte +108 -108
  107. package/dist/components/form/FormFieldset.svelte.d.ts +2 -2
  108. package/dist/components/form/FormMarkdownEditor.svelte +758 -725
  109. package/dist/components/form/FormNumberField.stories.svelte +25 -25
  110. package/dist/components/form/FormNumberField.stories.svelte.d.ts +1 -1
  111. package/dist/components/form/FormNumberField.svelte +88 -88
  112. package/dist/components/form/FormRangeField.stories.svelte +20 -20
  113. package/dist/components/form/FormRangeField.stories.svelte.d.ts +1 -1
  114. package/dist/components/form/FormRangeField.svelte +234 -226
  115. package/dist/components/form/FormSelect.stories.svelte +38 -38
  116. package/dist/components/form/FormSelect.stories.svelte.d.ts +1 -1
  117. package/dist/components/form/FormSelect.svelte +101 -101
  118. package/dist/components/form/FormSelect.svelte.d.ts +1 -1
  119. package/dist/components/form/FormTemplateEditor.svelte +847 -798
  120. package/dist/components/form/FormTemplateEditor.svelte.d.ts +1 -1
  121. package/dist/components/form/FormTextField.stories.svelte +29 -23
  122. package/dist/components/form/FormTextField.stories.svelte.d.ts +1 -1
  123. package/dist/components/form/FormTextField.svelte +68 -68
  124. package/dist/components/form/FormTextarea.stories.svelte +28 -25
  125. package/dist/components/form/FormTextarea.stories.svelte.d.ts +1 -1
  126. package/dist/components/form/FormTextarea.svelte +74 -74
  127. package/dist/components/form/FormToggle.stories.svelte +23 -20
  128. package/dist/components/form/FormToggle.stories.svelte.d.ts +1 -1
  129. package/dist/components/form/FormToggle.svelte +98 -98
  130. package/dist/components/form/FormUISchemaRenderer.svelte +120 -113
  131. package/dist/components/form/FormUISchemaRenderer.svelte.d.ts +3 -3
  132. package/dist/components/form/index.d.ts +19 -19
  133. package/dist/components/form/index.js +18 -18
  134. package/dist/components/form/templateAutocomplete.d.ts +2 -2
  135. package/dist/components/form/templateAutocomplete.js +64 -55
  136. package/dist/components/form/types.d.ts +6 -6
  137. package/dist/components/form/types.js +9 -4
  138. package/dist/components/icons/AlertCircleIcon.svelte +11 -0
  139. package/dist/components/icons/AlertCircleIcon.svelte.d.ts +26 -0
  140. package/dist/components/icons/CogIcon.svelte +11 -0
  141. package/dist/components/icons/CogIcon.svelte.d.ts +26 -0
  142. package/dist/components/interrupt/ChoicePrompt.stories.svelte +54 -38
  143. package/dist/components/interrupt/ChoicePrompt.stories.svelte.d.ts +1 -1
  144. package/dist/components/interrupt/ChoicePrompt.svelte +407 -383
  145. package/dist/components/interrupt/ChoicePrompt.svelte.d.ts +1 -1
  146. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte +48 -48
  147. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte.d.ts +1 -1
  148. package/dist/components/interrupt/ConfirmationPrompt.svelte +280 -274
  149. package/dist/components/interrupt/ConfirmationPrompt.svelte.d.ts +1 -1
  150. package/dist/components/interrupt/FormPrompt.svelte +223 -218
  151. package/dist/components/interrupt/FormPrompt.svelte.d.ts +1 -1
  152. package/dist/components/interrupt/InterruptBubble.svelte +617 -583
  153. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +2 -2
  154. package/dist/components/interrupt/ReviewPrompt.stories.svelte +66 -56
  155. package/dist/components/interrupt/ReviewPrompt.stories.svelte.d.ts +1 -1
  156. package/dist/components/interrupt/ReviewPrompt.svelte +861 -841
  157. package/dist/components/interrupt/ReviewPrompt.svelte.d.ts +1 -1
  158. package/dist/components/interrupt/TextInputPrompt.stories.svelte +38 -33
  159. package/dist/components/interrupt/TextInputPrompt.stories.svelte.d.ts +1 -1
  160. package/dist/components/interrupt/TextInputPrompt.svelte +333 -328
  161. package/dist/components/interrupt/TextInputPrompt.svelte.d.ts +1 -1
  162. package/dist/components/interrupt/index.d.ts +5 -5
  163. package/dist/components/interrupt/index.js +5 -5
  164. package/dist/components/layouts/MainLayout.svelte +724 -691
  165. package/dist/components/layouts/MainLayout.svelte.d.ts +6 -6
  166. package/dist/components/nodes/GatewayNode.stories.svelte +100 -99
  167. package/dist/components/nodes/GatewayNode.svelte +605 -571
  168. package/dist/components/nodes/GatewayNode.svelte.d.ts +3 -3
  169. package/dist/components/nodes/IdeaNode.stories.svelte +44 -43
  170. package/dist/components/nodes/IdeaNode.svelte +451 -437
  171. package/dist/components/nodes/IdeaNode.svelte.d.ts +1 -1
  172. package/dist/components/nodes/NotesNode.stories.svelte +65 -64
  173. package/dist/components/nodes/NotesNode.svelte +380 -369
  174. package/dist/components/nodes/NotesNode.svelte.d.ts +1 -1
  175. package/dist/components/nodes/SimpleNode.stories.svelte +145 -144
  176. package/dist/components/nodes/SimpleNode.svelte +486 -424
  177. package/dist/components/nodes/SimpleNode.svelte.d.ts +1 -1
  178. package/dist/components/nodes/SquareNode.stories.svelte +73 -73
  179. package/dist/components/nodes/SquareNode.svelte +439 -380
  180. package/dist/components/nodes/SquareNode.svelte.d.ts +1 -1
  181. package/dist/components/nodes/TerminalNode.stories.svelte +13 -13
  182. package/dist/components/nodes/TerminalNode.svelte +709 -670
  183. package/dist/components/nodes/TerminalNode.svelte.d.ts +1 -1
  184. package/dist/components/nodes/ToolNode.stories.svelte +181 -180
  185. package/dist/components/nodes/ToolNode.svelte +505 -447
  186. package/dist/components/nodes/ToolNode.svelte.d.ts +1 -1
  187. package/dist/components/nodes/WorkflowNode.stories.svelte +70 -46
  188. package/dist/components/nodes/WorkflowNode.svelte +621 -551
  189. package/dist/components/nodes/WorkflowNode.svelte.d.ts +3 -3
  190. package/dist/components/playground/ChatPanel.svelte +945 -889
  191. package/dist/components/playground/ExecutionLogs.svelte +495 -472
  192. package/dist/components/playground/InputCollector.svelte +449 -428
  193. package/dist/components/playground/MessageBubble.stories.svelte +47 -47
  194. package/dist/components/playground/MessageBubble.stories.svelte.d.ts +1 -1
  195. package/dist/components/playground/MessageBubble.svelte +626 -610
  196. package/dist/components/playground/MessageBubble.svelte.d.ts +1 -1
  197. package/dist/components/playground/Playground.svelte +1088 -1057
  198. package/dist/components/playground/Playground.svelte.d.ts +3 -3
  199. package/dist/components/playground/PlaygroundModal.svelte +208 -204
  200. package/dist/components/playground/PlaygroundModal.svelte.d.ts +3 -3
  201. package/dist/components/playground/SessionManager.svelte +527 -521
  202. package/dist/components/playground/SessionManager.svelte.d.ts +1 -1
  203. package/dist/config/agentSpecEndpoints.d.ts +1 -1
  204. package/dist/config/agentSpecEndpoints.js +20 -20
  205. package/dist/config/constants.js +2 -2
  206. package/dist/config/defaultCategories.d.ts +1 -1
  207. package/dist/config/defaultCategories.js +86 -86
  208. package/dist/config/defaultPortConfig.d.ts +1 -1
  209. package/dist/config/defaultPortConfig.js +144 -144
  210. package/dist/config/endpoints.d.ts +4 -4
  211. package/dist/config/endpoints.js +65 -65
  212. package/dist/config/runtimeConfig.d.ts +2 -2
  213. package/dist/config/runtimeConfig.js +8 -8
  214. package/dist/core/index.d.ts +63 -59
  215. package/dist/core/index.js +35 -33
  216. package/dist/display/index.d.ts +2 -2
  217. package/dist/display/index.js +2 -2
  218. package/dist/editor/index.d.ts +62 -62
  219. package/dist/editor/index.js +53 -53
  220. package/dist/form/code.d.ts +5 -5
  221. package/dist/form/code.js +14 -14
  222. package/dist/form/fieldRegistry.d.ts +3 -3
  223. package/dist/form/fieldRegistry.js +11 -9
  224. package/dist/form/full.d.ts +8 -8
  225. package/dist/form/full.js +9 -9
  226. package/dist/form/index.d.ts +18 -18
  227. package/dist/form/index.js +16 -16
  228. package/dist/form/markdown.d.ts +4 -4
  229. package/dist/form/markdown.js +8 -8
  230. package/dist/helpers/proximityConnect.d.ts +3 -3
  231. package/dist/helpers/proximityConnect.js +34 -32
  232. package/dist/helpers/workflowEditorHelper.d.ts +5 -5
  233. package/dist/helpers/workflowEditorHelper.js +108 -96
  234. package/dist/index.d.ts +6 -6
  235. package/dist/index.js +6 -6
  236. package/dist/mocks/app-environment.js +2 -2
  237. package/dist/mocks/app-forms.js +9 -9
  238. package/dist/mocks/app-navigation.js +11 -11
  239. package/dist/mocks/app-stores.js +8 -8
  240. package/dist/playground/index.d.ts +19 -19
  241. package/dist/playground/index.js +16 -16
  242. package/dist/playground/mount.d.ts +3 -3
  243. package/dist/playground/mount.js +24 -24
  244. package/dist/registry/builtinFormats.js +13 -13
  245. package/dist/registry/builtinNodes.d.ts +2 -2
  246. package/dist/registry/builtinNodes.js +77 -77
  247. package/dist/registry/index.d.ts +4 -4
  248. package/dist/registry/index.js +4 -4
  249. package/dist/registry/nodeComponentRegistry.d.ts +8 -8
  250. package/dist/registry/nodeComponentRegistry.js +11 -9
  251. package/dist/registry/plugin.d.ts +2 -2
  252. package/dist/registry/plugin.js +11 -11
  253. package/dist/registry/workflowFormatRegistry.d.ts +3 -3
  254. package/dist/registry/workflowFormatRegistry.js +2 -2
  255. package/dist/schema/index.d.ts +1 -1
  256. package/dist/schema/index.js +2 -2
  257. package/dist/services/agentSpecExecutionService.d.ts +3 -3
  258. package/dist/services/agentSpecExecutionService.js +59 -55
  259. package/dist/services/api.d.ts +2 -2
  260. package/dist/services/api.js +37 -37
  261. package/dist/services/apiVariableService.d.ts +1 -1
  262. package/dist/services/apiVariableService.js +41 -34
  263. package/dist/services/autoSaveService.js +8 -8
  264. package/dist/services/categoriesApi.d.ts +2 -2
  265. package/dist/services/categoriesApi.js +8 -8
  266. package/dist/services/draftStorage.d.ts +1 -1
  267. package/dist/services/draftStorage.js +11 -11
  268. package/dist/services/dynamicSchemaService.d.ts +1 -1
  269. package/dist/services/dynamicSchemaService.js +41 -39
  270. package/dist/services/globalSave.d.ts +2 -2
  271. package/dist/services/globalSave.js +41 -38
  272. package/dist/services/historyService.d.ts +1 -1
  273. package/dist/services/historyService.js +8 -8
  274. package/dist/services/interruptService.d.ts +1 -1
  275. package/dist/services/interruptService.js +35 -29
  276. package/dist/services/nodeExecutionService.d.ts +1 -1
  277. package/dist/services/nodeExecutionService.js +45 -44
  278. package/dist/services/playgroundService.d.ts +1 -1
  279. package/dist/services/playgroundService.js +29 -29
  280. package/dist/services/portConfigApi.d.ts +2 -2
  281. package/dist/services/portConfigApi.js +8 -8
  282. package/dist/services/settingsService.d.ts +2 -2
  283. package/dist/services/settingsService.js +25 -19
  284. package/dist/services/toastService.d.ts +4 -4
  285. package/dist/services/toastService.js +33 -33
  286. package/dist/services/variableService.d.ts +1 -1
  287. package/dist/services/variableService.js +36 -36
  288. package/dist/services/workflowStorage.d.ts +2 -2
  289. package/dist/services/workflowStorage.js +13 -13
  290. package/dist/settings/index.d.ts +7 -7
  291. package/dist/settings/index.js +6 -6
  292. package/dist/skins/default.d.ts +2 -0
  293. package/dist/skins/default.js +1 -0
  294. package/dist/skins/index.d.ts +13 -0
  295. package/dist/skins/index.js +30 -0
  296. package/dist/skins/slate.d.ts +2 -0
  297. package/dist/skins/slate.js +78 -0
  298. package/dist/stores/categoriesStore.svelte.d.ts +1 -1
  299. package/dist/stores/categoriesStore.svelte.js +5 -5
  300. package/dist/stores/editorStateMachine.svelte.d.ts +2 -2
  301. package/dist/stores/editorStateMachine.svelte.js +65 -33
  302. package/dist/stores/historyStore.svelte.d.ts +4 -4
  303. package/dist/stores/historyStore.svelte.js +4 -4
  304. package/dist/stores/interruptStore.svelte.d.ts +3 -3
  305. package/dist/stores/interruptStore.svelte.js +21 -21
  306. package/dist/stores/playgroundStore.svelte.d.ts +2 -2
  307. package/dist/stores/playgroundStore.svelte.js +25 -18
  308. package/dist/stores/portCoordinateStore.svelte.d.ts +2 -2
  309. package/dist/stores/portCoordinateStore.svelte.js +15 -8
  310. package/dist/stores/settingsStore.svelte.d.ts +2 -2
  311. package/dist/stores/settingsStore.svelte.js +62 -57
  312. package/dist/stores/workflowStore.svelte.d.ts +3 -3
  313. package/dist/stores/workflowStore.svelte.js +50 -47
  314. package/dist/stories/CanvasDecorator.svelte +35 -32
  315. package/dist/stories/CanvasDecorator.svelte.d.ts +2 -2
  316. package/dist/stories/EdgeDecorator.svelte +102 -99
  317. package/dist/stories/EdgeDecorator.svelte.d.ts +1 -1
  318. package/dist/stories/NodeDecorator.svelte +59 -53
  319. package/dist/stories/NodeDecorator.svelte.d.ts +1 -1
  320. package/dist/stories/utils.d.ts +2 -2
  321. package/dist/stories/utils.js +105 -67
  322. package/dist/styles/base.css +599 -595
  323. package/dist/styles/toast.css +14 -14
  324. package/dist/styles/tokens.css +409 -378
  325. package/dist/svelte-app.d.ts +12 -9
  326. package/dist/svelte-app.js +40 -39
  327. package/dist/themes/default.d.ts +2 -0
  328. package/dist/themes/default.js +9 -0
  329. package/dist/themes/index.d.ts +13 -0
  330. package/dist/themes/index.js +44 -0
  331. package/dist/themes/minimal.d.ts +2 -0
  332. package/dist/themes/minimal.js +11 -0
  333. package/dist/types/agentspec.d.ts +18 -18
  334. package/dist/types/agentspec.js +2 -2
  335. package/dist/types/auth.d.ts +1 -1
  336. package/dist/types/auth.js +6 -6
  337. package/dist/types/config.d.ts +6 -6
  338. package/dist/types/events.d.ts +2 -2
  339. package/dist/types/events.js +2 -2
  340. package/dist/types/index.d.ts +32 -32
  341. package/dist/types/index.js +6 -6
  342. package/dist/types/interrupt.d.ts +6 -6
  343. package/dist/types/interrupt.js +21 -21
  344. package/dist/types/interruptState.d.ts +12 -12
  345. package/dist/types/interruptState.js +66 -66
  346. package/dist/types/playground.d.ts +7 -7
  347. package/dist/types/playground.js +14 -14
  348. package/dist/types/settings.d.ts +5 -3
  349. package/dist/types/settings.js +25 -18
  350. package/dist/types/skin.d.ts +31 -0
  351. package/dist/types/skin.js +1 -0
  352. package/dist/types/theme.d.ts +35 -0
  353. package/dist/types/theme.js +1 -0
  354. package/dist/types/uischema.d.ts +4 -4
  355. package/dist/types/uischema.js +3 -3
  356. package/dist/utils/colors.d.ts +1 -1
  357. package/dist/utils/colors.js +97 -95
  358. package/dist/utils/config.d.ts +2 -2
  359. package/dist/utils/config.js +48 -48
  360. package/dist/utils/connections.d.ts +2 -2
  361. package/dist/utils/connections.js +15 -15
  362. package/dist/utils/errors.js +3 -3
  363. package/dist/utils/fetchWithAuth.d.ts +1 -1
  364. package/dist/utils/fetchWithAuth.js +2 -2
  365. package/dist/utils/handleIds.d.ts +2 -2
  366. package/dist/utils/handleIds.js +8 -8
  367. package/dist/utils/handlePositioning.d.ts +1 -1
  368. package/dist/utils/handlePositioning.js +2 -2
  369. package/dist/utils/icons.d.ts +1 -1
  370. package/dist/utils/icons.js +74 -74
  371. package/dist/utils/logger.d.ts +1 -1
  372. package/dist/utils/logger.js +7 -7
  373. package/dist/utils/nodeStatus.d.ts +1 -1
  374. package/dist/utils/nodeStatus.js +48 -48
  375. package/dist/utils/nodeTypes.d.ts +1 -1
  376. package/dist/utils/nodeTypes.js +21 -20
  377. package/dist/utils/nodeWrapper.d.ts +7 -7
  378. package/dist/utils/nodeWrapper.js +21 -19
  379. package/dist/utils/performanceUtils.d.ts +1 -1
  380. package/dist/utils/performanceUtils.js +2 -1
  381. package/dist/utils/sanitize.js +1 -1
  382. package/dist/utils/uischema.d.ts +2 -2
  383. package/dist/utils/uischema.js +8 -8
  384. package/dist/utils/validation.js +20 -8
  385. package/package.json +1 -1
@@ -21,989 +21,1032 @@
21
21
  -->
22
22
 
23
23
  <script lang="ts">
24
- import { getContext, onMount } from 'svelte';
25
- import Icon from '@iconify/svelte';
26
- import type { AutocompleteConfig, AuthProvider } from '../../types/index.js';
27
- import type { FieldOption } from './types.js';
28
- import { buildFetchHeaders } from '../../utils/fetchWithAuth.js';
29
- import { logger } from '../../utils/logger.js';
30
-
31
- /**
32
- * Props interface for FormAutocomplete component
33
- */
34
- interface Props {
35
- /** Field identifier */
36
- id: string;
37
- /** Current selected value (string for single, string[] for multiple) */
38
- value: string | string[];
39
- /** Autocomplete configuration */
40
- autocomplete: AutocompleteConfig;
41
- /** Whether the field is required */
42
- required?: boolean;
43
- /** Placeholder text */
44
- placeholder?: string;
45
- /** Whether the field is disabled */
46
- disabled?: boolean;
47
- /** ARIA description ID */
48
- ariaDescribedBy?: string;
49
- /** Callback when value changes */
50
- onChange: (value: string | string[]) => void;
51
- }
52
-
53
- let {
54
- id,
55
- value = '',
56
- autocomplete,
57
- required = false,
58
- placeholder = '',
59
- disabled = false,
60
- ariaDescribedBy,
61
- onChange
62
- }: Props = $props();
63
-
64
- // Get AuthProvider and baseUrl from context via getter functions
65
- // This pattern ensures we always get the current value, even if props change after mount
66
- const getAuthProvider = getContext<(() => AuthProvider | undefined) | undefined>(
67
- 'flowdrop:getAuthProvider'
68
- );
69
- const getBaseUrl = getContext<(() => string) | undefined>('flowdrop:getBaseUrl');
70
-
71
- // Configuration with defaults
72
- const queryParam = $derived(autocomplete.queryParam ?? 'q');
73
- const minChars = $derived(autocomplete.minChars ?? 0);
74
- const debounceMs = $derived(autocomplete.debounceMs ?? 300);
75
- const fetchOnFocus = $derived(autocomplete.fetchOnFocus ?? false);
76
- const labelField = $derived(autocomplete.labelField ?? 'label');
77
- const valueField = $derived(autocomplete.valueField ?? 'value');
78
- const allowFreeText = $derived(autocomplete.allowFreeText ?? false);
79
- const multiple = $derived(autocomplete.multiple ?? false);
80
-
81
- // Component state
82
- let inputElement: HTMLInputElement | undefined = $state(undefined);
83
- let containerElement: HTMLDivElement | undefined = $state(undefined);
84
- let popoverElement: HTMLDivElement | undefined = $state(undefined);
85
- let inputValue = $state('');
86
- let suggestions = $state<FieldOption[]>([]);
87
- let isOpen = $state(false);
88
- let isLoading = $state(false);
89
- let error = $state<string | null>(null);
90
- let highlightedIndex = $state(-1);
91
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
92
- let abortController: AbortController | null = null;
93
-
94
- // Popover positioning style
95
- let popoverStyle = $state('');
96
-
97
- // Cache of value-to-label mappings for selected items
98
- let labelCache = $state<Map<string, string>>(new Map());
99
-
100
- // Generate unique IDs for accessibility
101
- // svelte-ignore state_referenced_locally — id prop never changes
102
- const listboxId = `${id}-listbox`;
103
- // svelte-ignore state_referenced_locally
104
- const getOptionId = (index: number): string => `${id}-option-${index}`;
105
-
106
- /**
107
- * Get the selected values as an array (normalizes single/multiple)
108
- */
109
- const selectedValues = $derived<string[]>(
110
- multiple
111
- ? Array.isArray(value)
112
- ? value
113
- : value
114
- ? [String(value)]
115
- : []
116
- : value
117
- ? [String(value)]
118
- : []
119
- );
120
-
121
- /**
122
- * Check if a value is selected
123
- */
124
- function isSelected(optionValue: string): boolean {
125
- return selectedValues.includes(optionValue);
126
- }
127
-
128
- /**
129
- * Get display label for a selected value
130
- */
131
- function getDisplayLabel(val: string): string {
132
- // Check cache first
133
- if (labelCache.has(val)) {
134
- return labelCache.get(val) ?? val;
135
- }
136
- // Check current suggestions
137
- const match = suggestions.find((s) => s.value === val);
138
- if (match) {
139
- labelCache.set(val, match.label);
140
- return match.label;
141
- }
142
- // Return value as fallback
143
- return val;
144
- }
145
-
146
- /**
147
- * Build the full URL for fetching suggestions
148
- * @param query - The search query
149
- * @returns Full URL with query parameter
150
- */
151
- function buildUrl(query: string): string {
152
- const baseUrl = getBaseUrl?.() ?? '';
153
- const url = autocomplete.url.startsWith('http')
154
- ? autocomplete.url
155
- : `${baseUrl}${autocomplete.url}`;
156
-
157
- const separator = url.includes('?') ? '&' : '?';
158
- return `${url}${separator}${encodeURIComponent(queryParam)}=${encodeURIComponent(query)}`;
159
- }
160
-
161
- /**
162
- * Map API response to FieldOption array
163
- * @param data - Response data from API
164
- * @returns Array of FieldOption objects
165
- */
166
- function mapResponse(data: unknown): FieldOption[] {
167
- if (!Array.isArray(data)) {
168
- logger.warn('[FormAutocomplete] Response is not an array:', data);
169
- return [];
170
- }
171
-
172
- return data.map((item: Record<string, unknown>) => ({
173
- label: String(item[labelField] ?? item[valueField] ?? ''),
174
- value: String(item[valueField] ?? '')
175
- }));
176
- }
177
-
178
- /**
179
- * Fetch suggestions from the callback URL
180
- * @param query - The search query
181
- */
182
- async function fetchSuggestions(query: string): Promise<void> {
183
- // Cancel any pending request
184
- if (abortController) {
185
- abortController.abort();
186
- }
187
-
188
- // Check minimum characters requirement
189
- if (query.length < minChars && query.length > 0) {
190
- suggestions = [];
191
- return;
192
- }
193
-
194
- isLoading = true;
195
- error = null;
196
- abortController = new AbortController();
197
-
198
- try {
199
- // Build headers with authentication (call getter to get current value)
200
- const headers = await buildFetchHeaders(getAuthProvider?.());
201
-
202
- // Fetch with timeout
203
- const timeoutId = setTimeout(() => {
204
- abortController?.abort();
205
- }, 5000);
206
-
207
- const response = await fetch(buildUrl(query), {
208
- method: 'GET',
209
- headers,
210
- signal: abortController.signal
211
- });
212
-
213
- clearTimeout(timeoutId);
214
-
215
- if (!response.ok) {
216
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
217
- }
218
-
219
- const data = await response.json();
220
- const mapped = mapResponse(data);
221
-
222
- // Update label cache with fetched suggestions
223
- mapped.forEach((opt) => {
224
- labelCache.set(opt.value, opt.label);
225
- });
226
-
227
- suggestions = mapped;
228
- highlightedIndex = -1;
229
- } catch (err) {
230
- if (err instanceof Error && err.name === 'AbortError') {
231
- // Request was cancelled, ignore
232
- return;
233
- }
234
- logger.error('[FormAutocomplete] Fetch error:', err);
235
- error = err instanceof Error ? err.message : 'Failed to fetch suggestions';
236
- suggestions = [];
237
- } finally {
238
- isLoading = false;
239
- abortController = null;
240
- }
241
- }
242
-
243
- /**
244
- * Debounced fetch for input changes
245
- * @param query - The search query
246
- */
247
- function debouncedFetch(query: string): void {
248
- if (debounceTimer) {
249
- clearTimeout(debounceTimer);
250
- }
251
-
252
- debounceTimer = setTimeout(() => {
253
- fetchSuggestions(query);
254
- }, debounceMs);
255
- }
256
-
257
- /**
258
- * Handle input value changes
259
- * @param event - Input event
260
- */
261
- function handleInput(event: Event): void {
262
- const target = event.currentTarget as HTMLInputElement;
263
- inputValue = target.value;
264
-
265
- // Open dropdown
266
- showDropdown();
267
-
268
- // If allowFreeText and single mode, update the value immediately
269
- if (allowFreeText && !multiple) {
270
- onChange(inputValue);
271
- }
272
-
273
- // Fetch suggestions with debounce
274
- debouncedFetch(inputValue);
275
- }
276
-
277
- /**
278
- * Handle input focus
279
- */
280
- function handleFocus(): void {
281
- if (fetchOnFocus && suggestions.length === 0 && !isLoading) {
282
- fetchSuggestions(inputValue);
283
- }
284
- showDropdown();
285
- }
286
-
287
- /**
288
- * Handle input blur
289
- * Delayed to allow click events on options to fire first
290
- */
291
- function handleBlur(): void {
292
- setTimeout(() => {
293
- hideDropdown();
294
-
295
- // If not allowFreeText and single mode, validate the input
296
- if (!allowFreeText && !multiple && inputValue !== '') {
297
- const currentVal = selectedValues;
298
- const matchingSuggestion = suggestions.find(
299
- (s) => s.value === currentVal[0] || s.label.toLowerCase() === inputValue.toLowerCase()
300
- );
301
- if (!matchingSuggestion && currentVal.length === 0) {
302
- inputValue = '';
303
- }
304
- }
305
- }, 200);
306
- }
307
-
308
- /**
309
- * Handle option selection
310
- * @param option - Selected option
311
- */
312
- function selectOption(option: FieldOption): void {
313
- // Update label cache
314
- labelCache.set(option.value, option.label);
315
-
316
- if (multiple) {
317
- const current = selectedValues;
318
- if (current.includes(option.value)) {
319
- // Remove if already selected
320
- const newValues = current.filter((v) => v !== option.value);
321
- onChange(newValues);
322
- } else {
323
- // Add to selection
324
- const newValues = [...current, option.value];
325
- onChange(newValues);
326
- }
327
- // Clear input and keep dropdown open for more selections
328
- inputValue = '';
329
- inputElement?.focus();
330
- } else {
331
- // Single selection mode
332
- inputValue = '';
333
- onChange(option.value);
334
- hideDropdown();
335
- }
336
- highlightedIndex = -1;
337
- }
338
-
339
- /**
340
- * Remove a selected tag
341
- * @param valueToRemove - The value to remove
342
- */
343
- function removeTag(valueToRemove: string): void {
344
- if (disabled) return;
345
-
346
- if (multiple) {
347
- const current = selectedValues;
348
- const newValues = current.filter((v) => v !== valueToRemove);
349
- onChange(newValues);
350
- } else {
351
- onChange('');
352
- }
353
- inputElement?.focus();
354
- }
355
-
356
- /**
357
- * Handle keyboard navigation
358
- * @param event - Keyboard event
359
- */
360
- function handleKeydown(event: KeyboardEvent): void {
361
- // Handle backspace to remove last tag in multiple mode
362
- if (event.key === 'Backspace' && inputValue === '' && selectedValues.length > 0) {
363
- event.preventDefault();
364
- const current = selectedValues;
365
- if (multiple) {
366
- const newValues = current.slice(0, -1);
367
- onChange(newValues);
368
- } else {
369
- onChange('');
370
- }
371
- return;
372
- }
373
-
374
- if (!isOpen && event.key !== 'ArrowDown' && event.key !== 'Enter') {
375
- return;
376
- }
377
-
378
- switch (event.key) {
379
- case 'ArrowDown':
380
- event.preventDefault();
381
- if (!isOpen) {
382
- showDropdown();
383
- if (fetchOnFocus && suggestions.length === 0) {
384
- fetchSuggestions(inputValue);
385
- }
386
- } else {
387
- highlightedIndex = Math.min(highlightedIndex + 1, suggestions.length - 1);
388
- }
389
- break;
390
-
391
- case 'ArrowUp':
392
- event.preventDefault();
393
- highlightedIndex = Math.max(highlightedIndex - 1, -1);
394
- break;
395
-
396
- case 'Enter':
397
- event.preventDefault();
398
- if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) {
399
- selectOption(suggestions[highlightedIndex]);
400
- } else if (allowFreeText && inputValue !== '') {
401
- if (multiple) {
402
- const current = selectedValues;
403
- if (!current.includes(inputValue)) {
404
- onChange([...current, inputValue]);
405
- }
406
- inputValue = '';
407
- } else {
408
- onChange(inputValue);
409
- hideDropdown();
410
- }
411
- }
412
- break;
413
-
414
- case 'Escape':
415
- event.preventDefault();
416
- hideDropdown();
417
- highlightedIndex = -1;
418
- break;
419
-
420
- case 'Tab':
421
- // Allow tab to close dropdown naturally
422
- hideDropdown();
423
- break;
424
- }
425
- }
426
-
427
- /**
428
- * Retry failed fetch
429
- */
430
- function handleRetry(): void {
431
- error = null;
432
- fetchSuggestions(inputValue);
433
- }
434
-
435
- /**
436
- * Clear all selections
437
- */
438
- function handleClearAll(): void {
439
- inputValue = '';
440
- onChange(multiple ? [] : '');
441
- suggestions = [];
442
- inputElement?.focus();
443
- }
444
-
445
- /**
446
- * Sync label cache when value prop changes externally
447
- */
448
- $effect(() => {
449
- // When value changes, try to find labels from suggestions
450
- const vals = selectedValues;
451
- vals.forEach((val) => {
452
- if (!labelCache.has(val)) {
453
- const match = suggestions.find((s) => s.value === val);
454
- if (match) {
455
- labelCache.set(val, match.label);
456
- }
457
- }
458
- });
459
- });
460
-
461
- /**
462
- * Calculate popover position relative to viewport
463
- * Popover API renders in top layer, bypassing all stacking contexts
464
- */
465
- function updatePopoverPosition(): void {
466
- if (!containerElement) return;
467
-
468
- const rect = containerElement.getBoundingClientRect();
469
- const viewportHeight = window.innerHeight;
470
- const maxDropdownHeight = 240; // 15rem in pixels approximately
471
- const spaceBelow = viewportHeight - rect.bottom;
472
- const spaceAbove = rect.top;
473
-
474
- const left = rect.left;
475
- const width = rect.width;
476
-
477
- if (spaceBelow < maxDropdownHeight && spaceAbove > spaceBelow) {
478
- // Position above the input
479
- const bottom = viewportHeight - rect.top + 4;
480
- const maxHeight = Math.min(spaceAbove - 8, maxDropdownHeight);
481
- popoverStyle = `bottom: ${bottom}px; left: ${left}px; width: ${width}px; max-height: ${maxHeight}px;`;
482
- } else {
483
- // Position below the input (default)
484
- const top = rect.bottom + 4;
485
- const maxHeight = Math.min(spaceBelow - 8, maxDropdownHeight);
486
- popoverStyle = `top: ${top}px; left: ${left}px; width: ${width}px; max-height: ${maxHeight}px;`;
487
- }
488
- }
489
-
490
- /**
491
- * Show the popover dropdown
492
- */
493
- function showDropdown(): void {
494
- if (!popoverElement || disabled) return;
495
-
496
- updatePopoverPosition();
497
-
498
- try {
499
- popoverElement.showPopover();
500
- isOpen = true;
501
- } catch {
502
- // Fallback for browsers without popover support - just set isOpen
503
- isOpen = true;
504
- }
505
- }
506
-
507
- /**
508
- * Hide the popover dropdown
509
- */
510
- function hideDropdown(): void {
511
- if (!popoverElement) return;
512
-
513
- try {
514
- popoverElement.hidePopover();
515
- } catch {
516
- // Fallback for browsers without popover support
517
- }
518
- isOpen = false;
519
- }
520
-
521
- /**
522
- * Effect to update popover position on scroll/resize when open
523
- */
524
- $effect(() => {
525
- if (isOpen && containerElement) {
526
- const handlePositionUpdate = (): void => {
527
- updatePopoverPosition();
528
- };
529
-
530
- window.addEventListener('scroll', handlePositionUpdate, true);
531
- window.addEventListener('resize', handlePositionUpdate);
532
-
533
- return () => {
534
- window.removeEventListener('scroll', handlePositionUpdate, true);
535
- window.removeEventListener('resize', handlePositionUpdate);
536
- };
537
- }
538
- });
539
-
540
- /**
541
- * Cleanup on unmount
542
- */
543
- onMount(() => {
544
- return () => {
545
- // Cleanup debounce timer
546
- if (debounceTimer) {
547
- clearTimeout(debounceTimer);
548
- }
549
- // Cleanup abort controller
550
- if (abortController) {
551
- abortController.abort();
552
- }
553
- };
554
- });
24
+ import { getContext, onMount } from "svelte";
25
+ import Icon from "@iconify/svelte";
26
+ import type { AutocompleteConfig, AuthProvider } from "../../types/index.js";
27
+ import type { FieldOption } from "./types.js";
28
+ import { buildFetchHeaders } from "../../utils/fetchWithAuth.js";
29
+ import { logger } from "../../utils/logger.js";
30
+
31
+ /**
32
+ * Props interface for FormAutocomplete component
33
+ */
34
+ interface Props {
35
+ /** Field identifier */
36
+ id: string;
37
+ /** Current selected value (string for single, string[] for multiple) */
38
+ value: string | string[];
39
+ /** Autocomplete configuration */
40
+ autocomplete: AutocompleteConfig;
41
+ /** Whether the field is required */
42
+ required?: boolean;
43
+ /** Placeholder text */
44
+ placeholder?: string;
45
+ /** Whether the field is disabled */
46
+ disabled?: boolean;
47
+ /** ARIA description ID */
48
+ ariaDescribedBy?: string;
49
+ /** Callback when value changes */
50
+ onChange: (value: string | string[]) => void;
51
+ }
52
+
53
+ let {
54
+ id,
55
+ value = "",
56
+ autocomplete,
57
+ required = false,
58
+ placeholder = "",
59
+ disabled = false,
60
+ ariaDescribedBy,
61
+ onChange,
62
+ }: Props = $props();
63
+
64
+ // Get AuthProvider and baseUrl from context via getter functions
65
+ // This pattern ensures we always get the current value, even if props change after mount
66
+ const getAuthProvider = getContext<
67
+ (() => AuthProvider | undefined) | undefined
68
+ >("flowdrop:getAuthProvider");
69
+ const getBaseUrl = getContext<(() => string) | undefined>(
70
+ "flowdrop:getBaseUrl",
71
+ );
72
+
73
+ // Configuration with defaults
74
+ const queryParam = $derived(autocomplete.queryParam ?? "q");
75
+ const minChars = $derived(autocomplete.minChars ?? 0);
76
+ const debounceMs = $derived(autocomplete.debounceMs ?? 300);
77
+ const fetchOnFocus = $derived(autocomplete.fetchOnFocus ?? false);
78
+ const labelField = $derived(autocomplete.labelField ?? "label");
79
+ const valueField = $derived(autocomplete.valueField ?? "value");
80
+ const allowFreeText = $derived(autocomplete.allowFreeText ?? false);
81
+ const multiple = $derived(autocomplete.multiple ?? false);
82
+
83
+ // Component state
84
+ let inputElement: HTMLInputElement | undefined = $state(undefined);
85
+ let containerElement: HTMLDivElement | undefined = $state(undefined);
86
+ let popoverElement: HTMLDivElement | undefined = $state(undefined);
87
+ let inputValue = $state("");
88
+ let suggestions = $state<FieldOption[]>([]);
89
+ let isOpen = $state(false);
90
+ let isLoading = $state(false);
91
+ let error = $state<string | null>(null);
92
+ let highlightedIndex = $state(-1);
93
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
94
+ let abortController: AbortController | null = null;
95
+
96
+ // Popover positioning style
97
+ let popoverStyle = $state("");
98
+
99
+ // Cache of value-to-label mappings for selected items
100
+ let labelCache = $state<Map<string, string>>(new Map());
101
+
102
+ // Generate unique IDs for accessibility
103
+ // svelte-ignore state_referenced_locally — id prop never changes
104
+ const listboxId = `${id}-listbox`;
105
+ // svelte-ignore state_referenced_locally
106
+ const getOptionId = (index: number): string => `${id}-option-${index}`;
107
+
108
+ /**
109
+ * Get the selected values as an array (normalizes single/multiple)
110
+ */
111
+ const selectedValues = $derived<string[]>(
112
+ multiple
113
+ ? Array.isArray(value)
114
+ ? value
115
+ : value
116
+ ? [String(value)]
117
+ : []
118
+ : value
119
+ ? [String(value)]
120
+ : [],
121
+ );
122
+
123
+ /**
124
+ * Check if a value is selected
125
+ */
126
+ function isSelected(optionValue: string): boolean {
127
+ return selectedValues.includes(optionValue);
128
+ }
129
+
130
+ /**
131
+ * Get display label for a selected value
132
+ */
133
+ function getDisplayLabel(val: string): string {
134
+ // Check cache first
135
+ if (labelCache.has(val)) {
136
+ return labelCache.get(val) ?? val;
137
+ }
138
+ // Check current suggestions
139
+ const match = suggestions.find((s) => s.value === val);
140
+ if (match) {
141
+ labelCache.set(val, match.label);
142
+ return match.label;
143
+ }
144
+ // Return value as fallback
145
+ return val;
146
+ }
147
+
148
+ /**
149
+ * Build the full URL for fetching suggestions
150
+ * @param query - The search query
151
+ * @returns Full URL with query parameter
152
+ */
153
+ function buildUrl(query: string): string {
154
+ const baseUrl = getBaseUrl?.() ?? "";
155
+ const url = autocomplete.url.startsWith("http")
156
+ ? autocomplete.url
157
+ : `${baseUrl}${autocomplete.url}`;
158
+
159
+ const separator = url.includes("?") ? "&" : "?";
160
+ return `${url}${separator}${encodeURIComponent(queryParam)}=${encodeURIComponent(query)}`;
161
+ }
162
+
163
+ /**
164
+ * Map API response to FieldOption array
165
+ * @param data - Response data from API
166
+ * @returns Array of FieldOption objects
167
+ */
168
+ function mapResponse(data: unknown): FieldOption[] {
169
+ if (!Array.isArray(data)) {
170
+ logger.warn("[FormAutocomplete] Response is not an array:", data);
171
+ return [];
172
+ }
173
+
174
+ return data.map((item: Record<string, unknown>) => ({
175
+ label: String(item[labelField] ?? item[valueField] ?? ""),
176
+ value: String(item[valueField] ?? ""),
177
+ }));
178
+ }
179
+
180
+ /**
181
+ * Fetch suggestions from the callback URL
182
+ * @param query - The search query
183
+ */
184
+ async function fetchSuggestions(query: string): Promise<void> {
185
+ // Cancel any pending request
186
+ if (abortController) {
187
+ abortController.abort();
188
+ }
189
+
190
+ // Check minimum characters requirement
191
+ if (query.length < minChars && query.length > 0) {
192
+ suggestions = [];
193
+ return;
194
+ }
195
+
196
+ isLoading = true;
197
+ error = null;
198
+ abortController = new AbortController();
199
+
200
+ try {
201
+ // Build headers with authentication (call getter to get current value)
202
+ const headers = await buildFetchHeaders(getAuthProvider?.());
203
+
204
+ // Fetch with timeout
205
+ const timeoutId = setTimeout(() => {
206
+ abortController?.abort();
207
+ }, 5000);
208
+
209
+ const response = await fetch(buildUrl(query), {
210
+ method: "GET",
211
+ headers,
212
+ signal: abortController.signal,
213
+ });
214
+
215
+ clearTimeout(timeoutId);
216
+
217
+ if (!response.ok) {
218
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
219
+ }
220
+
221
+ const data = await response.json();
222
+ const mapped = mapResponse(data);
223
+
224
+ // Update label cache with fetched suggestions
225
+ mapped.forEach((opt) => {
226
+ labelCache.set(opt.value, opt.label);
227
+ });
228
+
229
+ suggestions = mapped;
230
+ highlightedIndex = -1;
231
+ } catch (err) {
232
+ if (err instanceof Error && err.name === "AbortError") {
233
+ // Request was cancelled, ignore
234
+ return;
235
+ }
236
+ logger.error("[FormAutocomplete] Fetch error:", err);
237
+ error =
238
+ err instanceof Error ? err.message : "Failed to fetch suggestions";
239
+ suggestions = [];
240
+ } finally {
241
+ isLoading = false;
242
+ abortController = null;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Debounced fetch for input changes
248
+ * @param query - The search query
249
+ */
250
+ function debouncedFetch(query: string): void {
251
+ if (debounceTimer) {
252
+ clearTimeout(debounceTimer);
253
+ }
254
+
255
+ debounceTimer = setTimeout(() => {
256
+ fetchSuggestions(query);
257
+ }, debounceMs);
258
+ }
259
+
260
+ /**
261
+ * Handle input value changes
262
+ * @param event - Input event
263
+ */
264
+ function handleInput(event: Event): void {
265
+ const target = event.currentTarget as HTMLInputElement;
266
+ inputValue = target.value;
267
+
268
+ // Open dropdown
269
+ showDropdown();
270
+
271
+ // If allowFreeText and single mode, update the value immediately
272
+ if (allowFreeText && !multiple) {
273
+ onChange(inputValue);
274
+ }
275
+
276
+ // Fetch suggestions with debounce
277
+ debouncedFetch(inputValue);
278
+ }
279
+
280
+ /**
281
+ * Handle input focus
282
+ */
283
+ function handleFocus(): void {
284
+ if (fetchOnFocus && suggestions.length === 0 && !isLoading) {
285
+ fetchSuggestions(inputValue);
286
+ }
287
+ showDropdown();
288
+ }
289
+
290
+ /**
291
+ * Handle input blur
292
+ * Delayed to allow click events on options to fire first
293
+ */
294
+ function handleBlur(): void {
295
+ setTimeout(() => {
296
+ hideDropdown();
297
+
298
+ // If not allowFreeText and single mode, validate the input
299
+ if (!allowFreeText && !multiple && inputValue !== "") {
300
+ const currentVal = selectedValues;
301
+ const matchingSuggestion = suggestions.find(
302
+ (s) =>
303
+ s.value === currentVal[0] ||
304
+ s.label.toLowerCase() === inputValue.toLowerCase(),
305
+ );
306
+ if (!matchingSuggestion && currentVal.length === 0) {
307
+ inputValue = "";
308
+ }
309
+ }
310
+ }, 200);
311
+ }
312
+
313
+ /**
314
+ * Handle option selection
315
+ * @param option - Selected option
316
+ */
317
+ function selectOption(option: FieldOption): void {
318
+ // Update label cache
319
+ labelCache.set(option.value, option.label);
320
+
321
+ if (multiple) {
322
+ const current = selectedValues;
323
+ if (current.includes(option.value)) {
324
+ // Remove if already selected
325
+ const newValues = current.filter((v) => v !== option.value);
326
+ onChange(newValues);
327
+ } else {
328
+ // Add to selection
329
+ const newValues = [...current, option.value];
330
+ onChange(newValues);
331
+ }
332
+ // Clear input and keep dropdown open for more selections
333
+ inputValue = "";
334
+ inputElement?.focus();
335
+ } else {
336
+ // Single selection mode
337
+ inputValue = "";
338
+ onChange(option.value);
339
+ hideDropdown();
340
+ }
341
+ highlightedIndex = -1;
342
+ }
343
+
344
+ /**
345
+ * Remove a selected tag
346
+ * @param valueToRemove - The value to remove
347
+ */
348
+ function removeTag(valueToRemove: string): void {
349
+ if (disabled) return;
350
+
351
+ if (multiple) {
352
+ const current = selectedValues;
353
+ const newValues = current.filter((v) => v !== valueToRemove);
354
+ onChange(newValues);
355
+ } else {
356
+ onChange("");
357
+ }
358
+ inputElement?.focus();
359
+ }
360
+
361
+ /**
362
+ * Handle keyboard navigation
363
+ * @param event - Keyboard event
364
+ */
365
+ function handleKeydown(event: KeyboardEvent): void {
366
+ // Handle backspace to remove last tag in multiple mode
367
+ if (
368
+ event.key === "Backspace" &&
369
+ inputValue === "" &&
370
+ selectedValues.length > 0
371
+ ) {
372
+ event.preventDefault();
373
+ const current = selectedValues;
374
+ if (multiple) {
375
+ const newValues = current.slice(0, -1);
376
+ onChange(newValues);
377
+ } else {
378
+ onChange("");
379
+ }
380
+ return;
381
+ }
382
+
383
+ if (!isOpen && event.key !== "ArrowDown" && event.key !== "Enter") {
384
+ return;
385
+ }
386
+
387
+ switch (event.key) {
388
+ case "ArrowDown":
389
+ event.preventDefault();
390
+ if (!isOpen) {
391
+ showDropdown();
392
+ if (fetchOnFocus && suggestions.length === 0) {
393
+ fetchSuggestions(inputValue);
394
+ }
395
+ } else {
396
+ highlightedIndex = Math.min(
397
+ highlightedIndex + 1,
398
+ suggestions.length - 1,
399
+ );
400
+ }
401
+ break;
402
+
403
+ case "ArrowUp":
404
+ event.preventDefault();
405
+ highlightedIndex = Math.max(highlightedIndex - 1, -1);
406
+ break;
407
+
408
+ case "Enter":
409
+ event.preventDefault();
410
+ if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) {
411
+ selectOption(suggestions[highlightedIndex]);
412
+ } else if (allowFreeText && inputValue !== "") {
413
+ if (multiple) {
414
+ const current = selectedValues;
415
+ if (!current.includes(inputValue)) {
416
+ onChange([...current, inputValue]);
417
+ }
418
+ inputValue = "";
419
+ } else {
420
+ onChange(inputValue);
421
+ hideDropdown();
422
+ }
423
+ }
424
+ break;
425
+
426
+ case "Escape":
427
+ event.preventDefault();
428
+ hideDropdown();
429
+ highlightedIndex = -1;
430
+ break;
431
+
432
+ case "Tab":
433
+ // Allow tab to close dropdown naturally
434
+ hideDropdown();
435
+ break;
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Retry failed fetch
441
+ */
442
+ function handleRetry(): void {
443
+ error = null;
444
+ fetchSuggestions(inputValue);
445
+ }
446
+
447
+ /**
448
+ * Clear all selections
449
+ */
450
+ function handleClearAll(): void {
451
+ inputValue = "";
452
+ onChange(multiple ? [] : "");
453
+ suggestions = [];
454
+ inputElement?.focus();
455
+ }
456
+
457
+ /**
458
+ * Sync label cache when value prop changes externally
459
+ */
460
+ $effect(() => {
461
+ // When value changes, try to find labels from suggestions
462
+ const vals = selectedValues;
463
+ vals.forEach((val) => {
464
+ if (!labelCache.has(val)) {
465
+ const match = suggestions.find((s) => s.value === val);
466
+ if (match) {
467
+ labelCache.set(val, match.label);
468
+ }
469
+ }
470
+ });
471
+ });
472
+
473
+ /**
474
+ * Calculate popover position relative to viewport
475
+ * Popover API renders in top layer, bypassing all stacking contexts
476
+ */
477
+ function updatePopoverPosition(): void {
478
+ if (!containerElement) return;
479
+
480
+ const rect = containerElement.getBoundingClientRect();
481
+ const viewportHeight = window.innerHeight;
482
+ const maxDropdownHeight = 240; // 15rem in pixels approximately
483
+ const spaceBelow = viewportHeight - rect.bottom;
484
+ const spaceAbove = rect.top;
485
+
486
+ const left = rect.left;
487
+ const width = rect.width;
488
+
489
+ if (spaceBelow < maxDropdownHeight && spaceAbove > spaceBelow) {
490
+ // Position above the input
491
+ const bottom = viewportHeight - rect.top + 4;
492
+ const maxHeight = Math.min(spaceAbove - 8, maxDropdownHeight);
493
+ popoverStyle = `bottom: ${bottom}px; left: ${left}px; width: ${width}px; max-height: ${maxHeight}px;`;
494
+ } else {
495
+ // Position below the input (default)
496
+ const top = rect.bottom + 4;
497
+ const maxHeight = Math.min(spaceBelow - 8, maxDropdownHeight);
498
+ popoverStyle = `top: ${top}px; left: ${left}px; width: ${width}px; max-height: ${maxHeight}px;`;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Show the popover dropdown
504
+ */
505
+ function showDropdown(): void {
506
+ if (!popoverElement || disabled) return;
507
+
508
+ updatePopoverPosition();
509
+
510
+ try {
511
+ popoverElement.showPopover();
512
+ isOpen = true;
513
+ } catch {
514
+ // Fallback for browsers without popover support - just set isOpen
515
+ isOpen = true;
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Hide the popover dropdown
521
+ */
522
+ function hideDropdown(): void {
523
+ if (!popoverElement) return;
524
+
525
+ try {
526
+ popoverElement.hidePopover();
527
+ } catch {
528
+ // Fallback for browsers without popover support
529
+ }
530
+ isOpen = false;
531
+ }
532
+
533
+ /**
534
+ * Effect to update popover position on scroll/resize when open
535
+ */
536
+ $effect(() => {
537
+ if (isOpen && containerElement) {
538
+ const handlePositionUpdate = (): void => {
539
+ updatePopoverPosition();
540
+ };
541
+
542
+ window.addEventListener("scroll", handlePositionUpdate, true);
543
+ window.addEventListener("resize", handlePositionUpdate);
544
+
545
+ return () => {
546
+ window.removeEventListener("scroll", handlePositionUpdate, true);
547
+ window.removeEventListener("resize", handlePositionUpdate);
548
+ };
549
+ }
550
+ });
551
+
552
+ /**
553
+ * Cleanup on unmount
554
+ */
555
+ onMount(() => {
556
+ return () => {
557
+ // Cleanup debounce timer
558
+ if (debounceTimer) {
559
+ clearTimeout(debounceTimer);
560
+ }
561
+ // Cleanup abort controller
562
+ if (abortController) {
563
+ abortController.abort();
564
+ }
565
+ };
566
+ });
555
567
  </script>
556
568
 
557
569
  <div
558
- bind:this={containerElement}
559
- class="form-autocomplete"
560
- class:form-autocomplete--disabled={disabled}
561
- class:form-autocomplete--multiple={multiple}
562
- class:form-autocomplete--has-value={selectedValues.length > 0}
570
+ bind:this={containerElement}
571
+ class="form-autocomplete"
572
+ class:form-autocomplete--disabled={disabled}
573
+ class:form-autocomplete--multiple={multiple}
574
+ class:form-autocomplete--has-value={selectedValues.length > 0}
563
575
  >
564
- <!-- Main input container styled like a textfield/textarea -->
565
- <div
566
- class="form-autocomplete__field"
567
- class:form-autocomplete__field--focused={isOpen}
568
- onclick={() => inputElement?.focus()}
569
- onkeydown={() => {}}
570
- role="presentation"
571
- >
572
- <!-- Selected tags -->
573
- {#if selectedValues.length > 0}
574
- <div class="form-autocomplete__tags">
575
- {#each selectedValues as selectedVal (selectedVal)}
576
- <span class="form-autocomplete__tag">
577
- <span class="form-autocomplete__tag-label">{getDisplayLabel(selectedVal)}</span>
578
- {#if !disabled}
579
- <button
580
- type="button"
581
- class="form-autocomplete__tag-remove"
582
- aria-label={`Remove ${getDisplayLabel(selectedVal)}`}
583
- onclick={(e) => {
584
- e.stopPropagation();
585
- removeTag(selectedVal);
586
- }}
587
- tabindex={-1}
588
- >
589
- <Icon icon="heroicons:x-mark" />
590
- </button>
591
- {/if}
592
- </span>
593
- {/each}
594
- </div>
595
- {/if}
596
-
597
- <!-- Input area -->
598
- <div class="form-autocomplete__input-area">
599
- <input
600
- bind:this={inputElement}
601
- type="text"
602
- {id}
603
- class="form-autocomplete__input"
604
- value={inputValue}
605
- placeholder={selectedValues.length === 0 ? placeholder : ''}
606
- {disabled}
607
- aria-required={required}
608
- aria-describedby={ariaDescribedBy}
609
- role="combobox"
610
- aria-expanded={isOpen}
611
- aria-controls={listboxId}
612
- aria-activedescendant={highlightedIndex >= 0 ? getOptionId(highlightedIndex) : undefined}
613
- aria-autocomplete="list"
614
- autocomplete="off"
615
- oninput={handleInput}
616
- onfocus={handleFocus}
617
- onblur={handleBlur}
618
- onkeydown={handleKeydown}
619
- />
620
- </div>
621
-
622
- <!-- Status icons -->
623
- <div class="form-autocomplete__icons">
624
- {#if isLoading}
625
- <span class="form-autocomplete__spinner" aria-label="Loading suggestions">
626
- <Icon icon="heroicons:arrow-path" />
627
- </span>
628
- {:else if selectedValues.length > 0 && !disabled}
629
- <button
630
- type="button"
631
- class="form-autocomplete__clear"
632
- aria-label="Clear all selections"
633
- onclick={(e) => {
634
- e.stopPropagation();
635
- handleClearAll();
636
- }}
637
- tabindex={-1}
638
- >
639
- <Icon icon="heroicons:x-mark" />
640
- </button>
641
- {:else}
642
- <span class="form-autocomplete__chevron" aria-hidden="true">
643
- <Icon icon="heroicons:chevron-down" />
644
- </span>
645
- {/if}
646
- </div>
647
- </div>
648
-
649
- <!-- Dropdown popover (uses Popover API to render in top layer, bypassing stacking contexts) -->
650
- <!-- svelte-ignore a11y_no_static_element_interactions — role="presentation" container; onmousedown prevents focus loss from input -->
651
- <div
652
- bind:this={popoverElement}
653
- id={listboxId}
654
- class="form-autocomplete__popover"
655
- popover="manual"
656
- role="presentation"
657
- style={popoverStyle}
658
- onmousedown={(e) => e.preventDefault()}
659
- >
660
- <ul class="form-autocomplete__listbox" role="listbox" aria-label="Suggestions">
661
- {#if isLoading}
662
- <li class="form-autocomplete__status form-autocomplete__status--loading">
663
- <Icon icon="heroicons:arrow-path" class="form-autocomplete__status-icon" />
664
- <span>Loading suggestions...</span>
665
- </li>
666
- {:else if error}
667
- <li class="form-autocomplete__status form-autocomplete__status--error">
668
- <Icon icon="heroicons:exclamation-triangle" class="form-autocomplete__status-icon" />
669
- <span>{error}</span>
670
- <button type="button" class="form-autocomplete__retry" onclick={handleRetry}>
671
- Retry
672
- </button>
673
- </li>
674
- {:else if suggestions.length === 0}
675
- <li class="form-autocomplete__status form-autocomplete__status--empty">
676
- <Icon icon="heroicons:magnifying-glass" class="form-autocomplete__status-icon" />
677
- <span>No results found</span>
678
- </li>
679
- {:else}
680
- {#each suggestions as option, index (option.value)}
681
- <!-- svelte-ignore a11y_click_events_have_key_events — WAI-ARIA combobox: keyboard nav handled on input, not individual options -->
682
- <li
683
- id={getOptionId(index)}
684
- class="form-autocomplete__option"
685
- class:form-autocomplete__option--highlighted={index === highlightedIndex}
686
- class:form-autocomplete__option--selected={isSelected(option.value)}
687
- role="option"
688
- aria-selected={isSelected(option.value)}
689
- onmouseenter={() => (highlightedIndex = index)}
690
- onclick={() => selectOption(option)}
691
- >
692
- <span class="form-autocomplete__option-label">{option.label}</span>
693
- {#if isSelected(option.value)}
694
- <Icon icon="heroicons:check" class="form-autocomplete__option-check" />
695
- {/if}
696
- </li>
697
- {/each}
698
- {/if}
699
- </ul>
700
- </div>
576
+ <!-- Main input container styled like a textfield/textarea -->
577
+ <div
578
+ class="form-autocomplete__field"
579
+ class:form-autocomplete__field--focused={isOpen}
580
+ onclick={() => inputElement?.focus()}
581
+ onkeydown={() => {}}
582
+ role="presentation"
583
+ >
584
+ <!-- Selected tags -->
585
+ {#if selectedValues.length > 0}
586
+ <div class="form-autocomplete__tags">
587
+ {#each selectedValues as selectedVal (selectedVal)}
588
+ <span class="form-autocomplete__tag">
589
+ <span class="form-autocomplete__tag-label"
590
+ >{getDisplayLabel(selectedVal)}</span
591
+ >
592
+ {#if !disabled}
593
+ <button
594
+ type="button"
595
+ class="form-autocomplete__tag-remove"
596
+ aria-label={`Remove ${getDisplayLabel(selectedVal)}`}
597
+ onclick={(e) => {
598
+ e.stopPropagation();
599
+ removeTag(selectedVal);
600
+ }}
601
+ tabindex={-1}
602
+ >
603
+ <Icon icon="heroicons:x-mark" />
604
+ </button>
605
+ {/if}
606
+ </span>
607
+ {/each}
608
+ </div>
609
+ {/if}
610
+
611
+ <!-- Input area -->
612
+ <div class="form-autocomplete__input-area">
613
+ <input
614
+ bind:this={inputElement}
615
+ type="text"
616
+ {id}
617
+ class="form-autocomplete__input"
618
+ value={inputValue}
619
+ placeholder={selectedValues.length === 0 ? placeholder : ""}
620
+ {disabled}
621
+ aria-required={required}
622
+ aria-describedby={ariaDescribedBy}
623
+ role="combobox"
624
+ aria-expanded={isOpen}
625
+ aria-controls={listboxId}
626
+ aria-activedescendant={highlightedIndex >= 0
627
+ ? getOptionId(highlightedIndex)
628
+ : undefined}
629
+ aria-autocomplete="list"
630
+ autocomplete="off"
631
+ oninput={handleInput}
632
+ onfocus={handleFocus}
633
+ onblur={handleBlur}
634
+ onkeydown={handleKeydown}
635
+ />
636
+ </div>
637
+
638
+ <!-- Status icons -->
639
+ <div class="form-autocomplete__icons">
640
+ {#if isLoading}
641
+ <span
642
+ class="form-autocomplete__spinner"
643
+ aria-label="Loading suggestions"
644
+ >
645
+ <Icon icon="heroicons:arrow-path" />
646
+ </span>
647
+ {:else if selectedValues.length > 0 && !disabled}
648
+ <button
649
+ type="button"
650
+ class="form-autocomplete__clear"
651
+ aria-label="Clear all selections"
652
+ onclick={(e) => {
653
+ e.stopPropagation();
654
+ handleClearAll();
655
+ }}
656
+ tabindex={-1}
657
+ >
658
+ <Icon icon="heroicons:x-mark" />
659
+ </button>
660
+ {:else}
661
+ <span class="form-autocomplete__chevron" aria-hidden="true">
662
+ <Icon icon="heroicons:chevron-down" />
663
+ </span>
664
+ {/if}
665
+ </div>
666
+ </div>
667
+
668
+ <!-- Dropdown popover (uses Popover API to render in top layer, bypassing stacking contexts) -->
669
+ <!-- svelte-ignore a11y_no_static_element_interactions — role="presentation" container; onmousedown prevents focus loss from input -->
670
+ <div
671
+ bind:this={popoverElement}
672
+ id={listboxId}
673
+ class="form-autocomplete__popover"
674
+ popover="manual"
675
+ role="presentation"
676
+ style={popoverStyle}
677
+ onmousedown={(e) => e.preventDefault()}
678
+ >
679
+ <ul
680
+ class="form-autocomplete__listbox"
681
+ role="listbox"
682
+ aria-label="Suggestions"
683
+ >
684
+ {#if isLoading}
685
+ <li
686
+ class="form-autocomplete__status form-autocomplete__status--loading"
687
+ >
688
+ <Icon
689
+ icon="heroicons:arrow-path"
690
+ class="form-autocomplete__status-icon"
691
+ />
692
+ <span>Loading suggestions...</span>
693
+ </li>
694
+ {:else if error}
695
+ <li class="form-autocomplete__status form-autocomplete__status--error">
696
+ <Icon
697
+ icon="heroicons:exclamation-triangle"
698
+ class="form-autocomplete__status-icon"
699
+ />
700
+ <span>{error}</span>
701
+ <button
702
+ type="button"
703
+ class="form-autocomplete__retry"
704
+ onclick={handleRetry}
705
+ >
706
+ Retry
707
+ </button>
708
+ </li>
709
+ {:else if suggestions.length === 0}
710
+ <li class="form-autocomplete__status form-autocomplete__status--empty">
711
+ <Icon
712
+ icon="heroicons:magnifying-glass"
713
+ class="form-autocomplete__status-icon"
714
+ />
715
+ <span>No results found</span>
716
+ </li>
717
+ {:else}
718
+ {#each suggestions as option, index (option.value)}
719
+ <!-- svelte-ignore a11y_click_events_have_key_events — WAI-ARIA combobox: keyboard nav handled on input, not individual options -->
720
+ <li
721
+ id={getOptionId(index)}
722
+ class="form-autocomplete__option"
723
+ class:form-autocomplete__option--highlighted={index ===
724
+ highlightedIndex}
725
+ class:form-autocomplete__option--selected={isSelected(option.value)}
726
+ role="option"
727
+ aria-selected={isSelected(option.value)}
728
+ onmouseenter={() => (highlightedIndex = index)}
729
+ onclick={() => selectOption(option)}
730
+ >
731
+ <span class="form-autocomplete__option-label">{option.label}</span>
732
+ {#if isSelected(option.value)}
733
+ <Icon
734
+ icon="heroicons:check"
735
+ class="form-autocomplete__option-check"
736
+ />
737
+ {/if}
738
+ </li>
739
+ {/each}
740
+ {/if}
741
+ </ul>
742
+ </div>
701
743
  </div>
702
744
 
703
745
  <style>
704
- .form-autocomplete {
705
- position: relative;
706
- width: 100%;
707
- }
708
-
709
- .form-autocomplete--disabled {
710
- opacity: 0.6;
711
- pointer-events: none;
712
- }
713
-
714
- /* Field container styled like a textfield */
715
- .form-autocomplete__field {
716
- display: flex;
717
- flex-wrap: wrap;
718
- align-items: flex-start;
719
- gap: var(--fd-space-3xs);
720
- min-height: 2.625rem;
721
- padding: var(--fd-space-3xs) 2.5rem var(--fd-space-3xs) var(--fd-space-md);
722
- border: 1px solid var(--fd-border);
723
- border-radius: var(--fd-radius-lg);
724
- font-size: var(--fd-text-sm);
725
- font-family: inherit;
726
- color: var(--fd-foreground);
727
- background-color: var(--fd-muted);
728
- transition: all var(--fd-transition-normal);
729
- box-shadow: var(--fd-shadow-sm);
730
- cursor: text;
731
- position: relative;
732
- }
733
-
734
- .form-autocomplete__field:hover {
735
- border-color: var(--fd-border-strong);
736
- background-color: var(--fd-background);
737
- }
738
-
739
- .form-autocomplete__field--focused {
740
- border-color: var(--fd-primary);
741
- background-color: var(--fd-background);
742
- box-shadow:
743
- 0 0 0 3px rgba(59, 130, 246, 0.12),
744
- var(--fd-shadow-sm);
745
- }
746
-
747
- /* Multiple mode - textarea-like styling */
748
- .form-autocomplete--multiple .form-autocomplete__field {
749
- min-height: 3rem;
750
- align-content: flex-start;
751
- }
752
-
753
- /* Tags container */
754
- .form-autocomplete__tags {
755
- display: flex;
756
- flex-wrap: wrap;
757
- gap: var(--fd-space-3xs);
758
- align-items: center;
759
- }
760
-
761
- /* Individual tag - selected item chip */
762
- .form-autocomplete__tag {
763
- display: inline-flex;
764
- align-items: center;
765
- gap: var(--fd-space-3xs);
766
- padding: var(--fd-space-3xs) var(--fd-space-3xs) var(--fd-space-3xs) var(--fd-space-xs);
767
- background-color: var(--fd-primary-muted);
768
- border: 1px solid var(--fd-primary-muted);
769
- border-radius: var(--fd-radius-md);
770
- font-size: 0.8125rem;
771
- color: var(--fd-primary-hover);
772
- line-height: 1.2;
773
- max-width: 100%;
774
- }
775
-
776
- .form-autocomplete__tag-label {
777
- overflow: hidden;
778
- text-overflow: ellipsis;
779
- white-space: nowrap;
780
- max-width: 12rem;
781
- }
782
-
783
- .form-autocomplete__tag-remove {
784
- display: flex;
785
- align-items: center;
786
- justify-content: center;
787
- padding: 0.125rem;
788
- margin-left: 0.125rem;
789
- border: none;
790
- border-radius: var(--fd-radius-sm);
791
- background: transparent;
792
- color: var(--fd-primary);
793
- cursor: pointer;
794
- transition: all var(--fd-transition-fast);
795
- flex-shrink: 0;
796
- }
797
-
798
- .form-autocomplete__tag-remove:hover {
799
- background-color: var(--fd-primary-muted);
800
- color: var(--fd-primary-hover);
801
- }
802
-
803
- .form-autocomplete__tag-remove :global(svg) {
804
- width: 0.875rem;
805
- height: 0.875rem;
806
- }
807
-
808
- /* Input area */
809
- .form-autocomplete__input-area {
810
- flex: 1;
811
- min-width: 4rem;
812
- display: flex;
813
- align-items: center;
814
- }
815
-
816
- .form-autocomplete__input {
817
- width: 100%;
818
- padding: var(--fd-space-3xs) 0;
819
- border: none;
820
- outline: none;
821
- font-size: var(--fd-text-sm);
822
- font-family: inherit;
823
- color: var(--fd-foreground);
824
- background-color: transparent;
825
- }
826
-
827
- .form-autocomplete__input::placeholder {
828
- color: var(--fd-muted-foreground);
829
- }
830
-
831
- /* Status icons */
832
- .form-autocomplete__icons {
833
- position: absolute;
834
- right: var(--fd-space-xs);
835
- top: 0.625rem;
836
- display: flex;
837
- align-items: center;
838
- gap: var(--fd-space-3xs);
839
- }
840
-
841
- .form-autocomplete__chevron,
842
- .form-autocomplete__spinner {
843
- display: flex;
844
- align-items: center;
845
- justify-content: center;
846
- color: var(--fd-muted-foreground);
847
- pointer-events: none;
848
- }
849
-
850
- .form-autocomplete__chevron :global(svg),
851
- .form-autocomplete__spinner :global(svg) {
852
- width: 1rem;
853
- height: 1rem;
854
- }
855
-
856
- .form-autocomplete__spinner {
857
- animation: autocomplete-spin 1s linear infinite;
858
- }
859
-
860
- @keyframes autocomplete-spin {
861
- from {
862
- transform: rotate(0deg);
863
- }
864
- to {
865
- transform: rotate(360deg);
866
- }
867
- }
868
-
869
- .form-autocomplete__clear {
870
- display: flex;
871
- align-items: center;
872
- justify-content: center;
873
- padding: var(--fd-space-3xs);
874
- border: none;
875
- border-radius: var(--fd-radius-sm);
876
- background: transparent;
877
- color: var(--fd-muted-foreground);
878
- cursor: pointer;
879
- transition: all var(--fd-transition-fast);
880
- }
881
-
882
- .form-autocomplete__clear:hover {
883
- background-color: var(--fd-muted);
884
- color: var(--fd-foreground);
885
- }
886
-
887
- .form-autocomplete__clear :global(svg) {
888
- width: 1rem;
889
- height: 1rem;
890
- }
891
-
892
- /* Popover container - renders in top layer via Popover API */
893
- .form-autocomplete__popover {
894
- position: fixed;
895
- margin: 0;
896
- padding: 0;
897
- border: none;
898
- background: transparent;
899
- overflow: visible;
900
- }
901
-
902
- /* Reset default popover styles */
903
- .form-autocomplete__popover:popover-open {
904
- display: block;
905
- }
906
-
907
- /* Dropdown listbox inside popover */
908
- .form-autocomplete__listbox {
909
- margin: 0;
910
- padding: var(--fd-space-3xs);
911
- list-style: none;
912
- background-color: var(--fd-background);
913
- border: 1px solid var(--fd-border);
914
- border-radius: var(--fd-radius-lg);
915
- box-shadow: var(--fd-shadow-lg);
916
- overflow-y: auto;
917
- max-height: inherit;
918
- }
919
-
920
- .form-autocomplete__option {
921
- display: flex;
922
- align-items: center;
923
- justify-content: space-between;
924
- padding: var(--fd-space-xs) var(--fd-space-md);
925
- border-radius: var(--fd-radius-md);
926
- cursor: pointer;
927
- transition: background-color var(--fd-transition-fast);
928
- }
929
-
930
- .form-autocomplete__option:hover,
931
- .form-autocomplete__option--highlighted {
932
- background-color: var(--fd-muted);
933
- }
934
-
935
- .form-autocomplete__option--selected {
936
- background-color: var(--fd-primary-muted);
937
- }
938
-
939
- .form-autocomplete__option--highlighted.form-autocomplete__option--selected {
940
- background-color: var(--fd-primary-muted);
941
- }
942
-
943
- .form-autocomplete__option-label {
944
- font-size: var(--fd-text-sm);
945
- color: var(--fd-foreground);
946
- flex: 1;
947
- overflow: hidden;
948
- text-overflow: ellipsis;
949
- white-space: nowrap;
950
- }
951
-
952
- .form-autocomplete__option :global(.form-autocomplete__option-check) {
953
- width: 1rem;
954
- height: 1rem;
955
- color: var(--fd-primary);
956
- flex-shrink: 0;
957
- margin-left: var(--fd-space-xs);
958
- }
959
-
960
- /* Status messages */
961
- .form-autocomplete__status {
962
- display: flex;
963
- align-items: center;
964
- gap: var(--fd-space-xs);
965
- padding: var(--fd-space-md);
966
- font-size: var(--fd-text-sm);
967
- color: var(--fd-muted-foreground);
968
- }
969
-
970
- .form-autocomplete__status--loading {
971
- color: var(--fd-primary);
972
- }
973
-
974
- .form-autocomplete__status--error {
975
- color: var(--fd-error);
976
- flex-wrap: wrap;
977
- }
978
-
979
- .form-autocomplete__status--empty {
980
- color: var(--fd-muted-foreground);
981
- }
982
-
983
- .form-autocomplete__status :global(.form-autocomplete__status-icon) {
984
- width: 1rem;
985
- height: 1rem;
986
- flex-shrink: 0;
987
- }
988
-
989
- .form-autocomplete__status--loading :global(.form-autocomplete__status-icon) {
990
- animation: autocomplete-spin 1s linear infinite;
991
- }
992
-
993
- .form-autocomplete__retry {
994
- margin-left: auto;
995
- padding: var(--fd-space-3xs) var(--fd-space-xs);
996
- border: 1px solid var(--fd-error);
997
- border-radius: var(--fd-radius-sm);
998
- background-color: transparent;
999
- color: var(--fd-error);
1000
- font-size: var(--fd-text-xs);
1001
- font-weight: 500;
1002
- cursor: pointer;
1003
- transition: all var(--fd-transition-fast);
1004
- }
1005
-
1006
- .form-autocomplete__retry:hover {
1007
- background-color: var(--fd-error-muted);
1008
- }
746
+ .form-autocomplete {
747
+ position: relative;
748
+ width: 100%;
749
+ }
750
+
751
+ .form-autocomplete--disabled {
752
+ opacity: 0.6;
753
+ pointer-events: none;
754
+ }
755
+
756
+ /* Field container styled like a textfield */
757
+ .form-autocomplete__field {
758
+ display: flex;
759
+ flex-wrap: wrap;
760
+ align-items: flex-start;
761
+ gap: var(--fd-space-3xs);
762
+ min-height: 2.625rem;
763
+ padding: var(--fd-space-3xs) 2.5rem var(--fd-space-3xs) var(--fd-space-md);
764
+ border: 1px solid var(--fd-border);
765
+ border-radius: var(--fd-radius-lg);
766
+ font-size: var(--fd-text-sm);
767
+ font-family: inherit;
768
+ color: var(--fd-foreground);
769
+ background-color: var(--fd-muted);
770
+ transition: all var(--fd-transition-normal);
771
+ box-shadow: var(--fd-shadow-sm);
772
+ cursor: text;
773
+ position: relative;
774
+ }
775
+
776
+ .form-autocomplete__field:hover {
777
+ border-color: var(--fd-border-strong);
778
+ background-color: var(--fd-background);
779
+ }
780
+
781
+ .form-autocomplete__field--focused {
782
+ border-color: var(--fd-primary);
783
+ background-color: var(--fd-background);
784
+ box-shadow:
785
+ 0 0 0 3px rgba(59, 130, 246, 0.12),
786
+ var(--fd-shadow-sm);
787
+ }
788
+
789
+ /* Multiple mode - textarea-like styling */
790
+ .form-autocomplete--multiple .form-autocomplete__field {
791
+ min-height: 3rem;
792
+ align-content: flex-start;
793
+ }
794
+
795
+ /* Tags container */
796
+ .form-autocomplete__tags {
797
+ display: flex;
798
+ flex-wrap: wrap;
799
+ gap: var(--fd-space-3xs);
800
+ align-items: center;
801
+ }
802
+
803
+ /* Individual tag - selected item chip */
804
+ .form-autocomplete__tag {
805
+ display: inline-flex;
806
+ align-items: center;
807
+ gap: var(--fd-space-3xs);
808
+ padding: var(--fd-space-3xs) var(--fd-space-3xs) var(--fd-space-3xs)
809
+ var(--fd-space-xs);
810
+ background-color: var(--fd-primary-muted);
811
+ border: 1px solid var(--fd-primary-muted);
812
+ border-radius: var(--fd-radius-md);
813
+ font-size: 0.8125rem;
814
+ color: var(--fd-primary-hover);
815
+ line-height: 1.2;
816
+ max-width: 100%;
817
+ }
818
+
819
+ .form-autocomplete__tag-label {
820
+ overflow: hidden;
821
+ text-overflow: ellipsis;
822
+ white-space: nowrap;
823
+ max-width: 12rem;
824
+ }
825
+
826
+ .form-autocomplete__tag-remove {
827
+ display: flex;
828
+ align-items: center;
829
+ justify-content: center;
830
+ padding: 0.125rem;
831
+ margin-left: 0.125rem;
832
+ border: none;
833
+ border-radius: var(--fd-radius-sm);
834
+ background: transparent;
835
+ color: var(--fd-primary);
836
+ cursor: pointer;
837
+ transition: all var(--fd-transition-fast);
838
+ flex-shrink: 0;
839
+ }
840
+
841
+ .form-autocomplete__tag-remove:hover {
842
+ background-color: var(--fd-primary-muted);
843
+ color: var(--fd-primary-hover);
844
+ }
845
+
846
+ .form-autocomplete__tag-remove :global(svg) {
847
+ width: 0.875rem;
848
+ height: 0.875rem;
849
+ }
850
+
851
+ /* Input area */
852
+ .form-autocomplete__input-area {
853
+ flex: 1;
854
+ min-width: 4rem;
855
+ display: flex;
856
+ align-items: center;
857
+ }
858
+
859
+ .form-autocomplete__input {
860
+ width: 100%;
861
+ padding: var(--fd-space-3xs) 0;
862
+ border: none;
863
+ outline: none;
864
+ font-size: var(--fd-text-sm);
865
+ font-family: inherit;
866
+ color: var(--fd-foreground);
867
+ background-color: transparent;
868
+ }
869
+
870
+ .form-autocomplete__input::placeholder {
871
+ color: var(--fd-muted-foreground);
872
+ }
873
+
874
+ /* Status icons */
875
+ .form-autocomplete__icons {
876
+ position: absolute;
877
+ right: var(--fd-space-xs);
878
+ top: 0.625rem;
879
+ display: flex;
880
+ align-items: center;
881
+ gap: var(--fd-space-3xs);
882
+ }
883
+
884
+ .form-autocomplete__chevron,
885
+ .form-autocomplete__spinner {
886
+ display: flex;
887
+ align-items: center;
888
+ justify-content: center;
889
+ color: var(--fd-muted-foreground);
890
+ pointer-events: none;
891
+ }
892
+
893
+ .form-autocomplete__chevron :global(svg),
894
+ .form-autocomplete__spinner :global(svg) {
895
+ width: 1rem;
896
+ height: 1rem;
897
+ }
898
+
899
+ .form-autocomplete__spinner {
900
+ animation: autocomplete-spin 1s linear infinite;
901
+ }
902
+
903
+ @keyframes autocomplete-spin {
904
+ from {
905
+ transform: rotate(0deg);
906
+ }
907
+ to {
908
+ transform: rotate(360deg);
909
+ }
910
+ }
911
+
912
+ .form-autocomplete__clear {
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ padding: var(--fd-space-3xs);
917
+ border: none;
918
+ border-radius: var(--fd-radius-sm);
919
+ background: transparent;
920
+ color: var(--fd-muted-foreground);
921
+ cursor: pointer;
922
+ transition: all var(--fd-transition-fast);
923
+ }
924
+
925
+ .form-autocomplete__clear:hover {
926
+ background-color: var(--fd-muted);
927
+ color: var(--fd-foreground);
928
+ }
929
+
930
+ .form-autocomplete__clear :global(svg) {
931
+ width: 1rem;
932
+ height: 1rem;
933
+ }
934
+
935
+ /* Popover container - renders in top layer via Popover API */
936
+ .form-autocomplete__popover {
937
+ position: fixed;
938
+ margin: 0;
939
+ padding: 0;
940
+ border: none;
941
+ background: transparent;
942
+ overflow: visible;
943
+ }
944
+
945
+ /* Reset default popover styles */
946
+ .form-autocomplete__popover:popover-open {
947
+ display: block;
948
+ }
949
+
950
+ /* Dropdown listbox inside popover */
951
+ .form-autocomplete__listbox {
952
+ margin: 0;
953
+ padding: var(--fd-space-3xs);
954
+ list-style: none;
955
+ background-color: var(--fd-background);
956
+ border: 1px solid var(--fd-border);
957
+ border-radius: var(--fd-radius-lg);
958
+ box-shadow: var(--fd-shadow-lg);
959
+ overflow-y: auto;
960
+ max-height: inherit;
961
+ }
962
+
963
+ .form-autocomplete__option {
964
+ display: flex;
965
+ align-items: center;
966
+ justify-content: space-between;
967
+ padding: var(--fd-space-xs) var(--fd-space-md);
968
+ border-radius: var(--fd-radius-md);
969
+ cursor: pointer;
970
+ transition: background-color var(--fd-transition-fast);
971
+ }
972
+
973
+ .form-autocomplete__option:hover,
974
+ .form-autocomplete__option--highlighted {
975
+ background-color: var(--fd-muted);
976
+ }
977
+
978
+ .form-autocomplete__option--selected {
979
+ background-color: var(--fd-primary-muted);
980
+ }
981
+
982
+ .form-autocomplete__option--highlighted.form-autocomplete__option--selected {
983
+ background-color: var(--fd-primary-muted);
984
+ }
985
+
986
+ .form-autocomplete__option-label {
987
+ font-size: var(--fd-text-sm);
988
+ color: var(--fd-foreground);
989
+ flex: 1;
990
+ overflow: hidden;
991
+ text-overflow: ellipsis;
992
+ white-space: nowrap;
993
+ }
994
+
995
+ .form-autocomplete__option :global(.form-autocomplete__option-check) {
996
+ width: 1rem;
997
+ height: 1rem;
998
+ color: var(--fd-primary);
999
+ flex-shrink: 0;
1000
+ margin-left: var(--fd-space-xs);
1001
+ }
1002
+
1003
+ /* Status messages */
1004
+ .form-autocomplete__status {
1005
+ display: flex;
1006
+ align-items: center;
1007
+ gap: var(--fd-space-xs);
1008
+ padding: var(--fd-space-md);
1009
+ font-size: var(--fd-text-sm);
1010
+ color: var(--fd-muted-foreground);
1011
+ }
1012
+
1013
+ .form-autocomplete__status--loading {
1014
+ color: var(--fd-primary);
1015
+ }
1016
+
1017
+ .form-autocomplete__status--error {
1018
+ color: var(--fd-error);
1019
+ flex-wrap: wrap;
1020
+ }
1021
+
1022
+ .form-autocomplete__status--empty {
1023
+ color: var(--fd-muted-foreground);
1024
+ }
1025
+
1026
+ .form-autocomplete__status :global(.form-autocomplete__status-icon) {
1027
+ width: 1rem;
1028
+ height: 1rem;
1029
+ flex-shrink: 0;
1030
+ }
1031
+
1032
+ .form-autocomplete__status--loading :global(.form-autocomplete__status-icon) {
1033
+ animation: autocomplete-spin 1s linear infinite;
1034
+ }
1035
+
1036
+ .form-autocomplete__retry {
1037
+ margin-left: auto;
1038
+ padding: var(--fd-space-3xs) var(--fd-space-xs);
1039
+ border: 1px solid var(--fd-error);
1040
+ border-radius: var(--fd-radius-sm);
1041
+ background-color: transparent;
1042
+ color: var(--fd-error);
1043
+ font-size: var(--fd-text-xs);
1044
+ font-weight: 500;
1045
+ cursor: pointer;
1046
+ transition: all var(--fd-transition-fast);
1047
+ }
1048
+
1049
+ .form-autocomplete__retry:hover {
1050
+ background-color: var(--fd-error-muted);
1051
+ }
1009
1052
  </style>