@morscherlab/mint-sdk 1.0.0-beta.1 → 1.0.0-beta.3

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 (421) hide show
  1. package/README.md +218 -6
  2. package/dist/__tests__/components/ActionItem.test.d.ts +1 -0
  3. package/dist/__tests__/components/AppAvatarMenu.test.d.ts +1 -0
  4. package/dist/__tests__/components/AppPageSelector.test.d.ts +1 -0
  5. package/dist/__tests__/components/AppPillNav.test.d.ts +1 -0
  6. package/dist/__tests__/components/AppPluginSwitcher.test.d.ts +1 -0
  7. package/dist/__tests__/components/AppToastContainer.test.d.ts +1 -0
  8. package/dist/__tests__/components/BaseRadioGroup.test.d.ts +1 -0
  9. package/dist/__tests__/components/BaseSelect.test.d.ts +1 -0
  10. package/dist/__tests__/components/BaseTabs.test.d.ts +1 -0
  11. package/dist/__tests__/components/BatchProgressList.test.d.ts +1 -0
  12. package/dist/__tests__/components/BioTemplateExperimentWorkspaceView.test.d.ts +1 -0
  13. package/dist/__tests__/components/BioTemplatePackWorkspaceView.test.d.ts +1 -0
  14. package/dist/__tests__/components/BioTemplatePresetWorkspaceView.test.d.ts +1 -0
  15. package/dist/__tests__/components/BioTemplateRenderer.test.d.ts +1 -0
  16. package/dist/__tests__/components/Breadcrumb.test.d.ts +1 -0
  17. package/dist/__tests__/components/CalendarGridPanel.test.d.ts +1 -0
  18. package/dist/__tests__/components/ConcentrationInput.test.d.ts +1 -0
  19. package/dist/__tests__/components/ControlWorkspaceView.test.d.ts +1 -0
  20. package/dist/__tests__/components/DatePicker.test.d.ts +1 -0
  21. package/dist/__tests__/components/DateTimePicker.test.d.ts +1 -0
  22. package/dist/__tests__/components/EmptyState.test.d.ts +1 -0
  23. package/dist/__tests__/components/ExperimentPopover.test.d.ts +1 -0
  24. package/dist/__tests__/components/FormBuilder.test.d.ts +1 -0
  25. package/dist/__tests__/components/FormCompatibility.test.d.ts +1 -0
  26. package/dist/__tests__/components/GroupAssigner.test.d.ts +1 -0
  27. package/dist/__tests__/components/GroupingModal.test.d.ts +1 -0
  28. package/dist/__tests__/components/MultiSelect.test.d.ts +1 -0
  29. package/dist/__tests__/components/PluginIcon.test.d.ts +1 -0
  30. package/dist/__tests__/components/ProtocolStepEditor.test.d.ts +1 -0
  31. package/dist/__tests__/components/ReagentList.test.d.ts +1 -0
  32. package/dist/__tests__/components/SampleHierarchyTree.test.d.ts +1 -0
  33. package/dist/__tests__/components/SampleSelector.test.d.ts +1 -0
  34. package/dist/__tests__/components/SegmentedControl.test.d.ts +1 -0
  35. package/dist/__tests__/components/SettingsButton.test.d.ts +1 -0
  36. package/dist/__tests__/components/SettingsModal.test.d.ts +1 -0
  37. package/dist/__tests__/components/TagsInput.test.d.ts +1 -0
  38. package/dist/__tests__/components/ThemeToggle.test.d.ts +1 -0
  39. package/dist/__tests__/components/TimePicker.test.d.ts +1 -0
  40. package/dist/__tests__/composables/useBioTemplatePackWorkspace.test.d.ts +1 -0
  41. package/dist/__tests__/composables/useBioTemplatePresetWorkspace.test.d.ts +1 -0
  42. package/dist/__tests__/composables/useBioTemplateWorkspace.test.d.ts +1 -0
  43. package/dist/__tests__/composables/useCalendarGrid.test.d.ts +1 -0
  44. package/dist/__tests__/composables/useControlSchema.test.d.ts +1 -0
  45. package/dist/__tests__/composables/useDebouncedWatch.test.d.ts +1 -0
  46. package/dist/__tests__/composables/useDropdownState.test.d.ts +1 -0
  47. package/dist/__tests__/composables/useEventListener.test.d.ts +1 -0
  48. package/dist/__tests__/composables/useExpansionSet.test.d.ts +1 -0
  49. package/dist/__tests__/composables/useExperimentData.test.d.ts +1 -0
  50. package/dist/__tests__/composables/useExperimentSelector.test.d.ts +1 -0
  51. package/dist/__tests__/composables/useGroupAssignment.test.d.ts +1 -0
  52. package/dist/__tests__/composables/useListSelection.test.d.ts +1 -0
  53. package/dist/__tests__/composables/usePluginClient.test.d.ts +1 -0
  54. package/dist/__tests__/composables/usePluginConfig.test.d.ts +1 -0
  55. package/dist/__tests__/composables/useRequestSyncState.test.d.ts +1 -0
  56. package/dist/__tests__/composables/useSampleGroups.test.d.ts +1 -0
  57. package/dist/__tests__/composables/useSelectionLimit.test.d.ts +1 -0
  58. package/dist/__tests__/composables/useSortedItems.test.d.ts +1 -0
  59. package/dist/__tests__/composables/useTemplateCollection.test.d.ts +1 -0
  60. package/dist/__tests__/composables/useTextSearch.test.d.ts +1 -0
  61. package/dist/__tests__/composables/useTheme.test.d.ts +1 -0
  62. package/dist/__tests__/composables/useTimeUtils.test.d.ts +1 -0
  63. package/dist/__tests__/docs/frontendDocsCatalog.test.d.ts +1 -0
  64. package/dist/__tests__/templates/templates.test.d.ts +1 -0
  65. package/dist/{auth-DsI0rQ7_.js → auth-QQj2kkze.js} +12 -5
  66. package/dist/auth-QQj2kkze.js.map +1 -0
  67. package/dist/components/ActionItem.vue.d.ts +32 -0
  68. package/dist/components/AppAvatarMenu.vue.d.ts +2 -7
  69. package/dist/components/AppPageSelector.vue.d.ts +3 -6
  70. package/dist/components/AppPillNav.vue.d.ts +2 -2
  71. package/dist/components/AppSidebar.vue.d.ts +56 -3
  72. package/dist/components/AppToastContainer.vue.d.ts +2 -0
  73. package/dist/components/AppTopBar.vue.d.ts +43 -10
  74. package/dist/components/BaseButton.vue.d.ts +2 -2
  75. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  76. package/dist/components/BaseInput.vue.d.ts +2 -2
  77. package/dist/components/BasePill.vue.d.ts +3 -3
  78. package/dist/components/BaseRadioGroup.vue.d.ts +4 -4
  79. package/dist/components/BaseSelect.vue.d.ts +4 -4
  80. package/dist/components/BaseSlider.vue.d.ts +1 -1
  81. package/dist/components/BaseTabs.vue.d.ts +2 -2
  82. package/dist/components/BaseTextarea.vue.d.ts +2 -2
  83. package/dist/components/BaseToggle.vue.d.ts +1 -1
  84. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +117 -0
  85. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +92 -0
  86. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +82 -0
  87. package/dist/components/BioTemplateRenderer.vue.d.ts +29 -0
  88. package/dist/components/Breadcrumb.vue.d.ts +2 -2
  89. package/dist/components/Calendar.vue.d.ts +1 -1
  90. package/dist/components/CalendarGridPanel.vue.d.ts +25 -0
  91. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  92. package/dist/components/ColorSlider.vue.d.ts +1 -1
  93. package/dist/components/ConcentrationInput.vue.d.ts +2 -2
  94. package/dist/components/ConfirmDialog.vue.d.ts +2 -2
  95. package/dist/components/ControlWorkspaceView.vue.d.ts +130 -0
  96. package/dist/components/DatePicker.vue.d.ts +2 -2
  97. package/dist/components/DateTimePicker.vue.d.ts +3 -3
  98. package/dist/components/DropdownButton.vue.d.ts +4 -4
  99. package/dist/components/EmptyState.vue.d.ts +1 -2
  100. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  101. package/dist/components/ExperimentTimeline.vue.d.ts +2 -2
  102. package/dist/components/FileUploader.vue.d.ts +2 -2
  103. package/dist/components/FitPanel.vue.d.ts +1 -1
  104. package/dist/components/FormActions.vue.d.ts +4 -4
  105. package/dist/components/FormBuilder.vue.d.ts +22 -8
  106. package/dist/components/FormFieldRenderer.vue.d.ts +7 -10
  107. package/dist/components/FormSection.vue.d.ts +11 -24
  108. package/dist/components/FormulaInput.vue.d.ts +2 -2
  109. package/dist/components/IconButton.vue.d.ts +1 -1
  110. package/dist/components/LoadingSpinner.vue.d.ts +1 -1
  111. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  112. package/dist/components/MultiSelect.vue.d.ts +3 -3
  113. package/dist/components/NumberInput.vue.d.ts +2 -2
  114. package/dist/components/PluginIcon.vue.d.ts +11 -0
  115. package/dist/components/ProgressBar.vue.d.ts +2 -2
  116. package/dist/components/ProtocolStepEditor.vue.d.ts +3 -1
  117. package/dist/components/RackEditor.vue.d.ts +2 -2
  118. package/dist/components/ReagentEditor.vue.d.ts +1 -1
  119. package/dist/components/ResourceCard.vue.d.ts +1 -1
  120. package/dist/components/SampleLegend.vue.d.ts +2 -2
  121. package/dist/components/SampleSelector.vue.d.ts +1 -1
  122. package/dist/components/ScheduleCalendar.vue.d.ts +2 -2
  123. package/dist/components/ScientificNumber.vue.d.ts +1 -1
  124. package/dist/components/SegmentedControl.vue.d.ts +3 -3
  125. package/dist/components/SequenceInput.vue.d.ts +3 -3
  126. package/dist/components/SettingsButton.vue.d.ts +2 -2
  127. package/dist/components/SettingsModal.vue.d.ts +32 -4
  128. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  129. package/dist/components/TagsInput.vue.d.ts +3 -2
  130. package/dist/components/TimePicker.vue.d.ts +3 -3
  131. package/dist/components/TimeRangeInput.vue.d.ts +2 -2
  132. package/dist/components/UnitInput.vue.d.ts +2 -2
  133. package/dist/components/WellPlate.vue.d.ts +8 -8
  134. package/dist/components/index.d.ts +12 -1
  135. package/dist/components/index.js +3 -3
  136. package/dist/components/internal/FormFieldRendererInternal.vue.d.ts +31 -0
  137. package/dist/components/internal/FormSectionRenderer.vue.d.ts +43 -0
  138. package/dist/{components-CzbQQPCb.js → components-D_Sr0adg.js} +9629 -8647
  139. package/dist/components-D_Sr0adg.js.map +1 -0
  140. package/dist/composables/index.d.ts +21 -2
  141. package/dist/composables/index.js +4 -3
  142. package/dist/composables/platformContextHelpers.d.ts +14 -0
  143. package/dist/composables/useBioTemplateComponents.d.ts +20 -0
  144. package/dist/composables/useBioTemplateControls.d.ts +6 -0
  145. package/dist/composables/useBioTemplatePackWorkspace.d.ts +45 -0
  146. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +74 -0
  147. package/dist/composables/useBioTemplateWorkspace.d.ts +50 -0
  148. package/dist/composables/useCalendarGrid.d.ts +26 -0
  149. package/dist/composables/useControlSchema.d.ts +321 -0
  150. package/dist/composables/useDebouncedWatch.d.ts +20 -0
  151. package/dist/composables/useDropdownState.d.ts +19 -0
  152. package/dist/composables/useEventListener.d.ts +13 -0
  153. package/dist/composables/useExpansionSet.d.ts +21 -0
  154. package/dist/composables/useExperimentData.d.ts +10 -0
  155. package/dist/composables/useExperimentSave.d.ts +31 -2
  156. package/dist/composables/useExperimentSelector.d.ts +20 -0
  157. package/dist/composables/useForm.d.ts +2 -0
  158. package/dist/composables/useGroupAssignment.d.ts +31 -0
  159. package/dist/composables/useListSelection.d.ts +35 -0
  160. package/dist/composables/usePlatformContext.d.ts +24 -3
  161. package/dist/composables/usePluginApi.d.ts +7 -14
  162. package/dist/composables/usePluginClient.d.ts +109 -0
  163. package/dist/composables/usePluginConfig.d.ts +12 -0
  164. package/dist/composables/useRequestSyncState.d.ts +34 -0
  165. package/dist/composables/useSampleGroups.d.ts +32 -0
  166. package/dist/composables/useSelectionLimit.d.ts +17 -0
  167. package/dist/composables/useSortedItems.d.ts +32 -0
  168. package/dist/composables/useTemplateCollection.d.ts +58 -0
  169. package/dist/composables/useTextSearch.d.ts +18 -0
  170. package/dist/composables/useTimeUtils.d.ts +8 -0
  171. package/dist/{composables-BXklV5ii.js → composables-C3dpXQN5.js} +228 -146
  172. package/dist/composables-C3dpXQN5.js.map +1 -0
  173. package/dist/index.d.ts +12 -3
  174. package/dist/index.js +6 -5
  175. package/dist/install.d.ts +7 -2
  176. package/dist/install.js +2 -2
  177. package/dist/install.js.map +1 -1
  178. package/dist/stores/auth.d.ts +1 -1
  179. package/dist/stores/index.js +1 -1
  180. package/dist/stores/settings.d.ts +4 -1
  181. package/dist/styles.css +5255 -5654
  182. package/dist/templates/adapters.d.ts +43 -0
  183. package/dist/templates/builders.d.ts +63 -0
  184. package/dist/templates/catalog.d.ts +188 -0
  185. package/dist/templates/componentBindings.d.ts +58 -0
  186. package/dist/templates/controlSchemas.d.ts +25 -0
  187. package/dist/templates/index.d.ts +15 -0
  188. package/dist/templates/index.js +2 -0
  189. package/dist/templates/lookup.d.ts +4 -0
  190. package/dist/templates/packs.d.ts +18 -0
  191. package/dist/templates/presets.d.ts +90 -0
  192. package/dist/templates/types.d.ts +531 -0
  193. package/dist/templates-50NPjaxL.js +9333 -0
  194. package/dist/templates-50NPjaxL.js.map +1 -0
  195. package/dist/types/components.d.ts +62 -1
  196. package/dist/types/form-builder.d.ts +6 -8
  197. package/dist/types/index.d.ts +2 -2
  198. package/dist/types/platform.d.ts +8 -1
  199. package/dist/useScheduleDrag-D4oWdh41.js +4371 -0
  200. package/dist/useScheduleDrag-D4oWdh41.js.map +1 -0
  201. package/dist/utils/formModelSync.d.ts +5 -0
  202. package/dist/utils/items.d.ts +8 -0
  203. package/dist/utils/options.d.ts +6 -0
  204. package/dist/utils/pluginIcon.d.ts +9 -0
  205. package/package.json +7 -2
  206. package/src/__tests__/components/ActionItem.test.ts +99 -0
  207. package/src/__tests__/components/AppAvatarMenu.test.ts +27 -0
  208. package/src/__tests__/components/AppPageSelector.test.ts +134 -0
  209. package/src/__tests__/components/AppPillNav.test.ts +78 -0
  210. package/src/__tests__/components/AppPluginSwitcher.test.ts +44 -0
  211. package/src/__tests__/components/AppSidebar.test.ts +370 -0
  212. package/src/__tests__/components/AppToastContainer.test.ts +48 -0
  213. package/src/__tests__/components/AppTopBar.test.ts +414 -13
  214. package/src/__tests__/components/BaseRadioGroup.test.ts +25 -0
  215. package/src/__tests__/components/BaseSelect.test.ts +21 -0
  216. package/src/__tests__/components/BaseTabs.test.ts +25 -0
  217. package/src/__tests__/components/BatchProgressList.test.ts +52 -0
  218. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +153 -0
  219. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +161 -0
  220. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +281 -0
  221. package/src/__tests__/components/BioTemplateRenderer.test.ts +71 -0
  222. package/src/__tests__/components/Breadcrumb.test.ts +23 -0
  223. package/src/__tests__/components/CalendarGridPanel.test.ts +36 -0
  224. package/src/__tests__/components/ConcentrationInput.test.ts +45 -0
  225. package/src/__tests__/components/ControlWorkspaceView.test.ts +1031 -0
  226. package/src/__tests__/components/DataFrame.test.ts +11 -0
  227. package/src/__tests__/components/DatePicker.test.ts +45 -0
  228. package/src/__tests__/components/DateTimePicker.test.ts +48 -0
  229. package/src/__tests__/components/DropdownButton.test.ts +23 -0
  230. package/src/__tests__/components/EmptyState.test.ts +23 -0
  231. package/src/__tests__/components/ExperimentPopover.test.ts +56 -0
  232. package/src/__tests__/components/FormBuilder.test.ts +296 -0
  233. package/src/__tests__/components/FormCompatibility.test.ts +94 -0
  234. package/src/__tests__/components/GroupAssigner.test.ts +30 -0
  235. package/src/__tests__/components/GroupingModal.test.ts +73 -0
  236. package/src/__tests__/components/MultiSelect.test.ts +48 -0
  237. package/src/__tests__/components/PluginIcon.test.ts +119 -0
  238. package/src/__tests__/components/ProtocolStepEditor.test.ts +33 -0
  239. package/src/__tests__/components/ReagentList.test.ts +82 -0
  240. package/src/__tests__/components/SampleHierarchyTree.test.ts +53 -0
  241. package/src/__tests__/components/SampleSelector.test.ts +60 -0
  242. package/src/__tests__/components/SegmentedControl.test.ts +24 -0
  243. package/src/__tests__/components/SettingsButton.test.ts +44 -0
  244. package/src/__tests__/components/SettingsModal.test.ts +296 -0
  245. package/src/__tests__/components/TagsInput.test.ts +75 -0
  246. package/src/__tests__/components/ThemeToggle.test.ts +47 -0
  247. package/src/__tests__/components/TimePicker.test.ts +38 -0
  248. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +122 -0
  249. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +199 -0
  250. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +99 -0
  251. package/src/__tests__/composables/useCalendarGrid.test.ts +38 -0
  252. package/src/__tests__/composables/useControlSchema.test.ts +919 -0
  253. package/src/__tests__/composables/useDebouncedWatch.test.ts +93 -0
  254. package/src/__tests__/composables/useDropdownState.test.ts +95 -0
  255. package/src/__tests__/composables/useEventListener.test.ts +116 -0
  256. package/src/__tests__/composables/useExpansionSet.test.ts +62 -0
  257. package/src/__tests__/composables/useExperimentData.test.ts +4 -0
  258. package/src/__tests__/composables/useExperimentSave.test.ts +203 -8
  259. package/src/__tests__/composables/useExperimentSelector.test.ts +164 -0
  260. package/src/__tests__/composables/useForm.test.ts +58 -0
  261. package/src/__tests__/composables/useFormBuilder.test.ts +77 -0
  262. package/src/__tests__/composables/useGroupAssignment.test.ts +73 -0
  263. package/src/__tests__/composables/useListSelection.test.ts +66 -0
  264. package/src/__tests__/composables/usePluginClient.test.ts +444 -0
  265. package/src/__tests__/composables/usePluginConfig.test.ts +5 -0
  266. package/src/__tests__/composables/useRequestSyncState.test.ts +92 -0
  267. package/src/__tests__/composables/useSampleGroups.test.ts +66 -0
  268. package/src/__tests__/composables/useSelectionLimit.test.ts +41 -0
  269. package/src/__tests__/composables/useSortedItems.test.ts +87 -0
  270. package/src/__tests__/composables/useTemplateCollection.test.ts +147 -0
  271. package/src/__tests__/composables/useTextSearch.test.ts +55 -0
  272. package/src/__tests__/composables/useTheme.test.ts +91 -0
  273. package/src/__tests__/composables/useTimeUtils.test.ts +35 -0
  274. package/src/__tests__/docs/frontendDocsCatalog.test.ts +229 -0
  275. package/src/__tests__/fixtures/templates/dose-response.json +81 -0
  276. package/src/__tests__/fixtures/templates/plate-map.json +54 -0
  277. package/src/__tests__/fixtures/templates/qpcr-plate.json +96 -0
  278. package/src/__tests__/fixtures/templates/sample-sheet.json +71 -0
  279. package/src/__tests__/templates/templates.test.ts +1043 -0
  280. package/src/components/ActionItem.vue +82 -0
  281. package/src/components/AppAvatarMenu.vue +15 -69
  282. package/src/components/AppLayout.story.vue +25 -25
  283. package/src/components/AppPageSelector.vue +63 -94
  284. package/src/components/AppPillNav.vue +44 -39
  285. package/src/components/AppPluginSwitcher.vue +41 -145
  286. package/src/components/AppSidebar.story.vue +94 -0
  287. package/src/components/AppSidebar.vue +187 -12
  288. package/src/components/{ToastNotification.story.vue → AppToastContainer.story.vue} +6 -6
  289. package/src/components/AppToastContainer.vue +62 -0
  290. package/src/components/AppTopBar.story.vue +7 -30
  291. package/src/components/AppTopBar.vue +283 -84
  292. package/src/components/BaseModal.vue +3 -5
  293. package/src/components/BaseRadioGroup.vue +7 -3
  294. package/src/components/BaseSelect.vue +11 -7
  295. package/src/components/BaseTabs.vue +6 -4
  296. package/src/components/BatchProgressList.vue +5 -8
  297. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +123 -0
  298. package/src/components/BioTemplateExperimentWorkspaceView.vue +337 -0
  299. package/src/components/BioTemplatePackWorkspaceView.story.vue +107 -0
  300. package/src/components/BioTemplatePackWorkspaceView.vue +176 -0
  301. package/src/components/BioTemplatePresetWorkspaceView.story.vue +151 -0
  302. package/src/components/BioTemplatePresetWorkspaceView.vue +392 -0
  303. package/src/components/BioTemplateRenderer.story.vue +57 -0
  304. package/src/components/BioTemplateRenderer.vue +269 -0
  305. package/src/components/Breadcrumb.vue +14 -8
  306. package/src/components/CalendarGridPanel.vue +120 -0
  307. package/src/components/ConcentrationInput.vue +27 -64
  308. package/src/components/ControlWorkspaceView.story.vue +336 -0
  309. package/src/components/ControlWorkspaceView.vue +347 -0
  310. package/src/components/DataFrame.vue +34 -50
  311. package/src/components/DatePicker.vue +59 -192
  312. package/src/components/DateTimePicker.vue +50 -171
  313. package/src/components/DropdownButton.vue +14 -32
  314. package/src/components/EmptyState.vue +4 -2
  315. package/src/components/ExperimentPopover.vue +5 -22
  316. package/src/components/FormBuilder.vue +124 -27
  317. package/src/components/FormFieldRenderer.vue +15 -38
  318. package/src/components/FormSection.vue +20 -73
  319. package/src/components/GroupAssigner.vue +24 -56
  320. package/src/components/GroupingModal.story.vue +3 -3
  321. package/src/components/GroupingModal.vue +30 -391
  322. package/src/components/MultiSelect.vue +17 -12
  323. package/src/components/PlateMapEditor.vue +3 -8
  324. package/src/components/PluginIcon.story.vue +71 -0
  325. package/src/components/PluginIcon.vue +68 -0
  326. package/src/components/ProtocolStepEditor.vue +13 -22
  327. package/src/components/ReagentList.vue +25 -33
  328. package/src/components/SampleHierarchyTree.vue +12 -23
  329. package/src/components/SampleSelector.vue +42 -122
  330. package/src/components/SegmentedControl.vue +7 -3
  331. package/src/components/SettingsButton.story.vue +1 -1
  332. package/src/components/SettingsButton.vue +15 -27
  333. package/src/components/SettingsModal.story.vue +337 -45
  334. package/src/components/SettingsModal.vue +344 -66
  335. package/src/components/TagsInput.vue +29 -14
  336. package/src/components/ThemeToggle.vue +9 -7
  337. package/src/components/TimePicker.vue +19 -41
  338. package/src/components/ToastNotification.vue +4 -57
  339. package/src/components/Tooltip.vue +7 -12
  340. package/src/components/WellEditPopup.vue +3 -8
  341. package/src/components/WellPlate.vue +4 -10
  342. package/src/components/index.ts +12 -1
  343. package/src/components/internal/FormFieldRendererInternal.vue +50 -0
  344. package/src/components/internal/FormSectionRenderer.vue +78 -0
  345. package/src/composables/index.ts +212 -0
  346. package/src/composables/platformContextHelpers.ts +74 -0
  347. package/src/composables/useBioTemplateComponents.ts +93 -0
  348. package/src/composables/useBioTemplateControls.ts +41 -0
  349. package/src/composables/useBioTemplatePackWorkspace.ts +181 -0
  350. package/src/composables/useBioTemplatePresetWorkspace.ts +337 -0
  351. package/src/composables/useBioTemplateWorkspace.ts +139 -0
  352. package/src/composables/useCalendarGrid.ts +140 -0
  353. package/src/composables/useControlSchema.ts +1274 -0
  354. package/src/composables/useDebouncedWatch.ts +119 -0
  355. package/src/composables/useDropdownState.ts +83 -0
  356. package/src/composables/useEventListener.ts +111 -0
  357. package/src/composables/useExpansionSet.ts +117 -0
  358. package/src/composables/useExperimentData.ts +20 -11
  359. package/src/composables/useExperimentSave.ts +202 -50
  360. package/src/composables/useExperimentSelector.ts +86 -72
  361. package/src/composables/useForm.ts +49 -4
  362. package/src/composables/useFormBuilder.ts +93 -42
  363. package/src/composables/useGroupAssignment.ts +148 -0
  364. package/src/composables/useListSelection.ts +158 -0
  365. package/src/composables/usePluginApi.ts +7 -14
  366. package/src/composables/usePluginClient.ts +425 -0
  367. package/src/composables/usePluginConfig.ts +34 -13
  368. package/src/composables/useRequestSyncState.ts +126 -0
  369. package/src/composables/useSampleGroups.ts +126 -0
  370. package/src/composables/useSelectionLimit.ts +57 -0
  371. package/src/composables/useSortedItems.ts +118 -0
  372. package/src/composables/useTemplateCollection.ts +229 -0
  373. package/src/composables/useTextSearch.ts +60 -0
  374. package/src/composables/useTheme.ts +2 -28
  375. package/src/composables/useTimeUtils.ts +26 -2
  376. package/src/composables/useWellPlateEditor.ts +13 -9
  377. package/src/index.ts +228 -4
  378. package/src/install.ts +11 -4
  379. package/src/stores/settings.ts +13 -9
  380. package/src/styles/components/app-page-selector.css +23 -0
  381. package/src/styles/components/app-pill-nav.css +8 -2
  382. package/src/styles/components/app-top-bar.css +35 -2
  383. package/src/styles/components/button.css +3 -7
  384. package/src/styles/components/concentration-input.css +3 -142
  385. package/src/styles/components/dropdown-button.css +4 -4
  386. package/src/styles/components/empty-state.css +0 -16
  387. package/src/styles/components/input.css +4 -5
  388. package/src/styles/components/number-input.css +3 -3
  389. package/src/styles/components/plugin-icon.css +38 -0
  390. package/src/styles/components/segmented-control.css +4 -7
  391. package/src/styles/components/settings-button.css +3 -66
  392. package/src/styles/components/settings-modal.css +184 -0
  393. package/src/styles/components/tabs.css +1 -2
  394. package/src/styles/components/textarea.css +4 -5
  395. package/src/styles/components/theme-toggle.css +3 -66
  396. package/src/styles/components/unit-input.css +3 -3
  397. package/src/styles/index.css +0 -1
  398. package/src/templates/adapters.ts +785 -0
  399. package/src/templates/builders.ts +2149 -0
  400. package/src/templates/catalog.ts +245 -0
  401. package/src/templates/componentBindings.ts +615 -0
  402. package/src/templates/controlSchemas.ts +718 -0
  403. package/src/templates/index.ts +314 -0
  404. package/src/templates/lookup.ts +18 -0
  405. package/src/templates/packs.ts +156 -0
  406. package/src/templates/presets.ts +146 -0
  407. package/src/templates/types.ts +668 -0
  408. package/src/types/components.ts +80 -1
  409. package/src/types/form-builder.ts +7 -2
  410. package/src/types/index.ts +17 -0
  411. package/src/types/platform.ts +8 -1
  412. package/src/utils/formModelSync.ts +52 -0
  413. package/src/utils/items.ts +28 -0
  414. package/src/utils/options.ts +23 -0
  415. package/src/utils/pluginIcon.ts +30 -0
  416. package/dist/auth-DsI0rQ7_.js.map +0 -1
  417. package/dist/components-CzbQQPCb.js.map +0 -1
  418. package/dist/composables-BXklV5ii.js.map +0 -1
  419. package/dist/useScheduleDrag-CxBeqYcu.js +0 -7181
  420. package/dist/useScheduleDrag-CxBeqYcu.js.map +0 -1
  421. package/src/styles/components/grouping-modal.css +0 -323
@@ -0,0 +1,1043 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ assertTemplateEnvelope,
4
+ bioTemplatePresetControlValuesToOptions,
5
+ createBioTemplatePackCollection,
6
+ createBioTemplatePresetCollection,
7
+ createBioTemplatePresetCollectionFromControls,
8
+ createAssayMatrixTemplate,
9
+ createCalibrationCurveTemplate,
10
+ createDoseResponseTemplate,
11
+ createElisaAssayCollection,
12
+ createFlowCytometryAssayCollection,
13
+ createFlowCytometryPanelTemplate,
14
+ createInstrumentRunTemplate,
15
+ createLcmsBatchCollection,
16
+ createPlateMapTemplate,
17
+ createProtocolStepsTemplate,
18
+ createQpcrExpressionCollection,
19
+ createQpcrPlateTemplate,
20
+ createReagentListTemplate,
21
+ createSampleSheetTemplate,
22
+ createSamplePrepTemplate,
23
+ createDefaultBioTemplate,
24
+ createTemplateCollection,
25
+ createTimeCourseTemplate,
26
+ createWellPlateScreenCollection,
27
+ createWesternBlotAssayCollection,
28
+ ensureTemplateEnvelope,
29
+ ensureTemplateFromCollection,
30
+ extractTemplateCollection,
31
+ getBioTemplateComponentBindings,
32
+ getBioTemplateComponentProps,
33
+ getBioTemplateControlDefaults,
34
+ getBioTemplateControlSchema,
35
+ getBioTemplatePackInfo,
36
+ getBioTemplatePresetInfo,
37
+ getBioTemplateInfo,
38
+ listBioTemplateCatalog,
39
+ listBioTemplateComponentBindings,
40
+ listBioTemplateControlSchemas,
41
+ listBioTemplatePacks,
42
+ listBioTemplatePresets,
43
+ searchBioTemplatePacks,
44
+ searchBioTemplateCatalog,
45
+ searchBioTemplatePresets,
46
+ toBioTemplateComponentImports,
47
+ toBioTemplateComponentProps,
48
+ toBioTemplateComponentPropsByComponent,
49
+ toBioTemplateComponentPropsById,
50
+ toBioTemplateComponentSnippets,
51
+ toBioTemplateComponentUsage,
52
+ toAssayMatrixColumns,
53
+ toAssayMatrixDataFrame,
54
+ toAssayMatrixRows,
55
+ toCalibrationCurveDataFrame,
56
+ toCalibrationCurveRows,
57
+ toDoseConditions,
58
+ toDoseLayoutState,
59
+ toFlowPanelDataFrame,
60
+ toFlowPanelRows,
61
+ toInstrumentRunDataFrame,
62
+ toInstrumentRunRows,
63
+ toPlateMapEditorState,
64
+ toProtocolDataFrame,
65
+ toProtocolSteps,
66
+ toQpcrDataFrame,
67
+ toQpcrRows,
68
+ toQpcrWellPlateWells,
69
+ toReagentDataFrame,
70
+ toReagentListItems,
71
+ toSampleColumns,
72
+ toSampleDataFrame,
73
+ toSampleOptions,
74
+ toSamplePrepDataFrame,
75
+ toSamplePrepRows,
76
+ toSampleRows,
77
+ toTemplateDataFrame,
78
+ toTimeCourseDataFrame,
79
+ toTimeCourseRows,
80
+ toTimeCourseSteps,
81
+ toWellPlateWells,
82
+ TEMPLATE_COLLECTION_KEY,
83
+ } from '../../templates'
84
+ import bioTemplateCatalogContract from '../../../../bio-template-catalog.contract.json'
85
+ import bioTemplatePacksContract from '../../../../bio-template-packs.contract.json'
86
+ import bioTemplatePresetsContract from '../../../../bio-template-presets.contract.json'
87
+ import type {
88
+ DoseResponseTemplate,
89
+ FlowCytometryPanelTemplate,
90
+ InstrumentRunTemplate,
91
+ PlateMapTemplate,
92
+ ProtocolStepsTemplate,
93
+ QpcrPlateTemplate,
94
+ ReagentListTemplate,
95
+ SampleSheetTemplate,
96
+ } from '../../templates'
97
+ import doseResponseFixture from '../fixtures/templates/dose-response.json'
98
+ import plateMapFixture from '../fixtures/templates/plate-map.json'
99
+ import qpcrPlateFixture from '../fixtures/templates/qpcr-plate.json'
100
+ import sampleSheetFixture from '../fixtures/templates/sample-sheet.json'
101
+
102
+ describe('bio data templates', () => {
103
+ it('exposes a discoverable template catalog', () => {
104
+ const catalog = listBioTemplateCatalog()
105
+
106
+ expect(catalog.map(entry => entry.name)).toEqual([
107
+ 'plate-map',
108
+ 'sample-sheet',
109
+ 'sample-prep',
110
+ 'dose-response',
111
+ 'calibration-curve',
112
+ 'time-course',
113
+ 'protocol-steps',
114
+ 'assay-matrix',
115
+ 'reagent-list',
116
+ 'flow-cytometry-panel',
117
+ 'instrument-run',
118
+ 'qpcr-plate',
119
+ ])
120
+ expect(getBioTemplateInfo('wellplate')?.template_id).toBe('plate-map')
121
+ expect(getBioTemplateInfo('sample preparation')?.template_id).toBe('sample-prep')
122
+ expect(getBioTemplateInfo('dose design')?.template_id).toBe('dose-response')
123
+ expect(getBioTemplateInfo('dose-design')?.template_id).toBe('dose-response')
124
+ expect(getBioTemplateInfo('dose calculator')?.template_id).toBe('dose-response')
125
+ expect(getBioTemplateInfo('dose_calculator')?.template_id).toBe('dose-response')
126
+ expect(getBioTemplateInfo('protocol')?.template_id).toBe('protocol-steps')
127
+ expect(getBioTemplateInfo('standard curve')?.template_id).toBe('calibration-curve')
128
+ expect(getBioTemplateInfo('omics matrix')?.template_id).toBe('assay-matrix')
129
+ expect(getBioTemplateInfo('antibodies')?.template_id).toBe('reagent-list')
130
+ expect(getBioTemplateInfo('facs panel')?.template_id).toBe('flow-cytometry-panel')
131
+ expect(getBioTemplateInfo('run queue')?.template_id).toBe('instrument-run')
132
+ expect(getBioTemplateInfo('gene expression')?.template_id).toBe('qpcr-plate')
133
+ expect(searchBioTemplateCatalog('DoseCalculator')[0].name).toBe('dose-response')
134
+ })
135
+
136
+ it('matches the shared Python/frontend catalog contract', () => {
137
+ expect(listBioTemplateCatalog()).toEqual(bioTemplateCatalogContract)
138
+ })
139
+
140
+ it('exposes discoverable template collection packs', () => {
141
+ const packs = listBioTemplatePacks()
142
+
143
+ expect(packs.map(entry => entry.name)).toEqual([
144
+ 'cell-culture-screen',
145
+ 'omics-assay',
146
+ 'longitudinal-study',
147
+ 'molecular-assay',
148
+ ])
149
+ expect(getBioTemplatePackInfo('cell culture')?.templates).toEqual([
150
+ 'sample-sheet',
151
+ 'sample-prep',
152
+ 'plate-map',
153
+ 'reagent-list',
154
+ 'protocol-steps',
155
+ 'dose-response',
156
+ 'flow-cytometry-panel',
157
+ ])
158
+ expect(getBioTemplatePackInfo('metabolomics')?.name).toBe('omics-assay')
159
+ expect(getBioTemplatePackInfo('longitudinal')?.init_command).toBe('mint init --template longitudinal-study')
160
+ expect(getBioTemplatePackInfo('gene expression')?.templates.at(-1)).toBe('qpcr-plate')
161
+ expect(getBioTemplatePackInfo('gene-expression')?.templates.at(-1)).toBe('qpcr-plate')
162
+ expect(searchBioTemplatePacks('WellPlate').map(entry => entry.name)).toEqual([
163
+ 'cell-culture-screen',
164
+ 'omics-assay',
165
+ 'molecular-assay',
166
+ ])
167
+ })
168
+
169
+ it('matches the shared Python/frontend pack contract', () => {
170
+ expect(listBioTemplatePacks()).toEqual(bioTemplatePacksContract)
171
+ })
172
+
173
+ it('creates default template collections from curated packs', () => {
174
+ const plate = createDefaultBioTemplate('wellplate') as PlateMapTemplate
175
+ const pack = createBioTemplatePackCollection('cell culture')
176
+ const templates = extractTemplateCollection(pack)
177
+
178
+ expect(plate.template_id).toBe('plate-map')
179
+ expect(pack.metadata?.pack).toBe('cell-culture-screen')
180
+ expect(Object.keys(templates)).toEqual([
181
+ 'sample-sheet',
182
+ 'sample-prep',
183
+ 'plate-map',
184
+ 'reagent-list',
185
+ 'protocol-steps',
186
+ 'dose-response',
187
+ 'flow-cytometry-panel',
188
+ ])
189
+ expect(toPlateMapEditorState(templates['plate-map'] as PlateMapTemplate).samples).toHaveLength(2)
190
+ expect(toDoseConditions(templates['dose-response'] as DoseResponseTemplate)[0].label).toBe('Compound A')
191
+ expect(getBioTemplateControlSchema('cell-culture-screen')?.sampleSheetSampleCount.default).toBe(2)
192
+ expect(getBioTemplateComponentBindings('cell-culture-screen').map(binding => binding.component)).toContain('DoseCalculator')
193
+ expect(() => createBioTemplatePackCollection('missing-pack')).toThrow("Unknown template pack 'missing-pack'.")
194
+ })
195
+
196
+ it('exposes discoverable template collection presets', () => {
197
+ const presets = listBioTemplatePresets()
198
+
199
+ expect(presets.map(entry => entry.name)).toEqual([
200
+ 'wellplate-screen',
201
+ 'qpcr-expression',
202
+ 'lcms-batch',
203
+ 'elisa-assay',
204
+ 'flow-cytometry-assay',
205
+ 'western-blot-assay',
206
+ ])
207
+ expect(getBioTemplatePresetInfo('wellplate')?.name).toBe('wellplate-screen')
208
+ expect(getBioTemplatePresetInfo('dose planner')?.name).toBe('wellplate-screen')
209
+ expect(getBioTemplatePresetInfo('dose-planner')?.name).toBe('wellplate-screen')
210
+ expect(getBioTemplatePresetInfo('gene expression')?.templates).toEqual([
211
+ 'sample-sheet',
212
+ 'qpcr-plate',
213
+ ])
214
+ expect(searchBioTemplatePresets('run queue')[0].name).toBe('lcms-batch')
215
+ expect(getBioTemplatePresetInfo('immunoassay')?.templates).toEqual([
216
+ 'plate-map',
217
+ 'sample-sheet',
218
+ 'calibration-curve',
219
+ 'assay-matrix',
220
+ ])
221
+ expect(getBioTemplatePresetInfo('facs')?.templates).toEqual([
222
+ 'sample-sheet',
223
+ 'flow-cytometry-panel',
224
+ 'assay-matrix',
225
+ ])
226
+ expect(getBioTemplatePresetInfo('immunoblot')?.templates).toEqual([
227
+ 'sample-sheet',
228
+ 'reagent-list',
229
+ 'protocol-steps',
230
+ 'assay-matrix',
231
+ ])
232
+ })
233
+
234
+ it('matches the shared Python/frontend preset contract', () => {
235
+ expect(listBioTemplatePresets()).toEqual(bioTemplatePresetsContract)
236
+ })
237
+
238
+ it('prefers generic dataframe adapters in catalog entries', () => {
239
+ const dataframeTemplates = listBioTemplateCatalog().filter(entry =>
240
+ entry.components.includes('DataFrame')
241
+ )
242
+
243
+ expect(dataframeTemplates.map(entry => entry.name)).toEqual([
244
+ 'sample-sheet',
245
+ 'sample-prep',
246
+ 'calibration-curve',
247
+ 'time-course',
248
+ 'protocol-steps',
249
+ 'assay-matrix',
250
+ 'reagent-list',
251
+ 'flow-cytometry-panel',
252
+ 'instrument-run',
253
+ 'qpcr-plate',
254
+ ])
255
+ for (const entry of dataframeTemplates) {
256
+ expect(entry.frontend_adapters[0]).toBe('toTemplateDataFrame')
257
+ expect(entry.frontend_import).toContain('toTemplateDataFrame')
258
+ }
259
+ })
260
+
261
+ it('exposes control schemas for built-in templates and presets', () => {
262
+ const schemas = listBioTemplateControlSchemas()
263
+ const plateSchema = getBioTemplateControlSchema('wellplate')
264
+ const presetSchema = getBioTemplateControlSchema('wellplate-screen')
265
+ const defaults = getBioTemplateControlDefaults('lcms-batch')
266
+ const elisaDefaults = getBioTemplateControlDefaults('elisa-assay')
267
+ const flowDefaults = getBioTemplateControlDefaults('flow-cytometry-assay')
268
+ const westernDefaults = getBioTemplateControlDefaults('western-blot-assay')
269
+
270
+ expect(Object.keys(schemas)).toContain('plate-map')
271
+ expect(plateSchema?.plateFormat.default).toBe(96)
272
+ expect(presetSchema?.replicates.default).toBe(3)
273
+ expect(defaults.instrument).toBe('LC-MS')
274
+ expect(defaults.featureNames).toEqual(['Glucose', 'Lactate'])
275
+ expect(elisaDefaults.analyte).toBe('Analyte')
276
+ expect(elisaDefaults.standardConcentrations).toEqual(['1000', '100', '10', '1'])
277
+ expect(flowDefaults.markers).toEqual(['CD3', 'CD4', 'CD8'])
278
+ expect(flowDefaults.includeDefaultControls).toBe(true)
279
+ expect(westernDefaults.targets).toEqual(['Target protein'])
280
+ expect(westernDefaults.loadingControl).toBe('ACTB')
281
+ })
282
+
283
+ it('exposes component bindings for template-driven frontend rendering', () => {
284
+ const bindings = listBioTemplateComponentBindings()
285
+ const wellplateBindings = getBioTemplateComponentBindings('wellplate-screen')
286
+ const qpcrBindings = getBioTemplateComponentBindings('gene expression')
287
+ const elisaBindings = getBioTemplateComponentBindings('elisa-assay')
288
+ const flowBindings = getBioTemplateComponentBindings('flow-cytometry-assay')
289
+ const westernBindings = getBioTemplateComponentBindings('western-blot-assay')
290
+ const wellplateImports = toBioTemplateComponentImports('wellplate-screen')
291
+
292
+ expect(bindings['plate-map'].map(binding => binding.component)).toEqual(['PlateMapEditor', 'WellPlate'])
293
+ expect(wellplateBindings.map(binding => binding.component)).toContain('DoseCalculator')
294
+ expect(wellplateBindings.map(binding => binding.component)).toContain('WellPlate')
295
+ expect(qpcrBindings.map(binding => binding.component)).toContain('WellPlate')
296
+ expect(qpcrBindings.map(binding => binding.component)).toContain('DataFrame')
297
+ expect(elisaBindings.map(binding => binding.component)).toContain('PlateMapEditor')
298
+ expect(elisaBindings.map(binding => binding.component)).toContain('DataFrame')
299
+ expect(flowBindings.map(binding => binding.template_id)).toEqual([
300
+ 'sample-sheet',
301
+ 'sample-sheet',
302
+ 'flow-cytometry-panel',
303
+ 'assay-matrix',
304
+ ])
305
+ expect(westernBindings.map(binding => binding.component)).toContain('ReagentList')
306
+ expect(westernBindings.map(binding => binding.component)).toContain('ExperimentTimeline')
307
+ expect(wellplateImports).toEqual([
308
+ {
309
+ importPath: '@morscherlab/mint-sdk',
310
+ components: ['PlateMapEditor', 'WellPlate', 'DataFrame', 'SampleSelector', 'DoseCalculator'],
311
+ statement: "import { PlateMapEditor, WellPlate, DataFrame, SampleSelector, DoseCalculator } from '@morscherlab/mint-sdk'",
312
+ },
313
+ ])
314
+ })
315
+
316
+ it('creates plate-map envelopes and adapts them to plate editor state', () => {
317
+ const template = createPlateMapTemplate({
318
+ name: 'Drug screen plate',
319
+ samples: ['DMSO', 'Drug A'],
320
+ })
321
+ const state = toPlateMapEditorState(template)
322
+
323
+ expect(template.template_id).toBe('plate-map')
324
+ expect(state.activePlateId).toBe('plate-1')
325
+ expect(state.plates[0].format).toBe(96)
326
+ expect(state.samples[1].id).toBe('drug-a')
327
+ })
328
+
329
+ it('loads the shared plate-map fixture', () => {
330
+ const state = toPlateMapEditorState(plateMapFixture as PlateMapTemplate)
331
+ const wells = toWellPlateWells(plateMapFixture as PlateMapTemplate)
332
+ const componentProps = toBioTemplateComponentProps(plateMapFixture as PlateMapTemplate)
333
+ const componentPropsById = toBioTemplateComponentPropsById(plateMapFixture as PlateMapTemplate)
334
+ const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
335
+
336
+ expect(state.plates[0].name).toBe('Drug screen plate')
337
+ expect(wells.A2.sampleType).toBe('drug-a')
338
+ expect(wellPlate?.propsObject.format).toBe(96)
339
+ expect((wellPlate?.propsObject.wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
340
+ expect(componentPropsById['plate-map:WellPlate']).toEqual(wellPlate?.propsObject)
341
+ expect((componentPropsById['plate-map:WellPlate'].wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
342
+ })
343
+
344
+ it('returns component props keyed by stable template component id', () => {
345
+ const collection = createWellPlateScreenCollection({
346
+ samples: ['Control', 'Treatment'],
347
+ compounds: { 'Drug A': [10, 1] },
348
+ })
349
+ const propsById = toBioTemplateComponentPropsById(collection)
350
+ const propsByComponent = toBioTemplateComponentPropsByComponent(collection)
351
+ const doseCalculatorProps = getBioTemplateComponentProps(collection, 'DoseCalculator')
352
+ const doseWellPlateProps = getBioTemplateComponentProps(collection, 'WellPlate', {
353
+ templateId: 'dose-response',
354
+ })
355
+
356
+ expect(Object.keys(propsById)).toEqual([
357
+ 'plate-map:PlateMapEditor',
358
+ 'plate-map:WellPlate',
359
+ 'sample-sheet:DataFrame',
360
+ 'sample-sheet:SampleSelector',
361
+ 'dose-response:DoseCalculator',
362
+ 'dose-response:WellPlate',
363
+ ])
364
+ expect(propsById['dose-response:DoseCalculator'].mode).toBe('serial')
365
+ expect(propsById['plate-map:WellPlate'].wells).toBeDefined()
366
+ expect(propsById['dose-response:WellPlate'].wells).toBeDefined()
367
+ expect(propsByComponent.WellPlate).toHaveLength(2)
368
+ expect(propsByComponent.DoseCalculator[0].mode).toBe('serial')
369
+ expect(doseCalculatorProps?.mode).toBe('serial')
370
+ expect(doseWellPlateProps?.wells).toBeDefined()
371
+ expect(getBioTemplateComponentProps(collection, 'MissingWidget')).toBeUndefined()
372
+ })
373
+
374
+ it('creates ELISA assay preset collections with plate, standards, and readout matrix', () => {
375
+ const collection = createElisaAssayCollection({
376
+ samples: ['Control', 'Treatment'],
377
+ analyte: 'IL-6',
378
+ standardConcentrations: [1000, 100, 10, 1],
379
+ unit: 'pg/mL',
380
+ })
381
+ const templates = extractTemplateCollection(collection)
382
+ const componentPropsById = toBioTemplateComponentPropsById(collection)
383
+
384
+ expect(Object.keys(templates)).toEqual([
385
+ 'plate-map',
386
+ 'sample-sheet',
387
+ 'calibration-curve',
388
+ 'assay-matrix',
389
+ ])
390
+ expect(collection.metadata?.preset).toBe('elisa-assay')
391
+ expect(templates['calibration-curve'].data).toMatchObject({
392
+ analyte: 'IL-6',
393
+ model: 'four-parameter-logistic',
394
+ xUnit: 'pg/mL',
395
+ responseUnit: 'OD450',
396
+ })
397
+ expect(templates['assay-matrix'].data).toMatchObject({
398
+ features: [{ id: 'il-6', name: 'IL-6', type: 'protein', unit: 'OD450', metadata: {} }],
399
+ })
400
+ expect(componentPropsById['plate-map:WellPlate'].wells).toBeDefined()
401
+ expect(componentPropsById['calibration-curve:DataFrame'].data).toBeDefined()
402
+ expect(componentPropsById['assay-matrix:DataFrame'].columns).toBeDefined()
403
+ })
404
+
405
+ it('generates Vue snippets for concrete template component props', () => {
406
+ const snippets = toBioTemplateComponentSnippets(plateMapFixture as PlateMapTemplate, {
407
+ propsVariable: 'bindings',
408
+ })
409
+
410
+ expect(snippets.map(snippet => snippet.component)).toEqual(['PlateMapEditor', 'WellPlate'])
411
+ expect(snippets.map(snippet => snippet.propsName)).toEqual([
412
+ 'plateMapPlateMapEditorProps',
413
+ 'plateMapWellPlateProps',
414
+ ])
415
+ expect(snippets[0].propsExpression).toBe('bindings[0].propsObject')
416
+ expect(snippets[0].template).toBe('<PlateMapEditor v-bind="bindings[0].propsObject" />')
417
+ expect(snippets[1].template).toBe('<WellPlate v-bind="bindings[1].propsObject" />')
418
+ })
419
+
420
+ it('generates a Vue usage bundle for concrete template component props', () => {
421
+ const usage = toBioTemplateComponentUsage(plateMapFixture as PlateMapTemplate, {
422
+ propsVariable: 'bindings',
423
+ targetExpression: 'plateMap',
424
+ })
425
+
426
+ expect(usage.imports[0].statement).toBe(
427
+ "import { PlateMapEditor, WellPlate } from '@morscherlab/mint-sdk'"
428
+ )
429
+ expect(usage.componentProps).toHaveLength(2)
430
+ expect(usage.propsDeclarations).toEqual([
431
+ 'const plateMapPlateMapEditorProps = bindings[0].propsObject',
432
+ 'const plateMapWellPlateProps = bindings[1].propsObject',
433
+ ])
434
+ expect(usage.scriptSetup).toBe([
435
+ 'const bindings = toBioTemplateComponentProps(plateMap)',
436
+ 'const plateMapPlateMapEditorProps = bindings[0].propsObject',
437
+ 'const plateMapWellPlateProps = bindings[1].propsObject',
438
+ ].join('\n'))
439
+ expect(usage.template).toBe([
440
+ '<PlateMapEditor v-bind="plateMapPlateMapEditorProps" />',
441
+ '<WellPlate v-bind="plateMapWellPlateProps" />',
442
+ ].join('\n'))
443
+ })
444
+
445
+ it('generates a complete Vue SFC for concrete template component props', () => {
446
+ const usage = toBioTemplateComponentUsage(plateMapFixture as PlateMapTemplate)
447
+
448
+ expect(usage.scriptSetup).toContain(
449
+ 'const props = defineProps<{ templateCollection: BioTemplateEnvelope<unknown> | TemplateCollectionEnvelope }>()'
450
+ )
451
+ expect(usage.sfc).toContain("import { PlateMapEditor, WellPlate } from '@morscherlab/mint-sdk'")
452
+ expect(usage.sfc).toContain("import { toBioTemplateComponentProps } from '@morscherlab/mint-sdk/templates'")
453
+ expect(usage.sfc).toContain('defineProps<{ templateCollection: BioTemplateEnvelope<unknown> | TemplateCollectionEnvelope }>()')
454
+ expect(usage.sfc).toContain('<PlateMapEditor v-bind="plateMapPlateMapEditorProps" />')
455
+ expect(usage.sfc).toContain('<WellPlate v-bind="plateMapWellPlateProps" />')
456
+ expect(usage.sfc).toMatch(/^<script setup lang="ts">[\s\S]*<\/template>$/)
457
+ })
458
+
459
+ it('rejects mismatched template envelopes', () => {
460
+ expect(() => assertTemplateEnvelope(plateMapFixture, 'sample-sheet')).toThrow(
461
+ "Expected template_id 'sample-sheet'"
462
+ )
463
+ })
464
+
465
+ it('returns validated typed template envelopes', () => {
466
+ const template = ensureTemplateEnvelope<PlateMapTemplate>(plateMapFixture, 'plate-map')
467
+
468
+ expect(template.data.plates[0].id).toBe('plate-1')
469
+ expect(() => ensureTemplateEnvelope<SampleSheetTemplate>(plateMapFixture, 'sample-sheet')).toThrow(
470
+ "Expected template_id 'sample-sheet'"
471
+ )
472
+ })
473
+
474
+ it('builds and reads template collections', () => {
475
+ const plate = createPlateMapTemplate({
476
+ name: 'Drug screen plate',
477
+ samples: ['DMSO', 'Drug A'],
478
+ })
479
+ const sampleSheet = createSampleSheetTemplate({
480
+ samples: [
481
+ { sampleId: 'S001', name: 'Control 1', group: 'Control' },
482
+ ],
483
+ })
484
+ const collection = createTemplateCollection([plate, sampleSheet], { source: 'unit-test' })
485
+ const extracted = extractTemplateCollection(collection)
486
+ const typedPlate = ensureTemplateFromCollection<PlateMapTemplate>(collection, 'plate-map')
487
+
488
+ expect(Object.keys(collection[TEMPLATE_COLLECTION_KEY])).toEqual(['plate-map', 'sample-sheet'])
489
+ expect(collection.metadata?.source).toBe('unit-test')
490
+ expect(extracted['sample-sheet'].template_id).toBe('sample-sheet')
491
+ expect(typedPlate.data.plates[0].name).toBe('Drug screen plate')
492
+ expect(extractTemplateCollection(plate)['plate-map'].template_id).toBe('plate-map')
493
+ expect(() => ensureTemplateFromCollection<SampleSheetTemplate>(collection, 'sample-prep')).toThrow(
494
+ "Template 'sample-prep' was not found"
495
+ )
496
+ expect(() => extractTemplateCollection({ templates: { 'sample-sheet': plate } })).toThrow(
497
+ 'does not match envelope template_id'
498
+ )
499
+ expect(() => createTemplateCollection([plate, plate])).toThrow('Duplicate template_id')
500
+ })
501
+
502
+ it('creates ready-to-save template preset collections', () => {
503
+ const wellplate = createWellPlateScreenCollection({
504
+ samples: ['Control', 'Treatment'],
505
+ compounds: { 'Drug A': [10, 1, 0.1] },
506
+ unit: 'uM',
507
+ })
508
+ const qpcr = createQpcrExpressionCollection({
509
+ samples: [{ sampleId: 'S001', name: 'Control', group: 'Control' }],
510
+ targets: ['ACTB'],
511
+ })
512
+ const lcms = createLcmsBatchCollection({
513
+ samples: ['S001', 'S002'],
514
+ features: ['Glucose'],
515
+ instrument: 'LC-MS',
516
+ })
517
+ const flow = createFlowCytometryAssayCollection({
518
+ samples: ['Control', 'Treatment'],
519
+ markers: ['CD3', 'CD4'],
520
+ instrument: 'BD LSRFortessa',
521
+ })
522
+ const western = createWesternBlotAssayCollection({
523
+ samples: ['Control', 'Treatment'],
524
+ targets: ['pERK', 'ERK'],
525
+ loadingControl: 'ACTB',
526
+ })
527
+
528
+ const wellplateTemplates = extractTemplateCollection(wellplate)
529
+ const qpcrTemplates = extractTemplateCollection(qpcr)
530
+ const lcmsTemplates = extractTemplateCollection(lcms)
531
+ const flowTemplates = extractTemplateCollection(flow)
532
+ const westernTemplates = extractTemplateCollection(western)
533
+ const wellplateComponentProps = toBioTemplateComponentProps(wellplate)
534
+
535
+ expect(Object.keys(wellplateTemplates)).toEqual(['plate-map', 'sample-sheet', 'dose-response'])
536
+ expect(wellplate.metadata?.preset).toBe('wellplate-screen')
537
+ expect(toPlateMapEditorState(wellplateTemplates['plate-map'] as PlateMapTemplate).samples[1].id).toBe('treatment')
538
+ expect(toDoseConditions(wellplateTemplates['dose-response'] as DoseResponseTemplate)[0].label).toBe('Drug A')
539
+ expect(wellplateComponentProps.map(binding => binding.component)).toContain('PlateMapEditor')
540
+ expect(wellplateComponentProps.map(binding => binding.component)).toContain('DoseCalculator')
541
+ expect(Object.keys(qpcrTemplates)).toEqual(['sample-sheet', 'qpcr-plate'])
542
+ expect(toQpcrRows(qpcrTemplates['qpcr-plate'] as QpcrPlateTemplate)[0].sampleId).toBe('S001')
543
+ expect(Object.keys(lcmsTemplates)).toEqual(['sample-sheet', 'instrument-run', 'assay-matrix'])
544
+ expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[0].kind).toBe('blank')
545
+ expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[2].sampleId).toBe('s001')
546
+ expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('glucose')
547
+ expect(Object.keys(flowTemplates)).toEqual(['sample-sheet', 'flow-cytometry-panel', 'assay-matrix'])
548
+ expect(flow.metadata?.preset).toBe('flow-cytometry-assay')
549
+ expect(flowTemplates['flow-cytometry-panel'].data).toMatchObject({
550
+ instrument: 'BD LSRFortessa',
551
+ markers: [
552
+ { id: 'cd3', marker: 'CD3' },
553
+ { id: 'cd4', marker: 'CD4' },
554
+ ],
555
+ })
556
+ expect(toTemplateDataFrame(flowTemplates['assay-matrix']).columns.map(column => column.key)).toContain('cd4')
557
+ expect(Object.keys(westernTemplates)).toEqual(['sample-sheet', 'reagent-list', 'protocol-steps', 'assay-matrix'])
558
+ expect(western.metadata?.preset).toBe('western-blot-assay')
559
+ expect(toReagentDataFrame(westernTemplates['reagent-list'] as ReagentListTemplate).data[0].kind).toBe('antibody')
560
+ expect(toProtocolDataFrame(westernTemplates['protocol-steps'] as ProtocolStepsTemplate).data[2].name).toBe('Transfer to membrane')
561
+ expect(toTemplateDataFrame(westernTemplates['assay-matrix']).columns.map(column => column.key)).toContain('actb')
562
+
563
+ const aliasCollection = createBioTemplatePresetCollection('wellplate', {
564
+ samples: ['Control', 'Treatment'],
565
+ compounds: { 'Drug A': [10, 1] },
566
+ })
567
+ expect(aliasCollection.metadata?.preset).toBe('wellplate-screen')
568
+ expect(Object.keys(extractTemplateCollection(aliasCollection))).toEqual(['plate-map', 'sample-sheet', 'dose-response'])
569
+ expect(createBioTemplatePresetCollection('facs').metadata?.preset).toBe('flow-cytometry-assay')
570
+ expect(createBioTemplatePresetCollection('immunoblot').metadata?.preset).toBe('western-blot-assay')
571
+ expect(() => createBioTemplatePresetCollection('unknown-preset')).toThrow("Unknown template preset 'unknown-preset'.")
572
+ })
573
+
574
+ it('maps preset control values into template collection previews', () => {
575
+ const wellplateOptions = bioTemplatePresetControlValuesToOptions('wellplate-screen', {
576
+ sampleNames: 'Vehicle, Drug A, Drug B',
577
+ plateName: 'Custom screen',
578
+ plateFormat: '384',
579
+ unit: 'nM',
580
+ replicates: '2',
581
+ })
582
+ const wellplate = createBioTemplatePresetCollectionFromControls('wellplate-screen', {
583
+ sampleNames: 'Vehicle, Drug A, Drug B',
584
+ plateName: 'Custom screen',
585
+ plateFormat: '384',
586
+ unit: 'nM',
587
+ replicates: '2',
588
+ })
589
+ const wellplateTemplates = extractTemplateCollection(wellplate)
590
+ const plate = wellplateTemplates['plate-map'] as PlateMapTemplate
591
+ const dose = wellplateTemplates['dose-response'] as DoseResponseTemplate
592
+
593
+ expect(wellplateOptions.samples).toEqual(['Vehicle', 'Drug A', 'Drug B'])
594
+ expect(wellplateOptions.plateFormat).toBe(384)
595
+ expect(wellplateOptions.replicates).toBe(2)
596
+ expect(plate.data.plates[0].name).toBe('Custom screen')
597
+ expect(plate.data.plates[0].format).toBe(384)
598
+ expect(plate.data.samples.map(sample => sample.id)).toEqual(['vehicle', 'drug-a', 'drug-b'])
599
+ expect(dose.data.unit).toBe('nM')
600
+ expect(dose.data.replicates).toBe(2)
601
+
602
+ const qpcrOptions = bioTemplatePresetControlValuesToOptions('qpcr-expression', {
603
+ sampleNames: ['Control', 'Knockdown'],
604
+ targets: 'ACTB, MYC',
605
+ chemistry: 'taqman',
606
+ plateFormat: 384,
607
+ includeNoTemplateControls: false,
608
+ replicates: 2,
609
+ instrument: 'QuantStudio',
610
+ })
611
+ const qpcr = ensureTemplateFromCollection<QpcrPlateTemplate>(
612
+ createBioTemplatePresetCollectionFromControls('qpcr-expression', {
613
+ sampleNames: ['Control', 'Knockdown'],
614
+ targets: 'ACTB, MYC',
615
+ chemistry: 'taqman',
616
+ plateFormat: 384,
617
+ includeNoTemplateControls: false,
618
+ replicates: 2,
619
+ instrument: 'QuantStudio',
620
+ }),
621
+ 'qpcr-plate',
622
+ )
623
+
624
+ expect(qpcrOptions.targets).toEqual(['ACTB', 'MYC'])
625
+ expect(qpcrOptions.includeNoTemplateControls).toBe(false)
626
+ expect(qpcr.data.chemistry).toBe('taqman')
627
+ expect(qpcr.data.format).toBe(384)
628
+ expect(qpcr.data.instrument).toBe('QuantStudio')
629
+ expect(qpcr.data.reactions).toHaveLength(8)
630
+ expect(qpcr.data.reactions.some(reaction => reaction.controlKind === 'no-template')).toBe(false)
631
+
632
+ const lcmsOptions = bioTemplatePresetControlValuesToOptions('lcms-batch', {
633
+ sampleNames: 'S001, S002',
634
+ featureNames: 'Glucose, Pyruvate',
635
+ instrument: 'Orbitrap',
636
+ method: 'Polar metabolites',
637
+ includeQc: false,
638
+ })
639
+ const lcmsTemplates = extractTemplateCollection(
640
+ createBioTemplatePresetCollectionFromControls('lcms-batch', {
641
+ sampleNames: 'S001, S002',
642
+ featureNames: 'Glucose, Pyruvate',
643
+ instrument: 'Orbitrap',
644
+ method: 'Polar metabolites',
645
+ includeQc: false,
646
+ }),
647
+ )
648
+ const run = lcmsTemplates['instrument-run'] as InstrumentRunTemplate
649
+
650
+ expect(lcmsOptions.features).toEqual(['Glucose', 'Pyruvate'])
651
+ expect(lcmsOptions.includeQc).toBe(false)
652
+ expect(run.data.instrument).toBe('Orbitrap')
653
+ expect(run.data.methods[0].name).toBe('Polar metabolites')
654
+ expect(run.data.items.map(item => item.kind)).toEqual(['blank', 'sample', 'sample'])
655
+ expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('pyruvate')
656
+
657
+ const flowOptions = bioTemplatePresetControlValuesToOptions('flow-cytometry-assay', {
658
+ sampleNames: 'Control, Treatment',
659
+ markers: 'CD3, CD19',
660
+ featureNames: 'T cells, B cells',
661
+ instrument: 'Cytek Aurora',
662
+ includeDefaultControls: false,
663
+ })
664
+ const flowTemplates = extractTemplateCollection(
665
+ createBioTemplatePresetCollectionFromControls('flow-cytometry-assay', {
666
+ sampleNames: 'Control, Treatment',
667
+ markers: 'CD3, CD19',
668
+ featureNames: 'T cells, B cells',
669
+ instrument: 'Cytek Aurora',
670
+ includeDefaultControls: false,
671
+ }),
672
+ )
673
+ const flowPanel = flowTemplates['flow-cytometry-panel'] as FlowCytometryPanelTemplate
674
+
675
+ expect(flowOptions.markers).toEqual(['CD3', 'CD19'])
676
+ expect(flowOptions.features).toEqual(['T cells', 'B cells'])
677
+ expect(flowPanel.data.instrument).toBe('Cytek Aurora')
678
+ expect(flowPanel.data.controls).toEqual([])
679
+ expect(toTemplateDataFrame(flowTemplates['assay-matrix']).columns.map(column => column.key)).toContain('b-cells')
680
+
681
+ const westernOptions = bioTemplatePresetControlValuesToOptions('western-blot-assay', {
682
+ sampleNames: 'Control, Treatment',
683
+ targets: 'pERK, ERK',
684
+ loadingControl: 'ACTB',
685
+ })
686
+ const westernTemplates = extractTemplateCollection(
687
+ createBioTemplatePresetCollectionFromControls('western-blot-assay', {
688
+ sampleNames: 'Control, Treatment',
689
+ targets: 'pERK, ERK',
690
+ loadingControl: 'ACTB',
691
+ }),
692
+ )
693
+
694
+ expect(westernOptions.targets).toEqual(['pERK', 'ERK'])
695
+ expect(westernOptions.loadingControl).toBe('ACTB')
696
+ expect(toProtocolSteps(westernTemplates['protocol-steps'] as ProtocolStepsTemplate).map(step => step.name)).toContain('Run SDS-PAGE')
697
+ expect(toTemplateDataFrame(westernTemplates['assay-matrix']).columns.map(column => column.key)).toContain('actb')
698
+ })
699
+
700
+ it('creates sample-sheet templates and adapts rows, columns, and options', () => {
701
+ const template = createSampleSheetTemplate({
702
+ samples: [
703
+ {
704
+ sampleId: 'S001',
705
+ name: 'Control 1',
706
+ group: 'Control',
707
+ metadata: { cell_line: 'HCT116' },
708
+ },
709
+ {
710
+ sampleId: 'S002',
711
+ name: 'Control 2',
712
+ group: 'Control',
713
+ },
714
+ ],
715
+ columns: [
716
+ { id: 'sampleId', label: 'Sample ID', type: 'string', required: true, groupable: false },
717
+ { id: 'cell_line', label: 'Cell Line', type: 'string', required: false, groupable: true },
718
+ ],
719
+ })
720
+
721
+ expect(toSampleRows(template)[0].cell_line).toBe('HCT116')
722
+ expect(template.data.samples[1].metadata).toEqual({})
723
+ expect(toSampleColumns(template)[0].key).toBe('sampleId')
724
+ expect(toSampleColumns(template)[1].key).toBe('cell_line')
725
+ const sampleFrame = toSampleDataFrame(template)
726
+ expect(sampleFrame.rowKey).toBe('sampleId')
727
+ expect(sampleFrame.data[0].cell_line).toBe('HCT116')
728
+ expect(sampleFrame.columns.map(column => column.key)).toEqual(['sampleId', 'cell_line'])
729
+ expect(toTemplateDataFrame(template).rowKey).toBe('sampleId')
730
+ expect(toSampleOptions(template)[0].label).toBe('Control 1')
731
+ })
732
+
733
+ it('loads the shared sample-sheet fixture', () => {
734
+ const rows = toSampleRows(sampleSheetFixture as SampleSheetTemplate)
735
+ const columns = toSampleColumns(sampleSheetFixture as SampleSheetTemplate)
736
+ const options = toSampleOptions(sampleSheetFixture as SampleSheetTemplate)
737
+
738
+ expect(rows[1].group).toBe('Drug A')
739
+ expect(columns[0].key).toBe('sampleId')
740
+ expect(options[0].value).toBe('S001')
741
+ })
742
+
743
+ it('creates sample-prep templates and adapts prep steps', () => {
744
+ const template = createSamplePrepTemplate({
745
+ samples: ['S001', { name: 'Dilute S002', sourceSampleId: 'S002', type: 'dilution' }],
746
+ prepType: 'extraction',
747
+ outputVolume: 50,
748
+ })
749
+ const rows = toSamplePrepRows(template)
750
+ const frame = toSamplePrepDataFrame(template)
751
+
752
+ expect(template.template_id).toBe('sample-prep')
753
+ expect(rows[0].sourceSampleId).toBe('s001')
754
+ expect(rows[0].destinationSampleId).toBe('s001-prepared')
755
+ expect(rows[0].outputVolume).toBe(50)
756
+ expect(rows[1].id).toBe('dilute-s002')
757
+ expect(frame.rowKey).toBe('id')
758
+ expect(frame.columns.map(column => column.key)).toContain('status')
759
+ expect(toTemplateDataFrame(template).data[1].type).toBe('dilution')
760
+ })
761
+
762
+ it('creates dose-response templates and adapts dose conditions', () => {
763
+ const template = createDoseResponseTemplate({
764
+ compounds: { 'Drug A': [0.1, 10, 1] },
765
+ unit: 'uM',
766
+ replicates: 2,
767
+ })
768
+ const conditions = toDoseConditions(template)
769
+
770
+ expect(template.template_id).toBe('dose-response')
771
+ expect(template.data.compounds[0].concentrations).toEqual([10, 1, 0.1])
772
+ expect(conditions[0].unit).toBe('uM')
773
+ expect(conditions[0].concentrations).toEqual([10, 1, 0.1])
774
+ })
775
+
776
+ it('defaults metadata for explicit template records', () => {
777
+ const samplePrep = createSamplePrepTemplate({
778
+ samples: [{ name: 'Extract S001', sourceSampleId: 'S001' }],
779
+ })
780
+ const doseResponse = createDoseResponseTemplate({
781
+ compounds: [{ id: 'drug-a', name: 'Drug A', concentrations: [1, 0.1] }],
782
+ unit: 'uM',
783
+ controls: [{ id: 'vehicle', name: 'DMSO', role: 'vehicle' }],
784
+ })
785
+ const timeCourse = createTimeCourseTemplate({
786
+ timepoints: [{ id: 't0-hour', label: '0 hour', value: 0, unit: 'hour' }],
787
+ unit: 'hour',
788
+ conditions: [{ id: 'control', name: 'Control' }],
789
+ })
790
+ const assayMatrix = createAssayMatrixTemplate({
791
+ samples: [{ sampleId: 'S001', name: 'Control 1' }],
792
+ features: [{ id: 'lactate', name: 'Lactate', type: 'metabolite' }],
793
+ })
794
+
795
+ expect(samplePrep.data.steps[0].metadata).toEqual({})
796
+ expect(doseResponse.data.compounds[0].metadata).toEqual({})
797
+ expect(doseResponse.data.controls[0].metadata).toEqual({})
798
+ expect(timeCourse.data.timepoints[0].metadata).toEqual({})
799
+ expect(timeCourse.data.conditions[0].metadata).toEqual({})
800
+ expect(assayMatrix.data.samples[0].metadata).toEqual({})
801
+ expect(assayMatrix.data.features[0].metadata).toEqual({})
802
+ expect(toDoseConditions(doseResponse)[0].color).toBe('#3B82F6')
803
+ expect(toTimeCourseRows(timeCourse)[0].sampleId).toBe('control-t0-hour-r1')
804
+ expect(toAssayMatrixRows(assayMatrix)[0].sampleId).toBe('S001')
805
+ })
806
+
807
+ it('loads the shared dose-response fixture with layout', () => {
808
+ const conditions = toDoseConditions(doseResponseFixture as DoseResponseTemplate)
809
+ const layout = toDoseLayoutState(doseResponseFixture as DoseResponseTemplate)
810
+ const componentProps = toBioTemplateComponentProps(doseResponseFixture as DoseResponseTemplate)
811
+ const doseCalculator = componentProps.find(binding => binding.component === 'DoseCalculator')
812
+ const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
813
+
814
+ expect(conditions[0].label).toBe('Drug A')
815
+ expect(layout?.plates[0].wells.A2.value).toBe(10)
816
+ expect(doseCalculator?.propsObject.mode).toBe('serial')
817
+ expect((doseCalculator?.propsObject.targetWells as string[])).toContain('A2')
818
+ expect((wellPlate?.propsObject.wells as Record<string, { value?: number }>).A2.value).toBe(10)
819
+ })
820
+
821
+ it('rejects invalid dose-response data', () => {
822
+ expect(() => createDoseResponseTemplate({
823
+ compounds: { 'Drug A': [-1] },
824
+ unit: 'uM',
825
+ })).toThrow('negative concentration')
826
+ })
827
+
828
+ it('creates calibration-curve templates and adapts standard curves', () => {
829
+ const template = createCalibrationCurveTemplate({
830
+ concentrations: [0.1, { level: 'Standard 2', concentration: 1 }, 10],
831
+ analyte: 'Glucose',
832
+ unit: 'uM',
833
+ })
834
+ const rows = toCalibrationCurveRows(template)
835
+ const frame = toCalibrationCurveDataFrame(template)
836
+
837
+ expect(template.template_id).toBe('calibration-curve')
838
+ expect(template.data.points.map(point => point.role)).toEqual([
839
+ 'blank',
840
+ 'standard',
841
+ 'standard',
842
+ 'standard',
843
+ 'qc',
844
+ 'qc',
845
+ ])
846
+ expect(rows[2].id).toBe('standard-2')
847
+ expect(rows[2].analyte).toBe('Glucose')
848
+ expect(frame.rowKey).toBe('id')
849
+ expect(frame.columns.map(column => column.key)).toContain('status')
850
+ expect(toTemplateDataFrame(template).data[0].role).toBe('blank')
851
+ })
852
+
853
+ it('creates time-course templates and adapts samples and steps', () => {
854
+ const template = createTimeCourseTemplate({
855
+ timepoints: [24, 0, 6],
856
+ unit: 'hour',
857
+ conditions: ['Control', 'Treatment'],
858
+ replicates: 2,
859
+ })
860
+ const rows = toTimeCourseRows(template)
861
+ const steps = toTimeCourseSteps(template)
862
+
863
+ expect(template.template_id).toBe('time-course')
864
+ expect(template.data.timepoints.map(timepoint => timepoint.id)).toEqual(['t0-hour', 't6-hour', 't24-hour'])
865
+ expect(rows).toHaveLength(12)
866
+ expect(toTimeCourseDataFrame(template).columns[0].key).toBe('sampleId')
867
+ expect(toTemplateDataFrame(template).data).toHaveLength(12)
868
+ expect(steps[0].type).toBe('measurement')
869
+ })
870
+
871
+ it('creates protocol-step templates and adapts timeline and dataframe props', () => {
872
+ const template = createProtocolStepsTemplate({
873
+ steps: [
874
+ {
875
+ name: 'Measure readout',
876
+ type: 'measurement',
877
+ duration: 45,
878
+ order: 2,
879
+ },
880
+ 'Seed cells',
881
+ ],
882
+ })
883
+ const steps = toProtocolSteps(template)
884
+ const frame = toProtocolDataFrame(template)
885
+
886
+ expect(template.template_id).toBe('protocol-steps')
887
+ expect(steps.map(step => step.name)).toEqual(['Seed cells', 'Measure readout'])
888
+ expect(steps[1].type).toBe('measurement')
889
+ expect(frame.rowKey).toBe('id')
890
+ expect(frame.columns.map(column => column.key)).toContain('duration')
891
+ expect(toTemplateDataFrame(template).data[0].id).toBe('seed-cells')
892
+ })
893
+
894
+ it('creates assay-matrix templates and adapts pivoted rows and columns', () => {
895
+ const template = createAssayMatrixTemplate({
896
+ samples: ['S001', 'S002'],
897
+ features: ['Lactate', 'Glucose'],
898
+ values: {
899
+ s001: { lactate: 1.2, glucose: 5.4 },
900
+ s002: { lactate: 2.1, glucose: 4.8 },
901
+ },
902
+ })
903
+ const rows = toAssayMatrixRows(template)
904
+ const columns = toAssayMatrixColumns(template)
905
+
906
+ expect(template.template_id).toBe('assay-matrix')
907
+ expect(rows[0].lactate).toBe(1.2)
908
+ expect(columns.map(column => column.key)).toContain('glucose')
909
+ const assayFrame = toAssayMatrixDataFrame(template)
910
+ expect(assayFrame.rowKey).toBe('sampleId')
911
+ expect(assayFrame.data[0].lactate).toBe(1.2)
912
+ expect(toTemplateDataFrame(template).columns.map(column => column.key)).toContain('glucose')
913
+ })
914
+
915
+ it('creates reagent-list templates and adapts items and dataframe props', () => {
916
+ const template = createReagentListTemplate({
917
+ reagents: [
918
+ {
919
+ id: 'dmso',
920
+ name: 'DMSO',
921
+ kind: 'compound',
922
+ storageCondition: 'RT',
923
+ stockLevel: 80,
924
+ stockUnit: '%',
925
+ },
926
+ 'Anti-HA antibody',
927
+ ],
928
+ })
929
+ const items = toReagentListItems(template)
930
+ const frame = toReagentDataFrame(template)
931
+
932
+ expect(template.template_id).toBe('reagent-list')
933
+ expect(items[0].name).toBe('DMSO')
934
+ expect(items[0].storageCondition).toBe('RT')
935
+ expect(template.data.reagents[1].id).toBe('anti-ha-antibody')
936
+ expect(frame.rowKey).toBe('id')
937
+ expect(frame.columns.map(column => column.key)).toContain('stockLevel')
938
+ expect(toTemplateDataFrame(template).data[0].kind).toBe('compound')
939
+ })
940
+
941
+ it('creates flow-cytometry panel templates and adapts marker rows', () => {
942
+ const template = createFlowCytometryPanelTemplate({
943
+ markers: [
944
+ { id: 'cd3', marker: 'CD3', fluorophore: 'FITC', detector: 'B530/30' },
945
+ { id: 'cd4', marker: 'CD4', fluorophore: 'PE', detector: 'B585/42' },
946
+ ],
947
+ instrument: 'BD LSRFortessa',
948
+ })
949
+ const rows = toFlowPanelRows(template)
950
+ const frame = toFlowPanelDataFrame(template)
951
+
952
+ expect(template.template_id).toBe('flow-cytometry-panel')
953
+ expect(template.data.controls.map(control => control.kind)).toContain('single-stain')
954
+ expect(rows[0].marker).toBe('CD3')
955
+ expect(rows[0].controls).toContain('CD3 single stain')
956
+ expect(frame.rowKey).toBe('id')
957
+ expect(frame.columns.map(column => column.key)).toContain('fluorophore')
958
+ expect(toTemplateDataFrame(template).data[1].detector).toBe('B585/42')
959
+ })
960
+
961
+ it('creates instrument-run templates and adapts run queues', () => {
962
+ const template = createInstrumentRunTemplate({
963
+ items: ['S001', { name: 'S002', vial: 'A2' }],
964
+ instrument: 'LC-MS',
965
+ method: 'Default method',
966
+ })
967
+ const rows = toInstrumentRunRows(template)
968
+ const frame = toInstrumentRunDataFrame(template)
969
+
970
+ expect(template.template_id).toBe('instrument-run')
971
+ expect(template.data.items.map(item => item.kind)).toEqual(['blank', 'qc', 'sample', 'sample', 'qc'])
972
+ expect(rows[0].name).toBe('Blank start')
973
+ expect(rows[2].sampleId).toBe('s001')
974
+ expect(rows[3].vial).toBe('A2')
975
+ expect(frame.rowKey).toBe('id')
976
+ expect(frame.columns.map(column => column.key)).toContain('status')
977
+ expect(toTemplateDataFrame(template).data[3].sampleId).toBe('s002')
978
+ })
979
+
980
+ it('creates qPCR plate templates and adapts reactions to dataframe and wells', () => {
981
+ const template = createQpcrPlateTemplate({
982
+ samples: ['Control', 'Treatment'],
983
+ targets: ['ACTB', 'GAPDH'],
984
+ replicates: 2,
985
+ instrument: 'QuantStudio',
986
+ })
987
+ const rows = toQpcrRows(template)
988
+ const frame = toQpcrDataFrame(template)
989
+ const wells = toQpcrWellPlateWells(template)
990
+
991
+ expect(template.template_id).toBe('qpcr-plate')
992
+ expect(template.data.reactions).toHaveLength(10)
993
+ expect(template.data.reactions.at(-1)?.controlKind).toBe('no-template')
994
+ expect(rows[0].sample).toBe('Control')
995
+ expect(rows[0].target).toBe('ACTB')
996
+ expect(frame.rowKey).toBe('id')
997
+ expect(frame.columns.map(column => column.key)).toContain('cq')
998
+ expect(wells.A1.sampleType).toBe('control')
999
+ expect(wells.A1.metadata?.targetId).toBe('actb')
1000
+ expect(toTemplateDataFrame(template).data.at(-1)?.controlKind).toBe('no-template')
1001
+ })
1002
+
1003
+ it('loads the shared qPCR plate fixture', () => {
1004
+ const template = ensureTemplateEnvelope<QpcrPlateTemplate>(qpcrPlateFixture, 'qpcr-plate')
1005
+ const frame = toQpcrDataFrame(template)
1006
+ const wells = toQpcrWellPlateWells(template)
1007
+ const componentProps = toBioTemplateComponentProps(template)
1008
+ const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
1009
+ const table = componentProps.find(binding => binding.component === 'DataFrame')
1010
+
1011
+ expect(template.data.chemistry).toBe('taqman')
1012
+ expect(frame.data[2].flags).toBe('review')
1013
+ expect(frame.data[3].sample).toBe('no-template')
1014
+ expect(wells.A1.row).toBe(0)
1015
+ expect(wells.A1.col).toBe(0)
1016
+ expect(wells.A4.sampleType).toBe('no-template')
1017
+ expect((wellPlate?.propsObject.wells as Record<string, { sampleType?: string }>).A4.sampleType).toBe('no-template')
1018
+ expect((table?.propsObject.data as Record<string, unknown>[])[0].target).toBe('ACTB')
1019
+ })
1020
+
1021
+ it('rejects templates without a dataframe adapter', () => {
1022
+ expect(() => toTemplateDataFrame(createPlateMapTemplate())).toThrow(
1023
+ "Template 'plate-map' does not expose a DataFrame adapter."
1024
+ )
1025
+ expect(() => toTemplateDataFrame(createDoseResponseTemplate({
1026
+ compounds: { 'Drug A': [1] },
1027
+ unit: 'uM',
1028
+ }))).toThrow("Template 'dose-response' does not expose a DataFrame adapter.")
1029
+ })
1030
+
1031
+ it('accepts assay-matrix display names for values', () => {
1032
+ const template = createAssayMatrixTemplate({
1033
+ samples: ['S001'],
1034
+ features: ['Lactate'],
1035
+ values: {
1036
+ S001: { Lactate: 1.2 },
1037
+ },
1038
+ })
1039
+
1040
+ expect(template.data.measurements[0].sampleId).toBe('s001')
1041
+ expect(template.data.measurements[0].featureId).toBe('lactate')
1042
+ })
1043
+ })