@morscherlab/mint-sdk 1.0.0-beta.3 → 1.0.0-beta.4

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 (165) hide show
  1. package/README.md +9 -2
  2. package/dist/__tests__/composables/experiment-utils.test.d.ts +1 -0
  3. package/dist/__tests__/composables/useApi.test.d.ts +1 -0
  4. package/dist/components/AppContainer.vue.d.ts +1 -1
  5. package/dist/components/AppLayout.vue.d.ts +20 -1
  6. package/dist/components/AppSidebar.vue.d.ts +56 -4
  7. package/dist/components/AppTopBar.vue.d.ts +7 -25
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +3 -1
  9. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -0
  10. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +5 -0
  11. package/dist/components/ComponentBindingRenderer.vue.d.ts +44 -0
  12. package/dist/components/ControlWorkspaceView.vue.d.ts +24 -7
  13. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +149 -0
  14. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  15. package/dist/components/FormBuilder.vue.d.ts +9 -9
  16. package/dist/components/PlateMapEditor.vue.d.ts +1 -1
  17. package/dist/components/PluginWorkspaceView.vue.d.ts +310 -0
  18. package/dist/components/SettingsModal.vue.d.ts +1 -1
  19. package/dist/components/WellPlate.vue.d.ts +2 -2
  20. package/dist/components/index.d.ts +3 -12
  21. package/dist/components/index.js +3 -3
  22. package/dist/components/{AppPageSelector.vue.d.ts → internal/AppPageSelectorInternal.vue.d.ts} +1 -1
  23. package/dist/components/{AppPillNav.vue.d.ts → internal/AppPillNavInternal.vue.d.ts} +3 -1
  24. package/dist/components/{CalendarGridPanel.vue.d.ts → internal/CalendarGridPanelInternal.vue.d.ts} +1 -1
  25. package/dist/components/internal/FormSectionRenderer.vue.d.ts +4 -4
  26. package/dist/components/{WellEditPopup.vue.d.ts → internal/WellEditPopupInternal.vue.d.ts} +1 -1
  27. package/dist/{components-D_Sr0adg.js → components-BkGF4B4y.js} +4484 -3967
  28. package/dist/components-BkGF4B4y.js.map +1 -0
  29. package/dist/composables/experiment-utils.d.ts +8 -0
  30. package/dist/composables/index.d.ts +5 -7
  31. package/dist/composables/index.js +4 -4
  32. package/dist/composables/useAppExperiment.d.ts +31 -2
  33. package/dist/composables/useBioTemplateComponents.d.ts +5 -3
  34. package/dist/composables/useBioTemplatePackWorkspace.d.ts +3 -2
  35. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +6 -5
  36. package/dist/composables/useBioTemplateWorkspace.d.ts +5 -4
  37. package/dist/composables/useControlSchema.d.ts +43 -21
  38. package/dist/composables/usePluginClient.d.ts +5 -2
  39. package/dist/{composables-C3dpXQN5.js → composables-CHsME9H1.js} +40 -28
  40. package/dist/composables-CHsME9H1.js.map +1 -0
  41. package/dist/index.d.ts +5 -12
  42. package/dist/index.js +5 -5
  43. package/dist/install.js +2 -2
  44. package/dist/styles.css +3625 -3651
  45. package/dist/templates/componentBindings.d.ts +13 -0
  46. package/dist/templates/index.d.ts +3 -3
  47. package/dist/templates/index.js +2 -2
  48. package/dist/{templates-50NPjaxL.js → templates-B5jmTWuk.js} +111 -56
  49. package/dist/templates-B5jmTWuk.js.map +1 -0
  50. package/dist/types/components.d.ts +6 -25
  51. package/dist/types/index.d.ts +1 -1
  52. package/dist/{useScheduleDrag-D4oWdh41.js → useScheduleDrag-BgzpQT53.js} +160 -117
  53. package/dist/useScheduleDrag-BgzpQT53.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/__tests__/components/ActionItem.test.ts +6 -6
  56. package/src/__tests__/components/AppLayout.test.ts +44 -0
  57. package/src/__tests__/components/AppPageSelector.test.ts +8 -8
  58. package/src/__tests__/components/AppPillNav.test.ts +53 -6
  59. package/src/__tests__/components/AppSidebar.test.ts +126 -0
  60. package/src/__tests__/components/AppToastContainer.test.ts +0 -11
  61. package/src/__tests__/components/AppTopBar.test.ts +182 -119
  62. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +7 -1
  63. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +15 -1
  64. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +26 -1
  65. package/src/__tests__/components/CalendarGridPanel.test.ts +3 -3
  66. package/src/__tests__/components/ComponentBindingRenderer.test.ts +161 -0
  67. package/src/__tests__/components/ControlWorkspaceView.test.ts +134 -63
  68. package/src/__tests__/components/DateTimePicker.test.ts +2 -2
  69. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
  70. package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
  71. package/src/__tests__/composables/experiment-utils.test.ts +30 -0
  72. package/src/__tests__/composables/useApi.test.ts +30 -0
  73. package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
  74. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +6 -3
  75. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +6 -6
  76. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +6 -1
  77. package/src/__tests__/composables/useControlSchema.test.ts +150 -36
  78. package/src/__tests__/composables/usePluginClient.test.ts +99 -2
  79. package/src/__tests__/docs/frontendDocsCatalog.test.ts +120 -25
  80. package/src/__tests__/templates/templates.test.ts +12 -0
  81. package/src/components/AppAvatarMenu.vue +3 -3
  82. package/src/components/AppLayout.story.vue +39 -0
  83. package/src/components/AppLayout.vue +83 -2
  84. package/src/components/AppPluginSwitcher.vue +5 -5
  85. package/src/components/AppSidebar.story.vue +113 -5
  86. package/src/components/AppSidebar.vue +144 -24
  87. package/src/components/AppTopBar.story.vue +2 -5
  88. package/src/components/AppTopBar.vue +35 -425
  89. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +2 -2
  90. package/src/components/BioTemplateExperimentWorkspaceView.vue +6 -0
  91. package/src/components/BioTemplatePackWorkspaceView.story.vue +4 -4
  92. package/src/components/BioTemplatePackWorkspaceView.vue +1 -0
  93. package/src/components/BioTemplatePresetWorkspaceView.story.vue +14 -2
  94. package/src/components/BioTemplatePresetWorkspaceView.vue +11 -2
  95. package/src/components/BioTemplateRenderer.vue +15 -227
  96. package/src/components/ComponentBindingRenderer.story.vue +57 -0
  97. package/src/components/ComponentBindingRenderer.vue +308 -0
  98. package/src/components/ControlWorkspaceView.story.vue +20 -9
  99. package/src/components/ControlWorkspaceView.vue +43 -12
  100. package/src/components/DatePicker.vue +2 -2
  101. package/src/components/DateTimePicker.vue +2 -2
  102. package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
  103. package/src/components/DoseDesignWorkspaceView.vue +255 -0
  104. package/src/components/ExperimentPopover.vue +2 -6
  105. package/src/components/ExperimentSelectorModal.vue +6 -5
  106. package/src/components/FormBuilder.story.vue +190 -0
  107. package/src/components/PluginWorkspaceView.story.vue +334 -0
  108. package/src/components/PluginWorkspaceView.vue +708 -0
  109. package/src/components/SettingsModal.story.vue +87 -0
  110. package/src/components/WellPlate.vue +2 -2
  111. package/src/components/index.ts +3 -12
  112. package/src/components/{AppPageSelector.vue → internal/AppPageSelectorInternal.vue} +9 -9
  113. package/src/components/internal/AppPillNavInternal.vue +194 -0
  114. package/src/components/{CalendarGridPanel.vue → internal/CalendarGridPanelInternal.vue} +1 -1
  115. package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +3 -3
  116. package/src/composables/experiment-utils.ts +26 -0
  117. package/src/composables/index.ts +21 -7
  118. package/src/composables/useApi.ts +9 -2
  119. package/src/composables/useAppExperiment.ts +85 -13
  120. package/src/composables/useBioTemplateComponents.ts +12 -0
  121. package/src/composables/useBioTemplatePackWorkspace.ts +6 -2
  122. package/src/composables/useBioTemplatePresetWorkspace.ts +10 -21
  123. package/src/composables/useBioTemplateWorkspace.ts +6 -4
  124. package/src/composables/useControlSchema.ts +157 -69
  125. package/src/composables/usePluginClient.ts +50 -9
  126. package/src/index.ts +6 -563
  127. package/src/styles/components/app-layout.css +82 -0
  128. package/src/styles/components/app-pill-nav.css +70 -0
  129. package/src/styles/components/app-sidebar.css +119 -0
  130. package/src/styles/components/app-top-bar.css +0 -235
  131. package/src/styles/index.css +0 -1
  132. package/src/templates/componentBindings.ts +38 -0
  133. package/src/templates/index.ts +4 -0
  134. package/src/types/components.ts +6 -31
  135. package/src/types/index.ts +2 -6
  136. package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
  137. package/dist/components/FormFieldRenderer.vue.d.ts +0 -28
  138. package/dist/components/FormSection.vue.d.ts +0 -30
  139. package/dist/components/GroupingModal.vue.d.ts +0 -12
  140. package/dist/components/SettingsButton.vue.d.ts +0 -30
  141. package/dist/components/ToastNotification.vue.d.ts +0 -2
  142. package/dist/components-D_Sr0adg.js.map +0 -1
  143. package/dist/composables/usePluginApi.d.ts +0 -22
  144. package/dist/composables-C3dpXQN5.js.map +0 -1
  145. package/dist/templates-50NPjaxL.js.map +0 -1
  146. package/dist/useScheduleDrag-D4oWdh41.js.map +0 -1
  147. package/src/__tests__/components/FormCompatibility.test.ts +0 -94
  148. package/src/__tests__/components/GroupingModal.test.ts +0 -73
  149. package/src/__tests__/components/SettingsButton.test.ts +0 -44
  150. package/src/__tests__/composables/usePluginApi.test.ts +0 -81
  151. package/src/components/AppPillNav.vue +0 -71
  152. package/src/components/FormFieldRenderer.vue +0 -35
  153. package/src/components/FormSection.vue +0 -37
  154. package/src/components/GroupingModal.story.vue +0 -52
  155. package/src/components/GroupingModal.vue +0 -61
  156. package/src/components/SettingsButton.story.vue +0 -58
  157. package/src/components/SettingsButton.vue +0 -64
  158. package/src/components/ToastNotification.vue +0 -9
  159. package/src/composables/usePluginApi.ts +0 -32
  160. package/src/styles/components/settings-button.css +0 -31
  161. /package/dist/__tests__/components/{FormCompatibility.test.d.ts → ComponentBindingRenderer.test.d.ts} +0 -0
  162. /package/dist/__tests__/components/{GroupingModal.test.d.ts → DoseDesignWorkspaceView.test.d.ts} +0 -0
  163. /package/dist/__tests__/components/{SettingsButton.test.d.ts → PluginWorkspaceView.test.d.ts} +0 -0
  164. /package/dist/components/{ActionItem.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +0 -0
  165. /package/src/components/{ActionItem.vue → internal/ActionItemInternal.vue} +0 -0
@@ -0,0 +1,161 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { defineComponent } from 'vue'
3
+ import { describe, expect, it } from 'vitest'
4
+ import ComponentBindingRenderer from '../../components/ComponentBindingRenderer.vue'
5
+
6
+ const WellPlateStub = defineComponent({
7
+ name: 'WellPlate',
8
+ props: {
9
+ modelValue: { type: Array, default: () => [] },
10
+ readonly: { type: Boolean, default: false },
11
+ size: { type: String, default: undefined },
12
+ },
13
+ template: '<div data-test="well-plate" />',
14
+ })
15
+
16
+ const DoseCalculatorStub = defineComponent({
17
+ name: 'DoseCalculator',
18
+ props: {
19
+ mode: { type: String, default: undefined },
20
+ targetWells: { type: Array, default: () => [] },
21
+ },
22
+ template: '<div data-test="dose-calculator" />',
23
+ })
24
+
25
+ const DataFrameStub = defineComponent({
26
+ name: 'DataFrame',
27
+ props: {
28
+ data: { type: Array, default: () => [] },
29
+ columns: { type: Array, default: () => [] },
30
+ size: { type: String, default: undefined },
31
+ maxHeight: { type: String, default: undefined },
32
+ },
33
+ template: '<div data-test="data-frame" />',
34
+ })
35
+
36
+ const PlateMapEditorStub = defineComponent({
37
+ name: 'PlateMapEditor',
38
+ props: {
39
+ modelValue: { type: Object, default: () => ({}) },
40
+ showToolbar: { type: Boolean, default: true },
41
+ showSidebar: { type: Boolean, default: true },
42
+ allowAddPlates: { type: Boolean, default: true },
43
+ allowAddSamples: { type: Boolean, default: true },
44
+ size: { type: String, default: undefined },
45
+ },
46
+ template: '<div data-test="plate-map-editor" />',
47
+ })
48
+
49
+ const globalOptions = {
50
+ stubs: {
51
+ DataFrame: DataFrameStub,
52
+ DoseCalculator: DoseCalculatorStub,
53
+ PlateMapEditor: PlateMapEditorStub,
54
+ WellPlate: WellPlateStub,
55
+ },
56
+ }
57
+
58
+ describe('ComponentBindingRenderer', () => {
59
+ it('renders known SDK component bindings and skips unsupported names', () => {
60
+ const wrapper = mount(ComponentBindingRenderer, {
61
+ props: {
62
+ bindings: [
63
+ {
64
+ id: 'plate',
65
+ component: 'WellPlate',
66
+ props: { modelValue: ['A1'] },
67
+ description: 'Selected wells',
68
+ },
69
+ {
70
+ id: 'dose',
71
+ component: 'DoseCalculator',
72
+ props: { mode: 'serial', targetWells: ['A1'] },
73
+ },
74
+ {
75
+ id: 'custom',
76
+ component: 'CustomChart',
77
+ props: { score: 0.5 },
78
+ },
79
+ ],
80
+ },
81
+ global: globalOptions,
82
+ })
83
+
84
+ expect(wrapper.find('[data-component-binding-id="plate"]').exists()).toBe(true)
85
+ expect(wrapper.find('[data-component-binding-id="dose"]').exists()).toBe(true)
86
+ expect(wrapper.find('[data-component-binding-id="custom"]').exists()).toBe(false)
87
+ expect(wrapper.findComponent(WellPlateStub).props()).toMatchObject({
88
+ modelValue: ['A1'],
89
+ readonly: true,
90
+ size: 'fill',
91
+ })
92
+ expect(wrapper.findComponent(DoseCalculatorStub).props()).toMatchObject({
93
+ mode: 'serial',
94
+ targetWells: ['A1'],
95
+ })
96
+ expect(wrapper.text()).toContain('Selected wells')
97
+ })
98
+
99
+ it('accepts a single binding and preview-safe dense props', () => {
100
+ const wrapper = mount(ComponentBindingRenderer, {
101
+ props: {
102
+ binding: {
103
+ id: 'plate-map',
104
+ component: 'PlateMapEditor',
105
+ propsObject: { modelValue: { plates: [] } },
106
+ },
107
+ dense: true,
108
+ },
109
+ global: globalOptions,
110
+ })
111
+
112
+ expect(wrapper.findComponent(PlateMapEditorStub).props()).toMatchObject({
113
+ showToolbar: false,
114
+ showSidebar: false,
115
+ allowAddPlates: false,
116
+ allowAddSamples: false,
117
+ size: 'md',
118
+ })
119
+ })
120
+
121
+ it('filters bindings and renders a compact empty state', () => {
122
+ const wrapper = mount(ComponentBindingRenderer, {
123
+ props: {
124
+ bindings: [
125
+ {
126
+ id: 'table',
127
+ component: 'DataFrame',
128
+ props: { data: [{ id: 1 }], columns: [{ key: 'id', label: 'ID' }] },
129
+ },
130
+ ],
131
+ include: ['WellPlate'],
132
+ emptyText: 'No components.',
133
+ },
134
+ global: globalOptions,
135
+ })
136
+
137
+ expect(wrapper.find('[data-component-binding-id="table"]').exists()).toBe(false)
138
+ expect(wrapper.text()).toContain('No components.')
139
+ })
140
+
141
+ it('normalizes DataFrame sizing in dense mode', () => {
142
+ const wrapper = mount(ComponentBindingRenderer, {
143
+ props: {
144
+ bindings: [
145
+ {
146
+ id: 'table',
147
+ component: 'DataFrame',
148
+ props: { data: [{ id: 1 }], columns: [{ key: 'id', label: 'ID' }] },
149
+ },
150
+ ],
151
+ dense: true,
152
+ },
153
+ global: globalOptions,
154
+ })
155
+
156
+ expect(wrapper.findComponent(DataFrameStub).props()).toMatchObject({
157
+ size: 'sm',
158
+ maxHeight: '280px',
159
+ })
160
+ })
161
+ })
@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
2
2
  import { createPinia } from 'pinia'
3
3
  import { computed, h, nextTick, ref } from 'vue'
4
4
  import { describe, expect, it, vi } from 'vitest'
5
+ import AppLayout from '../../components/AppLayout.vue'
5
6
  import AppSidebar from '../../components/AppSidebar.vue'
6
7
  import AppTopBar from '../../components/AppTopBar.vue'
7
8
  import ControlWorkspaceView from '../../components/ControlWorkspaceView.vue'
@@ -96,16 +97,38 @@ describe('ControlWorkspaceView', () => {
96
97
  const wrapper = mountView(workspace)
97
98
 
98
99
  expect(wrapper.findComponent(AppTopBar).props('title')).toBe('Analysis Workspace')
99
- expect(wrapper.findComponent(AppTopBar).props('tabs')).toBeUndefined()
100
+ expect('tabs' in wrapper.findComponent(AppTopBar).props()).toBe(false)
100
101
  expect(wrapper.findComponent(AppTopBar).props('pillNav')).toEqual([
101
102
  { id: 'analysis', label: 'Run' },
102
103
  { id: 'results', label: 'Results' },
103
104
  ])
104
105
  expect(wrapper.findComponent(AppTopBar).props('currentPillId')).toBe('analysis')
106
+ expect(wrapper.findComponent(AppLayout).props('responsiveSidebar')).toBe(true)
105
107
  expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('analysis')
108
+ expect(wrapper.findComponent(AppSidebar).props('variant')).toBe('analysis')
106
109
  expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject(workspace.values)
107
110
  })
108
111
 
112
+ it('lets callers override the generated sidebar chrome', () => {
113
+ const wrapper = mount(ControlWorkspaceView, {
114
+ props: {
115
+ workspace: createWorkspace(),
116
+ sidebarVariant: 'default',
117
+ responsiveSidebar: false,
118
+ sidebarTitle: 'Peak Picking',
119
+ sidebarSubtitle: 'Current experiment',
120
+ sidebarBadge: 3,
121
+ },
122
+ global: globalOptions,
123
+ })
124
+
125
+ expect(wrapper.findComponent(AppLayout).props('responsiveSidebar')).toBe(false)
126
+ expect(wrapper.findComponent(AppSidebar).props('variant')).toBe('default')
127
+ expect(wrapper.findComponent(AppSidebar).props('title')).toBe('Peak Picking')
128
+ expect(wrapper.findComponent(AppSidebar).props('subtitle')).toBe('Current experiment')
129
+ expect(wrapper.findComponent(AppSidebar).props('badge')).toBe(3)
130
+ })
131
+
109
132
  it('can create the workspace internally from direct controls', async () => {
110
133
  const controls = defineControls({
111
134
  threshold: {
@@ -365,26 +388,49 @@ describe('ControlWorkspaceView', () => {
365
388
  model,
366
389
  },
367
390
  slots: {
368
- default: ({ componentPropsById }) => h(
391
+ default: ({ componentBindings, componentPropsById }) => h(
369
392
  'pre',
370
393
  { class: 'component-props-by-id' },
371
- JSON.stringify(componentPropsById),
394
+ JSON.stringify({ componentBindings, componentPropsById }),
372
395
  ),
373
396
  },
374
397
  global: globalOptions,
375
398
  })
376
399
 
377
400
  expect(JSON.parse(wrapper.find('.component-props-by-id').text())).toEqual({
378
- plate: {
379
- modelValue: ['A1', 'A2'],
380
- format: 96,
381
- disabled: false,
382
- },
383
- dose: {
384
- mode: 'serial',
385
- targetWells: ['A1', 'A2'],
386
- disabled: false,
387
- molecularWeight: 300,
401
+ componentBindings: [
402
+ {
403
+ id: 'plate',
404
+ component: 'WellPlate',
405
+ props: {
406
+ modelValue: ['A1', 'A2'],
407
+ format: 96,
408
+ disabled: false,
409
+ },
410
+ },
411
+ {
412
+ id: 'dose',
413
+ component: 'DoseCalculator',
414
+ props: {
415
+ mode: 'serial',
416
+ targetWells: ['A1', 'A2'],
417
+ disabled: false,
418
+ molecularWeight: 300,
419
+ },
420
+ },
421
+ ],
422
+ componentPropsById: {
423
+ plate: {
424
+ modelValue: ['A1', 'A2'],
425
+ format: 96,
426
+ disabled: false,
427
+ },
428
+ dose: {
429
+ mode: 'serial',
430
+ targetWells: ['A1', 'A2'],
431
+ disabled: false,
432
+ molecularWeight: 300,
433
+ },
388
434
  },
389
435
  })
390
436
  })
@@ -423,6 +469,23 @@ describe('ControlWorkspaceView', () => {
423
469
  disabled: values => !values.enabled,
424
470
  },
425
471
  },
472
+ componentBindings: {
473
+ dose: {
474
+ component: 'DoseCalculator',
475
+ props: {
476
+ mode: 'doseMode',
477
+ targetWells: 'targetWells',
478
+ disabled: values => !values.enabled,
479
+ },
480
+ },
481
+ plate: {
482
+ component: 'WellPlate',
483
+ props: {
484
+ modelValue: 'targetWells',
485
+ disabled: values => !values.enabled,
486
+ },
487
+ },
488
+ },
426
489
  })
427
490
 
428
491
  const wrapper = mount(ControlWorkspaceView, {
@@ -430,16 +493,35 @@ describe('ControlWorkspaceView', () => {
430
493
  ...model,
431
494
  },
432
495
  slots: {
433
- default: ({ componentProps, componentPropsById }) => h(
496
+ default: ({ componentBindingsById, componentProps, componentPropsById }) => h(
434
497
  'pre',
435
498
  { class: 'component-props' },
436
- JSON.stringify({ componentProps, componentPropsById }),
499
+ JSON.stringify({ componentBindingsById, componentProps, componentPropsById }),
437
500
  ),
438
501
  },
439
502
  global: globalOptions,
440
503
  })
441
504
 
442
505
  expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
506
+ componentBindingsById: {
507
+ dose: {
508
+ id: 'dose',
509
+ component: 'DoseCalculator',
510
+ props: {
511
+ mode: 'serial',
512
+ targetWells: ['A1', 'A2'],
513
+ disabled: false,
514
+ },
515
+ },
516
+ plate: {
517
+ id: 'plate',
518
+ component: 'WellPlate',
519
+ props: {
520
+ modelValue: ['A1', 'A2'],
521
+ disabled: false,
522
+ },
523
+ },
524
+ },
443
525
  componentProps: {
444
526
  mode: 'serial',
445
527
  targetWells: ['A1', 'A2'],
@@ -466,6 +548,25 @@ describe('ControlWorkspaceView', () => {
466
548
  await nextTick()
467
549
 
468
550
  expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
551
+ componentBindingsById: {
552
+ dose: {
553
+ id: 'dose',
554
+ component: 'DoseCalculator',
555
+ props: {
556
+ mode: 'dilution',
557
+ targetWells: ['B1'],
558
+ disabled: true,
559
+ },
560
+ },
561
+ plate: {
562
+ id: 'plate',
563
+ component: 'WellPlate',
564
+ props: {
565
+ modelValue: ['B1'],
566
+ disabled: true,
567
+ },
568
+ },
569
+ },
469
570
  componentProps: {
470
571
  mode: 'dilution',
471
572
  targetWells: ['B1'],
@@ -839,51 +940,6 @@ describe('ControlWorkspaceView', () => {
839
940
  expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('results')
840
941
  })
841
942
 
842
- it('can opt into legacy topbar tab navigation from the generated workspace', async () => {
843
- const controls = defineControls({
844
- threshold: {
845
- type: 'number',
846
- default: 0.05,
847
- section: 'parameters',
848
- view: 'analysis',
849
- },
850
- chartScale: {
851
- default: 'linear',
852
- options: ['linear', 'log'],
853
- section: 'display',
854
- view: 'results',
855
- },
856
- })
857
-
858
- const wrapper = mount(ControlWorkspaceView, {
859
- props: {
860
- controls,
861
- controlOptions: {
862
- views: {
863
- analysis: { label: 'Run' },
864
- results: { label: 'Results' },
865
- },
866
- },
867
- navigation: 'tabs',
868
- },
869
- global: globalOptions,
870
- })
871
-
872
- const topBar = wrapper.findComponent(AppTopBar)
873
- expect(topBar.props('pillNav')).toBeUndefined()
874
- expect(topBar.props('tabs')).toEqual([
875
- { id: 'analysis', label: 'Run' },
876
- { id: 'results', label: 'Results' },
877
- ])
878
- expect(topBar.props('currentTabId')).toBe('analysis')
879
-
880
- topBar.vm.$emit('tab-select', { id: 'results', label: 'Results' })
881
- await nextTick()
882
-
883
- expect(wrapper.findComponent(AppTopBar).props('currentTabId')).toBe('results')
884
- expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('results')
885
- })
886
-
887
943
  it('exposes workspace data to the default slot', () => {
888
944
  const workspace = createWorkspace()
889
945
  const wrapper = mount(ControlWorkspaceView, {
@@ -976,6 +1032,15 @@ describe('ControlWorkspaceView', () => {
976
1032
  disabled: values => !values.enabled,
977
1033
  },
978
1034
  },
1035
+ componentBindings: {
1036
+ chart: {
1037
+ component: 'ResultChart',
1038
+ props: {
1039
+ score: 'threshold',
1040
+ disabled: values => !values.enabled,
1041
+ },
1042
+ },
1043
+ },
979
1044
  })
980
1045
 
981
1046
  const wrapper = mount(ControlWorkspaceView, {
@@ -987,7 +1052,7 @@ describe('ControlWorkspaceView', () => {
987
1052
  'pre',
988
1053
  { class: 'topbar-bindings' },
989
1054
  JSON.stringify({
990
- legacyTabId: topBar.currentTabId,
1055
+ slotPillId: topBar.currentPillId,
991
1056
  pillId: bindings.topBar.value.currentPillId,
992
1057
  }),
993
1058
  ),
@@ -999,12 +1064,15 @@ describe('ControlWorkspaceView', () => {
999
1064
  bindingActiveView: bindings.sidebar.activeView,
1000
1065
  }),
1001
1066
  ),
1002
- default: ({ bindings, componentPropsById }) => h(
1067
+ default: ({ bindings, componentBindingsById, componentPropsById }) => h(
1003
1068
  'pre',
1004
1069
  { class: 'default-bindings' },
1005
1070
  JSON.stringify({
1006
1071
  formThreshold: bindings.form.modelValue.threshold,
1072
+ component: componentBindingsById.chart.component,
1073
+ bindingComponent: bindings.componentBindingsById.value.chart.component,
1007
1074
  bindingScore: bindings.componentProps.value.score,
1075
+ bindingComponentScore: bindings.componentBindings.value[0].props.score,
1008
1076
  slotScore: componentPropsById.chart.score,
1009
1077
  bindingSlotScore: bindings.componentPropsById.value.chart.score,
1010
1078
  }),
@@ -1014,7 +1082,7 @@ describe('ControlWorkspaceView', () => {
1014
1082
  })
1015
1083
 
1016
1084
  expect(JSON.parse(wrapper.find('.topbar-bindings').text())).toEqual({
1017
- legacyTabId: 'analysis',
1085
+ slotPillId: 'analysis',
1018
1086
  pillId: 'analysis',
1019
1087
  })
1020
1088
  expect(JSON.parse(wrapper.find('.sidebar-bindings').text())).toEqual({
@@ -1023,7 +1091,10 @@ describe('ControlWorkspaceView', () => {
1023
1091
  })
1024
1092
  expect(JSON.parse(wrapper.find('.default-bindings').text())).toEqual({
1025
1093
  formThreshold: 0.05,
1094
+ component: 'ResultChart',
1095
+ bindingComponent: 'ResultChart',
1026
1096
  bindingScore: 0.05,
1097
+ bindingComponentScore: 0.05,
1027
1098
  slotScore: 0.05,
1028
1099
  bindingSlotScore: 0.05,
1029
1100
  })
@@ -1,7 +1,7 @@
1
1
  import { mount } from '@vue/test-utils'
2
2
  import { nextTick } from 'vue'
3
3
  import { describe, expect, it } from 'vitest'
4
- import CalendarGridPanel from '../../components/CalendarGridPanel.vue'
4
+ import CalendarGridPanelInternal from '../../components/internal/CalendarGridPanelInternal.vue'
5
5
  import DateTimePicker from '../../components/DateTimePicker.vue'
6
6
 
7
7
  describe('DateTimePicker', () => {
@@ -15,7 +15,7 @@ describe('DateTimePicker', () => {
15
15
  await wrapper.find('input').trigger('click')
16
16
  await nextTick()
17
17
 
18
- expect(wrapper.findComponent(CalendarGridPanel).exists()).toBe(true)
18
+ expect(wrapper.findComponent(CalendarGridPanelInternal).exists()).toBe(true)
19
19
  expect(wrapper.find('.mint-date-picker__day--selected').text()).toBe('22')
20
20
 
21
21
  const timeButton = wrapper.findAll('.mint-datetime-picker__time-chip')
@@ -0,0 +1,185 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { createPinia } from 'pinia'
3
+ import { computed, defineComponent, h, ref, nextTick } from 'vue'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import AppLayout from '../../components/AppLayout.vue'
6
+ import AppSidebar from '../../components/AppSidebar.vue'
7
+ import DoseDesignWorkspaceView from '../../components/DoseDesignWorkspaceView.vue'
8
+
9
+ vi.mock('../../composables/usePlatformContext', () => ({
10
+ usePlatformContext: vi.fn(() => ({
11
+ isIntegrated: computed(() => false),
12
+ context: ref({ isIntegrated: false, theme: 'system' }),
13
+ plugin: computed(() => undefined),
14
+ user: computed(() => undefined),
15
+ theme: computed(() => 'system' as const),
16
+ features: computed(() => undefined),
17
+ navigate: vi.fn(),
18
+ notify: vi.fn(),
19
+ sendToPlatform: vi.fn(),
20
+ })),
21
+ }))
22
+
23
+ vi.mock('../../composables/useTheme', () => ({
24
+ useTheme: vi.fn(() => ({
25
+ isDark: ref(false),
26
+ theme: ref('light'),
27
+ toggleTheme: vi.fn(),
28
+ })),
29
+ }))
30
+
31
+ const WellPlateStub = defineComponent({
32
+ name: 'WellPlate',
33
+ props: {
34
+ modelValue: { type: Array, default: () => [] },
35
+ format: { type: Number, default: 96 },
36
+ wells: { type: Object, default: undefined },
37
+ disabled: { type: Boolean, default: false },
38
+ size: { type: String, default: undefined },
39
+ selectionMode: { type: String, default: undefined },
40
+ heatmap: { type: Object, default: undefined },
41
+ },
42
+ emits: ['update:modelValue'],
43
+ template: '<button data-test="well-plate" @click="$emit(\'update:modelValue\', [\'B1\', \'B2\'])" />',
44
+ })
45
+
46
+ const DoseCalculatorStub = defineComponent({
47
+ name: 'DoseCalculator',
48
+ props: {
49
+ mode: { type: String, default: 'auto' },
50
+ targetWells: { type: Array, default: () => [] },
51
+ disabled: { type: Boolean, default: false },
52
+ molecularWeight: { type: Number, default: undefined },
53
+ },
54
+ template: '<div data-test="dose-calculator" />',
55
+ })
56
+
57
+ function mountWorkspace(
58
+ props: Record<string, unknown> = {},
59
+ slots: Record<string, unknown> = {},
60
+ ) {
61
+ return mount(DoseDesignWorkspaceView, {
62
+ props,
63
+ slots: slots as never,
64
+ global: {
65
+ plugins: [createPinia()],
66
+ stubs: {
67
+ WellPlate: WellPlateStub,
68
+ DoseCalculator: DoseCalculatorStub,
69
+ 'router-link': {
70
+ template: '<a><slot /></a>',
71
+ },
72
+ BaseModal: {
73
+ template: '<div><slot /></div>',
74
+ },
75
+ },
76
+ },
77
+ })
78
+ }
79
+
80
+ describe('DoseDesignWorkspaceView', () => {
81
+ it('renders WellPlate and DoseCalculator from one dose-design model', async () => {
82
+ const wrapper = mountWorkspace({
83
+ modelValue: {
84
+ selectedWells: ['A1'],
85
+ plateFormat: 384,
86
+ doseMode: 'serial',
87
+ disabled: false,
88
+ },
89
+ doseDesignOptions: {
90
+ includeMolecularWeight: true,
91
+ },
92
+ wellPlateProps: {
93
+ size: 'lg',
94
+ selectionMode: 'single',
95
+ heatmap: { enabled: true },
96
+ },
97
+ doseCalculatorProps: {
98
+ disabled: true,
99
+ },
100
+ })
101
+
102
+ const wellPlate = wrapper.findComponent(WellPlateStub)
103
+ const doseCalculator = wrapper.findComponent(DoseCalculatorStub)
104
+
105
+ expect(wrapper.findComponent(AppSidebar).props('variant')).toBe('analysis')
106
+ expect(wellPlate.props()).toMatchObject({
107
+ modelValue: ['A1'],
108
+ format: 384,
109
+ size: 'lg',
110
+ selectionMode: 'single',
111
+ heatmap: { enabled: true },
112
+ })
113
+ expect(doseCalculator.props()).toMatchObject({
114
+ mode: 'serial',
115
+ targetWells: ['A1'],
116
+ disabled: true,
117
+ molecularWeight: 300,
118
+ })
119
+
120
+ await wellPlate.trigger('click')
121
+ await nextTick()
122
+
123
+ expect(wrapper.emitted('update:modelValue')?.at(-1)?.[0]).toMatchObject({
124
+ selectedWells: ['B1', 'B2'],
125
+ })
126
+ })
127
+
128
+ it('uses custom generated component prop ids from doseDesignOptions', () => {
129
+ const wrapper = mountWorkspace({
130
+ doseDesignOptions: {
131
+ selectedWells: ['C1'],
132
+ componentProps: {
133
+ plateId: 'plateView',
134
+ doseId: 'doseTool',
135
+ },
136
+ },
137
+ })
138
+
139
+ expect(wrapper.findComponent(WellPlateStub).props('modelValue')).toEqual(['C1'])
140
+ expect(wrapper.findComponent(DoseCalculatorStub).props('targetWells')).toEqual(['C1'])
141
+ })
142
+
143
+ it('passes through sidebar chrome overrides', () => {
144
+ const wrapper = mountWorkspace({
145
+ sidebarVariant: 'default',
146
+ responsiveSidebar: false,
147
+ sidebarTitle: 'Dose Controls',
148
+ sidebarSubtitle: 'Plate setup',
149
+ sidebarBadge: 'ready',
150
+ })
151
+
152
+ expect(wrapper.findComponent(AppLayout).props('responsiveSidebar')).toBe(false)
153
+ expect(wrapper.findComponent(AppSidebar).props('variant')).toBe('default')
154
+ expect(wrapper.findComponent(AppSidebar).props('title')).toBe('Dose Controls')
155
+ expect(wrapper.findComponent(AppSidebar).props('subtitle')).toBe('Plate setup')
156
+ expect(wrapper.findComponent(AppSidebar).props('badge')).toBe('ready')
157
+ })
158
+
159
+ it('exposes merged component props to the default slot', () => {
160
+ const wrapper = mountWorkspace(
161
+ {
162
+ doseDesignOptions: {
163
+ selectedWells: ['D1'],
164
+ },
165
+ },
166
+ {
167
+ default: (rawProps: unknown) => {
168
+ const props = rawProps as {
169
+ wellPlateProps: Record<string, unknown>
170
+ doseCalculatorProps: Record<string, unknown>
171
+ }
172
+ return h('pre', { class: 'slot-props' }, JSON.stringify({
173
+ plate: props.wellPlateProps.modelValue,
174
+ dose: props.doseCalculatorProps.targetWells,
175
+ }))
176
+ },
177
+ },
178
+ )
179
+
180
+ expect(JSON.parse(wrapper.find('.slot-props').text())).toEqual({
181
+ plate: ['D1'],
182
+ dose: ['D1'],
183
+ })
184
+ })
185
+ })