@morscherlab/mint-sdk 1.0.0-rc.4 → 1.0.0-rc.6

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 (324) hide show
  1. package/dist/__tests__/components/AppTopBar.navigation.test.d.ts +1 -0
  2. package/dist/__tests__/components/DoseCalculatorVolumeField.test.d.ts +1 -0
  3. package/dist/__tests__/components/PlateMapEditorToolbarInternal.test.d.ts +1 -0
  4. package/dist/__tests__/components/PluginWorkspaceView.controls.test.d.ts +1 -0
  5. package/dist/__tests__/components/PluginWorkspaceView.navigation.test.d.ts +1 -0
  6. package/dist/__tests__/components/PluginWorkspaceView.shell.test.d.ts +1 -0
  7. package/dist/__tests__/components/ProtocolStep.presentation.test.d.ts +1 -0
  8. package/dist/__tests__/components/ProtocolStepEditor.state.test.d.ts +1 -0
  9. package/dist/__tests__/components/ProtocolStepParameterField.test.d.ts +1 -0
  10. package/dist/__tests__/components/ReagentList.presentation.test.d.ts +1 -0
  11. package/dist/__tests__/components/SampleSelector.colors.test.d.ts +1 -0
  12. package/dist/__tests__/components/SampleSelector.drag.test.d.ts +1 -0
  13. package/dist/__tests__/components/SampleSelector.groups.test.d.ts +1 -0
  14. package/dist/__tests__/components/SampleSelector.selection.test.d.ts +1 -0
  15. package/dist/__tests__/components/SampleSelectorSampleRow.test.d.ts +1 -0
  16. package/dist/__tests__/components/ScheduleCalendar.test.d.ts +1 -0
  17. package/dist/__tests__/components/SettingsModal.schema.test.d.ts +1 -0
  18. package/dist/__tests__/components/WellPlate.colors.test.d.ts +1 -0
  19. package/dist/__tests__/components/WellPlate.conditions.test.d.ts +1 -0
  20. package/dist/__tests__/components/WellPlate.geometry.test.d.ts +1 -0
  21. package/dist/__tests__/components/WellPlate.interaction.test.d.ts +1 -0
  22. package/dist/__tests__/components/WellPlate.legend.test.d.ts +1 -0
  23. package/dist/__tests__/components/WellPlate.rendering.test.d.ts +1 -0
  24. package/dist/__tests__/components/WellPlate.sampleDrop.test.d.ts +1 -0
  25. package/dist/__tests__/composables/autoGroup/classify.test.d.ts +1 -0
  26. package/dist/__tests__/composables/autoGroup/columns.test.d.ts +1 -0
  27. package/dist/__tests__/composables/autoGroup/compose.test.d.ts +1 -0
  28. package/dist/__tests__/composables/autoGroup/cooccurrence.test.d.ts +1 -0
  29. package/dist/__tests__/composables/autoGroup/fingerprint.test.d.ts +1 -0
  30. package/dist/__tests__/composables/autoGroup/integration.test.d.ts +1 -0
  31. package/dist/__tests__/composables/autoGroup/template.test.d.ts +1 -0
  32. package/dist/__tests__/composables/autoGroup/tokenize.test.d.ts +1 -0
  33. package/dist/__tests__/composables/useAutoGroupInputSources.test.d.ts +1 -0
  34. package/dist/__tests__/composables/useScheduleCalendarLayout.test.d.ts +1 -0
  35. package/dist/__tests__/docs/extractDocsComponents.test.d.ts +1 -0
  36. package/dist/__tests__/docs/extractDocsExports.test.d.ts +1 -0
  37. package/dist/__tests__/docs/extractDocsParsing.test.d.ts +1 -0
  38. package/dist/__tests__/docs/extractDocsTemplates.test.d.ts +1 -0
  39. package/dist/__tests__/docs/extractDocsTheme.test.d.ts +1 -0
  40. package/dist/components/AppSidebar.vue.d.ts +9 -6
  41. package/dist/components/AppTopBar.navigation.d.ts +11 -0
  42. package/dist/components/BaseButton.vue.d.ts +1 -1
  43. package/dist/components/BaseCheckbox.vue.d.ts +7 -2
  44. package/dist/components/BaseInput.vue.d.ts +2 -2
  45. package/dist/components/BasePill.vue.d.ts +2 -2
  46. package/dist/components/BaseRadioGroup.vue.d.ts +2 -2
  47. package/dist/components/BaseSelect.vue.d.ts +1 -1
  48. package/dist/components/BaseSlider.vue.d.ts +2 -2
  49. package/dist/components/BaseTextarea.vue.d.ts +2 -2
  50. package/dist/components/BaseToggle.vue.d.ts +7 -2
  51. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  52. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
  53. package/dist/components/CollapsibleCard.vue.d.ts +9 -0
  54. package/dist/components/ColorSlider.vue.d.ts +2 -2
  55. package/dist/components/ConcentrationInput.vue.d.ts +2 -2
  56. package/dist/components/ControlWorkspaceView.vue.d.ts +4 -4
  57. package/dist/components/DatePicker.vue.d.ts +1 -1
  58. package/dist/components/DateTimePicker.vue.d.ts +2 -2
  59. package/dist/components/DoseCalculatorVolumeField.vue.d.ts +15 -0
  60. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +2 -2
  61. package/dist/components/DropdownButton.vue.d.ts +1 -1
  62. package/dist/components/FileUploader.vue.d.ts +2 -2
  63. package/dist/components/FormulaInput.vue.d.ts +2 -2
  64. package/dist/components/IconButton.vue.d.ts +1 -1
  65. package/dist/components/LoadingSpinner.vue.d.ts +1 -1
  66. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  67. package/dist/components/MultiSelect.vue.d.ts +1 -1
  68. package/dist/components/NumberInput.vue.d.ts +1 -1
  69. package/dist/components/PlateMapEditor.vue.d.ts +6 -6
  70. package/dist/components/PluginWorkspaceView.controls.d.ts +28 -0
  71. package/dist/components/PluginWorkspaceView.navigation.d.ts +29 -0
  72. package/dist/components/PluginWorkspaceView.props.d.ts +151 -0
  73. package/dist/components/PluginWorkspaceView.shell.d.ts +19 -0
  74. package/dist/components/PluginWorkspaceView.vue.d.ts +51 -196
  75. package/dist/components/ProgressBar.vue.d.ts +1 -1
  76. package/dist/components/ProtocolStep.presentation.d.ts +4 -0
  77. package/dist/components/ProtocolStepEditor.state.d.ts +18 -0
  78. package/dist/components/ProtocolStepParameterField.vue.d.ts +12 -0
  79. package/dist/components/ReagentList.presentation.d.ts +16 -0
  80. package/dist/components/ResourceCard.vue.d.ts +1 -1
  81. package/dist/components/SampleSelector.colors.d.ts +13 -0
  82. package/dist/components/SampleSelector.drag.d.ts +24 -0
  83. package/dist/components/SampleSelector.groups.d.ts +15 -0
  84. package/dist/components/SampleSelector.selection.d.ts +26 -0
  85. package/dist/components/SampleSelector.vue.d.ts +4 -1
  86. package/dist/components/SampleSelectorSampleRow.vue.d.ts +21 -0
  87. package/dist/components/SegmentedControl.vue.d.ts +1 -1
  88. package/dist/components/SequenceInput.vue.d.ts +2 -2
  89. package/dist/components/SequenceProgressBar.vue.d.ts +1 -1
  90. package/dist/components/SettingsModal.schema.d.ts +9 -0
  91. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  92. package/dist/components/TagsInput.vue.d.ts +2 -2
  93. package/dist/components/TimePicker.vue.d.ts +2 -2
  94. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  95. package/dist/components/UnitInput.vue.d.ts +2 -2
  96. package/dist/components/WellPlate.colors.d.ts +9 -0
  97. package/dist/components/WellPlate.conditions.d.ts +26 -0
  98. package/dist/components/WellPlate.geometry.d.ts +23 -0
  99. package/dist/components/WellPlate.interaction.d.ts +71 -0
  100. package/dist/components/WellPlate.legend.d.ts +2 -0
  101. package/dist/components/WellPlate.rendering.d.ts +24 -0
  102. package/dist/components/WellPlate.sampleDrop.d.ts +8 -0
  103. package/dist/components/WellPlate.vue.d.ts +1 -1
  104. package/dist/components/index.js +2 -2
  105. package/dist/components/internal/ActionItemInternal.vue.d.ts +1 -1
  106. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +28 -0
  107. package/dist/{components-DafPc4rM.js → components-Blx4MG--.js} +4073 -4222
  108. package/dist/components-Blx4MG--.js.map +1 -0
  109. package/dist/composables/autoGroup/classKey.d.ts +4 -0
  110. package/dist/composables/autoGroup/classify.d.ts +28 -0
  111. package/dist/composables/autoGroup/colors.d.ts +2 -0
  112. package/dist/composables/autoGroup/columns.d.ts +10 -0
  113. package/dist/composables/autoGroup/compose.d.ts +8 -0
  114. package/dist/composables/autoGroup/cooccurrence.d.ts +2 -0
  115. package/dist/composables/autoGroup/csv-shim.d.ts +2 -0
  116. package/dist/composables/autoGroup/fingerprint.d.ts +3 -0
  117. package/dist/composables/autoGroup/index.d.ts +16 -0
  118. package/dist/composables/autoGroup/replicatePreGroup.d.ts +38 -0
  119. package/dist/composables/autoGroup/template.d.ts +15 -0
  120. package/dist/composables/autoGroup/tokenize.d.ts +8 -0
  121. package/dist/composables/autoGroupConstants.d.ts +1 -0
  122. package/dist/composables/autoGroupGrouping.d.ts +3 -0
  123. package/dist/composables/controlComponentBindings.d.ts +1 -1
  124. package/dist/composables/controlSchemaAdapters.d.ts +1 -1
  125. package/dist/composables/controlSchemaDoseDesign.d.ts +11 -0
  126. package/dist/composables/controlSchemaLayout.d.ts +1 -1
  127. package/dist/composables/controlSchemaModel.d.ts +5 -0
  128. package/dist/composables/controlSchemaNormalize.d.ts +1 -1
  129. package/dist/composables/controlSchemaTypes.d.ts +311 -0
  130. package/dist/composables/controlWorkspaceOptions.d.ts +1 -1
  131. package/dist/composables/formBuilderSchema.d.ts +18 -0
  132. package/dist/composables/index.js +3 -3
  133. package/dist/composables/pluginEndpointBuilder.d.ts +13 -0
  134. package/dist/composables/protocolTemplateCatalog.d.ts +26 -0
  135. package/dist/composables/useAutoGroup.d.ts +61 -74
  136. package/dist/composables/useAutoGroupInputSources.d.ts +32 -0
  137. package/dist/composables/useBioTemplateControls.d.ts +1 -1
  138. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +1 -1
  139. package/dist/composables/useBioTemplateWorkspace.d.ts +1 -1
  140. package/dist/composables/useControlSchema.d.ts +4 -316
  141. package/dist/composables/useControlWorkspace.d.ts +1 -1
  142. package/dist/composables/useForm.d.ts +2 -33
  143. package/dist/composables/useFormBuilder.d.ts +2 -9
  144. package/dist/composables/useFormValidation.d.ts +34 -0
  145. package/dist/composables/usePluginClient.d.ts +1 -4
  146. package/dist/composables/useProtocolTemplates.d.ts +2 -24
  147. package/dist/composables/useScheduleCalendarLayout.d.ts +49 -0
  148. package/dist/{composables-BMkPQhVK.js → composables-CHDjDIQT.js} +33 -31
  149. package/dist/composables-CHDjDIQT.js.map +1 -0
  150. package/dist/index.js +4 -4
  151. package/dist/install.js +2 -2
  152. package/dist/styles.css +1432 -660
  153. package/dist/templates/controlSchemaTypes.d.ts +1 -1
  154. package/dist/templates/index.js +1 -1
  155. package/dist/templates/templateAdapterTypes.d.ts +48 -0
  156. package/dist/templates/templateCreateOptions.d.ts +165 -0
  157. package/dist/templates/templateQpcrTypes.d.ts +42 -0
  158. package/dist/templates/types.d.ts +5 -250
  159. package/dist/{templates-bUAWMn5L.js → templates-DSbHJC4v.js} +1536 -297
  160. package/dist/templates-DSbHJC4v.js.map +1 -0
  161. package/dist/types/auto-group.d.ts +79 -9
  162. package/dist/types/componentLabTypes.d.ts +161 -0
  163. package/dist/types/componentWorkflowTypes.d.ts +150 -0
  164. package/dist/types/components.d.ts +14 -311
  165. package/dist/types/form-builder.d.ts +3 -0
  166. package/dist/types/index.d.ts +1 -1
  167. package/dist/{useProtocolTemplates-QZtHFFH2.js → useProtocolTemplates-BbPOYPzO.js} +1220 -454
  168. package/dist/useProtocolTemplates-BbPOYPzO.js.map +1 -0
  169. package/package.json +1 -1
  170. package/src/__tests__/components/AppSidebar.test.ts +67 -0
  171. package/src/__tests__/components/AppTopBar.navigation.test.ts +70 -0
  172. package/src/__tests__/components/CollapsibleCard.test.ts +47 -0
  173. package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +53 -0
  174. package/src/__tests__/components/FormBuilder.test.ts +57 -0
  175. package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +54 -0
  176. package/src/__tests__/components/PluginWorkspaceView.controls.test.ts +156 -0
  177. package/src/__tests__/components/PluginWorkspaceView.navigation.test.ts +102 -0
  178. package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +43 -0
  179. package/src/__tests__/components/ProtocolStep.presentation.test.ts +31 -0
  180. package/src/__tests__/components/ProtocolStepEditor.state.test.ts +165 -0
  181. package/src/__tests__/components/ProtocolStepParameterField.test.ts +44 -0
  182. package/src/__tests__/components/ReagentList.presentation.test.ts +68 -0
  183. package/src/__tests__/components/SampleSelector.colors.test.ts +49 -0
  184. package/src/__tests__/components/SampleSelector.drag.test.ts +100 -0
  185. package/src/__tests__/components/SampleSelector.groups.test.ts +81 -0
  186. package/src/__tests__/components/SampleSelector.selection.test.ts +70 -0
  187. package/src/__tests__/components/SampleSelector.test.ts +32 -0
  188. package/src/__tests__/components/SampleSelectorSampleRow.test.ts +37 -0
  189. package/src/__tests__/components/ScheduleCalendar.test.ts +44 -0
  190. package/src/__tests__/components/SettingsModal.schema.test.ts +97 -0
  191. package/src/__tests__/components/WellPlate.colors.test.ts +28 -0
  192. package/src/__tests__/components/WellPlate.conditions.test.ts +68 -0
  193. package/src/__tests__/components/WellPlate.geometry.test.ts +54 -0
  194. package/src/__tests__/components/WellPlate.interaction.test.ts +171 -0
  195. package/src/__tests__/components/WellPlate.legend.test.ts +13 -0
  196. package/src/__tests__/components/WellPlate.rendering.test.ts +122 -0
  197. package/src/__tests__/components/WellPlate.sampleDrop.test.ts +70 -0
  198. package/src/__tests__/composables/autoGroup/classify.test.ts +107 -0
  199. package/src/__tests__/composables/autoGroup/columns.test.ts +135 -0
  200. package/src/__tests__/composables/autoGroup/compose.test.ts +227 -0
  201. package/src/__tests__/composables/autoGroup/cooccurrence.test.ts +91 -0
  202. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +50 -0
  203. package/src/__tests__/composables/autoGroup/integration.test.ts +79 -0
  204. package/src/__tests__/composables/autoGroup/template.test.ts +70 -0
  205. package/src/__tests__/composables/autoGroup/tokenize.test.ts +33 -0
  206. package/src/__tests__/composables/useAutoGroup.test.ts +129 -625
  207. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +107 -0
  208. package/src/__tests__/composables/useControlSchema.test.ts +23 -0
  209. package/src/__tests__/composables/useScheduleCalendarLayout.test.ts +89 -0
  210. package/src/__tests__/docs/extractDocsComponents.test.ts +142 -0
  211. package/src/__tests__/docs/extractDocsExports.test.ts +77 -0
  212. package/src/__tests__/docs/extractDocsParsing.test.ts +69 -0
  213. package/src/__tests__/docs/extractDocsTemplates.test.ts +54 -0
  214. package/src/__tests__/docs/extractDocsTheme.test.ts +89 -0
  215. package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -1
  216. package/src/__tests__/fixtures/auto-group/mixed-lc-ms-batch.txt +187 -0
  217. package/src/components/AppSidebar.story.vue +79 -6
  218. package/src/components/AppSidebar.vue +74 -6
  219. package/src/components/AppTopBar.navigation.ts +62 -0
  220. package/src/components/AppTopBar.vue +17 -44
  221. package/src/components/AutoGroupModal.story.vue +50 -0
  222. package/src/components/AutoGroupModal.vue +441 -158
  223. package/src/components/BaseCheckbox.story.vue +27 -0
  224. package/src/components/BaseCheckbox.vue +63 -1
  225. package/src/components/BaseToggle.story.vue +27 -0
  226. package/src/components/BaseToggle.vue +66 -1
  227. package/src/components/CollapsibleCard.vue +123 -45
  228. package/src/components/ControlWorkspaceView.vue +2 -6
  229. package/src/components/DoseCalculator.vue +13 -73
  230. package/src/components/DoseCalculatorVolumeField.vue +61 -0
  231. package/src/components/ExperimentTimeline.vue +6 -31
  232. package/src/components/FormBuilder.story.vue +13 -0
  233. package/src/components/FormBuilder.vue +2 -7
  234. package/src/components/PlateMapEditor.vue +32 -106
  235. package/src/components/PluginWorkspaceView.controls.ts +182 -0
  236. package/src/components/PluginWorkspaceView.navigation.ts +106 -0
  237. package/src/components/PluginWorkspaceView.props.ts +174 -0
  238. package/src/components/PluginWorkspaceView.shell.ts +67 -0
  239. package/src/components/PluginWorkspaceView.vue +88 -404
  240. package/src/components/ProtocolStep.presentation.ts +31 -0
  241. package/src/components/ProtocolStepEditor.state.ts +104 -0
  242. package/src/components/ProtocolStepEditor.vue +48 -179
  243. package/src/components/ProtocolStepParameterField.vue +134 -0
  244. package/src/components/ReagentList.presentation.ts +105 -0
  245. package/src/components/ReagentList.vue +16 -79
  246. package/src/components/SampleSelector.colors.ts +43 -0
  247. package/src/components/SampleSelector.drag.ts +164 -0
  248. package/src/components/SampleSelector.groups.ts +109 -0
  249. package/src/components/SampleSelector.selection.ts +103 -0
  250. package/src/components/SampleSelector.vue +82 -349
  251. package/src/components/SampleSelectorSampleRow.vue +64 -0
  252. package/src/components/ScheduleCalendar.vue +44 -199
  253. package/src/components/SettingsModal.schema.ts +71 -0
  254. package/src/components/SettingsModal.vue +16 -46
  255. package/src/components/WellPlate.colors.ts +56 -0
  256. package/src/components/WellPlate.conditions.ts +100 -0
  257. package/src/components/WellPlate.geometry.ts +91 -0
  258. package/src/components/WellPlate.interaction.ts +272 -0
  259. package/src/components/WellPlate.legend.ts +8 -0
  260. package/src/components/WellPlate.rendering.ts +105 -0
  261. package/src/components/WellPlate.sampleDrop.ts +73 -0
  262. package/src/components/WellPlate.vue +102 -550
  263. package/src/components/internal/FormFieldRendererInternal.vue +23 -5
  264. package/src/components/internal/PlateMapEditorToolbarInternal.vue +128 -0
  265. package/src/composables/autoGroup/classKey.ts +5 -0
  266. package/src/composables/autoGroup/classify.ts +205 -0
  267. package/src/composables/autoGroup/colors.ts +6 -0
  268. package/src/composables/autoGroup/columns.ts +226 -0
  269. package/src/composables/autoGroup/compose.ts +156 -0
  270. package/src/composables/autoGroup/cooccurrence.ts +46 -0
  271. package/src/composables/autoGroup/csv-shim.ts +44 -0
  272. package/src/composables/autoGroup/fingerprint.ts +49 -0
  273. package/src/composables/autoGroup/index.ts +20 -0
  274. package/src/composables/autoGroup/replicatePreGroup.ts +90 -0
  275. package/src/composables/autoGroup/template.ts +126 -0
  276. package/src/composables/autoGroup/tokenize.ts +41 -0
  277. package/src/composables/autoGroup/vocab.json +67 -0
  278. package/src/composables/autoGroupConstants.ts +4 -0
  279. package/src/composables/autoGroupGrouping.ts +148 -0
  280. package/src/composables/controlComponentBindings.ts +1 -1
  281. package/src/composables/controlSchemaAdapters.ts +4 -1
  282. package/src/composables/controlSchemaDoseDesign.ts +215 -0
  283. package/src/composables/controlSchemaFormFields.ts +4 -1
  284. package/src/composables/controlSchemaLayout.ts +1 -1
  285. package/src/composables/controlSchemaModel.ts +163 -0
  286. package/src/composables/controlSchemaNormalize.ts +1 -1
  287. package/src/composables/controlSchemaTypes.ts +372 -0
  288. package/src/composables/controlWorkspaceOptions.ts +1 -1
  289. package/src/composables/formBuilderSchema.ts +153 -0
  290. package/src/composables/pluginEndpointBuilder.ts +203 -0
  291. package/src/composables/protocolTemplateCatalog.ts +325 -0
  292. package/src/composables/useAutoGroup.ts +395 -549
  293. package/src/composables/useAutoGroupInputSources.ts +147 -0
  294. package/src/composables/useBioTemplateControls.ts +1 -1
  295. package/src/composables/useBioTemplatePresetWorkspace.ts +1 -1
  296. package/src/composables/useBioTemplateWorkspace.ts +1 -1
  297. package/src/composables/useControlSchema.ts +21 -692
  298. package/src/composables/useControlWorkspace.ts +7 -13
  299. package/src/composables/useForm.ts +5 -187
  300. package/src/composables/useFormBuilder.ts +11 -153
  301. package/src/composables/useFormValidation.ts +154 -0
  302. package/src/composables/usePluginClient.ts +10 -193
  303. package/src/composables/useProtocolTemplates.ts +10 -328
  304. package/src/composables/useScheduleCalendarLayout.ts +287 -0
  305. package/src/styles/components/app-sidebar.css +134 -6
  306. package/src/styles/components/auto-group-modal.css +248 -310
  307. package/src/styles/components/checkbox.css +87 -0
  308. package/src/styles/components/collapsible-card.css +154 -14
  309. package/src/styles/components/toggle.css +80 -0
  310. package/src/templates/controlSchemaTypes.ts +1 -1
  311. package/src/templates/templateAdapterTypes.ts +58 -0
  312. package/src/templates/templateCreateOptions.ts +208 -0
  313. package/src/templates/templateQpcrTypes.ts +48 -0
  314. package/src/templates/types.ts +79 -275
  315. package/src/types/auto-group.ts +107 -9
  316. package/src/types/componentLabTypes.ts +235 -0
  317. package/src/types/componentWorkflowTypes.ts +190 -0
  318. package/src/types/components.ts +95 -424
  319. package/src/types/form-builder.ts +3 -0
  320. package/src/types/index.ts +2 -0
  321. package/dist/components-DafPc4rM.js.map +0 -1
  322. package/dist/composables-BMkPQhVK.js.map +0 -1
  323. package/dist/templates-bUAWMn5L.js.map +0 -1
  324. package/dist/useProtocolTemplates-QZtHFFH2.js.map +0 -1
@@ -15,6 +15,24 @@ const props = defineProps<Props>()
15
15
 
16
16
  const entry = computed(() => getFieldRegistryEntry(props.field.type))
17
17
 
18
+ const isChoiceField = computed(() =>
19
+ props.field.type === 'checkbox' || props.field.type === 'toggle',
20
+ )
21
+
22
+ const componentProps = computed<Record<string, unknown>>(() => {
23
+ if (!isChoiceField.value) return props.resolvedProps
24
+
25
+ return {
26
+ variant: 'row',
27
+ label: props.field.label,
28
+ description: props.field.hint,
29
+ icon: props.field.icon,
30
+ iconColor: props.field.iconColor,
31
+ iconBg: props.field.iconBg,
32
+ ...props.resolvedProps,
33
+ }
34
+ })
35
+
18
36
  const errorMessage = computed(() => {
19
37
  const name = props.field.name
20
38
  return props.form.touched[name] ? props.form.errors[name] : null
@@ -27,22 +45,22 @@ function handleUpload(files: File[]) {
27
45
 
28
46
  <template>
29
47
  <FormField
30
- :label="field.label"
48
+ :label="isChoiceField ? undefined : field.label"
31
49
  :error="errorMessage ?? undefined"
32
- :hint="field.hint"
50
+ :hint="isChoiceField ? undefined : field.hint"
33
51
  :required="!!field.validation?.required"
34
- :html-for="field.name"
52
+ :html-for="isChoiceField ? undefined : field.name"
35
53
  >
36
54
  <slot :name="`field:${field.name}`" :field="field" :form="form" :field-props="form.getFieldProps(field.name)">
37
55
  <component
38
56
  :is="entry.component"
39
57
  v-if="entry.vModel"
40
- v-bind="resolvedProps"
58
+ v-bind="componentProps"
41
59
  />
42
60
  <component
43
61
  :is="entry.component"
44
62
  v-else
45
- v-bind="resolvedProps"
63
+ v-bind="componentProps"
46
64
  @upload="handleUpload"
47
65
  />
48
66
  </slot>
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ /** Toolbar for PlateMapEditor plate tabs and import/export history actions. */
3
+ import type { PlateMap } from '../../types'
4
+
5
+ interface Props {
6
+ plates: PlateMap[]
7
+ activePlateId?: string
8
+ plateWellCounts: Record<string, number>
9
+ canAddPlate: boolean
10
+ canUndo: boolean
11
+ canRedo: boolean
12
+ slotColorForPlate: (plateId: string, plateIndex: number) => string
13
+ }
14
+
15
+ const props = defineProps<Props>()
16
+
17
+ const emit = defineEmits<{
18
+ 'select-plate': [plateId: string]
19
+ 'remove-plate': [plateId: string]
20
+ 'add-plate': []
21
+ undo: []
22
+ redo: []
23
+ import: []
24
+ 'export-json': []
25
+ }>()
26
+ </script>
27
+
28
+ <template>
29
+ <div class="mint-plate-editor__toolbar">
30
+ <div class="mint-plate-editor__tabs">
31
+ <button
32
+ v-for="(plate, index) in plates"
33
+ :key="plate.id"
34
+ type="button"
35
+ :class="['mint-plate-editor__tab', { 'mint-plate-editor__tab--active': plate.id === activePlateId }]"
36
+ @click="emit('select-plate', plate.id)"
37
+ >
38
+ <span
39
+ class="mint-plate-editor__tab-slot"
40
+ :style="{ backgroundColor: slotColorForPlate(plate.id, index) }"
41
+ />
42
+ <span class="mint-plate-editor__tab-name">{{ plate.name }}</span>
43
+ <span
44
+ v-if="plateWellCounts[plate.id] > 0"
45
+ class="mint-plate-editor__tab-count"
46
+ >
47
+ {{ plateWellCounts[plate.id] }}
48
+ </span>
49
+ <button
50
+ v-if="props.plates.length > 1"
51
+ type="button"
52
+ class="mint-plate-editor__tab-remove"
53
+ :aria-label="`Remove ${plate.name}`"
54
+ @click.stop="emit('remove-plate', plate.id)"
55
+ >
56
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
57
+ <path d="M18 6 6 18" /><path d="m6 6 12 12" />
58
+ </svg>
59
+ </button>
60
+ </button>
61
+
62
+ <button
63
+ v-if="canAddPlate"
64
+ type="button"
65
+ class="mint-plate-editor__add-plate"
66
+ aria-label="Add plate"
67
+ @click="emit('add-plate')"
68
+ >
69
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
70
+ <path d="M5 12h14" /><path d="M12 5v14" />
71
+ </svg>
72
+ <span>Add</span>
73
+ </button>
74
+ </div>
75
+
76
+ <div class="mint-plate-editor__spacer" />
77
+
78
+ <div class="mint-plate-editor__actions">
79
+ <button
80
+ type="button"
81
+ :disabled="!canUndo"
82
+ class="mint-plate-editor__action-btn"
83
+ title="Undo (Ctrl+Z)"
84
+ @click="emit('undo')"
85
+ >
86
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
87
+ <path d="M9 14 4 9l5-5" /><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" />
88
+ </svg>
89
+ </button>
90
+
91
+ <button
92
+ type="button"
93
+ :disabled="!canRedo"
94
+ class="mint-plate-editor__action-btn"
95
+ title="Redo (Ctrl+Shift+Z)"
96
+ @click="emit('redo')"
97
+ >
98
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
99
+ <path d="m15 14 5-5-5-5" /><path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" />
100
+ </svg>
101
+ </button>
102
+
103
+ <div class="mint-plate-editor__divider" />
104
+
105
+ <button
106
+ type="button"
107
+ class="mint-plate-editor__action-btn"
108
+ title="Import"
109
+ @click="emit('import')"
110
+ >
111
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
112
+ <path d="M12 3v12" /><path d="m17 8-5-5-5 5" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
113
+ </svg>
114
+ </button>
115
+
116
+ <button
117
+ type="button"
118
+ class="mint-plate-editor__action-btn"
119
+ title="Export JSON"
120
+ @click="emit('export-json')"
121
+ >
122
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
123
+ <path d="M12 15V3" /><path d="m7 10 5 5 5-5" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
124
+ </svg>
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </template>
@@ -0,0 +1,5 @@
1
+ import type { SampleClass } from '../../types/auto-group'
2
+
3
+ export function classKey(c: Pick<SampleClass, 'kind'> & { subKind?: string }): string {
4
+ return c.subKind ? `${c.kind}:${c.subKind}` : c.kind
5
+ }
@@ -0,0 +1,205 @@
1
+ import type { SampleClass, SampleClassKind, ClassDisposition } from '../../types/auto-group'
2
+ import { classKey } from './classKey'
3
+ import vocabData from './vocab.json'
4
+
5
+ export interface ClassVocab {
6
+ blankPattern: RegExp
7
+ iqcPattern: RegExp
8
+ eqcPattern: RegExp
9
+ standardPattern: RegExp
10
+ standardAllow: Set<string>
11
+ spikeInPattern: RegExp
12
+ stockPattern: RegExp
13
+ /**
14
+ * Matrix lookup: lowercase token → canonical display name. Matching is
15
+ * case-insensitive, and several aliases can collapse to one display name
16
+ * (cell → Cells, medium → Medium).
17
+ */
18
+ matrixVocab: Map<string, string>
19
+ /** Lowercase polarity tokens → POS / NEG. */
20
+ ionizationModes: Map<string, 'POS' | 'NEG'>
21
+ /** Lowercase tokens that flag the next token as the organ subKind. */
22
+ tissueParentTokens: Set<string>
23
+ }
24
+
25
+ type VocabFile = typeof vocabData
26
+ type IonMode = 'POS' | 'NEG'
27
+
28
+ function compileVocab(data: VocabFile): ClassVocab {
29
+ return {
30
+ blankPattern: new RegExp(data.patterns.blank, 'i'),
31
+ iqcPattern: new RegExp(data.patterns.iqc, 'i'),
32
+ eqcPattern: new RegExp(data.patterns.eqc, 'i'),
33
+ standardPattern: new RegExp(data.patterns.standard, 'i'),
34
+ spikeInPattern: new RegExp(data.patterns.spikeIn, 'i'),
35
+ stockPattern: new RegExp(data.patterns.stock, 'i'),
36
+ standardAllow: new Set(data.standardAllow),
37
+ matrixVocab: new Map(Object.entries(data.matrixVocab)),
38
+ ionizationModes: new Map(
39
+ Object.entries(data.ionizationModes).map(([k, v]) => [k, v as IonMode]),
40
+ ),
41
+ tissueParentTokens: new Set(data.tissueParentTokens),
42
+ }
43
+ }
44
+
45
+ export const classVocab: ClassVocab = compileVocab(vocabData)
46
+
47
+ function matrixCanonical(token: string, vocab: ClassVocab): string | undefined {
48
+ return vocab.matrixVocab.get(token.toLowerCase())
49
+ }
50
+
51
+ function isTissueParentToken(token: string, vocab: ClassVocab): boolean {
52
+ return vocab.tissueParentTokens.has(token.toLowerCase())
53
+ }
54
+
55
+ export interface DetectClassOptions {
56
+ vocab?: Partial<ClassVocab>
57
+ /** Per-sample explicit `sample_type` hint (e.g. from experiment design data). */
58
+ sampleTypeHints?: (string | undefined)[]
59
+ }
60
+
61
+ const DEFAULT_DISPOSITION: Record<SampleClassKind, ClassDisposition> = {
62
+ biological: 'group',
63
+ iqc: 'overlay',
64
+ eqc: 'overlay',
65
+ standard: 'overlay',
66
+ 'spike-in': 'overlay',
67
+ stock: 'overlay',
68
+ blank: 'overlay',
69
+ unknown: 'group',
70
+ }
71
+
72
+ function classifyTokens(
73
+ tokens: string[],
74
+ vocab: ClassVocab,
75
+ ): { kind: SampleClassKind; subKind?: string; tagPositions: number[] } {
76
+ // First-match-wins ordering: Blank → IQC → EQC → Standard → SpikeIn → Stock → Biological → Unknown.
77
+ for (let i = 0; i < tokens.length; i++) {
78
+ if (vocab.blankPattern.test(tokens[i])) return { kind: 'blank', tagPositions: [i] }
79
+ }
80
+ for (let i = 0; i < tokens.length; i++) {
81
+ if (vocab.iqcPattern.test(tokens[i])) return { kind: 'iqc', tagPositions: [i] }
82
+ }
83
+ for (let i = 0; i < tokens.length; i++) {
84
+ if (vocab.eqcPattern.test(tokens[i])) return { kind: 'eqc', tagPositions: [i] }
85
+ }
86
+ for (let i = 0; i < tokens.length; i++) {
87
+ if (vocab.standardPattern.test(tokens[i]) || vocab.standardAllow.has(tokens[i])) {
88
+ return { kind: 'standard', tagPositions: [i] }
89
+ }
90
+ }
91
+ for (let i = 0; i < tokens.length; i++) {
92
+ if (vocab.spikeInPattern.test(tokens[i])) return { kind: 'spike-in', tagPositions: [i] }
93
+ }
94
+ for (let i = 0; i < tokens.length; i++) {
95
+ if (vocab.stockPattern.test(tokens[i])) return { kind: 'stock', tagPositions: [i] }
96
+ }
97
+ for (let i = 0; i < tokens.length; i++) {
98
+ const canonical = matrixCanonical(tokens[i], vocab)
99
+ if (canonical) {
100
+ const isTissueParent = isTissueParentToken(tokens[i], vocab)
101
+ const tagPositions = [i]
102
+ if (isTissueParent) {
103
+ // "Tissues"/"Tissue" is the parent group; tag the next token (organ name) as well.
104
+ if (i + 1 < tokens.length) tagPositions.push(i + 1)
105
+ return { kind: 'biological', subKind: 'Tissues', tagPositions }
106
+ }
107
+ // Non-parent matrix token: tag any immediately preceding "Tissues"/"Tissue" parent.
108
+ if (i > 0 && isTissueParentToken(tokens[i - 1], vocab)) {
109
+ tagPositions.unshift(i - 1)
110
+ return { kind: 'biological', subKind: 'Tissues', tagPositions }
111
+ }
112
+ return { kind: 'biological', subKind: canonical, tagPositions }
113
+ }
114
+ }
115
+ return { kind: 'unknown', tagPositions: [] }
116
+ }
117
+
118
+ /**
119
+ * LC-MS batches run positive- and negative-ion-mode acquisitions separately,
120
+ * and the two produce different feature tables that should never be grouped
121
+ * together. When a POS / NEG / positive / negative token is present we suffix
122
+ * it to the class label so the schema builder treats each mode as its own class.
123
+ */
124
+ function ionizationModeFor(
125
+ tokens: string[],
126
+ vocab: ClassVocab,
127
+ ): { mode: IonMode; position: number } | undefined {
128
+ for (let i = 0; i < tokens.length; i++) {
129
+ const mode = vocab.ionizationModes.get(tokens[i].toLowerCase())
130
+ if (mode) return { mode, position: i }
131
+ }
132
+ return undefined
133
+ }
134
+
135
+ function labelFor(kind: SampleClassKind, subKind?: string): string {
136
+ const base: Record<SampleClassKind, string> = {
137
+ biological: 'Biological',
138
+ iqc: 'IQC',
139
+ eqc: 'EQC',
140
+ standard: 'Standard',
141
+ 'spike-in': 'SpikeIn',
142
+ stock: 'Stock',
143
+ blank: 'Blank',
144
+ unknown: 'Unknown',
145
+ }
146
+ return subKind ? `${base[kind]} / ${subKind}` : base[kind]
147
+ }
148
+
149
+ /** Classify each tokenized sample into a SampleClass. */
150
+ export function detectClass(
151
+ tokenizedSamples: string[][],
152
+ options: DetectClassOptions,
153
+ ): SampleClass[] {
154
+ const vocab = { ...classVocab, ...(options.vocab ?? {}) }
155
+ const hints = options.sampleTypeHints ?? []
156
+ const buckets = new Map<string, SampleClass>()
157
+
158
+ for (let i = 0; i < tokenizedSamples.length; i++) {
159
+ const hint = hints[i]?.toLowerCase()
160
+ let kind: SampleClassKind
161
+ let subKind: string | undefined
162
+ let tagPositions: number[]
163
+
164
+ if (hint === 'qc') {
165
+ kind = 'iqc'
166
+ tagPositions = []
167
+ } else if (hint === 'blank') {
168
+ kind = 'blank'
169
+ tagPositions = []
170
+ } else {
171
+ const r = classifyTokens(tokenizedSamples[i], vocab)
172
+ kind = r.kind
173
+ subKind = r.subKind
174
+ tagPositions = r.tagPositions
175
+ }
176
+
177
+ // Split classes by ionization mode (POS / NEG) so the two acquisitions get
178
+ // independent schemas even when they share a matrix and sample-type vocab.
179
+ const mode = ionizationModeFor(tokenizedSamples[i], vocab)
180
+ if (mode) {
181
+ subKind = subKind ? `${subKind} / ${mode.mode}` : mode.mode
182
+ if (!tagPositions.includes(mode.position)) tagPositions.push(mode.position)
183
+ }
184
+
185
+ const key = classKey({ kind, subKind })
186
+ const bucket = buckets.get(key)
187
+ if (bucket) {
188
+ bucket.members.push(i)
189
+ for (const p of tagPositions) {
190
+ if (!bucket.classTagPositions.includes(p)) bucket.classTagPositions.push(p)
191
+ }
192
+ } else {
193
+ buckets.set(key, {
194
+ kind,
195
+ subKind,
196
+ label: labelFor(kind, subKind),
197
+ members: [i],
198
+ classTagPositions: [...tagPositions],
199
+ disposition: DEFAULT_DISPOSITION[kind],
200
+ })
201
+ }
202
+ }
203
+
204
+ return [...buckets.values()]
205
+ }
@@ -0,0 +1,6 @@
1
+ export const DEFAULT_COLORS = [
2
+ '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
3
+ '#EC4899', '#06B6D4', '#84CC16', '#F97316', '#6366F1',
4
+ ]
5
+
6
+ export const QC_OVERLAY_COLOR = '#6B7280'
@@ -0,0 +1,226 @@
1
+ import type {
2
+ ColumnInfo,
3
+ ColumnRole,
4
+ ClassSchema,
5
+ NumericParse,
6
+ SampleClassKind,
7
+ } from '../../types/auto-group'
8
+
9
+ const NUMERIC_VALUE_UNIT = /^(\d+(?:dot\d+)?)([A-Za-z%/μ]+)$/
10
+ const RATIO = /^(\d+)to(\d+)$/
11
+ const RUN_ORDER_PATTERN = /^\d{2,4}[A-Z]?$/
12
+ // Explicit replicate-token vocabulary: rep1 / Rep2 / REP3 / replicate1 / R1.
13
+ // These tokens almost always identify a within-group replicate dimension that
14
+ // users do NOT want in the default group key — e.g. ctrl/rep1, ctrl/rep2
15
+ // should land in one Ctrl group with two replicates, not two singleton groups.
16
+ const REPLICATE_TOKEN_PATTERN = /^(?:rep(?:licate)?|r)\d+$/i
17
+
18
+ // Real-world LC-MS batches often have 30+ distinct conditions (subject IDs,
19
+ // calibration levels, treatment combinations) in a single class. The earlier
20
+ // cutoff of 20 routed everything above it to `ignore`, leaving such columns
21
+ // out of the default groupBy — meaning a single giant group with no useful
22
+ // split. 50 covers the common cases without making singleton-heavy columns
23
+ // (sample IDs, timestamps) into factors.
24
+ const DEFAULT_FACTOR_CARDINALITY_MAX = 50
25
+ // Numerics with only a handful of distinct levels (e.g. `5min` / `10min`,
26
+ // `1nM` / `5nM` / `25nM`) read as factor levels by default. Users opt into
27
+ // numeric binning explicitly via the column popover when they want it.
28
+ const LOW_CARDINALITY_NUMERIC_AS_FACTOR_MAX = 5
29
+
30
+ export function parseNumeric(token: string): NumericParse | null {
31
+ const ratio = RATIO.exec(token)
32
+ if (ratio) {
33
+ const num = Number(ratio[1])
34
+ const den = Number(ratio[2])
35
+ if (den === 0) return null
36
+ return { value: num / den, unit: 'ratio', raw: token }
37
+ }
38
+ const m = NUMERIC_VALUE_UNIT.exec(token)
39
+ if (!m) return null
40
+ const valueStr = m[1].replace(/dot/g, '.')
41
+ const value = Number(valueStr)
42
+ if (Number.isNaN(value)) return null
43
+ return { value, unit: m[2], raw: token }
44
+ }
45
+
46
+ export interface InferRoleInput {
47
+ values: string[]
48
+ position: number
49
+ totalPositions: number
50
+ classSize: number
51
+ }
52
+
53
+ export function inferRole(input: InferRoleInput): ColumnRole {
54
+ const { values, position, totalPositions, classSize } = input
55
+ const unique = new Set(values)
56
+ const cardinality = unique.size
57
+
58
+ if (cardinality === 1) return 'constant'
59
+
60
+ // run-order: trailing position, all values match \d{2,4}[A-Z]? and cardinality is high.
61
+ if (position === totalPositions - 1) {
62
+ const allRunLike = values.every(v => RUN_ORDER_PATTERN.test(v))
63
+ if (allRunLike && cardinality >= classSize * 0.5) return 'run-order'
64
+ }
65
+
66
+ // numeric: every value parses as value+unit (or ratio).
67
+ const allNumeric = values.every(v => parseNumeric(v) !== null)
68
+ if (allNumeric) {
69
+ // A column like 5min / 10min reads as a factor in practice — users want to
70
+ // group on the timepoint, not bin it. Numeric binning UI is opt-in via the
71
+ // column popover for columns the user explicitly wants binned.
72
+ if (cardinality <= LOW_CARDINALITY_NUMERIC_AS_FACTOR_MAX) return 'factor'
73
+ return 'numeric'
74
+ }
75
+
76
+ // replicate by explicit name (rep1, Rep2, replicate3, R1) — regardless of
77
+ // cardinality. The cardinality-based ID_PATTERN check that used to live
78
+ // here (allId && cardinality >= classSize * 0.5) was removed: with the
79
+ // replicate pre-grouping pass running upstream, real replicates have
80
+ // already been collapsed by name, so a column like C1/C2/M1/M2 reaching
81
+ // role inference is almost certainly a distinct-condition factor, not a
82
+ // replicate. Users who want a residual replicate column can still mark
83
+ // it via the Workspace popover.
84
+ const allReplicateNamed = values.every(v => REPLICATE_TOKEN_PATTERN.test(v))
85
+ if (allReplicateNamed) return 'replicate'
86
+
87
+ if (cardinality <= DEFAULT_FACTOR_CARDINALITY_MAX) return 'factor'
88
+
89
+ return 'ignore'
90
+ }
91
+
92
+ /**
93
+ * Auto-detect a friendlier column name from the column's values + role.
94
+ * Returns null when nothing matches, so callers can fall back to "Token N".
95
+ *
96
+ * Heuristics are content-derived and deterministic. They cover the common
97
+ * LC-MS / proteomics naming conventions; users can override anything via
98
+ * the column popover (rename input).
99
+ */
100
+ function inferColumnName(
101
+ values: string[],
102
+ uniqueValues: string[],
103
+ role: ColumnRole,
104
+ ): string | null {
105
+ if (uniqueValues.length === 0) return null
106
+
107
+ // Numeric+unit columns name themselves by the dominant unit category.
108
+ // We tally all values so a mixed-unit column (`1uL`, `1nM`) picks the
109
+ // majority rather than the first sample's unit.
110
+ const allNumeric = (role === 'numeric' || role === 'factor') && values.every(v => parseNumeric(v) !== null)
111
+ if (allNumeric) {
112
+ const categoryFor = (u: string): string | null => {
113
+ const ul = u.toLowerCase()
114
+ if (['min', 'sec', 's', 'hr', 'h', 'd', 'day', 'days', 'wk', 'week', 'hour', 'hours'].includes(ul)) return 'Timepoint'
115
+ if (['nm', 'um', 'mm', 'm', 'molar', 'mol'].includes(ul)) return 'Concentration'
116
+ if (['mg', 'g', 'kg', 'ng', 'ug', 'pg'].includes(ul)) return 'Mass'
117
+ if (['ml', 'l', 'ul', 'nl', 'pl'].includes(ul)) return 'Volume'
118
+ if (ul === '%') return 'Percentage'
119
+ if (['c', 'k', 'celsius', 'kelvin'].includes(ul)) return 'Temperature'
120
+ return null
121
+ }
122
+ const tally = new Map<string, number>()
123
+ for (const v of values) {
124
+ const p = parseNumeric(v)
125
+ const cat = p ? categoryFor(p.unit) : null
126
+ if (cat) tally.set(cat, (tally.get(cat) ?? 0) + 1)
127
+ }
128
+ let best: { cat: string; count: number } | null = null
129
+ for (const [cat, count] of tally) {
130
+ if (!best || count > best.count) best = { cat, count }
131
+ }
132
+ if (best && best.count >= values.length / 2) return best.cat
133
+ }
134
+
135
+ // Date-like all-digits tokens (YYMMDD or YYYYMMDD).
136
+ if (values.every(v => /^\d{6}$/.test(v) || /^\d{8}$/.test(v))) return 'Date'
137
+
138
+ // Run-order column already has the right semantic role.
139
+ if (role === 'run-order') return 'Run Order'
140
+
141
+ // Polarity tokens (POS/NEG/positive/negative).
142
+ if (values.every(v => /^(pos|neg|positive|negative)$/i.test(v))) return 'Polarity'
143
+
144
+ // Common biological matrices — kept in sync with classify.ts matrixVocab.
145
+ if (values.every(v => /^(plasma|serum|tissue|tissues|kidney|liver|tumor|urine|brain|muscle|adipose|csf|feces|faeces|cells?|media|medium|extract)$/i.test(v))) {
146
+ return 'Matrix'
147
+ }
148
+
149
+ // Experiment / study identifiers (exp268, study42, expt7).
150
+ if (values.every(v => /^(exp|expt|study)\d+$/i.test(v))) return 'Experiment'
151
+
152
+ // Operator / Username (2-3 uppercase letters per value, low cardinality).
153
+ // Constant or small-set 2-3-letter all-caps columns are almost always
154
+ // operator initials (LCK, JD, AB). Larger 2-4-letter columns could be
155
+ // project codes — disambiguation happens after the loop via the
156
+ // `Username → Project` post-pass.
157
+ if (uniqueValues.length <= 4 && values.every(v => /^[A-Z]{2,3}$/.test(v))) return 'Username'
158
+
159
+ return null
160
+ }
161
+
162
+ export function buildClassSchema(
163
+ tokenizedMembers: string[][],
164
+ classKind: SampleClassKind,
165
+ subKind: string | undefined,
166
+ classTagPositions: number[],
167
+ ): ClassSchema {
168
+ const totalPositions = tokenizedMembers[0]?.length ?? 0
169
+ const classSize = tokenizedMembers.length
170
+
171
+ const columns: ColumnInfo[] = []
172
+ const tagSet = new Set(classTagPositions)
173
+
174
+ for (let pos = 0; pos < totalPositions; pos++) {
175
+ const values = tokenizedMembers.map(t => t[pos] ?? '')
176
+ const uniqueValues = [...new Set(values)]
177
+
178
+ let role: ColumnRole
179
+ if (tagSet.has(pos) && uniqueValues.length === 1) {
180
+ // Only constant tag positions become class-tag; multi-valued positions (e.g. organ
181
+ // type within a Tissues class) fall through to normal role inference so they can be
182
+ // used as factor columns for grouping.
183
+ role = 'class-tag'
184
+ } else {
185
+ role = inferRole({ values, position: pos, totalPositions, classSize })
186
+ }
187
+
188
+ const numeric =
189
+ role === 'numeric'
190
+ ? {
191
+ byValue: Object.fromEntries(
192
+ uniqueValues
193
+ .map(v => [v, parseNumeric(v)] as const)
194
+ .filter((e): e is readonly [string, NumericParse] => e[1] !== null),
195
+ ),
196
+ }
197
+ : undefined
198
+
199
+ const inferredName = inferColumnName(values, uniqueValues, role)
200
+ columns.push({
201
+ index: pos,
202
+ name: inferredName ?? `Token ${pos + 1}`,
203
+ sourceIndices: [pos],
204
+ uniqueValues,
205
+ cardinality: uniqueValues.length,
206
+ role,
207
+ numeric,
208
+ // Legacy field
209
+ type: pos === 0 ? 'prefix' : 'suffix',
210
+ })
211
+ }
212
+
213
+ // Disambiguate Username when there are multiple constant 2-4 letter columns:
214
+ // first hit stays "Username", subsequent ones become "Project".
215
+ let usernameAssigned = false
216
+ for (const c of columns) {
217
+ if (c.name === 'Username') {
218
+ if (usernameAssigned) c.name = 'Project'
219
+ else usernameAssigned = true
220
+ }
221
+ }
222
+
223
+ const groupBy = columns.filter(c => c.role === 'factor').map(c => c.index)
224
+
225
+ return { classKind, subKind, columns, groupBy }
226
+ }