@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,830 @@
1
+ <script setup lang="ts">
2
+ /** Interactive 96/384-well plate grid with drag-to-select, heatmap overlays, per-well editing, and sample-color mapping. */
3
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
4
+ import type { WellPlateFormat, WellPlateSelectionMode, WellPlateSize, Well, HeatmapConfig, WellShape, WellEditField, WellEditData, WellLegendItem, ColumnCondition, RowCondition } from '../types'
5
+ import WellEditPopup from './WellEditPopup.vue'
6
+
7
+ interface Props {
8
+ modelValue?: string[]
9
+ format?: WellPlateFormat
10
+ wells?: Record<string, Partial<Well>>
11
+ selectionMode?: WellPlateSelectionMode
12
+ showLabels?: boolean
13
+ showWellIds?: boolean
14
+ showSampleTypeIndicator?: boolean
15
+ heatmap?: HeatmapConfig
16
+ sampleColors?: Record<string, string>
17
+ zoom?: number
18
+ disabled?: boolean
19
+ readonly?: boolean
20
+ size?: WellPlateSize
21
+ wellShape?: WellShape
22
+ showWellLabels?: boolean
23
+ showBadges?: boolean
24
+ editable?: boolean
25
+ editFields?: WellEditField[]
26
+ defaultInjectionVolume?: number
27
+ showLegend?: boolean
28
+ legendItems?: WellLegendItem[]
29
+ columnConditions?: ColumnCondition[]
30
+ rowConditions?: RowCondition[]
31
+ }
32
+
33
+ // Drag state for moving wells
34
+ const dragSourceWell = ref<string | null>(null)
35
+ const dragTargetWell = ref<string | null>(null)
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ modelValue: () => [],
39
+ format: 96,
40
+ wells: () => ({}),
41
+ selectionMode: 'multiple',
42
+ showLabels: true,
43
+ showWellIds: false,
44
+ showSampleTypeIndicator: false,
45
+ heatmap: () => ({ enabled: false }),
46
+ sampleColors: () => ({}),
47
+ zoom: 1,
48
+ disabled: false,
49
+ readonly: false,
50
+ size: 'md',
51
+ wellShape: 'rounded',
52
+ showWellLabels: false,
53
+ showBadges: false,
54
+ editable: false,
55
+ editFields: () => ['label', 'sampleType', 'injectionVolume', 'injectionCount', 'customMethod'],
56
+ defaultInjectionVolume: 5,
57
+ showLegend: false,
58
+ legendItems: undefined,
59
+ columnConditions: () => [],
60
+ rowConditions: () => [],
61
+ })
62
+
63
+ const emit = defineEmits<{
64
+ 'update:modelValue': [wellIds: string[]]
65
+ 'well-click': [wellId: string, event: MouseEvent]
66
+ 'well-hover': [wellId: string | null, event?: MouseEvent]
67
+ 'selection-change': [wellIds: string[]]
68
+ 'context-menu': [wellId: string, event: MouseEvent]
69
+ 'well-move': [sourceWellId: string, targetWellId: string]
70
+ 'well-edit': [wellId: string, data: WellEditData]
71
+ 'well-clear': [wellId: string]
72
+ }>()
73
+
74
+ const plateRef = ref<HTMLElement | null>(null)
75
+ const tableRef = ref<HTMLElement | null>(null)
76
+ const isDragging = ref(false)
77
+ const dragStart = ref<{ row: number; col: number } | null>(null)
78
+ const dragEnd = ref<{ row: number; col: number } | null>(null)
79
+ const hoveredWell = ref<string | null>(null)
80
+
81
+ // Edit popup state
82
+ const editingWellId = ref<string | null>(null)
83
+ const editPopupPosition = ref({ x: 0, y: 0 })
84
+
85
+ const PLATE_CONFIGS: Record<WellPlateFormat, { rows: number; cols: number }> = {
86
+ 6: { rows: 2, cols: 3 },
87
+ 12: { rows: 3, cols: 4 },
88
+ 24: { rows: 4, cols: 6 },
89
+ 48: { rows: 6, cols: 8 },
90
+ 54: { rows: 6, cols: 9 },
91
+ 96: { rows: 8, cols: 12 },
92
+ 384: { rows: 16, cols: 24 },
93
+ }
94
+
95
+ const plateConfig = computed(() => PLATE_CONFIGS[props.format])
96
+
97
+ const rowLabels = computed(() =>
98
+ Array.from({ length: plateConfig.value.rows }, (_, i) => String.fromCharCode(65 + i))
99
+ )
100
+
101
+ const colLabels = computed(() =>
102
+ Array.from({ length: plateConfig.value.cols }, (_, i) => i + 1)
103
+ )
104
+
105
+ const wellGrid = computed(() => {
106
+ const grid: Well[][] = []
107
+ for (let row = 0; row < plateConfig.value.rows; row++) {
108
+ const rowWells: Well[] = []
109
+ for (let col = 0; col < plateConfig.value.cols; col++) {
110
+ const id = `${rowLabels.value[row]}${col + 1}`
111
+ const wellData = props.wells[id] || {}
112
+ rowWells.push({
113
+ id,
114
+ row,
115
+ col,
116
+ state: wellData.state || 'empty',
117
+ sampleType: wellData.sampleType,
118
+ value: wellData.value,
119
+ metadata: wellData.metadata,
120
+ })
121
+ }
122
+ grid.push(rowWells)
123
+ }
124
+ return grid
125
+ })
126
+
127
+ const selectedWellSet = computed(() => new Set(props.modelValue))
128
+
129
+ const dragSelectedWells = computed(() => {
130
+ if (!isDragging.value || !dragStart.value || !dragEnd.value) return new Set<string>()
131
+
132
+ const minRow = Math.min(dragStart.value.row, dragEnd.value.row)
133
+ const maxRow = Math.max(dragStart.value.row, dragEnd.value.row)
134
+ const minCol = Math.min(dragStart.value.col, dragEnd.value.col)
135
+ const maxCol = Math.max(dragStart.value.col, dragEnd.value.col)
136
+
137
+ const wells = new Set<string>()
138
+ for (let row = minRow; row <= maxRow; row++) {
139
+ for (let col = minCol; col <= maxCol; col++) {
140
+ const id = `${rowLabels.value[row]}${col + 1}`
141
+ wells.add(id)
142
+ }
143
+ }
144
+ return wells
145
+ })
146
+
147
+ // Size presets with pixel values for reliable sizing
148
+ const sizeConfig = computed(() => {
149
+ const sizes = {
150
+ sm: { cellWidth: '40px', cellHeight: '40px', headerWidth: '32px', headerHeight: '32px', fontSize: '0.625rem', gap: '2px' },
151
+ md: { cellWidth: '56px', cellHeight: '32px', headerWidth: '32px', headerHeight: '24px', fontSize: '0.75rem', gap: '3px' },
152
+ lg: { cellWidth: '80px', cellHeight: '40px', headerWidth: '40px', headerHeight: '32px', fontSize: '0.875rem', gap: '4px' },
153
+ xl: { cellWidth: '96px', cellHeight: '48px', headerWidth: '48px', headerHeight: '40px', fontSize: '1rem', gap: '4px' },
154
+ fill: { cellWidth: '100%', cellHeight: '32px', headerWidth: '32px', headerHeight: '24px', fontSize: '0.75rem', gap: '2px' },
155
+ }
156
+ return sizes[props.size]
157
+ })
158
+
159
+ const isFillMode = computed(() => props.size === 'fill')
160
+
161
+ // Default legend items
162
+ const defaultLegendItems: WellLegendItem[] = [
163
+ { type: 'sample', label: 'Sample', color: '#10b981' },
164
+ { type: 'blank', label: 'Blank', color: '#f97316' },
165
+ { type: 'qc', label: 'QC', color: '#8b5cf6' },
166
+ ]
167
+
168
+ const activeLegendItems = computed(() => props.legendItems ?? defaultLegendItems)
169
+
170
+ // --- Condition header helpers ---
171
+
172
+ const hasColumnConditions = computed(() => props.columnConditions.length > 0)
173
+ const hasRowConditions = computed(() => props.rowConditions.length > 0)
174
+
175
+ // Map column index (1-based) → ColumnCondition
176
+ const colConditionMap = computed(() => {
177
+ const map = new Map<number, { condition: ColumnCondition; indexInGroup: number }>()
178
+ for (const cond of props.columnConditions) {
179
+ cond.cols.forEach((col, i) => {
180
+ map.set(col, { condition: cond, indexInGroup: i })
181
+ })
182
+ }
183
+ return map
184
+ })
185
+
186
+ // Map row letter → RowCondition
187
+ const rowConditionMap = computed(() => {
188
+ const map = new Map<string, { condition: RowCondition; indexInGroup: number }>()
189
+ for (const cond of props.rowConditions) {
190
+ cond.rows.forEach((row, i) => {
191
+ map.set(row, { condition: cond, indexInGroup: i })
192
+ })
193
+ }
194
+ return map
195
+ })
196
+
197
+ type ColSpan = { condition: ColumnCondition; colspan: number } | { gap: true; colspan: number }
198
+ type RowSpan = { condition: RowCondition; rowspan: number; startRow: number } | { gap: true; rowspan: number; startRow: number }
199
+
200
+ // Build column condition header spans for the label row (drug name + unit)
201
+ const colConditionSpans = computed<ColSpan[]>(() => {
202
+ const spans: ColSpan[] = []
203
+ const cols = colLabels.value
204
+ let i = 0
205
+ while (i < cols.length) {
206
+ const entry = colConditionMap.value.get(cols[i])
207
+ if (entry?.indexInGroup === 0) {
208
+ spans.push({ condition: entry.condition, colspan: entry.condition.cols.length })
209
+ i += entry.condition.cols.length
210
+ } else {
211
+ let gapCount = 0
212
+ while (i + gapCount < cols.length && !colConditionMap.value.has(cols[i + gapCount])) {
213
+ gapCount++
214
+ }
215
+ spans.push({ gap: true, colspan: gapCount || 1 })
216
+ i += gapCount || 1
217
+ }
218
+ }
219
+ return spans
220
+ })
221
+
222
+ // Build row condition spans for the label column (drug name rotated)
223
+ const rowConditionSpans = computed<RowSpan[]>(() => {
224
+ const spans: RowSpan[] = []
225
+ const rows = rowLabels.value
226
+ let i = 0
227
+ while (i < rows.length) {
228
+ const entry = rowConditionMap.value.get(rows[i])
229
+ if (entry?.indexInGroup === 0) {
230
+ spans.push({ condition: entry.condition, rowspan: entry.condition.rows.length, startRow: i })
231
+ i += entry.condition.rows.length
232
+ } else {
233
+ let gapCount = 0
234
+ while (i + gapCount < rows.length && !rowConditionMap.value.has(rows[i + gapCount])) {
235
+ gapCount++
236
+ }
237
+ spans.push({ gap: true, rowspan: gapCount || 1, startRow: i })
238
+ i += gapCount || 1
239
+ }
240
+ }
241
+ return spans
242
+ })
243
+
244
+ // Map row index → span info (only contains entries for first row of each span)
245
+ const rowConditionSpanByRow = computed(() => {
246
+ const map = new Map<number, RowSpan>()
247
+ for (const span of rowConditionSpans.value) {
248
+ map.set(span.startRow, span)
249
+ }
250
+ return map
251
+ })
252
+
253
+ // Compute gradient style for a condition cell: background opacity scales with concentration,
254
+ // text color adapts based on effective luminance against white background
255
+ function conditionGradientStyle(color: string, conc: number, concentrations: number[]): Record<string, string> {
256
+ const min = Math.min(...concentrations)
257
+ const max = Math.max(...concentrations)
258
+ const t = max <= min ? 1 : (conc - min) / (max - min)
259
+ const opacity = 0.25 + t * 0.50
260
+ const r = parseInt(color.slice(1, 3), 16)
261
+ const g = parseInt(color.slice(3, 5), 16)
262
+ const b = parseInt(color.slice(5, 7), 16)
263
+ // Blend with white to get effective RGB, then compute relative luminance
264
+ const er = 255 * (1 - opacity) + r * opacity
265
+ const eg = 255 * (1 - opacity) + g * opacity
266
+ const eb = 255 * (1 - opacity) + b * opacity
267
+ const luminance = (0.299 * er + 0.587 * eg + 0.114 * eb) / 255
268
+ return {
269
+ backgroundColor: `rgba(${r}, ${g}, ${b}, ${opacity})`,
270
+ color: luminance > 0.55 ? '#1e293b' : '#ffffff',
271
+ }
272
+ }
273
+
274
+ // Format concentration value (drop trailing zeros)
275
+ function formatConc(value: number): string {
276
+ if (value >= 1000) return `${value / 1000}k`
277
+ if (value < 0.01) return value.toExponential(0)
278
+ return String(value)
279
+ }
280
+
281
+ // Sample type colors (matching MSExpDesigner)
282
+ const defaultSampleTypeColors: Record<string, { bg: string; border: string }> = {
283
+ sample: { bg: 'rgba(16, 185, 129, 0.15)', border: 'rgba(16, 185, 129, 0.4)' },
284
+ control: { bg: 'rgba(59, 130, 246, 0.15)', border: 'rgba(59, 130, 246, 0.4)' },
285
+ blank: { bg: 'rgba(249, 115, 22, 0.15)', border: 'rgba(249, 115, 22, 0.4)' },
286
+ qc: { bg: 'rgba(139, 92, 246, 0.15)', border: 'rgba(139, 92, 246, 0.4)' },
287
+ }
288
+
289
+ const heatmapColors: Record<string, string[]> = {
290
+ viridis: ['#440154', '#482878', '#3e4989', '#31688e', '#26828e', '#1f9e89', '#35b779', '#6ece58', '#b5de2b', '#fde725'],
291
+ plasma: ['#0d0887', '#46039f', '#7201a8', '#9c179e', '#bd3786', '#d8576b', '#ed7953', '#fb9f3a', '#fdca26', '#f0f921'],
292
+ turbo: ['#30123b', '#4145ab', '#4675ed', '#39a2fc', '#1bcfd4', '#24e79e', '#71f05f', '#c1f034', '#f1c83c', '#f99538', '#e45a31', '#ba2512', '#7a0403'],
293
+ }
294
+
295
+ function getHeatmapColor(value: number | undefined): string | null {
296
+ if (!props.heatmap?.enabled || value === undefined) return null
297
+
298
+ const min = props.heatmap.min ?? 0
299
+ const max = props.heatmap.max ?? 1
300
+ const normalized = Math.max(0, Math.min(1, (value - min) / (max - min)))
301
+
302
+ const colors = props.heatmap.colorScale === 'custom' && props.heatmap.customColors?.length
303
+ ? props.heatmap.customColors
304
+ : heatmapColors[props.heatmap.colorScale || 'viridis']
305
+
306
+ const index = Math.min(Math.floor(normalized * (colors.length - 1)), colors.length - 1)
307
+ return colors[index]
308
+ }
309
+
310
+ function getWellClasses(well: Well): string[] {
311
+ const isWellSelected = isSelected(well.id)
312
+ const isDragOver = dragSelectedWells.value.has(well.id)
313
+ const isHovered = hoveredWell.value === well.id
314
+ const isDisabled = props.disabled || well.state === 'disabled'
315
+ const isDragSource = dragSourceWell.value === well.id
316
+ const isDragTarget = dragTargetWell.value === well.id
317
+
318
+ const classes = [
319
+ 'mld-well-plate__well',
320
+ props.wellShape === 'circle' ? 'mld-well-plate__well--circle' : 'mld-well-plate__well--rounded',
321
+ ]
322
+
323
+ if (props.selectionMode === 'drag' && well.sampleType) {
324
+ classes.push('mld-well-plate__well--draggable')
325
+ }
326
+
327
+ if (isDragSource) classes.push('mld-well-plate__well--drag-source')
328
+ else if (isDragTarget) classes.push('mld-well-plate__well--drag-target')
329
+ else if (isWellSelected) classes.push('mld-well-plate__well--selected')
330
+ else if (isDragOver) classes.push('mld-well-plate__well--drag-over')
331
+ else if (isHovered && !props.readonly) classes.push('mld-well-plate__well--hovered')
332
+
333
+ if (isDisabled) classes.push('mld-well-plate__well--disabled')
334
+ if (well.state === 'filled') classes.push('mld-well-plate__well--filled')
335
+
336
+ return classes
337
+ }
338
+
339
+ function getWellStyle(well: Well): Record<string, string> {
340
+ const heatmapColor = getHeatmapColor(well.value)
341
+ if (heatmapColor) {
342
+ return { backgroundColor: heatmapColor, border: '1px solid transparent' }
343
+ }
344
+
345
+ if (well.sampleType && props.sampleColors[well.sampleType]) {
346
+ const color = props.sampleColors[well.sampleType]
347
+ return {
348
+ backgroundColor: `${color}26`,
349
+ border: `1px solid ${color}66`,
350
+ }
351
+ }
352
+
353
+ if (well.sampleType && defaultSampleTypeColors[well.sampleType]) {
354
+ const colors = defaultSampleTypeColors[well.sampleType]
355
+ return {
356
+ backgroundColor: colors.bg,
357
+ border: `1px solid ${colors.border}`,
358
+ }
359
+ }
360
+
361
+ const borderStyle = well.state === 'filled' ? 'solid' : 'dashed'
362
+ return {
363
+ backgroundColor: 'var(--bg-tertiary)',
364
+ border: `1px ${borderStyle} var(--border-color)`,
365
+ }
366
+ }
367
+
368
+ function getSampleTypeIndicator(well: Well): string | null {
369
+ if (!props.showSampleTypeIndicator || !well.sampleType) return null
370
+ const typeMap: Record<string, string> = {
371
+ sample: 'S',
372
+ control: 'C',
373
+ blank: 'B',
374
+ qc: 'Q',
375
+ }
376
+ return typeMap[well.sampleType] || well.sampleType.charAt(0).toUpperCase()
377
+ }
378
+
379
+ function getWellLabel(well: Well): string | undefined {
380
+ if (!props.showWellLabels) return undefined
381
+ return well.metadata?.label as string | undefined
382
+ }
383
+
384
+ function getWellBadge(well: Well): { text: string; color: string } | null {
385
+ if (!props.showBadges || !well.metadata) return null
386
+ const count = well.metadata.injectionCount as number | undefined
387
+ if (count && count > 1) {
388
+ return { text: String(count), color: '#6366f1' }
389
+ }
390
+ if (well.metadata.customMethod) {
391
+ return { text: '+', color: '#ec4899' }
392
+ }
393
+ return null
394
+ }
395
+
396
+ function isSelected(wellId: string): boolean {
397
+ return selectedWellSet.value.has(wellId) || dragSelectedWells.value.has(wellId)
398
+ }
399
+
400
+ function handleWellClick(well: Well, event: MouseEvent) {
401
+ if (props.disabled || props.readonly) return
402
+
403
+ emit('well-click', well.id, event)
404
+
405
+ // When editable, open popup instead of modifying selection
406
+ if (props.editable) {
407
+ openEditPopup(well.id, event)
408
+ return
409
+ }
410
+
411
+ if (props.selectionMode === 'none') return
412
+
413
+ const isCurrentlySelected = selectedWellSet.value.has(well.id)
414
+ const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey
415
+
416
+ let newSelection: string[]
417
+ if (props.selectionMode === 'single') {
418
+ newSelection = isCurrentlySelected ? [] : [well.id]
419
+ } else if (isMultiSelect) {
420
+ newSelection = isCurrentlySelected
421
+ ? props.modelValue.filter(id => id !== well.id)
422
+ : [...props.modelValue, well.id]
423
+ } else {
424
+ newSelection = isCurrentlySelected && props.modelValue.length === 1 ? [] : [well.id]
425
+ }
426
+
427
+ emit('update:modelValue', newSelection)
428
+ emit('selection-change', newSelection)
429
+ }
430
+
431
+ function openEditPopup(wellId: string, event: MouseEvent) {
432
+ const target = event.currentTarget as HTMLElement
433
+ const rect = target.getBoundingClientRect()
434
+ editPopupPosition.value = {
435
+ x: rect.right + 8,
436
+ y: rect.top,
437
+ }
438
+ editingWellId.value = wellId
439
+ }
440
+
441
+ function handleEditSave(data: WellEditData) {
442
+ emit('well-edit', data.wellId, data)
443
+ editingWellId.value = null
444
+ }
445
+
446
+ function handleEditClear() {
447
+ if (editingWellId.value) {
448
+ emit('well-clear', editingWellId.value)
449
+ }
450
+ editingWellId.value = null
451
+ }
452
+
453
+ function handleEditClose() {
454
+ editingWellId.value = null
455
+ }
456
+
457
+ function handleWellMouseDown(well: Well, event: MouseEvent) {
458
+ if (props.disabled || props.readonly || props.selectionMode !== 'rectangle') return
459
+ if (props.editable) return
460
+ if (event.button !== 0) return
461
+
462
+ isDragging.value = true
463
+ dragStart.value = { row: well.row, col: well.col }
464
+ dragEnd.value = { row: well.row, col: well.col }
465
+ }
466
+
467
+ function handleWellMouseEnter(well: Well, event: MouseEvent) {
468
+ hoveredWell.value = well.id
469
+ emit('well-hover', well.id, event)
470
+
471
+ if (isDragging.value && props.selectionMode === 'rectangle') {
472
+ dragEnd.value = { row: well.row, col: well.col }
473
+ }
474
+ }
475
+
476
+ function handleWellMouseLeave() {
477
+ hoveredWell.value = null
478
+ emit('well-hover', null)
479
+ }
480
+
481
+ function handleMouseUp() {
482
+ if (!isDragging.value || props.selectionMode !== 'rectangle') return
483
+
484
+ const newSelection = Array.from(dragSelectedWells.value)
485
+ isDragging.value = false
486
+ dragStart.value = null
487
+ dragEnd.value = null
488
+
489
+ if (newSelection.length > 0) {
490
+ emit('update:modelValue', newSelection)
491
+ emit('selection-change', newSelection)
492
+ }
493
+ }
494
+
495
+ function handleContextMenu(well: Well, event: MouseEvent) {
496
+ event.preventDefault()
497
+ emit('context-menu', well.id, event)
498
+ }
499
+
500
+ // Drag handlers for moving well contents
501
+ function handleDragStart(well: Well, event: DragEvent) {
502
+ if (props.disabled || props.readonly || props.selectionMode !== 'drag') return
503
+ if (!well.sampleType) return
504
+
505
+ dragSourceWell.value = well.id
506
+ if (event.dataTransfer) {
507
+ event.dataTransfer.effectAllowed = 'move'
508
+ event.dataTransfer.setData('text/plain', well.id)
509
+ }
510
+ }
511
+
512
+ function handleDragOver(well: Well, event: DragEvent) {
513
+ if (props.selectionMode !== 'drag' || !dragSourceWell.value) return
514
+ event.preventDefault()
515
+ if (event.dataTransfer) {
516
+ event.dataTransfer.dropEffect = 'move'
517
+ }
518
+ dragTargetWell.value = well.id
519
+ }
520
+
521
+ function handleDragLeave() {
522
+ dragTargetWell.value = null
523
+ }
524
+
525
+ function handleDrop(well: Well, event: DragEvent) {
526
+ event.preventDefault()
527
+ if (props.selectionMode !== 'drag' || !dragSourceWell.value) return
528
+
529
+ const sourceId = dragSourceWell.value
530
+ const targetId = well.id
531
+
532
+ if (sourceId !== targetId) {
533
+ emit('well-move', sourceId, targetId)
534
+ }
535
+
536
+ dragSourceWell.value = null
537
+ dragTargetWell.value = null
538
+ }
539
+
540
+ function handleDragEnd() {
541
+ dragSourceWell.value = null
542
+ dragTargetWell.value = null
543
+ }
544
+
545
+ function handleKeyDown(event: KeyboardEvent) {
546
+ if (props.disabled || props.readonly) return
547
+
548
+ if (event.key === 'Escape') {
549
+ if (editingWellId.value) {
550
+ editingWellId.value = null
551
+ return
552
+ }
553
+ emit('update:modelValue', [])
554
+ emit('selection-change', [])
555
+ }
556
+
557
+ if ((event.key === 'a' || event.key === 'A') && (event.metaKey || event.ctrlKey)) {
558
+ event.preventDefault()
559
+ const allWells = wellGrid.value.flat().map(w => w.id)
560
+ emit('update:modelValue', allWells)
561
+ emit('selection-change', allWells)
562
+ }
563
+ }
564
+
565
+ onMounted(() => {
566
+ document.addEventListener('mouseup', handleMouseUp)
567
+ document.addEventListener('keydown', handleKeyDown)
568
+ })
569
+
570
+ onUnmounted(() => {
571
+ document.removeEventListener('mouseup', handleMouseUp)
572
+ document.removeEventListener('keydown', handleKeyDown)
573
+ })
574
+
575
+ const containerStyle = computed(() =>
576
+ isFillMode.value ? {} : {
577
+ transform: `scale(${props.zoom})`,
578
+ transformOrigin: 'top left',
579
+ }
580
+ )
581
+
582
+ const tableStyle = computed(() => ({
583
+ borderCollapse: 'separate' as const,
584
+ borderSpacing: sizeConfig.value.gap,
585
+ ...(isFillMode.value ? { width: '100%', tableLayout: 'fixed' as const } : {}),
586
+ }))
587
+ </script>
588
+
589
+ <template>
590
+ <div
591
+ ref="plateRef"
592
+ :class="['mld-well-plate', isFillMode ? 'mld-well-plate--fill' : 'mld-well-plate--inline']"
593
+ :style="containerStyle"
594
+ >
595
+ <div class="mld-well-plate__scroll">
596
+ <div class="mld-well-plate__content">
597
+ <table
598
+ ref="tableRef"
599
+ class="mld-well-plate__table"
600
+ role="grid"
601
+ :aria-label="`${props.format}-well plate`"
602
+ :style="tableStyle"
603
+ >
604
+ <!-- Column condition label row (drug name + unit) -->
605
+ <thead v-if="hasColumnConditions">
606
+ <tr>
607
+ <!-- Spacer for row header column -->
608
+ <th :style="{ width: sizeConfig.headerWidth }"></th>
609
+ <!-- Spacer for row condition column -->
610
+ <th v-if="hasRowConditions" :style="{ width: sizeConfig.headerWidth }"></th>
611
+ <template v-for="(span, idx) in colConditionSpans" :key="'clabel-' + idx">
612
+ <th
613
+ v-if="'condition' in span"
614
+ :colspan="span.colspan"
615
+ class="mld-well-plate__condition-label"
616
+ :style="{
617
+ height: sizeConfig.headerHeight,
618
+ fontSize: sizeConfig.fontSize,
619
+ color: span.condition.color,
620
+ }"
621
+ >
622
+ {{ span.condition.label }}<template v-if="span.condition.unit"> ({{ span.condition.unit }})</template>
623
+ </th>
624
+ <th v-else :colspan="span.colspan"></th>
625
+ </template>
626
+ </tr>
627
+ </thead>
628
+ <!-- Column headers (with concentration overlay when conditions present) -->
629
+ <thead v-if="props.showLabels">
630
+ <tr>
631
+ <th :style="{ width: sizeConfig.headerWidth, height: sizeConfig.headerHeight }"></th>
632
+ <!-- Spacer for row condition column in column header row -->
633
+ <th v-if="hasRowConditions" :style="{ width: sizeConfig.headerWidth, height: sizeConfig.headerHeight }"></th>
634
+ <th
635
+ v-for="col in colLabels"
636
+ :key="col"
637
+ class="mld-well-plate__col-header"
638
+ :style="{ height: sizeConfig.headerHeight, fontSize: sizeConfig.fontSize }"
639
+ >
640
+ <template v-for="entry in [colConditionMap.get(col)]" :key="col">
641
+ <span
642
+ v-if="entry"
643
+ class="mld-well-plate__condition-cell"
644
+ :style="conditionGradientStyle(
645
+ entry.condition.color,
646
+ entry.condition.concentrations[entry.indexInGroup],
647
+ entry.condition.concentrations,
648
+ )"
649
+ >
650
+ {{ formatConc(entry.condition.concentrations[entry.indexInGroup]) }}
651
+ </span>
652
+ <template v-else>{{ col }}</template>
653
+ </template>
654
+ </th>
655
+ </tr>
656
+ </thead>
657
+ <tbody>
658
+ <tr v-for="(row, rowIndex) in wellGrid" :key="rowIndex" role="row">
659
+ <!-- Row condition label cell (drug name, spans multiple rows) -->
660
+ <template v-if="hasRowConditions && rowConditionSpanByRow.has(rowIndex)">
661
+ <td
662
+ v-for="span in [rowConditionSpanByRow.get(rowIndex)!]"
663
+ :key="rowIndex"
664
+ :rowspan="span.rowspan"
665
+ :class="[
666
+ 'mld-well-plate__row-condition-label',
667
+ 'gap' in span ? 'mld-well-plate__row-condition-label--empty' : '',
668
+ ]"
669
+ :style="{
670
+ width: sizeConfig.headerWidth,
671
+ minWidth: sizeConfig.headerWidth,
672
+ fontSize: sizeConfig.fontSize,
673
+ ...('condition' in span ? { color: span.condition.color } : {}),
674
+ }"
675
+ >
676
+ <div v-if="'condition' in span" class="mld-well-plate__row-condition-wrap">
677
+ <span class="mld-well-plate__row-condition-text">{{ span.condition.label }}</span>
678
+ </div>
679
+ </td>
680
+ </template>
681
+ <!-- Row header (with concentration overlay when conditions present) -->
682
+ <td
683
+ v-if="props.showLabels"
684
+ class="mld-well-plate__row-header"
685
+ :style="{
686
+ width: sizeConfig.headerWidth,
687
+ height: sizeConfig.cellHeight,
688
+ minWidth: sizeConfig.headerWidth,
689
+ minHeight: sizeConfig.cellHeight,
690
+ fontSize: sizeConfig.fontSize,
691
+ }"
692
+ >
693
+ <template v-for="entry in [rowConditionMap.get(rowLabels[rowIndex])]" :key="rowIndex">
694
+ <span
695
+ v-if="entry"
696
+ class="mld-well-plate__condition-cell"
697
+ :style="conditionGradientStyle(
698
+ entry.condition.color,
699
+ entry.condition.concentrations[entry.indexInGroup],
700
+ entry.condition.concentrations,
701
+ )"
702
+ >
703
+ {{ formatConc(entry.condition.concentrations[entry.indexInGroup]) }}
704
+ </span>
705
+ <template v-else>{{ rowLabels[rowIndex] }}</template>
706
+ </template>
707
+ </td>
708
+
709
+ <!-- Wells -->
710
+ <td v-for="well in row" :key="well.id" class="mld-well-plate__cell">
711
+ <div
712
+ role="gridcell"
713
+ tabindex="0"
714
+ :aria-label="`Well ${well.id}${well.sampleType ? `, Sample type: ${well.sampleType}` : ''}${well.value !== undefined ? `, Value: ${well.value}` : ''}`"
715
+ :aria-selected="isSelected(well.id)"
716
+ :aria-disabled="props.disabled || well.state === 'disabled'"
717
+ :draggable="props.selectionMode === 'drag' && !!well.sampleType"
718
+ :class="getWellClasses(well)"
719
+ :style="{
720
+ width: isFillMode ? '100%' : sizeConfig.cellWidth,
721
+ height: sizeConfig.cellHeight,
722
+ minWidth: isFillMode ? '0' : sizeConfig.cellWidth,
723
+ minHeight: sizeConfig.cellHeight,
724
+ boxSizing: 'border-box',
725
+ fontSize: sizeConfig.fontSize,
726
+ ...getWellStyle(well),
727
+ }"
728
+ :title="`${well.id}${well.sampleType ? `: ${well.sampleType}` : ''}${getWellLabel(well) ? ` - ${getWellLabel(well)}` : ''}`"
729
+ @click="handleWellClick(well, $event)"
730
+ @mousedown="handleWellMouseDown(well, $event)"
731
+ @mouseenter="handleWellMouseEnter(well, $event)"
732
+ @mouseleave="handleWellMouseLeave"
733
+ @contextmenu="handleContextMenu(well, $event)"
734
+ @dragstart="handleDragStart(well, $event)"
735
+ @dragover.prevent="handleDragOver(well, $event)"
736
+ @dragleave="handleDragLeave"
737
+ @drop.prevent="handleDrop(well, $event)"
738
+ @dragend="handleDragEnd"
739
+ @keydown.enter="handleWellClick(well, $event as unknown as MouseEvent)"
740
+ @keydown.space.prevent="handleWellClick(well, $event as unknown as MouseEvent)"
741
+ >
742
+ <!-- Well label text (from metadata.label) -->
743
+ <span
744
+ v-if="getWellLabel(well)"
745
+ class="mld-well-plate__label"
746
+ >
747
+ {{ getWellLabel(well) }}
748
+ </span>
749
+ <!-- Sample type indicator (S/B/Q/C) -->
750
+ <span
751
+ v-else-if="getSampleTypeIndicator(well)"
752
+ class="mld-well-plate__indicator"
753
+ >
754
+ {{ getSampleTypeIndicator(well) }}
755
+ </span>
756
+ <!-- Well ID -->
757
+ <span
758
+ v-else-if="props.showWellIds"
759
+ class="mld-well-plate__well-id"
760
+ >
761
+ {{ well.id }}
762
+ </span>
763
+
764
+ <!-- Badge (injection count or custom method) -->
765
+ <span
766
+ v-if="getWellBadge(well)"
767
+ class="mld-well-plate__badge"
768
+ :style="{ backgroundColor: getWellBadge(well)!.color }"
769
+ :title="getWellBadge(well)!.text === '+' ? 'Custom method' : `${getWellBadge(well)!.text}x injections`"
770
+ >
771
+ {{ getWellBadge(well)!.text }}
772
+ </span>
773
+ </div>
774
+ </td>
775
+ </tr>
776
+ </tbody>
777
+ </table>
778
+
779
+ <!-- Heatmap legend (inside content wrapper to match table width) -->
780
+ <div
781
+ v-if="props.heatmap?.enabled && props.heatmap.showLegend"
782
+ class="mld-well-plate__legend"
783
+ >
784
+ <span class="mld-well-plate__legend-label">{{ props.heatmap.min ?? 0 }}</span>
785
+ <div class="mld-well-plate__legend-bar">
786
+ <div
787
+ v-for="(color, index) in (props.heatmap.colorScale === 'custom' && props.heatmap.customColors?.length ? props.heatmap.customColors : heatmapColors[props.heatmap.colorScale || 'viridis'])"
788
+ :key="index"
789
+ class="mld-well-plate__legend-segment"
790
+ :style="{ backgroundColor: color }"
791
+ />
792
+ </div>
793
+ <span class="mld-well-plate__legend-label">{{ props.heatmap.max ?? 1 }}</span>
794
+ </div>
795
+
796
+ <!-- Sample type legend bar -->
797
+ <div v-if="props.showLegend" class="mld-well-plate__sample-legend">
798
+ <div
799
+ v-for="item in activeLegendItems"
800
+ :key="item.type"
801
+ class="mld-well-plate__sample-legend-item"
802
+ >
803
+ <span
804
+ class="mld-well-plate__sample-legend-dot"
805
+ :style="{ backgroundColor: `${item.color}26`, border: `1px solid ${item.color}66` }"
806
+ />
807
+ <span class="mld-well-plate__sample-legend-text">{{ item.label }}</span>
808
+ </div>
809
+ </div>
810
+ </div>
811
+ </div>
812
+
813
+ <!-- Edit popup -->
814
+ <WellEditPopup
815
+ v-if="editable && editingWellId"
816
+ :well-id="editingWellId"
817
+ :well-data="wells[editingWellId]"
818
+ :edit-fields="editFields"
819
+ :default-injection-volume="defaultInjectionVolume"
820
+ :position="editPopupPosition"
821
+ @save="handleEditSave"
822
+ @clear="handleEditClear"
823
+ @close="handleEditClose"
824
+ />
825
+ </div>
826
+ </template>
827
+
828
+ <style>
829
+ @import '../styles/components/well-plate.css';
830
+ </style>