@morscherlab/mint-sdk 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (491) hide show
  1. package/README.md +326 -0
  2. package/dist/__stories__/experiment-helpers.d.ts +25 -0
  3. package/dist/__tests__/components/AppLayout.test.d.ts +1 -0
  4. package/dist/__tests__/components/AppSidebar.test.d.ts +1 -0
  5. package/dist/__tests__/components/AppTopBar.test.d.ts +1 -0
  6. package/dist/__tests__/components/BaseInput.test.d.ts +1 -0
  7. package/dist/__tests__/components/BasePill.test.d.ts +1 -0
  8. package/dist/__tests__/components/Calendar.test.d.ts +1 -0
  9. package/dist/__tests__/components/CollapsibleCard.test.d.ts +1 -0
  10. package/dist/__tests__/components/DataFrame.test.d.ts +1 -0
  11. package/dist/__tests__/components/DropdownButton.test.d.ts +1 -0
  12. package/dist/__tests__/composables/formBuilderRegistry.test.d.ts +1 -0
  13. package/dist/__tests__/composables/useAppExperiment.test.d.ts +1 -0
  14. package/dist/__tests__/composables/useAuth.test.d.ts +1 -0
  15. package/dist/__tests__/composables/useAutoGroup.test.d.ts +1 -0
  16. package/dist/__tests__/composables/useExperimentData.test.d.ts +13 -0
  17. package/dist/__tests__/composables/useExperimentSave.test.d.ts +1 -0
  18. package/dist/__tests__/composables/useForm.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFormBuilder.test.d.ts +1 -0
  20. package/dist/__tests__/composables/usePlatformContext.test.d.ts +1 -0
  21. package/dist/__tests__/composables/usePluginApi.test.d.ts +13 -0
  22. package/dist/__tests__/composables/usePluginConfig.test.d.ts +14 -0
  23. package/dist/__tests__/utils/color.test.d.ts +1 -0
  24. package/dist/auth-BYmxZdJl.js +297 -0
  25. package/dist/auth-BYmxZdJl.js.map +1 -0
  26. package/dist/components/AlertBox.vue.d.ts +34 -0
  27. package/dist/components/AppAvatarMenu.vue.d.ts +58 -0
  28. package/dist/components/AppContainer.vue.d.ts +28 -0
  29. package/dist/components/AppLayout.vue.d.ts +31 -0
  30. package/dist/components/AppPageSelector.vue.d.ts +43 -0
  31. package/dist/components/AppPillNav.vue.d.ts +11 -0
  32. package/dist/components/AppPluginSwitcher.vue.d.ts +38 -0
  33. package/dist/components/AppSidebar.vue.d.ts +47 -0
  34. package/dist/components/AppTopBar.vue.d.ts +111 -0
  35. package/dist/components/AuditTrail.vue.d.ts +38 -0
  36. package/dist/components/AutoGroupModal.vue.d.ts +124 -0
  37. package/dist/components/Avatar.vue.d.ts +14 -0
  38. package/dist/components/BaseButton.vue.d.ts +37 -0
  39. package/dist/components/BaseCheckbox.vue.d.ts +17 -0
  40. package/dist/components/BaseInput.vue.d.ts +34 -0
  41. package/dist/components/BaseModal.vue.d.ts +46 -0
  42. package/dist/components/BasePill.vue.d.ts +57 -0
  43. package/dist/components/BaseRadioGroup.vue.d.ts +21 -0
  44. package/dist/components/BaseSelect.vue.d.ts +20 -0
  45. package/dist/components/BaseSlider.vue.d.ts +22 -0
  46. package/dist/components/BaseTabs.vue.d.ts +14 -0
  47. package/dist/components/BaseTextarea.vue.d.ts +30 -0
  48. package/dist/components/BaseToggle.vue.d.ts +19 -0
  49. package/dist/components/BatchProgressList.vue.d.ts +43 -0
  50. package/dist/components/Breadcrumb.vue.d.ts +33 -0
  51. package/dist/components/Calendar.vue.d.ts +107 -0
  52. package/dist/components/ChartContainer.vue.d.ts +31 -0
  53. package/dist/components/ChemicalFormula.vue.d.ts +8 -0
  54. package/dist/components/CollapsibleCard.vue.d.ts +41 -0
  55. package/dist/components/ColorSlider.vue.d.ts +34 -0
  56. package/dist/components/ConcentrationInput.vue.d.ts +25 -0
  57. package/dist/components/ConfirmDialog.vue.d.ts +42 -0
  58. package/dist/components/DataFrame.vue.d.ts +107 -0
  59. package/dist/components/DatePicker.vue.d.ts +25 -0
  60. package/dist/components/DateTimePicker.vue.d.ts +30 -0
  61. package/dist/components/Divider.vue.d.ts +14 -0
  62. package/dist/components/DoseCalculator.vue.d.ts +19 -0
  63. package/dist/components/DropdownButton.vue.d.ts +47 -0
  64. package/dist/components/EmptyState.vue.d.ts +36 -0
  65. package/dist/components/ExperimentCodeBadge.vue.d.ts +14 -0
  66. package/dist/components/ExperimentDataViewer.vue.d.ts +29 -0
  67. package/dist/components/ExperimentPopover.vue.d.ts +32 -0
  68. package/dist/components/ExperimentSelectorModal.vue.d.ts +28 -0
  69. package/dist/components/ExperimentTimeline.vue.d.ts +44 -0
  70. package/dist/components/FileUploader.vue.d.ts +40 -0
  71. package/dist/components/FitPanel.vue.d.ts +46 -0
  72. package/dist/components/FormActions.vue.d.ts +33 -0
  73. package/dist/components/FormBuilder.vue.d.ts +287 -0
  74. package/dist/components/FormField.vue.d.ts +28 -0
  75. package/dist/components/FormFieldRenderer.vue.d.ts +31 -0
  76. package/dist/components/FormSection.vue.d.ts +43 -0
  77. package/dist/components/FormulaInput.vue.d.ts +25 -0
  78. package/dist/components/GroupAssigner.vue.d.ts +25 -0
  79. package/dist/components/GroupingModal.vue.d.ts +12 -0
  80. package/dist/components/IconButton.vue.d.ts +34 -0
  81. package/dist/components/LoadingSpinner.vue.d.ts +12 -0
  82. package/dist/components/MoleculeInput.vue.d.ts +27 -0
  83. package/dist/components/MultiSelect.vue.d.ts +19 -0
  84. package/dist/components/NumberInput.vue.d.ts +22 -0
  85. package/dist/components/PlateMapEditor.vue.d.ts +50 -0
  86. package/dist/components/ProgressBar.vue.d.ts +23 -0
  87. package/dist/components/ProtocolStepEditor.vue.d.ts +24 -0
  88. package/dist/components/RackEditor.vue.d.ts +40 -0
  89. package/dist/components/ReagentEditor.vue.d.ts +30 -0
  90. package/dist/components/ReagentList.vue.d.ts +32 -0
  91. package/dist/components/ResourceCard.vue.d.ts +50 -0
  92. package/dist/components/SampleHierarchyTree.vue.d.ts +26 -0
  93. package/dist/components/SampleLegend.vue.d.ts +32 -0
  94. package/dist/components/SampleSelector.vue.d.ts +29 -0
  95. package/dist/components/ScheduleCalendar.vue.d.ts +110 -0
  96. package/dist/components/ScientificNumber.vue.d.ts +14 -0
  97. package/dist/components/SegmentedControl.vue.d.ts +20 -0
  98. package/dist/components/SequenceInput.vue.d.ts +54 -0
  99. package/dist/components/SettingsButton.vue.d.ts +30 -0
  100. package/dist/components/SettingsModal.vue.d.ts +36 -0
  101. package/dist/components/Skeleton.vue.d.ts +11 -0
  102. package/dist/components/StatusIndicator.vue.d.ts +13 -0
  103. package/dist/components/StepWizard.vue.d.ts +65 -0
  104. package/dist/components/TagsInput.vue.d.ts +39 -0
  105. package/dist/components/ThemeToggle.vue.d.ts +7 -0
  106. package/dist/components/TimePicker.vue.d.ts +29 -0
  107. package/dist/components/TimeRangeInput.vue.d.ts +27 -0
  108. package/dist/components/ToastNotification.vue.d.ts +2 -0
  109. package/dist/components/Tooltip.vue.d.ts +35 -0
  110. package/dist/components/UnitInput.vue.d.ts +39 -0
  111. package/dist/components/WellEditPopup.vue.d.ts +25 -0
  112. package/dist/components/WellPlate.vue.d.ts +73 -0
  113. package/dist/components/index.d.ts +87 -0
  114. package/dist/components/index.js +3 -0
  115. package/dist/components-CKf-UpGi.js +15089 -0
  116. package/dist/components-CKf-UpGi.js.map +1 -0
  117. package/dist/composables/experiment-utils.d.ts +8 -0
  118. package/dist/composables/formBuilderRegistry.d.ts +13 -0
  119. package/dist/composables/index.d.ts +28 -0
  120. package/dist/composables/index.js +3 -0
  121. package/dist/composables/useApi.d.ts +20 -0
  122. package/dist/composables/useAppExperiment.d.ts +37 -0
  123. package/dist/composables/useAsync.d.ts +128 -0
  124. package/dist/composables/useAuth.d.ts +47 -0
  125. package/dist/composables/useAutoGroup.d.ts +106 -0
  126. package/dist/composables/useChemicalFormula.d.ts +21 -0
  127. package/dist/composables/useConcentrationUnits.d.ts +29 -0
  128. package/dist/composables/useDoseCalculator.d.ts +58 -0
  129. package/dist/composables/useExperimentData.d.ts +18 -0
  130. package/dist/composables/useExperimentSave.d.ts +36 -0
  131. package/dist/composables/useExperimentSelector.d.ts +30 -0
  132. package/dist/composables/useForm.d.ts +92 -0
  133. package/dist/composables/useFormBuilder.d.ts +24 -0
  134. package/dist/composables/usePasskey.d.ts +10 -0
  135. package/dist/composables/usePlatformContext.d.ts +131 -0
  136. package/dist/composables/usePluginApi.d.ts +29 -0
  137. package/dist/composables/usePluginConfig.d.ts +13 -0
  138. package/dist/composables/useProtocolTemplates.d.ts +44 -0
  139. package/dist/composables/useRackEditor.d.ts +31 -0
  140. package/dist/composables/useReagentSeries.d.ts +23 -0
  141. package/dist/composables/useScheduleDrag.d.ts +78 -0
  142. package/dist/composables/useSequenceUtils.d.ts +14 -0
  143. package/dist/composables/useTheme.d.ts +8 -0
  144. package/dist/composables/useTimeUtils.d.ts +29 -0
  145. package/dist/composables/useToast.d.ts +22 -0
  146. package/dist/composables/useWellPlateEditor.d.ts +33 -0
  147. package/dist/composables-D0QfFzq1.js +805 -0
  148. package/dist/composables-D0QfFzq1.js.map +1 -0
  149. package/dist/histoire.setup.d.ts +1 -0
  150. package/dist/index.d.ts +6 -0
  151. package/dist/index.js +7 -0
  152. package/dist/install.d.ts +16 -0
  153. package/dist/install.js +23 -0
  154. package/dist/install.js.map +1 -0
  155. package/dist/stores/auth.d.ts +146 -0
  156. package/dist/stores/index.d.ts +2 -0
  157. package/dist/stores/index.js +2 -0
  158. package/dist/stores/settings.d.ts +75 -0
  159. package/dist/styles.css +29728 -0
  160. package/dist/tailwind.preset.d.ts +58 -0
  161. package/dist/tailwind.preset.js +66 -0
  162. package/dist/tailwind.preset.js.map +1 -0
  163. package/dist/types/auth.d.ts +42 -0
  164. package/dist/types/auto-group.d.ts +34 -0
  165. package/dist/types/components.d.ts +528 -0
  166. package/dist/types/form-builder.d.ts +167 -0
  167. package/dist/types/index.d.ts +5 -0
  168. package/dist/types/index.js +0 -0
  169. package/dist/types/platform.d.ts +75 -0
  170. package/dist/useScheduleDrag-DAJueTbK.js +7181 -0
  171. package/dist/useScheduleDrag-DAJueTbK.js.map +1 -0
  172. package/dist/utils/color.d.ts +24 -0
  173. package/package.json +114 -0
  174. package/src/__stories__/experiment-helpers.ts +83 -0
  175. package/src/__tests__/components/AppLayout.test.ts +163 -0
  176. package/src/__tests__/components/AppSidebar.test.ts +292 -0
  177. package/src/__tests__/components/AppTopBar.test.ts +683 -0
  178. package/src/__tests__/components/BaseInput.test.ts +99 -0
  179. package/src/__tests__/components/BasePill.test.ts +291 -0
  180. package/src/__tests__/components/Calendar.test.ts +566 -0
  181. package/src/__tests__/components/CollapsibleCard.test.ts +524 -0
  182. package/src/__tests__/components/DataFrame.test.ts +767 -0
  183. package/src/__tests__/components/DropdownButton.test.ts +471 -0
  184. package/src/__tests__/composables/formBuilderRegistry.test.ts +187 -0
  185. package/src/__tests__/composables/useAppExperiment.test.ts +560 -0
  186. package/src/__tests__/composables/useAuth.test.ts +188 -0
  187. package/src/__tests__/composables/useAutoGroup.test.ts +860 -0
  188. package/src/__tests__/composables/useExperimentData.test.ts +127 -0
  189. package/src/__tests__/composables/useExperimentSave.test.ts +347 -0
  190. package/src/__tests__/composables/useForm.test.ts +205 -0
  191. package/src/__tests__/composables/useFormBuilder.test.ts +917 -0
  192. package/src/__tests__/composables/usePlatformContext.test.ts +116 -0
  193. package/src/__tests__/composables/usePluginApi.test.ts +81 -0
  194. package/src/__tests__/composables/usePluginConfig.test.ts +176 -0
  195. package/src/__tests__/utils/color.test.ts +96 -0
  196. package/src/components/AlertBox.story.vue +204 -0
  197. package/src/components/AlertBox.vue +88 -0
  198. package/src/components/AppAvatarMenu.story.vue +155 -0
  199. package/src/components/AppAvatarMenu.vue +184 -0
  200. package/src/components/AppContainer.story.vue +104 -0
  201. package/src/components/AppContainer.vue +34 -0
  202. package/src/components/AppLayout.story.vue +292 -0
  203. package/src/components/AppLayout.vue +75 -0
  204. package/src/components/AppPageSelector.vue +159 -0
  205. package/src/components/AppPillNav.vue +66 -0
  206. package/src/components/AppPluginSwitcher.vue +241 -0
  207. package/src/components/AppSidebar.story.vue +309 -0
  208. package/src/components/AppSidebar.vue +119 -0
  209. package/src/components/AppTopBar.story.vue +304 -0
  210. package/src/components/AppTopBar.vue +661 -0
  211. package/src/components/AuditTrail.story.vue +163 -0
  212. package/src/components/AuditTrail.vue +151 -0
  213. package/src/components/AutoGroupModal.story.vue +273 -0
  214. package/src/components/AutoGroupModal.vue +566 -0
  215. package/src/components/Avatar.story.vue +115 -0
  216. package/src/components/Avatar.vue +79 -0
  217. package/src/components/BaseButton.story.vue +96 -0
  218. package/src/components/BaseButton.vue +73 -0
  219. package/src/components/BaseCheckbox.story.vue +73 -0
  220. package/src/components/BaseCheckbox.vue +69 -0
  221. package/src/components/BaseInput.story.vue +98 -0
  222. package/src/components/BaseInput.vue +74 -0
  223. package/src/components/BaseModal.story.vue +237 -0
  224. package/src/components/BaseModal.vue +182 -0
  225. package/src/components/BasePill.story.vue +142 -0
  226. package/src/components/BasePill.vue +89 -0
  227. package/src/components/BaseRadioGroup.story.vue +145 -0
  228. package/src/components/BaseRadioGroup.vue +124 -0
  229. package/src/components/BaseSelect.story.vue +120 -0
  230. package/src/components/BaseSelect.vue +71 -0
  231. package/src/components/BaseSlider.story.vue +122 -0
  232. package/src/components/BaseSlider.vue +126 -0
  233. package/src/components/BaseTabs.story.vue +127 -0
  234. package/src/components/BaseTabs.vue +59 -0
  235. package/src/components/BaseTextarea.story.vue +91 -0
  236. package/src/components/BaseTextarea.vue +62 -0
  237. package/src/components/BaseToggle.story.vue +81 -0
  238. package/src/components/BaseToggle.vue +76 -0
  239. package/src/components/BatchProgressList.story.vue +92 -0
  240. package/src/components/BatchProgressList.vue +184 -0
  241. package/src/components/Breadcrumb.story.vue +106 -0
  242. package/src/components/Breadcrumb.vue +75 -0
  243. package/src/components/Calendar.story.vue +106 -0
  244. package/src/components/Calendar.vue +363 -0
  245. package/src/components/ChartContainer.story.vue +113 -0
  246. package/src/components/ChartContainer.vue +64 -0
  247. package/src/components/ChemicalFormula.story.vue +102 -0
  248. package/src/components/ChemicalFormula.vue +39 -0
  249. package/src/components/CollapsibleCard.story.vue +135 -0
  250. package/src/components/CollapsibleCard.vue +167 -0
  251. package/src/components/ColorSlider.story.vue +120 -0
  252. package/src/components/ColorSlider.vue +164 -0
  253. package/src/components/ConcentrationInput.story.vue +77 -0
  254. package/src/components/ConcentrationInput.vue +185 -0
  255. package/src/components/ConfirmDialog.story.vue +248 -0
  256. package/src/components/ConfirmDialog.vue +93 -0
  257. package/src/components/DataFrame.story.vue +148 -0
  258. package/src/components/DataFrame.vue +419 -0
  259. package/src/components/DatePicker.story.vue +119 -0
  260. package/src/components/DatePicker.vue +330 -0
  261. package/src/components/DateTimePicker.story.vue +112 -0
  262. package/src/components/DateTimePicker.vue +392 -0
  263. package/src/components/Divider.story.vue +80 -0
  264. package/src/components/Divider.vue +49 -0
  265. package/src/components/DoseCalculator.story.vue +68 -0
  266. package/src/components/DoseCalculator.vue +476 -0
  267. package/src/components/DropdownButton.story.vue +102 -0
  268. package/src/components/DropdownButton.vue +181 -0
  269. package/src/components/EmptyState.story.vue +135 -0
  270. package/src/components/EmptyState.vue +69 -0
  271. package/src/components/ExperimentCodeBadge.story.vue +77 -0
  272. package/src/components/ExperimentCodeBadge.vue +64 -0
  273. package/src/components/ExperimentDataViewer.story.vue +174 -0
  274. package/src/components/ExperimentDataViewer.vue +288 -0
  275. package/src/components/ExperimentPopover.story.vue +384 -0
  276. package/src/components/ExperimentPopover.vue +241 -0
  277. package/src/components/ExperimentSelectorModal.story.vue +391 -0
  278. package/src/components/ExperimentSelectorModal.vue +387 -0
  279. package/src/components/ExperimentTimeline.story.vue +161 -0
  280. package/src/components/ExperimentTimeline.vue +382 -0
  281. package/src/components/FileUploader.story.vue +107 -0
  282. package/src/components/FileUploader.vue +386 -0
  283. package/src/components/FitPanel.story.vue +125 -0
  284. package/src/components/FitPanel.vue +120 -0
  285. package/src/components/FormActions.vue +92 -0
  286. package/src/components/FormBuilder.vue +214 -0
  287. package/src/components/FormField.story.vue +132 -0
  288. package/src/components/FormField.vue +59 -0
  289. package/src/components/FormFieldRenderer.vue +58 -0
  290. package/src/components/FormSection.vue +90 -0
  291. package/src/components/FormulaInput.story.vue +96 -0
  292. package/src/components/FormulaInput.vue +125 -0
  293. package/src/components/GroupAssigner.story.vue +83 -0
  294. package/src/components/GroupAssigner.vue +284 -0
  295. package/src/components/GroupingModal.story.vue +52 -0
  296. package/src/components/GroupingModal.vue +422 -0
  297. package/src/components/IconButton.story.vue +135 -0
  298. package/src/components/IconButton.vue +73 -0
  299. package/src/components/LoadingSpinner.story.vue +70 -0
  300. package/src/components/LoadingSpinner.vue +50 -0
  301. package/src/components/MoleculeInput.story.vue +66 -0
  302. package/src/components/MoleculeInput.vue +426 -0
  303. package/src/components/MultiSelect.story.vue +132 -0
  304. package/src/components/MultiSelect.vue +118 -0
  305. package/src/components/NumberInput.story.vue +122 -0
  306. package/src/components/NumberInput.vue +160 -0
  307. package/src/components/PlateMapEditor.story.vue +92 -0
  308. package/src/components/PlateMapEditor.vue +513 -0
  309. package/src/components/ProgressBar.story.vue +148 -0
  310. package/src/components/ProgressBar.vue +114 -0
  311. package/src/components/ProtocolStepEditor.story.vue +69 -0
  312. package/src/components/ProtocolStepEditor.vue +522 -0
  313. package/src/components/RackEditor.story.vue +100 -0
  314. package/src/components/RackEditor.vue +371 -0
  315. package/src/components/ReagentEditor.story.vue +153 -0
  316. package/src/components/ReagentEditor.vue +418 -0
  317. package/src/components/ReagentList.story.vue +137 -0
  318. package/src/components/ReagentList.vue +463 -0
  319. package/src/components/ResourceCard.story.vue +150 -0
  320. package/src/components/ResourceCard.vue +161 -0
  321. package/src/components/SampleHierarchyTree.story.vue +161 -0
  322. package/src/components/SampleHierarchyTree.vue +256 -0
  323. package/src/components/SampleLegend.story.vue +91 -0
  324. package/src/components/SampleLegend.vue +119 -0
  325. package/src/components/SampleSelector.story.vue +111 -0
  326. package/src/components/SampleSelector.vue +1033 -0
  327. package/src/components/ScheduleCalendar.story.vue +195 -0
  328. package/src/components/ScheduleCalendar.vue +569 -0
  329. package/src/components/ScientificNumber.story.vue +127 -0
  330. package/src/components/ScientificNumber.vue +197 -0
  331. package/src/components/SegmentedControl.story.vue +132 -0
  332. package/src/components/SegmentedControl.vue +79 -0
  333. package/src/components/SequenceInput.story.vue +119 -0
  334. package/src/components/SequenceInput.vue +209 -0
  335. package/src/components/SettingsButton.story.vue +58 -0
  336. package/src/components/SettingsButton.vue +76 -0
  337. package/src/components/SettingsModal.story.vue +145 -0
  338. package/src/components/SettingsModal.vue +146 -0
  339. package/src/components/Skeleton.story.vue +141 -0
  340. package/src/components/Skeleton.vue +74 -0
  341. package/src/components/StatusIndicator.story.vue +99 -0
  342. package/src/components/StatusIndicator.vue +40 -0
  343. package/src/components/StepWizard.story.vue +155 -0
  344. package/src/components/StepWizard.vue +223 -0
  345. package/src/components/TagsInput.story.vue +155 -0
  346. package/src/components/TagsInput.vue +265 -0
  347. package/src/components/ThemeToggle.story.vue +36 -0
  348. package/src/components/ThemeToggle.vue +54 -0
  349. package/src/components/TimePicker.story.vue +96 -0
  350. package/src/components/TimePicker.vue +273 -0
  351. package/src/components/TimeRangeInput.story.vue +104 -0
  352. package/src/components/TimeRangeInput.vue +122 -0
  353. package/src/components/ToastNotification.story.vue +157 -0
  354. package/src/components/ToastNotification.vue +62 -0
  355. package/src/components/Tooltip.story.vue +138 -0
  356. package/src/components/Tooltip.vue +119 -0
  357. package/src/components/UnitInput.story.vue +194 -0
  358. package/src/components/UnitInput.vue +213 -0
  359. package/src/components/WellEditPopup.vue +234 -0
  360. package/src/components/WellPlate.story.vue +282 -0
  361. package/src/components/WellPlate.vue +830 -0
  362. package/src/components/index.ts +118 -0
  363. package/src/composables/experiment-utils.ts +57 -0
  364. package/src/composables/formBuilderRegistry.ts +79 -0
  365. package/src/composables/index.ts +140 -0
  366. package/src/composables/useApi.ts +167 -0
  367. package/src/composables/useAppExperiment.ts +159 -0
  368. package/src/composables/useAsync.ts +323 -0
  369. package/src/composables/useAuth.ts +445 -0
  370. package/src/composables/useAutoGroup.ts +641 -0
  371. package/src/composables/useChemicalFormula.ts +275 -0
  372. package/src/composables/useConcentrationUnits.ts +246 -0
  373. package/src/composables/useDoseCalculator.ts +370 -0
  374. package/src/composables/useExperimentData.ts +86 -0
  375. package/src/composables/useExperimentSave.ts +192 -0
  376. package/src/composables/useExperimentSelector.ts +292 -0
  377. package/src/composables/useForm.ts +416 -0
  378. package/src/composables/useFormBuilder.ts +383 -0
  379. package/src/composables/usePasskey.ts +216 -0
  380. package/src/composables/usePlatformContext.ts +299 -0
  381. package/src/composables/usePluginApi.ts +39 -0
  382. package/src/composables/usePluginConfig.ts +93 -0
  383. package/src/composables/useProtocolTemplates.ts +518 -0
  384. package/src/composables/useRackEditor.ts +222 -0
  385. package/src/composables/useReagentSeries.ts +91 -0
  386. package/src/composables/useScheduleDrag.ts +245 -0
  387. package/src/composables/useSequenceUtils.ts +105 -0
  388. package/src/composables/useTheme.ts +58 -0
  389. package/src/composables/useTimeUtils.ts +131 -0
  390. package/src/composables/useToast.ts +40 -0
  391. package/src/composables/useWellPlateEditor.ts +421 -0
  392. package/src/histoire.setup.ts +17 -0
  393. package/src/index.ts +367 -0
  394. package/src/install.ts +32 -0
  395. package/src/stores/auth.ts +152 -0
  396. package/src/stores/index.ts +2 -0
  397. package/src/stores/settings.ts +218 -0
  398. package/src/styles/components/alert-box.css +150 -0
  399. package/src/styles/components/app-avatar-menu.css +155 -0
  400. package/src/styles/components/app-container.css +33 -0
  401. package/src/styles/components/app-layout.css +98 -0
  402. package/src/styles/components/app-page-selector.css +191 -0
  403. package/src/styles/components/app-pill-nav.css +57 -0
  404. package/src/styles/components/app-plugin-switcher.css +209 -0
  405. package/src/styles/components/app-sidebar.css +145 -0
  406. package/src/styles/components/app-top-bar.css +492 -0
  407. package/src/styles/components/audit-trail.css +143 -0
  408. package/src/styles/components/auto-group-modal.css +644 -0
  409. package/src/styles/components/avatar.css +73 -0
  410. package/src/styles/components/batch-progress-list.css +196 -0
  411. package/src/styles/components/breadcrumb.css +64 -0
  412. package/src/styles/components/button.css +188 -0
  413. package/src/styles/components/calendar.css +192 -0
  414. package/src/styles/components/chart-container.css +69 -0
  415. package/src/styles/components/checkbox.css +123 -0
  416. package/src/styles/components/chemical-formula.css +46 -0
  417. package/src/styles/components/collapsible-card.css +253 -0
  418. package/src/styles/components/color-slider.css +110 -0
  419. package/src/styles/components/concentration-input.css +156 -0
  420. package/src/styles/components/confirm-dialog.css +183 -0
  421. package/src/styles/components/dataframe.css +382 -0
  422. package/src/styles/components/date-picker.css +243 -0
  423. package/src/styles/components/datetime-picker.css +229 -0
  424. package/src/styles/components/divider.css +63 -0
  425. package/src/styles/components/dose-calculator.css +301 -0
  426. package/src/styles/components/dropdown-button.css +280 -0
  427. package/src/styles/components/empty-state.css +151 -0
  428. package/src/styles/components/experiment-code-badge.css +33 -0
  429. package/src/styles/components/experiment-data-viewer.css +138 -0
  430. package/src/styles/components/experiment-popover.css +562 -0
  431. package/src/styles/components/experiment-selector-modal.css +285 -0
  432. package/src/styles/components/experiment-timeline.css +529 -0
  433. package/src/styles/components/file-uploader.css +310 -0
  434. package/src/styles/components/fit-panel.css +67 -0
  435. package/src/styles/components/form-builder.css +69 -0
  436. package/src/styles/components/form-field.css +48 -0
  437. package/src/styles/components/formula-input.css +103 -0
  438. package/src/styles/components/group-assigner.css +200 -0
  439. package/src/styles/components/grouping-modal.css +323 -0
  440. package/src/styles/components/icon-button.css +192 -0
  441. package/src/styles/components/input.css +66 -0
  442. package/src/styles/components/loading-spinner.css +67 -0
  443. package/src/styles/components/modal.css +350 -0
  444. package/src/styles/components/molecule-input.css +186 -0
  445. package/src/styles/components/multi-select.css +131 -0
  446. package/src/styles/components/number-input.css +199 -0
  447. package/src/styles/components/pill.css +188 -0
  448. package/src/styles/components/plate-map-editor.css +464 -0
  449. package/src/styles/components/progress-bar.css +133 -0
  450. package/src/styles/components/protocol-step-editor.css +449 -0
  451. package/src/styles/components/rack-editor.css +265 -0
  452. package/src/styles/components/radio-group.css +240 -0
  453. package/src/styles/components/reagent-editor.css +510 -0
  454. package/src/styles/components/reagent-list.css +407 -0
  455. package/src/styles/components/resource-card.css +360 -0
  456. package/src/styles/components/sample-hierarchy-tree.css +314 -0
  457. package/src/styles/components/sample-legend.css +201 -0
  458. package/src/styles/components/sample-selector.css +751 -0
  459. package/src/styles/components/schedule-calendar.css +478 -0
  460. package/src/styles/components/scientific-number.css +63 -0
  461. package/src/styles/components/segmented-control.css +197 -0
  462. package/src/styles/components/select.css +77 -0
  463. package/src/styles/components/sequence-input.css +184 -0
  464. package/src/styles/components/settings-button.css +94 -0
  465. package/src/styles/components/settings-modal.css +95 -0
  466. package/src/styles/components/skeleton.css +49 -0
  467. package/src/styles/components/slider.css +74 -0
  468. package/src/styles/components/status-indicator.css +66 -0
  469. package/src/styles/components/step-wizard.css +192 -0
  470. package/src/styles/components/tabs.css +95 -0
  471. package/src/styles/components/tags-input.css +195 -0
  472. package/src/styles/components/textarea.css +82 -0
  473. package/src/styles/components/theme-toggle.css +69 -0
  474. package/src/styles/components/time-picker.css +171 -0
  475. package/src/styles/components/time-range-input.css +42 -0
  476. package/src/styles/components/toast.css +91 -0
  477. package/src/styles/components/toggle.css +146 -0
  478. package/src/styles/components/tooltip.css +91 -0
  479. package/src/styles/components/unit-input.css +123 -0
  480. package/src/styles/components/well-edit-popup.css +252 -0
  481. package/src/styles/components/well-plate.css +307 -0
  482. package/src/styles/index.css +87 -0
  483. package/src/styles/variables.css +1117 -0
  484. package/src/tailwind.preset.ts +61 -0
  485. package/src/types/auth.ts +55 -0
  486. package/src/types/auto-group.ts +40 -0
  487. package/src/types/components.ts +710 -0
  488. package/src/types/form-builder.ts +197 -0
  489. package/src/types/index.ts +207 -0
  490. package/src/types/platform.ts +116 -0
  491. package/src/utils/color.ts +96 -0
@@ -0,0 +1,1033 @@
1
+ <script setup lang="ts">
2
+ /** Interactive sample list with drag-and-drop group assignment, color labeling, and smart-group support. */
3
+ import { ref, computed } from 'vue'
4
+ import BaseButton from './BaseButton.vue'
5
+ import BaseInput from './BaseInput.vue'
6
+ import AutoGroupModal from './AutoGroupModal.vue'
7
+ import type { SampleGroup } from '../types'
8
+ import type { AutoGroupResult } from '../types/auto-group'
9
+ import { deriveShade } from '../utils/color'
10
+ import { DEFAULT_COLORS } from '../composables/useAutoGroup'
11
+
12
+ interface Props {
13
+ samples: string[]
14
+ modelValue: string[]
15
+ groups?: SampleGroup[]
16
+ enableGrouping?: boolean
17
+ enableSmartGroup?: boolean
18
+ experimentId?: number
19
+ designData?: Record<string, unknown>
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ groups: () => [],
24
+ enableGrouping: true,
25
+ enableSmartGroup: true,
26
+ experimentId: undefined,
27
+ designData: undefined,
28
+ })
29
+
30
+ const emit = defineEmits<{
31
+ 'update:modelValue': [samples: string[]]
32
+ 'update:groups': [groups: SampleGroup[]]
33
+ smartGroup: [result: AutoGroupResult]
34
+ }>()
35
+
36
+ // UI State
37
+ type ColorEdit =
38
+ | { kind: 'single'; name: string }
39
+ | { kind: 'family'; names: string[] }
40
+
41
+ const showSmartGroupModal = ref(false)
42
+ const newGroupName = ref('')
43
+ const editingColor = ref<ColorEdit | null>(null)
44
+ const colorPickerInput = ref<HTMLInputElement | null>(null)
45
+ const expandedGroups = ref<Record<string, boolean>>({})
46
+ const searchQuery = ref('')
47
+
48
+ // Sample Drag State
49
+ const draggingSample = ref<string | null>(null)
50
+ const dragSourceGroup = ref<string | null>(null)
51
+ const dragOverGroup = ref<string | null>(null)
52
+
53
+ // Group-Reorder Drag State
54
+ type GroupDragKind = 'major' | 'sub' | 'flat'
55
+ const draggingGroup = ref<string | null>(null)
56
+ const draggingGroupKind = ref<GroupDragKind | null>(null)
57
+ const reorderTarget = ref<string | null>(null)
58
+ const reorderPosition = ref<'before' | 'after' | null>(null)
59
+
60
+ // Computed: groups from props
61
+ const internalGroups = computed({
62
+ get: () => props.groups,
63
+ set: (value) => emit('update:groups', value),
64
+ })
65
+
66
+ interface DisplaySubGroup extends SampleGroup {
67
+ displayColor: string
68
+ // Pre-built `displayColor + alpha` strings hoisted out of the template so
69
+ // each render re-uses the same string reference instead of re-allocating.
70
+ displayBg: string
71
+ displayBorder: string
72
+ }
73
+
74
+ interface MajorGroup {
75
+ name: string
76
+ color: string
77
+ subGroups: DisplaySubGroup[]
78
+ allSamples: string[]
79
+ }
80
+
81
+ const hierarchicalGroups = computed<MajorGroup[]>(() => {
82
+ const groups = internalGroups.value
83
+ if (groups.length === 0) return []
84
+
85
+ // Detect separator: use '/' if any group name contains it, otherwise '_'
86
+ const separator = groups.some(g => g.name.includes('/')) ? '/' : '_'
87
+ const majorGroupMap: Record<string, SampleGroup[]> = {}
88
+
89
+ for (const group of groups) {
90
+ const parts = group.name.split(separator)
91
+ // Use first part as major group, or full name if no separator
92
+ const majorPrefix = parts.length > 1 ? parts[0] : group.name
93
+
94
+ if (!majorGroupMap[majorPrefix]) {
95
+ majorGroupMap[majorPrefix] = []
96
+ }
97
+ majorGroupMap[majorPrefix].push(group)
98
+ }
99
+
100
+ const result: MajorGroup[] = []
101
+
102
+ // Preserve insertion order from internalGroups so manual drag-reorder sticks.
103
+ for (const [majorName, subGroups] of Object.entries(majorGroupMap)) {
104
+ const allSamples = subGroups.flatMap(g => g.samples)
105
+ const color = subGroups[0]?.color || '#3B82F6'
106
+ const displaySubs: DisplaySubGroup[] = subGroups.map((sub, i) => {
107
+ const displayColor = deriveShade(color, i, subGroups.length)
108
+ return {
109
+ ...sub,
110
+ displayColor,
111
+ displayBg: displayColor + '20',
112
+ displayBorder: displayColor + '40',
113
+ }
114
+ })
115
+
116
+ result.push({ name: majorName, color, subGroups: displaySubs, allSamples })
117
+ }
118
+
119
+ return result
120
+ })
121
+
122
+ // Check if hierarchy is meaningful (major groups have multiple sub-groups)
123
+ // If each major group only has 1 sub-group with same name, show flat view instead
124
+ const showHierarchy = computed(() => {
125
+ const groups = hierarchicalGroups.value
126
+ if (groups.length === 0) return false
127
+
128
+ // Show hierarchy if any major group has multiple sub-groups
129
+ // OR if major group name differs from its sub-group name
130
+ return groups.some(major =>
131
+ major.subGroups.length > 1 ||
132
+ (major.subGroups.length === 1 && major.name !== major.subGroups[0].name)
133
+ )
134
+ })
135
+
136
+ const groupingEnabled = computed(() => internalGroups.value.length > 0)
137
+
138
+ const ungroupedSamples = computed(() => {
139
+ const groupedSamples = new Set(
140
+ internalGroups.value.flatMap(g => g.samples)
141
+ )
142
+ return props.samples.filter(s => !groupedSamples.has(s))
143
+ })
144
+
145
+ const filteredSamples = computed(() => {
146
+ if (!searchQuery.value.trim()) return props.samples
147
+ const query = searchQuery.value.toLowerCase()
148
+ return props.samples.filter(s => s.toLowerCase().includes(query))
149
+ })
150
+
151
+
152
+ // Selection state
153
+ const isAllSelected = computed(() =>
154
+ props.samples.length > 0 && props.modelValue.length === props.samples.length
155
+ )
156
+
157
+ // Toggle functions
158
+ function toggleSelectAll() {
159
+ if (isAllSelected.value) {
160
+ emit('update:modelValue', [])
161
+ } else {
162
+ emit('update:modelValue', [...props.samples])
163
+ }
164
+ }
165
+
166
+ function toggleSample(sample: string) {
167
+ const newSelection = props.modelValue.includes(sample)
168
+ ? props.modelValue.filter(s => s !== sample)
169
+ : [...props.modelValue, sample]
170
+ emit('update:modelValue', newSelection)
171
+ }
172
+
173
+ function toggleSamplesSelection(samples: string[]) {
174
+ const allSelected = samples.every(s => props.modelValue.includes(s))
175
+ const newSelection = allSelected
176
+ ? props.modelValue.filter(s => !samples.includes(s))
177
+ : [...props.modelValue, ...samples.filter(s => !props.modelValue.includes(s))]
178
+
179
+ emit('update:modelValue', newSelection)
180
+ }
181
+
182
+ function toggleGroupSamples(groupName: string) {
183
+ const group = internalGroups.value.find(g => g.name === groupName)
184
+ if (!group) return
185
+ toggleSamplesSelection(group.samples)
186
+ }
187
+
188
+ function toggleMajorGroupSamples(majorGroup: MajorGroup) {
189
+ toggleSamplesSelection(majorGroup.allSamples)
190
+ }
191
+
192
+ // Selection state checks
193
+ function isFullySelected(samples: string[]): boolean {
194
+ return samples.length > 0 && samples.every(s => props.modelValue.includes(s))
195
+ }
196
+
197
+ function isPartiallySelected(samples: string[]): boolean {
198
+ if (samples.length === 0) return false
199
+ const selectedCount = samples.filter(s => props.modelValue.includes(s)).length
200
+ return selectedCount > 0 && selectedCount < samples.length
201
+ }
202
+
203
+ function isGroupFullySelected(groupName: string): boolean {
204
+ const group = internalGroups.value.find(g => g.name === groupName)
205
+ return group ? isFullySelected(group.samples) : false
206
+ }
207
+
208
+ function isGroupPartiallySelected(groupName: string): boolean {
209
+ const group = internalGroups.value.find(g => g.name === groupName)
210
+ return group ? isPartiallySelected(group.samples) : false
211
+ }
212
+
213
+ function isMajorGroupFullySelected(majorGroup: MajorGroup): boolean {
214
+ return isFullySelected(majorGroup.allSamples)
215
+ }
216
+
217
+ function isMajorGroupPartiallySelected(majorGroup: MajorGroup): boolean {
218
+ return isPartiallySelected(majorGroup.allSamples)
219
+ }
220
+
221
+ // Expand/collapse
222
+ function toggleGroupExpanded(groupName: string) {
223
+ expandedGroups.value[groupName] = !expandedGroups.value[groupName]
224
+ }
225
+
226
+ function isGroupExpanded(groupName: string): boolean {
227
+ return !!expandedGroups.value[groupName]
228
+ }
229
+
230
+ function expandAllGroups() {
231
+ const expanded: Record<string, boolean> = {}
232
+ for (const major of hierarchicalGroups.value) {
233
+ expanded[`major:${major.name}`] = true
234
+ for (const sub of major.subGroups) {
235
+ expanded[sub.name] = true
236
+ }
237
+ }
238
+ expanded['__ungrouped__'] = true
239
+ expandedGroups.value = expanded
240
+ }
241
+
242
+ function collapseAllGroups() {
243
+ expandedGroups.value = {}
244
+ }
245
+
246
+ // Smart group
247
+ function handleSmartGroupApply(result: AutoGroupResult) {
248
+ emit('smartGroup', result)
249
+ emit('update:groups', result.groups)
250
+ }
251
+
252
+ // Group management
253
+ function clearGroups() {
254
+ internalGroups.value = []
255
+ }
256
+
257
+ function deleteMajorGroup(majorGroup: MajorGroup) {
258
+ internalGroups.value = internalGroups.value.filter(
259
+ g => !majorGroup.subGroups.some(sg => sg.name === g.name)
260
+ )
261
+ }
262
+
263
+ function deleteGroup(groupName: string) {
264
+ internalGroups.value = internalGroups.value.filter(g => g.name !== groupName)
265
+ }
266
+
267
+ function removeSampleFromGroup(sample: string, groupName: string) {
268
+ internalGroups.value = internalGroups.value.map(g =>
269
+ g.name === groupName
270
+ ? { ...g, samples: g.samples.filter((s: string) => s !== sample) }
271
+ : g
272
+ )
273
+ }
274
+
275
+ // Drag and Drop handlers
276
+ function handleDragStart(sample: string, sourceGroup: string | null, event: DragEvent) {
277
+ draggingSample.value = sample
278
+ dragSourceGroup.value = sourceGroup
279
+ if (event.dataTransfer) {
280
+ event.dataTransfer.effectAllowed = 'move'
281
+ event.dataTransfer.setData('text/plain', sample)
282
+ }
283
+ }
284
+
285
+ function resetDragState() {
286
+ draggingSample.value = null
287
+ dragSourceGroup.value = null
288
+ dragOverGroup.value = null
289
+ }
290
+
291
+ function handleDragEnd() {
292
+ resetDragState()
293
+ }
294
+
295
+ function handleDragOver(groupName: string, event: DragEvent) {
296
+ // Only accept this drop target when a sample is being dragged; group drags are handled separately.
297
+ if (!draggingSample.value) return
298
+ event.preventDefault()
299
+ if (event.dataTransfer) {
300
+ event.dataTransfer.dropEffect = 'move'
301
+ }
302
+ dragOverGroup.value = groupName
303
+ }
304
+
305
+ function handleDragLeave() {
306
+ dragOverGroup.value = null
307
+ }
308
+
309
+ function handleDrop(targetGroupName: string, event: DragEvent) {
310
+ if (!draggingSample.value) return
311
+ event.preventDefault()
312
+
313
+ const sample = draggingSample.value
314
+ const sourceGroup = dragSourceGroup.value
315
+
316
+ if (sourceGroup === targetGroupName) {
317
+ resetDragState()
318
+ return
319
+ }
320
+
321
+ internalGroups.value = internalGroups.value.map(g => {
322
+ if (sourceGroup && g.name === sourceGroup) {
323
+ return { ...g, samples: g.samples.filter((s: string) => s !== sample) }
324
+ }
325
+ if (g.name === targetGroupName && !g.samples.includes(sample)) {
326
+ return { ...g, samples: [...g.samples, sample] }
327
+ }
328
+ return g
329
+ })
330
+
331
+ resetDragState()
332
+ }
333
+
334
+ // Group reorder helpers
335
+ function detectSeparator(groups: SampleGroup[]): string {
336
+ return groups.some(g => g.name.includes('/')) ? '/' : '_'
337
+ }
338
+
339
+ function getMajorPrefix(groupName: string, separator: string): string {
340
+ const parts = groupName.split(separator)
341
+ return parts.length > 1 ? parts[0] : groupName
342
+ }
343
+
344
+ function handleGroupDragStart(name: string, kind: GroupDragKind, event: DragEvent) {
345
+ draggingGroup.value = name
346
+ draggingGroupKind.value = kind
347
+ if (event.dataTransfer) {
348
+ event.dataTransfer.effectAllowed = 'move'
349
+ event.dataTransfer.setData('text/plain', `mld-group:${name}`)
350
+ }
351
+ }
352
+
353
+ function handleGroupDragEnd() {
354
+ draggingGroup.value = null
355
+ draggingGroupKind.value = null
356
+ reorderTarget.value = null
357
+ reorderPosition.value = null
358
+ }
359
+
360
+ function handleGroupDragOver(name: string, kind: GroupDragKind, event: DragEvent) {
361
+ if (!draggingGroup.value || draggingGroupKind.value !== kind) return
362
+ if (draggingGroup.value === name) return
363
+
364
+ // Sub-group reorder is restricted to siblings under the same major prefix —
365
+ // crossing majors would silently rename the group, which is too magical.
366
+ if (kind === 'sub') {
367
+ const sep = detectSeparator(internalGroups.value)
368
+ if (getMajorPrefix(draggingGroup.value, sep) !== getMajorPrefix(name, sep)) return
369
+ }
370
+
371
+ event.preventDefault()
372
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
373
+
374
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
375
+ reorderTarget.value = name
376
+ reorderPosition.value = event.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
377
+ }
378
+
379
+ function handleGroupDragLeave(event: DragEvent) {
380
+ const cur = event.currentTarget as HTMLElement
381
+ const rel = event.relatedTarget as Node | null
382
+ if (rel && cur.contains(rel)) return
383
+ reorderTarget.value = null
384
+ reorderPosition.value = null
385
+ }
386
+
387
+ function handleGroupDrop(name: string, kind: GroupDragKind, event: DragEvent) {
388
+ if (!draggingGroup.value || draggingGroupKind.value !== kind) {
389
+ handleGroupDragEnd()
390
+ return
391
+ }
392
+ const position = reorderPosition.value
393
+ if (!position || draggingGroup.value === name) {
394
+ handleGroupDragEnd()
395
+ return
396
+ }
397
+
398
+ event.preventDefault()
399
+
400
+ if (kind === 'major') {
401
+ moveMajorGroup(draggingGroup.value, name, position)
402
+ } else {
403
+ moveGroup(draggingGroup.value, name, position)
404
+ }
405
+
406
+ handleGroupDragEnd()
407
+ }
408
+
409
+ function moveGroup(source: string, target: string, position: 'before' | 'after') {
410
+ if (source === target) return
411
+ const groups = [...internalGroups.value]
412
+ const srcIdx = groups.findIndex(g => g.name === source)
413
+ if (srcIdx === -1) return
414
+ const [item] = groups.splice(srcIdx, 1)
415
+ const targetIdx = groups.findIndex(g => g.name === target)
416
+ if (targetIdx === -1) return
417
+ groups.splice(position === 'before' ? targetIdx : targetIdx + 1, 0, item)
418
+ internalGroups.value = groups
419
+ }
420
+
421
+ function moveMajorGroup(source: string, target: string, position: 'before' | 'after') {
422
+ if (source === target) return
423
+ const groups = [...internalGroups.value]
424
+ const sep = detectSeparator(groups)
425
+
426
+ const isSource = (g: SampleGroup) => getMajorPrefix(g.name, sep) === source
427
+ const isTarget = (g: SampleGroup) => getMajorPrefix(g.name, sep) === target
428
+
429
+ const sourceGroups = groups.filter(isSource)
430
+ if (sourceGroups.length === 0) return
431
+ const remaining = groups.filter(g => !isSource(g))
432
+
433
+ let firstTarget = -1
434
+ let lastTarget = -1
435
+ remaining.forEach((g, i) => {
436
+ if (isTarget(g)) {
437
+ if (firstTarget === -1) firstTarget = i
438
+ lastTarget = i
439
+ }
440
+ })
441
+ if (firstTarget === -1) return
442
+
443
+ const insertIdx = position === 'before' ? firstTarget : lastTarget + 1
444
+ remaining.splice(insertIdx, 0, ...sourceGroups)
445
+ internalGroups.value = remaining
446
+ }
447
+
448
+ // Color picker
449
+ function openColorPicker(groupName: string, event: Event) {
450
+ event.stopPropagation()
451
+ editingColor.value = { kind: 'single', name: groupName }
452
+ colorPickerInput.value?.click()
453
+ }
454
+
455
+ function openMajorGroupColorPicker(majorGroup: MajorGroup, event: Event) {
456
+ event.stopPropagation()
457
+ editingColor.value = { kind: 'family', names: majorGroup.subGroups.map(sg => sg.name) }
458
+ colorPickerInput.value?.click()
459
+ }
460
+
461
+ function handleColorChange(event: Event) {
462
+ const edit = editingColor.value
463
+ if (!edit) return
464
+
465
+ const newColor = (event.target as HTMLInputElement).value
466
+ // For a family edit, every sub-group gets the new seed; the hierarchical
467
+ // computed re-derives shades from it.
468
+ const targets = edit.kind === 'family' ? new Set(edit.names) : new Set([edit.name])
469
+ internalGroups.value = internalGroups.value.map(g =>
470
+ targets.has(g.name) ? { ...g, color: newColor } : g,
471
+ )
472
+ editingColor.value = null
473
+ }
474
+
475
+ function getGroupColor(groupName: string): string {
476
+ const group = internalGroups.value.find(g => g.name === groupName)
477
+ return group?.color || '#3B82F6'
478
+ }
479
+
480
+ const colorPickerSeed = computed(() => {
481
+ const edit = editingColor.value
482
+ if (!edit) return '#3B82F6'
483
+ return getGroupColor(edit.kind === 'family' ? edit.names[0] : edit.name)
484
+ })
485
+
486
+ // New group
487
+ function addNewGroup() {
488
+ if (!newGroupName.value.trim()) return
489
+
490
+ const usedColors = new Set(internalGroups.value.map(g => g.color))
491
+ const availableColor = DEFAULT_COLORS.find(c => !usedColors.has(c)) || DEFAULT_COLORS[0]
492
+
493
+ const newGroup: SampleGroup = {
494
+ name: newGroupName.value.trim(),
495
+ color: availableColor,
496
+ samples: [],
497
+ }
498
+
499
+ internalGroups.value = [...internalGroups.value, newGroup]
500
+ newGroupName.value = ''
501
+ }
502
+
503
+ </script>
504
+
505
+ <template>
506
+ <div class="mld-sample-selector">
507
+ <!-- Select All Row -->
508
+ <label class="mld-sample-selector__select-all">
509
+ <input
510
+ type="checkbox"
511
+ :checked="isAllSelected"
512
+ @change="toggleSelectAll"
513
+ class="mld-sample-selector__checkbox"
514
+ />
515
+ <span class="mld-sample-selector__select-all-label">Select All</span>
516
+ <span class="mld-sample-selector__select-all-count">{{ samples.length }} samples</span>
517
+ </label>
518
+
519
+ <!-- Action Buttons Row -->
520
+ <div v-if="enableGrouping" class="mld-sample-selector__actions">
521
+ <div class="mld-sample-selector__actions-row">
522
+ <!-- Smart Group Button -->
523
+ <BaseButton
524
+ v-if="enableSmartGroup"
525
+ :variant="groupingEnabled ? 'primary' : 'secondary'"
526
+ size="sm"
527
+ :disabled="samples.length === 0"
528
+ class="mld-sample-selector__action-btn"
529
+ @click="showSmartGroupModal = true"
530
+ >
531
+ <svg class="mld-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
532
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
533
+ </svg>
534
+ <span>Smart Group</span>
535
+ </BaseButton>
536
+
537
+ <!-- Reset Button -->
538
+ <BaseButton
539
+ variant="ghost"
540
+ size="sm"
541
+ :disabled="internalGroups.length === 0"
542
+ class="mld-sample-selector__action-btn mld-sample-selector__action-btn--reset"
543
+ @click="clearGroups"
544
+ title="Clear all groups"
545
+ >
546
+ <svg class="mld-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
547
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
548
+ </svg>
549
+ </BaseButton>
550
+ </div>
551
+ </div>
552
+
553
+ <!-- Grouped View -->
554
+ <div v-if="groupingEnabled" class="mld-sample-selector__grouped">
555
+ <!-- Groups Header -->
556
+ <div class="mld-sample-selector__groups-header">
557
+ <span class="mld-sample-selector__groups-title">Groups ({{ internalGroups.length }})</span>
558
+ <div class="mld-sample-selector__groups-controls">
559
+ <button type="button" class="mld-sample-selector__expand-btn" @click="expandAllGroups" title="Expand all">
560
+ <svg class="mld-sample-selector__expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
561
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
562
+ </svg>
563
+ </button>
564
+ <button type="button" class="mld-sample-selector__expand-btn" @click="collapseAllGroups" title="Collapse all">
565
+ <svg class="mld-sample-selector__expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
566
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
567
+ </svg>
568
+ </button>
569
+ </div>
570
+ </div>
571
+
572
+ <!-- Hierarchical Tree -->
573
+ <div class="mld-sample-selector__tree">
574
+ <!-- Major Groups (when hierarchy is meaningful) -->
575
+ <template v-if="showHierarchy">
576
+ <div
577
+ v-for="majorGroup in hierarchicalGroups"
578
+ :key="majorGroup.name"
579
+ class="mld-sample-selector__major-group"
580
+ >
581
+ <!-- Major Group Header -->
582
+ <div
583
+ :class="[
584
+ 'mld-sample-selector__major-header',
585
+ draggingGroup === majorGroup.name && draggingGroupKind === 'major' ? 'mld-sample-selector__header--dragging' : '',
586
+ reorderTarget === majorGroup.name && draggingGroupKind === 'major' && reorderPosition === 'before' ? 'mld-sample-selector__header--drag-over-before' : '',
587
+ reorderTarget === majorGroup.name && draggingGroupKind === 'major' && reorderPosition === 'after' ? 'mld-sample-selector__header--drag-over-after' : '',
588
+ ]"
589
+ draggable="true"
590
+ @click="toggleGroupExpanded(`major:${majorGroup.name}`)"
591
+ @dragstart="handleGroupDragStart(majorGroup.name, 'major', $event)"
592
+ @dragend="handleGroupDragEnd"
593
+ @dragover="handleGroupDragOver(majorGroup.name, 'major', $event)"
594
+ @dragleave="handleGroupDragLeave($event)"
595
+ @drop="handleGroupDrop(majorGroup.name, 'major', $event)"
596
+ >
597
+ <svg
598
+ :class="[
599
+ 'mld-sample-selector__chevron',
600
+ isGroupExpanded(`major:${majorGroup.name}`) ? 'mld-sample-selector__chevron--open' : '',
601
+ ]"
602
+ fill="none" stroke="currentColor" viewBox="0 0 24 24"
603
+ >
604
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
605
+ </svg>
606
+
607
+ <input
608
+ type="checkbox"
609
+ :checked="isMajorGroupFullySelected(majorGroup)"
610
+ :indeterminate="isMajorGroupPartiallySelected(majorGroup)"
611
+ class="mld-sample-selector__checkbox"
612
+ :style="{ accentColor: majorGroup.color }"
613
+ @click.stop
614
+ @change="toggleMajorGroupSamples(majorGroup)"
615
+ />
616
+
617
+ <button
618
+ type="button"
619
+ class="mld-sample-selector__color-dot mld-sample-selector__color-dot--large mld-sample-selector__color-dot--clickable"
620
+ :style="{ backgroundColor: majorGroup.color }"
621
+ @click.stop="openMajorGroupColorPicker(majorGroup, $event)"
622
+ title="Click to change color family"
623
+ />
624
+
625
+ <span class="mld-sample-selector__major-name">{{ majorGroup.name }}</span>
626
+
627
+ <span
628
+ class="mld-sample-selector__count-badge"
629
+ :style="{ backgroundColor: majorGroup.color + '20', color: majorGroup.color }"
630
+ >
631
+ {{ majorGroup.allSamples.length }}
632
+ </span>
633
+
634
+ <button
635
+ type="button"
636
+ class="mld-sample-selector__delete-btn mld-sample-selector__delete-btn--hidden"
637
+ @click.stop="deleteMajorGroup(majorGroup)"
638
+ title="Remove major group"
639
+ >
640
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
641
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
642
+ </svg>
643
+ </button>
644
+ </div>
645
+
646
+ <!-- Sub Groups (collapsible) -->
647
+ <Transition name="mld-collapse">
648
+ <div
649
+ v-if="isGroupExpanded(`major:${majorGroup.name}`)"
650
+ class="mld-sample-selector__sub-groups"
651
+ :style="{ borderColor: majorGroup.color + '30' }"
652
+ >
653
+ <div
654
+ v-for="subGroup in majorGroup.subGroups"
655
+ :key="subGroup.name"
656
+ :class="[
657
+ 'mld-sample-selector__sub-group',
658
+ dragOverGroup === subGroup.name ? 'mld-sample-selector__sub-group--drag-over' : '',
659
+ ]"
660
+ @dragover="handleDragOver(subGroup.name, $event)"
661
+ @dragleave="handleDragLeave"
662
+ @drop="handleDrop(subGroup.name, $event)"
663
+ >
664
+ <!-- Sub Group Header -->
665
+ <div
666
+ :class="[
667
+ 'mld-sample-selector__sub-header',
668
+ draggingGroup === subGroup.name && draggingGroupKind === 'sub' ? 'mld-sample-selector__header--dragging' : '',
669
+ reorderTarget === subGroup.name && draggingGroupKind === 'sub' && reorderPosition === 'before' ? 'mld-sample-selector__header--drag-over-before' : '',
670
+ reorderTarget === subGroup.name && draggingGroupKind === 'sub' && reorderPosition === 'after' ? 'mld-sample-selector__header--drag-over-after' : '',
671
+ ]"
672
+ draggable="true"
673
+ @click="toggleGroupExpanded(subGroup.name)"
674
+ @dragstart="handleGroupDragStart(subGroup.name, 'sub', $event)"
675
+ @dragend="handleGroupDragEnd"
676
+ @dragover="handleGroupDragOver(subGroup.name, 'sub', $event)"
677
+ @dragleave="handleGroupDragLeave($event)"
678
+ @drop="handleGroupDrop(subGroup.name, 'sub', $event)"
679
+ >
680
+ <svg
681
+ :class="[
682
+ 'mld-sample-selector__chevron mld-sample-selector__chevron--small',
683
+ isGroupExpanded(subGroup.name) ? 'mld-sample-selector__chevron--open' : '',
684
+ ]"
685
+ fill="none" stroke="currentColor" viewBox="0 0 24 24"
686
+ >
687
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
688
+ </svg>
689
+
690
+ <input
691
+ type="checkbox"
692
+ :checked="isGroupFullySelected(subGroup.name)"
693
+ :indeterminate="isGroupPartiallySelected(subGroup.name)"
694
+ class="mld-sample-selector__checkbox mld-sample-selector__checkbox--small"
695
+ :style="{ accentColor: subGroup.displayColor }"
696
+ @click.stop
697
+ @change="toggleGroupSamples(subGroup.name)"
698
+ />
699
+
700
+ <div
701
+ class="mld-sample-selector__color-dot"
702
+ :style="{ backgroundColor: subGroup.displayColor }"
703
+ aria-hidden="true"
704
+ />
705
+
706
+ <span class="mld-sample-selector__sub-name">{{ subGroup.name }}</span>
707
+
708
+ <span
709
+ class="mld-sample-selector__count-badge mld-sample-selector__count-badge--small"
710
+ :style="{ backgroundColor: subGroup.displayBg, color: subGroup.displayColor }"
711
+ >
712
+ {{ subGroup.samples.length }}
713
+ </span>
714
+
715
+ <button
716
+ type="button"
717
+ class="mld-sample-selector__delete-btn mld-sample-selector__delete-btn--hidden"
718
+ @click.stop="deleteGroup(subGroup.name)"
719
+ title="Remove sub group"
720
+ >
721
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
722
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
723
+ </svg>
724
+ </button>
725
+ </div>
726
+
727
+ <!-- Samples (collapsible) -->
728
+ <Transition name="mld-collapse">
729
+ <div
730
+ v-if="isGroupExpanded(subGroup.name)"
731
+ class="mld-sample-selector__samples"
732
+ :style="{ borderColor: subGroup.displayBorder }"
733
+ >
734
+ <div
735
+ v-for="sample in subGroup.samples"
736
+ :key="sample"
737
+ :class="[
738
+ 'mld-sample-selector__sample',
739
+ draggingSample === sample ? 'mld-sample-selector__sample--dragging' : '',
740
+ ]"
741
+ draggable="true"
742
+ @dragstart="handleDragStart(sample, subGroup.name, $event)"
743
+ @dragend="handleDragEnd"
744
+ >
745
+ <svg class="mld-sample-selector__drag-handle" fill="none" stroke="currentColor" viewBox="0 0 24 24">
746
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
747
+ </svg>
748
+ <input
749
+ type="checkbox"
750
+ :checked="modelValue.includes(sample)"
751
+ class="mld-sample-selector__checkbox mld-sample-selector__checkbox--tiny"
752
+ :style="{ accentColor: subGroup.displayColor }"
753
+ @change="toggleSample(sample)"
754
+ />
755
+ <span class="mld-sample-selector__sample-name">{{ sample }}</span>
756
+ <button
757
+ type="button"
758
+ class="mld-sample-selector__remove-btn"
759
+ @click="removeSampleFromGroup(sample, subGroup.name)"
760
+ title="Remove from group"
761
+ >
762
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
763
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
764
+ </svg>
765
+ </button>
766
+ </div>
767
+ </div>
768
+ </Transition>
769
+ </div>
770
+ </div>
771
+ </Transition>
772
+ </div>
773
+ </template>
774
+
775
+ <!-- Flat Groups (when no meaningful hierarchy - e.g., level 1) -->
776
+ <template v-else>
777
+ <div
778
+ v-for="group in internalGroups"
779
+ :key="group.name"
780
+ :class="[
781
+ 'mld-sample-selector__sub-group',
782
+ dragOverGroup === group.name ? 'mld-sample-selector__sub-group--drag-over' : '',
783
+ ]"
784
+ @dragover="handleDragOver(group.name, $event)"
785
+ @dragleave="handleDragLeave"
786
+ @drop="handleDrop(group.name, $event)"
787
+ >
788
+ <!-- Group Header -->
789
+ <div
790
+ :class="[
791
+ 'mld-sample-selector__major-header',
792
+ draggingGroup === group.name && draggingGroupKind === 'flat' ? 'mld-sample-selector__header--dragging' : '',
793
+ reorderTarget === group.name && draggingGroupKind === 'flat' && reorderPosition === 'before' ? 'mld-sample-selector__header--drag-over-before' : '',
794
+ reorderTarget === group.name && draggingGroupKind === 'flat' && reorderPosition === 'after' ? 'mld-sample-selector__header--drag-over-after' : '',
795
+ ]"
796
+ draggable="true"
797
+ @click="toggleGroupExpanded(group.name)"
798
+ @dragstart="handleGroupDragStart(group.name, 'flat', $event)"
799
+ @dragend="handleGroupDragEnd"
800
+ @dragover="handleGroupDragOver(group.name, 'flat', $event)"
801
+ @dragleave="handleGroupDragLeave($event)"
802
+ @drop="handleGroupDrop(group.name, 'flat', $event)"
803
+ >
804
+ <svg
805
+ :class="[
806
+ 'mld-sample-selector__chevron',
807
+ isGroupExpanded(group.name) ? 'mld-sample-selector__chevron--open' : '',
808
+ ]"
809
+ fill="none" stroke="currentColor" viewBox="0 0 24 24"
810
+ >
811
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
812
+ </svg>
813
+
814
+ <input
815
+ type="checkbox"
816
+ :checked="isGroupFullySelected(group.name)"
817
+ :indeterminate="isGroupPartiallySelected(group.name)"
818
+ class="mld-sample-selector__checkbox"
819
+ :style="{ accentColor: group.color }"
820
+ @click.stop
821
+ @change="toggleGroupSamples(group.name)"
822
+ />
823
+
824
+ <button
825
+ type="button"
826
+ class="mld-sample-selector__color-dot mld-sample-selector__color-dot--large mld-sample-selector__color-dot--clickable"
827
+ :style="{ backgroundColor: group.color }"
828
+ @click.stop="openColorPicker(group.name, $event)"
829
+ title="Click to change color"
830
+ />
831
+
832
+ <span class="mld-sample-selector__major-name">{{ group.name }}</span>
833
+
834
+ <span
835
+ class="mld-sample-selector__count-badge"
836
+ :style="{ backgroundColor: group.color + '20', color: group.color }"
837
+ >
838
+ {{ group.samples.length }}
839
+ </span>
840
+
841
+ <button
842
+ type="button"
843
+ class="mld-sample-selector__delete-btn mld-sample-selector__delete-btn--hidden"
844
+ @click.stop="deleteGroup(group.name)"
845
+ title="Remove group"
846
+ >
847
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
848
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
849
+ </svg>
850
+ </button>
851
+ </div>
852
+
853
+ <!-- Samples (collapsible) -->
854
+ <Transition name="mld-collapse">
855
+ <div
856
+ v-if="isGroupExpanded(group.name)"
857
+ class="mld-sample-selector__samples"
858
+ :style="{ borderColor: group.color + '40' }"
859
+ >
860
+ <div
861
+ v-for="sample in group.samples"
862
+ :key="sample"
863
+ :class="[
864
+ 'mld-sample-selector__sample',
865
+ draggingSample === sample ? 'mld-sample-selector__sample--dragging' : '',
866
+ ]"
867
+ draggable="true"
868
+ @dragstart="handleDragStart(sample, group.name, $event)"
869
+ @dragend="handleDragEnd"
870
+ >
871
+ <svg class="mld-sample-selector__drag-handle" fill="none" stroke="currentColor" viewBox="0 0 24 24">
872
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
873
+ </svg>
874
+ <input
875
+ type="checkbox"
876
+ :checked="modelValue.includes(sample)"
877
+ class="mld-sample-selector__checkbox mld-sample-selector__checkbox--tiny"
878
+ :style="{ accentColor: group.color }"
879
+ @change="toggleSample(sample)"
880
+ />
881
+ <span class="mld-sample-selector__sample-name">{{ sample }}</span>
882
+ <button
883
+ type="button"
884
+ class="mld-sample-selector__remove-btn"
885
+ @click="removeSampleFromGroup(sample, group.name)"
886
+ title="Remove from group"
887
+ >
888
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
889
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
890
+ </svg>
891
+ </button>
892
+ </div>
893
+ </div>
894
+ </Transition>
895
+ </div>
896
+ </template>
897
+
898
+ <!-- Empty state -->
899
+ <div v-if="internalGroups.length === 0" class="mld-sample-selector__empty">
900
+ Click Smart Group to auto-group samples
901
+ </div>
902
+ </div>
903
+
904
+ <!-- Ungrouped Samples Section -->
905
+ <div v-if="ungroupedSamples.length > 0" class="mld-sample-selector__ungrouped">
906
+ <div
907
+ class="mld-sample-selector__ungrouped-header"
908
+ @click="toggleGroupExpanded('__ungrouped__')"
909
+ >
910
+ <svg
911
+ :class="[
912
+ 'mld-sample-selector__chevron',
913
+ isGroupExpanded('__ungrouped__') ? 'mld-sample-selector__chevron--open' : '',
914
+ ]"
915
+ fill="none" stroke="currentColor" viewBox="0 0 24 24"
916
+ >
917
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
918
+ </svg>
919
+ <span class="mld-sample-selector__ungrouped-label">Ungrouped</span>
920
+ <span class="mld-sample-selector__ungrouped-count">{{ ungroupedSamples.length }}</span>
921
+ </div>
922
+
923
+ <Transition name="mld-collapse">
924
+ <div
925
+ v-if="isGroupExpanded('__ungrouped__')"
926
+ class="mld-sample-selector__ungrouped-list"
927
+ >
928
+ <div
929
+ v-for="sample in ungroupedSamples"
930
+ :key="sample"
931
+ :class="[
932
+ 'mld-sample-selector__sample',
933
+ draggingSample === sample ? 'mld-sample-selector__sample--dragging' : '',
934
+ ]"
935
+ draggable="true"
936
+ @dragstart="handleDragStart(sample, null, $event)"
937
+ @dragend="handleDragEnd"
938
+ >
939
+ <svg class="mld-sample-selector__drag-handle" fill="none" stroke="currentColor" viewBox="0 0 24 24">
940
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
941
+ </svg>
942
+ <input
943
+ type="checkbox"
944
+ :checked="modelValue.includes(sample)"
945
+ class="mld-sample-selector__checkbox mld-sample-selector__checkbox--small"
946
+ @change="toggleSample(sample)"
947
+ />
948
+ <span class="mld-sample-selector__sample-name">{{ sample }}</span>
949
+ </div>
950
+ </div>
951
+ </Transition>
952
+ </div>
953
+
954
+ <!-- New Group Input -->
955
+ <div class="mld-sample-selector__new-group">
956
+ <BaseInput
957
+ v-model="newGroupName"
958
+ placeholder="New group name..."
959
+ class="mld-sample-selector__new-group-input"
960
+ @keyup.enter="addNewGroup"
961
+ />
962
+ <BaseButton
963
+ variant="ghost"
964
+ size="sm"
965
+ :disabled="!newGroupName.trim()"
966
+ class="mld-sample-selector__new-group-btn"
967
+ @click="addNewGroup"
968
+ >
969
+ Add
970
+ </BaseButton>
971
+ </div>
972
+
973
+ <!-- Hidden color picker input -->
974
+ <input
975
+ ref="colorPickerInput"
976
+ type="color"
977
+ class="mld-sample-selector__color-input"
978
+ :value="colorPickerSeed"
979
+ @change="handleColorChange"
980
+ />
981
+ </div>
982
+
983
+ <!-- Flat View (when no groups) -->
984
+ <div v-if="!groupingEnabled" class="mld-sample-selector__flat">
985
+ <!-- Search -->
986
+ <div class="mld-sample-selector__search">
987
+ <svg class="mld-sample-selector__search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
988
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
989
+ </svg>
990
+ <input
991
+ v-model="searchQuery"
992
+ type="text"
993
+ placeholder="Search samples..."
994
+ class="mld-sample-selector__search-input"
995
+ />
996
+ </div>
997
+
998
+ <!-- Flat samples list -->
999
+ <div class="mld-sample-selector__flat-list">
1000
+ <div
1001
+ v-for="sample in filteredSamples"
1002
+ :key="sample"
1003
+ class="mld-sample-selector__flat-item"
1004
+ >
1005
+ <input
1006
+ type="checkbox"
1007
+ :checked="modelValue.includes(sample)"
1008
+ class="mld-sample-selector__checkbox"
1009
+ @change="toggleSample(sample)"
1010
+ />
1011
+ <span class="mld-sample-selector__flat-name">{{ sample }}</span>
1012
+ </div>
1013
+
1014
+ <div v-if="filteredSamples.length === 0 && searchQuery.trim()" class="mld-sample-selector__empty">
1015
+ No samples match "{{ searchQuery }}"
1016
+ </div>
1017
+ </div>
1018
+ </div>
1019
+
1020
+ <!-- Smart Grouping Modal -->
1021
+ <AutoGroupModal
1022
+ v-model="showSmartGroupModal"
1023
+ :samples="samples"
1024
+ :experiment-id="experimentId"
1025
+ :design-data="designData"
1026
+ @apply="handleSmartGroupApply"
1027
+ />
1028
+ </div>
1029
+ </template>
1030
+
1031
+ <style>
1032
+ @import '../styles/components/sample-selector.css';
1033
+ </style>