@morscherlab/mint-sdk 1.0.15 → 1.0.16

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 (56) hide show
  1. package/dist/{BaseSelect-DksaKYq_.js → BaseSelect-ekgr9fDo.js} +4 -1
  2. package/dist/BaseSelect-ekgr9fDo.js.map +1 -0
  3. package/dist/{ExperimentSelectorModal-DIFyL5ta.js → ExperimentSelectorModal-BOzDs8TU.js} +2 -2
  4. package/dist/{ExperimentSelectorModal-CHsU-LIh.js → ExperimentSelectorModal-CX0oBzpV.js} +2 -2
  5. package/dist/{ExperimentSelectorModal-CHsU-LIh.js.map → ExperimentSelectorModal-CX0oBzpV.js.map} +1 -1
  6. package/dist/{SettingsModal-LEKI6Ebl.js → SettingsModal-BTyXD0uP.js} +3 -3
  7. package/dist/{SettingsModal-LEKI6Ebl.js.map → SettingsModal-BTyXD0uP.js.map} +1 -1
  8. package/dist/SettingsModal-DXcSKk9D.js +5 -0
  9. package/dist/__tests__/components/MobileSupportGate.test.d.ts +1 -0
  10. package/dist/__tests__/composables/useMobileSupportGate.test.d.ts +1 -0
  11. package/dist/components/AutoGroupModal.vue.d.ts +6 -0
  12. package/dist/components/BaseInput.vue.d.ts +1 -0
  13. package/dist/components/MobileSupportGate.vue.d.ts +40 -0
  14. package/dist/components/SampleSelector.colors.d.ts +2 -1
  15. package/dist/components/index.d.ts +1 -0
  16. package/dist/components/index.js +6 -6
  17. package/dist/{components-Cyk8QEyL.js → components-D0CzE0XK.js} +1061 -473
  18. package/dist/components-D0CzE0XK.js.map +1 -0
  19. package/dist/composables/index.d.ts +1 -0
  20. package/dist/composables/index.js +7 -7
  21. package/dist/composables/useMobileSupportGate.d.ts +14 -0
  22. package/dist/{composables-D9mexHSW.js → composables-Da-4XOe2.js} +3 -3
  23. package/dist/{composables-D9mexHSW.js.map → composables-Da-4XOe2.js.map} +1 -1
  24. package/dist/index.js +10 -10
  25. package/dist/install.js +5 -5
  26. package/dist/styles.css +3705 -2286
  27. package/dist/templates/index.js +3 -3
  28. package/dist/{templates-Do43ZIMb.js → templates-Dnf8UNxg.js} +2 -2
  29. package/dist/{templates-Do43ZIMb.js.map → templates-Dnf8UNxg.js.map} +1 -1
  30. package/dist/{useControlSchema-0n8Bcftq.js → useControlSchema-Dkm-W_lg.js} +2 -2
  31. package/dist/{useControlSchema-0n8Bcftq.js.map → useControlSchema-Dkm-W_lg.js.map} +1 -1
  32. package/dist/{useFormBuilder-COfYWDuC.js → useFormBuilder-BOJ52N4M.js} +2 -2
  33. package/dist/{useFormBuilder-COfYWDuC.js.map → useFormBuilder-BOJ52N4M.js.map} +1 -1
  34. package/dist/{useProtocolTemplates-DODHlhxr.js → useProtocolTemplates-r2GOnnH1.js} +55 -5
  35. package/dist/useProtocolTemplates-r2GOnnH1.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/__tests__/components/MobileSupportGate.test.ts +120 -0
  38. package/src/__tests__/components/SampleSelector.test.ts +119 -0
  39. package/src/__tests__/composables/useMobileSupportGate.test.ts +74 -0
  40. package/src/components/AutoGroupModal.story.vue +46 -0
  41. package/src/components/AutoGroupModal.vue +578 -2
  42. package/src/components/BaseInput.vue +2 -0
  43. package/src/components/MobileSupportGate.story.vue +52 -0
  44. package/src/components/MobileSupportGate.vue +115 -0
  45. package/src/components/SampleSelector.colors.ts +7 -2
  46. package/src/components/SampleSelector.story.vue +34 -0
  47. package/src/components/SampleSelector.vue +22 -1
  48. package/src/components/index.ts +1 -0
  49. package/src/composables/index.ts +8 -0
  50. package/src/composables/useMobileSupportGate.ts +80 -0
  51. package/src/styles/components/auto-group-modal.css +744 -0
  52. package/src/styles/components/mobile-support-gate.css +119 -0
  53. package/dist/BaseSelect-DksaKYq_.js.map +0 -1
  54. package/dist/SettingsModal-L7Ejny45.js +0 -5
  55. package/dist/components-Cyk8QEyL.js.map +0 -1
  56. package/dist/useProtocolTemplates-DODHlhxr.js.map +0 -1
@@ -10,12 +10,23 @@ import AlertBox from './AlertBox.vue'
10
10
  import { useAutoGroup, parseCSV } from '../composables/useAutoGroup'
11
11
  import { classKey } from '../composables/autoGroup'
12
12
  import { useApi } from '../composables/useApi'
13
+ import { useTextSearch } from '../composables/useTextSearch'
14
+ import { useSampleGroups } from '../composables/useSampleGroups'
15
+ import {
16
+ createSampleGroup,
17
+ SAMPLE_GROUP_COLOR_OPTIONS,
18
+ } from './SampleSelector.colors'
13
19
  import type { WizardStep } from '../types/components'
14
20
  import type { AutoGroupResult, MergeSuggestion, ColumnRole } from '../types/auto-group'
21
+ import type { SampleGroup } from '../types'
22
+
23
+ type GroupingWorkflow = 'auto' | 'manual'
15
24
 
16
25
  export interface Props {
17
26
  modelValue: boolean
18
27
  samples?: string[]
28
+ groups?: SampleGroup[]
29
+ initialMode?: GroupingWorkflow
19
30
  experimentId?: number
20
31
  /** Pre-fetched design data — bypasses API fetch when provided */
21
32
  designData?: Record<string, unknown>
@@ -23,6 +34,8 @@ export interface Props {
23
34
 
24
35
  const props = withDefaults(defineProps<Props>(), {
25
36
  samples: () => [],
37
+ groups: () => [],
38
+ initialMode: 'auto',
26
39
  experimentId: undefined,
27
40
  designData: undefined,
28
41
  })
@@ -41,6 +54,15 @@ const csvFileName = ref('')
41
54
  const experimentLoading = ref(false)
42
55
  const experimentError = ref<string | null>(null)
43
56
  const csvError = ref<string | null>(null)
57
+ const activeWorkflow = ref<GroupingWorkflow>('auto')
58
+ const manualDraftGroups = ref<SampleGroup[]>([])
59
+ const manualSearchQuery = ref('')
60
+ const manualUngroupedOnly = ref(true)
61
+ const manualSelectedSamples = ref<string[]>([])
62
+ const manualGroupName = ref('')
63
+ const manualSubGroupName = ref('')
64
+ const manualColorOptions = SAMPLE_GROUP_COLOR_OPTIONS
65
+ const manualSelectedColor = ref(manualColorOptions[0])
44
66
 
45
67
  // Workspace step refs
46
68
  const openPopoverIdx = ref<number | null>(null)
@@ -91,6 +113,194 @@ const totalQc = computed(() =>
91
113
  (autoGroup.qcGroups.value ?? []).reduce((acc, g) => acc + g.samples.length, 0),
92
114
  )
93
115
 
116
+ function cloneGroups(groups: SampleGroup[]): SampleGroup[] {
117
+ return groups.map(group => ({
118
+ ...group,
119
+ samples: [...group.samples],
120
+ }))
121
+ }
122
+
123
+ function manualResetSelection() {
124
+ manualSearchQuery.value = ''
125
+ manualUngroupedOnly.value = true
126
+ manualSelectedSamples.value = []
127
+ manualGroupName.value = ''
128
+ manualSubGroupName.value = ''
129
+ manualSelectedColor.value = manualColorOptions[0]
130
+ }
131
+
132
+ function manualToggleSample(sample: string) {
133
+ if (manualSelectedSet.value.has(sample)) {
134
+ manualSelectedSamples.value = manualSelectedSamples.value.filter(item => item !== sample)
135
+ } else {
136
+ manualSelectedSamples.value = [...manualSelectedSamples.value, sample]
137
+ }
138
+ }
139
+
140
+ function manualSelectVisibleSamples() {
141
+ const next = new Set(manualSelectedSamples.value)
142
+ for (const sample of manualFilteredSamples.value) {
143
+ next.add(sample)
144
+ }
145
+ manualSelectedSamples.value = [...next]
146
+ }
147
+
148
+ function manualClearSamples() {
149
+ manualSelectedSamples.value = []
150
+ }
151
+
152
+ function manualToggleVisibleSamples() {
153
+ if (manualAllVisibleSelected.value) {
154
+ manualSelectedSamples.value = manualSelectedSamples.value
155
+ .filter(sample => !manualVisibleSet.value.has(sample))
156
+ return
157
+ }
158
+
159
+ manualSelectVisibleSamples()
160
+ }
161
+
162
+ function manualGetSampleGroup(sample: string): SampleGroup | undefined {
163
+ return manualDraftGroups.value.find(group => group.samples.includes(sample))
164
+ }
165
+
166
+ function manualGetSampleStatus(sample: string): string {
167
+ return manualGetSampleGroup(sample)?.name.replace('/', ' · ') || 'unassigned'
168
+ }
169
+
170
+ function manualGetGroupPrimaryName(name: string): string {
171
+ return name.split('/')[0]
172
+ }
173
+
174
+ function manualGetGroupSecondaryName(name: string): string | null {
175
+ const separatorIndex = name.indexOf('/')
176
+ return separatorIndex === -1 ? null : name.slice(separatorIndex + 1)
177
+ }
178
+
179
+ function manualChooseColor(color: string) {
180
+ manualSelectedColor.value = color
181
+ }
182
+
183
+ function manualUseExistingGroupAsTarget(name: string) {
184
+ const existingGroup = manualDraftGroups.value.find(group => group.name === name)
185
+ if (existingGroup) {
186
+ manualSelectedColor.value = existingGroup.color
187
+ }
188
+
189
+ const separatorIndex = name.indexOf('/')
190
+ if (separatorIndex === -1) {
191
+ manualGroupName.value = name
192
+ manualSubGroupName.value = ''
193
+ return
194
+ }
195
+
196
+ manualGroupName.value = name.slice(0, separatorIndex)
197
+ manualSubGroupName.value = name.slice(separatorIndex + 1)
198
+ }
199
+
200
+ function manualAssignSamplesToGroup() {
201
+ const target = manualTargetGroupName.value
202
+ if (!manualCanAssign.value || !target) return
203
+
204
+ const selected = manualAssignableSamples.value
205
+ const selectedSet = new Set(selected)
206
+ if (selectedSet.size === 0) return
207
+
208
+ let nextGroups = manualDraftGroups.value.map(group => ({
209
+ ...group,
210
+ samples: group.samples.filter(sample => !selectedSet.has(sample)),
211
+ }))
212
+
213
+ const targetIndex = nextGroups.findIndex(group => group.name === target)
214
+ if (targetIndex === -1) {
215
+ const newGroup = createSampleGroup(target, nextGroups, manualSelectedColor.value)
216
+ if (!newGroup) return
217
+ nextGroups = [...nextGroups, { ...newGroup, samples: selected }]
218
+ } else {
219
+ const group = nextGroups[targetIndex]
220
+ nextGroups[targetIndex] = {
221
+ ...group,
222
+ color: manualSelectedColor.value,
223
+ samples: [...group.samples, ...selected.filter(sample => !group.samples.includes(sample))],
224
+ }
225
+ }
226
+
227
+ manualDraftGroups.value = nextGroups
228
+ manualSelectedSamples.value = []
229
+ }
230
+
231
+ const manualSamples = computed(() =>
232
+ props.samples.length > 0 ? props.samples : autoGroup.samples.value,
233
+ )
234
+
235
+ const manualGroupsModel = computed({
236
+ get: () => manualDraftGroups.value,
237
+ set: (value: SampleGroup[]) => {
238
+ manualDraftGroups.value = value
239
+ },
240
+ })
241
+
242
+ const manualSampleGroups = useSampleGroups({
243
+ samples: () => manualSamples.value,
244
+ groups: manualGroupsModel,
245
+ })
246
+
247
+ const manualHierarchicalGroups = manualSampleGroups.hierarchicalGroups
248
+ const manualShowHierarchy = manualSampleGroups.showHierarchy
249
+ const manualUngroupedSamples = manualSampleGroups.ungroupedSamples
250
+ const manualSamplePool = computed(() =>
251
+ manualUngroupedOnly.value ? manualUngroupedSamples.value : manualSamples.value,
252
+ )
253
+ const manualSampleSearch = useTextSearch({
254
+ items: () => manualSamplePool.value,
255
+ query: manualSearchQuery,
256
+ getText: sample => sample,
257
+ })
258
+ const manualFilteredSamples = manualSampleSearch.filteredItems
259
+ const manualSelectedSet = computed(() => new Set(manualSelectedSamples.value))
260
+ const manualVisibleSet = computed(() => new Set(manualFilteredSamples.value))
261
+ const manualAllVisibleSelected = computed(() =>
262
+ manualFilteredSamples.value.length > 0
263
+ && manualFilteredSamples.value.every(sample => manualSelectedSet.value.has(sample)),
264
+ )
265
+ const manualSomeVisibleSelected = computed(() =>
266
+ manualFilteredSamples.value.some(sample => manualSelectedSet.value.has(sample))
267
+ && !manualAllVisibleSelected.value,
268
+ )
269
+ const manualAssignableSamples = computed(() =>
270
+ manualSelectedSamples.value.filter(sample => manualSamples.value.includes(sample)),
271
+ )
272
+ const manualTargetGroupName = computed(() => {
273
+ const major = manualGroupName.value.trim()
274
+ const sub = manualSubGroupName.value.trim()
275
+ if (!major) return ''
276
+ return sub ? `${major}/${sub}` : major
277
+ })
278
+ const manualCanAssign = computed(() =>
279
+ manualAssignableSamples.value.length > 0 && manualTargetGroupName.value.length > 0,
280
+ )
281
+ const manualGroupedSamples = computed(() => {
282
+ const validSamples = new Set(manualSamples.value)
283
+ return new Set(
284
+ manualDraftGroups.value
285
+ .flatMap(group => group.samples)
286
+ .filter(sample => validSamples.has(sample)),
287
+ )
288
+ })
289
+ const manualGroupedCount = computed(() => manualGroupedSamples.value.size)
290
+ const manualUngroupedCount = computed(() =>
291
+ Math.max(manualSamples.value.length - manualGroupedCount.value, 0),
292
+ )
293
+ const manualResult = computed<AutoGroupResult>(() => {
294
+ const groups = cloneGroups(manualDraftGroups.value)
295
+ return {
296
+ groups,
297
+ experimentalGroups: groups,
298
+ qcGroups: [],
299
+ metadata: [],
300
+ excludedSamples: [],
301
+ }
302
+ })
303
+
94
304
  function acceptSuggestion(s: MergeSuggestion) {
95
305
  autoGroup.mergeColumns(autoGroup.activeClassKey.value, s.columnIndices)
96
306
  }
@@ -200,6 +410,9 @@ function mergeWithNext() {
200
410
  // experiment) where users paste their own sample list or upload a CSV.
201
411
  watch(() => props.modelValue, (open) => {
202
412
  if (open) {
413
+ activeWorkflow.value = props.initialMode
414
+ manualDraftGroups.value = cloneGroups(props.groups)
415
+ manualResetSelection()
203
416
  autoGroup.reset()
204
417
  currentStep.value = 0
205
418
  csvFileName.value = ''
@@ -221,6 +434,17 @@ watch(() => props.modelValue, (open) => {
221
434
  }
222
435
  }, { immediate: true })
223
436
 
437
+ watch(manualSamples, (samples) => {
438
+ manualSelectedSamples.value = manualSelectedSamples.value.filter(sample => samples.includes(sample))
439
+ })
440
+
441
+ watch(manualTargetGroupName, (target) => {
442
+ if (!target) return
443
+ const existingGroup = manualDraftGroups.value.find(group => group.name === target)
444
+ if (!existingGroup) return
445
+ manualSelectedColor.value = existingGroup.color
446
+ })
447
+
224
448
  // Wizard steps: three steps, unconditional
225
449
  const allSteps: WizardStep[] = [
226
450
  { id: 'input', label: 'Input' },
@@ -271,6 +495,11 @@ function handleApply() {
271
495
  emit('update:modelValue', false)
272
496
  }
273
497
 
498
+ function handleManualApply() {
499
+ emit('apply', manualResult.value)
500
+ emit('update:modelValue', false)
501
+ }
502
+
274
503
  function handleCancel() {
275
504
  emit('update:modelValue', false)
276
505
  }
@@ -380,15 +609,62 @@ const isFirstStep = computed(() => currentStep.value === 0)
380
609
  <template>
381
610
  <BaseModal
382
611
  :model-value="modelValue"
383
- title="Smart Group"
384
- size="lg"
612
+ title="Group Samples"
613
+ :size="activeWorkflow === 'manual' ? 'xl' : 'lg'"
385
614
  :close-on-overlay="false"
386
615
  :close-on-escape="false"
387
616
  @update:model-value="emit('update:modelValue', $event)"
388
617
  @close="handleCancel"
389
618
  >
390
619
  <div class="mint-auto-group">
620
+ <div
621
+ :class="[
622
+ 'mint-auto-group__workflow-bar',
623
+ activeWorkflow === 'manual' ? 'mint-auto-group__workflow-bar--manual' : '',
624
+ ]"
625
+ >
626
+ <div class="mint-auto-group__workflow-tabs" role="tablist" aria-label="Grouping workflow">
627
+ <button
628
+ type="button"
629
+ :class="[
630
+ 'mint-auto-group__workflow-tab',
631
+ activeWorkflow === 'auto' ? 'mint-auto-group__workflow-tab--active' : '',
632
+ ]"
633
+ role="tab"
634
+ :aria-selected="activeWorkflow === 'auto'"
635
+ @click="activeWorkflow = 'auto'"
636
+ >
637
+ Auto
638
+ </button>
639
+ <button
640
+ type="button"
641
+ :class="[
642
+ 'mint-auto-group__workflow-tab',
643
+ activeWorkflow === 'manual' ? 'mint-auto-group__workflow-tab--active' : '',
644
+ ]"
645
+ role="tab"
646
+ :aria-selected="activeWorkflow === 'manual'"
647
+ @click="activeWorkflow = 'manual'"
648
+ >
649
+ Manual
650
+ </button>
651
+ </div>
652
+
653
+ <div v-if="activeWorkflow === 'manual'" class="mint-auto-group__manual-summary">
654
+ <div class="mint-auto-group__manual-summary-text">
655
+ <span class="mint-auto-group__manual-eyebrow">Manual workflow</span>
656
+ <strong>Assign selected samples to cohorts</strong>
657
+ </div>
658
+ <div class="mint-auto-group__manual-stats" aria-label="Manual grouping status">
659
+ <span><strong>{{ manualGroupedCount }}</strong> grouped</span>
660
+ <span><strong>{{ manualUngroupedCount }}</strong> unassigned</span>
661
+ <span><strong>{{ manualDraftGroups.length }}</strong> cohorts</span>
662
+ </div>
663
+ </div>
664
+ </div>
665
+
391
666
  <StepWizard
667
+ v-if="activeWorkflow === 'auto'"
392
668
  ref="wizardRef"
393
669
  v-model="currentStep"
394
670
  :steps="dynamicSteps"
@@ -840,7 +1116,307 @@ const isFirstStep = computed(() => currentStep.value === 0)
840
1116
  </div>
841
1117
  </template>
842
1118
  </StepWizard>
1119
+
1120
+ <div v-else class="mint-auto-group__manual-mode">
1121
+ <div class="mint-auto-group__manual-grid">
1122
+ <section class="mint-auto-group__manual-panel" aria-label="Manual sample selection">
1123
+ <div class="mint-auto-group__manual-panel-head">
1124
+ <div>
1125
+ <span class="mint-auto-group__manual-step">1</span>
1126
+ <h3>Samples</h3>
1127
+ </div>
1128
+ <div class="mint-auto-group__manual-panel-meta">
1129
+ <span>{{ manualFilteredSamples.length }} of {{ manualSamplePool.length }}</span>
1130
+ <label class="mint-auto-group__manual-filter">
1131
+ <input
1132
+ v-model="manualUngroupedOnly"
1133
+ type="checkbox"
1134
+ class="mint-auto-group__manual-checkbox"
1135
+ />
1136
+ <span>Ungrouped only</span>
1137
+ </label>
1138
+ </div>
1139
+ </div>
1140
+
1141
+ <div class="mint-auto-group__manual-search">
1142
+ <svg class="mint-auto-group__manual-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1143
+ <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" />
1144
+ </svg>
1145
+ <input
1146
+ v-model="manualSearchQuery"
1147
+ type="text"
1148
+ aria-label="Search samples for manual grouping"
1149
+ placeholder="Search sample keyword..."
1150
+ class="mint-auto-group__manual-search-input"
1151
+ />
1152
+ </div>
1153
+
1154
+ <label class="mint-auto-group__manual-selectbar">
1155
+ <input
1156
+ type="checkbox"
1157
+ :checked="manualAllVisibleSelected"
1158
+ :indeterminate="manualSomeVisibleSelected"
1159
+ :disabled="manualFilteredSamples.length === 0"
1160
+ class="mint-auto-group__manual-checkbox"
1161
+ @change="manualToggleVisibleSamples"
1162
+ />
1163
+ <span>Select all matching</span>
1164
+ <span class="mint-auto-group__manual-selectbar-count">
1165
+ {{ manualFilteredSamples.length }} shown
1166
+ </span>
1167
+ </label>
1168
+
1169
+ <div class="mint-auto-group__manual-list">
1170
+ <label
1171
+ v-for="sample in manualFilteredSamples"
1172
+ :key="sample"
1173
+ :class="[
1174
+ 'mint-auto-group__manual-sample',
1175
+ manualSelectedSet.has(sample) ? 'mint-auto-group__manual-sample--selected' : '',
1176
+ ]"
1177
+ :title="sample"
1178
+ >
1179
+ <input
1180
+ type="checkbox"
1181
+ :checked="manualSelectedSet.has(sample)"
1182
+ class="mint-auto-group__manual-checkbox"
1183
+ @change="manualToggleSample(sample)"
1184
+ />
1185
+ <span class="mint-auto-group__manual-sample-name">{{ sample }}</span>
1186
+ <span
1187
+ :class="[
1188
+ 'mint-auto-group__manual-status',
1189
+ manualGetSampleGroup(sample) ? '' : 'mint-auto-group__manual-status--empty',
1190
+ ]"
1191
+ >
1192
+ <span
1193
+ v-if="manualGetSampleGroup(sample)"
1194
+ class="mint-auto-group__manual-dot"
1195
+ :style="{ backgroundColor: manualGetSampleGroup(sample)?.color }"
1196
+ aria-hidden="true"
1197
+ />
1198
+ {{ manualGetSampleStatus(sample) }}
1199
+ </span>
1200
+ <span class="mint-auto-group__manual-tooltip">{{ sample }}</span>
1201
+ </label>
1202
+
1203
+ <div v-if="manualFilteredSamples.length === 0" class="mint-auto-group__manual-empty">
1204
+ No samples
1205
+ </div>
1206
+ </div>
1207
+
1208
+ <div class="mint-auto-group__manual-selection-footer">
1209
+ <span><strong>{{ manualSelectedSamples.length }}</strong> selected</span>
1210
+ <button
1211
+ type="button"
1212
+ class="mint-auto-group__manual-link"
1213
+ :disabled="manualSelectedSamples.length === 0"
1214
+ @click="manualClearSamples"
1215
+ >
1216
+ Clear
1217
+ </button>
1218
+ </div>
1219
+ </section>
1220
+
1221
+ <section class="mint-auto-group__manual-panel" aria-label="Manual cohort assignment">
1222
+ <div class="mint-auto-group__manual-panel-head">
1223
+ <div>
1224
+ <span class="mint-auto-group__manual-step">2</span>
1225
+ <h3>Cohort target</h3>
1226
+ </div>
1227
+ </div>
1228
+
1229
+ <div
1230
+ :class="[
1231
+ 'mint-auto-group__manual-assignment',
1232
+ manualSelectedSamples.length === 0 ? 'mint-auto-group__manual-assignment--empty' : '',
1233
+ ]"
1234
+ >
1235
+ <div class="mint-auto-group__manual-selected-summary">
1236
+ <span
1237
+ class="mint-auto-group__manual-dot"
1238
+ :style="{ backgroundColor: manualSelectedColor }"
1239
+ aria-hidden="true"
1240
+ />
1241
+ <span>
1242
+ {{ manualSelectedSamples.length > 0
1243
+ ? `${manualSelectedSamples.length} selected`
1244
+ : 'No sample selected' }}
1245
+ </span>
1246
+ </div>
1247
+
1248
+ <div class="mint-auto-group__manual-fields">
1249
+ <label class="mint-auto-group__manual-field">
1250
+ <span>Group</span>
1251
+ <div class="mint-auto-group__manual-combo">
1252
+ <span
1253
+ class="mint-auto-group__manual-dot"
1254
+ :style="{ backgroundColor: manualSelectedColor }"
1255
+ aria-hidden="true"
1256
+ />
1257
+ <BaseInput
1258
+ v-model="manualGroupName"
1259
+ list="mint-auto-group-manual-groups"
1260
+ placeholder="Type or pick..."
1261
+ size="sm"
1262
+ class="mint-auto-group__manual-combo-input"
1263
+ />
1264
+ <svg class="mint-auto-group__manual-combo-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1265
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" />
1266
+ </svg>
1267
+ <datalist id="mint-auto-group-manual-groups">
1268
+ <option
1269
+ v-for="group in manualDraftGroups"
1270
+ :key="`manual-group-option-${group.name}`"
1271
+ :value="manualGetGroupPrimaryName(group.name)"
1272
+ />
1273
+ </datalist>
1274
+ </div>
1275
+ </label>
1276
+
1277
+ <label class="mint-auto-group__manual-field">
1278
+ <span>Subgroup <small>optional</small></span>
1279
+ <div class="mint-auto-group__manual-combo">
1280
+ <BaseInput
1281
+ v-model="manualSubGroupName"
1282
+ list="mint-auto-group-manual-subgroups"
1283
+ placeholder="e.g. Day 7"
1284
+ size="sm"
1285
+ class="mint-auto-group__manual-combo-input"
1286
+ />
1287
+ <svg class="mint-auto-group__manual-combo-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1288
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" />
1289
+ </svg>
1290
+ <datalist id="mint-auto-group-manual-subgroups">
1291
+ <option
1292
+ v-for="group in manualDraftGroups"
1293
+ :key="`manual-subgroup-option-${group.name}`"
1294
+ :value="manualGetGroupSecondaryName(group.name) || group.name"
1295
+ />
1296
+ </datalist>
1297
+ </div>
1298
+ </label>
1299
+ </div>
1300
+
1301
+ <div class="mint-auto-group__manual-field">
1302
+ <span>Color</span>
1303
+ <div class="mint-auto-group__manual-swatches">
1304
+ <button
1305
+ v-for="color in manualColorOptions"
1306
+ :key="color"
1307
+ type="button"
1308
+ :class="[
1309
+ 'mint-auto-group__manual-swatch',
1310
+ manualSelectedColor === color ? 'mint-auto-group__manual-swatch--active' : '',
1311
+ ]"
1312
+ :style="{ backgroundColor: color }"
1313
+ :aria-label="`Use color ${color}`"
1314
+ @click="manualChooseColor(color)"
1315
+ />
1316
+ </div>
1317
+ </div>
1318
+
1319
+ <div class="mint-auto-group__manual-target">
1320
+ <span>Target cohort</span>
1321
+ <code>{{ manualTargetGroupName || 'Group/Subgroup' }}</code>
1322
+ </div>
1323
+
1324
+ <BaseButton
1325
+ variant="primary"
1326
+ size="sm"
1327
+ class="mint-auto-group__manual-assign"
1328
+ :disabled="!manualCanAssign"
1329
+ @click="manualAssignSamplesToGroup"
1330
+ >
1331
+ <svg class="mint-auto-group__manual-btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1332
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-6-6 6 6-6 6" />
1333
+ </svg>
1334
+ Assign
1335
+ </BaseButton>
1336
+ </div>
1337
+
1338
+ <div class="mint-auto-group__manual-cohorts">
1339
+ <div class="mint-auto-group__manual-cohorts-head">
1340
+ <span>Cohorts so far</span>
1341
+ <span>{{ manualDraftGroups.length }} groups</span>
1342
+ </div>
1343
+
1344
+ <template v-if="manualShowHierarchy">
1345
+ <template
1346
+ v-for="majorGroup in manualHierarchicalGroups"
1347
+ :key="`manual-major-${majorGroup.name}`"
1348
+ >
1349
+ <button
1350
+ type="button"
1351
+ class="mint-auto-group__manual-chip mint-auto-group__manual-chip--major"
1352
+ :title="majorGroup.name"
1353
+ @click="manualUseExistingGroupAsTarget(majorGroup.subGroups[0]?.name || majorGroup.name)"
1354
+ >
1355
+ <span
1356
+ class="mint-auto-group__manual-chip-dot"
1357
+ :style="{ backgroundColor: majorGroup.color }"
1358
+ aria-hidden="true"
1359
+ />
1360
+ <span class="mint-auto-group__manual-chip-main">{{ majorGroup.name }}</span>
1361
+ <span class="mint-auto-group__manual-chip-count">{{ majorGroup.allSamples.length }}</span>
1362
+ </button>
1363
+
1364
+ <button
1365
+ v-for="subGroup in majorGroup.subGroups"
1366
+ :key="`manual-sub-${subGroup.name}`"
1367
+ type="button"
1368
+ class="mint-auto-group__manual-chip mint-auto-group__manual-chip--sub"
1369
+ :title="subGroup.name"
1370
+ @click="manualUseExistingGroupAsTarget(subGroup.name)"
1371
+ >
1372
+ <span class="mint-auto-group__manual-chip-sub">
1373
+ {{ manualGetGroupSecondaryName(subGroup.name) || subGroup.name }}
1374
+ </span>
1375
+ <span class="mint-auto-group__manual-chip-count">{{ subGroup.samples.length }}</span>
1376
+ </button>
1377
+ </template>
1378
+ </template>
1379
+
1380
+ <template v-else>
1381
+ <button
1382
+ v-for="group in manualDraftGroups"
1383
+ :key="group.name"
1384
+ type="button"
1385
+ class="mint-auto-group__manual-chip"
1386
+ :title="group.name"
1387
+ @click="manualUseExistingGroupAsTarget(group.name)"
1388
+ >
1389
+ <span
1390
+ class="mint-auto-group__manual-chip-dot"
1391
+ :style="{ backgroundColor: group.color }"
1392
+ aria-hidden="true"
1393
+ />
1394
+ <span class="mint-auto-group__manual-chip-main">{{ group.name }}</span>
1395
+ <span class="mint-auto-group__manual-chip-count">{{ group.samples.length }}</span>
1396
+ </button>
1397
+ </template>
1398
+
1399
+ <div v-if="manualDraftGroups.length === 0" class="mint-auto-group__manual-empty">
1400
+ No cohorts yet
1401
+ </div>
1402
+ </div>
1403
+ </section>
1404
+ </div>
1405
+
1406
+ </div>
843
1407
  </div>
1408
+
1409
+ <template v-if="activeWorkflow === 'manual'" #footer>
1410
+ <div class="mint-auto-group__nav mint-auto-group__nav--manual">
1411
+ <BaseButton variant="secondary" @click="handleCancel">
1412
+ Cancel
1413
+ </BaseButton>
1414
+ <div class="mint-auto-group__nav-spacer" />
1415
+ <BaseButton variant="primary" @click="handleManualApply">
1416
+ Apply
1417
+ </BaseButton>
1418
+ </div>
1419
+ </template>
844
1420
  </BaseModal>
845
1421
  </template>
846
1422
 
@@ -15,6 +15,7 @@ interface Props {
15
15
  min?: number
16
16
  max?: number
17
17
  step?: number
18
+ list?: string
18
19
  ariaDescribedby?: string
19
20
  }
20
21
 
@@ -54,6 +55,7 @@ function handleInput(event: Event) {
54
55
  :min="min"
55
56
  :max="max"
56
57
  :step="step"
58
+ :list="list"
57
59
  :aria-invalid="error || undefined"
58
60
  :aria-describedby="ariaDescribedby || undefined"
59
61
  :class="[