@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
@@ -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>