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