@morscherlab/mint-sdk 1.2.0-beta.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/README.md +60 -0
  2. package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-CJdNlCZb.js} +24 -15
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-DCXmwKNS.js} +5 -5
  5. package/dist/ExperimentSelectorModal-DCXmwKNS.js.map +1 -0
  6. package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-BYR20I4c.js} +5 -5
  7. package/dist/SettingsModal-BYR20I4c.js.map +1 -0
  8. package/dist/{EmptyState-NBqf2_8z.js → Skeleton-BAF3CKY7.js} +111 -93
  9. package/dist/Skeleton-BAF3CKY7.js.map +1 -0
  10. package/dist/__tests__/components/DataStates.test.d.ts +1 -0
  11. package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
  12. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  14. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  15. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  16. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  17. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  18. package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
  19. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  20. package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
  21. package/dist/apiErrors.d.ts +26 -0
  22. package/dist/components/ActionMenu.vue.d.ts +1 -1
  23. package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
  24. package/dist/components/AuditTrail.vue.d.ts +13 -0
  25. package/dist/components/BaseInput.vue.d.ts +1 -1
  26. package/dist/components/BaseSelect.vue.d.ts +1 -1
  27. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  28. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  29. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  30. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  31. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  32. package/dist/components/DataFrame.vue.d.ts +14 -2
  33. package/dist/components/DatePicker.vue.d.ts +2 -2
  34. package/dist/components/DateTimePicker.vue.d.ts +3 -1
  35. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  36. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  37. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  38. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  39. package/dist/components/FilePicker.vue.d.ts +32 -0
  40. package/dist/components/FileUploader.vue.d.ts +5 -2
  41. package/dist/components/FormulaInput.vue.d.ts +1 -1
  42. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  43. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  44. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  45. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  46. package/dist/components/NumberInput.vue.d.ts +1 -1
  47. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  48. package/dist/components/ProgressBar.vue.d.ts +1 -1
  49. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  50. package/dist/components/ReagentList.vue.d.ts +29 -1
  51. package/dist/components/ResourceCard.vue.d.ts +1 -1
  52. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  53. package/dist/components/SampleSelector.drag.d.ts +13 -0
  54. package/dist/components/SampleSelector.tree.d.ts +2 -0
  55. package/dist/components/SampleSelector.vue.d.ts +34 -1
  56. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  57. package/dist/components/SecretInput.vue.d.ts +1 -1
  58. package/dist/components/SequenceInput.vue.d.ts +1 -1
  59. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
  60. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  61. package/dist/components/TagsInput.vue.d.ts +1 -1
  62. package/dist/components/TimePicker.vue.d.ts +2 -1
  63. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  64. package/dist/components/UnitInput.vue.d.ts +1 -1
  65. package/dist/components/WellPlate.geometry.d.ts +15 -0
  66. package/dist/components/WellPlate.interaction.d.ts +13 -0
  67. package/dist/components/WellPlate.vue.d.ts +30 -1
  68. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  69. package/dist/components/index.d.ts +4 -0
  70. package/dist/components/index.js +8 -8
  71. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  72. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  73. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  74. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  75. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  76. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  77. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  78. package/dist/{components-B80gV0P6.js → components-DIum5hkx.js} +5415 -2753
  79. package/dist/components-DIum5hkx.js.map +1 -0
  80. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  81. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  82. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  83. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  84. package/dist/composables/filePicker/validation.d.ts +5 -0
  85. package/dist/composables/index.d.ts +3 -0
  86. package/dist/composables/index.js +10 -10
  87. package/dist/composables/useApi.d.ts +14 -7
  88. package/dist/composables/useDropdownState.d.ts +18 -2
  89. package/dist/composables/useEventStream.d.ts +9 -0
  90. package/dist/composables/useFileBrowser.d.ts +55 -0
  91. package/dist/composables/useFilePicker.d.ts +189 -0
  92. package/dist/composables/useListReorder.d.ts +90 -0
  93. package/dist/composables/useMenuKeyboard.d.ts +2 -2
  94. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  95. package/dist/composables/usePluginConfig.d.ts +3 -1
  96. package/dist/composables/useRequestSyncState.d.ts +42 -2
  97. package/dist/composables/useScheduleDrag.d.ts +1 -0
  98. package/dist/{composables-C8mmD5ta.js → composables-mFXsTA4g.js} +307 -74
  99. package/dist/composables-mFXsTA4g.js.map +1 -0
  100. package/dist/{useApi-CDBq6z23.js → errors-BLuqMee5.js} +178 -16
  101. package/dist/errors-BLuqMee5.js.map +1 -0
  102. package/dist/{experiment-Y9Iz7KYi.js → experiment-CVDRAnqp.js} +3 -8
  103. package/dist/experiment-CVDRAnqp.js.map +1 -0
  104. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  105. package/dist/experiment-utils-CV2Wza3z.js.map +1 -0
  106. package/dist/index.d.ts +1 -0
  107. package/dist/index.js +15 -15
  108. package/dist/install.js +2 -7
  109. package/dist/install.js.map +1 -1
  110. package/dist/permissions.d.ts +42 -1
  111. package/dist/permissions.js +20 -10
  112. package/dist/permissions.js.map +1 -1
  113. package/dist/stores/index.d.ts +1 -1
  114. package/dist/stores/index.js +3 -3
  115. package/dist/stores/settings.d.ts +31 -1
  116. package/dist/styles.css +15162 -32475
  117. package/dist/templates/index.js +2 -4
  118. package/dist/{templates-CK72aKO1.js → templates-CmGfmBRA.js} +291 -85
  119. package/dist/templates-CmGfmBRA.js.map +1 -0
  120. package/dist/types/fileBrowserTypes.d.ts +77 -0
  121. package/dist/types/filePicker.d.ts +77 -0
  122. package/dist/types/index.d.ts +2 -0
  123. package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-DldpxNOP.js} +234 -1121
  124. package/dist/useControlSchema-DldpxNOP.js.map +1 -0
  125. package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-C7hRiVQf.js} +22 -6
  126. package/dist/useDropdownState-C7hRiVQf.js.map +1 -0
  127. package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-CEcIaueJ.js} +4 -4
  128. package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
  129. package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-z5L_dUIy.js} +2 -2
  130. package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
  131. package/dist/utils/color.d.ts +13 -0
  132. package/dist/utils/errors.d.ts +9 -0
  133. package/dist/{useProtocolTemplates-Dq5Pzwyd.js → validation-uwoJGEb6.js} +231 -158
  134. package/dist/validation-uwoJGEb6.js.map +1 -0
  135. package/package.json +1 -1
  136. package/src/__tests__/apiErrors.test.ts +81 -0
  137. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  138. package/src/__tests__/components/DataFrame.test.ts +183 -11
  139. package/src/__tests__/components/DataStates.test.ts +176 -0
  140. package/src/__tests__/components/DatePicker.test.ts +76 -0
  141. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  142. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  143. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  144. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  145. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  146. package/src/__tests__/components/FilePicker.test.ts +487 -0
  147. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  148. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  149. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  150. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  151. package/src/__tests__/components/RackEditor.test.ts +35 -0
  152. package/src/__tests__/components/ReagentList.test.ts +64 -2
  153. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  154. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  155. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  156. package/src/__tests__/components/SegmentedControl.test.ts +107 -0
  157. package/src/__tests__/components/TimePicker.test.ts +61 -0
  158. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  159. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  160. package/src/__tests__/composables/useApi.test.ts +92 -9
  161. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  162. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  163. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  164. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  165. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  166. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  167. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  168. package/src/__tests__/stores/settings.test.ts +71 -1
  169. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  170. package/src/__tests__/utils/permissions.test.ts +68 -0
  171. package/src/apiErrors.ts +85 -0
  172. package/src/components/ActionMenu.vue +45 -88
  173. package/src/components/AlertBox.vue +0 -4
  174. package/src/components/AppAvatarMenu.vue +2 -14
  175. package/src/components/AppLayout.vue +0 -4
  176. package/src/components/AppPluginSwitcher.story.vue +87 -0
  177. package/src/components/AppPluginSwitcher.vue +2 -5
  178. package/src/components/AppSidebar.vue +0 -4
  179. package/src/components/AppToastContainer.vue +6 -8
  180. package/src/components/AppTopBar.vue +0 -4
  181. package/src/components/ArtifactSaveDialog.vue +0 -4
  182. package/src/components/ArtifactSelectorModal.vue +0 -4
  183. package/src/components/AuditTrail.story.vue +18 -0
  184. package/src/components/AuditTrail.vue +27 -7
  185. package/src/components/AutoGroupModal.vue +0 -4
  186. package/src/components/Avatar.vue +0 -4
  187. package/src/components/BaseButton.vue +0 -4
  188. package/src/components/BaseCheckbox.vue +0 -4
  189. package/src/components/BaseInput.vue +0 -4
  190. package/src/components/BaseModal.vue +0 -4
  191. package/src/components/BasePill.vue +0 -4
  192. package/src/components/BaseRadioGroup.vue +0 -4
  193. package/src/components/BaseSelect.vue +0 -4
  194. package/src/components/BaseSlider.vue +0 -4
  195. package/src/components/BaseTabs.vue +0 -4
  196. package/src/components/BaseTextarea.vue +0 -4
  197. package/src/components/BaseToggle.vue +0 -4
  198. package/src/components/BatchProgressList.story.vue +18 -0
  199. package/src/components/BatchProgressList.vue +32 -4
  200. package/src/components/Breadcrumb.vue +0 -4
  201. package/src/components/Calendar.vue +0 -4
  202. package/src/components/ChartContainer.story.vue +1 -1
  203. package/src/components/ChartContainer.vue +0 -4
  204. package/src/components/ChemicalFormula.vue +0 -4
  205. package/src/components/CollapsibleCard.vue +0 -4
  206. package/src/components/ColorSlider.vue +0 -4
  207. package/src/components/ConcentrationInput.vue +0 -4
  208. package/src/components/ConfirmDialog.vue +0 -4
  209. package/src/components/DataFrame.story.vue +14 -0
  210. package/src/components/DataFrame.vue +97 -23
  211. package/src/components/DatePicker.vue +73 -27
  212. package/src/components/DateTimePicker.vue +45 -17
  213. package/src/components/Divider.vue +0 -4
  214. package/src/components/DoseCalculator.vue +0 -4
  215. package/src/components/DropdownButton.vue +2 -14
  216. package/src/components/EmptyState.vue +0 -4
  217. package/src/components/ExperimentCodeBadge.vue +0 -4
  218. package/src/components/ExperimentDataViewer.story.vue +3 -0
  219. package/src/components/ExperimentDataViewer.vue +33 -13
  220. package/src/components/ExperimentPopover.vue +12 -7
  221. package/src/components/ExperimentSelectorModal.vue +0 -4
  222. package/src/components/ExperimentTimeline.story.vue +18 -0
  223. package/src/components/ExperimentTimeline.vue +81 -75
  224. package/src/components/FileBrowserModal.story.vue +203 -0
  225. package/src/components/FileBrowserModal.vue +384 -0
  226. package/src/components/FilePicker.story.vue +44 -0
  227. package/src/components/FilePicker.vue +284 -0
  228. package/src/components/FileUploader.vue +32 -18
  229. package/src/components/FitPanel.vue +0 -4
  230. package/src/components/FormActions.story.vue +96 -0
  231. package/src/components/FormActions.vue +0 -4
  232. package/src/components/FormBuilder.vue +0 -4
  233. package/src/components/FormField.vue +0 -4
  234. package/src/components/FormulaInput.vue +0 -4
  235. package/src/components/GeneratedPathInput.story.vue +68 -0
  236. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  237. package/src/components/GeneratedPlotlyResult.vue +34 -69
  238. package/src/components/GeneratedPluginView.story.vue +145 -0
  239. package/src/components/GeneratedPluginView.vue +10 -10
  240. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  241. package/src/components/GeneratedResultRenderer.vue +4 -9
  242. package/src/components/GroupAssigner.vue +67 -5
  243. package/src/components/IconButton.vue +0 -4
  244. package/src/components/InstrumentAlertLog.vue +0 -4
  245. package/src/components/InstrumentStateBadge.vue +0 -4
  246. package/src/components/InstrumentStatusCard.vue +0 -4
  247. package/src/components/JobsStatusTray.vue +0 -4
  248. package/src/components/LcmsSequenceTable.vue +81 -50
  249. package/src/components/LoadingSpinner.vue +0 -4
  250. package/src/components/MobileSupportGate.vue +0 -4
  251. package/src/components/MoleculeInput.vue +0 -4
  252. package/src/components/MultiSelect.vue +0 -4
  253. package/src/components/NumberInput.vue +0 -4
  254. package/src/components/PlateMapEditor.vue +3 -14
  255. package/src/components/PlotlyChart.story.vue +61 -0
  256. package/src/components/PlotlyChart.vue +233 -0
  257. package/src/components/PluginIcon.vue +0 -4
  258. package/src/components/ProgressBar.vue +0 -4
  259. package/src/components/ProtocolStepEditor.vue +2 -4
  260. package/src/components/RackEditor.vue +51 -70
  261. package/src/components/ReagentEditor.vue +0 -4
  262. package/src/components/ReagentList.story.vue +18 -0
  263. package/src/components/ReagentList.vue +127 -82
  264. package/src/components/ResourceCard.vue +0 -4
  265. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  266. package/src/components/RuntimeUpdateNotice.vue +4 -4
  267. package/src/components/SampleHierarchyTree.story.vue +18 -0
  268. package/src/components/SampleHierarchyTree.vue +32 -9
  269. package/src/components/SampleLegend.vue +2 -8
  270. package/src/components/SampleSelector.drag.ts +51 -0
  271. package/src/components/SampleSelector.story.vue +6 -0
  272. package/src/components/SampleSelector.tree.ts +2 -0
  273. package/src/components/SampleSelector.vue +49 -10
  274. package/src/components/SampleSelectorGroupNode.vue +19 -0
  275. package/src/components/ScheduleCalendar.story.vue +6 -0
  276. package/src/components/ScheduleCalendar.vue +82 -7
  277. package/src/components/ScientificNumber.vue +0 -4
  278. package/src/components/SectionCard.vue +0 -4
  279. package/src/components/SegmentedControl.vue +50 -7
  280. package/src/components/SequenceInput.vue +0 -4
  281. package/src/components/SequenceProgressBar.story.vue +110 -0
  282. package/src/components/SequenceProgressBar.vue +0 -4
  283. package/src/components/SettingsModal.vue +0 -4
  284. package/src/components/Skeleton.vue +0 -4
  285. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  286. package/src/components/SmartGroupManual.vue +0 -4
  287. package/src/components/StatusIndicator.vue +0 -4
  288. package/src/components/StepWizard.vue +0 -4
  289. package/src/components/TagsInput.vue +4 -6
  290. package/src/components/ThemeToggle.vue +0 -4
  291. package/src/components/TimePicker.vue +36 -7
  292. package/src/components/TimeRangeInput.vue +0 -4
  293. package/src/components/Tooltip.story.vue +3 -3
  294. package/src/components/Tooltip.vue +0 -4
  295. package/src/components/UnitInput.vue +0 -4
  296. package/src/components/WellPlate.geometry.ts +60 -1
  297. package/src/components/WellPlate.interaction.ts +44 -0
  298. package/src/components/WellPlate.story.vue +8 -0
  299. package/src/components/WellPlate.vue +53 -7
  300. package/src/components/fileBrowserPresentation.ts +39 -0
  301. package/src/components/index.ts +5 -0
  302. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  303. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  304. package/src/components/internal/DataStateInternal.vue +74 -0
  305. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  306. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  307. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  308. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  309. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  310. package/src/components/internal/FormSectionRenderer.vue +0 -4
  311. package/src/components/internal/WellEditPopupInternal.vue +0 -4
  312. package/src/composables/controlSchemaTypes.ts +34 -0
  313. package/src/composables/filePicker/localFiles.ts +58 -0
  314. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  315. package/src/composables/filePicker/usePickerSelection.ts +259 -0
  316. package/src/composables/filePicker/validation.ts +41 -0
  317. package/src/composables/index.ts +17 -0
  318. package/src/composables/useApi.ts +57 -13
  319. package/src/composables/useDropdownState.ts +35 -5
  320. package/src/composables/useEventStream.ts +14 -2
  321. package/src/composables/useFileBrowser.ts +314 -0
  322. package/src/composables/useFileImport.ts +2 -7
  323. package/src/composables/useFilePicker.ts +366 -0
  324. package/src/composables/useGeneratedAnalysis.ts +18 -26
  325. package/src/composables/useListReorder.story.vue +143 -0
  326. package/src/composables/useListReorder.ts +242 -0
  327. package/src/composables/useMenuKeyboard.ts +2 -2
  328. package/src/composables/useOptimisticMutation.ts +1 -15
  329. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  330. package/src/composables/usePluginClient.ts +3 -0
  331. package/src/composables/usePluginConfig.ts +3 -1
  332. package/src/composables/useRackEditor.ts +2 -11
  333. package/src/composables/useRequestSyncState.ts +98 -28
  334. package/src/composables/useResourceCrud.ts +1 -15
  335. package/src/composables/useScheduleDrag.ts +37 -0
  336. package/src/composables/useWellPainting.ts +5 -29
  337. package/src/composables/useWellPlateEditor.ts +1 -11
  338. package/src/index.ts +3 -0
  339. package/src/permissions.ts +48 -9
  340. package/src/stores/experiment.ts +2 -7
  341. package/src/stores/index.ts +9 -1
  342. package/src/stores/settings.ts +93 -1
  343. package/src/styles/components/audit-trail.css +5 -5
  344. package/src/styles/components/batch-progress-list.css +7 -0
  345. package/src/styles/components/color-slider.css +2 -2
  346. package/src/styles/components/data-state.css +30 -0
  347. package/src/styles/components/dataframe.css +29 -35
  348. package/src/styles/components/experiment-data-viewer.css +4 -4
  349. package/src/styles/components/experiment-timeline.css +3 -30
  350. package/src/styles/components/file-browser-modal.css +489 -0
  351. package/src/styles/components/file-picker.css +502 -0
  352. package/src/styles/components/form-builder.css +5 -5
  353. package/src/styles/components/icon-button.css +9 -0
  354. package/src/styles/components/lcms-sequence-table.css +16 -1
  355. package/src/styles/components/multi-select.css +1 -1
  356. package/src/styles/components/reagent-list.css +38 -31
  357. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  358. package/src/styles/components/sample-selector.css +9 -2
  359. package/src/styles/components/schedule-calendar.css +8 -0
  360. package/src/styles/components/scientific-number.css +1 -1
  361. package/src/styles/components/well-plate.css +8 -0
  362. package/src/styles/index.css +11 -0
  363. package/src/styles/variables.css +29 -0
  364. package/src/templates/builderPresetControls.ts +1 -1
  365. package/src/templates/componentPlateHelpers.ts +3 -1
  366. package/src/templates/doseResponseAdapters.ts +2 -13
  367. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  368. package/src/templates/plateMapBuilder.ts +2 -2
  369. package/src/templates/qpcrPlateBuilder.ts +1 -1
  370. package/src/templates/templateValidators.ts +1 -1
  371. package/src/templates/timeCourseBuilder.ts +2 -2
  372. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  373. package/src/types/fileBrowserTypes.ts +85 -0
  374. package/src/types/filePicker.ts +72 -0
  375. package/src/types/index.ts +14 -0
  376. package/src/utils/color.ts +51 -0
  377. package/src/utils/errors.ts +29 -0
  378. package/dist/EmptyState-NBqf2_8z.js.map +0 -1
  379. package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
  380. package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
  381. package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
  382. package/dist/components-B80gV0P6.js.map +0 -1
  383. package/dist/composables/autoGroupConstants.d.ts +0 -1
  384. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  385. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  386. package/dist/composables-C8mmD5ta.js.map +0 -1
  387. package/dist/experiment-Y9Iz7KYi.js.map +0 -1
  388. package/dist/experiment-utils-DV3DrvE_.js.map +0 -1
  389. package/dist/templates/builderDefaults.d.ts +0 -1
  390. package/dist/templates/plateGeometry.d.ts +0 -4
  391. package/dist/templates-CK72aKO1.js.map +0 -1
  392. package/dist/useApi-CDBq6z23.js.map +0 -1
  393. package/dist/useControlSchema-_OVhavKq.js.map +0 -1
  394. package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
  395. package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
  396. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  397. package/src/composables/autoGroupConstants.ts +0 -4
  398. package/src/composables/autoGroupGrouping.ts +0 -148
  399. package/src/composables/useAutoGroupInputSources.ts +0 -147
  400. package/src/templates/builderDefaults.ts +0 -11
  401. package/src/templates/plateGeometry.ts +0 -62
  402. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → apiErrors.test.d.ts} +0 -0
@@ -0,0 +1,305 @@
1
+ import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
2
+ import { reactive } from 'vue'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import PlotlyChart from '../../components/PlotlyChart.vue'
6
+
7
+ const plotly = vi.hoisted(() => ({
8
+ Plots: { resize: vi.fn() },
9
+ purge: vi.fn(),
10
+ react: vi.fn(),
11
+ }))
12
+
13
+ vi.mock('plotly.js-dist-min', () => ({ default: plotly }))
14
+ enableAutoUnmount(afterEach)
15
+
16
+ describe('PlotlyChart', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks()
19
+ plotly.react.mockReset().mockResolvedValue(undefined)
20
+ plotly.Plots.resize.mockReset().mockResolvedValue(undefined)
21
+ document.documentElement.classList.remove('dark')
22
+ })
23
+
24
+ afterEach(() => {
25
+ vi.unstubAllGlobals()
26
+ document.documentElement.classList.remove('dark')
27
+ document.documentElement.removeAttribute('style')
28
+ })
29
+
30
+ it('should merge MINT defaults with native Plotly props', async () => {
31
+ const trace = { type: 'scatter' as const, x: [1], y: [2] }
32
+ mount(PlotlyChart, {
33
+ props: {
34
+ data: [trace],
35
+ layout: { title: { text: 'QC' } },
36
+ config: { toImageButtonOptions: { filename: 'qc-export' } },
37
+ },
38
+ })
39
+ await flushPromises()
40
+
41
+ expect(plotly.react).toHaveBeenCalledWith(
42
+ expect.any(HTMLElement),
43
+ [trace],
44
+ expect.objectContaining({
45
+ autosize: true,
46
+ title: { text: 'QC' },
47
+ paper_bgcolor: 'rgba(0,0,0,0)',
48
+ plot_bgcolor: 'rgba(0,0,0,0)',
49
+ }),
50
+ expect.objectContaining({
51
+ responsive: true,
52
+ displaylogo: false,
53
+ toImageButtonOptions: { filename: 'qc-export' },
54
+ }),
55
+ )
56
+ })
57
+
58
+ it('should render the shared chart empty state when explicitly empty', async () => {
59
+ const wrapper = mount(PlotlyChart, {
60
+ props: { data: [], empty: true, emptyMessage: 'No peaks found' },
61
+ })
62
+ await flushPromises()
63
+
64
+ expect(wrapper.text()).toContain('No peaks found')
65
+ })
66
+
67
+ it('should show a safe error when Plotly rejects rendering', async () => {
68
+ plotly.react.mockRejectedValue(new Error('<script>unsafe details</script>'))
69
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
70
+ await flushPromises()
71
+
72
+ expect(wrapper.get('[role="alert"]').text()).toBe('Unable to display this chart.')
73
+ })
74
+
75
+ it('should rerender when immutable native props change', async () => {
76
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
77
+ await flushPromises()
78
+
79
+ await wrapper.setProps({ data: [{ x: [3], y: [4] }] })
80
+ await flushPromises()
81
+
82
+ expect(plotly.react).toHaveBeenCalledTimes(2)
83
+ })
84
+
85
+ it('should rerender when the MINT dark theme changes', async () => {
86
+ let themeChanged: MutationCallback | undefined
87
+ vi.stubGlobal('MutationObserver', class {
88
+ constructor(callback: MutationCallback) {
89
+ themeChanged = callback
90
+ }
91
+
92
+ observe(): void {}
93
+ disconnect(): void {}
94
+ takeRecords(): MutationRecord[] { return [] }
95
+ })
96
+ mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
97
+ await flushPromises()
98
+
99
+ document.documentElement.classList.add('dark')
100
+ themeChanged?.([], {} as MutationObserver)
101
+ await flushPromises()
102
+
103
+ expect(plotly.react).toHaveBeenCalledTimes(2)
104
+ })
105
+
106
+ it('should resize the Plotly canvas when its container changes size', async () => {
107
+ let resize: ResizeObserverCallback | undefined
108
+ vi.stubGlobal('ResizeObserver', class {
109
+ constructor(callback: ResizeObserverCallback) {
110
+ resize = callback
111
+ }
112
+
113
+ observe(): void {}
114
+ disconnect(): void {}
115
+ })
116
+ mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
117
+ await flushPromises()
118
+
119
+ resize?.([], {} as ResizeObserver)
120
+
121
+ expect(plotly.Plots.resize).toHaveBeenCalledTimes(1)
122
+ })
123
+
124
+ it('should observe the Plotly canvas after leaving the empty state', async () => {
125
+ const observe = vi.fn()
126
+ vi.stubGlobal('ResizeObserver', class {
127
+ observe = observe
128
+ disconnect(): void {}
129
+ })
130
+ const wrapper = mount(PlotlyChart, {
131
+ props: { data: [], empty: true },
132
+ })
133
+ await flushPromises()
134
+
135
+ await wrapper.setProps({ empty: false })
136
+ await flushPromises()
137
+
138
+ expect(observe).toHaveBeenCalledWith(expect.any(HTMLElement))
139
+ })
140
+
141
+ it('should render once when data arrives as loading completes', async () => {
142
+ const wrapper = mount(PlotlyChart, {
143
+ props: { data: [], loading: true },
144
+ })
145
+ await flushPromises()
146
+
147
+ await wrapper.setProps({ data: [{ x: [1], y: [2] }], loading: false })
148
+ await flushPromises()
149
+
150
+ expect(plotly.react).toHaveBeenCalledTimes(1)
151
+ })
152
+
153
+ it('should serialize renders and use the latest props after an in-flight render', async () => {
154
+ let finish!: () => void
155
+ plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
156
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
157
+ await flushPromises()
158
+ await wrapper.setProps({ data: [{ x: [2] }] })
159
+ await wrapper.setProps({ data: [{ x: [3] }] })
160
+ await flushPromises()
161
+ expect(plotly.react).toHaveBeenCalledTimes(1)
162
+
163
+ finish()
164
+ await flushPromises()
165
+ expect(plotly.react).toHaveBeenCalledTimes(2)
166
+ expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [3] }])
167
+ })
168
+
169
+ it('should skip rendering when unmounted before the initial DOM update', async () => {
170
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
171
+ wrapper.unmount()
172
+ await flushPromises()
173
+ expect(plotly.react).not.toHaveBeenCalled()
174
+ })
175
+
176
+ it('should recover from a stale failure without overwriting the newer render', async () => {
177
+ let reject!: (error: Error) => void
178
+ plotly.react.mockImplementationOnce(() => new Promise<void>((_resolve, fail) => { reject = fail }))
179
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
180
+ await flushPromises()
181
+ await wrapper.setProps({ data: [{ x: [2] }] })
182
+ reject(new Error('Old render failed'))
183
+ await flushPromises()
184
+ expect(plotly.react).toHaveBeenCalledTimes(2)
185
+ expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [2] }])
186
+ expect(wrapper.find('[role="alert"]').exists()).toBe(false)
187
+ })
188
+
189
+ it('should purge an in-flight canvas removed by loading and then render its replacement', async () => {
190
+ let finish!: () => void
191
+ plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
192
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
193
+ await flushPromises()
194
+ const element = wrapper.get('.mint-plotly-chart__plot').element
195
+ await wrapper.setProps({ loading: true })
196
+ await wrapper.setProps({ loading: false, data: [{ x: [3] }] })
197
+ finish()
198
+ await flushPromises()
199
+ expect(plotly.purge).toHaveBeenCalledWith(element)
200
+ expect(plotly.react).toHaveBeenCalledTimes(2)
201
+ expect(plotly.react.mock.lastCall?.[0]).toBe(wrapper.get('.mint-plotly-chart__plot').element)
202
+ expect(plotly.react.mock.lastCall?.[0]).not.toBe(element)
203
+ })
204
+
205
+ it('should clean up a render that completes after unmount', async () => {
206
+ let finish!: () => void
207
+ plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
208
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
209
+ await flushPromises()
210
+ const element = wrapper.get('.mint-plotly-chart__plot').element
211
+ wrapper.unmount()
212
+ plotly.purge.mockClear()
213
+ finish()
214
+ await flushPromises()
215
+ expect(plotly.purge).toHaveBeenCalledWith(element)
216
+ expect(plotly.react).toHaveBeenCalledTimes(1)
217
+ })
218
+
219
+ it('should purge the removed canvas and render new data after loading or empty states', async () => {
220
+ const disconnect = vi.fn()
221
+ vi.stubGlobal('ResizeObserver', class {
222
+ observe = vi.fn()
223
+ unobserve = vi.fn()
224
+ disconnect = disconnect
225
+ })
226
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
227
+ await flushPromises()
228
+ for (const state of ['loading', 'empty'] as const) {
229
+ const element = wrapper.get('.mint-plotly-chart__plot').element
230
+ await wrapper.setProps({ [state]: true })
231
+ await flushPromises()
232
+ expect(plotly.purge).toHaveBeenCalledWith(element)
233
+ expect(disconnect).toHaveBeenCalled()
234
+ await wrapper.setProps({ [state]: false, data: [{ x: [2] }] })
235
+ await flushPromises()
236
+ expect(wrapper.get('.mint-plotly-chart__plot').element).not.toBe(element)
237
+ }
238
+ expect(plotly.react).toHaveBeenCalledTimes(3)
239
+ })
240
+
241
+ it('should use MINT colors and protect reactive native data and nested layout from Plotly mutations', async () => {
242
+ document.documentElement.style.setProperty('--text-primary', '#f8fafc')
243
+ document.documentElement.style.setProperty('--border-color', '#334155')
244
+ const data = reactive([{ x: [1], y: [2] }])
245
+ const layout = reactive({ title: { text: 'Original' }, xaxis: { title: { text: 'Sample' } } })
246
+ plotly.react.mockImplementationOnce((_element, traces, options) => {
247
+ traces[0].x.push(9)
248
+ options.title.text = 'Changed'
249
+ options.xaxis.title.text = 'Changed'
250
+ return Promise.resolve()
251
+ })
252
+ mount(PlotlyChart, { attachTo: document.body, props: { data, layout } })
253
+ await flushPromises()
254
+ expect(plotly.react.mock.lastCall?.[2].font.color).toBe('#f8fafc')
255
+ expect(plotly.react.mock.lastCall?.[2].xaxis.gridcolor).toBe('#334155')
256
+ expect(data[0].x).toEqual([1])
257
+ expect(layout.title.text).toBe('Original')
258
+ expect(layout.xaxis.title.text).toBe('Sample')
259
+ })
260
+
261
+ it('should accept nested reactive arrays, typed arrays, dates, and native export callbacks', async () => {
262
+ const x = reactive([new Date('2026-01-01')])
263
+ const y = new Float64Array([2])
264
+ const click = vi.fn()
265
+ mount(PlotlyChart, {
266
+ props: {
267
+ data: [{ x, y }],
268
+ config: { modeBarButtonsToAdd: [{ name: 'Export', title: 'Export', icon: 'camera', click }] },
269
+ },
270
+ })
271
+ await flushPromises()
272
+ expect(plotly.react).toHaveBeenCalledOnce()
273
+ const [, traces, , config] = plotly.react.mock.lastCall!
274
+ expect(traces[0].x).toEqual(x)
275
+ expect(traces[0].x).not.toBe(x)
276
+ expect(traces[0].x[0]).toBeInstanceOf(Date)
277
+ expect(traces[0].y).toEqual(y)
278
+ expect(traces[0].y).not.toBe(y)
279
+ expect(config.modeBarButtonsToAdd[0].click).toBe(click)
280
+ })
281
+
282
+ it('should ignore resize rejection when the canvas becomes hidden', async () => {
283
+ let resize!: ResizeObserverCallback
284
+ vi.stubGlobal('ResizeObserver', class {
285
+ constructor(callback: ResizeObserverCallback) { resize = callback }
286
+ observe(): void {}
287
+ disconnect(): void {}
288
+ })
289
+ plotly.Plots.resize.mockRejectedValueOnce(new Error('Resize must be passed a displayed plot div'))
290
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
291
+ await flushPromises()
292
+ resize([], {} as ResizeObserver)
293
+ await flushPromises()
294
+ expect(wrapper.find('[role="alert"]').exists()).toBe(false)
295
+ })
296
+
297
+ it('should purge the Plotly canvas when unmounted', async () => {
298
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
299
+ await flushPromises()
300
+
301
+ wrapper.unmount()
302
+
303
+ expect(plotly.purge).toHaveBeenCalledTimes(1)
304
+ })
305
+ })
@@ -122,4 +122,39 @@ describe('RackEditor', () => {
122
122
  }),
123
123
  ])
124
124
  })
125
+
126
+ it('reorders rack tabs with Alt and the arrow keys', async () => {
127
+ const racks = makeRacks()
128
+ racks.push({ ...racks[0], id: 'rack-2', name: 'Rack 2', slot: 'G', wells: {} })
129
+
130
+ const wrapper = mount(RackEditor, { props: { modelValue: racks } })
131
+
132
+ await wrapper.findAll('.mint-rack-editor__tab')[0].trigger('keydown', {
133
+ key: 'ArrowRight',
134
+ altKey: true,
135
+ })
136
+ await nextTick()
137
+
138
+ expect(wrapper.emitted('rack-reorder')?.at(-1)?.[0]).toEqual(['rack-2', 'rack-1'])
139
+ expect(wrapper.findAll('.mint-rack-editor__tab').map(tab => tab.text()))
140
+ .toEqual([expect.stringContaining('Rack 2'), expect.stringContaining('Rack 1')])
141
+ expect(wrapper.get('[role="status"]').attributes('aria-live')).toBe('polite')
142
+ })
143
+
144
+ it('does not reorder rack tabs from the keyboard when reordering is off', async () => {
145
+ const racks = makeRacks()
146
+ racks.push({ ...racks[0], id: 'rack-2', name: 'Rack 2', slot: 'G', wells: {} })
147
+
148
+ const wrapper = mount(RackEditor, {
149
+ props: { modelValue: racks, allowReorder: false },
150
+ })
151
+
152
+ await wrapper.findAll('.mint-rack-editor__tab')[0].trigger('keydown', {
153
+ key: 'ArrowRight',
154
+ altKey: true,
155
+ })
156
+ await nextTick()
157
+
158
+ expect(wrapper.emitted('rack-reorder')).toBeUndefined()
159
+ })
125
160
  })
@@ -63,7 +63,7 @@ describe('ReagentList', () => {
63
63
  },
64
64
  })
65
65
 
66
- await wrapper.findAll('th')[1].trigger('click')
66
+ await wrapper.findAll('th')[1].get('.mint-reagent-list__sort-btn').trigger('click')
67
67
 
68
68
  const rows = wrapper.findAll('tbody tr')
69
69
  expect(rows.map(row => row.text())).toEqual([
@@ -72,11 +72,73 @@ describe('ReagentList', () => {
72
72
  expect.stringContaining('Pyruvate'),
73
73
  ])
74
74
 
75
- await wrapper.findAll('th')[1].trigger('click')
75
+ await wrapper.findAll('th')[1].get('.mint-reagent-list__sort-btn').trigger('click')
76
76
  expect(wrapper.findAll('tbody tr').map(row => row.text())).toEqual([
77
77
  expect.stringContaining('Lactate'),
78
78
  expect.stringContaining('Glucose'),
79
79
  expect.stringContaining('Pyruvate'),
80
80
  ])
81
81
  })
82
+
83
+ it('exposes sortable headers as buttons that sort from the keyboard', async () => {
84
+ const wrapper = mount(ReagentList, {
85
+ props: { modelValue: reagents, readonly: true, columns: ['name', 'catalog'] },
86
+ })
87
+
88
+ const nameHeader = () => wrapper.findAll('th')[0]
89
+ expect(nameHeader().attributes('aria-sort')).toBe('none')
90
+ expect(nameHeader().get('.mint-reagent-list__sort-btn').element.tagName).toBe('BUTTON')
91
+
92
+ await nameHeader().get('.mint-reagent-list__sort-btn').trigger('keydown', { key: 'Enter' })
93
+ expect(nameHeader().attributes('aria-sort')).toBe('ascending')
94
+
95
+ await nameHeader().get('.mint-reagent-list__sort-btn').trigger('keydown', { key: ' ' })
96
+ expect(nameHeader().attributes('aria-sort')).toBe('descending')
97
+ })
98
+
99
+ it('omits aria-sort when sorting is disabled', () => {
100
+ const wrapper = mount(ReagentList, {
101
+ props: { modelValue: reagents, readonly: true, sortable: false, columns: ['name'] },
102
+ })
103
+
104
+ expect(wrapper.findAll('th')[0].attributes('aria-sort')).toBeUndefined()
105
+ })
106
+
107
+ it('reorders reagents from the drag handle with Alt and the arrow keys', async () => {
108
+ const wrapper = mount(ReagentList, {
109
+ props: { modelValue: reagents, columns: ['name'] },
110
+ })
111
+
112
+ const handle = wrapper.findAll('.mint-reagent-list__drag-handle-btn')[0]
113
+ expect(handle.attributes('aria-label')).toContain('Reorder Glucose')
114
+
115
+ await handle.trigger('keydown', { key: 'ArrowDown', altKey: true })
116
+
117
+ expect(wrapper.emitted('reorder')?.[0]).toEqual(['reagent-1', 0, 1])
118
+ expect(
119
+ (wrapper.emitted('update:modelValue')?.[0]?.[0] as Reagent[]).map(reagent => reagent.id),
120
+ ).toEqual(['reagent-2', 'reagent-1'])
121
+ })
122
+
123
+ it('does not reorder from the keyboard while readonly', async () => {
124
+ const wrapper = mount(ReagentList, {
125
+ props: { modelValue: reagents, readonly: true, columns: ['name'] },
126
+ })
127
+
128
+ expect(wrapper.find('.mint-reagent-list__drag-handle-btn').exists()).toBe(false)
129
+ })
130
+
131
+ it('announces a keyboard reorder in a polite live region', async () => {
132
+ const wrapper = mount(ReagentList, {
133
+ props: { modelValue: reagents, columns: ['name'] },
134
+ })
135
+
136
+ await wrapper.findAll('.mint-reagent-list__drag-handle-btn')[0]
137
+ .trigger('keydown', { key: 'ArrowDown', altKey: true })
138
+ await wrapper.vm.$nextTick()
139
+
140
+ const liveRegion = wrapper.get('[role="status"]')
141
+ expect(liveRegion.attributes('aria-live')).toBe('polite')
142
+ expect(liveRegion.text()).toBe('Glucose moved to position 2 of 2.')
143
+ })
82
144
  })
@@ -6,9 +6,21 @@ const retiredSelectors: Record<string, readonly string[]> = {
6
6
  'app-plugin-switcher.css': [
7
7
  'mint-plugin-switcher__swatch',
8
8
  ],
9
+ 'audit-trail.css': [
10
+ 'mint-audit-trail__empty',
11
+ ],
12
+ 'dataframe.css': [
13
+ 'mint-dataframe__empty',
14
+ 'mint-dataframe__empty-icon',
15
+ 'mint-dataframe__empty-text',
16
+ 'mint-dataframe__loading-spinner',
17
+ ],
9
18
  'dose-calculator.css': [
10
19
  'mint-dose-calculator__mw-input',
11
20
  ],
21
+ 'experiment-data-viewer.css': [
22
+ 'mint-data-viewer__empty',
23
+ ],
12
24
  'experiment-popover.css': [
13
25
  'mint-experiment-popover__check-icon',
14
26
  'mint-experiment-popover__divider',
@@ -24,13 +36,31 @@ const retiredSelectors: Record<string, readonly string[]> = {
24
36
  'mint-experiment-selector__filters',
25
37
  'mint-experiment-selector__status-filter',
26
38
  ],
39
+ 'experiment-timeline.css': [
40
+ 'mint-timeline__empty',
41
+ 'mint-timeline__empty-btn',
42
+ 'mint-timeline__empty-content',
43
+ 'mint-timeline__empty-text',
44
+ ],
45
+ 'lcms-sequence-table.css': [
46
+ 'mint-lcms-sequence-table__empty',
47
+ ],
27
48
  'protocol-step-editor.css': [
28
49
  'mint-protocol-editor__field-row',
29
50
  ],
30
51
  'reagent-editor.css': [
31
52
  'mint-reagent-editor__row',
32
53
  ],
54
+ 'reagent-list.css': [
55
+ 'mint-reagent-list__empty',
56
+ 'mint-reagent-list__empty-btn',
57
+ 'mint-reagent-list__empty-icon',
58
+ 'mint-reagent-list__empty-text',
59
+ ],
33
60
  'sample-hierarchy-tree.css': [
61
+ 'mint-sample-tree__empty',
62
+ 'mint-sample-tree__empty-icon',
63
+ 'mint-sample-tree__empty-text',
34
64
  'mint-sample-tree__node--selected',
35
65
  ],
36
66
  'sample-selector.css': [
@@ -38,6 +68,7 @@ const retiredSelectors: Record<string, readonly string[]> = {
38
68
  'mint-popover-enter-from',
39
69
  'mint-popover-leave-active',
40
70
  'mint-popover-leave-to',
71
+ 'mint-sample-selector__empty',
41
72
  'mint-sample-selector__level-badge',
42
73
  'mint-sample-selector__level-btn',
43
74
  'mint-sample-selector__level-desc',
@@ -1,4 +1,4 @@
1
- import { ref } from 'vue'
1
+ import { nextTick, ref } from 'vue'
2
2
  import { describe, expect, it } from 'vitest'
3
3
  import type { SampleGroup } from '../../types'
4
4
  import { useSampleSelectorDrag } from '../../components/SampleSelector.drag'
@@ -9,6 +9,10 @@ type TestDragEvent = Omit<DragEvent, 'dataTransfer'> & {
9
9
  defaultPrevented: boolean
10
10
  }
11
11
 
12
+ function keyboardEvent(key: string, altKey = true): KeyboardEvent {
13
+ return { key, altKey, preventDefault: () => {} } as unknown as KeyboardEvent
14
+ }
15
+
12
16
  function createDragEvent(options: { clientY?: number; top?: number; height?: number } = {}) {
13
17
  const data: Record<string, string> = {}
14
18
  const transfer = {
@@ -123,4 +127,50 @@ describe('SampleSelector drag helpers', () => {
123
127
 
124
128
  expect(drag.reorderTarget.value).toBe('Liver / High / 10min')
125
129
  })
130
+
131
+ it('reorders sibling groups with Alt and the arrow keys', async () => {
132
+ const state = ref<SampleGroup[]>([
133
+ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] },
134
+ { name: 'Liver / High / 10min', color: '#222222', samples: ['L2'] },
135
+ { name: 'Liver / High / 20min', color: '#333333', samples: ['L3'] },
136
+ ])
137
+ const drag = useSampleSelectorDrag(state)
138
+
139
+ drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown'))
140
+
141
+ expect(state.value.map(group => group.name)).toEqual([
142
+ 'Liver / High / 10min',
143
+ 'Liver / High / 5min',
144
+ 'Liver / High / 20min',
145
+ ])
146
+
147
+ await nextTick()
148
+ expect(drag.announcement.value).toBe('Liver / High / 5min moved down to position 2 of 3.')
149
+ })
150
+
151
+ it('keeps a group inside its own family and ignores arrows without Alt', () => {
152
+ const state = ref<SampleGroup[]>([
153
+ { name: 'Liver / High / 5min', color: '#111111', samples: ['L1'] },
154
+ { name: 'Muscle / High / 5min', color: '#222222', samples: ['M1'] },
155
+ ])
156
+ const drag = useSampleSelectorDrag(state)
157
+ const before = state.value
158
+
159
+ drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown'))
160
+ drag.handleGroupReorderKeydown('Liver / High / 5min', 'sub', keyboardEvent('ArrowDown', false))
161
+
162
+ expect(state.value).toBe(before)
163
+ })
164
+
165
+ it('reorders whole major families with Alt and the arrow keys', () => {
166
+ const state = ref<SampleGroup[]>([
167
+ { name: 'Liver / High', color: '#111111', samples: ['L1'] },
168
+ { name: 'Muscle / High', color: '#222222', samples: ['M1'] },
169
+ ])
170
+ const drag = useSampleSelectorDrag(state)
171
+
172
+ drag.handleGroupReorderKeydown('Muscle', 'major', keyboardEvent('ArrowUp'))
173
+
174
+ expect(state.value.map(group => group.name)).toEqual(['Muscle / High', 'Liver / High'])
175
+ })
126
176
  })
@@ -41,4 +41,67 @@ describe('ScheduleCalendar', () => {
41
41
  expect(wrapper.findAll('.mint-schedule__month-cell')).toHaveLength(42)
42
42
  expect(wrapper.find('.mint-schedule__title').text()).toBe('May 2024')
43
43
  })
44
+
45
+ it('reschedules an event with Alt and the arrow keys', async () => {
46
+ const wrapper = mount(ScheduleCalendar, {
47
+ props: {
48
+ modelValue: new Date('2024-05-15T12:00:00'),
49
+ dayStartHour: 6,
50
+ dayEndHour: 10,
51
+ slotDuration: 30,
52
+ showNowIndicator: false,
53
+ events: [
54
+ {
55
+ id: 'run-1',
56
+ title: 'LCMS run',
57
+ start: '2024-05-15T07:00:00',
58
+ end: '2024-05-15T08:00:00',
59
+ },
60
+ ],
61
+ },
62
+ })
63
+
64
+ const event = wrapper.get('.mint-schedule__event')
65
+ expect(event.attributes('tabindex')).toBe('0')
66
+ expect(event.attributes('aria-label')).toContain('Press Alt with the up or down arrow keys')
67
+
68
+ await event.trigger('keydown', { key: 'ArrowDown', altKey: true })
69
+
70
+ const update = wrapper.emitted('event-update')?.[0]?.[0] as {
71
+ newStart: Date
72
+ newEnd: Date
73
+ }
74
+ expect(update.newStart.getHours()).toBe(7)
75
+ expect(update.newStart.getMinutes()).toBe(30)
76
+ expect(update.newEnd.getHours()).toBe(8)
77
+ expect(update.newEnd.getMinutes()).toBe(30)
78
+
79
+ await wrapper.vm.$nextTick()
80
+ expect(wrapper.get('[role="status"]').attributes('aria-live')).toBe('polite')
81
+ })
82
+
83
+ it('does not reschedule from the keyboard while readonly', async () => {
84
+ const wrapper = mount(ScheduleCalendar, {
85
+ props: {
86
+ modelValue: new Date('2024-05-15T12:00:00'),
87
+ dayStartHour: 6,
88
+ dayEndHour: 10,
89
+ slotDuration: 30,
90
+ showNowIndicator: false,
91
+ readonly: true,
92
+ events: [
93
+ {
94
+ id: 'run-1',
95
+ title: 'LCMS run',
96
+ start: '2024-05-15T07:00:00',
97
+ end: '2024-05-15T08:00:00',
98
+ },
99
+ ],
100
+ },
101
+ })
102
+
103
+ await wrapper.get('.mint-schedule__event').trigger('keydown', { key: 'ArrowDown', altKey: true })
104
+
105
+ expect(wrapper.emitted('event-update')).toBeUndefined()
106
+ })
44
107
  })