@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,560 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { ref, computed } from 'vue'
3
+
4
+ // Capture side-effect callbacks so tests can invoke them directly
5
+ let capturedScopeDispose: (() => void) | null = null
6
+ let capturedProvideKey: symbol | null = null
7
+ let capturedProvideValue: unknown = null
8
+
9
+ vi.mock('vue', async () => {
10
+ const actual = await vi.importActual('vue')
11
+ return {
12
+ ...actual,
13
+ provide: vi.fn((key: symbol, value: unknown) => {
14
+ capturedProvideKey = key
15
+ capturedProvideValue = value
16
+ }),
17
+ onScopeDispose: vi.fn((cb: () => void) => {
18
+ capturedScopeDispose = cb
19
+ }),
20
+ }
21
+ })
22
+
23
+ import { useAppExperiment, APP_EXPERIMENT_KEY } from '../../composables/useAppExperiment'
24
+ import type { AppExperimentState } from '../../composables/useAppExperiment'
25
+ import type { ExperimentSummary } from '../../types'
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function makeExperiment(overrides: Partial<ExperimentSummary> = {}): ExperimentSummary {
32
+ return {
33
+ id: 42,
34
+ name: 'Test Experiment',
35
+ status: 'planned',
36
+ experiment_type: 'default',
37
+ created_at: '2025-01-01T00:00:00Z',
38
+ updated_at: '2025-01-01T00:00:00Z',
39
+ has_design_data: false,
40
+ ...overrides,
41
+ }
42
+ }
43
+
44
+ function getState(): AppExperimentState {
45
+ return capturedProvideValue as AppExperimentState
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Tests
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe('useAppExperiment', () => {
53
+ beforeEach(() => {
54
+ capturedScopeDispose = null
55
+ capturedProvideKey = null
56
+ capturedProvideValue = null
57
+ vi.useFakeTimers()
58
+ })
59
+
60
+ afterEach(() => {
61
+ vi.useRealTimers()
62
+ vi.restoreAllMocks()
63
+ })
64
+
65
+ // -------------------------------------------------------------------------
66
+ // provide()
67
+ // -------------------------------------------------------------------------
68
+
69
+ describe('provide', () => {
70
+ it('should call provide with APP_EXPERIMENT_KEY', () => {
71
+ useAppExperiment()
72
+ expect(capturedProvideKey).toBe(APP_EXPERIMENT_KEY)
73
+ })
74
+
75
+ it('should provide a non-null state object', () => {
76
+ useAppExperiment()
77
+ expect(capturedProvideValue).toBeTruthy()
78
+ })
79
+ })
80
+
81
+ // -------------------------------------------------------------------------
82
+ // set() and clear()
83
+ // -------------------------------------------------------------------------
84
+
85
+ describe('set', () => {
86
+ it('should update experimentName and experimentId on set', () => {
87
+ const { set, experimentName, experimentId } = useAppExperiment()
88
+ const experiment = makeExperiment({ id: 7, name: 'Alpha Run' })
89
+
90
+ set(experiment)
91
+
92
+ expect(experimentName.value).toBe('Alpha Run')
93
+ expect(experimentId.value).toBe(7)
94
+ })
95
+
96
+ it('should update experimentStatus in provided state', () => {
97
+ const { set } = useAppExperiment()
98
+ const experiment = makeExperiment({ status: 'completed' })
99
+
100
+ set(experiment)
101
+
102
+ expect(getState().experimentStatus.value).toBe('completed')
103
+ })
104
+
105
+ it('should overwrite previously set values', () => {
106
+ const { set, experimentName, experimentId } = useAppExperiment()
107
+
108
+ set(makeExperiment({ id: 1, name: 'First' }))
109
+ set(makeExperiment({ id: 2, name: 'Second' }))
110
+
111
+ expect(experimentName.value).toBe('Second')
112
+ expect(experimentId.value).toBe(2)
113
+ })
114
+ })
115
+
116
+ describe('clear', () => {
117
+ it('should reset experimentName and experimentId to initial values', () => {
118
+ const { set, clear, experimentName, experimentId } = useAppExperiment()
119
+
120
+ set(makeExperiment({ id: 99, name: 'To Be Cleared' }))
121
+ clear()
122
+
123
+ expect(experimentName.value).toBeUndefined()
124
+ expect(experimentId.value).toBeNull()
125
+ })
126
+
127
+ it('should reset experimentStatus in provided state', () => {
128
+ const { set, clear } = useAppExperiment()
129
+
130
+ set(makeExperiment({ status: 'ongoing' }))
131
+ clear()
132
+
133
+ expect(getState().experimentStatus.value).toBeUndefined()
134
+ })
135
+ })
136
+
137
+ describe('returned refs are readonly', () => {
138
+ it('should expose experimentName as a readonly ref', () => {
139
+ const { experimentName } = useAppExperiment()
140
+ // Readonly refs still have a .value property for reading
141
+ expect(experimentName.value).toBeUndefined()
142
+ })
143
+
144
+ it('should expose experimentId as a readonly ref', () => {
145
+ const { experimentId } = useAppExperiment()
146
+ expect(experimentId.value).toBeNull()
147
+ })
148
+ })
149
+
150
+ // -------------------------------------------------------------------------
151
+ // openModal / closeModal
152
+ // -------------------------------------------------------------------------
153
+
154
+ describe('openModal and closeModal', () => {
155
+ it('should set showModal to true when openModal is called', () => {
156
+ useAppExperiment()
157
+ const state = getState()
158
+
159
+ expect(state.showModal.value).toBe(false)
160
+ state.openModal()
161
+ expect(state.showModal.value).toBe(true)
162
+ })
163
+
164
+ it('should set showModal to false when closeModal is called', () => {
165
+ useAppExperiment()
166
+ const state = getState()
167
+
168
+ state.openModal()
169
+ state.closeModal()
170
+ expect(state.showModal.value).toBe(false)
171
+ })
172
+ })
173
+
174
+ // -------------------------------------------------------------------------
175
+ // handleSelect
176
+ // -------------------------------------------------------------------------
177
+
178
+ describe('handleSelect', () => {
179
+ it('should call onSelect callback with the experiment', () => {
180
+ const onSelect = vi.fn()
181
+ useAppExperiment({ onSelect })
182
+ const state = getState()
183
+ const experiment = makeExperiment()
184
+
185
+ state.handleSelect(experiment)
186
+
187
+ expect(onSelect).toHaveBeenCalledOnce()
188
+ expect(onSelect).toHaveBeenCalledWith(experiment)
189
+ })
190
+
191
+ it('should set experimentId and experimentName via handleSelect', () => {
192
+ const { experimentId, experimentName } = useAppExperiment()
193
+ const state = getState()
194
+ const experiment = makeExperiment({ id: 5, name: 'Selected Exp' })
195
+
196
+ state.handleSelect(experiment)
197
+
198
+ expect(experimentId.value).toBe(5)
199
+ expect(experimentName.value).toBe('Selected Exp')
200
+ })
201
+
202
+ it('should close the modal after selection', () => {
203
+ useAppExperiment()
204
+ const state = getState()
205
+
206
+ state.openModal()
207
+ state.handleSelect(makeExperiment())
208
+
209
+ expect(state.showModal.value).toBe(false)
210
+ })
211
+
212
+ it('should not throw when no onSelect callback is provided', () => {
213
+ useAppExperiment()
214
+ const state = getState()
215
+
216
+ expect(() => state.handleSelect(makeExperiment())).not.toThrow()
217
+ })
218
+ })
219
+
220
+ // -------------------------------------------------------------------------
221
+ // handleSave
222
+ // -------------------------------------------------------------------------
223
+
224
+ describe('handleSave', () => {
225
+ it('should call onSave when invoked', async () => {
226
+ const onSave = vi.fn().mockResolvedValue(null)
227
+ useAppExperiment({ onSave })
228
+ const state = getState()
229
+
230
+ await state.handleSave()
231
+
232
+ expect(onSave).toHaveBeenCalledOnce()
233
+ })
234
+
235
+ it('should set saveSuccessMessage when onSave returns a string', async () => {
236
+ const onSave = vi.fn().mockResolvedValue('Saved successfully!')
237
+ useAppExperiment({ onSave })
238
+ const state = getState()
239
+
240
+ await state.handleSave()
241
+
242
+ expect(state.saveSuccessMessage.value).toBe('Saved successfully!')
243
+ })
244
+
245
+ it('should auto-clear saveSuccessMessage after 3 seconds', async () => {
246
+ const onSave = vi.fn().mockResolvedValue('Done')
247
+ useAppExperiment({ onSave })
248
+ const state = getState()
249
+
250
+ await state.handleSave()
251
+ expect(state.saveSuccessMessage.value).toBe('Done')
252
+
253
+ vi.advanceTimersByTime(3000)
254
+ expect(state.saveSuccessMessage.value).toBeUndefined()
255
+ })
256
+
257
+ it('should not clear saveSuccessMessage before 3 seconds have elapsed', async () => {
258
+ const onSave = vi.fn().mockResolvedValue('Pending')
259
+ useAppExperiment({ onSave })
260
+ const state = getState()
261
+
262
+ await state.handleSave()
263
+ vi.advanceTimersByTime(2999)
264
+
265
+ expect(state.saveSuccessMessage.value).toBe('Pending')
266
+ })
267
+
268
+ it('should reset the 3-second timer on a subsequent successful save', async () => {
269
+ const onSave = vi.fn().mockResolvedValue('Resaved')
270
+ useAppExperiment({ onSave })
271
+ const state = getState()
272
+
273
+ await state.handleSave()
274
+ vi.advanceTimersByTime(2000)
275
+
276
+ await state.handleSave()
277
+ vi.advanceTimersByTime(2000)
278
+ // Only 2s have passed since the second save — message should still be set
279
+ expect(state.saveSuccessMessage.value).toBe('Resaved')
280
+
281
+ vi.advanceTimersByTime(1000)
282
+ expect(state.saveSuccessMessage.value).toBeUndefined()
283
+ })
284
+
285
+ it('should manage saveLoading: true during save, false after', async () => {
286
+ let resolveSave!: (v: string | null) => void
287
+ const onSave = vi.fn(
288
+ () => new Promise<string | null>((resolve) => { resolveSave = resolve })
289
+ )
290
+ useAppExperiment({ onSave })
291
+ const state = getState()
292
+
293
+ const savePromise = state.handleSave()
294
+ expect(state.saveLoading.value).toBe(true)
295
+
296
+ resolveSave(null)
297
+ await savePromise
298
+ expect(state.saveLoading.value).toBe(false)
299
+ })
300
+
301
+ it('should set saveLoading to false even when onSave rejects', async () => {
302
+ const onSave = vi.fn().mockRejectedValue(new Error('network error'))
303
+ useAppExperiment({ onSave })
304
+ const state = getState()
305
+
306
+ await state.handleSave().catch(() => {})
307
+
308
+ expect(state.saveLoading.value).toBe(false)
309
+ })
310
+
311
+ it('should be a no-op (not call onSave) when saveLoading is already true', async () => {
312
+ let resolveSave!: (v: string | null) => void
313
+ const onSave = vi.fn(
314
+ () => new Promise<string | null>((resolve) => { resolveSave = resolve })
315
+ )
316
+ useAppExperiment({ onSave })
317
+ const state = getState()
318
+
319
+ // Start first save but do not await it yet
320
+ const first = state.handleSave()
321
+
322
+ // Attempt second save while first is in-flight
323
+ await state.handleSave()
324
+
325
+ resolveSave(null)
326
+ await first
327
+
328
+ expect(onSave).toHaveBeenCalledTimes(1)
329
+ })
330
+
331
+ it('should not call onSave and return immediately when no onSave option is provided', async () => {
332
+ useAppExperiment()
333
+ const state = getState()
334
+
335
+ // Should not throw and saveLoading should remain false
336
+ await state.handleSave()
337
+ expect(state.saveLoading.value).toBe(false)
338
+ })
339
+
340
+ it('should not set saveSuccessMessage when onSave returns null', async () => {
341
+ const onSave = vi.fn().mockResolvedValue(null)
342
+ useAppExperiment({ onSave })
343
+ const state = getState()
344
+
345
+ await state.handleSave()
346
+
347
+ expect(state.saveSuccessMessage.value).toBeUndefined()
348
+ })
349
+ })
350
+
351
+ // -------------------------------------------------------------------------
352
+ // handleDetach
353
+ // -------------------------------------------------------------------------
354
+
355
+ describe('handleDetach', () => {
356
+ it('should clear experiment state', () => {
357
+ const { set, experimentId, experimentName } = useAppExperiment()
358
+ const state = getState()
359
+
360
+ set(makeExperiment({ id: 3, name: 'Detachable' }))
361
+ state.handleDetach()
362
+
363
+ expect(experimentId.value).toBeNull()
364
+ expect(experimentName.value).toBeUndefined()
365
+ })
366
+
367
+ it('should call onDetach callback', () => {
368
+ const onDetach = vi.fn()
369
+ useAppExperiment({ onDetach })
370
+ const state = getState()
371
+
372
+ state.handleDetach()
373
+
374
+ expect(onDetach).toHaveBeenCalledOnce()
375
+ })
376
+
377
+ it('should not throw when no onDetach callback is provided', () => {
378
+ useAppExperiment()
379
+ const state = getState()
380
+
381
+ expect(() => state.handleDetach()).not.toThrow()
382
+ })
383
+
384
+ it('should call onDetach after clearing state', () => {
385
+ const calls: string[] = []
386
+ const { set, experimentId } = useAppExperiment({
387
+ onDetach: () => {
388
+ // By the time onDetach fires, experimentId should already be null
389
+ calls.push(experimentId.value === null ? 'cleared' : 'not-cleared')
390
+ },
391
+ })
392
+ const state = getState()
393
+
394
+ set(makeExperiment())
395
+ state.handleDetach()
396
+
397
+ expect(calls).toEqual(['cleared'])
398
+ })
399
+ })
400
+
401
+ // -------------------------------------------------------------------------
402
+ // showSave
403
+ // -------------------------------------------------------------------------
404
+
405
+ describe('showSave', () => {
406
+ it('should be true when onSave is provided', () => {
407
+ useAppExperiment({ onSave: vi.fn().mockResolvedValue(null) })
408
+ expect(getState().showSave.value).toBe(true)
409
+ })
410
+
411
+ it('should be false when onSave is not provided', () => {
412
+ useAppExperiment()
413
+ expect(getState().showSave.value).toBe(false)
414
+ })
415
+ })
416
+
417
+ // -------------------------------------------------------------------------
418
+ // showDetach
419
+ // -------------------------------------------------------------------------
420
+
421
+ describe('showDetach', () => {
422
+ it('should be false initially (no experiment set)', () => {
423
+ useAppExperiment()
424
+ expect(getState().showDetach.value).toBe(false)
425
+ })
426
+
427
+ it('should be true after an experiment is set', () => {
428
+ const { set } = useAppExperiment()
429
+
430
+ set(makeExperiment())
431
+
432
+ expect(getState().showDetach.value).toBe(true)
433
+ })
434
+
435
+ it('should revert to false after clear is called', () => {
436
+ const { set, clear } = useAppExperiment()
437
+
438
+ set(makeExperiment())
439
+ clear()
440
+
441
+ expect(getState().showDetach.value).toBe(false)
442
+ })
443
+ })
444
+
445
+ // -------------------------------------------------------------------------
446
+ // saveDisabled
447
+ // -------------------------------------------------------------------------
448
+
449
+ describe('saveDisabled', () => {
450
+ it('should be false by default when no saveDisabled option is provided', () => {
451
+ useAppExperiment()
452
+ expect(getState().saveDisabled.value).toBe(false)
453
+ })
454
+
455
+ it('should unwrap a raw Ref<boolean>', () => {
456
+ const flag = ref(true)
457
+ useAppExperiment({ saveDisabled: flag })
458
+
459
+ expect(getState().saveDisabled.value).toBe(true)
460
+
461
+ flag.value = false
462
+ expect(getState().saveDisabled.value).toBe(false)
463
+ })
464
+
465
+ it('should unwrap a ComputedRef<boolean>', () => {
466
+ const source = ref(false)
467
+ const flag = computed(() => source.value)
468
+ useAppExperiment({ saveDisabled: flag })
469
+
470
+ expect(getState().saveDisabled.value).toBe(false)
471
+
472
+ source.value = true
473
+ expect(getState().saveDisabled.value).toBe(true)
474
+ })
475
+ })
476
+
477
+ // -------------------------------------------------------------------------
478
+ // saveDisabledMessage
479
+ // -------------------------------------------------------------------------
480
+
481
+ describe('saveDisabledMessage', () => {
482
+ it('should be undefined when no saveDisabledMessage option is provided', () => {
483
+ useAppExperiment()
484
+ expect(getState().saveDisabledMessage.value).toBeUndefined()
485
+ })
486
+
487
+ it('should unwrap a plain string', () => {
488
+ useAppExperiment({ saveDisabledMessage: 'Cannot save right now' })
489
+ expect(getState().saveDisabledMessage.value).toBe('Cannot save right now')
490
+ })
491
+
492
+ it('should unwrap a Ref<string | undefined>', () => {
493
+ const msg = ref<string | undefined>('Initial message')
494
+ useAppExperiment({ saveDisabledMessage: msg })
495
+
496
+ expect(getState().saveDisabledMessage.value).toBe('Initial message')
497
+
498
+ msg.value = undefined
499
+ expect(getState().saveDisabledMessage.value).toBeUndefined()
500
+ })
501
+
502
+ it('should unwrap a ComputedRef<string | undefined>', () => {
503
+ const active = ref(true)
504
+ const msg = computed(() => (active.value ? 'Disabled because active' : undefined))
505
+ useAppExperiment({ saveDisabledMessage: msg })
506
+
507
+ expect(getState().saveDisabledMessage.value).toBe('Disabled because active')
508
+
509
+ active.value = false
510
+ expect(getState().saveDisabledMessage.value).toBeUndefined()
511
+ })
512
+ })
513
+
514
+ // -------------------------------------------------------------------------
515
+ // Timer cleanup via onScopeDispose
516
+ // -------------------------------------------------------------------------
517
+
518
+ describe('onScopeDispose cleanup', () => {
519
+ it('should register an onScopeDispose callback', () => {
520
+ useAppExperiment()
521
+ expect(capturedScopeDispose).toBeTypeOf('function')
522
+ })
523
+
524
+ it('should clear the success timer when the scope is disposed', async () => {
525
+ const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout')
526
+ const onSave = vi.fn().mockResolvedValue('Saved')
527
+ useAppExperiment({ onSave })
528
+ const state = getState()
529
+
530
+ await state.handleSave()
531
+ // Timer is active — disposing the scope should cancel it
532
+ capturedScopeDispose!()
533
+
534
+ expect(clearTimeoutSpy).toHaveBeenCalled()
535
+ })
536
+
537
+ it('should not call clearTimeout if no timer is active at disposal time', () => {
538
+ const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout')
539
+ useAppExperiment()
540
+
541
+ // Dispose without ever triggering a save
542
+ capturedScopeDispose!()
543
+
544
+ expect(clearTimeoutSpy).not.toHaveBeenCalled()
545
+ })
546
+
547
+ it('should not clear saveSuccessMessage after disposal if timer was cancelled', async () => {
548
+ const onSave = vi.fn().mockResolvedValue('Will be cancelled')
549
+ useAppExperiment({ onSave })
550
+ const state = getState()
551
+
552
+ await state.handleSave()
553
+ capturedScopeDispose!()
554
+
555
+ // Advancing time past 3s should NOT clear message since timer was cancelled
556
+ vi.advanceTimersByTime(3000)
557
+ expect(state.saveSuccessMessage.value).toBe('Will be cancelled')
558
+ })
559
+ })
560
+ })