@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
@@ -11,6 +11,7 @@ interface Props {
11
11
  size?: SegmentedControlSize
12
12
  fullWidth?: boolean
13
13
  disabled?: boolean
14
+ disabledValues?: Array<string | number>
14
15
  }
15
16
 
16
17
  const props = withDefaults(defineProps<Props>(), {
@@ -18,6 +19,7 @@ const props = withDefaults(defineProps<Props>(), {
18
19
  size: 'md',
19
20
  fullWidth: false,
20
21
  disabled: false,
22
+ disabledValues: () => [],
21
23
  })
22
24
 
23
25
  const emit = defineEmits<{
@@ -25,9 +27,18 @@ const emit = defineEmits<{
25
27
  }>()
26
28
 
27
29
  const normalizedOptions = computed<SegmentedOption[]>(() => props.options.map(normalizeOptionInput))
30
+ const disabledValueSet = computed(() => new Set(props.disabledValues))
31
+
32
+ function isOptionDisabled(option: SegmentedOption) {
33
+ return option.disabled === true || disabledValueSet.value.has(option.value)
34
+ }
35
+
36
+ function isSelectionDisabled(option: SegmentedOption) {
37
+ return props.disabled || isOptionDisabled(option)
38
+ }
28
39
 
29
40
  function handleSelect(option: SegmentedOption) {
30
- if (props.disabled || option.disabled) return
41
+ if (isSelectionDisabled(option)) return
31
42
  emit('update:modelValue', option.value)
32
43
  }
33
44
 
@@ -56,13 +67,13 @@ function handleKeydown(event: KeyboardEvent, option: SegmentedOption) {
56
67
  type="button"
57
68
  role="radio"
58
69
  :aria-checked="modelValue === option.value"
59
- :disabled="disabled || option.disabled"
70
+ :disabled="isSelectionDisabled(option)"
60
71
  :class="[
61
72
  'mint-segmented-control__option',
62
73
  `mint-segmented-control__option--${variant}`,
63
74
  `mint-segmented-control__option--${size}`,
64
75
  modelValue === option.value ? 'mint-segmented-control__option--active' : '',
65
- option.disabled ? 'mint-segmented-control__option--disabled' : '',
76
+ isOptionDisabled(option) ? 'mint-segmented-control__option--disabled' : '',
66
77
  ]"
67
78
  @click="handleSelect(option)"
68
79
  @keydown="handleKeydown($event, option)"
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ /** Displays LC/MS or GC/MS acquisition sequence progress with ETA and remaining-time labels. */
3
+ import { computed } from 'vue'
4
+ import {
5
+ formatSequenceEta,
6
+ formatSequenceRemaining,
7
+ sequenceProgressPercent,
8
+ } from '../instrument'
9
+ import type { SequenceProgress } from '../types'
10
+ import ProgressBar from './ProgressBar.vue'
11
+
12
+ interface Props {
13
+ progress: SequenceProgress
14
+ compact?: boolean
15
+ label?: string
16
+ showEta?: boolean
17
+ showRemaining?: boolean
18
+ }
19
+
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ compact: false,
22
+ label: 'samples',
23
+ showEta: true,
24
+ showRemaining: true,
25
+ })
26
+
27
+ const percent = computed(() => sequenceProgressPercent(props.progress))
28
+ const remainingLabel = computed(() => formatSequenceRemaining(props.progress))
29
+ const etaLabel = computed(() => formatSequenceEta(props.progress))
30
+ const hasFooter = computed(() => {
31
+ if (props.compact) return false
32
+ return (props.showRemaining && remainingLabel.value) || (props.showEta && etaLabel.value)
33
+ })
34
+ </script>
35
+
36
+ <template>
37
+ <div class="mint-sequence-progress">
38
+ <div class="mint-sequence-progress__header">
39
+ <span class="mint-sequence-progress__label">
40
+ {{ progress.current_sample }} / {{ progress.total_samples }} {{ label }}
41
+ </span>
42
+ <span class="mint-sequence-progress__percent">{{ percent }}%</span>
43
+ </div>
44
+
45
+ <ProgressBar
46
+ :value="percent"
47
+ color="info"
48
+ size="sm"
49
+ aria-label="Sequence progress"
50
+ />
51
+
52
+ <div v-if="hasFooter" class="mint-sequence-progress__footer">
53
+ <span
54
+ v-if="showRemaining && remainingLabel"
55
+ class="mint-sequence-progress__remaining"
56
+ >
57
+ {{ remainingLabel }}
58
+ </span>
59
+ <span
60
+ v-if="showEta && etaLabel"
61
+ class="mint-sequence-progress__eta"
62
+ >
63
+ ETA {{ etaLabel }}
64
+ </span>
65
+ </div>
66
+ </div>
67
+ </template>
68
+
69
+ <style>
70
+ @import '../styles/components/sequence-progress-bar.css';
71
+ </style>
@@ -32,6 +32,9 @@ import {
32
32
  type ControlWorkspaceOptions,
33
33
  } from '../composables/useControlSchema'
34
34
  import { useSettingsStore, colorPalettes } from '../stores/settings'
35
+ import { useAuthStore } from '../stores/auth'
36
+ import { usePlatformContext } from '../composables/usePlatformContext'
37
+ import { canAccessByPolicy } from '../permissions'
35
38
  import {
36
39
  formSchemaFieldNames,
37
40
  pickExistingRecordKeys,
@@ -46,10 +49,12 @@ import type {
46
49
  SettingsTabInput,
47
50
  SettingsModalLayout,
48
51
  SettingsModalSchema,
52
+ SettingsUserType,
49
53
  FormSchema,
50
54
  FormSectionSchema,
51
55
  FormEnhancements,
52
56
  } from '../types'
57
+ import type { AccessControlled, PermissionUser } from '../permissions'
53
58
  import { normalizeItemInput } from '../utils/items'
54
59
 
55
60
  // Map our settings groups onto the form-builder's flat-section shape.
@@ -86,6 +91,8 @@ interface Props {
86
91
  values?: Record<string, unknown>
87
92
  /** Optional dynamic enhancements (validators, dynamic options, callbacks). */
88
93
  enhancements?: FormEnhancements<Record<string, unknown>>
94
+ /** Optional user type override for permission-filtered settings content. Defaults to SDK auth/platform context. */
95
+ userType?: SettingsUserType
89
96
  }
90
97
 
91
98
  const props = withDefaults(defineProps<Props>(), {
@@ -104,6 +111,8 @@ const emit = defineEmits<{
104
111
  }>()
105
112
 
106
113
  const settings = useSettingsStore()
114
+ const auth = useAuthStore()
115
+ const { user: platformUser } = usePlatformContext()
107
116
 
108
117
  const APPEARANCE_TAB_ID = 'appearance'
109
118
 
@@ -130,13 +139,34 @@ const resolvedControls = computed<ControlSchema | undefined>(() =>
130
139
  const resolvedControlOptions = computed<ControlWorkspaceOptions>(() =>
131
140
  mergeControlWorkspaceOptions(resolvedModel.value?.controlOptions ?? {}, props.controlOptions)
132
141
  )
133
- const settingsSchema = computed<SettingsModalSchema | undefined>(() =>
142
+ const sourceSettingsSchema = computed<SettingsModalSchema | undefined>(() =>
134
143
  props.schema ?? (
135
144
  resolvedControls.value
136
145
  ? controlsToSettingsSchema(resolvedControls.value, resolvedControlOptions.value)
137
146
  : undefined
138
147
  ),
139
148
  )
149
+ const currentAccessUser = computed<PermissionUser | null>(() => {
150
+ if (props.userType) {
151
+ return { role: props.userType === 'admin' ? 'admin' : 'user' }
152
+ }
153
+ return auth.userInfo ?? platformUser.value ?? null
154
+ })
155
+ const isAccessUserAuthenticated = computed(() =>
156
+ props.userType !== undefined || auth.isAuthenticated || !!platformUser.value,
157
+ )
158
+ const settingsSchema = computed<SettingsModalSchema | undefined>(() => {
159
+ if (!sourceSettingsSchema.value) return undefined
160
+ const groups = sourceSettingsSchema.value.groups.flatMap((group) => {
161
+ if (!canShowForCurrentUser(group)) return []
162
+
163
+ const fields = group.fields.filter(canShowForCurrentUser)
164
+ if (fields.length === 0) return []
165
+
166
+ return [{ ...group, fields }]
167
+ })
168
+ return { groups }
169
+ })
140
170
  const isSchemaDriven = computed(() => !!settingsSchema.value)
141
171
  const resolvedValues = computed<Record<string, unknown>>(() => ({
142
172
  ...(resolvedControlOptions.value.initialValues ?? {}),
@@ -201,7 +231,9 @@ const visibleSchemaGroups = computed(() =>
201
231
  : [],
202
232
  )
203
233
 
204
- const manualTabs = computed<SettingsTab[]>(() => props.tabs.map(normalizeItemInput))
234
+ const manualTabs = computed<SettingsTab[]>(() =>
235
+ props.tabs.map(normalizeItemInput).filter(canShowForCurrentUser)
236
+ )
205
237
 
206
238
  const allTabs = computed<SettingsTab[]>(() => {
207
239
  const base: SettingsTab[] = settingsSchema.value
@@ -256,6 +288,14 @@ function builderFieldNames(): string[] {
256
288
  function isControlModelBinding(model: ControlModel | ControlModelBinding): model is ControlModelBinding {
257
289
  return 'controls' in model && 'controlOptions' in model
258
290
  }
291
+
292
+ function canShowForCurrentUser(item: AccessControlled): boolean {
293
+ return canAccessByPolicy(
294
+ currentAccessUser.value,
295
+ item,
296
+ isAccessUserAuthenticated.value,
297
+ )
298
+ }
259
299
  </script>
260
300
 
261
301
  <template>
@@ -416,6 +456,13 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
416
456
  </div>
417
457
  </component>
418
458
  </div>
459
+ <div v-if="$slots.footer" class="mint-settings-modal__footer">
460
+ <slot
461
+ name="footer"
462
+ :values="builder.form.data"
463
+ :close="handleClose"
464
+ />
465
+ </div>
419
466
  </BaseModal>
420
467
  </template>
421
468
 
@@ -47,8 +47,12 @@ const groupedUnits = computed(() => {
47
47
  const ungrouped: UnitOption[] = []
48
48
  for (const u of props.units) {
49
49
  if (u.group) {
50
- if (!groups.has(u.group)) groups.set(u.group, [])
51
- groups.get(u.group)!.push(u)
50
+ const group = groups.get(u.group)
51
+ if (group) {
52
+ group.push(u)
53
+ } else {
54
+ groups.set(u.group, [u])
55
+ }
52
56
  } else {
53
57
  ungrouped.push(u)
54
58
  }
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /** Interactive 96/384-well plate grid with drag-to-select, heatmap overlays, per-well editing, and sample-color mapping. */
3
3
  import { ref, computed } from 'vue'
4
- import type { WellPlateFormat, WellPlateSelectionMode, WellPlateSize, Well, HeatmapConfig, WellShape, WellEditField, WellEditData, WellLegendItem, ColumnCondition, RowCondition } from '../types'
4
+ import type { WellPlateFormat, WellPlateSelectionMode, WellPlateSize, Well, HeatmapConfig, WellShape, WellEditField, WellEditData, WellLegendItem, ColumnCondition, RowCondition, WellSampleDropData, WellSampleDropParser } from '../types'
5
5
  import { useEventListener } from '../composables/useEventListener'
6
6
  import WellEditPopupInternal from './internal/WellEditPopupInternal.vue'
7
7
 
@@ -29,6 +29,8 @@ interface Props {
29
29
  legendItems?: WellLegendItem[]
30
30
  columnConditions?: ColumnCondition[]
31
31
  rowConditions?: RowCondition[]
32
+ allowSampleDrop?: boolean
33
+ sampleDropParser?: WellSampleDropParser
32
34
  }
33
35
 
34
36
  // Drag state for moving wells
@@ -59,6 +61,8 @@ const props = withDefaults(defineProps<Props>(), {
59
61
  legendItems: undefined,
60
62
  columnConditions: () => [],
61
63
  rowConditions: () => [],
64
+ allowSampleDrop: false,
65
+ sampleDropParser: undefined,
62
66
  })
63
67
 
64
68
  const emit = defineEmits<{
@@ -70,6 +74,22 @@ const emit = defineEmits<{
70
74
  'well-move': [sourceWellId: string, targetWellId: string]
71
75
  'well-edit': [wellId: string, data: WellEditData]
72
76
  'well-clear': [wellId: string]
77
+ 'sample-drop': [wellId: string, data: WellSampleDropData, event: DragEvent]
78
+ }>()
79
+
80
+ interface WellEditorSlotProps {
81
+ wellId: string
82
+ wellData?: Partial<Well>
83
+ editFields: WellEditField[]
84
+ defaultInjectionVolume: number
85
+ position: { x: number; y: number }
86
+ save: (data: WellEditData) => void
87
+ clear: () => void
88
+ close: () => void
89
+ }
90
+
91
+ defineSlots<{
92
+ 'well-editor'?: (props: WellEditorSlotProps) => unknown
73
93
  }>()
74
94
 
75
95
  const plateRef = ref<HTMLElement | null>(null)
@@ -164,6 +184,7 @@ const defaultLegendItems: WellLegendItem[] = [
164
184
  { type: 'sample', label: 'Sample', color: '#10b981' },
165
185
  { type: 'blank', label: 'Blank', color: '#f97316' },
166
186
  { type: 'qc', label: 'QC', color: '#8b5cf6' },
187
+ { type: 'iqc', label: 'IQC', color: '#ec4899' },
167
188
  ]
168
189
 
169
190
  const activeLegendItems = computed(() => props.legendItems ?? defaultLegendItems)
@@ -285,6 +306,7 @@ const defaultSampleTypeColors: Record<string, { bg: string; border: string }> =
285
306
  control: { bg: 'rgba(59, 130, 246, 0.15)', border: 'rgba(59, 130, 246, 0.4)' },
286
307
  blank: { bg: 'rgba(249, 115, 22, 0.15)', border: 'rgba(249, 115, 22, 0.4)' },
287
308
  qc: { bg: 'rgba(139, 92, 246, 0.15)', border: 'rgba(139, 92, 246, 0.4)' },
309
+ iqc: { bg: 'rgba(236, 72, 153, 0.15)', border: 'rgba(236, 72, 153, 0.4)' },
288
310
  }
289
311
 
290
312
  const heatmapColors: Record<string, string[]> = {
@@ -373,6 +395,7 @@ function getSampleTypeIndicator(well: Well): string | null {
373
395
  control: 'C',
374
396
  blank: 'B',
375
397
  qc: 'Q',
398
+ iqc: 'I',
376
399
  }
377
400
  return typeMap[well.sampleType] || well.sampleType.charAt(0).toUpperCase()
378
401
  }
@@ -511,10 +534,12 @@ function handleDragStart(well: Well, event: DragEvent) {
511
534
  }
512
535
 
513
536
  function handleDragOver(well: Well, event: DragEvent) {
514
- if (props.selectionMode !== 'drag' || !dragSourceWell.value) return
537
+ const isMovingWell = props.selectionMode === 'drag' && !!dragSourceWell.value
538
+ if (!isMovingWell && !props.allowSampleDrop) return
539
+
515
540
  event.preventDefault()
516
541
  if (event.dataTransfer) {
517
- event.dataTransfer.dropEffect = 'move'
542
+ event.dataTransfer.dropEffect = isMovingWell ? 'move' : 'copy'
518
543
  }
519
544
  dragTargetWell.value = well.id
520
545
  }
@@ -524,14 +549,27 @@ function handleDragLeave() {
524
549
  }
525
550
 
526
551
  function handleDrop(well: Well, event: DragEvent) {
552
+ const isMovingWell = props.selectionMode === 'drag' && !!dragSourceWell.value
553
+ if (!isMovingWell && !props.allowSampleDrop) return
554
+
527
555
  event.preventDefault()
528
- if (props.selectionMode !== 'drag' || !dragSourceWell.value) return
529
556
 
530
- const sourceId = dragSourceWell.value
531
- const targetId = well.id
557
+ if (isMovingWell && dragSourceWell.value) {
558
+ const sourceId = dragSourceWell.value
559
+ const targetId = well.id
560
+
561
+ if (sourceId !== targetId) {
562
+ emit('well-move', sourceId, targetId)
563
+ }
532
564
 
533
- if (sourceId !== targetId) {
534
- emit('well-move', sourceId, targetId)
565
+ dragSourceWell.value = null
566
+ dragTargetWell.value = null
567
+ return
568
+ }
569
+
570
+ const droppedSample = parseDroppedSample(event, well.id)
571
+ if (droppedSample) {
572
+ emit('sample-drop', well.id, droppedSample, event)
535
573
  }
536
574
 
537
575
  dragSourceWell.value = null
@@ -543,6 +581,76 @@ function handleDragEnd() {
543
581
  dragTargetWell.value = null
544
582
  }
545
583
 
584
+ function parseDroppedSample(event: DragEvent, wellId: string): WellSampleDropData | null {
585
+ if (props.sampleDropParser) {
586
+ return props.sampleDropParser(event, { wellId, event }) ?? null
587
+ }
588
+
589
+ const transfer = event.dataTransfer
590
+ if (!transfer) return null
591
+
592
+ const json = transfer.getData('application/json') || transfer.getData('text/json')
593
+ if (json) {
594
+ try {
595
+ return normalizeDroppedSample(JSON.parse(json))
596
+ } catch {
597
+ return null
598
+ }
599
+ }
600
+
601
+ const text = transfer.getData('text/plain')?.trim()
602
+ if (!text) return null
603
+ return {
604
+ sampleName: text,
605
+ label: text,
606
+ raw: text,
607
+ }
608
+ }
609
+
610
+ function normalizeDroppedSample(raw: unknown): WellSampleDropData | null {
611
+ if (!raw || typeof raw !== 'object') return null
612
+
613
+ const record = raw as Record<string, unknown>
614
+ const sampleName =
615
+ getString(record.sampleName) ??
616
+ getString(record.sample_name) ??
617
+ getString(record.name) ??
618
+ getString(record.label)
619
+ const label = getString(record.label) ?? sampleName
620
+ const sampleType =
621
+ getString(record.sampleType) ??
622
+ getString(record.sample_type) ??
623
+ (record.type === 'sample' ? 'sample' : undefined)
624
+
625
+ if (!sampleName && !label && !getString(record.id)) return null
626
+
627
+ return {
628
+ id: getString(record.id),
629
+ sampleName,
630
+ label,
631
+ sampleType,
632
+ injectionVolume: getNumber(record.injectionVolume ?? record.injection_volume),
633
+ injectionCount: getNumber(record.injectionCount ?? record.injection_count),
634
+ customMethod: getString(record.customMethod ?? record.custom_method) ?? null,
635
+ metadata: getRecord(record.metadata),
636
+ raw,
637
+ }
638
+ }
639
+
640
+ function getString(value: unknown): string | undefined {
641
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined
642
+ }
643
+
644
+ function getNumber(value: unknown): number | undefined {
645
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
646
+ }
647
+
648
+ function getRecord(value: unknown): Record<string, unknown> | undefined {
649
+ return value && typeof value === 'object' && !Array.isArray(value)
650
+ ? value as Record<string, unknown>
651
+ : undefined
652
+ }
653
+
546
654
  function handleKeyDown(event: KeyboardEvent) {
547
655
  if (props.disabled || props.readonly) return
548
656
 
@@ -726,9 +834,9 @@ const tableStyle = computed(() => ({
726
834
  @mouseleave="handleWellMouseLeave"
727
835
  @contextmenu="handleContextMenu(well, $event)"
728
836
  @dragstart="handleDragStart(well, $event)"
729
- @dragover.prevent="handleDragOver(well, $event)"
837
+ @dragover="handleDragOver(well, $event)"
730
838
  @dragleave="handleDragLeave"
731
- @drop.prevent="handleDrop(well, $event)"
839
+ @drop="handleDrop(well, $event)"
732
840
  @dragend="handleDragEnd"
733
841
  @keydown.enter="handleWellClick(well, $event as unknown as MouseEvent)"
734
842
  @keydown.space.prevent="handleWellClick(well, $event as unknown as MouseEvent)"
@@ -759,10 +867,10 @@ const tableStyle = computed(() => ({
759
867
  <span
760
868
  v-if="getWellBadge(well)"
761
869
  class="mint-well-plate__badge"
762
- :style="{ backgroundColor: getWellBadge(well)!.color }"
763
- :title="getWellBadge(well)!.text === '+' ? 'Custom method' : `${getWellBadge(well)!.text}x injections`"
870
+ :style="{ backgroundColor: getWellBadge(well)?.color }"
871
+ :title="getWellBadge(well)?.text === '+' ? 'Custom method' : `${getWellBadge(well)?.text ?? ''}x injections`"
764
872
  >
765
- {{ getWellBadge(well)!.text }}
873
+ {{ getWellBadge(well)?.text }}
766
874
  </span>
767
875
  </div>
768
876
  </td>
@@ -805,17 +913,30 @@ const tableStyle = computed(() => ({
805
913
  </div>
806
914
 
807
915
  <!-- Edit popup -->
808
- <WellEditPopupInternal
809
- v-if="editable && editingWellId"
810
- :well-id="editingWellId"
811
- :well-data="wells[editingWellId]"
812
- :edit-fields="editFields"
813
- :default-injection-volume="defaultInjectionVolume"
814
- :position="editPopupPosition"
815
- @save="handleEditSave"
816
- @clear="handleEditClear"
817
- @close="handleEditClose"
818
- />
916
+ <template v-if="editable && editingWellId">
917
+ <slot
918
+ name="well-editor"
919
+ :well-id="editingWellId"
920
+ :well-data="wells[editingWellId]"
921
+ :edit-fields="editFields"
922
+ :default-injection-volume="defaultInjectionVolume"
923
+ :position="editPopupPosition"
924
+ :save="handleEditSave"
925
+ :clear="handleEditClear"
926
+ :close="handleEditClose"
927
+ >
928
+ <WellEditPopupInternal
929
+ :well-id="editingWellId"
930
+ :well-data="wells[editingWellId]"
931
+ :edit-fields="editFields"
932
+ :default-injection-volume="defaultInjectionVolume"
933
+ :position="editPopupPosition"
934
+ @save="handleEditSave"
935
+ @clear="handleEditClear"
936
+ @close="handleEditClose"
937
+ />
938
+ </slot>
939
+ </template>
819
940
  </div>
820
941
  </template>
821
942
 
@@ -89,6 +89,11 @@ export { default as ProtocolStepEditor } from './ProtocolStepEditor.vue'
89
89
  // Scientific display components
90
90
  export { default as ScientificNumber } from './ScientificNumber.vue'
91
91
  export { default as ChemicalFormula } from './ChemicalFormula.vue'
92
+ export { default as SequenceProgressBar } from './SequenceProgressBar.vue'
93
+ export { default as InstrumentAlertLog } from './InstrumentAlertLog.vue'
94
+ export { default as InstrumentStateBadge } from './InstrumentStateBadge.vue'
95
+ export { default as InstrumentStatusCard } from './InstrumentStatusCard.vue'
96
+ export { default as LcmsSequenceTable } from './LcmsSequenceTable.vue'
92
97
 
93
98
  // Scientific input components
94
99
  export { default as FormulaInput } from './FormulaInput.vue'
@@ -90,6 +90,7 @@ const sampleTypeButtons = [
90
90
  { type: 'sample', label: 'S', tooltip: 'Sample' },
91
91
  { type: 'blank', label: 'B', tooltip: 'Blank' },
92
92
  { type: 'qc', label: 'Q', tooltip: 'QC' },
93
+ { type: 'iqc', label: 'I', tooltip: 'IQC' },
93
94
  ]
94
95
  </script>
95
96