@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,292 @@
1
+ import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'
2
+ import { useApi } from './useApi'
3
+ import { datePresetToISO } from './experiment-utils'
4
+ import type {
5
+ ExperimentSummary,
6
+ ExperimentListResponse,
7
+ ExperimentFilters,
8
+ ExperimentTypeOption,
9
+ ExperimentSortField,
10
+ PlatformContext,
11
+ SelectOption,
12
+ } from '../types'
13
+
14
+ function getPlatformContext(): PlatformContext | undefined {
15
+ if (typeof window === 'undefined') return undefined
16
+ return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__
17
+ }
18
+
19
+ function getPlatformApiUrl(): string | undefined {
20
+ return getPlatformContext()?.platformApiUrl
21
+ }
22
+
23
+ export interface UseExperimentSelectorOptions {
24
+ experimentType?: string
25
+ apiBaseUrl?: string
26
+ limit?: number
27
+ immediate?: boolean
28
+ }
29
+
30
+ export interface UseExperimentSelectorReturn {
31
+ experiments: Ref<ExperimentSummary[]>
32
+ total: Ref<number>
33
+ selectedExperiment: Ref<ExperimentSummary | null>
34
+ filters: ExperimentFilters
35
+ isLoading: Ref<boolean>
36
+ error: Ref<string | null>
37
+ page: Ref<number>
38
+ hasMore: ComputedRef<boolean>
39
+ // Sort
40
+ sortKey: Ref<string>
41
+ // Filter options
42
+ experimentTypes: Ref<ExperimentTypeOption[]>
43
+ projects: Ref<SelectOption<string>[]>
44
+ // Grouped view
45
+ groupedByProject: ComputedRef<[string, ExperimentSummary[]][]>
46
+ // Methods
47
+ fetch: () => Promise<void>
48
+ loadMore: () => Promise<void>
49
+ reset: () => void
50
+ select: (experiment: ExperimentSummary) => void
51
+ clear: () => void
52
+ fetchFilterOptions: () => Promise<void>
53
+ }
54
+
55
+ /** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */
56
+ export function useExperimentSelector(
57
+ options: UseExperimentSelectorOptions = {},
58
+ ): UseExperimentSelectorReturn {
59
+ const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options
60
+ const platformBase = apiBaseUrl ?? getPlatformApiUrl()
61
+ const api = useApi()
62
+
63
+ const experiments = ref<ExperimentSummary[]>([])
64
+ const total = ref(0)
65
+ const selectedExperiment = ref<ExperimentSummary | null>(null)
66
+ const isLoading = ref(false)
67
+ const error = ref<string | null>(null)
68
+ const page = ref(0)
69
+
70
+ // Sort: combined key like "created_at:desc"
71
+ const sortKey = ref<string>('created_at:desc')
72
+
73
+ // Filter option data (fetched once, cached)
74
+ const experimentTypes = ref<ExperimentTypeOption[]>([])
75
+ const projects = ref<SelectOption<string>[]>([])
76
+ let filterOptionsFetched = false
77
+
78
+ const hasMore = computed(() => experiments.value.length < total.value)
79
+
80
+ const filters: ExperimentFilters = reactive({
81
+ search: undefined,
82
+ status: undefined,
83
+ project: undefined,
84
+ experimentType: undefined,
85
+ datePreset: undefined,
86
+ })
87
+
88
+ function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {
89
+ const [field, order] = sortKey.value.split(':')
90
+ return {
91
+ sortBy: (field || 'created_at') as ExperimentSortField,
92
+ sortOrder: (order || 'desc') as 'asc' | 'desc',
93
+ }
94
+ }
95
+
96
+ async function fetchExperiments(): Promise<void> {
97
+ isLoading.value = true
98
+ error.value = null
99
+ try {
100
+ const params = new URLSearchParams()
101
+ // Priority: explicit option > platform context (single type) > filter dropdown > no filter
102
+ const allowedTypes = getPlatformContext()?.allowedExperimentTypes
103
+ const effectiveType = experimentType
104
+ ?? (allowedTypes?.length === 1 ? allowedTypes[0] : undefined)
105
+ ?? filters.experimentType
106
+ ?? undefined
107
+ if (effectiveType) params.set('experiment_type', effectiveType)
108
+ if (filters.status) params.set('status', filters.status)
109
+ if (filters.search) params.set('search', filters.search)
110
+ if (filters.project) params.set('project', filters.project)
111
+
112
+ // Sort params
113
+ const { sortBy, sortOrder } = parseSortKey()
114
+ params.set('sort_by', sortBy)
115
+ params.set('sort_order', sortOrder)
116
+
117
+ // Date preset → created_after
118
+ if (filters.datePreset) {
119
+ params.set('created_after', datePresetToISO(filters.datePreset))
120
+ }
121
+
122
+ params.set('limit', String(limit))
123
+ params.set('skip', String(page.value * limit))
124
+
125
+ const query = params.toString()
126
+ const base = platformBase ?? ''
127
+ const url = `${base}/experiments${query ? `?${query}` : ''}`
128
+ const data = await api.get<ExperimentListResponse>(url)
129
+
130
+ // Client-side filter for multiple allowed types (backend only supports single type)
131
+ let filtered = data.experiments
132
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
133
+ const typeSet = new Set(allowedTypes)
134
+ filtered = filtered.filter(e => typeSet.has(e.experiment_type))
135
+ }
136
+
137
+ if (page.value === 0) {
138
+ experiments.value = filtered
139
+ } else {
140
+ experiments.value = [...experiments.value, ...filtered]
141
+ }
142
+ // When client-side filtering is active (multiple allowedTypes), we can't
143
+ // use data.total since it counts all types. Check if server has more pages.
144
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
145
+ if (data.experiments.length < limit) {
146
+ // Server returned less than a full page — no more data
147
+ total.value = experiments.value.length
148
+ } else {
149
+ // Might be more pages on the server
150
+ total.value = experiments.value.length + 1
151
+ }
152
+ } else {
153
+ total.value = data.total
154
+ }
155
+ } catch (e) {
156
+ error.value = e instanceof Error ? e.message : 'Failed to fetch experiments'
157
+ if (page.value === 0) {
158
+ experiments.value = []
159
+ total.value = 0
160
+ }
161
+ } finally {
162
+ isLoading.value = false
163
+ }
164
+ }
165
+
166
+ async function fetchFilterOptions(): Promise<void> {
167
+ if (filterOptionsFetched) return
168
+ filterOptionsFetched = true
169
+
170
+ const base = platformBase ?? ''
171
+ const [typesRes, projectsRes] = await Promise.allSettled([
172
+ api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),
173
+ api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),
174
+ ])
175
+
176
+ if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {
177
+ experimentTypes.value = typesRes.value.map(t => ({
178
+ value: t.value,
179
+ label: t.label,
180
+ color: t.color,
181
+ }))
182
+ }
183
+
184
+ if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {
185
+ projects.value = projectsRes.value.projects.map(p => ({
186
+ value: p.name,
187
+ label: p.name,
188
+ }))
189
+ }
190
+ }
191
+
192
+ async function loadMore(): Promise<void> {
193
+ if (!hasMore.value || isLoading.value) return
194
+ page.value++
195
+ await fetchExperiments()
196
+ }
197
+
198
+ function reset(): void {
199
+ page.value = 0
200
+ experiments.value = []
201
+ total.value = 0
202
+ fetchExperiments()
203
+ }
204
+
205
+ function select(experiment: ExperimentSummary): void {
206
+ selectedExperiment.value = experiment
207
+ }
208
+
209
+ function clear(): void {
210
+ selectedExperiment.value = null
211
+ filters.search = undefined
212
+ filters.status = undefined
213
+ filters.project = undefined
214
+ filters.experimentType = undefined
215
+ filters.datePreset = undefined
216
+ sortKey.value = 'created_at:desc'
217
+ page.value = 0
218
+ }
219
+
220
+ // Group experiments by project (client-side)
221
+ const groupedByProject = computed<[string, ExperimentSummary[]][]>(() => {
222
+ const groups = new Map<string, ExperimentSummary[]>()
223
+ for (const exp of experiments.value) {
224
+ const key = exp.project_name ?? exp.project ?? 'No project'
225
+ const list = groups.get(key)
226
+ if (list) {
227
+ list.push(exp)
228
+ } else {
229
+ groups.set(key, [exp])
230
+ }
231
+ }
232
+ // Sort alphabetically, "No project" last
233
+ return [...groups.entries()].sort(([a], [b]) => {
234
+ if (a === 'No project') return 1
235
+ if (b === 'No project') return -1
236
+ return a.localeCompare(b)
237
+ })
238
+ })
239
+
240
+ // Debounced watch on search filter
241
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
242
+ watch(
243
+ () => filters.search,
244
+ () => {
245
+ if (debounceTimer) clearTimeout(debounceTimer)
246
+ debounceTimer = setTimeout(() => {
247
+ page.value = 0
248
+ fetchExperiments()
249
+ }, 300)
250
+ },
251
+ )
252
+
253
+ // Immediate watch on discrete filters and sort (cancel any pending search debounce)
254
+ watch(
255
+ () => [filters.status, filters.project, filters.experimentType, filters.datePreset, sortKey.value],
256
+ () => {
257
+ if (debounceTimer) clearTimeout(debounceTimer)
258
+ debounceTimer = null
259
+ page.value = 0
260
+ fetchExperiments()
261
+ },
262
+ )
263
+
264
+ onScopeDispose(() => {
265
+ if (debounceTimer) clearTimeout(debounceTimer)
266
+ })
267
+
268
+ if (immediate) {
269
+ fetchExperiments()
270
+ }
271
+
272
+ return {
273
+ experiments,
274
+ total,
275
+ selectedExperiment,
276
+ filters,
277
+ isLoading,
278
+ error,
279
+ page,
280
+ hasMore,
281
+ sortKey,
282
+ experimentTypes,
283
+ projects,
284
+ groupedByProject,
285
+ fetch: fetchExperiments,
286
+ loadMore,
287
+ reset,
288
+ select,
289
+ clear,
290
+ fetchFilterOptions,
291
+ }
292
+ }
@@ -0,0 +1,416 @@
1
+ import { ref, reactive, computed, watch, type Ref } from 'vue'
2
+
3
+ /**
4
+ * Validation rule function type.
5
+ * Returns error message string if invalid, undefined/null if valid.
6
+ */
7
+ export type ValidationRule<T = unknown> = (value: T, formData: Record<string, unknown>) => string | undefined | null
8
+
9
+ /**
10
+ * Field validation rules configuration.
11
+ */
12
+ export interface FieldRules<T = unknown> {
13
+ required?: boolean | string
14
+ minLength?: number | { value: number; message: string }
15
+ maxLength?: number | { value: number; message: string }
16
+ min?: number | { value: number; message: string }
17
+ max?: number | { value: number; message: string }
18
+ pattern?: RegExp | { value: RegExp; message: string }
19
+ email?: boolean | string
20
+ custom?: ValidationRule<T> | ValidationRule<T>[]
21
+ }
22
+
23
+ /**
24
+ * Field validation rules configuration.
25
+ */
26
+ export interface FieldState {
27
+ value: unknown
28
+ error: string | null
29
+ touched: boolean
30
+ dirty: boolean
31
+ }
32
+
33
+ /**
34
+ * Form state and methods.
35
+ */
36
+ export interface UseFormReturn<T extends Record<string, unknown>> {
37
+ // Form data (reactive)
38
+ data: T
39
+
40
+ // Field errors
41
+ errors: Record<string, string | null>
42
+
43
+ // Field touched state
44
+ touched: Record<string, boolean>
45
+
46
+ // Field dirty state (value changed from initial)
47
+ dirty: Record<string, boolean>
48
+
49
+ // Overall form state
50
+ isValid: Ref<boolean>
51
+ isDirty: Ref<boolean>
52
+ isSubmitting: Ref<boolean>
53
+
54
+ // Methods
55
+ setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void
56
+ setFieldError: (field: string, error: string | null) => void
57
+ setFieldTouched: (field: string, touched?: boolean) => void
58
+ validateField: (field: string) => boolean
59
+ validate: () => boolean
60
+ reset: (values?: Partial<T>) => void
61
+ handleSubmit: (onSubmit: (data: T) => Promise<void> | void) => (e?: Event) => Promise<void>
62
+ getFieldProps: <K extends keyof T>(field: K) => {
63
+ modelValue: T[K]
64
+ 'onUpdate:modelValue': (value: T[K]) => void
65
+ onBlur: () => void
66
+ error: string | null
67
+ }
68
+ }
69
+
70
+ // Built-in validators
71
+ const validators = {
72
+ required: (value: unknown, message = 'This field is required'): string | null => {
73
+ if (value === null || value === undefined || value === '') {
74
+ return message
75
+ }
76
+ if (Array.isArray(value) && value.length === 0) {
77
+ return message
78
+ }
79
+ return null
80
+ },
81
+
82
+ minLength: (value: unknown, min: number, message?: string): string | null => {
83
+ if (typeof value !== 'string') return null
84
+ if (value.length < min) {
85
+ return message || `Must be at least ${min} characters`
86
+ }
87
+ return null
88
+ },
89
+
90
+ maxLength: (value: unknown, max: number, message?: string): string | null => {
91
+ if (typeof value !== 'string') return null
92
+ if (value.length > max) {
93
+ return message || `Must be at most ${max} characters`
94
+ }
95
+ return null
96
+ },
97
+
98
+ min: (value: unknown, min: number, message?: string): string | null => {
99
+ if (typeof value !== 'number') return null
100
+ if (value < min) {
101
+ return message || `Must be at least ${min}`
102
+ }
103
+ return null
104
+ },
105
+
106
+ max: (value: unknown, max: number, message?: string): string | null => {
107
+ if (typeof value !== 'number') return null
108
+ if (value > max) {
109
+ return message || `Must be at most ${max}`
110
+ }
111
+ return null
112
+ },
113
+
114
+ pattern: (value: unknown, pattern: RegExp, message?: string): string | null => {
115
+ if (typeof value !== 'string') return null
116
+ if (!pattern.test(value)) {
117
+ return message || 'Invalid format'
118
+ }
119
+ return null
120
+ },
121
+
122
+ email: (value: unknown, message = 'Invalid email address'): string | null => {
123
+ if (typeof value !== 'string' || !value) return null
124
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
125
+ if (!emailRegex.test(value)) {
126
+ return message
127
+ }
128
+ return null
129
+ },
130
+ }
131
+
132
+ /**
133
+ * Form state management composable with validation.
134
+ *
135
+ * @param initialValues - Initial form values
136
+ * @param rules - Validation rules for each field
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(
141
+ * { email: '', password: '' },
142
+ * {
143
+ * email: { required: true, email: true },
144
+ * password: { required: true, minLength: 8 },
145
+ * }
146
+ * )
147
+ *
148
+ * // In template
149
+ * <BaseInput v-bind="getFieldProps('email')" label="Email" />
150
+ * <BaseInput v-bind="getFieldProps('password')" type="password" label="Password" />
151
+ * <BaseButton @click="handleSubmit(onSubmit)" :disabled="!isValid">Submit</BaseButton>
152
+ * ```
153
+ */
154
+ /** Reactive form state with field-level validation, dirty tracking, and submit handling. */
155
+ export function useForm<T extends Record<string, unknown>>(
156
+ initialValues: T,
157
+ rules: Partial<Record<keyof T, FieldRules>> = {}
158
+ ): UseFormReturn<T> {
159
+ // Deep copy initial values so nested objects are not shared
160
+ const _initialValues = structuredClone(initialValues)
161
+
162
+ // Reactive form data
163
+ const data = reactive(structuredClone(initialValues)) as T
164
+
165
+ // Field state - use simple Record types for better TS compatibility
166
+ const errors = reactive<Record<string, string | null>>(
167
+ Object.keys(initialValues).reduce((acc, key) => {
168
+ acc[key] = null
169
+ return acc
170
+ }, {} as Record<string, string | null>)
171
+ )
172
+
173
+ const touched = reactive<Record<string, boolean>>(
174
+ Object.keys(initialValues).reduce((acc, key) => {
175
+ acc[key] = false
176
+ return acc
177
+ }, {} as Record<string, boolean>)
178
+ )
179
+
180
+ const dirty = reactive<Record<string, boolean>>(
181
+ Object.keys(initialValues).reduce((acc, key) => {
182
+ acc[key] = false
183
+ return acc
184
+ }, {} as Record<string, boolean>)
185
+ )
186
+
187
+ const isSubmitting = ref(false)
188
+
189
+ // Watch data changes to track dirty state
190
+ watch(
191
+ () => ({ ...data }),
192
+ (newData) => {
193
+ for (const key of Object.keys(newData)) {
194
+ dirty[key] = newData[key as keyof T] !== _initialValues[key as keyof T]
195
+ }
196
+ },
197
+ { deep: true }
198
+ )
199
+
200
+ // Validate a single field
201
+ function validateField(field: string): boolean {
202
+ const value = data[field as keyof T]
203
+ const fieldRules = rules[field as keyof T]
204
+
205
+ if (!fieldRules) {
206
+ errors[field] = null
207
+ return true
208
+ }
209
+
210
+ // Check required
211
+ if (fieldRules.required) {
212
+ const message = typeof fieldRules.required === 'string' ? fieldRules.required : undefined
213
+ const error = validators.required(value, message)
214
+ if (error) {
215
+ errors[field] = error
216
+ return false
217
+ }
218
+ }
219
+
220
+ // Skip other validations if empty and not required
221
+ if (value === null || value === undefined || value === '') {
222
+ errors[field] = null
223
+ return true
224
+ }
225
+
226
+ // Check minLength
227
+ if (fieldRules.minLength !== undefined) {
228
+ const config = typeof fieldRules.minLength === 'number'
229
+ ? { value: fieldRules.minLength, message: undefined }
230
+ : fieldRules.minLength
231
+ const error = validators.minLength(value, config.value, config.message)
232
+ if (error) {
233
+ errors[field] = error
234
+ return false
235
+ }
236
+ }
237
+
238
+ // Check maxLength
239
+ if (fieldRules.maxLength !== undefined) {
240
+ const config = typeof fieldRules.maxLength === 'number'
241
+ ? { value: fieldRules.maxLength, message: undefined }
242
+ : fieldRules.maxLength
243
+ const error = validators.maxLength(value, config.value, config.message)
244
+ if (error) {
245
+ errors[field] = error
246
+ return false
247
+ }
248
+ }
249
+
250
+ // Check min
251
+ if (fieldRules.min !== undefined) {
252
+ const config = typeof fieldRules.min === 'number'
253
+ ? { value: fieldRules.min, message: undefined }
254
+ : fieldRules.min
255
+ const error = validators.min(value, config.value, config.message)
256
+ if (error) {
257
+ errors[field] = error
258
+ return false
259
+ }
260
+ }
261
+
262
+ // Check max
263
+ if (fieldRules.max !== undefined) {
264
+ const config = typeof fieldRules.max === 'number'
265
+ ? { value: fieldRules.max, message: undefined }
266
+ : fieldRules.max
267
+ const error = validators.max(value, config.value, config.message)
268
+ if (error) {
269
+ errors[field] = error
270
+ return false
271
+ }
272
+ }
273
+
274
+ // Check pattern
275
+ if (fieldRules.pattern !== undefined) {
276
+ const config = fieldRules.pattern instanceof RegExp
277
+ ? { value: fieldRules.pattern, message: undefined }
278
+ : fieldRules.pattern
279
+ const error = validators.pattern(value, config.value, config.message)
280
+ if (error) {
281
+ errors[field] = error
282
+ return false
283
+ }
284
+ }
285
+
286
+ // Check email
287
+ if (fieldRules.email) {
288
+ const message = typeof fieldRules.email === 'string' ? fieldRules.email : undefined
289
+ const error = validators.email(value, message)
290
+ if (error) {
291
+ errors[field] = error
292
+ return false
293
+ }
294
+ }
295
+
296
+ // Check custom validators
297
+ if (fieldRules.custom) {
298
+ const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom]
299
+ for (const rule of customRules) {
300
+ const error = rule(value, data as Record<string, unknown>)
301
+ if (error) {
302
+ errors[field] = error
303
+ return false
304
+ }
305
+ }
306
+ }
307
+
308
+ errors[field] = null
309
+ return true
310
+ }
311
+
312
+ // Validate all fields
313
+ function validate(): boolean {
314
+ let isAllValid = true
315
+ for (const field of Object.keys(data)) {
316
+ if (!validateField(field)) {
317
+ isAllValid = false
318
+ }
319
+ }
320
+ return isAllValid
321
+ }
322
+
323
+ // Computed overall validity
324
+ const isValid = computed(() => {
325
+ return Object.values(errors).every(error => error === null)
326
+ })
327
+
328
+ // Computed overall dirty state
329
+ const isDirty = computed(() => {
330
+ return Object.values(dirty).some(d => d)
331
+ })
332
+
333
+ // Set field value
334
+ function setFieldValue<K extends keyof T>(field: K, value: T[K]): void {
335
+ ;(data as Record<string, unknown>)[field as string] = value
336
+ if (touched[field as string]) {
337
+ validateField(field as string)
338
+ }
339
+ }
340
+
341
+ // Set field error
342
+ function setFieldError(field: string, error: string | null): void {
343
+ errors[field] = error
344
+ }
345
+
346
+ // Set field touched
347
+ function setFieldTouched(field: string, isTouched = true): void {
348
+ touched[field] = isTouched
349
+ if (isTouched) {
350
+ validateField(field)
351
+ }
352
+ }
353
+
354
+ // Reset form
355
+ function reset(values?: Partial<T>): void {
356
+ const resetValues = values ? { ..._initialValues, ...values } : _initialValues
357
+ for (const key of Object.keys(data)) {
358
+ ;(data as Record<string, unknown>)[key] = structuredClone(resetValues[key as keyof T])
359
+ errors[key] = null
360
+ touched[key] = false
361
+ dirty[key] = false
362
+ }
363
+ }
364
+
365
+ // Handle submit
366
+ function handleSubmit(onSubmit: (data: T) => Promise<void> | void) {
367
+ return async (e?: Event): Promise<void> => {
368
+ e?.preventDefault()
369
+
370
+ // Mark all fields as touched
371
+ for (const field of Object.keys(data)) {
372
+ touched[field] = true
373
+ }
374
+
375
+ if (!validate()) {
376
+ return
377
+ }
378
+
379
+ isSubmitting.value = true
380
+ try {
381
+ await onSubmit(data)
382
+ } finally {
383
+ isSubmitting.value = false
384
+ }
385
+ }
386
+ }
387
+
388
+ // Get props for a field (for v-bind)
389
+ function getFieldProps<K extends keyof T>(field: K) {
390
+ const fieldStr = field as string
391
+ return {
392
+ modelValue: data[field],
393
+ 'onUpdate:modelValue': (value: T[K]) => setFieldValue(field, value),
394
+ onBlur: () => setFieldTouched(fieldStr),
395
+ error: touched[fieldStr] ? errors[fieldStr] : null,
396
+ }
397
+ }
398
+
399
+ return {
400
+ data,
401
+ errors,
402
+ touched,
403
+ dirty,
404
+ isValid,
405
+ isDirty,
406
+ isSubmitting,
407
+ setFieldValue,
408
+ setFieldError,
409
+ setFieldTouched,
410
+ validateField,
411
+ validate,
412
+ reset,
413
+ handleSubmit,
414
+ getFieldProps,
415
+ }
416
+ }