@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
@@ -7,6 +7,7 @@ import BaseSelect from './BaseSelect.vue'
7
7
  import BaseCheckbox from './BaseCheckbox.vue'
8
8
  import BaseToggle from './BaseToggle.vue'
9
9
  import NumberInput from './NumberInput.vue'
10
+ import { defineControlModel } from '../composables/useControlSchema'
10
11
  import type {
11
12
  SettingsTab,
12
13
  SettingsModalLayout,
@@ -19,6 +20,7 @@ const customOpen = ref(false)
19
20
  const appearanceOnlyOpen = ref(false)
20
21
  const verticalOpen = ref(false)
21
22
  const schemaOpen = ref(false)
23
+ const modelOpen = ref(false)
22
24
 
23
25
  const customTabs: SettingsTab[] = [
24
26
  { id: 'general', label: 'General' },
@@ -208,6 +210,66 @@ const settingsSchema: SettingsModalSchema = {
208
210
  ],
209
211
  }
210
212
 
213
+ const settingsModel = defineControlModel({
214
+ views: {
215
+ settings: {
216
+ label: 'Settings',
217
+ sections: {
218
+ general: {
219
+ label: 'General',
220
+ description: 'Plugin name, defaults, locale',
221
+ icon: iconGeneral,
222
+ controls: {
223
+ pluginName: {
224
+ label: 'Plugin Display Name',
225
+ default: 'IC50 Calculator',
226
+ required: true,
227
+ },
228
+ locale: {
229
+ label: 'Locale',
230
+ default: 'en-US',
231
+ options: localeOptions,
232
+ },
233
+ },
234
+ },
235
+ model: {
236
+ label: 'Model Parameters',
237
+ description: 'Curve choice and bounds',
238
+ icon: iconModel,
239
+ columns: 2,
240
+ controls: {
241
+ curveModel: {
242
+ label: 'Default Curve Model',
243
+ default: '4pl',
244
+ options: curveModelOptions,
245
+ },
246
+ bottom: { type: 'number', label: 'Bottom bound', default: 0 },
247
+ top: { type: 'number', label: 'Top bound', default: 100 },
248
+ },
249
+ },
250
+ fitting: {
251
+ label: 'Curve Fitting',
252
+ description: 'Optimizer and transforms',
253
+ icon: iconCurve,
254
+ controls: {
255
+ optimizer: {
256
+ label: 'Optimizer',
257
+ default: 'lm',
258
+ options: optimizerOptions,
259
+ },
260
+ logTransform: {
261
+ label: 'Apply log-dose transform',
262
+ default: true,
263
+ type: 'toggle',
264
+ },
265
+ },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ })
271
+ const modelSettings = ref<Record<string, unknown>>({})
272
+
211
273
  const sizes: Array<'md' | 'lg' | 'xl'> = ['md', 'lg', 'xl']
212
274
  const layouts: SettingsModalLayout[] = ['horizontal', 'vertical']
213
275
  </script>
@@ -406,6 +468,31 @@ const layouts: SettingsModalLayout[] = ['horizontal', 'vertical']
406
468
  </div>
407
469
  </Variant>
408
470
 
471
+ <Variant title="Model Driven Settings">
472
+ <div style="padding: 2rem; display: flex; flex-direction: column; gap: 1rem; align-items: center;">
473
+ <button
474
+ type="button"
475
+ style="padding: 0.5rem 1rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; background: var(--bg-card, #fff); color: var(--text-primary, #1e293b); cursor: pointer; font-size: 0.875rem;"
476
+ @click="modelOpen = true"
477
+ >
478
+ Open Settings (from model)
479
+ </button>
480
+ <details style="font-size: 0.75rem; color: var(--text-secondary); max-width: 36rem; width: 100%;">
481
+ <summary style="cursor: pointer; user-select: none;">Live values</summary>
482
+ <pre style="margin: 0.5rem 0 0; padding: 0.75rem; background: var(--bg-tertiary); border-radius: 0.375rem; font-size: 0.6875rem; overflow-x: auto;">{{ JSON.stringify(modelSettings, null, 2) }}</pre>
483
+ </details>
484
+ <SettingsModal
485
+ v-model="modelOpen"
486
+ v-model:values="modelSettings"
487
+ title="IC50 Plugin Settings"
488
+ :model="settingsModel"
489
+ :show-appearance="true"
490
+ size="xl"
491
+ layout="vertical"
492
+ />
493
+ </div>
494
+ </Variant>
495
+
409
496
  <Variant title="Schema-Driven (Auto-Render)">
410
497
  <div style="padding: 2rem; display: flex; flex-direction: column; gap: 1rem; align-items: center;">
411
498
  <button
@@ -3,7 +3,7 @@
3
3
  import { ref, computed } from 'vue'
4
4
  import type { WellPlateFormat, WellPlateSelectionMode, WellPlateSize, Well, HeatmapConfig, WellShape, WellEditField, WellEditData, WellLegendItem, ColumnCondition, RowCondition } from '../types'
5
5
  import { useEventListener } from '../composables/useEventListener'
6
- import WellEditPopup from './WellEditPopup.vue'
6
+ import WellEditPopupInternal from './internal/WellEditPopupInternal.vue'
7
7
 
8
8
  interface Props {
9
9
  modelValue?: string[]
@@ -805,7 +805,7 @@ const tableStyle = computed(() => ({
805
805
  </div>
806
806
 
807
807
  <!-- Edit popup -->
808
- <WellEditPopup
808
+ <WellEditPopupInternal
809
809
  v-if="editable && editingWellId"
810
810
  :well-id="editingWellId"
811
811
  :well-data="wells[editingWellId]"
@@ -27,26 +27,23 @@ export { default as FileUploader } from './FileUploader.vue'
27
27
 
28
28
  // Feedback components
29
29
  export { default as AlertBox } from './AlertBox.vue'
30
- /** @deprecated Use AppToastContainer instead. */
31
- export { default as ToastNotification } from './ToastNotification.vue'
32
30
  export { default as AppToastContainer } from './AppToastContainer.vue'
33
31
 
34
32
  // Action components
35
33
  export { default as IconButton } from './IconButton.vue'
36
34
  export { default as ThemeToggle } from './ThemeToggle.vue'
37
- /** @deprecated Use AppTopBar instead. */
38
- export { default as SettingsButton } from './SettingsButton.vue'
39
35
 
40
36
  // Layout components
41
37
  export { default as CollapsibleCard } from './CollapsibleCard.vue'
42
38
  export { default as AppTopBar } from './AppTopBar.vue'
43
- export { default as AppPageSelector } from './AppPageSelector.vue'
44
- export { default as AppPillNav } from './AppPillNav.vue'
45
39
  export { default as AppAvatarMenu } from './AppAvatarMenu.vue'
46
40
  export { default as AppPluginSwitcher } from './AppPluginSwitcher.vue'
47
41
  export { default as AppSidebar } from './AppSidebar.vue'
48
42
  export { default as AppLayout } from './AppLayout.vue'
43
+ export { default as PluginWorkspaceView } from './PluginWorkspaceView.vue'
44
+ export { default as ComponentBindingRenderer } from './ComponentBindingRenderer.vue'
49
45
  export { default as ControlWorkspaceView } from './ControlWorkspaceView.vue'
46
+ export { default as DoseDesignWorkspaceView } from './DoseDesignWorkspaceView.vue'
50
47
  export { default as AppContainer } from './AppContainer.vue'
51
48
  export { default as PluginIcon } from './PluginIcon.vue'
52
49
 
@@ -77,8 +74,6 @@ export { default as ExperimentTimeline } from './ExperimentTimeline.vue'
77
74
 
78
75
  // Sample management components
79
76
  export { default as SampleSelector } from './SampleSelector.vue'
80
- /** @deprecated Use AutoGroupModal instead. */
81
- export { default as GroupingModal } from './GroupingModal.vue'
82
77
  export { default as AutoGroupModal } from './AutoGroupModal.vue'
83
78
  export { default as GroupAssigner } from './GroupAssigner.vue'
84
79
 
@@ -107,11 +102,7 @@ export { default as BatchProgressList } from './BatchProgressList.vue'
107
102
 
108
103
  // Form builder components
109
104
  export { default as FormBuilder } from './FormBuilder.vue'
110
- /** @deprecated Use FormBuilder instead. */
111
- export { default as FormSection } from './FormSection.vue'
112
105
  export { default as FormActions } from './FormActions.vue'
113
- /** @deprecated Use FormBuilder instead. */
114
- export { default as FormFieldRenderer } from './FormFieldRenderer.vue'
115
106
 
116
107
  // Experiment data display components
117
108
  export { default as ExperimentDataViewer } from './ExperimentDataViewer.vue'
@@ -1,12 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  /** Dropdown trigger that switches between named pages, closing on outside click or Escape. */
3
3
  import { computed } from 'vue'
4
- import { useDropdownState } from '../composables/useDropdownState'
5
- import type { PageSelectorItem, PageSelectorItemInput } from '../types/components'
6
- import { normalizeItemInput } from '../utils/items'
7
- import { isPluginIconFormat } from '../utils/pluginIcon'
8
- import ActionItem from './ActionItem.vue'
9
- import PluginIcon from './PluginIcon.vue'
4
+ import { useDropdownState } from '../../composables/useDropdownState'
5
+ import type { PageSelectorItem, PageSelectorItemInput } from '../../types/components'
6
+ import { normalizeItemInput } from '../../utils/items'
7
+ import { isPluginIconFormat } from '../../utils/pluginIcon'
8
+ import ActionItemInternal from './ActionItemInternal.vue'
9
+ import PluginIcon from '../PluginIcon.vue'
10
10
 
11
11
  interface Props {
12
12
  pages: PageSelectorItemInput[]
@@ -87,7 +87,7 @@ function handleSelect(page: PageSelectorItem) {
87
87
 
88
88
  <div v-if="isOpen" class="mint-page-selector__menu" role="menu">
89
89
  <div class="mint-page-selector__menu-title">Go to</div>
90
- <ActionItem
90
+ <ActionItemInternal
91
91
  v-for="page in normalizedPages"
92
92
  :key="page.id"
93
93
  :href="page.href"
@@ -118,11 +118,11 @@ function handleSelect(page: PageSelectorItem) {
118
118
  </span>
119
119
  <span class="mint-page-selector__item-label">{{ page.label }}</span>
120
120
  <span v-if="page.hint" class="mint-page-selector__item-hint">{{ page.hint }}</span>
121
- </ActionItem>
121
+ </ActionItemInternal>
122
122
  </div>
123
123
  </div>
124
124
  </template>
125
125
 
126
126
  <style>
127
- @import '../styles/components/app-page-selector.css';
127
+ @import '../../styles/components/app-page-selector.css';
128
128
  </style>
@@ -0,0 +1,194 @@
1
+ <script setup lang="ts">
2
+ /** Horizontal pill-style navigation bar that emits select events and supports href, router-link, button, or dropdown items. */
3
+ import { computed, ref } from 'vue'
4
+ import type { PillNavItem, PillNavItemInput, PillNavOption, PillNavOptionInput } from '../../types/components'
5
+ import { normalizeItemInput } from '../../utils/items'
6
+ import { useEventListener } from '../../composables/useEventListener'
7
+ import ActionItemInternal from './ActionItemInternal.vue'
8
+
9
+ interface Props {
10
+ items: PillNavItemInput[]
11
+ currentItemId?: string
12
+ }
13
+
14
+ const props = defineProps<Props>()
15
+
16
+ const emit = defineEmits<{
17
+ select: [item: PillNavItem]
18
+ 'option-select': [option: PillNavOption, item: PillNavItem]
19
+ }>()
20
+
21
+ const openItemId = ref<string | null>(null)
22
+ const itemRefs = ref<Map<string, HTMLElement>>(new Map())
23
+
24
+ const normalizedItems = computed<PillNavItem[]>(() => props.items.map(normalizePillNavItem))
25
+
26
+ function handleClick(item: PillNavItem) {
27
+ if (item.disabled) return
28
+ if (item.children?.length) {
29
+ toggleDropdown(item.id)
30
+ return
31
+ }
32
+ if (item.href || item.to) return
33
+ emit('select', item)
34
+ }
35
+
36
+ function handleOptionClick(option: PillNavOption, item: PillNavItem) {
37
+ if (option.disabled) return
38
+ if (!option.href && !option.to) emit('option-select', option, item)
39
+ openItemId.value = null
40
+ }
41
+
42
+ function normalizePillNavItem(item: PillNavItemInput): PillNavItem {
43
+ const normalized = normalizeItemInput<Omit<PillNavItem, 'children'> & { children?: PillNavOptionInput[] }>(item)
44
+ return {
45
+ ...normalized,
46
+ children: normalized.children?.map(normalizeItemInput),
47
+ }
48
+ }
49
+
50
+ function toggleDropdown(itemId: string) {
51
+ openItemId.value = openItemId.value === itemId ? null : itemId
52
+ }
53
+
54
+ function setItemRef(el: HTMLElement | null, itemId: string) {
55
+ if (el) {
56
+ itemRefs.value.set(itemId, el)
57
+ } else {
58
+ itemRefs.value.delete(itemId)
59
+ }
60
+ }
61
+
62
+ function handleClickOutside(event: MouseEvent) {
63
+ const target = event.target as Node
64
+ const clickedInside = Array.from(itemRefs.value.values()).some(el => el.contains(target))
65
+ if (!clickedInside) openItemId.value = null
66
+ }
67
+
68
+ const hasActiveChild = (item: PillNavItem) =>
69
+ item.children?.some(child => child.id === props.currentItemId) ?? false
70
+
71
+ function isSvgIcon(icon: PillNavItem['icon']): icon is string | string[] {
72
+ if (!icon) return false
73
+ return Array.isArray(icon) || icon.startsWith('M') || icon.startsWith('m')
74
+ }
75
+
76
+ useEventListener(() => document, 'click', handleClickOutside)
77
+ </script>
78
+
79
+ <template>
80
+ <nav class="mint-pill-nav" aria-label="Primary">
81
+ <div
82
+ v-for="item in normalizedItems"
83
+ :key="item.id"
84
+ :ref="(el) => setItemRef(el as HTMLElement | null, item.id)"
85
+ class="mint-pill-nav__item-wrap"
86
+ >
87
+ <button
88
+ v-if="item.children?.length"
89
+ type="button"
90
+ :class="[
91
+ 'mint-pill-nav__item',
92
+ { 'mint-pill-nav__item--active': item.id === currentItemId || hasActiveChild(item) },
93
+ { 'mint-pill-nav__item--disabled': item.disabled },
94
+ ]"
95
+ :disabled="item.disabled"
96
+ :aria-expanded="openItemId === item.id"
97
+ aria-haspopup="menu"
98
+ @click.stop="handleClick(item)"
99
+ >
100
+ <svg
101
+ v-if="isSvgIcon(item.icon)"
102
+ class="mint-pill-nav__icon"
103
+ viewBox="0 0 24 24"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ stroke-width="2"
107
+ stroke-linecap="round"
108
+ stroke-linejoin="round"
109
+ aria-hidden="true"
110
+ >
111
+ <template v-if="Array.isArray(item.icon)">
112
+ <path v-for="(d, index) in item.icon" :key="index" :d="d" />
113
+ </template>
114
+ <path v-else :d="item.icon" />
115
+ </svg>
116
+ {{ item.label }}
117
+ <svg
118
+ class="mint-pill-nav__chevron"
119
+ :class="{ 'mint-pill-nav__chevron--open': openItemId === item.id }"
120
+ viewBox="0 0 24 24"
121
+ fill="none"
122
+ stroke="currentColor"
123
+ stroke-width="2"
124
+ stroke-linecap="round"
125
+ stroke-linejoin="round"
126
+ aria-hidden="true"
127
+ >
128
+ <path d="m6 9 6 6 6-6" />
129
+ </svg>
130
+ </button>
131
+
132
+ <ActionItemInternal
133
+ v-else
134
+ :href="item.href"
135
+ :to="item.to"
136
+ :disabled="item.disabled"
137
+ :class="[
138
+ 'mint-pill-nav__item',
139
+ { 'mint-pill-nav__item--active': item.id === currentItemId },
140
+ { 'mint-pill-nav__item--disabled': item.disabled },
141
+ ]"
142
+ @click="handleClick(item)"
143
+ >
144
+ <svg
145
+ v-if="isSvgIcon(item.icon)"
146
+ class="mint-pill-nav__icon"
147
+ viewBox="0 0 24 24"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ stroke-width="2"
151
+ stroke-linecap="round"
152
+ stroke-linejoin="round"
153
+ aria-hidden="true"
154
+ >
155
+ <template v-if="Array.isArray(item.icon)">
156
+ <path v-for="(d, index) in item.icon" :key="index" :d="d" />
157
+ </template>
158
+ <path v-else :d="item.icon" />
159
+ </svg>
160
+ {{ item.label }}
161
+ </ActionItemInternal>
162
+
163
+ <div
164
+ v-if="item.children?.length && openItemId === item.id"
165
+ class="mint-pill-nav__dropdown"
166
+ role="menu"
167
+ >
168
+ <ActionItemInternal
169
+ v-for="option in item.children"
170
+ :key="option.id"
171
+ :href="option.href"
172
+ :to="option.to"
173
+ :disabled="option.disabled"
174
+ :class="[
175
+ 'mint-pill-nav__dropdown-item',
176
+ { 'mint-pill-nav__dropdown-item--active': option.id === currentItemId },
177
+ { 'mint-pill-nav__dropdown-item--disabled': option.disabled },
178
+ ]"
179
+ role="menuitem"
180
+ @click="handleOptionClick(option, item)"
181
+ >
182
+ <span class="mint-pill-nav__dropdown-label">{{ option.label }}</span>
183
+ <span v-if="option.description" class="mint-pill-nav__dropdown-description">
184
+ {{ option.description }}
185
+ </span>
186
+ </ActionItemInternal>
187
+ </div>
188
+ </div>
189
+ </nav>
190
+ </template>
191
+
192
+ <style>
193
+ @import '../../styles/components/app-pill-nav.css';
194
+ </style>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  /** Shared calendar grid panel used by date-oriented picker components. */
3
- import type { CalendarGridDay } from '../composables/useCalendarGrid'
3
+ import type { CalendarGridDay } from '../../composables/useCalendarGrid'
4
4
 
5
5
  interface Props {
6
6
  weekDays: readonly string[]
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch, computed } from 'vue'
3
- import type { Well, WellEditData, WellEditField } from '../types'
4
- import { useEventListener } from '../composables/useEventListener'
3
+ import type { Well, WellEditData, WellEditField } from '../../types'
4
+ import { useEventListener } from '../../composables/useEventListener'
5
5
 
6
6
  interface Props {
7
7
  wellId: string
@@ -225,5 +225,5 @@ const sampleTypeButtons = [
225
225
  </template>
226
226
 
227
227
  <style>
228
- @import '../styles/components/well-edit-popup.css';
228
+ @import '../../styles/components/well-edit-popup.css';
229
229
  </style>
@@ -1,5 +1,10 @@
1
1
  import type { DatePreset, ExperimentStatus, SelectOption, PillVariant } from '../types'
2
2
 
3
+ interface ExperimentCodeSource {
4
+ id?: number | null
5
+ experiment_code?: string | null
6
+ }
7
+
3
8
  export function formatExperimentDate(dateStr: string): string {
4
9
  try {
5
10
  return new Date(dateStr).toLocaleDateString(undefined, {
@@ -41,6 +46,27 @@ export const EXPERIMENT_STATUS_LABELS: Record<ExperimentStatus, string> = {
41
46
  cancelled: 'Cancelled',
42
47
  }
43
48
 
49
+ export function formatExperimentStatus(status?: ExperimentStatus | string | null): string {
50
+ if (!status) return ''
51
+ if (status in EXPERIMENT_STATUS_LABELS) {
52
+ return EXPERIMENT_STATUS_LABELS[status as ExperimentStatus]
53
+ }
54
+ const label = String(status).replace(/[-_]+/g, ' ').trim()
55
+ return label ? label.replace(/^\w/, c => c.toUpperCase()) : ''
56
+ }
57
+
58
+ export function getExperimentStatusVariant(status?: ExperimentStatus | string | null): PillVariant {
59
+ if (status && status in EXPERIMENT_STATUS_VARIANT_MAP) {
60
+ return EXPERIMENT_STATUS_VARIANT_MAP[status as ExperimentStatus]
61
+ }
62
+ return 'default'
63
+ }
64
+
65
+ export function resolveExperimentCode(experiment: ExperimentCodeSource): string | undefined {
66
+ if (experiment.experiment_code) return experiment.experiment_code
67
+ return experiment.id != null ? `EXP-${experiment.id}` : undefined
68
+ }
69
+
44
70
  export const DATE_PRESET_OPTIONS: SelectOption<string>[] = [
45
71
  { value: '', label: 'Any time' },
46
72
  { value: 'last_7_days', label: 'Last 7 days' },
@@ -129,6 +129,9 @@ export {
129
129
  } from './useExperimentSelector'
130
130
  export {
131
131
  formatExperimentDate,
132
+ formatExperimentStatus,
133
+ getExperimentStatusVariant,
134
+ resolveExperimentCode,
132
135
  datePresetToISO,
133
136
  EXPERIMENT_STATUS_OPTIONS,
134
137
  EXPERIMENT_STATUS_VARIANT_MAP,
@@ -149,9 +152,13 @@ export {
149
152
  export {
150
153
  useAppExperiment,
151
154
  APP_EXPERIMENT_KEY,
155
+ type AppExperimentRecord,
156
+ type AppExperimentSource,
152
157
  type UseAppExperimentOptions,
153
158
  type UseAppExperimentReturn,
154
159
  type AppExperimentState,
160
+ type AppExperimentPopoverBinding,
161
+ type AppExperimentSelectorModalBinding,
155
162
  } from './useAppExperiment'
156
163
  export {
157
164
  useExperimentSave,
@@ -227,11 +234,6 @@ export {
227
234
  type UseTextSearchOptions,
228
235
  type UseTextSearchReturn,
229
236
  } from './useTextSearch'
230
- /** @deprecated Use generated plugin clients from `mint sdk generate` instead. */
231
- export {
232
- usePluginApi,
233
- type UsePluginApiOptions,
234
- } from './usePluginApi'
235
237
  export {
236
238
  controlsToFormSchema,
237
239
  controlsToSectionFormSchema,
@@ -239,21 +241,29 @@ export {
239
241
  controlsToSidebarPanels,
240
242
  controlsToSettingsSchema,
241
243
  controlsToTopBarSettingsConfig,
242
- controlsToTopBarTabs,
243
244
  controlsToViewIds,
244
245
  controlsToViewItems,
246
+ controlValuesToComponentBindings,
247
+ controlValuesToComponentBindingsById,
245
248
  controlValuesToComponentProps,
249
+ defineControlComponentBindings,
246
250
  defineControlModel,
247
251
  defineDoseDesignControlModel,
248
252
  defineDoseCalculatorControlProps,
249
253
  defineControls,
250
254
  defineWellPlateControlProps,
255
+ defineWellPlateDoseComponentBindings,
251
256
  defineWellPlateDoseControlProps,
252
257
  getDefaultControlView,
253
258
  getControlDefaults,
254
259
  useControlSchema,
255
260
  useControlWorkspace,
256
261
  type ControlDefinition,
262
+ type ControlComponentBinding,
263
+ type ControlComponentBindingConfig,
264
+ type ControlComponentBindingRecordConfig,
265
+ type ControlComponentBindingsById,
266
+ type ControlComponentBindingsConfig,
257
267
  type ControlComponentPropSource,
258
268
  type ControlComponentPropsByIdMap,
259
269
  type ControlComponentPropsMap,
@@ -282,7 +292,6 @@ export {
282
292
  type ControlValues,
283
293
  type UseControlSchemaReturn,
284
294
  type ControlWorkspaceAppTopBarPillBinding,
285
- type ControlWorkspaceAppTopBarTabsBinding,
286
295
  type ControlWorkspaceComponentBindings,
287
296
  type ControlWorkspaceFormBinding,
288
297
  type ControlWorkspacePillNavBinding,
@@ -303,15 +312,19 @@ export {
303
312
  export {
304
313
  getBioTemplateComponentProps,
305
314
  getBioTemplateComponentBindings,
315
+ toBioTemplateComponentBindings,
316
+ toBioTemplateComponentBindingsById,
306
317
  toBioTemplateComponentProps,
307
318
  toBioTemplateComponentPropsByComponent,
308
319
  toBioTemplateComponentPropsById,
309
320
  useBioTemplateComponents,
310
321
  type BioTemplateComponentBinding,
322
+ type BioTemplateComponentBindingsById,
311
323
  type BioTemplateComponentPropsByComponent,
312
324
  type BioTemplateComponentPropsBinding,
313
325
  type BioTemplateComponentPropsById,
314
326
  type BioTemplateComponentPropsLookupOptions,
327
+ type BioTemplateResolvedComponentBinding,
315
328
  type BioTemplateComponentTarget,
316
329
  type UseBioTemplateComponentsReturn,
317
330
  } from './useBioTemplateComponents'
@@ -336,6 +349,7 @@ export {
336
349
  export {
337
350
  buildPluginEndpointUrl,
338
351
  createPluginClient,
352
+ getPluginPageSelectorItems,
339
353
  resolvePluginBaseUrl,
340
354
  usePluginClient,
341
355
  usePluginSettings,
@@ -5,6 +5,13 @@ import { useAuthStore } from '../stores/auth'
5
5
  let apiClientInstance: AxiosInstance | null = null
6
6
  let interceptorAttached = false
7
7
 
8
+ function joinUrlPath(baseUrl: string, path: string): string {
9
+ if (!path) return baseUrl
10
+ const normalizedBase = baseUrl.replace(/\/+$/, '')
11
+ const normalizedPath = path.replace(/^\/+/, '/')
12
+ return `${normalizedBase}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`
13
+ }
14
+
8
15
  function getApiClient(): AxiosInstance {
9
16
  if (!apiClientInstance) {
10
17
  apiClientInstance = axios.create({
@@ -144,12 +151,12 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
144
151
  // Build full URL for external use (e.g., <a href="...">)
145
152
  function buildUrl(path: string): string {
146
153
  const baseUrl = options.baseUrl ?? settingsStore.getApiBaseUrl()
147
- return `${baseUrl}${path}`
154
+ return joinUrlPath(baseUrl, path)
148
155
  }
149
156
 
150
157
  // WebSocket URL builder
151
158
  function buildWsUrl(path: string): string {
152
- return `${settingsStore.getWsBaseUrl()}${path}`
159
+ return joinUrlPath(settingsStore.getWsBaseUrl(), path)
153
160
  }
154
161
 
155
162
  return {