@morscherlab/mint-sdk 1.0.0-alpha.2

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 (491) hide show
  1. package/README.md +326 -0
  2. package/dist/__stories__/experiment-helpers.d.ts +25 -0
  3. package/dist/__tests__/components/AppLayout.test.d.ts +1 -0
  4. package/dist/__tests__/components/AppSidebar.test.d.ts +1 -0
  5. package/dist/__tests__/components/AppTopBar.test.d.ts +1 -0
  6. package/dist/__tests__/components/BaseInput.test.d.ts +1 -0
  7. package/dist/__tests__/components/BasePill.test.d.ts +1 -0
  8. package/dist/__tests__/components/Calendar.test.d.ts +1 -0
  9. package/dist/__tests__/components/CollapsibleCard.test.d.ts +1 -0
  10. package/dist/__tests__/components/DataFrame.test.d.ts +1 -0
  11. package/dist/__tests__/components/DropdownButton.test.d.ts +1 -0
  12. package/dist/__tests__/composables/formBuilderRegistry.test.d.ts +1 -0
  13. package/dist/__tests__/composables/useAppExperiment.test.d.ts +1 -0
  14. package/dist/__tests__/composables/useAuth.test.d.ts +1 -0
  15. package/dist/__tests__/composables/useAutoGroup.test.d.ts +1 -0
  16. package/dist/__tests__/composables/useExperimentData.test.d.ts +13 -0
  17. package/dist/__tests__/composables/useExperimentSave.test.d.ts +1 -0
  18. package/dist/__tests__/composables/useForm.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFormBuilder.test.d.ts +1 -0
  20. package/dist/__tests__/composables/usePlatformContext.test.d.ts +1 -0
  21. package/dist/__tests__/composables/usePluginApi.test.d.ts +13 -0
  22. package/dist/__tests__/composables/usePluginConfig.test.d.ts +14 -0
  23. package/dist/__tests__/utils/color.test.d.ts +1 -0
  24. package/dist/auth-BYmxZdJl.js +297 -0
  25. package/dist/auth-BYmxZdJl.js.map +1 -0
  26. package/dist/components/AlertBox.vue.d.ts +34 -0
  27. package/dist/components/AppAvatarMenu.vue.d.ts +58 -0
  28. package/dist/components/AppContainer.vue.d.ts +28 -0
  29. package/dist/components/AppLayout.vue.d.ts +31 -0
  30. package/dist/components/AppPageSelector.vue.d.ts +43 -0
  31. package/dist/components/AppPillNav.vue.d.ts +11 -0
  32. package/dist/components/AppPluginSwitcher.vue.d.ts +38 -0
  33. package/dist/components/AppSidebar.vue.d.ts +47 -0
  34. package/dist/components/AppTopBar.vue.d.ts +111 -0
  35. package/dist/components/AuditTrail.vue.d.ts +38 -0
  36. package/dist/components/AutoGroupModal.vue.d.ts +124 -0
  37. package/dist/components/Avatar.vue.d.ts +14 -0
  38. package/dist/components/BaseButton.vue.d.ts +37 -0
  39. package/dist/components/BaseCheckbox.vue.d.ts +17 -0
  40. package/dist/components/BaseInput.vue.d.ts +34 -0
  41. package/dist/components/BaseModal.vue.d.ts +46 -0
  42. package/dist/components/BasePill.vue.d.ts +57 -0
  43. package/dist/components/BaseRadioGroup.vue.d.ts +21 -0
  44. package/dist/components/BaseSelect.vue.d.ts +20 -0
  45. package/dist/components/BaseSlider.vue.d.ts +22 -0
  46. package/dist/components/BaseTabs.vue.d.ts +14 -0
  47. package/dist/components/BaseTextarea.vue.d.ts +30 -0
  48. package/dist/components/BaseToggle.vue.d.ts +19 -0
  49. package/dist/components/BatchProgressList.vue.d.ts +43 -0
  50. package/dist/components/Breadcrumb.vue.d.ts +33 -0
  51. package/dist/components/Calendar.vue.d.ts +107 -0
  52. package/dist/components/ChartContainer.vue.d.ts +31 -0
  53. package/dist/components/ChemicalFormula.vue.d.ts +8 -0
  54. package/dist/components/CollapsibleCard.vue.d.ts +41 -0
  55. package/dist/components/ColorSlider.vue.d.ts +34 -0
  56. package/dist/components/ConcentrationInput.vue.d.ts +25 -0
  57. package/dist/components/ConfirmDialog.vue.d.ts +42 -0
  58. package/dist/components/DataFrame.vue.d.ts +107 -0
  59. package/dist/components/DatePicker.vue.d.ts +25 -0
  60. package/dist/components/DateTimePicker.vue.d.ts +30 -0
  61. package/dist/components/Divider.vue.d.ts +14 -0
  62. package/dist/components/DoseCalculator.vue.d.ts +19 -0
  63. package/dist/components/DropdownButton.vue.d.ts +47 -0
  64. package/dist/components/EmptyState.vue.d.ts +36 -0
  65. package/dist/components/ExperimentCodeBadge.vue.d.ts +14 -0
  66. package/dist/components/ExperimentDataViewer.vue.d.ts +29 -0
  67. package/dist/components/ExperimentPopover.vue.d.ts +32 -0
  68. package/dist/components/ExperimentSelectorModal.vue.d.ts +28 -0
  69. package/dist/components/ExperimentTimeline.vue.d.ts +44 -0
  70. package/dist/components/FileUploader.vue.d.ts +40 -0
  71. package/dist/components/FitPanel.vue.d.ts +46 -0
  72. package/dist/components/FormActions.vue.d.ts +33 -0
  73. package/dist/components/FormBuilder.vue.d.ts +287 -0
  74. package/dist/components/FormField.vue.d.ts +28 -0
  75. package/dist/components/FormFieldRenderer.vue.d.ts +31 -0
  76. package/dist/components/FormSection.vue.d.ts +43 -0
  77. package/dist/components/FormulaInput.vue.d.ts +25 -0
  78. package/dist/components/GroupAssigner.vue.d.ts +25 -0
  79. package/dist/components/GroupingModal.vue.d.ts +12 -0
  80. package/dist/components/IconButton.vue.d.ts +34 -0
  81. package/dist/components/LoadingSpinner.vue.d.ts +12 -0
  82. package/dist/components/MoleculeInput.vue.d.ts +27 -0
  83. package/dist/components/MultiSelect.vue.d.ts +19 -0
  84. package/dist/components/NumberInput.vue.d.ts +22 -0
  85. package/dist/components/PlateMapEditor.vue.d.ts +50 -0
  86. package/dist/components/ProgressBar.vue.d.ts +23 -0
  87. package/dist/components/ProtocolStepEditor.vue.d.ts +24 -0
  88. package/dist/components/RackEditor.vue.d.ts +40 -0
  89. package/dist/components/ReagentEditor.vue.d.ts +30 -0
  90. package/dist/components/ReagentList.vue.d.ts +32 -0
  91. package/dist/components/ResourceCard.vue.d.ts +50 -0
  92. package/dist/components/SampleHierarchyTree.vue.d.ts +26 -0
  93. package/dist/components/SampleLegend.vue.d.ts +32 -0
  94. package/dist/components/SampleSelector.vue.d.ts +29 -0
  95. package/dist/components/ScheduleCalendar.vue.d.ts +110 -0
  96. package/dist/components/ScientificNumber.vue.d.ts +14 -0
  97. package/dist/components/SegmentedControl.vue.d.ts +20 -0
  98. package/dist/components/SequenceInput.vue.d.ts +54 -0
  99. package/dist/components/SettingsButton.vue.d.ts +30 -0
  100. package/dist/components/SettingsModal.vue.d.ts +36 -0
  101. package/dist/components/Skeleton.vue.d.ts +11 -0
  102. package/dist/components/StatusIndicator.vue.d.ts +13 -0
  103. package/dist/components/StepWizard.vue.d.ts +65 -0
  104. package/dist/components/TagsInput.vue.d.ts +39 -0
  105. package/dist/components/ThemeToggle.vue.d.ts +7 -0
  106. package/dist/components/TimePicker.vue.d.ts +29 -0
  107. package/dist/components/TimeRangeInput.vue.d.ts +27 -0
  108. package/dist/components/ToastNotification.vue.d.ts +2 -0
  109. package/dist/components/Tooltip.vue.d.ts +35 -0
  110. package/dist/components/UnitInput.vue.d.ts +39 -0
  111. package/dist/components/WellEditPopup.vue.d.ts +25 -0
  112. package/dist/components/WellPlate.vue.d.ts +73 -0
  113. package/dist/components/index.d.ts +87 -0
  114. package/dist/components/index.js +3 -0
  115. package/dist/components-CKf-UpGi.js +15089 -0
  116. package/dist/components-CKf-UpGi.js.map +1 -0
  117. package/dist/composables/experiment-utils.d.ts +8 -0
  118. package/dist/composables/formBuilderRegistry.d.ts +13 -0
  119. package/dist/composables/index.d.ts +28 -0
  120. package/dist/composables/index.js +3 -0
  121. package/dist/composables/useApi.d.ts +20 -0
  122. package/dist/composables/useAppExperiment.d.ts +37 -0
  123. package/dist/composables/useAsync.d.ts +128 -0
  124. package/dist/composables/useAuth.d.ts +47 -0
  125. package/dist/composables/useAutoGroup.d.ts +106 -0
  126. package/dist/composables/useChemicalFormula.d.ts +21 -0
  127. package/dist/composables/useConcentrationUnits.d.ts +29 -0
  128. package/dist/composables/useDoseCalculator.d.ts +58 -0
  129. package/dist/composables/useExperimentData.d.ts +18 -0
  130. package/dist/composables/useExperimentSave.d.ts +36 -0
  131. package/dist/composables/useExperimentSelector.d.ts +30 -0
  132. package/dist/composables/useForm.d.ts +92 -0
  133. package/dist/composables/useFormBuilder.d.ts +24 -0
  134. package/dist/composables/usePasskey.d.ts +10 -0
  135. package/dist/composables/usePlatformContext.d.ts +131 -0
  136. package/dist/composables/usePluginApi.d.ts +29 -0
  137. package/dist/composables/usePluginConfig.d.ts +13 -0
  138. package/dist/composables/useProtocolTemplates.d.ts +44 -0
  139. package/dist/composables/useRackEditor.d.ts +31 -0
  140. package/dist/composables/useReagentSeries.d.ts +23 -0
  141. package/dist/composables/useScheduleDrag.d.ts +78 -0
  142. package/dist/composables/useSequenceUtils.d.ts +14 -0
  143. package/dist/composables/useTheme.d.ts +8 -0
  144. package/dist/composables/useTimeUtils.d.ts +29 -0
  145. package/dist/composables/useToast.d.ts +22 -0
  146. package/dist/composables/useWellPlateEditor.d.ts +33 -0
  147. package/dist/composables-D0QfFzq1.js +805 -0
  148. package/dist/composables-D0QfFzq1.js.map +1 -0
  149. package/dist/histoire.setup.d.ts +1 -0
  150. package/dist/index.d.ts +6 -0
  151. package/dist/index.js +7 -0
  152. package/dist/install.d.ts +16 -0
  153. package/dist/install.js +23 -0
  154. package/dist/install.js.map +1 -0
  155. package/dist/stores/auth.d.ts +146 -0
  156. package/dist/stores/index.d.ts +2 -0
  157. package/dist/stores/index.js +2 -0
  158. package/dist/stores/settings.d.ts +75 -0
  159. package/dist/styles.css +29728 -0
  160. package/dist/tailwind.preset.d.ts +58 -0
  161. package/dist/tailwind.preset.js +66 -0
  162. package/dist/tailwind.preset.js.map +1 -0
  163. package/dist/types/auth.d.ts +42 -0
  164. package/dist/types/auto-group.d.ts +34 -0
  165. package/dist/types/components.d.ts +528 -0
  166. package/dist/types/form-builder.d.ts +167 -0
  167. package/dist/types/index.d.ts +5 -0
  168. package/dist/types/index.js +0 -0
  169. package/dist/types/platform.d.ts +75 -0
  170. package/dist/useScheduleDrag-DAJueTbK.js +7181 -0
  171. package/dist/useScheduleDrag-DAJueTbK.js.map +1 -0
  172. package/dist/utils/color.d.ts +24 -0
  173. package/package.json +114 -0
  174. package/src/__stories__/experiment-helpers.ts +83 -0
  175. package/src/__tests__/components/AppLayout.test.ts +163 -0
  176. package/src/__tests__/components/AppSidebar.test.ts +292 -0
  177. package/src/__tests__/components/AppTopBar.test.ts +683 -0
  178. package/src/__tests__/components/BaseInput.test.ts +99 -0
  179. package/src/__tests__/components/BasePill.test.ts +291 -0
  180. package/src/__tests__/components/Calendar.test.ts +566 -0
  181. package/src/__tests__/components/CollapsibleCard.test.ts +524 -0
  182. package/src/__tests__/components/DataFrame.test.ts +767 -0
  183. package/src/__tests__/components/DropdownButton.test.ts +471 -0
  184. package/src/__tests__/composables/formBuilderRegistry.test.ts +187 -0
  185. package/src/__tests__/composables/useAppExperiment.test.ts +560 -0
  186. package/src/__tests__/composables/useAuth.test.ts +188 -0
  187. package/src/__tests__/composables/useAutoGroup.test.ts +860 -0
  188. package/src/__tests__/composables/useExperimentData.test.ts +127 -0
  189. package/src/__tests__/composables/useExperimentSave.test.ts +347 -0
  190. package/src/__tests__/composables/useForm.test.ts +205 -0
  191. package/src/__tests__/composables/useFormBuilder.test.ts +917 -0
  192. package/src/__tests__/composables/usePlatformContext.test.ts +116 -0
  193. package/src/__tests__/composables/usePluginApi.test.ts +81 -0
  194. package/src/__tests__/composables/usePluginConfig.test.ts +176 -0
  195. package/src/__tests__/utils/color.test.ts +96 -0
  196. package/src/components/AlertBox.story.vue +204 -0
  197. package/src/components/AlertBox.vue +88 -0
  198. package/src/components/AppAvatarMenu.story.vue +155 -0
  199. package/src/components/AppAvatarMenu.vue +184 -0
  200. package/src/components/AppContainer.story.vue +104 -0
  201. package/src/components/AppContainer.vue +34 -0
  202. package/src/components/AppLayout.story.vue +292 -0
  203. package/src/components/AppLayout.vue +75 -0
  204. package/src/components/AppPageSelector.vue +159 -0
  205. package/src/components/AppPillNav.vue +66 -0
  206. package/src/components/AppPluginSwitcher.vue +241 -0
  207. package/src/components/AppSidebar.story.vue +309 -0
  208. package/src/components/AppSidebar.vue +119 -0
  209. package/src/components/AppTopBar.story.vue +304 -0
  210. package/src/components/AppTopBar.vue +661 -0
  211. package/src/components/AuditTrail.story.vue +163 -0
  212. package/src/components/AuditTrail.vue +151 -0
  213. package/src/components/AutoGroupModal.story.vue +273 -0
  214. package/src/components/AutoGroupModal.vue +566 -0
  215. package/src/components/Avatar.story.vue +115 -0
  216. package/src/components/Avatar.vue +79 -0
  217. package/src/components/BaseButton.story.vue +96 -0
  218. package/src/components/BaseButton.vue +73 -0
  219. package/src/components/BaseCheckbox.story.vue +73 -0
  220. package/src/components/BaseCheckbox.vue +69 -0
  221. package/src/components/BaseInput.story.vue +98 -0
  222. package/src/components/BaseInput.vue +74 -0
  223. package/src/components/BaseModal.story.vue +237 -0
  224. package/src/components/BaseModal.vue +182 -0
  225. package/src/components/BasePill.story.vue +142 -0
  226. package/src/components/BasePill.vue +89 -0
  227. package/src/components/BaseRadioGroup.story.vue +145 -0
  228. package/src/components/BaseRadioGroup.vue +124 -0
  229. package/src/components/BaseSelect.story.vue +120 -0
  230. package/src/components/BaseSelect.vue +71 -0
  231. package/src/components/BaseSlider.story.vue +122 -0
  232. package/src/components/BaseSlider.vue +126 -0
  233. package/src/components/BaseTabs.story.vue +127 -0
  234. package/src/components/BaseTabs.vue +59 -0
  235. package/src/components/BaseTextarea.story.vue +91 -0
  236. package/src/components/BaseTextarea.vue +62 -0
  237. package/src/components/BaseToggle.story.vue +81 -0
  238. package/src/components/BaseToggle.vue +76 -0
  239. package/src/components/BatchProgressList.story.vue +92 -0
  240. package/src/components/BatchProgressList.vue +184 -0
  241. package/src/components/Breadcrumb.story.vue +106 -0
  242. package/src/components/Breadcrumb.vue +75 -0
  243. package/src/components/Calendar.story.vue +106 -0
  244. package/src/components/Calendar.vue +363 -0
  245. package/src/components/ChartContainer.story.vue +113 -0
  246. package/src/components/ChartContainer.vue +64 -0
  247. package/src/components/ChemicalFormula.story.vue +102 -0
  248. package/src/components/ChemicalFormula.vue +39 -0
  249. package/src/components/CollapsibleCard.story.vue +135 -0
  250. package/src/components/CollapsibleCard.vue +167 -0
  251. package/src/components/ColorSlider.story.vue +120 -0
  252. package/src/components/ColorSlider.vue +164 -0
  253. package/src/components/ConcentrationInput.story.vue +77 -0
  254. package/src/components/ConcentrationInput.vue +185 -0
  255. package/src/components/ConfirmDialog.story.vue +248 -0
  256. package/src/components/ConfirmDialog.vue +93 -0
  257. package/src/components/DataFrame.story.vue +148 -0
  258. package/src/components/DataFrame.vue +419 -0
  259. package/src/components/DatePicker.story.vue +119 -0
  260. package/src/components/DatePicker.vue +330 -0
  261. package/src/components/DateTimePicker.story.vue +112 -0
  262. package/src/components/DateTimePicker.vue +392 -0
  263. package/src/components/Divider.story.vue +80 -0
  264. package/src/components/Divider.vue +49 -0
  265. package/src/components/DoseCalculator.story.vue +68 -0
  266. package/src/components/DoseCalculator.vue +476 -0
  267. package/src/components/DropdownButton.story.vue +102 -0
  268. package/src/components/DropdownButton.vue +181 -0
  269. package/src/components/EmptyState.story.vue +135 -0
  270. package/src/components/EmptyState.vue +69 -0
  271. package/src/components/ExperimentCodeBadge.story.vue +77 -0
  272. package/src/components/ExperimentCodeBadge.vue +64 -0
  273. package/src/components/ExperimentDataViewer.story.vue +174 -0
  274. package/src/components/ExperimentDataViewer.vue +288 -0
  275. package/src/components/ExperimentPopover.story.vue +384 -0
  276. package/src/components/ExperimentPopover.vue +241 -0
  277. package/src/components/ExperimentSelectorModal.story.vue +391 -0
  278. package/src/components/ExperimentSelectorModal.vue +387 -0
  279. package/src/components/ExperimentTimeline.story.vue +161 -0
  280. package/src/components/ExperimentTimeline.vue +382 -0
  281. package/src/components/FileUploader.story.vue +107 -0
  282. package/src/components/FileUploader.vue +386 -0
  283. package/src/components/FitPanel.story.vue +125 -0
  284. package/src/components/FitPanel.vue +120 -0
  285. package/src/components/FormActions.vue +92 -0
  286. package/src/components/FormBuilder.vue +214 -0
  287. package/src/components/FormField.story.vue +132 -0
  288. package/src/components/FormField.vue +59 -0
  289. package/src/components/FormFieldRenderer.vue +58 -0
  290. package/src/components/FormSection.vue +90 -0
  291. package/src/components/FormulaInput.story.vue +96 -0
  292. package/src/components/FormulaInput.vue +125 -0
  293. package/src/components/GroupAssigner.story.vue +83 -0
  294. package/src/components/GroupAssigner.vue +284 -0
  295. package/src/components/GroupingModal.story.vue +52 -0
  296. package/src/components/GroupingModal.vue +422 -0
  297. package/src/components/IconButton.story.vue +135 -0
  298. package/src/components/IconButton.vue +73 -0
  299. package/src/components/LoadingSpinner.story.vue +70 -0
  300. package/src/components/LoadingSpinner.vue +50 -0
  301. package/src/components/MoleculeInput.story.vue +66 -0
  302. package/src/components/MoleculeInput.vue +426 -0
  303. package/src/components/MultiSelect.story.vue +132 -0
  304. package/src/components/MultiSelect.vue +118 -0
  305. package/src/components/NumberInput.story.vue +122 -0
  306. package/src/components/NumberInput.vue +160 -0
  307. package/src/components/PlateMapEditor.story.vue +92 -0
  308. package/src/components/PlateMapEditor.vue +513 -0
  309. package/src/components/ProgressBar.story.vue +148 -0
  310. package/src/components/ProgressBar.vue +114 -0
  311. package/src/components/ProtocolStepEditor.story.vue +69 -0
  312. package/src/components/ProtocolStepEditor.vue +522 -0
  313. package/src/components/RackEditor.story.vue +100 -0
  314. package/src/components/RackEditor.vue +371 -0
  315. package/src/components/ReagentEditor.story.vue +153 -0
  316. package/src/components/ReagentEditor.vue +418 -0
  317. package/src/components/ReagentList.story.vue +137 -0
  318. package/src/components/ReagentList.vue +463 -0
  319. package/src/components/ResourceCard.story.vue +150 -0
  320. package/src/components/ResourceCard.vue +161 -0
  321. package/src/components/SampleHierarchyTree.story.vue +161 -0
  322. package/src/components/SampleHierarchyTree.vue +256 -0
  323. package/src/components/SampleLegend.story.vue +91 -0
  324. package/src/components/SampleLegend.vue +119 -0
  325. package/src/components/SampleSelector.story.vue +111 -0
  326. package/src/components/SampleSelector.vue +1033 -0
  327. package/src/components/ScheduleCalendar.story.vue +195 -0
  328. package/src/components/ScheduleCalendar.vue +569 -0
  329. package/src/components/ScientificNumber.story.vue +127 -0
  330. package/src/components/ScientificNumber.vue +197 -0
  331. package/src/components/SegmentedControl.story.vue +132 -0
  332. package/src/components/SegmentedControl.vue +79 -0
  333. package/src/components/SequenceInput.story.vue +119 -0
  334. package/src/components/SequenceInput.vue +209 -0
  335. package/src/components/SettingsButton.story.vue +58 -0
  336. package/src/components/SettingsButton.vue +76 -0
  337. package/src/components/SettingsModal.story.vue +145 -0
  338. package/src/components/SettingsModal.vue +146 -0
  339. package/src/components/Skeleton.story.vue +141 -0
  340. package/src/components/Skeleton.vue +74 -0
  341. package/src/components/StatusIndicator.story.vue +99 -0
  342. package/src/components/StatusIndicator.vue +40 -0
  343. package/src/components/StepWizard.story.vue +155 -0
  344. package/src/components/StepWizard.vue +223 -0
  345. package/src/components/TagsInput.story.vue +155 -0
  346. package/src/components/TagsInput.vue +265 -0
  347. package/src/components/ThemeToggle.story.vue +36 -0
  348. package/src/components/ThemeToggle.vue +54 -0
  349. package/src/components/TimePicker.story.vue +96 -0
  350. package/src/components/TimePicker.vue +273 -0
  351. package/src/components/TimeRangeInput.story.vue +104 -0
  352. package/src/components/TimeRangeInput.vue +122 -0
  353. package/src/components/ToastNotification.story.vue +157 -0
  354. package/src/components/ToastNotification.vue +62 -0
  355. package/src/components/Tooltip.story.vue +138 -0
  356. package/src/components/Tooltip.vue +119 -0
  357. package/src/components/UnitInput.story.vue +194 -0
  358. package/src/components/UnitInput.vue +213 -0
  359. package/src/components/WellEditPopup.vue +234 -0
  360. package/src/components/WellPlate.story.vue +282 -0
  361. package/src/components/WellPlate.vue +830 -0
  362. package/src/components/index.ts +118 -0
  363. package/src/composables/experiment-utils.ts +57 -0
  364. package/src/composables/formBuilderRegistry.ts +79 -0
  365. package/src/composables/index.ts +140 -0
  366. package/src/composables/useApi.ts +167 -0
  367. package/src/composables/useAppExperiment.ts +159 -0
  368. package/src/composables/useAsync.ts +323 -0
  369. package/src/composables/useAuth.ts +445 -0
  370. package/src/composables/useAutoGroup.ts +641 -0
  371. package/src/composables/useChemicalFormula.ts +275 -0
  372. package/src/composables/useConcentrationUnits.ts +246 -0
  373. package/src/composables/useDoseCalculator.ts +370 -0
  374. package/src/composables/useExperimentData.ts +86 -0
  375. package/src/composables/useExperimentSave.ts +192 -0
  376. package/src/composables/useExperimentSelector.ts +292 -0
  377. package/src/composables/useForm.ts +416 -0
  378. package/src/composables/useFormBuilder.ts +383 -0
  379. package/src/composables/usePasskey.ts +216 -0
  380. package/src/composables/usePlatformContext.ts +299 -0
  381. package/src/composables/usePluginApi.ts +39 -0
  382. package/src/composables/usePluginConfig.ts +93 -0
  383. package/src/composables/useProtocolTemplates.ts +518 -0
  384. package/src/composables/useRackEditor.ts +222 -0
  385. package/src/composables/useReagentSeries.ts +91 -0
  386. package/src/composables/useScheduleDrag.ts +245 -0
  387. package/src/composables/useSequenceUtils.ts +105 -0
  388. package/src/composables/useTheme.ts +58 -0
  389. package/src/composables/useTimeUtils.ts +131 -0
  390. package/src/composables/useToast.ts +40 -0
  391. package/src/composables/useWellPlateEditor.ts +421 -0
  392. package/src/histoire.setup.ts +17 -0
  393. package/src/index.ts +367 -0
  394. package/src/install.ts +32 -0
  395. package/src/stores/auth.ts +152 -0
  396. package/src/stores/index.ts +2 -0
  397. package/src/stores/settings.ts +218 -0
  398. package/src/styles/components/alert-box.css +150 -0
  399. package/src/styles/components/app-avatar-menu.css +155 -0
  400. package/src/styles/components/app-container.css +33 -0
  401. package/src/styles/components/app-layout.css +98 -0
  402. package/src/styles/components/app-page-selector.css +191 -0
  403. package/src/styles/components/app-pill-nav.css +57 -0
  404. package/src/styles/components/app-plugin-switcher.css +209 -0
  405. package/src/styles/components/app-sidebar.css +145 -0
  406. package/src/styles/components/app-top-bar.css +492 -0
  407. package/src/styles/components/audit-trail.css +143 -0
  408. package/src/styles/components/auto-group-modal.css +644 -0
  409. package/src/styles/components/avatar.css +73 -0
  410. package/src/styles/components/batch-progress-list.css +196 -0
  411. package/src/styles/components/breadcrumb.css +64 -0
  412. package/src/styles/components/button.css +188 -0
  413. package/src/styles/components/calendar.css +192 -0
  414. package/src/styles/components/chart-container.css +69 -0
  415. package/src/styles/components/checkbox.css +123 -0
  416. package/src/styles/components/chemical-formula.css +46 -0
  417. package/src/styles/components/collapsible-card.css +253 -0
  418. package/src/styles/components/color-slider.css +110 -0
  419. package/src/styles/components/concentration-input.css +156 -0
  420. package/src/styles/components/confirm-dialog.css +183 -0
  421. package/src/styles/components/dataframe.css +382 -0
  422. package/src/styles/components/date-picker.css +243 -0
  423. package/src/styles/components/datetime-picker.css +229 -0
  424. package/src/styles/components/divider.css +63 -0
  425. package/src/styles/components/dose-calculator.css +301 -0
  426. package/src/styles/components/dropdown-button.css +280 -0
  427. package/src/styles/components/empty-state.css +151 -0
  428. package/src/styles/components/experiment-code-badge.css +33 -0
  429. package/src/styles/components/experiment-data-viewer.css +138 -0
  430. package/src/styles/components/experiment-popover.css +562 -0
  431. package/src/styles/components/experiment-selector-modal.css +285 -0
  432. package/src/styles/components/experiment-timeline.css +529 -0
  433. package/src/styles/components/file-uploader.css +310 -0
  434. package/src/styles/components/fit-panel.css +67 -0
  435. package/src/styles/components/form-builder.css +69 -0
  436. package/src/styles/components/form-field.css +48 -0
  437. package/src/styles/components/formula-input.css +103 -0
  438. package/src/styles/components/group-assigner.css +200 -0
  439. package/src/styles/components/grouping-modal.css +323 -0
  440. package/src/styles/components/icon-button.css +192 -0
  441. package/src/styles/components/input.css +66 -0
  442. package/src/styles/components/loading-spinner.css +67 -0
  443. package/src/styles/components/modal.css +350 -0
  444. package/src/styles/components/molecule-input.css +186 -0
  445. package/src/styles/components/multi-select.css +131 -0
  446. package/src/styles/components/number-input.css +199 -0
  447. package/src/styles/components/pill.css +188 -0
  448. package/src/styles/components/plate-map-editor.css +464 -0
  449. package/src/styles/components/progress-bar.css +133 -0
  450. package/src/styles/components/protocol-step-editor.css +449 -0
  451. package/src/styles/components/rack-editor.css +265 -0
  452. package/src/styles/components/radio-group.css +240 -0
  453. package/src/styles/components/reagent-editor.css +510 -0
  454. package/src/styles/components/reagent-list.css +407 -0
  455. package/src/styles/components/resource-card.css +360 -0
  456. package/src/styles/components/sample-hierarchy-tree.css +314 -0
  457. package/src/styles/components/sample-legend.css +201 -0
  458. package/src/styles/components/sample-selector.css +751 -0
  459. package/src/styles/components/schedule-calendar.css +478 -0
  460. package/src/styles/components/scientific-number.css +63 -0
  461. package/src/styles/components/segmented-control.css +197 -0
  462. package/src/styles/components/select.css +77 -0
  463. package/src/styles/components/sequence-input.css +184 -0
  464. package/src/styles/components/settings-button.css +94 -0
  465. package/src/styles/components/settings-modal.css +95 -0
  466. package/src/styles/components/skeleton.css +49 -0
  467. package/src/styles/components/slider.css +74 -0
  468. package/src/styles/components/status-indicator.css +66 -0
  469. package/src/styles/components/step-wizard.css +192 -0
  470. package/src/styles/components/tabs.css +95 -0
  471. package/src/styles/components/tags-input.css +195 -0
  472. package/src/styles/components/textarea.css +82 -0
  473. package/src/styles/components/theme-toggle.css +69 -0
  474. package/src/styles/components/time-picker.css +171 -0
  475. package/src/styles/components/time-range-input.css +42 -0
  476. package/src/styles/components/toast.css +91 -0
  477. package/src/styles/components/toggle.css +146 -0
  478. package/src/styles/components/tooltip.css +91 -0
  479. package/src/styles/components/unit-input.css +123 -0
  480. package/src/styles/components/well-edit-popup.css +252 -0
  481. package/src/styles/components/well-plate.css +307 -0
  482. package/src/styles/index.css +87 -0
  483. package/src/styles/variables.css +1117 -0
  484. package/src/tailwind.preset.ts +61 -0
  485. package/src/types/auth.ts +55 -0
  486. package/src/types/auto-group.ts +40 -0
  487. package/src/types/components.ts +710 -0
  488. package/src/types/form-builder.ts +197 -0
  489. package/src/types/index.ts +207 -0
  490. package/src/types/platform.ts +116 -0
  491. package/src/utils/color.ts +96 -0
@@ -0,0 +1,275 @@
1
+ // Types
2
+ export interface ParsedElement {
3
+ symbol: string
4
+ count: number
5
+ }
6
+
7
+ export interface FormulaParseResult {
8
+ elements: Record<string, number>
9
+ valid: boolean
10
+ error?: string
11
+ }
12
+
13
+ export type FormulaPartType = 'element' | 'subscript' | 'superscript' | 'paren' | 'dot' | 'charge'
14
+
15
+ export interface FormulaPart {
16
+ type: FormulaPartType
17
+ text: string
18
+ }
19
+
20
+ // ~30 common elements with atomic weights
21
+ export const ATOMIC_WEIGHTS: Record<string, number> = {
22
+ H: 1.008, He: 4.003, Li: 6.941, Be: 9.012, B: 10.81,
23
+ C: 12.011, N: 14.007, O: 15.999, F: 18.998, Ne: 20.180,
24
+ Na: 22.990, Mg: 24.305, Al: 26.982, Si: 28.086, P: 30.974,
25
+ S: 32.065, Cl: 35.453, Ar: 39.948, K: 39.098, Ca: 40.078,
26
+ Mn: 54.938, Fe: 55.845, Co: 58.933, Ni: 58.693, Cu: 63.546,
27
+ Zn: 65.38, Br: 79.904, Ag: 107.868, I: 126.904, Au: 196.967,
28
+ }
29
+
30
+ function parseFormulaSegment(formula: string): FormulaParseResult {
31
+ const elements: Record<string, number> = {}
32
+ const stack: Record<string, number>[] = [{}]
33
+ let i = 0
34
+
35
+ while (i < formula.length) {
36
+ const ch = formula[i]
37
+
38
+ if (ch === '(') {
39
+ stack.push({})
40
+ i++
41
+ } else if (ch === ')') {
42
+ if (stack.length < 2) {
43
+ return { elements: {}, valid: false, error: 'Unmatched closing parenthesis' }
44
+ }
45
+ i++
46
+ // Read multiplier after closing paren
47
+ let numStr = ''
48
+ while (i < formula.length && /\d/.test(formula[i])) {
49
+ numStr += formula[i]
50
+ i++
51
+ }
52
+ const multiplier = numStr ? parseInt(numStr, 10) : 1
53
+ const top = stack.pop()!
54
+ const current = stack[stack.length - 1]
55
+ for (const [el, count] of Object.entries(top)) {
56
+ current[el] = (current[el] || 0) + count * multiplier
57
+ }
58
+ } else if (/[A-Z]/.test(ch)) {
59
+ // Element symbol: uppercase letter optionally followed by lowercase
60
+ let symbol = ch
61
+ i++
62
+ while (i < formula.length && /[a-z]/.test(formula[i])) {
63
+ symbol += formula[i]
64
+ i++
65
+ }
66
+ // Validate element
67
+ if (!(symbol in ATOMIC_WEIGHTS)) {
68
+ return { elements: {}, valid: false, error: `Unknown element: ${symbol}` }
69
+ }
70
+ // Read count
71
+ let numStr = ''
72
+ while (i < formula.length && /\d/.test(formula[i])) {
73
+ numStr += formula[i]
74
+ i++
75
+ }
76
+ const count = numStr ? parseInt(numStr, 10) : 1
77
+ const current = stack[stack.length - 1]
78
+ current[symbol] = (current[symbol] || 0) + count
79
+ } else {
80
+ return { elements: {}, valid: false, error: `Unexpected character: ${ch}` }
81
+ }
82
+ }
83
+
84
+ if (stack.length !== 1) {
85
+ return { elements: {}, valid: false, error: 'Unmatched opening parenthesis' }
86
+ }
87
+
88
+ Object.assign(elements, stack[0])
89
+ return { elements, valid: true }
90
+ }
91
+
92
+ /** Parses and renders chemical formulas, computes molecular weight, and generates rich-text formula parts. */
93
+ export function useChemicalFormula() {
94
+ function parseFormula(formula: string): FormulaParseResult {
95
+ const trimmed = formula.trim()
96
+ if (!trimmed) {
97
+ return { elements: {}, valid: false, error: 'Empty formula' }
98
+ }
99
+
100
+ // Split on hydrate separator (· or .)
101
+ const parts = trimmed.split(/[·.]/)
102
+ const combined: Record<string, number> = {}
103
+
104
+ for (const part of parts) {
105
+ const segment = part.trim()
106
+ if (!segment) continue
107
+
108
+ // Check for leading coefficient (e.g., 5H2O)
109
+ let coefficient = 1
110
+ let formulaStr = segment
111
+ const coeffMatch = segment.match(/^(\d+)([A-Z].*)$/)
112
+ if (coeffMatch) {
113
+ coefficient = parseInt(coeffMatch[1], 10)
114
+ formulaStr = coeffMatch[2]
115
+ }
116
+
117
+ const result = parseFormulaSegment(formulaStr)
118
+ if (!result.valid) {
119
+ return result
120
+ }
121
+
122
+ for (const [el, count] of Object.entries(result.elements)) {
123
+ combined[el] = (combined[el] || 0) + count * coefficient
124
+ }
125
+ }
126
+
127
+ return { elements: combined, valid: true }
128
+ }
129
+
130
+ function calculateMW(elements: Record<string, number>): number {
131
+ let mw = 0
132
+ for (const [el, count] of Object.entries(elements)) {
133
+ const weight = ATOMIC_WEIGHTS[el]
134
+ if (weight !== undefined) {
135
+ mw += weight * count
136
+ }
137
+ }
138
+ return Math.round(mw * 1000) / 1000
139
+ }
140
+
141
+ function renderFormulaParts(formula: string): FormulaPart[] {
142
+ const parts: FormulaPart[] = []
143
+ const trimmed = formula.trim()
144
+ let i = 0
145
+
146
+ while (i < trimmed.length) {
147
+ const ch = trimmed[i]
148
+
149
+ // Hydrate dot separator
150
+ if (ch === '·' || (ch === '.' && i > 0 && /\d/.test(trimmed[i - 1]) && i + 1 < trimmed.length && /\d/.test(trimmed[i + 1]))) {
151
+ // Disambiguate: '.' as hydrate separator only if followed by a digit then uppercase
152
+ // For simplicity, treat '·' always as dot
153
+ if (ch === '·') {
154
+ parts.push({ type: 'dot', text: '·' })
155
+ i++
156
+ continue
157
+ }
158
+ }
159
+
160
+ // Parentheses
161
+ if (ch === '(' || ch === ')') {
162
+ parts.push({ type: 'paren', text: ch })
163
+ i++
164
+ // After ')' check for subscript number
165
+ if (ch === ')' && i < trimmed.length && /\d/.test(trimmed[i])) {
166
+ let numStr = ''
167
+ while (i < trimmed.length && /\d/.test(trimmed[i])) {
168
+ numStr += trimmed[i]
169
+ i++
170
+ }
171
+ parts.push({ type: 'subscript', text: numStr })
172
+ }
173
+ continue
174
+ }
175
+
176
+ // Charge notation: ^2+, ^2-, 2+, 2- at end, or +, -
177
+ if (ch === '^') {
178
+ i++
179
+ let chargeStr = ''
180
+ while (i < trimmed.length && /[\d+\-]/.test(trimmed[i])) {
181
+ chargeStr += trimmed[i]
182
+ i++
183
+ }
184
+ if (chargeStr) {
185
+ parts.push({ type: 'charge', text: chargeStr })
186
+ }
187
+ continue
188
+ }
189
+
190
+ // Element symbol: uppercase followed by optional lowercase
191
+ if (/[A-Z]/.test(ch)) {
192
+ let symbol = ch
193
+ i++
194
+ while (i < trimmed.length && /[a-z]/.test(trimmed[i])) {
195
+ symbol += trimmed[i]
196
+ i++
197
+ }
198
+ parts.push({ type: 'element', text: symbol })
199
+
200
+ // Check for subscript number after element
201
+ if (i < trimmed.length && /\d/.test(trimmed[i])) {
202
+ let numStr = ''
203
+ while (i < trimmed.length && /\d/.test(trimmed[i])) {
204
+ numStr += trimmed[i]
205
+ i++
206
+ }
207
+ parts.push({ type: 'subscript', text: numStr })
208
+
209
+ // Check for charge after subscript (e.g., Fe2+ or SO4^2-)
210
+ if (i < trimmed.length && (trimmed[i] === '+' || trimmed[i] === '-')) {
211
+ parts.push({ type: 'charge', text: numStr + trimmed[i] })
212
+ // Remove the subscript we just added since it's actually part of charge
213
+ // Actually, for cases like Fe2+, the 2+ is the charge, not a subscript
214
+ // But for H2O the 2 is a subscript. We need context.
215
+ // Simplification: if followed by +/-, treat trailing digits as part of charge
216
+ parts.splice(parts.length - 2, 1) // Remove the subscript
217
+ i++
218
+ continue
219
+ }
220
+ }
221
+ continue
222
+ }
223
+
224
+ // Leading digit (hydrate coefficient like 5H2O)
225
+ if (/\d/.test(ch)) {
226
+ let numStr = ''
227
+ while (i < trimmed.length && /\d/.test(trimmed[i])) {
228
+ numStr += trimmed[i]
229
+ i++
230
+ }
231
+ // Could be a subscript or coefficient depending on context
232
+ // If next char is uppercase, it's a coefficient (render as element-like text)
233
+ // If at end or followed by +/-, it's a charge component
234
+ if (i < trimmed.length && (trimmed[i] === '+' || trimmed[i] === '-')) {
235
+ parts.push({ type: 'charge', text: numStr + trimmed[i] })
236
+ i++
237
+ } else {
238
+ parts.push({ type: 'subscript', text: numStr })
239
+ }
240
+ continue
241
+ }
242
+
243
+ // Standalone + or - (charge)
244
+ if (ch === '+' || ch === '-') {
245
+ parts.push({ type: 'charge', text: ch })
246
+ i++
247
+ continue
248
+ }
249
+
250
+ // Dot as hydrate separator
251
+ if (ch === '.') {
252
+ parts.push({ type: 'dot', text: '·' })
253
+ i++
254
+ continue
255
+ }
256
+
257
+ // Skip whitespace
258
+ if (/\s/.test(ch)) {
259
+ i++
260
+ continue
261
+ }
262
+
263
+ // Unknown character, skip
264
+ i++
265
+ }
266
+
267
+ return parts
268
+ }
269
+
270
+ return {
271
+ parseFormula,
272
+ calculateMW,
273
+ renderFormulaParts,
274
+ }
275
+ }
@@ -0,0 +1,246 @@
1
+ import { computed, type ComputedRef } from 'vue'
2
+
3
+ // Concentration unit types
4
+ export type MolarityUnit = 'pM' | 'nM' | 'µM' | 'mM' | 'M'
5
+ export type MassVolumeUnit = 'pg/mL' | 'ng/mL' | 'µg/mL' | 'mg/mL' | 'g/mL'
6
+ export type PercentageUnit = '% v/v' | '% w/v' | '% w/w'
7
+ export type ConcentrationUnit = MolarityUnit | MassVolumeUnit | PercentageUnit | string
8
+
9
+ export interface ConcentrationValue {
10
+ value: number
11
+ unit: ConcentrationUnit
12
+ }
13
+
14
+ export interface UnitCategory {
15
+ label: string
16
+ units: ConcentrationUnit[]
17
+ }
18
+
19
+ export interface UseConcentrationUnitsReturn {
20
+ unitCategories: ComputedRef<UnitCategory[]>
21
+ molarityUnits: MolarityUnit[]
22
+ massVolumeUnits: MassVolumeUnit[]
23
+ percentageUnits: PercentageUnit[]
24
+ convert: (value: number, from: ConcentrationUnit, to: ConcentrationUnit, mw?: number) => number | null
25
+ formatWithUnit: (concentration: ConcentrationValue, precision?: number) => string
26
+ parseConcentration: (input: string) => ConcentrationValue | null
27
+ getBaseUnit: (unit: ConcentrationUnit) => ConcentrationUnit
28
+ getConversionHint: (concentration: ConcentrationValue) => string | null
29
+ isMolarity: (unit: ConcentrationUnit) => boolean
30
+ isMassVolume: (unit: ConcentrationUnit) => boolean
31
+ isPercentage: (unit: ConcentrationUnit) => boolean
32
+ }
33
+
34
+ // Conversion factors to base unit (M for molarity, g/mL for mass/volume)
35
+ const MOLARITY_FACTORS: Record<MolarityUnit, number> = {
36
+ 'pM': 1e-12,
37
+ 'nM': 1e-9,
38
+ 'µM': 1e-6,
39
+ 'mM': 1e-3,
40
+ 'M': 1,
41
+ }
42
+
43
+ const MASS_VOLUME_FACTORS: Record<MassVolumeUnit, number> = {
44
+ 'pg/mL': 1e-12,
45
+ 'ng/mL': 1e-9,
46
+ 'µg/mL': 1e-6,
47
+ 'mg/mL': 1e-3,
48
+ 'g/mL': 1,
49
+ }
50
+
51
+ // Percentage conversion factors (for potential future use)
52
+ // const PERCENTAGE_FACTORS: Record<PercentageUnit, number> = {
53
+ // '% v/v': 0.01,
54
+ // '% w/v': 0.01,
55
+ // '% w/w': 0.01,
56
+ // }
57
+
58
+ const MOLARITY_UNITS: MolarityUnit[] = ['pM', 'nM', 'µM', 'mM', 'M']
59
+ const MASS_VOLUME_UNITS: MassVolumeUnit[] = ['pg/mL', 'ng/mL', 'µg/mL', 'mg/mL', 'g/mL']
60
+ const PERCENTAGE_UNITS: PercentageUnit[] = ['% v/v', '% w/v', '% w/w']
61
+
62
+ /** Converts between molarity, mass/volume, and percentage concentration units with formatting helpers. */
63
+ export function useConcentrationUnits(): UseConcentrationUnitsReturn {
64
+ const unitCategories = computed<UnitCategory[]>(() => [
65
+ { label: 'Molarity', units: MOLARITY_UNITS },
66
+ { label: 'Mass/Volume', units: MASS_VOLUME_UNITS },
67
+ { label: 'Percentage', units: PERCENTAGE_UNITS },
68
+ ])
69
+
70
+ function isMolarity(unit: ConcentrationUnit): unit is MolarityUnit {
71
+ return MOLARITY_UNITS.includes(unit as MolarityUnit)
72
+ }
73
+
74
+ function isMassVolume(unit: ConcentrationUnit): unit is MassVolumeUnit {
75
+ return MASS_VOLUME_UNITS.includes(unit as MassVolumeUnit)
76
+ }
77
+
78
+ function isPercentage(unit: ConcentrationUnit): unit is PercentageUnit {
79
+ return PERCENTAGE_UNITS.includes(unit as PercentageUnit)
80
+ }
81
+
82
+ function getBaseUnit(unit: ConcentrationUnit): ConcentrationUnit {
83
+ if (isMolarity(unit)) return 'M'
84
+ if (isMassVolume(unit)) return 'g/mL'
85
+ if (isPercentage(unit)) return '% w/v'
86
+ return unit
87
+ }
88
+
89
+ function convert(
90
+ value: number,
91
+ from: ConcentrationUnit,
92
+ to: ConcentrationUnit,
93
+ mw?: number
94
+ ): number | null {
95
+ if (from === to) return value
96
+ if (value === 0) return 0
97
+
98
+ // Same category conversion
99
+ if (isMolarity(from) && isMolarity(to)) {
100
+ const baseValue = value * MOLARITY_FACTORS[from]
101
+ return baseValue / MOLARITY_FACTORS[to]
102
+ }
103
+
104
+ if (isMassVolume(from) && isMassVolume(to)) {
105
+ const baseValue = value * MASS_VOLUME_FACTORS[from]
106
+ return baseValue / MASS_VOLUME_FACTORS[to]
107
+ }
108
+
109
+ if (isPercentage(from) && isPercentage(to)) {
110
+ // Percentage conversions within same category are direct
111
+ return value
112
+ }
113
+
114
+ // Cross-category conversion (requires molecular weight)
115
+ if (mw && mw > 0) {
116
+ // Molarity to Mass/Volume
117
+ if (isMolarity(from) && isMassVolume(to)) {
118
+ // Convert to M, then to g/mL using MW, then to target unit
119
+ const molarConc = value * MOLARITY_FACTORS[from]
120
+ const massConc = molarConc * mw // g/mL (MW in g/mol, concentration in mol/L = g/L)
121
+ return massConc / MASS_VOLUME_FACTORS[to]
122
+ }
123
+
124
+ // Mass/Volume to Molarity
125
+ if (isMassVolume(from) && isMolarity(to)) {
126
+ // Convert to g/mL, then to M using MW, then to target unit
127
+ const massConc = value * MASS_VOLUME_FACTORS[from]
128
+ const molarConc = massConc / mw // M (g/mL / g/mol = mol/L)
129
+ return molarConc / MOLARITY_FACTORS[to]
130
+ }
131
+ }
132
+
133
+ return null // Cannot convert without MW or between incompatible units
134
+ }
135
+
136
+ function formatWithUnit(concentration: ConcentrationValue, precision: number = 3): string {
137
+ const { value, unit } = concentration
138
+ if (value === 0) return `0 ${unit}`
139
+
140
+ // Use appropriate precision based on value magnitude
141
+ let formattedValue: string
142
+ if (Math.abs(value) >= 1000) {
143
+ formattedValue = value.toExponential(precision - 1)
144
+ } else if (Math.abs(value) < 0.001) {
145
+ formattedValue = value.toExponential(precision - 1)
146
+ } else {
147
+ formattedValue = value.toPrecision(precision)
148
+ }
149
+
150
+ // Remove trailing zeros after decimal point
151
+ formattedValue = formattedValue.replace(/\.?0+$/, '')
152
+ formattedValue = formattedValue.replace(/\.?0+e/, 'e')
153
+
154
+ return `${formattedValue} ${unit}`
155
+ }
156
+
157
+ function parseConcentration(input: string): ConcentrationValue | null {
158
+ const trimmed = input.trim()
159
+ if (!trimmed) return null
160
+
161
+ // Match number followed by optional unit
162
+ const match = trimmed.match(/^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*(.*)$/)
163
+ if (!match) return null
164
+
165
+ const value = parseFloat(match[1])
166
+ if (isNaN(value)) return null
167
+
168
+ let unit = match[2].trim()
169
+ if (!unit) {
170
+ return null // Unit is required
171
+ }
172
+
173
+ // Normalize common variations
174
+ unit = unit
175
+ .replace(/uM/i, 'µM')
176
+ .replace(/um/i, 'µM')
177
+ .replace(/ug\/ml/i, 'µg/mL')
178
+ .replace(/ng\/ml/i, 'ng/mL')
179
+ .replace(/mg\/ml/i, 'mg/mL')
180
+ .replace(/pg\/ml/i, 'pg/mL')
181
+ .replace(/g\/ml/i, 'g/mL')
182
+
183
+ return { value, unit }
184
+ }
185
+
186
+ function getConversionHint(concentration: ConcentrationValue): string | null {
187
+ const { value, unit } = concentration
188
+ if (value === 0) return null
189
+
190
+ // Find adjacent unit for conversion hint
191
+ if (isMolarity(unit)) {
192
+ const index = MOLARITY_UNITS.indexOf(unit)
193
+ // If value is large, suggest next higher unit
194
+ if (value >= 1000 && index < MOLARITY_UNITS.length - 1) {
195
+ const nextUnit = MOLARITY_UNITS[index + 1]
196
+ const converted = convert(value, unit, nextUnit)
197
+ if (converted !== null) {
198
+ return formatWithUnit({ value: converted, unit: nextUnit })
199
+ }
200
+ }
201
+ // If value is small, suggest next lower unit
202
+ if (value < 1 && index > 0) {
203
+ const prevUnit = MOLARITY_UNITS[index - 1]
204
+ const converted = convert(value, unit, prevUnit)
205
+ if (converted !== null) {
206
+ return formatWithUnit({ value: converted, unit: prevUnit })
207
+ }
208
+ }
209
+ }
210
+
211
+ if (isMassVolume(unit)) {
212
+ const index = MASS_VOLUME_UNITS.indexOf(unit)
213
+ if (value >= 1000 && index < MASS_VOLUME_UNITS.length - 1) {
214
+ const nextUnit = MASS_VOLUME_UNITS[index + 1]
215
+ const converted = convert(value, unit, nextUnit)
216
+ if (converted !== null) {
217
+ return formatWithUnit({ value: converted, unit: nextUnit })
218
+ }
219
+ }
220
+ if (value < 1 && index > 0) {
221
+ const prevUnit = MASS_VOLUME_UNITS[index - 1]
222
+ const converted = convert(value, unit, prevUnit)
223
+ if (converted !== null) {
224
+ return formatWithUnit({ value: converted, unit: prevUnit })
225
+ }
226
+ }
227
+ }
228
+
229
+ return null
230
+ }
231
+
232
+ return {
233
+ unitCategories,
234
+ molarityUnits: MOLARITY_UNITS,
235
+ massVolumeUnits: MASS_VOLUME_UNITS,
236
+ percentageUnits: PERCENTAGE_UNITS,
237
+ convert,
238
+ formatWithUnit,
239
+ parseConcentration,
240
+ getBaseUnit,
241
+ getConversionHint,
242
+ isMolarity,
243
+ isMassVolume,
244
+ isPercentage,
245
+ }
246
+ }