@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
@@ -1,35 +1,52 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Floating, source-driven job status tray for plugin and platform workflows.
3
+ * Compact, non-modal job panel under the top bar, driven by a job center the parent owns.
4
+ *
5
+ * The parent passes the center's `topBarJob`, `completedCount` and `handleTopBarClick`
6
+ * to `AppTopBar`; its run slot opens this panel. `PluginWorkspaceView` does this for you
7
+ * when its spec sets `jobs`.
4
8
  *
5
9
  * @example Recommended
6
10
  * ```vue
7
- * <JobsStatusTray :source="jobs" />
11
+ * <AppTopBar :job="center.topBarJob.value" :jobs-done="center.completedCount.value"
12
+ * @job-click="center.handleTopBarClick(openResult)" />
13
+ * <JobsStatusTray :center="center" @select="openResult" />
8
14
  * ```
9
15
  */
10
- import { computed, nextTick, ref, toRef, useId } from 'vue'
11
-
12
- import {
13
- jobStatusLabel,
14
- normalizeJobPercent,
15
- } from '../jobs'
16
+ import { computed, nextTick, onBeforeUnmount, ref, watch, type Component } from 'vue'
16
17
  import {
17
- useJobCenter,
18
- type PluginJobCenterSource,
19
- } from '../composables/useJobCenter'
20
- import type { JobState } from '../types/jobs'
18
+ CircleAlert,
19
+ CircleCheck,
20
+ CircleSlash,
21
+ CircleX,
22
+ Clock,
23
+ LoaderCircle,
24
+ RefreshCw,
25
+ Square,
26
+ Trash2,
27
+ X,
28
+ } from 'lucide-vue-next'
29
+
30
+ import { isActiveJobStatus, jobStatusLabel } from '../jobs'
31
+ import type { UseJobCenterReturn } from '../composables/useJobCenter'
32
+ import type { JobState, JobStatus } from '../types/jobs'
33
+ import ConfirmDialog from './ConfirmDialog.vue'
34
+
35
+ type ConfirmAction = 'cancel' | 'delete' | 'clear'
21
36
 
22
37
  interface Props {
23
- source?: PluginJobCenterSource
24
- loadOnMount?: boolean
38
+ /** Job center from `useJobCenter()`; the parent owns it and opens the panel through it. */
39
+ center: UseJobCenterReturn
25
40
  title?: string
26
41
  teleportTo?: string | HTMLElement | false
42
+ /** Ask before Cancel, Delete and Clear (`true`), or before the named ones only. Default off. */
43
+ confirm?: boolean | Partial<Record<ConfirmAction, boolean>>
27
44
  }
28
45
 
29
46
  const props = withDefaults(defineProps<Props>(), {
30
- loadOnMount: true,
31
- title: 'Analysis jobs',
47
+ title: 'Jobs',
32
48
  teleportTo: 'body',
49
+ confirm: false,
33
50
  })
34
51
 
35
52
  const emit = defineEmits<{
@@ -40,107 +57,187 @@ const emit = defineEmits<{
40
57
  defineSlots<{
41
58
  /** Extra per-row actions, rendered before the built-in Cancel / Delete. */
42
59
  'job-actions'?: (props: { job: JobState }) => unknown
60
+ /** Row title. Default: the job name. */
61
+ 'job-title'?: (props: { job: JobState }) => unknown
62
+ /** Detail line under the title. Default: the error, progress message or status. */
63
+ 'job-detail'?: (props: { job: JobState }) => unknown
43
64
  }>()
44
65
 
45
- const panelId = `mint-jobs-tray-${useId().replace(/:/g, '')}`
46
- const titleId = `${panelId}-title`
47
- const triggerRef = ref<HTMLButtonElement | null>(null)
66
+ const STATUS_ICONS: Record<JobStatus, Component> = {
67
+ running: LoaderCircle,
68
+ awaiting_input: CircleAlert,
69
+ queued: Clock,
70
+ completed: CircleCheck,
71
+ failed: CircleX,
72
+ cancelled: CircleSlash,
73
+ }
74
+
48
75
  const panelRef = ref<HTMLElement | null>(null)
49
76
  const teleportTarget = computed(() => props.teleportTo || 'body')
50
- const tray = useJobCenter({
51
- source: toRef(props, 'source'),
52
- loadOnMount: props.loadOnMount,
77
+ const tray = props.center
78
+ const titleId = `${tray.panelId}-title`
79
+ const rows = (jobs: JobState[]) => jobs.map(job => ({ job, view: tray.rowView(job) }))
80
+ // A job waiting for input is the only one that needs a person: pin it first.
81
+ const activeRows = computed(() => {
82
+ const active = rows(tray.activeJobs.value)
83
+ return [
84
+ ...active.filter(row => row.view.status === 'awaiting_input'),
85
+ ...active.filter(row => row.view.status !== 'awaiting_input'),
86
+ ]
53
87
  })
54
- // 2f: a job waiting for input is the only one that needs a person — pin it first.
55
- const activeRows = computed(() => [
56
- ...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),
57
- ...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),
58
- ])
59
- const subtitle = computed(() => {
60
- const count = (status: JobState['status']) => tray.activeJobs.value.filter(job => job.status === status).length
88
+ const sections = computed(() => [
89
+ { key: 'active', title: 'Active', rows: activeRows.value },
90
+ { key: 'finished', title: 'Finished', rows: rows(tray.historyJobs.value) },
91
+ ].filter(section => section.rows.length > 0))
92
+ const headCount = computed(() => {
61
93
  const parts = [
62
- [count('running'), 'running'],
63
- [count('queued'), 'queued'],
64
- [count('awaiting_input'), 'waiting'],
94
+ [tray.activeJobs.value.length, 'active'],
95
+ [tray.completedCount.value, 'done'],
96
+ [tray.historyJobs.value.filter(job => job.status === 'failed').length, 'failed'],
65
97
  ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)
66
- return parts.length ? parts.join(' · ') : `${tray.jobs.value.length} total`
98
+ return parts.length ? parts.join(' · ') : 'No jobs'
67
99
  })
68
100
  const canClear = computed(() => (
69
101
  tray.deletableJobs.value.length > 0
70
102
  && tray.canClearFinished.value
71
103
  ))
72
104
 
73
- async function openTray(): Promise<void> {
74
- tray.open()
105
+ // Escape returns focus to the element that opened the panel (the top-bar chip).
106
+ // The panel is non-modal, so Escape is heard on the document while it is open.
107
+ // An Escape that an inner control handled, or that belongs to a modal dialog
108
+ // above the tray (its own confirm dialog too), leaves the tray open.
109
+ let opener: HTMLElement | null = null
110
+ function onDocumentKeydown(event: KeyboardEvent): void {
111
+ if (event.key !== 'Escape' || event.defaultPrevented || confirmOpen.value) return
112
+ if (event.target instanceof Element && event.target.closest('[aria-modal="true"]')) return
113
+ closeTray()
114
+ }
115
+ watch(tray.isOpen, async (open) => {
116
+ if (!open) {
117
+ document.removeEventListener('keydown', onDocumentKeydown)
118
+ return
119
+ }
120
+ opener = document.activeElement instanceof HTMLElement ? document.activeElement : null
121
+ document.addEventListener('keydown', onDocumentKeydown)
75
122
  await nextTick()
76
123
  panelRef.value?.focus()
77
- }
124
+ }, { flush: 'sync' })
125
+ onBeforeUnmount(() => document.removeEventListener('keydown', onDocumentKeydown))
78
126
 
79
- async function closeTray(): Promise<void> {
127
+ // A focused row that leaves the list (an action or a refresh) drops focus to body:
128
+ // note the focused element before the update and keep focus in the panel after it.
129
+ let focusedInPanel: Element | null = null
130
+ watch(tray.jobs, () => {
131
+ const focused = document.activeElement
132
+ focusedInPanel = focused && panelRef.value?.contains(focused) ? focused : null
133
+ }, { flush: 'pre' })
134
+ watch(tray.jobs, () => {
135
+ if (tray.isOpen.value && focusedInPanel && !focusedInPanel.isConnected) panelRef.value?.focus()
136
+ focusedInPanel = null
137
+ }, { flush: 'post' })
138
+
139
+ function closeTray(): void {
80
140
  tray.close()
81
- await nextTick()
82
- triggerRef.value?.focus()
141
+ // Safari leaves a clicked button unfocused (opener = body), and the chip node is
142
+ // replaced when it switches between job chip and idle button: fall back to the
143
+ // top-bar jobs control.
144
+ const target = opener?.isConnected && opener !== document.body
145
+ ? opener
146
+ : document.querySelector<HTMLElement>('.mint-topbar__job')
147
+ target?.focus()
148
+ }
149
+
150
+ function formatTime(value: string | null): string {
151
+ if (!value) return ''
152
+ const date = new Date(value)
153
+ return Number.isNaN(date.getTime())
154
+ ? ''
155
+ : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
83
156
  }
84
157
 
85
- async function toggleTray(): Promise<void> {
86
- if (tray.isOpen.value) await closeTray()
87
- else await openTray()
158
+ /** Right-hand value: the percent while a job moves, else when it ended or started. */
159
+ function jobValue(job: JobState, percent: number | null): string {
160
+ if (percent != null) return `${percent}%`
161
+ return formatTime(job.completed_at || job.started_at || job.created_at)
88
162
  }
89
163
 
90
- function onPanelKeydown(event: KeyboardEvent): void {
91
- if (event.key === 'Escape') {
92
- void closeTray()
93
- return
164
+ // Confirmation: the action waits for the dialog; it runs on the live job.
165
+ // `ids`: the finished jobs that the user confirmed to clear.
166
+ const pending = ref<{ action: ConfirmAction; job?: JobState; ids?: string[] } | null>(null)
167
+ const confirmOpen = ref(false)
168
+ // Set on request and kept, so the dialog text stays while it fades out.
169
+ const confirmText = ref<{ title: string; message: string; confirmLabel: string; cancelLabel: string } | null>(null)
170
+ const confirmEnabled = computed(() => props.confirm === true
171
+ || (typeof props.confirm === 'object' && Object.values(props.confirm).some(Boolean)))
172
+
173
+ function textFor(action: ConfirmAction, job?: JobState) {
174
+ if (action === 'clear') {
175
+ const count = tray.deletableJobs.value.length
176
+ return {
177
+ title: 'Clear finished jobs?',
178
+ message: `${count} finished ${count === 1 ? 'job' : 'jobs'} and their stored results are deleted.`,
179
+ confirmLabel: 'Clear jobs',
180
+ cancelLabel: 'Cancel',
181
+ }
94
182
  }
95
- if (event.key !== 'Tab' || !panelRef.value) return
96
- const focusable = Array.from(panelRef.value.querySelectorAll<HTMLElement>(
97
- 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
98
- ))
99
- if (focusable.length === 0) {
100
- event.preventDefault()
101
- panelRef.value.focus()
183
+ return action === 'cancel'
184
+ ? { title: 'Cancel job?', message: `${job?.name} stops. Its partial results are lost.`, confirmLabel: 'Cancel job', cancelLabel: 'Keep running' }
185
+ : { title: 'Delete job?', message: `${job?.name} and its stored result are deleted.`, confirmLabel: 'Delete job', cancelLabel: 'Cancel' }
186
+ }
187
+
188
+ function needsConfirm(action: ConfirmAction): boolean {
189
+ return props.confirm === true || (typeof props.confirm === 'object' && props.confirm[action] === true)
190
+ }
191
+
192
+ function request(action: ConfirmAction, job?: JobState): void {
193
+ if (!needsConfirm(action)) {
194
+ void run(action, job)
102
195
  return
103
196
  }
104
- const first = focusable[0]
105
- const last = focusable[focusable.length - 1]
106
- const activeElement = document.activeElement as HTMLElement | null
107
- if (!activeElement || !focusable.includes(activeElement)) {
108
- event.preventDefault()
109
- if (event.shiftKey) last?.focus()
110
- else first?.focus()
111
- } else if (event.shiftKey && activeElement === first) {
112
- event.preventDefault()
113
- last?.focus()
114
- } else if (!event.shiftKey && activeElement === last) {
115
- event.preventDefault()
116
- first?.focus()
117
- }
197
+ pending.value = { action, job, ids: action === 'clear' ? deletableIds() : undefined }
198
+ confirmText.value = textFor(action, job)
199
+ confirmOpen.value = true
118
200
  }
119
201
 
120
- function progressText(job: JobState): string {
121
- if (job.status === 'awaiting_input') {
122
- return job.progress.message || 'Waiting for external input'
123
- }
124
- if (job.status === 'queued' && job.queue_position > 0) {
125
- return `Queue position ${job.queue_position}`
126
- }
127
- return job.progress.message || job.progress.stage || jobStatusLabel(job.status)
202
+ function deletableIds(): string[] {
203
+ return tray.deletableJobs.value.map(job => job.job_id).sort()
128
204
  }
129
205
 
130
- function formatTimestamp(value: string | null): string {
131
- if (!value) return ''
132
- const date = new Date(value)
133
- return Number.isNaN(date.getTime())
134
- ? ''
135
- : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)
206
+ // A new source is a new job list: the confirmed target is gone.
207
+ watch(tray.source, () => onConfirmOpen(false))
208
+
209
+ function onConfirmOpen(open: boolean): void {
210
+ confirmOpen.value = open
211
+ if (!open) pending.value = null
136
212
  }
137
213
 
138
- function statusClass(job: JobState): string {
139
- return `mint-jobs-tray__status--${job.status.replace('_', '-')}`
214
+ async function confirmPending(): Promise<void> {
215
+ // A second Confirm click while the dialog fades out does nothing.
216
+ const current = pending.value
217
+ if (!confirmOpen.value || !current) return
218
+ if (current.action === 'clear') {
219
+ // Polling changed the finished jobs: ask again with the new count.
220
+ const ids = deletableIds()
221
+ if (ids.join() !== current.ids?.join()) {
222
+ pending.value = { ...current, ids }
223
+ confirmText.value = textFor('clear')
224
+ return
225
+ }
226
+ }
227
+ confirmOpen.value = false
228
+ pending.value = null
229
+ if (current.action === 'clear') return clearFinished()
230
+ // Polling can end or remove the job while the dialog is open: then do nothing.
231
+ const live = tray.jobs.value.find(job => job.job_id === current.job?.job_id)
232
+ const allowed = live && (current.action === 'cancel'
233
+ ? isActiveJobStatus(live.status) && live.can_cancel
234
+ : live.can_delete)
235
+ if (allowed) await run(current.action, live)
140
236
  }
141
237
 
142
- function jobPercent(job: JobState): number {
143
- return Math.round(normalizeJobPercent(job.progress.percent))
238
+ function run(action: ConfirmAction, job?: JobState): Promise<void> {
239
+ if (action === 'clear') return clearFinished()
240
+ return action === 'cancel' ? cancelJob(job!) : deleteJob(job!)
144
241
  }
145
242
 
146
243
  async function cancelJob(job: JobState): Promise<void> {
@@ -162,249 +259,149 @@ async function clearFinished(): Promise<void> {
162
259
  <template>
163
260
  <Teleport :to="teleportTarget" :disabled="teleportTo === false">
164
261
  <div class="mint-jobs-tray">
165
- <button
166
- ref="triggerRef"
167
- type="button"
168
- class="mint-jobs-tray__trigger"
169
- aria-haspopup="dialog"
170
- :aria-expanded="tray.isOpen.value"
171
- :aria-controls="panelId"
172
- @click="toggleTray"
173
- >
174
- <svg
175
- class="mint-jobs-tray__trigger-icon"
176
- :class="{ 'mint-jobs-tray__trigger-icon--active': tray.activeJobs.value.length > 0 }"
177
- viewBox="0 0 24 24"
178
- fill="none"
179
- stroke="currentColor"
180
- aria-hidden="true"
181
- >
182
- <path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" />
183
- <path d="M9 5a3 3 0 0 1 6 0v2H9V5Z" />
184
- </svg>
185
- <span class="mint-jobs-tray__trigger-label">
186
- {{ tray.featuredJob.value ? jobStatusLabel(tray.featuredJob.value.status) : 'Jobs' }}
187
- </span>
188
- <span
189
- v-if="tray.activeJobs.value.length > 0"
190
- class="mint-jobs-tray__count"
191
- aria-live="polite"
192
- >
193
- {{ tray.activeJobs.value.length }}
194
- </span>
195
- </button>
262
+ <!-- Clicks outside close the panel; the page is not dimmed. -->
263
+ <div
264
+ v-if="tray.isOpen.value"
265
+ class="mint-jobs-tray__scrim"
266
+ aria-hidden="true"
267
+ @click="tray.close()"
268
+ />
196
269
 
197
270
  <Transition name="mint-jobs-tray-fade">
198
- <div
199
- v-if="tray.isOpen.value"
200
- class="mint-jobs-tray__backdrop"
201
- aria-hidden="true"
202
- @click="closeTray"
203
- />
204
- </Transition>
205
-
206
- <Transition name="mint-jobs-tray-slide">
207
271
  <section
208
272
  v-if="tray.isOpen.value"
209
- :id="panelId"
273
+ :id="tray.panelId"
210
274
  ref="panelRef"
211
275
  class="mint-jobs-tray__panel"
212
- role="dialog"
213
- aria-modal="true"
214
276
  :aria-labelledby="titleId"
215
277
  tabindex="-1"
216
- @keydown="onPanelKeydown"
217
278
  >
218
- <header class="mint-jobs-tray__header">
219
- <div>
220
- <h2 :id="titleId" class="mint-jobs-tray__title">{{ title }}</h2>
221
- <p class="mint-jobs-tray__subtitle">{{ subtitle }}</p>
222
- </div>
223
- <div class="mint-jobs-tray__header-actions">
224
- <button
225
- v-if="tray.canRefresh.value"
226
- type="button"
227
- class="mint-jobs-tray__icon-button"
228
- aria-label="Refresh jobs"
229
- :disabled="tray.isLoading.value"
230
- @click="tray.refresh"
231
- >
232
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
233
- <path d="M20 6v5h-5M4 18v-5h5" />
234
- <path d="M18.4 9A7 7 0 0 0 6.1 6.1L4 8m2 7a7 7 0 0 0 11.9 2.9L20 16" />
235
- </svg>
236
- </button>
237
- <button
238
- type="button"
239
- class="mint-jobs-tray__icon-button"
240
- aria-label="Close jobs"
241
- @click="closeTray"
242
- >
243
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
244
- <path d="m6 6 12 12M18 6 6 18" />
245
- </svg>
246
- </button>
247
- </div>
248
- </header>
249
-
250
- <div class="mint-jobs-tray__tabs" role="tablist" aria-label="Job groups">
279
+ <header class="mint-jobs-tray__head">
280
+ <h2 :id="titleId" class="mint-jobs-tray__title">{{ title }}</h2>
281
+ <span class="mint-jobs-tray__head-count">{{ headCount }}</span>
251
282
  <button
283
+ v-if="tray.canRefresh.value"
252
284
  type="button"
253
- role="tab"
254
- data-tab="active"
255
- class="mint-jobs-tray__tab"
256
- :class="{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'active' }"
257
- :aria-selected="tray.activeTab.value === 'active'"
258
- @click="tray.activeTab.value = 'active'"
285
+ class="mint-jobs-tray__icon-button"
286
+ aria-label="Refresh jobs"
287
+ title="Refresh jobs"
288
+ :disabled="tray.isLoading.value"
289
+ @click="tray.refresh"
259
290
  >
260
- Active
261
- <span class="mint-jobs-tray__tab-count">{{ tray.activeJobs.value.length }}</span>
291
+ <RefreshCw :size="14" aria-hidden="true" />
262
292
  </button>
263
293
  <button
264
294
  type="button"
265
- role="tab"
266
- data-tab="history"
267
- class="mint-jobs-tray__tab"
268
- :class="{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'history' }"
269
- :aria-selected="tray.activeTab.value === 'history'"
270
- @click="tray.activeTab.value = 'history'"
295
+ class="mint-jobs-tray__icon-button"
296
+ aria-label="Close jobs"
297
+ title="Close"
298
+ @click="closeTray"
271
299
  >
272
- History
273
- <span class="mint-jobs-tray__tab-count">{{ tray.historyJobs.value.length }}</span>
300
+ <X :size="14" aria-hidden="true" />
274
301
  </button>
275
- </div>
302
+ </header>
276
303
 
277
304
  <p v-if="tray.error.value" class="mint-jobs-tray__error" role="alert">
278
305
  {{ tray.error.value }}
279
306
  </p>
280
307
 
281
308
  <div class="mint-jobs-tray__body">
282
- <template v-if="tray.activeTab.value === 'active'">
283
- <p v-if="tray.activeJobs.value.length === 0" class="mint-jobs-tray__empty">
284
- No active jobs
285
- </p>
286
- <article
287
- v-for="job in activeRows"
309
+ <p v-if="sections.length === 0" class="mint-jobs-tray__empty">No jobs yet</p>
310
+
311
+ <template v-for="section in sections" :key="section.key">
312
+ <div class="mint-jobs-tray__section" :data-section="section.key">
313
+ {{ section.title }}
314
+ <span class="mint-jobs-tray__section-count">· {{ section.rows.length }}</span>
315
+ <button
316
+ v-if="section.key === 'finished' && canClear"
317
+ type="button"
318
+ class="mint-jobs-tray__section-action"
319
+ @click="request('clear')"
320
+ >
321
+ Clear
322
+ </button>
323
+ </div>
324
+
325
+ <div
326
+ v-for="{ job, view } in section.rows"
288
327
  :key="job.job_id"
289
- class="mint-jobs-tray__job"
290
- :class="{ 'mint-jobs-tray__job--awaiting-input': job.status === 'awaiting_input' }"
328
+ class="mint-jobs-tray__row"
329
+ :class="{ 'mint-jobs-tray__row--awaiting-input': view.status === 'awaiting_input' }"
330
+ :data-status="job.status"
291
331
  >
292
- <div class="mint-jobs-tray__job-heading">
293
- <div class="mint-jobs-tray__job-identity">
294
- <button
295
- type="button"
296
- class="mint-jobs-tray__job-select"
297
- @click="emit('select', job)"
298
- >
299
- {{ job.name }}
300
- </button>
301
- <span class="mint-jobs-tray__kind">{{ job.kind }}</span>
302
- </div>
303
- <span class="mint-jobs-tray__status" :class="statusClass(job)">
304
- <span class="mint-jobs-tray__status-dot" aria-hidden="true" />
305
- {{ jobStatusLabel(job.status) }}
306
- </span>
307
- <span v-if="job.status === 'running'" class="mint-jobs-tray__percent">{{ jobPercent(job) }}%</span>
308
- </div>
332
+ <!-- data-status is the real job status; the row view only changes how it looks. -->
333
+ <component
334
+ :is="STATUS_ICONS[view.status]"
335
+ :size="15"
336
+ :class="['mint-jobs-tray__status', `mint-jobs-tray__status--${view.status.replace('_', '-')}`]"
337
+ role="img"
338
+ :aria-label="jobStatusLabel(view.status)"
339
+ />
340
+ <button type="button" class="mint-jobs-tray__row-title" @click="emit('select', job)">
341
+ <slot name="job-title" :job="job">{{ job.name }}</slot>
342
+ </button>
343
+ <span class="mint-jobs-tray__tools">
344
+ <span class="mint-jobs-tray__value">{{ jobValue(job, view.percent) }}</span>
345
+ <slot name="job-actions" :job="job" />
346
+ <button
347
+ v-if="isActiveJobStatus(job.status) && job.can_cancel && tray.canCancel.value"
348
+ type="button"
349
+ class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
350
+ :aria-label="`Cancel ${job.name}`"
351
+ title="Cancel"
352
+ :disabled="tray.isPending(job)"
353
+ @click="request('cancel', job)"
354
+ >
355
+ <Square :size="12" aria-hidden="true" />
356
+ </button>
357
+ <button
358
+ v-if="job.can_delete && tray.canDelete.value"
359
+ type="button"
360
+ class="mint-jobs-tray__icon-button"
361
+ :aria-label="`Delete ${job.name}`"
362
+ title="Delete"
363
+ :disabled="tray.isPending(job)"
364
+ @click="request('delete', job)"
365
+ >
366
+ <Trash2 :size="14" aria-hidden="true" />
367
+ </button>
368
+ </span>
309
369
  <div
310
- v-if="job.status === 'running'"
370
+ v-if="view.percent != null"
311
371
  class="mint-jobs-tray__progress"
312
372
  role="progressbar"
313
- aria-label="Job progress"
373
+ :aria-label="`${job.name} progress`"
314
374
  aria-valuemin="0"
315
375
  aria-valuemax="100"
316
- :aria-valuenow="jobPercent(job)"
376
+ :aria-valuenow="view.percent"
317
377
  >
318
- <span :style="{ width: `${jobPercent(job)}%` }" />
378
+ <span :style="{ width: `${view.percent}%` }" />
319
379
  </div>
320
- <div class="mint-jobs-tray__job-footer">
321
- <span>{{ progressText(job) }}</span>
322
- <div class="mint-jobs-tray__job-actions">
323
- <slot name="job-actions" :job="job" />
324
- <button
325
- v-if="job.can_cancel && tray.canCancel.value"
326
- type="button"
327
- class="mint-jobs-tray__action mint-jobs-tray__action--danger"
328
- aria-label="Cancel job"
329
- :disabled="tray.isPending(job)"
330
- @click="cancelJob(job)"
331
- >
332
- Cancel
333
- </button>
334
- <button
335
- v-if="job.can_delete && tray.canDelete.value"
336
- type="button"
337
- class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
338
- aria-label="Delete job"
339
- :disabled="tray.isPending(job)"
340
- @click="deleteJob(job)"
341
- >
342
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
343
- <path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
344
- </svg>
345
- </button>
346
- </div>
347
- </div>
348
- </article>
349
- </template>
350
-
351
- <template v-else>
352
- <div v-if="canClear" class="mint-jobs-tray__clear-row">
353
- <button
354
- type="button"
355
- class="mint-jobs-tray__action mint-jobs-tray__action--danger"
356
- @click="clearFinished"
380
+ <span
381
+ class="mint-jobs-tray__detail"
382
+ :class="{ 'mint-jobs-tray__detail--error': job.status === 'failed' }"
357
383
  >
358
- Clear finished
359
- </button>
384
+ <slot name="job-detail" :job="job">{{ view.message }}</slot>
385
+ </span>
360
386
  </div>
361
- <p v-if="tray.historyJobs.value.length === 0" class="mint-jobs-tray__empty">
362
- No job history
363
- </p>
364
- <article
365
- v-for="job in tray.historyJobs.value"
366
- :key="job.job_id"
367
- class="mint-jobs-tray__job"
368
- >
369
- <div class="mint-jobs-tray__job-heading">
370
- <div class="mint-jobs-tray__job-identity">
371
- <button
372
- type="button"
373
- class="mint-jobs-tray__job-select"
374
- @click="emit('select', job)"
375
- >
376
- {{ job.name }}
377
- </button>
378
- <span class="mint-jobs-tray__kind">{{ job.kind }}</span>
379
- </div>
380
- <span class="mint-jobs-tray__status" :class="statusClass(job)">
381
- <span class="mint-jobs-tray__status-dot" aria-hidden="true" />
382
- {{ jobStatusLabel(job.status) }}
383
- </span>
384
- </div>
385
- <div class="mint-jobs-tray__job-footer">
386
- <span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>
387
- <div class="mint-jobs-tray__job-actions">
388
- <slot name="job-actions" :job="job" />
389
- <button
390
- v-if="job.can_delete && tray.canDelete.value"
391
- type="button"
392
- class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
393
- aria-label="Delete job"
394
- :disabled="tray.isPending(job)"
395
- @click="deleteJob(job)"
396
- >
397
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
398
- <path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
399
- </svg>
400
- </button>
401
- </div>
402
- </div>
403
- </article>
404
387
  </template>
405
388
  </div>
406
389
  </section>
407
390
  </Transition>
391
+
392
+ <ConfirmDialog
393
+ v-if="confirmEnabled"
394
+ :open="confirmOpen"
395
+ tone="error"
396
+ :title="confirmText?.title"
397
+ :confirm-label="confirmText?.confirmLabel"
398
+ :cancel-label="confirmText?.cancelLabel"
399
+ @update:open="onConfirmOpen"
400
+ @confirm="confirmPending"
401
+ >
402
+ <!-- The marker class lifts the dialog above the tray (jobs-status-tray.css). -->
403
+ <p class="mint-confirm__message mint-jobs-tray__confirm">{{ confirmText?.message }}</p>
404
+ </ConfirmDialog>
408
405
  </div>
409
406
  </Teleport>
410
407
  </template>