@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,4371 @@
1
+ import { Dn as useControlWorkspace, Ft as extractTemplateCollection, Mn as useConcentrationUnits, On as getFieldRegistryEntry, Ot as createTemplateCollection, Pt as ensureTemplateFromCollection, Xt as getBioTemplatePresetInfo, _ as toBioTemplateComponentSnippets, d as getBioTemplateComponentProps$1, dt as createBioTemplatePresetCollection, en as getBioTemplatePackInfo, ft as createBioTemplatePresetCollectionFromControls, g as toBioTemplateComponentPropsById, h as toBioTemplateComponentPropsByComponent$1, i as createBioTemplateControlToolkit, kn as getTypeDefault, m as toBioTemplateComponentProps, p as toBioTemplateComponentImports, u as getBioTemplateComponentBindings, ut as createBioTemplatePackCollection, v as toBioTemplateComponentUsage } from "./templates-50NPjaxL.js";
2
+ import { r as useSettingsStore, t as useAuthStore } from "./auth-QQj2kkze.js";
3
+ import { computed, effectScope, getCurrentScope, onMounted, onScopeDispose, onUnmounted, provide, reactive, readonly, ref, shallowRef, toRaw, toValue, watch } from "vue";
4
+ import axios from "axios";
5
+ //#region src/composables/useSortedItems.ts
6
+ /** Shared sorting for SDK tables and lists with stable empty-value handling. */
7
+ function useSortedItems(options) {
8
+ const sort = computed(() => toValue(options.sort));
9
+ const enabled = computed(() => toValue(options.enabled) ?? true);
10
+ return {
11
+ sort,
12
+ sortedItems: computed(() => {
13
+ const items = [...toValue(options.items)];
14
+ const activeSort = sort.value;
15
+ if (!enabled.value || !activeSort?.key || !activeSort.direction) return items;
16
+ const direction = activeSort.direction;
17
+ const caseSensitive = toValue(options.caseSensitive) ?? true;
18
+ return items.sort((a, b) => {
19
+ const aValue = options.getValue(a, activeSort.key);
20
+ const bValue = options.getValue(b, activeSort.key);
21
+ if (options.compare) return options.compare(aValue, bValue, {
22
+ key: activeSort.key,
23
+ direction,
24
+ a,
25
+ b
26
+ });
27
+ return compareSortValues(aValue, bValue, direction, { caseSensitive });
28
+ });
29
+ })
30
+ };
31
+ }
32
+ function compareSortValues(aValue, bValue, direction = "asc", options = {}) {
33
+ const aEmpty = aValue === null || aValue === void 0;
34
+ const bEmpty = bValue === null || bValue === void 0;
35
+ if (aEmpty && bEmpty) return 0;
36
+ if (aEmpty) return 1;
37
+ if (bEmpty) return -1;
38
+ const aComparable = toComparableSortValue(aValue, options);
39
+ const bComparable = toComparableSortValue(bValue, options);
40
+ let comparison = 0;
41
+ if (typeof aComparable === "number" && typeof bComparable === "number") comparison = aComparable - bComparable;
42
+ else comparison = String(aComparable).localeCompare(String(bComparable));
43
+ return direction === "asc" ? comparison : -comparison;
44
+ }
45
+ function toComparableSortValue(value, options) {
46
+ if (value instanceof Date) return value.getTime();
47
+ if (typeof value === "string" && options.caseSensitive === false) return value.toLowerCase();
48
+ return value;
49
+ }
50
+ //#endregion
51
+ //#region src/composables/useTextSearch.ts
52
+ /** Shared text-search filtering for tables, lists, selectors, and chip inputs. */
53
+ function useTextSearch(options) {
54
+ const query = computed(() => normalizeSearchQuery(toValue(options.query)));
55
+ const enabled = computed(() => toValue(options.enabled) ?? true);
56
+ function matches(item, overrideQuery) {
57
+ const activeQuery = overrideQuery === void 0 ? query.value : normalizeSearchQuery(overrideQuery);
58
+ if (!activeQuery) return true;
59
+ return candidateMatchesSearch(options.getText(item), activeQuery);
60
+ }
61
+ return {
62
+ query,
63
+ filteredItems: computed(() => {
64
+ const items = [...toValue(options.items)];
65
+ if (!enabled.value || !query.value) return items;
66
+ return items.filter((item) => matches(item));
67
+ }),
68
+ matches
69
+ };
70
+ }
71
+ function normalizeSearchQuery(value) {
72
+ return (value ?? "").trim().toLowerCase();
73
+ }
74
+ function candidateMatchesSearch(candidate, query) {
75
+ const normalizedQuery = normalizeSearchQuery(query);
76
+ if (!normalizedQuery) return true;
77
+ return (Array.isArray(candidate) ? candidate : [candidate]).some((value) => {
78
+ if (value === null || value === void 0) return false;
79
+ return String(value).toLowerCase().includes(normalizedQuery);
80
+ });
81
+ }
82
+ //#endregion
83
+ //#region src/composables/useToast.ts
84
+ var toasts = ref([]);
85
+ var nextId = 0;
86
+ /** Reactive global toast queue with success/error/warning/info variants and auto-dismiss timers. */
87
+ function useToast() {
88
+ function show(message, type = "success", duration = 3500) {
89
+ const id = nextId++;
90
+ toasts.value.push({
91
+ id,
92
+ message,
93
+ type,
94
+ duration
95
+ });
96
+ setTimeout(() => dismiss(id), duration);
97
+ }
98
+ function success(message, duration = 3500) {
99
+ show(message, "success", duration);
100
+ }
101
+ function error(message, duration = 5e3) {
102
+ show(message, "error", duration);
103
+ }
104
+ function warning(message, duration = 4e3) {
105
+ show(message, "warning", duration);
106
+ }
107
+ function info(message, duration = 3500) {
108
+ show(message, "info", duration);
109
+ }
110
+ function dismiss(id) {
111
+ toasts.value = toasts.value.filter((t) => t.id !== id);
112
+ }
113
+ function clear() {
114
+ toasts.value = [];
115
+ }
116
+ return {
117
+ toasts,
118
+ show,
119
+ success,
120
+ error,
121
+ warning,
122
+ info,
123
+ dismiss,
124
+ clear
125
+ };
126
+ }
127
+ //#endregion
128
+ //#region src/composables/useTheme.ts
129
+ /** Reads and toggles the active theme (light/dark/system) with reactivity to OS preference changes. */
130
+ function useTheme() {
131
+ const settings = useSettingsStore();
132
+ settings.initialize();
133
+ const isDark = computed(() => settings.isDark());
134
+ function toggleTheme() {
135
+ settings.theme = isDark.value ? "light" : "dark";
136
+ }
137
+ function setTheme(theme) {
138
+ settings.theme = theme;
139
+ }
140
+ return {
141
+ isDark,
142
+ toggleTheme,
143
+ setTheme
144
+ };
145
+ }
146
+ //#endregion
147
+ //#region src/composables/useForm.ts
148
+ var validators = {
149
+ required: (value, message = "This field is required") => {
150
+ if (value === null || value === void 0 || value === "") return message;
151
+ if (Array.isArray(value) && value.length === 0) return message;
152
+ return null;
153
+ },
154
+ minLength: (value, min, message) => {
155
+ if (typeof value !== "string") return null;
156
+ if (value.length < min) return message || `Must be at least ${min} characters`;
157
+ return null;
158
+ },
159
+ maxLength: (value, max, message) => {
160
+ if (typeof value !== "string") return null;
161
+ if (value.length > max) return message || `Must be at most ${max} characters`;
162
+ return null;
163
+ },
164
+ min: (value, min, message) => {
165
+ if (typeof value !== "number") return null;
166
+ if (value < min) return message || `Must be at least ${min}`;
167
+ return null;
168
+ },
169
+ max: (value, max, message) => {
170
+ if (typeof value !== "number") return null;
171
+ if (value > max) return message || `Must be at most ${max}`;
172
+ return null;
173
+ },
174
+ pattern: (value, pattern, message) => {
175
+ if (typeof value !== "string") return null;
176
+ if (!pattern.test(value)) return message || "Invalid format";
177
+ return null;
178
+ },
179
+ email: (value, message = "Invalid email address") => {
180
+ if (typeof value !== "string" || !value) return null;
181
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return message;
182
+ return null;
183
+ }
184
+ };
185
+ /**
186
+ * Form state management composable with validation.
187
+ *
188
+ * @param initialValues - Initial form values
189
+ * @param rules - Validation rules for each field
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(
194
+ * { email: '', password: '' },
195
+ * {
196
+ * email: { required: true, email: true },
197
+ * password: { required: true, minLength: 8 },
198
+ * }
199
+ * )
200
+ *
201
+ * // In template
202
+ * <BaseInput v-bind="getFieldProps('email')" label="Email" />
203
+ * <BaseInput v-bind="getFieldProps('password')" type="password" label="Password" />
204
+ * <BaseButton @click="handleSubmit(onSubmit)" :disabled="!isValid">Submit</BaseButton>
205
+ * ```
206
+ */
207
+ /** Reactive form state with field-level validation, dirty tracking, and submit handling. */
208
+ function useForm(initialValues, rules = {}) {
209
+ const cloneableInitialValues = deepToRaw(initialValues);
210
+ let _initialValues = structuredClone(cloneableInitialValues);
211
+ const data = reactive(structuredClone(cloneableInitialValues));
212
+ const errors = reactive(Object.keys(initialValues).reduce((acc, key) => {
213
+ acc[key] = null;
214
+ return acc;
215
+ }, {}));
216
+ const touched = reactive(Object.keys(initialValues).reduce((acc, key) => {
217
+ acc[key] = false;
218
+ return acc;
219
+ }, {}));
220
+ const dirty = reactive(Object.keys(initialValues).reduce((acc, key) => {
221
+ acc[key] = false;
222
+ return acc;
223
+ }, {}));
224
+ const isSubmitting = ref(false);
225
+ watch(() => ({ ...data }), (newData) => {
226
+ for (const key of Object.keys(newData)) dirty[key] = newData[key] !== _initialValues[key];
227
+ }, { deep: true });
228
+ function validateField(field) {
229
+ const value = data[field];
230
+ const fieldRules = rules[field];
231
+ if (!fieldRules) {
232
+ errors[field] = null;
233
+ return true;
234
+ }
235
+ if (fieldRules.required) {
236
+ const message = typeof fieldRules.required === "string" ? fieldRules.required : void 0;
237
+ const error = validators.required(value, message);
238
+ if (error) {
239
+ errors[field] = error;
240
+ return false;
241
+ }
242
+ }
243
+ if (value === null || value === void 0 || value === "") {
244
+ errors[field] = null;
245
+ return true;
246
+ }
247
+ if (fieldRules.minLength !== void 0) {
248
+ const config = typeof fieldRules.minLength === "number" ? {
249
+ value: fieldRules.minLength,
250
+ message: void 0
251
+ } : fieldRules.minLength;
252
+ const error = validators.minLength(value, config.value, config.message);
253
+ if (error) {
254
+ errors[field] = error;
255
+ return false;
256
+ }
257
+ }
258
+ if (fieldRules.maxLength !== void 0) {
259
+ const config = typeof fieldRules.maxLength === "number" ? {
260
+ value: fieldRules.maxLength,
261
+ message: void 0
262
+ } : fieldRules.maxLength;
263
+ const error = validators.maxLength(value, config.value, config.message);
264
+ if (error) {
265
+ errors[field] = error;
266
+ return false;
267
+ }
268
+ }
269
+ if (fieldRules.min !== void 0) {
270
+ const config = typeof fieldRules.min === "number" ? {
271
+ value: fieldRules.min,
272
+ message: void 0
273
+ } : fieldRules.min;
274
+ const error = validators.min(value, config.value, config.message);
275
+ if (error) {
276
+ errors[field] = error;
277
+ return false;
278
+ }
279
+ }
280
+ if (fieldRules.max !== void 0) {
281
+ const config = typeof fieldRules.max === "number" ? {
282
+ value: fieldRules.max,
283
+ message: void 0
284
+ } : fieldRules.max;
285
+ const error = validators.max(value, config.value, config.message);
286
+ if (error) {
287
+ errors[field] = error;
288
+ return false;
289
+ }
290
+ }
291
+ if (fieldRules.pattern !== void 0) {
292
+ const config = fieldRules.pattern instanceof RegExp ? {
293
+ value: fieldRules.pattern,
294
+ message: void 0
295
+ } : fieldRules.pattern;
296
+ const error = validators.pattern(value, config.value, config.message);
297
+ if (error) {
298
+ errors[field] = error;
299
+ return false;
300
+ }
301
+ }
302
+ if (fieldRules.email) {
303
+ const message = typeof fieldRules.email === "string" ? fieldRules.email : void 0;
304
+ const error = validators.email(value, message);
305
+ if (error) {
306
+ errors[field] = error;
307
+ return false;
308
+ }
309
+ }
310
+ if (fieldRules.custom) {
311
+ const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom];
312
+ for (const rule of customRules) {
313
+ const error = rule(value, data);
314
+ if (error) {
315
+ errors[field] = error;
316
+ return false;
317
+ }
318
+ }
319
+ }
320
+ errors[field] = null;
321
+ return true;
322
+ }
323
+ function validate() {
324
+ let isAllValid = true;
325
+ for (const field of Object.keys(data)) if (!validateField(field)) isAllValid = false;
326
+ return isAllValid;
327
+ }
328
+ const isValid = computed(() => {
329
+ return Object.values(errors).every((error) => error === null);
330
+ });
331
+ const isDirty = computed(() => {
332
+ return Object.values(dirty).some((d) => d);
333
+ });
334
+ function setFieldValue(field, value) {
335
+ data[field] = value;
336
+ if (touched[field]) validateField(field);
337
+ }
338
+ function setFieldError(field, error) {
339
+ errors[field] = error;
340
+ }
341
+ function setFieldTouched(field, isTouched = true) {
342
+ touched[field] = isTouched;
343
+ if (isTouched) validateField(field);
344
+ }
345
+ function reset(values) {
346
+ const resetValues = values ? {
347
+ ..._initialValues,
348
+ ...values
349
+ } : _initialValues;
350
+ for (const key of Object.keys(data)) {
351
+ data[key] = structuredClone(deepToRaw(resetValues[key]));
352
+ errors[key] = null;
353
+ touched[key] = false;
354
+ dirty[key] = false;
355
+ }
356
+ }
357
+ function replaceState(values) {
358
+ const nextValues = structuredClone(deepToRaw(values));
359
+ _initialValues = structuredClone(nextValues);
360
+ for (const key of Object.keys(data)) {
361
+ if (key in nextValues) continue;
362
+ delete data[key];
363
+ delete errors[key];
364
+ delete touched[key];
365
+ delete dirty[key];
366
+ }
367
+ for (const key of Object.keys(nextValues)) {
368
+ data[key] = structuredClone(deepToRaw(nextValues[key]));
369
+ errors[key] = null;
370
+ touched[key] = false;
371
+ dirty[key] = false;
372
+ }
373
+ }
374
+ function handleSubmit(onSubmit) {
375
+ return async (e) => {
376
+ e?.preventDefault();
377
+ for (const field of Object.keys(data)) touched[field] = true;
378
+ if (!validate()) return;
379
+ isSubmitting.value = true;
380
+ try {
381
+ await onSubmit(data);
382
+ } finally {
383
+ isSubmitting.value = false;
384
+ }
385
+ };
386
+ }
387
+ function getFieldProps(field) {
388
+ const fieldStr = field;
389
+ return {
390
+ modelValue: data[field],
391
+ "onUpdate:modelValue": (value) => setFieldValue(field, value),
392
+ onBlur: () => setFieldTouched(fieldStr),
393
+ error: touched[fieldStr] ? errors[fieldStr] : null
394
+ };
395
+ }
396
+ return {
397
+ data,
398
+ errors,
399
+ touched,
400
+ dirty,
401
+ isValid,
402
+ isDirty,
403
+ isSubmitting,
404
+ setFieldValue,
405
+ setFieldError,
406
+ setFieldTouched,
407
+ validateField,
408
+ validate,
409
+ reset,
410
+ replaceState,
411
+ handleSubmit,
412
+ getFieldProps
413
+ };
414
+ }
415
+ function deepToRaw(value) {
416
+ const raw = toRaw(value);
417
+ if (Array.isArray(raw)) return raw.map((item) => deepToRaw(item));
418
+ if (isPlainRecord(raw)) return Object.fromEntries(Object.entries(raw).map(([key, item]) => [key, deepToRaw(item)]));
419
+ return raw;
420
+ }
421
+ function isPlainRecord(value) {
422
+ return Object.prototype.toString.call(value) === "[object Object]";
423
+ }
424
+ //#endregion
425
+ //#region src/composables/useFormBuilder.ts
426
+ /**
427
+ * Evaluate a JSON-serializable field condition against the current form data.
428
+ *
429
+ * Supports logical operators (`and`, `or`, `not`) and comparison operators
430
+ * (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`,
431
+ * `contains`). Returns `true` if the condition passes.
432
+ */
433
+ function evaluateCondition(condition, data) {
434
+ if ("and" in condition) return condition.and.every((c) => evaluateCondition(c, data));
435
+ if ("or" in condition) return condition.or.some((c) => evaluateCondition(c, data));
436
+ if ("not" in condition) return !evaluateCondition(condition.not, data);
437
+ const value = data[condition.field];
438
+ if ("eq" in condition) return value === condition.eq;
439
+ if ("neq" in condition) return value !== condition.neq;
440
+ if ("gt" in condition) return typeof value === "number" && value > condition.gt;
441
+ if ("lt" in condition) return typeof value === "number" && value < condition.lt;
442
+ if ("gte" in condition) return typeof value === "number" && value >= condition.gte;
443
+ if ("lte" in condition) return typeof value === "number" && value <= condition.lte;
444
+ if ("in" in condition) return condition.in.includes(value);
445
+ if ("notIn" in condition) return !condition.notIn.includes(value);
446
+ if ("truthy" in condition) return !!value;
447
+ if ("falsy" in condition) return !value;
448
+ if ("contains" in condition) {
449
+ if (typeof value === "string") return value.includes(condition.contains);
450
+ if (Array.isArray(value)) return value.includes(condition.contains);
451
+ return false;
452
+ }
453
+ return true;
454
+ }
455
+ /** Return all sections across steps (wizard) or directly from a flat schema. */
456
+ function collectSections(schema) {
457
+ return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections;
458
+ }
459
+ /** Return all field schemas in schema order, flattening sections and steps. */
460
+ function flattenFields(schema) {
461
+ return collectSections(schema).flatMap((section) => section.fields);
462
+ }
463
+ /** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */
464
+ function convertValidation(v) {
465
+ const rules = {};
466
+ if (v.required !== void 0) rules.required = v.required;
467
+ if (v.minLength !== void 0) rules.minLength = v.minLength;
468
+ if (v.maxLength !== void 0) rules.maxLength = v.maxLength;
469
+ if (v.min !== void 0) rules.min = v.min;
470
+ if (v.max !== void 0) rules.max = v.max;
471
+ if (v.email !== void 0) rules.email = v.email;
472
+ if (v.pattern !== void 0) rules.pattern = typeof v.pattern === "string" ? new RegExp(v.pattern) : {
473
+ value: new RegExp(v.pattern.value),
474
+ message: v.pattern.message
475
+ };
476
+ return rules;
477
+ }
478
+ function buildInitialValues(fields, initialData) {
479
+ const initialValues = {};
480
+ for (const field of fields) {
481
+ const key = field.name;
482
+ if (initialData && hasOwnKey(initialData, key)) initialValues[key] = initialData[key];
483
+ else if (field.defaultValue !== void 0) initialValues[key] = field.defaultValue;
484
+ else initialValues[key] = getTypeDefault(field.type);
485
+ }
486
+ return initialValues;
487
+ }
488
+ function buildRules(fields, enhancements) {
489
+ const rules = {};
490
+ for (const field of fields) {
491
+ const base = field.validation ? convertValidation(field.validation) : {};
492
+ const enhancement = enhancements?.fields?.[field.name];
493
+ const customValidators = [];
494
+ if (enhancement?.validate) {
495
+ const fn = enhancement.validate;
496
+ customValidators.push((value, formData) => fn(value, formData));
497
+ }
498
+ if (customValidators.length > 0) base.custom = customValidators;
499
+ if (Object.keys(base).length > 0) rules[field.name] = base;
500
+ }
501
+ return rules;
502
+ }
503
+ function replaceArray(target, values) {
504
+ target.splice(0, target.length, ...values);
505
+ }
506
+ function replaceRecord(target, source) {
507
+ for (const key of Object.keys(target)) delete target[key];
508
+ Object.assign(target, source);
509
+ }
510
+ function hasOwnKey(source, key) {
511
+ return Object.prototype.hasOwnProperty.call(source, key);
512
+ }
513
+ /**
514
+ * Drive a `FormSchema` as reactive form state.
515
+ *
516
+ * Builds initial values from schema defaults and `initialData`, derives
517
+ * validation rules from `FieldValidation` descriptors and enhancement
518
+ * validators, evaluates `FieldCondition` expressions for field/section
519
+ * visibility, and wires wizard step navigation when `schema.steps` is set.
520
+ *
521
+ * @param schema - Declarative form or wizard schema.
522
+ * @param initialData - Values that override schema defaults.
523
+ * @param enhancements - TypeScript-only callbacks (dynamic options, validators,
524
+ * submit handler, transform, field-change watcher).
525
+ */
526
+ /** Drives a declarative FormSchema as reactive state with conditional fields, wizard steps, and validation. */
527
+ function useFormBuilder(schema, initialData, enhancements) {
528
+ const currentSchema = shallowRef(schema);
529
+ const fields = reactive(flattenFields(schema));
530
+ const sections = reactive(collectSections(schema));
531
+ const rules = reactive(buildRules(fields, enhancements));
532
+ const form = useForm(buildInitialValues(fields, initialData), rules);
533
+ function isFieldVisible(name) {
534
+ const enhancement = enhancements?.fields?.[name];
535
+ if (enhancement?.visible) return enhancement.visible(form.data);
536
+ const field = fields.find((f) => f.name === name);
537
+ if (field?.condition) return evaluateCondition(field.condition, form.data);
538
+ return true;
539
+ }
540
+ function isSectionVisible(id) {
541
+ const section = sections.find((s) => s.id === id);
542
+ if (section?.condition) return evaluateCondition(section.condition, form.data);
543
+ return true;
544
+ }
545
+ function getResolvedFieldProps(field) {
546
+ const entry = getFieldRegistryEntry(field.type);
547
+ const formProps = form.getFieldProps(field.name);
548
+ const merged = {
549
+ ...entry.defaults,
550
+ ...field.props ?? {}
551
+ };
552
+ const enhancement = enhancements?.fields?.[field.name];
553
+ if (enhancement?.props) Object.assign(merged, enhancement.props(form.data));
554
+ const options = getFieldOptions(field.name);
555
+ if (options) merged.options = options;
556
+ if (entry.vModel) {
557
+ merged.modelValue = formProps.modelValue;
558
+ merged["onUpdate:modelValue"] = formProps["onUpdate:modelValue"];
559
+ }
560
+ merged.onBlur = formProps.onBlur;
561
+ if (formProps.error) merged.error = true;
562
+ if (field.placeholder) merged.placeholder = field.placeholder;
563
+ if (field.size) merged.size = field.size;
564
+ if (field.disabled) merged.disabled = true;
565
+ if (field.readonly) merged.readonly = true;
566
+ if (field.type === "radio" && !merged.name) merged.name = field.name;
567
+ return merged;
568
+ }
569
+ function getFieldOptions(name) {
570
+ const enhancement = enhancements?.fields?.[name];
571
+ if (enhancement?.options) return enhancement.options(form.data);
572
+ const field = fields.find((f) => f.name === name);
573
+ if (field?.props?.options) return field.props.options;
574
+ }
575
+ const currentStep = ref(0);
576
+ const isCurrentStepValid = computed(() => {
577
+ if (!currentSchema.value.steps) return form.isValid.value;
578
+ const step = currentSchema.value.steps[currentStep.value];
579
+ if (!step) return true;
580
+ for (const section of step.sections) {
581
+ if (!isSectionVisible(section.id)) continue;
582
+ for (const field of section.fields) {
583
+ if (!isFieldVisible(field.name)) continue;
584
+ if (!form.validateField(field.name)) return false;
585
+ }
586
+ }
587
+ return true;
588
+ });
589
+ function goNext() {
590
+ if (!currentSchema.value.steps) return false;
591
+ const step = currentSchema.value.steps[currentStep.value];
592
+ if (!step) return false;
593
+ let valid = true;
594
+ for (const section of step.sections) {
595
+ if (!isSectionVisible(section.id)) continue;
596
+ for (const field of section.fields) {
597
+ if (!isFieldVisible(field.name)) continue;
598
+ form.setFieldTouched(field.name, true);
599
+ if (!form.validateField(field.name)) valid = false;
600
+ }
601
+ }
602
+ if (!valid) return false;
603
+ if (currentStep.value < currentSchema.value.steps.length - 1) currentStep.value++;
604
+ return true;
605
+ }
606
+ function goBack() {
607
+ if (currentStep.value > 0) currentStep.value--;
608
+ }
609
+ function goToStep(index) {
610
+ if (!currentSchema.value.steps) return;
611
+ if (index >= 0 && index < currentSchema.value.steps.length) currentStep.value = index;
612
+ }
613
+ function validate() {
614
+ let allValid = true;
615
+ for (const field of fields) {
616
+ if (!isFieldVisible(field.name)) continue;
617
+ form.setFieldTouched(field.name, true);
618
+ if (!form.validateField(field.name)) allValid = false;
619
+ }
620
+ return allValid;
621
+ }
622
+ async function submit() {
623
+ if (!validate()) return;
624
+ let submitData = {};
625
+ for (const field of fields) if (isFieldVisible(field.name)) submitData[field.name] = form.data[field.name];
626
+ if (enhancements?.transform) submitData = enhancements.transform(submitData);
627
+ if (enhancements?.onSubmit) {
628
+ form.isSubmitting.value = true;
629
+ try {
630
+ await enhancements.onSubmit(submitData);
631
+ } finally {
632
+ form.isSubmitting.value = false;
633
+ }
634
+ }
635
+ }
636
+ function reset(values) {
637
+ form.reset(values);
638
+ currentStep.value = 0;
639
+ }
640
+ function updateSchema(nextSchema, nextData) {
641
+ currentSchema.value = nextSchema;
642
+ const nextFields = flattenFields(nextSchema);
643
+ replaceArray(fields, nextFields);
644
+ replaceArray(sections, collectSections(nextSchema));
645
+ replaceRecord(rules, buildRules(nextFields, enhancements));
646
+ form.replaceState(buildInitialValues(nextFields, nextData));
647
+ currentStep.value = 0;
648
+ }
649
+ if (enhancements?.onFieldChange) {
650
+ const callback = enhancements.onFieldChange;
651
+ watch(() => ({ ...form.data }), (newData, oldData) => {
652
+ if (!oldData) return;
653
+ for (const key of Object.keys(newData)) if (newData[key] !== oldData[key]) callback(key, newData[key], newData);
654
+ }, { deep: true });
655
+ }
656
+ return {
657
+ form,
658
+ rules,
659
+ isFieldVisible,
660
+ isSectionVisible,
661
+ fields,
662
+ getResolvedFieldProps,
663
+ getFieldOptions,
664
+ currentStep,
665
+ isCurrentStepValid,
666
+ goNext,
667
+ goBack,
668
+ goToStep,
669
+ validate,
670
+ reset,
671
+ submit,
672
+ updateSchema
673
+ };
674
+ }
675
+ //#endregion
676
+ //#region src/composables/useApi.ts
677
+ var apiClientInstance = null;
678
+ var interceptorAttached = false;
679
+ function getApiClient() {
680
+ if (!apiClientInstance) apiClientInstance = axios.create({ headers: { "Content-Type": "application/json" } });
681
+ return apiClientInstance;
682
+ }
683
+ /** Axios-based API client that injects the plugin base URL and JWT auth header on every request. */
684
+ function useApi(options = {}) {
685
+ const settingsStore = useSettingsStore();
686
+ const authStore = useAuthStore();
687
+ const apiClient = getApiClient();
688
+ if (!authStore.isInitialized) authStore.initialize();
689
+ if (!interceptorAttached) {
690
+ apiClient.interceptors.request.use((config) => {
691
+ if (authStore.token && config.headers && !config.headers.Authorization) config.headers.Authorization = `Bearer ${authStore.token}`;
692
+ return config;
693
+ });
694
+ interceptorAttached = true;
695
+ }
696
+ function requestConfig(config) {
697
+ const base = {
698
+ baseURL: options.baseUrl ?? settingsStore.getApiBaseUrl(),
699
+ timeout: options.timeout ?? settingsStore.requestTimeout,
700
+ ...config
701
+ };
702
+ if (options.withAuth === false) base.headers = {
703
+ ...base.headers,
704
+ Authorization: void 0
705
+ };
706
+ return base;
707
+ }
708
+ async function get(url, config) {
709
+ return (await apiClient.get(url, requestConfig(config))).data;
710
+ }
711
+ async function post(url, data, config) {
712
+ return (await apiClient.post(url, data, requestConfig(config))).data;
713
+ }
714
+ async function put(url, data, config) {
715
+ return (await apiClient.put(url, data, requestConfig(config))).data;
716
+ }
717
+ async function patch(url, data, config) {
718
+ return (await apiClient.patch(url, data, requestConfig(config))).data;
719
+ }
720
+ async function del(url, config) {
721
+ return (await apiClient.delete(url, requestConfig(config))).data;
722
+ }
723
+ async function upload(url, file, fieldName = "file", additionalData) {
724
+ const formData = new FormData();
725
+ formData.append(fieldName, file);
726
+ if (additionalData) Object.entries(additionalData).forEach(([key, value]) => {
727
+ if (value !== void 0 && value !== null) formData.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
728
+ });
729
+ return (await apiClient.post(url, formData, requestConfig({ headers: { "Content-Type": void 0 } }))).data;
730
+ }
731
+ async function download(url, filename) {
732
+ const response = await apiClient.get(url, requestConfig({ responseType: "blob" }));
733
+ const blob = new Blob([response.data]);
734
+ const blobUrl = URL.createObjectURL(blob);
735
+ if (filename) {
736
+ const link = document.createElement("a");
737
+ link.href = blobUrl;
738
+ link.download = filename;
739
+ document.body.appendChild(link);
740
+ link.click();
741
+ document.body.removeChild(link);
742
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
743
+ return "";
744
+ }
745
+ return blobUrl;
746
+ }
747
+ function buildUrl(path) {
748
+ return `${options.baseUrl ?? settingsStore.getApiBaseUrl()}${path}`;
749
+ }
750
+ function buildWsUrl(path) {
751
+ return `${settingsStore.getWsBaseUrl()}${path}`;
752
+ }
753
+ return {
754
+ client: apiClient,
755
+ get,
756
+ post,
757
+ put,
758
+ patch,
759
+ delete: del,
760
+ upload,
761
+ download,
762
+ buildUrl,
763
+ buildWsUrl
764
+ };
765
+ }
766
+ //#endregion
767
+ //#region src/composables/useDebouncedWatch.ts
768
+ /** Watch a Vue source with debounced callback execution and explicit cancel/flush controls. */
769
+ function useDebouncedWatch(source, callback, options = {}) {
770
+ const { delay = 300, ...watchOptions } = options;
771
+ const isPending = ref(false);
772
+ let timer = null;
773
+ let hasLatestValue = false;
774
+ let latestValue;
775
+ let latestOldValue;
776
+ let callbackCleanup = null;
777
+ let stopped = false;
778
+ function clearTimer() {
779
+ if (timer) {
780
+ clearTimeout(timer);
781
+ timer = null;
782
+ }
783
+ isPending.value = false;
784
+ }
785
+ function runCallbackCleanup() {
786
+ if (!callbackCleanup) return;
787
+ callbackCleanup();
788
+ callbackCleanup = null;
789
+ }
790
+ function cancel() {
791
+ clearTimer();
792
+ hasLatestValue = false;
793
+ latestOldValue = void 0;
794
+ }
795
+ function flush() {
796
+ if (!hasLatestValue) return;
797
+ const value = latestValue;
798
+ const oldValue = latestOldValue;
799
+ cancel();
800
+ runCallbackCleanup();
801
+ callback(value, oldValue, (cleanup) => {
802
+ callbackCleanup = cleanup;
803
+ });
804
+ }
805
+ function schedule(value, oldValue) {
806
+ cancel();
807
+ hasLatestValue = true;
808
+ latestValue = value;
809
+ latestOldValue = oldValue;
810
+ isPending.value = true;
811
+ timer = setTimeout(flush, delay);
812
+ }
813
+ const stopWatch = watch(source, (value, oldValue, onCleanup) => {
814
+ schedule(value, oldValue);
815
+ onCleanup(() => {
816
+ cancel();
817
+ runCallbackCleanup();
818
+ });
819
+ }, watchOptions);
820
+ function stop() {
821
+ if (stopped) return;
822
+ stopped = true;
823
+ stopWatch();
824
+ cancel();
825
+ runCallbackCleanup();
826
+ }
827
+ onScopeDispose(stop);
828
+ return {
829
+ isPending,
830
+ cancel,
831
+ flush,
832
+ stop
833
+ };
834
+ }
835
+ //#endregion
836
+ //#region src/composables/useRequestSyncState.ts
837
+ /** Shared loading/error/timestamp state for generated plugin request helpers. */
838
+ function useRequestSyncState(defaultErrorMessage = "Request failed.") {
839
+ const loading = ref(false);
840
+ const error = ref(null);
841
+ const lastLoadedAt = ref(null);
842
+ const lastSavedAt = ref(null);
843
+ const lastRunAt = ref(null);
844
+ function clearError() {
845
+ error.value = null;
846
+ }
847
+ function readErrorMessage(value, fallback = defaultErrorMessage) {
848
+ if (value instanceof Error && value.message) return value.message;
849
+ if (typeof value === "string" && value.trim()) return value;
850
+ if (typeof value === "object" && value !== null && "message" in value && typeof value.message === "string" && value.message) return value.message;
851
+ return fallback;
852
+ }
853
+ function setError(value, fallback) {
854
+ const message = readErrorMessage(value, fallback);
855
+ error.value = message;
856
+ return message;
857
+ }
858
+ function markLoaded(date = /* @__PURE__ */ new Date()) {
859
+ lastLoadedAt.value = date;
860
+ }
861
+ function markSaved(date = /* @__PURE__ */ new Date()) {
862
+ lastSavedAt.value = date;
863
+ }
864
+ function markRun(date = /* @__PURE__ */ new Date()) {
865
+ lastRunAt.value = date;
866
+ }
867
+ async function run(operation, options = {}) {
868
+ loading.value = true;
869
+ clearError();
870
+ try {
871
+ const response = await operation();
872
+ if (options.success === "load") markLoaded();
873
+ else if (options.success === "save") markSaved();
874
+ else if (options.success === "run") markRun();
875
+ return response;
876
+ } catch (err) {
877
+ setError(err, options.errorMessage);
878
+ throw err;
879
+ } finally {
880
+ loading.value = false;
881
+ }
882
+ }
883
+ return {
884
+ loading,
885
+ error,
886
+ lastLoadedAt,
887
+ lastSavedAt,
888
+ lastRunAt,
889
+ clearError,
890
+ readErrorMessage,
891
+ setError,
892
+ markLoaded,
893
+ markSaved,
894
+ markRun,
895
+ run
896
+ };
897
+ }
898
+ //#endregion
899
+ //#region src/composables/experiment-utils.ts
900
+ function formatExperimentDate(dateStr) {
901
+ try {
902
+ return new Date(dateStr).toLocaleDateString(void 0, {
903
+ year: "numeric",
904
+ month: "short",
905
+ day: "numeric"
906
+ });
907
+ } catch {
908
+ return dateStr;
909
+ }
910
+ }
911
+ function datePresetToISO(preset) {
912
+ const now = /* @__PURE__ */ new Date();
913
+ const days = preset === "last_7_days" ? 7 : preset === "last_30_days" ? 30 : 90;
914
+ return (/* @__PURE__ */ new Date(now.getTime() - days * 864e5)).toISOString();
915
+ }
916
+ var EXPERIMENT_STATUS_OPTIONS = [
917
+ {
918
+ value: "",
919
+ label: "All statuses"
920
+ },
921
+ {
922
+ value: "planned",
923
+ label: "Planned"
924
+ },
925
+ {
926
+ value: "ongoing",
927
+ label: "Ongoing"
928
+ },
929
+ {
930
+ value: "completed",
931
+ label: "Completed"
932
+ },
933
+ {
934
+ value: "cancelled",
935
+ label: "Cancelled"
936
+ }
937
+ ];
938
+ var EXPERIMENT_STATUS_VARIANT_MAP = {
939
+ planned: "default",
940
+ ongoing: "primary",
941
+ completed: "success",
942
+ cancelled: "error"
943
+ };
944
+ var EXPERIMENT_STATUS_LABELS = {
945
+ planned: "Planned",
946
+ ongoing: "Ongoing",
947
+ completed: "Completed",
948
+ cancelled: "Cancelled"
949
+ };
950
+ var DATE_PRESET_OPTIONS = [
951
+ {
952
+ value: "",
953
+ label: "Any time"
954
+ },
955
+ {
956
+ value: "last_7_days",
957
+ label: "Last 7 days"
958
+ },
959
+ {
960
+ value: "last_30_days",
961
+ label: "Last 30 days"
962
+ },
963
+ {
964
+ value: "last_90_days",
965
+ label: "Last 90 days"
966
+ }
967
+ ];
968
+ var SORT_OPTIONS = [
969
+ {
970
+ value: "created_at:desc",
971
+ label: "Newest first"
972
+ },
973
+ {
974
+ value: "created_at:asc",
975
+ label: "Oldest first"
976
+ },
977
+ {
978
+ value: "updated_at:desc",
979
+ label: "Recently updated"
980
+ },
981
+ {
982
+ value: "name:asc",
983
+ label: "Name A–Z"
984
+ },
985
+ {
986
+ value: "name:desc",
987
+ label: "Name Z–A"
988
+ }
989
+ ];
990
+ //#endregion
991
+ //#region src/composables/useExperimentSelector.ts
992
+ function getPlatformContext() {
993
+ if (typeof window === "undefined") return void 0;
994
+ return window.__MINT_PLATFORM__;
995
+ }
996
+ function getPlatformApiUrl() {
997
+ return getPlatformContext()?.platformApiUrl;
998
+ }
999
+ /** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */
1000
+ function useExperimentSelector(options = {}) {
1001
+ const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options;
1002
+ const platformBase = apiBaseUrl ?? getPlatformApiUrl();
1003
+ const api = useApi();
1004
+ const experiments = ref([]);
1005
+ const total = ref(0);
1006
+ const selectedExperiment = ref(null);
1007
+ const request = useRequestSyncState("Failed to fetch experiments");
1008
+ const isLoading = request.loading;
1009
+ const error = request.error;
1010
+ const lastLoadedAt = request.lastLoadedAt;
1011
+ const page = ref(0);
1012
+ const sortKey = ref("created_at:desc");
1013
+ const experimentTypes = ref([]);
1014
+ const projects = ref([]);
1015
+ let filterOptionsFetched = false;
1016
+ const hasMore = computed(() => experiments.value.length < total.value);
1017
+ const filters = reactive({
1018
+ search: void 0,
1019
+ status: void 0,
1020
+ project: void 0,
1021
+ experimentType: void 0,
1022
+ datePreset: void 0
1023
+ });
1024
+ function parseSortKey() {
1025
+ const [field, order] = sortKey.value.split(":");
1026
+ return {
1027
+ sortBy: field || "created_at",
1028
+ sortOrder: order || "desc"
1029
+ };
1030
+ }
1031
+ async function fetchExperiments() {
1032
+ try {
1033
+ await request.run(async () => {
1034
+ const params = new URLSearchParams();
1035
+ const allowedTypes = getPlatformContext()?.allowedExperimentTypes;
1036
+ const effectiveType = experimentType ?? (allowedTypes?.length === 1 ? allowedTypes[0] : void 0) ?? filters.experimentType ?? void 0;
1037
+ if (effectiveType) params.set("experiment_type", effectiveType);
1038
+ if (filters.status) params.set("status", filters.status);
1039
+ if (filters.search) params.set("search", filters.search);
1040
+ if (filters.project) params.set("project", filters.project);
1041
+ const { sortBy, sortOrder } = parseSortKey();
1042
+ params.set("sort_by", sortBy);
1043
+ params.set("sort_order", sortOrder);
1044
+ if (filters.datePreset) params.set("created_after", datePresetToISO(filters.datePreset));
1045
+ params.set("limit", String(limit));
1046
+ params.set("skip", String(page.value * limit));
1047
+ const query = params.toString();
1048
+ const url = `${platformBase ?? ""}/experiments${query ? `?${query}` : ""}`;
1049
+ const data = await api.get(url);
1050
+ let filtered = data.experiments;
1051
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
1052
+ const typeSet = new Set(allowedTypes);
1053
+ filtered = filtered.filter((e) => typeSet.has(e.experiment_type));
1054
+ }
1055
+ if (page.value === 0) experiments.value = filtered;
1056
+ else experiments.value = [...experiments.value, ...filtered];
1057
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) if (data.experiments.length < limit) total.value = experiments.value.length;
1058
+ else total.value = experiments.value.length + 1;
1059
+ else total.value = data.total;
1060
+ }, {
1061
+ success: "load",
1062
+ errorMessage: "Failed to fetch experiments"
1063
+ });
1064
+ } catch {
1065
+ if (page.value === 0) {
1066
+ experiments.value = [];
1067
+ total.value = 0;
1068
+ }
1069
+ }
1070
+ }
1071
+ async function fetchFilterOptions() {
1072
+ if (filterOptionsFetched) return;
1073
+ filterOptionsFetched = true;
1074
+ const base = platformBase ?? "";
1075
+ const [typesRes, projectsRes] = await Promise.allSettled([api.get(`${base}/experiments/experiment-types`), api.get(`${base}/projects`)]);
1076
+ if (typesRes.status === "fulfilled" && Array.isArray(typesRes.value)) experimentTypes.value = typesRes.value.map((t) => ({
1077
+ value: t.value,
1078
+ label: t.label,
1079
+ color: t.color
1080
+ }));
1081
+ if (projectsRes.status === "fulfilled" && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) projects.value = projectsRes.value.projects.map((p) => ({
1082
+ value: p.name,
1083
+ label: p.name
1084
+ }));
1085
+ }
1086
+ async function loadMore() {
1087
+ if (!hasMore.value || isLoading.value) return;
1088
+ page.value++;
1089
+ await fetchExperiments();
1090
+ }
1091
+ function reset() {
1092
+ page.value = 0;
1093
+ experiments.value = [];
1094
+ total.value = 0;
1095
+ fetchExperiments();
1096
+ }
1097
+ function select(experiment) {
1098
+ selectedExperiment.value = experiment;
1099
+ }
1100
+ function clear() {
1101
+ selectedExperiment.value = null;
1102
+ filters.search = void 0;
1103
+ filters.status = void 0;
1104
+ filters.project = void 0;
1105
+ filters.experimentType = void 0;
1106
+ filters.datePreset = void 0;
1107
+ sortKey.value = "created_at:desc";
1108
+ page.value = 0;
1109
+ request.clearError();
1110
+ }
1111
+ const groupedByProject = computed(() => {
1112
+ const groups = /* @__PURE__ */ new Map();
1113
+ for (const exp of experiments.value) {
1114
+ const key = exp.project_name ?? exp.project ?? "No project";
1115
+ const list = groups.get(key);
1116
+ if (list) list.push(exp);
1117
+ else groups.set(key, [exp]);
1118
+ }
1119
+ return [...groups.entries()].sort(([a], [b]) => {
1120
+ if (a === "No project") return 1;
1121
+ if (b === "No project") return -1;
1122
+ return a.localeCompare(b);
1123
+ });
1124
+ });
1125
+ const searchWatch = useDebouncedWatch(() => filters.search, () => {
1126
+ page.value = 0;
1127
+ fetchExperiments();
1128
+ }, { delay: 300 });
1129
+ watch(() => [
1130
+ filters.status,
1131
+ filters.project,
1132
+ filters.experimentType,
1133
+ filters.datePreset,
1134
+ sortKey.value
1135
+ ], () => {
1136
+ searchWatch.cancel();
1137
+ page.value = 0;
1138
+ fetchExperiments();
1139
+ });
1140
+ if (immediate) fetchExperiments();
1141
+ return {
1142
+ experiments,
1143
+ total,
1144
+ selectedExperiment,
1145
+ filters,
1146
+ isLoading,
1147
+ error,
1148
+ lastLoadedAt,
1149
+ page,
1150
+ hasMore,
1151
+ sortKey,
1152
+ experimentTypes,
1153
+ projects,
1154
+ groupedByProject,
1155
+ fetch: fetchExperiments,
1156
+ loadMore,
1157
+ reset,
1158
+ select,
1159
+ clear,
1160
+ fetchFilterOptions
1161
+ };
1162
+ }
1163
+ //#endregion
1164
+ //#region src/composables/usePlatformContext.ts
1165
+ var DEFAULT_CONTEXT = {
1166
+ isIntegrated: false,
1167
+ theme: "system"
1168
+ };
1169
+ var platformContext = ref({ ...DEFAULT_CONTEXT });
1170
+ var inferredOrigins = /* @__PURE__ */ new Set();
1171
+ var allowedOrigins = /* @__PURE__ */ new Set();
1172
+ var allowAnyOrigin = false;
1173
+ var initialized = false;
1174
+ var listenerCount = 0;
1175
+ var nextConsumerId = 0;
1176
+ var currentHandler = null;
1177
+ var consumerOrigins = /* @__PURE__ */ new Map();
1178
+ var allowAnyOriginConsumers = /* @__PURE__ */ new Set();
1179
+ /**
1180
+ * Derive origin from URL (protocol + host)
1181
+ */
1182
+ function getOriginFromUrl(url) {
1183
+ try {
1184
+ return new URL(url).origin;
1185
+ } catch {
1186
+ return null;
1187
+ }
1188
+ }
1189
+ function normalizeAllowedOrigins(origins) {
1190
+ const normalized = /* @__PURE__ */ new Set();
1191
+ if (!origins) return normalized;
1192
+ for (const origin of origins) normalized.add(getOriginFromUrl(origin) || origin);
1193
+ return normalized;
1194
+ }
1195
+ function recomputeOriginPolicy() {
1196
+ allowedOrigins = new Set(inferredOrigins);
1197
+ for (const origins of consumerOrigins.values()) for (const origin of origins) allowedOrigins.add(origin);
1198
+ allowAnyOrigin = allowAnyOriginConsumers.size > 0;
1199
+ }
1200
+ function resetPlatformContextState() {
1201
+ inferredOrigins = /* @__PURE__ */ new Set();
1202
+ allowedOrigins = /* @__PURE__ */ new Set();
1203
+ allowAnyOrigin = false;
1204
+ initialized = false;
1205
+ listenerCount = 0;
1206
+ currentHandler = null;
1207
+ consumerOrigins.clear();
1208
+ allowAnyOriginConsumers.clear();
1209
+ platformContext.value = { ...DEFAULT_CONTEXT };
1210
+ }
1211
+ /**
1212
+ * Check if an origin is allowed for postMessage communication
1213
+ */
1214
+ function isOriginAllowed(origin) {
1215
+ if (allowAnyOrigin) {
1216
+ console.warn("[MINT SDK] postMessage origin validation disabled - only use in development");
1217
+ return true;
1218
+ }
1219
+ if (origin === window.location.origin) return true;
1220
+ return allowedOrigins.has(origin);
1221
+ }
1222
+ /**
1223
+ * Platform context composable for plugin integration with MINT Platform.
1224
+ *
1225
+ * Provides secure communication with the parent platform via postMessage.
1226
+ *
1227
+ * @param options - Configuration options
1228
+ * @param options.allowedOrigins - List of allowed origins for postMessage
1229
+ * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)
1230
+ *
1231
+ * @example
1232
+ * ```typescript
1233
+ * // Basic usage - derives origin from platform injection
1234
+ * const { isIntegrated, user, theme } = usePlatformContext()
1235
+ *
1236
+ * // With explicit allowed origins
1237
+ * const { isIntegrated } = usePlatformContext({
1238
+ * allowedOrigins: ['https://mint.example.com']
1239
+ * })
1240
+ *
1241
+ * // Development mode (UNSAFE)
1242
+ * const { isIntegrated } = usePlatformContext({
1243
+ * allowAnyOrigin: import.meta.env.DEV
1244
+ * })
1245
+ * ```
1246
+ */
1247
+ /** Connects a plugin to the MINT platform via postMessage, exposing user, theme, and experiment context. */
1248
+ function usePlatformContext(options = {}) {
1249
+ const consumerId = ++nextConsumerId;
1250
+ const instanceOrigins = normalizeAllowedOrigins(options.allowedOrigins);
1251
+ const instanceAllowAnyOrigin = options.allowAnyOrigin === true;
1252
+ function detectPlatform() {
1253
+ const detectedOrigins = /* @__PURE__ */ new Set();
1254
+ const platformData = window.__MINT_PLATFORM__;
1255
+ if (platformData) {
1256
+ platformContext.value = {
1257
+ ...platformData,
1258
+ isIntegrated: true
1259
+ };
1260
+ if (platformData.platformOrigin) detectedOrigins.add(platformData.platformOrigin);
1261
+ else if (platformData.platformApiUrl) {
1262
+ const origin = getOriginFromUrl(platformData.platformApiUrl);
1263
+ if (origin) detectedOrigins.add(origin);
1264
+ }
1265
+ } else {
1266
+ const urlParams = new URLSearchParams(window.location.search);
1267
+ const hasPluginParam = urlParams.has("mint-plugin");
1268
+ const platformOrigin = urlParams.get("mint-origin");
1269
+ if (platformOrigin) {
1270
+ const origin = getOriginFromUrl(platformOrigin);
1271
+ if (origin) detectedOrigins.add(origin);
1272
+ }
1273
+ platformContext.value = {
1274
+ isIntegrated: hasPluginParam,
1275
+ theme: (() => {
1276
+ try {
1277
+ const s = localStorage.getItem("mint-settings");
1278
+ if (s) return JSON.parse(s).theme || "system";
1279
+ } catch {}
1280
+ return "system";
1281
+ })(),
1282
+ platformOrigin: platformOrigin || void 0
1283
+ };
1284
+ }
1285
+ inferredOrigins = detectedOrigins;
1286
+ }
1287
+ function handlePlatformMessage(event) {
1288
+ if (event.source !== window.parent) return;
1289
+ if (!isOriginAllowed(event.origin)) {
1290
+ console.warn(`[MINT SDK] Rejected postMessage from untrusted origin: ${event.origin}`);
1291
+ return;
1292
+ }
1293
+ try {
1294
+ const platformEvent = event.data;
1295
+ if (!platformEvent.type?.startsWith("mint:")) return;
1296
+ switch (platformEvent.type) {
1297
+ case "mint:theme-changed":
1298
+ platformContext.value.theme = platformEvent.payload;
1299
+ break;
1300
+ case "mint:user-changed":
1301
+ platformContext.value.user = platformEvent.payload;
1302
+ break;
1303
+ }
1304
+ } catch {}
1305
+ }
1306
+ /**
1307
+ * Send a message to the parent platform.
1308
+ * Uses validated target origin for security.
1309
+ */
1310
+ function sendToPlatform(event) {
1311
+ if (!platformContext.value.isIntegrated || window.parent === window) return;
1312
+ let targetOrigin;
1313
+ if (platformContext.value.platformOrigin) targetOrigin = platformContext.value.platformOrigin;
1314
+ else if (allowedOrigins.size > 0) targetOrigin = allowedOrigins.values().next().value;
1315
+ else if (allowAnyOrigin) {
1316
+ targetOrigin = "*";
1317
+ console.warn("[MINT SDK] Using wildcard origin for postMessage - only use in development");
1318
+ } else {
1319
+ console.warn("[MINT SDK] Cannot send postMessage: no platform origin configured");
1320
+ return;
1321
+ }
1322
+ window.parent.postMessage(event, targetOrigin);
1323
+ }
1324
+ /**
1325
+ * Request navigation to a path in the platform.
1326
+ */
1327
+ function navigate(path) {
1328
+ sendToPlatform({
1329
+ type: "mint:navigate",
1330
+ payload: path
1331
+ });
1332
+ }
1333
+ /**
1334
+ * Show a notification in the platform.
1335
+ */
1336
+ function notify(message, type = "info") {
1337
+ sendToPlatform({
1338
+ type: "mint:notification",
1339
+ payload: {
1340
+ message,
1341
+ type
1342
+ }
1343
+ });
1344
+ }
1345
+ onMounted(() => {
1346
+ consumerOrigins.set(consumerId, instanceOrigins);
1347
+ if (instanceAllowAnyOrigin) allowAnyOriginConsumers.add(consumerId);
1348
+ if (!initialized) {
1349
+ detectPlatform();
1350
+ currentHandler = handlePlatformMessage;
1351
+ window.addEventListener("message", handlePlatformMessage);
1352
+ initialized = true;
1353
+ }
1354
+ listenerCount++;
1355
+ recomputeOriginPolicy();
1356
+ });
1357
+ onUnmounted(() => {
1358
+ consumerOrigins.delete(consumerId);
1359
+ allowAnyOriginConsumers.delete(consumerId);
1360
+ listenerCount = Math.max(0, listenerCount - 1);
1361
+ if (listenerCount === 0) {
1362
+ if (currentHandler) window.removeEventListener("message", currentHandler);
1363
+ resetPlatformContextState();
1364
+ return;
1365
+ }
1366
+ recomputeOriginPolicy();
1367
+ });
1368
+ return {
1369
+ context: platformContext,
1370
+ isIntegrated: computed(() => platformContext.value.isIntegrated),
1371
+ plugin: computed(() => platformContext.value.plugin),
1372
+ user: computed(() => platformContext.value.user),
1373
+ theme: computed(() => platformContext.value.theme),
1374
+ features: computed(() => platformContext.value.features),
1375
+ navigate,
1376
+ notify,
1377
+ sendToPlatform
1378
+ };
1379
+ }
1380
+ //#endregion
1381
+ //#region src/composables/useAppExperiment.ts
1382
+ var APP_EXPERIMENT_KEY = Symbol("app-experiment");
1383
+ /** Manages the active experiment selection, save flow, and detach action for a plugin's app shell. */
1384
+ function useAppExperiment(options = {}) {
1385
+ const experimentName = ref();
1386
+ const experimentCode = ref();
1387
+ const experimentStatus = ref();
1388
+ const experimentId = ref(null);
1389
+ const showModal = ref(false);
1390
+ const saveLoading = ref(false);
1391
+ const saveSuccessMessage = ref();
1392
+ let successTimer = null;
1393
+ const showSave = ref(!!options.onSave);
1394
+ const showDetach = computed(() => experimentId.value !== null);
1395
+ const saveDisabled = computed(() => toValue(options.saveDisabled) ?? false);
1396
+ const saveDisabledMessage = computed(() => toValue(options.saveDisabledMessage));
1397
+ function set(experiment) {
1398
+ experimentId.value = experiment.id;
1399
+ experimentName.value = experiment.name;
1400
+ experimentCode.value = experiment.experiment_code ?? (experiment.id != null ? `EXP-${experiment.id}` : void 0);
1401
+ experimentStatus.value = experiment.status;
1402
+ }
1403
+ function clear() {
1404
+ experimentId.value = null;
1405
+ experimentName.value = void 0;
1406
+ experimentCode.value = void 0;
1407
+ experimentStatus.value = void 0;
1408
+ }
1409
+ function openModal() {
1410
+ showModal.value = true;
1411
+ }
1412
+ function closeModal() {
1413
+ showModal.value = false;
1414
+ }
1415
+ function handleSelect(experiment) {
1416
+ set(experiment);
1417
+ showModal.value = false;
1418
+ options.onSelect?.(experiment);
1419
+ }
1420
+ async function handleSave() {
1421
+ if (!options.onSave || saveLoading.value) return;
1422
+ saveLoading.value = true;
1423
+ try {
1424
+ const message = await options.onSave();
1425
+ if (message) {
1426
+ saveSuccessMessage.value = message;
1427
+ if (successTimer) clearTimeout(successTimer);
1428
+ successTimer = setTimeout(() => {
1429
+ saveSuccessMessage.value = void 0;
1430
+ successTimer = null;
1431
+ }, 3e3);
1432
+ }
1433
+ } finally {
1434
+ saveLoading.value = false;
1435
+ }
1436
+ }
1437
+ function handleDetach() {
1438
+ clear();
1439
+ options.onDetach?.();
1440
+ }
1441
+ onScopeDispose(() => {
1442
+ if (successTimer) clearTimeout(successTimer);
1443
+ });
1444
+ provide(APP_EXPERIMENT_KEY, {
1445
+ experimentName,
1446
+ experimentCode,
1447
+ experimentStatus,
1448
+ experimentId,
1449
+ showModal,
1450
+ saveLoading,
1451
+ saveSuccessMessage,
1452
+ showSave,
1453
+ showDetach,
1454
+ saveDisabled,
1455
+ saveDisabledMessage,
1456
+ openModal,
1457
+ closeModal,
1458
+ handleSelect,
1459
+ handleSave,
1460
+ handleDetach
1461
+ });
1462
+ return {
1463
+ set,
1464
+ clear,
1465
+ experimentName: readonly(experimentName),
1466
+ experimentCode: readonly(experimentCode),
1467
+ experimentId: readonly(experimentId)
1468
+ };
1469
+ }
1470
+ //#endregion
1471
+ //#region src/composables/useDoseCalculator.ts
1472
+ var VOLUME_UNITS = [
1473
+ "µL",
1474
+ "mL",
1475
+ "L"
1476
+ ];
1477
+ var VOLUME_FACTORS = {
1478
+ "µL": 1e-6,
1479
+ "mL": .001,
1480
+ "L": 1
1481
+ };
1482
+ /** Calculates single dilutions, serial dilution series, and plate-filling volumes for dose-response experiments. */
1483
+ function useDoseCalculator() {
1484
+ const { convert } = useConcentrationUnits();
1485
+ function convertVolume(value, from, to) {
1486
+ if (from === to) return value;
1487
+ return value * VOLUME_FACTORS[from] / VOLUME_FACTORS[to];
1488
+ }
1489
+ function formatVolume(volume, precision = 3) {
1490
+ const { value, unit } = volume;
1491
+ if (value === 0) return `0 ${unit}`;
1492
+ let formattedValue;
1493
+ if (Math.abs(value) >= 1e3) formattedValue = value.toExponential(precision - 1);
1494
+ else if (Math.abs(value) < .001) formattedValue = value.toExponential(precision - 1);
1495
+ else formattedValue = value.toPrecision(precision);
1496
+ formattedValue = formattedValue.replace(/\.?0+$/, "");
1497
+ formattedValue = formattedValue.replace(/\.?0+e/, "e");
1498
+ return `${formattedValue} ${unit}`;
1499
+ }
1500
+ function calculateDilution(params) {
1501
+ const { stockConcentration, finalConcentration, finalVolume } = params;
1502
+ if (stockConcentration.value <= 0) return {
1503
+ stockVolume: {
1504
+ value: 0,
1505
+ unit: "µL"
1506
+ },
1507
+ diluentVolume: {
1508
+ value: 0,
1509
+ unit: "µL"
1510
+ },
1511
+ dilutionFactor: 0,
1512
+ valid: false,
1513
+ error: "Stock concentration must be positive"
1514
+ };
1515
+ if (finalConcentration.value <= 0) return {
1516
+ stockVolume: {
1517
+ value: 0,
1518
+ unit: "µL"
1519
+ },
1520
+ diluentVolume: {
1521
+ value: 0,
1522
+ unit: "µL"
1523
+ },
1524
+ dilutionFactor: 0,
1525
+ valid: false,
1526
+ error: "Final concentration must be positive"
1527
+ };
1528
+ if (finalVolume.value <= 0) return {
1529
+ stockVolume: {
1530
+ value: 0,
1531
+ unit: "µL"
1532
+ },
1533
+ diluentVolume: {
1534
+ value: 0,
1535
+ unit: "µL"
1536
+ },
1537
+ dilutionFactor: 0,
1538
+ valid: false,
1539
+ error: "Final volume must be positive"
1540
+ };
1541
+ const convertedFinal = convert(finalConcentration.value, finalConcentration.unit, stockConcentration.unit);
1542
+ if (convertedFinal === null) return {
1543
+ stockVolume: {
1544
+ value: 0,
1545
+ unit: "µL"
1546
+ },
1547
+ diluentVolume: {
1548
+ value: 0,
1549
+ unit: "µL"
1550
+ },
1551
+ dilutionFactor: 0,
1552
+ valid: false,
1553
+ error: "Cannot convert between concentration units. Provide molecular weight for mass↔molarity conversion."
1554
+ };
1555
+ if (convertedFinal >= stockConcentration.value) return {
1556
+ stockVolume: {
1557
+ value: 0,
1558
+ unit: "µL"
1559
+ },
1560
+ diluentVolume: {
1561
+ value: 0,
1562
+ unit: "µL"
1563
+ },
1564
+ dilutionFactor: 0,
1565
+ valid: false,
1566
+ error: "Stock concentration must be higher than final concentration"
1567
+ };
1568
+ const dilutionFactor = stockConcentration.value / convertedFinal;
1569
+ const finalVolumeInL = convertVolume(finalVolume.value, finalVolume.unit, "L");
1570
+ const stockVolumeInL = finalVolumeInL / dilutionFactor;
1571
+ const diluentVolumeInL = finalVolumeInL - stockVolumeInL;
1572
+ let outputUnit = "µL";
1573
+ if (stockVolumeInL >= .001) outputUnit = "mL";
1574
+ if (stockVolumeInL >= 1) outputUnit = "L";
1575
+ return {
1576
+ stockVolume: {
1577
+ value: convertVolume(stockVolumeInL, "L", outputUnit),
1578
+ unit: outputUnit
1579
+ },
1580
+ diluentVolume: {
1581
+ value: convertVolume(diluentVolumeInL, "L", outputUnit),
1582
+ unit: outputUnit
1583
+ },
1584
+ dilutionFactor,
1585
+ valid: true
1586
+ };
1587
+ }
1588
+ function calculateSerialDilution(params) {
1589
+ const { startingConcentration, dilutionFactor, numberOfDilutions, volumePerWell } = params;
1590
+ if (startingConcentration.value <= 0) return {
1591
+ steps: [],
1592
+ totalStockVolume: {
1593
+ value: 0,
1594
+ unit: "µL"
1595
+ },
1596
+ valid: false,
1597
+ error: "Starting concentration must be positive"
1598
+ };
1599
+ if (dilutionFactor <= 1) return {
1600
+ steps: [],
1601
+ totalStockVolume: {
1602
+ value: 0,
1603
+ unit: "µL"
1604
+ },
1605
+ valid: false,
1606
+ error: "Dilution factor must be greater than 1"
1607
+ };
1608
+ if (numberOfDilutions < 1) return {
1609
+ steps: [],
1610
+ totalStockVolume: {
1611
+ value: 0,
1612
+ unit: "µL"
1613
+ },
1614
+ valid: false,
1615
+ error: "Number of dilutions must be at least 1"
1616
+ };
1617
+ if (volumePerWell.value <= 0) return {
1618
+ steps: [],
1619
+ totalStockVolume: {
1620
+ value: 0,
1621
+ unit: "µL"
1622
+ },
1623
+ valid: false,
1624
+ error: "Volume per well must be positive"
1625
+ };
1626
+ const steps = [];
1627
+ let currentConcentration = startingConcentration.value;
1628
+ const transferVolume = volumePerWell.value / (dilutionFactor - 1);
1629
+ const diluentVolume = volumePerWell.value - transferVolume;
1630
+ for (let i = 0; i < numberOfDilutions; i++) {
1631
+ steps.push({
1632
+ stepNumber: i + 1,
1633
+ concentration: {
1634
+ value: currentConcentration,
1635
+ unit: startingConcentration.unit
1636
+ },
1637
+ transferVolume: {
1638
+ value: i === 0 ? volumePerWell.value : transferVolume,
1639
+ unit: volumePerWell.unit
1640
+ },
1641
+ diluentVolume: {
1642
+ value: i === 0 ? 0 : diluentVolume,
1643
+ unit: volumePerWell.unit
1644
+ }
1645
+ });
1646
+ currentConcentration /= dilutionFactor;
1647
+ }
1648
+ return {
1649
+ steps,
1650
+ totalStockVolume: {
1651
+ value: volumePerWell.value + transferVolume * (numberOfDilutions - 1),
1652
+ unit: volumePerWell.unit
1653
+ },
1654
+ valid: true
1655
+ };
1656
+ }
1657
+ function convertMassToMolar(mass, massUnit, mw) {
1658
+ const converted = convert(mass, massUnit, "µM", mw);
1659
+ if (converted !== null) return {
1660
+ value: converted,
1661
+ unit: "µM"
1662
+ };
1663
+ return {
1664
+ value: mass * getMassVolumeFactor(massUnit) / mw * 1e6,
1665
+ unit: "µM"
1666
+ };
1667
+ }
1668
+ function convertMolarToMass(molar, molarUnit, mw) {
1669
+ const converted = convert(molar, molarUnit, "µg/mL", mw);
1670
+ if (converted !== null) return {
1671
+ value: converted,
1672
+ unit: "µg/mL"
1673
+ };
1674
+ return {
1675
+ value: molar * getMolarityFactor(molarUnit) * mw * 1e6,
1676
+ unit: "µg/mL"
1677
+ };
1678
+ }
1679
+ function getMassVolumeFactor(unit) {
1680
+ return {
1681
+ "pg/mL": 1e-12,
1682
+ "ng/mL": 1e-9,
1683
+ "µg/mL": 1e-6,
1684
+ "mg/mL": .001,
1685
+ "g/mL": 1
1686
+ }[unit];
1687
+ }
1688
+ function getMolarityFactor(unit) {
1689
+ return {
1690
+ "pM": 1e-12,
1691
+ "nM": 1e-9,
1692
+ "µM": 1e-6,
1693
+ "mM": .001,
1694
+ "M": 1
1695
+ }[unit];
1696
+ }
1697
+ function generateWellConcentrations(result, wellIds) {
1698
+ if (!result.valid || result.steps.length === 0) return [];
1699
+ return result.steps.slice(0, wellIds.length).map((step, index) => ({
1700
+ wellId: wellIds[index],
1701
+ concentration: step.concentration,
1702
+ volume: step.transferVolume
1703
+ }));
1704
+ }
1705
+ return {
1706
+ volumeUnits: VOLUME_UNITS,
1707
+ calculateDilution,
1708
+ calculateSerialDilution,
1709
+ convertMassToMolar,
1710
+ convertMolarToMass,
1711
+ convertVolume,
1712
+ formatVolume,
1713
+ generateWellConcentrations
1714
+ };
1715
+ }
1716
+ //#endregion
1717
+ //#region src/composables/useWellPlateEditor.ts
1718
+ var DEFAULT_PALETTE = [
1719
+ "#3B82F6",
1720
+ "#10B981",
1721
+ "#EF4444",
1722
+ "#F59E0B",
1723
+ "#8B5CF6",
1724
+ "#F97316",
1725
+ "#06B6D4",
1726
+ "#14B8A6",
1727
+ "#6B7280"
1728
+ ];
1729
+ var MAX_HISTORY = 50;
1730
+ function createEmptyPlate(name, format, id) {
1731
+ return {
1732
+ id: id || `plate-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
1733
+ name,
1734
+ format,
1735
+ wells: {}
1736
+ };
1737
+ }
1738
+ function generateSampleId() {
1739
+ return `sample-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1740
+ }
1741
+ function cloneValue(value) {
1742
+ return structuredClone(toRaw(value));
1743
+ }
1744
+ /** Manages multi-plate well-plate state with sample assignment, selection, and undo/redo history. */
1745
+ function useWellPlateEditor(initialState, options = {}) {
1746
+ const { maxHistory = MAX_HISTORY, defaultFormat = 96 } = options;
1747
+ const defaultPlate = createEmptyPlate("Plate 1", defaultFormat);
1748
+ const internalState = ref({
1749
+ plates: initialState?.plates || [defaultPlate],
1750
+ activePlateId: initialState?.activePlateId || defaultPlate.id,
1751
+ samples: initialState?.samples || [],
1752
+ selectedWells: initialState?.selectedWells || [],
1753
+ activeSampleId: initialState?.activeSampleId
1754
+ });
1755
+ const history = ref([]);
1756
+ const historyIndex = ref(-1);
1757
+ const state = computed(() => internalState.value);
1758
+ const plates = computed(() => internalState.value.plates);
1759
+ const activePlate = computed(() => internalState.value.plates.find((p) => p.id === internalState.value.activePlateId));
1760
+ const samples = computed(() => internalState.value.samples);
1761
+ const selectedWells = computed(() => internalState.value.selectedWells);
1762
+ const activeSampleId = computed(() => internalState.value.activeSampleId);
1763
+ const canUndo = computed(() => historyIndex.value >= 0);
1764
+ const canRedo = computed(() => historyIndex.value < history.value.length - 1);
1765
+ function saveToHistory() {
1766
+ const entry = {
1767
+ plates: cloneValue(internalState.value.plates),
1768
+ samples: cloneValue(internalState.value.samples)
1769
+ };
1770
+ if (historyIndex.value < history.value.length - 1) history.value = history.value.slice(0, historyIndex.value + 1);
1771
+ history.value.push(entry);
1772
+ if (history.value.length > maxHistory) history.value.shift();
1773
+ else historyIndex.value++;
1774
+ }
1775
+ function setActivePlate(plateId) {
1776
+ if (internalState.value.plates.some((p) => p.id === plateId)) {
1777
+ internalState.value.activePlateId = plateId;
1778
+ internalState.value.selectedWells = [];
1779
+ }
1780
+ }
1781
+ function setActiveSample(sampleId) {
1782
+ internalState.value.activeSampleId = sampleId;
1783
+ }
1784
+ function setSelectedWells(wellIds) {
1785
+ internalState.value.selectedWells = wellIds;
1786
+ }
1787
+ function addPlate(name, format) {
1788
+ saveToHistory();
1789
+ const plateNumber = internalState.value.plates.length + 1;
1790
+ const plate = createEmptyPlate(name || `Plate ${plateNumber}`, format || defaultFormat);
1791
+ internalState.value.plates.push(plate);
1792
+ internalState.value.activePlateId = plate.id;
1793
+ internalState.value.selectedWells = [];
1794
+ return plate;
1795
+ }
1796
+ function removePlate(plateId) {
1797
+ if (internalState.value.plates.length <= 1) return;
1798
+ saveToHistory();
1799
+ const index = internalState.value.plates.findIndex((p) => p.id === plateId);
1800
+ if (index === -1) return;
1801
+ internalState.value.plates.splice(index, 1);
1802
+ if (internalState.value.activePlateId === plateId) {
1803
+ internalState.value.activePlateId = internalState.value.plates[0].id;
1804
+ internalState.value.selectedWells = [];
1805
+ }
1806
+ }
1807
+ function addSample(name, color) {
1808
+ saveToHistory();
1809
+ const sample = {
1810
+ id: generateSampleId(),
1811
+ name,
1812
+ color: color || DEFAULT_PALETTE[internalState.value.samples.length % DEFAULT_PALETTE.length],
1813
+ count: 0
1814
+ };
1815
+ internalState.value.samples.push(sample);
1816
+ return sample;
1817
+ }
1818
+ function removeSample(sampleId) {
1819
+ saveToHistory();
1820
+ const index = internalState.value.samples.findIndex((s) => s.id === sampleId);
1821
+ if (index === -1) return;
1822
+ internalState.value.samples.splice(index, 1);
1823
+ for (const plate of internalState.value.plates) for (const well of Object.values(plate.wells)) if (well.sampleType === sampleId) {
1824
+ delete well.sampleType;
1825
+ well.state = "empty";
1826
+ }
1827
+ if (internalState.value.activeSampleId === sampleId) internalState.value.activeSampleId = void 0;
1828
+ updateSampleCounts();
1829
+ }
1830
+ function assignSample(wellIds, sampleId) {
1831
+ if (wellIds.length === 0) return;
1832
+ saveToHistory();
1833
+ const plate = activePlate.value;
1834
+ if (!plate) return;
1835
+ for (const wellId of wellIds) {
1836
+ const well = plate.wells[wellId] || {
1837
+ id: wellId,
1838
+ row: wellId.charCodeAt(0) - 65,
1839
+ col: parseInt(wellId.slice(1)) - 1,
1840
+ state: "empty"
1841
+ };
1842
+ if (sampleId) {
1843
+ well.sampleType = sampleId;
1844
+ well.state = "filled";
1845
+ } else {
1846
+ delete well.sampleType;
1847
+ well.state = "empty";
1848
+ }
1849
+ plate.wells[wellId] = well;
1850
+ }
1851
+ updateSampleCounts();
1852
+ }
1853
+ function clearWells(wellIds) {
1854
+ assignSample(wellIds, void 0);
1855
+ }
1856
+ function updateSampleCounts() {
1857
+ const counts = {};
1858
+ for (const plate of internalState.value.plates) for (const well of Object.values(plate.wells)) if (well.sampleType) counts[well.sampleType] = (counts[well.sampleType] || 0) + 1;
1859
+ for (const sample of internalState.value.samples) sample.count = counts[sample.id] || 0;
1860
+ }
1861
+ function undo() {
1862
+ if (!canUndo.value) return;
1863
+ const entry = history.value[historyIndex.value];
1864
+ historyIndex.value--;
1865
+ internalState.value.plates = cloneValue(entry.plates);
1866
+ internalState.value.samples = cloneValue(entry.samples);
1867
+ if (!internalState.value.plates.some((p) => p.id === internalState.value.activePlateId)) internalState.value.activePlateId = internalState.value.plates[0]?.id || "";
1868
+ internalState.value.selectedWells = [];
1869
+ }
1870
+ function redo() {
1871
+ if (!canRedo.value) return;
1872
+ historyIndex.value++;
1873
+ const entry = history.value[historyIndex.value];
1874
+ internalState.value.plates = cloneValue(entry.plates);
1875
+ internalState.value.samples = cloneValue(entry.samples);
1876
+ internalState.value.selectedWells = [];
1877
+ }
1878
+ function exportData(format) {
1879
+ if (format === "json") return JSON.stringify({
1880
+ plates: internalState.value.plates,
1881
+ samples: internalState.value.samples
1882
+ }, null, 2);
1883
+ const sampleMap = new Map(internalState.value.samples.map((s) => [s.id, s.name]));
1884
+ const rows = ["Plate,Well,Sample Type,Sample Name"];
1885
+ for (const plate of internalState.value.plates) for (const [wellId, well] of Object.entries(plate.wells)) if (well.sampleType) {
1886
+ const sampleName = sampleMap.get(well.sampleType) || "";
1887
+ rows.push(`"${plate.name}","${wellId}","${well.sampleType}","${sampleName}"`);
1888
+ }
1889
+ return rows.join("\n");
1890
+ }
1891
+ function importData(data, format) {
1892
+ try {
1893
+ saveToHistory();
1894
+ if (format === "json") {
1895
+ const parsed = JSON.parse(data);
1896
+ if (parsed.plates && Array.isArray(parsed.plates)) {
1897
+ internalState.value.plates = parsed.plates;
1898
+ internalState.value.activePlateId = parsed.plates[0]?.id || "";
1899
+ }
1900
+ if (parsed.samples && Array.isArray(parsed.samples)) internalState.value.samples = parsed.samples;
1901
+ internalState.value.selectedWells = [];
1902
+ updateSampleCounts();
1903
+ return true;
1904
+ }
1905
+ const lines = data.trim().split("\n");
1906
+ if (lines.length < 2) return false;
1907
+ const plateMap = /* @__PURE__ */ new Map();
1908
+ const sampleMap = /* @__PURE__ */ new Map();
1909
+ for (let i = 1; i < lines.length; i++) {
1910
+ const parts = lines[i].match(/(?:[^",]+|"[^"]*")+/g);
1911
+ if (!parts || parts.length < 4) continue;
1912
+ const plateName = parts[0].replace(/^"|"$/g, "");
1913
+ const wellId = parts[1].replace(/^"|"$/g, "");
1914
+ const sampleId = parts[2].replace(/^"|"$/g, "");
1915
+ const sampleName = parts[3].replace(/^"|"$/g, "");
1916
+ if (!plateMap.has(plateName)) plateMap.set(plateName, createEmptyPlate(plateName, defaultFormat));
1917
+ if (sampleId && !sampleMap.has(sampleId)) sampleMap.set(sampleId, {
1918
+ id: sampleId,
1919
+ name: sampleName || sampleId,
1920
+ color: DEFAULT_PALETTE[sampleMap.size % DEFAULT_PALETTE.length],
1921
+ count: 0
1922
+ });
1923
+ const plate = plateMap.get(plateName);
1924
+ plate.wells[wellId] = {
1925
+ id: wellId,
1926
+ row: wellId.charCodeAt(0) - 65,
1927
+ col: parseInt(wellId.slice(1)) - 1,
1928
+ state: sampleId ? "filled" : "empty",
1929
+ sampleType: sampleId || void 0
1930
+ };
1931
+ }
1932
+ internalState.value.plates = Array.from(plateMap.values());
1933
+ internalState.value.samples = Array.from(sampleMap.values());
1934
+ internalState.value.activePlateId = internalState.value.plates[0]?.id || "";
1935
+ internalState.value.selectedWells = [];
1936
+ updateSampleCounts();
1937
+ return true;
1938
+ } catch {
1939
+ return false;
1940
+ }
1941
+ }
1942
+ function loadState(state) {
1943
+ saveToHistory();
1944
+ if (state.plates && state.plates.length > 0) {
1945
+ internalState.value.plates = cloneValue(state.plates);
1946
+ internalState.value.activePlateId = state.activePlateId ?? state.plates[0].id;
1947
+ }
1948
+ if (state.samples) internalState.value.samples = cloneValue(state.samples);
1949
+ internalState.value.selectedWells = state.selectedWells ?? [];
1950
+ internalState.value.activeSampleId = state.activeSampleId;
1951
+ updateSampleCounts();
1952
+ }
1953
+ function reset() {
1954
+ const plate = createEmptyPlate("Plate 1", defaultFormat);
1955
+ internalState.value = {
1956
+ plates: [plate],
1957
+ activePlateId: plate.id,
1958
+ samples: [],
1959
+ selectedWells: [],
1960
+ activeSampleId: void 0
1961
+ };
1962
+ history.value = [];
1963
+ historyIndex.value = -1;
1964
+ }
1965
+ return {
1966
+ state,
1967
+ plates,
1968
+ activePlate,
1969
+ samples,
1970
+ selectedWells,
1971
+ activeSampleId,
1972
+ canUndo,
1973
+ canRedo,
1974
+ setActivePlate,
1975
+ setActiveSample,
1976
+ setSelectedWells,
1977
+ addPlate,
1978
+ removePlate,
1979
+ addSample,
1980
+ removeSample,
1981
+ assignSample,
1982
+ clearWells,
1983
+ undo,
1984
+ redo,
1985
+ exportData,
1986
+ importData,
1987
+ loadState,
1988
+ reset
1989
+ };
1990
+ }
1991
+ //#endregion
1992
+ //#region src/composables/useAutoGroup.ts
1993
+ var DEFAULT_COLORS = [
1994
+ "#3B82F6",
1995
+ "#10B981",
1996
+ "#F59E0B",
1997
+ "#EF4444",
1998
+ "#8B5CF6",
1999
+ "#EC4899",
2000
+ "#06B6D4",
2001
+ "#84CC16",
2002
+ "#F97316",
2003
+ "#6366F1"
2004
+ ];
2005
+ var DELIMITER_CANDIDATES = [
2006
+ "_",
2007
+ "-",
2008
+ "."
2009
+ ];
2010
+ function analyzeDelimiter(lines) {
2011
+ if (lines.length === 0) return {
2012
+ delimiter: "_",
2013
+ dominantFieldCount: 1,
2014
+ minFieldCount: 1,
2015
+ consistency: 0
2016
+ };
2017
+ let bestDelimiter = "_";
2018
+ let bestConsistency = -1;
2019
+ let bestFieldCount = 1;
2020
+ for (const candidate of DELIMITER_CANDIDATES) {
2021
+ const fieldCounts = lines.map((line) => line.split(candidate).length);
2022
+ const countFrequency = /* @__PURE__ */ new Map();
2023
+ for (const count of fieldCounts) countFrequency.set(count, (countFrequency.get(count) ?? 0) + 1);
2024
+ let modeCount = 1;
2025
+ let modeFrequency = 0;
2026
+ for (const [count, freq] of countFrequency) if (freq > modeFrequency || freq === modeFrequency && count > modeCount) {
2027
+ modeCount = count;
2028
+ modeFrequency = freq;
2029
+ }
2030
+ const rawConsistency = modeFrequency / lines.length;
2031
+ const consistency = modeCount > 1 ? rawConsistency : 0;
2032
+ if (consistency > bestConsistency || consistency === bestConsistency && DELIMITER_CANDIDATES.indexOf(candidate) < DELIMITER_CANDIDATES.indexOf(bestDelimiter)) {
2033
+ bestDelimiter = candidate;
2034
+ bestConsistency = consistency;
2035
+ bestFieldCount = modeCount;
2036
+ }
2037
+ }
2038
+ const multiFieldCounts = lines.map((line) => line.split(bestDelimiter).length).filter((c) => c >= 2);
2039
+ const minFieldCount = multiFieldCounts.length > 0 ? Math.min(...multiFieldCounts) : 1;
2040
+ return {
2041
+ delimiter: bestDelimiter,
2042
+ dominantFieldCount: bestFieldCount,
2043
+ minFieldCount,
2044
+ consistency: bestConsistency
2045
+ };
2046
+ }
2047
+ function detectOutliers(lines, delimiter, minFieldCount) {
2048
+ const outliers = [];
2049
+ for (let i = 0; i < lines.length; i++) {
2050
+ const fieldCount = lines[i].split(delimiter).length;
2051
+ if (fieldCount < minFieldCount) outliers.push({
2052
+ sample: lines[i],
2053
+ index: i,
2054
+ fieldCount,
2055
+ action: "include"
2056
+ });
2057
+ }
2058
+ return outliers;
2059
+ }
2060
+ var QC_KEYWORDS = new Set([
2061
+ "eqc",
2062
+ "iqc",
2063
+ "qc",
2064
+ "blank",
2065
+ "std",
2066
+ "standard",
2067
+ "test"
2068
+ ]);
2069
+ function classifyOutlierAction(sample, delimiter) {
2070
+ return sample.split(delimiter).some((seg) => QC_KEYWORDS.has(seg.toLowerCase())) ? "qc" : "include";
2071
+ }
2072
+ function isUsefulField(field, rowCount) {
2073
+ return field.cardinality > 1 && !(rowCount > 1 && field.cardinality === rowCount);
2074
+ }
2075
+ function extractColumns(samples, delimiter, minFieldCount) {
2076
+ if (samples.length === 0) return [];
2077
+ const suffixCount = minFieldCount - 1;
2078
+ const rows = samples.map((s) => {
2079
+ const parts = s.split(delimiter);
2080
+ const splitAt = parts.length - suffixCount;
2081
+ return [parts.slice(0, splitAt).join(delimiter), ...parts.slice(splitAt)];
2082
+ });
2083
+ const columnCount = minFieldCount;
2084
+ const columns = [];
2085
+ for (let col = 0; col < columnCount; col++) {
2086
+ const values = rows.map((row) => row[col]);
2087
+ const unique = [...new Set(values)];
2088
+ columns.push({
2089
+ index: col,
2090
+ name: col === 0 ? "Condition" : `Field ${col + 1}`,
2091
+ uniqueValues: unique,
2092
+ cardinality: unique.length,
2093
+ type: col === 0 ? "prefix" : "suffix"
2094
+ });
2095
+ }
2096
+ return columns;
2097
+ }
2098
+ function parseCSVLine(line, delimiter = ",") {
2099
+ const result = [];
2100
+ let current = "";
2101
+ let inQuotes = false;
2102
+ for (let i = 0; i < line.length; i++) {
2103
+ const char = line[i];
2104
+ if (char === "\"") inQuotes = !inQuotes;
2105
+ else if (char === delimiter && !inQuotes) {
2106
+ result.push(current.trim());
2107
+ current = "";
2108
+ } else current += char;
2109
+ }
2110
+ result.push(current.trim());
2111
+ return result;
2112
+ }
2113
+ function parseCSV(text) {
2114
+ const lines = text.trim().split("\n");
2115
+ if (lines.length < 2) throw new Error("CSV must have at least a header and one data row");
2116
+ const firstLine = lines[0];
2117
+ const csvDelimiter = firstLine.includes(" ") ? " " : ",";
2118
+ const headers = parseCSVLine(firstLine, csvDelimiter);
2119
+ const rows = [];
2120
+ for (let i = 1; i < lines.length; i++) {
2121
+ const values = parseCSVLine(lines[i], csvDelimiter);
2122
+ if (values.length !== headers.length) continue;
2123
+ const row = {};
2124
+ headers.forEach((header, idx) => {
2125
+ row[header] = values[idx];
2126
+ });
2127
+ rows.push(row);
2128
+ }
2129
+ const sampleKeywords = [
2130
+ "sample",
2131
+ "name",
2132
+ "id",
2133
+ "sample_name",
2134
+ "samplename",
2135
+ "file name",
2136
+ "filename",
2137
+ "file_name"
2138
+ ];
2139
+ return {
2140
+ columns: headers,
2141
+ rows,
2142
+ sampleColumn: headers.find((h) => sampleKeywords.includes(h.toLowerCase())) ?? headers[0],
2143
+ delimiter: csvDelimiter
2144
+ };
2145
+ }
2146
+ function computeGroups(allSamples, columns, enabledFields, outlierActions, delimiter, minFieldCount) {
2147
+ const excludedSamples = [];
2148
+ const qcSamples = [];
2149
+ const conformingSamples = [];
2150
+ for (let i = 0; i < allSamples.length; i++) {
2151
+ const action = outlierActions.get(i);
2152
+ if (action === "exclude") excludedSamples.push(allSamples[i]);
2153
+ else if (action === "qc") qcSamples.push(allSamples[i]);
2154
+ else conformingSamples.push(allSamples[i]);
2155
+ }
2156
+ const groupMap = /* @__PURE__ */ new Map();
2157
+ const metadata = [];
2158
+ const enabledIndices = [...enabledFields].sort((a, b) => a - b);
2159
+ const suffixCount = minFieldCount - 1;
2160
+ for (const sample of conformingSamples) {
2161
+ const parts = sample.split(delimiter);
2162
+ const splitAt = Math.max(1, parts.length - suffixCount);
2163
+ const row = [parts.slice(0, splitAt).join(delimiter), ...parts.slice(splitAt)];
2164
+ const keyParts = [];
2165
+ for (const idx of enabledIndices) if (idx < row.length && idx < columns.length) keyParts.push(row[idx]);
2166
+ const groupKey = keyParts.join(" / ");
2167
+ if (!groupMap.has(groupKey)) groupMap.set(groupKey, []);
2168
+ groupMap.get(groupKey).push(sample);
2169
+ const fields = {};
2170
+ for (const col of columns) if (col.index < row.length) fields[col.name] = row[col.index];
2171
+ metadata.push({
2172
+ sampleName: sample,
2173
+ fields,
2174
+ group: groupKey
2175
+ });
2176
+ }
2177
+ const groups = [];
2178
+ let colorIdx = 0;
2179
+ for (const [name, samples] of groupMap) {
2180
+ groups.push({
2181
+ name,
2182
+ color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
2183
+ samples
2184
+ });
2185
+ colorIdx++;
2186
+ }
2187
+ if (qcSamples.length > 0) {
2188
+ groups.push({
2189
+ name: "QC",
2190
+ color: "#6B7280",
2191
+ samples: qcSamples
2192
+ });
2193
+ for (const sample of qcSamples) metadata.push({
2194
+ sampleName: sample,
2195
+ fields: {},
2196
+ group: "QC"
2197
+ });
2198
+ }
2199
+ return {
2200
+ groups,
2201
+ metadata,
2202
+ excludedSamples
2203
+ };
2204
+ }
2205
+ /**
2206
+ * Extract sample metadata from raw design_data into ParsedCsvData format.
2207
+ *
2208
+ * Looks for a `samples` array in the design data. For each sample, merges
2209
+ * the `conditions` dict (the metadata table) with the `sample_name` to
2210
+ * produce a flat tabular row. QC and blank samples are filtered out by
2211
+ * their explicit `sample_type` field.
2212
+ *
2213
+ * Returns null if no samples with conditions are found.
2214
+ */
2215
+ function extractSamplesFromDesignData(rawData) {
2216
+ const samples = rawData.samples;
2217
+ if (!Array.isArray(samples) || samples.length === 0) return null;
2218
+ const allConditionKeys = [];
2219
+ const keySet = /* @__PURE__ */ new Set();
2220
+ const filteredSamples = [];
2221
+ for (const sample of samples) {
2222
+ const sampleType = String(sample.sample_type ?? "sample").toLowerCase();
2223
+ if (sampleType === "qc" || sampleType === "blank") continue;
2224
+ filteredSamples.push(sample);
2225
+ const conditions = sample.conditions;
2226
+ if (conditions && typeof conditions === "object") {
2227
+ for (const key of Object.keys(conditions)) if (!keySet.has(key)) {
2228
+ keySet.add(key);
2229
+ allConditionKeys.push(key);
2230
+ }
2231
+ }
2232
+ }
2233
+ if (filteredSamples.length === 0 || allConditionKeys.length === 0) return null;
2234
+ return {
2235
+ columns: ["sample_name", ...allConditionKeys],
2236
+ rows: filteredSamples.map((sample) => {
2237
+ const conditions = sample.conditions ?? {};
2238
+ const row = { sample_name: String(sample.sample_name ?? "") };
2239
+ for (const key of allConditionKeys) row[key] = conditions[key] ?? "";
2240
+ return row;
2241
+ }),
2242
+ sampleColumn: "sample_name",
2243
+ delimiter: ","
2244
+ };
2245
+ }
2246
+ function computeGroupsFromCsv(csvData, columns, enabledFields) {
2247
+ const groupMap = /* @__PURE__ */ new Map();
2248
+ const metadata = [];
2249
+ const enabledCols = columns.filter((c) => enabledFields.has(c.index)).sort((a, b) => a.index - b.index);
2250
+ for (const row of csvData.rows) {
2251
+ const sampleName = row[csvData.sampleColumn];
2252
+ const groupKey = enabledCols.map((col) => row[col.originalName ?? col.name]).join(" / ");
2253
+ if (!groupMap.has(groupKey)) groupMap.set(groupKey, []);
2254
+ groupMap.get(groupKey).push(sampleName);
2255
+ const fields = {};
2256
+ for (const col of columns) fields[col.name] = row[col.originalName ?? col.name];
2257
+ metadata.push({
2258
+ sampleName,
2259
+ fields,
2260
+ group: groupKey
2261
+ });
2262
+ }
2263
+ const groups = [];
2264
+ let colorIdx = 0;
2265
+ for (const [name, samples] of groupMap) {
2266
+ groups.push({
2267
+ name,
2268
+ color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
2269
+ samples
2270
+ });
2271
+ colorIdx++;
2272
+ }
2273
+ return {
2274
+ groups,
2275
+ metadata,
2276
+ excludedSamples: []
2277
+ };
2278
+ }
2279
+ /** Parses sample names or CSV data to propose group assignments with outlier detection and preview. */
2280
+ function useAutoGroup() {
2281
+ const inputMode = ref("paste");
2282
+ const rawText = ref("");
2283
+ const csvData = ref(null);
2284
+ const delimiter = ref("_");
2285
+ const dominantFieldCount = ref(1);
2286
+ const minFieldCount = ref(1);
2287
+ const outliers = ref([]);
2288
+ const fields = ref([]);
2289
+ const fieldNames = ref({});
2290
+ const enabledFields = ref(/* @__PURE__ */ new Set());
2291
+ const isTabularMode = computed(() => (inputMode.value === "csv" || inputMode.value === "experiment") && csvData.value !== null);
2292
+ const samples = computed(() => {
2293
+ if (isTabularMode.value && csvData.value) return csvData.value.rows.map((r) => r[csvData.value.sampleColumn]);
2294
+ return rawText.value.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
2295
+ });
2296
+ const hasOutliers = computed(() => outliers.value.length > 0);
2297
+ const conformingSamples = computed(() => {
2298
+ const outlierIndices = new Set(outliers.value.map((o) => o.index));
2299
+ return samples.value.filter((_, i) => !outlierIndices.has(i));
2300
+ });
2301
+ const outlierActions = computed(() => {
2302
+ const map = /* @__PURE__ */ new Map();
2303
+ for (const o of outliers.value) map.set(o.index, o.action);
2304
+ return map;
2305
+ });
2306
+ const effectiveColumns = computed(() => {
2307
+ return fields.value.map((col) => ({
2308
+ ...col,
2309
+ name: fieldNames.value[col.index] ?? col.name
2310
+ }));
2311
+ });
2312
+ const _computedResult = computed(() => {
2313
+ if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) return {
2314
+ groups: [],
2315
+ metadata: [],
2316
+ excludedSamples: []
2317
+ };
2318
+ if (isTabularMode.value && csvData.value) return computeGroupsFromCsv(csvData.value, effectiveColumns.value, enabledFields.value);
2319
+ return computeGroups(samples.value, effectiveColumns.value, enabledFields.value, outlierActions.value, delimiter.value, minFieldCount.value);
2320
+ });
2321
+ const groups = computed(() => _computedResult.value.groups);
2322
+ const metadata = computed(() => _computedResult.value.metadata);
2323
+ const excludedSamples = computed(() => _computedResult.value.excludedSamples);
2324
+ const allSingletons = computed(() => groups.value.length > 1 && groups.value.every((g) => g.samples.length === 1));
2325
+ const result = _computedResult;
2326
+ function parseInput() {
2327
+ if (isTabularMode.value) parseCsvInput();
2328
+ else parsePasteInput();
2329
+ }
2330
+ function parsePasteInput() {
2331
+ const lines = samples.value;
2332
+ if (lines.length === 0) return;
2333
+ const analysis = analyzeDelimiter(lines);
2334
+ delimiter.value = analysis.delimiter;
2335
+ dominantFieldCount.value = analysis.dominantFieldCount;
2336
+ outliers.value = detectOutliers(lines, analysis.delimiter, analysis.dominantFieldCount);
2337
+ for (const outlier of outliers.value) outlier.action = classifyOutlierAction(outlier.sample, analysis.delimiter);
2338
+ const conforming = lines.filter((_, i) => !outliers.value.some((o) => o.index === i));
2339
+ const conformingFieldCounts = conforming.map((s) => s.split(analysis.delimiter).length);
2340
+ minFieldCount.value = conformingFieldCounts.length > 0 ? Math.min(...conformingFieldCounts) : analysis.dominantFieldCount;
2341
+ fields.value = extractColumns(conforming, analysis.delimiter, minFieldCount.value);
2342
+ fieldNames.value = {};
2343
+ const rowCount = conforming.length;
2344
+ enabledFields.value = new Set(fields.value.filter((f) => isUsefulField(f, rowCount)).map((f) => f.index));
2345
+ }
2346
+ function parseCsvInput() {
2347
+ if (!csvData.value) return;
2348
+ const csv = csvData.value;
2349
+ fields.value = csv.columns.filter((c) => c !== csv.sampleColumn).map((col, i) => {
2350
+ const values = csv.rows.map((r) => r[col]);
2351
+ const unique = [...new Set(values)];
2352
+ return {
2353
+ index: i,
2354
+ name: col,
2355
+ originalName: col,
2356
+ uniqueValues: unique,
2357
+ cardinality: unique.length
2358
+ };
2359
+ });
2360
+ outliers.value = [];
2361
+ delimiter.value = csv.delimiter;
2362
+ dominantFieldCount.value = csv.columns.length;
2363
+ fieldNames.value = Object.fromEntries(fields.value.map((f) => [f.index, f.name]));
2364
+ const rowCount = csv.rows.length;
2365
+ enabledFields.value = new Set(fields.value.filter((f) => isUsefulField(f, rowCount)).map((f) => f.index));
2366
+ }
2367
+ function setOutlierAction(index, action) {
2368
+ const outlier = outliers.value.find((o) => o.index === index);
2369
+ if (outlier) {
2370
+ outlier.action = action;
2371
+ outliers.value = [...outliers.value];
2372
+ }
2373
+ }
2374
+ function setAllOutlierActions(action) {
2375
+ for (const outlier of outliers.value) outlier.action = action;
2376
+ outliers.value = [...outliers.value];
2377
+ }
2378
+ function toggleField(index) {
2379
+ const newSet = new Set(enabledFields.value);
2380
+ if (newSet.has(index)) newSet.delete(index);
2381
+ else newSet.add(index);
2382
+ enabledFields.value = newSet;
2383
+ }
2384
+ function renameField(index, name) {
2385
+ fieldNames.value = {
2386
+ ...fieldNames.value,
2387
+ [index]: name
2388
+ };
2389
+ }
2390
+ function loadExperimentData(rawData) {
2391
+ const parsed = extractSamplesFromDesignData(rawData);
2392
+ if (!parsed) return false;
2393
+ inputMode.value = "experiment";
2394
+ csvData.value = parsed;
2395
+ parseCsvInput();
2396
+ return true;
2397
+ }
2398
+ function reset() {
2399
+ rawText.value = "";
2400
+ csvData.value = null;
2401
+ delimiter.value = "_";
2402
+ dominantFieldCount.value = 1;
2403
+ minFieldCount.value = 1;
2404
+ outliers.value = [];
2405
+ fields.value = [];
2406
+ fieldNames.value = {};
2407
+ enabledFields.value = /* @__PURE__ */ new Set();
2408
+ }
2409
+ return {
2410
+ inputMode,
2411
+ rawText,
2412
+ csvData,
2413
+ delimiter,
2414
+ dominantFieldCount,
2415
+ minFieldCount,
2416
+ outliers,
2417
+ fields,
2418
+ fieldNames,
2419
+ enabledFields,
2420
+ samples,
2421
+ hasOutliers,
2422
+ conformingSamples,
2423
+ groups,
2424
+ metadata,
2425
+ excludedSamples,
2426
+ allSingletons,
2427
+ result,
2428
+ effectiveColumns,
2429
+ parseInput,
2430
+ loadExperimentData,
2431
+ setOutlierAction,
2432
+ setAllOutlierActions,
2433
+ toggleField,
2434
+ renameField,
2435
+ reset
2436
+ };
2437
+ }
2438
+ //#endregion
2439
+ //#region src/utils/color.ts
2440
+ var HEX_RE = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
2441
+ function clamp(n, min, max) {
2442
+ return Math.min(max, Math.max(min, n));
2443
+ }
2444
+ function hexToHsl(hex) {
2445
+ const match = HEX_RE.exec(hex.trim());
2446
+ if (!match) return {
2447
+ h: 0,
2448
+ s: 0,
2449
+ l: 50
2450
+ };
2451
+ let raw = match[1];
2452
+ if (raw.length === 3) raw = raw.split("").map((c) => c + c).join("");
2453
+ const r = parseInt(raw.slice(0, 2), 16) / 255;
2454
+ const g = parseInt(raw.slice(2, 4), 16) / 255;
2455
+ const b = parseInt(raw.slice(4, 6), 16) / 255;
2456
+ const max = Math.max(r, g, b);
2457
+ const min = Math.min(r, g, b);
2458
+ const delta = max - min;
2459
+ let h = 0;
2460
+ if (delta !== 0) if (max === r) h = (g - b) / delta % 6;
2461
+ else if (max === g) h = (b - r) / delta + 2;
2462
+ else h = (r - g) / delta + 4;
2463
+ h = (h * 60 + 360) % 360;
2464
+ const l = (max + min) / 2;
2465
+ const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
2466
+ return {
2467
+ h,
2468
+ s: s * 100,
2469
+ l: l * 100
2470
+ };
2471
+ }
2472
+ function hslToHex(h, s, l) {
2473
+ const sNorm = clamp(s, 0, 100) / 100;
2474
+ const lNorm = clamp(l, 0, 100) / 100;
2475
+ const hNorm = (h % 360 + 360) % 360;
2476
+ const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
2477
+ const x = c * (1 - Math.abs(hNorm / 60 % 2 - 1));
2478
+ const m = lNorm - c / 2;
2479
+ let r = 0, g = 0, b = 0;
2480
+ if (hNorm < 60) [r, g, b] = [
2481
+ c,
2482
+ x,
2483
+ 0
2484
+ ];
2485
+ else if (hNorm < 120) [r, g, b] = [
2486
+ x,
2487
+ c,
2488
+ 0
2489
+ ];
2490
+ else if (hNorm < 180) [r, g, b] = [
2491
+ 0,
2492
+ c,
2493
+ x
2494
+ ];
2495
+ else if (hNorm < 240) [r, g, b] = [
2496
+ 0,
2497
+ x,
2498
+ c
2499
+ ];
2500
+ else if (hNorm < 300) [r, g, b] = [
2501
+ x,
2502
+ 0,
2503
+ c
2504
+ ];
2505
+ else [r, g, b] = [
2506
+ c,
2507
+ 0,
2508
+ x
2509
+ ];
2510
+ const toHex = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0");
2511
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
2512
+ }
2513
+ /**
2514
+ * Derive a same-hue-family shade of `parentHex` for child at `index` of `total`.
2515
+ *
2516
+ * Lightness is distributed evenly within [35, 75] so all children remain
2517
+ * readable on light/dark backgrounds. Hue and saturation are inherited from
2518
+ * the parent. With `total <= 1`, returns the parent unchanged.
2519
+ *
2520
+ * Children are ordered light → dark by index, so index 0 is the brightest
2521
+ * sibling and index `total - 1` is the darkest.
2522
+ */
2523
+ function deriveShade(parentHex, index, total) {
2524
+ if (total <= 1) return parentHex;
2525
+ const { h, s, l } = hexToHsl(parentHex);
2526
+ const L_MIN = 35;
2527
+ const L_MAX = 75;
2528
+ const spread = Math.min(L_MAX - L_MIN, 40);
2529
+ const center = clamp(l, L_MIN + spread / 2, L_MAX - spread / 2);
2530
+ const top = center + spread / 2;
2531
+ const bot = center - spread / 2;
2532
+ const childL = top - (total === 1 ? .5 : index / (total - 1)) * (top - bot);
2533
+ return hslToHex(h, clamp(s, 35, 100), childL);
2534
+ }
2535
+ //#endregion
2536
+ //#region src/composables/useSampleGroups.ts
2537
+ /** Shared sample-group hierarchy, color, and ungrouped-sample derivation for lab selectors. */
2538
+ function useSampleGroups(options) {
2539
+ const groups = computed(() => [...toValue(options.groups)]);
2540
+ const samples = computed(() => [...toValue(options.samples) ?? []]);
2541
+ const fallbackColor = computed(() => toValue(options.fallbackColor) || "#3B82F6");
2542
+ const separator = computed(() => {
2543
+ const explicit = toValue(options.separator);
2544
+ if (explicit) return explicit;
2545
+ return groups.value.some((group) => group.name.includes("/")) ? "/" : "_";
2546
+ });
2547
+ const hierarchicalGroups = computed(() => {
2548
+ if (groups.value.length === 0) return [];
2549
+ const groupedByMajor = /* @__PURE__ */ new Map();
2550
+ for (const group of groups.value) {
2551
+ const majorName = getMajorGroupName(group.name, separator.value);
2552
+ const existing = groupedByMajor.get(majorName);
2553
+ if (existing) existing.push(group);
2554
+ else groupedByMajor.set(majorName, [group]);
2555
+ }
2556
+ return [...groupedByMajor.entries()].map(([majorName, subGroups]) => {
2557
+ const color = subGroups[0]?.color || fallbackColor.value;
2558
+ return {
2559
+ name: majorName,
2560
+ color,
2561
+ subGroups: subGroups.map((subGroup, index) => {
2562
+ const displayColor = deriveShade(color, index, subGroups.length);
2563
+ return {
2564
+ ...subGroup,
2565
+ displayColor,
2566
+ displayBg: displayColor + "20",
2567
+ displayBorder: displayColor + "40"
2568
+ };
2569
+ }),
2570
+ allSamples: subGroups.flatMap((group) => group.samples)
2571
+ };
2572
+ });
2573
+ });
2574
+ const showHierarchy = computed(() => hierarchicalGroups.value.some((major) => major.subGroups.length > 1 || major.subGroups.length === 1 && major.name !== major.subGroups[0].name));
2575
+ const groupedSamples = computed(() => new Set(groups.value.flatMap((group) => group.samples)));
2576
+ const ungroupedSamples = computed(() => samples.value.filter((sample) => !groupedSamples.value.has(sample)));
2577
+ function findGroup(groupName) {
2578
+ return groups.value.find((group) => group.name === groupName);
2579
+ }
2580
+ function getGroupColor(groupName) {
2581
+ return findGroup(groupName)?.color || fallbackColor.value;
2582
+ }
2583
+ return {
2584
+ groups,
2585
+ separator,
2586
+ hierarchicalGroups,
2587
+ showHierarchy,
2588
+ groupedSamples,
2589
+ ungroupedSamples,
2590
+ findGroup,
2591
+ getGroupColor
2592
+ };
2593
+ }
2594
+ function getMajorGroupName(groupName, separator) {
2595
+ const parts = groupName.split(separator);
2596
+ return parts.length > 1 ? parts[0] : groupName;
2597
+ }
2598
+ //#endregion
2599
+ //#region src/composables/useExpansionSet.ts
2600
+ /** Shared expansion state for trees, grouped selectors, and disclosure lists. */
2601
+ function useExpansionSet(options = {}) {
2602
+ const expandedIds = ref(new Set(normalizeIds(toValue(options.defaultIds))));
2603
+ const expandedList = computed(() => [...expandedIds.value]);
2604
+ if (options.expandAll !== void 0) watch(() => toValue(options.expandAll), (shouldExpandAll) => {
2605
+ if (shouldExpandAll === void 0 || shouldExpandAll === null) return;
2606
+ if (shouldExpandAll) setExpanded(normalizeIds(toValue(options.allIds)));
2607
+ else reset();
2608
+ }, { immediate: true });
2609
+ if (options.allIds !== void 0) watch(() => normalizeIds(toValue(options.allIds)), (ids) => {
2610
+ if (toValue(options.expandAll)) setExpanded(ids);
2611
+ });
2612
+ function isExpanded(id) {
2613
+ return expandedIds.value.has(id);
2614
+ }
2615
+ function expand(id) {
2616
+ if (expandedIds.value.has(id)) return;
2617
+ expandedIds.value = new Set([...expandedIds.value, id]);
2618
+ }
2619
+ function collapse(id) {
2620
+ if (!expandedIds.value.has(id)) return;
2621
+ const next = new Set(expandedIds.value);
2622
+ next.delete(id);
2623
+ expandedIds.value = next;
2624
+ }
2625
+ function toggle(id) {
2626
+ if (expandedIds.value.has(id)) {
2627
+ collapse(id);
2628
+ return false;
2629
+ }
2630
+ expand(id);
2631
+ return true;
2632
+ }
2633
+ function expandMany(ids) {
2634
+ expandedIds.value = new Set([...expandedIds.value, ...normalizeIds(ids)]);
2635
+ }
2636
+ function setExpanded(ids) {
2637
+ expandedIds.value = new Set(normalizeIds(ids));
2638
+ }
2639
+ function collapseAll() {
2640
+ expandedIds.value = /* @__PURE__ */ new Set();
2641
+ }
2642
+ function reset() {
2643
+ setExpanded(normalizeIds(toValue(options.defaultIds)));
2644
+ }
2645
+ return {
2646
+ expandedIds,
2647
+ expandedList,
2648
+ isExpanded,
2649
+ expand,
2650
+ collapse,
2651
+ toggle,
2652
+ expandMany,
2653
+ setExpanded,
2654
+ collapseAll,
2655
+ reset
2656
+ };
2657
+ }
2658
+ function normalizeIds(ids) {
2659
+ if (!ids) return [];
2660
+ return [...new Set(ids.filter(Boolean))];
2661
+ }
2662
+ //#endregion
2663
+ //#region src/composables/platformContextHelpers.ts
2664
+ function getInjectedPlatformContext() {
2665
+ if (typeof window === "undefined") return void 0;
2666
+ return window.__MINT_PLATFORM__;
2667
+ }
2668
+ function getInjectedExperimentContext() {
2669
+ return getInjectedPlatformContext();
2670
+ }
2671
+ function currentExperimentFromContext(context = getInjectedExperimentContext()) {
2672
+ const candidate = context?.currentExperiment ?? context?.experiment;
2673
+ return candidate && typeof candidate === "object" ? candidate : void 0;
2674
+ }
2675
+ function currentExperimentIdFromContext(context = getInjectedExperimentContext()) {
2676
+ return parseExperimentId(context?.currentExperimentId ?? context?.experimentId ?? context?.currentExperiment ?? context?.experiment);
2677
+ }
2678
+ function currentExperimentIdFromUrl() {
2679
+ if (typeof window === "undefined") return void 0;
2680
+ const params = new URLSearchParams(window.location.search);
2681
+ return parseExperimentId(params.get("experimentId") ?? params.get("experiment_id")) ?? experimentIdFromPath(window.location.pathname) ?? experimentIdFromPath(window.location.hash.replace(/^#\/?/, "/"));
2682
+ }
2683
+ function resolveCurrentExperimentId(context = getInjectedExperimentContext()) {
2684
+ return currentExperimentIdFromContext(context) ?? currentExperimentIdFromUrl();
2685
+ }
2686
+ function parseExperimentId(value) {
2687
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2688
+ if (typeof value === "string" && value.trim()) {
2689
+ const parsed = Number(value);
2690
+ return Number.isFinite(parsed) ? parsed : void 0;
2691
+ }
2692
+ if (value && typeof value === "object" && "id" in value) return parseExperimentId(value.id);
2693
+ }
2694
+ function experimentIdFromPath(path) {
2695
+ const segments = path.split("/").filter(Boolean);
2696
+ for (let index = 0; index < segments.length - 1; index += 1) {
2697
+ const segment = segments[index]?.toLowerCase();
2698
+ if (segment === "experiment" || segment === "experiments") return parseExperimentId(decodeURIComponent(segments[index + 1] ?? ""));
2699
+ }
2700
+ }
2701
+ //#endregion
2702
+ //#region src/composables/useExperimentSave.ts
2703
+ /** Persists experiment design, analysis, and built-in template preset data through the platform API. */
2704
+ function useExperimentSave(options = {}) {
2705
+ const injectedContext = getInjectedPlatformContext();
2706
+ const api = useApi({ baseUrl: options.apiBaseUrl ?? injectedContext?.platformApiUrl });
2707
+ const pluginId = options.pluginId ?? injectedContext?.plugin?.id;
2708
+ const schemaVersion = options.schemaVersion ?? "1.0";
2709
+ const currentExperimentId = computed(() => {
2710
+ return resolveCurrentExperimentId(injectedContext);
2711
+ });
2712
+ const hasCurrentExperiment = computed(() => currentExperimentId.value !== void 0);
2713
+ const request = useRequestSyncState("Experiment request failed.");
2714
+ const isSaving = ref(false);
2715
+ const isLoading = ref(false);
2716
+ const error = request.error;
2717
+ const lastLoadedAt = request.lastLoadedAt;
2718
+ const lastSavedAt = request.lastSavedAt;
2719
+ function requireCurrentExperimentId() {
2720
+ const id = currentExperimentId.value;
2721
+ if (id === void 0) throw new Error("[MINT SDK] No current experiment is selected.");
2722
+ return id;
2723
+ }
2724
+ function currentExperimentIdOrError(action) {
2725
+ const id = currentExperimentId.value;
2726
+ if (id === void 0) {
2727
+ request.setError(`No current experiment is selected for ${action}`);
2728
+ return;
2729
+ }
2730
+ return id;
2731
+ }
2732
+ async function saveDesign(experimentId, data) {
2733
+ if (!pluginId) {
2734
+ request.setError("pluginId is required for saveDesign");
2735
+ return false;
2736
+ }
2737
+ isSaving.value = true;
2738
+ try {
2739
+ await request.run(async () => {
2740
+ await api.put(`/experiments/${experimentId}/data`, {
2741
+ plugin_id: pluginId,
2742
+ data,
2743
+ schema_version: schemaVersion
2744
+ });
2745
+ }, { success: "save" });
2746
+ return true;
2747
+ } catch {
2748
+ return false;
2749
+ } finally {
2750
+ isSaving.value = false;
2751
+ }
2752
+ }
2753
+ async function saveCurrentDesign(data) {
2754
+ const experimentId = currentExperimentIdOrError("saveDesign");
2755
+ return experimentId === void 0 ? false : saveDesign(experimentId, data);
2756
+ }
2757
+ async function saveTemplatePreset(experimentId, name, presetOptions = {}) {
2758
+ return saveDesign(experimentId, createBioTemplatePresetCollection(name, presetOptions));
2759
+ }
2760
+ async function saveCurrentTemplatePreset(name, presetOptions = {}) {
2761
+ const experimentId = currentExperimentIdOrError("saveTemplatePreset");
2762
+ return experimentId === void 0 ? false : saveTemplatePreset(experimentId, name, presetOptions);
2763
+ }
2764
+ async function saveAnalysis(experimentId, result) {
2765
+ if (!pluginId) {
2766
+ request.setError("pluginId is required for saveAnalysis");
2767
+ return false;
2768
+ }
2769
+ isSaving.value = true;
2770
+ try {
2771
+ await request.run(async () => {
2772
+ await api.put(`/experiments/${experimentId}/results/${pluginId}`, { result });
2773
+ }, { success: "save" });
2774
+ return true;
2775
+ } catch {
2776
+ return false;
2777
+ } finally {
2778
+ isSaving.value = false;
2779
+ }
2780
+ }
2781
+ async function saveCurrentAnalysis(result) {
2782
+ const experimentId = currentExperimentIdOrError("saveAnalysis");
2783
+ return experimentId === void 0 ? false : saveAnalysis(experimentId, result);
2784
+ }
2785
+ async function save(experimentId, opts) {
2786
+ if ((opts.design || opts.analysis) && !pluginId) {
2787
+ request.setError("pluginId is required for save");
2788
+ return false;
2789
+ }
2790
+ isSaving.value = true;
2791
+ try {
2792
+ await request.run(async () => {
2793
+ if (opts.design) await api.put(`/experiments/${experimentId}/data`, {
2794
+ plugin_id: pluginId,
2795
+ data: opts.design,
2796
+ schema_version: schemaVersion
2797
+ });
2798
+ if (opts.analysis) await api.put(`/experiments/${experimentId}/results/${pluginId}`, { result: opts.analysis });
2799
+ }, { success: "save" });
2800
+ return true;
2801
+ } catch {
2802
+ return false;
2803
+ } finally {
2804
+ isSaving.value = false;
2805
+ }
2806
+ }
2807
+ async function loadDesign(experimentId) {
2808
+ isLoading.value = true;
2809
+ try {
2810
+ return await request.run(() => api.get(`/experiments/${experimentId}/data`), { success: "load" });
2811
+ } catch {
2812
+ return null;
2813
+ } finally {
2814
+ isLoading.value = false;
2815
+ }
2816
+ }
2817
+ async function loadCurrentDesign() {
2818
+ const experimentId = currentExperimentIdOrError("loadDesign");
2819
+ return experimentId === void 0 ? null : loadDesign(experimentId);
2820
+ }
2821
+ async function loadAnalysis(experimentId) {
2822
+ if (!pluginId) {
2823
+ request.setError("pluginId is required for loadAnalysis");
2824
+ return null;
2825
+ }
2826
+ isLoading.value = true;
2827
+ try {
2828
+ return await request.run(() => api.get(`/experiments/${experimentId}/results/${pluginId}`), { success: "load" });
2829
+ } catch {
2830
+ return null;
2831
+ } finally {
2832
+ isLoading.value = false;
2833
+ }
2834
+ }
2835
+ async function loadCurrentAnalysis() {
2836
+ const experimentId = currentExperimentIdOrError("loadAnalysis");
2837
+ return experimentId === void 0 ? null : loadAnalysis(experimentId);
2838
+ }
2839
+ async function deleteDesign(experimentId) {
2840
+ isLoading.value = true;
2841
+ try {
2842
+ await request.run(() => api.delete(`/experiments/${experimentId}/data`));
2843
+ return true;
2844
+ } catch {
2845
+ return false;
2846
+ } finally {
2847
+ isLoading.value = false;
2848
+ }
2849
+ }
2850
+ async function deleteCurrentDesign() {
2851
+ const experimentId = currentExperimentIdOrError("deleteDesign");
2852
+ return experimentId === void 0 ? false : deleteDesign(experimentId);
2853
+ }
2854
+ async function deleteAnalysis(experimentId) {
2855
+ if (!pluginId) {
2856
+ request.setError("pluginId is required for deleteAnalysis");
2857
+ return false;
2858
+ }
2859
+ isLoading.value = true;
2860
+ try {
2861
+ await request.run(() => api.delete(`/experiments/${experimentId}/results/${pluginId}`));
2862
+ return true;
2863
+ } catch {
2864
+ return false;
2865
+ } finally {
2866
+ isLoading.value = false;
2867
+ }
2868
+ }
2869
+ async function deleteCurrentAnalysis() {
2870
+ const experimentId = currentExperimentIdOrError("deleteAnalysis");
2871
+ return experimentId === void 0 ? false : deleteAnalysis(experimentId);
2872
+ }
2873
+ return {
2874
+ currentExperimentId,
2875
+ hasCurrentExperiment,
2876
+ isSaving,
2877
+ isLoading,
2878
+ error,
2879
+ lastLoadedAt,
2880
+ lastSavedAt,
2881
+ saveDesign,
2882
+ saveCurrentDesign,
2883
+ saveTemplatePreset,
2884
+ saveCurrentTemplatePreset,
2885
+ saveAnalysis,
2886
+ saveCurrentAnalysis,
2887
+ save,
2888
+ loadDesign,
2889
+ loadCurrentDesign,
2890
+ loadAnalysis,
2891
+ loadCurrentAnalysis,
2892
+ deleteDesign,
2893
+ deleteCurrentDesign,
2894
+ deleteAnalysis,
2895
+ deleteCurrentAnalysis,
2896
+ requireCurrentExperimentId
2897
+ };
2898
+ }
2899
+ //#endregion
2900
+ //#region src/composables/useTemplateCollection.ts
2901
+ /** Manages a biology template collection in the current experiment's design_data. */
2902
+ function useTemplateCollection(options = {}) {
2903
+ const experimentSave = useExperimentSave(options);
2904
+ const templateCollection = ref(createInitialCollection(options));
2905
+ const templates = computed(() => extractTemplateCollection(templateCollection.value));
2906
+ const metadata = computed(() => templateCollection.value.metadata ?? {});
2907
+ const templateIds = computed(() => Object.keys(templates.value));
2908
+ const hasTemplates = computed(() => templateIds.value.length > 0);
2909
+ function apply(value, metadata) {
2910
+ templateCollection.value = normalizeTemplateCollection(value, metadata);
2911
+ return templateCollection.value;
2912
+ }
2913
+ function reset() {
2914
+ templateCollection.value = createInitialCollection(options);
2915
+ return templateCollection.value;
2916
+ }
2917
+ function setTemplate(template) {
2918
+ const nextTemplates = {
2919
+ ...templates.value,
2920
+ [template.template_id]: template
2921
+ };
2922
+ templateCollection.value = createTemplateCollectionEnvelope(Object.values(nextTemplates), templateCollection.value.metadata);
2923
+ return templateCollection.value;
2924
+ }
2925
+ function removeTemplate(templateId) {
2926
+ if (!templates.value[templateId]) return false;
2927
+ const nextTemplates = { ...templates.value };
2928
+ delete nextTemplates[templateId];
2929
+ templateCollection.value = createTemplateCollectionEnvelope(Object.values(nextTemplates), templateCollection.value.metadata);
2930
+ return true;
2931
+ }
2932
+ function getTemplate(templateId) {
2933
+ return templates.value[templateId] ?? null;
2934
+ }
2935
+ function requireTemplate(templateId) {
2936
+ return ensureTemplateFromCollection(templateCollection.value, templateId);
2937
+ }
2938
+ async function load(experimentId) {
2939
+ const data = await experimentSave.loadDesign(experimentId);
2940
+ return data === null ? null : apply(data);
2941
+ }
2942
+ async function loadCurrent() {
2943
+ const data = await experimentSave.loadCurrentDesign();
2944
+ return data === null ? null : apply(data);
2945
+ }
2946
+ async function save(experimentId) {
2947
+ return experimentSave.saveDesign(experimentId, templateCollection.value);
2948
+ }
2949
+ async function saveCurrent() {
2950
+ return experimentSave.saveCurrentDesign(templateCollection.value);
2951
+ }
2952
+ return {
2953
+ currentExperimentId: experimentSave.currentExperimentId,
2954
+ hasCurrentExperiment: experimentSave.hasCurrentExperiment,
2955
+ templateCollection,
2956
+ templates,
2957
+ metadata,
2958
+ templateIds,
2959
+ hasTemplates,
2960
+ isLoading: experimentSave.isLoading,
2961
+ isSaving: experimentSave.isSaving,
2962
+ error: experimentSave.error,
2963
+ lastLoadedAt: experimentSave.lastLoadedAt,
2964
+ lastSavedAt: experimentSave.lastSavedAt,
2965
+ apply,
2966
+ reset,
2967
+ setTemplate,
2968
+ removeTemplate,
2969
+ getTemplate,
2970
+ requireTemplate,
2971
+ load,
2972
+ loadCurrent,
2973
+ save,
2974
+ saveCurrent,
2975
+ requireCurrentExperimentId: experimentSave.requireCurrentExperimentId
2976
+ };
2977
+ }
2978
+ function createInitialCollection(options) {
2979
+ const initial = typeof options.initial === "function" ? options.initial() : options.initial;
2980
+ if (initial === void 0) return { templates: {} };
2981
+ return normalizeTemplateCollection(initial);
2982
+ }
2983
+ function normalizeTemplateCollection(value, metadata) {
2984
+ if (Array.isArray(value)) return createTemplateCollectionEnvelope(value, metadata);
2985
+ return createTemplateCollectionEnvelope(Object.values(extractTemplateCollection(value)), readCollectionMetadata(value) ?? metadata);
2986
+ }
2987
+ function createTemplateCollectionEnvelope(templates, metadata) {
2988
+ return metadata === void 0 ? createTemplateCollection(templates) : createTemplateCollection(templates, metadata);
2989
+ }
2990
+ function readCollectionMetadata(value) {
2991
+ if (typeof value !== "object" || value === null || Array.isArray(value) || !("metadata" in value)) return;
2992
+ const metadata = value.metadata;
2993
+ if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) return metadata;
2994
+ }
2995
+ //#endregion
2996
+ //#region src/composables/useBioTemplateControls.ts
2997
+ /** Build FormBuilder schema, AppSidebar panels, and defaults for a built-in biology template or preset. */
2998
+ function useBioTemplateControls(target, options = {}) {
2999
+ const { initialValues, topBarSettings: _topBarSettings, ...schemaOptions } = options;
3000
+ const controls = createBioTemplateControlToolkit(target, schemaOptions);
3001
+ if (initialValues === void 0) return controls;
3002
+ return {
3003
+ ...controls,
3004
+ initialValues: {
3005
+ ...controls.initialValues,
3006
+ ...initialValues
3007
+ }
3008
+ };
3009
+ }
3010
+ //#endregion
3011
+ //#region src/composables/useBioTemplateComponents.ts
3012
+ /** Return component props grouped by SDK component name for direct WellPlate/DoseCalculator binding. */
3013
+ function toBioTemplateComponentPropsByComponent(target) {
3014
+ return toBioTemplateComponentPropsByComponent$1(target);
3015
+ }
3016
+ /** Return the first props object for an SDK component, optionally restricted by template id or binding id. */
3017
+ function getBioTemplateComponentProps(target, component, options = {}) {
3018
+ return getBioTemplateComponentProps$1(target, component, options);
3019
+ }
3020
+ /** Map built-in biology templates to SDK components such as WellPlate, DataFrame, DoseCalculator, and PlateMapEditor. */
3021
+ function useBioTemplateComponents(target) {
3022
+ return {
3023
+ bindings: getBioTemplateComponentBindings(target),
3024
+ imports: toBioTemplateComponentImports(target),
3025
+ componentProps: typeof target === "string" ? [] : toBioTemplateComponentProps(target),
3026
+ componentPropsById: typeof target === "string" ? {} : toBioTemplateComponentPropsById(target),
3027
+ componentPropsByComponent: typeof target === "string" ? {} : toBioTemplateComponentPropsByComponent(target),
3028
+ snippets: typeof target === "string" ? [] : toBioTemplateComponentSnippets(target),
3029
+ usage: typeof target === "string" ? null : toBioTemplateComponentUsage(target),
3030
+ getComponentProps: (component, options) => typeof target === "string" ? void 0 : getBioTemplateComponentProps(target, component, options)
3031
+ };
3032
+ }
3033
+ //#endregion
3034
+ //#region src/composables/useBioTemplateWorkspace.ts
3035
+ /** One-stop wiring for BioTemplateRenderer, FormBuilder, AppSidebar, and template-aware SDK components. */
3036
+ function useBioTemplateWorkspace(target, options = {}) {
3037
+ const controls = useBioTemplateControls(target, options);
3038
+ const workspace = useControlWorkspace(controls.controls, options);
3039
+ const components = useBioTemplateComponents(target);
3040
+ const renderer = isTemplateObjectTarget(target) ? { target } : null;
3041
+ const bindings = {
3042
+ renderer,
3043
+ form: workspace.bindings.form,
3044
+ sidebar: workspace.bindings.sidebar,
3045
+ topBar: workspace.bindings.topBar,
3046
+ topBarTabs: workspace.bindings.topBarTabs,
3047
+ topBarSettings: workspace.bindings.topBarSettings,
3048
+ pillNav: workspace.bindings.pillNav,
3049
+ componentBindings: components.bindings,
3050
+ componentProps: components.componentProps,
3051
+ componentPropsById: components.componentPropsById,
3052
+ componentPropsByComponent: components.componentPropsByComponent,
3053
+ componentSnippets: components.snippets,
3054
+ componentUsage: components.usage,
3055
+ getComponentProps: components.getComponentProps
3056
+ };
3057
+ return {
3058
+ target,
3059
+ controls,
3060
+ workspace,
3061
+ controlSchema: controls.controls,
3062
+ components,
3063
+ values: workspace.values,
3064
+ activeView: workspace.activeView,
3065
+ form: workspace.form,
3066
+ sidebar: workspace.sidebar,
3067
+ topBar: workspace.topBar,
3068
+ pillNav: workspace.pillNav,
3069
+ topBarSettings: workspace.topBarSettings,
3070
+ bindings,
3071
+ renderer,
3072
+ componentBindings: components.bindings,
3073
+ componentImports: components.imports,
3074
+ componentProps: components.componentProps,
3075
+ componentPropsById: components.componentPropsById,
3076
+ componentPropsByComponent: components.componentPropsByComponent,
3077
+ componentSnippets: components.snippets,
3078
+ componentUsage: components.usage,
3079
+ getComponentProps: components.getComponentProps
3080
+ };
3081
+ }
3082
+ function isTemplateObjectTarget(target) {
3083
+ return typeof target !== "string";
3084
+ }
3085
+ //#endregion
3086
+ //#region src/composables/useBioTemplatePackWorkspace.ts
3087
+ /** Create a complete workspace for a curated biology template pack from a single pack id. */
3088
+ function useBioTemplatePackWorkspace(packId, options = {}) {
3089
+ const pack = getBioTemplatePackInfo(packId);
3090
+ if (!pack) throw new Error(`Unknown template pack '${packId}'.`);
3091
+ const resolvedPack = pack;
3092
+ const templateCollection = useTemplateCollection({
3093
+ ...options,
3094
+ initial: createDefaultCollection
3095
+ });
3096
+ const collection = templateCollection.templateCollection;
3097
+ let workspaceScope;
3098
+ function createWorkspace(value) {
3099
+ workspaceScope?.stop();
3100
+ workspaceScope = effectScope();
3101
+ return workspaceScope.run(() => useBioTemplateWorkspace(value));
3102
+ }
3103
+ const workspaceRef = shallowRef(createWorkspace(collection.value));
3104
+ const workspace = computed(() => workspaceRef.value);
3105
+ const renderer = computed(() => ({ target: collection.value }));
3106
+ const form = computed(() => workspace.value.form);
3107
+ const sidebar = computed(() => workspace.value.sidebar);
3108
+ const topBar = computed(() => workspace.value.topBar);
3109
+ const pillNav = computed(() => workspace.value.pillNav);
3110
+ const topBarSettings = computed(() => workspace.value.topBarSettings);
3111
+ const bindings = computed(() => workspace.value.bindings);
3112
+ const componentBindings = computed(() => workspace.value.componentBindings);
3113
+ const componentImports = computed(() => workspace.value.componentImports);
3114
+ const componentProps = computed(() => workspace.value.componentProps);
3115
+ const componentPropsById = computed(() => workspace.value.componentPropsById);
3116
+ const componentPropsByComponent = computed(() => workspace.value.componentPropsByComponent);
3117
+ const componentSnippets = computed(() => workspace.value.componentSnippets);
3118
+ const componentUsage = computed(() => workspace.value.componentUsage);
3119
+ watch(collection, (value) => {
3120
+ workspaceRef.value = createWorkspace(value);
3121
+ });
3122
+ if (getCurrentScope()) onScopeDispose(() => workspaceScope?.stop());
3123
+ function createDefaultCollection() {
3124
+ return createBioTemplatePackCollection(resolvedPack.name);
3125
+ }
3126
+ function applyCollection(value, metadata) {
3127
+ return templateCollection.apply(value, metadata);
3128
+ }
3129
+ function resetToDefaultCollection() {
3130
+ return templateCollection.apply(createDefaultCollection());
3131
+ }
3132
+ return {
3133
+ packId: resolvedPack.name,
3134
+ pack: resolvedPack,
3135
+ collection,
3136
+ templates: templateCollection.templates,
3137
+ templateIds: templateCollection.templateIds,
3138
+ currentExperimentId: templateCollection.currentExperimentId,
3139
+ hasCurrentExperiment: templateCollection.hasCurrentExperiment,
3140
+ loading: templateCollection.isLoading,
3141
+ saving: templateCollection.isSaving,
3142
+ error: templateCollection.error,
3143
+ lastLoadedAt: templateCollection.lastLoadedAt,
3144
+ lastSavedAt: templateCollection.lastSavedAt,
3145
+ renderer,
3146
+ workspace,
3147
+ form,
3148
+ sidebar,
3149
+ topBar,
3150
+ pillNav,
3151
+ topBarSettings,
3152
+ bindings,
3153
+ componentBindings,
3154
+ componentImports,
3155
+ componentProps,
3156
+ componentPropsById,
3157
+ componentPropsByComponent,
3158
+ componentSnippets,
3159
+ componentUsage,
3160
+ getComponentProps: (component, options) => workspace.value.getComponentProps(component, options),
3161
+ applyCollection,
3162
+ resetToDefaultCollection,
3163
+ save: templateCollection.save,
3164
+ saveCurrent: templateCollection.saveCurrent,
3165
+ load: templateCollection.load,
3166
+ loadCurrent: templateCollection.loadCurrent,
3167
+ requireCurrentExperimentId: templateCollection.requireCurrentExperimentId
3168
+ };
3169
+ }
3170
+ //#endregion
3171
+ //#region src/composables/useBioTemplatePresetWorkspace.ts
3172
+ /** Create a complete editable WellPlate/DoseCalculator workspace for dose design from a built-in biology template preset with current-experiment save wiring. */
3173
+ function useBioTemplatePresetWorkspace(presetId, options = {}) {
3174
+ const { controlOptions, initialValues, ...collectionOptions } = options;
3175
+ const resolvedControlOptions = mergeInitialValues(controlOptions, initialValues);
3176
+ const presetWorkspace = useBioTemplateWorkspace(presetId, resolvedControlOptions);
3177
+ const presetInfo = computed(() => getBioTemplatePresetInfo(presetId));
3178
+ const presetLabel = computed(() => presetInfo.value?.label ?? humanizePresetId(presetId));
3179
+ const controls = presetWorkspace.controls;
3180
+ const controlValues = ref({ ...controls.initialValues });
3181
+ const controlViewIds = computed(() => controls.viewIds);
3182
+ const controlViewItems = computed(() => controls.viewItems);
3183
+ const activeControlView = ref(controls.defaultView);
3184
+ const templateCollection = useTemplateCollection({
3185
+ ...collectionOptions,
3186
+ initial: createDefaultCollection
3187
+ });
3188
+ const collection = templateCollection.templateCollection;
3189
+ const workspace = computed(() => useBioTemplateWorkspace(collection.value, resolvedControlOptions));
3190
+ const renderer = computed(() => ({ target: collection.value }));
3191
+ const componentBindings = computed(() => workspace.value.componentBindings);
3192
+ const componentImports = computed(() => workspace.value.componentImports);
3193
+ const componentProps = computed(() => workspace.value.componentProps);
3194
+ const componentPropsById = computed(() => workspace.value.componentPropsById);
3195
+ const componentPropsByComponent = computed(() => workspace.value.componentPropsByComponent);
3196
+ const componentSnippets = computed(() => workspace.value.componentSnippets);
3197
+ const componentUsage = computed(() => workspace.value.componentUsage);
3198
+ const form = reactive({
3199
+ ...controls.form,
3200
+ modelValue: controlValues.value,
3201
+ "onUpdate:modelValue": setControlValues
3202
+ });
3203
+ const sidebar = reactive({
3204
+ ...controls.sidebar,
3205
+ activeView: activeControlView.value,
3206
+ modelValue: controlValues.value,
3207
+ "onUpdate:modelValue": setControlValues,
3208
+ values: controlValues.value,
3209
+ "onUpdate:values": setControlValues
3210
+ });
3211
+ const topBar = reactive({
3212
+ tabs: controls.topBarTabs,
3213
+ currentTabId: activeControlView.value,
3214
+ onTabSelect: (tab) => setActiveControlView(tab.id)
3215
+ });
3216
+ const pillNav = reactive({
3217
+ items: controls.viewItems,
3218
+ currentItemId: activeControlView.value,
3219
+ onSelect: (item) => setActiveControlView(item.id)
3220
+ });
3221
+ const topBarSettings = reactive({
3222
+ showSettings: true,
3223
+ settingsConfig: {
3224
+ ...controls.topBarSettingsConfig,
3225
+ values: controlValues.value
3226
+ },
3227
+ onSettingsValuesChange: setControlValues
3228
+ });
3229
+ const topBarProps = computed(() => ({
3230
+ pillNav: pillNav.items,
3231
+ currentPillId: pillNav.currentItemId,
3232
+ onPillSelect: pillNav.onSelect,
3233
+ ...topBarSettings
3234
+ }));
3235
+ const topBarTabsProps = computed(() => ({
3236
+ tabs: topBar.tabs,
3237
+ currentTabId: topBar.currentTabId,
3238
+ onTabSelect: topBar.onTabSelect,
3239
+ ...topBarSettings
3240
+ }));
3241
+ function getComponentProps(component, options) {
3242
+ return workspace.value.getComponentProps(component, options);
3243
+ }
3244
+ const bindings = {
3245
+ renderer,
3246
+ form,
3247
+ sidebar,
3248
+ topBar: topBarProps,
3249
+ topBarTabs: topBarTabsProps,
3250
+ topBarSettings,
3251
+ pillNav,
3252
+ componentBindings,
3253
+ componentProps,
3254
+ componentPropsById,
3255
+ componentPropsByComponent,
3256
+ componentSnippets,
3257
+ componentUsage,
3258
+ getComponentProps
3259
+ };
3260
+ function createDefaultCollection() {
3261
+ return createBioTemplatePresetCollectionFromControls(presetId, controls.initialValues);
3262
+ }
3263
+ watch(controlValues, (values) => {
3264
+ syncControlValueBindings(values);
3265
+ templateCollection.apply(createBioTemplatePresetCollectionFromControls(presetId, values));
3266
+ }, { deep: true });
3267
+ watch(activeControlView, (viewId) => {
3268
+ sidebar.activeView = viewId;
3269
+ topBar.currentTabId = viewId;
3270
+ pillNav.currentItemId = viewId;
3271
+ }, { flush: "sync" });
3272
+ watch(() => sidebar.activeView, setActiveControlView, { flush: "sync" });
3273
+ watch(() => topBar.currentTabId, setActiveControlView, { flush: "sync" });
3274
+ watch(() => pillNav.currentItemId, setActiveControlView, { flush: "sync" });
3275
+ function applyCollection(value) {
3276
+ return templateCollection.apply(value);
3277
+ }
3278
+ function setActiveControlView(viewId) {
3279
+ if (!controlViewIds.value.includes(viewId)) return;
3280
+ activeControlView.value = viewId;
3281
+ }
3282
+ function setControlValues(values) {
3283
+ const nextValues = { ...values };
3284
+ controlValues.value = nextValues;
3285
+ syncControlValueBindings(nextValues);
3286
+ return templateCollection.apply(createBioTemplatePresetCollectionFromControls(presetId, nextValues));
3287
+ }
3288
+ function updateControlValues(values) {
3289
+ return setControlValues({
3290
+ ...controlValues.value,
3291
+ ...values
3292
+ });
3293
+ }
3294
+ function resetToDefaultCollection() {
3295
+ return setControlValues({ ...controls.initialValues });
3296
+ }
3297
+ function syncControlValueBindings(values) {
3298
+ form.modelValue = values;
3299
+ sidebar.modelValue = values;
3300
+ sidebar.values = values;
3301
+ topBarSettings.settingsConfig = {
3302
+ ...topBarSettings.settingsConfig,
3303
+ values
3304
+ };
3305
+ }
3306
+ return {
3307
+ presetId,
3308
+ presetInfo,
3309
+ presetLabel,
3310
+ collection,
3311
+ templates: templateCollection.templates,
3312
+ templateIds: templateCollection.templateIds,
3313
+ currentExperimentId: templateCollection.currentExperimentId,
3314
+ hasCurrentExperiment: templateCollection.hasCurrentExperiment,
3315
+ saving: templateCollection.isSaving,
3316
+ error: templateCollection.error,
3317
+ lastSavedAt: templateCollection.lastSavedAt,
3318
+ controls,
3319
+ controlSchema: controls.controls,
3320
+ controlValues,
3321
+ controlViewIds,
3322
+ controlViewItems,
3323
+ activeControlView,
3324
+ form,
3325
+ sidebar,
3326
+ topBar,
3327
+ pillNav,
3328
+ topBarSettings,
3329
+ bindings,
3330
+ renderer,
3331
+ workspace,
3332
+ componentBindings,
3333
+ componentImports,
3334
+ componentProps,
3335
+ componentPropsById,
3336
+ componentPropsByComponent,
3337
+ componentSnippets,
3338
+ componentUsage,
3339
+ getComponentProps,
3340
+ setActiveControlView,
3341
+ setControlValues,
3342
+ updateControlValues,
3343
+ applyCollection,
3344
+ resetToDefaultCollection,
3345
+ save: templateCollection.save,
3346
+ saveCurrent: templateCollection.saveCurrent
3347
+ };
3348
+ }
3349
+ function humanizePresetId(value) {
3350
+ return value.replace(/[-_]+/g, " ").replace(/\b\w/g, (match) => match.toUpperCase());
3351
+ }
3352
+ function mergeInitialValues(controlOptions, initialValues) {
3353
+ if (initialValues === void 0) return controlOptions;
3354
+ return {
3355
+ ...controlOptions ?? {},
3356
+ initialValues: {
3357
+ ...controlOptions?.initialValues ?? {},
3358
+ ...initialValues
3359
+ }
3360
+ };
3361
+ }
3362
+ //#endregion
3363
+ //#region src/composables/useRackEditor.ts
3364
+ var SLOT_CYCLE = [
3365
+ "R",
3366
+ "G",
3367
+ "B",
3368
+ "Y"
3369
+ ];
3370
+ var PLATE_CONFIGS = {
3371
+ 6: {
3372
+ rows: 2,
3373
+ cols: 3
3374
+ },
3375
+ 12: {
3376
+ rows: 3,
3377
+ cols: 4
3378
+ },
3379
+ 24: {
3380
+ rows: 4,
3381
+ cols: 6
3382
+ },
3383
+ 48: {
3384
+ rows: 6,
3385
+ cols: 8
3386
+ },
3387
+ 54: {
3388
+ rows: 6,
3389
+ cols: 9
3390
+ },
3391
+ 96: {
3392
+ rows: 8,
3393
+ cols: 12
3394
+ },
3395
+ 384: {
3396
+ rows: 16,
3397
+ cols: 24
3398
+ }
3399
+ };
3400
+ var globalIdCounter = 0;
3401
+ function generateId() {
3402
+ globalIdCounter++;
3403
+ return `rack-${Date.now()}-${globalIdCounter}`;
3404
+ }
3405
+ /** Manages a set of sample racks with well-level assignment, series fill, and multi-rack reordering. */
3406
+ function useRackEditor(initialRacks, options) {
3407
+ const defaultFormat = options?.defaultFormat ?? 54;
3408
+ const defaultInjVol = options?.defaultInjectionVolume ?? 5;
3409
+ const maxRacks = options?.maxRacks ?? 10;
3410
+ const minRacks = options?.minRacks ?? 1;
3411
+ function createDefaultRack(name, slotIndex) {
3412
+ const slot = SLOT_CYCLE[(slotIndex ?? 0) % SLOT_CYCLE.length];
3413
+ return {
3414
+ id: generateId(),
3415
+ name: name ?? `Rack ${racks.value.length + 1}`,
3416
+ format: defaultFormat,
3417
+ slot,
3418
+ injectionVolume: defaultInjVol,
3419
+ wells: {}
3420
+ };
3421
+ }
3422
+ const racks = ref(initialRacks && initialRacks.length > 0 ? initialRacks.map((r) => ({ ...r })) : [createDefaultRack("Rack 1", 0)]);
3423
+ const activeRackId = ref(racks.value[0]?.id ?? "");
3424
+ const activeRack = computed(() => racks.value.find((r) => r.id === activeRackId.value) ?? racks.value[0]);
3425
+ function addRack(name) {
3426
+ if (racks.value.length >= maxRacks) return racks.value[racks.value.length - 1];
3427
+ const rack = createDefaultRack(name, racks.value.length);
3428
+ racks.value.push(rack);
3429
+ activeRackId.value = rack.id;
3430
+ return rack;
3431
+ }
3432
+ function removeRack(rackId) {
3433
+ if (racks.value.length <= minRacks) return;
3434
+ const index = racks.value.findIndex((r) => r.id === rackId);
3435
+ if (index === -1) return;
3436
+ racks.value.splice(index, 1);
3437
+ if (activeRackId.value === rackId && racks.value[0]) activeRackId.value = racks.value[0].id;
3438
+ }
3439
+ function reorderRacks(fromIndex, toIndex) {
3440
+ if (fromIndex < 0 || fromIndex >= racks.value.length) return;
3441
+ if (toIndex < 0 || toIndex >= racks.value.length) return;
3442
+ if (fromIndex === toIndex) return;
3443
+ const [moved] = racks.value.splice(fromIndex, 1);
3444
+ if (moved) racks.value.splice(toIndex, 0, moved);
3445
+ }
3446
+ function updateRack(rackId, data) {
3447
+ const rack = racks.value.find((r) => r.id === rackId);
3448
+ if (!rack) return;
3449
+ if (data.name !== void 0) rack.name = data.name;
3450
+ if (data.format !== void 0) rack.format = data.format;
3451
+ if (data.slot !== void 0) rack.slot = data.slot;
3452
+ if (data.injectionVolume !== void 0) rack.injectionVolume = data.injectionVolume;
3453
+ if (data.wells !== void 0) rack.wells = data.wells;
3454
+ }
3455
+ function setActiveRack(rackId) {
3456
+ if (racks.value.some((r) => r.id === rackId)) activeRackId.value = rackId;
3457
+ }
3458
+ function setWellData(rackId, wellId, data) {
3459
+ const rack = racks.value.find((r) => r.id === rackId);
3460
+ if (!rack) return;
3461
+ rack.wells[wellId] = {
3462
+ ...rack.wells[wellId],
3463
+ ...data
3464
+ };
3465
+ }
3466
+ function clearWell(rackId, wellId) {
3467
+ const rack = racks.value.find((r) => r.id === rackId);
3468
+ if (!rack) return;
3469
+ delete rack.wells[wellId];
3470
+ }
3471
+ function clearAllWells(rackId) {
3472
+ const rack = racks.value.find((r) => r.id === rackId);
3473
+ if (!rack) return;
3474
+ rack.wells = {};
3475
+ }
3476
+ function fillSeries(rackId, prefix = "S") {
3477
+ const rack = racks.value.find((r) => r.id === rackId);
3478
+ if (!rack) return;
3479
+ const config = PLATE_CONFIGS[rack.format];
3480
+ if (!config) return;
3481
+ let counter = 1;
3482
+ for (let row = 0; row < config.rows; row++) for (let col = 0; col < config.cols; col++) {
3483
+ const wellId = `${String.fromCharCode(65 + row)}${col + 1}`;
3484
+ if (!rack.wells[wellId]?.sampleType) {
3485
+ const paddedNum = String(counter).padStart(3, "0");
3486
+ rack.wells[wellId] = {
3487
+ id: wellId,
3488
+ row,
3489
+ col,
3490
+ state: "filled",
3491
+ sampleType: "sample",
3492
+ metadata: {
3493
+ label: `${prefix}${paddedNum}`,
3494
+ injectionVolume: rack.injectionVolume,
3495
+ injectionCount: 1
3496
+ }
3497
+ };
3498
+ }
3499
+ counter++;
3500
+ }
3501
+ }
3502
+ function getAllWells() {
3503
+ const result = [];
3504
+ for (const rack of racks.value) for (const [wellId, well] of Object.entries(rack.wells)) result.push({
3505
+ rackId: rack.id,
3506
+ wellId,
3507
+ well
3508
+ });
3509
+ return result;
3510
+ }
3511
+ const totalSampleCount = computed(() => {
3512
+ let count = 0;
3513
+ for (const rack of racks.value) count += Object.keys(rack.wells).length;
3514
+ return count;
3515
+ });
3516
+ function reset() {
3517
+ racks.value = [createDefaultRack("Rack 1", 0)];
3518
+ activeRackId.value = racks.value[0].id;
3519
+ }
3520
+ return {
3521
+ racks,
3522
+ activeRack,
3523
+ activeRackId,
3524
+ addRack,
3525
+ removeRack,
3526
+ reorderRacks,
3527
+ updateRack,
3528
+ setActiveRack,
3529
+ setWellData,
3530
+ clearWell,
3531
+ clearAllWells,
3532
+ fillSeries,
3533
+ getAllWells,
3534
+ totalSampleCount,
3535
+ reset
3536
+ };
3537
+ }
3538
+ //#endregion
3539
+ //#region src/composables/useGroupAssignment.ts
3540
+ /** Shared two-zone group assignment model for control/treatment style workflows. */
3541
+ function useGroupAssignment(options) {
3542
+ const groups = computed(() => [...toValue(options.groups)]);
3543
+ const group1 = computed(() => [...toValue(options.group1)]);
3544
+ const group2 = computed(() => [...toValue(options.group2)]);
3545
+ const label1 = computed(() => toValue(options.label1) || "Control");
3546
+ const label2 = computed(() => toValue(options.label2) || "Treatment");
3547
+ const minPerGroup = computed(() => normalizeMinimum(toValue(options.minPerGroup)));
3548
+ const group1Set = computed(() => new Set(group1.value));
3549
+ const group2Set = computed(() => new Set(group2.value));
3550
+ const unassignedGroups = computed(() => groups.value.filter((group) => !group1Set.value.has(group.name) && !group2Set.value.has(group.name)));
3551
+ const zone1Groups = computed(() => groups.value.filter((group) => group1Set.value.has(group.name)));
3552
+ const zone2Groups = computed(() => groups.value.filter((group) => group2Set.value.has(group.name)));
3553
+ const zone1Count = computed(() => zone1Groups.value.reduce((sum, group) => sum + group.count, 0));
3554
+ const zone2Count = computed(() => zone2Groups.value.reduce((sum, group) => sum + group.count, 0));
3555
+ const isValid = computed(() => group1.value.length >= minPerGroup.value && group2.value.length >= minPerGroup.value);
3556
+ const validationMessage = computed(() => {
3557
+ if (isValid.value) return null;
3558
+ const missing1 = Math.max(0, minPerGroup.value - group1.value.length);
3559
+ const missing2 = Math.max(0, minPerGroup.value - group2.value.length);
3560
+ const parts = [];
3561
+ if (missing1 > 0) parts.push(`${missing1} more to ${label1.value}`);
3562
+ if (missing2 > 0) parts.push(`${missing2} more to ${label2.value}`);
3563
+ return `Add ${parts.join(" and ")}`;
3564
+ });
3565
+ function getZoneForGroup(groupName) {
3566
+ if (group1Set.value.has(groupName)) return "zone1";
3567
+ if (group2Set.value.has(groupName)) return "zone2";
3568
+ return null;
3569
+ }
3570
+ function assignToZone(groupName, zone) {
3571
+ const next = removeFromBoth(groupName);
3572
+ if (zone === "zone1") return {
3573
+ group1: [...next.group1, groupName],
3574
+ group2: next.group2
3575
+ };
3576
+ return {
3577
+ group1: next.group1,
3578
+ group2: [...next.group2, groupName]
3579
+ };
3580
+ }
3581
+ function removeFromZone(groupName, zone) {
3582
+ if (zone === "zone1") return {
3583
+ group1: group1.value.filter((name) => name !== groupName),
3584
+ group2: group2.value
3585
+ };
3586
+ return {
3587
+ group1: group1.value,
3588
+ group2: group2.value.filter((name) => name !== groupName)
3589
+ };
3590
+ }
3591
+ function clearAll() {
3592
+ return {
3593
+ group1: [],
3594
+ group2: []
3595
+ };
3596
+ }
3597
+ function removeFromBoth(groupName) {
3598
+ return {
3599
+ group1: group1.value.filter((name) => name !== groupName),
3600
+ group2: group2.value.filter((name) => name !== groupName)
3601
+ };
3602
+ }
3603
+ return {
3604
+ unassignedGroups,
3605
+ zone1Groups,
3606
+ zone2Groups,
3607
+ zone1Count,
3608
+ zone2Count,
3609
+ isValid,
3610
+ validationMessage,
3611
+ getZoneForGroup,
3612
+ assignToZone,
3613
+ removeFromZone,
3614
+ clearAll
3615
+ };
3616
+ }
3617
+ function normalizeMinimum(value) {
3618
+ if (value === null || value === void 0 || !Number.isFinite(value)) return 1;
3619
+ return Math.max(0, Math.floor(value));
3620
+ }
3621
+ //#endregion
3622
+ //#region src/composables/useReagentSeries.ts
3623
+ var DEFAULT_PRESETS = [
3624
+ {
3625
+ label: "2x",
3626
+ factor: 2
3627
+ },
3628
+ {
3629
+ label: "3x",
3630
+ factor: 3
3631
+ },
3632
+ {
3633
+ label: "½ log",
3634
+ factor: "half-log"
3635
+ },
3636
+ {
3637
+ label: "10x",
3638
+ factor: 10
3639
+ }
3640
+ ];
3641
+ var DEFAULT_UNITS = [
3642
+ "µM",
3643
+ "nM",
3644
+ "mM",
3645
+ "pM"
3646
+ ];
3647
+ function resolveFactor(factor) {
3648
+ return factor === "half-log" ? Math.sqrt(10) : factor;
3649
+ }
3650
+ function roundToSignificant(value, digits = 4) {
3651
+ if (value === 0) return 0;
3652
+ const magnitude = Math.floor(Math.log10(Math.abs(value))) + 1;
3653
+ const factor = Math.pow(10, digits - magnitude);
3654
+ return Math.round(value * factor) / factor;
3655
+ }
3656
+ function generateDilutionSeries(startConcentration, count, factor, defaultReplicates = 1) {
3657
+ if (count < 1 || startConcentration <= 0) return [];
3658
+ const divisor = resolveFactor(factor);
3659
+ const levels = [];
3660
+ for (let i = 0; i < count; i++) levels.push({
3661
+ value: roundToSignificant(startConcentration / Math.pow(divisor, i)),
3662
+ replicates: defaultReplicates
3663
+ });
3664
+ return levels;
3665
+ }
3666
+ /** Generates and sorts geometric dilution series (2x, 3x, ½ log, 10x) for plate-based experiments. */
3667
+ function useReagentSeries() {
3668
+ function generateSeries(start, count, factor, defaultReplicates = 1) {
3669
+ return generateDilutionSeries(start, count, factor, defaultReplicates);
3670
+ }
3671
+ function sortLevels(levels, order = "desc") {
3672
+ return [...levels].sort((a, b) => order === "desc" ? b.value - a.value : a.value - b.value);
3673
+ }
3674
+ function totalPositions(levels) {
3675
+ return levels.reduce((sum, l) => sum + l.replicates, 0);
3676
+ }
3677
+ return {
3678
+ generateSeries,
3679
+ sortLevels,
3680
+ totalPositions
3681
+ };
3682
+ }
3683
+ //#endregion
3684
+ //#region src/composables/useProtocolTemplates.ts
3685
+ var BUILT_IN_TEMPLATES = [
3686
+ {
3687
+ id: "builtin-incubation",
3688
+ type: "incubation",
3689
+ name: "Incubation",
3690
+ description: "Incubate samples at specified conditions",
3691
+ defaultDuration: 60,
3692
+ isBuiltIn: true,
3693
+ parameters: [
3694
+ {
3695
+ key: "temperature",
3696
+ label: "Temperature",
3697
+ type: "temperature",
3698
+ unit: "°C",
3699
+ required: true,
3700
+ default: 37,
3701
+ min: -80,
3702
+ max: 100
3703
+ },
3704
+ {
3705
+ key: "duration",
3706
+ label: "Duration",
3707
+ type: "duration",
3708
+ unit: "hours",
3709
+ required: true,
3710
+ default: 24,
3711
+ min: 0
3712
+ },
3713
+ {
3714
+ key: "co2",
3715
+ label: "CO2",
3716
+ type: "number",
3717
+ unit: "%",
3718
+ required: false,
3719
+ default: 5,
3720
+ min: 0,
3721
+ max: 100
3722
+ },
3723
+ {
3724
+ key: "humidity",
3725
+ label: "Humidity",
3726
+ type: "number",
3727
+ unit: "%",
3728
+ required: false,
3729
+ default: 95,
3730
+ min: 0,
3731
+ max: 100
3732
+ }
3733
+ ]
3734
+ },
3735
+ {
3736
+ id: "builtin-wash",
3737
+ type: "wash",
3738
+ name: "Wash",
3739
+ description: "Wash samples with buffer",
3740
+ defaultDuration: 5,
3741
+ isBuiltIn: true,
3742
+ parameters: [
3743
+ {
3744
+ key: "buffer",
3745
+ label: "Buffer",
3746
+ type: "select",
3747
+ required: true,
3748
+ default: "PBS",
3749
+ options: [
3750
+ {
3751
+ value: "PBS",
3752
+ label: "PBS"
3753
+ },
3754
+ {
3755
+ value: "PBST",
3756
+ label: "PBST"
3757
+ },
3758
+ {
3759
+ value: "TBS",
3760
+ label: "TBS"
3761
+ },
3762
+ {
3763
+ value: "TBST",
3764
+ label: "TBST"
3765
+ },
3766
+ {
3767
+ value: "Water",
3768
+ label: "Water"
3769
+ },
3770
+ {
3771
+ value: "Other",
3772
+ label: "Other"
3773
+ }
3774
+ ]
3775
+ },
3776
+ {
3777
+ key: "volume",
3778
+ label: "Volume",
3779
+ type: "number",
3780
+ unit: "µL",
3781
+ required: true,
3782
+ default: 200,
3783
+ min: 0
3784
+ },
3785
+ {
3786
+ key: "cycles",
3787
+ label: "Wash Cycles",
3788
+ type: "number",
3789
+ required: true,
3790
+ default: 3,
3791
+ min: 1,
3792
+ max: 10
3793
+ }
3794
+ ]
3795
+ },
3796
+ {
3797
+ id: "builtin-addition",
3798
+ type: "addition",
3799
+ name: "Addition",
3800
+ description: "Add reagent or compound to samples",
3801
+ defaultDuration: 5,
3802
+ isBuiltIn: true,
3803
+ parameters: [
3804
+ {
3805
+ key: "reagent",
3806
+ label: "Reagent",
3807
+ type: "text",
3808
+ required: true,
3809
+ placeholder: "Enter reagent name"
3810
+ },
3811
+ {
3812
+ key: "volume",
3813
+ label: "Volume",
3814
+ type: "number",
3815
+ unit: "µL",
3816
+ required: true,
3817
+ default: 100,
3818
+ min: 0
3819
+ },
3820
+ {
3821
+ key: "concentration",
3822
+ label: "Concentration",
3823
+ type: "concentration",
3824
+ required: false
3825
+ }
3826
+ ]
3827
+ },
3828
+ {
3829
+ id: "builtin-measurement",
3830
+ type: "measurement",
3831
+ name: "Measurement",
3832
+ description: "Take measurements using specified instrument",
3833
+ defaultDuration: 15,
3834
+ isBuiltIn: true,
3835
+ parameters: [
3836
+ {
3837
+ key: "instrument",
3838
+ label: "Instrument",
3839
+ type: "select",
3840
+ required: true,
3841
+ options: [
3842
+ {
3843
+ value: "plate_reader",
3844
+ label: "Plate Reader"
3845
+ },
3846
+ {
3847
+ value: "microscope",
3848
+ label: "Microscope"
3849
+ },
3850
+ {
3851
+ value: "flow_cytometer",
3852
+ label: "Flow Cytometer"
3853
+ },
3854
+ {
3855
+ value: "spectrophotometer",
3856
+ label: "Spectrophotometer"
3857
+ },
3858
+ {
3859
+ value: "other",
3860
+ label: "Other"
3861
+ }
3862
+ ]
3863
+ },
3864
+ {
3865
+ key: "readout",
3866
+ label: "Readout Type",
3867
+ type: "text",
3868
+ required: false,
3869
+ placeholder: "e.g., Absorbance 450nm"
3870
+ },
3871
+ {
3872
+ key: "notes",
3873
+ label: "Parameters",
3874
+ type: "text",
3875
+ required: false,
3876
+ placeholder: "Additional parameters"
3877
+ }
3878
+ ]
3879
+ },
3880
+ {
3881
+ id: "builtin-centrifuge",
3882
+ type: "centrifuge",
3883
+ name: "Centrifuge",
3884
+ description: "Centrifuge samples",
3885
+ defaultDuration: 10,
3886
+ isBuiltIn: true,
3887
+ parameters: [
3888
+ {
3889
+ key: "speed",
3890
+ label: "Speed",
3891
+ type: "number",
3892
+ unit: "RPM",
3893
+ required: true,
3894
+ default: 1e3,
3895
+ min: 100,
3896
+ max: 2e4
3897
+ },
3898
+ {
3899
+ key: "duration",
3900
+ label: "Duration",
3901
+ type: "number",
3902
+ unit: "min",
3903
+ required: true,
3904
+ default: 5,
3905
+ min: 1
3906
+ },
3907
+ {
3908
+ key: "temperature",
3909
+ label: "Temperature",
3910
+ type: "temperature",
3911
+ unit: "°C",
3912
+ required: false,
3913
+ default: 4,
3914
+ min: -10,
3915
+ max: 40
3916
+ }
3917
+ ]
3918
+ },
3919
+ {
3920
+ id: "builtin-transfer",
3921
+ type: "transfer",
3922
+ name: "Transfer",
3923
+ description: "Transfer samples between containers",
3924
+ defaultDuration: 10,
3925
+ isBuiltIn: true,
3926
+ parameters: [
3927
+ {
3928
+ key: "source",
3929
+ label: "Source",
3930
+ type: "text",
3931
+ required: true,
3932
+ placeholder: "Source container"
3933
+ },
3934
+ {
3935
+ key: "destination",
3936
+ label: "Destination",
3937
+ type: "text",
3938
+ required: true,
3939
+ placeholder: "Destination container"
3940
+ },
3941
+ {
3942
+ key: "volume",
3943
+ label: "Volume",
3944
+ type: "number",
3945
+ unit: "µL",
3946
+ required: true,
3947
+ default: 100,
3948
+ min: 0
3949
+ }
3950
+ ]
3951
+ },
3952
+ {
3953
+ id: "builtin-mix",
3954
+ type: "mix",
3955
+ name: "Mix",
3956
+ description: "Mix samples",
3957
+ defaultDuration: 5,
3958
+ isBuiltIn: true,
3959
+ parameters: [
3960
+ {
3961
+ key: "method",
3962
+ label: "Method",
3963
+ type: "select",
3964
+ required: true,
3965
+ default: "pipette",
3966
+ options: [
3967
+ {
3968
+ value: "pipette",
3969
+ label: "Pipette mixing"
3970
+ },
3971
+ {
3972
+ value: "vortex",
3973
+ label: "Vortex"
3974
+ },
3975
+ {
3976
+ value: "shaker",
3977
+ label: "Orbital shaker"
3978
+ },
3979
+ {
3980
+ value: "inversion",
3981
+ label: "Inversion"
3982
+ }
3983
+ ]
3984
+ },
3985
+ {
3986
+ key: "duration",
3987
+ label: "Duration",
3988
+ type: "number",
3989
+ unit: "sec",
3990
+ required: false,
3991
+ default: 30,
3992
+ min: 1
3993
+ },
3994
+ {
3995
+ key: "speed",
3996
+ label: "Speed/Intensity",
3997
+ type: "text",
3998
+ required: false,
3999
+ placeholder: "e.g., 300 RPM"
4000
+ }
4001
+ ]
4002
+ },
4003
+ {
4004
+ id: "builtin-custom",
4005
+ type: "custom",
4006
+ name: "Custom Step",
4007
+ description: "Custom protocol step",
4008
+ defaultDuration: 15,
4009
+ isBuiltIn: true,
4010
+ parameters: [{
4011
+ key: "description",
4012
+ label: "Description",
4013
+ type: "text",
4014
+ required: true,
4015
+ placeholder: "Describe the step"
4016
+ }, {
4017
+ key: "notes",
4018
+ label: "Notes",
4019
+ type: "text",
4020
+ required: false,
4021
+ placeholder: "Additional notes"
4022
+ }]
4023
+ }
4024
+ ];
4025
+ var STORAGE_KEY = "mint-custom-protocol-templates";
4026
+ function loadCustomTemplates() {
4027
+ try {
4028
+ const stored = localStorage.getItem(STORAGE_KEY);
4029
+ if (stored) return JSON.parse(stored);
4030
+ } catch {}
4031
+ return [];
4032
+ }
4033
+ function saveCustomTemplatesToStorage(templates) {
4034
+ try {
4035
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
4036
+ } catch {}
4037
+ }
4038
+ /** Provides built-in and custom protocol step templates with validation and step creation helpers. */
4039
+ function useProtocolTemplates() {
4040
+ const customTemplates = ref(loadCustomTemplates());
4041
+ const allTemplates = computed(() => {
4042
+ return [...BUILT_IN_TEMPLATES, ...customTemplates.value];
4043
+ });
4044
+ function getTemplateByType(type) {
4045
+ return customTemplates.value.find((t) => t.type === type) || BUILT_IN_TEMPLATES.find((t) => t.type === type);
4046
+ }
4047
+ function getTemplateById(id) {
4048
+ return customTemplates.value.find((t) => t.id === id) || BUILT_IN_TEMPLATES.find((t) => t.id === id);
4049
+ }
4050
+ function saveCustomTemplate(template) {
4051
+ const newTemplate = {
4052
+ ...template,
4053
+ isBuiltIn: false,
4054
+ id: template.id || `custom-${Date.now()}`
4055
+ };
4056
+ const index = customTemplates.value.findIndex((t) => t.id === newTemplate.id);
4057
+ if (index >= 0) customTemplates.value[index] = newTemplate;
4058
+ else customTemplates.value.push(newTemplate);
4059
+ saveCustomTemplatesToStorage(customTemplates.value);
4060
+ }
4061
+ function deleteCustomTemplate(templateId) {
4062
+ const index = customTemplates.value.findIndex((t) => t.id === templateId);
4063
+ if (index >= 0) {
4064
+ customTemplates.value.splice(index, 1);
4065
+ saveCustomTemplatesToStorage(customTemplates.value);
4066
+ }
4067
+ }
4068
+ function validateStep(step, template) {
4069
+ const errors = {};
4070
+ if (!step.name || step.name.trim() === "") errors.name = "Step name is required";
4071
+ for (const param of template.parameters) {
4072
+ if (param.required) {
4073
+ const value = step.parameters?.[param.key];
4074
+ if (value === void 0 || value === null || value === "") errors[param.key] = `${param.label} is required`;
4075
+ }
4076
+ if (param.type === "number" || param.type === "temperature" || param.type === "duration") {
4077
+ const value = step.parameters?.[param.key];
4078
+ if (value !== void 0 && value !== null && value !== "") {
4079
+ const numValue = Number(value);
4080
+ if (isNaN(numValue)) errors[param.key] = `${param.label} must be a number`;
4081
+ else {
4082
+ if (param.min !== void 0 && numValue < param.min) errors[param.key] = `${param.label} must be at least ${param.min}`;
4083
+ if (param.max !== void 0 && numValue > param.max) errors[param.key] = `${param.label} must be at most ${param.max}`;
4084
+ }
4085
+ }
4086
+ }
4087
+ }
4088
+ return {
4089
+ valid: Object.keys(errors).length === 0,
4090
+ errors
4091
+ };
4092
+ }
4093
+ function createStepFromTemplate(template, overrides) {
4094
+ const parameters = {};
4095
+ for (const param of template.parameters) if (param.default !== void 0) parameters[param.key] = param.default;
4096
+ return {
4097
+ id: `step-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
4098
+ type: template.type,
4099
+ name: template.name,
4100
+ description: template.description,
4101
+ duration: template.defaultDuration,
4102
+ status: "pending",
4103
+ parameters,
4104
+ order: 0,
4105
+ ...overrides
4106
+ };
4107
+ }
4108
+ function formatParameterValue(value, param) {
4109
+ if (value === void 0 || value === null || value === "") return "-";
4110
+ if (param.type === "select" && param.options) {
4111
+ const option = param.options.find((o) => o.value === value);
4112
+ if (option) return option.label;
4113
+ }
4114
+ if (param.type === "concentration" && typeof value === "object") {
4115
+ const conc = value;
4116
+ return `${conc.value} ${conc.unit}`;
4117
+ }
4118
+ const strValue = String(value);
4119
+ if (param.unit) return `${strValue} ${param.unit}`;
4120
+ return strValue;
4121
+ }
4122
+ return {
4123
+ builtInTemplates: BUILT_IN_TEMPLATES,
4124
+ customTemplates,
4125
+ allTemplates,
4126
+ getTemplateByType,
4127
+ getTemplateById,
4128
+ saveCustomTemplate,
4129
+ deleteCustomTemplate,
4130
+ validateStep,
4131
+ createStepFromTemplate,
4132
+ formatParameterValue
4133
+ };
4134
+ }
4135
+ //#endregion
4136
+ //#region src/composables/useExperimentData.ts
4137
+ /** Fetches and normalises experiment output data (tree, table, summary) from the platform API. */
4138
+ function useExperimentData(options = {}) {
4139
+ const api = useApi({ baseUrl: options.apiBaseUrl });
4140
+ const data = ref(null);
4141
+ const request = useRequestSyncState("Failed to fetch experiment data");
4142
+ const isLoading = request.loading;
4143
+ const error = request.error;
4144
+ const lastLoadedAt = request.lastLoadedAt;
4145
+ let lastExperimentId = null;
4146
+ const treeData = computed(() => {
4147
+ if (!data.value) return [];
4148
+ const tree = data.value.tree_data ?? data.value.treeData;
4149
+ return Array.isArray(tree) ? tree : [];
4150
+ });
4151
+ const tableData = computed(() => {
4152
+ if (!data.value) return [];
4153
+ const table = data.value.table_data ?? data.value.tableData;
4154
+ return Array.isArray(table) ? table : [];
4155
+ });
4156
+ const summaryData = computed(() => {
4157
+ if (!data.value) return null;
4158
+ const summary = data.value.summary_data ?? data.value.summaryData;
4159
+ if (summary && typeof summary === "object" && "metadata" in summary) return summary;
4160
+ return null;
4161
+ });
4162
+ async function fetchData(experimentId) {
4163
+ lastExperimentId = experimentId;
4164
+ try {
4165
+ data.value = await request.run(() => api.get(`/experiments/${experimentId}/data`), {
4166
+ success: "load",
4167
+ errorMessage: "Failed to fetch experiment data"
4168
+ });
4169
+ } catch {
4170
+ data.value = null;
4171
+ }
4172
+ }
4173
+ async function refresh() {
4174
+ if (lastExperimentId !== null) await fetchData(lastExperimentId);
4175
+ }
4176
+ return {
4177
+ data,
4178
+ treeData,
4179
+ tableData,
4180
+ summaryData,
4181
+ isLoading,
4182
+ error,
4183
+ lastLoadedAt,
4184
+ fetch: fetchData,
4185
+ refresh
4186
+ };
4187
+ }
4188
+ //#endregion
4189
+ //#region src/composables/useScheduleDrag.ts
4190
+ /** Handles pointer-driven create, move, and resize drag interactions for the ScheduleCalendar grid. */
4191
+ function useScheduleDrag(options) {
4192
+ const isDragging = ref(false);
4193
+ const dragState = ref(null);
4194
+ const ghost = computed(() => {
4195
+ if (!dragState.value) return null;
4196
+ const state = dragState.value;
4197
+ const slotH = options.slotHeight.value;
4198
+ const slotMin = options.slotDuration.value;
4199
+ const dayStart = options.dayStartHour.value * 60;
4200
+ if (state.type === "create") {
4201
+ const deltaY = state.currentY - state.startY;
4202
+ const startOffset = state.startY;
4203
+ const endOffset = startOffset + deltaY;
4204
+ const topPx = Math.min(startOffset, endOffset);
4205
+ const bottomPx = Math.max(startOffset, endOffset);
4206
+ const startMinutes = dayStart + Math.round(topPx / slotH * slotMin);
4207
+ const endMinutes = dayStart + Math.round(bottomPx / slotH * slotMin);
4208
+ const snappedStart = snapMinutes(startMinutes, slotMin);
4209
+ const snappedEnd = Math.max(snapMinutes(endMinutes, slotMin), snappedStart + slotMin);
4210
+ const snappedTopPx = (snappedStart - dayStart) / slotMin * slotH;
4211
+ const snappedHeightPx = (snappedEnd - snappedStart) / slotMin * slotH;
4212
+ return {
4213
+ start: minutesToDate(state.startDate, snappedStart),
4214
+ end: minutesToDate(state.startDate, snappedEnd),
4215
+ dayIndex: state.dayIndex,
4216
+ style: {
4217
+ top: `${snappedTopPx}px`,
4218
+ height: `${snappedHeightPx}px`
4219
+ }
4220
+ };
4221
+ }
4222
+ if (state.type === "move" && state.event) {
4223
+ const deltaY = state.currentY - state.startY;
4224
+ const eventStart = dateToMinutes(new Date(state.event.start));
4225
+ const duration = dateToMinutes(new Date(state.event.end)) - eventStart;
4226
+ const newStart = snapMinutes(eventStart + Math.round(deltaY / slotH * slotMin), slotMin);
4227
+ const clamped = clampRange(newStart, newStart + duration, dayStart, options.dayEndHour.value * 60);
4228
+ const topPx = (clamped.start - dayStart) / slotMin * slotH;
4229
+ const heightPx = (clamped.end - clamped.start) / slotMin * slotH;
4230
+ return {
4231
+ start: minutesToDate(state.startDate, clamped.start),
4232
+ end: minutesToDate(state.startDate, clamped.end),
4233
+ dayIndex: state.currentDayIndex,
4234
+ style: {
4235
+ top: `${topPx}px`,
4236
+ height: `${heightPx}px`
4237
+ }
4238
+ };
4239
+ }
4240
+ if ((state.type === "resize-top" || state.type === "resize-bottom") && state.event) {
4241
+ const eventStart = dateToMinutes(new Date(state.event.start));
4242
+ const eventEnd = dateToMinutes(new Date(state.event.end));
4243
+ const deltaY = state.currentY - state.startY;
4244
+ const pixelDelta = Math.round(deltaY / slotH * slotMin);
4245
+ let newStart = eventStart;
4246
+ let newEnd = eventEnd;
4247
+ if (state.type === "resize-top") {
4248
+ newStart = snapMinutes(eventStart + pixelDelta, slotMin);
4249
+ newStart = Math.min(newStart, newEnd - slotMin);
4250
+ } else {
4251
+ newEnd = snapMinutes(eventEnd + pixelDelta, slotMin);
4252
+ newEnd = Math.max(newEnd, newStart + slotMin);
4253
+ }
4254
+ const clamped = clampRange(newStart, newEnd, dayStart, options.dayEndHour.value * 60);
4255
+ const topPx = (clamped.start - dayStart) / slotMin * slotH;
4256
+ const heightPx = (clamped.end - clamped.start) / slotMin * slotH;
4257
+ return {
4258
+ start: minutesToDate(state.startDate, clamped.start),
4259
+ end: minutesToDate(state.startDate, clamped.end),
4260
+ dayIndex: state.dayIndex,
4261
+ style: {
4262
+ top: `${topPx}px`,
4263
+ height: `${heightPx}px`
4264
+ }
4265
+ };
4266
+ }
4267
+ return null;
4268
+ });
4269
+ function startCreate(date, y, dayIndex) {
4270
+ if (options.readonly.value) return;
4271
+ isDragging.value = true;
4272
+ dragState.value = {
4273
+ type: "create",
4274
+ startDate: date,
4275
+ startY: y,
4276
+ currentY: y,
4277
+ dayIndex,
4278
+ currentDayIndex: dayIndex
4279
+ };
4280
+ addListeners();
4281
+ }
4282
+ function startMove(event, y, dayIndex) {
4283
+ if (options.readonly.value || event.draggable === false) return;
4284
+ isDragging.value = true;
4285
+ dragState.value = {
4286
+ type: "move",
4287
+ event,
4288
+ startDate: new Date(event.start),
4289
+ startY: y,
4290
+ currentY: y,
4291
+ dayIndex,
4292
+ currentDayIndex: dayIndex
4293
+ };
4294
+ addListeners();
4295
+ }
4296
+ function startResize(event, edge, y, dayIndex) {
4297
+ if (options.readonly.value || event.resizable === false) return;
4298
+ isDragging.value = true;
4299
+ dragState.value = {
4300
+ type: edge === "top" ? "resize-top" : "resize-bottom",
4301
+ event,
4302
+ startDate: new Date(event.start),
4303
+ startY: y,
4304
+ currentY: y,
4305
+ dayIndex,
4306
+ currentDayIndex: dayIndex
4307
+ };
4308
+ addListeners();
4309
+ }
4310
+ function onPointerMove(e) {
4311
+ if (!dragState.value) return;
4312
+ dragState.value = {
4313
+ ...dragState.value,
4314
+ currentY: e.clientY
4315
+ };
4316
+ }
4317
+ function onPointerUp() {
4318
+ if (!dragState.value || !ghost.value) {
4319
+ cleanup();
4320
+ return;
4321
+ }
4322
+ const g = ghost.value;
4323
+ const state = dragState.value;
4324
+ if (state.type === "create" && options.onCreateComplete) options.onCreateComplete(g.start, g.end);
4325
+ else if (state.type === "move" && state.event && options.onMoveComplete) options.onMoveComplete(state.event, g.start, g.end);
4326
+ else if ((state.type === "resize-top" || state.type === "resize-bottom") && state.event && options.onResizeComplete) options.onResizeComplete(state.event, g.start, g.end);
4327
+ cleanup();
4328
+ }
4329
+ function addListeners() {
4330
+ document.addEventListener("pointermove", onPointerMove);
4331
+ document.addEventListener("pointerup", onPointerUp);
4332
+ }
4333
+ function cleanup() {
4334
+ isDragging.value = false;
4335
+ dragState.value = null;
4336
+ document.removeEventListener("pointermove", onPointerMove);
4337
+ document.removeEventListener("pointerup", onPointerUp);
4338
+ }
4339
+ onUnmounted(cleanup);
4340
+ return {
4341
+ isDragging,
4342
+ dragState,
4343
+ ghost,
4344
+ startCreate,
4345
+ startMove,
4346
+ startResize
4347
+ };
4348
+ }
4349
+ function snapMinutes(minutes, step) {
4350
+ return Math.round(minutes / step) * step;
4351
+ }
4352
+ function dateToMinutes(date) {
4353
+ return date.getHours() * 60 + date.getMinutes();
4354
+ }
4355
+ function minutesToDate(baseDate, minutes) {
4356
+ const d = new Date(baseDate);
4357
+ d.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
4358
+ return d;
4359
+ }
4360
+ function clampRange(start, end, min, max) {
4361
+ const s = Math.max(start, min);
4362
+ const e = Math.min(end, max);
4363
+ return {
4364
+ start: s,
4365
+ end: Math.max(e, s)
4366
+ };
4367
+ }
4368
+ //#endregion
4369
+ export { normalizeSearchQuery as $, useAutoGroup as A, EXPERIMENT_STATUS_VARIANT_MAP as B, useSampleGroups as C, DEFAULT_COLORS as D, hslToHex as E, usePlatformContext as F, useDebouncedWatch as G, datePresetToISO as H, useExperimentSelector as I, useFormBuilder as J, useApi as K, DATE_PRESET_OPTIONS as L, useDoseCalculator as M, APP_EXPERIMENT_KEY as N, extractSamplesFromDesignData as O, useAppExperiment as P, candidateMatchesSearch as Q, EXPERIMENT_STATUS_LABELS as R, useExpansionSet as S, hexToHsl as T, formatExperimentDate as U, SORT_OPTIONS as V, useRequestSyncState as W, useTheme as X, useForm as Y, useToast as Z, useTemplateCollection as _, DEFAULT_UNITS as a, getInjectedPlatformContext as b, useGroupAssignment as c, useBioTemplatePackWorkspace as d, useTextSearch as et, useBioTemplateWorkspace as f, useBioTemplateControls as g, useBioTemplateComponents as h, DEFAULT_PRESETS as i, useWellPlateEditor as j, parseCSV as k, useRackEditor as l, toBioTemplateComponentPropsByComponent as m, useExperimentData as n, useSortedItems as nt, generateDilutionSeries as o, getBioTemplateComponentProps as p, evaluateCondition as q, useProtocolTemplates as r, useReagentSeries as s, useScheduleDrag as t, compareSortValues as tt, useBioTemplatePresetWorkspace as u, useExperimentSave as v, deriveShade as w, resolveCurrentExperimentId as x, currentExperimentFromContext as y, EXPERIMENT_STATUS_OPTIONS as z };
4370
+
4371
+ //# sourceMappingURL=useScheduleDrag-D4oWdh41.js.map