@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
@@ -94,6 +94,15 @@ html.dark .mint-settings-modal__option-btn--active {
94
94
  padding: 0 !important;
95
95
  }
96
96
 
97
+ .mint-settings-modal__footer {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 0.75rem;
101
+ margin-top: 1rem;
102
+ padding-top: 1rem;
103
+ border-top: 1px solid var(--border-light);
104
+ }
105
+
97
106
  /* ─────────────────────────────────────────────────────────────────────
98
107
  * Schema-driven content grid (used by both layouts)
99
108
  * Cols come from `--mint-settings-cols` set inline by the component;
@@ -9,10 +9,13 @@
9
9
  }
10
10
 
11
11
  .mint-tabs--pills {
12
+ width: fit-content;
13
+ max-width: 100%;
12
14
  gap: 0.5rem;
13
15
  padding: 0.25rem;
14
16
  background-color: var(--bg-tertiary);
15
17
  border-radius: var(--radius-md);
18
+ overflow-x: auto;
16
19
  }
17
20
 
18
21
  .mint-tab {
@@ -84,6 +87,12 @@
84
87
  color: currentColor;
85
88
  }
86
89
 
90
+ .mint-tab__icon--svg {
91
+ width: 1rem;
92
+ height: 1rem;
93
+ flex-shrink: 0;
94
+ }
95
+
87
96
  .mint-tab__badge {
88
97
  margin-left: 0.25rem;
89
98
  padding: 0.125rem 0.375rem;
@@ -120,7 +120,7 @@
120
120
  /* Sample type buttons */
121
121
  .mint-well-edit-popup__type-grid {
122
122
  display: grid;
123
- grid-template-columns: repeat(3, 1fr);
123
+ grid-template-columns: repeat(4, 1fr);
124
124
  gap: 0.25rem;
125
125
  }
126
126
 
@@ -154,6 +154,12 @@
154
154
  color: #8b5cf6;
155
155
  }
156
156
 
157
+ .mint-well-edit-popup__type-btn--iqc {
158
+ background-color: rgba(236, 72, 153, 0.15);
159
+ border-color: rgba(236, 72, 153, 0.4);
160
+ color: #ec4899;
161
+ }
162
+
157
163
  /* Injection count buttons */
158
164
  .mint-well-edit-popup__count-grid {
159
165
  display: grid;
@@ -183,6 +183,11 @@
183
183
  border: 1px solid rgba(139, 92, 246, 0.4);
184
184
  }
185
185
 
186
+ .mint-well-plate__well--iqc {
187
+ background-color: rgba(236, 72, 153, 0.15);
188
+ border: 1px solid rgba(236, 72, 153, 0.4);
189
+ }
190
+
186
191
  /* Condition header - drug label row */
187
192
  .mint-well-plate__condition-label {
188
193
  font-weight: 600;
@@ -20,6 +20,7 @@
20
20
  @import './components/file-uploader.css';
21
21
  @import './components/form-field.css';
22
22
  @import './components/icon-button.css';
23
+ @import './components/instrument-monitor.css';
23
24
  @import './components/input.css';
24
25
  @import './components/modal.css';
25
26
  @import './components/number-input.css';
@@ -46,6 +47,7 @@
46
47
  @import './components/sample-hierarchy-tree.css';
47
48
  @import './components/protocol-step-editor.css';
48
49
  @import './components/segmented-control.css';
50
+ @import './components/sequence-progress-bar.css';
49
51
  @import './components/multi-select.css';
50
52
  @import './components/pill.css';
51
53
  @import './components/dropdown-button.css';
@@ -54,6 +56,7 @@
54
56
  @import './components/well-edit-popup.css';
55
57
  @import './components/rack-editor.css';
56
58
  @import './components/loading-spinner.css';
59
+ @import './components/lcms-sequence-table.css';
57
60
  @import './components/divider.css';
58
61
  @import './components/status-indicator.css';
59
62
  @import './components/progress-bar.css';
@@ -14,6 +14,7 @@ import type {
14
14
  CreateBioTemplatePackCollectionOptions,
15
15
  CreateElisaAssayCollectionOptions,
16
16
  CreateLcmsBatchCollectionOptions,
17
+ CreateTargetedMetabolomicsCollectionOptions,
17
18
  CreateAssayMatrixTemplateOptions,
18
19
  CreateCalibrationCurveTemplateOptions,
19
20
  CreateDoseResponseTemplateOptions,
@@ -38,6 +39,7 @@ import type {
38
39
  FlowPanelMarker,
39
40
  InstrumentMethod,
40
41
  InstrumentRunItem,
42
+ InstrumentRunItemInput,
41
43
  InstrumentRunTemplate,
42
44
  InstrumentRunTemplateData,
43
45
  PlateMapTemplate,
@@ -858,6 +860,67 @@ export function createLcmsBatchCollection(
858
860
  )
859
861
  }
860
862
 
863
+ export function createTargetedMetabolomicsCollection(
864
+ options: CreateTargetedMetabolomicsCollectionOptions = {}
865
+ ): TemplateCollectionEnvelope {
866
+ const sampleRecords = presetSampleRecords(options.samples ?? ['S001', 'S002'])
867
+ const responseUnit = options.responseUnit ?? 'peak area ratio'
868
+ const concentrationUnit = options.concentrationUnit ?? 'uM'
869
+ const metaboliteFeatures = normalizeMetaboliteFeatures(
870
+ options.metabolites ?? ['Glucose', 'Lactate', 'Pyruvate'],
871
+ responseUnit,
872
+ )
873
+ const sampleSheet = createSampleSheetTemplate({ samples: sampleRecords })
874
+ const samplePrep = createSamplePrepTemplate({
875
+ samples: sampleRecords.map(sample => sample.sampleId),
876
+ prepType: options.prepType ?? 'extraction',
877
+ protocolName: 'Metabolomics sample preparation',
878
+ outputVolume: options.outputVolume ?? 50,
879
+ metadata: { assay: 'targeted-metabolomics' },
880
+ })
881
+ const reagents = createReagentListTemplate({
882
+ reagents: normalizeInternalStandardReagents(
883
+ options.internalStandards ?? ['Stable isotope internal standard mix'],
884
+ ),
885
+ metadata: { assay: 'targeted-metabolomics' },
886
+ })
887
+ const calibrationCurve = createCalibrationCurveTemplate({
888
+ concentrations: options.standardConcentrations ?? [0.1, 1, 10, 100],
889
+ analyte: options.calibrationAnalyte ?? firstFeatureName(metaboliteFeatures, 'Metabolite panel'),
890
+ unit: concentrationUnit,
891
+ responseUnit,
892
+ model: 'linear-weighted-1-x',
893
+ includeQc: options.includeQc,
894
+ metadata: { assay: 'targeted-metabolomics' },
895
+ })
896
+ const instrumentRun = createInstrumentRunTemplate({
897
+ items: targetedMetabolomicsRunItems(
898
+ sampleRecords,
899
+ calibrationCurve.data.points,
900
+ options.includeQc !== false,
901
+ ),
902
+ method: options.method ?? 'Targeted metabolomics',
903
+ instrument: options.instrument ?? 'LC-MS',
904
+ includeBlanks: false,
905
+ includeQc: false,
906
+ metadata: { assay: 'targeted-metabolomics' },
907
+ })
908
+ const assayMatrix = createAssayMatrixTemplate({
909
+ samples: sampleRecords.map(sample => ({
910
+ sampleId: sample.sampleId,
911
+ name: sample.name,
912
+ group: sample.group,
913
+ metadata: sample.metadata ?? {},
914
+ })),
915
+ features: metaboliteFeatures,
916
+ metadata: { assay: 'targeted-metabolomics', responseUnit },
917
+ })
918
+ return createTemplateCollection(
919
+ [sampleSheet, samplePrep, reagents, instrumentRun, calibrationCurve, assayMatrix],
920
+ { preset: 'targeted-metabolomics', ...(options.metadata ?? {}) }
921
+ )
922
+ }
923
+
861
924
  export function createElisaAssayCollection(
862
925
  options: CreateElisaAssayCollectionOptions = {}
863
926
  ): TemplateCollectionEnvelope {
@@ -1009,6 +1072,10 @@ export function createBioTemplatePresetCollection(
1009
1072
  name: 'lcms-batch',
1010
1073
  options?: CreateLcmsBatchCollectionOptions
1011
1074
  ): TemplateCollectionEnvelope
1075
+ export function createBioTemplatePresetCollection(
1076
+ name: 'targeted-metabolomics',
1077
+ options?: CreateTargetedMetabolomicsCollectionOptions
1078
+ ): TemplateCollectionEnvelope
1012
1079
  export function createBioTemplatePresetCollection(
1013
1080
  name: 'elisa-assay',
1014
1081
  options?: CreateElisaAssayCollectionOptions
@@ -1027,6 +1094,7 @@ export function createBioTemplatePresetCollection(
1027
1094
  | CreateWellPlateScreenCollectionOptions
1028
1095
  | CreateQpcrExpressionCollectionOptions
1029
1096
  | CreateLcmsBatchCollectionOptions
1097
+ | CreateTargetedMetabolomicsCollectionOptions
1030
1098
  | CreateElisaAssayCollectionOptions
1031
1099
  | CreateFlowCytometryAssayCollectionOptions
1032
1100
  | CreateWesternBlotAssayCollectionOptions
@@ -1037,6 +1105,7 @@ export function createBioTemplatePresetCollection(
1037
1105
  | CreateWellPlateScreenCollectionOptions
1038
1106
  | CreateQpcrExpressionCollectionOptions
1039
1107
  | CreateLcmsBatchCollectionOptions
1108
+ | CreateTargetedMetabolomicsCollectionOptions
1040
1109
  | CreateElisaAssayCollectionOptions
1041
1110
  | CreateFlowCytometryAssayCollectionOptions
1042
1111
  | CreateWesternBlotAssayCollectionOptions = {}
@@ -1054,6 +1123,9 @@ export function createBioTemplatePresetCollection(
1054
1123
  if (preset.name === 'lcms-batch') {
1055
1124
  return createLcmsBatchCollection(options as CreateLcmsBatchCollectionOptions)
1056
1125
  }
1126
+ if (preset.name === 'targeted-metabolomics') {
1127
+ return createTargetedMetabolomicsCollection(options as CreateTargetedMetabolomicsCollectionOptions)
1128
+ }
1057
1129
  if (preset.name === 'elisa-assay') {
1058
1130
  return createElisaAssayCollection(options as CreateElisaAssayCollectionOptions)
1059
1131
  }
@@ -1080,6 +1152,10 @@ export function bioTemplatePresetControlValuesToOptions(
1080
1152
  name: 'lcms-batch',
1081
1153
  values: BioTemplateControlValues
1082
1154
  ): CreateLcmsBatchCollectionOptions
1155
+ export function bioTemplatePresetControlValuesToOptions(
1156
+ name: 'targeted-metabolomics',
1157
+ values: BioTemplateControlValues
1158
+ ): CreateTargetedMetabolomicsCollectionOptions
1083
1159
  export function bioTemplatePresetControlValuesToOptions(
1084
1160
  name: 'elisa-assay',
1085
1161
  values: BioTemplateControlValues
@@ -1099,6 +1175,7 @@ export function bioTemplatePresetControlValuesToOptions(
1099
1175
  | CreateWellPlateScreenCollectionOptions
1100
1176
  | CreateQpcrExpressionCollectionOptions
1101
1177
  | CreateLcmsBatchCollectionOptions
1178
+ | CreateTargetedMetabolomicsCollectionOptions
1102
1179
  | CreateElisaAssayCollectionOptions
1103
1180
  | CreateFlowCytometryAssayCollectionOptions
1104
1181
  | CreateWesternBlotAssayCollectionOptions
@@ -1109,6 +1186,7 @@ export function bioTemplatePresetControlValuesToOptions(
1109
1186
  | CreateWellPlateScreenCollectionOptions
1110
1187
  | CreateQpcrExpressionCollectionOptions
1111
1188
  | CreateLcmsBatchCollectionOptions
1189
+ | CreateTargetedMetabolomicsCollectionOptions
1112
1190
  | CreateElisaAssayCollectionOptions
1113
1191
  | CreateFlowCytometryAssayCollectionOptions
1114
1192
  | CreateWesternBlotAssayCollectionOptions {
@@ -1149,6 +1227,26 @@ export function bioTemplatePresetControlValuesToOptions(
1149
1227
  }
1150
1228
  }
1151
1229
 
1230
+ if (preset.name === 'targeted-metabolomics') {
1231
+ return {
1232
+ samples: readStringList(values.sampleNames, ['S001', 'S002']),
1233
+ metabolites: readStringList(values.metaboliteNames, ['Glucose', 'Lactate', 'Pyruvate']),
1234
+ internalStandards: readStringList(
1235
+ values.internalStandards,
1236
+ ['Stable isotope internal standard mix'],
1237
+ ),
1238
+ standardConcentrations: readNumberList(values.standardConcentrations, [0.1, 1, 10, 100]),
1239
+ concentrationUnit: readString(values.concentrationUnit, 'uM'),
1240
+ responseUnit: readString(values.responseUnit, 'peak area ratio'),
1241
+ calibrationAnalyte: readOptionalString(values.calibrationAnalyte),
1242
+ instrument: readString(values.instrument, 'LC-MS'),
1243
+ method: readString(values.method, 'Targeted metabolomics'),
1244
+ prepType: readString(values.prepType, 'extraction') as CreateSamplePrepTemplateOptions['prepType'],
1245
+ outputVolume: readNumber(values.outputVolume, 50),
1246
+ includeQc: readBoolean(values.includeQc, true),
1247
+ }
1248
+ }
1249
+
1152
1250
  if (preset.name === 'elisa-assay') {
1153
1251
  return {
1154
1252
  samples: readStringList(values.sampleNames, ['Control', 'Treatment']),
@@ -1194,6 +1292,10 @@ export function createBioTemplatePresetCollectionFromControls(
1194
1292
  name: 'lcms-batch',
1195
1293
  values: BioTemplateControlValues
1196
1294
  ): TemplateCollectionEnvelope
1295
+ export function createBioTemplatePresetCollectionFromControls(
1296
+ name: 'targeted-metabolomics',
1297
+ values: BioTemplateControlValues
1298
+ ): TemplateCollectionEnvelope
1197
1299
  export function createBioTemplatePresetCollectionFromControls(
1198
1300
  name: 'elisa-assay',
1199
1301
  values: BioTemplateControlValues
@@ -1863,6 +1965,96 @@ function normalizeReagent(reagent: ReagentTemplateInput): ReagentRecord {
1863
1965
  }
1864
1966
  }
1865
1967
 
1968
+ function normalizeMetaboliteFeatures(
1969
+ metabolites: Array<string | AssayFeature>,
1970
+ responseUnit: string,
1971
+ ): AssayFeature[] {
1972
+ return metabolites.map((metabolite, index) => {
1973
+ if (typeof metabolite === 'string') {
1974
+ return {
1975
+ id: idFromName(metabolite, `metabolite-${index + 1}`),
1976
+ name: metabolite,
1977
+ type: 'metabolite',
1978
+ unit: responseUnit,
1979
+ metadata: { quantitation: 'targeted' },
1980
+ }
1981
+ }
1982
+
1983
+ const feature = normalizeAssayFeature(metabolite)
1984
+ return {
1985
+ ...feature,
1986
+ type: feature.type ?? 'metabolite',
1987
+ unit: feature.unit ?? responseUnit,
1988
+ metadata: {
1989
+ quantitation: 'targeted',
1990
+ ...feature.metadata,
1991
+ },
1992
+ }
1993
+ })
1994
+ }
1995
+
1996
+ function normalizeInternalStandardReagents(
1997
+ internalStandards: Array<string | ReagentTemplateInput>,
1998
+ ): ReagentTemplateInput[] {
1999
+ return internalStandards.map((standard, index) => {
2000
+ if (typeof standard === 'string') {
2001
+ return {
2002
+ id: idFromName(standard, `internal-standard-${index + 1}`),
2003
+ name: standard,
2004
+ kind: 'compound',
2005
+ metadata: { role: 'internal-standard' },
2006
+ }
2007
+ }
2008
+
2009
+ const reagent = normalizeReagent(standard)
2010
+ return {
2011
+ ...reagent,
2012
+ kind: standard.kind ?? 'compound',
2013
+ metadata: {
2014
+ role: 'internal-standard',
2015
+ ...reagent.metadata,
2016
+ },
2017
+ }
2018
+ })
2019
+ }
2020
+
2021
+ function targetedMetabolomicsRunItems(
2022
+ samples: SampleRecord[],
2023
+ calibrationPoints: CalibrationPoint[],
2024
+ includeQc: boolean,
2025
+ ): InstrumentRunItemInput[] {
2026
+ const calibrationItems = calibrationPoints
2027
+ .filter(point => point.role !== 'qc' || includeQc)
2028
+ .filter(point => point.role === 'blank' || point.role === 'standard' || point.role === 'qc')
2029
+ .map((point): InstrumentRunItemInput => ({
2030
+ id: `${point.id}-run`,
2031
+ kind: calibrationRunItemKind(point.role),
2032
+ name: point.level,
2033
+ metadata: {
2034
+ concentration: point.concentration,
2035
+ unit: point.unit,
2036
+ role: point.role,
2037
+ },
2038
+ }))
2039
+
2040
+ const sampleItems = samples.map((sample): InstrumentRunItemInput => ({
2041
+ sampleId: sample.sampleId,
2042
+ name: sample.name ?? sample.sampleId,
2043
+ kind: 'sample',
2044
+ }))
2045
+
2046
+ return [...calibrationItems, ...sampleItems]
2047
+ }
2048
+
2049
+ function calibrationRunItemKind(role: CalibrationPoint['role']): 'blank' | 'standard' | 'qc' {
2050
+ if (role === 'blank' || role === 'standard' || role === 'qc') return role
2051
+ return 'standard'
2052
+ }
2053
+
2054
+ function firstFeatureName(features: AssayFeature[], fallback: string): string {
2055
+ return features.find(feature => feature.name)?.name ?? fallback
2056
+ }
2057
+
1866
2058
  function normalizeDateString(value: Date | string | null | undefined): string | undefined {
1867
2059
  if (value === undefined || value === null) return undefined
1868
2060
  return value instanceof Date ? value.toISOString() : value
@@ -2114,6 +2306,15 @@ function readInteger(value: unknown, fallback: number): number {
2114
2306
  return Number.isFinite(parsed) ? Math.max(1, Math.round(parsed)) : fallback
2115
2307
  }
2116
2308
 
2309
+ function readNumber(value: unknown, fallback: number): number {
2310
+ const parsed = typeof value === 'number'
2311
+ ? value
2312
+ : typeof value === 'string'
2313
+ ? Number(value)
2314
+ : Number.NaN
2315
+ return Number.isFinite(parsed) ? parsed : fallback
2316
+ }
2317
+
2117
2318
  function readBoolean(value: unknown, fallback: boolean): boolean {
2118
2319
  return typeof value === 'boolean' ? value : fallback
2119
2320
  }
@@ -412,6 +412,74 @@ const presetControlSchemas = {
412
412
  ...templateControlSchemas['instrument-run'],
413
413
  ...templateControlSchemas['assay-matrix'],
414
414
  },
415
+ 'targeted-metabolomics': {
416
+ sampleNames: {
417
+ type: 'tags',
418
+ label: 'Samples',
419
+ default: ['S001', 'S002'],
420
+ section: 'samples',
421
+ sectionLabel: 'Samples',
422
+ sectionSubtitle: 'Unknowns and QC pool',
423
+ view: 'run',
424
+ sidebar: { icon: ICONS.samples, iconColor: '#10b981', iconBg: '#d1fae5' },
425
+ },
426
+ metaboliteNames: {
427
+ type: 'tags',
428
+ label: 'Metabolites',
429
+ default: ['Glucose', 'Lactate', 'Pyruvate'],
430
+ section: 'features',
431
+ sectionLabel: 'Metabolites',
432
+ sectionSubtitle: 'Quantitation panel',
433
+ view: 'analysis',
434
+ sidebar: { icon: ICONS.table, iconColor: '#0ea5e9', iconBg: '#e0f2fe' },
435
+ },
436
+ internalStandards: {
437
+ type: 'tags',
438
+ label: 'Internal standards',
439
+ default: ['Stable isotope internal standard mix'],
440
+ section: 'standards',
441
+ sectionLabel: 'Standards',
442
+ sectionSubtitle: 'Calibration and internal standards',
443
+ view: 'analysis',
444
+ sidebar: { icon: ICONS.dose, iconColor: '#8b5cf6', iconBg: '#ede9fe' },
445
+ },
446
+ standardConcentrations: {
447
+ type: 'tags',
448
+ label: 'Calibration levels',
449
+ default: ['0.1', '1', '10', '100'],
450
+ section: 'standards',
451
+ view: 'analysis',
452
+ },
453
+ concentrationUnit: {
454
+ label: 'Concentration unit',
455
+ default: 'uM',
456
+ options: CONCENTRATION_UNITS,
457
+ section: 'standards',
458
+ view: 'analysis',
459
+ },
460
+ responseUnit: {
461
+ label: 'Response unit',
462
+ default: 'peak area ratio',
463
+ section: 'readout',
464
+ sectionLabel: 'Readout',
465
+ view: 'analysis',
466
+ sidebar: { icon: ICONS.table, iconColor: '#0ea5e9', iconBg: '#e0f2fe' },
467
+ },
468
+ instrument: {
469
+ ...templateControlSchemas['instrument-run'].instrument,
470
+ default: 'LC-MS',
471
+ },
472
+ method: {
473
+ ...templateControlSchemas['instrument-run'].method,
474
+ default: 'Targeted metabolomics',
475
+ },
476
+ prepType: {
477
+ ...templateControlSchemas['sample-prep'].prepType,
478
+ default: 'extraction',
479
+ },
480
+ outputVolume: templateControlSchemas['sample-prep'].outputVolume,
481
+ includeQc: templateControlSchemas['instrument-run'].includeQc,
482
+ },
415
483
  'elisa-assay': {
416
484
  sampleNames: {
417
485
  type: 'tags',
@@ -20,6 +20,7 @@ export {
20
20
  createReagentListTemplate,
21
21
  createSampleSheetTemplate,
22
22
  createSamplePrepTemplate,
23
+ createTargetedMetabolomicsCollection,
23
24
  createTemplateCollection,
24
25
  createDefaultBioTemplate,
25
26
  createTimeCourseTemplate,
@@ -186,6 +187,7 @@ export type {
186
187
  CreateFlowCytometryPanelTemplateOptions,
187
188
  CreateInstrumentRunTemplateOptions,
188
189
  CreateLcmsBatchCollectionOptions,
190
+ CreateTargetedMetabolomicsCollectionOptions,
189
191
  CreatePlateMapTemplateOptions,
190
192
  CreateProtocolStepsTemplateOptions,
191
193
  CreateQpcrExpressionCollectionOptions,
@@ -74,6 +74,29 @@ export const bioTemplatePresets = [
74
74
  frontend_adapters: ['toTemplateDataFrame', 'toInstrumentRunDataFrame', 'toInstrumentRunSteps', 'toInstrumentRunScheduleEvents', 'toAssayMatrixDataFrame', 'toAssayMatrixSampleOptions'],
75
75
  components: ['DataFrame', 'ExperimentTimeline', 'ScheduleCalendar', 'ChartContainer', 'SampleSelector'],
76
76
  },
77
+ {
78
+ name: 'targeted-metabolomics',
79
+ label: 'Targeted metabolomics',
80
+ category: 'bio-template-preset',
81
+ purpose: 'Sample metadata, extraction prep, internal standards, LC/GC-MS sequence, calibration curve, and metabolite readout matrix for quantitative metabolomics.',
82
+ templates: ['sample-sheet', 'sample-prep', 'reagent-list', 'instrument-run', 'calibration-curve', 'assay-matrix'],
83
+ aliases: [
84
+ 'targeted metabolomics',
85
+ 'quantitative metabolomics',
86
+ 'lc-ms metabolomics',
87
+ 'gc-ms metabolomics',
88
+ 'targeted lcms',
89
+ 'metabolite quantitation',
90
+ 'mass spec quantitation',
91
+ 'internal standards',
92
+ 'peak area ratio',
93
+ ],
94
+ python_import: 'from mint_sdk.templates import create_targeted_metabolomics_collection, save_template_collection',
95
+ python_example: 'create_targeted_metabolomics_collection(samples=["S001", "S002"], metabolites=["Glucose", "Lactate"], internal_standards=["13C Glucose"], instrument="LC-MS")',
96
+ frontend_import: "import { createTargetedMetabolomicsCollection, toInstrumentRunDataFrame, toCalibrationCurveDataFrame, toAssayMatrixDataFrame, toTemplateDataFrame } from '@morscherlab/mint-sdk/templates'",
97
+ frontend_adapters: ['toTemplateDataFrame', 'toSamplePrepDataFrame', 'toReagentDataFrame', 'toInstrumentRunDataFrame', 'toInstrumentRunSteps', 'toInstrumentRunScheduleEvents', 'toCalibrationCurveDataFrame', 'toAssayMatrixDataFrame', 'toAssayMatrixSampleOptions'],
98
+ components: ['DataFrame', 'ReagentList', 'ExperimentTimeline', 'ScheduleCalendar', 'FitPanel', 'ChartContainer', 'SampleSelector'],
99
+ },
77
100
  {
78
101
  name: 'elisa-assay',
79
102
  label: 'ELISA assay',
@@ -31,6 +31,7 @@ export type TemplatePresetId =
31
31
  | 'wellplate-screen'
32
32
  | 'qpcr-expression'
33
33
  | 'lcms-batch'
34
+ | 'targeted-metabolomics'
34
35
  | 'elisa-assay'
35
36
  | 'flow-cytometry-assay'
36
37
  | 'western-blot-assay'
@@ -596,6 +597,22 @@ export interface CreateLcmsBatchCollectionOptions {
596
597
  metadata?: Record<string, unknown>
597
598
  }
598
599
 
600
+ export interface CreateTargetedMetabolomicsCollectionOptions {
601
+ samples?: Array<string | SampleRecord>
602
+ metabolites?: Array<string | AssayFeature>
603
+ internalStandards?: Array<string | ReagentTemplateInput>
604
+ standardConcentrations?: number[]
605
+ concentrationUnit?: string
606
+ responseUnit?: string
607
+ calibrationAnalyte?: string
608
+ instrument?: string
609
+ method?: string | InstrumentMethodInput
610
+ prepType?: SamplePrepStepType
611
+ outputVolume?: number
612
+ includeQc?: boolean
613
+ metadata?: Record<string, unknown>
614
+ }
615
+
599
616
  export interface CreateElisaAssayCollectionOptions {
600
617
  samples?: Array<string | SampleRecord>
601
618
  analyte?: string
package/src/types/auth.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { RoleInfo } from '../permissions'
2
+
1
3
  // Authentication configuration
2
4
  export interface AuthConfig {
3
5
  authRequired: boolean
@@ -14,6 +16,7 @@ export interface UserInfo {
14
16
  shortname: string | null
15
17
  email: string | null
16
18
  role: string
19
+ roleObj?: RoleInfo | null
17
20
  isActive: boolean
18
21
  }
19
22
 
@@ -1,4 +1,5 @@
1
1
  import type { UserSummary } from './platform'
2
+ import type { AccessControlled, AccessAudience, AccessAudienceInput } from '../permissions'
2
3
 
3
4
  // Container types
4
5
  export type ContainerDirection = 'row' | 'column'
@@ -29,7 +30,7 @@ export interface Toast {
29
30
  export interface TabItem {
30
31
  id: string
31
32
  label: string
32
- icon?: string
33
+ icon?: string | string[]
33
34
  disabled?: boolean
34
35
  badge?: string | number
35
36
  }
@@ -121,6 +122,8 @@ export interface TopBarSettingsConfig {
121
122
  values?: Record<string, unknown>
122
123
  /** Dynamic enhancements (validators, dynamic options, callbacks). Forwarded to SettingsModal. */
123
124
  enhancements?: import('./form-builder').FormEnhancements
125
+ /** Optional user type override for permission-filtered settings content. Defaults to SDK auth/platform context. */
126
+ userType?: SettingsUserType
124
127
  }
125
128
 
126
129
  // TopBar pill-nav items (centered top-level navigation)
@@ -232,6 +235,29 @@ export interface WellEditData {
232
235
  // Fields shown in the edit popup
233
236
  export type WellEditField = 'label' | 'sampleType' | 'injectionVolume' | 'injectionCount' | 'customMethod'
234
237
 
238
+ // External sample drop payloads for WellPlate / RackEditor
239
+ export interface WellSampleDropData {
240
+ id?: string
241
+ sampleName?: string
242
+ label?: string
243
+ sampleType?: string
244
+ injectionVolume?: number
245
+ injectionCount?: number
246
+ customMethod?: string | null
247
+ metadata?: Record<string, unknown>
248
+ raw?: unknown
249
+ }
250
+
251
+ export interface WellSampleDropContext {
252
+ wellId: string
253
+ event: DragEvent
254
+ }
255
+
256
+ export type WellSampleDropParser = (
257
+ event: DragEvent,
258
+ context: WellSampleDropContext
259
+ ) => WellSampleDropData | null | undefined
260
+
235
261
  // Legend item for sample type display
236
262
  export interface WellLegendItem {
237
263
  type: string
@@ -265,6 +291,18 @@ export interface Rack {
265
291
  wells: Record<string, Partial<Well>>
266
292
  }
267
293
 
294
+ export interface RackSampleDropContext {
295
+ rack: Rack
296
+ rackId: string
297
+ wellId: string
298
+ event: DragEvent
299
+ }
300
+
301
+ export type RackSampleDropMapper = (
302
+ data: WellSampleDropData,
303
+ context: RackSampleDropContext
304
+ ) => WellEditData | null | undefined
305
+
268
306
  // Sample Legend types
269
307
  export interface SampleType {
270
308
  id: string
@@ -531,7 +569,7 @@ export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'
531
569
  export type ConfirmVariant = 'danger' | 'warning' | 'info'
532
570
 
533
571
  // SettingsModal types
534
- export interface SettingsTab {
572
+ export interface SettingsTab extends AccessControlled {
535
573
  id: string
536
574
  label: string
537
575
  /**
@@ -553,7 +591,7 @@ export type SettingsModalLayout = 'horizontal' | 'vertical'
553
591
  * SDK form components (BaseInput / BaseSelect / NumberInput / Toggle / …)
554
592
  * via the same registry that powers FormBuilder.
555
593
  */
556
- export interface SettingsGroup {
594
+ export interface SettingsGroup extends AccessControlled {
557
595
  id: string
558
596
  label: string
559
597
  description?: string
@@ -574,6 +612,10 @@ export interface SettingsModalSchema {
574
612
  groups: SettingsGroup[]
575
613
  }
576
614
 
615
+ export type SettingsUserType = Exclude<AccessAudience, 'all'>
616
+ export type SettingsAudience = AccessAudience
617
+ export type SettingsAudienceInput = AccessAudienceInput
618
+
577
619
  // ScientificNumber types
578
620
  export type NumberNotation = 'auto' | 'scientific' | 'engineering' | 'compact'
579
621
 
@@ -1,5 +1,6 @@
1
1
  import type { FieldRules } from '../composables/useForm'
2
2
  import type { OptionPrimitive, SelectOption } from './components'
3
+ import type { AccessControlled } from '../permissions'
3
4
 
4
5
  // ---------------------------------------------------------------------------
5
6
  // Field types
@@ -71,7 +72,7 @@ export interface FieldValidation {
71
72
  // Field schema
72
73
  // ---------------------------------------------------------------------------
73
74
 
74
- export interface FormFieldSchema {
75
+ export interface FormFieldSchema extends AccessControlled {
75
76
  name: string
76
77
  label: string
77
78
  type: FormFieldType