@morscherlab/mint-sdk 1.2.0-beta.8 → 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 (402) hide show
  1. package/README.md +60 -0
  2. package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-CJdNlCZb.js} +24 -15
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-DCXmwKNS.js} +5 -5
  5. package/dist/ExperimentSelectorModal-DCXmwKNS.js.map +1 -0
  6. package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-BYR20I4c.js} +5 -5
  7. package/dist/SettingsModal-BYR20I4c.js.map +1 -0
  8. package/dist/{EmptyState-NBqf2_8z.js → Skeleton-BAF3CKY7.js} +111 -93
  9. package/dist/Skeleton-BAF3CKY7.js.map +1 -0
  10. package/dist/__tests__/components/DataStates.test.d.ts +1 -0
  11. package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
  12. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  14. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  15. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  16. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  17. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  18. package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
  19. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  20. package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
  21. package/dist/apiErrors.d.ts +26 -0
  22. package/dist/components/ActionMenu.vue.d.ts +1 -1
  23. package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
  24. package/dist/components/AuditTrail.vue.d.ts +13 -0
  25. package/dist/components/BaseInput.vue.d.ts +1 -1
  26. package/dist/components/BaseSelect.vue.d.ts +1 -1
  27. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  28. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  29. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  30. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  31. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  32. package/dist/components/DataFrame.vue.d.ts +14 -2
  33. package/dist/components/DatePicker.vue.d.ts +2 -2
  34. package/dist/components/DateTimePicker.vue.d.ts +3 -1
  35. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  36. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  37. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  38. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  39. package/dist/components/FilePicker.vue.d.ts +32 -0
  40. package/dist/components/FileUploader.vue.d.ts +5 -2
  41. package/dist/components/FormulaInput.vue.d.ts +1 -1
  42. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  43. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  44. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  45. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  46. package/dist/components/NumberInput.vue.d.ts +1 -1
  47. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  48. package/dist/components/ProgressBar.vue.d.ts +1 -1
  49. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  50. package/dist/components/ReagentList.vue.d.ts +29 -1
  51. package/dist/components/ResourceCard.vue.d.ts +1 -1
  52. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  53. package/dist/components/SampleSelector.drag.d.ts +13 -0
  54. package/dist/components/SampleSelector.tree.d.ts +2 -0
  55. package/dist/components/SampleSelector.vue.d.ts +34 -1
  56. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  57. package/dist/components/SecretInput.vue.d.ts +1 -1
  58. package/dist/components/SequenceInput.vue.d.ts +1 -1
  59. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
  60. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  61. package/dist/components/TagsInput.vue.d.ts +1 -1
  62. package/dist/components/TimePicker.vue.d.ts +2 -1
  63. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  64. package/dist/components/UnitInput.vue.d.ts +1 -1
  65. package/dist/components/WellPlate.geometry.d.ts +15 -0
  66. package/dist/components/WellPlate.interaction.d.ts +13 -0
  67. package/dist/components/WellPlate.vue.d.ts +30 -1
  68. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  69. package/dist/components/index.d.ts +4 -0
  70. package/dist/components/index.js +8 -8
  71. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  72. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  73. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  74. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  75. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  76. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  77. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  78. package/dist/{components-B80gV0P6.js → components-DIum5hkx.js} +5415 -2753
  79. package/dist/components-DIum5hkx.js.map +1 -0
  80. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  81. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  82. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  83. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  84. package/dist/composables/filePicker/validation.d.ts +5 -0
  85. package/dist/composables/index.d.ts +3 -0
  86. package/dist/composables/index.js +10 -10
  87. package/dist/composables/useApi.d.ts +14 -7
  88. package/dist/composables/useDropdownState.d.ts +18 -2
  89. package/dist/composables/useEventStream.d.ts +9 -0
  90. package/dist/composables/useFileBrowser.d.ts +55 -0
  91. package/dist/composables/useFilePicker.d.ts +189 -0
  92. package/dist/composables/useListReorder.d.ts +90 -0
  93. package/dist/composables/useMenuKeyboard.d.ts +2 -2
  94. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  95. package/dist/composables/usePluginConfig.d.ts +3 -1
  96. package/dist/composables/useRequestSyncState.d.ts +42 -2
  97. package/dist/composables/useScheduleDrag.d.ts +1 -0
  98. package/dist/{composables-C8mmD5ta.js → composables-mFXsTA4g.js} +307 -74
  99. package/dist/composables-mFXsTA4g.js.map +1 -0
  100. package/dist/{useApi-CDBq6z23.js → errors-BLuqMee5.js} +178 -16
  101. package/dist/errors-BLuqMee5.js.map +1 -0
  102. package/dist/{experiment-Y9Iz7KYi.js → experiment-CVDRAnqp.js} +3 -8
  103. package/dist/experiment-CVDRAnqp.js.map +1 -0
  104. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  105. package/dist/experiment-utils-CV2Wza3z.js.map +1 -0
  106. package/dist/index.d.ts +1 -0
  107. package/dist/index.js +15 -15
  108. package/dist/install.js +2 -7
  109. package/dist/install.js.map +1 -1
  110. package/dist/permissions.d.ts +42 -1
  111. package/dist/permissions.js +20 -10
  112. package/dist/permissions.js.map +1 -1
  113. package/dist/stores/index.d.ts +1 -1
  114. package/dist/stores/index.js +3 -3
  115. package/dist/stores/settings.d.ts +31 -1
  116. package/dist/styles.css +15162 -32475
  117. package/dist/templates/index.js +2 -4
  118. package/dist/{templates-CK72aKO1.js → templates-CmGfmBRA.js} +291 -85
  119. package/dist/templates-CmGfmBRA.js.map +1 -0
  120. package/dist/types/fileBrowserTypes.d.ts +77 -0
  121. package/dist/types/filePicker.d.ts +77 -0
  122. package/dist/types/index.d.ts +2 -0
  123. package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-DldpxNOP.js} +234 -1121
  124. package/dist/useControlSchema-DldpxNOP.js.map +1 -0
  125. package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-C7hRiVQf.js} +22 -6
  126. package/dist/useDropdownState-C7hRiVQf.js.map +1 -0
  127. package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-CEcIaueJ.js} +4 -4
  128. package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
  129. package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-z5L_dUIy.js} +2 -2
  130. package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
  131. package/dist/utils/color.d.ts +13 -0
  132. package/dist/utils/errors.d.ts +9 -0
  133. package/dist/{useProtocolTemplates-Dq5Pzwyd.js → validation-uwoJGEb6.js} +231 -158
  134. package/dist/validation-uwoJGEb6.js.map +1 -0
  135. package/package.json +1 -1
  136. package/src/__tests__/apiErrors.test.ts +81 -0
  137. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  138. package/src/__tests__/components/DataFrame.test.ts +183 -11
  139. package/src/__tests__/components/DataStates.test.ts +176 -0
  140. package/src/__tests__/components/DatePicker.test.ts +76 -0
  141. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  142. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  143. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  144. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  145. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  146. package/src/__tests__/components/FilePicker.test.ts +487 -0
  147. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  148. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  149. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  150. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  151. package/src/__tests__/components/RackEditor.test.ts +35 -0
  152. package/src/__tests__/components/ReagentList.test.ts +64 -2
  153. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  154. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  155. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  156. package/src/__tests__/components/SegmentedControl.test.ts +107 -0
  157. package/src/__tests__/components/TimePicker.test.ts +61 -0
  158. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  159. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  160. package/src/__tests__/composables/useApi.test.ts +92 -9
  161. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  162. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  163. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  164. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  165. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  166. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  167. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  168. package/src/__tests__/stores/settings.test.ts +71 -1
  169. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  170. package/src/__tests__/utils/permissions.test.ts +68 -0
  171. package/src/apiErrors.ts +85 -0
  172. package/src/components/ActionMenu.vue +45 -88
  173. package/src/components/AlertBox.vue +0 -4
  174. package/src/components/AppAvatarMenu.vue +2 -14
  175. package/src/components/AppLayout.vue +0 -4
  176. package/src/components/AppPluginSwitcher.story.vue +87 -0
  177. package/src/components/AppPluginSwitcher.vue +2 -5
  178. package/src/components/AppSidebar.vue +0 -4
  179. package/src/components/AppToastContainer.vue +6 -8
  180. package/src/components/AppTopBar.vue +0 -4
  181. package/src/components/ArtifactSaveDialog.vue +0 -4
  182. package/src/components/ArtifactSelectorModal.vue +0 -4
  183. package/src/components/AuditTrail.story.vue +18 -0
  184. package/src/components/AuditTrail.vue +27 -7
  185. package/src/components/AutoGroupModal.vue +0 -4
  186. package/src/components/Avatar.vue +0 -4
  187. package/src/components/BaseButton.vue +0 -4
  188. package/src/components/BaseCheckbox.vue +0 -4
  189. package/src/components/BaseInput.vue +0 -4
  190. package/src/components/BaseModal.vue +0 -4
  191. package/src/components/BasePill.vue +0 -4
  192. package/src/components/BaseRadioGroup.vue +0 -4
  193. package/src/components/BaseSelect.vue +0 -4
  194. package/src/components/BaseSlider.vue +0 -4
  195. package/src/components/BaseTabs.vue +0 -4
  196. package/src/components/BaseTextarea.vue +0 -4
  197. package/src/components/BaseToggle.vue +0 -4
  198. package/src/components/BatchProgressList.story.vue +18 -0
  199. package/src/components/BatchProgressList.vue +32 -4
  200. package/src/components/Breadcrumb.vue +0 -4
  201. package/src/components/Calendar.vue +0 -4
  202. package/src/components/ChartContainer.story.vue +1 -1
  203. package/src/components/ChartContainer.vue +0 -4
  204. package/src/components/ChemicalFormula.vue +0 -4
  205. package/src/components/CollapsibleCard.vue +0 -4
  206. package/src/components/ColorSlider.vue +0 -4
  207. package/src/components/ConcentrationInput.vue +0 -4
  208. package/src/components/ConfirmDialog.vue +0 -4
  209. package/src/components/DataFrame.story.vue +14 -0
  210. package/src/components/DataFrame.vue +97 -23
  211. package/src/components/DatePicker.vue +73 -27
  212. package/src/components/DateTimePicker.vue +45 -17
  213. package/src/components/Divider.vue +0 -4
  214. package/src/components/DoseCalculator.vue +0 -4
  215. package/src/components/DropdownButton.vue +2 -14
  216. package/src/components/EmptyState.vue +0 -4
  217. package/src/components/ExperimentCodeBadge.vue +0 -4
  218. package/src/components/ExperimentDataViewer.story.vue +3 -0
  219. package/src/components/ExperimentDataViewer.vue +33 -13
  220. package/src/components/ExperimentPopover.vue +12 -7
  221. package/src/components/ExperimentSelectorModal.vue +0 -4
  222. package/src/components/ExperimentTimeline.story.vue +18 -0
  223. package/src/components/ExperimentTimeline.vue +81 -75
  224. package/src/components/FileBrowserModal.story.vue +203 -0
  225. package/src/components/FileBrowserModal.vue +384 -0
  226. package/src/components/FilePicker.story.vue +44 -0
  227. package/src/components/FilePicker.vue +284 -0
  228. package/src/components/FileUploader.vue +32 -18
  229. package/src/components/FitPanel.vue +0 -4
  230. package/src/components/FormActions.story.vue +96 -0
  231. package/src/components/FormActions.vue +0 -4
  232. package/src/components/FormBuilder.vue +0 -4
  233. package/src/components/FormField.vue +0 -4
  234. package/src/components/FormulaInput.vue +0 -4
  235. package/src/components/GeneratedPathInput.story.vue +68 -0
  236. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  237. package/src/components/GeneratedPlotlyResult.vue +34 -69
  238. package/src/components/GeneratedPluginView.story.vue +145 -0
  239. package/src/components/GeneratedPluginView.vue +10 -10
  240. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  241. package/src/components/GeneratedResultRenderer.vue +4 -9
  242. package/src/components/GroupAssigner.vue +67 -5
  243. package/src/components/IconButton.vue +0 -4
  244. package/src/components/InstrumentAlertLog.vue +0 -4
  245. package/src/components/InstrumentStateBadge.vue +0 -4
  246. package/src/components/InstrumentStatusCard.vue +0 -4
  247. package/src/components/JobsStatusTray.vue +0 -4
  248. package/src/components/LcmsSequenceTable.vue +81 -50
  249. package/src/components/LoadingSpinner.vue +0 -4
  250. package/src/components/MobileSupportGate.vue +0 -4
  251. package/src/components/MoleculeInput.vue +0 -4
  252. package/src/components/MultiSelect.vue +0 -4
  253. package/src/components/NumberInput.vue +0 -4
  254. package/src/components/PlateMapEditor.vue +3 -14
  255. package/src/components/PlotlyChart.story.vue +61 -0
  256. package/src/components/PlotlyChart.vue +233 -0
  257. package/src/components/PluginIcon.vue +0 -4
  258. package/src/components/ProgressBar.vue +0 -4
  259. package/src/components/ProtocolStepEditor.vue +2 -4
  260. package/src/components/RackEditor.vue +51 -70
  261. package/src/components/ReagentEditor.vue +0 -4
  262. package/src/components/ReagentList.story.vue +18 -0
  263. package/src/components/ReagentList.vue +127 -82
  264. package/src/components/ResourceCard.vue +0 -4
  265. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  266. package/src/components/RuntimeUpdateNotice.vue +4 -4
  267. package/src/components/SampleHierarchyTree.story.vue +18 -0
  268. package/src/components/SampleHierarchyTree.vue +32 -9
  269. package/src/components/SampleLegend.vue +2 -8
  270. package/src/components/SampleSelector.drag.ts +51 -0
  271. package/src/components/SampleSelector.story.vue +6 -0
  272. package/src/components/SampleSelector.tree.ts +2 -0
  273. package/src/components/SampleSelector.vue +49 -10
  274. package/src/components/SampleSelectorGroupNode.vue +19 -0
  275. package/src/components/ScheduleCalendar.story.vue +6 -0
  276. package/src/components/ScheduleCalendar.vue +82 -7
  277. package/src/components/ScientificNumber.vue +0 -4
  278. package/src/components/SectionCard.vue +0 -4
  279. package/src/components/SegmentedControl.vue +50 -7
  280. package/src/components/SequenceInput.vue +0 -4
  281. package/src/components/SequenceProgressBar.story.vue +110 -0
  282. package/src/components/SequenceProgressBar.vue +0 -4
  283. package/src/components/SettingsModal.vue +0 -4
  284. package/src/components/Skeleton.vue +0 -4
  285. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  286. package/src/components/SmartGroupManual.vue +0 -4
  287. package/src/components/StatusIndicator.vue +0 -4
  288. package/src/components/StepWizard.vue +0 -4
  289. package/src/components/TagsInput.vue +4 -6
  290. package/src/components/ThemeToggle.vue +0 -4
  291. package/src/components/TimePicker.vue +36 -7
  292. package/src/components/TimeRangeInput.vue +0 -4
  293. package/src/components/Tooltip.story.vue +3 -3
  294. package/src/components/Tooltip.vue +0 -4
  295. package/src/components/UnitInput.vue +0 -4
  296. package/src/components/WellPlate.geometry.ts +60 -1
  297. package/src/components/WellPlate.interaction.ts +44 -0
  298. package/src/components/WellPlate.story.vue +8 -0
  299. package/src/components/WellPlate.vue +53 -7
  300. package/src/components/fileBrowserPresentation.ts +39 -0
  301. package/src/components/index.ts +5 -0
  302. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  303. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  304. package/src/components/internal/DataStateInternal.vue +74 -0
  305. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  306. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  307. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  308. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  309. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  310. package/src/components/internal/FormSectionRenderer.vue +0 -4
  311. package/src/components/internal/WellEditPopupInternal.vue +0 -4
  312. package/src/composables/controlSchemaTypes.ts +34 -0
  313. package/src/composables/filePicker/localFiles.ts +58 -0
  314. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  315. package/src/composables/filePicker/usePickerSelection.ts +259 -0
  316. package/src/composables/filePicker/validation.ts +41 -0
  317. package/src/composables/index.ts +17 -0
  318. package/src/composables/useApi.ts +57 -13
  319. package/src/composables/useDropdownState.ts +35 -5
  320. package/src/composables/useEventStream.ts +14 -2
  321. package/src/composables/useFileBrowser.ts +314 -0
  322. package/src/composables/useFileImport.ts +2 -7
  323. package/src/composables/useFilePicker.ts +366 -0
  324. package/src/composables/useGeneratedAnalysis.ts +18 -26
  325. package/src/composables/useListReorder.story.vue +143 -0
  326. package/src/composables/useListReorder.ts +242 -0
  327. package/src/composables/useMenuKeyboard.ts +2 -2
  328. package/src/composables/useOptimisticMutation.ts +1 -15
  329. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  330. package/src/composables/usePluginClient.ts +3 -0
  331. package/src/composables/usePluginConfig.ts +3 -1
  332. package/src/composables/useRackEditor.ts +2 -11
  333. package/src/composables/useRequestSyncState.ts +98 -28
  334. package/src/composables/useResourceCrud.ts +1 -15
  335. package/src/composables/useScheduleDrag.ts +37 -0
  336. package/src/composables/useWellPainting.ts +5 -29
  337. package/src/composables/useWellPlateEditor.ts +1 -11
  338. package/src/index.ts +3 -0
  339. package/src/permissions.ts +48 -9
  340. package/src/stores/experiment.ts +2 -7
  341. package/src/stores/index.ts +9 -1
  342. package/src/stores/settings.ts +93 -1
  343. package/src/styles/components/audit-trail.css +5 -5
  344. package/src/styles/components/batch-progress-list.css +7 -0
  345. package/src/styles/components/color-slider.css +2 -2
  346. package/src/styles/components/data-state.css +30 -0
  347. package/src/styles/components/dataframe.css +29 -35
  348. package/src/styles/components/experiment-data-viewer.css +4 -4
  349. package/src/styles/components/experiment-timeline.css +3 -30
  350. package/src/styles/components/file-browser-modal.css +489 -0
  351. package/src/styles/components/file-picker.css +502 -0
  352. package/src/styles/components/form-builder.css +5 -5
  353. package/src/styles/components/icon-button.css +9 -0
  354. package/src/styles/components/lcms-sequence-table.css +16 -1
  355. package/src/styles/components/multi-select.css +1 -1
  356. package/src/styles/components/reagent-list.css +38 -31
  357. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  358. package/src/styles/components/sample-selector.css +9 -2
  359. package/src/styles/components/schedule-calendar.css +8 -0
  360. package/src/styles/components/scientific-number.css +1 -1
  361. package/src/styles/components/well-plate.css +8 -0
  362. package/src/styles/index.css +11 -0
  363. package/src/styles/variables.css +29 -0
  364. package/src/templates/builderPresetControls.ts +1 -1
  365. package/src/templates/componentPlateHelpers.ts +3 -1
  366. package/src/templates/doseResponseAdapters.ts +2 -13
  367. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  368. package/src/templates/plateMapBuilder.ts +2 -2
  369. package/src/templates/qpcrPlateBuilder.ts +1 -1
  370. package/src/templates/templateValidators.ts +1 -1
  371. package/src/templates/timeCourseBuilder.ts +2 -2
  372. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  373. package/src/types/fileBrowserTypes.ts +85 -0
  374. package/src/types/filePicker.ts +72 -0
  375. package/src/types/index.ts +14 -0
  376. package/src/utils/color.ts +51 -0
  377. package/src/utils/errors.ts +29 -0
  378. package/dist/EmptyState-NBqf2_8z.js.map +0 -1
  379. package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
  380. package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
  381. package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
  382. package/dist/components-B80gV0P6.js.map +0 -1
  383. package/dist/composables/autoGroupConstants.d.ts +0 -1
  384. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  385. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  386. package/dist/composables-C8mmD5ta.js.map +0 -1
  387. package/dist/experiment-Y9Iz7KYi.js.map +0 -1
  388. package/dist/experiment-utils-DV3DrvE_.js.map +0 -1
  389. package/dist/templates/builderDefaults.d.ts +0 -1
  390. package/dist/templates/plateGeometry.d.ts +0 -4
  391. package/dist/templates-CK72aKO1.js.map +0 -1
  392. package/dist/useApi-CDBq6z23.js.map +0 -1
  393. package/dist/useControlSchema-_OVhavKq.js.map +0 -1
  394. package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
  395. package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
  396. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  397. package/src/composables/autoGroupConstants.ts +0 -4
  398. package/src/composables/autoGroupGrouping.ts +0 -148
  399. package/src/composables/useAutoGroupInputSources.ts +0 -147
  400. package/src/templates/builderDefaults.ts +0 -11
  401. package/src/templates/plateGeometry.ts +0 -62
  402. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → apiErrors.test.d.ts} +0 -0
@@ -0,0 +1,259 @@
1
+ import { computed, ref, shallowRef, watch } from 'vue'
2
+ import type {
3
+ PickerCapability,
4
+ PickerExcluded,
5
+ PickerFile,
6
+ PickerNode,
7
+ PickerSelection,
8
+ PickerSelectionMode,
9
+ } from '../../types/filePicker'
10
+ import type { usePickerNavigation } from './usePickerNavigation'
11
+ import { pickerExclusion } from './validation'
12
+
13
+ type Navigation = ReturnType<typeof usePickerNavigation>
14
+ export function usePickerSelection(
15
+ nav: Navigation,
16
+ mode: () => PickerSelectionMode,
17
+ capability: () => PickerCapability | undefined,
18
+ skipBlanks: () => boolean,
19
+ ) {
20
+ const keys = ref(new Set<string>())
21
+ const dropped = ref(new Set<string>())
22
+ const focusPath = shallowRef('')
23
+ const resolving = shallowRef(false)
24
+ const resolutionError = shallowRef('')
25
+ let anchor = ''
26
+ let generation = 0
27
+
28
+ function ancestorSelected(node: PickerNode, candidates = keys.value): boolean {
29
+ const seen = new Set<string>()
30
+ let parent = node.parentPath
31
+ while (parent && !seen.has(parent)) {
32
+ if (candidates.has(parent)) return true
33
+ seen.add(parent)
34
+ parent = nav.nodes.value.get(parent)?.parentPath
35
+ }
36
+ return false
37
+ }
38
+ function exclusion(node: PickerNode) {
39
+ if (!nav.isAllowed(node))
40
+ return { path: node.path, name: node.name, reason: 'forbidden' as const }
41
+ const own = pickerExclusion(node, capability(), skipBlanks())
42
+ if (own) return own
43
+ const seen = new Set<string>()
44
+ let parent = node.parentPath
45
+ while (parent && !seen.has(parent)) {
46
+ seen.add(parent)
47
+ const folder = nav.nodes.value.get(parent)
48
+ if (!folder) break
49
+ const inherited = pickerExclusion(folder, capability(), skipBlanks())
50
+ if (inherited) return { ...inherited, path: node.path, name: node.name }
51
+ parent = folder.parentPath
52
+ }
53
+ return null
54
+ }
55
+ function selected(node: PickerNode): boolean {
56
+ return (
57
+ !exclusion(node) &&
58
+ !dropped.value.has(node.path) &&
59
+ !ancestorSelected(node, dropped.value) &&
60
+ (keys.value.has(node.path) || (mode() === 'folder+files' && ancestorSelected(node)))
61
+ )
62
+ }
63
+ function selectable(node: PickerNode): boolean {
64
+ if (exclusion(node)) return false
65
+ return node.kind === 'folder'
66
+ ? mode() === 'folder' || mode() === 'folder+files'
67
+ : mode() !== 'folder'
68
+ }
69
+ function select(
70
+ node: PickerNode,
71
+ modifiers: { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; toggle?: boolean } = {},
72
+ ) {
73
+ if (exclusion(node)) return
74
+ focusPath.value = node.path
75
+ if (!selectable(node)) return
76
+ const multi = mode() === 'multi-file' || mode() === 'folder+files'
77
+ // Multi modes behave like a checklist: a plain click (or Space) adds or
78
+ // removes one row, Shift extends a range, Ctrl/Cmd is accepted for habit.
79
+ const additive = multi && (modifiers.metaKey || modifiers.ctrlKey || modifiers.toggle)
80
+ if (multi && modifiers.shiftKey && anchor) {
81
+ const rows = nav.rows.value.map((row) => row.node)
82
+ const from = rows.findIndex((item) => item.path === anchor)
83
+ const to = rows.findIndex((item) => item.path === node.path)
84
+ if (from >= 0 && to >= 0) {
85
+ keys.value = new Set([
86
+ ...(additive ? keys.value : []),
87
+ ...rows
88
+ .slice(Math.min(from, to), Math.max(from, to) + 1)
89
+ .filter(selectable)
90
+ .map((item) => item.path),
91
+ ])
92
+ dropped.value.clear()
93
+ return
94
+ }
95
+ }
96
+ if (additive || (modifiers.toggle && (selected(node) || dropped.value.has(node.path)))) {
97
+ if (mode() === 'folder+files' && ancestorSelected(node)) {
98
+ if (dropped.value.has(node.path)) dropped.value.delete(node.path)
99
+ else dropped.value.add(node.path)
100
+ } else if (keys.value.has(node.path)) keys.value.delete(node.path)
101
+ else {
102
+ keys.value.add(node.path)
103
+ dropped.value.delete(node.path)
104
+ }
105
+ } else {
106
+ keys.value = new Set([node.path])
107
+ dropped.value.clear()
108
+ }
109
+ anchor = node.path
110
+ }
111
+ function reset(selection?: PickerSelection | null) {
112
+ ++generation
113
+ keys.value.clear()
114
+ dropped.value.clear()
115
+ resolving.value = false
116
+ resolutionError.value = ''
117
+ focusPath.value = ''
118
+ if (selection?.source === 'server') {
119
+ const files =
120
+ mode() === 'folder'
121
+ ? []
122
+ : mode() === 'single-file'
123
+ ? selection.files.slice(0, 1)
124
+ : selection.files
125
+ const folders =
126
+ mode() === 'folder'
127
+ ? selection.folders.slice(0, 1)
128
+ : mode() === 'folder+files'
129
+ ? selection.folders
130
+ : []
131
+ nav.register(files)
132
+ folders.forEach((path) => {
133
+ if (!nav.nodes.value.has(path))
134
+ nav.register([
135
+ { kind: 'folder', path, name: 'Selected folder', parentName: 'Selected location' },
136
+ ])
137
+ keys.value.add(path)
138
+ })
139
+ files.forEach((file) => keys.value.add(file.path))
140
+ selection.excluded
141
+ .filter((item) => item.reason === 'deselected')
142
+ .forEach((item) => dropped.value.add(item.path))
143
+ }
144
+ }
145
+ const roots = computed(() =>
146
+ [...keys.value]
147
+ .map((key) => nav.nodes.value.get(key))
148
+ .filter((node): node is PickerNode => !!node && !ancestorSelected(node) && nav.isAllowed(node)),
149
+ )
150
+ const chosen = computed(() => {
151
+ const files: PickerFile[] = []
152
+ const excluded: PickerExcluded[] = []
153
+ for (const node of nav.nodes.value.values()) {
154
+ if (!nav.isAllowed(node)) continue
155
+ if (!keys.value.has(node.path) && !ancestorSelected(node)) continue
156
+ const reason =
157
+ exclusion(node) ??
158
+ (dropped.value.has(node.path)
159
+ ? { path: node.path, name: node.name, reason: 'deselected' as const }
160
+ : null)
161
+ // A dropped folder excludes its descendants as well.
162
+ const droppedByParent = ancestorSelected(node, dropped.value)
163
+ if (reason || droppedByParent) {
164
+ if (node.kind === 'file' || reason?.reason === 'unreadable')
165
+ excluded.push(reason ?? { path: node.path, name: node.name, reason: 'deselected' })
166
+ } else if (node.kind === 'file') files.push(node)
167
+ }
168
+ return { files, excluded, bytes: files.reduce((total, node) => total + (node.bytes ?? 0), 0) }
169
+ })
170
+ async function resolve() {
171
+ const token = ++generation
172
+ resolving.value = true
173
+ resolutionError.value = ''
174
+ let count = 0
175
+ const seen = new Set<string>()
176
+ async function walk(node: PickerNode, ancestors: string[] = []): Promise<PickerFile[]> {
177
+ if (token !== generation) throw new DOMException('Cancelled', 'AbortError')
178
+ if (!nav.isAllowed(node)) return []
179
+ if (node.kind === 'file') return [node]
180
+ if (node.unavailableReason) return []
181
+ if (ancestors.includes(node.path))
182
+ throw new Error('The adapter returned a circular folder tree.')
183
+ if (seen.has(node.path)) return []
184
+ seen.add(node.path)
185
+ if (++count > 500 || ancestors.length > 30)
186
+ throw new Error('This selection is too large to validate. Choose a smaller folder.')
187
+ const children = await nav.loadChildren(node.path, ancestors)
188
+ const files: PickerFile[] = []
189
+ for (const child of children) files.push(...(await walk(child, [...ancestors, node.path])))
190
+ if (token !== generation) throw new DOMException('Cancelled', 'AbortError')
191
+ const formats: Record<string, number> = {}
192
+ files.forEach((file) => {
193
+ formats[file.format] = (formats[file.format] ?? 0) + 1
194
+ })
195
+ nav.register([
196
+ {
197
+ ...node,
198
+ fileCount: files.length,
199
+ bytes: files.reduce((sum, file) => sum + (file.bytes ?? 0), 0),
200
+ formats,
201
+ },
202
+ ])
203
+ return files
204
+ }
205
+ try {
206
+ for (const node of roots.value) await walk(node)
207
+ if (token === generation) {
208
+ // Resolved descendants inherit their folder, including restored selections.
209
+ for (const key of [...keys.value]) {
210
+ const node = nav.nodes.value.get(key)
211
+ if (node && ancestorSelected(node)) keys.value.delete(key)
212
+ }
213
+ }
214
+ } catch (cause) {
215
+ if (token === generation)
216
+ resolutionError.value =
217
+ cause instanceof Error ? cause.message : 'Could not validate the selection.'
218
+ } finally {
219
+ if (token === generation) resolving.value = false
220
+ }
221
+ }
222
+ const focused = computed(() => {
223
+ const node = nav.nodes.value.get(focusPath.value)
224
+ return node && nav.isAllowed(node) ? node : roots.value[roots.value.length - 1] ?? null
225
+ })
226
+ watch(
227
+ () => new Map([...nav.nodes.value].map(([path, node]) => [path, nav.isAllowed(node)])),
228
+ (allowed, previous) => {
229
+ if (keys.value.size && [...allowed].some(([path, value]) =>
230
+ previous.has(path) && value !== previous.get(path),
231
+ )) void resolve()
232
+ },
233
+ )
234
+ function folderProgress(node: PickerNode): string {
235
+ if (node.kind !== 'folder' || node.fileCount == null) return ''
236
+ const allowed = chosen.value.files.filter((file) =>
237
+ ancestorSelected(file, new Set([node.path])),
238
+ ).length
239
+ return `${selected(node) ? allowed : 0} / ${node.fileCount}`
240
+ }
241
+ return {
242
+ keys,
243
+ dropped,
244
+ focusPath,
245
+ resolving,
246
+ resolutionError,
247
+ roots,
248
+ chosen,
249
+ focused,
250
+ select,
251
+ selected,
252
+ selectable,
253
+ exclusion,
254
+ reset,
255
+ resolve,
256
+ folderProgress,
257
+ ancestorSelected,
258
+ }
259
+ }
@@ -0,0 +1,41 @@
1
+ import type { PickerCapability, PickerExcluded, PickerNode } from '../../types/filePicker'
2
+
3
+ export function normalizePickerFormat(format: string): string {
4
+ return format.trim().replace(/^\./, '').toLowerCase()
5
+ }
6
+
7
+ export function pickerExclusion(
8
+ node: PickerNode,
9
+ capability?: PickerCapability,
10
+ skipBlanks = false,
11
+ ): PickerExcluded | null {
12
+ const base = { path: node.path, name: node.name }
13
+ if (node.unavailableReason) return { ...base, reason: 'unreadable' }
14
+ if (skipBlanks && capability?.isBlank?.(node)) return { ...base, reason: 'blank' }
15
+ if (node.kind === 'folder') return null
16
+ const formats = capability?.formats.map(normalizePickerFormat) ?? []
17
+ if (formats.length && !formats.some((format) => node.name.toLowerCase().endsWith(`.${format}`))) {
18
+ return { ...base, reason: 'unsupported' }
19
+ }
20
+ if (capability?.maxBytes != null && node.bytes != null && node.bytes > capability.maxBytes) {
21
+ return { ...base, reason: 'tooLarge' }
22
+ }
23
+ return null
24
+ }
25
+
26
+ export function pickerReason(excluded: PickerExcluded | null, node?: PickerNode): string {
27
+ if (!excluded) return ''
28
+ return {
29
+ unsupported: 'This format is not supported by this source.',
30
+ tooLarge: 'This file exceeds the size limit for this source.',
31
+ blank: 'Skipped by the blank-file rule.',
32
+ unreadable: node?.unavailableReason || 'This item cannot be read.',
33
+ deselected: 'Removed from the selection.',
34
+ forbidden: 'This item is not permitted by the system filter.',
35
+ }[excluded.reason]
36
+ }
37
+
38
+ export function pickerFormat(name: string): string {
39
+ const compound = /\.(mzml|mzxml)\.gz$/i.exec(name)
40
+ return compound ? `${compound[1]}.gz` : name.includes('.') ? name.split('.').pop()! : ''
41
+ }
@@ -174,6 +174,11 @@ export {
174
174
  type UseAnalysisArtifactsOptions,
175
175
  type UseAnalysisArtifactsReturn,
176
176
  } from './useAnalysisArtifacts'
177
+ export {
178
+ useFileBrowser,
179
+ type UseFileBrowserOptions,
180
+ type UseFileBrowserReturn,
181
+ } from './useFileBrowser'
177
182
  export {
178
183
  buildAnalysisArtifactTargetQuery,
179
184
  parseAnalysisArtifactTarget,
@@ -549,8 +554,20 @@ export {
549
554
  type UseRovingFocusOptions,
550
555
  type UseRovingFocusReturn,
551
556
  } from './useRovingFocus'
557
+ export {
558
+ reorderList,
559
+ useListReorder,
560
+ useReorderAnnouncer,
561
+ REORDER_ANNOUNCER_STYLE,
562
+ type ListReorderMove,
563
+ type UseListReorderOptions,
564
+ type UseListReorderReturn,
565
+ type UseReorderAnnouncerReturn,
566
+ } from './useListReorder'
552
567
  export {
553
568
  useMenuKeyboard,
554
569
  type UseMenuKeyboardOptions,
555
570
  type UseMenuKeyboardReturn,
556
571
  } from './useMenuKeyboard'
572
+
573
+ export { usePlatformFilePickerAdapter, encodePlatformPickerPath, decodePlatformPickerPath } from './usePlatformFilePickerAdapter'
@@ -1,6 +1,7 @@
1
1
  import axios, { type AxiosInstance, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
2
2
  import { useSettingsStore } from '../stores/settings'
3
3
  import { useAuthStore } from '../stores/auth'
4
+ import { MintApiError, mintApiErrorFromResponse } from '../apiErrors'
4
5
 
5
6
  let apiClientInstance: AxiosInstance | null = null
6
7
  let interceptorAttached = false
@@ -8,11 +9,13 @@ let interceptorAttached = false
8
9
  interface MintAxiosRequestConfig extends AxiosRequestConfig {
9
10
  _mintSkipAuth?: boolean
10
11
  _mintAuthToken?: string
12
+ _mintTypedErrors?: boolean
11
13
  }
12
14
 
13
15
  interface MintInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
14
16
  _mintSkipAuth?: boolean
15
17
  _mintAuthToken?: string
18
+ _mintTypedErrors?: boolean
16
19
  }
17
20
 
18
21
  type MutableHeaders = Record<string, unknown> & {
@@ -103,14 +106,27 @@ function getAuthorizationHeader(headers: AxiosRequestConfig['headers']): unknown
103
106
  return key ? bag[key] : undefined
104
107
  }
105
108
 
106
- /** Raised when an authenticated SDK request is rejected because the login is no longer valid. */
107
- export class AuthenticationRequiredError extends Error {
108
- readonly code = 'AUTHENTICATION_REQUIRED'
109
- readonly status = 401
110
-
111
- constructor(readonly cause?: unknown) {
112
- super('Authentication required. Please sign in again.')
113
- this.name = 'AuthenticationRequiredError'
109
+ /**
110
+ * Raised when an authenticated SDK request is rejected because the login is no
111
+ * longer valid. It is also a `MintApiError` (status 401), so typed-error callers
112
+ * and `instanceof AuthenticationRequiredError` callers both recognise it.
113
+ */
114
+ export class AuthenticationRequiredError extends MintApiError {
115
+ override readonly name: string = 'AuthenticationRequiredError'
116
+ declare readonly code: 'AUTHENTICATION_REQUIRED'
117
+ declare readonly status: 401
118
+
119
+ constructor(cause?: unknown, envelope?: MintApiError) {
120
+ super(
121
+ 'Authentication required. Please sign in again.',
122
+ 'AUTHENTICATION_REQUIRED',
123
+ 401,
124
+ envelope?.requestId ?? null,
125
+ envelope?.details ?? null,
126
+ envelope?.detail,
127
+ envelope?.body ?? null,
128
+ cause,
129
+ )
114
130
  }
115
131
  }
116
132
 
@@ -118,12 +134,13 @@ export class AuthenticationRequiredError extends Error {
118
134
  export function authenticationRequiredError(
119
135
  cause: unknown,
120
136
  rejectedToken?: string,
137
+ envelope?: MintApiError,
121
138
  ): AuthenticationRequiredError {
122
139
  const currentAuthStore = useAuthStore()
123
140
  if (rejectedToken && currentAuthStore.token === rejectedToken) {
124
141
  currentAuthStore.clearToken()
125
142
  }
126
- return new AuthenticationRequiredError(cause)
143
+ return new AuthenticationRequiredError(cause, envelope)
127
144
  }
128
145
 
129
146
  function getApiClient(): AxiosInstance {
@@ -141,6 +158,8 @@ export interface ApiClientOptions {
141
158
  baseUrl?: string
142
159
  timeout?: number
143
160
  withAuth?: boolean
161
+ /** Normalize failed responses into MintApiError. Generated clients enable this by default. */
162
+ typedErrors?: boolean
144
163
  }
145
164
 
146
165
  export interface UseApiReturn {
@@ -187,11 +206,35 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
187
206
  })
188
207
  apiClient.interceptors.response.use(
189
208
  (response) => response,
190
- (error: unknown) => {
191
- if (axios.isAxiosError(error) && error.response?.status === 401) {
209
+ async (error: unknown) => {
210
+ if (axios.isAxiosError(error) && error.response) {
192
211
  const request = error.config as MintInternalAxiosRequestConfig | undefined
193
- if (request?._mintSkipAuth !== true) {
194
- return Promise.reject(authenticationRequiredError(error, request?._mintAuthToken))
212
+ if (request?._mintTypedErrors) {
213
+ let payload: unknown = error.response.data
214
+ if (payload instanceof Blob) {
215
+ try {
216
+ payload = JSON.parse(await payload.text())
217
+ } catch {
218
+ // Keep non-JSON download errors available as the original body.
219
+ }
220
+ }
221
+ const typed = mintApiErrorFromResponse(
222
+ error.response.status,
223
+ payload,
224
+ {
225
+ requestId: String(error.response.headers?.['x-request-id'] ?? '') || null,
226
+ cause: error,
227
+ },
228
+ )
229
+ if (error.response.status === 401 && request._mintSkipAuth !== true) {
230
+ return Promise.reject(authenticationRequiredError(error, request._mintAuthToken, typed))
231
+ }
232
+ return Promise.reject(typed)
233
+ }
234
+ if (error.response.status === 401) {
235
+ if (request?._mintSkipAuth !== true) {
236
+ return Promise.reject(authenticationRequiredError(error, request?._mintAuthToken))
237
+ }
195
238
  }
196
239
  }
197
240
  return Promise.reject(error)
@@ -213,6 +256,7 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
213
256
  const base: MintAxiosRequestConfig = {
214
257
  baseURL: getBaseUrl(),
215
258
  timeout: options.timeout ?? settingsStore.requestTimeout,
259
+ _mintTypedErrors: options.typedErrors,
216
260
  ...config,
217
261
  }
218
262
  // Strip auth header if explicitly disabled
@@ -1,10 +1,18 @@
1
- import { ref, type Ref } from 'vue'
1
+ import { nextTick, ref, type Ref } from 'vue'
2
2
  import { useEventListener } from './useEventListener'
3
3
 
4
4
  export interface UseDropdownStateOptions {
5
5
  closeOnOutsideClick?: boolean
6
6
  closeOnEscape?: boolean
7
+ /** Close the panel when the page scrolls or the viewport resizes. Default `false`. */
8
+ closeOnScroll?: boolean
7
9
  insideRefs?: Array<Ref<HTMLElement | null>>
10
+ /**
11
+ * Element focus returns to when the panel closes with `close(true)` (Escape).
12
+ * Pass one when the trigger lives outside `rootRef`; otherwise bind the
13
+ * returned `triggerRef` to the trigger element.
14
+ */
15
+ triggerRef?: Ref<HTMLElement | null>
8
16
  onOpen?: () => void
9
17
  onClose?: () => void
10
18
  }
@@ -12,19 +20,29 @@ export interface UseDropdownStateOptions {
12
20
  export interface UseDropdownStateReturn {
13
21
  isOpen: Ref<boolean>
14
22
  rootRef: Ref<HTMLElement | null>
23
+ /** Bind to the trigger element so Escape can return focus to it. */
24
+ triggerRef: Ref<HTMLElement | null>
15
25
  open: () => void
16
- close: () => void
26
+ /** Closes the panel; `returnFocus` moves focus back to `triggerRef`. */
27
+ close: (returnFocus?: boolean) => void
17
28
  toggle: () => void
18
29
  handleClickOutside: (event: MouseEvent) => void
19
30
  handleKeydown: (event: KeyboardEvent) => void
20
31
  }
21
32
 
22
- /** Shared disclosure state for dropdown-style SDK components. */
33
+ /**
34
+ * Shared disclosure state for dropdown-style SDK components: open/close state,
35
+ * outside-click and Escape closing, optional scroll/resize closing, and focus
36
+ * return to the trigger. `close(returnFocus)` mirrors `useMenuKeyboard`'s
37
+ * contract, so the two compose directly.
38
+ */
23
39
  export function useDropdownState(options: UseDropdownStateOptions = {}): UseDropdownStateReturn {
24
40
  const isOpen = ref(false)
25
41
  const rootRef = ref<HTMLElement | null>(null)
42
+ const triggerRef = options.triggerRef ?? ref<HTMLElement | null>(null)
26
43
  const closeOnOutsideClick = options.closeOnOutsideClick ?? true
27
44
  const closeOnEscape = options.closeOnEscape ?? true
45
+ const closeOnScroll = options.closeOnScroll ?? false
28
46
 
29
47
  function open(): void {
30
48
  if (isOpen.value) return
@@ -32,10 +50,13 @@ export function useDropdownState(options: UseDropdownStateOptions = {}): UseDrop
32
50
  options.onOpen?.()
33
51
  }
34
52
 
35
- function close(): void {
53
+ function close(returnFocus = false): void {
36
54
  if (!isOpen.value) return
37
55
  isOpen.value = false
38
56
  options.onClose?.()
57
+ if (returnFocus) {
58
+ void nextTick(() => triggerRef.value?.focus())
59
+ }
39
60
  }
40
61
 
41
62
  function toggle(): void {
@@ -60,20 +81,29 @@ export function useDropdownState(options: UseDropdownStateOptions = {}): UseDrop
60
81
 
61
82
  function handleKeydown(event: KeyboardEvent): void {
62
83
  if (closeOnEscape && event.key === 'Escape') {
63
- close()
84
+ close(true)
64
85
  }
65
86
  }
66
87
 
88
+ function handleViewportChange(): void {
89
+ if (isOpen.value) close()
90
+ }
91
+
67
92
  if (closeOnOutsideClick) {
68
93
  useEventListener(() => document, 'click', handleClickOutside)
69
94
  }
70
95
  if (closeOnEscape) {
71
96
  useEventListener(() => document, 'keydown', handleKeydown)
72
97
  }
98
+ if (closeOnScroll) {
99
+ useEventListener(() => window, 'scroll', handleViewportChange, true)
100
+ useEventListener(() => window, 'resize', handleViewportChange)
101
+ }
73
102
 
74
103
  return {
75
104
  isOpen,
76
105
  rootRef,
106
+ triggerRef,
77
107
  open,
78
108
  close,
79
109
  toggle,
@@ -7,6 +7,7 @@ import {
7
7
  shallowRef,
8
8
  type Ref,
9
9
  } from 'vue'
10
+ import { tryUseSettingsStore } from '../stores/settings'
10
11
 
11
12
  export type EventStreamTransport = (
12
13
  input: RequestInfo | URL,
@@ -26,7 +27,16 @@ export interface EventStreamOptions<TData = string> {
26
27
  headers?: HeadersInit
27
28
  credentials?: RequestCredentials
28
29
  immediate?: boolean
30
+ /**
31
+ * Reconnect after a dropped stream. Left unset, it follows the platform's
32
+ * `wsAutoReconnect` setting, defaulting to `true` without a settings store.
33
+ */
29
34
  reconnect?: boolean
35
+ /**
36
+ * Delay before the first reconnect attempt. Left unset, it follows the
37
+ * platform's `wsReconnectInterval` setting, defaulting to 2000 ms without a
38
+ * settings store. A server-sent `retry:` field still overrides it.
39
+ */
30
40
  reconnectDelayMs?: number
31
41
  parseJson?: boolean
32
42
  shouldReconnect?: (cause?: unknown) => boolean
@@ -114,7 +124,9 @@ export function useEventStream<TData = string>(
114
124
  let controller: AbortController | null = null
115
125
  let reader: ReadableStreamDefaultReader<Uint8Array> | null = null
116
126
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
117
- let reconnectDelayMs = options.reconnectDelayMs ?? 2_000
127
+ const settings = tryUseSettingsStore()
128
+ const reconnectEnabled = options.reconnect ?? settings?.wsAutoReconnect ?? true
129
+ let reconnectDelayMs = options.reconnectDelayMs ?? settings?.wsReconnectInterval ?? 2_000
118
130
  let stopped = true
119
131
  let generation = 0
120
132
 
@@ -135,7 +147,7 @@ export function useEventStream<TData = string>(
135
147
 
136
148
  function scheduleReconnect(connectionGeneration: number, cause?: unknown): void {
137
149
  if (stopped || generation !== connectionGeneration) return
138
- if (options.reconnect === false || options.shouldReconnect?.(cause) === false) {
150
+ if (!reconnectEnabled || options.shouldReconnect?.(cause) === false) {
139
151
  stopped = true
140
152
  return
141
153
  }