@morscherlab/mint-sdk 1.0.0-beta.7 → 1.0.0-rc.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 (163) hide show
  1. package/README.md +9 -1
  2. package/dist/__tests__/components/LcmsSequenceTable.test.d.ts +1 -0
  3. package/dist/__tests__/components/ProgressBar.test.d.ts +1 -0
  4. package/dist/__tests__/components/RackEditor.test.d.ts +1 -0
  5. package/dist/__tests__/components/SequenceProgressBar.test.d.ts +1 -0
  6. package/dist/__tests__/composables/useExperimentSamples.test.d.ts +1 -0
  7. package/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
  8. package/dist/__tests__/stores/settings.test.d.ts +1 -0
  9. package/dist/__tests__/utils/instrument.test.d.ts +1 -0
  10. package/dist/__tests__/utils/lcms.test.d.ts +1 -0
  11. package/dist/__tests__/utils/permissions.test.d.ts +1 -0
  12. package/dist/__tests__/utils/rack.test.d.ts +1 -0
  13. package/dist/{auth-QQj2kkze.js → auth-B7g4J4ZF.js} +148 -24
  14. package/dist/auth-B7g4J4ZF.js.map +1 -0
  15. package/dist/components/AutoGroupModal.vue.d.ts +1 -1
  16. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  17. package/dist/components/BaseToggle.vue.d.ts +2 -2
  18. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  19. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
  20. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +1 -1
  21. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
  22. package/dist/components/FormulaInput.vue.d.ts +1 -1
  23. package/dist/components/InstrumentAlertLog.vue.d.ts +22 -0
  24. package/dist/components/InstrumentStateBadge.vue.d.ts +11 -0
  25. package/dist/components/InstrumentStatusCard.vue.d.ts +13 -0
  26. package/dist/components/LcmsSequenceTable.vue.d.ts +26 -0
  27. package/dist/components/ProgressBar.vue.d.ts +1 -0
  28. package/dist/components/RackEditor.vue.d.ts +41 -3
  29. package/dist/components/ReagentList.vue.d.ts +1 -1
  30. package/dist/components/SampleSelector.vue.d.ts +5 -2
  31. package/dist/components/SegmentedControl.vue.d.ts +2 -0
  32. package/dist/components/SequenceInput.vue.d.ts +1 -1
  33. package/dist/components/SequenceProgressBar.vue.d.ts +15 -0
  34. package/dist/components/SettingsModal.vue.d.ts +8 -1
  35. package/dist/components/TagsInput.vue.d.ts +1 -1
  36. package/dist/components/WellPlate.vue.d.ts +42 -3
  37. package/dist/components/index.d.ts +5 -0
  38. package/dist/components/index.js +3 -3
  39. package/dist/{components-DihbSJjU.js → components-BhK-dW99.js} +2135 -1075
  40. package/dist/components-BhK-dW99.js.map +1 -0
  41. package/dist/composables/experimentDesignData.d.ts +17 -0
  42. package/dist/composables/index.d.ts +2 -0
  43. package/dist/composables/index.js +4 -4
  44. package/dist/composables/useControlSchema.d.ts +11 -0
  45. package/dist/composables/useExperimentData.d.ts +11 -3
  46. package/dist/composables/useExperimentSamples.d.ts +42 -0
  47. package/dist/composables/usePlatformContext.d.ts +54 -0
  48. package/dist/{composables-BcgZ6diz.js → composables-Bg7CFuNz.js} +5 -3
  49. package/dist/composables-Bg7CFuNz.js.map +1 -0
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.js +168 -6
  52. package/dist/index.js.map +1 -0
  53. package/dist/install.js +2 -2
  54. package/dist/instrument.d.ts +7 -0
  55. package/dist/lcms.d.ts +27 -0
  56. package/dist/permissions.d.ts +46 -0
  57. package/dist/stores/auth.d.ts +74 -2
  58. package/dist/stores/index.js +1 -1
  59. package/dist/styles.css +3186 -1070
  60. package/dist/templates/builders.d.ts +7 -3
  61. package/dist/templates/index.d.ts +2 -2
  62. package/dist/templates/index.js +2 -2
  63. package/dist/templates/presets.d.ts +12 -0
  64. package/dist/templates/types.d.ts +16 -1
  65. package/dist/{templates-Cyt0Suwf.js → templates-BorLR_7p.js} +324 -10
  66. package/dist/templates-BorLR_7p.js.map +1 -0
  67. package/dist/types/auth.d.ts +2 -0
  68. package/dist/types/components.d.ts +32 -3
  69. package/dist/types/form-builder.d.ts +2 -1
  70. package/dist/types/index.d.ts +4 -1
  71. package/dist/types/instrument.d.ts +56 -0
  72. package/dist/types/platform.d.ts +3 -0
  73. package/dist/{useExperimentData-CM6Y0u5L.js → useProtocolTemplates-n6AJqSqv.js} +627 -380
  74. package/dist/useProtocolTemplates-n6AJqSqv.js.map +1 -0
  75. package/dist/utils/rack.d.ts +47 -0
  76. package/package.json +1 -1
  77. package/src/__tests__/components/AppTopBar.test.ts +15 -0
  78. package/src/__tests__/components/BaseTabs.test.ts +15 -0
  79. package/src/__tests__/components/GroupAssigner.test.ts +18 -0
  80. package/src/__tests__/components/LcmsSequenceTable.test.ts +57 -0
  81. package/src/__tests__/components/ProgressBar.test.ts +18 -0
  82. package/src/__tests__/components/RackEditor.test.ts +125 -0
  83. package/src/__tests__/components/SampleSelector.test.ts +25 -0
  84. package/src/__tests__/components/SegmentedControl.test.ts +45 -0
  85. package/src/__tests__/components/SequenceProgressBar.test.ts +39 -0
  86. package/src/__tests__/components/SettingsModal.test.ts +83 -2
  87. package/src/__tests__/composables/useApi.test.ts +45 -0
  88. package/src/__tests__/composables/useAuth.test.ts +20 -0
  89. package/src/__tests__/composables/useControlSchema.test.ts +4 -0
  90. package/src/__tests__/composables/useExperimentData.test.ts +23 -0
  91. package/src/__tests__/composables/useExperimentSamples.test.ts +91 -0
  92. package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
  93. package/src/__tests__/stores/settings.test.ts +78 -0
  94. package/src/__tests__/templates/templates.test.ts +86 -0
  95. package/src/__tests__/utils/instrument.test.ts +47 -0
  96. package/src/__tests__/utils/lcms.test.ts +73 -0
  97. package/src/__tests__/utils/permissions.test.ts +50 -0
  98. package/src/__tests__/utils/rack.test.ts +120 -0
  99. package/src/components/AppAvatarMenu.vue +6 -3
  100. package/src/components/AppTopBar.vue +16 -10
  101. package/src/components/AuditTrail.vue +1 -1
  102. package/src/components/BaseTabs.vue +22 -1
  103. package/src/components/Calendar.vue +6 -2
  104. package/src/components/ConcentrationInput.vue +3 -2
  105. package/src/components/GroupAssigner.vue +8 -3
  106. package/src/components/InstrumentAlertLog.vue +191 -0
  107. package/src/components/InstrumentStateBadge.vue +50 -0
  108. package/src/components/InstrumentStatusCard.vue +188 -0
  109. package/src/components/LcmsSequenceTable.vue +191 -0
  110. package/src/components/NumberInput.vue +5 -3
  111. package/src/components/ProgressBar.vue +3 -0
  112. package/src/components/RackEditor.vue +73 -2
  113. package/src/components/SampleHierarchyTree.vue +3 -2
  114. package/src/components/SampleSelector.vue +28 -9
  115. package/src/components/SegmentedControl.story.vue +17 -0
  116. package/src/components/SegmentedControl.vue +14 -3
  117. package/src/components/SequenceProgressBar.vue +71 -0
  118. package/src/components/SettingsModal.vue +49 -2
  119. package/src/components/UnitInput.vue +6 -2
  120. package/src/components/WellPlate.vue +145 -24
  121. package/src/components/index.ts +5 -0
  122. package/src/components/internal/WellEditPopupInternal.vue +1 -0
  123. package/src/composables/experimentDesignData.ts +182 -0
  124. package/src/composables/index.ts +14 -0
  125. package/src/composables/useApi.ts +113 -16
  126. package/src/composables/useAuth.ts +4 -0
  127. package/src/composables/useAutoGroup.ts +18 -9
  128. package/src/composables/useControlSchema.ts +21 -0
  129. package/src/composables/useExperimentData.ts +57 -16
  130. package/src/composables/useExperimentSamples.ts +142 -0
  131. package/src/composables/useProtocolTemplates.ts +13 -1
  132. package/src/composables/useRackEditor.ts +3 -2
  133. package/src/index.ts +27 -0
  134. package/src/instrument.ts +90 -0
  135. package/src/lcms.ts +108 -0
  136. package/src/permissions.ts +143 -0
  137. package/src/stores/auth.ts +79 -26
  138. package/src/stores/settings.ts +10 -0
  139. package/src/styles/components/instrument-monitor.css +478 -0
  140. package/src/styles/components/lcms-sequence-table.css +189 -0
  141. package/src/styles/components/sequence-progress-bar.css +63 -0
  142. package/src/styles/components/settings-modal.css +9 -0
  143. package/src/styles/components/tabs.css +9 -0
  144. package/src/styles/components/well-edit-popup.css +7 -1
  145. package/src/styles/components/well-plate.css +5 -0
  146. package/src/styles/index.css +3 -0
  147. package/src/templates/builders.ts +201 -0
  148. package/src/templates/controlSchemas.ts +68 -0
  149. package/src/templates/index.ts +2 -0
  150. package/src/templates/presets.ts +23 -0
  151. package/src/templates/types.ts +17 -0
  152. package/src/types/auth.ts +3 -0
  153. package/src/types/components.ts +45 -3
  154. package/src/types/form-builder.ts +2 -1
  155. package/src/types/index.ts +35 -0
  156. package/src/types/instrument.ts +61 -0
  157. package/src/types/platform.ts +4 -0
  158. package/src/utils/rack.ts +209 -0
  159. package/dist/auth-QQj2kkze.js.map +0 -1
  160. package/dist/components-DihbSJjU.js.map +0 -1
  161. package/dist/composables-BcgZ6diz.js.map +0 -1
  162. package/dist/templates-Cyt0Suwf.js.map +0 -1
  163. package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
@@ -0,0 +1,188 @@
1
+ <script setup lang="ts">
2
+ /** Compact card for live instrument monitor status, sample, method, sequence progress, and ETA. */
3
+ import { computed } from 'vue'
4
+ import {
5
+ estimateSequenceFinishDate,
6
+ formatSequenceRemaining,
7
+ estimateSequenceRemainingSeconds,
8
+ } from '../instrument'
9
+ import type { InstrumentState, InstrumentStatus, SequenceProgress } from '../types'
10
+ import InstrumentStateBadge from './InstrumentStateBadge.vue'
11
+ import SequenceProgressBar from './SequenceProgressBar.vue'
12
+
13
+ interface Props {
14
+ status: InstrumentStatus
15
+ name?: string
16
+ showPlaceholders?: boolean
17
+ locale?: string
18
+ }
19
+
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ name: undefined,
22
+ showPlaceholders: true,
23
+ locale: undefined,
24
+ })
25
+
26
+ const displayName = computed(() =>
27
+ props.name
28
+ ?? props.status.instrument_name
29
+ ?? props.status.instrument_id
30
+ )
31
+ const isRunning = computed(() => props.status.state === 'running')
32
+ const sampleLabel = computed(() => {
33
+ const sample = props.status.current_sample
34
+ return sample ? (sample.sample_name ?? sample.sample_id ?? sample.file_name) : null
35
+ })
36
+
37
+ const etaDisplay = computed(() => {
38
+ const progress = props.status.sequence_progress
39
+ if (!progress) return null
40
+ const finish = estimateSequenceFinishDate(progress)
41
+ if (!finish) return null
42
+
43
+ const now = new Date()
44
+ const sameDay =
45
+ finish.getFullYear() === now.getFullYear()
46
+ && finish.getMonth() === now.getMonth()
47
+ && finish.getDate() === now.getDate()
48
+
49
+ const time = finish.toLocaleTimeString(props.locale, { hour: 'numeric', minute: '2-digit' })
50
+ if (sameDay) return time
51
+
52
+ const sameYear = finish.getFullYear() === now.getFullYear()
53
+ const date = finish.toLocaleDateString(props.locale, {
54
+ month: 'short',
55
+ day: 'numeric',
56
+ ...(sameYear ? {} : { year: 'numeric' }),
57
+ })
58
+ return `${date}, ${time}`
59
+ })
60
+
61
+ const remainingLabel = computed(() => {
62
+ const progress = props.status.sequence_progress
63
+ if (!progress) return null
64
+ return formatSequenceRemaining(progress)
65
+ })
66
+
67
+ const lastSeenAgo = computed(() => {
68
+ const value = props.status.last_seen ?? props.status.timestamp
69
+ if (!value) return null
70
+ return formatRelativeAge(value)
71
+ })
72
+
73
+ const stateClass = computed(() => `mint-instrument-card--${props.status.state}`)
74
+
75
+ function stateTone(state: InstrumentState | string): string {
76
+ if (state === 'running') return 'success'
77
+ if (state === 'standby') return 'warning'
78
+ if (state === 'error') return 'error'
79
+ if (state === 'connected') return 'info'
80
+ return 'muted'
81
+ }
82
+
83
+ function formatRelativeAge(value: string | Date): string | null {
84
+ const date = value instanceof Date ? value : new Date(value)
85
+ if (Number.isNaN(date.getTime())) return null
86
+ const minutes = Math.floor((Date.now() - date.getTime()) / 60_000)
87
+ if (minutes < 1) return 'just now'
88
+ if (minutes < 60) return `${minutes}m ago`
89
+ const hours = Math.floor(minutes / 60)
90
+ if (hours < 24) return `${hours}h ago`
91
+ const days = Math.floor(hours / 24)
92
+ return days === 1 ? '1 day ago' : `${days} days ago`
93
+ }
94
+
95
+ function hasRemaining(progress: SequenceProgress | null | undefined): boolean {
96
+ const remaining = estimateSequenceRemainingSeconds(progress)
97
+ return remaining !== null && remaining > 0
98
+ }
99
+ </script>
100
+
101
+ <template>
102
+ <div class="mint-instrument-card" :class="stateClass">
103
+ <div class="mint-instrument-card__header">
104
+ <span
105
+ class="mint-instrument-card__dot"
106
+ :class="`mint-instrument-card__dot--${stateTone(status.state)}`"
107
+ />
108
+ <strong class="mint-instrument-card__name">{{ displayName }}</strong>
109
+ <InstrumentStateBadge :state="status.state" />
110
+ </div>
111
+
112
+ <template v-if="isRunning">
113
+ <div v-if="status.current_sample" class="mint-instrument-card__sample">
114
+ <span class="mint-instrument-card__sample-label">Currently Running</span>
115
+ <div class="mint-instrument-card__sample-row">
116
+ <span class="mint-instrument-card__sample-dot" />
117
+ <span class="mint-instrument-card__sample-name">{{ sampleLabel }}</span>
118
+ <span
119
+ v-if="status.current_sample.vial_position"
120
+ class="mint-instrument-card__sample-vial"
121
+ >
122
+ {{ status.current_sample.vial_position }}
123
+ </span>
124
+ </div>
125
+ </div>
126
+
127
+ <span
128
+ v-if="status.active_method"
129
+ class="mint-instrument-card__method"
130
+ :title="status.active_method"
131
+ >
132
+ {{ status.active_method }}
133
+ </span>
134
+
135
+ <SequenceProgressBar
136
+ v-if="status.sequence_progress"
137
+ :progress="status.sequence_progress"
138
+ compact
139
+ />
140
+
141
+ <div
142
+ v-if="etaDisplay || hasRemaining(status.sequence_progress)"
143
+ class="mint-instrument-card__eta"
144
+ >
145
+ <span class="mint-instrument-card__eta-label">ETA</span>
146
+ <span v-if="etaDisplay" class="mint-instrument-card__eta-time">{{ etaDisplay }}</span>
147
+ <span v-if="remainingLabel" class="mint-instrument-card__eta-remaining">
148
+ {{ remainingLabel }}
149
+ </span>
150
+ </div>
151
+
152
+ <span
153
+ v-if="status.sequence_progress?.sequence_name"
154
+ class="mint-instrument-card__sequence"
155
+ :title="status.sequence_progress.sequence_name"
156
+ >
157
+ {{ status.sequence_progress.sequence_name }}
158
+ </span>
159
+ </template>
160
+
161
+ <template v-else-if="status.state === 'error'">
162
+ <div class="mint-instrument-card__error">
163
+ <div class="mint-instrument-card__error-message">
164
+ {{ status.active_method ?? 'Instrument error' }}
165
+ </div>
166
+ <div v-if="lastSeenAgo" class="mint-instrument-card__error-detail">
167
+ Last seen {{ lastSeenAgo }}
168
+ </div>
169
+ </div>
170
+ </template>
171
+
172
+ <template v-else-if="showPlaceholders">
173
+ <div class="mint-instrument-card__placeholder">
174
+ <span class="mint-instrument-card__muted">No sample running</span>
175
+ <span class="mint-instrument-card__muted">No method</span>
176
+ <SequenceProgressBar
177
+ :progress="{ current_sample: 0, total_samples: 0 }"
178
+ compact
179
+ />
180
+ <span class="mint-instrument-card__muted">No sequence</span>
181
+ </div>
182
+ </template>
183
+ </div>
184
+ </template>
185
+
186
+ <style>
187
+ @import '../styles/components/instrument-monitor.css';
188
+ </style>
@@ -0,0 +1,191 @@
1
+ <script setup lang="ts">
2
+ /** Table for Xcalibur-compatible LCMS sequence rows with optional reorder/remove/duplicate controls. */
3
+ import { computed, ref } from 'vue'
4
+ import {
5
+ basenameFromWindowsPath,
6
+ DEFAULT_LCMS_SEQUENCE_COLUMNS,
7
+ type LcmsSequenceItem,
8
+ type LcmsSequenceTableColumn,
9
+ } from '../lcms'
10
+
11
+ interface Props {
12
+ items?: LcmsSequenceItem[]
13
+ columns?: LcmsSequenceTableColumn[]
14
+ editable?: boolean
15
+ maxRows?: number
16
+ showMoreLabel?: boolean
17
+ emptyMessage?: string
18
+ }
19
+
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ items: () => [],
22
+ columns: () => DEFAULT_LCMS_SEQUENCE_COLUMNS,
23
+ editable: false,
24
+ maxRows: undefined,
25
+ showMoreLabel: true,
26
+ emptyMessage: 'No sequence items to display',
27
+ })
28
+
29
+ const emit = defineEmits<{
30
+ reorder: [fromIndex: number, toIndex: number]
31
+ remove: [index: number]
32
+ duplicate: [index: number]
33
+ }>()
34
+
35
+ const dragIndex = ref<number | null>(null)
36
+ const dropTargetIndex = ref<number | null>(null)
37
+
38
+ const displayItems = computed(() => {
39
+ if (!props.maxRows) return props.items
40
+ return props.items.slice(0, props.maxRows)
41
+ })
42
+
43
+ const hasMore = computed(() =>
44
+ props.maxRows != null && props.items.length > props.maxRows
45
+ )
46
+
47
+ function sampleTypeClass(type: string): string {
48
+ const normalized = type.toLowerCase()
49
+ if (normalized === 'blank') return 'mint-lcms-sequence-table__type--blank'
50
+ if (normalized === 'qc' || normalized === 'iqc' || normalized === 'eqc') {
51
+ return 'mint-lcms-sequence-table__type--qc'
52
+ }
53
+ return 'mint-lcms-sequence-table__type--sample'
54
+ }
55
+
56
+ function cellValue(item: LcmsSequenceItem, column: LcmsSequenceTableColumn, index: number): string | number {
57
+ if (column.key === 'index') return index + 1
58
+ if (column.key === 'actions') return ''
59
+ if (column.key === 'instrument_method') return basenameFromWindowsPath(item.instrument_method)
60
+ return item[column.key] ?? ''
61
+ }
62
+
63
+ function onDragStart(event: DragEvent, index: number) {
64
+ if (!props.editable) return
65
+ dragIndex.value = index
66
+ if (event.dataTransfer) {
67
+ event.dataTransfer.effectAllowed = 'move'
68
+ event.dataTransfer.setData('text/plain', String(index))
69
+ }
70
+ }
71
+
72
+ function onDragOver(event: DragEvent, index: number) {
73
+ if (!props.editable) return
74
+ event.preventDefault()
75
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
76
+ if (dragIndex.value !== null && dragIndex.value !== index) {
77
+ dropTargetIndex.value = index
78
+ }
79
+ }
80
+
81
+ function clearDragState() {
82
+ dragIndex.value = null
83
+ dropTargetIndex.value = null
84
+ }
85
+
86
+ function onDrop(event: DragEvent, toIndex: number) {
87
+ if (!props.editable) return
88
+ event.preventDefault()
89
+ if (dragIndex.value !== null && dragIndex.value !== toIndex) {
90
+ emit('reorder', dragIndex.value, toIndex)
91
+ }
92
+ clearDragState()
93
+ }
94
+ </script>
95
+
96
+ <template>
97
+ <div class="mint-lcms-sequence-table">
98
+ <div class="mint-lcms-sequence-table__scroller">
99
+ <table class="mint-lcms-sequence-table__table">
100
+ <thead>
101
+ <tr>
102
+ <th v-if="editable" class="mint-lcms-sequence-table__drag-header" />
103
+ <th
104
+ v-for="column in columns"
105
+ :key="column.key"
106
+ :class="[
107
+ 'mint-lcms-sequence-table__th',
108
+ { 'mint-lcms-sequence-table__th--right': column.align === 'right' },
109
+ ]"
110
+ >
111
+ {{ column.label }}
112
+ </th>
113
+ <th v-if="editable" class="mint-lcms-sequence-table__actions-header" />
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ <tr
118
+ v-for="(item, index) in displayItems"
119
+ :key="`${item.file_name}-${item.position}-${index}`"
120
+ :draggable="editable"
121
+ class="mint-lcms-sequence-table__row"
122
+ :class="{
123
+ 'mint-lcms-sequence-table__row--dragging': dragIndex === index,
124
+ 'mint-lcms-sequence-table__row--drop-above': dropTargetIndex === index && dragIndex !== null && dragIndex > index,
125
+ 'mint-lcms-sequence-table__row--drop-below': dropTargetIndex === index && dragIndex !== null && dragIndex < index,
126
+ }"
127
+ @dragstart="onDragStart($event, index)"
128
+ @dragenter.prevent
129
+ @dragover="onDragOver($event, index)"
130
+ @dragleave="dropTargetIndex = null"
131
+ @drop="onDrop($event, index)"
132
+ @dragend="clearDragState"
133
+ >
134
+ <td v-if="editable" class="mint-lcms-sequence-table__drag-cell">
135
+ <span class="mint-lcms-sequence-table__drag-handle" aria-hidden="true">::</span>
136
+ </td>
137
+ <td
138
+ v-for="column in columns"
139
+ :key="column.key"
140
+ :class="[
141
+ 'mint-lcms-sequence-table__td',
142
+ `mint-lcms-sequence-table__td--${column.key}`,
143
+ { 'mint-lcms-sequence-table__td--right': column.align === 'right' },
144
+ ]"
145
+ :title="column.key === 'instrument_method' ? item.instrument_method : undefined"
146
+ >
147
+ <span
148
+ v-if="column.key === 'sample_type'"
149
+ class="mint-lcms-sequence-table__type"
150
+ :class="sampleTypeClass(item.sample_type)"
151
+ >
152
+ {{ item.sample_type }}
153
+ </span>
154
+ <template v-else>{{ cellValue(item, column, index) }}</template>
155
+ </td>
156
+ <td v-if="editable" class="mint-lcms-sequence-table__actions">
157
+ <button
158
+ type="button"
159
+ class="mint-lcms-sequence-table__action"
160
+ title="Duplicate"
161
+ @click="emit('duplicate', index)"
162
+ >
163
+ +
164
+ </button>
165
+ <button
166
+ type="button"
167
+ class="mint-lcms-sequence-table__action mint-lcms-sequence-table__action--danger"
168
+ title="Delete"
169
+ @click="emit('remove', index)"
170
+ >
171
+ x
172
+ </button>
173
+ </td>
174
+ </tr>
175
+ </tbody>
176
+ </table>
177
+ </div>
178
+
179
+ <div v-if="items.length === 0" class="mint-lcms-sequence-table__empty">
180
+ {{ emptyMessage }}
181
+ </div>
182
+
183
+ <div v-if="hasMore && showMoreLabel" class="mint-lcms-sequence-table__more">
184
+ Showing first {{ maxRows }} of {{ items.length }} items
185
+ </div>
186
+ </div>
187
+ </template>
188
+
189
+ <style>
190
+ @import '../styles/components/lcms-sequence-table.css';
191
+ </style>
@@ -28,10 +28,12 @@ const emit = defineEmits<{
28
28
  const isSliderMode = computed(() => props.min !== undefined && props.max !== undefined)
29
29
 
30
30
  const sliderPercent = computed(() => {
31
- if (!isSliderMode.value || props.modelValue === undefined) return 0
32
- const range = (props.max! - props.min!)
31
+ const min = props.min
32
+ const max = props.max
33
+ if (min === undefined || max === undefined || props.modelValue === undefined) return 0
34
+ const range = max - min
33
35
  if (range === 0) return 0
34
- return ((props.modelValue - props.min!) / range) * 100
36
+ return ((props.modelValue - min) / range) * 100
35
37
  })
36
38
 
37
39
  const canDecrement = computed(() => {
@@ -12,6 +12,7 @@ interface Props {
12
12
  indeterminate?: boolean
13
13
  steps?: string[]
14
14
  currentStep?: number
15
+ ariaLabel?: string
15
16
  }
16
17
 
17
18
  const props = withDefaults(defineProps<Props>(), {
@@ -58,6 +59,7 @@ const segmentCount = computed(() => props.steps.length || 0)
58
59
  :aria-valuenow="indeterminate ? undefined : clampedValue"
59
60
  :aria-valuemin="indeterminate ? undefined : 0"
60
61
  :aria-valuemax="indeterminate ? undefined : 100"
62
+ :aria-label="ariaLabel ?? label"
61
63
  >
62
64
  <div
63
65
  :class="[
@@ -77,6 +79,7 @@ const segmentCount = computed(() => props.steps.length || 0)
77
79
  :aria-valuenow="currentStep"
78
80
  :aria-valuemin="0"
79
81
  :aria-valuemax="segmentCount"
82
+ :aria-label="ariaLabel ?? label"
80
83
  >
81
84
  <span
82
85
  v-for="(_step, i) in steps"
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /** Multi-rack editor for managing collections of well-plate racks with add/remove/reorder controls and per-well editing. */
3
3
  import { ref, computed, watch } from 'vue'
4
- import type { Rack, SlotPosition, WellPlateFormat, WellPlateSize, WellEditData, Well } from '../types'
4
+ import type { Rack, SlotPosition, WellPlateFormat, WellPlateSize, WellEditData, Well, WellEditField, WellSampleDropData, RackSampleDropMapper } from '../types'
5
5
  import { useRackEditor } from '../composables/useRackEditor'
6
6
  import WellPlate from './WellPlate.vue'
7
7
 
@@ -16,6 +16,8 @@ interface Props {
16
16
  wellPlateSize?: WellPlateSize
17
17
  showLegend?: boolean
18
18
  showBadges?: boolean
19
+ allowSampleDrop?: boolean
20
+ sampleDropMapper?: RackSampleDropMapper
19
21
  }
20
22
 
21
23
  const props = withDefaults(defineProps<Props>(), {
@@ -29,6 +31,8 @@ const props = withDefaults(defineProps<Props>(), {
29
31
  wellPlateSize: 'md',
30
32
  showLegend: true,
31
33
  showBadges: true,
34
+ allowSampleDrop: false,
35
+ sampleDropMapper: undefined,
32
36
  })
33
37
 
34
38
  const emit = defineEmits<{
@@ -38,6 +42,24 @@ const emit = defineEmits<{
38
42
  'rack-remove': [rackId: string]
39
43
  'rack-reorder': [rackIds: string[]]
40
44
  'well-edit': [rackId: string, wellId: string, data: WellEditData]
45
+ 'sample-drop': [rackId: string, wellId: string, data: WellSampleDropData, event: DragEvent]
46
+ }>()
47
+
48
+ interface RackWellEditorSlotProps {
49
+ rack?: Rack
50
+ rackId: string
51
+ wellId: string
52
+ wellData?: Partial<Well>
53
+ editFields: WellEditField[]
54
+ defaultInjectionVolume: number
55
+ position: { x: number; y: number }
56
+ save: (data: WellEditData) => void
57
+ clear: () => void
58
+ close: () => void
59
+ }
60
+
61
+ defineSlots<{
62
+ 'well-editor'?: (props: RackWellEditorSlotProps) => unknown
41
63
  }>()
42
64
 
43
65
  const editor = useRackEditor(props.modelValue, {
@@ -214,6 +236,37 @@ function handleWellClear(wellId: string) {
214
236
  editor.clearWell(editor.activeRack.value.id, wellId)
215
237
  }
216
238
 
239
+ function handleSampleDrop(wellId: string, data: WellSampleDropData, event: DragEvent) {
240
+ const rack = editor.activeRack.value
241
+ if (!rack) return
242
+
243
+ emit('sample-drop', rack.id, wellId, data, event)
244
+
245
+ const editData = props.sampleDropMapper?.(data, {
246
+ rack,
247
+ rackId: rack.id,
248
+ wellId,
249
+ event,
250
+ }) ?? defaultSampleDropEditData(wellId, data, rack)
251
+
252
+ if (!editData) return
253
+ handleWellEdit(wellId, { ...editData, wellId })
254
+ }
255
+
256
+ function defaultSampleDropEditData(wellId: string, data: WellSampleDropData, rack: Rack): WellEditData | null {
257
+ const label = data.label ?? data.sampleName ?? data.id ?? ''
258
+ if (!label.trim()) return null
259
+
260
+ return {
261
+ wellId,
262
+ label: label.trim(),
263
+ sampleType: data.sampleType ?? 'sample',
264
+ injectionVolume: data.injectionVolume ?? rack.injectionVolume,
265
+ injectionCount: data.injectionCount ?? 1,
266
+ customMethod: data.customMethod ?? '',
267
+ }
268
+ }
269
+
217
270
  // Computed well count per rack
218
271
  function getWellCount(rack: Rack): number {
219
272
  return Object.keys(rack.wells).length
@@ -359,9 +412,27 @@ const activeRackWells = computed(() => editor.activeRack.value?.wells ?? {})
359
412
  :readonly="readonly"
360
413
  :size="wellPlateSize"
361
414
  :show-sample-type-indicator="true"
415
+ :allow-sample-drop="allowSampleDrop && !readonly"
362
416
  @well-edit="handleWellEdit"
363
417
  @well-clear="handleWellClear"
364
- />
418
+ @sample-drop="handleSampleDrop"
419
+ >
420
+ <template v-if="$slots['well-editor']" #well-editor="slotProps">
421
+ <slot
422
+ name="well-editor"
423
+ :rack="editor.activeRack.value"
424
+ :rack-id="editor.activeRack.value?.id ?? ''"
425
+ :well-id="slotProps.wellId"
426
+ :well-data="slotProps.wellData"
427
+ :edit-fields="slotProps.editFields"
428
+ :default-injection-volume="slotProps.defaultInjectionVolume"
429
+ :position="slotProps.position"
430
+ :save="slotProps.save"
431
+ :clear="slotProps.clear"
432
+ :close="slotProps.close"
433
+ />
434
+ </template>
435
+ </WellPlate>
365
436
  </div>
366
437
  </div>
367
438
  </template>
@@ -149,6 +149,7 @@ function canShowChildren(node: TreeNode, depth: number): boolean {
149
149
 
150
150
  // Render tree node recursively
151
151
  function renderNode(node: TreeNode, depth: number): VNode {
152
+ const children = node.children
152
153
  const expanded = isExpanded(node.id)
153
154
  const canExpand = hasChildren(node)
154
155
  const showChildNodes = canShowChildren(node, depth)
@@ -197,11 +198,11 @@ function renderNode(node: TreeNode, depth: number): VNode {
197
198
  )
198
199
 
199
200
  const childNodes =
200
- showChildNodes && node.children
201
+ showChildNodes && children
201
202
  ? h(
202
203
  Transition,
203
204
  { enterActiveClass: 'mint-sample-tree__children--entering', leaveActiveClass: 'mint-sample-tree__children--leaving' },
204
- () => h('div', { class: 'mint-sample-tree__children' }, node.children!.map((child) => renderNode(child, depth + 1)))
205
+ () => h('div', { class: 'mint-sample-tree__children' }, children.map((child) => renderNode(child, depth + 1)))
205
206
  )
206
207
  : null
207
208
 
@@ -11,23 +11,27 @@ import { useTextSearch } from '../composables/useTextSearch'
11
11
  import { useListSelection } from '../composables/useListSelection'
12
12
  import { useSampleGroups, type SampleMajorGroup } from '../composables/useSampleGroups'
13
13
  import { useExpansionSet } from '../composables/useExpansionSet'
14
+ import { useExperimentSamples } from '../composables/useExperimentSamples'
14
15
 
15
16
  interface Props {
16
- samples: string[]
17
+ samples?: string[]
17
18
  modelValue: string[]
18
19
  groups?: SampleGroup[]
19
20
  enableGrouping?: boolean
20
21
  enableSmartGroup?: boolean
21
22
  experimentId?: number
22
23
  designData?: Record<string, unknown>
24
+ autoloadExperimentData?: boolean
23
25
  }
24
26
 
25
27
  const props = withDefaults(defineProps<Props>(), {
28
+ samples: () => [],
26
29
  groups: () => [],
27
30
  enableGrouping: true,
28
31
  enableSmartGroup: true,
29
32
  experimentId: undefined,
30
33
  designData: undefined,
34
+ autoloadExperimentData: true,
31
35
  })
32
36
 
33
37
  const emit = defineEmits<{
@@ -66,8 +70,23 @@ const internalGroups = computed({
66
70
  set: (value) => emit('update:groups', value),
67
71
  })
68
72
 
73
+ const providedSamples = computed(() => props.samples)
74
+ const shouldLoadExperimentSamples = computed(() =>
75
+ props.autoloadExperimentData && providedSamples.value.length === 0,
76
+ )
77
+ const experimentSamples = useExperimentSamples({
78
+ experimentId: () => props.experimentId,
79
+ designData: () => props.designData,
80
+ enabled: shouldLoadExperimentSamples,
81
+ })
82
+ const resolvedSamples = computed(() =>
83
+ providedSamples.value.length > 0 ? providedSamples.value : experimentSamples.samples.value,
84
+ )
85
+ const resolvedExperimentId = computed(() => props.experimentId ?? experimentSamples.experimentId.value)
86
+ const resolvedDesignData = computed(() => props.designData ?? experimentSamples.designData.value ?? undefined)
87
+
69
88
  const sampleGroups = useSampleGroups({
70
- samples: () => props.samples,
89
+ samples: () => resolvedSamples.value,
71
90
  groups: internalGroups,
72
91
  })
73
92
  const hierarchicalGroups = sampleGroups.hierarchicalGroups
@@ -77,7 +96,7 @@ const groupingEnabled = computed(() => internalGroups.value.length > 0)
77
96
  const ungroupedSamples = sampleGroups.ungroupedSamples
78
97
 
79
98
  const sampleSearch = useTextSearch({
80
- items: () => props.samples,
99
+ items: () => resolvedSamples.value,
81
100
  query: searchQuery,
82
101
  getText: sample => sample,
83
102
  })
@@ -85,7 +104,7 @@ const filteredSamples = sampleSearch.filteredItems
85
104
 
86
105
  const sampleSelection = useListSelection({
87
106
  selected: () => props.modelValue,
88
- items: () => props.samples,
107
+ items: () => resolvedSamples.value,
89
108
  })
90
109
  const isAllSelected = sampleSelection.isAllSelected
91
110
 
@@ -433,7 +452,7 @@ function addNewGroup() {
433
452
  class="mint-sample-selector__checkbox"
434
453
  />
435
454
  <span class="mint-sample-selector__select-all-label">Select All</span>
436
- <span class="mint-sample-selector__select-all-count">{{ samples.length }} samples</span>
455
+ <span class="mint-sample-selector__select-all-count">{{ resolvedSamples.length }} samples</span>
437
456
  </label>
438
457
 
439
458
  <!-- Action Buttons Row -->
@@ -444,7 +463,7 @@ function addNewGroup() {
444
463
  v-if="enableSmartGroup"
445
464
  :variant="groupingEnabled ? 'primary' : 'secondary'"
446
465
  size="sm"
447
- :disabled="samples.length === 0"
466
+ :disabled="resolvedSamples.length === 0"
448
467
  class="mint-sample-selector__action-btn"
449
468
  @click="showSmartGroupModal = true"
450
469
  >
@@ -940,9 +959,9 @@ function addNewGroup() {
940
959
  <!-- Smart Grouping Modal -->
941
960
  <AutoGroupModal
942
961
  v-model="showSmartGroupModal"
943
- :samples="samples"
944
- :experiment-id="experimentId"
945
- :design-data="designData"
962
+ :samples="resolvedSamples"
963
+ :experiment-id="resolvedExperimentId"
964
+ :design-data="resolvedDesignData"
946
965
  @apply="handleSmartGroupApply"
947
966
  />
948
967
  </div>
@@ -33,7 +33,13 @@ const withDisabledOptions: SegmentedOption[] = [
33
33
  { value: 'deleted', label: 'Deleted', disabled: true },
34
34
  ]
35
35
 
36
+ const twoTabOptions: SegmentedOption[] = [
37
+ { value: 'table', label: 'Table' },
38
+ { value: 'chart', label: 'Chart' },
39
+ ]
40
+
36
41
  const cardSelectionP3 = ref('list')
42
+ const twoTabSelection = ref('table')
37
43
  </script>
38
44
 
39
45
  <template>
@@ -100,6 +106,17 @@ const cardSelectionP3 = ref('list')
100
106
  </div>
101
107
  </Variant>
102
108
 
109
+ <Variant title="Disabled By Value">
110
+ <div style="padding: 2rem; max-width: 500px; margin: 0 auto;">
111
+ <SegmentedControl
112
+ v-model="twoTabSelection"
113
+ :options="twoTabOptions"
114
+ :disabled-values="['chart']"
115
+ variant="simple"
116
+ />
117
+ </div>
118
+ </Variant>
119
+
103
120
  <Variant title="Not Full Width">
104
121
  <div style="padding: 2rem;">
105
122
  <SegmentedControl :model-value="'day'" :options="basicOptions" :full-width="false" />