@morscherlab/mint-sdk 1.2.0-beta.4 → 1.2.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 (136) 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/components/AppAvatarMenu.vue +34 -3
  80. package/src/components/BaseModal.vue +8 -56
  81. package/src/components/BaseTabs.vue +26 -1
  82. package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
  83. package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
  84. package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
  85. package/src/components/ComponentBindingRenderer.vue +1 -3
  86. package/src/components/ControlWorkspaceView.vue +11 -22
  87. package/src/components/DoseDesignWorkspaceView.vue +6 -25
  88. package/src/components/DropdownButton.vue +32 -2
  89. package/src/components/PluginWorkspaceView.props.ts +38 -0
  90. package/src/components/PluginWorkspaceView.vue +53 -224
  91. package/src/components/SecretInput.story.vue +79 -0
  92. package/src/components/SecretInput.vue +150 -0
  93. package/src/components/index.ts +9 -0
  94. package/src/components/internal/WellEditPopupInternal.vue +20 -1
  95. package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
  96. package/src/components/internal/WorkspaceShellInternal.vue +125 -0
  97. package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
  98. package/src/composables/experimentDesignData.ts +1 -3
  99. package/src/composables/flattenWorkspaceBindings.ts +104 -0
  100. package/src/composables/formBuilderRegistry.ts +5 -0
  101. package/src/composables/index.ts +26 -0
  102. package/src/composables/pluginWorkspaceModel.ts +182 -0
  103. package/src/composables/useAsync.ts +5 -1
  104. package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
  105. package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
  106. package/src/composables/useBioTemplateWorkspace.ts +18 -43
  107. package/src/composables/useCommandHistory.ts +5 -1
  108. package/src/composables/useFocusTrap.ts +91 -0
  109. package/src/composables/useMenuKeyboard.ts +90 -0
  110. package/src/composables/useOptimisticMutation.ts +5 -1
  111. package/src/composables/usePluginClient.ts +42 -4
  112. package/src/composables/usePluginWorkspace.ts +208 -0
  113. package/src/composables/useResourceCrud.ts +5 -1
  114. package/src/composables/useRovingFocus.ts +66 -0
  115. package/src/composables/useWellPainting.ts +5 -1
  116. package/src/composables/useWellPlateAdapter.ts +5 -1
  117. package/src/composables/useWellPlateValidation.ts +5 -1
  118. package/src/composables/workspaceRecipes.ts +267 -0
  119. package/src/index.ts +16 -0
  120. package/src/styles/components/secret-input.css +60 -0
  121. package/src/styles/index.css +1 -0
  122. package/src/templates/builderReadUtils.ts +1 -3
  123. package/src/templates/componentTargetResolvers.ts +1 -3
  124. package/src/templates/controlSchemaTargets.ts +1 -3
  125. package/src/types/form-builder.ts +1 -0
  126. package/src/utils/formModelSync.ts +1 -3
  127. package/src/utils/pluginIcon.ts +1 -3
  128. package/src/utils/pluginSecrets.ts +102 -0
  129. package/src/utils/records.ts +9 -0
  130. package/dist/SettingsModal-CkNM8qAI.js.map +0 -1
  131. package/dist/components-MGNL6tRn.js.map +0 -1
  132. package/dist/composables-CKxOcvgh.js.map +0 -1
  133. package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
  134. package/dist/templates-BTJ9C9Kb.js.map +0 -1
  135. package/dist/useControlSchema-CXhxTul7.js.map +0 -1
  136. package/dist/useProtocolTemplates-BWhQIWV5.js.map +0 -1
@@ -14,6 +14,7 @@ import type {
14
14
  UseBioTemplatePresetWorkspaceReturn,
15
15
  } from '../composables/useBioTemplatePresetWorkspace'
16
16
  import { useBioTemplatePresetWorkspace } from '../composables/useBioTemplatePresetWorkspace'
17
+ import { bioTemplatePresetWorkspaceProps } from '../composables/workspaceRecipes'
17
18
  import AlertBox from './AlertBox.vue'
18
19
  import AppSidebar from './AppSidebar.vue'
19
20
  import BaseButton from './BaseButton.vue'
@@ -96,19 +97,13 @@ const props = withDefaults(defineProps<Props>(), {
96
97
  resetLabel: 'Reset defaults',
97
98
  })
98
99
 
99
- const externalValues = computed(() => props.modelValue ?? props.values)
100
- const resolvedWorkspaceOptions = computed<UseBioTemplatePresetWorkspaceOptions>(() => {
101
- if (props.initialValues === undefined && externalValues.value === undefined) return props.workspaceOptions
102
-
103
- return {
104
- ...props.workspaceOptions,
105
- initialValues: {
106
- ...(props.workspaceOptions.initialValues ?? {}),
107
- ...(props.initialValues ?? {}),
108
- ...(externalValues.value ?? {}),
109
- },
110
- }
111
- })
100
+ // Thin recipe over bioTemplatePresetWorkspaceProps(): merges workspaceOptions/initialValues/
101
+ // v-model into the options useBioTemplatePresetWorkspace() consumes (see workspaceRecipes.ts).
102
+ const resolvedPresetProps = computed(() => bioTemplatePresetWorkspaceProps(props))
103
+ const externalValues = computed(() => resolvedPresetProps.value.externalValues)
104
+ const resolvedWorkspaceOptions = computed<UseBioTemplatePresetWorkspaceOptions>(() =>
105
+ resolvedPresetProps.value.resolvedWorkspaceOptions
106
+ )
112
107
 
113
108
  let generatedWorkspaceScope: EffectScope | undefined
114
109
 
@@ -16,6 +16,7 @@ import ReagentList from './ReagentList.vue'
16
16
  import SampleSelector from './SampleSelector.vue'
17
17
  import ScheduleCalendar from './ScheduleCalendar.vue'
18
18
  import WellPlate from './WellPlate.vue'
19
+ import { isRecord } from '../utils/records'
19
20
 
20
21
  export interface ComponentBindingRendererBinding {
21
22
  id?: string
@@ -173,9 +174,6 @@ function normalizeProps(binding: ComponentBindingRendererBinding): Record<string
173
174
  }
174
175
  }
175
176
 
176
- function isRecord(value: unknown): value is Record<string, unknown> {
177
- return typeof value === 'object' && value !== null && !Array.isArray(value)
178
- }
179
177
  </script>
180
178
 
181
179
  <template>
@@ -20,7 +20,8 @@ import type {
20
20
  ControlWorkspaceOptions,
21
21
  UseControlWorkspaceReturn,
22
22
  } from '../composables/useControlSchema'
23
- import { mergeControlWorkspaceOptions, resolveControlModel, useControlWorkspace } from '../composables/useControlSchema'
23
+ import { useControlWorkspace } from '../composables/useControlSchema'
24
+ import { controlWorkspaceProps } from '../composables/workspaceRecipes'
24
25
  import AppLayout from './AppLayout.vue'
25
26
  import AppSidebar from './AppSidebar.vue'
26
27
  import AppTopBar from './AppTopBar.vue'
@@ -161,27 +162,15 @@ const props = withDefaults(defineProps<Props>(), {
161
162
  formSize: 'md',
162
163
  })
163
164
 
164
- const externalValues = computed(() => props.modelValue ?? props.values)
165
- const resolvedModel = computed<ControlModelBinding | undefined>(() => {
166
- return resolveControlModel(props.model)
167
- })
168
- const resolvedControls = computed<ControlSchema>(() => props.controls ?? resolvedModel.value?.controls ?? {})
169
- const baseControlOptions = computed<ControlWorkspaceOptions>(() =>
170
- mergeControlWorkspaceOptions(resolvedModel.value?.controlOptions ?? {}, props.controlOptions)
171
- )
172
-
173
- const resolvedControlOptions = computed<ControlWorkspaceOptions>(() => {
174
- if (props.initialValues === undefined && externalValues.value === undefined) return baseControlOptions.value
175
-
176
- return {
177
- ...baseControlOptions.value,
178
- initialValues: {
179
- ...(baseControlOptions.value.initialValues ?? {}),
180
- ...(props.initialValues ?? {}),
181
- ...(externalValues.value ?? {}),
182
- },
183
- }
184
- })
165
+ // Thin recipe over controlWorkspaceProps(): resolves model/controls/controlOptions/v-model
166
+ // into the schema + options pair useControlWorkspace() consumes, shared with
167
+ // DoseDesignWorkspaceView (which forwards its own props here) and
168
+ // BioTemplatePresetWorkspaceView's equivalent options merge.
169
+ const resolvedWorkspaceModel = computed(() => controlWorkspaceProps(props))
170
+ const externalValues = computed(() => resolvedWorkspaceModel.value.externalValues)
171
+ const resolvedModel = computed(() => resolvedWorkspaceModel.value.resolvedModel)
172
+ const resolvedControls = computed(() => resolvedWorkspaceModel.value.resolvedControls)
173
+ const resolvedControlOptions = computed(() => resolvedWorkspaceModel.value.resolvedControlOptions)
185
174
 
186
175
  let generatedWorkspaceScope: EffectScope | undefined
187
176
 
@@ -19,6 +19,7 @@ import type {
19
19
  UseControlWorkspaceReturn,
20
20
  } from '../composables/useControlSchema'
21
21
  import { defineDoseDesignControlModel } from '../composables/useControlSchema'
22
+ import { doseDesignWorkspaceProps } from '../composables/workspaceRecipes'
22
23
  import ControlWorkspaceView from './ControlWorkspaceView.vue'
23
24
  import DoseCalculator from './DoseCalculator.vue'
24
25
  import WellPlate from './WellPlate.vue'
@@ -158,6 +159,10 @@ const resolvedModel = computed<ControlModel | ControlModelBinding>(() =>
158
159
  props.model ?? defineDoseDesignControlModel(props.doseDesignOptions)
159
160
  )
160
161
 
162
+ // Thin recipe: forwards the ~25 props this view shares verbatim with ControlWorkspaceView
163
+ // (see workspaceRecipes.ts) instead of spelling each one out in the template.
164
+ const controlWorkspaceRecipe = computed(() => doseDesignWorkspaceProps(props, resolvedModel.value))
165
+
161
166
  function resolvedPlateId(): string {
162
167
  return props.plateId ?? props.doseDesignOptions.componentProps?.plateId ?? 'plate'
163
168
  }
@@ -200,31 +205,7 @@ function slotProps(slotProps: {
200
205
 
201
206
  <template>
202
207
  <ControlWorkspaceView
203
- :model="resolvedModel"
204
- :workspace="workspace"
205
- :control-options="controlOptions"
206
- :initial-values="initialValues"
207
- :model-value="modelValue"
208
- :values="values"
209
- :title="title"
210
- :subtitle="subtitle"
211
- :top-bar-variant="topBarVariant"
212
- :sidebar-width="sidebarWidth"
213
- :sidebar-variant="sidebarVariant"
214
- :responsive-sidebar="responsiveSidebar"
215
- :sidebar-position="sidebarPosition"
216
- :sidebar-title="sidebarTitle"
217
- :sidebar-subtitle="sidebarSubtitle"
218
- :sidebar-badge="sidebarBadge"
219
- :floating="floating"
220
- :dense="dense"
221
- :show-settings="showSettings"
222
- :show-form-actions="showFormActions"
223
- :form-enhancements="formEnhancements"
224
- :form-loading="formLoading"
225
- :form-disabled="formDisabled"
226
- :form-readonly="formReadonly"
227
- :form-size="formSize"
208
+ v-bind="controlWorkspaceRecipe"
228
209
  @update:model-value="emit('update:modelValue', $event)"
229
210
  @update:values="emit('update:values', $event)"
230
211
  @submit="emit('submit', $event)"
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  /** Button that opens a dropdown menu for selecting one option from a list. */
3
- import { computed } from 'vue'
3
+ import { computed, nextTick, ref } from 'vue'
4
4
  import { useDropdownState } from '../composables/useDropdownState'
5
+ import { useMenuKeyboard } from '../composables/useMenuKeyboard'
5
6
  import type { SelectOption, SelectOptionInput, ButtonVariant, ButtonSize } from '../types'
6
7
  import { normalizeOptionInput } from '../utils/options'
7
8
 
@@ -53,11 +54,33 @@ const emit = defineEmits<{
53
54
  select: [option: SelectOption<string | number>]
54
55
  }>()
55
56
 
56
- const { isOpen, rootRef, close, toggle: toggleDropdown } = useDropdownState()
57
+ const { isOpen, rootRef, open, close: closeDropdown, toggle: toggleDropdown } = useDropdownState()
57
58
  const normalizedOptions = computed<SelectOption<string | number>[]>(() =>
58
59
  props.options.map(normalizeOptionInput)
59
60
  )
60
61
 
62
+ const triggerRef = ref<HTMLElement | null>(null)
63
+ const panelRef = ref<HTMLElement | null>(null)
64
+
65
+ function optionItems(): HTMLElement[] {
66
+ if (!panelRef.value) return []
67
+ return Array.from(panelRef.value.querySelectorAll<HTMLElement>('[role="option"]:not([aria-disabled="true"])'))
68
+ }
69
+
70
+ function close(returnFocus = false) {
71
+ closeDropdown()
72
+ if (returnFocus) {
73
+ void nextTick(() => triggerRef.value?.focus())
74
+ }
75
+ }
76
+
77
+ const { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({
78
+ items: optionItems,
79
+ isOpen: () => isOpen.value,
80
+ open,
81
+ close,
82
+ })
83
+
61
84
  const selectedLabel = computed(() => {
62
85
  if (props.modelValue === undefined) return props.placeholder ?? 'Select...'
63
86
  const found = normalizedOptions.value.find(o => o.value === props.modelValue)
@@ -80,6 +103,7 @@ function selectOption(option: SelectOption<string | number>) {
80
103
  <template>
81
104
  <div ref="rootRef" class="mint-dropdown-button">
82
105
  <button
106
+ ref="triggerRef"
83
107
  type="button"
84
108
  :disabled="disabled || loading"
85
109
  :class="[
@@ -94,6 +118,7 @@ function selectOption(option: SelectOption<string | number>) {
94
118
  :aria-expanded="isOpen"
95
119
  aria-haspopup="listbox"
96
120
  @click="toggle"
121
+ @keydown="handleTriggerKeydown"
97
122
  >
98
123
  <svg v-if="loading" class="mint-dropdown-button__spinner" viewBox="0 0 24 24" fill="none" aria-hidden="true">
99
124
  <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25" />
@@ -116,15 +141,18 @@ function selectOption(option: SelectOption<string | number>) {
116
141
  <Transition name="mint-dropdown-button-menu">
117
142
  <ul
118
143
  v-if="isOpen"
144
+ ref="panelRef"
119
145
  class="mint-dropdown-button__menu"
120
146
  role="listbox"
121
147
  :aria-activedescendant="modelValue !== undefined ? `mint-dropdown-option-${modelValue}` : undefined"
148
+ @keydown="handlePanelKeydown"
122
149
  >
123
150
  <li
124
151
  v-for="option in normalizedOptions"
125
152
  :id="`mint-dropdown-option-${option.value}`"
126
153
  :key="String(option.value)"
127
154
  role="option"
155
+ :tabindex="-1"
128
156
  :aria-selected="option.value === modelValue"
129
157
  :aria-disabled="option.disabled"
130
158
  :class="[
@@ -135,6 +163,8 @@ function selectOption(option: SelectOption<string | number>) {
135
163
  },
136
164
  ]"
137
165
  @click="selectOption(option)"
166
+ @keydown.enter.prevent="selectOption(option)"
167
+ @keydown.space.prevent="selectOption(option)"
138
168
  >
139
169
  <span class="mint-dropdown-button__option-label">{{ option.label }}</span>
140
170
  <span v-if="option.description" class="mint-dropdown-button__option-description">
@@ -1,8 +1,13 @@
1
1
  import type {
2
2
  AccountMenuItem,
3
+ ExperimentSummary,
4
+ PageSelectorItem,
3
5
  PageSelectorItemInput,
6
+ PillNavItem,
4
7
  PillNavItemInput,
8
+ PillNavOption,
5
9
  PluginSwitcherInfo,
10
+ PluginSwitcherPlugin,
6
11
  SidebarToolSection,
7
12
  TopBarSettingsConfig,
8
13
  TopBarVariant,
@@ -156,6 +161,39 @@ export interface PluginWorkspaceViewProps {
156
161
  sidebarCloseLabel?: string
157
162
  }
158
163
 
164
+ /**
165
+ * Event map for PluginWorkspaceView, shared with the {@link usePluginWorkspace} composable
166
+ * so its internal pieces can emit through the exact same typed contract as the public component.
167
+ */
168
+ export interface PluginWorkspaceViewEmits {
169
+ 'update:activeView': [value: string]
170
+ 'page-selector-select': [page: PageSelectorItem]
171
+ 'pill-select': [item: PillNavItem]
172
+ 'pill-option-select': [option: PillNavOption, item: PillNavItem]
173
+ 'plugin-switcher-select': [plugin: PluginSwitcherPlugin]
174
+ 'plugin-switcher-install': []
175
+ 'account-menu-select': [item: AccountMenuItem]
176
+ 'sign-out': []
177
+ 'notifications-click': []
178
+ 'settings-values-change': [data: Record<string, unknown>]
179
+ 'admin-click': []
180
+ 'experiment-select': [experiment: ExperimentSummary]
181
+ 'experiment-detach': []
182
+ 'update:modelValue': [values: Record<string, unknown>]
183
+ 'update:values': [values: Record<string, unknown>]
184
+ 'update:sidebarCollapsed': [value: boolean]
185
+ 'update:toggle': [sectionId: string, value: boolean]
186
+ 'section-action': [sectionId: string, actionId: string]
187
+ 'form-submit': [sectionId: string, values: Record<string, unknown>]
188
+ 'form-cancel': [sectionId: string]
189
+ }
190
+
191
+ /** Typed `emit` function shape produced by `defineEmits<PluginWorkspaceViewEmits>()`. */
192
+ export type PluginWorkspaceViewEmit = <K extends keyof PluginWorkspaceViewEmits>(
193
+ event: K,
194
+ ...args: PluginWorkspaceViewEmits[K]
195
+ ) => void
196
+
159
197
  export interface PluginWorkspaceSlotProps {
160
198
  activeView: string
161
199
  setActiveView: (viewId: string) => void
@@ -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>