@morscherlab/mint-sdk 1.2.0-beta.4 → 1.2.0-beta.7

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 (137) hide show
  1. package/dist/{EmptyState-p90quq8Z.js → EmptyState-NBqf2_8z.js} +2 -2
  2. package/dist/{EmptyState-p90quq8Z.js.map → EmptyState-NBqf2_8z.js.map} +1 -1
  3. package/dist/{ExperimentPopover-tLwJ7XF4.js → ExperimentPopover-CfOdh5Rm.js} +3 -3
  4. package/dist/{ExperimentPopover-tLwJ7XF4.js.map → ExperimentPopover-CfOdh5Rm.js.map} +1 -1
  5. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DvIagrxY.js} +4 -4
  6. package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DvIagrxY.js.map} +1 -1
  7. package/dist/{SettingsModal-CkNM8qAI.js → SettingsModal-W3TYfnP9.js} +4 -7
  8. package/dist/SettingsModal-W3TYfnP9.js.map +1 -0
  9. package/dist/__tests__/components/PluginWorkspaceView.internal.test.d.ts +1 -0
  10. package/dist/__tests__/components/SecretInput.test.d.ts +1 -0
  11. package/dist/__tests__/components/WellEditPopupInternal.test.d.ts +1 -0
  12. package/dist/components/AppAvatarMenu.vue.d.ts +4 -0
  13. package/dist/components/AppSidebar.vue.d.ts +1 -1
  14. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  15. package/dist/components/DropdownButton.vue.d.ts +2 -0
  16. package/dist/components/FitPanel.vue.d.ts +1 -1
  17. package/dist/components/PluginWorkspaceView.props.d.ts +29 -1
  18. package/dist/components/PluginWorkspaceView.vue.d.ts +5 -5
  19. package/dist/components/SecretInput.vue.d.ts +28 -0
  20. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  21. package/dist/components/index.d.ts +9 -0
  22. package/dist/components/index.js +8 -8
  23. package/dist/components/internal/WellEditPopupInternal.vue.d.ts +3 -1
  24. package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +23 -0
  25. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +70 -0
  26. package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +43 -0
  27. package/dist/{components-MGNL6tRn.js → components-B80gV0P6.js} +575 -561
  28. package/dist/components-B80gV0P6.js.map +1 -0
  29. package/dist/composables/flattenWorkspaceBindings.d.ts +61 -0
  30. package/dist/composables/index.d.ts +5 -0
  31. package/dist/composables/index.js +9 -9
  32. package/dist/composables/pluginWorkspaceModel.d.ts +164 -0
  33. package/dist/composables/useAsync.d.ts +5 -1
  34. package/dist/composables/useBioTemplateWorkspace.d.ts +4 -20
  35. package/dist/composables/useCommandHistory.d.ts +5 -1
  36. package/dist/composables/useFocusTrap.d.ts +21 -0
  37. package/dist/composables/useMenuKeyboard.d.ts +29 -0
  38. package/dist/composables/useOptimisticMutation.d.ts +5 -1
  39. package/dist/composables/usePluginWorkspace.d.ts +51 -0
  40. package/dist/composables/useResourceCrud.d.ts +5 -1
  41. package/dist/composables/useRovingFocus.d.ts +24 -0
  42. package/dist/composables/useWellPainting.d.ts +5 -1
  43. package/dist/composables/useWellPlateAdapter.d.ts +5 -1
  44. package/dist/composables/useWellPlateValidation.d.ts +5 -1
  45. package/dist/composables/workspaceRecipes.d.ts +188 -0
  46. package/dist/{composables-CKxOcvgh.js → composables-C8mmD5ta.js} +65 -17
  47. package/dist/composables-C8mmD5ta.js.map +1 -0
  48. package/dist/{experiment-utils-CLnzPPq5.js → experiment-utils-DV3DrvE_.js} +70 -51
  49. package/dist/experiment-utils-DV3DrvE_.js.map +1 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +13 -13
  52. package/dist/install.js +7 -7
  53. package/dist/styles.css +100 -49
  54. package/dist/templates/builderReadUtils.d.ts +1 -1
  55. package/dist/templates/index.js +4 -4
  56. package/dist/{templates-BTJ9C9Kb.js → templates-CK72aKO1.js} +14 -23
  57. package/dist/templates-CK72aKO1.js.map +1 -0
  58. package/dist/types/form-builder.d.ts +1 -1
  59. package/dist/{useControlSchema-CXhxTul7.js → useControlSchema-_OVhavKq.js} +280 -63
  60. package/dist/useControlSchema-_OVhavKq.js.map +1 -0
  61. package/dist/{useDropdownState-DSb3BC3N.js → useDropdownState-D5G4UvWd.js} +2 -2
  62. package/dist/{useDropdownState-DSb3BC3N.js.map → useDropdownState-D5G4UvWd.js.map} +1 -1
  63. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-DSEckf_g.js} +3 -3
  64. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-DSEckf_g.js.map} +1 -1
  65. package/dist/{useFormBuilder-CuOFB3aP.js → useFormBuilder-Bd714C7y.js} +2 -2
  66. package/dist/{useFormBuilder-CuOFB3aP.js.map → useFormBuilder-Bd714C7y.js.map} +1 -1
  67. package/dist/{useProtocolTemplates-BWhQIWV5.js → useProtocolTemplates-Dq5Pzwyd.js} +481 -46
  68. package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +1 -0
  69. package/dist/utils/pluginSecrets.d.ts +53 -0
  70. package/dist/utils/records.d.ts +4 -0
  71. package/package.json +1 -1
  72. package/src/__tests__/components/AppAvatarMenu.test.ts +123 -0
  73. package/src/__tests__/components/BaseTabs.test.ts +67 -0
  74. package/src/__tests__/components/DropdownButton.test.ts +134 -0
  75. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +230 -0
  76. package/src/__tests__/components/SecretInput.test.ts +87 -0
  77. package/src/__tests__/components/WellEditPopupInternal.test.ts +108 -0
  78. package/src/__tests__/composables/usePluginClient.test.ts +140 -0
  79. package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
  80. package/src/components/AppAvatarMenu.vue +34 -3
  81. package/src/components/BaseModal.vue +8 -56
  82. package/src/components/BaseTabs.vue +26 -1
  83. package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
  84. package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
  85. package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
  86. package/src/components/ComponentBindingRenderer.vue +1 -3
  87. package/src/components/ControlWorkspaceView.vue +11 -22
  88. package/src/components/DoseDesignWorkspaceView.vue +6 -25
  89. package/src/components/DropdownButton.vue +32 -2
  90. package/src/components/PluginWorkspaceView.props.ts +38 -0
  91. package/src/components/PluginWorkspaceView.vue +53 -224
  92. package/src/components/SecretInput.story.vue +79 -0
  93. package/src/components/SecretInput.vue +150 -0
  94. package/src/components/index.ts +9 -0
  95. package/src/components/internal/WellEditPopupInternal.vue +20 -1
  96. package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
  97. package/src/components/internal/WorkspaceShellInternal.vue +125 -0
  98. package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
  99. package/src/composables/experimentDesignData.ts +1 -3
  100. package/src/composables/flattenWorkspaceBindings.ts +104 -0
  101. package/src/composables/formBuilderRegistry.ts +5 -0
  102. package/src/composables/index.ts +26 -0
  103. package/src/composables/pluginWorkspaceModel.ts +182 -0
  104. package/src/composables/useAsync.ts +5 -1
  105. package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
  106. package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
  107. package/src/composables/useBioTemplateWorkspace.ts +18 -43
  108. package/src/composables/useCommandHistory.ts +5 -1
  109. package/src/composables/useFocusTrap.ts +91 -0
  110. package/src/composables/useMenuKeyboard.ts +90 -0
  111. package/src/composables/useOptimisticMutation.ts +5 -1
  112. package/src/composables/usePluginClient.ts +42 -4
  113. package/src/composables/usePluginWorkspace.ts +208 -0
  114. package/src/composables/useResourceCrud.ts +5 -1
  115. package/src/composables/useRovingFocus.ts +66 -0
  116. package/src/composables/useWellPainting.ts +5 -1
  117. package/src/composables/useWellPlateAdapter.ts +5 -1
  118. package/src/composables/useWellPlateValidation.ts +5 -1
  119. package/src/composables/workspaceRecipes.ts +267 -0
  120. package/src/index.ts +16 -0
  121. package/src/styles/components/secret-input.css +60 -0
  122. package/src/styles/index.css +1 -0
  123. package/src/templates/builderReadUtils.ts +1 -3
  124. package/src/templates/componentTargetResolvers.ts +1 -3
  125. package/src/templates/controlSchemaTargets.ts +1 -3
  126. package/src/types/form-builder.ts +1 -0
  127. package/src/utils/formModelSync.ts +1 -3
  128. package/src/utils/pluginIcon.ts +1 -3
  129. package/src/utils/pluginSecrets.ts +102 -0
  130. package/src/utils/records.ts +9 -0
  131. package/dist/SettingsModal-CkNM8qAI.js.map +0 -1
  132. package/dist/components-MGNL6tRn.js.map +0 -1
  133. package/dist/composables-CKxOcvgh.js.map +0 -1
  134. package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
  135. package/dist/templates-BTJ9C9Kb.js.map +0 -1
  136. package/dist/useControlSchema-CXhxTul7.js.map +0 -1
  137. package/dist/useProtocolTemplates-BWhQIWV5.js.map +0 -1
@@ -12,26 +12,19 @@
12
12
  import { computed, useSlots } from 'vue'
13
13
  import type {
14
14
  AccountMenuItem,
15
- ExperimentSummary,
16
- PageSelectorItem,
17
15
  PillNavItem,
18
16
  PluginSwitcherPlugin,
19
17
  PillNavOption,
20
18
  } from '../types'
21
19
  import { useAppExperiment } from '../composables/useAppExperiment'
22
- import AppLayout from './AppLayout.vue'
23
- import AppSidebar from './AppSidebar.vue'
24
- import AppTopBar from './AppTopBar.vue'
25
- import { usePluginWorkspaceControls } from './PluginWorkspaceView.controls'
26
- import { usePluginWorkspaceNavigation } from './PluginWorkspaceView.navigation'
27
- import {
28
- getPluginWorkspaceSidebarSlotNames,
29
- getPluginWorkspaceTopBarSlotNames,
30
- hasPluginWorkspaceSidebarSurface,
31
- } from './PluginWorkspaceView.shell'
20
+ import { usePluginWorkspace } from '../composables/usePluginWorkspace'
21
+ import WorkspaceControlsInternal from './internal/WorkspaceControlsInternal.vue'
22
+ import WorkspaceShellInternal from './internal/WorkspaceShellInternal.vue'
23
+ import WorkspaceSidebarInternal from './internal/WorkspaceSidebarInternal.vue'
32
24
  import type {
33
25
  PluginWorkspaceForwardedSlotProps,
34
26
  PluginWorkspaceSlotProps,
27
+ PluginWorkspaceViewEmits,
35
28
  PluginWorkspaceViewProps,
36
29
  } from './PluginWorkspaceView.props'
37
30
 
@@ -103,28 +96,7 @@ const props = withDefaults(defineProps<PluginWorkspaceViewProps>(), {
103
96
  sidebarCloseLabel: 'Close workspace sidebar',
104
97
  })
105
98
 
106
- const emit = defineEmits<{
107
- 'update:activeView': [value: string]
108
- 'page-selector-select': [page: PageSelectorItem]
109
- 'pill-select': [item: PillNavItem]
110
- 'pill-option-select': [option: PillNavOption, item: PillNavItem]
111
- 'plugin-switcher-select': [plugin: PluginSwitcherPlugin]
112
- 'plugin-switcher-install': []
113
- 'account-menu-select': [item: AccountMenuItem]
114
- 'sign-out': []
115
- 'notifications-click': []
116
- 'settings-values-change': [data: Record<string, unknown>]
117
- 'admin-click': []
118
- 'experiment-select': [experiment: ExperimentSummary]
119
- 'experiment-detach': []
120
- 'update:modelValue': [values: Record<string, unknown>]
121
- 'update:values': [values: Record<string, unknown>]
122
- 'update:sidebarCollapsed': [value: boolean]
123
- 'update:toggle': [sectionId: string, value: boolean]
124
- 'section-action': [sectionId: string, actionId: string]
125
- 'form-submit': [sectionId: string, values: Record<string, unknown>]
126
- 'form-cancel': [sectionId: string]
127
- }>()
99
+ const emit = defineEmits<PluginWorkspaceViewEmits>()
128
100
 
129
101
  defineSlots<{
130
102
  default?: (props: PluginWorkspaceSlotProps) => unknown
@@ -158,82 +130,19 @@ if (props.experimentShell) {
158
130
  })
159
131
  }
160
132
 
161
- const topBarSlotNames = computed<string[]>(() =>
162
- getPluginWorkspaceTopBarSlotNames(slots),
163
- )
164
-
165
- const sidebarSlotNames = computed<string[]>(() =>
166
- getPluginWorkspaceSidebarSlotNames(slots),
167
- )
168
-
169
133
  const {
170
- generatedPillNav,
171
- generatedDefaultView,
172
- internalControlValues,
173
- resolvedComponentBindings,
174
- resolvedComponentBindingsById,
175
- resolvedComponentProps,
176
- resolvedComponentPropsById,
134
+ shell,
135
+ sidebar,
136
+ controls,
177
137
  handleControlValuesUpdate,
178
- } = usePluginWorkspaceControls({
179
- model: () => props.model,
180
- controls: () => props.controls,
181
- controlOptions: () => props.controlOptions,
182
- modelValue: () => props.modelValue,
183
- values: () => props.values,
184
- componentBindings: () => props.componentBindings,
185
- componentProps: () => props.componentProps,
186
- componentPropsById: () => props.componentPropsById,
187
- emitValues: (values) => {
188
- emit('update:modelValue', values)
189
- emit('update:values', values)
190
- },
191
- })
192
-
193
- const {
194
- resolvedPillNav,
195
- resolvedActiveView,
196
- resolvedCurrentPillId,
197
- resolvedCurrentPageSelectorId,
198
- setActiveView,
199
138
  handlePageSelectorSelect,
200
139
  handlePillSelect,
201
- } = usePluginWorkspaceNavigation({
202
- activeView: () => props.activeView,
203
- defaultActiveView: () => props.defaultActiveView,
204
- currentPillId: () => props.currentPillId,
205
- currentPageSelectorId: () => props.currentPageSelectorId,
206
- pillNav: () => props.pillNav,
207
- pageSelector: () => props.pageSelector,
208
- panelIds: () => Object.keys(props.panels),
209
- generatedPillNav,
210
- generatedDefaultView,
211
- emitActiveView: (viewId) => emit('update:activeView', viewId),
212
- emitPageSelectorSelect: (page) => emit('page-selector-select', page),
213
- emitPillSelect: (item) => emit('pill-select', item),
140
+ } = usePluginWorkspace({
141
+ props,
142
+ slots,
143
+ emit,
214
144
  })
215
145
 
216
- const hasSidebarSurface = computed(() => {
217
- return hasPluginWorkspaceSidebarSurface({
218
- showSidebar: props.showSidebar,
219
- panels: props.panels,
220
- forms: props.forms,
221
- model: props.model,
222
- controls: props.controls,
223
- showSidebarWhenEmpty: props.showSidebarWhenEmpty,
224
- sidebarContentId: props.sidebarContentId,
225
- sidebarTitle: props.sidebarTitle,
226
- sidebarSubtitle: props.sidebarSubtitle,
227
- sidebarBadge: props.sidebarBadge,
228
- hasSidebarSlot: Boolean(slots.sidebar),
229
- sidebarSlotCount: sidebarSlotNames.value.length,
230
- })
231
- })
232
-
233
- function handlePillOptionSelect(option: PillNavOption, item: PillNavItem) {
234
- emit('pill-option-select', option, item)
235
- }
236
-
237
146
  function handleToggle(sectionId: string, value: boolean) {
238
147
  emit('update:toggle', sectionId, value)
239
148
  }
@@ -249,126 +158,48 @@ function handleFormCancel(sectionId: string) {
249
158
  </script>
250
159
 
251
160
  <template>
252
- <AppLayout
253
- class="mint-plugin-workspace-view"
254
- :sidebar-position="sidebarPosition"
255
- :sidebar-width="sidebarWidth"
256
- :floating="floating"
257
- :responsive-sidebar="responsiveSidebar"
258
- :sidebar-toggle-label="sidebarToggleLabel"
259
- :sidebar-close-label="sidebarCloseLabel"
161
+ <WorkspaceShellInternal
162
+ :shell="shell"
163
+ @page-selector-select="handlePageSelectorSelect"
164
+ @pill-select="handlePillSelect"
165
+ @pill-option-select="(option: PillNavOption, item: PillNavItem) => emit('pill-option-select', option, item)"
166
+ @plugin-switcher-select="(plugin: PluginSwitcherPlugin) => emit('plugin-switcher-select', plugin)"
167
+ @plugin-switcher-install="emit('plugin-switcher-install')"
168
+ @account-menu-select="(item: AccountMenuItem) => emit('account-menu-select', item)"
169
+ @sign-out="emit('sign-out')"
170
+ @notifications-click="emit('notifications-click')"
171
+ @settings-values-change="emit('settings-values-change', $event)"
172
+ @admin-click="emit('admin-click')"
260
173
  >
261
- <template #topbar>
262
- <slot
263
- name="topbar"
264
- :active-view="resolvedActiveView"
265
- :set-active-view="setActiveView"
266
- :values="internalControlValues"
267
- :component-bindings="resolvedComponentBindings"
268
- :component-bindings-by-id="resolvedComponentBindingsById"
269
- :component-props="resolvedComponentProps"
270
- :component-props-by-id="resolvedComponentPropsById"
271
- >
272
- <AppTopBar
273
- :title="title"
274
- :subtitle="subtitle"
275
- :variant="topBarVariant"
276
- :home-path="homePath"
277
- :show-logo="showLogo"
278
- :page-selector="pageSelector"
279
- :current-page-selector-id="resolvedCurrentPageSelectorId"
280
- :plugin-switcher="pluginSwitcher"
281
- :pill-nav="resolvedPillNav"
282
- :current-pill-id="resolvedCurrentPillId"
283
- :show-theme-toggle="showThemeToggle"
284
- :show-settings="showSettings"
285
- :settings-config="settingsConfig"
286
- :show-standalone-label="showStandaloneLabel"
287
- :standalone-label="standaloneLabel"
288
- :account-menu="accountMenu"
289
- :show-notifications="showNotifications"
290
- :has-notification-dot="hasNotificationDot"
291
- :show-admin="showAdmin"
292
- :admin-path="adminPath"
293
- :user-name="userName"
294
- :user-initial="userInitial"
295
- :user-email="userEmail"
296
- @page-selector-select="handlePageSelectorSelect"
297
- @pill-select="handlePillSelect"
298
- @pill-option-select="handlePillOptionSelect"
299
- @plugin-switcher-select="emit('plugin-switcher-select', $event)"
300
- @plugin-switcher-install="emit('plugin-switcher-install')"
301
- @account-menu-select="emit('account-menu-select', $event)"
302
- @sign-out="emit('sign-out')"
303
- @notifications-click="emit('notifications-click')"
304
- @settings-values-change="emit('settings-values-change', $event)"
305
- @admin-click="emit('admin-click')"
306
- >
307
- <template
308
- v-for="slotName in topBarSlotNames"
309
- :key="slotName"
310
- #[slotName]="slotProps"
311
- >
312
- <!-- @vue-ignore dynamic SDK slot forwarding for AppTopBar passthrough slots -->
313
- <slot
314
- :name="slotName"
315
- v-bind="slotProps ?? {}"
316
- />
317
- </template>
318
- </AppTopBar>
319
- </slot>
174
+ <template v-if="slots.topbar" #topbar="scope">
175
+ <slot name="topbar" v-bind="scope" />
320
176
  </template>
321
177
 
322
- <template v-if="hasSidebarSurface" #sidebar>
178
+ <template
179
+ v-for="slotName in shell.topBarSlotNames"
180
+ :key="slotName"
181
+ #[slotName]="slotProps"
182
+ >
183
+ <!-- @vue-ignore dynamic SDK slot forwarding for AppTopBar passthrough slots -->
323
184
  <slot
324
- name="sidebar"
325
- :active-view="resolvedActiveView"
326
- :set-active-view="setActiveView"
327
- :values="internalControlValues"
328
- :component-bindings="resolvedComponentBindings"
329
- :component-bindings-by-id="resolvedComponentBindingsById"
330
- :component-props="resolvedComponentProps"
331
- :component-props-by-id="resolvedComponentPropsById"
332
- >
333
- <AppSidebar
334
- :title="sidebarTitle"
335
- :subtitle="sidebarSubtitle"
336
- :badge="sidebarBadge"
337
- :variant="sidebarVariant"
338
- :panels="panels"
339
- :active-view="resolvedActiveView"
340
- :floating="false"
341
- :dense="dense"
342
- :width="sidebarWidth"
343
- :side="sidebarPosition"
344
- :forms="forms"
345
- :content-id="sidebarContentId"
346
- :show-when-empty="showSidebarWhenEmpty || sidebarContentId !== undefined"
347
- :model="model"
348
- :controls="controls"
349
- :control-options="controlOptions"
350
- :values="internalControlValues"
351
- :form-enhancements="formEnhancements"
352
- :show-form-actions="showFormActions"
353
- :form-loading="formLoading"
354
- :form-disabled="formDisabled"
355
- :form-readonly="formReadonly"
356
- :form-size="formSize"
357
- :collapsible="sidebarCollapsible"
358
- :collapsed="sidebarCollapsed"
359
- :default-collapsed="defaultSidebarCollapsed"
360
- :collapsed-width="sidebarCollapsedWidth"
361
- :collapse-button-label="sidebarCollapseButtonLabel"
362
- :expand-button-label="sidebarExpandButtonLabel"
185
+ :name="slotName"
186
+ v-bind="slotProps ?? {}"
187
+ />
188
+ </template>
189
+
190
+ <template v-if="shell.hasSidebarSurface" #sidebar="scope">
191
+ <slot name="sidebar" v-bind="scope">
192
+ <WorkspaceSidebarInternal
193
+ :sidebar="sidebar"
363
194
  @update:model-value="handleControlValuesUpdate"
364
195
  @update:collapsed="emit('update:sidebarCollapsed', $event)"
365
196
  @update:toggle="handleToggle"
366
- @section-action="(sectionId, actionId) => emit('section-action', sectionId, actionId)"
197
+ @section-action="(sectionId: string, actionId: string) => emit('section-action', sectionId, actionId)"
367
198
  @form-submit="handleFormSubmit"
368
199
  @form-cancel="handleFormCancel"
369
200
  >
370
201
  <template
371
- v-for="slotName in sidebarSlotNames"
202
+ v-for="slotName in sidebar.slotNames"
372
203
  :key="slotName"
373
204
  #[slotName]="slotProps"
374
205
  >
@@ -378,18 +209,16 @@ function handleFormCancel(sectionId: string) {
378
209
  v-bind="slotProps ?? {}"
379
210
  />
380
211
  </template>
381
- </AppSidebar>
212
+ </WorkspaceSidebarInternal>
382
213
  </slot>
383
214
  </template>
384
215
 
385
- <slot
386
- :active-view="resolvedActiveView"
387
- :set-active-view="setActiveView"
388
- :values="internalControlValues"
389
- :component-bindings="resolvedComponentBindings"
390
- :component-bindings-by-id="resolvedComponentBindingsById"
391
- :component-props="resolvedComponentProps"
392
- :component-props-by-id="resolvedComponentPropsById"
393
- />
394
- </AppLayout>
216
+ <template #default>
217
+ <WorkspaceControlsInternal :controls="controls">
218
+ <template #default="controlsScope">
219
+ <slot v-bind="controlsScope" />
220
+ </template>
221
+ </WorkspaceControlsInternal>
222
+ </template>
223
+ </WorkspaceShellInternal>
395
224
  </template>
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import SecretInput from './SecretInput.vue'
4
+ import type { PluginSecretRef } from '../utils/pluginSecrets'
5
+
6
+ const sizes = ['sm', 'md', 'lg'] as const
7
+
8
+ const storedRef: PluginSecretRef = { $secret: 'sec_0f2a4c6e8a0c2e4a6c8e0a2c4e6a8c0e', set: true }
9
+ const lockedRef: PluginSecretRef = { $secret: null, locked: true }
10
+
11
+ const stored = ref<PluginSecretRef | string | null>({ ...storedRef })
12
+ const unset = ref<PluginSecretRef | string | null>(null)
13
+ const locked = ref<PluginSecretRef | string | null>({ ...lockedRef })
14
+ </script>
15
+
16
+ <template>
17
+ <Story title="Base Inputs/SecretInput">
18
+ <Variant title="Playground">
19
+ <template #default="{ state }">
20
+ <div style="padding: 2rem; max-width: 600px;">
21
+ <SecretInput
22
+ v-model="state.value"
23
+ :placeholder="state.placeholder"
24
+ :disabled="state.disabled"
25
+ :error="state.error"
26
+ :size="state.size"
27
+ />
28
+ <p style="margin-top: 1rem; font-size: 0.875rem; color: var(--text-muted, #94a3b8);">
29
+ Submits: <code>{{ JSON.stringify(state.value) }}</code>
30
+ </p>
31
+ </div>
32
+ </template>
33
+
34
+ <template #controls="{ state }">
35
+ <HstText v-model="state.placeholder" title="Placeholder" />
36
+ <HstSelect
37
+ v-model="state.size"
38
+ title="Size"
39
+ :options="sizes.map(size => ({ label: size, value: size }))"
40
+ />
41
+ <HstCheckbox v-model="state.disabled" title="Disabled" />
42
+ <HstCheckbox v-model="state.error" title="Error" />
43
+ </template>
44
+
45
+ <template #initState>
46
+ {{ {
47
+ value: { $secret: 'sec_0f2a4c6e8a0c2e4a6c8e0a2c4e6a8c0e', set: true },
48
+ placeholder: 'Enter a new value',
49
+ disabled: false,
50
+ error: false,
51
+ size: 'md',
52
+ } }}
53
+ </template>
54
+ </Variant>
55
+
56
+ <Variant title="States">
57
+ <div style="padding: 2rem; max-width: 600px; display: flex; flex-direction: column; gap: 1.5rem;">
58
+ <div>
59
+ <p style="margin: 0 0 0.5rem; font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;">
60
+ Stored
61
+ </p>
62
+ <SecretInput v-model="stored" />
63
+ </div>
64
+ <div>
65
+ <p style="margin: 0 0 0.5rem; font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;">
66
+ Not set
67
+ </p>
68
+ <SecretInput v-model="unset" />
69
+ </div>
70
+ <div>
71
+ <p style="margin: 0 0 0.5rem; font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;">
72
+ Provided by environment
73
+ </p>
74
+ <SecretInput v-model="locked" />
75
+ </div>
76
+ </div>
77
+ </Variant>
78
+ </Story>
79
+ </template>
@@ -0,0 +1,150 @@
1
+ <script setup lang="ts">
2
+ /** Edits one plugin secret setting: shows set / not set / provided by environment, and submits a `$secret` reference, a new value, or `null`. */
3
+ import { computed, ref, watch } from 'vue'
4
+ import BaseInput from './BaseInput.vue'
5
+ import BaseButton from './BaseButton.vue'
6
+ import {
7
+ clearPluginSecret,
8
+ isPluginSecretLocked,
9
+ isPluginSecretRef,
10
+ keepPluginSecret,
11
+ pluginSecretLabel,
12
+ pluginSecretState,
13
+ setPluginSecret,
14
+ type PluginSecretRef,
15
+ type PluginSecretState,
16
+ } from '../utils/pluginSecrets'
17
+
18
+ type SecretValue = PluginSecretRef | string | null
19
+
20
+ interface Props {
21
+ /** The `$secret` marker from the settings response, a typed string, or `null`. */
22
+ modelValue?: SecretValue
23
+ placeholder?: string
24
+ disabled?: boolean
25
+ error?: boolean
26
+ size?: 'sm' | 'md' | 'lg'
27
+ /** Label on the control that starts entering a replacement value. */
28
+ changeLabel?: string
29
+ /** Label on the control that clears a stored secret. */
30
+ clearLabel?: string
31
+ }
32
+
33
+ const props = withDefaults(defineProps<Props>(), {
34
+ modelValue: null,
35
+ placeholder: 'Enter a new value',
36
+ disabled: false,
37
+ error: false,
38
+ size: 'md',
39
+ changeLabel: 'Change',
40
+ clearLabel: 'Clear',
41
+ })
42
+
43
+ const emit = defineEmits<{
44
+ 'update:modelValue': [value: SecretValue]
45
+ }>()
46
+
47
+ /** The reference this field started from, so an abandoned edit can restore it. */
48
+ const storedReference = ref<PluginSecretRef | null>(keepPluginSecret(props.modelValue))
49
+ const isEditing = ref(typeof props.modelValue === 'string')
50
+
51
+ watch(
52
+ () => props.modelValue,
53
+ (value) => {
54
+ if (isPluginSecretRef(value)) {
55
+ storedReference.value = keepPluginSecret(value)
56
+ isEditing.value = false
57
+ }
58
+ },
59
+ )
60
+
61
+ const state = computed<PluginSecretState>(() => pluginSecretState(props.modelValue))
62
+ const locked = computed(() => isPluginSecretLocked(props.modelValue)
63
+ || isPluginSecretLocked(storedReference.value))
64
+ const statusLabel = computed(() => isEditing.value
65
+ ? 'New value'
66
+ : pluginSecretLabel(props.modelValue))
67
+ const typedValue = computed(() => typeof props.modelValue === 'string' ? props.modelValue : '')
68
+ const canClear = computed(() => !locked.value && (state.value === 'set' || isEditing.value))
69
+
70
+ function startEditing(): void {
71
+ if (props.disabled || locked.value) return
72
+ isEditing.value = true
73
+ emit('update:modelValue', setPluginSecret(''))
74
+ }
75
+
76
+ function updateTyped(value: string | number | null): void {
77
+ emit('update:modelValue', setPluginSecret(value == null ? '' : String(value)))
78
+ }
79
+
80
+ function cancelEditing(): void {
81
+ isEditing.value = false
82
+ emit('update:modelValue', storedReference.value ? { ...storedReference.value } : null)
83
+ }
84
+
85
+ function clear(): void {
86
+ if (props.disabled || locked.value) return
87
+ isEditing.value = false
88
+ storedReference.value = null
89
+ emit('update:modelValue', clearPluginSecret())
90
+ }
91
+ </script>
92
+
93
+ <template>
94
+ <div class="mint-secret-input" :class="{ 'mint-secret-input--error': error }">
95
+ <div class="mint-secret-input__status">
96
+ <span
97
+ class="mint-secret-input__badge"
98
+ :class="`mint-secret-input__badge--${isEditing ? 'pending' : state}`"
99
+ >{{ statusLabel }}</span>
100
+ <span v-if="locked" class="mint-secret-input__hint">
101
+ This value comes from the environment and cannot be edited here.
102
+ </span>
103
+ </div>
104
+
105
+ <BaseInput
106
+ v-if="isEditing"
107
+ type="password"
108
+ autocomplete="new-password"
109
+ :model-value="typedValue"
110
+ :placeholder="placeholder"
111
+ :disabled="disabled"
112
+ :error="error"
113
+ :size="size"
114
+ @update:model-value="updateTyped"
115
+ />
116
+
117
+ <div class="mint-secret-input__actions">
118
+ <BaseButton
119
+ v-if="!isEditing"
120
+ type="button"
121
+ variant="secondary"
122
+ :size="size === 'lg' ? 'md' : 'sm'"
123
+ :disabled="disabled || locked"
124
+ @click="startEditing"
125
+ >
126
+ {{ changeLabel }}
127
+ </BaseButton>
128
+ <BaseButton
129
+ v-else
130
+ type="button"
131
+ variant="ghost"
132
+ :size="size === 'lg' ? 'md' : 'sm'"
133
+ :disabled="disabled"
134
+ @click="cancelEditing"
135
+ >
136
+ Cancel
137
+ </BaseButton>
138
+ <BaseButton
139
+ v-if="canClear"
140
+ type="button"
141
+ variant="ghost"
142
+ :size="size === 'lg' ? 'md' : 'sm'"
143
+ :disabled="disabled"
144
+ @click="clear"
145
+ >
146
+ {{ clearLabel }}
147
+ </BaseButton>
148
+ </div>
149
+ </div>
150
+ </template>
@@ -7,12 +7,14 @@ export { default as BaseCheckbox } from './BaseCheckbox.vue'
7
7
  export { default as BaseToggle } from './BaseToggle.vue'
8
8
  export { default as BaseRadioGroup } from './BaseRadioGroup.vue'
9
9
  export { default as BaseSlider } from './BaseSlider.vue'
10
+ /** Range slider with a gradient color track and color-coded value badge for threshold visualization. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; use `BaseSlider` instead. */
10
11
  export { default as ColorSlider } from './ColorSlider.vue'
11
12
  export { default as BaseTabs } from './BaseTabs.vue'
12
13
  export { default as BaseModal } from './BaseModal.vue'
13
14
  export { default as SegmentedControl } from './SegmentedControl.vue'
14
15
  export { default as MultiSelect } from './MultiSelect.vue'
15
16
  export { default as BasePill } from './BasePill.vue'
17
+ /** Button that opens a dropdown menu for selecting one option from a list. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; use `ActionMenu` instead. */
16
18
  export { default as DropdownButton } from './DropdownButton.vue'
17
19
  export { default as Calendar } from './Calendar.vue'
18
20
  export { default as DataFrame } from './DataFrame.vue'
@@ -22,6 +24,7 @@ export { default as FormField } from './FormField.vue'
22
24
  export { default as DatePicker } from './DatePicker.vue'
23
25
  export { default as TimePicker } from './TimePicker.vue'
24
26
  export { default as TagsInput } from './TagsInput.vue'
27
+ export { default as SecretInput } from './SecretInput.vue'
25
28
  export { default as NumberInput } from './NumberInput.vue'
26
29
  export { default as FileUploader } from './FileUploader.vue'
27
30
 
@@ -97,11 +100,15 @@ export { default as ProtocolStepEditor } from './ProtocolStepEditor.vue'
97
100
 
98
101
  // Scientific display components
99
102
  export { default as ScientificNumber } from './ScientificNumber.vue'
103
+ /** Renders a chemical formula string (H₂O, C₆H₁₂O₆, Ca(OH)₂) with typographically correct subscripts and superscripts for charges. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; no platform replacement is planned. */
100
104
  export { default as ChemicalFormula } from './ChemicalFormula.vue'
101
105
  export { default as SequenceProgressBar } from './SequenceProgressBar.vue'
106
+ /** Filterable instrument alert/event list keyed by each alert's required stable event_key. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; instrument UI moves to the mld-ms plugins. */
102
107
  export { default as InstrumentAlertLog } from './InstrumentAlertLog.vue'
103
108
  export { default as InstrumentStateBadge } from './InstrumentStateBadge.vue'
109
+ /** Compact card for live instrument monitor status, sample, method, sequence progress, and ETA. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; instrument UI moves to the mld-ms plugins. */
104
110
  export { default as InstrumentStatusCard } from './InstrumentStatusCard.vue'
111
+ /** Table for Xcalibur-compatible LCMS sequence rows with optional reorder/remove/duplicate controls. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; instrument UI moves to the mld-ms plugins. */
105
112
  export { default as LcmsSequenceTable } from './LcmsSequenceTable.vue'
106
113
 
107
114
  // Scientific input components
@@ -125,8 +132,10 @@ export { default as ExperimentCodeBadge } from './ExperimentCodeBadge.vue'
125
132
 
126
133
  // Scheduling / booking components
127
134
  export { default as DateTimePicker } from './DateTimePicker.vue'
135
+ /** Paired start/end time pickers that validate range order and display computed duration. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; use `TimePicker` instead. */
128
136
  export { default as TimeRangeInput } from './TimeRangeInput.vue'
129
137
  export { default as ScheduleCalendar } from './ScheduleCalendar.vue'
138
+ /** Card displaying a lab resource with availability status, specs, tags, and a book action. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; no platform replacement is planned. */
130
139
  export { default as ResourceCard } from './ResourceCard.vue'
131
140
  export { default as SectionCard } from './SectionCard.vue'
132
141
 
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch, computed } from 'vue'
2
+ import { onBeforeUnmount, onMounted, ref, watch, computed } from 'vue'
3
3
  import type { Well, WellEditData, WellEditField } from '../../types'
4
4
  import { useEventListener } from '../../composables/useEventListener'
5
+ import { useFocusTrap } from '../../composables/useFocusTrap'
5
6
 
6
7
  interface Props {
7
8
  wellId: string
@@ -86,6 +87,22 @@ function handleKeydown(e: KeyboardEvent) {
86
87
 
87
88
  useEventListener(() => document, 'keydown', handleKeydown)
88
89
 
90
+ // The popup has no open/close prop of its own — the parent mounts and unmounts it —
91
+ // so the trap activates on mount and releases (restoring focus) just before unmount,
92
+ // the same lifecycle BaseModal drives off `modelValue`.
93
+ const containerRef = ref<HTMLElement | null>(null)
94
+ const trapActive = ref(false)
95
+
96
+ useFocusTrap({ containerRef, active: trapActive })
97
+
98
+ onMounted(() => {
99
+ trapActive.value = true
100
+ })
101
+
102
+ onBeforeUnmount(() => {
103
+ trapActive.value = false
104
+ })
105
+
89
106
  const sampleTypeButtons = [
90
107
  { type: 'sample', label: 'S', tooltip: 'Sample' },
91
108
  { type: 'blank', label: 'B', tooltip: 'Blank' },
@@ -101,8 +118,10 @@ const sampleTypeButtons = [
101
118
 
102
119
  <!-- Popup -->
103
120
  <div
121
+ ref="containerRef"
104
122
  class="mint-well-edit-popup"
105
123
  :style="popupStyle"
124
+ tabindex="-1"
106
125
  @click.stop
107
126
  >
108
127
  <!-- Header -->