@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
@@ -2,12 +2,7 @@
2
2
  /** Full application top bar with brand logo, page selector or plugin switcher, centered pill nav, experiment popover, and avatar menu. */
3
3
  import { ref, computed, inject } from 'vue'
4
4
  import type {
5
- TopBarPage,
6
- TopBarPageInput,
7
- TopBarTab,
8
- TopBarTabInput,
9
- TopBarTabOption,
10
- TopBarTabOptionInput,
5
+ PillNavOption,
11
6
  TopBarSettingsConfig,
12
7
  TopBarVariant,
13
8
  PillNavItem,
@@ -21,20 +16,17 @@ import type {
21
16
  } from '../types/components'
22
17
  import type { PluginNavItem } from '../types/platform'
23
18
  import { normalizeItemInput } from '../utils/items'
24
- import { isPluginIconFormat } from '../utils/pluginIcon'
25
19
  import ThemeToggle from './ThemeToggle.vue'
26
20
  import SettingsModal from './SettingsModal.vue'
27
21
  import ExperimentPopover from './ExperimentPopover.vue'
28
22
  import ExperimentSelectorModal from './ExperimentSelectorModal.vue'
29
- import AppPageSelector from './AppPageSelector.vue'
30
- import AppPillNav from './AppPillNav.vue'
23
+ import AppTopBarPageSelectorInternal from './internal/AppTopBarPageSelectorInternal.vue'
24
+ import AppTopBarPillNavInternal from './internal/AppTopBarPillNavInternal.vue'
31
25
  import AppAvatarMenu from './AppAvatarMenu.vue'
32
26
  import AppPluginSwitcher from './AppPluginSwitcher.vue'
33
27
  import PluginIcon from './PluginIcon.vue'
34
28
  import { usePlatformContext } from '../composables/usePlatformContext'
35
29
  import { APP_EXPERIMENT_KEY } from '../composables/useAppExperiment'
36
- import { useEventListener } from '../composables/useEventListener'
37
- import { useDropdownState } from '../composables/useDropdownState'
38
30
 
39
31
  interface Props {
40
32
  /** App or plugin title shown in the left title group when no page selector is present. */
@@ -45,10 +37,10 @@ interface Props {
45
37
  showLogo?: boolean
46
38
  /** Top bar visual treatment. */
47
39
  variant?: TopBarVariant
48
- /** Home link used by classic breadcrumb layouts. */
40
+ /** Home link used by the brand icon. */
49
41
  homePath?: string
50
42
 
51
- /** Preferred route-level page switch entries for plugin and platform pages. */
43
+ /** Preferred route-level page switch entries for plugin and platform pages. Integrated plugins read platform plugin.nav_items metadata automatically when pageSelector is omitted. */
52
44
  pageSelector?: PageSelectorItemInput[]
53
45
  /** Active id for the preferred page selector. */
54
46
  currentPageSelectorId?: string
@@ -67,17 +59,6 @@ interface Props {
67
59
  /** Draw a notification dot on the notifications icon. */
68
60
  hasNotificationDot?: boolean
69
61
 
70
- /** Compatibility breadcrumb plugin name. Prefer pageSelector for route-level pages. */
71
- pluginName?: string
72
- /** Compatibility page dropdown items. Prefer pageSelector for new route-level navigation. */
73
- pages?: TopBarPageInput[]
74
- /** Active id for the compatibility page dropdown. */
75
- currentPageId?: string
76
- /** Compatibility center tabs. Prefer pillNav for new in-page modes. */
77
- tabs?: TopBarTabInput[]
78
- /** Active id for compatibility center tabs. */
79
- currentTabId?: string
80
-
81
62
  /** Show the theme toggle button. */
82
63
  showThemeToggle?: boolean
83
64
  /** Show the settings button and modal. */
@@ -118,14 +99,11 @@ const props = withDefaults(defineProps<Props>(), {
118
99
  })
119
100
 
120
101
  const emit = defineEmits<{
121
- 'page-select': [page: TopBarPage]
122
- 'tab-select': [tab: TopBarTab]
123
- 'tab-option-select': [option: TopBarTabOption, tab: TopBarTab]
124
102
  'profile-click': []
125
103
  'admin-click': []
126
- // New
127
104
  'page-selector-select': [page: PageSelectorItem]
128
105
  'pill-select': [item: PillNavItem]
106
+ 'pill-option-select': [option: PillNavOption, item: PillNavItem]
129
107
  'plugin-switcher-select': [plugin: PluginSwitcherPlugin]
130
108
  'plugin-switcher-install': []
131
109
  'account-menu-select': [item: AccountMenuItem]
@@ -147,20 +125,17 @@ const profileInitial = computed(() => {
147
125
  return 'U'
148
126
  })
149
127
 
150
- const hasPlatformPages = computed(() => props.pages === undefined && !!plugin.value?.nav_items?.length)
151
- const effectivePluginName = computed(() => props.pluginName ?? (hasPlatformPages.value ? plugin.value?.name : '') ?? '')
152
- const hasPageSelector = computed(() => !!props.pageSelector?.length)
128
+ const hasPlatformPageSelector = computed(() =>
129
+ props.pageSelector === undefined && (plugin.value?.nav_items?.length ?? 0) > 1,
130
+ )
131
+ const hasPageSelector = computed(() => (props.pageSelector?.length ?? 0) > 1 || hasPlatformPageSelector.value)
153
132
  const hasPluginSwitcher = computed(() => !!props.pluginSwitcher)
154
133
  const hasPillNav = computed(() => !!props.pillNav?.length)
155
134
  const hasAccountMenu = computed(() => !!props.accountMenu?.length)
156
- const hasLegacyBreadcrumb = computed(
157
- () => !hasPageSelector.value && !hasPluginSwitcher.value && (!!effectivePluginName.value || normalizedPages.value.length > 0),
158
- )
159
135
  const hasTitleGroup = computed(
160
136
  () =>
161
137
  !hasPageSelector.value &&
162
138
  !hasPluginSwitcher.value &&
163
- !hasLegacyBreadcrumb.value &&
164
139
  !!props.title &&
165
140
  !!props.subtitle
166
141
  )
@@ -169,58 +144,28 @@ const hasTitleOnly = computed(
169
144
  !hasPageSelector.value &&
170
145
  !hasPluginSwitcher.value &&
171
146
  !hasTitleGroup.value &&
172
- !hasLegacyBreadcrumb.value &&
173
147
  !!props.title,
174
148
  )
175
- const normalizedPages = computed<TopBarPage[]>(() =>
176
- props.pages !== undefined
177
- ? props.pages.map(normalizeItemInput)
178
- : plugin.value?.nav_items?.map(pluginNavItemToPage) ?? [],
149
+ const platformPageSelector = computed<PageSelectorItem[]>(() =>
150
+ hasPlatformPageSelector.value
151
+ ? plugin.value?.nav_items?.map(pluginNavItemToPageSelectorItem) ?? []
152
+ : [],
179
153
  )
180
- const normalizedTabs = computed<TopBarTab[]>(() => props.tabs?.map(normalizeTopBarTabInput) ?? [])
181
- const normalizedPageSelector = computed<PageSelectorItem[]>(() => props.pageSelector?.map(normalizeItemInput) ?? [])
182
- const normalizedPillNav = computed<PillNavItem[]>(() => props.pillNav?.map(normalizeItemInput) ?? [])
154
+ const normalizedPageSelector = computed<PageSelectorItem[]>(() =>
155
+ props.pageSelector !== undefined
156
+ ? props.pageSelector.map(normalizeItemInput)
157
+ : platformPageSelector.value,
158
+ )
159
+ const normalizedPillNav = computed<PillNavItemInput[]>(() => props.pillNav ?? [])
183
160
  const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
184
161
  props.settingsConfig?.tabs?.map(normalizeItemInput) ?? [],
185
162
  )
186
- const effectiveCurrentPageId = computed(() =>
187
- props.currentPageId
188
- ?? (hasPlatformPages.value ? currentPageIdFromLocation(normalizedPages.value) : undefined)
189
- ?? (hasPlatformPages.value ? normalizedPages.value[0]?.id : undefined),
190
- )
191
- const currentLegacyPage = computed(() =>
192
- normalizedPages.value.find((page) => page.id === effectiveCurrentPageId.value),
193
- )
194
- const effectiveTitle = computed(() =>
195
- props.title ?? (hasPlatformPages.value ? currentLegacyPage.value?.label : undefined),
163
+ const effectiveCurrentPageSelectorId = computed(() =>
164
+ props.currentPageSelectorId
165
+ ?? (hasPlatformPageSelector.value ? currentItemIdFromLocation(normalizedPageSelector.value) : undefined)
166
+ ?? (hasPlatformPageSelector.value ? normalizedPageSelector.value[0]?.id : undefined),
196
167
  )
197
168
 
198
- const {
199
- isOpen: showPagesDropdown,
200
- rootRef: dropdownRef,
201
- close: closePagesDropdown,
202
- toggle: togglePagesDropdownBase,
203
- } = useDropdownState()
204
- const openTabDropdown = ref<string | null>(null)
205
- const tabDropdownRefs = ref<Map<string, HTMLElement>>(new Map())
206
-
207
- function togglePagesDropdown() {
208
- togglePagesDropdownBase()
209
- openTabDropdown.value = null
210
- }
211
-
212
- function normalizeTopBarTabInput(tab: TopBarTabInput): TopBarTab {
213
- const normalized = normalizeItemInput<Omit<TopBarTab, 'children'> & { children?: TopBarTabOptionInput[] }>(tab)
214
- return {
215
- ...normalized,
216
- children: normalized.children?.map(normalizeItemInput),
217
- }
218
- }
219
-
220
- function isSvgTabIcon(icon?: string): icon is string {
221
- return !!icon && (icon.startsWith('M') || icon.startsWith('m'))
222
- }
223
-
224
169
  function normalizeNavPath(path?: string): string {
225
170
  const raw = path?.trim() || '/'
226
171
  const prefixed = raw.startsWith('/') ? raw : `/${raw}`
@@ -233,14 +178,14 @@ function pageIdFromPath(path: string, fallback: string): string {
233
178
  return normalized.replace(/^\/+/, '').replace(/\/+/g, '-') || fallback
234
179
  }
235
180
 
236
- function pluginNavItemToPage(item: PluginNavItem, index: number): TopBarPage {
181
+ function pluginNavItemToPageSelectorItem(item: PluginNavItem, index: number): PageSelectorItem {
237
182
  const path = normalizeNavPath(item.path)
238
183
  return {
239
184
  id: item.id || pageIdFromPath(path, `page-${index + 1}`),
240
185
  label: item.label,
241
186
  to: path,
242
187
  icon: item.icon || plugin.value?.icon,
243
- description: item.description,
188
+ hint: item.description || plugin.value?.name,
244
189
  }
245
190
  }
246
191
 
@@ -256,59 +201,11 @@ function currentPagePath(): string | undefined {
256
201
  return pathname
257
202
  }
258
203
 
259
- function currentPageIdFromLocation(pages: TopBarPage[]): string | undefined {
204
+ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 'to' | 'href'>>): string | undefined {
260
205
  const path = currentPagePath()
261
206
  if (!path) return undefined
262
207
  return pages.find((page) => normalizeNavPath(page.to || page.href) === path)?.id
263
208
  }
264
-
265
- function handlePageClick(page: TopBarPage) {
266
- if (page.disabled) return
267
- emit('page-select', page)
268
- closePagesDropdown()
269
- }
270
-
271
- function toggleTabDropdown(tabId: string) {
272
- closePagesDropdown()
273
- openTabDropdown.value = openTabDropdown.value === tabId ? null : tabId
274
- }
275
-
276
- function handleTabClick(tab: TopBarTab) {
277
- if (tab.disabled) return
278
- if (tab.children?.length) {
279
- toggleTabDropdown(tab.id)
280
- } else {
281
- emit('tab-select', tab)
282
- openTabDropdown.value = null
283
- }
284
- }
285
-
286
- function handleTabOptionClick(option: TopBarTabOption, tab: TopBarTab) {
287
- if (option.disabled) return
288
- emit('tab-option-select', option, tab)
289
- openTabDropdown.value = null
290
- }
291
-
292
- function setTabDropdownRef(el: HTMLElement | null, tabId: string) {
293
- if (el) {
294
- tabDropdownRefs.value.set(tabId, el)
295
- } else {
296
- tabDropdownRefs.value.delete(tabId)
297
- }
298
- }
299
-
300
- function handleClickOutside(event: MouseEvent) {
301
- const target = event.target as Node
302
-
303
- if (openTabDropdown.value !== null) {
304
- const clickedInside = Array.from(tabDropdownRefs.value.values()).some((el) => el.contains(target))
305
- if (!clickedInside) {
306
- openTabDropdown.value = null
307
- }
308
- }
309
- }
310
-
311
- useEventListener(() => document, 'click', handleClickOutside)
312
209
  </script>
313
210
 
314
211
  <template>
@@ -398,10 +295,10 @@ useEventListener(() => document, 'click', handleClickOutside)
398
295
  @select="emit('plugin-switcher-select', $event)"
399
296
  @install-click="emit('plugin-switcher-install')"
400
297
  />
401
- <AppPageSelector
298
+ <AppTopBarPageSelectorInternal
402
299
  v-else-if="hasPageSelector"
403
300
  :pages="normalizedPageSelector"
404
- :current-page-id="currentPageSelectorId"
301
+ :current-page-id="effectiveCurrentPageSelectorId"
405
302
  @select="emit('page-selector-select', $event)"
406
303
  >
407
304
  <template v-if="$slots['page-selector-icon']" #icon="slotProps">
@@ -410,121 +307,14 @@ useEventListener(() => document, 'click', handleClickOutside)
410
307
  <template v-if="$slots['page-selector-item-icon']" #item-icon="slotProps">
411
308
  <slot name="page-selector-item-icon" v-bind="slotProps" />
412
309
  </template>
413
- </AppPageSelector>
310
+ </AppTopBarPageSelectorInternal>
414
311
 
415
- <!-- Left: classic title / breadcrumb (classic prop surface) -->
312
+ <!-- Left: title -->
416
313
  <div v-if="hasTitleGroup" class="mint-topbar-title-group">
417
314
  <span class="mint-topbar-title">{{ title }}</span>
418
315
  <span class="mint-topbar-subtitle">{{ subtitle }}</span>
419
316
  </div>
420
317
 
421
- <div v-else-if="hasLegacyBreadcrumb" ref="dropdownRef" class="mint-topbar-breadcrumb">
422
- <button
423
- v-if="normalizedPages.length"
424
- type="button"
425
- class="mint-topbar-plugin-name"
426
- @click.stop="togglePagesDropdown"
427
- >
428
- {{ effectivePluginName }}
429
- <svg
430
- class="mint-topbar-chevron"
431
- :class="{ 'mint-topbar-chevron--open': showPagesDropdown }"
432
- width="16"
433
- height="16"
434
- viewBox="0 0 24 24"
435
- fill="none"
436
- stroke="currentColor"
437
- stroke-width="2"
438
- stroke-linecap="round"
439
- stroke-linejoin="round"
440
- >
441
- <path d="m6 9 6 6 6-6" />
442
- </svg>
443
- </button>
444
- <span v-else class="mint-topbar-plugin-name--static">{{ effectivePluginName }}</span>
445
-
446
- <svg
447
- v-if="effectiveTitle"
448
- class="mint-topbar-separator"
449
- width="16"
450
- height="16"
451
- viewBox="0 0 24 24"
452
- fill="none"
453
- stroke="currentColor"
454
- stroke-width="2"
455
- stroke-linecap="round"
456
- stroke-linejoin="round"
457
- >
458
- <path d="m9 18 6-6-6-6" />
459
- </svg>
460
- <span v-if="effectiveTitle" class="mint-topbar-current-page">{{ effectiveTitle }}</span>
461
-
462
- <div v-show="showPagesDropdown" class="mint-topbar-dropdown">
463
- <template v-for="page in normalizedPages" :key="page.id">
464
- <a
465
- v-if="page.href"
466
- :href="page.href"
467
- :class="['mint-topbar-dropdown-item', { 'mint-topbar-dropdown-item--active': page.id === effectiveCurrentPageId, 'mint-topbar-dropdown-item--disabled': page.disabled }]"
468
- @click="closePagesDropdown"
469
- >
470
- <span class="mint-topbar-dropdown-item__page">
471
- <PluginIcon
472
- v-if="isPluginIconFormat(page.icon)"
473
- class="mint-topbar-dropdown-item__icon"
474
- :icon="page.icon"
475
- size="sm"
476
- variant="tinted"
477
- />
478
- <span class="mint-topbar-dropdown-item__copy">
479
- <span class="mint-topbar-dropdown-item__label">{{ page.label }}</span>
480
- <span v-if="page.description" class="mint-topbar-dropdown-item__description">{{ page.description }}</span>
481
- </span>
482
- </span>
483
- </a>
484
- <router-link
485
- v-else-if="page.to"
486
- :to="page.to"
487
- :class="['mint-topbar-dropdown-item', { 'mint-topbar-dropdown-item--active': page.id === effectiveCurrentPageId, 'mint-topbar-dropdown-item--disabled': page.disabled }]"
488
- @click="closePagesDropdown"
489
- >
490
- <span class="mint-topbar-dropdown-item__page">
491
- <PluginIcon
492
- v-if="isPluginIconFormat(page.icon)"
493
- class="mint-topbar-dropdown-item__icon"
494
- :icon="page.icon"
495
- size="sm"
496
- variant="tinted"
497
- />
498
- <span class="mint-topbar-dropdown-item__copy">
499
- <span class="mint-topbar-dropdown-item__label">{{ page.label }}</span>
500
- <span v-if="page.description" class="mint-topbar-dropdown-item__description">{{ page.description }}</span>
501
- </span>
502
- </span>
503
- </router-link>
504
- <button
505
- v-else
506
- type="button"
507
- :class="['mint-topbar-dropdown-item', { 'mint-topbar-dropdown-item--active': page.id === effectiveCurrentPageId, 'mint-topbar-dropdown-item--disabled': page.disabled }]"
508
- @click="handlePageClick(page)"
509
- >
510
- <span class="mint-topbar-dropdown-item__page">
511
- <PluginIcon
512
- v-if="isPluginIconFormat(page.icon)"
513
- class="mint-topbar-dropdown-item__icon"
514
- :icon="page.icon"
515
- size="sm"
516
- variant="tinted"
517
- />
518
- <span class="mint-topbar-dropdown-item__copy">
519
- <span class="mint-topbar-dropdown-item__label">{{ page.label }}</span>
520
- <span v-if="page.description" class="mint-topbar-dropdown-item__description">{{ page.description }}</span>
521
- </span>
522
- </span>
523
- </button>
524
- </template>
525
- </div>
526
- </div>
527
-
528
318
  <span v-else-if="hasTitleOnly" class="mint-topbar__title-only">{{ title }}</span>
529
319
 
530
320
  <!-- Nav slot (inline, after brand/selector) -->
@@ -533,187 +323,16 @@ useEventListener(() => document, 'click', handleClickOutside)
533
323
  <!-- Center: pill nav (new) -->
534
324
  <div v-if="hasPillNav || $slots.center" class="mint-topbar__center">
535
325
  <slot name="center">
536
- <AppPillNav
326
+ <AppTopBarPillNavInternal
537
327
  v-if="hasPillNav && pillNav"
538
328
  :items="normalizedPillNav"
539
329
  :current-item-id="currentPillId"
540
330
  @select="emit('pill-select', $event)"
331
+ @option-select="(option, item) => emit('pill-option-select', option, item)"
541
332
  />
542
333
  </slot>
543
334
  </div>
544
335
 
545
- <!-- Center: classic tabs (when no pillNav) — wrapped in the same centered
546
- container as AppPillNav so classic :tabs consumers get centered pill
547
- layout without migrating to :pill-nav. -->
548
- <div v-if="!hasPillNav && normalizedTabs.length" class="mint-topbar__center">
549
- <div class="mint-topbar__tabs">
550
- <template v-for="tab in normalizedTabs" :key="tab.id">
551
- <div
552
- :ref="(el) => tab.children?.length ? setTabDropdownRef(el as HTMLElement, tab.id) : null"
553
- class="mint-topbar-tab-wrapper"
554
- >
555
- <button
556
- v-if="tab.children?.length"
557
- type="button"
558
- :class="[
559
- 'mint-topbar-tab',
560
- { 'mint-topbar-tab--active': tab.id === currentTabId || tab.children.some(c => c.id === currentTabId) },
561
- { 'mint-topbar-tab--disabled': tab.disabled }
562
- ]"
563
- @click.stop="handleTabClick(tab)"
564
- >
565
- <svg
566
- v-if="isSvgTabIcon(tab.icon)"
567
- class="mint-topbar-tab-icon"
568
- viewBox="0 0 24 24"
569
- fill="none"
570
- stroke="currentColor"
571
- stroke-width="2"
572
- stroke-linecap="round"
573
- stroke-linejoin="round"
574
- aria-hidden="true"
575
- >
576
- <path :d="tab.icon" />
577
- </svg>
578
- {{ tab.label }}
579
- <svg
580
- class="mint-topbar-tab-chevron"
581
- :class="{ 'mint-topbar-tab-chevron--open': openTabDropdown === tab.id }"
582
- width="14"
583
- height="14"
584
- viewBox="0 0 24 24"
585
- fill="none"
586
- stroke="currentColor"
587
- stroke-width="2"
588
- stroke-linecap="round"
589
- stroke-linejoin="round"
590
- >
591
- <path d="m6 9 6 6 6-6" />
592
- </svg>
593
- </button>
594
-
595
- <a
596
- v-else-if="tab.href"
597
- :href="tab.href"
598
- :class="[
599
- 'mint-topbar-tab',
600
- { 'mint-topbar-tab--active': tab.id === currentTabId },
601
- { 'mint-topbar-tab--disabled': tab.disabled }
602
- ]"
603
- >
604
- <svg
605
- v-if="isSvgTabIcon(tab.icon)"
606
- class="mint-topbar-tab-icon"
607
- viewBox="0 0 24 24"
608
- fill="none"
609
- stroke="currentColor"
610
- stroke-width="2"
611
- stroke-linecap="round"
612
- stroke-linejoin="round"
613
- aria-hidden="true"
614
- >
615
- <path :d="tab.icon" />
616
- </svg>
617
- {{ tab.label }}
618
- </a>
619
- <router-link
620
- v-else-if="tab.to"
621
- :to="tab.to"
622
- :class="[
623
- 'mint-topbar-tab',
624
- { 'mint-topbar-tab--active': tab.id === currentTabId },
625
- { 'mint-topbar-tab--disabled': tab.disabled }
626
- ]"
627
- >
628
- <svg
629
- v-if="isSvgTabIcon(tab.icon)"
630
- class="mint-topbar-tab-icon"
631
- viewBox="0 0 24 24"
632
- fill="none"
633
- stroke="currentColor"
634
- stroke-width="2"
635
- stroke-linecap="round"
636
- stroke-linejoin="round"
637
- aria-hidden="true"
638
- >
639
- <path :d="tab.icon" />
640
- </svg>
641
- {{ tab.label }}
642
- </router-link>
643
- <button
644
- v-else
645
- type="button"
646
- :class="[
647
- 'mint-topbar-tab',
648
- { 'mint-topbar-tab--active': tab.id === currentTabId },
649
- { 'mint-topbar-tab--disabled': tab.disabled }
650
- ]"
651
- @click="handleTabClick(tab)"
652
- >
653
- <svg
654
- v-if="isSvgTabIcon(tab.icon)"
655
- class="mint-topbar-tab-icon"
656
- viewBox="0 0 24 24"
657
- fill="none"
658
- stroke="currentColor"
659
- stroke-width="2"
660
- stroke-linecap="round"
661
- stroke-linejoin="round"
662
- aria-hidden="true"
663
- >
664
- <path :d="tab.icon" />
665
- </svg>
666
- {{ tab.label }}
667
- </button>
668
-
669
- <div v-if="tab.children?.length" v-show="openTabDropdown === tab.id" class="mint-topbar-tab-dropdown">
670
- <template v-for="option in tab.children" :key="option.id">
671
- <a
672
- v-if="option.href"
673
- :href="option.href"
674
- :class="[
675
- 'mint-topbar-dropdown-item',
676
- { 'mint-topbar-dropdown-item--active': option.id === currentTabId },
677
- { 'mint-topbar-dropdown-item--disabled': option.disabled }
678
- ]"
679
- @click="openTabDropdown = null"
680
- >
681
- <span class="mint-topbar-dropdown-item__label">{{ option.label }}</span>
682
- <span v-if="option.description" class="mint-topbar-dropdown-item__description">{{ option.description }}</span>
683
- </a>
684
- <router-link
685
- v-else-if="option.to"
686
- :to="option.to"
687
- :class="[
688
- 'mint-topbar-dropdown-item',
689
- { 'mint-topbar-dropdown-item--active': option.id === currentTabId },
690
- { 'mint-topbar-dropdown-item--disabled': option.disabled }
691
- ]"
692
- @click="openTabDropdown = null"
693
- >
694
- <span class="mint-topbar-dropdown-item__label">{{ option.label }}</span>
695
- <span v-if="option.description" class="mint-topbar-dropdown-item__description">{{ option.description }}</span>
696
- </router-link>
697
- <button
698
- v-else
699
- type="button"
700
- :class="[
701
- 'mint-topbar-dropdown-item',
702
- { 'mint-topbar-dropdown-item--active': option.id === currentTabId },
703
- { 'mint-topbar-dropdown-item--disabled': option.disabled }
704
- ]"
705
- @click="handleTabOptionClick(option, tab)"
706
- >
707
- <span class="mint-topbar-dropdown-item__label">{{ option.label }}</span>
708
- <span v-if="option.description" class="mint-topbar-dropdown-item__description">{{ option.description }}</span>
709
- </button>
710
- </template>
711
- </div>
712
- </div>
713
- </template>
714
- </div>
715
- </div>
716
-
717
336
  <!-- Right section -->
718
337
  <div class="mint-topbar__right">
719
338
  <span v-if="showStandaloneLabel && isStandalone && !appExperiment" class="mint-topbar__standalone-badge">
@@ -722,15 +341,7 @@ useEventListener(() => document, 'click', handleClickOutside)
722
341
 
723
342
  <ExperimentPopover
724
343
  v-if="appExperiment && !isStandalone"
725
- :experiment-name="appExperiment.experimentName.value"
726
- :experiment-code="appExperiment.experimentCode.value"
727
- :experiment-status="appExperiment.experimentStatus.value"
728
- :show-save="appExperiment.showSave.value"
729
- :show-detach="appExperiment.showDetach.value"
730
- :save-disabled="appExperiment.saveDisabled.value"
731
- :save-disabled-message="appExperiment.saveDisabledMessage.value"
732
- :save-loading="appExperiment.saveLoading.value"
733
- :save-success-message="appExperiment.saveSuccessMessage.value"
344
+ v-bind="appExperiment.popover.value"
734
345
  @select="appExperiment.openModal()"
735
346
  @save="appExperiment.handleSave()"
736
347
  @detach="appExperiment.handleDetach()"
@@ -847,8 +458,7 @@ useEventListener(() => document, 'click', handleClickOutside)
847
458
 
848
459
  <ExperimentSelectorModal
849
460
  v-if="appExperiment && !isStandalone"
850
- :model-value="appExperiment.showModal.value"
851
- :current-experiment-id="appExperiment.experimentId.value"
461
+ v-bind="appExperiment.selectorModal.value"
852
462
  @update:model-value="$event ? appExperiment.openModal() : appExperiment.closeModal()"
853
463
  @select="appExperiment.handleSelect($event)"
854
464
  @deselect="appExperiment.handleDetach()"
@@ -69,14 +69,14 @@ function initState() {
69
69
  <div class="experiment-component-layout">
70
70
  <div class="experiment-component-panel experiment-component-panel--plate">
71
71
  <WellPlate
72
- v-bind="bindings.componentPropsById['plate-map:WellPlate']"
72
+ v-bind="bindings.componentBindingsById['plate-map:WellPlate'].props"
73
73
  size="sm"
74
74
  readonly
75
75
  />
76
76
  </div>
77
77
  <div class="experiment-component-panel experiment-component-panel--dose">
78
78
  <DoseCalculator
79
- v-bind="bindings.componentPropsById['dose-response:DoseCalculator']"
79
+ v-bind="bindings.componentBindingsById['dose-response:DoseCalculator'].props"
80
80
  />
81
81
  </div>
82
82
  </div>
@@ -4,6 +4,7 @@ import { computed } from 'vue'
4
4
  import {
5
5
  extractTemplateCollection,
6
6
  getBioTemplateComponentProps,
7
+ toBioTemplateComponentBindingsById,
7
8
  toBioTemplateComponentProps,
8
9
  toBioTemplateComponentPropsByComponent,
9
10
  toBioTemplateComponentPropsById,
@@ -135,6 +136,7 @@ const templateSummaries = computed(() =>
135
136
  }))
136
137
  )
137
138
  const componentProps = computed(() => toBioTemplateComponentProps(props.target))
139
+ const componentBindingsById = computed(() => toBioTemplateComponentBindingsById(props.target))
138
140
  const componentPropsById = computed(() => toBioTemplateComponentPropsById(props.target))
139
141
  const componentPropsByComponent = computed(() => toBioTemplateComponentPropsByComponent(props.target))
140
142
  const renderer = computed(() => ({ target: props.target }))
@@ -146,6 +148,7 @@ function getComponentProps(component: string, options?: BioTemplateComponentProp
146
148
  interface BioTemplateExperimentWorkspaceBindings {
147
149
  renderer: { target: BioTemplateWorkspaceTarget }
148
150
  componentProps: ReturnType<typeof toBioTemplateComponentProps>
151
+ componentBindingsById: ReturnType<typeof toBioTemplateComponentBindingsById>
149
152
  componentPropsById: ReturnType<typeof toBioTemplateComponentPropsById>
150
153
  componentPropsByComponent: ReturnType<typeof toBioTemplateComponentPropsByComponent>
151
154
  getComponentProps: typeof getComponentProps
@@ -154,6 +157,7 @@ interface BioTemplateExperimentWorkspaceBindings {
154
157
  const bindings = computed<BioTemplateExperimentWorkspaceBindings>(() => ({
155
158
  renderer: renderer.value,
156
159
  componentProps: componentProps.value,
160
+ componentBindingsById: componentBindingsById.value,
157
161
  componentPropsById: componentPropsById.value,
158
162
  componentPropsByComponent: componentPropsByComponent.value,
159
163
  getComponentProps,
@@ -163,6 +167,7 @@ interface BioTemplateExperimentWorkspaceSlotProps {
163
167
  target: BioTemplateWorkspaceTarget
164
168
  bindings: BioTemplateExperimentWorkspaceBindings
165
169
  componentProps: ReturnType<typeof toBioTemplateComponentProps>
170
+ componentBindingsById: ReturnType<typeof toBioTemplateComponentBindingsById>
166
171
  componentPropsById: ReturnType<typeof toBioTemplateComponentPropsById>
167
172
  componentPropsByComponent: ReturnType<typeof toBioTemplateComponentPropsByComponent>
168
173
  getComponentProps: typeof getComponentProps
@@ -255,6 +260,7 @@ function handleSave() {
255
260
  :target="target"
256
261
  :bindings="bindings"
257
262
  :component-props="componentProps"
263
+ :component-bindings-by-id="componentBindingsById"
258
264
  :component-props-by-id="componentPropsById"
259
265
  :component-props-by-component="componentPropsByComponent"
260
266
  :get-component-props="getComponentProps"