@morscherlab/mint-sdk 1.0.43 → 1.0.45

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 (49) hide show
  1. package/dist/__tests__/components/SmartGroupFieldRecipe.groups.test.d.ts +1 -0
  2. package/dist/__tests__/components/SmartGroupManual.cohorts.test.d.ts +1 -0
  3. package/dist/components/AutoGroupModal.adapter.d.ts +42 -0
  4. package/dist/components/AutoGroupModal.vue.d.ts +1 -105
  5. package/dist/components/SmartGroup.types.d.ts +71 -0
  6. package/dist/components/SmartGroupFieldRecipe.groups.d.ts +53 -0
  7. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +37 -0
  8. package/dist/components/SmartGroupManual.cohorts.d.ts +52 -0
  9. package/dist/components/SmartGroupManual.vue.d.ts +17 -0
  10. package/dist/components/SmartGroupModal.vue.d.ts +27 -0
  11. package/dist/components/index.d.ts +3 -0
  12. package/dist/components/index.js +2 -2
  13. package/dist/{components-BGVwavdd.js → components-DjfatNJv.js} +2519 -2335
  14. package/dist/components-DjfatNJv.js.map +1 -0
  15. package/dist/composables/index.js +2 -2
  16. package/dist/{composables-C_hPF0Gn.js → composables-CpBhNKHf.js} +7 -147
  17. package/dist/composables-CpBhNKHf.js.map +1 -0
  18. package/dist/index.js +4 -4
  19. package/dist/install.js +1 -1
  20. package/dist/styles.css +12711 -12222
  21. package/dist/{useProtocolTemplates-BbvlHoPD.js → useProtocolTemplates-C8-YlHj1.js} +206 -66
  22. package/dist/useProtocolTemplates-C8-YlHj1.js.map +1 -0
  23. package/package.json +1 -1
  24. package/src/__tests__/components/AutoGroupModal.adapter.test.ts +164 -0
  25. package/src/__tests__/components/SampleSelector.test.ts +176 -16
  26. package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +96 -0
  27. package/src/__tests__/components/SmartGroupManual.cohorts.test.ts +97 -0
  28. package/src/components/AutoGroupModal.adapter.ts +147 -0
  29. package/src/components/AutoGroupModal.vue +176 -1321
  30. package/src/components/SampleSelector.vue +8 -23
  31. package/src/components/SmartGroup.types.ts +93 -0
  32. package/src/components/SmartGroupFieldRecipe.groups.ts +105 -0
  33. package/src/components/SmartGroupFieldRecipe.story.vue +58 -0
  34. package/src/components/SmartGroupFieldRecipe.vue +427 -0
  35. package/src/components/SmartGroupManual.cohorts.ts +112 -0
  36. package/src/components/SmartGroupManual.story.vue +55 -0
  37. package/src/components/SmartGroupManual.vue +398 -0
  38. package/src/components/SmartGroupModal.story.vue +61 -0
  39. package/src/components/SmartGroupModal.vue +61 -0
  40. package/src/components/index.ts +3 -0
  41. package/src/styles/components/sample-selector.css +1 -5
  42. package/src/styles/components/smart-group.css +708 -0
  43. package/src/styles/index.css +1 -1
  44. package/dist/components-BGVwavdd.js.map +0 -1
  45. package/dist/composables-C_hPF0Gn.js.map +0 -1
  46. package/dist/useProtocolTemplates-BbvlHoPD.js.map +0 -1
  47. package/src/__tests__/components/AutoGroupModal.preview.test.ts +0 -46
  48. package/src/styles/components/auto-group-modal.css +0 -1336
  49. /package/dist/__tests__/components/{AutoGroupModal.preview.test.d.ts → AutoGroupModal.adapter.test.d.ts} +0 -0
@@ -110,6 +110,7 @@ const showHierarchy = sampleGroups.showHierarchy
110
110
 
111
111
  const groupingEnabled = computed(() => internalGroups.value.length > 0)
112
112
  const ungroupedSamples = sampleGroups.ungroupedSamples
113
+ const groupingActionMode = computed<'auto' | 'manual'>(() => props.enableSmartGroup ? 'auto' : 'manual')
113
114
 
114
115
  const sampleSearch = useTextSearch({
115
116
  items: () => resolvedSamples.value,
@@ -245,38 +246,22 @@ defineExpose({ handleSmartGroupApply })
245
246
 
246
247
  <!-- Action Buttons Row -->
247
248
  <div v-if="enableGrouping" class="mint-sample-selector__actions">
248
- <div
249
- :class="[
250
- 'mint-sample-selector__actions-row',
251
- !enableSmartGroup ? 'mint-sample-selector__actions-row--single-primary' : '',
252
- ]"
253
- >
254
- <!-- Smart Button -->
249
+ <div class="mint-sample-selector__actions-row">
250
+ <!-- Grouping Button -->
255
251
  <BaseButton
256
- v-if="enableSmartGroup"
257
- :variant="groupingEnabled ? 'primary' : 'secondary'"
252
+ :variant="enableSmartGroup && groupingEnabled ? 'primary' : 'secondary'"
258
253
  size="sm"
259
254
  :disabled="resolvedSamples.length === 0"
260
255
  class="mint-sample-selector__action-btn"
261
- @click="openGroupingModal('auto')"
256
+ @click="openGroupingModal(groupingActionMode)"
262
257
  >
263
- <svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
258
+ <svg v-if="enableSmartGroup" class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
264
259
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
265
260
  </svg>
266
- <span class="mint-sample-selector__action-label">Smart</span>
267
- </BaseButton>
268
-
269
- <BaseButton
270
- variant="secondary"
271
- size="sm"
272
- :disabled="resolvedSamples.length === 0"
273
- class="mint-sample-selector__action-btn"
274
- @click="openGroupingModal('manual')"
275
- >
276
- <svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
261
+ <svg v-else class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
277
262
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3 3 7.5l9 4.5 9-4.5L12 3ZM3 12l9 4.5 9-4.5M3 16.5l9 4.5 9-4.5" />
278
263
  </svg>
279
- <span class="mint-sample-selector__action-label">Manual</span>
264
+ <span class="mint-sample-selector__action-label">Grouping</span>
280
265
  </BaseButton>
281
266
 
282
267
  <!-- Reset Button -->
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared prop and payload types for the Smart Group — Full View components
3
+ * (`SmartGroupModal` shell + `SmartGroupFieldRecipe` auto mode +
4
+ * `SmartGroupManual` manual mode).
5
+ */
6
+ import type {
7
+ FieldRecipeGroup,
8
+ QcRoute,
9
+ SmartGroupField,
10
+ SmartGroupSampleRecord,
11
+ } from './SmartGroupFieldRecipe.groups'
12
+ import type { ManualSample } from './SmartGroupManual.cohorts'
13
+
14
+ export type SmartGroupMode = 'auto' | 'manual'
15
+
16
+ /** A colour swatch in the manual assignment bar. */
17
+ export interface ManualPaletteEntry {
18
+ /** CSS colour value (e.g. a `var(--grp-*)` reference). */
19
+ c: string
20
+ /** Human-readable swatch key for the `aria-label`. */
21
+ k: string
22
+ }
23
+
24
+ /** Initial cohort assignment for a sample, keyed by sample name. */
25
+ export interface SmartGroupSeed {
26
+ group?: string
27
+ sub?: string
28
+ sub2?: string
29
+ color?: string
30
+ }
31
+
32
+ /** The QC summary chip shown in the rail when QC is not mixed into groups. */
33
+ export interface FieldRecipeQcChip {
34
+ name: string
35
+ count: number
36
+ }
37
+
38
+ export interface SmartGroupFieldRecipeProps {
39
+ /** Sample records behind the experiment (one row per real sample). */
40
+ samples?: SmartGroupSampleRecord[]
41
+ /** Candidate grouping fields with role + value preview. */
42
+ fields?: SmartGroupField[]
43
+ /** Experiment code shown in the header; omitted from the subtitle when empty. */
44
+ experimentLabel?: string
45
+ /** Sample count shown in the header (real + QC). */
46
+ headerCount?: number
47
+ /** Which fields start switched on; defaults to every non-replicate field. */
48
+ initialOn?: boolean[]
49
+ /** Initial QC routing. */
50
+ initialRoute?: QcRoute
51
+ /** Group colours, cycled in group order. */
52
+ palette?: string[]
53
+ /** Colour used for the QC pool. */
54
+ qcColor?: string
55
+
56
+ // ── Controlled mode (driven by AutoGroupModal over the useAutoGroup engine).
57
+ // When these are provided the component renders the supplied data and emits
58
+ // events instead of running its own fixture grouping logic.
59
+ /** Controls the field toggles; when set, clicking a toggle emits `toggle`. */
60
+ enabled?: boolean[]
61
+ /** Pre-computed resulting groups; when set, the rail renders these directly. */
62
+ groups?: FieldRecipeGroup[]
63
+ /** QC summary chip to show in the rail; `null` hides it (e.g. when mixed). */
64
+ qcChip?: FieldRecipeQcChip | null
65
+ /** Controlled QC routing; when set, changing the select emits `update:route`. */
66
+ route?: QcRoute
67
+ /** QC sample count for the footer hint; defaults to `headerCount - samples`. */
68
+ qcCount?: number
69
+ /** Whether the template-download menu items are enabled (default `true`). */
70
+ canDownloadTemplate?: boolean
71
+ /** Show a loading state in the main body (e.g. while experiment data loads). */
72
+ loading?: boolean
73
+ }
74
+
75
+ export interface FieldRecipeApplyPayload {
76
+ groups: FieldRecipeGroup[]
77
+ route: QcRoute
78
+ sampleTotal: number
79
+ activeFactors: string[]
80
+ }
81
+
82
+ export interface SmartGroupManualProps {
83
+ /** Flat list of sample names to group. */
84
+ sampleNames?: string[]
85
+ /** Initial assignments keyed by sample name. */
86
+ seed?: Record<string, SmartGroupSeed>
87
+ /** Colour swatches offered in the assignment bar. */
88
+ palette?: ManualPaletteEntry[]
89
+ }
90
+
91
+ export interface ManualDonePayload {
92
+ samples: ManualSample[]
93
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Pure grouping logic for SmartGroupFieldRecipe (the "auto" / Field Recipe mode).
3
+ *
4
+ * Mirrors the design prototype's live grouping engine: groups are formed by
5
+ * joining the values of whichever fields are switched on, in field order.
6
+ * Kept framework-free so it is unit-testable in isolation from the component.
7
+ */
8
+
9
+ /** A field/factor the user can group by. */
10
+ export interface SmartGroupField {
11
+ /** Column key as it appears in each sample record. */
12
+ key: string
13
+ /** Display role, e.g. `Factor` or `Replicate`. */
14
+ role: string
15
+ /** Comma-separated preview of the field's distinct values. */
16
+ vals: string
17
+ /** Whether this field is a replicate dimension (grouping by it shatters groups). */
18
+ rep: boolean
19
+ }
20
+
21
+ /** A single sample as a flat key → value record. */
22
+ export type SmartGroupSampleRecord = Record<string, string>
23
+
24
+ /** A resolved group in the live preview rail. */
25
+ export interface FieldRecipeGroup {
26
+ name: string
27
+ count: number
28
+ color: string
29
+ }
30
+
31
+ /** How QC samples are routed relative to the biological groups. */
32
+ export type QcRoute = 'Overlay only' | 'Exclude' | 'Mix into groups'
33
+
34
+ /** Kind of inline summary callout to render. */
35
+ export type FieldRecipeSummaryKind = 'mute' | 'warn' | 'info'
36
+
37
+ export interface FieldRecipeSummary {
38
+ kind: FieldRecipeSummaryKind
39
+ /** Number of base (non-QC) groups the current toggles produce. */
40
+ groupCount: number
41
+ }
42
+
43
+ /**
44
+ * Build the ordered group list from whichever fields are switched on.
45
+ * Returns an empty list when no field is active.
46
+ */
47
+ export function computeFieldGroups(
48
+ samples: SmartGroupSampleRecord[],
49
+ fields: SmartGroupField[],
50
+ on: boolean[],
51
+ palette: string[],
52
+ ): FieldRecipeGroup[] {
53
+ const keys = fields.filter((_, i) => on[i]).map(field => field.key)
54
+ if (keys.length === 0) return []
55
+
56
+ const order: string[] = []
57
+ const counts = new Map<string, number>()
58
+ for (const sample of samples) {
59
+ const name = keys.map(key => sample[key]).join('_')
60
+ if (!counts.has(name)) {
61
+ counts.set(name, 0)
62
+ order.push(name)
63
+ }
64
+ counts.set(name, (counts.get(name) ?? 0) + 1)
65
+ }
66
+
67
+ return order.map((name, index) => ({
68
+ name,
69
+ count: counts.get(name) ?? 0,
70
+ color: palette[index % palette.length],
71
+ }))
72
+ }
73
+
74
+ /**
75
+ * Append the QC pool group when QC is mixed into the biological groups.
76
+ * `Overlay only` and `Exclude` leave the group list untouched.
77
+ */
78
+ export function appendQcGroup(
79
+ groups: FieldRecipeGroup[],
80
+ route: QcRoute,
81
+ qcColor: string,
82
+ qcCount = 3,
83
+ ): FieldRecipeGroup[] {
84
+ if (route === 'Mix into groups') {
85
+ return [...groups, { name: 'QC_Pool', count: qcCount, color: qcColor }]
86
+ }
87
+ return groups
88
+ }
89
+
90
+ /** Total samples the grouping applies to: QC is dropped only when excluded. */
91
+ export function sampleTotalFor(route: QcRoute, realCount = 9, qcCount = 3): number {
92
+ return route === 'Exclude' ? realCount : realCount + qcCount
93
+ }
94
+
95
+ /** Decide which inline summary callout the Field Recipe should show. */
96
+ export function fieldRecipeSummary(input: {
97
+ activeFactors: string[]
98
+ repOn: boolean
99
+ baseGroups: FieldRecipeGroup[]
100
+ }): FieldRecipeSummary {
101
+ const groupCount = input.baseGroups.length
102
+ if (input.activeFactors.length === 0) return { kind: 'mute', groupCount }
103
+ if (input.repOn) return { kind: 'warn', groupCount }
104
+ return { kind: 'info', groupCount }
105
+ }
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import SmartGroupFieldRecipe from './SmartGroupFieldRecipe.vue'
3
+ import type { FieldRecipeApplyPayload } from './SmartGroup.types'
4
+ </script>
5
+
6
+ <template>
7
+ <Story title="Lab/SmartGroupFieldRecipe">
8
+ <Variant title="Playground" :init-state="() => ({ log: '' })">
9
+ <template #default="{ state }">
10
+ <div class="sg-story">
11
+ <div class="sg-story__stage">
12
+ <SmartGroupFieldRecipe
13
+ @apply="(r: FieldRecipeApplyPayload) => (state.log = `${r.groups.length} groups · ${r.route} · ${r.sampleTotal} samples`)"
14
+ @manual="state.log = 'switch to manual grouping'"
15
+ @cancel="state.log = 'cancelled'"
16
+ @close="state.log = 'closed'"
17
+ @csv-file="(f: File) => (state.log = `csv selected: ${f.name}`)"
18
+ @download-template="state.log = 'download blank template'"
19
+ @download-prefilled="state.log = 'download pre-filled template'"
20
+ />
21
+ </div>
22
+ <p v-if="state.log" class="sg-story__log">{{ state.log }}</p>
23
+ </div>
24
+ </template>
25
+ </Variant>
26
+
27
+ <Variant title="Replicate singleton trap">
28
+ <template #default>
29
+ <div class="sg-story">
30
+ <div class="sg-story__stage">
31
+ <SmartGroupFieldRecipe :initial-on="[true, true, true]" />
32
+ </div>
33
+ </div>
34
+ </template>
35
+ </Variant>
36
+ </Story>
37
+ </template>
38
+
39
+ <style scoped>
40
+ .sg-story {
41
+ padding: 2rem;
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 1rem;
45
+ align-items: flex-start;
46
+ }
47
+ .sg-story__stage {
48
+ width: 980px;
49
+ height: 640px;
50
+ max-width: 100%;
51
+ }
52
+ .sg-story__log {
53
+ margin: 0;
54
+ font-family: 'Fira Code', monospace;
55
+ font-size: 0.8125rem;
56
+ color: var(--text-secondary, #64748b);
57
+ }
58
+ </style>