@morscherlab/mint-sdk 1.1.25 → 1.2.0-beta.10

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 (403) hide show
  1. package/README.md +44 -12
  2. package/dist/{ExperimentPopover-tLwJ7XF4.js → ExperimentPopover-oL0fg_A0.js} +6 -4
  3. package/dist/ExperimentPopover-oL0fg_A0.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-BeEOomw_.js} +5 -5
  5. package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
  6. package/dist/{SettingsModal-BW3Ar-uy.js → SettingsModal-Dphh3RrI.js} +5 -8
  7. package/dist/SettingsModal-Dphh3RrI.js.map +1 -0
  8. package/dist/{EmptyState-p90quq8Z.js → Skeleton-DxtwJ5gt.js} +111 -93
  9. package/dist/Skeleton-DxtwJ5gt.js.map +1 -0
  10. package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
  11. package/dist/__tests__/components/PluginWorkspaceView.internal.test.d.ts +1 -0
  12. package/dist/__tests__/components/SecretInput.test.d.ts +1 -0
  13. package/dist/__tests__/components/WellEditPopupInternal.test.d.ts +1 -0
  14. package/dist/__tests__/composables/useEventStream.test.d.ts +1 -0
  15. package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
  16. package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
  17. package/dist/components/ActionMenu.vue.d.ts +1 -1
  18. package/dist/components/AppAvatarMenu.vue.d.ts +4 -0
  19. package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
  20. package/dist/components/AppSidebar.vue.d.ts +1 -1
  21. package/dist/components/AuditTrail.vue.d.ts +13 -0
  22. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  23. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  24. package/dist/components/DataFrame.vue.d.ts +14 -2
  25. package/dist/components/DatePicker.vue.d.ts +1 -1
  26. package/dist/components/DateTimePicker.vue.d.ts +2 -0
  27. package/dist/components/DropdownButton.vue.d.ts +2 -0
  28. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  29. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  30. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  31. package/dist/components/FitPanel.vue.d.ts +1 -1
  32. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  33. package/dist/components/PluginWorkspaceView.props.d.ts +29 -1
  34. package/dist/components/PluginWorkspaceView.vue.d.ts +5 -5
  35. package/dist/components/ProgressBar.vue.d.ts +1 -1
  36. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  37. package/dist/components/ReagentList.vue.d.ts +29 -1
  38. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  39. package/dist/components/SampleSelector.drag.d.ts +13 -0
  40. package/dist/components/SampleSelector.tree.d.ts +2 -0
  41. package/dist/components/SampleSelector.vue.d.ts +34 -1
  42. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  43. package/dist/components/SecretInput.vue.d.ts +28 -0
  44. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -1
  45. package/dist/components/TimePicker.vue.d.ts +1 -0
  46. package/dist/components/WellPlate.geometry.d.ts +15 -0
  47. package/dist/components/WellPlate.interaction.d.ts +13 -0
  48. package/dist/components/WellPlate.vue.d.ts +29 -0
  49. package/dist/components/index.d.ts +9 -0
  50. package/dist/components/index.js +8 -8
  51. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  52. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  53. package/dist/components/internal/WellEditPopupInternal.vue.d.ts +3 -1
  54. package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +23 -0
  55. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +70 -0
  56. package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +43 -0
  57. package/dist/{components-DQt0tcpY.js → components-CgAUzF8F.js} +2491 -2235
  58. package/dist/components-CgAUzF8F.js.map +1 -0
  59. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  60. package/dist/composables/flattenWorkspaceBindings.d.ts +61 -0
  61. package/dist/composables/index.d.ts +8 -2
  62. package/dist/composables/index.js +10 -10
  63. package/dist/composables/pluginWorkspaceModel.d.ts +164 -0
  64. package/dist/composables/useAsync.d.ts +5 -1
  65. package/dist/composables/useBioTemplateWorkspace.d.ts +4 -20
  66. package/dist/composables/useCommandHistory.d.ts +5 -1
  67. package/dist/composables/useDropdownState.d.ts +18 -2
  68. package/dist/composables/useEventStream.d.ts +52 -0
  69. package/dist/composables/useFocusTrap.d.ts +21 -0
  70. package/dist/composables/useJobsStatusTray.d.ts +1 -1
  71. package/dist/composables/useListReorder.d.ts +90 -0
  72. package/dist/composables/useMenuKeyboard.d.ts +29 -0
  73. package/dist/composables/useOptimisticMutation.d.ts +5 -1
  74. package/dist/composables/usePluginClient.d.ts +4 -17
  75. package/dist/composables/usePluginConfig.d.ts +3 -1
  76. package/dist/composables/usePluginWorkspace.d.ts +51 -0
  77. package/dist/composables/useRequestSyncState.d.ts +42 -2
  78. package/dist/composables/useResourceCrud.d.ts +5 -1
  79. package/dist/composables/useRovingFocus.d.ts +24 -0
  80. package/dist/composables/useScheduleDrag.d.ts +1 -0
  81. package/dist/composables/useWellPainting.d.ts +5 -1
  82. package/dist/composables/useWellPlateAdapter.d.ts +5 -1
  83. package/dist/composables/useWellPlateValidation.d.ts +5 -1
  84. package/dist/composables/workspaceRecipes.d.ts +188 -0
  85. package/dist/{composables-CkXDe7nm.js → composables-B_pia3hO.js} +105 -254
  86. package/dist/composables-B_pia3hO.js.map +1 -0
  87. package/dist/{useApi-CDBq6z23.js → errors-_W0-LbA6.js} +102 -3
  88. package/dist/errors-_W0-LbA6.js.map +1 -0
  89. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cwl9AYxw.js} +3 -8
  90. package/dist/experiment-Cwl9AYxw.js.map +1 -0
  91. package/dist/{experiment-utils-CLnzPPq5.js → experiment-utils-DV3DrvE_.js} +70 -51
  92. package/dist/experiment-utils-DV3DrvE_.js.map +1 -0
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.js +15 -15
  95. package/dist/install.js +2 -7
  96. package/dist/install.js.map +1 -1
  97. package/dist/permissions.d.ts +42 -1
  98. package/dist/permissions.js +20 -10
  99. package/dist/permissions.js.map +1 -1
  100. package/dist/stores/index.d.ts +1 -1
  101. package/dist/stores/index.js +3 -3
  102. package/dist/stores/settings.d.ts +31 -1
  103. package/dist/styles.css +11247 -29501
  104. package/dist/templates/builderReadUtils.d.ts +1 -1
  105. package/dist/templates/index.js +2 -4
  106. package/dist/{templates-CovvRZ-9.js → templates-Qq113pyZ.js} +303 -106
  107. package/dist/templates-Qq113pyZ.js.map +1 -0
  108. package/dist/types/form-builder.d.ts +1 -1
  109. package/dist/{useControlSchema-BhoJzWzx.js → useControlSchema-aMZlb4Sc.js} +684 -1216
  110. package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
  111. package/dist/{useDropdownState-DSb3BC3N.js → useDropdownState-DitCtrTT.js} +22 -6
  112. package/dist/useDropdownState-DitCtrTT.js.map +1 -0
  113. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-I8HC8hUu.js} +4 -4
  114. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
  115. package/dist/{useFormBuilder-DiEcDis2.js → useFormBuilder-TCxNnqeb.js} +2 -2
  116. package/dist/{useFormBuilder-DiEcDis2.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
  117. package/dist/{useProtocolTemplates-DHwSveE6.js → useProtocolTemplates-BmX_iVBR.js} +660 -197
  118. package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
  119. package/dist/utils/color.d.ts +13 -0
  120. package/dist/utils/errors.d.ts +9 -0
  121. package/dist/utils/pluginSecrets.d.ts +53 -0
  122. package/dist/utils/records.d.ts +4 -0
  123. package/package.json +1 -1
  124. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  125. package/src/__tests__/components/AppAvatarMenu.test.ts +123 -0
  126. package/src/__tests__/components/BaseTabs.test.ts +67 -0
  127. package/src/__tests__/components/DataFrame.test.ts +183 -11
  128. package/src/__tests__/components/DataStates.test.ts +176 -0
  129. package/src/__tests__/components/DatePicker.test.ts +76 -0
  130. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  131. package/src/__tests__/components/DropdownButton.test.ts +134 -0
  132. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  133. package/src/__tests__/components/GeneratedPluginView.test.ts +0 -5
  134. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  135. package/src/__tests__/components/JobsStatusTray.test.ts +20 -18
  136. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  137. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +230 -0
  138. package/src/__tests__/components/RackEditor.test.ts +35 -0
  139. package/src/__tests__/components/ReagentList.test.ts +64 -2
  140. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  141. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  142. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  143. package/src/__tests__/components/SecretInput.test.ts +87 -0
  144. package/src/__tests__/components/SegmentedControl.test.ts +88 -0
  145. package/src/__tests__/components/TimePicker.test.ts +61 -0
  146. package/src/__tests__/components/WellEditPopupInternal.test.ts +108 -0
  147. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  148. package/src/__tests__/composables/useAuth.test.ts +25 -0
  149. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  150. package/src/__tests__/composables/useEventStream.test.ts +140 -0
  151. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +103 -0
  152. package/src/__tests__/composables/useJobsStatusTray.test.ts +25 -161
  153. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  154. package/src/__tests__/composables/usePluginClient.test.ts +148 -13
  155. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  156. package/src/__tests__/docs/frontendDocsCatalog.test.ts +55 -2
  157. package/src/__tests__/stores/settings.test.ts +71 -1
  158. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  159. package/src/__tests__/utils/permissions.test.ts +68 -0
  160. package/src/components/ActionMenu.vue +45 -88
  161. package/src/components/AlertBox.vue +0 -4
  162. package/src/components/AppAvatarMenu.vue +26 -7
  163. package/src/components/AppLayout.vue +0 -4
  164. package/src/components/AppPluginSwitcher.story.vue +87 -0
  165. package/src/components/AppPluginSwitcher.vue +2 -5
  166. package/src/components/AppSidebar.vue +0 -4
  167. package/src/components/AppToastContainer.vue +6 -8
  168. package/src/components/AppTopBar.vue +0 -4
  169. package/src/components/ArtifactSaveDialog.vue +0 -4
  170. package/src/components/ArtifactSelectorModal.vue +0 -4
  171. package/src/components/AuditTrail.story.vue +18 -0
  172. package/src/components/AuditTrail.vue +27 -7
  173. package/src/components/AutoGroupModal.vue +0 -4
  174. package/src/components/Avatar.vue +0 -4
  175. package/src/components/BaseButton.vue +0 -4
  176. package/src/components/BaseCheckbox.vue +0 -4
  177. package/src/components/BaseInput.vue +0 -4
  178. package/src/components/BaseModal.vue +8 -60
  179. package/src/components/BasePill.vue +0 -4
  180. package/src/components/BaseRadioGroup.vue +0 -4
  181. package/src/components/BaseSelect.vue +0 -4
  182. package/src/components/BaseSlider.vue +0 -4
  183. package/src/components/BaseTabs.vue +26 -5
  184. package/src/components/BaseTextarea.vue +0 -4
  185. package/src/components/BaseToggle.vue +0 -4
  186. package/src/components/BatchProgressList.story.vue +18 -0
  187. package/src/components/BatchProgressList.vue +32 -4
  188. package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
  189. package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
  190. package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
  191. package/src/components/Breadcrumb.vue +0 -4
  192. package/src/components/Calendar.vue +0 -4
  193. package/src/components/ChartContainer.story.vue +1 -1
  194. package/src/components/ChartContainer.vue +0 -4
  195. package/src/components/ChemicalFormula.vue +0 -4
  196. package/src/components/CollapsibleCard.vue +0 -4
  197. package/src/components/ColorSlider.vue +0 -4
  198. package/src/components/ComponentBindingRenderer.vue +1 -3
  199. package/src/components/ConcentrationInput.vue +0 -4
  200. package/src/components/ConfirmDialog.vue +0 -4
  201. package/src/components/ControlWorkspaceView.vue +11 -22
  202. package/src/components/DataFrame.story.vue +14 -0
  203. package/src/components/DataFrame.vue +97 -23
  204. package/src/components/DatePicker.vue +73 -27
  205. package/src/components/DateTimePicker.vue +45 -17
  206. package/src/components/Divider.vue +0 -4
  207. package/src/components/DoseCalculator.vue +0 -4
  208. package/src/components/DoseDesignWorkspaceView.vue +6 -25
  209. package/src/components/DropdownButton.vue +24 -6
  210. package/src/components/EmptyState.vue +0 -4
  211. package/src/components/ExperimentCodeBadge.vue +0 -4
  212. package/src/components/ExperimentDataViewer.story.vue +3 -0
  213. package/src/components/ExperimentDataViewer.vue +33 -13
  214. package/src/components/ExperimentPopover.vue +2 -5
  215. package/src/components/ExperimentSelectorModal.vue +0 -4
  216. package/src/components/ExperimentTimeline.story.vue +18 -0
  217. package/src/components/ExperimentTimeline.vue +81 -75
  218. package/src/components/FileUploader.vue +0 -4
  219. package/src/components/FitPanel.vue +0 -4
  220. package/src/components/FormActions.story.vue +96 -0
  221. package/src/components/FormActions.vue +0 -4
  222. package/src/components/FormBuilder.vue +0 -4
  223. package/src/components/FormField.vue +0 -4
  224. package/src/components/FormulaInput.vue +0 -4
  225. package/src/components/GeneratedPathInput.story.vue +68 -0
  226. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  227. package/src/components/GeneratedPlotlyResult.vue +3 -3
  228. package/src/components/GeneratedPluginView.story.vue +145 -0
  229. package/src/components/GeneratedPluginView.vue +10 -10
  230. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  231. package/src/components/GeneratedResultRenderer.vue +4 -4
  232. package/src/components/GroupAssigner.vue +67 -5
  233. package/src/components/IconButton.vue +0 -4
  234. package/src/components/InstrumentAlertLog.vue +0 -4
  235. package/src/components/InstrumentStateBadge.vue +0 -4
  236. package/src/components/InstrumentStatusCard.vue +0 -4
  237. package/src/components/JobsStatusTray.story.vue +6 -10
  238. package/src/components/JobsStatusTray.vue +0 -4
  239. package/src/components/LcmsSequenceTable.vue +81 -50
  240. package/src/components/LoadingSpinner.vue +0 -4
  241. package/src/components/MobileSupportGate.vue +0 -4
  242. package/src/components/MoleculeInput.vue +0 -4
  243. package/src/components/MultiSelect.vue +0 -4
  244. package/src/components/NumberInput.vue +0 -4
  245. package/src/components/PlateMapEditor.vue +3 -14
  246. package/src/components/PluginIcon.vue +0 -4
  247. package/src/components/PluginWorkspaceView.props.ts +38 -0
  248. package/src/components/PluginWorkspaceView.vue +53 -224
  249. package/src/components/ProgressBar.vue +0 -4
  250. package/src/components/ProtocolStepEditor.vue +2 -4
  251. package/src/components/RackEditor.vue +51 -70
  252. package/src/components/ReagentEditor.vue +0 -4
  253. package/src/components/ReagentList.story.vue +18 -0
  254. package/src/components/ReagentList.vue +127 -82
  255. package/src/components/ResourceCard.vue +0 -4
  256. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  257. package/src/components/RuntimeUpdateNotice.vue +4 -4
  258. package/src/components/SampleHierarchyTree.story.vue +18 -0
  259. package/src/components/SampleHierarchyTree.vue +32 -9
  260. package/src/components/SampleLegend.vue +2 -8
  261. package/src/components/SampleSelector.drag.ts +51 -0
  262. package/src/components/SampleSelector.story.vue +6 -0
  263. package/src/components/SampleSelector.tree.ts +2 -0
  264. package/src/components/SampleSelector.vue +49 -10
  265. package/src/components/SampleSelectorGroupNode.vue +19 -0
  266. package/src/components/ScheduleCalendar.story.vue +6 -0
  267. package/src/components/ScheduleCalendar.vue +82 -7
  268. package/src/components/ScientificNumber.vue +0 -4
  269. package/src/components/SecretInput.story.vue +79 -0
  270. package/src/components/SecretInput.vue +150 -0
  271. package/src/components/SectionCard.vue +0 -4
  272. package/src/components/SegmentedControl.vue +48 -5
  273. package/src/components/SequenceInput.vue +0 -4
  274. package/src/components/SequenceProgressBar.story.vue +110 -0
  275. package/src/components/SequenceProgressBar.vue +0 -4
  276. package/src/components/SettingsModal.vue +0 -4
  277. package/src/components/Skeleton.vue +0 -4
  278. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  279. package/src/components/SmartGroupManual.vue +0 -4
  280. package/src/components/StatusIndicator.vue +0 -4
  281. package/src/components/StepWizard.vue +0 -4
  282. package/src/components/TagsInput.vue +4 -6
  283. package/src/components/ThemeToggle.vue +0 -4
  284. package/src/components/TimePicker.vue +36 -7
  285. package/src/components/TimeRangeInput.vue +0 -4
  286. package/src/components/Tooltip.story.vue +3 -3
  287. package/src/components/Tooltip.vue +0 -4
  288. package/src/components/UnitInput.vue +0 -4
  289. package/src/components/WellPlate.geometry.ts +60 -1
  290. package/src/components/WellPlate.interaction.ts +44 -0
  291. package/src/components/WellPlate.story.vue +8 -0
  292. package/src/components/WellPlate.vue +53 -7
  293. package/src/components/index.ts +9 -0
  294. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  295. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  296. package/src/components/internal/DataStateInternal.vue +74 -0
  297. package/src/components/internal/FormSectionRenderer.vue +0 -4
  298. package/src/components/internal/WellEditPopupInternal.vue +20 -5
  299. package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
  300. package/src/components/internal/WorkspaceShellInternal.vue +125 -0
  301. package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
  302. package/src/composables/controlSchemaTypes.ts +34 -0
  303. package/src/composables/experimentDesignData.ts +1 -3
  304. package/src/composables/flattenWorkspaceBindings.ts +104 -0
  305. package/src/composables/formBuilderRegistry.ts +5 -0
  306. package/src/composables/index.ts +41 -5
  307. package/src/composables/pluginWorkspaceModel.ts +182 -0
  308. package/src/composables/useAsync.ts +5 -1
  309. package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
  310. package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
  311. package/src/composables/useBioTemplateWorkspace.ts +18 -43
  312. package/src/composables/useCommandHistory.ts +5 -1
  313. package/src/composables/useDropdownState.ts +35 -5
  314. package/src/composables/useEventStream.ts +326 -0
  315. package/src/composables/useFileImport.ts +2 -7
  316. package/src/composables/useFocusTrap.ts +91 -0
  317. package/src/composables/useGeneratedAnalysis.ts +46 -103
  318. package/src/composables/useJobsStatusTray.ts +1 -1
  319. package/src/composables/useListReorder.story.vue +143 -0
  320. package/src/composables/useListReorder.ts +242 -0
  321. package/src/composables/useMenuKeyboard.ts +90 -0
  322. package/src/composables/useOptimisticMutation.ts +6 -16
  323. package/src/composables/usePluginClient.ts +91 -252
  324. package/src/composables/usePluginConfig.ts +3 -1
  325. package/src/composables/usePluginWorkspace.ts +208 -0
  326. package/src/composables/useRackEditor.ts +2 -11
  327. package/src/composables/useRequestSyncState.ts +98 -28
  328. package/src/composables/useResourceCrud.ts +6 -16
  329. package/src/composables/useRovingFocus.ts +66 -0
  330. package/src/composables/useScheduleDrag.ts +37 -0
  331. package/src/composables/useWellPainting.ts +10 -30
  332. package/src/composables/useWellPlateAdapter.ts +5 -1
  333. package/src/composables/useWellPlateEditor.ts +1 -11
  334. package/src/composables/useWellPlateValidation.ts +5 -1
  335. package/src/composables/workspaceRecipes.ts +267 -0
  336. package/src/index.ts +16 -0
  337. package/src/permissions.ts +48 -9
  338. package/src/stores/experiment.ts +2 -7
  339. package/src/stores/index.ts +9 -1
  340. package/src/stores/settings.ts +93 -1
  341. package/src/styles/components/audit-trail.css +5 -5
  342. package/src/styles/components/batch-progress-list.css +7 -0
  343. package/src/styles/components/color-slider.css +2 -2
  344. package/src/styles/components/data-state.css +30 -0
  345. package/src/styles/components/dataframe.css +29 -35
  346. package/src/styles/components/experiment-data-viewer.css +4 -4
  347. package/src/styles/components/experiment-timeline.css +3 -30
  348. package/src/styles/components/form-builder.css +5 -5
  349. package/src/styles/components/lcms-sequence-table.css +16 -1
  350. package/src/styles/components/multi-select.css +1 -1
  351. package/src/styles/components/reagent-list.css +38 -31
  352. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  353. package/src/styles/components/sample-selector.css +9 -2
  354. package/src/styles/components/schedule-calendar.css +8 -0
  355. package/src/styles/components/scientific-number.css +1 -1
  356. package/src/styles/components/secret-input.css +60 -0
  357. package/src/styles/components/well-plate.css +8 -0
  358. package/src/styles/index.css +9 -0
  359. package/src/styles/variables.css +29 -0
  360. package/src/templates/builderPresetControls.ts +1 -1
  361. package/src/templates/builderReadUtils.ts +1 -3
  362. package/src/templates/componentPlateHelpers.ts +3 -1
  363. package/src/templates/componentTargetResolvers.ts +1 -3
  364. package/src/templates/controlSchemaTargets.ts +1 -3
  365. package/src/templates/doseResponseAdapters.ts +2 -13
  366. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  367. package/src/templates/plateMapBuilder.ts +2 -2
  368. package/src/templates/qpcrPlateBuilder.ts +1 -1
  369. package/src/templates/templateValidators.ts +1 -1
  370. package/src/templates/timeCourseBuilder.ts +2 -2
  371. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  372. package/src/types/form-builder.ts +1 -0
  373. package/src/utils/color.ts +51 -0
  374. package/src/utils/errors.ts +29 -0
  375. package/src/utils/formModelSync.ts +1 -3
  376. package/src/utils/pluginIcon.ts +1 -3
  377. package/src/utils/pluginSecrets.ts +102 -0
  378. package/src/utils/records.ts +9 -0
  379. package/dist/EmptyState-p90quq8Z.js.map +0 -1
  380. package/dist/ExperimentPopover-tLwJ7XF4.js.map +0 -1
  381. package/dist/ExperimentSelectorModal-B6P2msT4.js.map +0 -1
  382. package/dist/SettingsModal-BW3Ar-uy.js.map +0 -1
  383. package/dist/components-DQt0tcpY.js.map +0 -1
  384. package/dist/composables/autoGroupConstants.d.ts +0 -1
  385. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  386. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  387. package/dist/composables-CkXDe7nm.js.map +0 -1
  388. package/dist/experiment-Y9Iz7KYi.js.map +0 -1
  389. package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
  390. package/dist/templates/builderDefaults.d.ts +0 -1
  391. package/dist/templates/plateGeometry.d.ts +0 -4
  392. package/dist/templates-CovvRZ-9.js.map +0 -1
  393. package/dist/useApi-CDBq6z23.js.map +0 -1
  394. package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
  395. package/dist/useDropdownState-DSb3BC3N.js.map +0 -1
  396. package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
  397. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  398. package/src/composables/autoGroupConstants.ts +0 -4
  399. package/src/composables/autoGroupGrouping.ts +0 -148
  400. package/src/composables/useAutoGroupInputSources.ts +0 -147
  401. package/src/templates/builderDefaults.ts +0 -11
  402. package/src/templates/plateGeometry.ts +0 -62
  403. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
@@ -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,
@@ -0,0 +1,326 @@
1
+ import {
2
+ getCurrentInstance,
3
+ getCurrentScope,
4
+ onMounted,
5
+ onScopeDispose,
6
+ ref,
7
+ shallowRef,
8
+ type Ref,
9
+ } from 'vue'
10
+ import { tryUseSettingsStore } from '../stores/settings'
11
+
12
+ export type EventStreamTransport = (
13
+ input: RequestInfo | URL,
14
+ init?: RequestInit,
15
+ ) => Promise<Response>
16
+
17
+ export interface EventStreamMessage<TData = string> {
18
+ event: string
19
+ data: TData
20
+ id?: string
21
+ retry?: number
22
+ raw: string
23
+ }
24
+
25
+ export interface EventStreamOptions<TData = string> {
26
+ transport?: EventStreamTransport
27
+ headers?: HeadersInit
28
+ credentials?: RequestCredentials
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
+ */
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
+ */
40
+ reconnectDelayMs?: number
41
+ parseJson?: boolean
42
+ shouldReconnect?: (cause?: unknown) => boolean
43
+ onOpen?: () => void
44
+ onMessage?: (message: EventStreamMessage<TData>) => void
45
+ onError?: (cause: unknown) => void
46
+ onClose?: (cause?: unknown) => void
47
+ errorLabel?: string
48
+ }
49
+
50
+ export interface UseEventStreamReturn<TData = string> {
51
+ isConnected: Ref<boolean>
52
+ isConnecting: Ref<boolean>
53
+ error: Ref<string | null>
54
+ lastMessage: Ref<EventStreamMessage<TData> | null>
55
+ lastEventId: Ref<string | null>
56
+ start: () => void
57
+ stop: () => void
58
+ }
59
+
60
+ interface ParsedEventStreamBlock<TData> {
61
+ message?: EventStreamMessage<TData>
62
+ id?: string
63
+ retry?: number
64
+ }
65
+
66
+ class EventStreamResponseError extends Error {
67
+ constructor(readonly response: Response, label: string) {
68
+ super(`${label} failed with HTTP ${response.status}`)
69
+ this.name = 'EventStreamResponseError'
70
+ }
71
+ }
72
+
73
+ /** Parse one complete SSE block without owning connection policy. */
74
+ export function parseEventStreamBlock<TData>(
75
+ block: string,
76
+ parseJson: boolean,
77
+ ): ParsedEventStreamBlock<TData> | null {
78
+ let event = 'message'
79
+ let id: string | undefined
80
+ let retry: number | undefined
81
+ const dataLines: string[] = []
82
+
83
+ for (const line of block.split(/\r\n|\r|\n/)) {
84
+ if (!line || line.startsWith(':')) continue
85
+ const separator = line.indexOf(':')
86
+ const field = separator === -1 ? line : line.slice(0, separator)
87
+ const value = separator === -1
88
+ ? ''
89
+ : line.slice(separator + 1).replace(/^ /, '')
90
+
91
+ if (field === 'event') event = value || 'message'
92
+ if (field === 'data') dataLines.push(value)
93
+ if (field === 'id' && !value.includes('\0')) id = value
94
+ if (field === 'retry' && /^\d+$/.test(value)) retry = Number(value)
95
+ }
96
+
97
+ if (!dataLines.length && id === undefined && retry === undefined) return null
98
+ const data = dataLines.join('\n')
99
+ return {
100
+ message: dataLines.length
101
+ ? {
102
+ event,
103
+ data: (parseJson && data ? JSON.parse(data) : data) as TData,
104
+ id,
105
+ retry,
106
+ raw: block,
107
+ }
108
+ : undefined,
109
+ id,
110
+ retry,
111
+ }
112
+ }
113
+
114
+ /** Shared fetch-based SSE lifecycle with cancellation and reconnect safety. */
115
+ export function useEventStream<TData = string>(
116
+ url: string | (() => string),
117
+ options: EventStreamOptions<TData> = {},
118
+ ): UseEventStreamReturn<TData> {
119
+ const isConnected = ref(false)
120
+ const isConnecting = ref(false)
121
+ const error = ref<string | null>(null)
122
+ const lastMessage = shallowRef<EventStreamMessage<TData> | null>(null)
123
+ const lastEventId = ref<string | null>(null)
124
+ let controller: AbortController | null = null
125
+ let reader: ReadableStreamDefaultReader<Uint8Array> | null = null
126
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null
127
+ const settings = tryUseSettingsStore()
128
+ const reconnectEnabled = options.reconnect ?? settings?.wsAutoReconnect ?? true
129
+ let reconnectDelayMs = options.reconnectDelayMs ?? settings?.wsReconnectInterval ?? 2_000
130
+ let stopped = true
131
+ let generation = 0
132
+
133
+ function clearReconnectTimer(): void {
134
+ if (reconnectTimer === null) return
135
+ clearTimeout(reconnectTimer)
136
+ reconnectTimer = null
137
+ }
138
+
139
+ function ownsConnection(
140
+ connectionGeneration: number,
141
+ connectionController: AbortController,
142
+ ): boolean {
143
+ return !stopped
144
+ && generation === connectionGeneration
145
+ && controller === connectionController
146
+ }
147
+
148
+ function scheduleReconnect(connectionGeneration: number, cause?: unknown): void {
149
+ if (stopped || generation !== connectionGeneration) return
150
+ if (!reconnectEnabled || options.shouldReconnect?.(cause) === false) {
151
+ stopped = true
152
+ return
153
+ }
154
+ clearReconnectTimer()
155
+ reconnectTimer = setTimeout(() => {
156
+ reconnectTimer = null
157
+ if (stopped || generation !== connectionGeneration) return
158
+ void connect(connectionGeneration)
159
+ }, reconnectDelayMs)
160
+ }
161
+
162
+ function dispatchBlock(
163
+ block: string,
164
+ connectionGeneration: number,
165
+ connectionController: AbortController,
166
+ ): void {
167
+ const parsed = parseEventStreamBlock<TData>(block, options.parseJson ?? false)
168
+ if (!parsed || !ownsConnection(connectionGeneration, connectionController)) return
169
+ if (parsed.id !== undefined) {
170
+ lastEventId.value = parsed.id
171
+ if (!ownsConnection(connectionGeneration, connectionController)) return
172
+ }
173
+ if (parsed.retry !== undefined) reconnectDelayMs = parsed.retry
174
+ const message = parsed.message
175
+ if (!message || !ownsConnection(connectionGeneration, connectionController)) return
176
+ lastMessage.value = message
177
+ if (!ownsConnection(connectionGeneration, connectionController)) return
178
+ options.onMessage?.(message)
179
+ }
180
+
181
+ async function consume(
182
+ body: ReadableStream<Uint8Array>,
183
+ connectionGeneration: number,
184
+ connectionController: AbortController,
185
+ ): Promise<void> {
186
+ const connectionReader = body.getReader()
187
+ reader = connectionReader
188
+ const decoder = new TextDecoder()
189
+ let buffer = ''
190
+ try {
191
+ for (;;) {
192
+ const { value, done } = await connectionReader.read()
193
+ if (!ownsConnection(connectionGeneration, connectionController)) return
194
+ buffer += decoder.decode(value, { stream: !done })
195
+
196
+ let boundary = /(?:\r\n|\r|\n){2}/.exec(buffer)
197
+ while (boundary?.index !== undefined) {
198
+ const block = buffer.slice(0, boundary.index)
199
+ buffer = buffer.slice(boundary.index + boundary[0].length)
200
+ dispatchBlock(block, connectionGeneration, connectionController)
201
+ if (!ownsConnection(connectionGeneration, connectionController)) return
202
+ boundary = /(?:\r\n|\r|\n){2}/.exec(buffer)
203
+ }
204
+
205
+ if (!done) continue
206
+ if (buffer) dispatchBlock(buffer, connectionGeneration, connectionController)
207
+ return
208
+ }
209
+ } finally {
210
+ if (reader === connectionReader) reader = null
211
+ connectionReader.releaseLock()
212
+ }
213
+ }
214
+
215
+ async function connect(connectionGeneration: number): Promise<void> {
216
+ if (stopped || generation !== connectionGeneration) return
217
+ controller?.abort()
218
+ const connectionController = new AbortController()
219
+ controller = connectionController
220
+ isConnecting.value = true
221
+ if (!ownsConnection(connectionGeneration, connectionController)) return
222
+ error.value = null
223
+ if (!ownsConnection(connectionGeneration, connectionController)) return
224
+
225
+ let cause: unknown
226
+ try {
227
+ const headers = new Headers(options.headers)
228
+ headers.set('Accept', 'text/event-stream')
229
+ if (lastEventId.value && !headers.has('Last-Event-ID')) {
230
+ headers.set('Last-Event-ID', lastEventId.value)
231
+ }
232
+ const transport = options.transport ?? globalThis.fetch?.bind(globalThis)
233
+ if (!transport) throw new Error('[MINT SDK] fetch is required for event streams.')
234
+ const response = await transport(typeof url === 'function' ? url() : url, {
235
+ method: 'GET',
236
+ headers,
237
+ credentials: options.credentials ?? 'same-origin',
238
+ signal: connectionController.signal,
239
+ })
240
+ if (!ownsConnection(connectionGeneration, connectionController)) return
241
+ if (!response.ok) {
242
+ throw new EventStreamResponseError(response, options.errorLabel ?? 'Event stream')
243
+ }
244
+ if (!response.body) {
245
+ throw new Error(`${options.errorLabel ?? 'Event stream'} response did not include a readable body.`)
246
+ }
247
+
248
+ isConnected.value = true
249
+ if (!ownsConnection(connectionGeneration, connectionController)) return
250
+ isConnecting.value = false
251
+ if (!ownsConnection(connectionGeneration, connectionController)) return
252
+ options.onOpen?.()
253
+ if (!ownsConnection(connectionGeneration, connectionController)) return
254
+ await consume(response.body, connectionGeneration, connectionController)
255
+ } catch (errorCause) {
256
+ cause = errorCause
257
+ if (
258
+ ownsConnection(connectionGeneration, connectionController)
259
+ && !isAbortError(errorCause)
260
+ ) {
261
+ error.value = errorCause instanceof Error
262
+ ? errorCause.message
263
+ : `${options.errorLabel ?? 'Event stream'} failed`
264
+ if (ownsConnection(connectionGeneration, connectionController)) {
265
+ options.onError?.(errorCause)
266
+ }
267
+ }
268
+ } finally {
269
+ if (ownsConnection(connectionGeneration, connectionController)) {
270
+ isConnecting.value = false
271
+ if (!ownsConnection(connectionGeneration, connectionController)) return
272
+ isConnected.value = false
273
+ if (!ownsConnection(connectionGeneration, connectionController)) return
274
+ controller = null
275
+ options.onClose?.(cause)
276
+ if (!stopped && generation === connectionGeneration) {
277
+ scheduleReconnect(connectionGeneration, cause)
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ function start(): void {
284
+ if (!stopped) return
285
+ stopped = false
286
+ clearReconnectTimer()
287
+ generation += 1
288
+ void connect(generation)
289
+ }
290
+
291
+ function stop(): void {
292
+ if (stopped) return
293
+ stopped = true
294
+ generation += 1
295
+ clearReconnectTimer()
296
+ controller?.abort()
297
+ controller = null
298
+ if (reader) void reader.cancel().catch(() => undefined)
299
+ reader = null
300
+ isConnecting.value = false
301
+ isConnected.value = false
302
+ }
303
+
304
+ const currentInstance = getCurrentInstance()
305
+ if (currentInstance && options.immediate !== false && typeof window === 'undefined') {
306
+ onMounted(start)
307
+ }
308
+ if (getCurrentScope()) onScopeDispose(stop)
309
+ if (options.immediate !== false && (typeof window !== 'undefined' || !currentInstance)) {
310
+ start()
311
+ }
312
+
313
+ return {
314
+ isConnected,
315
+ isConnecting,
316
+ error,
317
+ lastMessage,
318
+ lastEventId,
319
+ start,
320
+ stop,
321
+ }
322
+ }
323
+
324
+ function isAbortError(cause: unknown): boolean {
325
+ return cause instanceof DOMException && cause.name === 'AbortError'
326
+ }
@@ -1,4 +1,5 @@
1
1
  import { ref, shallowRef, type Ref, type ShallowRef } from 'vue'
2
+ import { readErrorMessage } from '../utils/errors'
2
3
 
3
4
  export interface DelimitedTable {
4
5
  columns: string[]
@@ -63,7 +64,7 @@ export function useFileImport<TResult = string>(
63
64
  result.value = parsed
64
65
  return parsed
65
66
  } catch (err) {
66
- const message = readErrorMessage(err)
67
+ const message = readErrorMessage(err, 'Failed to import file.')
67
68
  error.value = message
68
69
  throw err
69
70
  } finally {
@@ -223,9 +224,3 @@ function formatBytes(bytes: number): string {
223
224
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`
224
225
  return `${Math.round(bytes / (1024 * 1024))} MB`
225
226
  }
226
-
227
- function readErrorMessage(value: unknown): string {
228
- if (value instanceof Error && value.message) return value.message
229
- if (typeof value === 'string' && value.trim()) return value
230
- return 'Failed to import file.'
231
- }
@@ -0,0 +1,91 @@
1
+ import { nextTick, watch, type Ref } from 'vue'
2
+ import { useEventListener } from './useEventListener'
3
+
4
+ export interface UseFocusTrapOptions {
5
+ /** Ref to the trap's container element. Focusable elements are queried within it. */
6
+ containerRef: Ref<HTMLElement | null>
7
+ /** Reactive flag — the trap activates when this becomes `true` and releases when it becomes `false`. */
8
+ active: Ref<boolean>
9
+ /** Restore focus to the element that was focused before activation, when the trap releases. Default `true`. */
10
+ restoreFocus?: boolean
11
+ }
12
+
13
+ export interface UseFocusTrapReturn {
14
+ /** Focusable descendants of the container, in DOM order, excluding roving-tabindex (`tabindex="-1"`) elements. */
15
+ getFocusableElements: () => HTMLElement[]
16
+ }
17
+
18
+ const FOCUSABLE_SELECTOR = [
19
+ 'a[href]',
20
+ 'button:not([disabled])',
21
+ 'input:not([disabled])',
22
+ 'select:not([disabled])',
23
+ 'textarea:not([disabled])',
24
+ '[tabindex]:not([tabindex="-1"])',
25
+ ].join(', ')
26
+
27
+ /**
28
+ * Traps Tab focus within a container while `active` is true: Tab wraps from the last
29
+ * focusable element back to the first (and Shift+Tab from the first to the last), the
30
+ * first focusable element (or the container itself, if none) receives focus when the
31
+ * trap activates, and focus returns to whatever was focused beforehand when it releases.
32
+ * Used by BaseModal and WellEditPopupInternal.
33
+ */
34
+ export function useFocusTrap(options: UseFocusTrapOptions): UseFocusTrapReturn {
35
+ const { containerRef, active } = options
36
+ const restoreFocus = options.restoreFocus ?? true
37
+ let previouslyFocused: HTMLElement | null = null
38
+
39
+ function getFocusableElements(): HTMLElement[] {
40
+ if (!containerRef.value) return []
41
+ // Roving tabindex (inactive tabs/rail items) stays out of the Tab cycle. Checking
42
+ // the attribute directly (rather than the `.tabIndex` IDL property) avoids relying
43
+ // on a browser/engine's default-tabindex computation for freshly-patched elements.
44
+ return Array.from(containerRef.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
45
+ .filter((el) => el.getAttribute('tabindex') !== '-1')
46
+ }
47
+
48
+ function handleKeydown(event: KeyboardEvent) {
49
+ if (event.key !== 'Tab' || !active.value || !containerRef.value) return
50
+
51
+ const focusable = getFocusableElements()
52
+ if (focusable.length === 0) {
53
+ event.preventDefault()
54
+ return
55
+ }
56
+
57
+ const first = focusable[0]
58
+ const last = focusable[focusable.length - 1]
59
+
60
+ if (event.shiftKey && document.activeElement === first) {
61
+ event.preventDefault()
62
+ last.focus()
63
+ } else if (!event.shiftKey && document.activeElement === last) {
64
+ event.preventDefault()
65
+ first.focus()
66
+ }
67
+ }
68
+
69
+ // `flush: 'sync'` so releasing the trap from `onBeforeUnmount` restores focus
70
+ // before the component's reactive scope is torn down — a batched (default
71
+ // 'pre') watcher would be scheduled but never flushed once that scope stops.
72
+ watch(active, async (isActive) => {
73
+ if (isActive) {
74
+ previouslyFocused = document.activeElement as HTMLElement | null
75
+ await nextTick()
76
+ const focusable = getFocusableElements()
77
+ if (focusable.length > 0) {
78
+ focusable[0].focus()
79
+ } else {
80
+ containerRef.value?.focus()
81
+ }
82
+ } else {
83
+ if (restoreFocus) previouslyFocused?.focus()
84
+ previouslyFocused = null
85
+ }
86
+ }, { flush: 'sync' })
87
+
88
+ useEventListener(() => document, 'keydown', handleKeydown)
89
+
90
+ return { getFocusableElements }
91
+ }