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

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 (181) 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 +57 -5
  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/AppTopBarPageSelectorInternal.vue.d.ts} +1 -1
  23. package/dist/components/{AppPillNav.vue.d.ts → internal/AppTopBarPillNavInternal.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-DihbSJjU.js} +5932 -5408
  28. package/dist/components-DihbSJjU.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-BcgZ6diz.js} +40 -28
  40. package/dist/composables-BcgZ6diz.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 +5637 -5663
  45. package/dist/templates/adapters.d.ts +7 -1
  46. package/dist/templates/catalog.d.ts +5 -5
  47. package/dist/templates/componentBindings.d.ts +13 -0
  48. package/dist/templates/index.d.ts +5 -5
  49. package/dist/templates/index.js +2 -2
  50. package/dist/templates/presets.d.ts +4 -4
  51. package/dist/templates/types.d.ts +4 -1
  52. package/dist/{templates-50NPjaxL.js → templates-Cyt0Suwf.js} +322 -73
  53. package/dist/templates-Cyt0Suwf.js.map +1 -0
  54. package/dist/types/components.d.ts +6 -25
  55. package/dist/types/index.d.ts +1 -1
  56. package/dist/{useScheduleDrag-D4oWdh41.js → useExperimentData-CM6Y0u5L.js} +400 -357
  57. package/dist/useExperimentData-CM6Y0u5L.js.map +1 -0
  58. package/package.json +1 -1
  59. package/src/__tests__/components/ActionItem.test.ts +6 -6
  60. package/src/__tests__/components/AppLayout.test.ts +44 -0
  61. package/src/__tests__/components/AppSidebar.test.ts +130 -2
  62. package/src/__tests__/components/AppToastContainer.test.ts +0 -11
  63. package/src/__tests__/components/AppTopBar.test.ts +189 -120
  64. package/src/__tests__/components/{AppPageSelector.test.ts → AppTopBarPageSelector.test.ts} +8 -8
  65. package/src/__tests__/components/{AppPillNav.test.ts → AppTopBarPillNav.test.ts} +53 -6
  66. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +7 -1
  67. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +32 -1
  68. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +48 -1
  69. package/src/__tests__/components/BioTemplateRenderer.test.ts +25 -0
  70. package/src/__tests__/components/CalendarGridPanel.test.ts +3 -3
  71. package/src/__tests__/components/ComponentBindingRenderer.test.ts +278 -0
  72. package/src/__tests__/components/ControlWorkspaceView.test.ts +134 -63
  73. package/src/__tests__/components/DateTimePicker.test.ts +2 -2
  74. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
  75. package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
  76. package/src/__tests__/composables/experiment-utils.test.ts +30 -0
  77. package/src/__tests__/composables/useApi.test.ts +30 -0
  78. package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
  79. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +7 -4
  80. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +7 -7
  81. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +6 -1
  82. package/src/__tests__/composables/useControlSchema.test.ts +151 -37
  83. package/src/__tests__/composables/usePluginClient.test.ts +99 -2
  84. package/src/__tests__/docs/frontendDocsCatalog.test.ts +120 -25
  85. package/src/__tests__/templates/templates.test.ts +56 -0
  86. package/src/components/AppAvatarMenu.vue +3 -3
  87. package/src/components/AppLayout.story.vue +39 -0
  88. package/src/components/AppLayout.vue +83 -2
  89. package/src/components/AppPluginSwitcher.vue +5 -5
  90. package/src/components/AppSidebar.story.vue +113 -5
  91. package/src/components/AppSidebar.vue +147 -27
  92. package/src/components/AppTopBar.story.vue +2 -5
  93. package/src/components/AppTopBar.vue +35 -425
  94. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +2 -2
  95. package/src/components/BioTemplateExperimentWorkspaceView.vue +6 -0
  96. package/src/components/BioTemplatePackWorkspaceView.story.vue +4 -4
  97. package/src/components/BioTemplatePackWorkspaceView.vue +1 -0
  98. package/src/components/BioTemplatePresetWorkspaceView.story.vue +14 -2
  99. package/src/components/BioTemplatePresetWorkspaceView.vue +12 -3
  100. package/src/components/BioTemplateRenderer.story.vue +2 -2
  101. package/src/components/BioTemplateRenderer.vue +15 -227
  102. package/src/components/ComponentBindingRenderer.story.vue +87 -0
  103. package/src/components/ComponentBindingRenderer.vue +317 -0
  104. package/src/components/ControlWorkspaceView.story.vue +20 -9
  105. package/src/components/ControlWorkspaceView.vue +43 -12
  106. package/src/components/DatePicker.vue +2 -2
  107. package/src/components/DateTimePicker.vue +2 -2
  108. package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
  109. package/src/components/DoseDesignWorkspaceView.vue +255 -0
  110. package/src/components/ExperimentPopover.story.vue +2 -2
  111. package/src/components/ExperimentPopover.vue +2 -6
  112. package/src/components/ExperimentSelectorModal.vue +6 -5
  113. package/src/components/FormBuilder.story.vue +190 -0
  114. package/src/components/PluginWorkspaceView.story.vue +334 -0
  115. package/src/components/PluginWorkspaceView.vue +708 -0
  116. package/src/components/SettingsModal.story.vue +87 -0
  117. package/src/components/WellPlate.vue +2 -2
  118. package/src/components/index.ts +3 -12
  119. package/src/components/{AppPageSelector.vue → internal/AppTopBarPageSelectorInternal.vue} +9 -9
  120. package/src/components/internal/AppTopBarPillNavInternal.vue +194 -0
  121. package/src/components/{CalendarGridPanel.vue → internal/CalendarGridPanelInternal.vue} +1 -1
  122. package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +3 -3
  123. package/src/composables/experiment-utils.ts +26 -0
  124. package/src/composables/index.ts +21 -7
  125. package/src/composables/useApi.ts +9 -2
  126. package/src/composables/useAppExperiment.ts +85 -13
  127. package/src/composables/useBioTemplateComponents.ts +12 -0
  128. package/src/composables/useBioTemplatePackWorkspace.ts +6 -2
  129. package/src/composables/useBioTemplatePresetWorkspace.ts +10 -21
  130. package/src/composables/useBioTemplateWorkspace.ts +6 -4
  131. package/src/composables/useControlSchema.ts +157 -69
  132. package/src/composables/usePluginClient.ts +50 -9
  133. package/src/index.ts +6 -563
  134. package/src/styles/components/app-layout.css +82 -0
  135. package/src/styles/components/app-page-selector.css +1 -1
  136. package/src/styles/components/app-pill-nav.css +71 -1
  137. package/src/styles/components/app-sidebar.css +119 -0
  138. package/src/styles/components/app-top-bar.css +0 -235
  139. package/src/styles/components/experiment-popover.css +2 -2
  140. package/src/styles/index.css +0 -1
  141. package/src/templates/adapters.ts +193 -0
  142. package/src/templates/catalog.ts +5 -5
  143. package/src/templates/componentBindings.ts +90 -3
  144. package/src/templates/index.ts +10 -0
  145. package/src/templates/packs.ts +10 -1
  146. package/src/templates/presets.ts +14 -4
  147. package/src/templates/types.ts +4 -0
  148. package/src/types/components.ts +6 -31
  149. package/src/types/index.ts +2 -6
  150. package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
  151. package/dist/components/FormFieldRenderer.vue.d.ts +0 -28
  152. package/dist/components/FormSection.vue.d.ts +0 -30
  153. package/dist/components/GroupingModal.vue.d.ts +0 -12
  154. package/dist/components/SettingsButton.vue.d.ts +0 -30
  155. package/dist/components/ToastNotification.vue.d.ts +0 -2
  156. package/dist/components-D_Sr0adg.js.map +0 -1
  157. package/dist/composables/usePluginApi.d.ts +0 -22
  158. package/dist/composables-C3dpXQN5.js.map +0 -1
  159. package/dist/templates-50NPjaxL.js.map +0 -1
  160. package/dist/useScheduleDrag-D4oWdh41.js.map +0 -1
  161. package/src/__tests__/components/FormCompatibility.test.ts +0 -94
  162. package/src/__tests__/components/GroupingModal.test.ts +0 -73
  163. package/src/__tests__/components/SettingsButton.test.ts +0 -44
  164. package/src/__tests__/composables/usePluginApi.test.ts +0 -81
  165. package/src/components/AppPillNav.vue +0 -71
  166. package/src/components/FormFieldRenderer.vue +0 -35
  167. package/src/components/FormSection.vue +0 -37
  168. package/src/components/GroupingModal.story.vue +0 -52
  169. package/src/components/GroupingModal.vue +0 -61
  170. package/src/components/SettingsButton.story.vue +0 -58
  171. package/src/components/SettingsButton.vue +0 -64
  172. package/src/components/ToastNotification.vue +0 -9
  173. package/src/composables/usePluginApi.ts +0 -32
  174. package/src/styles/components/settings-button.css +0 -31
  175. /package/dist/__tests__/components/{AppPageSelector.test.d.ts → AppTopBarPageSelector.test.d.ts} +0 -0
  176. /package/dist/__tests__/components/{AppPillNav.test.d.ts → AppTopBarPillNav.test.d.ts} +0 -0
  177. /package/dist/__tests__/components/{FormCompatibility.test.d.ts → ComponentBindingRenderer.test.d.ts} +0 -0
  178. /package/dist/__tests__/components/{GroupingModal.test.d.ts → DoseDesignWorkspaceView.test.d.ts} +0 -0
  179. /package/dist/__tests__/components/{SettingsButton.test.d.ts → PluginWorkspaceView.test.d.ts} +0 -0
  180. /package/dist/components/{ActionItem.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +0 -0
  181. /package/src/components/{ActionItem.vue → internal/ActionItemInternal.vue} +0 -0
@@ -5,6 +5,7 @@ import axios, { type AxiosRequestConfig } from 'axios'
5
5
  import {
6
6
  buildPluginEndpointUrl,
7
7
  createPluginClient,
8
+ getPluginPageSelectorItems,
8
9
  resolvePluginBaseUrl,
9
10
  useCurrentExperiment,
10
11
  usePluginClient,
@@ -121,12 +122,44 @@ describe('usePluginClient', () => {
121
122
  it('resolves plugin base URLs without making requests', () => {
122
123
  expect(resolvePluginBaseUrl(contract)).toBe('/api/drp')
123
124
 
124
- expect(resolvePluginBaseUrl(contract, '/api/explicit')).toBe('/api/explicit')
125
+ expect(resolvePluginBaseUrl(
126
+ { ...contract, plugin: { ...contract.plugin, apiPrefix: '/api/drp/' } },
127
+ )).toBe('/api/drp')
128
+ expect(resolvePluginBaseUrl(contract, '/api/explicit/')).toBe('/api/explicit')
125
129
 
126
- vi.stubEnv('VITE_API_PREFIX', '/api/env')
130
+ vi.stubEnv('VITE_API_PREFIX', '/api/env/')
127
131
  expect(resolvePluginBaseUrl(contract, '/api/explicit')).toBe('/api/env')
128
132
  })
129
133
 
134
+ it('normalizes platform-injected plugin base URLs', () => {
135
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
136
+ isIntegrated: true,
137
+ theme: 'light',
138
+ plugin: {
139
+ id: 'drp',
140
+ name: 'DRP',
141
+ version: '1.0.0',
142
+ route_prefix: '/ignored/',
143
+ api_prefix: '/api/platform-drp/',
144
+ },
145
+ }
146
+
147
+ expect(resolvePluginBaseUrl(contract)).toBe('/api/platform-drp')
148
+
149
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
150
+ isIntegrated: true,
151
+ theme: 'light',
152
+ plugin: {
153
+ id: 'drp',
154
+ name: 'DRP',
155
+ version: '1.0.0',
156
+ route_prefix: '/platform-drp/',
157
+ },
158
+ }
159
+
160
+ expect(resolvePluginBaseUrl(contract)).toBe('/api/platform-drp')
161
+ })
162
+
130
163
  it('builds concrete endpoint URLs without making requests', () => {
131
164
  const url = buildPluginEndpointUrl(
132
165
  contract,
@@ -146,6 +179,70 @@ describe('usePluginClient', () => {
146
179
  expect(requestConfigs).toHaveLength(0)
147
180
  })
148
181
 
182
+ it('maps plugin contract nav items to AppTopBar page selector metadata', () => {
183
+ const pageSelectorItems = getPluginPageSelectorItems({
184
+ ...contract,
185
+ plugin: {
186
+ ...contract.plugin,
187
+ name: 'Dose Designer',
188
+ description: 'Build dose designs',
189
+ icon: 'PLUGIN_ICON',
190
+ navItems: [
191
+ { path: '/', label: 'Dashboard', icon: 'DASHBOARD_ICON', description: 'Overview' },
192
+ { id: 'dose-design', path: 'dose-design', label: 'Dose Design' },
193
+ { path: '/qc/review/', label: 'QC Review', icon: 'QC_ICON' },
194
+ ],
195
+ },
196
+ })
197
+
198
+ expect(pageSelectorItems).toEqual([
199
+ {
200
+ id: 'dashboard',
201
+ label: 'Dashboard',
202
+ to: '/',
203
+ icon: 'DASHBOARD_ICON',
204
+ hint: 'Overview',
205
+ },
206
+ {
207
+ id: 'dose-design',
208
+ label: 'Dose Design',
209
+ to: '/dose-design',
210
+ icon: 'PLUGIN_ICON',
211
+ hint: 'Dose Designer',
212
+ },
213
+ {
214
+ id: 'qc-review',
215
+ label: 'QC Review',
216
+ to: '/qc/review',
217
+ icon: 'QC_ICON',
218
+ hint: 'Dose Designer',
219
+ },
220
+ ])
221
+ expect(requestConfigs).toHaveLength(0)
222
+ })
223
+
224
+ it('provides a dashboard page selector fallback when contract nav items are absent', () => {
225
+ expect(getPluginPageSelectorItems({
226
+ ...contract,
227
+ plugin: {
228
+ ...contract.plugin,
229
+ name: 'Dose Designer',
230
+ description: 'Build dose designs',
231
+ icon: 'PLUGIN_ICON',
232
+ navItems: [],
233
+ },
234
+ })).toEqual([
235
+ {
236
+ id: 'dashboard',
237
+ label: 'Dose Designer',
238
+ to: '/',
239
+ icon: 'PLUGIN_ICON',
240
+ hint: 'Build dose designs',
241
+ },
242
+ ])
243
+ expect(requestConfigs).toHaveLength(0)
244
+ })
245
+
149
246
  it('can build path-only endpoint URLs with inferred experiment ids', () => {
150
247
  window.history.replaceState({}, '', '/experiments/42/plugins/drp')
151
248
 
@@ -18,6 +18,7 @@ interface FrontendManifest {
18
18
  templateExports: Array<{ name: string }>
19
19
  templatePacks: Array<{ name: string }>
20
20
  }
21
+ exports: Array<{ name: string }>
21
22
  }
22
23
 
23
24
  const TEST_DIR = dirname(fileURLToPath(import.meta.url))
@@ -42,6 +43,27 @@ function parseComponentExports(): string[] {
42
43
  .sort()
43
44
  }
44
45
 
46
+ function parseRootComponentExports(): string[] {
47
+ const content = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
48
+ if (/export\s+\*\s+from\s+['"]\.\/components['"]/.test(content)) {
49
+ return parseComponentExports()
50
+ }
51
+
52
+ const names = new Set<string>()
53
+
54
+ for (const match of content.matchAll(/export\s+\{([^}]*)\}\s+from\s+['"]\.\/components['"]/g)) {
55
+ const body = match[1].replace(/\/\/.*$/gm, '')
56
+ for (const item of body.split(',')) {
57
+ const spec = item.trim()
58
+ if (!spec || spec.startsWith('type ')) continue
59
+ const name = spec.split(/\s+as\s+/, 1)[0].trim()
60
+ if (/^[A-Z]\w*$/.test(name)) names.add(name)
61
+ }
62
+ }
63
+
64
+ return [...names].sort()
65
+ }
66
+
45
67
  function parseDeprecatedComponentExports(): Record<string, string> {
46
68
  const componentsDir = join(SDK_ROOT, 'src', 'components')
47
69
  const content = readFileSync(join(componentsDir, 'index.ts'), 'utf8')
@@ -115,6 +137,27 @@ function parseDocumentedComposableExports(): string[] {
115
137
  return [...new Set(names)].sort()
116
138
  }
117
139
 
140
+ function parseRootComposableExports(): string[] {
141
+ const rootContent = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
142
+ const composableExports = parseDocumentedComposableExports()
143
+
144
+ if (/export\s+\*\s+from\s+['"]\.\/composables['"]/.test(rootContent)) {
145
+ return composableExports
146
+ }
147
+
148
+ const names: string[] = []
149
+ for (const match of rootContent.matchAll(/export\s+\{([^}]*)\}\s+from\s+['"]\.\/composables['"]/g)) {
150
+ for (const item of match[1].split(',')) {
151
+ const spec = item.trim()
152
+ if (!spec || spec.startsWith('type ')) continue
153
+ const name = spec.split(/\s+as\s+/, 1)[0].trim()
154
+ if (name) names.push(name)
155
+ }
156
+ }
157
+
158
+ return [...new Set(names)].sort()
159
+ }
160
+
118
161
  function parseTemplateExports(): string[] {
119
162
  const indexContent = readFileSync(join(SDK_ROOT, 'src', 'templates', 'index.ts'), 'utf8')
120
163
  const names = new Set<string>()
@@ -148,6 +191,13 @@ describe('frontend docs catalog coverage', () => {
148
191
  expect(missingFromManifest(documentedComponents, publicComponents)).toEqual([])
149
192
  })
150
193
 
194
+ it('re-exports every public component from the root SDK entrypoint', () => {
195
+ const publicComponents = parseComponentExports()
196
+ const rootComponents = parseRootComponentExports()
197
+
198
+ expect(missingFromManifest(publicComponents, rootComponents)).toEqual([])
199
+ })
200
+
151
201
  it('marks every deprecated public component export with its replacement', () => {
152
202
  const manifest = extractFrontendManifest()
153
203
  const expected = parseDeprecatedComponentExports()
@@ -161,34 +211,28 @@ describe('frontend docs catalog coverage', () => {
161
211
  expect(documented).toEqual(expected)
162
212
  })
163
213
 
164
- it('keeps compatibility exports deprecated at public SDK entrypoints', () => {
214
+ it('keeps retired compatibility APIs out of public SDK entrypoints', () => {
165
215
  const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
216
+ const componentsIndex = readFileSync(join(SDK_ROOT, 'src', 'components', 'index.ts'), 'utf8')
166
217
  const composablesIndex = readFileSync(join(SDK_ROOT, 'src', 'composables', 'index.ts'), 'utf8')
167
218
 
168
- expect(rootIndex).toContain(
169
- "/** @deprecated Use AppToastContainer instead. */\nexport { ToastNotification } from './components'",
170
- )
171
- expect(rootIndex).toContain(
172
- "/** @deprecated Use AppTopBar settingsConfig or SettingsModal instead. */\nexport { SettingsButton } from './components'",
173
- )
174
- expect(rootIndex).toContain(
175
- "/** @deprecated Use AutoGroupModal instead. */\nexport { GroupingModal } from './components'",
176
- )
177
- expect(rootIndex).toContain(
178
- "/** @deprecated Use generated plugin clients from `mint sdk generate` instead. */\nexport {\n usePluginApi,\n type UsePluginApiOptions,\n} from './composables'",
179
- )
180
- expect(composablesIndex).toContain(
181
- "/** @deprecated Use generated plugin clients from `mint sdk generate` instead. */\nexport {\n usePluginApi,\n type UsePluginApiOptions,\n} from './usePluginApi'",
182
- )
183
- })
184
-
185
- it('keeps deprecated usePluginApi docs focused on migration', () => {
186
- const content = readFileSync(join(SDK_ROOT, 'src', 'composables', 'usePluginApi.ts'), 'utf8')
187
-
188
- expect(content).toContain('Migration shape for generated plugins')
189
- expect(content).toContain('useGeneratedPluginClient')
190
- expect(content).not.toContain('const api = usePluginApi')
191
- expect(content).not.toContain("fallbackPrefix: '/api/drp'")
219
+ const retiredNames = [
220
+ 'ToastNotification',
221
+ 'SettingsButton',
222
+ 'GroupingModal',
223
+ 'FormSection',
224
+ 'FormFieldRenderer',
225
+ 'AppPageSelector',
226
+ 'AppPillNav',
227
+ 'usePluginApi',
228
+ 'UsePluginApiOptions',
229
+ ]
230
+
231
+ for (const name of retiredNames) {
232
+ expect(rootIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
233
+ expect(componentsIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
234
+ expect(composablesIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
235
+ }
192
236
  })
193
237
 
194
238
  it('documents every public composable/helper export used by docs and doctor', () => {
@@ -200,6 +244,57 @@ describe('frontend docs catalog coverage', () => {
200
244
  expect(missingFromManifest(documentedComposables, publicComposables)).toEqual([])
201
245
  })
202
246
 
247
+ it('documents generated component binding helpers for control models', () => {
248
+ const manifest = extractFrontendManifest()
249
+ const documentedComposables = new Set(manifest.categories.composables.map(item => item.name))
250
+
251
+ expect(documentedComposables.has('defineControlComponentBindings')).toBe(true)
252
+ expect(documentedComposables.has('controlValuesToComponentBindings')).toBe(true)
253
+ expect(documentedComposables.has('controlValuesToComponentBindingsById')).toBe(true)
254
+ expect(documentedComposables.has('defineWellPlateDoseComponentBindings')).toBe(true)
255
+ })
256
+
257
+ it('re-exports every documented composable/helper from the root SDK entrypoint', () => {
258
+ const publicComposables = parseDocumentedComposableExports()
259
+ const rootComposables = parseRootComposableExports()
260
+
261
+ expect(missingFromManifest(publicComposables, rootComposables)).toEqual([])
262
+ })
263
+
264
+ it('re-exports the public types barrel from the root SDK entrypoint', () => {
265
+ const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
266
+
267
+ expect(rootIndex).toMatch(/export\s+type\s+\*\s+from\s+['"]\.\/types['"]/)
268
+ })
269
+
270
+ it('re-exports the public stores barrel from the root SDK entrypoint', () => {
271
+ const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
272
+
273
+ expect(rootIndex).toMatch(/export\s+\*\s+from\s+['"]\.\/stores['"]/)
274
+ })
275
+
276
+ it('re-exports the public templates barrel from the root SDK entrypoint', () => {
277
+ const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
278
+
279
+ expect(rootIndex).toMatch(/export\s+\*\s+from\s+['"]\.\/templates['"]/)
280
+ })
281
+
282
+ it('expands root barrel exports into the docs manifest', () => {
283
+ const manifest = extractFrontendManifest()
284
+ const rootExports = manifest.exports.map(item => item.name)
285
+
286
+ expect(rootExports).toEqual(expect.arrayContaining([
287
+ 'BaseButton',
288
+ 'DoseDesignWorkspaceView',
289
+ 'WellPlate',
290
+ 'useForm',
291
+ 'useReagentSeries',
292
+ 'ConversionResult',
293
+ 'FormSectionSchema',
294
+ 'createWellPlateScreenCollection',
295
+ ]))
296
+ })
297
+
203
298
  it('documents curated biology template packs', () => {
204
299
  const manifest = extractFrontendManifest()
205
300
 
@@ -43,6 +43,8 @@ import {
43
43
  searchBioTemplatePacks,
44
44
  searchBioTemplateCatalog,
45
45
  searchBioTemplatePresets,
46
+ toBioTemplateComponentBindings,
47
+ toBioTemplateComponentBindingsById,
46
48
  toBioTemplateComponentImports,
47
49
  toBioTemplateComponentProps,
48
50
  toBioTemplateComponentPropsByComponent,
@@ -52,6 +54,7 @@ import {
52
54
  toAssayMatrixColumns,
53
55
  toAssayMatrixDataFrame,
54
56
  toAssayMatrixRows,
57
+ toAssayMatrixSampleOptions,
55
58
  toCalibrationCurveDataFrame,
56
59
  toCalibrationCurveRows,
57
60
  toDoseConditions,
@@ -60,6 +63,8 @@ import {
60
63
  toFlowPanelRows,
61
64
  toInstrumentRunDataFrame,
62
65
  toInstrumentRunRows,
66
+ toInstrumentRunScheduleEvents,
67
+ toInstrumentRunSteps,
63
68
  toPlateMapEditorState,
64
69
  toProtocolDataFrame,
65
70
  toProtocolSteps,
@@ -85,6 +90,7 @@ import bioTemplateCatalogContract from '../../../../bio-template-catalog.contrac
85
90
  import bioTemplatePacksContract from '../../../../bio-template-packs.contract.json'
86
91
  import bioTemplatePresetsContract from '../../../../bio-template-presets.contract.json'
87
92
  import type {
93
+ AssayMatrixTemplate,
88
94
  DoseResponseTemplate,
89
95
  FlowCytometryPanelTemplate,
90
96
  InstrumentRunTemplate,
@@ -156,6 +162,9 @@ describe('bio data templates', () => {
156
162
  'flow-cytometry-panel',
157
163
  ])
158
164
  expect(getBioTemplatePackInfo('metabolomics')?.name).toBe('omics-assay')
165
+ expect(getBioTemplatePackInfo('metabolism')?.name).toBe('omics-assay')
166
+ expect(getBioTemplatePackInfo('metabolite profiling')?.name).toBe('omics-assay')
167
+ expect(getBioTemplatePackInfo('metabolomics')?.components).toContain('ExperimentTimeline')
159
168
  expect(getBioTemplatePackInfo('longitudinal')?.init_command).toBe('mint init --template longitudinal-study')
160
169
  expect(getBioTemplatePackInfo('gene expression')?.templates.at(-1)).toBe('qpcr-plate')
161
170
  expect(getBioTemplatePackInfo('gene-expression')?.templates.at(-1)).toBe('qpcr-plate')
@@ -212,6 +221,9 @@ describe('bio data templates', () => {
212
221
  'qpcr-plate',
213
222
  ])
214
223
  expect(searchBioTemplatePresets('run queue')[0].name).toBe('lcms-batch')
224
+ expect(getBioTemplatePresetInfo('metabolomics')?.name).toBe('lcms-batch')
225
+ expect(getBioTemplatePresetInfo('metabolism')?.name).toBe('lcms-batch')
226
+ expect(getBioTemplatePresetInfo('metabolite profiling')?.name).toBe('lcms-batch')
215
227
  expect(getBioTemplatePresetInfo('immunoassay')?.templates).toEqual([
216
228
  'plate-map',
217
229
  'sample-sheet',
@@ -286,6 +298,7 @@ describe('bio data templates', () => {
286
298
  const qpcrBindings = getBioTemplateComponentBindings('gene expression')
287
299
  const elisaBindings = getBioTemplateComponentBindings('elisa-assay')
288
300
  const flowBindings = getBioTemplateComponentBindings('flow-cytometry-assay')
301
+ const lcmsBindings = getBioTemplateComponentBindings('lcms-batch')
289
302
  const westernBindings = getBioTemplateComponentBindings('western-blot-assay')
290
303
  const wellplateImports = toBioTemplateComponentImports('wellplate-screen')
291
304
 
@@ -296,11 +309,14 @@ describe('bio data templates', () => {
296
309
  expect(qpcrBindings.map(binding => binding.component)).toContain('DataFrame')
297
310
  expect(elisaBindings.map(binding => binding.component)).toContain('PlateMapEditor')
298
311
  expect(elisaBindings.map(binding => binding.component)).toContain('DataFrame')
312
+ expect(lcmsBindings.map(binding => binding.component)).toContain('ScheduleCalendar')
313
+ expect(lcmsBindings.map(binding => binding.component)).toContain('ExperimentTimeline')
299
314
  expect(flowBindings.map(binding => binding.template_id)).toEqual([
300
315
  'sample-sheet',
301
316
  'sample-sheet',
302
317
  'flow-cytometry-panel',
303
318
  'assay-matrix',
319
+ 'assay-matrix',
304
320
  ])
305
321
  expect(westernBindings.map(binding => binding.component)).toContain('ReagentList')
306
322
  expect(westernBindings.map(binding => binding.component)).toContain('ExperimentTimeline')
@@ -330,14 +346,21 @@ describe('bio data templates', () => {
330
346
  const state = toPlateMapEditorState(plateMapFixture as PlateMapTemplate)
331
347
  const wells = toWellPlateWells(plateMapFixture as PlateMapTemplate)
332
348
  const componentProps = toBioTemplateComponentProps(plateMapFixture as PlateMapTemplate)
349
+ const componentBindings = toBioTemplateComponentBindings(plateMapFixture as PlateMapTemplate)
333
350
  const componentPropsById = toBioTemplateComponentPropsById(plateMapFixture as PlateMapTemplate)
351
+ const componentBindingsById = toBioTemplateComponentBindingsById(plateMapFixture as PlateMapTemplate)
334
352
  const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
353
+ const wellPlateBinding = componentBindings.find(binding => binding.component === 'WellPlate')
335
354
 
336
355
  expect(state.plates[0].name).toBe('Drug screen plate')
337
356
  expect(wells.A2.sampleType).toBe('drug-a')
338
357
  expect(wellPlate?.propsObject.format).toBe(96)
339
358
  expect((wellPlate?.propsObject.wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
359
+ expect(wellPlateBinding?.props.format).toBe(96)
360
+ expect(wellPlateBinding?.propNames).toContain('wells')
340
361
  expect(componentPropsById['plate-map:WellPlate']).toEqual(wellPlate?.propsObject)
362
+ expect(componentBindingsById['plate-map:WellPlate'].component).toBe('WellPlate')
363
+ expect(componentBindingsById['plate-map:WellPlate'].props).toEqual(wellPlate?.propsObject)
341
364
  expect((componentPropsById['plate-map:WellPlate'].wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
342
365
  })
343
366
 
@@ -347,6 +370,7 @@ describe('bio data templates', () => {
347
370
  compounds: { 'Drug A': [10, 1] },
348
371
  })
349
372
  const propsById = toBioTemplateComponentPropsById(collection)
373
+ const bindingsById = toBioTemplateComponentBindingsById(collection)
350
374
  const propsByComponent = toBioTemplateComponentPropsByComponent(collection)
351
375
  const doseCalculatorProps = getBioTemplateComponentProps(collection, 'DoseCalculator')
352
376
  const doseWellPlateProps = getBioTemplateComponentProps(collection, 'WellPlate', {
@@ -362,6 +386,8 @@ describe('bio data templates', () => {
362
386
  'dose-response:WellPlate',
363
387
  ])
364
388
  expect(propsById['dose-response:DoseCalculator'].mode).toBe('serial')
389
+ expect(bindingsById['dose-response:DoseCalculator'].props.mode).toBe('serial')
390
+ expect(bindingsById['dose-response:DoseCalculator'].propNames).toContain('mode')
365
391
  expect(propsById['plate-map:WellPlate'].wells).toBeDefined()
366
392
  expect(propsById['dose-response:WellPlate'].wells).toBeDefined()
367
393
  expect(propsByComponent.WellPlate).toHaveLength(2)
@@ -400,6 +426,7 @@ describe('bio data templates', () => {
400
426
  expect(componentPropsById['plate-map:WellPlate'].wells).toBeDefined()
401
427
  expect(componentPropsById['calibration-curve:DataFrame'].data).toBeDefined()
402
428
  expect(componentPropsById['assay-matrix:DataFrame'].columns).toBeDefined()
429
+ expect(componentPropsById['assay-matrix:SampleSelector'].samples).toEqual(['Control', 'Treatment'])
403
430
  })
404
431
 
405
432
  it('generates Vue snippets for concrete template component props', () => {
@@ -544,6 +571,7 @@ describe('bio data templates', () => {
544
571
  expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[0].kind).toBe('blank')
545
572
  expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[2].sampleId).toBe('s001')
546
573
  expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('glucose')
574
+ expect(toAssayMatrixSampleOptions(lcmsTemplates['assay-matrix'] as AssayMatrixTemplate).map(option => option.label)).toEqual(['S001', 'S002'])
547
575
  expect(Object.keys(flowTemplates)).toEqual(['sample-sheet', 'flow-cytometry-panel', 'assay-matrix'])
548
576
  expect(flow.metadata?.preset).toBe('flow-cytometry-assay')
549
577
  expect(flowTemplates['flow-cytometry-panel'].data).toMatchObject({
@@ -909,6 +937,10 @@ describe('bio data templates', () => {
909
937
  const assayFrame = toAssayMatrixDataFrame(template)
910
938
  expect(assayFrame.rowKey).toBe('sampleId')
911
939
  expect(assayFrame.data[0].lactate).toBe(1.2)
940
+ expect(toAssayMatrixSampleOptions(template)).toEqual([
941
+ { value: 's001', label: 'S001', description: undefined },
942
+ { value: 's002', label: 'S002', description: undefined },
943
+ ])
912
944
  expect(toTemplateDataFrame(template).columns.map(column => column.key)).toContain('glucose')
913
945
  })
914
946
 
@@ -966,6 +998,11 @@ describe('bio data templates', () => {
966
998
  })
967
999
  const rows = toInstrumentRunRows(template)
968
1000
  const frame = toInstrumentRunDataFrame(template)
1001
+ const steps = toInstrumentRunSteps(template)
1002
+ const events = toInstrumentRunScheduleEvents(template)
1003
+ const componentProps = toBioTemplateComponentProps(template)
1004
+ const calendar = componentProps.find(binding => binding.component === 'ScheduleCalendar')
1005
+ const timeline = componentProps.find(binding => binding.component === 'ExperimentTimeline')
969
1006
 
970
1007
  expect(template.template_id).toBe('instrument-run')
971
1008
  expect(template.data.items.map(item => item.kind)).toEqual(['blank', 'qc', 'sample', 'sample', 'qc'])
@@ -975,6 +1012,25 @@ describe('bio data templates', () => {
975
1012
  expect(frame.rowKey).toBe('id')
976
1013
  expect(frame.columns.map(column => column.key)).toContain('status')
977
1014
  expect(toTemplateDataFrame(template).data[3].sampleId).toBe('s002')
1015
+ expect(steps[2]).toMatchObject({
1016
+ id: 's001-run',
1017
+ type: 'measurement',
1018
+ name: 'S001',
1019
+ status: 'pending',
1020
+ order: 3,
1021
+ })
1022
+ expect(steps[2].parameters?.method).toBe('Default method')
1023
+ expect(events[0]).toMatchObject({
1024
+ id: 'blank-start',
1025
+ title: 'Blank start',
1026
+ start: '2024-01-01T08:00:00.000Z',
1027
+ end: '2024-01-01T08:10:00.000Z',
1028
+ status: 'pending',
1029
+ })
1030
+ expect(events[2].title).toBe('S001')
1031
+ expect(calendar?.propsObject.modelValue).toBe(events[0].start)
1032
+ expect(calendar?.propsObject.events).toEqual(events)
1033
+ expect(timeline?.propsObject.modelValue).toEqual(steps)
978
1034
  })
979
1035
 
980
1036
  it('creates qPCR plate templates and adapts reactions to dataframe and wells', () => {
@@ -3,7 +3,7 @@
3
3
  import { computed } from 'vue'
4
4
  import { useDropdownState } from '../composables/useDropdownState'
5
5
  import type { AccountMenuItem } from '../types/components'
6
- import ActionItem from './ActionItem.vue'
6
+ import ActionItemInternal from './internal/ActionItemInternal.vue'
7
7
 
8
8
  interface Props {
9
9
  userName?: string
@@ -86,7 +86,7 @@ function handleSignOut() {
86
86
  <slot name="items" :close="closeSilently">
87
87
  <template v-for="item in items" :key="item.id">
88
88
  <div v-if="item.divider" class="mint-avatar-menu__divider" role="separator" />
89
- <ActionItem
89
+ <ActionItemInternal
90
90
  v-else
91
91
  :href="item.href"
92
92
  :to="item.to"
@@ -99,7 +99,7 @@ function handleSignOut() {
99
99
  </span>
100
100
  <span class="mint-avatar-menu__item-label">{{ item.label }}</span>
101
101
  <span v-if="item.rightLabel" class="mint-avatar-menu__item-right">{{ item.rightLabel }}</span>
102
- </ActionItem>
102
+ </ActionItemInternal>
103
103
  </template>
104
104
  </slot>
105
105
 
@@ -48,6 +48,7 @@ const logScale = ref(false)
48
48
  :sidebar-position="state.sidebarPosition"
49
49
  :floating="state.floating ?? true"
50
50
  :sidebar-width="state.sidebarWidth"
51
+ responsive-sidebar
51
52
  >
52
53
  <template #topbar>
53
54
  <AppTopBar
@@ -216,6 +217,44 @@ const logScale = ref(false)
216
217
  </div>
217
218
  </Variant>
218
219
 
220
+ <Variant title="Responsive Sidebar">
221
+ <div style="height: 500px; max-width: 760px; border: 1px solid var(--border-color, #e2e8f0);">
222
+ <AppLayout
223
+ floating
224
+ responsive-sidebar
225
+ sidebar-width="320px"
226
+ >
227
+ <template #topbar>
228
+ <AppTopBar title="Responsive Analysis" home-path="" />
229
+ </template>
230
+ <template #sidebar>
231
+ <AppSidebar
232
+ variant="analysis"
233
+ title="Feature Filters"
234
+ subtitle="12 active samples"
235
+ :badge="2"
236
+ :panels="toolPanels"
237
+ active-view="analysis"
238
+ >
239
+ <template #section-parameters>
240
+ <BaseSlider v-model="threshold" label="Threshold" :min="0" :max="100" />
241
+ <BaseSelect v-model="method" label="Method" :options="methods" />
242
+ </template>
243
+ <template #section-filters>
244
+ <BaseToggle v-model="showOutliers" label="Exclude outliers" />
245
+ </template>
246
+ </AppSidebar>
247
+ </template>
248
+ <div style="padding: 2rem;">
249
+ <p style="color: var(--text-muted, #94a3b8);">
250
+ At narrow widths, AppLayout owns the sidebar toggle and backdrop,
251
+ while AppSidebar owns the panel chrome and collapse state.
252
+ </p>
253
+ </div>
254
+ </AppLayout>
255
+ </div>
256
+ </Variant>
257
+
219
258
  <Variant title="With Experiment Selector">
220
259
  <ExperimentProvider>
221
260
  <div style="height: 600px;">