@morscherlab/mint-sdk 1.0.0-beta.2 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. package/README.md +218 -6
  2. package/dist/__tests__/components/ActionItem.test.d.ts +1 -0
  3. package/dist/__tests__/components/AppAvatarMenu.test.d.ts +1 -0
  4. package/dist/__tests__/components/AppPageSelector.test.d.ts +1 -0
  5. package/dist/__tests__/components/AppPillNav.test.d.ts +1 -0
  6. package/dist/__tests__/components/AppPluginSwitcher.test.d.ts +1 -0
  7. package/dist/__tests__/components/AppToastContainer.test.d.ts +1 -0
  8. package/dist/__tests__/components/BaseRadioGroup.test.d.ts +1 -0
  9. package/dist/__tests__/components/BaseSelect.test.d.ts +1 -0
  10. package/dist/__tests__/components/BaseTabs.test.d.ts +1 -0
  11. package/dist/__tests__/components/BatchProgressList.test.d.ts +1 -0
  12. package/dist/__tests__/components/BioTemplateExperimentWorkspaceView.test.d.ts +1 -0
  13. package/dist/__tests__/components/BioTemplatePackWorkspaceView.test.d.ts +1 -0
  14. package/dist/__tests__/components/BioTemplatePresetWorkspaceView.test.d.ts +1 -0
  15. package/dist/__tests__/components/BioTemplateRenderer.test.d.ts +1 -0
  16. package/dist/__tests__/components/Breadcrumb.test.d.ts +1 -0
  17. package/dist/__tests__/components/CalendarGridPanel.test.d.ts +1 -0
  18. package/dist/__tests__/components/ConcentrationInput.test.d.ts +1 -0
  19. package/dist/__tests__/components/ControlWorkspaceView.test.d.ts +1 -0
  20. package/dist/__tests__/components/DatePicker.test.d.ts +1 -0
  21. package/dist/__tests__/components/DateTimePicker.test.d.ts +1 -0
  22. package/dist/__tests__/components/EmptyState.test.d.ts +1 -0
  23. package/dist/__tests__/components/ExperimentPopover.test.d.ts +1 -0
  24. package/dist/__tests__/components/FormBuilder.test.d.ts +1 -0
  25. package/dist/__tests__/components/FormCompatibility.test.d.ts +1 -0
  26. package/dist/__tests__/components/GroupAssigner.test.d.ts +1 -0
  27. package/dist/__tests__/components/GroupingModal.test.d.ts +1 -0
  28. package/dist/__tests__/components/MultiSelect.test.d.ts +1 -0
  29. package/dist/__tests__/components/ProtocolStepEditor.test.d.ts +1 -0
  30. package/dist/__tests__/components/ReagentList.test.d.ts +1 -0
  31. package/dist/__tests__/components/SampleHierarchyTree.test.d.ts +1 -0
  32. package/dist/__tests__/components/SampleSelector.test.d.ts +1 -0
  33. package/dist/__tests__/components/SegmentedControl.test.d.ts +1 -0
  34. package/dist/__tests__/components/SettingsButton.test.d.ts +1 -0
  35. package/dist/__tests__/components/SettingsModal.test.d.ts +1 -0
  36. package/dist/__tests__/components/TagsInput.test.d.ts +1 -0
  37. package/dist/__tests__/components/ThemeToggle.test.d.ts +1 -0
  38. package/dist/__tests__/components/TimePicker.test.d.ts +1 -0
  39. package/dist/__tests__/composables/useBioTemplatePackWorkspace.test.d.ts +1 -0
  40. package/dist/__tests__/composables/useBioTemplatePresetWorkspace.test.d.ts +1 -0
  41. package/dist/__tests__/composables/useBioTemplateWorkspace.test.d.ts +1 -0
  42. package/dist/__tests__/composables/useCalendarGrid.test.d.ts +1 -0
  43. package/dist/__tests__/composables/useControlSchema.test.d.ts +1 -0
  44. package/dist/__tests__/composables/useDebouncedWatch.test.d.ts +1 -0
  45. package/dist/__tests__/composables/useDropdownState.test.d.ts +1 -0
  46. package/dist/__tests__/composables/useEventListener.test.d.ts +1 -0
  47. package/dist/__tests__/composables/useExpansionSet.test.d.ts +1 -0
  48. package/dist/__tests__/composables/useExperimentData.test.d.ts +1 -0
  49. package/dist/__tests__/composables/useExperimentSelector.test.d.ts +1 -0
  50. package/dist/__tests__/composables/useGroupAssignment.test.d.ts +1 -0
  51. package/dist/__tests__/composables/useListSelection.test.d.ts +1 -0
  52. package/dist/__tests__/composables/usePluginClient.test.d.ts +1 -0
  53. package/dist/__tests__/composables/usePluginConfig.test.d.ts +1 -0
  54. package/dist/__tests__/composables/useRequestSyncState.test.d.ts +1 -0
  55. package/dist/__tests__/composables/useSampleGroups.test.d.ts +1 -0
  56. package/dist/__tests__/composables/useSelectionLimit.test.d.ts +1 -0
  57. package/dist/__tests__/composables/useSortedItems.test.d.ts +1 -0
  58. package/dist/__tests__/composables/useTemplateCollection.test.d.ts +1 -0
  59. package/dist/__tests__/composables/useTextSearch.test.d.ts +1 -0
  60. package/dist/__tests__/composables/useTheme.test.d.ts +1 -0
  61. package/dist/__tests__/composables/useTimeUtils.test.d.ts +1 -0
  62. package/dist/__tests__/docs/frontendDocsCatalog.test.d.ts +1 -0
  63. package/dist/__tests__/templates/templates.test.d.ts +1 -0
  64. package/dist/{auth-DsI0rQ7_.js → auth-QQj2kkze.js} +12 -5
  65. package/dist/auth-QQj2kkze.js.map +1 -0
  66. package/dist/components/ActionItem.vue.d.ts +32 -0
  67. package/dist/components/AppAvatarMenu.vue.d.ts +2 -7
  68. package/dist/components/AppPageSelector.vue.d.ts +3 -6
  69. package/dist/components/AppPillNav.vue.d.ts +2 -2
  70. package/dist/components/AppSidebar.vue.d.ts +56 -3
  71. package/dist/components/AppToastContainer.vue.d.ts +2 -0
  72. package/dist/components/AppTopBar.vue.d.ts +41 -10
  73. package/dist/components/BaseButton.vue.d.ts +1 -1
  74. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  75. package/dist/components/BaseInput.vue.d.ts +2 -2
  76. package/dist/components/BasePill.vue.d.ts +2 -2
  77. package/dist/components/BaseRadioGroup.vue.d.ts +3 -3
  78. package/dist/components/BaseSelect.vue.d.ts +3 -3
  79. package/dist/components/BaseTabs.vue.d.ts +2 -2
  80. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  81. package/dist/components/BaseToggle.vue.d.ts +1 -1
  82. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +117 -0
  83. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +92 -0
  84. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +82 -0
  85. package/dist/components/BioTemplateRenderer.vue.d.ts +29 -0
  86. package/dist/components/Breadcrumb.vue.d.ts +2 -2
  87. package/dist/components/Calendar.vue.d.ts +1 -1
  88. package/dist/components/CalendarGridPanel.vue.d.ts +25 -0
  89. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  90. package/dist/components/ConcentrationInput.vue.d.ts +2 -2
  91. package/dist/components/ConfirmDialog.vue.d.ts +2 -2
  92. package/dist/components/ControlWorkspaceView.vue.d.ts +130 -0
  93. package/dist/components/DatePicker.vue.d.ts +1 -1
  94. package/dist/components/DateTimePicker.vue.d.ts +3 -3
  95. package/dist/components/Divider.vue.d.ts +1 -1
  96. package/dist/components/DropdownButton.vue.d.ts +3 -3
  97. package/dist/components/EmptyState.vue.d.ts +1 -2
  98. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  99. package/dist/components/ExperimentTimeline.vue.d.ts +2 -2
  100. package/dist/components/FileUploader.vue.d.ts +1 -1
  101. package/dist/components/FitPanel.vue.d.ts +1 -1
  102. package/dist/components/FormActions.vue.d.ts +4 -4
  103. package/dist/components/FormBuilder.vue.d.ts +22 -8
  104. package/dist/components/FormFieldRenderer.vue.d.ts +7 -10
  105. package/dist/components/FormSection.vue.d.ts +11 -24
  106. package/dist/components/FormulaInput.vue.d.ts +2 -2
  107. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  108. package/dist/components/MultiSelect.vue.d.ts +3 -3
  109. package/dist/components/NumberInput.vue.d.ts +1 -1
  110. package/dist/components/ProgressBar.vue.d.ts +1 -1
  111. package/dist/components/ProtocolStepEditor.vue.d.ts +3 -1
  112. package/dist/components/RackEditor.vue.d.ts +2 -2
  113. package/dist/components/SampleLegend.vue.d.ts +2 -2
  114. package/dist/components/ScheduleCalendar.vue.d.ts +2 -2
  115. package/dist/components/SegmentedControl.vue.d.ts +2 -2
  116. package/dist/components/SequenceInput.vue.d.ts +3 -3
  117. package/dist/components/SettingsButton.vue.d.ts +2 -2
  118. package/dist/components/SettingsModal.vue.d.ts +13 -5
  119. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  120. package/dist/components/TagsInput.vue.d.ts +3 -2
  121. package/dist/components/TimePicker.vue.d.ts +3 -3
  122. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  123. package/dist/components/UnitInput.vue.d.ts +2 -2
  124. package/dist/components/WellPlate.vue.d.ts +8 -8
  125. package/dist/components/index.d.ts +11 -1
  126. package/dist/components/index.js +3 -3
  127. package/dist/components/internal/FormFieldRendererInternal.vue.d.ts +31 -0
  128. package/dist/components/internal/FormSectionRenderer.vue.d.ts +43 -0
  129. package/dist/{components-_XqPEhP9.js → components-D_Sr0adg.js} +7290 -6518
  130. package/dist/components-D_Sr0adg.js.map +1 -0
  131. package/dist/composables/index.d.ts +21 -2
  132. package/dist/composables/index.js +4 -3
  133. package/dist/composables/platformContextHelpers.d.ts +14 -0
  134. package/dist/composables/useBioTemplateComponents.d.ts +20 -0
  135. package/dist/composables/useBioTemplateControls.d.ts +6 -0
  136. package/dist/composables/useBioTemplatePackWorkspace.d.ts +45 -0
  137. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +74 -0
  138. package/dist/composables/useBioTemplateWorkspace.d.ts +50 -0
  139. package/dist/composables/useCalendarGrid.d.ts +26 -0
  140. package/dist/composables/useControlSchema.d.ts +321 -0
  141. package/dist/composables/useDebouncedWatch.d.ts +20 -0
  142. package/dist/composables/useDropdownState.d.ts +19 -0
  143. package/dist/composables/useEventListener.d.ts +13 -0
  144. package/dist/composables/useExpansionSet.d.ts +21 -0
  145. package/dist/composables/useExperimentData.d.ts +10 -0
  146. package/dist/composables/useExperimentSave.d.ts +31 -2
  147. package/dist/composables/useExperimentSelector.d.ts +20 -0
  148. package/dist/composables/useForm.d.ts +2 -0
  149. package/dist/composables/useGroupAssignment.d.ts +31 -0
  150. package/dist/composables/useListSelection.d.ts +35 -0
  151. package/dist/composables/usePlatformContext.d.ts +21 -3
  152. package/dist/composables/usePluginApi.d.ts +7 -14
  153. package/dist/composables/usePluginClient.d.ts +109 -0
  154. package/dist/composables/usePluginConfig.d.ts +12 -0
  155. package/dist/composables/useRequestSyncState.d.ts +34 -0
  156. package/dist/composables/useSampleGroups.d.ts +32 -0
  157. package/dist/composables/useSelectionLimit.d.ts +17 -0
  158. package/dist/composables/useSortedItems.d.ts +32 -0
  159. package/dist/composables/useTemplateCollection.d.ts +58 -0
  160. package/dist/composables/useTextSearch.d.ts +18 -0
  161. package/dist/composables/useTimeUtils.d.ts +8 -0
  162. package/dist/{composables-tiZqLu1M.js → composables-C3dpXQN5.js} +228 -146
  163. package/dist/composables-C3dpXQN5.js.map +1 -0
  164. package/dist/index.d.ts +12 -3
  165. package/dist/index.js +6 -5
  166. package/dist/install.d.ts +7 -2
  167. package/dist/install.js +2 -2
  168. package/dist/install.js.map +1 -1
  169. package/dist/stores/index.js +1 -1
  170. package/dist/stores/settings.d.ts +4 -1
  171. package/dist/styles.css +5235 -5977
  172. package/dist/templates/adapters.d.ts +43 -0
  173. package/dist/templates/builders.d.ts +63 -0
  174. package/dist/templates/catalog.d.ts +188 -0
  175. package/dist/templates/componentBindings.d.ts +58 -0
  176. package/dist/templates/controlSchemas.d.ts +25 -0
  177. package/dist/templates/index.d.ts +15 -0
  178. package/dist/templates/index.js +2 -0
  179. package/dist/templates/lookup.d.ts +4 -0
  180. package/dist/templates/packs.d.ts +18 -0
  181. package/dist/templates/presets.d.ts +90 -0
  182. package/dist/templates/types.d.ts +531 -0
  183. package/dist/templates-50NPjaxL.js +9333 -0
  184. package/dist/templates-50NPjaxL.js.map +1 -0
  185. package/dist/types/components.d.ts +26 -4
  186. package/dist/types/form-builder.d.ts +6 -8
  187. package/dist/types/index.d.ts +2 -2
  188. package/dist/types/platform.d.ts +7 -1
  189. package/dist/useScheduleDrag-D4oWdh41.js +4371 -0
  190. package/dist/useScheduleDrag-D4oWdh41.js.map +1 -0
  191. package/dist/utils/formModelSync.d.ts +5 -0
  192. package/dist/utils/items.d.ts +8 -0
  193. package/dist/utils/options.d.ts +6 -0
  194. package/dist/utils/pluginIcon.d.ts +9 -0
  195. package/package.json +7 -2
  196. package/src/__tests__/components/ActionItem.test.ts +99 -0
  197. package/src/__tests__/components/AppAvatarMenu.test.ts +27 -0
  198. package/src/__tests__/components/AppPageSelector.test.ts +134 -0
  199. package/src/__tests__/components/AppPillNav.test.ts +78 -0
  200. package/src/__tests__/components/AppPluginSwitcher.test.ts +44 -0
  201. package/src/__tests__/components/AppSidebar.test.ts +370 -0
  202. package/src/__tests__/components/AppToastContainer.test.ts +48 -0
  203. package/src/__tests__/components/AppTopBar.test.ts +383 -0
  204. package/src/__tests__/components/BaseRadioGroup.test.ts +25 -0
  205. package/src/__tests__/components/BaseSelect.test.ts +21 -0
  206. package/src/__tests__/components/BaseTabs.test.ts +25 -0
  207. package/src/__tests__/components/BatchProgressList.test.ts +52 -0
  208. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +153 -0
  209. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +161 -0
  210. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +281 -0
  211. package/src/__tests__/components/BioTemplateRenderer.test.ts +71 -0
  212. package/src/__tests__/components/Breadcrumb.test.ts +23 -0
  213. package/src/__tests__/components/CalendarGridPanel.test.ts +36 -0
  214. package/src/__tests__/components/ConcentrationInput.test.ts +45 -0
  215. package/src/__tests__/components/ControlWorkspaceView.test.ts +1031 -0
  216. package/src/__tests__/components/DataFrame.test.ts +11 -0
  217. package/src/__tests__/components/DatePicker.test.ts +45 -0
  218. package/src/__tests__/components/DateTimePicker.test.ts +48 -0
  219. package/src/__tests__/components/DropdownButton.test.ts +23 -0
  220. package/src/__tests__/components/EmptyState.test.ts +23 -0
  221. package/src/__tests__/components/ExperimentPopover.test.ts +56 -0
  222. package/src/__tests__/components/FormBuilder.test.ts +296 -0
  223. package/src/__tests__/components/FormCompatibility.test.ts +94 -0
  224. package/src/__tests__/components/GroupAssigner.test.ts +30 -0
  225. package/src/__tests__/components/GroupingModal.test.ts +73 -0
  226. package/src/__tests__/components/MultiSelect.test.ts +48 -0
  227. package/src/__tests__/components/ProtocolStepEditor.test.ts +33 -0
  228. package/src/__tests__/components/ReagentList.test.ts +82 -0
  229. package/src/__tests__/components/SampleHierarchyTree.test.ts +53 -0
  230. package/src/__tests__/components/SampleSelector.test.ts +60 -0
  231. package/src/__tests__/components/SegmentedControl.test.ts +24 -0
  232. package/src/__tests__/components/SettingsButton.test.ts +44 -0
  233. package/src/__tests__/components/SettingsModal.test.ts +296 -0
  234. package/src/__tests__/components/TagsInput.test.ts +75 -0
  235. package/src/__tests__/components/ThemeToggle.test.ts +47 -0
  236. package/src/__tests__/components/TimePicker.test.ts +38 -0
  237. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +122 -0
  238. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +199 -0
  239. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +99 -0
  240. package/src/__tests__/composables/useCalendarGrid.test.ts +38 -0
  241. package/src/__tests__/composables/useControlSchema.test.ts +919 -0
  242. package/src/__tests__/composables/useDebouncedWatch.test.ts +93 -0
  243. package/src/__tests__/composables/useDropdownState.test.ts +95 -0
  244. package/src/__tests__/composables/useEventListener.test.ts +116 -0
  245. package/src/__tests__/composables/useExpansionSet.test.ts +62 -0
  246. package/src/__tests__/composables/useExperimentData.test.ts +4 -0
  247. package/src/__tests__/composables/useExperimentSave.test.ts +203 -8
  248. package/src/__tests__/composables/useExperimentSelector.test.ts +164 -0
  249. package/src/__tests__/composables/useForm.test.ts +58 -0
  250. package/src/__tests__/composables/useFormBuilder.test.ts +77 -0
  251. package/src/__tests__/composables/useGroupAssignment.test.ts +73 -0
  252. package/src/__tests__/composables/useListSelection.test.ts +66 -0
  253. package/src/__tests__/composables/usePluginClient.test.ts +444 -0
  254. package/src/__tests__/composables/usePluginConfig.test.ts +5 -0
  255. package/src/__tests__/composables/useRequestSyncState.test.ts +92 -0
  256. package/src/__tests__/composables/useSampleGroups.test.ts +66 -0
  257. package/src/__tests__/composables/useSelectionLimit.test.ts +41 -0
  258. package/src/__tests__/composables/useSortedItems.test.ts +87 -0
  259. package/src/__tests__/composables/useTemplateCollection.test.ts +147 -0
  260. package/src/__tests__/composables/useTextSearch.test.ts +55 -0
  261. package/src/__tests__/composables/useTheme.test.ts +91 -0
  262. package/src/__tests__/composables/useTimeUtils.test.ts +35 -0
  263. package/src/__tests__/docs/frontendDocsCatalog.test.ts +229 -0
  264. package/src/__tests__/fixtures/templates/dose-response.json +81 -0
  265. package/src/__tests__/fixtures/templates/plate-map.json +54 -0
  266. package/src/__tests__/fixtures/templates/qpcr-plate.json +96 -0
  267. package/src/__tests__/fixtures/templates/sample-sheet.json +71 -0
  268. package/src/__tests__/templates/templates.test.ts +1043 -0
  269. package/src/components/ActionItem.vue +82 -0
  270. package/src/components/AppAvatarMenu.vue +15 -69
  271. package/src/components/AppLayout.story.vue +25 -25
  272. package/src/components/AppPageSelector.vue +63 -94
  273. package/src/components/AppPillNav.vue +44 -39
  274. package/src/components/AppPluginSwitcher.vue +41 -145
  275. package/src/components/AppSidebar.story.vue +94 -0
  276. package/src/components/AppSidebar.vue +187 -12
  277. package/src/components/{ToastNotification.story.vue → AppToastContainer.story.vue} +6 -6
  278. package/src/components/AppToastContainer.vue +62 -0
  279. package/src/components/AppTopBar.story.vue +7 -30
  280. package/src/components/AppTopBar.vue +251 -57
  281. package/src/components/BaseModal.vue +3 -5
  282. package/src/components/BaseRadioGroup.vue +7 -3
  283. package/src/components/BaseSelect.vue +11 -7
  284. package/src/components/BaseTabs.vue +6 -4
  285. package/src/components/BatchProgressList.vue +5 -8
  286. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +123 -0
  287. package/src/components/BioTemplateExperimentWorkspaceView.vue +337 -0
  288. package/src/components/BioTemplatePackWorkspaceView.story.vue +107 -0
  289. package/src/components/BioTemplatePackWorkspaceView.vue +176 -0
  290. package/src/components/BioTemplatePresetWorkspaceView.story.vue +151 -0
  291. package/src/components/BioTemplatePresetWorkspaceView.vue +392 -0
  292. package/src/components/BioTemplateRenderer.story.vue +57 -0
  293. package/src/components/BioTemplateRenderer.vue +269 -0
  294. package/src/components/Breadcrumb.vue +14 -8
  295. package/src/components/CalendarGridPanel.vue +120 -0
  296. package/src/components/ConcentrationInput.vue +27 -64
  297. package/src/components/ControlWorkspaceView.story.vue +336 -0
  298. package/src/components/ControlWorkspaceView.vue +347 -0
  299. package/src/components/DataFrame.vue +34 -50
  300. package/src/components/DatePicker.vue +59 -192
  301. package/src/components/DateTimePicker.vue +50 -171
  302. package/src/components/DropdownButton.vue +14 -32
  303. package/src/components/EmptyState.vue +4 -2
  304. package/src/components/ExperimentPopover.vue +5 -22
  305. package/src/components/FormBuilder.vue +124 -27
  306. package/src/components/FormFieldRenderer.vue +15 -38
  307. package/src/components/FormSection.vue +20 -73
  308. package/src/components/GroupAssigner.vue +24 -56
  309. package/src/components/GroupingModal.story.vue +3 -3
  310. package/src/components/GroupingModal.vue +30 -391
  311. package/src/components/MultiSelect.vue +17 -12
  312. package/src/components/PlateMapEditor.vue +3 -8
  313. package/src/components/PluginIcon.vue +2 -22
  314. package/src/components/ProtocolStepEditor.vue +13 -22
  315. package/src/components/ReagentList.vue +25 -33
  316. package/src/components/SampleHierarchyTree.vue +12 -23
  317. package/src/components/SampleSelector.vue +42 -122
  318. package/src/components/SegmentedControl.vue +7 -3
  319. package/src/components/SettingsButton.story.vue +1 -1
  320. package/src/components/SettingsButton.vue +15 -27
  321. package/src/components/SettingsModal.story.vue +1 -1
  322. package/src/components/SettingsModal.vue +120 -29
  323. package/src/components/TagsInput.vue +29 -14
  324. package/src/components/ThemeToggle.vue +9 -7
  325. package/src/components/TimePicker.vue +19 -41
  326. package/src/components/ToastNotification.vue +4 -57
  327. package/src/components/Tooltip.vue +7 -12
  328. package/src/components/WellEditPopup.vue +3 -8
  329. package/src/components/WellPlate.vue +4 -10
  330. package/src/components/index.ts +11 -1
  331. package/src/components/internal/FormFieldRendererInternal.vue +50 -0
  332. package/src/components/internal/FormSectionRenderer.vue +78 -0
  333. package/src/composables/index.ts +212 -0
  334. package/src/composables/platformContextHelpers.ts +74 -0
  335. package/src/composables/useBioTemplateComponents.ts +93 -0
  336. package/src/composables/useBioTemplateControls.ts +41 -0
  337. package/src/composables/useBioTemplatePackWorkspace.ts +181 -0
  338. package/src/composables/useBioTemplatePresetWorkspace.ts +337 -0
  339. package/src/composables/useBioTemplateWorkspace.ts +139 -0
  340. package/src/composables/useCalendarGrid.ts +140 -0
  341. package/src/composables/useControlSchema.ts +1274 -0
  342. package/src/composables/useDebouncedWatch.ts +119 -0
  343. package/src/composables/useDropdownState.ts +83 -0
  344. package/src/composables/useEventListener.ts +111 -0
  345. package/src/composables/useExpansionSet.ts +117 -0
  346. package/src/composables/useExperimentData.ts +20 -11
  347. package/src/composables/useExperimentSave.ts +202 -50
  348. package/src/composables/useExperimentSelector.ts +86 -72
  349. package/src/composables/useForm.ts +49 -4
  350. package/src/composables/useFormBuilder.ts +93 -42
  351. package/src/composables/useGroupAssignment.ts +148 -0
  352. package/src/composables/useListSelection.ts +158 -0
  353. package/src/composables/usePluginApi.ts +7 -14
  354. package/src/composables/usePluginClient.ts +425 -0
  355. package/src/composables/usePluginConfig.ts +34 -13
  356. package/src/composables/useRequestSyncState.ts +126 -0
  357. package/src/composables/useSampleGroups.ts +126 -0
  358. package/src/composables/useSelectionLimit.ts +57 -0
  359. package/src/composables/useSortedItems.ts +118 -0
  360. package/src/composables/useTemplateCollection.ts +229 -0
  361. package/src/composables/useTextSearch.ts +60 -0
  362. package/src/composables/useTheme.ts +2 -28
  363. package/src/composables/useTimeUtils.ts +26 -2
  364. package/src/composables/useWellPlateEditor.ts +13 -9
  365. package/src/index.ts +224 -4
  366. package/src/install.ts +11 -4
  367. package/src/stores/settings.ts +13 -9
  368. package/src/styles/components/app-page-selector.css +23 -0
  369. package/src/styles/components/app-pill-nav.css +7 -0
  370. package/src/styles/components/app-top-bar.css +34 -0
  371. package/src/styles/components/concentration-input.css +3 -142
  372. package/src/styles/components/empty-state.css +0 -16
  373. package/src/styles/components/settings-button.css +3 -66
  374. package/src/styles/components/theme-toggle.css +3 -66
  375. package/src/styles/index.css +0 -1
  376. package/src/templates/adapters.ts +785 -0
  377. package/src/templates/builders.ts +2149 -0
  378. package/src/templates/catalog.ts +245 -0
  379. package/src/templates/componentBindings.ts +615 -0
  380. package/src/templates/controlSchemas.ts +718 -0
  381. package/src/templates/index.ts +314 -0
  382. package/src/templates/lookup.ts +18 -0
  383. package/src/templates/packs.ts +156 -0
  384. package/src/templates/presets.ts +146 -0
  385. package/src/templates/types.ts +668 -0
  386. package/src/types/components.ts +41 -4
  387. package/src/types/form-builder.ts +7 -2
  388. package/src/types/index.ts +14 -0
  389. package/src/types/platform.ts +7 -1
  390. package/src/utils/formModelSync.ts +52 -0
  391. package/src/utils/items.ts +28 -0
  392. package/src/utils/options.ts +23 -0
  393. package/src/utils/pluginIcon.ts +30 -0
  394. package/dist/auth-DsI0rQ7_.js.map +0 -1
  395. package/dist/components-_XqPEhP9.js.map +0 -1
  396. package/dist/composables-tiZqLu1M.js.map +0 -1
  397. package/dist/useScheduleDrag-CA9sGNJG.js +0 -7181
  398. package/dist/useScheduleDrag-CA9sGNJG.js.map +0 -1
  399. package/src/styles/components/grouping-modal.css +0 -323
@@ -1,422 +1,61 @@
1
1
  <!-- @deprecated Use AutoGroupModal instead -->
2
2
  <script setup lang="ts">
3
- /** @deprecated CSV-based sample grouping modal; use AutoGroupModal instead. */
4
- import { ref, computed, watch } from 'vue'
5
- import BaseModal from './BaseModal.vue'
6
- import BaseButton from './BaseButton.vue'
7
- import BaseSelect from './BaseSelect.vue'
8
- import type { SelectOption } from '../types'
3
+ /** @deprecated Compatibility wrapper for the legacy CSV grouping API. Use AutoGroupModal instead. */
4
+ import AutoGroupModal from './AutoGroupModal.vue'
5
+ import type { AutoGroupResult } from '../types/auto-group'
9
6
 
10
7
  interface Props {
11
8
  open: boolean
12
9
  samples: string[]
13
10
  }
14
11
 
15
- interface ParsedRow {
16
- [key: string]: string
17
- }
18
-
19
- const props = defineProps<Props>()
12
+ defineProps<Props>()
20
13
 
21
14
  const emit = defineEmits<{
22
15
  close: []
23
16
  apply: [mapping: Record<string, string[]>, columns: string[]]
24
17
  }>()
25
18
 
26
- const csvFile = ref<File | null>(null)
27
- const csvData = ref<ParsedRow[]>([])
28
- const csvColumns = ref<string[]>([])
29
- const sampleColumn = ref<string>('')
30
- const groupColumns = ref<string[]>([])
31
- const isDragOver = ref(false)
32
- const parseError = ref<string | null>(null)
33
-
34
- const columnOptions = computed<SelectOption[]>(() =>
35
- csvColumns.value.map(col => ({ value: col, label: col }))
36
- )
37
-
38
- const availableGroupColumns = computed<SelectOption[]>(() =>
39
- csvColumns.value
40
- .filter(col => col !== sampleColumn.value && !groupColumns.value.includes(col))
41
- .map(col => ({ value: col, label: col }))
42
- )
43
-
44
- const matchedSamples = computed(() => {
45
- if (!sampleColumn.value || csvData.value.length === 0) return []
46
- const csvSampleValues = csvData.value.map(row => row[sampleColumn.value])
47
- return props.samples.filter(sample => csvSampleValues.includes(sample))
48
- })
49
-
50
- const unmatchedSamples = computed(() => {
51
- if (!sampleColumn.value || csvData.value.length === 0) return []
52
- const csvSampleValues = csvData.value.map(row => row[sampleColumn.value])
53
- return props.samples.filter(sample => !csvSampleValues.includes(sample))
54
- })
55
-
56
- const previewData = computed(() => {
57
- if (csvData.value.length === 0) return []
58
- return csvData.value.slice(0, 5)
59
- })
60
-
61
- const canApply = computed(() =>
62
- sampleColumn.value &&
63
- groupColumns.value.length > 0 &&
64
- matchedSamples.value.length > 0
65
- )
19
+ let closeEmittedFromApply = false
66
20
 
67
- watch(() => props.open, (isOpen) => {
68
- if (!isOpen) {
69
- resetState()
70
- }
71
- })
72
-
73
- function resetState() {
74
- csvFile.value = null
75
- csvData.value = []
76
- csvColumns.value = []
77
- sampleColumn.value = ''
78
- groupColumns.value = []
79
- parseError.value = null
80
- }
81
-
82
- function handleDrop(event: DragEvent) {
83
- event.preventDefault()
84
- isDragOver.value = false
85
- const files = event.dataTransfer?.files
86
- if (files && files.length > 0) {
87
- handleFile(files[0])
88
- }
89
- }
90
-
91
- function handleFileInput(event: Event) {
92
- const target = event.target as HTMLInputElement
93
- if (target.files && target.files.length > 0) {
94
- handleFile(target.files[0])
95
- }
96
- }
97
-
98
- async function handleFile(file: File) {
99
- if (!file.name.endsWith('.csv')) {
100
- parseError.value = 'Please upload a CSV file'
101
- return
102
- }
103
-
104
- csvFile.value = file
105
- parseError.value = null
106
-
107
- try {
108
- const text = await file.text()
109
- parseCSV(text)
110
- } catch {
111
- parseError.value = 'Failed to read file'
112
- }
21
+ function handleApply(result: AutoGroupResult) {
22
+ emit('apply', groupsToLegacyMapping(result), metadataColumns(result))
23
+ closeEmittedFromApply = true
24
+ emit('close')
113
25
  }
114
26
 
115
- function parseCSV(text: string) {
116
- const lines = text.trim().split('\n')
117
- if (lines.length < 2) {
118
- parseError.value = 'CSV must have at least a header and one data row'
27
+ function handleOpenChange(isOpen: boolean) {
28
+ if (isOpen) return
29
+ if (closeEmittedFromApply) {
30
+ closeEmittedFromApply = false
119
31
  return
120
32
  }
121
-
122
- const headers = parseCSVLine(lines[0])
123
- csvColumns.value = headers
124
-
125
- const data: ParsedRow[] = []
126
- for (let i = 1; i < lines.length; i++) {
127
- const values = parseCSVLine(lines[i])
128
- if (values.length !== headers.length) continue
129
-
130
- const row: ParsedRow = {}
131
- headers.forEach((header, idx) => {
132
- row[header] = values[idx]
133
- })
134
- data.push(row)
135
- }
136
-
137
- csvData.value = data
138
-
139
- // Auto-select sample column if there's a likely match
140
- const sampleColGuess = headers.find(h =>
141
- h.toLowerCase().includes('sample') ||
142
- h.toLowerCase().includes('name') ||
143
- h.toLowerCase().includes('id')
144
- )
145
- if (sampleColGuess) {
146
- sampleColumn.value = sampleColGuess
147
- }
148
- }
149
-
150
- function parseCSVLine(line: string): string[] {
151
- const result: string[] = []
152
- let current = ''
153
- let inQuotes = false
154
-
155
- for (let i = 0; i < line.length; i++) {
156
- const char = line[i]
157
- if (char === '"') {
158
- inQuotes = !inQuotes
159
- } else if (char === ',' && !inQuotes) {
160
- result.push(current.trim())
161
- current = ''
162
- } else {
163
- current += char
164
- }
165
- }
166
- result.push(current.trim())
167
-
168
- return result
169
- }
170
-
171
- function addGroupColumn(column: string) {
172
- if (column && !groupColumns.value.includes(column)) {
173
- groupColumns.value.push(column)
174
- }
175
- }
176
-
177
- function removeGroupColumn(column: string) {
178
- groupColumns.value = groupColumns.value.filter(c => c !== column)
33
+ emit('close')
179
34
  }
180
35
 
181
- function moveGroupColumn(index: number, direction: 'up' | 'down') {
182
- const newIndex = direction === 'up' ? index - 1 : index + 1
183
- if (newIndex < 0 || newIndex >= groupColumns.value.length) return
184
-
185
- const cols = [...groupColumns.value]
186
- const temp = cols[index]
187
- cols[index] = cols[newIndex]
188
- cols[newIndex] = temp
189
- groupColumns.value = cols
36
+ function groupsToLegacyMapping(result: AutoGroupResult): Record<string, string[]> {
37
+ return Object.fromEntries(result.groups.map(group => [group.name, group.samples]))
190
38
  }
191
39
 
192
- function applyGrouping() {
193
- if (!canApply.value) return
194
-
195
- const mapping: Record<string, string[]> = {}
196
-
197
- for (const row of csvData.value) {
198
- const sampleName = row[sampleColumn.value]
199
- if (!props.samples.includes(sampleName)) continue
200
-
201
- // Build hierarchical group key
202
- const groupKey = groupColumns.value.map(col => row[col]).join(' / ')
203
-
204
- if (!mapping[groupKey]) {
205
- mapping[groupKey] = []
40
+ function metadataColumns(result: AutoGroupResult): string[] {
41
+ const columns: string[] = []
42
+ const seen = new Set<string>()
43
+ for (const row of result.metadata) {
44
+ for (const column of Object.keys(row.fields)) {
45
+ if (seen.has(column)) continue
46
+ seen.add(column)
47
+ columns.push(column)
206
48
  }
207
- mapping[groupKey].push(sampleName)
208
49
  }
209
-
210
- emit('apply', mapping, groupColumns.value)
211
- emit('close')
212
- }
213
-
214
- function close() {
215
- emit('close')
50
+ return columns
216
51
  }
217
52
  </script>
218
53
 
219
54
  <template>
220
- <BaseModal
55
+ <AutoGroupModal
221
56
  :model-value="open"
222
- title="Group by Metadata"
223
- size="lg"
224
- @update:model-value="!$event && close()"
225
- @close="close"
226
- >
227
- <div class="mint-grouping-modal">
228
- <!-- File upload area -->
229
- <div
230
- v-if="!csvFile"
231
- :class="[
232
- 'mint-grouping-modal__dropzone',
233
- isDragOver ? 'mint-grouping-modal__dropzone--dragover' : '',
234
- ]"
235
- @drop="handleDrop"
236
- @dragover.prevent="isDragOver = true"
237
- @dragleave="isDragOver = false"
238
- >
239
- <input
240
- type="file"
241
- accept=".csv"
242
- class="mint-grouping-modal__file-input"
243
- @change="handleFileInput"
244
- />
245
- <svg class="mint-grouping-modal__upload-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
246
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
247
- </svg>
248
- <p class="mint-grouping-modal__upload-text">
249
- <span class="mint-grouping-modal__upload-highlight">Click to upload</span>
250
- or drag and drop a CSV file
251
- </p>
252
- <p class="mint-grouping-modal__upload-hint">
253
- CSV should contain sample names and grouping columns
254
- </p>
255
- </div>
256
-
257
- <!-- Error message -->
258
- <div v-if="parseError" class="mint-grouping-modal__error">
259
- {{ parseError }}
260
- </div>
261
-
262
- <!-- Configuration section -->
263
- <template v-if="csvFile && csvData.length > 0">
264
- <div class="mint-grouping-modal__file-info">
265
- <svg class="mint-grouping-modal__file-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
266
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
267
- </svg>
268
- <span class="mint-grouping-modal__file-name">{{ csvFile.name }}</span>
269
- <button
270
- type="button"
271
- class="mint-grouping-modal__file-clear"
272
- @click="resetState"
273
- >
274
- Change file
275
- </button>
276
- </div>
277
-
278
- <!-- Column selection -->
279
- <div class="mint-grouping-modal__config">
280
- <div class="mint-grouping-modal__config-row">
281
- <label class="mint-grouping-modal__label">Sample Column</label>
282
- <BaseSelect
283
- v-model="sampleColumn"
284
- :options="columnOptions"
285
- placeholder="Select sample column..."
286
- />
287
- </div>
288
-
289
- <div class="mint-grouping-modal__config-row">
290
- <label class="mint-grouping-modal__label">Group Columns</label>
291
- <div class="mint-grouping-modal__group-columns">
292
- <!-- Selected group columns with reordering -->
293
- <div v-if="groupColumns.length > 0" class="mint-grouping-modal__selected-columns">
294
- <div
295
- v-for="(col, index) in groupColumns"
296
- :key="col"
297
- class="mint-grouping-modal__selected-column"
298
- >
299
- <span class="mint-grouping-modal__column-name">{{ col }}</span>
300
- <div class="mint-grouping-modal__column-actions">
301
- <button
302
- type="button"
303
- :disabled="index === 0"
304
- class="mint-grouping-modal__column-btn"
305
- @click="moveGroupColumn(index, 'up')"
306
- >
307
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
308
- <path d="M18 15l-6-6-6 6" />
309
- </svg>
310
- </button>
311
- <button
312
- type="button"
313
- :disabled="index === groupColumns.length - 1"
314
- class="mint-grouping-modal__column-btn"
315
- @click="moveGroupColumn(index, 'down')"
316
- >
317
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
318
- <path d="M6 9l6 6 6-6" />
319
- </svg>
320
- </button>
321
- <button
322
- type="button"
323
- class="mint-grouping-modal__column-btn mint-grouping-modal__column-btn--remove"
324
- @click="removeGroupColumn(col)"
325
- >
326
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
327
- <path d="M6 18L18 6M6 6l12 12" />
328
- </svg>
329
- </button>
330
- </div>
331
- </div>
332
- </div>
333
-
334
- <!-- Add column dropdown -->
335
- <BaseSelect
336
- v-if="availableGroupColumns.length > 0"
337
- model-value=""
338
- :options="availableGroupColumns"
339
- placeholder="Add group column..."
340
- @update:model-value="addGroupColumn($event as string)"
341
- />
342
- </div>
343
- </div>
344
- </div>
345
-
346
- <!-- Match status -->
347
- <div v-if="sampleColumn" class="mint-grouping-modal__match-status">
348
- <div class="mint-grouping-modal__match-stat mint-grouping-modal__match-stat--matched">
349
- <span class="mint-grouping-modal__match-count">{{ matchedSamples.length }}</span>
350
- <span class="mint-grouping-modal__match-label">samples matched</span>
351
- </div>
352
- <div
353
- v-if="unmatchedSamples.length > 0"
354
- class="mint-grouping-modal__match-stat mint-grouping-modal__match-stat--unmatched"
355
- >
356
- <span class="mint-grouping-modal__match-count">{{ unmatchedSamples.length }}</span>
357
- <span class="mint-grouping-modal__match-label">samples not found in CSV</span>
358
- </div>
359
- </div>
360
-
361
- <!-- Preview table -->
362
- <div v-if="previewData.length > 0" class="mint-grouping-modal__preview">
363
- <h4 class="mint-grouping-modal__preview-title">Data Preview</h4>
364
- <div class="mint-grouping-modal__table-wrapper">
365
- <table class="mint-grouping-modal__table">
366
- <thead>
367
- <tr>
368
- <th
369
- v-for="col in csvColumns"
370
- :key="col"
371
- :class="[
372
- 'mint-grouping-modal__th',
373
- col === sampleColumn ? 'mint-grouping-modal__th--sample' : '',
374
- groupColumns.includes(col) ? 'mint-grouping-modal__th--group' : '',
375
- ]"
376
- >
377
- {{ col }}
378
- </th>
379
- </tr>
380
- </thead>
381
- <tbody>
382
- <tr v-for="(row, idx) in previewData" :key="idx">
383
- <td
384
- v-for="col in csvColumns"
385
- :key="col"
386
- :class="[
387
- 'mint-grouping-modal__td',
388
- col === sampleColumn ? 'mint-grouping-modal__td--sample' : '',
389
- groupColumns.includes(col) ? 'mint-grouping-modal__td--group' : '',
390
- ]"
391
- >
392
- {{ row[col] }}
393
- </td>
394
- </tr>
395
- </tbody>
396
- </table>
397
- </div>
398
- <p v-if="csvData.length > 5" class="mint-grouping-modal__preview-note">
399
- Showing 5 of {{ csvData.length }} rows
400
- </p>
401
- </div>
402
- </template>
403
- </div>
404
-
405
- <template #footer>
406
- <div class="mint-grouping-modal__footer">
407
- <BaseButton variant="secondary" @click="close">Cancel</BaseButton>
408
- <BaseButton
409
- variant="primary"
410
- :disabled="!canApply"
411
- @click="applyGrouping"
412
- >
413
- Apply Grouping
414
- </BaseButton>
415
- </div>
416
- </template>
417
- </BaseModal>
57
+ :samples="samples"
58
+ @update:model-value="handleOpenChange"
59
+ @apply="handleApply"
60
+ />
418
61
  </template>
419
-
420
- <style>
421
- @import '../styles/components/grouping-modal.css';
422
- </style>
@@ -1,11 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  /** Lets users select multiple options from a checkbox list with optional selection cap. */
3
3
  import { computed } from 'vue'
4
- import type { MultiSelectOption, MultiSelectSize } from '../types'
4
+ import type { MultiSelectOption, MultiSelectOptionInput, MultiSelectSize } from '../types'
5
+ import { useListSelection } from '../composables/useListSelection'
6
+ import { normalizeOptionInput } from '../utils/options'
5
7
 
6
8
  interface Props {
7
9
  modelValue: (string | number)[]
8
- options: MultiSelectOption[]
10
+ options: MultiSelectOptionInput[]
9
11
  placeholder?: string
10
12
  disabled?: boolean
11
13
  maxSelections?: number
@@ -22,22 +24,25 @@ const emit = defineEmits<{
22
24
  'update:modelValue': [value: (string | number)[]]
23
25
  }>()
24
26
 
25
- const canSelectMore = computed(() => {
26
- if (props.maxSelections === undefined) return true
27
- return props.modelValue.length < props.maxSelections
27
+ const normalizedOptions = computed<MultiSelectOption[]>(() => props.options.map(normalizeOptionInput))
28
+ const optionSelection = useListSelection({
29
+ selected: () => props.modelValue,
30
+ items: normalizedOptions,
31
+ getValue: option => option.value,
32
+ disabled: () => props.disabled,
33
+ max: () => props.maxSelections,
28
34
  })
35
+ const canSelectMore = optionSelection.canAddMore
29
36
 
30
37
  function isSelected(value: string | number): boolean {
31
- return props.modelValue.includes(value)
38
+ return optionSelection.isSelected(value)
32
39
  }
33
40
 
34
41
  function toggleOption(option: MultiSelectOption) {
35
42
  if (props.disabled || option.disabled) return
36
43
 
37
- if (isSelected(option.value)) {
38
- emit('update:modelValue', props.modelValue.filter(v => v !== option.value))
39
- } else if (canSelectMore.value) {
40
- emit('update:modelValue', [...props.modelValue, option.value])
44
+ if (isSelected(option.value) || canSelectMore.value) {
45
+ emit('update:modelValue', optionSelection.toggleValue(option.value))
41
46
  }
42
47
  }
43
48
 
@@ -50,7 +55,7 @@ function handleKeydown(event: KeyboardEvent, option: MultiSelectOption) {
50
55
 
51
56
  const selectedLabels = computed(() => {
52
57
  return props.modelValue
53
- .map(v => props.options.find(o => o.value === v)?.label)
58
+ .map(v => normalizedOptions.value.find(o => o.value === v)?.label)
54
59
  .filter(Boolean)
55
60
  })
56
61
  </script>
@@ -72,7 +77,7 @@ const selectedLabels = computed(() => {
72
77
 
73
78
  <div class="mint-multi-select__options" role="group">
74
79
  <button
75
- v-for="option in options"
80
+ v-for="option in normalizedOptions"
76
81
  :key="String(option.value)"
77
82
  type="button"
78
83
  role="checkbox"
@@ -1,8 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  /** Full plate-map design environment for 96/384-well plates with sample assignment, slot-color coding, undo/redo, and CSV/JSON import-export. */
3
- import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
3
+ import { ref, computed, watch } from 'vue'
4
4
  import type { PlateMapEditorState, WellPlateFormat, SampleType, Well } from '../types'
5
5
  import { useWellPlateEditor } from '../composables/useWellPlateEditor'
6
+ import { useEventListener } from '../composables/useEventListener'
6
7
  import WellPlate from './WellPlate.vue'
7
8
  import SampleLegend from './SampleLegend.vue'
8
9
 
@@ -221,13 +222,7 @@ function handleKeyDown(event: KeyboardEvent) {
221
222
  }
222
223
  }
223
224
 
224
- onMounted(() => {
225
- document.addEventListener('keydown', handleKeyDown)
226
- })
227
-
228
- onUnmounted(() => {
229
- document.removeEventListener('keydown', handleKeyDown)
230
- })
225
+ useEventListener(() => document, 'keydown', handleKeyDown)
231
226
  </script>
232
227
 
233
228
  <template>
@@ -8,6 +8,7 @@
8
8
  * http:// URLs and data:image/svg+xml URLs are deliberately rejected
9
9
  * (mixed-content + XSS, see spec 2026-05-04-plugin-icon-component-design.md). */
10
10
  import { computed } from 'vue'
11
+ import { detectPluginIcon, type DetectedPluginIcon } from '../utils/pluginIcon'
11
12
 
12
13
  defineOptions({ name: 'PluginIcon' })
13
14
 
@@ -23,28 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
23
24
  variant: 'solid',
24
25
  })
25
26
 
26
- // MINT canonical plugin placeholder — page-with-corner-fold glyph.
27
- const FALLBACK_PATH = 'M14 7v4a1 1 0 0 0 1 1h4M5 3h9l5 5v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'
28
-
29
- const PATH_REGEX = /^[Mm][\s,\d\-.]/
30
- const RASTER_DATA_URL_REGEX = /^data:image\/(png|jpeg|jpg|gif|webp);/
31
-
32
- type Format = 'path' | 'data-url' | 'https-url' | 'fallback'
33
-
34
- interface Detected {
35
- format: Format
36
- value: string
37
- }
38
-
39
- const detected = computed<Detected>(() => {
40
- // Strip leading whitespace and BOM before pattern checks.
41
- const raw = (props.icon ?? '').replace(/^[\s]+/, '')
42
- if (!raw) return { format: 'fallback', value: FALLBACK_PATH }
43
- if (PATH_REGEX.test(raw)) return { format: 'path', value: raw }
44
- if (RASTER_DATA_URL_REGEX.test(raw)) return { format: 'data-url', value: raw }
45
- if (raw.startsWith('https://')) return { format: 'https-url', value: raw }
46
- return { format: 'fallback', value: FALLBACK_PATH }
47
- })
27
+ const detected = computed<DetectedPluginIcon>(() => detectPluginIcon(props.icon))
48
28
 
49
29
  const rootClasses = computed(() => [
50
30
  'mint-plugin-icon',
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  /** Form for creating or editing a single protocol step (incubation, wash, addition, centrifuge, etc.) with template picker, typed parameters, and duration. */
3
- import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
3
+ import { ref, computed, watch, onMounted } from 'vue'
4
4
  import type { ProtocolStep, ProtocolStepType, ProtocolStepStatus } from '../types'
5
+ import { useDropdownState } from '../composables/useDropdownState'
5
6
  import {
6
7
  useProtocolTemplates,
7
8
  type StepTemplate,
@@ -38,7 +39,12 @@ const {
38
39
 
39
40
  // State
40
41
  const selectedTemplateId = ref<string | null>(null)
41
- const dropdownOpen = ref(false)
42
+ const {
43
+ isOpen: templateDropdownOpen,
44
+ rootRef: templateDropdownRef,
45
+ close: closeTemplateDropdown,
46
+ toggle: toggleTemplateDropdown,
47
+ } = useDropdownState()
42
48
  const stepName = ref('')
43
49
  const stepDescription = ref('')
44
50
  const stepDuration = ref<number | undefined>()
@@ -113,7 +119,7 @@ function initFromStep(step: ProtocolStep) {
113
119
  // Handle template selection
114
120
  function selectTemplate(template: StepTemplate) {
115
121
  selectedTemplateId.value = template.id
116
- dropdownOpen.value = false
122
+ closeTemplateDropdown()
117
123
 
118
124
  // Reset to template defaults if creating new
119
125
  if (props.mode === 'create') {
@@ -185,14 +191,6 @@ function handleCancel() {
185
191
  emit('cancel')
186
192
  }
187
193
 
188
- // Close dropdown on click outside
189
- function handleClickOutside(event: MouseEvent) {
190
- const target = event.target as HTMLElement
191
- if (!target.closest('.mint-protocol-editor__template-dropdown')) {
192
- dropdownOpen.value = false
193
- }
194
- }
195
-
196
194
  // Format duration
197
195
  function formatDuration(minutes: number | undefined): string {
198
196
  if (minutes === undefined) return ''
@@ -202,10 +200,7 @@ function formatDuration(minutes: number | undefined): string {
202
200
  return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
203
201
  }
204
202
 
205
- // Lifecycle
206
203
  onMounted(() => {
207
- document.addEventListener('click', handleClickOutside)
208
-
209
204
  if (props.modelValue) {
210
205
  initFromStep(props.modelValue)
211
206
  } else if (availableTemplates.value.length > 0) {
@@ -213,10 +208,6 @@ onMounted(() => {
213
208
  }
214
209
  })
215
210
 
216
- onUnmounted(() => {
217
- document.removeEventListener('click', handleClickOutside)
218
- })
219
-
220
211
  // Watch for external changes
221
212
  watch(
222
213
  () => props.modelValue,
@@ -250,11 +241,11 @@ watch(
250
241
  <!-- Template selector -->
251
242
  <div class="mint-protocol-editor__template-select">
252
243
  <label class="mint-protocol-editor__template-label">Template</label>
253
- <div class="mint-protocol-editor__template-dropdown">
244
+ <div ref="templateDropdownRef" class="mint-protocol-editor__template-dropdown">
254
245
  <button
255
246
  type="button"
256
247
  class="mint-protocol-editor__template-btn"
257
- @click.stop="dropdownOpen = !dropdownOpen"
248
+ @click="toggleTemplateDropdown"
258
249
  >
259
250
  <svg
260
251
  v-if="selectedTemplate"
@@ -276,7 +267,7 @@ watch(
276
267
  <svg
277
268
  :class="[
278
269
  'mint-protocol-editor__template-arrow',
279
- dropdownOpen ? 'mint-protocol-editor__template-arrow--open' : '',
270
+ templateDropdownOpen ? 'mint-protocol-editor__template-arrow--open' : '',
280
271
  ]"
281
272
  fill="none"
282
273
  stroke="currentColor"
@@ -286,7 +277,7 @@ watch(
286
277
  </svg>
287
278
  </button>
288
279
 
289
- <div v-if="dropdownOpen" class="mint-protocol-editor__template-menu">
280
+ <div v-if="templateDropdownOpen" class="mint-protocol-editor__template-menu">
290
281
  <button
291
282
  v-for="template in availableTemplates"
292
283
  :key="template.id"