@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,5 +1,6 @@
1
1
  import { ref, computed, type Ref, type ComputedRef } from 'vue'
2
2
  import type { Rack, SlotPosition, WellPlateFormat, Well } from '../types'
3
+ import { PLATE_CONFIGS, rowLabel as plateRowLabel } from '../components/WellPlate.geometry'
3
4
 
4
5
  export interface UseRackEditorOptions {
5
6
  defaultFormat?: WellPlateFormat
@@ -31,15 +32,6 @@ export interface UseRackEditorReturn {
31
32
 
32
33
  const SLOT_CYCLE: SlotPosition[] = ['R', 'G', 'B', 'Y']
33
34
 
34
- const PLATE_CONFIGS: Record<WellPlateFormat, { rows: number; cols: number }> = {
35
- 6: { rows: 2, cols: 3 },
36
- 12: { rows: 3, cols: 4 },
37
- 24: { rows: 4, cols: 6 },
38
- 48: { rows: 6, cols: 8 },
39
- 54: { rows: 6, cols: 9 },
40
- 96: { rows: 8, cols: 12 },
41
- 384: { rows: 16, cols: 24 },
42
- }
43
35
 
44
36
  let globalIdCounter = 0
45
37
 
@@ -156,8 +148,7 @@ export function useRackEditor(
156
148
  let counter = 1
157
149
  for (let row = 0; row < config.rows; row++) {
158
150
  for (let col = 0; col < config.cols; col++) {
159
- const rowLabel = String.fromCharCode(65 + row)
160
- const wellId = `${rowLabel}${col + 1}`
151
+ const wellId = `${plateRowLabel(row)}${col + 1}`
161
152
  // Only fill empty wells
162
153
  if (!rack.wells[wellId]?.sampleType) {
163
154
  const paddedNum = String(counter).padStart(3, '0')
@@ -1,10 +1,28 @@
1
1
  import { ref, type Ref } from 'vue'
2
+ import { readErrorMessage as readSharedErrorMessage } from '../utils/errors'
2
3
 
3
4
  export type RequestSyncSuccessKind = 'load' | 'save' | 'run'
4
5
 
6
+ /**
7
+ * The unit of work `run()` drives.
8
+ *
9
+ * It receives an `AbortSignal` that fires when the run is superseded by a
10
+ * newer `run()` or by `cancel()`. Zero-argument operations stay valid and
11
+ * simply ignore it.
12
+ */
13
+ export type RequestSyncOperation<T> = (signal: AbortSignal) => Promise<T>
14
+
5
15
  export interface RequestSyncRunOptions {
6
16
  success?: RequestSyncSuccessKind
7
17
  errorMessage?: string
18
+ /**
19
+ * Abort the previously started run when this one starts. Default `true`;
20
+ * pass `false` to let overlapping runs finish their own work (the stale
21
+ * guard on shared state still applies either way).
22
+ */
23
+ abortPrevious?: boolean
24
+ /** Caller-owned signal; aborting it aborts this run's signal too. */
25
+ signal?: AbortSignal
8
26
  }
9
27
 
10
28
  export interface UseRequestSyncStateReturn {
@@ -30,8 +48,32 @@ export interface UseRequestSyncStateReturn {
30
48
  markSaved: (date?: Date) => void
31
49
  /** Mark the operation as successfully run. */
32
50
  markRun: (date?: Date) => void
33
- /** Run a request with shared loading/error handling and optional sync marking. */
34
- run: <T>(operation: () => Promise<T>, options?: RequestSyncRunOptions) => Promise<T>
51
+ /**
52
+ * Run a request with shared loading/error handling and optional sync marking.
53
+ *
54
+ * ## Stale-response semantics
55
+ *
56
+ * Every call takes a monotonically increasing token. Starting a run
57
+ * supersedes any run started before it, and a superseded run writes **no**
58
+ * shared state when it finally settles: it does not clear `loading`, does not
59
+ * set `error`, and does not move `lastLoadedAt` / `lastSavedAt` / `lastRunAt`.
60
+ * Only the newest run owns those.
61
+ *
62
+ * The caller's own promise is untouched by the guard — a superseded run still
63
+ * resolves with its value or rejects with its own error, exactly as the
64
+ * operation did. The guard never invents a rejection, so a caller awaiting
65
+ * `run()` never sees a "superseded" error it did not cause.
66
+ *
67
+ * The operation additionally receives an `AbortSignal` that fires when its
68
+ * run is superseded or `cancel()` is called, so an operation that forwards
69
+ * the signal (`fetch(url, { signal })`) stops work instead of racing.
70
+ */
71
+ run: <T>(operation: RequestSyncOperation<T>, options?: RequestSyncRunOptions) => Promise<T>
72
+ /**
73
+ * Abort the in-flight run and drop `loading`. The aborted run's completion
74
+ * is treated as stale and writes no state.
75
+ */
76
+ cancel: () => void
35
77
  }
36
78
 
37
79
  /** Shared loading/error/timestamp state for generated plugin request helpers. */
@@ -49,22 +91,7 @@ export function useRequestSyncState(
49
91
  }
50
92
 
51
93
  function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {
52
- if (value instanceof Error && value.message) {
53
- return value.message
54
- }
55
- if (typeof value === 'string' && value.trim()) {
56
- return value
57
- }
58
- if (
59
- typeof value === 'object'
60
- && value !== null
61
- && 'message' in value
62
- && typeof (value as { message?: unknown }).message === 'string'
63
- && (value as { message: string }).message
64
- ) {
65
- return (value as { message: string }).message
66
- }
67
- return fallback
94
+ return readSharedErrorMessage(value, fallback)
68
95
  }
69
96
 
70
97
  function setError(value: unknown, fallback?: string): string {
@@ -85,27 +112,69 @@ export function useRequestSyncState(
85
112
  lastRunAt.value = date
86
113
  }
87
114
 
115
+ // Monotonic token guarding every write to the shared loading/error/timestamp
116
+ // state. Overlapping runs settle in any order, and a superseded one must not
117
+ // clear a newer run's loading flag or overwrite its error.
118
+ let runToken = 0
119
+ let activeController: AbortController | null = null
120
+
121
+ function cancel(): void {
122
+ runToken += 1
123
+ activeController?.abort()
124
+ activeController = null
125
+ loading.value = false
126
+ }
127
+
88
128
  async function run<T>(
89
- operation: () => Promise<T>,
129
+ operation: RequestSyncOperation<T>,
90
130
  options: RequestSyncRunOptions = {},
91
131
  ): Promise<T> {
132
+ if (options.abortPrevious !== false) {
133
+ activeController?.abort()
134
+ }
135
+
136
+ const token = (runToken += 1)
137
+ const controller = new AbortController()
138
+ activeController = controller
139
+
140
+ const callerSignal = options.signal
141
+ if (callerSignal) {
142
+ if (callerSignal.aborted) controller.abort(callerSignal.reason)
143
+ else callerSignal.addEventListener(
144
+ 'abort',
145
+ () => controller.abort(callerSignal.reason),
146
+ { once: true },
147
+ )
148
+ }
149
+
150
+ const isCurrent = (): boolean => token === runToken
151
+
92
152
  loading.value = true
93
153
  clearError()
94
154
  try {
95
- const response = await operation()
96
- if (options.success === 'load') {
97
- markLoaded()
98
- } else if (options.success === 'save') {
99
- markSaved()
100
- } else if (options.success === 'run') {
101
- markRun()
155
+ const response = await operation(controller.signal)
156
+ if (isCurrent()) {
157
+ if (options.success === 'load') {
158
+ markLoaded()
159
+ } else if (options.success === 'save') {
160
+ markSaved()
161
+ } else if (options.success === 'run') {
162
+ markRun()
163
+ }
102
164
  }
103
165
  return response
104
166
  } catch (err) {
105
- setError(err, options.errorMessage)
167
+ if (isCurrent()) {
168
+ setError(err, options.errorMessage)
169
+ }
106
170
  throw err
107
171
  } finally {
108
- loading.value = false
172
+ if (isCurrent()) {
173
+ loading.value = false
174
+ }
175
+ if (activeController === controller) {
176
+ activeController = null
177
+ }
109
178
  }
110
179
  }
111
180
 
@@ -122,5 +191,6 @@ export function useRequestSyncState(
122
191
  markSaved,
123
192
  markRun,
124
193
  run,
194
+ cancel,
125
195
  }
126
196
  }
@@ -1,4 +1,5 @@
1
1
  import { computed, ref, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
2
+ import { readErrorMessage } from '../utils/errors'
2
3
 
3
4
  export type ResourceKey = string | number
4
5
 
@@ -232,18 +233,3 @@ function resolveKey<TItem, TKey extends ResourceKey>(
232
233
  }
233
234
  return getKey(keyOrItem as TItem)
234
235
  }
235
-
236
- function readErrorMessage(value: unknown): string {
237
- if (value instanceof Error && value.message) return value.message
238
- if (typeof value === 'string' && value.trim()) return value
239
- if (
240
- typeof value === 'object'
241
- && value !== null
242
- && 'message' in value
243
- && typeof (value as { message?: unknown }).message === 'string'
244
- && (value as { message: string }).message.trim()
245
- ) {
246
- return (value as { message: string }).message
247
- }
248
- return 'Request failed.'
249
- }
@@ -200,6 +200,42 @@ export function useScheduleDrag(options: UseScheduleDragOptions) {
200
200
  cleanup()
201
201
  }
202
202
 
203
+ /**
204
+ * Keyboard alternative to the pointer drag: shifts an event by one slot
205
+ * (`mode: 'move'`) or grows/shrinks its end by one slot (`mode: 'resize'`),
206
+ * staying inside the visible day. Returns whether the nudge was applied.
207
+ */
208
+ function nudgeEvent(
209
+ event: ScheduleEvent,
210
+ direction: -1 | 1,
211
+ mode: 'move' | 'resize' = 'move',
212
+ ): boolean {
213
+ if (options.readonly.value) return false
214
+ if (mode === 'move' && event.draggable === false) return false
215
+ if (mode === 'resize' && event.resizable === false) return false
216
+
217
+ const slotMin = options.slotDuration.value
218
+ const dayStart = options.dayStartHour.value * 60
219
+ const dayEnd = options.dayEndHour.value * 60
220
+ const start = dateToMinutes(new Date(event.start))
221
+ const end = dateToMinutes(new Date(event.end))
222
+
223
+ const nextStart = mode === 'move' ? start + direction * slotMin : start
224
+ const nextEnd = mode === 'move'
225
+ ? end + direction * slotMin
226
+ : Math.max(start + slotMin, end + direction * slotMin)
227
+
228
+ if (nextStart < dayStart || nextEnd > dayEnd || nextEnd <= nextStart) return false
229
+
230
+ const base = new Date(event.start)
231
+ const newStart = minutesToDate(base, nextStart)
232
+ const newEnd = minutesToDate(base, nextEnd)
233
+
234
+ if (mode === 'move') options.onMoveComplete?.(event, newStart, newEnd)
235
+ else options.onResizeComplete?.(event, newStart, newEnd)
236
+ return true
237
+ }
238
+
203
239
  function addListeners() {
204
240
  document.addEventListener('pointermove', onPointerMove)
205
241
  document.addEventListener('pointerup', onPointerUp)
@@ -221,6 +257,7 @@ export function useScheduleDrag(options: UseScheduleDragOptions) {
221
257
  startCreate,
222
258
  startMove,
223
259
  startResize,
260
+ nudgeEvent,
224
261
  }
225
262
  }
226
263
 
@@ -1,5 +1,10 @@
1
1
  import { computed, ref, toValue, type ComputedRef, type Ref } from 'vue'
2
2
  import type { WellPlateFormat } from '../types'
3
+ import {
4
+ PLATE_CONFIGS as PLATE_DIMENSIONS,
5
+ rowIndex as rowLabelToIndex,
6
+ rowLabel as indexToRowLabel,
7
+ } from '../components/WellPlate.geometry'
3
8
 
4
9
  export type WellPlateSource<T> = T | Ref<T> | ComputedRef<T> | (() => T)
5
10
 
@@ -34,17 +39,6 @@ export interface UseWellPaintingReturn<TMode extends string = string, TPayload =
34
39
  cancel: () => void
35
40
  }
36
41
 
37
- const ROW_LABELS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
38
-
39
- const PLATE_DIMENSIONS: Record<WellPlateFormat, { rows: number; cols: number }> = {
40
- 6: { rows: 2, cols: 3 },
41
- 12: { rows: 3, cols: 4 },
42
- 24: { rows: 4, cols: 6 },
43
- 48: { rows: 6, cols: 8 },
44
- 54: { rows: 6, cols: 9 },
45
- 96: { rows: 8, cols: 12 },
46
- 384: { rows: 16, cols: 24 },
47
- }
48
42
 
49
43
  /**
50
44
  * Pointer painting/rectangle selection state for WellPlate-based editors.
@@ -170,22 +164,4 @@ export function wellIdsInRectangle(
170
164
  return ids
171
165
  }
172
166
 
173
- function rowLabelToIndex(label: string): number {
174
- let result = 0
175
- for (const char of label) {
176
- result = result * 26 + (char.charCodeAt(0) - 64)
177
- }
178
- return result - 1
179
- }
180
167
 
181
- function indexToRowLabel(index: number): string {
182
- if (index < ROW_LABELS.length) return ROW_LABELS[index]
183
- let value = index + 1
184
- let label = ''
185
- while (value > 0) {
186
- value--
187
- label = ROW_LABELS[value % 26] + label
188
- value = Math.floor(value / 26)
189
- }
190
- return label
191
- }
@@ -5,18 +5,8 @@ import type {
5
5
  SampleType,
6
6
  WellPlateFormat,
7
7
  } from '../types'
8
+ import { SAMPLE_COLOR_SCALE as DEFAULT_PALETTE } from '../utils/color'
8
9
 
9
- const DEFAULT_PALETTE = [
10
- '#3B82F6', // blue
11
- '#10B981', // green
12
- '#EF4444', // red
13
- '#F59E0B', // amber
14
- '#8B5CF6', // purple
15
- '#F97316', // orange
16
- '#06B6D4', // cyan
17
- '#14B8A6', // teal
18
- '#6B7280', // gray
19
- ]
20
10
 
21
11
  const MAX_HISTORY = 50
22
12
 
@@ -2,7 +2,6 @@ export const ADMIN_ROLE = 'admin'
2
2
 
3
3
  export const ADMIN_PANEL_PERMISSIONS = [
4
4
  'users.view',
5
- 'users.invite',
6
5
  'users.manage',
7
6
  'platform.configure',
8
7
  'platform.view_logs',
@@ -41,11 +40,36 @@ export interface AccessPolicy {
41
40
  plugin?: string
42
41
  }
43
42
 
43
+ /**
44
+ * An object that carries an access rule.
45
+ *
46
+ * The nested `access` policy is the supported shape. The four flat fields are
47
+ * the pre-1.2 spelling of the same four `AccessPolicy` fields; they still work
48
+ * but are deprecated, and `normalizeAccessPolicy()` lets the nested policy win
49
+ * wherever both are given.
50
+ */
44
51
  export interface AccessControlled {
52
+ /** The access policy. Preferred over the flat fields below. */
45
53
  access?: AccessPolicy
54
+ /**
55
+ * @deprecated Since MINT 1.2; use `access.visibleFor` instead. The flat
56
+ * duplicate is removed in 1.3.
57
+ */
46
58
  visibleFor?: AccessAudienceInput
59
+ /**
60
+ * @deprecated Since MINT 1.2; use `access.requiresAdmin` instead. The flat
61
+ * duplicate is removed in 1.3.
62
+ */
47
63
  requiresAdmin?: boolean
64
+ /**
65
+ * @deprecated Since MINT 1.2; use `access.permissions` instead. The flat
66
+ * duplicate is removed in 1.3.
67
+ */
48
68
  permissions?: readonly string[]
69
+ /**
70
+ * @deprecated Since MINT 1.2; use `access.anyPermissions` instead. The flat
71
+ * duplicate is removed in 1.3.
72
+ */
49
73
  anyPermissions?: readonly string[]
50
74
  }
51
75
 
@@ -105,17 +129,32 @@ export function canAccessPlugin(
105
129
  return Array.isArray(access) && access.includes(pluginName)
106
130
  }
107
131
 
132
+ /**
133
+ * Flatten an access rule — either shape, or a mix of both — into one policy.
134
+ *
135
+ * ## Precedence
136
+ *
137
+ * 1. A defined field on the nested `access` policy wins. It is the supported
138
+ * spelling, so an object that carries both shapes is read as "the nested
139
+ * policy, with the deprecated flat fields as leftovers".
140
+ * 2. Otherwise the flat field on the rule itself is used.
141
+ * 3. A field set to `undefined` on either side counts as unset and never
142
+ * masks the other side.
143
+ *
144
+ * Only `AccessPolicy` fields the flat shape never had (`audience`,
145
+ * `requiresAuth`, `plugin`) can come from the nested policy alone; the four
146
+ * duplicated fields are the ones this ordering exists for.
147
+ */
108
148
  export function normalizeAccessPolicy(rule: AccessControlled | AccessPolicy | undefined): AccessPolicy {
109
149
  if (!rule) return {}
110
- const { access: nested, ...direct } = rule as AccessPolicy & { access?: AccessPolicy }
111
- return {
112
- ...(nested ?? {}),
113
- ...direct,
114
- ...('visibleFor' in rule && rule.visibleFor !== undefined ? { visibleFor: rule.visibleFor } : {}),
115
- ...('requiresAdmin' in rule && rule.requiresAdmin !== undefined ? { requiresAdmin: rule.requiresAdmin } : {}),
116
- ...('permissions' in rule && rule.permissions !== undefined ? { permissions: rule.permissions } : {}),
117
- ...('anyPermissions' in rule && rule.anyPermissions !== undefined ? { anyPermissions: rule.anyPermissions } : {}),
150
+ const { access: nested, ...flat } = rule as AccessPolicy & { access?: AccessPolicy }
151
+ const policy = { ...flat } as Record<string, unknown>
152
+
153
+ for (const [field, value] of Object.entries(nested ?? {})) {
154
+ if (value !== undefined) policy[field] = value
118
155
  }
156
+
157
+ return policy as AccessPolicy
119
158
  }
120
159
 
121
160
  export function canAccessByPolicy(
@@ -1,6 +1,7 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
3
  import { useApi } from '../composables/useApi'
4
+ import { readErrorMessage } from '../utils/errors'
4
5
  import type { ExperimentSummary, PlatformContext } from '../types'
5
6
 
6
7
  function getPlatformApiUrl(): string | undefined {
@@ -9,12 +10,6 @@ function getPlatformApiUrl(): string | undefined {
9
10
  ?.platformApiUrl
10
11
  }
11
12
 
12
- function readErrorMessage(value: unknown): string {
13
- if (value instanceof Error && value.message) return value.message
14
- if (typeof value === 'string' && value.trim()) return value
15
- return 'Failed to load experiment'
16
- }
17
-
18
13
  /**
19
14
  * The single source of truth for the experiment a plugin is currently working on.
20
15
  *
@@ -89,7 +84,7 @@ export const useExperimentStore = defineStore('mint-experiment', () => {
89
84
  } catch (err) {
90
85
  if (token !== selectionToken) return null
91
86
  current.value = null
92
- error.value = readErrorMessage(err)
87
+ error.value = readErrorMessage(err, 'Failed to load experiment')
93
88
  return null
94
89
  } finally {
95
90
  // Only the newest request owns the loading flag; otherwise a superseded
@@ -1,3 +1,11 @@
1
1
  export { useAuthStore } from './auth'
2
- export { useSettingsStore, colorPalettes, type SettingsState } from './settings'
2
+ export {
3
+ useSettingsStore,
4
+ tryUseSettingsStore,
5
+ colorPalettes,
6
+ paletteCssVariables,
7
+ tableDensityToSize,
8
+ PALETTE_CSS_VARIABLES,
9
+ type SettingsState,
10
+ } from './settings'
3
11
  export { useExperimentStore } from './experiment'
@@ -1,4 +1,4 @@
1
- import { defineStore } from 'pinia'
1
+ import { defineStore, getActivePinia } from 'pinia'
2
2
  import { ref, watch } from 'vue'
3
3
  import type { ThemeMode, ColorPalette, TableDensity } from '../types'
4
4
 
@@ -62,6 +62,46 @@ export const colorPalettes: Record<ColorPalette, { name: string; hues: [number,
62
62
  pastel: { name: 'Pastel', hues: [200, 340] },
63
63
  }
64
64
 
65
+ /**
66
+ * Custom properties a non-default palette overrides on `<html>`.
67
+ *
68
+ * The saturation/lightness pairs are lifted from the canonical indigo/orange
69
+ * tokens in `styles/variables.css` so only the hue moves; the `default`
70
+ * palette writes nothing at all and the stylesheet values stand.
71
+ */
72
+ export const PALETTE_CSS_VARIABLES = [
73
+ '--color-primary',
74
+ '--color-primary-hover',
75
+ '--color-primary-soft',
76
+ '--color-cta',
77
+ '--color-cta-hover',
78
+ ] as const
79
+
80
+ /** Build the `<html>` custom-property overrides for a palette. */
81
+ export function paletteCssVariables(palette: ColorPalette): Record<string, string> {
82
+ if (palette === 'default') return {}
83
+ const [primaryHue, accentHue] = colorPalettes[palette].hues
84
+ return {
85
+ '--color-primary': `hsl(${primaryHue} 84% 67%)`,
86
+ '--color-primary-hover': `hsl(${primaryHue} 75% 59%)`,
87
+ '--color-primary-soft': `hsl(${primaryHue} 84% 67% / 0.12)`,
88
+ '--color-cta': `hsl(${accentHue} 95% 53%)`,
89
+ '--color-cta-hover': `hsl(${accentHue} 90% 48%)`,
90
+ }
91
+ }
92
+
93
+ /** `DataFrame`-style size that each table density maps onto. */
94
+ const DENSITY_TABLE_SIZE: Record<TableDensity, 'sm' | 'md' | 'lg'> = {
95
+ compact: 'sm',
96
+ normal: 'md',
97
+ comfortable: 'lg',
98
+ }
99
+
100
+ /** Map a table density onto the `size` prop shared by the SDK table components. */
101
+ export function tableDensityToSize(density: TableDensity): 'sm' | 'md' | 'lg' {
102
+ return DENSITY_TABLE_SIZE[density]
103
+ }
104
+
65
105
  function loadSettings(): SettingsState {
66
106
  try {
67
107
  const stored = localStorage.getItem(STORAGE_KEY)
@@ -152,6 +192,8 @@ export const useSettingsStore = defineStore('mint-settings', () => {
152
192
  tableDensity.value = loaded.tableDensity
153
193
 
154
194
  applyTheme()
195
+ applyPalette()
196
+ applyDensity()
155
197
  }
156
198
 
157
199
  function applyTheme() {
@@ -160,11 +202,36 @@ export const useSettingsStore = defineStore('mint-settings', () => {
160
202
  document.documentElement.classList.toggle('dark', dark)
161
203
  }
162
204
 
205
+ /**
206
+ * Write the selected palette's hues onto `<html>` as custom-property
207
+ * overrides. The `default` palette clears them so `styles/variables.css`
208
+ * keeps ownership of the brand colors.
209
+ */
210
+ function applyPalette() {
211
+ if (typeof document === 'undefined') return
212
+ const style = document.documentElement.style
213
+ const overrides = paletteCssVariables(colorPalette.value)
214
+ for (const name of PALETTE_CSS_VARIABLES) {
215
+ const value = overrides[name]
216
+ if (value) style.setProperty(name, value)
217
+ else style.removeProperty(name)
218
+ }
219
+ }
220
+
221
+ /** Mirror the table density onto `<html data-density>` for CSS consumers. */
222
+ function applyDensity() {
223
+ if (typeof document === 'undefined') return
224
+ document.documentElement.dataset.density = tableDensity.value
225
+ }
226
+
163
227
  watch(theme, () => {
164
228
  applyTheme()
165
229
  persistSettings()
166
230
  })
167
231
 
232
+ watch(colorPalette, applyPalette)
233
+ watch(tableDensity, applyDensity)
234
+
168
235
  if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
169
236
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
170
237
  systemPrefersDark.value = event.matches
@@ -206,6 +273,11 @@ export const useSettingsStore = defineStore('mint-settings', () => {
206
273
  return colorPalettes[colorPalette.value].hues
207
274
  }
208
275
 
276
+ /** The `size` the SDK table components default to for the current density. */
277
+ function getTableSize(): 'sm' | 'md' | 'lg' {
278
+ return tableDensityToSize(tableDensity.value)
279
+ }
280
+
209
281
  function isDark(): boolean {
210
282
  return theme.value === 'system' ? systemPrefersDark.value : theme.value === 'dark'
211
283
  }
@@ -222,11 +294,31 @@ export const useSettingsStore = defineStore('mint-settings', () => {
222
294
  tableDensity,
223
295
  initialize,
224
296
  applyTheme,
297
+ applyPalette,
298
+ applyDensity,
225
299
  persistSettings,
226
300
  resetToDefaults,
227
301
  getPaletteHues,
302
+ getTableSize,
228
303
  isDark,
229
304
  getApiBaseUrl,
230
305
  getWsBaseUrl,
231
306
  }
232
307
  })
308
+
309
+ /**
310
+ * Read the settings store without requiring Pinia to be installed.
311
+ *
312
+ * SDK components are mounted standalone (unit tests, Histoire stories, plugin
313
+ * previews) where no Pinia instance exists. They use this to pick up a user's
314
+ * preferences when the platform is running and fall back to their own defaults
315
+ * otherwise, instead of throwing.
316
+ */
317
+ export function tryUseSettingsStore(): ReturnType<typeof useSettingsStore> | null {
318
+ if (!getActivePinia()) return null
319
+ try {
320
+ return useSettingsStore()
321
+ } catch {
322
+ return null
323
+ }
324
+ }
@@ -126,11 +126,11 @@
126
126
  margin-right: 0.125rem;
127
127
  }
128
128
 
129
- .mint-audit-trail__empty {
130
- text-align: center;
131
- padding: 2rem;
132
- color: var(--text-muted);
133
- font-size: 0.875rem;
129
+ /* Empty / loading / error placeholder host (content comes from EmptyState) */
130
+ .mint-audit-trail__state {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
134
  }
135
135
 
136
136
  /* Size variants */
@@ -194,3 +194,10 @@
194
194
  background-color: var(--bg-hover);
195
195
  color: var(--text-primary);
196
196
  }
197
+
198
+ /* Empty / loading / error placeholder host (content comes from EmptyState) */
199
+ .mint-batch-progress__state {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ }
@@ -61,7 +61,7 @@
61
61
  border-radius: 50%;
62
62
  background: white;
63
63
  cursor: pointer;
64
- border: 2px solid var(--mint-primary, #6366f1);
64
+ border: 2px solid var(--color-primary);
65
65
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
66
66
  transition: transform 0.15s ease;
67
67
  }
@@ -80,7 +80,7 @@
80
80
  border-radius: 50%;
81
81
  background: white;
82
82
  cursor: pointer;
83
- border: 2px solid var(--mint-primary, #6366f1);
83
+ border: 2px solid var(--color-primary);
84
84
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
85
85
  transition: transform 0.15s ease;
86
86
  }