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

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 (427) hide show
  1. package/README.md +225 -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/ComponentBindingRenderer.test.d.ts +1 -0
  19. package/dist/__tests__/components/ConcentrationInput.test.d.ts +1 -0
  20. package/dist/__tests__/components/ControlWorkspaceView.test.d.ts +1 -0
  21. package/dist/__tests__/components/DatePicker.test.d.ts +1 -0
  22. package/dist/__tests__/components/DateTimePicker.test.d.ts +1 -0
  23. package/dist/__tests__/components/DoseDesignWorkspaceView.test.d.ts +1 -0
  24. package/dist/__tests__/components/EmptyState.test.d.ts +1 -0
  25. package/dist/__tests__/components/ExperimentPopover.test.d.ts +1 -0
  26. package/dist/__tests__/components/FormBuilder.test.d.ts +1 -0
  27. package/dist/__tests__/components/GroupAssigner.test.d.ts +1 -0
  28. package/dist/__tests__/components/MultiSelect.test.d.ts +1 -0
  29. package/dist/__tests__/components/PluginWorkspaceView.test.d.ts +1 -0
  30. package/dist/__tests__/components/ProtocolStepEditor.test.d.ts +1 -0
  31. package/dist/__tests__/components/ReagentList.test.d.ts +1 -0
  32. package/dist/__tests__/components/SampleHierarchyTree.test.d.ts +1 -0
  33. package/dist/__tests__/components/SampleSelector.test.d.ts +1 -0
  34. package/dist/__tests__/components/SegmentedControl.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/experiment-utils.test.d.ts +1 -0
  40. package/dist/__tests__/composables/useApi.test.d.ts +1 -0
  41. package/dist/__tests__/composables/useBioTemplatePackWorkspace.test.d.ts +1 -0
  42. package/dist/__tests__/composables/useBioTemplatePresetWorkspace.test.d.ts +1 -0
  43. package/dist/__tests__/composables/useBioTemplateWorkspace.test.d.ts +1 -0
  44. package/dist/__tests__/composables/useCalendarGrid.test.d.ts +1 -0
  45. package/dist/__tests__/composables/useControlSchema.test.d.ts +1 -0
  46. package/dist/__tests__/composables/useDebouncedWatch.test.d.ts +1 -0
  47. package/dist/__tests__/composables/useDropdownState.test.d.ts +1 -0
  48. package/dist/__tests__/composables/useEventListener.test.d.ts +1 -0
  49. package/dist/__tests__/composables/useExpansionSet.test.d.ts +1 -0
  50. package/dist/__tests__/composables/useExperimentData.test.d.ts +1 -0
  51. package/dist/__tests__/composables/useExperimentSelector.test.d.ts +1 -0
  52. package/dist/__tests__/composables/useGroupAssignment.test.d.ts +1 -0
  53. package/dist/__tests__/composables/useListSelection.test.d.ts +1 -0
  54. package/dist/__tests__/composables/usePluginClient.test.d.ts +1 -0
  55. package/dist/__tests__/composables/usePluginConfig.test.d.ts +1 -0
  56. package/dist/__tests__/composables/useRequestSyncState.test.d.ts +1 -0
  57. package/dist/__tests__/composables/useSampleGroups.test.d.ts +1 -0
  58. package/dist/__tests__/composables/useSelectionLimit.test.d.ts +1 -0
  59. package/dist/__tests__/composables/useSortedItems.test.d.ts +1 -0
  60. package/dist/__tests__/composables/useTemplateCollection.test.d.ts +1 -0
  61. package/dist/__tests__/composables/useTextSearch.test.d.ts +1 -0
  62. package/dist/__tests__/composables/useTheme.test.d.ts +1 -0
  63. package/dist/__tests__/composables/useTimeUtils.test.d.ts +1 -0
  64. package/dist/__tests__/docs/frontendDocsCatalog.test.d.ts +1 -0
  65. package/dist/__tests__/templates/templates.test.d.ts +1 -0
  66. package/dist/{auth-DsI0rQ7_.js → auth-QQj2kkze.js} +12 -5
  67. package/dist/auth-QQj2kkze.js.map +1 -0
  68. package/dist/components/AppAvatarMenu.vue.d.ts +2 -7
  69. package/dist/components/AppContainer.vue.d.ts +1 -1
  70. package/dist/components/AppLayout.vue.d.ts +20 -1
  71. package/dist/components/AppSidebar.vue.d.ts +111 -6
  72. package/dist/components/AppTopBar.vue.d.ts +35 -22
  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 +119 -0
  83. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +93 -0
  84. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +87 -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/CollapsibleCard.vue.d.ts +1 -1
  89. package/dist/components/ComponentBindingRenderer.vue.d.ts +44 -0
  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 +147 -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/DoseDesignWorkspaceView.vue.d.ts +149 -0
  97. package/dist/components/DropdownButton.vue.d.ts +3 -3
  98. package/dist/components/EmptyState.vue.d.ts +1 -2
  99. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  100. package/dist/components/ExperimentTimeline.vue.d.ts +2 -2
  101. package/dist/components/FileUploader.vue.d.ts +1 -1
  102. package/dist/components/FitPanel.vue.d.ts +1 -1
  103. package/dist/components/FormActions.vue.d.ts +4 -4
  104. package/dist/components/FormBuilder.vue.d.ts +31 -17
  105. package/dist/components/FormulaInput.vue.d.ts +2 -2
  106. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  107. package/dist/components/MultiSelect.vue.d.ts +3 -3
  108. package/dist/components/NumberInput.vue.d.ts +1 -1
  109. package/dist/components/PlateMapEditor.vue.d.ts +1 -1
  110. package/dist/components/PluginWorkspaceView.vue.d.ts +310 -0
  111. package/dist/components/ProgressBar.vue.d.ts +1 -1
  112. package/dist/components/ProtocolStepEditor.vue.d.ts +3 -1
  113. package/dist/components/RackEditor.vue.d.ts +2 -2
  114. package/dist/components/SampleLegend.vue.d.ts +2 -2
  115. package/dist/components/ScheduleCalendar.vue.d.ts +2 -2
  116. package/dist/components/SegmentedControl.vue.d.ts +2 -2
  117. package/dist/components/SequenceInput.vue.d.ts +3 -3
  118. package/dist/components/SettingsModal.vue.d.ts +14 -6
  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 +6 -6
  125. package/dist/components/index.d.ts +9 -8
  126. package/dist/components/index.js +3 -3
  127. package/dist/components/{SettingsButton.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +11 -9
  128. package/dist/components/{AppPageSelector.vue.d.ts → internal/AppPageSelectorInternal.vue.d.ts} +3 -6
  129. package/dist/components/{AppPillNav.vue.d.ts → internal/AppPillNavInternal.vue.d.ts} +4 -2
  130. package/dist/components/internal/CalendarGridPanelInternal.vue.d.ts +25 -0
  131. package/dist/components/{FormFieldRenderer.vue.d.ts → internal/FormFieldRendererInternal.vue.d.ts} +2 -2
  132. package/dist/components/{FormSection.vue.d.ts → internal/FormSectionRenderer.vue.d.ts} +7 -7
  133. package/dist/components/{WellEditPopup.vue.d.ts → internal/WellEditPopupInternal.vue.d.ts} +1 -1
  134. package/dist/{components-_XqPEhP9.js → components-BkGF4B4y.js} +9760 -8471
  135. package/dist/components-BkGF4B4y.js.map +1 -0
  136. package/dist/composables/experiment-utils.d.ts +8 -0
  137. package/dist/composables/index.d.ts +22 -5
  138. package/dist/composables/index.js +4 -3
  139. package/dist/composables/platformContextHelpers.d.ts +14 -0
  140. package/dist/composables/useAppExperiment.d.ts +31 -2
  141. package/dist/composables/useBioTemplateComponents.d.ts +22 -0
  142. package/dist/composables/useBioTemplateControls.d.ts +6 -0
  143. package/dist/composables/useBioTemplatePackWorkspace.d.ts +46 -0
  144. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +75 -0
  145. package/dist/composables/useBioTemplateWorkspace.d.ts +51 -0
  146. package/dist/composables/useCalendarGrid.d.ts +26 -0
  147. package/dist/composables/useControlSchema.d.ts +343 -0
  148. package/dist/composables/useDebouncedWatch.d.ts +20 -0
  149. package/dist/composables/useDropdownState.d.ts +19 -0
  150. package/dist/composables/useEventListener.d.ts +13 -0
  151. package/dist/composables/useExpansionSet.d.ts +21 -0
  152. package/dist/composables/useExperimentData.d.ts +10 -0
  153. package/dist/composables/useExperimentSave.d.ts +31 -2
  154. package/dist/composables/useExperimentSelector.d.ts +20 -0
  155. package/dist/composables/useForm.d.ts +2 -0
  156. package/dist/composables/useGroupAssignment.d.ts +31 -0
  157. package/dist/composables/useListSelection.d.ts +35 -0
  158. package/dist/composables/usePlatformContext.d.ts +21 -3
  159. package/dist/composables/usePluginClient.d.ts +112 -0
  160. package/dist/composables/usePluginConfig.d.ts +12 -0
  161. package/dist/composables/useRequestSyncState.d.ts +34 -0
  162. package/dist/composables/useSampleGroups.d.ts +32 -0
  163. package/dist/composables/useSelectionLimit.d.ts +17 -0
  164. package/dist/composables/useSortedItems.d.ts +32 -0
  165. package/dist/composables/useTemplateCollection.d.ts +58 -0
  166. package/dist/composables/useTextSearch.d.ts +18 -0
  167. package/dist/composables/useTimeUtils.d.ts +8 -0
  168. package/dist/{composables-tiZqLu1M.js → composables-CHsME9H1.js} +240 -146
  169. package/dist/composables-CHsME9H1.js.map +1 -0
  170. package/dist/index.d.ts +6 -4
  171. package/dist/index.js +6 -5
  172. package/dist/install.d.ts +7 -2
  173. package/dist/install.js +2 -2
  174. package/dist/install.js.map +1 -1
  175. package/dist/stores/index.js +1 -1
  176. package/dist/stores/settings.d.ts +4 -1
  177. package/dist/styles.css +4746 -5514
  178. package/dist/templates/adapters.d.ts +43 -0
  179. package/dist/templates/builders.d.ts +63 -0
  180. package/dist/templates/catalog.d.ts +188 -0
  181. package/dist/templates/componentBindings.d.ts +71 -0
  182. package/dist/templates/controlSchemas.d.ts +25 -0
  183. package/dist/templates/index.d.ts +15 -0
  184. package/dist/templates/index.js +2 -0
  185. package/dist/templates/lookup.d.ts +4 -0
  186. package/dist/templates/packs.d.ts +18 -0
  187. package/dist/templates/presets.d.ts +90 -0
  188. package/dist/templates/types.d.ts +531 -0
  189. package/dist/templates-B5jmTWuk.js +9388 -0
  190. package/dist/templates-B5jmTWuk.js.map +1 -0
  191. package/dist/types/components.d.ts +26 -23
  192. package/dist/types/form-builder.d.ts +6 -8
  193. package/dist/types/index.d.ts +2 -2
  194. package/dist/types/platform.d.ts +7 -1
  195. package/dist/useScheduleDrag-BgzpQT53.js +4414 -0
  196. package/dist/useScheduleDrag-BgzpQT53.js.map +1 -0
  197. package/dist/utils/formModelSync.d.ts +5 -0
  198. package/dist/utils/items.d.ts +8 -0
  199. package/dist/utils/options.d.ts +6 -0
  200. package/dist/utils/pluginIcon.d.ts +9 -0
  201. package/package.json +7 -2
  202. package/src/__tests__/components/ActionItem.test.ts +99 -0
  203. package/src/__tests__/components/AppAvatarMenu.test.ts +27 -0
  204. package/src/__tests__/components/AppLayout.test.ts +44 -0
  205. package/src/__tests__/components/AppPageSelector.test.ts +134 -0
  206. package/src/__tests__/components/AppPillNav.test.ts +125 -0
  207. package/src/__tests__/components/AppPluginSwitcher.test.ts +44 -0
  208. package/src/__tests__/components/AppSidebar.test.ts +496 -0
  209. package/src/__tests__/components/AppToastContainer.test.ts +37 -0
  210. package/src/__tests__/components/AppTopBar.test.ts +455 -9
  211. package/src/__tests__/components/BaseRadioGroup.test.ts +25 -0
  212. package/src/__tests__/components/BaseSelect.test.ts +21 -0
  213. package/src/__tests__/components/BaseTabs.test.ts +25 -0
  214. package/src/__tests__/components/BatchProgressList.test.ts +52 -0
  215. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +159 -0
  216. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +175 -0
  217. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +306 -0
  218. package/src/__tests__/components/BioTemplateRenderer.test.ts +71 -0
  219. package/src/__tests__/components/Breadcrumb.test.ts +23 -0
  220. package/src/__tests__/components/CalendarGridPanel.test.ts +36 -0
  221. package/src/__tests__/components/ComponentBindingRenderer.test.ts +161 -0
  222. package/src/__tests__/components/ConcentrationInput.test.ts +45 -0
  223. package/src/__tests__/components/ControlWorkspaceView.test.ts +1102 -0
  224. package/src/__tests__/components/DataFrame.test.ts +11 -0
  225. package/src/__tests__/components/DatePicker.test.ts +45 -0
  226. package/src/__tests__/components/DateTimePicker.test.ts +48 -0
  227. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
  228. package/src/__tests__/components/DropdownButton.test.ts +23 -0
  229. package/src/__tests__/components/EmptyState.test.ts +23 -0
  230. package/src/__tests__/components/ExperimentPopover.test.ts +56 -0
  231. package/src/__tests__/components/FormBuilder.test.ts +296 -0
  232. package/src/__tests__/components/GroupAssigner.test.ts +30 -0
  233. package/src/__tests__/components/MultiSelect.test.ts +48 -0
  234. package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
  235. package/src/__tests__/components/ProtocolStepEditor.test.ts +33 -0
  236. package/src/__tests__/components/ReagentList.test.ts +82 -0
  237. package/src/__tests__/components/SampleHierarchyTree.test.ts +53 -0
  238. package/src/__tests__/components/SampleSelector.test.ts +60 -0
  239. package/src/__tests__/components/SegmentedControl.test.ts +24 -0
  240. package/src/__tests__/components/SettingsModal.test.ts +296 -0
  241. package/src/__tests__/components/TagsInput.test.ts +75 -0
  242. package/src/__tests__/components/ThemeToggle.test.ts +47 -0
  243. package/src/__tests__/components/TimePicker.test.ts +38 -0
  244. package/src/__tests__/composables/experiment-utils.test.ts +30 -0
  245. package/src/__tests__/composables/useApi.test.ts +30 -0
  246. package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
  247. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +125 -0
  248. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +199 -0
  249. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +104 -0
  250. package/src/__tests__/composables/useCalendarGrid.test.ts +38 -0
  251. package/src/__tests__/composables/useControlSchema.test.ts +1033 -0
  252. package/src/__tests__/composables/useDebouncedWatch.test.ts +93 -0
  253. package/src/__tests__/composables/useDropdownState.test.ts +95 -0
  254. package/src/__tests__/composables/useEventListener.test.ts +116 -0
  255. package/src/__tests__/composables/useExpansionSet.test.ts +62 -0
  256. package/src/__tests__/composables/useExperimentData.test.ts +4 -0
  257. package/src/__tests__/composables/useExperimentSave.test.ts +203 -8
  258. package/src/__tests__/composables/useExperimentSelector.test.ts +164 -0
  259. package/src/__tests__/composables/useForm.test.ts +58 -0
  260. package/src/__tests__/composables/useFormBuilder.test.ts +77 -0
  261. package/src/__tests__/composables/useGroupAssignment.test.ts +73 -0
  262. package/src/__tests__/composables/useListSelection.test.ts +66 -0
  263. package/src/__tests__/composables/usePluginClient.test.ts +541 -0
  264. package/src/__tests__/composables/usePluginConfig.test.ts +5 -0
  265. package/src/__tests__/composables/useRequestSyncState.test.ts +92 -0
  266. package/src/__tests__/composables/useSampleGroups.test.ts +66 -0
  267. package/src/__tests__/composables/useSelectionLimit.test.ts +41 -0
  268. package/src/__tests__/composables/useSortedItems.test.ts +87 -0
  269. package/src/__tests__/composables/useTemplateCollection.test.ts +147 -0
  270. package/src/__tests__/composables/useTextSearch.test.ts +55 -0
  271. package/src/__tests__/composables/useTheme.test.ts +91 -0
  272. package/src/__tests__/composables/useTimeUtils.test.ts +35 -0
  273. package/src/__tests__/docs/frontendDocsCatalog.test.ts +324 -0
  274. package/src/__tests__/fixtures/templates/dose-response.json +81 -0
  275. package/src/__tests__/fixtures/templates/plate-map.json +54 -0
  276. package/src/__tests__/fixtures/templates/qpcr-plate.json +96 -0
  277. package/src/__tests__/fixtures/templates/sample-sheet.json +71 -0
  278. package/src/__tests__/templates/templates.test.ts +1055 -0
  279. package/src/components/AppAvatarMenu.vue +15 -69
  280. package/src/components/AppLayout.story.vue +64 -25
  281. package/src/components/AppLayout.vue +83 -2
  282. package/src/components/AppPluginSwitcher.vue +41 -145
  283. package/src/components/AppSidebar.story.vue +203 -1
  284. package/src/components/AppSidebar.vue +320 -25
  285. package/src/components/{ToastNotification.story.vue → AppToastContainer.story.vue} +6 -6
  286. package/src/components/{ToastNotification.vue → AppToastContainer.vue} +1 -1
  287. package/src/components/AppTopBar.story.vue +7 -33
  288. package/src/components/AppTopBar.vue +104 -300
  289. package/src/components/BaseModal.vue +3 -5
  290. package/src/components/BaseRadioGroup.vue +7 -3
  291. package/src/components/BaseSelect.vue +11 -7
  292. package/src/components/BaseTabs.vue +6 -4
  293. package/src/components/BatchProgressList.vue +5 -8
  294. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +123 -0
  295. package/src/components/BioTemplateExperimentWorkspaceView.vue +343 -0
  296. package/src/components/BioTemplatePackWorkspaceView.story.vue +107 -0
  297. package/src/components/BioTemplatePackWorkspaceView.vue +177 -0
  298. package/src/components/BioTemplatePresetWorkspaceView.story.vue +163 -0
  299. package/src/components/BioTemplatePresetWorkspaceView.vue +401 -0
  300. package/src/components/BioTemplateRenderer.story.vue +57 -0
  301. package/src/components/BioTemplateRenderer.vue +57 -0
  302. package/src/components/Breadcrumb.vue +14 -8
  303. package/src/components/ComponentBindingRenderer.story.vue +57 -0
  304. package/src/components/ComponentBindingRenderer.vue +308 -0
  305. package/src/components/ConcentrationInput.vue +27 -64
  306. package/src/components/ControlWorkspaceView.story.vue +347 -0
  307. package/src/components/ControlWorkspaceView.vue +378 -0
  308. package/src/components/DataFrame.vue +34 -50
  309. package/src/components/DatePicker.vue +59 -192
  310. package/src/components/DateTimePicker.vue +50 -171
  311. package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
  312. package/src/components/DoseDesignWorkspaceView.vue +255 -0
  313. package/src/components/DropdownButton.vue +14 -32
  314. package/src/components/EmptyState.vue +4 -2
  315. package/src/components/ExperimentPopover.vue +7 -28
  316. package/src/components/ExperimentSelectorModal.vue +6 -5
  317. package/src/components/FormBuilder.story.vue +190 -0
  318. package/src/components/FormBuilder.vue +124 -27
  319. package/src/components/GroupAssigner.vue +24 -56
  320. package/src/components/MultiSelect.vue +17 -12
  321. package/src/components/PlateMapEditor.vue +3 -8
  322. package/src/components/PluginIcon.vue +2 -22
  323. package/src/components/PluginWorkspaceView.story.vue +334 -0
  324. package/src/components/PluginWorkspaceView.vue +708 -0
  325. package/src/components/ProtocolStepEditor.vue +13 -22
  326. package/src/components/ReagentList.vue +25 -33
  327. package/src/components/SampleHierarchyTree.vue +12 -23
  328. package/src/components/SampleSelector.vue +42 -122
  329. package/src/components/SegmentedControl.vue +7 -3
  330. package/src/components/SettingsModal.story.vue +88 -1
  331. package/src/components/SettingsModal.vue +120 -29
  332. package/src/components/TagsInput.vue +29 -14
  333. package/src/components/ThemeToggle.vue +9 -7
  334. package/src/components/TimePicker.vue +19 -41
  335. package/src/components/Tooltip.vue +7 -12
  336. package/src/components/WellPlate.vue +6 -12
  337. package/src/components/index.ts +9 -8
  338. package/src/components/internal/ActionItemInternal.vue +82 -0
  339. package/src/components/internal/AppPageSelectorInternal.vue +128 -0
  340. package/src/components/internal/AppPillNavInternal.vue +194 -0
  341. package/src/components/internal/CalendarGridPanelInternal.vue +120 -0
  342. package/src/components/{FormFieldRenderer.vue → internal/FormFieldRendererInternal.vue} +4 -12
  343. package/src/components/{FormSection.vue → internal/FormSectionRenderer.vue} +6 -18
  344. package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +5 -10
  345. package/src/composables/experiment-utils.ts +26 -0
  346. package/src/composables/index.ts +229 -3
  347. package/src/composables/platformContextHelpers.ts +74 -0
  348. package/src/composables/useApi.ts +9 -2
  349. package/src/composables/useAppExperiment.ts +85 -13
  350. package/src/composables/useBioTemplateComponents.ts +105 -0
  351. package/src/composables/useBioTemplateControls.ts +41 -0
  352. package/src/composables/useBioTemplatePackWorkspace.ts +185 -0
  353. package/src/composables/useBioTemplatePresetWorkspace.ts +326 -0
  354. package/src/composables/useBioTemplateWorkspace.ts +141 -0
  355. package/src/composables/useCalendarGrid.ts +140 -0
  356. package/src/composables/useControlSchema.ts +1362 -0
  357. package/src/composables/useDebouncedWatch.ts +119 -0
  358. package/src/composables/useDropdownState.ts +83 -0
  359. package/src/composables/useEventListener.ts +111 -0
  360. package/src/composables/useExpansionSet.ts +117 -0
  361. package/src/composables/useExperimentData.ts +20 -11
  362. package/src/composables/useExperimentSave.ts +202 -50
  363. package/src/composables/useExperimentSelector.ts +86 -72
  364. package/src/composables/useForm.ts +49 -4
  365. package/src/composables/useFormBuilder.ts +93 -42
  366. package/src/composables/useGroupAssignment.ts +148 -0
  367. package/src/composables/useListSelection.ts +158 -0
  368. package/src/composables/usePluginClient.ts +466 -0
  369. package/src/composables/usePluginConfig.ts +34 -13
  370. package/src/composables/useRequestSyncState.ts +126 -0
  371. package/src/composables/useSampleGroups.ts +126 -0
  372. package/src/composables/useSelectionLimit.ts +57 -0
  373. package/src/composables/useSortedItems.ts +118 -0
  374. package/src/composables/useTemplateCollection.ts +229 -0
  375. package/src/composables/useTextSearch.ts +60 -0
  376. package/src/composables/useTheme.ts +2 -28
  377. package/src/composables/useTimeUtils.ts +26 -2
  378. package/src/composables/useWellPlateEditor.ts +13 -9
  379. package/src/index.ts +11 -348
  380. package/src/install.ts +11 -4
  381. package/src/stores/settings.ts +13 -9
  382. package/src/styles/components/app-layout.css +82 -0
  383. package/src/styles/components/app-page-selector.css +23 -0
  384. package/src/styles/components/app-pill-nav.css +77 -0
  385. package/src/styles/components/app-sidebar.css +119 -0
  386. package/src/styles/components/app-top-bar.css +0 -201
  387. package/src/styles/components/concentration-input.css +3 -142
  388. package/src/styles/components/empty-state.css +0 -16
  389. package/src/styles/components/theme-toggle.css +3 -66
  390. package/src/styles/index.css +0 -2
  391. package/src/templates/adapters.ts +785 -0
  392. package/src/templates/builders.ts +2149 -0
  393. package/src/templates/catalog.ts +245 -0
  394. package/src/templates/componentBindings.ts +653 -0
  395. package/src/templates/controlSchemas.ts +718 -0
  396. package/src/templates/index.ts +318 -0
  397. package/src/templates/lookup.ts +18 -0
  398. package/src/templates/packs.ts +156 -0
  399. package/src/templates/presets.ts +146 -0
  400. package/src/templates/types.ts +668 -0
  401. package/src/types/components.ts +39 -27
  402. package/src/types/form-builder.ts +7 -2
  403. package/src/types/index.ts +13 -3
  404. package/src/types/platform.ts +7 -1
  405. package/src/utils/formModelSync.ts +52 -0
  406. package/src/utils/items.ts +28 -0
  407. package/src/utils/options.ts +23 -0
  408. package/src/utils/pluginIcon.ts +30 -0
  409. package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
  410. package/dist/auth-DsI0rQ7_.js.map +0 -1
  411. package/dist/components/GroupingModal.vue.d.ts +0 -12
  412. package/dist/components-_XqPEhP9.js.map +0 -1
  413. package/dist/composables/usePluginApi.d.ts +0 -29
  414. package/dist/composables-tiZqLu1M.js.map +0 -1
  415. package/dist/useScheduleDrag-CA9sGNJG.js +0 -7181
  416. package/dist/useScheduleDrag-CA9sGNJG.js.map +0 -1
  417. package/src/__tests__/composables/usePluginApi.test.ts +0 -81
  418. package/src/components/AppPageSelector.vue +0 -159
  419. package/src/components/AppPillNav.vue +0 -66
  420. package/src/components/GroupingModal.story.vue +0 -52
  421. package/src/components/GroupingModal.vue +0 -422
  422. package/src/components/SettingsButton.story.vue +0 -58
  423. package/src/components/SettingsButton.vue +0 -76
  424. package/src/composables/usePluginApi.ts +0 -39
  425. package/src/styles/components/grouping-modal.css +0 -323
  426. package/src/styles/components/settings-button.css +0 -94
  427. /package/dist/components/{ToastNotification.vue.d.ts → AppToastContainer.vue.d.ts} +0 -0
@@ -0,0 +1,1362 @@
1
+ import { computed, reactive, ref, watch, type ComputedRef, type Ref } from 'vue'
2
+ import type {
3
+ PillNavItem,
4
+ SidebarToolSection,
5
+ SelectOption,
6
+ SettingsModalSchema,
7
+ TopBarSettingsConfig,
8
+ } from '../types'
9
+ import type {
10
+ FieldCondition,
11
+ FieldValidation,
12
+ FormFieldSchema,
13
+ FormFieldType,
14
+ FormSchema,
15
+ FormSectionSchema,
16
+ } from '../types/form-builder'
17
+ import { getTypeDefault } from './formBuilderRegistry'
18
+ import type { WellConcentration } from './useDoseCalculator'
19
+
20
+ export type ControlOptionValue = string | number | boolean
21
+ export type ControlOption = ControlOptionValue | SelectOption<unknown>
22
+ export type ControlPrimitive = string | number | boolean
23
+ export type ControlShorthand = ControlPrimitive | readonly ControlOption[]
24
+ export type ControlInput = ControlDefinition | ControlShorthand
25
+
26
+ export interface ControlSectionConfig {
27
+ id?: string
28
+ label?: string
29
+ title?: string
30
+ description?: string
31
+ subtitle?: string
32
+ icon?: string | string[]
33
+ iconColor?: string
34
+ iconBg?: string
35
+ columns?: 1 | 2 | 3
36
+ condition?: FieldCondition
37
+ defaultOpen?: boolean
38
+ showToggle?: boolean
39
+ }
40
+
41
+ export interface ControlViewConfig {
42
+ label?: string
43
+ icon?: string
44
+ to?: string
45
+ href?: string
46
+ disabled?: boolean
47
+ children?: PillNavItem['children']
48
+ }
49
+
50
+ export interface ControlSidebarConfig extends Omit<ControlSectionConfig, 'id' | 'title' | 'description' | 'columns'> {
51
+ enabled?: boolean
52
+ }
53
+
54
+ export interface ControlDefinition {
55
+ type?: FormFieldType
56
+ label?: string
57
+ default?: unknown
58
+ defaultValue?: unknown
59
+ placeholder?: string
60
+ hint?: string
61
+ size?: 'sm' | 'md' | 'lg'
62
+ disabled?: boolean
63
+ readonly?: boolean
64
+ required?: boolean | string
65
+ min?: number | { value: number; message: string }
66
+ max?: number | { value: number; message: string }
67
+ minLength?: number | { value: number; message: string }
68
+ maxLength?: number | { value: number; message: string }
69
+ email?: boolean | string
70
+ pattern?: string | { value: string; message: string }
71
+ validation?: FieldValidation
72
+ condition?: FieldCondition
73
+ options?: readonly ControlOption[]
74
+ section?: string
75
+ sectionLabel?: string
76
+ sectionDescription?: string
77
+ sectionSubtitle?: string
78
+ view?: string
79
+ order?: number
80
+ colSpan?: number
81
+ props?: Record<string, unknown>
82
+ sidebar?: boolean | ControlSidebarConfig
83
+ }
84
+
85
+ export type ControlSchema = Record<string, ControlInput>
86
+ export type ControlFormSchema = Extract<FormSchema, { sections: FormSectionSchema[] }>
87
+
88
+ export interface ControlSchemaOptions {
89
+ view?: string
90
+ views?: Record<string, ControlViewConfig>
91
+ section?: string
92
+ sectionLabel?: string
93
+ sections?: Record<string, ControlSectionConfig>
94
+ columns?: 1 | 2 | 3
95
+ submitLabel?: string
96
+ cancelLabel?: string
97
+ showCancel?: boolean
98
+ }
99
+
100
+ export interface ControlWorkspaceOptions extends ControlSchemaOptions {
101
+ initialValues?: Record<string, unknown>
102
+ topBarSettings?: Omit<TopBarSettingsConfig, 'schema' | 'controls' | 'controlOptions' | 'values'>
103
+ }
104
+
105
+ export interface ControlModelSectionConfig extends ControlSectionConfig {
106
+ controls: ControlSchema
107
+ sidebar?: boolean | ControlSidebarConfig
108
+ }
109
+
110
+ export interface ControlModelViewConfig extends ControlViewConfig {
111
+ controls?: ControlSchema
112
+ section?: string
113
+ sectionLabel?: string
114
+ sectionDescription?: string
115
+ sections?: Record<string, ControlModelSectionConfig>
116
+ }
117
+
118
+ export interface ControlModel extends Omit<ControlWorkspaceOptions, 'sections' | 'views'> {
119
+ controls?: ControlSchema
120
+ sections?: Record<string, ControlModelSectionConfig>
121
+ views?: Record<string, ControlModelViewConfig>
122
+ /** Optional SDK component bindings returned with generated component props for custom workspace slots. */
123
+ componentBindings?: ControlComponentBindingsConfig
124
+ /** Alias for componentBindings in raw control models. */
125
+ components?: ControlComponentBindingsConfig
126
+ /** Optional ControlWorkspaceView componentProps mapping returned with the generated controls/options. */
127
+ componentProps?: ControlComponentPropsMap
128
+ /** Optional named componentProps mappings returned for multiple SDK components. */
129
+ componentPropsById?: ControlComponentPropsByIdMap
130
+ }
131
+
132
+ export interface ControlModelBinding {
133
+ controls: ControlSchema
134
+ controlOptions: ControlWorkspaceOptions
135
+ componentBindings?: ControlComponentBindingsConfig
136
+ componentProps?: ControlComponentPropsMap
137
+ componentPropsById?: ControlComponentPropsByIdMap
138
+ }
139
+
140
+ export interface ControlSidebarBinding {
141
+ panels: Record<string, SidebarToolSection[]>
142
+ forms: Record<string, ControlFormSchema>
143
+ viewIds: string[]
144
+ viewItems: PillNavItem[]
145
+ defaultView: string
146
+ }
147
+
148
+ export interface ControlFormBinding {
149
+ schema: ControlFormSchema
150
+ }
151
+
152
+ export interface ControlSettingsBinding {
153
+ schema: SettingsModalSchema
154
+ }
155
+
156
+ export interface ControlTopBarSettingsBinding {
157
+ showSettings: true
158
+ settingsConfig: TopBarSettingsConfig
159
+ }
160
+
161
+ export interface ControlWorkspaceFormBinding extends ControlFormBinding {
162
+ modelValue: Record<string, unknown>
163
+ 'onUpdate:modelValue': (values: Record<string, unknown>) => void
164
+ }
165
+
166
+ export interface ControlWorkspaceSidebarBinding extends ControlSidebarBinding {
167
+ activeView: string
168
+ modelValue: Record<string, unknown>
169
+ 'onUpdate:modelValue': (values: Record<string, unknown>) => void
170
+ values: Record<string, unknown>
171
+ 'onUpdate:values': (values: Record<string, unknown>) => void
172
+ }
173
+
174
+ export interface ControlWorkspaceTopBarSettingsBinding extends ControlTopBarSettingsBinding {
175
+ onSettingsValuesChange: (values: Record<string, unknown>) => void
176
+ }
177
+
178
+ export interface ControlWorkspacePillNavBinding {
179
+ items: PillNavItem[]
180
+ currentItemId: string
181
+ onSelect: (item: PillNavItem) => void
182
+ }
183
+
184
+ export interface ControlWorkspaceTopBarBinding extends ControlWorkspaceTopBarSettingsBinding {
185
+ pillNav: PillNavItem[]
186
+ currentPillId: string
187
+ onPillSelect: (item: PillNavItem) => void
188
+ }
189
+
190
+ export type ControlWorkspaceAppTopBarPillBinding = ControlWorkspaceTopBarBinding
191
+
192
+ export interface ControlWorkspaceComponentBindings {
193
+ form: ControlWorkspaceFormBinding
194
+ sidebar: ControlWorkspaceSidebarBinding
195
+ topBar: ComputedRef<ControlWorkspaceTopBarBinding>
196
+ topBarSettings: ControlWorkspaceTopBarSettingsBinding
197
+ pillNav: ControlWorkspacePillNavBinding
198
+ componentBindings: ComputedRef<ControlComponentBinding[]>
199
+ componentBindingsById: ComputedRef<ControlComponentBindingsById>
200
+ componentProps: ComputedRef<Record<string, unknown>>
201
+ componentPropsById: ComputedRef<Record<string, Record<string, unknown>>>
202
+ }
203
+
204
+ export type ControlComponentPropSource<TValues extends Record<string, unknown> = Record<string, unknown>> =
205
+ | (keyof TValues & string)
206
+ | ((values: TValues) => unknown)
207
+
208
+ export type ControlComponentPropsMap<TValues extends Record<string, unknown> = Record<string, unknown>> =
209
+ | readonly (keyof TValues & string)[]
210
+ | Record<string, ControlComponentPropSource<TValues>>
211
+
212
+ export type ControlComponentPropsByIdMap<TValues extends Record<string, unknown> = Record<string, unknown>> =
213
+ Record<string, ControlComponentPropsMap<TValues>>
214
+
215
+ export interface ControlComponentBindingConfig<TValues extends Record<string, unknown> = Record<string, unknown>> {
216
+ id?: string
217
+ component: string
218
+ props?: ControlComponentPropsMap<TValues>
219
+ }
220
+
221
+ export interface ControlComponentBindingRecordConfig<TValues extends Record<string, unknown> = Record<string, unknown>> {
222
+ component: string
223
+ props?: ControlComponentPropsMap<TValues>
224
+ }
225
+
226
+ export type ControlComponentBindingsConfig<TValues extends Record<string, unknown> = Record<string, unknown>> =
227
+ | readonly ControlComponentBindingConfig<TValues>[]
228
+ | Record<string, ControlComponentBindingRecordConfig<TValues>>
229
+
230
+ export interface ControlComponentBinding {
231
+ id: string
232
+ component: string
233
+ props: Record<string, unknown>
234
+ }
235
+
236
+ export type ControlComponentBindingsById = Record<string, ControlComponentBinding>
237
+
238
+ export interface WellPlateControlPropsOptions<TValues extends Record<string, unknown> = Record<string, unknown>> {
239
+ selectedWells?: ControlComponentPropSource<TValues>
240
+ format?: ControlComponentPropSource<TValues>
241
+ wells?: ControlComponentPropSource<TValues>
242
+ disabled?: ControlComponentPropSource<TValues>
243
+ readonly?: ControlComponentPropSource<TValues>
244
+ showLegend?: ControlComponentPropSource<TValues>
245
+ showSampleTypeIndicator?: ControlComponentPropSource<TValues>
246
+ /** Listener for WellPlate v-model updates. Set false to keep generated props one-way. */
247
+ onUpdateModelValue?: ((values: TValues) => (wellIds: string[]) => void) | false
248
+ }
249
+
250
+ export interface DoseCalculatorControlPropsOptions<TValues extends Record<string, unknown> = Record<string, unknown>> {
251
+ mode?: ControlComponentPropSource<TValues>
252
+ targetWells?: ControlComponentPropSource<TValues>
253
+ /** Control value key used by the default apply-to-wells handler. */
254
+ wells?: ControlComponentPropSource<TValues>
255
+ disabled?: ControlComponentPropSource<TValues>
256
+ molecularWeight?: ControlComponentPropSource<TValues>
257
+ /** Listener for DoseCalculator apply-to-wells. Set false to keep generated props one-way. */
258
+ onApplyToWells?: ((values: TValues) => (results: WellConcentration[]) => void) | false
259
+ }
260
+
261
+ export interface WellPlateDoseControlPropsOptions<TValues extends Record<string, unknown> = Record<string, unknown>> {
262
+ plateId?: string
263
+ doseId?: string
264
+ plate?: WellPlateControlPropsOptions<TValues>
265
+ dose?: DoseCalculatorControlPropsOptions<TValues>
266
+ }
267
+
268
+ export interface DoseDesignControlModelOptions {
269
+ viewId?: string
270
+ viewLabel?: string
271
+ sectionId?: string
272
+ sectionLabel?: string
273
+ sectionDescription?: string
274
+ selectedWells?: readonly string[]
275
+ plateFormat?: number
276
+ plateFormats?: readonly number[]
277
+ doseMode?: 'serial' | 'dilution' | 'conversion'
278
+ doseModes?: readonly ('serial' | 'dilution' | 'conversion')[]
279
+ disabled?: boolean
280
+ includeMolecularWeight?: boolean
281
+ molecularWeight?: number
282
+ componentProps?: WellPlateDoseControlPropsOptions
283
+ }
284
+
285
+ export interface UseControlSchemaReturn<TControls extends ControlSchema> {
286
+ controls: TControls
287
+ fields: FormFieldSchema[]
288
+ formSchema: ControlFormSchema
289
+ form: ControlFormBinding
290
+ settingsSchema: SettingsModalSchema
291
+ settings: ControlSettingsBinding
292
+ topBarSettingsConfig: TopBarSettingsConfig
293
+ topBarSettings: ControlTopBarSettingsBinding
294
+ sidebarPanels: Record<string, SidebarToolSection[]>
295
+ viewIds: string[]
296
+ viewItems: PillNavItem[]
297
+ defaultView: string
298
+ sectionSchemas: Record<string, ControlFormSchema>
299
+ sidebar: ControlSidebarBinding
300
+ initialValues: ControlValues<TControls>
301
+ sectionSchema: (sectionId: string) => ControlFormSchema
302
+ }
303
+
304
+ export interface UseControlWorkspaceReturn<TControls extends ControlSchema>
305
+ extends Omit<UseControlSchemaReturn<TControls>, 'form' | 'sidebar' | 'topBarSettings'> {
306
+ schema: UseControlSchemaReturn<TControls>
307
+ values: ControlValues<TControls> & Record<string, unknown>
308
+ activeView: Ref<string>
309
+ form: ControlWorkspaceFormBinding
310
+ sidebar: ControlWorkspaceSidebarBinding
311
+ topBar: ComputedRef<ControlWorkspaceTopBarBinding>
312
+ pillNav: ControlWorkspacePillNavBinding
313
+ topBarSettings: ControlWorkspaceTopBarSettingsBinding
314
+ bindings: ControlWorkspaceComponentBindings
315
+ componentBindings: ComputedRef<ControlComponentBinding[]>
316
+ componentBindingsById: ComputedRef<ControlComponentBindingsById>
317
+ componentProps: ComputedRef<Record<string, unknown>>
318
+ componentPropsById: ComputedRef<Record<string, Record<string, unknown>>>
319
+ setActiveView: (viewId: string) => void
320
+ setValues: (values: Record<string, unknown>) => void
321
+ resetValues: (values?: Record<string, unknown>) => void
322
+ getComponentProps: (
323
+ mapping?: ControlComponentPropsMap<ControlValues<TControls> & Record<string, unknown>>
324
+ ) => Record<string, unknown>
325
+ getComponentPropsById: (
326
+ mappings?: ControlComponentPropsByIdMap<ControlValues<TControls> & Record<string, unknown>>
327
+ ) => Record<string, Record<string, unknown>>
328
+ getComponentBindings: (
329
+ bindings?: ControlComponentBindingsConfig<ControlValues<TControls> & Record<string, unknown>>
330
+ ) => ControlComponentBinding[]
331
+ getComponentBindingsById: (
332
+ bindings?: ControlComponentBindingsConfig<ControlValues<TControls> & Record<string, unknown>>
333
+ ) => ControlComponentBindingsById
334
+ }
335
+
336
+ export type ControlValues<TControls extends ControlSchema> = {
337
+ [K in keyof TControls]: ControlValue<TControls[K]>
338
+ }
339
+
340
+ type ControlValue<TControl> =
341
+ TControl extends { default: infer TDefault }
342
+ ? TDefault
343
+ : TControl extends { defaultValue: infer TDefaultValue }
344
+ ? TDefaultValue
345
+ : TControl extends readonly [infer First, ...unknown[]]
346
+ ? OptionValue<First>
347
+ : TControl extends ControlPrimitive
348
+ ? TControl
349
+ : unknown
350
+
351
+ type OptionValue<TOption> = TOption extends SelectOption<infer TValue> ? TValue : TOption
352
+
353
+ interface NormalizedControl {
354
+ name: string
355
+ definition: ControlDefinition
356
+ type: FormFieldType
357
+ sectionId: string
358
+ viewId: string
359
+ order: number
360
+ }
361
+
362
+ /** Preserve literal control keys while marking an object as a MINT control schema. */
363
+ export function defineControls<TControls extends ControlSchema>(controls: TControls): TControls {
364
+ return controls
365
+ }
366
+
367
+ /** Preserve literal SDK component binding ids while marking an object as generated workspace component bindings. */
368
+ export function defineControlComponentBindings<TBindings extends ControlComponentBindingsConfig>(
369
+ bindings: TBindings,
370
+ ): TBindings {
371
+ return bindings
372
+ }
373
+
374
+ /** Create a complete workspace component binding from a simple controls data model for ControlWorkspaceView, generated forms, and sidebars. */
375
+ export function defineControlModel(model: ControlModel): ControlModelBinding {
376
+ const {
377
+ controls: rootControls,
378
+ sections: rootSections,
379
+ views: modelViews,
380
+ components,
381
+ componentBindings,
382
+ componentProps,
383
+ componentPropsById,
384
+ ...baseOptions
385
+ } = model
386
+ const resolvedComponentBindings = componentBindings ?? components
387
+ const controls: ControlSchema = {}
388
+ const views: Record<string, ControlViewConfig> = {}
389
+ const sections: Record<string, ControlSectionConfig> = {}
390
+
391
+ const defaultViewId = model.view ?? 'default'
392
+ const defaultSectionId = model.section ?? 'controls'
393
+
394
+ if (rootControls) {
395
+ registerSectionOptions(sections, defaultSectionId, {
396
+ id: defaultSectionId,
397
+ label: model.sectionLabel,
398
+ title: model.sectionLabel,
399
+ columns: model.columns,
400
+ })
401
+ appendModelControls(controls, rootControls, defaultViewId, defaultSectionId)
402
+ }
403
+
404
+ for (const [sectionId, section] of Object.entries(rootSections ?? {})) {
405
+ registerSectionOptions(sections, sectionId, controlModelSectionOptions(section))
406
+ appendModelControls(controls, section.controls, defaultViewId, sectionId, section.sidebar)
407
+ }
408
+
409
+ for (const [viewId, view] of Object.entries(modelViews ?? {})) {
410
+ const {
411
+ controls: viewControls,
412
+ sections: viewSections,
413
+ section,
414
+ sectionLabel,
415
+ sectionDescription,
416
+ ...viewConfig
417
+ } = view
418
+
419
+ views[viewId] = omitUndefined(viewConfig)
420
+
421
+ if (viewControls) {
422
+ const sectionId = section ?? `${viewId}-controls`
423
+ const label = sectionLabel ?? view.label
424
+ registerSectionOptions(sections, sectionId, {
425
+ id: sectionId,
426
+ label,
427
+ title: label,
428
+ description: sectionDescription,
429
+ columns: model.columns,
430
+ })
431
+ appendModelControls(controls, viewControls, viewId, sectionId)
432
+ }
433
+
434
+ for (const [sectionKey, sectionConfig] of Object.entries(viewSections ?? {})) {
435
+ const sectionId = sectionConfig.id ?? `${viewId}-${sectionKey}`
436
+ registerSectionOptions(sections, sectionId, controlModelSectionOptions(sectionConfig))
437
+ appendModelControls(controls, sectionConfig.controls, viewId, sectionId, sectionConfig.sidebar)
438
+ }
439
+ }
440
+
441
+ const controlOptions: ControlWorkspaceOptions = {
442
+ ...baseOptions,
443
+ }
444
+
445
+ if (Object.keys(views).length > 0) {
446
+ controlOptions.views = views
447
+ }
448
+
449
+ if (Object.keys(sections).length > 0) {
450
+ controlOptions.sections = sections
451
+ }
452
+
453
+ return {
454
+ controls,
455
+ controlOptions,
456
+ ...(resolvedComponentBindings === undefined ? {} : { componentBindings: resolvedComponentBindings }),
457
+ ...(componentProps === undefined ? {} : { componentProps }),
458
+ ...(componentPropsById === undefined ? {} : { componentPropsById }),
459
+ }
460
+ }
461
+
462
+ /** Build default values for a control schema, matching FormBuilder field defaults. */
463
+ export function getControlDefaults<TControls extends ControlSchema>(
464
+ controls: TControls,
465
+ ): ControlValues<TControls> {
466
+ const values: Record<string, unknown> = {}
467
+ for (const control of normalizeControls(controls)) {
468
+ values[control.name] = defaultValueForControl(control.definition, control.type)
469
+ }
470
+ return values as ControlValues<TControls>
471
+ }
472
+
473
+ /** Map control workspace values into component props for direct `v-bind` usage. */
474
+ export function controlValuesToComponentProps<TValues extends Record<string, unknown>>(
475
+ values: TValues,
476
+ mapping?: ControlComponentPropsMap<TValues>,
477
+ ): Record<string, unknown> {
478
+ if (mapping === undefined) return { ...values }
479
+
480
+ if (Array.isArray(mapping)) {
481
+ const props: Record<string, unknown> = {}
482
+ for (const key of mapping) {
483
+ props[key] = values[key]
484
+ }
485
+ return props
486
+ }
487
+
488
+ const props: Record<string, unknown> = {}
489
+ for (const [prop, source] of Object.entries(mapping) as Array<[string, ControlComponentPropSource<TValues>]>) {
490
+ props[prop] = typeof source === 'function' ? source(values) : values[source]
491
+ }
492
+ return props
493
+ }
494
+
495
+ /** Map control workspace values into named SDK component bindings for direct slot rendering. */
496
+ export function controlValuesToComponentBindings<TValues extends Record<string, unknown>>(
497
+ values: TValues,
498
+ bindings?: ControlComponentBindingsConfig<TValues>,
499
+ ): ControlComponentBinding[] {
500
+ if (bindings === undefined) return []
501
+
502
+ return normalizeControlComponentBindingConfigs(bindings).map(binding => ({
503
+ id: binding.id,
504
+ component: binding.component,
505
+ props: controlValuesToComponentProps(values, binding.props),
506
+ }))
507
+ }
508
+
509
+ /** Map control workspace values into SDK component bindings keyed by binding id. */
510
+ export function controlValuesToComponentBindingsById<TValues extends Record<string, unknown>>(
511
+ values: TValues,
512
+ bindings?: ControlComponentBindingsConfig<TValues>,
513
+ ): ControlComponentBindingsById {
514
+ return Object.fromEntries(
515
+ controlValuesToComponentBindings(values, bindings).map(binding => [binding.id, binding]),
516
+ )
517
+ }
518
+
519
+ /** Return a default WellPlate prop mapping for generated control workspaces. */
520
+ export function defineWellPlateControlProps<TValues extends Record<string, unknown> = Record<string, unknown>>(
521
+ options: WellPlateControlPropsOptions<TValues> = {},
522
+ ): ControlComponentPropsMap<TValues> {
523
+ const selectedWells = options.selectedWells ?? sourceKey<TValues>('selectedWells')
524
+ const onUpdateModelValue = options.onUpdateModelValue === false
525
+ ? undefined
526
+ : options.onUpdateModelValue ?? updateControlValueSource(selectedWells)
527
+
528
+ return compactComponentPropsMap<TValues>({
529
+ modelValue: selectedWells,
530
+ 'onUpdate:modelValue': onUpdateModelValue,
531
+ format: options.format ?? sourceKey<TValues>('plateFormat'),
532
+ wells: options.wells ?? sourceKey<TValues>('wells'),
533
+ disabled: options.disabled ?? sourceKey<TValues>('disabled'),
534
+ readonly: options.readonly ?? sourceKey<TValues>('readonly'),
535
+ showLegend: options.showLegend,
536
+ showSampleTypeIndicator: options.showSampleTypeIndicator,
537
+ })
538
+ }
539
+
540
+ /** Return a default DoseCalculator prop mapping for generated control workspaces. */
541
+ export function defineDoseCalculatorControlProps<TValues extends Record<string, unknown> = Record<string, unknown>>(
542
+ options: DoseCalculatorControlPropsOptions<TValues> = {},
543
+ ): ControlComponentPropsMap<TValues> {
544
+ const targetWells = options.targetWells ?? sourceKey<TValues>('selectedWells')
545
+ const wells = options.wells ?? sourceKey<TValues>('wells')
546
+ const onApplyToWells = options.onApplyToWells === false
547
+ ? undefined
548
+ : options.onApplyToWells ?? defaultDoseApplyHandler(wells, targetWells)
549
+
550
+ return compactComponentPropsMap<TValues>({
551
+ mode: options.mode ?? sourceKey<TValues>('doseMode'),
552
+ targetWells,
553
+ disabled: options.disabled ?? sourceKey<TValues>('disabled'),
554
+ molecularWeight: options.molecularWeight,
555
+ onApplyToWells,
556
+ })
557
+ }
558
+
559
+ /** Return named WellPlate + DoseCalculator prop mappings for one dose-design control model. */
560
+ export function defineWellPlateDoseControlProps<TValues extends Record<string, unknown> = Record<string, unknown>>(
561
+ options: WellPlateDoseControlPropsOptions<TValues> = {},
562
+ ): ControlComponentPropsByIdMap<TValues> {
563
+ return {
564
+ [options.plateId ?? 'plate']: defineWellPlateControlProps(options.plate),
565
+ [options.doseId ?? 'dose']: defineDoseCalculatorControlProps(options.dose),
566
+ }
567
+ }
568
+
569
+ /** Return named WellPlate + DoseCalculator component bindings for one dose-design control model. */
570
+ export function defineWellPlateDoseComponentBindings<TValues extends Record<string, unknown> = Record<string, unknown>>(
571
+ options: WellPlateDoseControlPropsOptions<TValues> = {},
572
+ ): ControlComponentBindingsConfig<TValues> {
573
+ return [
574
+ {
575
+ id: options.plateId ?? 'plate',
576
+ component: 'WellPlate',
577
+ props: defineWellPlateControlProps(options.plate),
578
+ },
579
+ {
580
+ id: options.doseId ?? 'dose',
581
+ component: 'DoseCalculator',
582
+ props: defineDoseCalculatorControlProps(options.dose),
583
+ },
584
+ ]
585
+ }
586
+
587
+ /** Return a complete ControlWorkspaceView model for WellPlate + DoseCalculator dose design. */
588
+ export function defineDoseDesignControlModel(
589
+ options: DoseDesignControlModelOptions = {},
590
+ ): ControlModelBinding {
591
+ const viewId = options.viewId ?? 'design'
592
+ const sectionId = options.sectionId ?? 'dose'
593
+ const doseControls: ControlSchema = {
594
+ selectedWells: {
595
+ type: 'tags',
596
+ label: 'Selected wells',
597
+ default: [...(options.selectedWells ?? ['A1', 'A2'])],
598
+ },
599
+ plateFormat: {
600
+ label: 'Plate format',
601
+ default: options.plateFormat ?? 96,
602
+ options: options.plateFormats ?? [96, 384],
603
+ },
604
+ doseMode: {
605
+ label: 'Dose mode',
606
+ default: options.doseMode ?? 'serial',
607
+ options: options.doseModes ?? ['serial', 'dilution', 'conversion'],
608
+ },
609
+ disabled: {
610
+ label: 'Disable tools',
611
+ default: options.disabled ?? false,
612
+ },
613
+ }
614
+
615
+ if (options.includeMolecularWeight) {
616
+ doseControls.molecularWeight = {
617
+ type: 'number',
618
+ label: 'Molecular weight',
619
+ default: options.molecularWeight ?? 300,
620
+ min: 0,
621
+ }
622
+ }
623
+
624
+ const componentProps: WellPlateDoseControlPropsOptions = {
625
+ ...options.componentProps,
626
+ dose: {
627
+ ...(options.componentProps?.dose ?? {}),
628
+ ...(options.includeMolecularWeight && options.componentProps?.dose?.molecularWeight === undefined
629
+ ? { molecularWeight: 'molecularWeight' }
630
+ : {}),
631
+ },
632
+ }
633
+
634
+ return defineControlModel({
635
+ componentBindings: defineWellPlateDoseComponentBindings(componentProps),
636
+ componentPropsById: defineWellPlateDoseControlProps(componentProps),
637
+ views: {
638
+ [viewId]: {
639
+ label: options.viewLabel ?? 'Design',
640
+ sections: {
641
+ [sectionId]: {
642
+ label: options.sectionLabel ?? 'Dose design',
643
+ description: options.sectionDescription ?? 'Well selection and dose calculator controls',
644
+ controls: doseControls,
645
+ },
646
+ },
647
+ },
648
+ },
649
+ })
650
+ }
651
+
652
+ function isControlModelBindingInput<TControls extends ControlSchema>(
653
+ value: TControls | (ControlModelBinding & { controls: TControls }),
654
+ ): value is ControlModelBinding & { controls: TControls } {
655
+ return (
656
+ typeof value === 'object'
657
+ && value !== null
658
+ && 'controls' in value
659
+ && 'controlOptions' in value
660
+ )
661
+ }
662
+
663
+ export function mergeControlWorkspaceOptions(
664
+ base?: ControlWorkspaceOptions,
665
+ override?: ControlWorkspaceOptions,
666
+ ): ControlWorkspaceOptions {
667
+ return {
668
+ ...(base ?? {}),
669
+ ...(override ?? {}),
670
+ initialValues: {
671
+ ...(base?.initialValues ?? {}),
672
+ ...(override?.initialValues ?? {}),
673
+ },
674
+ topBarSettings: {
675
+ ...(base?.topBarSettings ?? {}),
676
+ ...(override?.topBarSettings ?? {}),
677
+ },
678
+ }
679
+ }
680
+
681
+ /** Convert a compact control schema into a FormBuilder schema. */
682
+ export function controlsToFormSchema(
683
+ controls: ControlSchema,
684
+ options: ControlSchemaOptions = {},
685
+ ): ControlFormSchema {
686
+ const normalized = normalizeControls(controls, options)
687
+ const sectionIds = orderedUnique(normalized.map(control => control.sectionId))
688
+ const sections = sectionIds.map((sectionId): FormSectionSchema => {
689
+ const sectionControls = normalized.filter(control => control.sectionId === sectionId)
690
+ const config = sectionConfig(sectionId, sectionControls, options)
691
+ return {
692
+ id: sectionId,
693
+ title: config.title ?? config.label ?? humanize(sectionId),
694
+ description: config.description,
695
+ columns: config.columns ?? options.columns ?? 1,
696
+ defaultOpen: config.defaultOpen,
697
+ condition: config.condition,
698
+ fields: sectionControls.map(controlToFormField),
699
+ }
700
+ })
701
+
702
+ return {
703
+ sections,
704
+ submitLabel: options.submitLabel,
705
+ cancelLabel: options.cancelLabel,
706
+ showCancel: options.showCancel,
707
+ }
708
+ }
709
+
710
+ /** Convert controls into AppSidebar panels grouped by view and section. */
711
+ export function controlsToSidebarPanels(
712
+ controls: ControlSchema,
713
+ options: ControlSchemaOptions = {},
714
+ ): Record<string, SidebarToolSection[]> {
715
+ const normalized = normalizeControls(controls, options)
716
+ .filter(control => isSidebarEnabled(control.definition.sidebar))
717
+ const viewIds = orderedUnique(normalized.map(control => control.viewId))
718
+ const panels: Record<string, SidebarToolSection[]> = {}
719
+
720
+ for (const viewId of viewIds) {
721
+ const viewControls = normalized.filter(control => control.viewId === viewId)
722
+ const sectionIds = orderedUnique(viewControls.map(control => control.sectionId))
723
+ panels[viewId] = sectionIds.map((sectionId): SidebarToolSection => {
724
+ const sectionControls = viewControls.filter(control => control.sectionId === sectionId)
725
+ const config = sectionConfig(sectionId, sectionControls, options)
726
+ const sidebarConfig = firstSidebarConfig(sectionControls)
727
+ return {
728
+ id: sectionId,
729
+ label: sidebarConfig?.label ?? config.label ?? config.title ?? humanize(sectionId),
730
+ subtitle: sidebarConfig?.subtitle ?? config.subtitle,
731
+ icon: sidebarConfig?.icon ?? config.icon,
732
+ iconColor: sidebarConfig?.iconColor ?? config.iconColor,
733
+ iconBg: sidebarConfig?.iconBg ?? config.iconBg,
734
+ defaultOpen: sidebarConfig?.defaultOpen ?? config.defaultOpen,
735
+ showToggle: sidebarConfig?.showToggle ?? config.showToggle,
736
+ }
737
+ })
738
+ }
739
+
740
+ return panels
741
+ }
742
+
743
+ /** Convert controls into a SettingsModal schema grouped by the same sections. */
744
+ export function controlsToSettingsSchema(
745
+ controls: ControlSchema,
746
+ options: ControlSchemaOptions = {},
747
+ ): SettingsModalSchema {
748
+ const normalized = normalizeControls(controls, options)
749
+ const sectionIds = orderedUnique(normalized.map(control => control.sectionId))
750
+
751
+ return {
752
+ groups: sectionIds.map((sectionId) => {
753
+ const sectionControls = normalized.filter(control => control.sectionId === sectionId)
754
+ const config = sectionConfig(sectionId, sectionControls, options)
755
+ return {
756
+ id: sectionId,
757
+ label: config.label ?? config.title ?? humanize(sectionId),
758
+ description: config.description ?? config.subtitle,
759
+ icon: typeof config.icon === 'string' ? config.icon : undefined,
760
+ fields: sectionControls.map(controlToFormField),
761
+ columns: config.columns ?? options.columns ?? 1,
762
+ condition: config.condition,
763
+ }
764
+ }),
765
+ }
766
+ }
767
+
768
+ /** Convert controls into an AppTopBar settingsConfig object that passes compact controls through to SettingsModal. */
769
+ export function controlsToTopBarSettingsConfig(
770
+ controls: ControlSchema,
771
+ options: ControlSchemaOptions = {},
772
+ config: Omit<TopBarSettingsConfig, 'schema' | 'controls' | 'controlOptions'> = {},
773
+ ): TopBarSettingsConfig {
774
+ return {
775
+ ...config,
776
+ controls,
777
+ controlOptions: options,
778
+ }
779
+ }
780
+
781
+ /** Return generated control view IDs that have at least one sidebar panel. */
782
+ export function controlsToViewIds(
783
+ controls: ControlSchema,
784
+ options: ControlSchemaOptions = {},
785
+ ): string[] {
786
+ return controlsToViewItems(controls, options).map(item => item.id)
787
+ }
788
+
789
+ /** Return AppTopBar pillNav-compatible view items for switching generated control sidebars. */
790
+ export function controlsToViewItems(
791
+ controls: ControlSchema,
792
+ options: ControlSchemaOptions = {},
793
+ ): PillNavItem[] {
794
+ return Object.entries(controlsToSidebarPanels(controls, options))
795
+ .filter(([, sections]) => sections.length > 0)
796
+ .map(([id]) => controlViewItem(id, options))
797
+ }
798
+
799
+ /** Return the first generated sidebar view ID, or an empty string when controls render no sidebar panels. */
800
+ export function getDefaultControlView(
801
+ controls: ControlSchema,
802
+ options: ControlSchemaOptions = {},
803
+ ): string {
804
+ return controlsToViewIds(controls, options)[0] ?? ''
805
+ }
806
+
807
+ /** Return a headerless single-section FormBuilder schema for rendering inside an AppSidebar section slot. */
808
+ export function controlsToSectionFormSchema(
809
+ controls: ControlSchema,
810
+ sectionId: string,
811
+ options: ControlSchemaOptions = {},
812
+ ): ControlFormSchema {
813
+ const schema = controlsToFormSchema(controls, options)
814
+ return {
815
+ sections: schema.sections
816
+ .filter(section => section.id === sectionId)
817
+ .map(section => ({ ...section, title: '', description: undefined })),
818
+ submitLabel: schema.submitLabel,
819
+ cancelLabel: schema.cancelLabel,
820
+ showCancel: schema.showCancel,
821
+ }
822
+ }
823
+
824
+ /** Return headerless FormBuilder schemas keyed by section ID for AppSidebar auto-rendering. */
825
+ export function controlsToSectionFormSchemas(
826
+ controls: ControlSchema,
827
+ options: ControlSchemaOptions = {},
828
+ ): Record<string, ControlFormSchema> {
829
+ const schema = controlsToFormSchema(controls, options)
830
+ const schemas: Record<string, ControlFormSchema> = {}
831
+ for (const section of schema.sections) {
832
+ schemas[section.id] = {
833
+ sections: [{ ...section, title: '', description: undefined }],
834
+ submitLabel: schema.submitLabel,
835
+ cancelLabel: schema.cancelLabel,
836
+ showCancel: schema.showCancel,
837
+ }
838
+ }
839
+ return schemas
840
+ }
841
+
842
+ /** Prepare FormBuilder, SettingsModal, AppTopBar settings, AppSidebar, and initial values from one compact control model. */
843
+ export function useControlSchema<TControls extends ControlSchema>(
844
+ controls: TControls,
845
+ options: ControlSchemaOptions = {},
846
+ ): UseControlSchemaReturn<TControls> {
847
+ const formSchema = controlsToFormSchema(controls, options)
848
+ const settingsSchema = controlsToSettingsSchema(controls, options)
849
+ const topBarSettingsConfig = controlsToTopBarSettingsConfig(controls, options)
850
+ const sidebarPanels = controlsToSidebarPanels(controls, options)
851
+ const viewItems = controlsToViewItems(controls, options)
852
+ const viewIds = viewItems.map(item => item.id)
853
+ const defaultView = viewIds[0] ?? ''
854
+ const sectionSchemas = controlsToSectionFormSchemas(controls, options)
855
+
856
+ return {
857
+ controls,
858
+ fields: normalizeControls(controls, options).map(controlToFormField),
859
+ formSchema,
860
+ form: {
861
+ schema: formSchema,
862
+ },
863
+ settingsSchema,
864
+ settings: {
865
+ schema: settingsSchema,
866
+ },
867
+ topBarSettingsConfig,
868
+ topBarSettings: {
869
+ showSettings: true,
870
+ settingsConfig: topBarSettingsConfig,
871
+ },
872
+ sidebarPanels,
873
+ viewIds,
874
+ viewItems,
875
+ defaultView,
876
+ sectionSchemas,
877
+ sidebar: {
878
+ panels: sidebarPanels,
879
+ forms: sectionSchemas,
880
+ viewIds,
881
+ viewItems,
882
+ defaultView,
883
+ },
884
+ initialValues: getControlDefaults(controls),
885
+ sectionSchema: (sectionId: string) => controlsToSectionFormSchema(controls, sectionId, options),
886
+ }
887
+ }
888
+
889
+ /** Prepare shared reactive values plus AppTopBar/AppSidebar/FormBuilder bindings from one simple controls data model. */
890
+ export function useControlWorkspace<TControls extends ControlSchema>(
891
+ controlsOrModel: TControls | (ControlModelBinding & { controls: TControls }),
892
+ options: ControlWorkspaceOptions = {},
893
+ ): UseControlWorkspaceReturn<TControls> {
894
+ const model = isControlModelBindingInput<TControls>(controlsOrModel) ? controlsOrModel : undefined
895
+ const controls: TControls = model ? model.controls : controlsOrModel as TControls
896
+ const workspaceOptions = model
897
+ ? mergeControlWorkspaceOptions(model.controlOptions, options)
898
+ : options
899
+ const { initialValues, topBarSettings, ...schemaOptions } = workspaceOptions
900
+ const schema = useControlSchema(controls, schemaOptions)
901
+ const activeView = ref(schema.defaultView)
902
+ const values = reactive({
903
+ ...schema.initialValues,
904
+ ...(initialValues ?? {}),
905
+ }) as ControlValues<TControls> & Record<string, unknown>
906
+
907
+ function setActiveView(viewId: string) {
908
+ syncActiveView(viewId)
909
+ }
910
+
911
+ function syncActiveView(viewId: string) {
912
+ if (!schema.viewIds.includes(viewId)) return
913
+ if (activeView.value !== viewId) activeView.value = viewId
914
+ if (sidebar.activeView !== viewId) sidebar.activeView = viewId
915
+ if (pillNav.currentItemId !== viewId) pillNav.currentItemId = viewId
916
+ }
917
+
918
+ function setValues(nextValues: Record<string, unknown>) {
919
+ Object.assign(values, nextValues)
920
+ }
921
+
922
+ function resetValues(nextValues: Record<string, unknown> = {}) {
923
+ replaceRecord(values, {
924
+ ...schema.initialValues,
925
+ ...nextValues,
926
+ })
927
+ }
928
+
929
+ function getComponentProps(
930
+ mapping?: ControlComponentPropsMap<ControlValues<TControls> & Record<string, unknown>>,
931
+ ): Record<string, unknown> {
932
+ return controlValuesToComponentProps(values, mapping)
933
+ }
934
+
935
+ function getComponentPropsById(
936
+ mappings?: ControlComponentPropsByIdMap<ControlValues<TControls> & Record<string, unknown>>,
937
+ ): Record<string, Record<string, unknown>> {
938
+ if (mappings === undefined) return {}
939
+
940
+ return Object.fromEntries(
941
+ Object.entries(mappings).map(([id, mapping]) => [
942
+ id,
943
+ controlValuesToComponentProps(values, mapping),
944
+ ]),
945
+ )
946
+ }
947
+
948
+ function getComponentBindings(
949
+ bindings?: ControlComponentBindingsConfig<ControlValues<TControls> & Record<string, unknown>>,
950
+ ): ControlComponentBinding[] {
951
+ return controlValuesToComponentBindings(values, bindings)
952
+ }
953
+
954
+ function getComponentBindingsById(
955
+ bindings?: ControlComponentBindingsConfig<ControlValues<TControls> & Record<string, unknown>>,
956
+ ): ControlComponentBindingsById {
957
+ return controlValuesToComponentBindingsById(values, bindings)
958
+ }
959
+ const componentProps = computed(() => (
960
+ model?.componentProps === undefined ? {} : getComponentProps(model.componentProps)
961
+ ))
962
+ const componentPropsById = computed(() => getComponentPropsById(model?.componentPropsById))
963
+ const componentBindings = computed(() => getComponentBindings(model?.componentBindings))
964
+ const componentBindingsById = computed(() => getComponentBindingsById(model?.componentBindings))
965
+ const form = {
966
+ ...schema.form,
967
+ modelValue: values,
968
+ 'onUpdate:modelValue': setValues,
969
+ } as ControlWorkspaceFormBinding
970
+
971
+ const topBarSettingsConfig: TopBarSettingsConfig = {
972
+ ...schema.topBarSettingsConfig,
973
+ ...(topBarSettings ?? {}),
974
+ values,
975
+ }
976
+ const sidebar = reactive({
977
+ ...schema.sidebar,
978
+ activeView: activeView.value,
979
+ modelValue: values,
980
+ 'onUpdate:modelValue': setValues,
981
+ values,
982
+ 'onUpdate:values': setValues,
983
+ }) as ControlWorkspaceSidebarBinding
984
+ const pillNav = reactive({
985
+ items: schema.viewItems,
986
+ currentItemId: activeView.value,
987
+ onSelect: (item: PillNavItem) => setActiveView(item.id),
988
+ }) as ControlWorkspacePillNavBinding
989
+ const topBarSettingsBinding = {
990
+ showSettings: true,
991
+ settingsConfig: topBarSettingsConfig,
992
+ onSettingsValuesChange: setValues,
993
+ } as ControlWorkspaceTopBarSettingsBinding
994
+ const topBarProps = computed<ControlWorkspaceTopBarBinding>(() => ({
995
+ pillNav: pillNav.items,
996
+ currentPillId: pillNav.currentItemId,
997
+ onPillSelect: pillNav.onSelect,
998
+ ...topBarSettingsBinding,
999
+ }))
1000
+ const bindings: ControlWorkspaceComponentBindings = {
1001
+ form,
1002
+ sidebar,
1003
+ topBar: topBarProps,
1004
+ topBarSettings: topBarSettingsBinding,
1005
+ pillNav,
1006
+ componentBindings,
1007
+ componentBindingsById,
1008
+ componentProps,
1009
+ componentPropsById,
1010
+ }
1011
+
1012
+ watch(activeView, syncActiveView, { flush: 'sync' })
1013
+ watch(() => sidebar.activeView, syncActiveView, { flush: 'sync' })
1014
+ watch(() => pillNav.currentItemId, syncActiveView, { flush: 'sync' })
1015
+
1016
+ return {
1017
+ ...schema,
1018
+ schema,
1019
+ values,
1020
+ activeView,
1021
+ topBarSettingsConfig,
1022
+ form,
1023
+ sidebar,
1024
+ topBar: topBarProps,
1025
+ pillNav,
1026
+ topBarSettings: topBarSettingsBinding,
1027
+ bindings,
1028
+ componentBindings,
1029
+ componentBindingsById,
1030
+ componentProps,
1031
+ componentPropsById,
1032
+ setActiveView,
1033
+ setValues,
1034
+ resetValues,
1035
+ getComponentProps,
1036
+ getComponentPropsById,
1037
+ getComponentBindings,
1038
+ getComponentBindingsById,
1039
+ }
1040
+ }
1041
+
1042
+ function normalizeControls(
1043
+ controls: ControlSchema,
1044
+ options: ControlSchemaOptions = {},
1045
+ ): NormalizedControl[] {
1046
+ return Object.entries(controls)
1047
+ .map(([name, input], index): NormalizedControl => {
1048
+ const definition = normalizeControlDefinition(input)
1049
+ const type = definition.type ?? inferControlType(definition)
1050
+ return {
1051
+ name,
1052
+ definition,
1053
+ type,
1054
+ sectionId: definition.section ?? options.section ?? 'controls',
1055
+ viewId: definition.view ?? options.view ?? 'default',
1056
+ order: definition.order ?? index,
1057
+ }
1058
+ })
1059
+ .sort((a, b) => a.order - b.order)
1060
+ }
1061
+
1062
+ function normalizeControlDefinition(input: ControlInput): ControlDefinition {
1063
+ if (isControlOptionArray(input)) {
1064
+ if (input.length === 0) return { type: 'tags', default: [] }
1065
+ return {
1066
+ default: optionValue(input[0]),
1067
+ options: input,
1068
+ }
1069
+ }
1070
+
1071
+ if (typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean') {
1072
+ return { default: input }
1073
+ }
1074
+
1075
+ return input
1076
+ }
1077
+
1078
+ function appendModelControls(
1079
+ target: ControlSchema,
1080
+ source: ControlSchema,
1081
+ viewId: string,
1082
+ sectionId: string,
1083
+ sidebar?: boolean | ControlSidebarConfig,
1084
+ ): void {
1085
+ for (const [name, input] of Object.entries(source)) {
1086
+ if (Object.prototype.hasOwnProperty.call(target, name)) {
1087
+ throw new Error(
1088
+ `Duplicate control "${name}" in defineControlModel(). Control names must be unique across views and sections.`,
1089
+ )
1090
+ }
1091
+
1092
+ const definition = normalizeControlDefinition(input)
1093
+ target[name] = omitUndefined({
1094
+ ...definition,
1095
+ view: definition.view ?? viewId,
1096
+ section: definition.section ?? sectionId,
1097
+ sidebar: definition.sidebar ?? sidebar,
1098
+ })
1099
+ }
1100
+ }
1101
+
1102
+ function controlModelSectionOptions(section: ControlModelSectionConfig): ControlSectionConfig {
1103
+ const { controls: _controls, sidebar: _sidebar, ...options } = section
1104
+ return omitUndefined(options)
1105
+ }
1106
+
1107
+ function registerSectionOptions(
1108
+ target: Record<string, ControlSectionConfig>,
1109
+ sectionId: string,
1110
+ options: ControlSectionConfig,
1111
+ ): void {
1112
+ target[sectionId] = omitUndefined({
1113
+ ...(target[sectionId] ?? {}),
1114
+ ...options,
1115
+ id: sectionId,
1116
+ })
1117
+ }
1118
+
1119
+ function isControlOptionArray(input: ControlInput): input is readonly ControlOption[] {
1120
+ return Array.isArray(input)
1121
+ }
1122
+
1123
+ function inferControlType(definition: ControlDefinition): FormFieldType {
1124
+ const value = definition.default ?? definition.defaultValue
1125
+ if (definition.options?.length) return Array.isArray(value) ? 'multiselect' : 'select'
1126
+ if (typeof value === 'boolean') return 'toggle'
1127
+ if (typeof value === 'number') return 'number'
1128
+ if (Array.isArray(value)) return 'tags'
1129
+ return 'text'
1130
+ }
1131
+
1132
+ function defaultValueForControl(definition: ControlDefinition, type: FormFieldType): unknown {
1133
+ if (definition.default !== undefined) return definition.default
1134
+ if (definition.defaultValue !== undefined) return definition.defaultValue
1135
+ return getTypeDefault(type)
1136
+ }
1137
+
1138
+ function controlToFormField(control: NormalizedControl): FormFieldSchema {
1139
+ const { name, definition, type } = control
1140
+ const props = fieldProps(definition)
1141
+ return {
1142
+ name,
1143
+ label: definition.label ?? humanize(name),
1144
+ type,
1145
+ defaultValue: defaultValueForControl(definition, type),
1146
+ placeholder: definition.placeholder,
1147
+ hint: definition.hint,
1148
+ size: definition.size,
1149
+ disabled: definition.disabled,
1150
+ readonly: definition.readonly,
1151
+ validation: validationForControl(definition),
1152
+ condition: definition.condition,
1153
+ colSpan: definition.colSpan,
1154
+ props,
1155
+ }
1156
+ }
1157
+
1158
+ function fieldProps(definition: ControlDefinition): Record<string, unknown> {
1159
+ const props: Record<string, unknown> = {}
1160
+ if (definition.min !== undefined) props.min = numericValue(definition.min)
1161
+ if (definition.max !== undefined) props.max = numericValue(definition.max)
1162
+ if (definition.options) props.options = definition.options.map(normalizeOption)
1163
+ return { ...props, ...(definition.props ?? {}) }
1164
+ }
1165
+
1166
+ function validationForControl(definition: ControlDefinition): FieldValidation | undefined {
1167
+ const validation: FieldValidation = { ...(definition.validation ?? {}) }
1168
+ if (definition.required !== undefined) validation.required = definition.required
1169
+ if (definition.min !== undefined) validation.min = definition.min
1170
+ if (definition.max !== undefined) validation.max = definition.max
1171
+ if (definition.minLength !== undefined) validation.minLength = definition.minLength
1172
+ if (definition.maxLength !== undefined) validation.maxLength = definition.maxLength
1173
+ if (definition.email !== undefined) validation.email = definition.email
1174
+ if (definition.pattern !== undefined) validation.pattern = definition.pattern
1175
+ return Object.keys(validation).length > 0 ? validation : undefined
1176
+ }
1177
+
1178
+ function sectionConfig(
1179
+ sectionId: string,
1180
+ controls: NormalizedControl[],
1181
+ options: ControlSchemaOptions,
1182
+ ): ControlSectionConfig {
1183
+ const configured = options.sections?.[sectionId]
1184
+ const first = controls[0]?.definition
1185
+ return {
1186
+ ...configured,
1187
+ id: sectionId,
1188
+ label: configured?.label ?? first?.sectionLabel,
1189
+ title: configured?.title ?? first?.sectionLabel,
1190
+ description: configured?.description ?? first?.sectionDescription,
1191
+ subtitle: configured?.subtitle ?? first?.sectionSubtitle,
1192
+ }
1193
+ }
1194
+
1195
+ function firstSidebarConfig(controls: NormalizedControl[]): ControlSidebarConfig | undefined {
1196
+ for (const control of controls) {
1197
+ const sidebar = control.definition.sidebar
1198
+ if (sidebar && typeof sidebar === 'object' && sidebar.enabled !== false) return sidebar
1199
+ }
1200
+ return undefined
1201
+ }
1202
+
1203
+ function isSidebarEnabled(sidebar: ControlDefinition['sidebar']): boolean {
1204
+ if (sidebar === false) return false
1205
+ if (sidebar && typeof sidebar === 'object') return sidebar.enabled !== false
1206
+ return true
1207
+ }
1208
+
1209
+ function normalizeOption(option: ControlOption): SelectOption<unknown> {
1210
+ if (typeof option === 'object' && option !== null && 'label' in option) {
1211
+ return option
1212
+ }
1213
+ return {
1214
+ value: option,
1215
+ label: humanize(String(option)),
1216
+ }
1217
+ }
1218
+
1219
+ function optionValue(option: ControlOption): ControlOptionValue | unknown {
1220
+ if (typeof option === 'object' && option !== null && 'value' in option) {
1221
+ return option.value
1222
+ }
1223
+ return option
1224
+ }
1225
+
1226
+ function compactComponentPropsMap<TValues extends Record<string, unknown>>(
1227
+ mapping: Record<string, ControlComponentPropSource<TValues> | undefined>,
1228
+ ): ControlComponentPropsMap<TValues> {
1229
+ return Object.fromEntries(
1230
+ Object.entries(mapping).filter((entry): entry is [string, ControlComponentPropSource<TValues>] =>
1231
+ entry[1] !== undefined
1232
+ ),
1233
+ )
1234
+ }
1235
+
1236
+ function normalizeControlComponentBindingConfigs<TValues extends Record<string, unknown>>(
1237
+ bindings: ControlComponentBindingsConfig<TValues>,
1238
+ ): Array<Required<Pick<ControlComponentBindingConfig<TValues>, 'id' | 'component'>> & Pick<ControlComponentBindingConfig<TValues>, 'props'>> {
1239
+ if (Array.isArray(bindings)) {
1240
+ const usedIds = new Map<string, number>()
1241
+ return bindings.map(binding => ({
1242
+ id: uniqueComponentBindingId(binding.id ?? binding.component, usedIds),
1243
+ component: binding.component,
1244
+ props: binding.props,
1245
+ }))
1246
+ }
1247
+
1248
+ return Object.entries(bindings).map(([id, binding]) => ({
1249
+ id,
1250
+ component: binding.component,
1251
+ props: binding.props,
1252
+ }))
1253
+ }
1254
+
1255
+ function uniqueComponentBindingId(id: string, usedIds: Map<string, number>): string {
1256
+ const count = usedIds.get(id) ?? 0
1257
+ usedIds.set(id, count + 1)
1258
+ return count === 0 ? id : `${id}-${count + 1}`
1259
+ }
1260
+
1261
+ function sourceKey<TValues extends Record<string, unknown>>(key: string): keyof TValues & string {
1262
+ return key as keyof TValues & string
1263
+ }
1264
+
1265
+ function updateControlValueSource<TValues extends Record<string, unknown>>(
1266
+ source: ControlComponentPropSource<TValues>,
1267
+ ): ((values: TValues) => (value: string[]) => void) | undefined {
1268
+ if (typeof source !== 'string') return undefined
1269
+ return values => (value: string[]) => {
1270
+ const writableValues = values as Record<string, unknown>
1271
+ writableValues[source] = value
1272
+ }
1273
+ }
1274
+
1275
+ function defaultDoseApplyHandler<TValues extends Record<string, unknown>>(
1276
+ wells: ControlComponentPropSource<TValues>,
1277
+ targetWells: ControlComponentPropSource<TValues>,
1278
+ ): ((values: TValues) => (results: WellConcentration[]) => void) | undefined {
1279
+ if (wells === targetWells) return undefined
1280
+ return updateWellsFromDoseResults(wells)
1281
+ }
1282
+
1283
+ function updateWellsFromDoseResults<TValues extends Record<string, unknown>>(
1284
+ source: ControlComponentPropSource<TValues>,
1285
+ ): ((values: TValues) => (results: WellConcentration[]) => void) | undefined {
1286
+ if (typeof source !== 'string') return undefined
1287
+ return values => (results: WellConcentration[]) => {
1288
+ const writableValues = values as Record<string, unknown>
1289
+ const currentWells = recordValue(writableValues[source])
1290
+ const nextWells: Record<string, Record<string, unknown>> = {}
1291
+ for (const [wellId, well] of Object.entries(currentWells)) {
1292
+ nextWells[wellId] = recordValue(well)
1293
+ }
1294
+
1295
+ for (const result of results) {
1296
+ const existing = recordValue(nextWells[result.wellId])
1297
+ const metadata = recordValue(existing.metadata)
1298
+ nextWells[result.wellId] = {
1299
+ ...existing,
1300
+ state: 'filled',
1301
+ value: result.concentration.value,
1302
+ metadata: {
1303
+ ...metadata,
1304
+ concentration: result.concentration,
1305
+ ...(result.volume === undefined ? {} : { volume: result.volume }),
1306
+ },
1307
+ }
1308
+ }
1309
+
1310
+ writableValues[source] = nextWells
1311
+ }
1312
+ }
1313
+
1314
+ function recordValue(value: unknown): Record<string, unknown> {
1315
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
1316
+ ? value as Record<string, unknown>
1317
+ : {}
1318
+ }
1319
+
1320
+ function numericValue(value: number | { value: number; message: string }): number {
1321
+ return typeof value === 'number' ? value : value.value
1322
+ }
1323
+
1324
+ function orderedUnique(values: string[]): string[] {
1325
+ return [...new Set(values)]
1326
+ }
1327
+
1328
+ function controlViewItem(viewId: string, options: ControlSchemaOptions): PillNavItem {
1329
+ const config = options.views?.[viewId]
1330
+ return {
1331
+ id: viewId,
1332
+ label: config?.label ?? humanize(viewId),
1333
+ ...(config?.icon !== undefined ? { icon: config.icon } : {}),
1334
+ ...(config?.to !== undefined ? { to: config.to } : {}),
1335
+ ...(config?.href !== undefined ? { href: config.href } : {}),
1336
+ ...(config?.disabled !== undefined ? { disabled: config.disabled } : {}),
1337
+ ...(config?.children !== undefined ? { children: config.children } : {}),
1338
+ }
1339
+ }
1340
+
1341
+ function replaceRecord(target: Record<string, unknown>, values: Record<string, unknown>) {
1342
+ for (const key of Object.keys(target)) {
1343
+ delete target[key]
1344
+ }
1345
+ Object.assign(target, values)
1346
+ }
1347
+
1348
+ function omitUndefined<T extends object>(record: T): T {
1349
+ const next: Record<string, unknown> = {}
1350
+ for (const [key, value] of Object.entries(record as Record<string, unknown>)) {
1351
+ if (value !== undefined) next[key] = value
1352
+ }
1353
+ return next as T
1354
+ }
1355
+
1356
+ function humanize(value: string): string {
1357
+ return value
1358
+ .replace(/[_-]+/g, ' ')
1359
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
1360
+ .trim()
1361
+ .replace(/\w\S*/g, word => word.charAt(0).toUpperCase() + word.slice(1))
1362
+ }