@morscherlab/mint-sdk 1.3.0-beta.1 → 1.3.0-beta.11

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 (136) hide show
  1. package/README.md +23 -140
  2. package/dist/{ResourcePickerListInternal-c3IT8PIu.js → ResourcePickerListInternal-CFAw4vTZ.js} +26 -19
  3. package/dist/ResourcePickerListInternal-CFAw4vTZ.js.map +1 -0
  4. package/dist/components/FilePicker.vue.d.ts +4 -1
  5. package/dist/components/SettingsModal.schema.d.ts +1 -1
  6. package/dist/components/SettingsModal.vue.d.ts +2 -0
  7. package/dist/components/WellPlate.colors.d.ts +1 -5
  8. package/dist/components/WellPlate.rendering.d.ts +5 -2
  9. package/dist/components/index.d.ts +32 -17
  10. package/dist/components/index.js +3 -3
  11. package/dist/components/internal/FilePickerListInternal.vue.d.ts +2 -0
  12. package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +9 -3
  13. package/dist/{components-CvY105kJ.js → components-vHfxV2PB.js} +425 -392
  14. package/dist/components-vHfxV2PB.js.map +1 -0
  15. package/dist/composables/createFilePickerAdapter.d.ts +5 -1
  16. package/dist/composables/filePicker/usePickerNavigation.d.ts +3 -2
  17. package/dist/composables/index.d.ts +0 -3
  18. package/dist/composables/index.js +4 -4
  19. package/dist/composables/useAutoGroup.d.ts +1 -1
  20. package/dist/composables/useBioTemplateComponents.d.ts +15 -3
  21. package/dist/composables/useBioTemplateControls.d.ts +5 -1
  22. package/dist/composables/useBioTemplatePackWorkspace.d.ts +2 -0
  23. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +5 -1
  24. package/dist/composables/useBioTemplateWorkspace.d.ts +5 -1
  25. package/dist/composables/useExperimentSave.d.ts +5 -1
  26. package/dist/composables/useFilePicker.d.ts +4 -1
  27. package/dist/composables/useGroupAssignment.d.ts +5 -1
  28. package/dist/composables/usePlatformContext.d.ts +3 -0
  29. package/dist/composables/useTemplateCollection.d.ts +5 -1
  30. package/dist/composables-D6uqp0UF.js +591 -0
  31. package/dist/composables-D6uqp0UF.js.map +1 -0
  32. package/dist/index.js +24 -24
  33. package/dist/index.js.map +1 -1
  34. package/dist/install.js +1 -1
  35. package/dist/lcms.d.ts +11 -11
  36. package/dist/styles.css +228 -214
  37. package/dist/templates/index.js +1 -1
  38. package/dist/{templates-Bj9hZ7iV.js → templates-BU1pet28.js} +31 -44
  39. package/dist/templates-BU1pet28.js.map +1 -0
  40. package/dist/types/auth.d.ts +0 -27
  41. package/dist/types/components.d.ts +8 -0
  42. package/dist/types/fileBrowserTypes.d.ts +13 -0
  43. package/dist/types/filePicker.d.ts +8 -1
  44. package/dist/types/index.d.ts +2 -2
  45. package/dist/types/platform.d.ts +2 -0
  46. package/dist/utils/rack.d.ts +16 -16
  47. package/dist/{validation-B4Y8_eEe.js → validation-Dmp9Q5W0.js} +50 -12
  48. package/dist/validation-Dmp9Q5W0.js.map +1 -0
  49. package/package.json +1 -6
  50. package/src/__tests__/components/ActionMenu.test.ts +10 -0
  51. package/src/__tests__/components/AppTopBar.test.ts +24 -8
  52. package/src/__tests__/components/AppTopBarPageSelector.test.ts +4 -4
  53. package/src/__tests__/components/EmptyState.test.ts +15 -0
  54. package/src/__tests__/components/ExperimentSelectorModal.test.ts +12 -5
  55. package/src/__tests__/components/FilePicker.test.ts +65 -1
  56. package/src/__tests__/components/FileUploader.test.ts +6 -9
  57. package/src/__tests__/components/ResourcePickerListInternal.test.ts +23 -4
  58. package/src/__tests__/components/SettingsModal.test.ts +22 -0
  59. package/src/__tests__/components/WellPlate.decoration.test.ts +4 -28
  60. package/src/__tests__/components/WellPlate.rendering.test.ts +20 -33
  61. package/src/__tests__/components/WellPlate.sampleTypeIndicator.test.ts +7 -7
  62. package/src/__tests__/composables/createFilePickerAdapter.test.ts +92 -1
  63. package/src/__tests__/composables/usePickerNavigation.test.ts +61 -1
  64. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +18 -6
  65. package/src/__tests__/docs/frontendDocsCatalog.test.ts +2 -2
  66. package/src/__tests__/styles/controlRefreshRules.test.ts +21 -8
  67. package/src/__tests__/styles/menusShellRefresh.test.ts +20 -5
  68. package/src/__tests__/templates/templatesDeprecation.test.ts +58 -11
  69. package/src/components/ActionMenu.vue +10 -2
  70. package/src/components/AppTopBar.vue +6 -4
  71. package/src/components/EmptyState.vue +3 -1
  72. package/src/components/ExperimentSelectorModal.vue +4 -1
  73. package/src/components/FilePicker.vue +42 -0
  74. package/src/components/FileUploader.vue +30 -46
  75. package/src/components/SettingsModal.schema.ts +4 -2
  76. package/src/components/SettingsModal.vue +4 -2
  77. package/src/components/WellPlate.colors.ts +6 -10
  78. package/src/components/WellPlate.legend.ts +4 -4
  79. package/src/components/WellPlate.rendering.ts +9 -8
  80. package/src/components/WellPlate.vue +5 -22
  81. package/src/components/index.ts +32 -17
  82. package/src/components/internal/AppTopBarPageSelectorInternal.vue +3 -6
  83. package/src/components/internal/FilePickerListInternal.vue +11 -2
  84. package/src/components/internal/ResourcePickerListInternal.vue +21 -27
  85. package/src/composables/autoGroup/index.ts +1 -1
  86. package/src/composables/createFilePickerAdapter.ts +82 -22
  87. package/src/composables/filePicker/usePickerNavigation.ts +29 -2
  88. package/src/composables/index.ts +0 -7
  89. package/src/composables/useAutoGroup.ts +1 -1
  90. package/src/composables/useBioTemplateComponents.ts +15 -3
  91. package/src/composables/useBioTemplateControls.ts +5 -1
  92. package/src/composables/useBioTemplatePackWorkspace.ts +2 -0
  93. package/src/composables/useBioTemplatePresetWorkspace.ts +5 -1
  94. package/src/composables/useBioTemplateWorkspace.ts +5 -1
  95. package/src/composables/useExperimentSave.ts +5 -1
  96. package/src/composables/useFilePicker.ts +8 -1
  97. package/src/composables/useGroupAssignment.ts +5 -1
  98. package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
  99. package/src/composables/useTemplateCollection.ts +5 -1
  100. package/src/index.ts +1 -1
  101. package/src/lcms.ts +11 -11
  102. package/src/styles/components/action-menu.css +10 -0
  103. package/src/styles/components/app-page-selector.css +61 -17
  104. package/src/styles/components/app-pill-nav.css +1 -1
  105. package/src/styles/components/app-top-bar.css +18 -38
  106. package/src/styles/components/empty-state.css +3 -2
  107. package/src/styles/components/experiment-selector-modal.css +3 -9
  108. package/src/styles/components/file-picker.css +23 -1
  109. package/src/styles/components/file-uploader.css +22 -55
  110. package/src/styles/components/resource-picker-list.css +70 -36
  111. package/src/styles/components/well-plate.css +18 -55
  112. package/src/types/auth.ts +0 -37
  113. package/src/types/components.ts +8 -0
  114. package/src/types/fileBrowserTypes.ts +15 -0
  115. package/src/types/filePicker.ts +8 -1
  116. package/src/types/index.ts +2 -5
  117. package/src/types/platform.ts +2 -0
  118. package/src/utils/rack.ts +16 -16
  119. package/dist/ResourcePickerListInternal-c3IT8PIu.js.map +0 -1
  120. package/dist/__tests__/composables/useAuth.test.d.ts +0 -1
  121. package/dist/__tests__/composables/usePasskey.test.d.ts +0 -1
  122. package/dist/components-CvY105kJ.js.map +0 -1
  123. package/dist/composables/useAuth.d.ts +0 -47
  124. package/dist/composables/usePasskey.d.ts +0 -10
  125. package/dist/composables/useWellPlateEditor.d.ts +0 -33
  126. package/dist/composables-DL4Vca7L.js +0 -1193
  127. package/dist/composables-DL4Vca7L.js.map +0 -1
  128. package/dist/plotly.min-D6Ge6qgX.js +0 -168582
  129. package/dist/plotly.min-D6Ge6qgX.js.map +0 -1
  130. package/dist/templates-Bj9hZ7iV.js.map +0 -1
  131. package/dist/validation-B4Y8_eEe.js.map +0 -1
  132. package/src/__tests__/composables/useAuth.test.ts +0 -290
  133. package/src/__tests__/composables/usePasskey.test.ts +0 -38
  134. package/src/composables/useAuth.ts +0 -470
  135. package/src/composables/usePasskey.ts +0 -216
  136. package/src/composables/useWellPlateEditor.ts +0 -415
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.3.0-beta.1",
3
+ "version": "1.3.0-beta.11",
4
4
  "description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -67,16 +67,12 @@
67
67
  "story:preview": "histoire preview"
68
68
  },
69
69
  "peerDependencies": {
70
- "@simplewebauthn/browser": "^14.0.0",
71
70
  "pinia": "^4.0.3",
72
71
  "tailwindcss": "^4.3.3",
73
72
  "vue": "^3.5.43",
74
73
  "vue-router": "^5.3.1"
75
74
  },
76
75
  "peerDependenciesMeta": {
77
- "@simplewebauthn/browser": {
78
- "optional": true
79
- },
80
76
  "vue-router": {
81
77
  "optional": true
82
78
  }
@@ -89,7 +85,6 @@
89
85
  },
90
86
  "devDependencies": {
91
87
  "@histoire/plugin-vue": "1.0.0-beta.1",
92
- "@simplewebauthn/browser": "14.0.0",
93
88
  "@types/node": "22.20.4",
94
89
  "@vitejs/plugin-vue": "6.0.9",
95
90
  "@vue/language-core": "3.3.11",
@@ -267,4 +267,14 @@ describe('ActionMenu', () => {
267
267
 
268
268
  expect(document.activeElement).toBe(trigger.element)
269
269
  })
270
+
271
+ it('should render a checked item as a checked menu radio when checked is set', async () => {
272
+ const wrapper = mount(ActionMenu, {
273
+ attachTo: document.body,
274
+ props: { label: 'Sort', items: [{ id: 'a', label: 'A', checked: true }, { id: 'b', label: 'B', checked: false }] },
275
+ })
276
+ await wrapper.get('button').trigger('click')
277
+ const radios = [...document.body.querySelectorAll('[role="menuitemradio"]')]
278
+ expect(radios.map((item) => item.getAttribute('aria-checked'))).toEqual(['true', 'false'])
279
+ })
270
280
  })
@@ -1211,12 +1211,20 @@ describe('AppTopBar', () => {
1211
1211
  expect(wrapper.get('.mint-topbar__plugin-name').text()).toBe('LC-MS Analysis')
1212
1212
  })
1213
1213
 
1214
- it('should render the plugin version in mono after the name when integrated', () => {
1214
+ it('should render the platform display name when integrated', () => {
1215
+ mockPlatformCtx({ isIntegrated: true, plugin: { ...integratedPlugin, display_name: 'LC-MS Peak Review' } })
1216
+
1217
+ const wrapper = createWrapper()
1218
+
1219
+ expect(wrapper.get('.mint-topbar__plugin-name').text()).toBe('LC-MS Peak Review')
1220
+ })
1221
+
1222
+ it('should not render the plugin version when integrated', () => {
1215
1223
  mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
1216
1224
 
1217
1225
  const wrapper = createWrapper()
1218
1226
 
1219
- expect(wrapper.get('.mint-topbar__plugin-version').text()).toBe('v2.4.0')
1227
+ expect(wrapper.text()).not.toContain('v2.4.0')
1220
1228
  })
1221
1229
 
1222
1230
  it('should not render the plugin identity text when standalone', () => {
@@ -1225,21 +1233,29 @@ describe('AppTopBar', () => {
1225
1233
  expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
1226
1234
  })
1227
1235
 
1228
- it('should render a divider between the identity and the page selector', () => {
1236
+ it('should hide the plugin name when a page selector is shown', () => {
1229
1237
  mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
1230
1238
 
1231
1239
  const wrapper = createWrapper({ pageSelector: pages, currentPageSelectorId: 'peaks' })
1232
1240
 
1233
- expect(wrapper.get('.mint-topbar__divider').element.nextElementSibling?.classList)
1234
- .toContain('mint-page-selector')
1241
+ expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
1235
1242
  })
1236
1243
 
1237
- it('should not render the divider without a page selector', () => {
1238
- mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
1244
+ it('should hide the plugin name when the plugin declares several nav items', () => {
1245
+ mockPlatformCtx({
1246
+ isIntegrated: true,
1247
+ plugin: {
1248
+ ...integratedPlugin,
1249
+ nav_items: [
1250
+ { id: 'peaks', path: '/', label: 'Peaks' },
1251
+ { id: 'report', path: '/report', label: 'Report' },
1252
+ ],
1253
+ },
1254
+ })
1239
1255
 
1240
1256
  const wrapper = createWrapper()
1241
1257
 
1242
- expect(wrapper.find('.mint-topbar__divider').exists()).toBe(false)
1258
+ expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
1243
1259
  })
1244
1260
 
1245
1261
  it('should not repeat the title next to the integrated identity', () => {
@@ -154,23 +154,23 @@ describe('AppTopBarPageSelectorInternal', () => {
154
154
  expect(wrapper.get('.mint-plugin-icon__img').attributes('src')).toBe(httpsIcon)
155
155
  })
156
156
 
157
- it('should render the menu on the shared popover surface when opened', async () => {
157
+ it('should render the menu as its own panel, not the shared popover, when opened', async () => {
158
158
  const wrapper = mount(AppTopBarPageSelectorInternal, {
159
159
  props: { currentPageId: 'Workspace', pages: ['Workspace', 'Results'] },
160
160
  })
161
161
 
162
162
  await wrapper.get('.mint-page-selector__trigger').trigger('click')
163
163
 
164
- expect(wrapper.get('.mint-page-selector__menu').classes()).toContain('mint-popover')
164
+ expect(wrapper.get('.mint-page-selector__menu').classes()).not.toContain('mint-popover')
165
165
  })
166
166
 
167
- it('should mark the current page as a selected menu item when opened', async () => {
167
+ it('should mark the current page as the active item when opened', async () => {
168
168
  const wrapper = mount(AppTopBarPageSelectorInternal, {
169
169
  props: { currentPageId: 'Workspace', pages: ['Workspace', 'Results'] },
170
170
  })
171
171
 
172
172
  await wrapper.get('.mint-page-selector__trigger').trigger('click')
173
173
 
174
- expect(wrapper.findAll('.mint-page-selector__item')[0].classes()).toContain('mint-menu-item--selected')
174
+ expect(wrapper.findAll('.mint-page-selector__item')[0].classes()).toContain('mint-page-selector__item--active')
175
175
  })
176
176
  })
@@ -20,4 +20,19 @@ describe('EmptyState', () => {
20
20
 
21
21
  expect(wrapper.emitted('action')).toHaveLength(1)
22
22
  })
23
+
24
+ it('should wrap default-slot content in the spaced content block', () => {
25
+ const wrapper = mount(EmptyState, {
26
+ props: { title: 'No instruments yet', description: 'Add the lab systems.' },
27
+ slots: { default: '<button class="slot-action">New instrument</button>' },
28
+ })
29
+
30
+ expect(wrapper.find('.mint-empty-state__content .slot-action').exists()).toBe(true)
31
+ })
32
+
33
+ it('should not render the content block without default-slot content', () => {
34
+ const wrapper = mount(EmptyState, { props: { title: 'Empty' } })
35
+
36
+ expect(wrapper.find('.mint-empty-state__content').exists()).toBe(false)
37
+ })
23
38
  })
@@ -129,25 +129,32 @@ describe('ExperimentSelectorModal', () => {
129
129
  expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
130
130
  })
131
131
 
132
- it('should lead the row title with the experiment code in mono', () => {
132
+ it('should show the experiment code as a pill on the second line', () => {
133
133
  experiments.value = [experiment()]
134
134
  const wrapper = mountModal({ modelValue: true })
135
135
 
136
136
  expect(wrapper.get('.mint-resource-picker__code').text()).toBe('EXP-007')
137
137
  })
138
138
 
139
- it('should state the status as text on the second row line', () => {
139
+ it('should state the status as text in the right-hand column', () => {
140
140
  experiments.value = [experiment()]
141
141
  const wrapper = mountModal({ modelValue: true })
142
142
 
143
- expect(wrapper.get('.mint-resource-picker__meta').text()).toContain('Ongoing')
143
+ expect(wrapper.get('.mint-resource-picker__aside').text()).toContain('Ongoing')
144
144
  })
145
145
 
146
- it('should check the current experiment row', () => {
146
+ it('should tint the status with the experiment status variant', () => {
147
+ experiments.value = [experiment()]
148
+ const wrapper = mountModal({ modelValue: true })
149
+
150
+ expect(wrapper.get('.mint-resource-picker__status').classes()).toContain('mint-resource-picker__status--primary')
151
+ })
152
+
153
+ it('should fill the current experiment row', () => {
147
154
  experiments.value = [experiment()]
148
155
  const wrapper = mountModal({ modelValue: true, currentExperimentId: 7 })
149
156
 
150
- expect(wrapper.find('.mint-resource-picker__check').exists()).toBe(true)
157
+ expect(wrapper.find('.mint-resource-picker__row--selected').exists()).toBe(true)
151
158
  })
152
159
 
153
160
  it('clears the shared store from the clear-selection footer', async () => {
@@ -548,9 +548,10 @@ describe('FilePicker list view', () => {
548
548
  await row(wrapper, folder.path).trigger('dblclick')
549
549
  await flushPromises()
550
550
  expect(wrapper.findAll('[role="option"]').map((item) => item.attributes('data-path'))).toEqual([
551
+ // Undated rows fall back to name order under the default newest-first sort.
552
+ unsupported.path,
551
553
  one.path,
552
554
  two.path,
553
- unsupported.path,
554
555
  ])
555
556
  })
556
557
 
@@ -767,4 +768,67 @@ describe('formatModified', () => {
767
768
  it('should show the full date outside the current year', () => {
768
769
  expect(formatModified('2025-01-02T08:00:00', now)).toBe('2025-01-02')
769
770
  })
771
+
772
+ it('should list the newest folder first when opened', async () => {
773
+ const wrapper = await setup({
774
+ adapter: {
775
+ ...adapter(),
776
+ listRoot: vi.fn(async () => [
777
+ { ...folder, modifiedAt: '2026-01-01T00:00:00Z' },
778
+ { ...other, modifiedAt: '2026-09-29T00:00:00Z' },
779
+ ]),
780
+ },
781
+ })
782
+ expect(wrapper.findAll('[role="treeitem"]').map((item) => item.attributes('data-path'))).toEqual([
783
+ other.path, folder.path,
784
+ ])
785
+ })
786
+
787
+ it('should reorder rows by name when Name A–Z is chosen from the sort menu', async () => {
788
+ const wrapper = await setup({ defaultSort: 'name-desc' })
789
+ await wrapper.get('.mint-picker__sort').trigger('click')
790
+ await wrapper.findAll('[role="menuitemradio"]').find((item) => item.text() === 'Name A–Z')!.trigger('click')
791
+ expect(wrapper.findAll('[role="treeitem"]').map((item) => item.attributes('data-path'))).toEqual([
792
+ other.path, folder.path,
793
+ ])
794
+ })
795
+
796
+ it('should label the sort trigger with the active order', async () => {
797
+ const wrapper = await setup()
798
+ expect(wrapper.get('.mint-picker__sort').text()).toContain('Newest first')
799
+ })
800
+
801
+ it('should tell the user when search results are partial', async () => {
802
+ vi.useFakeTimers()
803
+ const base = adapter()
804
+ const wrapper = await setup({
805
+ adapter: { ...base, search: vi.fn(async () => ({ nodes: [one], truncated: true })) },
806
+ })
807
+ await wrapper.get('[aria-label="Search folders and files"]').setValue('sample')
808
+ await vi.advanceTimersByTimeAsync(400)
809
+ await flushPromises()
810
+ expect(wrapper.get('.mint-picker__truncated').text()).toContain('first 1')
811
+ })
812
+
813
+ it('should still list the partial search results', async () => {
814
+ vi.useFakeTimers()
815
+ const wrapper = await setup({
816
+ adapter: { ...adapter(), search: vi.fn(async () => ({ nodes: [one], truncated: true })) },
817
+ })
818
+ await wrapper.get('[aria-label="Search folders and files"]').setValue('sample')
819
+ await vi.advanceTimersByTimeAsync(400)
820
+ await flushPromises()
821
+ expect(wrapper.findAll('[role="treeitem"]').map((item) => item.attributes('data-path'))).toEqual([one.path])
822
+ })
823
+
824
+ it('should say the search stopped early when a truncated search found nothing', async () => {
825
+ vi.useFakeTimers()
826
+ const wrapper = await setup({
827
+ adapter: { ...adapter(), search: vi.fn(async () => ({ nodes: [], truncated: true })) },
828
+ })
829
+ await wrapper.get('[aria-label="Search folders and files"]').setValue('sample')
830
+ await vi.advanceTimersByTimeAsync(400)
831
+ await flushPromises()
832
+ expect(wrapper.get('.mint-picker__empty').text()).toContain('stopped')
833
+ })
770
834
  })
@@ -3,16 +3,13 @@ import { describe, expect, it } from 'vitest'
3
3
  import FileUploader from '../../components/FileUploader.vue'
4
4
 
5
5
  describe('FileUploader', () => {
6
- it('should prompt to release with the dragged file count when files are dragged over', async () => {
7
- const wrapper = mount(FileUploader, { props: { multiple: true } })
8
- await wrapper.get('.mint-file-uploader__dropzone').trigger('dragover', {
9
- dataTransfer: { items: { length: 3 } },
10
- })
11
- expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Release to add 3 files')
6
+ it('should name the accepted formats in the prompt when accept is set', () => {
7
+ const wrapper = mount(FileUploader, { props: { accept: '.raw, .mzML' } })
8
+ expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Drop RAW, MZML or click')
12
9
  })
13
10
 
14
- it('should list accepted formats as written when accept and maxSize are set', () => {
15
- const wrapper = mount(FileUploader, { props: { accept: '.raw, .mzML', maxSize: 2 * 1024 ** 3 } })
16
- expect(wrapper.get('.mint-file-uploader__formats').text()).toBe('.raw .mzML · max 2.0 GB')
11
+ it('should prompt for any file when accept is not set', () => {
12
+ const wrapper = mount(FileUploader)
13
+ expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Drop any file or click')
17
14
  })
18
15
  })
@@ -40,8 +40,27 @@ describe('ResourcePickerListInternal', () => {
40
40
  expect(setup().get('.mint-resource-picker__code').text()).toBe('EXP-042')
41
41
  })
42
42
 
43
- it('should join the meta and date on the second line', () => {
44
- expect(setup().get('.mint-resource-picker__meta').text()).toBe('Zhang · 09-24')
43
+ it('should render the code as the shared gray code pill', () => {
44
+ expect(setup().get('.mint-resource-picker__code').classes()).toContain('mint-exp-code')
45
+ })
46
+
47
+ it('should tint the status by its variant', () => {
48
+ const wrapper = setup({
49
+ describe: (item: unknown) => ({ title: (item as Item).name, status: { label: 'Completed', variant: 'success' } }),
50
+ })
51
+ expect(wrapper.get('.mint-resource-picker__status').classes()).toContain('mint-resource-picker__status--success')
52
+ })
53
+
54
+ it('should keep the title line to the title alone', () => {
55
+ expect(setup().get('.mint-resource-picker__title').text()).toBe('Liver microsome stability')
56
+ })
57
+
58
+ it('should lead the second line with the code and meta', () => {
59
+ expect(setup().get('.mint-resource-picker__meta').text()).toBe('EXP-042Zhang')
60
+ })
61
+
62
+ it('should put the date in the right-hand column', () => {
63
+ expect(setup().get('.mint-resource-picker__aside .mint-resource-picker__date').text()).toBe('09-24')
45
64
  })
46
65
 
47
66
  it('should mark the selected row', () => {
@@ -49,8 +68,8 @@ describe('ResourcePickerListInternal', () => {
49
68
  expect(wrapper.findAll('[role="option"]')[1]!.attributes('aria-selected')).toBe('true')
50
69
  })
51
70
 
52
- it('should draw a trailing check on the selected row only', () => {
53
- expect(setup({ selectedKey: 2 }).findAll('.mint-resource-picker__check')).toHaveLength(1)
71
+ it('should fill the selected row only', () => {
72
+ expect(setup({ selectedKey: 2 }).findAll('.mint-resource-picker__row--selected')).toHaveLength(1)
54
73
  })
55
74
 
56
75
  it('should emit select when a row is clicked', async () => {
@@ -540,6 +540,28 @@ describe('SettingsModal', () => {
540
540
  expect(wrapper.findAll('.mint-modal__rail-section').map(heading => heading.text())).toEqual(['Plugin', 'Personal'])
541
541
  })
542
542
 
543
+ it('should list and open Appearance first when appearanceFirst is set', () => {
544
+ const wrapper = mountSettingsModal({
545
+ tabs: [{ id: 'plugins', label: 'Plugins' }],
546
+ showAppearance: true,
547
+ appearanceFirst: true,
548
+ layout: 'vertical',
549
+ })
550
+
551
+ expect(wrapper.get('.mint-modal__rail-item--active').text()).toContain('Appearance')
552
+ })
553
+
554
+ it('should label a manual tab rail section from its section', () => {
555
+ const wrapper = mountSettingsModal({
556
+ tabs: [{ id: 'plugins', label: 'Plugins', section: 'Platform' }],
557
+ showAppearance: true,
558
+ appearanceFirst: true,
559
+ layout: 'vertical',
560
+ })
561
+
562
+ expect(wrapper.findAll('.mint-modal__rail-section').map(heading => heading.text())).toEqual(['Personal', 'Platform'])
563
+ })
564
+
543
565
  it('should not render rail section headings when appearance is hidden', () => {
544
566
  const wrapper = mountSettingsModal({ schema: railSchema })
545
567
 
@@ -26,7 +26,7 @@ describe('WellPlate per-well decoration', () => {
26
26
  .toEqual(['2px', 'solid', 'var(--mint-warning)'])
27
27
  })
28
28
 
29
- it('should render the QC marker glyph when a QC well has no label or id', () => {
29
+ it('should render no glyph when a QC well has no label or id', () => {
30
30
  const wrapper = mount(WellPlate, {
31
31
  props: {
32
32
  format: 6,
@@ -34,37 +34,13 @@ describe('WellPlate per-well decoration', () => {
34
34
  },
35
35
  })
36
36
 
37
- expect(wrapper.findAll('.mint-well-plate__well')[0].text()).toBe('Q')
37
+ expect(wrapper.findAll('.mint-well-plate__well')[0].text()).toBe('')
38
38
  })
39
39
 
40
- it('should keep the iQC marker glyph when well IDs are shown', () => {
41
- const wrapper = mount(WellPlate, {
42
- props: {
43
- format: 6,
44
- showWellIds: true,
45
- wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType: 'iqc' } },
46
- },
47
- })
48
-
49
- expect(wrapper.findAll('.mint-well-plate__well')[0].find('.mint-well-plate__corner-glyph').text()).toBe('i')
50
- })
51
-
52
- it('should keep the iQC marker glyph when a custom label is shown', () => {
53
- const wrapper = mount(WellPlate, {
54
- props: {
55
- format: 6,
56
- showWellLabels: true,
57
- wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType: 'iqc', metadata: { label: 'Std 1' } } },
58
- },
59
- })
60
-
61
- expect(wrapper.findAll('.mint-well-plate__well')[0].find('.mint-well-plate__corner-glyph').text()).toBe('i')
62
- })
63
-
64
- it('should draw legend swatches with the sample-type marker when the legend is shown', () => {
40
+ it('should draw plain legend swatches when the legend is shown', () => {
65
41
  const wrapper = mount(WellPlate, { props: { format: 6, showLegend: true } })
66
42
 
67
43
  const dots = wrapper.findAll('.mint-well-plate__sample-legend-dot')
68
- expect(dots.map(dot => dot.text())).toEqual(['', '', 'Q', 'i'])
44
+ expect(dots.map(dot => dot.text())).toEqual(['', '', '', ''])
69
45
  })
70
46
  })
@@ -1,6 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import {
3
- sampleTypeGlyph,
4
3
  wellBadge,
5
4
  wellClasses,
6
5
  wellLabel,
@@ -70,45 +69,45 @@ describe('WellPlate rendering helpers', () => {
70
69
  heatmap: { enabled: false },
71
70
  sampleColors: { treated: '#123456' },
72
71
  })).toEqual({
73
- backgroundColor: 'color-mix(in srgb, #123456 28%, var(--bg-card))',
74
- border: '1px solid #123456',
72
+ backgroundColor: 'color-mix(in srgb, #123456 15%, transparent)',
73
+ border: '1px solid color-mix(in srgb, #123456 40%, transparent)',
75
74
  })
76
75
 
77
76
  expect(wellStyle(well({ sampleType: 'qc' }), {
78
77
  heatmap: { enabled: false },
79
78
  sampleColors: {},
80
79
  })).toEqual({
81
- backgroundColor: 'var(--bg-card)',
82
- border: '1.5px solid var(--text-secondary-strong)',
80
+ backgroundColor: 'color-mix(in srgb, #8b5cf6 15%, transparent)',
81
+ border: '1px solid color-mix(in srgb, #8b5cf6 40%, transparent)',
83
82
  })
84
83
  })
85
84
 
86
- it('should fill a sample well with its group token when no custom color is set', () => {
85
+ it('should tint a sample well green when no custom color is set', () => {
87
86
  expect(wellStyle(well({ sampleType: 'sample' }), {
88
87
  heatmap: { enabled: false },
89
88
  sampleColors: {},
90
- }).border).toBe('1px solid var(--mint-sample-1)')
89
+ }).border).toBe('1px solid color-mix(in srgb, #10b981 40%, transparent)')
91
90
  })
92
91
 
93
- it('should color a well by its group before its sample type', () => {
94
- expect(wellStyle(well({ sampleType: 'iqc', group: 'trt' }), {
92
+ it('should tint a blank well orange when no custom color is set', () => {
93
+ expect(wellStyle(well({ sampleType: 'blank' }), {
95
94
  heatmap: { enabled: false },
96
- sampleColors: { trt: '#123456' },
97
- }).border).toBe('1px solid #123456')
95
+ sampleColors: {},
96
+ }).border).toBe('1px solid color-mix(in srgb, #f97316 40%, transparent)')
98
97
  })
99
98
 
100
- it('should keep a grouped blank well neutral', () => {
101
- expect(wellStyle(well({ sampleType: 'blank', group: 'trt' }), {
99
+ it('should color a well by its group before its sample type', () => {
100
+ expect(wellStyle(well({ sampleType: 'iqc', group: 'trt' }), {
102
101
  heatmap: { enabled: false },
103
102
  sampleColors: { trt: '#123456' },
104
- }).border).toBe('1.5px dashed var(--_control-border-strong)')
103
+ }).border).toBe('1px solid color-mix(in srgb, #123456 40%, transparent)')
105
104
  })
106
105
 
107
- it('should keep QC neutral when a custom QC color is set', () => {
108
- expect(wellStyle(well({ sampleType: 'qc' }), {
106
+ it('should tint a well with a group color token', () => {
107
+ expect(wellStyle(well({ sampleType: 'sample', group: 'trt' }), {
109
108
  heatmap: { enabled: false },
110
- sampleColors: { qc: '#8b5cf6' },
111
- }).border).toBe('1.5px solid var(--text-secondary-strong)')
109
+ sampleColors: { trt: 'var(--mint-sample-3)' },
110
+ }).backgroundColor).toBe('color-mix(in srgb, var(--mint-sample-3) 15%, transparent)')
112
111
  })
113
112
 
114
113
  it('should leave empty wells to the stylesheet when the well has no sample type', () => {
@@ -125,29 +124,17 @@ describe('WellPlate rendering helpers', () => {
125
124
  })).toEqual({})
126
125
  })
127
126
 
128
- it('should return the Q glyph when the well is QC', () => {
129
- expect(sampleTypeGlyph(well({ sampleType: 'qc' }))).toBe('Q')
130
- })
131
-
132
- it('should return the i glyph when the well is iQC', () => {
133
- expect(sampleTypeGlyph(well({ sampleType: 'iqc' }))).toBe('i')
134
- })
135
-
136
- it('should return no glyph when the well is a sample', () => {
137
- expect(sampleTypeGlyph(well({ sampleType: 'sample' }))).toBeNull()
138
- })
139
-
140
127
  it('formats labels and badges for optional overlays', () => {
141
128
  expect(wellLabel(well({ metadata: { label: 'Dose A' } }), true)).toBe('Dose A')
142
129
  expect(wellLabel(well({ metadata: { label: 'Dose A' } }), false)).toBeUndefined()
143
130
 
144
131
  expect(wellBadge(well({ metadata: { injectionCount: 3 } }), true)).toEqual({
145
132
  text: '3',
146
- color: 'var(--text-primary)',
133
+ color: '#6366f1',
147
134
  })
148
135
  expect(wellBadge(well({ metadata: { customMethod: 'LCMS-2' } }), true)).toEqual({
149
- text: '',
150
- color: 'var(--color-purple)',
136
+ text: '+',
137
+ color: '#ec4899',
151
138
  })
152
139
  expect(wellBadge(well({ metadata: { injectionCount: 1 } }), true)).toBeNull()
153
140
  })
@@ -4,9 +4,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
4
4
  import WellPlate from '../../components/WellPlate.vue'
5
5
  import type { Well } from '../../types'
6
6
 
7
- // `showSampleTypeIndicator` is deprecated (3a): shape encodes the type, only
8
- // QC `Q` and iQC `i` carry glyphs. The warning fires once per module, so the
9
- // order of the first two tests matters.
7
+ // `showSampleTypeIndicator` is deprecated (3a): wells carry no letters, color
8
+ // encodes the type. The warning fires once per module, so the order of the
9
+ // first two tests matters.
10
10
 
11
11
  function mountPlate(sampleType: string, showSampleTypeIndicator: boolean) {
12
12
  const wells: Record<string, Partial<Well>> = {
@@ -48,11 +48,11 @@ describe('WellPlate showSampleTypeIndicator (deprecated)', () => {
48
48
  expect(firstWellText('blank')).toBe('')
49
49
  })
50
50
 
51
- it('should keep the QC marker glyph when showSampleTypeIndicator is on', () => {
52
- expect(firstWellText('qc')).toBe('Q')
51
+ it('should render no letter for a QC well when showSampleTypeIndicator is on', () => {
52
+ expect(firstWellText('qc')).toBe('')
53
53
  })
54
54
 
55
- it('should keep the iQC marker glyph when showSampleTypeIndicator is on', () => {
56
- expect(firstWellText('iqc')).toBe('i')
55
+ it('should render no letter for an iQC well when showSampleTypeIndicator is on', () => {
56
+ expect(firstWellText('iqc')).toBe('')
57
57
  })
58
58
  })