@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
@@ -12,7 +12,8 @@ import type {
12
12
  JobsStatusTrayAdapter,
13
13
  PluginJobCenterSource,
14
14
  } from './useJobsStatusTray'
15
- import { normalizeJobState } from '../jobs'
15
+ import { useEventStream, type EventStreamMessage } from './useEventStream'
16
+ import { isActiveJobStatus, normalizeJobState } from '../jobs'
16
17
  import type {
17
18
  GeneratedAnalysisJob,
18
19
  GeneratedAnalysisJobInput,
@@ -110,7 +111,6 @@ export interface UsePluginJobsReturn extends PluginJobCenterSource {
110
111
 
111
112
  export type UseGeneratedAnalysisReturn = UsePluginJobsReturn
112
113
 
113
- const ACTIVE_STATUSES = new Set(['awaiting_input', 'queued', 'running'])
114
114
  const AUTH_TOKEN_KEY = 'mint-auth-token'
115
115
  const SESSION_NOT_FOUND_DETAIL = 'Session not found'
116
116
  const MAX_RESULT_CACHE_ENTRIES = 32
@@ -125,6 +125,10 @@ interface PluginJobWaiter {
125
125
  abort?: () => void
126
126
  }
127
127
 
128
+ type PluginJobStreamData = GeneratedAnalysisJobInput | {
129
+ jobs: GeneratedAnalysisJobInput[]
130
+ }
131
+
128
132
  class GeneratedAnalysisRequestError extends Error {
129
133
  constructor(
130
134
  readonly status: number,
@@ -200,10 +204,6 @@ function createPluginJobs(
200
204
  const error = shallowRef<string | null>(null)
201
205
  const status = shallowRef<'idle' | 'initializing' | 'ready' | 'paused' | 'error' | 'disposed'>('idle')
202
206
  let pollTimer: ReturnType<typeof setTimeout> | undefined
203
- let streamReconnectTimer: ReturnType<typeof setTimeout> | undefined
204
- let streamController: AbortController | undefined
205
- let streamConnected = false
206
- let streamConnecting = false
207
207
  let disposed = false
208
208
  let paused = false
209
209
  let initialized = false
@@ -217,6 +217,17 @@ function createPluginJobs(
217
217
  const resultRetryAttempts = new Map<string, number>()
218
218
  const resultRetryTimers = new Map<string, ReturnType<typeof setTimeout>>()
219
219
  const downloadRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>()
220
+ const jobStream = useEventStream<PluginJobStreamData>(`${baseUrl}/jobs/events`, {
221
+ transport,
222
+ immediate: false,
223
+ reconnectDelayMs: streamReconnectDelay,
224
+ parseJson: true,
225
+ errorLabel: 'Job stream',
226
+ onOpen: clearPoll,
227
+ onMessage: applyJobStreamMessage,
228
+ onError: schedulePollForActiveJobs,
229
+ onClose: schedulePollForActiveJobs,
230
+ })
220
231
 
221
232
  const latestResult = computed<GeneratedAnalysisResult | null>(() => {
222
233
  const job = currentCompletedJob()
@@ -242,7 +253,7 @@ function createPluginJobs(
242
253
  if (disposed) return
243
254
  initialized = true
244
255
  status.value = paused ? 'paused' : 'ready'
245
- if (streamEnabled && !paused) void connectJobStream()
256
+ if (streamEnabled && !paused) jobStream.start()
246
257
  })().catch((cause: unknown) => {
247
258
  if (!disposed) {
248
259
  error.value = errorMessage(cause, 'Failed to initialize generated analysis')
@@ -303,7 +314,7 @@ function createPluginJobs(
303
314
  )
304
315
  }
305
316
  const job = upsertJob(jobInput, definitionId)
306
- if (!streamConnected) schedulePoll()
317
+ if (!jobStream.isConnected.value) schedulePoll()
307
318
  return job
308
319
  } catch (cause) {
309
320
  error.value = errorMessage(cause, 'Failed to submit analysis')
@@ -461,12 +472,12 @@ function createPluginJobs(
461
472
  .then((payload) => {
462
473
  if (disposed) return jobs.value
463
474
  if (revision !== jobsRevision) {
464
- if (jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) schedulePoll()
475
+ if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
465
476
  return jobs.value
466
477
  }
467
478
  replaceJobs(payload.jobs)
468
479
  error.value = null
469
- if (!paused && !streamConnected && jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) {
480
+ if (!paused && !jobStream.isConnected.value && jobs.value.some(job => isActiveJobStatus(job.status))) {
470
481
  schedulePoll()
471
482
  }
472
483
  return jobs.value
@@ -518,7 +529,7 @@ function createPluginJobs(
518
529
  const current = typeof job === 'string'
519
530
  ? jobs.value.find(candidate => candidate.job_id === jobId)
520
531
  : jobs.value.find(candidate => candidate.job_id === jobId) ?? job
521
- if (current && !ACTIVE_STATUSES.has(current.status)) {
532
+ if (current && !isActiveJobStatus(current.status)) {
522
533
  return terminalWaitResult(current, options.rejectOnFailure === true)
523
534
  }
524
535
  if (paused) return Promise.reject(new Error('Plugin jobs runtime is paused'))
@@ -540,12 +551,12 @@ function createPluginJobs(
540
551
  waiting.add(waiter)
541
552
  jobWaiters.set(jobId, waiting)
542
553
  if (!paused) {
543
- if (streamEnabled) void connectJobStream()
544
- if (!streamConnected) schedulePoll()
554
+ if (streamEnabled) jobStream.start()
555
+ if (!jobStream.isConnected.value) schedulePoll()
545
556
  }
546
557
  void getJob(jobId, { signal: options.signal }).catch((cause: unknown) => {
547
558
  if (current && isRetryableJobLookupError(cause)) {
548
- if (!streamConnected) schedulePoll()
559
+ if (!jobStream.isConnected.value) schedulePoll()
549
560
  return
550
561
  }
551
562
  removeJobWaiter(jobId, waiter)
@@ -555,7 +566,7 @@ function createPluginJobs(
555
566
  }
556
567
 
557
568
  function settleJobWaiters(job: GeneratedAnalysisJob): void {
558
- if (ACTIVE_STATUSES.has(job.status)) return
569
+ if (isActiveJobStatus(job.status)) return
559
570
  const waiting = jobWaiters.get(job.job_id)
560
571
  if (!waiting) return
561
572
  jobWaiters.delete(job.job_id)
@@ -895,7 +906,7 @@ function createPluginJobs(
895
906
  }
896
907
 
897
908
  function schedulePoll(): void {
898
- if (disposed || paused || streamConnected || pollTimer !== undefined) return
909
+ if (disposed || paused || jobStream.isConnected.value || pollTimer !== undefined) return
899
910
  pollTimer = setTimeout(async () => {
900
911
  pollTimer = undefined
901
912
  if (disposed) return
@@ -903,90 +914,31 @@ function createPluginJobs(
903
914
  await refreshJobs()
904
915
  } catch (cause) {
905
916
  error.value = errorMessage(cause, 'Failed to refresh analysis jobs')
906
- if (jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) schedulePoll()
917
+ if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
907
918
  }
908
919
  }, pollInterval)
909
920
  }
910
921
 
911
- async function connectJobStream(): Promise<void> {
912
- if (disposed || paused || streamConnecting || streamConnected) return
913
- streamConnecting = true
914
- streamController = new AbortController()
915
- try {
916
- const response = await transport(`${baseUrl}/jobs/events`, {
917
- method: 'GET',
918
- headers: { Accept: 'text/event-stream' },
919
- signal: streamController.signal,
920
- })
921
- if (!response.ok) throw new Error(`Job stream failed (${response.status})`)
922
- if (!response.body) throw new Error('Job stream response has no readable body')
923
- streamConnected = true
924
- if (pollTimer !== undefined) {
925
- clearTimeout(pollTimer)
926
- pollTimer = undefined
927
- }
928
- await consumeJobStream(response.body)
929
- } catch (cause) {
930
- if (!isAbortError(cause) && !disposed) {
931
- if (jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) schedulePoll()
932
- }
933
- } finally {
934
- streamConnected = false
935
- streamConnecting = false
936
- streamController = undefined
937
- if (!disposed && !paused) scheduleStreamReconnect()
938
- }
922
+ function clearPoll(): void {
923
+ if (pollTimer === undefined) return
924
+ clearTimeout(pollTimer)
925
+ pollTimer = undefined
939
926
  }
940
927
 
941
- async function consumeJobStream(body: ReadableStream<Uint8Array>): Promise<void> {
942
- const reader = body.getReader()
943
- const decoder = new TextDecoder()
944
- let buffer = ''
945
- try {
946
- while (!disposed) {
947
- const { done, value } = await reader.read()
948
- buffer += decoder.decode(value, { stream: !done })
949
- const blocks = buffer.split(/\r?\n\r?\n/)
950
- buffer = blocks.pop() ?? ''
951
- for (const block of blocks) applyJobStreamEvent(block)
952
- if (done) {
953
- if (buffer.trim()) applyJobStreamEvent(buffer)
954
- return
955
- }
956
- }
957
- } finally {
958
- reader.releaseLock()
959
- }
928
+ function schedulePollForActiveJobs(): void {
929
+ if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
960
930
  }
961
931
 
962
- function applyJobStreamEvent(block: string): void {
963
- let event = 'message'
964
- const data: string[] = []
965
- for (const line of block.split(/\r?\n/)) {
966
- if (!line || line.startsWith(':')) continue
967
- if (line.startsWith('event:')) event = line.slice('event:'.length).trim()
968
- if (line.startsWith('data:')) data.push(line.slice('data:'.length).trimStart())
969
- }
970
- if (!data.length) return
971
- const payload = JSON.parse(data.join('\n')) as (
972
- GeneratedAnalysisJobInput | { jobs: GeneratedAnalysisJobInput[] }
973
- )
974
- if (event === 'snapshot' && 'jobs' in payload) {
932
+ function applyJobStreamMessage(message: EventStreamMessage<PluginJobStreamData>): void {
933
+ if (message.event === 'snapshot' && 'jobs' in message.data) {
934
+ const payload = message.data
975
935
  replaceJobs(payload.jobs)
976
- } else if (event === 'job' || event === 'terminal') {
977
- upsertJob(payload as GeneratedAnalysisJobInput)
936
+ } else if (message.event === 'job' || message.event === 'terminal') {
937
+ upsertJob(message.data as GeneratedAnalysisJobInput)
978
938
  }
979
939
  error.value = null
980
940
  }
981
941
 
982
- function scheduleStreamReconnect(): void {
983
- if (disposed || paused || streamReconnectTimer !== undefined) return
984
- streamReconnectTimer = setTimeout(() => {
985
- streamReconnectTimer = undefined
986
- void connectJobStream()
987
- }, streamReconnectDelay)
988
- }
989
-
990
942
  async function cancelJob(job: GeneratedAnalysisJob | string): Promise<GeneratedAnalysisJob> {
991
943
  const jobId = typeof job === 'string' ? job : job.job_id
992
944
  const revision = beginJobOperation(jobId)
@@ -1030,19 +982,17 @@ function createPluginJobs(
1030
982
  deleteJob: async job => deleteJob(job as GeneratedAnalysisJob),
1031
983
  clearFinishedJobs: async () => clearFinishedJobs(),
1032
984
  }
985
+
1033
986
  function pause(): void {
1034
987
  if (disposed || paused) return
1035
988
  paused = true
1036
989
  status.value = 'paused'
1037
- if (pollTimer !== undefined) clearTimeout(pollTimer)
1038
- if (streamReconnectTimer !== undefined) clearTimeout(streamReconnectTimer)
1039
- pollTimer = undefined
1040
- streamReconnectTimer = undefined
990
+ clearPoll()
1041
991
  for (const jobId of resultRetryTimers.keys()) clearResultRetry(jobId)
1042
992
  for (const jobId of downloadRefreshTimers.keys()) {
1043
993
  clearManagedDownloadRefresh(jobId)
1044
994
  }
1045
- streamController?.abort()
995
+ jobStream.stop()
1046
996
  }
1047
997
 
1048
998
  function resume(): void {
@@ -1053,8 +1003,8 @@ function createPluginJobs(
1053
1003
  for (const [jobId, result] of Object.entries(resultCache.value)) {
1054
1004
  scheduleManagedDownloadRefresh(jobId, managedDownloadRefreshDelay(result))
1055
1005
  }
1056
- if (streamEnabled) void connectJobStream()
1057
- else if (jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) schedulePoll()
1006
+ if (streamEnabled) jobStream.start()
1007
+ else if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
1058
1008
  }
1059
1009
 
1060
1010
  function dispose(): void {
@@ -1062,10 +1012,7 @@ function createPluginJobs(
1062
1012
  disposed = true
1063
1013
  paused = true
1064
1014
  status.value = 'disposed'
1065
- if (pollTimer !== undefined) clearTimeout(pollTimer)
1066
- if (streamReconnectTimer !== undefined) clearTimeout(streamReconnectTimer)
1067
- pollTimer = undefined
1068
- streamReconnectTimer = undefined
1015
+ clearPoll()
1069
1016
  for (const timer of resultRetryTimers.values()) clearTimeout(timer)
1070
1017
  resultRetryTimers.clear()
1071
1018
  resultRetryAttempts.clear()
@@ -1075,7 +1022,7 @@ function createPluginJobs(
1075
1022
  for (const jobId of jobWaiters.keys()) rejectJobWaiters(jobId, cause)
1076
1023
  jobRevisions.clear()
1077
1024
  pendingJobOperations.clear()
1078
- streamController?.abort()
1025
+ jobStream.stop()
1079
1026
  }
1080
1027
 
1081
1028
  async function request<T>(path: string, init: RequestInit): Promise<T> {
@@ -1229,10 +1176,6 @@ function createGeneratedSessionStorage(candidate?: Storage): GeneratedSessionSto
1229
1176
  }
1230
1177
  }
1231
1178
 
1232
- function isAbortError(error: unknown): boolean {
1233
- return error instanceof DOMException && error.name === 'AbortError'
1234
- }
1235
-
1236
1179
  function isSessionNotFoundError(error: unknown): boolean {
1237
1180
  return error instanceof GeneratedAnalysisRequestError
1238
1181
  && error.status === 404
@@ -485,7 +485,7 @@ export function usePluginJobCenter(
485
485
  }
486
486
  }
487
487
 
488
- /** Backward-compatible name for the plugin job-center controller. */
488
+ /** @deprecated Use `usePluginJobCenter()` for the plugin job-center controller. */
489
489
  export function useJobsStatusTray(
490
490
  options: UseJobsStatusTrayOptions = {},
491
491
  ): UseJobsStatusTrayReturn {
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ /** Demonstrates `useListReorder`: drag a row, or focus a handle and press Alt with the arrow keys. */
3
+ import { ref } from 'vue'
4
+ import { useListReorder } from './useListReorder'
5
+
6
+ interface Step { id: string; name: string }
7
+
8
+ const steps = ref<Step[]>([
9
+ { id: 's1', name: 'Thaw samples' },
10
+ { id: 's2', name: 'Quench metabolism' },
11
+ { id: 's3', name: 'Extract metabolites' },
12
+ { id: 's4', name: 'Dry down' },
13
+ ])
14
+
15
+ const handles = new Map<string, HTMLElement>()
16
+
17
+ function setHandle(id: string, el: Element | null) {
18
+ if (el instanceof HTMLElement) handles.set(id, el)
19
+ else handles.delete(id)
20
+ }
21
+
22
+ const {
23
+ draggingId,
24
+ dragOverId,
25
+ announcement,
26
+ announcerStyle,
27
+ onDragStart,
28
+ onDragOver,
29
+ onDragLeave,
30
+ onDrop,
31
+ onDragEnd,
32
+ onKeydown,
33
+ } = useListReorder<Step>({
34
+ items: () => steps.value,
35
+ getId: (step) => step.id,
36
+ getLabel: (step) => step.name,
37
+ getElement: (id) => handles.get(id),
38
+ itemNoun: 'Step',
39
+ onReorder: ({ items }) => {
40
+ steps.value = items
41
+ },
42
+ })
43
+
44
+ function initState() {
45
+ return { showAnnouncement: true }
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <Story title="Composables/useListReorder">
51
+ <Variant title="Playground" :init-state="initState">
52
+ <template #default="{ state }">
53
+ <div style="padding: 2rem; max-width: 600px;">
54
+ <p class="mint-story-hint">
55
+ Drag a row, or focus its handle and press Alt with the up/down arrow keys.
56
+ </p>
57
+ <ul class="mint-story-list">
58
+ <li
59
+ v-for="(step, index) in steps"
60
+ :key="step.id"
61
+ class="mint-story-row"
62
+ :class="{
63
+ 'mint-story-row--dragging': draggingId === step.id,
64
+ 'mint-story-row--over': dragOverId === step.id,
65
+ }"
66
+ draggable="true"
67
+ @dragstart="onDragStart($event, step, index)"
68
+ @dragover="onDragOver($event, step, index)"
69
+ @dragleave="onDragLeave"
70
+ @drop="onDrop($event, step, index)"
71
+ @dragend="onDragEnd"
72
+ >
73
+ <button
74
+ :ref="(el) => setHandle(step.id, el as Element | null)"
75
+ type="button"
76
+ class="mint-story-handle"
77
+ :aria-label="`Reorder ${step.name}. Press Alt with the arrow keys to move it.`"
78
+ @keydown="onKeydown($event, step, index)"
79
+ >
80
+ ⠿
81
+ </button>
82
+ <span>{{ index + 1 }}. {{ step.name }}</span>
83
+ </li>
84
+ </ul>
85
+
86
+ <p v-if="state.showAnnouncement" class="mint-story-hint">
87
+ Live region: <em>{{ announcement || '—' }}</em>
88
+ </p>
89
+ <div role="status" aria-live="polite" :style="announcerStyle">{{ announcement }}</div>
90
+ </div>
91
+ </template>
92
+
93
+ <template #controls="{ state }">
94
+ <HstCheckbox v-model="state.showAnnouncement" title="Show live-region text" />
95
+ </template>
96
+ </Variant>
97
+ </Story>
98
+ </template>
99
+
100
+ <style scoped>
101
+ .mint-story-list {
102
+ list-style: none;
103
+ margin: 0;
104
+ padding: 0;
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 0.25rem;
108
+ }
109
+
110
+ .mint-story-row {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 0.5rem;
114
+ padding: 0.5rem 0.75rem;
115
+ border: 1px solid var(--border-color);
116
+ border-radius: 0.5rem;
117
+ background: var(--bg-primary);
118
+ color: var(--text-primary);
119
+ }
120
+
121
+ .mint-story-row--dragging {
122
+ opacity: 0.5;
123
+ }
124
+
125
+ .mint-story-row--over {
126
+ border-color: var(--color-primary);
127
+ }
128
+
129
+ .mint-story-handle {
130
+ padding: 0;
131
+ border: 0;
132
+ background: none;
133
+ color: var(--text-muted);
134
+ cursor: grab;
135
+ font-size: 1rem;
136
+ }
137
+
138
+ .mint-story-hint {
139
+ margin: 0 0 0.75rem;
140
+ color: var(--text-secondary);
141
+ font-size: 0.8125rem;
142
+ }
143
+ </style>
@@ -0,0 +1,242 @@
1
+ import { nextTick, ref, type Ref } from 'vue'
2
+
3
+ /** Inline style for a visually hidden `aria-live` region (no global utility class in the SDK). */
4
+ export const REORDER_ANNOUNCER_STYLE = Object.freeze({
5
+ position: 'absolute',
6
+ width: '1px',
7
+ height: '1px',
8
+ padding: '0',
9
+ margin: '-1px',
10
+ overflow: 'hidden',
11
+ clip: 'rect(0, 0, 0, 0)',
12
+ whiteSpace: 'nowrap',
13
+ border: '0',
14
+ })
15
+
16
+ export interface UseReorderAnnouncerReturn {
17
+ /** Bind to a polite live region's text content. */
18
+ announcement: Ref<string>
19
+ /** Publishes a message to the live region. */
20
+ announce: (message: string) => void
21
+ /** Bind to the live region's `style` so it stays visually hidden. */
22
+ announcerStyle: typeof REORDER_ANNOUNCER_STYLE
23
+ }
24
+
25
+ /**
26
+ * Screen-reader announcements for drag/keyboard move interactions. Bind
27
+ * `announcement` to a `role="status" aria-live="polite"` element styled with
28
+ * `announcerStyle`.
29
+ */
30
+ export function useReorderAnnouncer(): UseReorderAnnouncerReturn {
31
+ const announcement = ref('')
32
+
33
+ function announce(message: string) {
34
+ // Re-announce identical consecutive messages by clearing first.
35
+ if (announcement.value === message) announcement.value = ''
36
+ void nextTick(() => {
37
+ announcement.value = message
38
+ })
39
+ }
40
+
41
+ return { announcement, announce, announcerStyle: REORDER_ANNOUNCER_STYLE }
42
+ }
43
+
44
+ export interface ListReorderMove<T> {
45
+ /** Index the item moved from, within `items()`. */
46
+ fromIndex: number
47
+ /** Index the item moved to, within `items()`. */
48
+ toIndex: number
49
+ /** The reordered array (a copy — `items()` is never mutated). */
50
+ items: T[]
51
+ /** Whether the move came from the keyboard or a pointer drag. */
52
+ source: 'keyboard' | 'drag'
53
+ }
54
+
55
+ export interface UseListReorderOptions<T> {
56
+ /** The list being reordered, in its canonical (not merely rendered) order. */
57
+ items: () => T[]
58
+ /** Stable id for an item; falls back to the render index when ids repeat. */
59
+ getId: (item: T, index: number) => string
60
+ /** Applies the move. The composable never mutates the source array. */
61
+ onReorder: (move: ListReorderMove<T>) => void
62
+ /** Human-readable name used in announcements. Defaults to the item's id. */
63
+ getLabel?: (item: T, index: number) => string
64
+ /** Blocks every reorder while it returns `true` (readonly / non-sortable lists). */
65
+ disabled?: () => boolean
66
+ /** Resolves the focusable element for an id so keyboard moves keep focus on the item. */
67
+ getElement?: (id: string) => HTMLElement | null | undefined
68
+ /** Noun used in announcements, e.g. `'Reagent'`. Default `'Item'`. */
69
+ itemNoun?: string
70
+ }
71
+
72
+ export interface UseListReorderReturn<T> {
73
+ /** Id of the item currently being dragged, or `null`. */
74
+ draggingId: Ref<string | null>
75
+ /** Id of the item the pointer is hovering as a drop target, or `null`. */
76
+ dragOverId: Ref<string | null>
77
+ isDragging: (id: string) => boolean
78
+ isDragOver: (id: string) => boolean
79
+ announcement: Ref<string>
80
+ announcerStyle: typeof REORDER_ANNOUNCER_STYLE
81
+ announce: (message: string) => void
82
+ /** Bind to the draggable element's `@dragstart`. */
83
+ onDragStart: (event: DragEvent, item: T, index: number) => void
84
+ /** Bind to `@dragover` — calls `preventDefault()` so the drop is accepted. */
85
+ onDragOver: (event: DragEvent, item: T, index: number) => void
86
+ /** Bind to `@dragleave`. */
87
+ onDragLeave: () => void
88
+ /** Bind to `@drop`. */
89
+ onDrop: (event: DragEvent, item: T, index: number) => void
90
+ /** Bind to `@dragend`. */
91
+ onDragEnd: () => void
92
+ /**
93
+ * Bind to the item's (or its drag handle's) `@keydown`: Alt+ArrowUp / Alt+ArrowLeft
94
+ * moves the item one position earlier, Alt+ArrowDown / Alt+ArrowRight one later.
95
+ */
96
+ onKeydown: (event: KeyboardEvent, item: T, index: number) => void
97
+ /** Moves an item between two indices of `items()` programmatically. */
98
+ move: (fromIndex: number, toIndex: number, source?: 'keyboard' | 'drag') => void
99
+ }
100
+
101
+ /** Pure helper: a copy of `items` with the entry at `from` moved to `to`. */
102
+ export function reorderList<T>(items: T[], from: number, to: number): T[] {
103
+ const next = [...items]
104
+ const [moved] = next.splice(from, 1)
105
+ next.splice(to, 0, moved)
106
+ return next
107
+ }
108
+
109
+ /**
110
+ * Keyboard-accessible list reordering shared by the SDK's drag-to-reorder lists.
111
+ * It owns the HTML5 drag state, the drop-index maths, and an Alt+Arrow keyboard
112
+ * alternative that announces every move through a polite live region — so a
113
+ * reorder never depends on a pointer.
114
+ */
115
+ export function useListReorder<T>(options: UseListReorderOptions<T>): UseListReorderReturn<T> {
116
+ const draggingId = ref<string | null>(null)
117
+ const dragOverId = ref<string | null>(null)
118
+ const { announcement, announce, announcerStyle } = useReorderAnnouncer()
119
+ const noun = options.itemNoun ?? 'Item'
120
+
121
+ function isDisabled(): boolean {
122
+ return options.disabled?.() === true
123
+ }
124
+
125
+ function idFor(item: T, index: number): string {
126
+ return options.getId(item, index)
127
+ }
128
+
129
+ /** Index within `items()`, resolved by id so filtered/sorted renders still map back. */
130
+ function indexOf(item: T, renderIndex: number): number {
131
+ const id = idFor(item, renderIndex)
132
+ const found = options.items().findIndex((candidate, index) => idFor(candidate, index) === id)
133
+ return found === -1 ? renderIndex : found
134
+ }
135
+
136
+ function labelFor(index: number): string {
137
+ const item = options.items()[index]
138
+ if (item === undefined) return noun
139
+ return options.getLabel?.(item, index) ?? idFor(item, index)
140
+ }
141
+
142
+ function clearDragState() {
143
+ draggingId.value = null
144
+ dragOverId.value = null
145
+ }
146
+
147
+ function move(fromIndex: number, toIndex: number, source: 'keyboard' | 'drag' = 'keyboard') {
148
+ if (isDisabled()) return
149
+ const items = options.items()
150
+ if (
151
+ fromIndex === toIndex ||
152
+ fromIndex < 0 ||
153
+ toIndex < 0 ||
154
+ fromIndex >= items.length ||
155
+ toIndex >= items.length
156
+ ) return
157
+
158
+ const movedId = idFor(items[fromIndex], fromIndex)
159
+ const label = labelFor(fromIndex)
160
+ options.onReorder({
161
+ fromIndex,
162
+ toIndex,
163
+ items: reorderList(items, fromIndex, toIndex),
164
+ source,
165
+ })
166
+ announce(`${label} moved to position ${toIndex + 1} of ${items.length}.`)
167
+ void nextTick(() => options.getElement?.(movedId)?.focus())
168
+ }
169
+
170
+ function onDragStart(event: DragEvent, item: T, index: number) {
171
+ if (isDisabled()) return
172
+ const id = idFor(item, index)
173
+ draggingId.value = id
174
+ if (event.dataTransfer) {
175
+ event.dataTransfer.effectAllowed = 'move'
176
+ event.dataTransfer.setData('text/plain', id)
177
+ }
178
+ }
179
+
180
+ function onDragOver(event: DragEvent, item: T, index: number) {
181
+ if (isDisabled() || draggingId.value === null) return
182
+ event.preventDefault()
183
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
184
+ const id = idFor(item, index)
185
+ dragOverId.value = id === draggingId.value ? null : id
186
+ }
187
+
188
+ function onDragLeave() {
189
+ dragOverId.value = null
190
+ }
191
+
192
+ function onDrop(event: DragEvent, item: T, index: number) {
193
+ if (isDisabled() || draggingId.value === null) {
194
+ clearDragState()
195
+ return
196
+ }
197
+ event.preventDefault()
198
+
199
+ const items = options.items()
200
+ const fromIndex = items.findIndex((candidate, i) => idFor(candidate, i) === draggingId.value)
201
+ const toIndex = indexOf(item, index)
202
+ clearDragState()
203
+ if (fromIndex === -1) return
204
+ move(fromIndex, toIndex, 'drag')
205
+ }
206
+
207
+ function onDragEnd() {
208
+ clearDragState()
209
+ }
210
+
211
+ function onKeydown(event: KeyboardEvent, item: T, index: number) {
212
+ if (isDisabled() || !event.altKey) return
213
+ const direction =
214
+ event.key === 'ArrowUp' || event.key === 'ArrowLeft'
215
+ ? -1
216
+ : event.key === 'ArrowDown' || event.key === 'ArrowRight'
217
+ ? 1
218
+ : 0
219
+ if (direction === 0) return
220
+
221
+ event.preventDefault()
222
+ const fromIndex = indexOf(item, index)
223
+ move(fromIndex, fromIndex + direction, 'keyboard')
224
+ }
225
+
226
+ return {
227
+ draggingId,
228
+ dragOverId,
229
+ isDragging: (id: string) => draggingId.value === id,
230
+ isDragOver: (id: string) => dragOverId.value === id,
231
+ announcement,
232
+ announcerStyle,
233
+ announce,
234
+ onDragStart,
235
+ onDragOver,
236
+ onDragLeave,
237
+ onDrop,
238
+ onDragEnd,
239
+ onKeydown,
240
+ move,
241
+ }
242
+ }