@morscherlab/mint-sdk 1.1.0-beta.1 → 1.1.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 (94) hide show
  1. package/README.md +18 -4
  2. package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
  3. package/dist/EmptyState-DgBRoYAy.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
  5. package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
  6. package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-Dpg-X1OM.js} +3 -3
  7. package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
  8. package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
  9. package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
  10. package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
  11. package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
  12. package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
  13. package/dist/components/BaseSelect.vue.d.ts +1 -1
  14. package/dist/components/BaseToggle.vue.d.ts +2 -0
  15. package/dist/components/FitPanel.vue.d.ts +1 -1
  16. package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
  17. package/dist/components/SectionCard.vue.d.ts +47 -0
  18. package/dist/components/index.d.ts +1 -0
  19. package/dist/components/index.js +6 -6
  20. package/dist/{components-UfkPN1bK.js → components-Ci_BTx00.js} +711 -633
  21. package/dist/components-Ci_BTx00.js.map +1 -0
  22. package/dist/composables/analysisArtifactTarget.d.ts +5 -0
  23. package/dist/composables/index.d.ts +3 -1
  24. package/dist/composables/index.js +8 -8
  25. package/dist/composables/platformContextHelpers.d.ts +2 -0
  26. package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
  27. package/dist/composables/useGeneratedAnalysis.d.ts +1 -1
  28. package/dist/composables/usePlatformContext.d.ts +3 -0
  29. package/dist/composables/usePluginClient.d.ts +11 -5
  30. package/dist/{composables-F4JFK9K1.js → composables-C0VEJ73O.js} +172 -30
  31. package/dist/composables-C0VEJ73O.js.map +1 -0
  32. package/dist/index.js +11 -11
  33. package/dist/install.js +5 -5
  34. package/dist/styles.css +312 -11
  35. package/dist/tailwind.preset.d.ts +5 -0
  36. package/dist/tailwind.preset.js +7 -2
  37. package/dist/tailwind.preset.js.map +1 -1
  38. package/dist/templates/index.js +3 -3
  39. package/dist/{templates-Cttbjbyc.js → templates-Cdj93S9C.js} +2 -2
  40. package/dist/{templates-Cttbjbyc.js.map → templates-Cdj93S9C.js.map} +1 -1
  41. package/dist/types/analysisArtifactTypes.d.ts +8 -0
  42. package/dist/types/components.d.ts +2 -0
  43. package/dist/types/generated-ui.d.ts +13 -0
  44. package/dist/types/index.d.ts +2 -2
  45. package/dist/types/platform.d.ts +2 -0
  46. package/dist/{useControlSchema-Dallso-L.js → useControlSchema-B_qgU4rW.js} +48 -19
  47. package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
  48. package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
  49. package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
  50. package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-D1G5J5Z5.js} +2 -2
  51. package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
  52. package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-DeaOmMaw.js} +4 -4
  53. package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-DeaOmMaw.js.map} +1 -1
  54. package/package.json +1 -1
  55. package/src/__tests__/components/BaseSelect.test.ts +52 -0
  56. package/src/__tests__/components/BaseToggle.test.ts +40 -0
  57. package/src/__tests__/components/GeneratedPluginView.test.ts +10 -0
  58. package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
  59. package/src/__tests__/components/SectionCard.test.ts +181 -0
  60. package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
  61. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
  62. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +2 -0
  63. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  64. package/src/__tests__/composables/usePluginClient.test.ts +365 -21
  65. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  66. package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
  67. package/src/components/BaseSelect.vue +16 -2
  68. package/src/components/BaseToggle.vue +3 -1
  69. package/src/components/GeneratedPluginView.vue +1 -1
  70. package/src/components/PluginWorkspaceView.props.ts +1 -1
  71. package/src/components/PluginWorkspaceView.vue +1 -5
  72. package/src/components/SectionCard.story.vue +163 -0
  73. package/src/components/SectionCard.vue +98 -0
  74. package/src/components/index.ts +1 -0
  75. package/src/composables/analysisArtifactTarget.ts +53 -0
  76. package/src/composables/index.ts +6 -0
  77. package/src/composables/platformContextHelpers.ts +10 -3
  78. package/src/composables/useAnalysisArtifacts.ts +48 -0
  79. package/src/composables/useGeneratedAnalysis.ts +30 -12
  80. package/src/composables/usePluginClient.ts +130 -20
  81. package/src/composables/usePluginConfig.ts +156 -20
  82. package/src/styles/components/section-card.css +161 -0
  83. package/src/styles/index.css +1 -0
  84. package/src/styles/variables.css +25 -0
  85. package/src/tailwind.preset.ts +5 -0
  86. package/src/types/analysisArtifactTypes.ts +9 -0
  87. package/src/types/components.ts +12 -0
  88. package/src/types/generated-ui.ts +13 -0
  89. package/src/types/index.ts +3 -0
  90. package/src/types/platform.ts +2 -0
  91. package/dist/EmptyState-D1c4RdC7.js.map +0 -1
  92. package/dist/components-UfkPN1bK.js.map +0 -1
  93. package/dist/composables-F4JFK9K1.js.map +0 -1
  94. package/dist/useControlSchema-Dallso-L.js.map +0 -1
@@ -38,16 +38,24 @@ type FakeResponse = Record<string, unknown>
38
38
 
39
39
  let getResponse: FakeResponse = { plugin_name: 'drp', config: {} }
40
40
  let patchResponse: FakeResponse = { plugin_name: 'drp', config: {} }
41
+ let putResponse: FakeResponse = { plugin_name: 'drp', config: {} }
41
42
  let getError: Error | null = null
42
43
  let patchError: Error | null = null
43
- let recordedCalls: { method: string; url: string; data?: unknown }[] = []
44
+ let recordedBaseUrls: Array<string | undefined> = []
45
+ let recordedCalls: {
46
+ method: string
47
+ url: string
48
+ data?: unknown
49
+ headers?: AxiosRequestConfig['headers']
50
+ }[] = []
44
51
 
45
52
  function mockAxios() {
46
53
  vi.spyOn(axios.Axios.prototype, 'get').mockImplementation(async function (
47
54
  this: unknown,
48
55
  url: string,
49
- _config?: AxiosRequestConfig,
56
+ config?: AxiosRequestConfig,
50
57
  ) {
58
+ recordedBaseUrls.push(config?.baseURL)
51
59
  recordedCalls.push({ method: 'GET', url })
52
60
  if (getError) throw getError
53
61
  return { data: getResponse }
@@ -56,12 +64,23 @@ function mockAxios() {
56
64
  this: unknown,
57
65
  url: string,
58
66
  data?: unknown,
59
- _config?: AxiosRequestConfig,
67
+ config?: AxiosRequestConfig,
60
68
  ) {
69
+ recordedBaseUrls.push(config?.baseURL)
61
70
  recordedCalls.push({ method: 'PATCH', url, data })
62
71
  if (patchError) throw patchError
63
72
  return { data: patchResponse }
64
73
  })
74
+ vi.spyOn(axios.Axios.prototype, 'put').mockImplementation(async function (
75
+ this: unknown,
76
+ url: string,
77
+ data?: unknown,
78
+ config?: AxiosRequestConfig,
79
+ ) {
80
+ recordedBaseUrls.push(config?.baseURL)
81
+ recordedCalls.push({ method: 'PUT', url, data, headers: config?.headers })
82
+ return { data: putResponse }
83
+ })
65
84
  }
66
85
 
67
86
  describe('usePluginConfig', () => {
@@ -70,13 +89,17 @@ describe('usePluginConfig', () => {
70
89
  autoLoadOnMount = false
71
90
  getResponse = { plugin_name: 'drp', config: {} }
72
91
  patchResponse = { plugin_name: 'drp', config: {} }
92
+ putResponse = { plugin_name: 'drp', config: {} }
73
93
  getError = null
74
94
  patchError = null
75
95
  recordedCalls = []
96
+ recordedBaseUrls = []
97
+ delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
76
98
  mockAxios()
77
99
  })
78
100
 
79
101
  afterEach(() => {
102
+ delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
80
103
  vi.restoreAllMocks()
81
104
  })
82
105
 
@@ -106,12 +129,148 @@ describe('usePluginConfig', () => {
106
129
  expect(recordedCalls[0]!.url).toBe('/plugins/my%2Fplugin/config')
107
130
  })
108
131
 
109
- it('save PATCHes the plugin config endpoint', async () => {
110
- getResponse = { plugin_name: 'drp', config: { n: 1 } }
111
- patchResponse = { plugin_name: 'drp', config: { n: 2 } }
132
+ it.each([
133
+ 'platform',
134
+ undefined,
135
+ ] as const)(
136
+ 'should use the platform config facade when settings_api is %s',
137
+ async (settingsApi) => {
138
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
139
+ isIntegrated: true,
140
+ theme: 'light',
141
+ platformApiUrl: '/api',
142
+ plugin: {
143
+ id: 'drp',
144
+ name: 'drp',
145
+ version: '1.0.0',
146
+ route_prefix: '/drp',
147
+ api_prefix: '/api/drp',
148
+ ...(settingsApi ? { settings_api: settingsApi } : {}),
149
+ },
150
+ }
151
+ getResponse = {
152
+ plugin_name: 'drp',
153
+ config: { n: 1 },
154
+ revision: 'revision-1',
155
+ }
156
+ const hook = usePluginConfig('drp')
157
+
158
+ await hook.load()
159
+
160
+ expect(recordedCalls).toEqual([
161
+ { method: 'GET', url: '/plugins/drp/config' },
162
+ ])
163
+ },
164
+ )
165
+
166
+ it('should use the plugin settings router for an integrated plugin capability', async () => {
167
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
168
+ isIntegrated: true,
169
+ theme: 'light',
170
+ platformApiUrl: '/api',
171
+ plugin: {
172
+ id: 'drp',
173
+ name: 'drp',
174
+ version: '1.0.0',
175
+ route_prefix: '/drp',
176
+ api_prefix: '/api/drp',
177
+ settings_api: 'plugin',
178
+ },
179
+ }
180
+ getResponse = {
181
+ settings: { n: 1 },
182
+ mode: 'integrated',
183
+ revision: 'revision-1',
184
+ }
185
+ putResponse = {
186
+ settings: { n: 2 },
187
+ mode: 'integrated',
188
+ revision: 'revision-2',
189
+ }
112
190
  const hook = usePluginConfig('drp')
113
191
  await hook.load()
114
192
  hook.config.value = { n: 2 }
193
+
194
+ const saved = await hook.save()
195
+
196
+ expect({
197
+ saved,
198
+ calls: recordedCalls,
199
+ baseUrls: recordedBaseUrls,
200
+ config: hook.config.value,
201
+ }).toEqual({
202
+ saved: true,
203
+ baseUrls: ['/api/drp', '/api/drp'],
204
+ calls: [
205
+ { method: 'GET', url: '/settings' },
206
+ {
207
+ method: 'PUT',
208
+ url: '/settings',
209
+ data: { n: 2 },
210
+ headers: { 'If-Match': '"revision-1"' },
211
+ },
212
+ ],
213
+ config: { n: 2 },
214
+ })
215
+ })
216
+
217
+ it('should use the platform facade for an explicit different plugin name', async () => {
218
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
219
+ isIntegrated: true,
220
+ theme: 'light',
221
+ platformApiUrl: '/api',
222
+ plugin: {
223
+ id: 'current-plugin',
224
+ name: 'current-plugin',
225
+ version: '1.0.0',
226
+ route_prefix: '/current-plugin',
227
+ api_prefix: '/api/current-plugin',
228
+ settings_api: 'plugin',
229
+ },
230
+ }
231
+ getResponse = {
232
+ plugin_name: 'other-plugin',
233
+ config: { n: 1 },
234
+ revision: 'revision-1',
235
+ }
236
+
237
+ const hook = usePluginConfig('other-plugin')
238
+ await hook.load()
239
+
240
+ expect({
241
+ calls: recordedCalls,
242
+ config: hook.config.value,
243
+ }).toEqual({
244
+ calls: [
245
+ { method: 'GET', url: '/plugins/other-plugin/config' },
246
+ ],
247
+ config: { n: 1 },
248
+ })
249
+ })
250
+
251
+ it('should PATCH only dirty typed fields through a versioned platform facade', async () => {
252
+ getResponse = {
253
+ plugin_name: 'drp',
254
+ config: {
255
+ n: 1,
256
+ allowed_experiment_types: ['metabolomics'],
257
+ },
258
+ revision: 'revision-1',
259
+ }
260
+ patchResponse = {
261
+ plugin_name: 'drp',
262
+ config: {
263
+ n: 2,
264
+ allowed_experiment_types: ['metabolomics'],
265
+ },
266
+ revision: 'revision-2',
267
+ }
268
+ const hook = usePluginConfig('drp')
269
+ await hook.load()
270
+ hook.config.value = {
271
+ n: 2,
272
+ allowed_experiment_types: ['metabolomics'],
273
+ }
115
274
  await nextTick()
116
275
  expect(hook.isDirty.value).toBe(true)
117
276
 
@@ -120,11 +279,168 @@ describe('usePluginConfig', () => {
120
279
  const patchCall = recordedCalls.find((c) => c.method === 'PATCH')!
121
280
  expect(patchCall.url).toBe('/plugins/drp/config')
122
281
  expect(patchCall.data).toEqual({ config: { n: 2 } })
123
- expect(hook.config.value).toEqual({ n: 2 })
282
+ expect(patchCall.headers).toBeUndefined()
283
+ expect(hook.config.value).toEqual({
284
+ n: 2,
285
+ allowed_experiment_types: ['metabolomics'],
286
+ })
124
287
  expect(hook.isDirty.value).toBe(false)
125
288
  expect(hook.lastSavedAt.value).toBeInstanceOf(Date)
126
289
  })
127
290
 
291
+ it('should split changed platform-owned config from a versioned typed save', async () => {
292
+ getResponse = {
293
+ plugin_name: 'drp',
294
+ config: {
295
+ n: 1,
296
+ allowed_experiment_types: ['metabolomics'],
297
+ },
298
+ revision: 'revision-1',
299
+ }
300
+ patchResponse = {
301
+ plugin_name: 'drp',
302
+ config: {
303
+ n: 2,
304
+ allowed_experiment_types: ['proteomics'],
305
+ },
306
+ revision: 'untrusted-platform-revision',
307
+ }
308
+ const hook = usePluginConfig('drp')
309
+ await hook.load()
310
+ hook.config.value = {
311
+ n: 2,
312
+ allowed_experiment_types: ['proteomics'],
313
+ }
314
+
315
+ const saved = await hook.save()
316
+ patchResponse = {
317
+ plugin_name: 'drp',
318
+ config: {
319
+ n: 3,
320
+ allowed_experiment_types: ['proteomics'],
321
+ },
322
+ revision: 'revision-3',
323
+ }
324
+ hook.config.value = {
325
+ n: 3,
326
+ allowed_experiment_types: ['proteomics'],
327
+ }
328
+ const savedAgain = await hook.save()
329
+
330
+ expect({
331
+ saved,
332
+ savedAgain,
333
+ calls: recordedCalls,
334
+ config: hook.config.value,
335
+ isDirty: hook.isDirty.value,
336
+ }).toEqual({
337
+ saved: true,
338
+ savedAgain: true,
339
+ calls: [
340
+ { method: 'GET', url: '/plugins/drp/config' },
341
+ {
342
+ method: 'PATCH',
343
+ url: '/plugins/drp/config',
344
+ data: { config: { n: 2 } },
345
+ },
346
+ {
347
+ method: 'PATCH',
348
+ url: '/plugins/drp/config',
349
+ data: { config: { allowed_experiment_types: ['proteomics'] } },
350
+ },
351
+ {
352
+ method: 'PATCH',
353
+ url: '/plugins/drp/config',
354
+ data: { config: { n: 3 } },
355
+ },
356
+ ],
357
+ config: {
358
+ n: 3,
359
+ allowed_experiment_types: ['proteomics'],
360
+ },
361
+ isDirty: false,
362
+ })
363
+ })
364
+
365
+ it('should avoid a typed PUT when only the platform-owned config changes', async () => {
366
+ getResponse = {
367
+ plugin_name: 'drp',
368
+ config: {
369
+ n: 1,
370
+ allowed_experiment_types: ['metabolomics'],
371
+ },
372
+ revision: 'revision-1',
373
+ }
374
+ patchResponse = {
375
+ plugin_name: 'drp',
376
+ config: {
377
+ n: 1,
378
+ allowed_experiment_types: ['proteomics'],
379
+ },
380
+ }
381
+ const hook = usePluginConfig('drp')
382
+ await hook.load()
383
+ hook.config.value = {
384
+ n: 1,
385
+ allowed_experiment_types: ['proteomics'],
386
+ }
387
+
388
+ const saved = await hook.save()
389
+
390
+ expect({
391
+ saved,
392
+ calls: recordedCalls,
393
+ config: hook.config.value,
394
+ }).toEqual({
395
+ saved: true,
396
+ calls: [
397
+ { method: 'GET', url: '/plugins/drp/config' },
398
+ {
399
+ method: 'PATCH',
400
+ url: '/plugins/drp/config',
401
+ data: { config: { allowed_experiment_types: ['proteomics'] } },
402
+ },
403
+ ],
404
+ config: {
405
+ n: 1,
406
+ allowed_experiment_types: ['proteomics'],
407
+ },
408
+ })
409
+ })
410
+
411
+ it('should detect nested config edits and restore the saved JSON snapshot', async () => {
412
+ getResponse = {
413
+ plugin_name: 'drp',
414
+ config: {
415
+ rules: {
416
+ thresholds: [1, 2],
417
+ },
418
+ },
419
+ }
420
+ const hook = usePluginConfig('drp')
421
+ await hook.load()
422
+ const rules = hook.config.value.rules as { thresholds: number[] }
423
+ rules.thresholds[0] = 9
424
+ await nextTick()
425
+
426
+ const dirtyAfterMutation = hook.isDirty.value
427
+ hook.reset()
428
+
429
+ expect({
430
+ dirtyAfterMutation,
431
+ config: hook.config.value,
432
+ isDirty: hook.isDirty.value,
433
+ }).toEqual({
434
+ dirtyAfterMutation: true,
435
+ config: {
436
+ rules: {
437
+ thresholds: [1, 2],
438
+ },
439
+ },
440
+ isDirty: false,
441
+ })
442
+ })
443
+
128
444
  it('isDirty reflects edits vs last saved snapshot', async () => {
129
445
  getResponse = { plugin_name: 'drp', config: { k: 'v' } }
130
446
  const hook = usePluginConfig('drp')
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { GeneratedJsonSchema } from '../../types/generated-ui'
4
+
5
+ describe('GeneratedJsonSchema', () => {
6
+ it('should accept minimum array size constraints emitted by Pydantic', () => {
7
+ const schema: GeneratedJsonSchema = { minItems: 1 }
8
+
9
+ expect(schema.minItems).toBe(1)
10
+ })
11
+
12
+ it('should accept array size constraints emitted by Pydantic', () => {
13
+ const schema: GeneratedJsonSchema = { maxItems: 100 }
14
+
15
+ expect(schema.maxItems).toBe(100)
16
+ })
17
+
18
+ it('should accept write-only fields emitted by Pydantic', () => {
19
+ const schema: GeneratedJsonSchema = { writeOnly: true }
20
+
21
+ expect(schema.writeOnly).toBe(true)
22
+ })
23
+
24
+ it('should accept tuple item schemas emitted by Pydantic', () => {
25
+ const schema: GeneratedJsonSchema = {
26
+ prefixItems: [{ type: 'string' }, { type: 'number' }],
27
+ }
28
+
29
+ expect(schema.prefixItems).toHaveLength(2)
30
+ })
31
+
32
+ it('should accept exclusive numeric bounds emitted by Pydantic', () => {
33
+ const schema: GeneratedJsonSchema = { exclusiveMinimum: 0 }
34
+
35
+ expect(schema.exclusiveMinimum).toBe(0)
36
+ })
37
+
38
+ it('should accept string patterns emitted by Pydantic', () => {
39
+ const schema: GeneratedJsonSchema = { pattern: '^[0-9a-f]{64}$' }
40
+
41
+ expect(schema.pattern).toBe('^[0-9a-f]{64}$')
42
+ })
43
+
44
+ it('should accept discriminators emitted for Pydantic unions', () => {
45
+ const schema: GeneratedJsonSchema = {
46
+ discriminator: {
47
+ propertyName: 'kind',
48
+ mapping: { targeted: '#/$defs/TargetedResult' },
49
+ },
50
+ }
51
+
52
+ expect(schema.discriminator?.propertyName).toBe('kind')
53
+ })
54
+
55
+ it('should accept exclusive union branches emitted by Pydantic', () => {
56
+ const schema: GeneratedJsonSchema = {
57
+ oneOf: [{ $ref: '#/$defs/TargetedResult' }],
58
+ }
59
+
60
+ expect(schema.oneOf).toHaveLength(1)
61
+ })
62
+ })
@@ -1,9 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  /** Renders a native `<select>` element with size, error, and disabled states. */
3
- import { computed } from 'vue'
3
+ import { computed, useAttrs } from 'vue'
4
+ import type { HTMLAttributes } from 'vue'
4
5
  import type { SelectOption, SelectOptionInput } from '../types'
5
6
  import { findOptionByDomValue, normalizeOptionInput } from '../utils/options'
6
7
 
8
+ defineOptions({ inheritAttrs: false })
9
+
7
10
  interface Props {
8
11
  modelValue?: string | number
9
12
  options: SelectOptionInput<string | number>[]
@@ -19,6 +22,16 @@ const props = withDefaults(defineProps<Props>(), {
19
22
  error: false,
20
23
  size: 'md',
21
24
  })
25
+ const attrs = useAttrs()
26
+
27
+ const controlAttrs = computed(() => {
28
+ const { class: _class, style: _style, ...nativeAttrs } = attrs
29
+ return nativeAttrs
30
+ })
31
+ const wrapperAttrs = computed<Pick<HTMLAttributes, 'class' | 'style'>>(() => ({
32
+ class: attrs.class as HTMLAttributes['class'],
33
+ style: attrs.style as HTMLAttributes['style'],
34
+ }))
22
35
 
23
36
  const emit = defineEmits<{
24
37
  'update:modelValue': [value: string | number]
@@ -36,8 +49,9 @@ function handleChange(event: Event) {
36
49
  </script>
37
50
 
38
51
  <template>
39
- <div class="mint-select">
52
+ <div v-bind="wrapperAttrs" class="mint-select">
40
53
  <select
54
+ v-bind="controlAttrs"
41
55
  :value="modelValue"
42
56
  :disabled="disabled"
43
57
  :aria-invalid="error || undefined"
@@ -5,6 +5,8 @@ import Tooltip from './Tooltip.vue'
5
5
  interface Props {
6
6
  modelValue?: boolean
7
7
  label?: string
8
+ /** Accessible switch name; defaults to the visual label, then "Toggle". */
9
+ ariaLabel?: string
8
10
  description?: string
9
11
  icon?: string | string[]
10
12
  iconColor?: string
@@ -111,7 +113,7 @@ function iconStyle(): Record<string, string> {
111
113
  role="switch"
112
114
  :tabindex="disabled ? -1 : 0"
113
115
  :aria-checked="modelValue"
114
- :aria-label="label || 'Toggle'"
116
+ :aria-label="ariaLabel || label || 'Toggle'"
115
117
  :class="[
116
118
  'mint-toggle__track',
117
119
  `mint-toggle__track--${size}`,
@@ -116,7 +116,7 @@ function resourceSummary(definition: GeneratedJobDefinition): string {
116
116
  if (definition.lifecycle === 'staged' || definition.profile?.kind === 'staged') {
117
117
  return 'Staged upload'
118
118
  }
119
- return `${definition.cpu} CPU`
119
+ return `Up to ${definition.cpu} CPU`
120
120
  }
121
121
 
122
122
  onMounted(() => {
@@ -34,7 +34,7 @@ export interface PluginWorkspaceViewProps {
34
34
  homePath?: string
35
35
  /** Show the default MINT logo when no icon/logo slot is provided. */
36
36
  showLogo?: boolean
37
- /** Preferred route-level page selector entries. */
37
+ /** Preferred route-level page selector entries. Pass [] to suppress integrated platform fallback. */
38
38
  pageSelector?: PageSelectorItemInput[]
39
39
  /** Active page selector id. Defaults to activeView. */
40
40
  currentPageSelectorId?: string
@@ -208,10 +208,6 @@ const {
208
208
  emitPillSelect: (item) => emit('pill-select', item),
209
209
  })
210
210
 
211
- const visiblePageSelector = computed(() =>
212
- (props.pageSelector?.length ?? 0) > 1 ? props.pageSelector : undefined,
213
- )
214
-
215
211
  const hasSidebarSurface = computed(() => {
216
212
  return hasPluginWorkspaceSidebarSurface({
217
213
  showSidebar: props.showSidebar,
@@ -274,7 +270,7 @@ function handleFormCancel(sectionId: string) {
274
270
  :variant="topBarVariant"
275
271
  :home-path="homePath"
276
272
  :show-logo="showLogo"
277
- :page-selector="visiblePageSelector"
273
+ :page-selector="pageSelector"
278
274
  :current-page-selector-id="resolvedCurrentPageSelectorId"
279
275
  :plugin-switcher="pluginSwitcher"
280
276
  :pill-nav="resolvedPillNav"