@morscherlab/mint-sdk 1.2.0-beta.7 → 1.2.0-beta.9

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 (307) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-oL0fg_A0.js} +5 -3
  3. package/dist/ExperimentPopover-oL0fg_A0.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-BeEOomw_.js} +4 -4
  5. package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
  6. package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-Dphh3RrI.js} +4 -4
  7. package/dist/SettingsModal-Dphh3RrI.js.map +1 -0
  8. package/dist/{EmptyState-NBqf2_8z.js → Skeleton-DxtwJ5gt.js} +110 -92
  9. package/dist/Skeleton-DxtwJ5gt.js.map +1 -0
  10. package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
  11. package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
  12. package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
  13. package/dist/components/ActionMenu.vue.d.ts +1 -1
  14. package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
  15. package/dist/components/AuditTrail.vue.d.ts +13 -0
  16. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  17. package/dist/components/DataFrame.vue.d.ts +14 -2
  18. package/dist/components/DatePicker.vue.d.ts +1 -1
  19. package/dist/components/DateTimePicker.vue.d.ts +2 -0
  20. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  21. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  22. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  23. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  24. package/dist/components/ProgressBar.vue.d.ts +1 -1
  25. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  26. package/dist/components/ReagentList.vue.d.ts +29 -1
  27. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  28. package/dist/components/SampleSelector.drag.d.ts +13 -0
  29. package/dist/components/SampleSelector.tree.d.ts +2 -0
  30. package/dist/components/SampleSelector.vue.d.ts +34 -1
  31. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  32. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
  33. package/dist/components/TimePicker.vue.d.ts +1 -0
  34. package/dist/components/WellPlate.geometry.d.ts +15 -0
  35. package/dist/components/WellPlate.interaction.d.ts +13 -0
  36. package/dist/components/WellPlate.vue.d.ts +29 -0
  37. package/dist/components/index.js +6 -6
  38. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  39. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  40. package/dist/{components-B80gV0P6.js → components-CgAUzF8F.js} +1930 -1688
  41. package/dist/components-CgAUzF8F.js.map +1 -0
  42. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  43. package/dist/composables/index.d.ts +1 -0
  44. package/dist/composables/index.js +9 -9
  45. package/dist/composables/useDropdownState.d.ts +18 -2
  46. package/dist/composables/useEventStream.d.ts +9 -0
  47. package/dist/composables/useListReorder.d.ts +90 -0
  48. package/dist/composables/useMenuKeyboard.d.ts +2 -2
  49. package/dist/composables/usePluginConfig.d.ts +3 -1
  50. package/dist/composables/useRequestSyncState.d.ts +42 -2
  51. package/dist/composables/useScheduleDrag.d.ts +1 -0
  52. package/dist/{composables-C8mmD5ta.js → composables-B_pia3hO.js} +13 -69
  53. package/dist/composables-B_pia3hO.js.map +1 -0
  54. package/dist/{useApi-CDBq6z23.js → errors-_W0-LbA6.js} +102 -3
  55. package/dist/errors-_W0-LbA6.js.map +1 -0
  56. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cwl9AYxw.js} +3 -8
  57. package/dist/experiment-Cwl9AYxw.js.map +1 -0
  58. package/dist/experiment-utils-DV3DrvE_.js.map +1 -1
  59. package/dist/index.js +14 -14
  60. package/dist/install.js +2 -7
  61. package/dist/install.js.map +1 -1
  62. package/dist/permissions.d.ts +42 -1
  63. package/dist/permissions.js +20 -10
  64. package/dist/permissions.js.map +1 -1
  65. package/dist/stores/index.d.ts +1 -1
  66. package/dist/stores/index.js +3 -3
  67. package/dist/stores/settings.d.ts +31 -1
  68. package/dist/styles.css +9572 -27877
  69. package/dist/templates/index.js +2 -4
  70. package/dist/{templates-CK72aKO1.js → templates-Qq113pyZ.js} +291 -85
  71. package/dist/templates-Qq113pyZ.js.map +1 -0
  72. package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-aMZlb4Sc.js} +197 -1089
  73. package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
  74. package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-DitCtrTT.js} +21 -5
  75. package/dist/useDropdownState-DitCtrTT.js.map +1 -0
  76. package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-I8HC8hUu.js} +3 -3
  77. package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
  78. package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-TCxNnqeb.js} +2 -2
  79. package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
  80. package/dist/{useProtocolTemplates-Dq5Pzwyd.js → useProtocolTemplates-BmX_iVBR.js} +185 -157
  81. package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
  82. package/dist/utils/color.d.ts +13 -0
  83. package/dist/utils/errors.d.ts +9 -0
  84. package/package.json +1 -1
  85. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  86. package/src/__tests__/components/DataFrame.test.ts +183 -11
  87. package/src/__tests__/components/DataStates.test.ts +176 -0
  88. package/src/__tests__/components/DatePicker.test.ts +76 -0
  89. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  90. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  91. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  92. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  93. package/src/__tests__/components/RackEditor.test.ts +35 -0
  94. package/src/__tests__/components/ReagentList.test.ts +64 -2
  95. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  96. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  97. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  98. package/src/__tests__/components/SegmentedControl.test.ts +88 -0
  99. package/src/__tests__/components/TimePicker.test.ts +61 -0
  100. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  101. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  102. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  103. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  104. package/src/__tests__/stores/settings.test.ts +71 -1
  105. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  106. package/src/__tests__/utils/permissions.test.ts +68 -0
  107. package/src/components/ActionMenu.vue +45 -88
  108. package/src/components/AlertBox.vue +0 -4
  109. package/src/components/AppAvatarMenu.vue +2 -14
  110. package/src/components/AppLayout.vue +0 -4
  111. package/src/components/AppPluginSwitcher.story.vue +87 -0
  112. package/src/components/AppPluginSwitcher.vue +2 -5
  113. package/src/components/AppSidebar.vue +0 -4
  114. package/src/components/AppToastContainer.vue +6 -8
  115. package/src/components/AppTopBar.vue +0 -4
  116. package/src/components/ArtifactSaveDialog.vue +0 -4
  117. package/src/components/ArtifactSelectorModal.vue +0 -4
  118. package/src/components/AuditTrail.story.vue +18 -0
  119. package/src/components/AuditTrail.vue +27 -7
  120. package/src/components/AutoGroupModal.vue +0 -4
  121. package/src/components/Avatar.vue +0 -4
  122. package/src/components/BaseButton.vue +0 -4
  123. package/src/components/BaseCheckbox.vue +0 -4
  124. package/src/components/BaseInput.vue +0 -4
  125. package/src/components/BaseModal.vue +0 -4
  126. package/src/components/BasePill.vue +0 -4
  127. package/src/components/BaseRadioGroup.vue +0 -4
  128. package/src/components/BaseSelect.vue +0 -4
  129. package/src/components/BaseSlider.vue +0 -4
  130. package/src/components/BaseTabs.vue +0 -4
  131. package/src/components/BaseTextarea.vue +0 -4
  132. package/src/components/BaseToggle.vue +0 -4
  133. package/src/components/BatchProgressList.story.vue +18 -0
  134. package/src/components/BatchProgressList.vue +32 -4
  135. package/src/components/Breadcrumb.vue +0 -4
  136. package/src/components/Calendar.vue +0 -4
  137. package/src/components/ChartContainer.story.vue +1 -1
  138. package/src/components/ChartContainer.vue +0 -4
  139. package/src/components/ChemicalFormula.vue +0 -4
  140. package/src/components/CollapsibleCard.vue +0 -4
  141. package/src/components/ColorSlider.vue +0 -4
  142. package/src/components/ConcentrationInput.vue +0 -4
  143. package/src/components/ConfirmDialog.vue +0 -4
  144. package/src/components/DataFrame.story.vue +14 -0
  145. package/src/components/DataFrame.vue +97 -23
  146. package/src/components/DatePicker.vue +73 -27
  147. package/src/components/DateTimePicker.vue +45 -17
  148. package/src/components/Divider.vue +0 -4
  149. package/src/components/DoseCalculator.vue +0 -4
  150. package/src/components/DropdownButton.vue +2 -14
  151. package/src/components/EmptyState.vue +0 -4
  152. package/src/components/ExperimentCodeBadge.vue +0 -4
  153. package/src/components/ExperimentDataViewer.story.vue +3 -0
  154. package/src/components/ExperimentDataViewer.vue +33 -13
  155. package/src/components/ExperimentPopover.vue +2 -5
  156. package/src/components/ExperimentSelectorModal.vue +0 -4
  157. package/src/components/ExperimentTimeline.story.vue +18 -0
  158. package/src/components/ExperimentTimeline.vue +81 -75
  159. package/src/components/FileUploader.vue +0 -4
  160. package/src/components/FitPanel.vue +0 -4
  161. package/src/components/FormActions.story.vue +96 -0
  162. package/src/components/FormActions.vue +0 -4
  163. package/src/components/FormBuilder.vue +0 -4
  164. package/src/components/FormField.vue +0 -4
  165. package/src/components/FormulaInput.vue +0 -4
  166. package/src/components/GeneratedPathInput.story.vue +68 -0
  167. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  168. package/src/components/GeneratedPlotlyResult.vue +3 -3
  169. package/src/components/GeneratedPluginView.story.vue +145 -0
  170. package/src/components/GeneratedPluginView.vue +10 -10
  171. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  172. package/src/components/GeneratedResultRenderer.vue +4 -4
  173. package/src/components/GroupAssigner.vue +67 -5
  174. package/src/components/IconButton.vue +0 -4
  175. package/src/components/InstrumentAlertLog.vue +0 -4
  176. package/src/components/InstrumentStateBadge.vue +0 -4
  177. package/src/components/InstrumentStatusCard.vue +0 -4
  178. package/src/components/JobsStatusTray.vue +0 -4
  179. package/src/components/LcmsSequenceTable.vue +81 -50
  180. package/src/components/LoadingSpinner.vue +0 -4
  181. package/src/components/MobileSupportGate.vue +0 -4
  182. package/src/components/MoleculeInput.vue +0 -4
  183. package/src/components/MultiSelect.vue +0 -4
  184. package/src/components/NumberInput.vue +0 -4
  185. package/src/components/PlateMapEditor.vue +3 -14
  186. package/src/components/PluginIcon.vue +0 -4
  187. package/src/components/ProgressBar.vue +0 -4
  188. package/src/components/ProtocolStepEditor.vue +2 -4
  189. package/src/components/RackEditor.vue +51 -70
  190. package/src/components/ReagentEditor.vue +0 -4
  191. package/src/components/ReagentList.story.vue +18 -0
  192. package/src/components/ReagentList.vue +127 -82
  193. package/src/components/ResourceCard.vue +0 -4
  194. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  195. package/src/components/RuntimeUpdateNotice.vue +4 -4
  196. package/src/components/SampleHierarchyTree.story.vue +18 -0
  197. package/src/components/SampleHierarchyTree.vue +32 -9
  198. package/src/components/SampleLegend.vue +2 -8
  199. package/src/components/SampleSelector.drag.ts +51 -0
  200. package/src/components/SampleSelector.story.vue +6 -0
  201. package/src/components/SampleSelector.tree.ts +2 -0
  202. package/src/components/SampleSelector.vue +49 -10
  203. package/src/components/SampleSelectorGroupNode.vue +19 -0
  204. package/src/components/ScheduleCalendar.story.vue +6 -0
  205. package/src/components/ScheduleCalendar.vue +82 -7
  206. package/src/components/ScientificNumber.vue +0 -4
  207. package/src/components/SectionCard.vue +0 -4
  208. package/src/components/SegmentedControl.vue +48 -5
  209. package/src/components/SequenceInput.vue +0 -4
  210. package/src/components/SequenceProgressBar.story.vue +110 -0
  211. package/src/components/SequenceProgressBar.vue +0 -4
  212. package/src/components/SettingsModal.vue +0 -4
  213. package/src/components/Skeleton.vue +0 -4
  214. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  215. package/src/components/SmartGroupManual.vue +0 -4
  216. package/src/components/StatusIndicator.vue +0 -4
  217. package/src/components/StepWizard.vue +0 -4
  218. package/src/components/TagsInput.vue +4 -6
  219. package/src/components/ThemeToggle.vue +0 -4
  220. package/src/components/TimePicker.vue +36 -7
  221. package/src/components/TimeRangeInput.vue +0 -4
  222. package/src/components/Tooltip.story.vue +3 -3
  223. package/src/components/Tooltip.vue +0 -4
  224. package/src/components/UnitInput.vue +0 -4
  225. package/src/components/WellPlate.geometry.ts +60 -1
  226. package/src/components/WellPlate.interaction.ts +44 -0
  227. package/src/components/WellPlate.story.vue +8 -0
  228. package/src/components/WellPlate.vue +53 -7
  229. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  230. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  231. package/src/components/internal/DataStateInternal.vue +74 -0
  232. package/src/components/internal/FormSectionRenderer.vue +0 -4
  233. package/src/components/internal/WellEditPopupInternal.vue +0 -4
  234. package/src/composables/controlSchemaTypes.ts +34 -0
  235. package/src/composables/index.ts +10 -0
  236. package/src/composables/useDropdownState.ts +35 -5
  237. package/src/composables/useEventStream.ts +14 -2
  238. package/src/composables/useFileImport.ts +2 -7
  239. package/src/composables/useGeneratedAnalysis.ts +8 -9
  240. package/src/composables/useListReorder.story.vue +143 -0
  241. package/src/composables/useListReorder.ts +242 -0
  242. package/src/composables/useMenuKeyboard.ts +2 -2
  243. package/src/composables/useOptimisticMutation.ts +1 -15
  244. package/src/composables/usePluginConfig.ts +3 -1
  245. package/src/composables/useRackEditor.ts +2 -11
  246. package/src/composables/useRequestSyncState.ts +98 -28
  247. package/src/composables/useResourceCrud.ts +1 -15
  248. package/src/composables/useScheduleDrag.ts +37 -0
  249. package/src/composables/useWellPainting.ts +5 -29
  250. package/src/composables/useWellPlateEditor.ts +1 -11
  251. package/src/permissions.ts +48 -9
  252. package/src/stores/experiment.ts +2 -7
  253. package/src/stores/index.ts +9 -1
  254. package/src/stores/settings.ts +93 -1
  255. package/src/styles/components/audit-trail.css +5 -5
  256. package/src/styles/components/batch-progress-list.css +7 -0
  257. package/src/styles/components/color-slider.css +2 -2
  258. package/src/styles/components/data-state.css +30 -0
  259. package/src/styles/components/dataframe.css +29 -35
  260. package/src/styles/components/experiment-data-viewer.css +4 -4
  261. package/src/styles/components/experiment-timeline.css +3 -30
  262. package/src/styles/components/form-builder.css +5 -5
  263. package/src/styles/components/lcms-sequence-table.css +16 -1
  264. package/src/styles/components/multi-select.css +1 -1
  265. package/src/styles/components/reagent-list.css +38 -31
  266. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  267. package/src/styles/components/sample-selector.css +9 -2
  268. package/src/styles/components/schedule-calendar.css +8 -0
  269. package/src/styles/components/scientific-number.css +1 -1
  270. package/src/styles/components/well-plate.css +8 -0
  271. package/src/styles/index.css +8 -0
  272. package/src/styles/variables.css +29 -0
  273. package/src/templates/builderPresetControls.ts +1 -1
  274. package/src/templates/componentPlateHelpers.ts +3 -1
  275. package/src/templates/doseResponseAdapters.ts +2 -13
  276. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  277. package/src/templates/plateMapBuilder.ts +2 -2
  278. package/src/templates/qpcrPlateBuilder.ts +1 -1
  279. package/src/templates/templateValidators.ts +1 -1
  280. package/src/templates/timeCourseBuilder.ts +2 -2
  281. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  282. package/src/utils/color.ts +51 -0
  283. package/src/utils/errors.ts +29 -0
  284. package/dist/EmptyState-NBqf2_8z.js.map +0 -1
  285. package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
  286. package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
  287. package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
  288. package/dist/components-B80gV0P6.js.map +0 -1
  289. package/dist/composables/autoGroupConstants.d.ts +0 -1
  290. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  291. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  292. package/dist/composables-C8mmD5ta.js.map +0 -1
  293. package/dist/experiment-Y9Iz7KYi.js.map +0 -1
  294. package/dist/templates/builderDefaults.d.ts +0 -1
  295. package/dist/templates/plateGeometry.d.ts +0 -4
  296. package/dist/templates-CK72aKO1.js.map +0 -1
  297. package/dist/useApi-CDBq6z23.js.map +0 -1
  298. package/dist/useControlSchema-_OVhavKq.js.map +0 -1
  299. package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
  300. package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
  301. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  302. package/src/composables/autoGroupConstants.ts +0 -4
  303. package/src/composables/autoGroupGrouping.ts +0 -148
  304. package/src/composables/useAutoGroupInputSources.ts +0 -147
  305. package/src/templates/builderDefaults.ts +0 -11
  306. package/src/templates/plateGeometry.ts +0 -62
  307. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
@@ -1,8 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  /** Combined date and time picker with a dropdown calendar, time slots, and 12/24 h format support. */
3
- import { computed, watch, nextTick } from 'vue'
3
+ import { computed, ref, watch, nextTick } from 'vue'
4
4
  import { useCalendarGrid, type CalendarGridDay } from '../composables/useCalendarGrid'
5
5
  import { useDropdownState } from '../composables/useDropdownState'
6
+ import { useFocusTrap } from '../composables/useFocusTrap'
6
7
  import { formatTime, formatTimeSlot, generateTimeSlots, toMinutes } from '../composables/useTimeUtils'
7
8
  import CalendarGridPanelInternal from './internal/CalendarGridPanelInternal.vue'
8
9
 
@@ -35,7 +36,10 @@ const emit = defineEmits<{
35
36
  'update:modelValue': [value: string | undefined]
36
37
  }>()
37
38
 
38
- const { isOpen, rootRef, close, toggle } = useDropdownState()
39
+ const dropdownRef = ref<HTMLElement | null>(null)
40
+ const { isOpen, rootRef, triggerRef, open, close, toggle } = useDropdownState()
41
+ // Non-modal dialog: Tab stays inside the panel, focus returns to the input on close.
42
+ useFocusTrap({ containerRef: dropdownRef, active: isOpen })
39
43
  const {
40
44
  weekDays,
41
45
  monthYear,
@@ -148,18 +152,42 @@ function toggleDropdown() {
148
152
  toggle()
149
153
  }
150
154
 
151
- watch(isOpen, (open) => {
152
- if (open && selectedDate.value) {
155
+ function handleTriggerKeydown(event: KeyboardEvent) {
156
+ if (props.disabled || isOpen.value) return
157
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
158
+ event.preventDefault()
159
+ open()
160
+ }
161
+ }
162
+
163
+ /** Moves focus into the calendar grid: selected day, else today, else the first day. */
164
+ function focusCalendar() {
165
+ const panel = dropdownRef.value
166
+ if (!panel) return
167
+ const target =
168
+ panel.querySelector<HTMLElement>('.mint-date-picker__day--selected') ??
169
+ panel.querySelector<HTMLElement>('.mint-date-picker__day--today') ??
170
+ panel.querySelector<HTMLElement>('.mint-date-picker__day:not([disabled])')
171
+ target?.focus()
172
+ }
173
+
174
+ watch(isOpen, async (opened) => {
175
+ if (!opened) return
176
+ if (selectedDate.value) {
153
177
  setCurrentMonthFromDate(selectedDate.value)
154
- nextTick(() => {
155
- // Scroll time grid to selected time
156
- const grid = rootRef.value?.querySelector('.mint-datetime-picker__time-grid')
157
- const active = rootRef.value?.querySelector('.mint-datetime-picker__time-chip--active')
158
- if (grid && active) {
159
- active.scrollIntoView({ block: 'center' })
160
- }
161
- })
162
178
  }
179
+ await nextTick()
180
+ if (selectedDate.value) {
181
+ // Scroll time grid to selected time
182
+ const grid = dropdownRef.value?.querySelector('.mint-datetime-picker__time-grid')
183
+ const active = dropdownRef.value?.querySelector('.mint-datetime-picker__time-chip--active')
184
+ if (grid && active) {
185
+ active.scrollIntoView({ block: 'center' })
186
+ }
187
+ }
188
+ // A second tick so this focus lands after the focus trap's initial focus.
189
+ await nextTick()
190
+ focusCalendar()
163
191
  })
164
192
  </script>
165
193
 
@@ -172,8 +200,11 @@ watch(isOpen, (open) => {
172
200
  </svg>
173
201
  </div>
174
202
  <input
203
+ ref="triggerRef"
175
204
  type="text"
176
205
  readonly
206
+ aria-haspopup="dialog"
207
+ :aria-expanded="isOpen"
177
208
  :value="displayValue"
178
209
  :placeholder="placeholder"
179
210
  :disabled="disabled"
@@ -185,6 +216,7 @@ watch(isOpen, (open) => {
185
216
  ]"
186
217
  aria-label="Select date and time"
187
218
  @click="toggleDropdown"
219
+ @keydown="handleTriggerKeydown"
188
220
  />
189
221
  <div class="mint-datetime-picker__icon-clock">
190
222
  <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -203,9 +235,9 @@ watch(isOpen, (open) => {
203
235
  >
204
236
  <div
205
237
  v-if="isOpen"
238
+ ref="dropdownRef"
206
239
  class="mint-datetime-picker__dropdown"
207
240
  role="dialog"
208
- aria-modal="true"
209
241
  aria-label="Date and time picker"
210
242
  >
211
243
  <!-- Calendar section -->
@@ -265,7 +297,3 @@ watch(isOpen, (open) => {
265
297
  </Transition>
266
298
  </div>
267
299
  </template>
268
-
269
- <style>
270
- @import '../styles/components/datetime-picker.css';
271
- </style>
@@ -43,7 +43,3 @@ withDefaults(defineProps<Props>(), {
43
43
  </template>
44
44
  </div>
45
45
  </template>
46
-
47
- <style>
48
- @import '../styles/components/divider.css';
49
- </style>
@@ -410,7 +410,3 @@ watch(activeMode, (mode) => {
410
410
  </div>
411
411
  </div>
412
412
  </template>
413
-
414
- <style>
415
- @import '../styles/components/dose-calculator.css';
416
- </style>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  /** Button that opens a dropdown menu for selecting one option from a list. */
3
- import { computed, nextTick, ref } from 'vue'
3
+ import { computed, ref } from 'vue'
4
4
  import { useDropdownState } from '../composables/useDropdownState'
5
5
  import { useMenuKeyboard } from '../composables/useMenuKeyboard'
6
6
  import type { SelectOption, SelectOptionInput, ButtonVariant, ButtonSize } from '../types'
@@ -54,12 +54,11 @@ const emit = defineEmits<{
54
54
  select: [option: SelectOption<string | number>]
55
55
  }>()
56
56
 
57
- const { isOpen, rootRef, open, close: closeDropdown, toggle: toggleDropdown } = useDropdownState()
57
+ const { isOpen, rootRef, triggerRef, open, close, toggle: toggleDropdown } = useDropdownState()
58
58
  const normalizedOptions = computed<SelectOption<string | number>[]>(() =>
59
59
  props.options.map(normalizeOptionInput)
60
60
  )
61
61
 
62
- const triggerRef = ref<HTMLElement | null>(null)
63
62
  const panelRef = ref<HTMLElement | null>(null)
64
63
 
65
64
  function optionItems(): HTMLElement[] {
@@ -67,13 +66,6 @@ function optionItems(): HTMLElement[] {
67
66
  return Array.from(panelRef.value.querySelectorAll<HTMLElement>('[role="option"]:not([aria-disabled="true"])'))
68
67
  }
69
68
 
70
- function close(returnFocus = false) {
71
- closeDropdown()
72
- if (returnFocus) {
73
- void nextTick(() => triggerRef.value?.focus())
74
- }
75
- }
76
-
77
69
  const { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({
78
70
  items: optionItems,
79
71
  isOpen: () => isOpen.value,
@@ -187,7 +179,3 @@ function selectOption(option: SelectOption<string | number>) {
187
179
  </Transition>
188
180
  </div>
189
181
  </template>
190
-
191
- <style>
192
- @import '../styles/components/dropdown-button.css';
193
- </style>
@@ -65,7 +65,3 @@ const defaultIconPaths = [
65
65
  </div>
66
66
  </div>
67
67
  </template>
68
-
69
- <style>
70
- @import '../styles/components/empty-state.css';
71
- </style>
@@ -58,7 +58,3 @@ function handleKeydown(event: KeyboardEvent) {
58
58
  {{ copied ? 'Copied!' : code }}
59
59
  </span>
60
60
  </template>
61
-
62
- <style>
63
- @import '../styles/components/experiment-code-badge.css';
64
- </style>
@@ -96,6 +96,7 @@ const views = ['summary', 'tree', 'table'] as const
96
96
  <template #default="{ state }">
97
97
  <div style="padding: 1rem; max-width: 800px; margin: 0 auto;">
98
98
  <ExperimentDataViewer
99
+ :error="state.error || null"
99
100
  :tree-data="treeData"
100
101
  :table-data="tableData"
101
102
  :summary-data="state.showSummary ? summaryData : null"
@@ -107,6 +108,8 @@ const views = ['summary', 'tree', 'table'] as const
107
108
  </template>
108
109
 
109
110
  <template #controls="{ state }">
111
+
112
+ <HstText v-model="state.error" title="Error Message" />
110
113
  <HstSelect
111
114
  v-model="state.defaultView"
112
115
  title="Default View"
@@ -8,6 +8,7 @@ import DataFrame from './DataFrame.vue'
8
8
  import SegmentedControl from './SegmentedControl.vue'
9
9
  import BaseButton from './BaseButton.vue'
10
10
  import Skeleton from './Skeleton.vue'
11
+ import DataStateInternal from './internal/DataStateInternal.vue'
11
12
 
12
13
  interface Props {
13
14
  treeData?: TreeNode[]
@@ -20,6 +21,11 @@ interface Props {
20
21
  pluginRoutePrefix?: string
21
22
  experimentId?: number
22
23
  loading?: boolean
24
+ /**
25
+ * Error message. When set (or when the auto-fetch fails), the content area
26
+ * renders an error state instead of the data views.
27
+ */
28
+ error?: string | null
23
29
  downloadJsonUrl?: string
24
30
  downloadCsvUrl?: string
25
31
  autoFetch?: boolean
@@ -29,6 +35,7 @@ const props = withDefaults(defineProps<Props>(), {
29
35
  title: 'Data',
30
36
  defaultView: 'summary',
31
37
  loading: false,
38
+ error: null,
32
39
  autoFetch: true,
33
40
  })
34
41
 
@@ -61,6 +68,9 @@ const mergedSummaryData = computed(() =>
61
68
  const isLoading = computed(() =>
62
69
  props.loading || expData.isLoading.value,
63
70
  )
71
+ const activeError = computed<string | null>(() =>
72
+ props.error ?? expData.error.value,
73
+ )
64
74
 
65
75
  const emit = defineEmits<{
66
76
  'open-plugin': []
@@ -179,13 +189,20 @@ function handleDownloadCsv() {
179
189
  </div>
180
190
 
181
191
  <div class="mint-data-viewer__content">
182
- <div v-if="isLoading" class="mint-data-viewer__loading">
183
- <div class="mint-data-viewer__skeleton">
184
- <Skeleton width="40%" height="16px" />
185
- <Skeleton width="100%" height="12px" />
186
- <Skeleton width="100%" height="12px" />
187
- <Skeleton width="75%" height="12px" />
188
- </div>
192
+ <div v-if="activeError" class="mint-data-viewer__state">
193
+ <slot name="error" :error="activeError">
194
+ <DataStateInternal state="error" :title="activeError" />
195
+ </slot>
196
+ </div>
197
+ <div v-else-if="isLoading" class="mint-data-viewer__loading">
198
+ <slot name="loading">
199
+ <div class="mint-data-viewer__skeleton">
200
+ <Skeleton width="40%" height="16px" />
201
+ <Skeleton width="100%" height="12px" />
202
+ <Skeleton width="100%" height="12px" />
203
+ <Skeleton width="75%" height="12px" />
204
+ </div>
205
+ </slot>
189
206
  </div>
190
207
  <template v-else>
191
208
  <!-- Summary View -->
@@ -280,14 +297,17 @@ function handleDownloadCsv() {
280
297
  :searchable="true"
281
298
  :sortable="true"
282
299
  />
283
- <div v-else-if="viewMode === 'table'" class="mint-data-viewer__empty">
284
- No tabular data available. Use tree view.
300
+ <div v-else-if="viewMode === 'table'" class="mint-data-viewer__state">
301
+ <slot name="empty">
302
+ <DataStateInternal
303
+ state="empty"
304
+ title="No tabular data available"
305
+ description="Switch to the tree view to inspect this experiment."
306
+ size="sm"
307
+ />
308
+ </slot>
285
309
  </div>
286
310
  </template>
287
311
  </div>
288
312
  </div>
289
313
  </template>
290
-
291
- <style>
292
- @import '../styles/components/experiment-data-viewer.css';
293
- </style>
@@ -57,7 +57,7 @@ const emit = defineEmits<{
57
57
  detach: []
58
58
  }>()
59
59
 
60
- const { isOpen, rootRef: popoverRef, close, toggle } = useDropdownState({
60
+ const { isOpen, rootRef: popoverRef, triggerRef, close, toggle } = useDropdownState({
61
61
  closeOnEscape: false,
62
62
  })
63
63
  const showSuccess = ref(false)
@@ -118,6 +118,7 @@ onUnmounted(() => {
118
118
  <!-- Left: experiment trigger (opens popover). Prefer the human-readable
119
119
  name; the formal code, when present, stays in the detail panel. -->
120
120
  <button
121
+ ref="triggerRef"
121
122
  type="button"
122
123
  :class="[
123
124
  'mint-experiment-popover__trigger',
@@ -306,7 +307,3 @@ onUnmounted(() => {
306
307
  />
307
308
  </div>
308
309
  </template>
309
-
310
- <style>
311
- @import '../styles/components/experiment-popover.css';
312
- </style>
@@ -426,7 +426,3 @@ watch(
426
426
  </div>
427
427
  </BaseModal>
428
428
  </template>
429
-
430
- <style>
431
- @import '../styles/components/experiment-selector-modal.css';
432
- </style>
@@ -57,6 +57,8 @@ const mockSteps: ProtocolStep[] = [
57
57
 
58
58
  function initState() {
59
59
  return {
60
+ loading: false,
61
+ error: '',
60
62
  modelValue: mockSteps,
61
63
  orientation: 'vertical' as const,
62
64
  showDuration: true,
@@ -76,6 +78,8 @@ function initState() {
76
78
  <template #default="{ state }">
77
79
  <div style="padding: 2rem; max-width: 600px;">
78
80
  <ExperimentTimeline
81
+ :loading="state.loading"
82
+ :error="state.error || null"
79
83
  v-model="state.modelValue"
80
84
  :orientation="state.orientation"
81
85
  :show-duration="state.showDuration"
@@ -89,6 +93,8 @@ function initState() {
89
93
  </div>
90
94
  </template>
91
95
  <template #controls="{ state }">
96
+ <HstCheckbox v-model="state.loading" title="Loading" />
97
+ <HstText v-model="state.error" title="Error Message" />
92
98
  <HstSelect
93
99
  v-model="state.orientation"
94
100
  title="Orientation"
@@ -157,5 +163,17 @@ function initState() {
157
163
  <ExperimentTimeline :model-value="[]" editable />
158
164
  </div>
159
165
  </Variant>
166
+
167
+ <Variant title="Loading">
168
+ <div style="padding: 2rem; max-width: 600px;">
169
+ <ExperimentTimeline :model-value="[]" loading />
170
+ </div>
171
+ </Variant>
172
+
173
+ <Variant title="Error">
174
+ <div style="padding: 2rem; max-width: 600px;">
175
+ <ExperimentTimeline :model-value="[]" error="Could not load the protocol." />
176
+ </div>
177
+ </Variant>
160
178
  </Story>
161
179
  </template>
@@ -1,12 +1,14 @@
1
1
  <script setup lang="ts">
2
2
  /** Horizontal or vertical timeline of ordered protocol steps with status indicators, drag-to-reorder, and step-type iconography. */
3
- import { ref, computed } from 'vue'
3
+ import { ref, computed, type ComponentPublicInstance } from 'vue'
4
4
  import type { ProtocolStep, ProtocolStepStatus } from '../types'
5
+ import { useListReorder } from '../composables/useListReorder'
5
6
  import {
6
7
  PROTOCOL_STEP_TYPE_COLORS as stepTypeColors,
7
8
  PROTOCOL_STEP_TYPE_ICONS as stepTypeIcons,
8
9
  formatProtocolDuration as formatDuration,
9
10
  } from './ProtocolStep.presentation'
11
+ import DataStateInternal from './internal/DataStateInternal.vue'
10
12
 
11
13
  interface Props {
12
14
  modelValue?: ProtocolStep[]
@@ -19,6 +21,13 @@ interface Props {
19
21
  size?: 'sm' | 'md' | 'lg'
20
22
  colorByStatus?: boolean
21
23
  colorByType?: boolean
24
+ /** Replace the timeline with a loading placeholder. */
25
+ loading?: boolean
26
+ /**
27
+ * Error message. When set, the timeline is replaced by an error state.
28
+ * `null`/`undefined` renders normally.
29
+ */
30
+ error?: string | null
22
31
  }
23
32
 
24
33
  const props = withDefaults(defineProps<Props>(), {
@@ -32,6 +41,8 @@ const props = withDefaults(defineProps<Props>(), {
32
41
  size: 'md',
33
42
  colorByStatus: true,
34
43
  colorByType: false,
44
+ loading: false,
45
+ error: null,
35
46
  })
36
47
 
37
48
  const emit = defineEmits<{
@@ -46,8 +57,7 @@ const emit = defineEmits<{
46
57
  }>()
47
58
 
48
59
  const localExpandedId = ref<string | undefined>(props.expandedStepId)
49
- const draggedStepId = ref<string | null>(null)
50
- const dragOverStepId = ref<string | null>(null)
60
+ const stepRefs = new Map<string, HTMLElement>()
51
61
 
52
62
  const expandedId = computed({
53
63
  get: () => props.expandedStepId ?? localExpandedId.value,
@@ -123,54 +133,38 @@ function handleAddStep(afterStepId?: string) {
123
133
  emit('step-add', afterStepId)
124
134
  }
125
135
 
126
- function handleDragStart(event: DragEvent, step: ProtocolStep) {
127
- if (!props.editable) return
128
- draggedStepId.value = step.id
129
- if (event.dataTransfer) {
130
- event.dataTransfer.effectAllowed = 'move'
131
- event.dataTransfer.setData('text/plain', step.id)
132
- }
133
- }
134
-
135
- function handleDragOver(event: DragEvent, step: ProtocolStep) {
136
- if (!props.editable || !draggedStepId.value) return
137
- event.preventDefault()
138
- dragOverStepId.value = step.id
139
- }
140
-
141
- function handleDragLeave() {
142
- dragOverStepId.value = null
143
- }
144
-
145
- function handleDrop(event: DragEvent, targetStep: ProtocolStep) {
146
- if (!props.editable || !draggedStepId.value) return
147
- event.preventDefault()
136
+ // Steps are a plain ordered list, so pointer drag and the Alt+Arrow keyboard
137
+ // alternative both come from the shared reorder composable.
138
+ const {
139
+ draggingId: draggedStepId,
140
+ dragOverId: dragOverStepId,
141
+ announcement: reorderAnnouncement,
142
+ announcerStyle,
143
+ onDragStart: onStepDragStart,
144
+ onDragOver: onStepDragOver,
145
+ onDragLeave: onStepDragLeave,
146
+ onDrop: onStepDrop,
147
+ onDragEnd: onStepDragEnd,
148
+ onKeydown: onStepReorderKeydown,
149
+ } = useListReorder<ProtocolStep>({
150
+ items: () => sortedSteps.value,
151
+ getId: (step) => step.id,
152
+ getLabel: (step) => step.name,
153
+ disabled: () => !props.editable,
154
+ getElement: (id) => stepRefs.get(id),
155
+ itemNoun: 'Step',
156
+ onReorder: ({ toIndex, items }) => {
157
+ emit('step-reorder', items[toIndex].id, toIndex)
158
+ emit('update:modelValue', items.map((step, index) => ({ ...step, order: index })))
159
+ },
160
+ })
148
161
 
149
- const draggedStep = props.modelValue.find(s => s.id === draggedStepId.value)
150
- if (!draggedStep || draggedStep.id === targetStep.id) {
151
- draggedStepId.value = null
152
- dragOverStepId.value = null
153
- return
162
+ function setStepRef(id: string, el: Element | ComponentPublicInstance | null) {
163
+ if (el instanceof HTMLElement) {
164
+ stepRefs.set(id, el)
165
+ } else {
166
+ stepRefs.delete(id)
154
167
  }
155
-
156
- const steps = [...props.modelValue]
157
- const draggedIndex = steps.findIndex(s => s.id === draggedStepId.value)
158
- const targetIndex = steps.findIndex(s => s.id === targetStep.id)
159
-
160
- steps.splice(draggedIndex, 1)
161
- steps.splice(targetIndex, 0, draggedStep)
162
-
163
- const reordered = steps.map((s, i) => ({ ...s, order: i }))
164
- emit('step-reorder', draggedStepId.value, targetIndex)
165
- emit('update:modelValue', reordered)
166
-
167
- draggedStepId.value = null
168
- dragOverStepId.value = null
169
- }
170
-
171
- function handleDragEnd() {
172
- draggedStepId.value = null
173
- dragOverStepId.value = null
174
168
  }
175
169
 
176
170
  </script>
@@ -182,16 +176,22 @@ function handleDragEnd() {
182
176
  :aria-label="`Protocol timeline with ${sortedSteps.length} steps`"
183
177
  >
184
178
  <div
185
- v-for="(step, index) in sortedSteps"
179
+ v-for="(step, index) in (loading || error ? [] : sortedSteps)"
186
180
  :key="step.id"
181
+ :ref="(el) => setStepRef(step.id, el)"
187
182
  class="mint-timeline__step"
188
183
  role="listitem"
189
184
  :draggable="editable"
190
- @dragstart="handleDragStart($event, step)"
191
- @dragover="handleDragOver($event, step)"
192
- @dragleave="handleDragLeave"
193
- @drop="handleDrop($event, step)"
194
- @dragend="handleDragEnd"
185
+ :tabindex="editable ? 0 : undefined"
186
+ :aria-label="editable
187
+ ? `${step.name}, step ${index + 1} of ${sortedSteps.length}. Press Alt with the arrow keys to reorder.`
188
+ : undefined"
189
+ @dragstart="onStepDragStart($event, step, index)"
190
+ @dragover="onStepDragOver($event, step, index)"
191
+ @dragleave="onStepDragLeave"
192
+ @drop="onStepDrop($event, step, index)"
193
+ @dragend="onStepDragEnd"
194
+ @keydown="onStepReorderKeydown($event, step, index)"
195
195
  >
196
196
  <div
197
197
  v-if="index < sortedSteps.length - 1"
@@ -321,25 +321,32 @@ function handleDragEnd() {
321
321
  <div v-if="orientation === 'horizontal' && index < sortedSteps.length - 1" class="mint-timeline__spacer--horizontal" />
322
322
  </div>
323
323
 
324
- <div
325
- v-if="sortedSteps.length === 0"
326
- class="mint-timeline__empty"
327
- >
328
- <div class="mint-timeline__empty-content">
329
- <p class="mint-timeline__empty-text">No protocol steps defined</p>
330
- <button
331
- v-if="editable"
332
- type="button"
333
- class="mint-timeline__empty-btn"
334
- @click="handleAddStep()"
335
- >
336
- Add First Step
337
- </button>
338
- </div>
324
+ <div v-if="error" class="mint-timeline__state">
325
+ <slot name="error" :error="error">
326
+ <DataStateInternal state="error" size="sm" :title="error" />
327
+ </slot>
328
+ </div>
329
+
330
+ <div v-else-if="loading" class="mint-timeline__state">
331
+ <slot name="loading">
332
+ <DataStateInternal state="loading" size="sm" loading-label="Loading protocol" />
333
+ </slot>
334
+ </div>
335
+
336
+ <div v-else-if="sortedSteps.length === 0" class="mint-timeline__state">
337
+ <slot name="empty">
338
+ <DataStateInternal
339
+ state="empty"
340
+ size="sm"
341
+ title="No protocol steps defined"
342
+ :action-label="editable ? 'Add First Step' : undefined"
343
+ @action="handleAddStep()"
344
+ />
345
+ </slot>
339
346
  </div>
340
347
 
341
348
  <button
342
- v-if="editable && sortedSteps.length > 0"
349
+ v-if="editable && !loading && !error && sortedSteps.length > 0"
343
350
  type="button"
344
351
  :class="['mint-timeline__add-end', `mint-timeline__add-end--${size}`]"
345
352
  @click="handleAddStep()"
@@ -349,9 +356,8 @@ function handleDragEnd() {
349
356
  </svg>
350
357
  <span :class="`mint-timeline__add-end-text--${size}`">Add Step</span>
351
358
  </button>
359
+
360
+ <!-- Reorder announcements for screen readers -->
361
+ <div role="status" aria-live="polite" :style="announcerStyle">{{ reorderAnnouncement }}</div>
352
362
  </div>
353
363
  </template>
354
-
355
- <style>
356
- @import '../styles/components/experiment-timeline.css';
357
- </style>
@@ -418,7 +418,3 @@ function formatFileSize(bytes: number): string {
418
418
  </TransitionGroup>
419
419
  </div>
420
420
  </template>
421
-
422
- <style>
423
- @import '../styles/components/file-uploader.css';
424
- </style>
@@ -114,7 +114,3 @@ function variantClass(variant?: string): string {
114
114
  <slot name="footer" />
115
115
  </div>
116
116
  </template>
117
-
118
- <style>
119
- @import '../styles/components/fit-panel.css';
120
- </style>