@morscherlab/mint-sdk 1.4.0-beta.2 → 1.4.0-beta.20

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 (330) hide show
  1. package/README.md +14 -3
  2. package/dist/{ActionMenu-CScINahU.js → AlertBox-CzLg0UaL.js} +40 -209
  3. package/dist/AlertBox-CzLg0UaL.js.map +1 -0
  4. package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-6oTT-gPO.js} +1249 -239
  5. package/dist/AppSidebar-6oTT-gPO.js.map +1 -0
  6. package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-CSls2Nuv.js} +2 -2
  7. package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-CSls2Nuv.js.map} +1 -1
  8. package/dist/ExperimentPopover-9l4QZlvQ.js +337 -0
  9. package/dist/ExperimentPopover-9l4QZlvQ.js.map +1 -0
  10. package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-pR5fK3pA.js} +244 -211
  11. package/dist/ExperimentSelectorModal-pR5fK3pA.js.map +1 -0
  12. package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-CJ7EAmOq.js} +2 -2
  13. package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-CJ7EAmOq.js.map} +1 -1
  14. package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-Dyitgzfp.js} +28 -27
  15. package/dist/SettingsModal-Dyitgzfp.js.map +1 -0
  16. package/dist/__stories__/experiment-helpers.d.ts +10 -0
  17. package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
  18. package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
  19. package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
  20. package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
  21. package/dist/components/AppSidebar.vue.d.ts +1 -1
  22. package/dist/components/AppTopBar.vue.d.ts +20 -5
  23. package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
  24. package/dist/components/AuditTrail.vue.d.ts +1 -1
  25. package/dist/components/BaseButton.vue.d.ts +4 -2
  26. package/dist/components/BaseSelect.vue.d.ts +2 -2
  27. package/dist/components/ChartContainer.vue.d.ts +2 -0
  28. package/dist/components/DatePicker.vue.d.ts +1 -1
  29. package/dist/components/ExperimentPopover.vue.d.ts +18 -0
  30. package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -5
  31. package/dist/components/FilePicker.vue.d.ts +1 -1
  32. package/dist/components/FileUploader.vue.d.ts +2 -2
  33. package/dist/components/JobsStatusTray.vue.d.ts +15 -4
  34. package/dist/components/PluginIcon.vue.d.ts +2 -2
  35. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  36. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  37. package/dist/components/SampleSelector.vue.d.ts +6 -6
  38. package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
  39. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  40. package/dist/components/StepWizard.vue.d.ts +3 -0
  41. package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
  42. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
  43. package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +1 -1
  44. package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
  45. package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
  46. package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
  47. package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
  48. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  49. package/dist/composables/createClient.d.ts +2 -0
  50. package/dist/composables/index.d.ts +1 -0
  51. package/dist/composables/pluginIdentity.d.ts +24 -0
  52. package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
  53. package/dist/composables/useAppExperiment.d.ts +8 -0
  54. package/dist/composables/useCalendarGrid.d.ts +1 -1
  55. package/dist/composables/useExperimentSelector.d.ts +6 -2
  56. package/dist/composables/useJobCenter.d.ts +41 -3
  57. package/dist/composables/useOnboarding.d.ts +119 -0
  58. package/dist/composables/usePlatform.d.ts +1 -0
  59. package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-DCUOx8ll.js} +2 -2
  60. package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-DCUOx8ll.js.map} +1 -1
  61. package/dist/{data-C2PwI_dn.js → data-DBnmeBZX.js} +179 -125
  62. package/dist/data-DBnmeBZX.js.map +1 -0
  63. package/dist/data.js +8 -8
  64. package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
  65. package/dist/experiment-DyO54FQB.js.map +1 -0
  66. package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-BFrKpHAH.js} +3 -3
  67. package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-BFrKpHAH.js.map} +1 -1
  68. package/dist/index.js +20 -20
  69. package/dist/{install-DAID6c2-.js → install-qjELyNwc.js} +15 -14
  70. package/dist/{install-DAID6c2-.js.map → install-qjELyNwc.js.map} +1 -1
  71. package/dist/install.js +1 -1
  72. package/dist/{lab-DLwRl_Ql.js → lab-UyTbokQC.js} +136 -148
  73. package/dist/lab-UyTbokQC.js.map +1 -0
  74. package/dist/lab.js +4 -3
  75. package/dist/{records-BzTcxekG.js → records-CR_pYzIP.js} +533 -335
  76. package/dist/records-CR_pYzIP.js.map +1 -0
  77. package/dist/{runtime-D6gVat6r.js → runtime-CoHhBP10.js} +417 -54
  78. package/dist/runtime-CoHhBP10.js.map +1 -0
  79. package/dist/runtime.d.ts +1 -1
  80. package/dist/runtime.js +7 -7
  81. package/dist/{shell-5ju97nOS.js → shell-BDuZf2zb.js} +10 -5
  82. package/dist/{shell-5ju97nOS.js.map → shell-BDuZf2zb.js.map} +1 -1
  83. package/dist/shell.d.ts +1 -0
  84. package/dist/shell.js +6 -6
  85. package/dist/stores/index.d.ts +1 -1
  86. package/dist/stores/index.js +2 -2
  87. package/dist/stores/settings.d.ts +4 -12
  88. package/dist/styles.css +2732 -2083
  89. package/dist/types/componentWorkflowTypes.d.ts +2 -0
  90. package/dist/types/components.d.ts +2 -0
  91. package/dist/types/generated/platformContext.d.ts +1 -0
  92. package/dist/types/generated-ui.d.ts +4 -0
  93. package/dist/types/index.d.ts +1 -1
  94. package/dist/types/jobs.d.ts +9 -0
  95. package/dist/types/platform.d.ts +1 -1
  96. package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-Bb2N4m40.js} +74 -4
  97. package/dist/useDropdownState-Bb2N4m40.js.map +1 -0
  98. package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-Dno1fFMq.js} +4 -4
  99. package/dist/{useExperimentSamples-CxcnxZ8h.js.map → useExperimentSamples-Dno1fFMq.js.map} +1 -1
  100. package/dist/{useListReorder-CcbEYxX-.js → useListReorder-CQ9OCScc.js} +183 -351
  101. package/dist/useListReorder-CQ9OCScc.js.map +1 -0
  102. package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-wL3kPrRT.js} +73 -59
  103. package/dist/useRackEditor-wL3kPrRT.js.map +1 -0
  104. package/dist/useRealtime-C3whhVvb.js +508 -0
  105. package/dist/useRealtime-C3whhVvb.js.map +1 -0
  106. package/dist/useRequest-CDkrXNU7.js +244 -0
  107. package/dist/useRequest-CDkrXNU7.js.map +1 -0
  108. package/dist/utils/time.d.ts +7 -0
  109. package/package.json +1 -1
  110. package/src/__stories__/experiment-helpers.ts +3 -3
  111. package/src/__tests__/components/AppTopBar.test.ts +257 -3
  112. package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
  113. package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
  114. package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
  115. package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
  116. package/src/__tests__/components/BaseButton.test.ts +19 -0
  117. package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
  118. package/src/__tests__/components/BaseSelect.test.ts +2 -1
  119. package/src/__tests__/components/BatchProgressList.test.ts +16 -0
  120. package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
  121. package/src/__tests__/components/ChartContainer.test.ts +11 -0
  122. package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
  123. package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
  124. package/src/__tests__/components/DatePicker.test.ts +21 -0
  125. package/src/__tests__/components/DatePicker.time.test.ts +2 -2
  126. package/src/__tests__/components/EmptyState.test.ts +1 -1
  127. package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
  128. package/src/__tests__/components/ExperimentSelectorModal.test.ts +153 -14
  129. package/src/__tests__/components/GeneratedPluginView.test.ts +23 -2
  130. package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
  131. package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
  132. package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
  133. package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
  134. package/src/__tests__/components/SampleSelector.test.ts +33 -8
  135. package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
  136. package/src/__tests__/components/SettingsModal.test.ts +9 -2
  137. package/src/__tests__/components/StepWizard.test.ts +11 -0
  138. package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
  139. package/src/__tests__/components/deprecationTags.test.ts +1 -1
  140. package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
  141. package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
  142. package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
  143. package/src/__tests__/composables/useJobCenter.test.ts +262 -3
  144. package/src/__tests__/composables/useOnboarding.test.ts +317 -0
  145. package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
  146. package/src/__tests__/stores/settings.test.ts +21 -21
  147. package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
  148. package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
  149. package/src/__tests__/styles/cssTokens.test.ts +6 -3
  150. package/src/__tests__/styles/menusShellRefresh.test.ts +22 -12
  151. package/src/__tests__/styles/paletteTokens.test.ts +44 -0
  152. package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
  153. package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
  154. package/src/__tests__/utils/time.test.ts +18 -1
  155. package/src/components/AdductText.story.vue +7 -0
  156. package/src/components/AdductText.vue +1 -1
  157. package/src/components/AlertBox.story.vue +2 -2
  158. package/src/components/AlertBox.vue +18 -16
  159. package/src/components/AppSidebar.story.vue +0 -28
  160. package/src/components/AppTopBar.onboarding.story.vue +69 -0
  161. package/src/components/AppTopBar.story.vue +105 -15
  162. package/src/components/AppTopBar.vue +248 -122
  163. package/src/components/ArtifactSaveDialog.story.vue +23 -0
  164. package/src/components/ArtifactSaveDialog.vue +102 -12
  165. package/src/components/AuditTrail.story.vue +2 -1
  166. package/src/components/AuditTrail.vue +2 -21
  167. package/src/components/Avatar.vue +10 -14
  168. package/src/components/BaseButton.vue +16 -2
  169. package/src/components/BaseSelect.multiple.story.vue +13 -2
  170. package/src/components/BaseSelect.vue +4 -3
  171. package/src/components/BatchProgressList.vue +35 -25
  172. package/src/components/ChartContainer.story.vue +2 -1
  173. package/src/components/ChartContainer.vue +8 -6
  174. package/src/components/ConfirmDialog.story.vue +1 -1
  175. package/src/components/EmptyState.vue +1 -1
  176. package/src/components/ExperimentPopover.story.vue +165 -53
  177. package/src/components/ExperimentPopover.vue +169 -187
  178. package/src/components/ExperimentSelectorModal.story.vue +73 -31
  179. package/src/components/ExperimentSelectorModal.vue +144 -111
  180. package/src/components/FormField.story.vue +8 -48
  181. package/src/components/GeneratedPluginView.vue +11 -10
  182. package/src/components/GeneratedResultRenderer.vue +3 -4
  183. package/src/components/InstrumentStateBadge.vue +1 -1
  184. package/src/components/JobsStatusTray.story.vue +97 -43
  185. package/src/components/JobsStatusTray.vue +287 -290
  186. package/src/components/LayoutResizeHandle.story.vue +65 -4
  187. package/src/components/LoadingSpinner.vue +1 -1
  188. package/src/components/NumberInput.vue +3 -6
  189. package/src/components/PluginIcon.story.vue +2 -10
  190. package/src/components/PluginIcon.vue +2 -2
  191. package/src/components/PluginWorkspaceView.props.ts +3 -0
  192. package/src/components/PluginWorkspaceView.story.vue +1 -1
  193. package/src/components/PluginWorkspaceView.vue +17 -0
  194. package/src/components/RuntimeUpdateNotice.vue +1 -1
  195. package/src/components/SampleSelector.story.vue +22 -7
  196. package/src/components/SampleSelector.vue +34 -37
  197. package/src/components/SampleSelectorGroupNode.vue +1 -1
  198. package/src/components/SectionCard.story.vue +35 -2
  199. package/src/components/SequenceProgressBar.story.vue +9 -0
  200. package/src/components/SequenceProgressBar.vue +6 -1
  201. package/src/components/SettingsModal.schema.ts +1 -1
  202. package/src/components/SettingsModal.vue +9 -14
  203. package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
  204. package/src/components/SmartGroupManual.story.vue +1 -1
  205. package/src/components/StepWizard.story.vue +33 -0
  206. package/src/components/StepWizard.vue +8 -1
  207. package/src/components/WellPlate.story.vue +3 -1
  208. package/src/components/WellPlate.vue +7 -0
  209. package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
  210. package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
  211. package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
  212. package/src/components/internal/DatePickerDateInternal.vue +16 -40
  213. package/src/components/internal/DatePickerDateTimeInternal.vue +3 -13
  214. package/src/components/internal/DatePickerTimeInternal.vue +5 -8
  215. package/src/components/internal/OnboardingLayerInternal.vue +173 -0
  216. package/src/components/internal/ResourcePickerListInternal.vue +45 -85
  217. package/src/components/internal/SelectMultipleInternal.vue +182 -65
  218. package/src/components/internal/SelectSearchableInternal.vue +2 -3
  219. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  220. package/src/composables/createClient.ts +2 -0
  221. package/src/composables/index.ts +1 -0
  222. package/src/composables/pluginIdentity.ts +31 -0
  223. package/src/composables/pluginWorkspaceModel.ts +2 -2
  224. package/src/composables/useAppExperiment.ts +20 -0
  225. package/src/composables/useCalendarGrid.ts +4 -3
  226. package/src/composables/useExperimentSelector.ts +77 -31
  227. package/src/composables/useJobCenter.ts +188 -6
  228. package/src/composables/useOnboarding.ts +465 -0
  229. package/src/composables/usePluginWorkspace.ts +1 -1
  230. package/src/runtime.ts +2 -1
  231. package/src/shell.ts +17 -0
  232. package/src/stores/index.ts +0 -2
  233. package/src/stores/settings.ts +20 -50
  234. package/src/styles/components/adduct-text.css +13 -5
  235. package/src/styles/components/alert-box.css +39 -58
  236. package/src/styles/components/app-layout.css +7 -5
  237. package/src/styles/components/app-page-selector.css +121 -44
  238. package/src/styles/components/app-pill-nav.css +11 -12
  239. package/src/styles/components/app-sidebar.css +9 -18
  240. package/src/styles/components/app-top-bar.css +219 -55
  241. package/src/styles/components/artifact-save-dialog.css +153 -2
  242. package/src/styles/components/audit-trail.css +16 -19
  243. package/src/styles/components/avatar.css +6 -4
  244. package/src/styles/components/batch-progress-list.css +38 -36
  245. package/src/styles/components/button.css +36 -59
  246. package/src/styles/components/calendar.css +12 -12
  247. package/src/styles/components/chart-container.css +34 -23
  248. package/src/styles/components/checkbox.css +2 -12
  249. package/src/styles/components/collapsible-card.css +27 -29
  250. package/src/styles/components/confirm-dialog.css +13 -14
  251. package/src/styles/components/dataframe.css +13 -5
  252. package/src/styles/components/date-picker.css +80 -92
  253. package/src/styles/components/datetime-picker.css +31 -43
  254. package/src/styles/components/divider.css +20 -20
  255. package/src/styles/components/dose-calculator.css +3 -3
  256. package/src/styles/components/empty-state.css +20 -17
  257. package/src/styles/components/experiment-code-badge.css +2 -0
  258. package/src/styles/components/experiment-popover.css +174 -329
  259. package/src/styles/components/experiment-selector-modal.css +88 -56
  260. package/src/styles/components/file-picker.css +3 -3
  261. package/src/styles/components/file-uploader.css +38 -49
  262. package/src/styles/components/form-builder.css +3 -3
  263. package/src/styles/components/formula-input.css +1 -1
  264. package/src/styles/components/icon-button.css +18 -24
  265. package/src/styles/components/input.css +5 -4
  266. package/src/styles/components/instrument-monitor.css +13 -15
  267. package/src/styles/components/jobs-status-tray.css +140 -296
  268. package/src/styles/components/layout-resize-handle.css +59 -16
  269. package/src/styles/components/modal.css +4 -6
  270. package/src/styles/components/molecule-input.css +2 -2
  271. package/src/styles/components/multi-select.css +65 -117
  272. package/src/styles/components/number-input.css +26 -19
  273. package/src/styles/components/onboarding.css +368 -0
  274. package/src/styles/components/pill.css +3 -2
  275. package/src/styles/components/plate-editor.css +0 -1
  276. package/src/styles/components/plugin-icon.css +26 -23
  277. package/src/styles/components/popover.css +2 -2
  278. package/src/styles/components/presence-avatars.css +1 -1
  279. package/src/styles/components/progress-bar.css +3 -3
  280. package/src/styles/components/protocol-step-editor.css +1 -3
  281. package/src/styles/components/resource-card.css +2 -2
  282. package/src/styles/components/resource-picker-list.css +6 -57
  283. package/src/styles/components/sample-hierarchy-tree.css +4 -4
  284. package/src/styles/components/sample-selector.css +138 -79
  285. package/src/styles/components/schedule-calendar.css +8 -8
  286. package/src/styles/components/searchable-select.css +12 -12
  287. package/src/styles/components/section-card.css +54 -49
  288. package/src/styles/components/segmented-control.css +18 -15
  289. package/src/styles/components/select.css +3 -3
  290. package/src/styles/components/sequence-input.css +2 -2
  291. package/src/styles/components/sequence-progress-bar.css +17 -8
  292. package/src/styles/components/settings-modal.css +7 -2
  293. package/src/styles/components/smart-group.css +18 -18
  294. package/src/styles/components/step-wizard.css +52 -16
  295. package/src/styles/components/tabs.css +2 -2
  296. package/src/styles/components/textarea.css +3 -3
  297. package/src/styles/components/time-picker.css +36 -21
  298. package/src/styles/components/toast.css +12 -10
  299. package/src/styles/components/toggle.css +1 -11
  300. package/src/styles/components/tooltip.css +10 -9
  301. package/src/styles/components/unit-input.css +8 -7
  302. package/src/styles/components/well-plate.css +9 -2
  303. package/src/styles/index.css +1 -0
  304. package/src/styles/variables.css +241 -78
  305. package/src/types/componentWorkflowTypes.ts +2 -0
  306. package/src/types/components.ts +2 -0
  307. package/src/types/generated/platformContext.ts +1 -0
  308. package/src/types/generated-ui.ts +4 -0
  309. package/src/types/index.ts +1 -0
  310. package/src/types/jobs.ts +10 -0
  311. package/src/types/platform.ts +1 -1
  312. package/src/utils/time.ts +21 -0
  313. package/dist/ActionMenu-CScINahU.js.map +0 -1
  314. package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
  315. package/dist/BaseModal-DfRIuVoj.js.map +0 -1
  316. package/dist/ExperimentPopover-C4sGw-is.js +0 -359
  317. package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
  318. package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
  319. package/dist/SettingsModal-CSLE31yg.js.map +0 -1
  320. package/dist/data-C2PwI_dn.js.map +0 -1
  321. package/dist/experiment-43OJtH7R.js.map +0 -1
  322. package/dist/lab-DLwRl_Ql.js.map +0 -1
  323. package/dist/records-BzTcxekG.js.map +0 -1
  324. package/dist/runtime-D6gVat6r.js.map +0 -1
  325. package/dist/useListReorder-CcbEYxX-.js.map +0 -1
  326. package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
  327. package/dist/useRealtime-PalKue84.js +0 -696
  328. package/dist/useRealtime-PalKue84.js.map +0 -1
  329. package/dist/useRequest-D5JDfsgZ.js +0 -76
  330. package/dist/useRequest-D5JDfsgZ.js.map +0 -1
@@ -0,0 +1,244 @@
1
+ import { a as BaseButton_default, t as useDropdownState } from "./useDropdownState-Bb2N4m40.js";
2
+ import { r as readErrorMessage } from "./experiment-DyO54FQB.js";
3
+ import { E as useMenuKeyboard } from "./records-CR_pYzIP.js";
4
+ import { Fragment, Teleport, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, unref, watch } from "vue";
5
+ import { Check, Ellipsis } from "lucide-vue-next";
6
+ import { computePosition, flip, offset, shift } from "@floating-ui/dom";
7
+ //#region src/components/ActionMenu.vue?vue&type=script&setup=true&lang.ts
8
+ var _hoisted_1 = {
9
+ key: 0,
10
+ class: "mint-menu-separator",
11
+ role: "separator"
12
+ };
13
+ var _hoisted_2 = [
14
+ "href",
15
+ "target",
16
+ "rel",
17
+ "onClick"
18
+ ];
19
+ var _hoisted_3 = [
20
+ "role",
21
+ "aria-checked",
22
+ "disabled",
23
+ "onClick"
24
+ ];
25
+ //#endregion
26
+ //#region src/components/ActionMenu.vue
27
+ var ActionMenu_default = /* @__PURE__ */ defineComponent({
28
+ __name: "ActionMenu",
29
+ props: {
30
+ label: {},
31
+ items: {},
32
+ disabled: {
33
+ type: Boolean,
34
+ default: false
35
+ }
36
+ },
37
+ emits: [
38
+ "select",
39
+ "open",
40
+ "close"
41
+ ],
42
+ setup(__props, { emit: __emit }) {
43
+ /** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */
44
+ const props = __props;
45
+ const emit = __emit;
46
+ const panelRef = ref(null);
47
+ const panelStyle = ref({ visibility: "hidden" });
48
+ const { isOpen, rootRef, triggerRef, open, close } = useDropdownState({
49
+ closeOnScroll: true,
50
+ insideRefs: [panelRef],
51
+ onOpen: () => {
52
+ panelStyle.value = { visibility: "hidden" };
53
+ emit("open");
54
+ },
55
+ onClose: () => emit("close")
56
+ });
57
+ onMounted(() => {
58
+ triggerRef.value = rootRef.value?.querySelector("button") ?? null;
59
+ });
60
+ function menuItems() {
61
+ if (!panelRef.value) return [];
62
+ return Array.from(panelRef.value.querySelectorAll(".mint-action-menu__item:not(:disabled)"));
63
+ }
64
+ async function positionPanel() {
65
+ const trigger = triggerRef.value;
66
+ const panel = panelRef.value;
67
+ if (!trigger || !panel) return;
68
+ const position = await computePosition(trigger, panel, {
69
+ placement: "bottom-end",
70
+ strategy: "fixed",
71
+ middleware: [
72
+ offset(4),
73
+ flip(),
74
+ shift({ padding: 8 })
75
+ ]
76
+ });
77
+ panelStyle.value = {
78
+ left: `${position.x}px`,
79
+ top: `${position.y}px`,
80
+ visibility: "visible"
81
+ };
82
+ }
83
+ /** `focusFirst` is false when the keyboard composable owns the initial focus. */
84
+ function openMenu(focusFirst = true) {
85
+ if (props.disabled || isOpen.value) return;
86
+ open();
87
+ if (focusFirst) nextTick(() => menuItems()[0]?.focus());
88
+ }
89
+ function toggleMenu() {
90
+ if (props.disabled) return;
91
+ if (isOpen.value) close();
92
+ else openMenu();
93
+ }
94
+ const { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({
95
+ items: menuItems,
96
+ isOpen: () => isOpen.value,
97
+ open: () => openMenu(false),
98
+ close
99
+ });
100
+ function handleSelect(item) {
101
+ if (item.disabled) return;
102
+ emit("select", item);
103
+ close();
104
+ }
105
+ watch(isOpen, async (opened) => {
106
+ if (!opened) return;
107
+ await nextTick();
108
+ await positionPanel();
109
+ });
110
+ return (_ctx, _cache) => {
111
+ return openBlock(), createElementBlock(Fragment, null, [createElementVNode("span", {
112
+ ref_key: "rootRef",
113
+ ref: rootRef,
114
+ class: "mint-action-menu"
115
+ }, [renderSlot(_ctx.$slots, "trigger", {
116
+ open: unref(isOpen),
117
+ toggle: toggleMenu,
118
+ keydown: unref(handleTriggerKeydown)
119
+ }, () => [createVNode(BaseButton_default, {
120
+ variant: "secondary",
121
+ size: "sm",
122
+ icon: unref(Ellipsis),
123
+ label: __props.label,
124
+ disabled: __props.disabled,
125
+ "aria-haspopup": "menu",
126
+ "aria-expanded": unref(isOpen),
127
+ onClick: toggleMenu,
128
+ onKeydown: unref(handleTriggerKeydown)
129
+ }, null, 8, [
130
+ "icon",
131
+ "label",
132
+ "disabled",
133
+ "aria-expanded",
134
+ "onKeydown"
135
+ ])])], 512), (openBlock(), createBlock(Teleport, { to: "body" }, [unref(isOpen) ? (openBlock(), createElementBlock("div", {
136
+ key: 0,
137
+ ref_key: "panelRef",
138
+ ref: panelRef,
139
+ class: "mint-popover mint-action-menu__panel",
140
+ style: normalizeStyle(panelStyle.value),
141
+ role: "menu",
142
+ onKeydown: _cache[0] || (_cache[0] = (...args) => unref(handlePanelKeydown) && unref(handlePanelKeydown)(...args))
143
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item, index) => {
144
+ return openBlock(), createElementBlock(Fragment, { key: item.id }, [item.danger && index > 0 && !__props.items[index - 1].danger ? (openBlock(), createElementBlock("div", _hoisted_1)) : createCommentVNode("", true), item.href !== void 0 && !item.disabled ? (openBlock(), createElementBlock("a", {
145
+ key: 1,
146
+ href: item.href,
147
+ target: item.target,
148
+ rel: item.target === "_blank" ? "noopener noreferrer" : void 0,
149
+ class: normalizeClass(["mint-menu-item mint-action-menu__item", { "mint-menu-item--danger mint-action-menu__item--danger": item.danger }]),
150
+ role: "menuitem",
151
+ onClick: ($event) => handleSelect(item)
152
+ }, toDisplayString(item.label), 11, _hoisted_2)) : (openBlock(), createElementBlock("button", {
153
+ key: 2,
154
+ type: "button",
155
+ class: normalizeClass(["mint-menu-item mint-action-menu__item", { "mint-menu-item--danger mint-action-menu__item--danger": item.danger }]),
156
+ role: item.checked === void 0 ? "menuitem" : "menuitemradio",
157
+ "aria-checked": item.checked,
158
+ disabled: item.disabled,
159
+ onClick: ($event) => handleSelect(item)
160
+ }, [item.checked !== void 0 ? (openBlock(), createBlock(unref(Check), {
161
+ key: 0,
162
+ class: normalizeClass(["mint-action-menu__check", { "mint-action-menu__check--off": !item.checked }]),
163
+ size: 14,
164
+ "aria-hidden": "true"
165
+ }, null, 8, ["class"])) : createCommentVNode("", true), createTextVNode(" " + toDisplayString(item.label), 1)], 10, _hoisted_3))], 64);
166
+ }), 128))], 36)) : createCommentVNode("", true)]))], 64);
167
+ };
168
+ }
169
+ });
170
+ //#endregion
171
+ //#region src/composables/useRequest.ts
172
+ /** Loading, error and timestamp state for one request at a time; `run()` drives it. */
173
+ function useRequest(defaultErrorMessage = "Request failed.") {
174
+ const isLoading = ref(false);
175
+ const error = ref(null);
176
+ const lastLoadedAt = ref(null);
177
+ const lastSavedAt = ref(null);
178
+ const lastRunAt = ref(null);
179
+ function clearError() {
180
+ error.value = null;
181
+ }
182
+ function readErrorMessage$1(value, fallback = defaultErrorMessage) {
183
+ return readErrorMessage(value, fallback);
184
+ }
185
+ function setError(value, fallback) {
186
+ const message = readErrorMessage$1(value, fallback);
187
+ error.value = message;
188
+ return message;
189
+ }
190
+ let runToken = 0;
191
+ let activeController = null;
192
+ function cancel() {
193
+ runToken += 1;
194
+ activeController?.abort();
195
+ activeController = null;
196
+ isLoading.value = false;
197
+ }
198
+ async function run(operation, options = {}) {
199
+ if (options.abortPrevious !== false) activeController?.abort();
200
+ const token = runToken += 1;
201
+ const controller = new AbortController();
202
+ activeController = controller;
203
+ const callerSignal = options.signal;
204
+ if (callerSignal) {
205
+ if (callerSignal.aborted) controller.abort(callerSignal.reason);
206
+ else callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason), { once: true });
207
+ }
208
+ const isCurrent = () => token === runToken;
209
+ isLoading.value = true;
210
+ clearError();
211
+ try {
212
+ const response = await operation(controller.signal);
213
+ if (isCurrent() && options.kind) {
214
+ const settledAt = /* @__PURE__ */ new Date();
215
+ if (options.kind === "load") lastLoadedAt.value = settledAt;
216
+ else if (options.kind === "save") lastSavedAt.value = settledAt;
217
+ else lastRunAt.value = settledAt;
218
+ }
219
+ return response;
220
+ } catch (err) {
221
+ if (isCurrent()) setError(err, options.errorMessage);
222
+ throw err;
223
+ } finally {
224
+ if (isCurrent()) isLoading.value = false;
225
+ if (activeController === controller) activeController = null;
226
+ }
227
+ }
228
+ return {
229
+ isLoading,
230
+ error,
231
+ lastLoadedAt,
232
+ lastSavedAt,
233
+ lastRunAt,
234
+ clearError,
235
+ readErrorMessage: readErrorMessage$1,
236
+ setError,
237
+ run,
238
+ cancel
239
+ };
240
+ }
241
+ //#endregion
242
+ export { ActionMenu_default as n, useRequest as t };
243
+
244
+ //# sourceMappingURL=useRequest-CDkrXNU7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRequest-CDkrXNU7.js","names":[],"sources":["../src/components/ActionMenu.vue","../src/components/ActionMenu.vue","../src/composables/useRequest.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom'\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref, watch } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })\n\n// Disclosure state (outside-click, Escape, scroll/resize closing, focus return)\n// comes from the shared composable; only floating-ui placement stays local.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n closeOnScroll: true,\n insideRefs: [panelRef],\n onOpen: () => {\n panelStyle.value = { visibility: 'hidden' }\n emit('open')\n },\n onClose: () => emit('close'),\n})\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\nasync function positionPanel() {\n const trigger = triggerRef.value\n const panel = panelRef.value\n if (!trigger || !panel) return\n\n const position = await computePosition(trigger, panel, {\n placement: 'bottom-end',\n strategy: 'fixed',\n middleware: [\n offset(4),\n flip(),\n shift({ padding: 8 }),\n ],\n })\n panelStyle.value = {\n left: `${position.x}px`,\n top: `${position.y}px`,\n visibility: 'visible',\n }\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n\nwatch(isOpen, async (opened) => {\n if (!opened) return\n await nextTick()\n await positionPanel()\n})\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom'\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref, watch } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })\n\n// Disclosure state (outside-click, Escape, scroll/resize closing, focus return)\n// comes from the shared composable; only floating-ui placement stays local.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n closeOnScroll: true,\n insideRefs: [panelRef],\n onOpen: () => {\n panelStyle.value = { visibility: 'hidden' }\n emit('open')\n },\n onClose: () => emit('close'),\n})\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\nasync function positionPanel() {\n const trigger = triggerRef.value\n const panel = panelRef.value\n if (!trigger || !panel) return\n\n const position = await computePosition(trigger, panel, {\n placement: 'bottom-end',\n strategy: 'fixed',\n middleware: [\n offset(4),\n flip(),\n shift({ padding: 8 }),\n ],\n })\n panelStyle.value = {\n left: `${position.x}px`,\n top: `${position.y}px`,\n visibility: 'visible',\n }\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n\nwatch(isOpen, async (opened) => {\n if (!opened) return\n await nextTick()\n await positionPanel()\n})\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","import { ref, type Ref } from 'vue'\nimport { readErrorMessage as readSharedErrorMessage } from '../utils/errors'\n\nexport type RequestKind = 'load' | 'save' | 'run'\n\n/**\n * The unit of work `run()` drives.\n *\n * It receives an `AbortSignal` that fires when the run is superseded by a\n * newer `run()` or by `cancel()`. Zero-argument operations stay valid and\n * simply ignore it.\n */\nexport type RequestOperation<T> = (signal: AbortSignal) => Promise<T>\n\nexport interface RequestRunOptions {\n /** Which timestamp a successful run moves: `lastLoadedAt`, `lastSavedAt` or `lastRunAt`. */\n kind?: RequestKind\n errorMessage?: string\n /**\n * Abort the previously started run when this one starts. Default `true`;\n * pass `false` to let overlapping runs finish their own work (the stale\n * guard on shared state still applies either way).\n */\n abortPrevious?: boolean\n /** Caller-owned signal; aborting it aborts this run's signal too. */\n signal?: AbortSignal\n}\n\nexport interface UseRequestReturn {\n /** Whether a request is in progress. */\n isLoading: Ref<boolean>\n /** Message from the last failed request, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful load operation, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Timestamp of the last successful save operation, or null. */\n lastSavedAt: Ref<Date | null>\n /** Timestamp of the last successful run operation, or null. */\n lastRunAt: Ref<Date | null>\n /** Clear the current error without changing loading or timestamps. */\n clearError: () => void\n /** Convert thrown values into a developer-facing error message. */\n readErrorMessage: (value: unknown, fallback?: string) => string\n /** Store and return a normalized error message. */\n setError: (value: unknown, fallback?: string) => string\n /**\n * Run a request with shared loading/error handling and optional sync marking.\n *\n * ## Stale-response semantics\n *\n * Every call takes a monotonically increasing token. Starting a run\n * supersedes any run started before it, and a superseded run writes **no**\n * shared state when it finally settles: it does not clear `isLoading`, does not\n * set `error`, and does not move `lastLoadedAt` / `lastSavedAt` / `lastRunAt`.\n * Only the newest run owns those.\n *\n * The caller's own promise is untouched by the guard — a superseded run still\n * resolves with its value or rejects with its own error, exactly as the\n * operation did. The guard never invents a rejection, so a caller awaiting\n * `run()` never sees a \"superseded\" error it did not cause.\n *\n * The operation additionally receives an `AbortSignal` that fires when its\n * run is superseded or `cancel()` is called, so an operation that forwards\n * the signal (`fetch(url, { signal })`) stops work instead of racing.\n */\n run: <T>(operation: RequestOperation<T>, options?: RequestRunOptions) => Promise<T>\n /**\n * Abort the in-flight run and drop `isLoading`. The aborted run's completion\n * is treated as stale and writes no state.\n */\n cancel: () => void\n}\n\n/** Loading, error and timestamp state for one request at a time; `run()` drives it. */\nexport function useRequest(\n defaultErrorMessage = 'Request failed.',\n): UseRequestReturn {\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n const lastLoadedAt = ref<Date | null>(null)\n const lastSavedAt = ref<Date | null>(null)\n const lastRunAt = ref<Date | null>(null)\n\n function clearError(): void {\n error.value = null\n }\n\n function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {\n return readSharedErrorMessage(value, fallback)\n }\n\n function setError(value: unknown, fallback?: string): string {\n const message = readErrorMessage(value, fallback)\n error.value = message\n return message\n }\n\n // Monotonic token guarding every write to the shared loading/error/timestamp\n // state. Overlapping runs settle in any order, and a superseded one must not\n // clear a newer run's loading flag or overwrite its error.\n let runToken = 0\n let activeController: AbortController | null = null\n\n function cancel(): void {\n runToken += 1\n activeController?.abort()\n activeController = null\n isLoading.value = false\n }\n\n async function run<T>(\n operation: RequestOperation<T>,\n options: RequestRunOptions = {},\n ): Promise<T> {\n if (options.abortPrevious !== false) {\n activeController?.abort()\n }\n\n const token = (runToken += 1)\n const controller = new AbortController()\n activeController = controller\n\n const callerSignal = options.signal\n if (callerSignal) {\n if (callerSignal.aborted) controller.abort(callerSignal.reason)\n else callerSignal.addEventListener(\n 'abort',\n () => controller.abort(callerSignal.reason),\n { once: true },\n )\n }\n\n const isCurrent = (): boolean => token === runToken\n\n isLoading.value = true\n clearError()\n try {\n const response = await operation(controller.signal)\n if (isCurrent() && options.kind) {\n const settledAt = new Date()\n if (options.kind === 'load') lastLoadedAt.value = settledAt\n else if (options.kind === 'save') lastSavedAt.value = settledAt\n else lastRunAt.value = settledAt\n }\n return response\n } catch (err) {\n if (isCurrent()) {\n setError(err, options.errorMessage)\n }\n throw err\n } finally {\n if (isCurrent()) {\n isLoading.value = false\n }\n if (activeController === controller) {\n activeController = null\n }\n }\n }\n\n return {\n isLoading,\n error,\n lastLoadedAt,\n lastSavedAt,\n lastRunAt,\n clearError,\n readErrorMessage,\n setError,\n run,\n cancel,\n }\n}\n"],"mappings":"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"}
@@ -15,5 +15,12 @@ export declare function snapToSlot(time: string, stepMinutes: number): string;
15
15
  export declare function addMinutes(time: string, minutes: number): string;
16
16
  export declare function compareTime(a: string, b: string): number;
17
17
  export declare function findNearestTimeSlotIndex(time: string, slots: readonly string[]): number;
18
+ /**
19
+ * "in 5 minutes" / "2 hours ago" up to a week, then a locale date.
20
+ * With `weeks`, times up to five weeks away read as a week count first.
21
+ */
22
+ export declare function formatRelativeTime(value: Date | string, { weeks }?: {
23
+ weeks?: boolean;
24
+ }): string;
18
25
  export declare function toMinutes(time: string): number;
19
26
  export declare function fromMinutes(total: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.4.0-beta.2",
3
+ "version": "1.4.0-beta.20",
4
4
  "description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -65,19 +65,19 @@ export function installMockInterceptor() {
65
65
  * Props:
66
66
  * preselect (boolean, default true) - pre-select the first mock experiment
67
67
  * plugin (PluginInfo, optional) - the integrated plugin, shown as AppTopBar's static identity
68
+ * save (boolean, default true) - show the Save half of the experiment chip
68
69
  */
69
70
  export const ExperimentProvider = defineComponent({
70
71
  props: {
71
72
  preselect: { type: Boolean, default: true },
72
73
  plugin: { type: Object as PropType<PluginInfo>, default: undefined },
74
+ save: { type: Boolean, default: true },
73
75
  },
74
76
  setup(props, { slots }) {
75
77
  ;(window as any).__MINT_PLATFORM__ = { isIntegrated: true, theme: 'system', plugin: props.plugin }
76
78
  installMockInterceptor()
77
79
  const experimentStore = useExperimentStore()
78
- useAppExperiment({
79
- onSave: async () => 'Saved!',
80
- })
80
+ useAppExperiment(props.save ? { onSave: async () => 'Saved!' } : {})
81
81
  if (props.preselect && mockExperiments[0]) {
82
82
  experimentStore.select(mockExperiments[0])
83
83
  }
@@ -6,11 +6,13 @@ import AppTopBar from '../../components/AppTopBar.vue'
6
6
  import ExperimentPopover from '../../components/ExperimentPopover.vue'
7
7
  import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vue'
8
8
  import { usePlatform } from '../../composables/usePlatform'
9
+ import { providePluginIdentity } from '../../composables/pluginIdentity'
9
10
  import { APP_EXPERIMENT_KEY, type AppExperimentState } from '../../composables/useAppExperiment'
10
11
  import { defineControlModel, defineControls } from '../../composables/useControlSchema'
11
12
  import ThemeToggle from '../../components/ThemeToggle.vue'
12
13
  import SettingsModal from '../../components/SettingsModal.vue'
13
14
  import type { TopBarSettingsConfig } from '../../types/components'
15
+ import type { TopBarJob } from '../../types/jobs'
14
16
 
15
17
  // Mock the usePlatform composable
16
18
  vi.mock('../../composables/usePlatform', () => ({
@@ -907,7 +909,7 @@ describe('AppTopBar', () => {
907
909
  const imageIcons = wrapper.findAll('.mint-plugin-icon__img')
908
910
  expect(imageIcons).toHaveLength(1)
909
911
  expect(imageIcons[0].attributes('src')).toBe(pngIcon)
910
- expect(wrapper.find('.mint-page-selector__item-hint').exists()).toBe(false)
912
+ expect(wrapper.get('.mint-page-selector__item-hint').text()).toBe('Overview')
911
913
  })
912
914
  })
913
915
 
@@ -1058,7 +1060,7 @@ describe('AppTopBar', () => {
1058
1060
  expect(wrapper.find('.mint-topbar__identity').exists()).toBe(true)
1059
1061
  })
1060
1062
 
1061
- it('should pass plugin.color to PluginIcon as color', () => {
1063
+ it('should not pass the manifest color to PluginIcon when the plugin declares one', () => {
1062
1064
  const iconPath = 'M13 10V3L4 14h7v7l9-11h-7z'
1063
1065
  mockPlatformCtx({
1064
1066
  isIntegrated: true,
@@ -1071,7 +1073,7 @@ describe('AppTopBar', () => {
1071
1073
 
1072
1074
  const wrapper = createWrapper({ title: 'Test App' })
1073
1075
 
1074
- expect(wrapper.findComponent({ name: 'PluginIcon' }).props('color')).toBe('#22C55E')
1076
+ expect(wrapper.findComponent({ name: 'PluginIcon' }).props('color')).toBeUndefined()
1075
1077
  })
1076
1078
 
1077
1079
  it('should ignore the custom icon slot when integrated', () => {
@@ -1236,6 +1238,26 @@ describe('AppTopBar', () => {
1236
1238
  expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
1237
1239
  })
1238
1240
 
1241
+ it.each([
1242
+ { case: 'integrated with display_name', integrated: true, plugin: { ...integratedPlugin, display_name: 'LC-MS Peak Review' }, props: { title: 'Ignored' }, seed: false, expected: 'LC-MS Peak Review' },
1243
+ { case: 'integrated without display_name', integrated: true, plugin: integratedPlugin, props: {}, seed: false, expected: 'LC-MS Analysis' },
1244
+ { case: 'standalone with a title', integrated: false, plugin: undefined, props: { title: 'Planner Dev' }, seed: true, expected: 'Planner Dev' },
1245
+ { case: 'standalone seeded', integrated: false, plugin: undefined, props: {}, seed: true, expected: 'MS Planner' },
1246
+ { case: 'standalone without a name', integrated: false, plugin: undefined, props: {}, seed: false, expected: null },
1247
+ ])('should put the plugin name in the page selector crumb when $case', ({ integrated, plugin, props, seed, expected }) => {
1248
+ mockPlatformCtx({ isIntegrated: integrated, plugin })
1249
+ if (seed) providePluginIdentity({ plugin: { name: 'ms-planner', displayName: 'MS Planner' } })
1250
+
1251
+ try {
1252
+ const wrapper = createWrapper({ ...props, pageSelector: pages, currentPageSelectorId: 'peaks' })
1253
+
1254
+ const name = wrapper.find('.mint-page-selector__plugin')
1255
+ expect(name.exists() ? name.text() : null).toBe(expected)
1256
+ } finally {
1257
+ providePluginIdentity({ plugin: { name: '' } })
1258
+ }
1259
+ })
1260
+
1239
1261
  it('should hide the plugin name when a page selector is shown', () => {
1240
1262
  mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
1241
1263
 
@@ -1268,6 +1290,54 @@ describe('AppTopBar', () => {
1268
1290
 
1269
1291
  expect(wrapper.find('.mint-topbar__title-only').exists()).toBe(false)
1270
1292
  })
1293
+
1294
+ it.each([
1295
+ { fields: { subtitle: 'Peak Review', description: 'Reviews LC-MS peaks' }, expected: 'Peak Review' },
1296
+ { fields: { description: 'Reviews LC-MS peaks' }, expected: 'Reviews LC-MS peaks' },
1297
+ { fields: {}, expected: null },
1298
+ ])('should show $expected under the integrated name when the plugin has $fields', ({ fields, expected }) => {
1299
+ mockPlatformCtx({ isIntegrated: true, plugin: { ...integratedPlugin, ...fields } })
1300
+
1301
+ const wrapper = createWrapper()
1302
+
1303
+ const subtitle = wrapper.find('.mint-topbar__plugin-subtitle')
1304
+ expect(subtitle.exists() ? subtitle.text() : null).toBe(expected)
1305
+ })
1306
+ })
1307
+
1308
+ describe('seeded standalone identity', () => {
1309
+ const seedIcon = 'M13 10V3L4 14h7v7l9-11h-7z'
1310
+
1311
+ beforeEach(() => {
1312
+ providePluginIdentity({
1313
+ plugin: { name: 'ms-planner', displayName: 'MS Planner', subtitle: 'Experiment Scheduler', icon: seedIcon },
1314
+ })
1315
+ })
1316
+
1317
+ afterEach(() => providePluginIdentity({ plugin: { name: '' } }))
1318
+
1319
+ it('should show the seeded name, subtitle and icon when standalone', () => {
1320
+ const wrapper = createWrapper()
1321
+
1322
+ expect(wrapper.get('.mint-topbar-title').text()).toBe('MS Planner')
1323
+ expect(wrapper.get('.mint-topbar-subtitle').text()).toBe('Experiment Scheduler')
1324
+ expect(wrapper.findComponent({ name: 'PluginIcon' }).props('icon')).toBe(seedIcon)
1325
+ })
1326
+
1327
+ it.each([
1328
+ { slot: 'icon' },
1329
+ { slot: 'logo' },
1330
+ ])('should keep the title props and the #$slot slot over the seed', ({ slot }) => {
1331
+ const wrapper = createWrapper(
1332
+ { title: 'Planner Dev', subtitle: 'Local build' },
1333
+ { [slot]: '<span class="custom-brand">P</span>' },
1334
+ )
1335
+
1336
+ expect(wrapper.get('.mint-topbar-title').text()).toBe('Planner Dev')
1337
+ expect(wrapper.get('.mint-topbar-subtitle').text()).toBe('Local build')
1338
+ expect(wrapper.find('.custom-brand').exists()).toBe(true)
1339
+ expect(wrapper.findComponent({ name: 'PluginIcon' }).exists()).toBe(false)
1340
+ })
1271
1341
  })
1272
1342
  })
1273
1343
 
@@ -1434,4 +1504,188 @@ describe('AppTopBar', () => {
1434
1504
  expect(removeListener).toHaveBeenCalledWith('scroll', expect.any(Function), expect.objectContaining({ capture: true }))
1435
1505
  })
1436
1506
  })
1507
+ describe('run slot', () => {
1508
+ function mountRunSlot(
1509
+ job: TopBarJob | null | undefined,
1510
+ { experiment, jobsDone, jobsOpen, jobsPanelId }: { experiment: boolean; jobsDone?: number; jobsOpen?: boolean; jobsPanelId?: string },
1511
+ ) {
1512
+ vi.mocked(usePlatform).mockReturnValueOnce({
1513
+ integrated: computed(() => experiment),
1514
+ isAdmin: computed(() => true),
1515
+ can: () => true,
1516
+ plugin: computed(() => undefined),
1517
+ user: computed(() => undefined),
1518
+ theme: computed(() => 'system' as const),
1519
+ features: computed(() => undefined),
1520
+ })
1521
+ const appExperiment = {
1522
+ popover: computed(() => ({ experimentName: 'Serine tracing', experimentCode: 'EXP-0142', showSave: true })),
1523
+ selectorModal: computed(() => ({ open: false })),
1524
+ openModal: vi.fn(),
1525
+ closeModal: vi.fn(),
1526
+ handleSave: vi.fn(),
1527
+ handleDetach: vi.fn(),
1528
+ } as unknown as AppExperimentState
1529
+ return mount(AppTopBar, {
1530
+ props: job === undefined
1531
+ ? { title: 'LEAF', jobsDone, jobsOpen, jobsPanelId }
1532
+ : { title: 'LEAF', job, jobsDone, jobsOpen, jobsPanelId },
1533
+ global: {
1534
+ plugins: [createPinia()],
1535
+ provide: { [APP_EXPERIMENT_KEY as symbol]: appExperiment },
1536
+ stubs: { 'router-link': { template: '<a><slot /></a>' }, BaseModal: { template: '<div><slot /></div>' }, transition: false },
1537
+ },
1538
+ })
1539
+ }
1540
+
1541
+ const running: TopBarJob = { status: 'running', label: 'Extracting', percent: 40 }
1542
+
1543
+ it.each([
1544
+ ['no job prop, experiment', undefined, true, { popover: true, chip: false, idle: false, joined: false }],
1545
+ ['idle, experiment', null, true, { popover: true, chip: true, idle: true, joined: false }],
1546
+ ['running, experiment', running, true, { popover: true, chip: true, idle: false, joined: true }],
1547
+ ['idle, standalone', null, false, { popover: false, chip: true, idle: true, joined: false }],
1548
+ ['running, standalone', running, false, { popover: false, chip: true, idle: false, joined: true }],
1549
+ ] as const)('should render the run slot for %s', async (_case, job, experiment, expected) => {
1550
+ const wrapper = mountRunSlot(job, { experiment })
1551
+ await waitForAsyncComponents(wrapper)
1552
+
1553
+ const chip = wrapper.find('.mint-topbar__job')
1554
+ expect({
1555
+ popover: wrapper.findComponent(ExperimentPopover).exists(),
1556
+ chip: chip.exists(),
1557
+ idle: chip.exists() && chip.classes('mint-topbar__job--idle'),
1558
+ joined: wrapper.find('.mint-topbar__run-slot--joined').exists(),
1559
+ }).toEqual(expected)
1560
+ })
1561
+
1562
+ it.each([
1563
+ [undefined, true],
1564
+ [null, true],
1565
+ [running, false],
1566
+ ])('should pass showSave to the experiment chip when the job is %s', async (job, showSave) => {
1567
+ const wrapper = mountRunSlot(job, { experiment: true })
1568
+ await waitForAsyncComponents(wrapper)
1569
+
1570
+ expect(wrapper.findComponent(ExperimentPopover).props('showSave')).toBe(showSave)
1571
+ })
1572
+
1573
+ it('should keep the slot joined until the job has left, then show the idle button', async () => {
1574
+ const wrapper = mountRunSlot(running, { experiment: true })
1575
+ await waitForAsyncComponents(wrapper)
1576
+ const joined = () => wrapper.find('.mint-topbar__run-slot--joined').exists()
1577
+
1578
+ await wrapper.setProps({ job: null })
1579
+ expect(joined()).toBe(true)
1580
+ await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job--idle').exists()).toBe(true))
1581
+ expect(joined()).toBe(false)
1582
+
1583
+ await wrapper.setProps({ job: running })
1584
+ await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job--primary').exists()).toBe(true))
1585
+ expect(joined()).toBe(true)
1586
+
1587
+ await wrapper.setProps({ job: undefined })
1588
+ await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job').exists()).toBe(false))
1589
+ expect(joined()).toBe(false)
1590
+ })
1591
+
1592
+ it('should join the slot when the job and the slot mount together', async () => {
1593
+ const wrapper = mountRunSlot(undefined, { experiment: false })
1594
+ await waitForAsyncComponents(wrapper)
1595
+
1596
+ await wrapper.setProps({ job: running })
1597
+
1598
+ await vi.waitFor(() => expect(wrapper.find('.mint-topbar__run-slot--joined').exists()).toBe(true))
1599
+ })
1600
+
1601
+ it.each([
1602
+ [120, '100%', 'Extracting 100%'],
1603
+ [-5, '0%', 'Extracting 0%'],
1604
+ ])('should clamp percent %s to %s', async (percent, width, name) => {
1605
+ const wrapper = mountRunSlot({ ...running, percent }, { experiment: false })
1606
+ await waitForAsyncComponents(wrapper)
1607
+
1608
+ const chip = wrapper.get('.mint-topbar__job')
1609
+ expect(chip.get('.mint-topbar__job-bar > span').attributes('style')).toContain(`width: ${width}`)
1610
+ expect(chip.attributes('aria-label')).toBe(name)
1611
+ })
1612
+
1613
+ it.each([
1614
+ [running, 'Job running'],
1615
+ [{ status: 'awaiting_input', label: 'Decoding' } as TopBarJob, 'Job needs input'],
1616
+ [{ status: 'failed', label: '1 failed' } as TopBarJob, 'Job failed'],
1617
+ [null, ''],
1618
+ ])('should announce the status of %o as %s', async (job, text) => {
1619
+ const wrapper = mountRunSlot(job, { experiment: false })
1620
+ await waitForAsyncComponents(wrapper)
1621
+
1622
+ const live = wrapper.get('.mint-topbar__job-status')
1623
+ expect(live.attributes('aria-live')).toBe('polite')
1624
+ expect(live.text()).toBe(text)
1625
+ })
1626
+
1627
+ it.each([
1628
+ ['queued', 'primary'],
1629
+ ['running', 'primary'],
1630
+ ['awaiting_input', 'warning'],
1631
+ ['completed', 'success'],
1632
+ ['failed', 'error'],
1633
+ ['cancelled', 'neutral'],
1634
+ ] as const)('should tone a %s job as %s', async (status, tone) => {
1635
+ const wrapper = mountRunSlot({ status, label: 'Job' }, { experiment: false })
1636
+ await waitForAsyncComponents(wrapper)
1637
+
1638
+ expect(wrapper.get('.mint-topbar__job').classes()).toContain(`mint-topbar__job--${tone}`)
1639
+ })
1640
+
1641
+ it.each([
1642
+ [running, 'Extracting 40%', true],
1643
+ [{ status: 'completed', label: 'Saved to EXP-0142' } as TopBarJob, 'Saved to EXP-0142', false],
1644
+ ])('should show only the status for %o and name it %s', async (job, name, bar) => {
1645
+ const wrapper = mountRunSlot(job, { experiment: false })
1646
+ await waitForAsyncComponents(wrapper)
1647
+
1648
+ const chip = wrapper.get('.mint-topbar__job')
1649
+ expect(chip.text()).toBe('')
1650
+ expect(chip.attributes('aria-label')).toBe(name)
1651
+ expect(chip.attributes('title')).toBe(name)
1652
+ expect(chip.find('.mint-topbar__job-bar').exists()).toBe(bar)
1653
+ if (bar) expect(chip.get('.mint-topbar__job-bar > span').attributes('style')).toContain('width: 40%')
1654
+ })
1655
+
1656
+ it.each([
1657
+ [null, undefined, null, undefined],
1658
+ [null, 0, null, undefined],
1659
+ [null, 2, '2', 'Jobs, 2 completed'],
1660
+ [running, 2, null, 'Extracting 40%'],
1661
+ ])('should show the completed count only on the idle button when the job is %o and jobsDone %s', async (job, jobsDone, count, name) => {
1662
+ const wrapper = mountRunSlot(job, { experiment: false, jobsDone })
1663
+ await waitForAsyncComponents(wrapper)
1664
+
1665
+ const chip = wrapper.get('.mint-topbar__job')
1666
+ expect([chip.find('.mint-topbar__job-count').exists() ? chip.get('.mint-topbar__job-count').text() : null, chip.attributes('aria-label')])
1667
+ .toEqual([count, name])
1668
+ })
1669
+
1670
+ it.each([
1671
+ [null, undefined, undefined, [undefined, undefined]],
1672
+ [null, true, 'jobs-panel', ['true', 'jobs-panel']],
1673
+ [running, false, 'jobs-panel', ['false', 'jobs-panel']],
1674
+ ])('should expose the tray state on the job control when it is %o and open is %s', async (job, jobsOpen, jobsPanelId, expected) => {
1675
+ const wrapper = mountRunSlot(job, { experiment: false, jobsOpen, jobsPanelId })
1676
+ await waitForAsyncComponents(wrapper)
1677
+
1678
+ const chip = wrapper.get('.mint-topbar__job')
1679
+ expect([chip.attributes('aria-expanded'), chip.attributes('aria-controls')]).toEqual(expected)
1680
+ })
1681
+
1682
+ it.each([null, running])('should emit job-click when the job control is %s', async (job) => {
1683
+ const wrapper = mountRunSlot(job, { experiment: false })
1684
+ await waitForAsyncComponents(wrapper)
1685
+
1686
+ await wrapper.get('.mint-topbar__job').trigger('click')
1687
+
1688
+ expect(wrapper.emitted('job-click')).toEqual([[]])
1689
+ })
1690
+ })
1437
1691
  })