@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
@@ -3,7 +3,8 @@
3
3
  * AppLayout - Page layout shell with topbar, sidebar, and main content slots
4
4
  *
5
5
  * Provides a responsive application layout structure with optional topbar and sidebar.
6
- * The sidebar slot is a simple pass-through; visibility is controlled by AppSidebar itself.
6
+ * The sidebar slot is a simple pass-through on desktop. When responsiveSidebar is
7
+ * enabled, AppLayout owns the mobile toggle/backdrop shell around the sidebar.
7
8
  *
8
9
  * @example
9
10
  * ```vue
@@ -20,7 +21,7 @@
20
21
  * </AppLayout>
21
22
  * ```
22
23
  */
23
- import { computed } from 'vue'
24
+ import { computed, ref } from 'vue'
24
25
 
25
26
  interface Props {
26
27
  /** Position of sidebar (left or right side of screen) */
@@ -29,23 +30,61 @@ interface Props {
29
30
  sidebarWidth?: string
30
31
  /** When true, topbar/sidebar/main render as floating cards with gaps */
31
32
  floating?: boolean
33
+ /** Convert the sidebar into a mobile overlay with built-in toggle and backdrop below 1024px. */
34
+ responsiveSidebar?: boolean
35
+ /** Controlled mobile sidebar open state. Desktop sidebar remains visible. */
36
+ sidebarOpen?: boolean
37
+ /** Initial mobile sidebar open state when sidebarOpen is uncontrolled. */
38
+ defaultSidebarOpen?: boolean
39
+ /** Accessible label for the mobile sidebar toggle. */
40
+ sidebarToggleLabel?: string
41
+ /** Accessible label used when the mobile sidebar is open. */
42
+ sidebarCloseLabel?: string
32
43
  }
33
44
 
34
45
  const props = withDefaults(defineProps<Props>(), {
35
46
  sidebarPosition: 'left',
36
47
  sidebarWidth: 'auto',
37
48
  floating: false,
49
+ responsiveSidebar: false,
50
+ sidebarOpen: undefined,
51
+ defaultSidebarOpen: false,
52
+ sidebarToggleLabel: 'Open sidebar',
53
+ sidebarCloseLabel: 'Close sidebar',
54
+ })
55
+
56
+ const emit = defineEmits<{
57
+ 'update:sidebarOpen': [value: boolean]
58
+ }>()
59
+
60
+ const internalSidebarOpen = ref(props.defaultSidebarOpen)
61
+ const sidebarOpenModel = computed({
62
+ get: () => props.sidebarOpen ?? internalSidebarOpen.value,
63
+ set: (value: boolean) => {
64
+ internalSidebarOpen.value = value
65
+ emit('update:sidebarOpen', value)
66
+ },
38
67
  })
39
68
 
40
69
  const layoutClasses = computed(() => [
41
70
  'mint-layout',
42
71
  props.sidebarPosition === 'right' ? 'mint-layout--sidebar-right' : '',
43
72
  props.floating ? 'mint-layout--floating' : '',
73
+ props.responsiveSidebar ? 'mint-layout--responsive-sidebar' : '',
74
+ sidebarOpenModel.value ? 'mint-layout--sidebar-open' : '',
44
75
  ])
45
76
 
46
77
  const sidebarStyle = computed(() => {
47
78
  return props.sidebarWidth !== 'auto' ? { width: props.sidebarWidth } : undefined
48
79
  })
80
+
81
+ function toggleSidebar() {
82
+ sidebarOpenModel.value = !sidebarOpenModel.value
83
+ }
84
+
85
+ function closeSidebar() {
86
+ sidebarOpenModel.value = false
87
+ }
49
88
  </script>
50
89
 
51
90
  <template>
@@ -55,6 +94,48 @@ const sidebarStyle = computed(() => {
55
94
  </div>
56
95
 
57
96
  <div class="mint-layout__body">
97
+ <button
98
+ v-if="responsiveSidebar && $slots.sidebar"
99
+ type="button"
100
+ class="mint-layout__sidebar-toggle"
101
+ :aria-label="sidebarOpenModel ? sidebarCloseLabel : sidebarToggleLabel"
102
+ :aria-expanded="sidebarOpenModel"
103
+ @click="toggleSidebar"
104
+ >
105
+ <svg
106
+ v-if="!sidebarOpenModel"
107
+ class="mint-layout__sidebar-toggle-icon"
108
+ viewBox="0 0 24 24"
109
+ fill="none"
110
+ stroke="currentColor"
111
+ stroke-width="2"
112
+ stroke-linecap="round"
113
+ stroke-linejoin="round"
114
+ aria-hidden="true"
115
+ >
116
+ <path d="M4 6h16M4 12h16M4 18h16" />
117
+ </svg>
118
+ <svg
119
+ v-else
120
+ class="mint-layout__sidebar-toggle-icon"
121
+ viewBox="0 0 24 24"
122
+ fill="none"
123
+ stroke="currentColor"
124
+ stroke-width="2"
125
+ stroke-linecap="round"
126
+ stroke-linejoin="round"
127
+ aria-hidden="true"
128
+ >
129
+ <path d="M6 18 18 6M6 6l12 12" />
130
+ </svg>
131
+ </button>
132
+
133
+ <div
134
+ v-if="responsiveSidebar && $slots.sidebar && sidebarOpenModel"
135
+ class="mint-layout__sidebar-backdrop"
136
+ @click="closeSidebar"
137
+ />
138
+
58
139
  <div
59
140
  v-if="$slots.sidebar"
60
141
  class="mint-layout__sidebar"
@@ -2,7 +2,7 @@
2
2
  /** Dropdown menu for switching between installed plugins with color swatches, version badges, and an install link. */
3
3
  import { useDropdownState } from '../composables/useDropdownState'
4
4
  import type { PluginSwitcherPlugin } from '../types/components'
5
- import ActionItem from './ActionItem.vue'
5
+ import ActionItemInternal from './internal/ActionItemInternal.vue'
6
6
 
7
7
  interface Props {
8
8
  current: PluginSwitcherPlugin
@@ -74,7 +74,7 @@ function handleInstall() {
74
74
 
75
75
  <div v-if="isOpen" class="mint-plugin-switcher__menu" role="menu">
76
76
  <div class="mint-plugin-switcher__menu-title">Switch plugin</div>
77
- <ActionItem
77
+ <ActionItemInternal
78
78
  v-for="plugin in plugins"
79
79
  :key="plugin.id"
80
80
  :href="plugin.href"
@@ -107,13 +107,13 @@ function handleInstall() {
107
107
  >
108
108
  <path d="M5 13l4 4L19 7" />
109
109
  </svg>
110
- </ActionItem>
110
+ </ActionItemInternal>
111
111
 
112
112
  <template v-if="plugins.length && (installHref || installTo || $slots.install)">
113
113
  <div class="mint-plugin-switcher__divider" role="separator" />
114
114
  </template>
115
115
  <slot name="install">
116
- <ActionItem
116
+ <ActionItemInternal
117
117
  v-if="installHref || installTo"
118
118
  :href="installHref"
119
119
  :to="installTo"
@@ -126,7 +126,7 @@ function handleInstall() {
126
126
  <line x1="5" y1="12" x2="19" y2="12" />
127
127
  </svg>
128
128
  <span>{{ installLabel }}</span>
129
- </ActionItem>
129
+ </ActionItemInternal>
130
130
  </slot>
131
131
  </div>
132
132
  </div>
@@ -10,11 +10,13 @@ import NumberInput from './NumberInput.vue'
10
10
  import FileUploader from './FileUploader.vue'
11
11
  import FormField from './FormField.vue'
12
12
  import WellPlate from './WellPlate.vue'
13
- import { defineControls, getControlDefaults } from '../composables/useControlSchema'
13
+ import { defineControlModel, defineControls, getControlDefaults } from '../composables/useControlSchema'
14
14
  import { useBioTemplateWorkspace } from '../composables/useBioTemplateWorkspace'
15
15
  import { createWellPlateScreenCollection } from '../templates'
16
16
  import type { SidebarToolSection } from '../types'
17
17
 
18
+ type SidebarVariant = 'analysis' | 'default'
19
+
18
20
  /* --- Icon paths (Lucide-style) --- */
19
21
  const icons = {
20
22
  upload: ['M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4', 'M17 8l-5-5-5 5', 'M12 3v12'],
@@ -130,6 +132,13 @@ const polarity = ref('negative')
130
132
  const container = ref('vial')
131
133
  const expNumber = ref(1)
132
134
  const initials = ref('XP')
135
+ const sequencePrefix = ref('exp001_260210_XP')
136
+ const runMode = ref('balanced')
137
+ const runModeOptions = [
138
+ { value: 'fast', label: 'Fast' },
139
+ { value: 'balanced', label: 'Balanced' },
140
+ { value: 'qc', label: 'QC focused' },
141
+ ]
133
142
  const toggleState = reactive<Record<string, boolean>>({ naming: true, randomize: false })
134
143
  const schemaControls = defineControls({
135
144
  threshold: {
@@ -172,13 +181,46 @@ const schemaControls = defineControls({
172
181
  },
173
182
  },
174
183
  })
175
- const controlValues = reactive<Record<string, unknown>>({ ...getControlDefaults(schemaControls) })
184
+ const controlValues = ref<Record<string, unknown>>({ ...getControlDefaults(schemaControls) })
185
+ const modelDrivenSidebarModel = defineControlModel({
186
+ views: {
187
+ analysis: {
188
+ label: 'Analysis',
189
+ sections: {
190
+ parameters: {
191
+ label: 'Parameters',
192
+ description: 'Thresholds and scoring',
193
+ icon: icons.settings,
194
+ controls: {
195
+ threshold: { type: 'number', default: 0.05, min: 0, max: 1 },
196
+ method: ['linear', 'logistic', 'spline'],
197
+ },
198
+ },
199
+ filters: {
200
+ label: 'Filters',
201
+ description: 'Result cleanup',
202
+ sidebar: {
203
+ icon: icons.zap,
204
+ iconColor: '#0ea5e9',
205
+ iconBg: '#e0f2fe',
206
+ defaultOpen: false,
207
+ },
208
+ controls: {
209
+ showOutliers: true,
210
+ minPeakArea: { type: 'number', default: 1000, min: 0 },
211
+ },
212
+ },
213
+ },
214
+ },
215
+ },
216
+ })
217
+ const modelDrivenValues = ref<Record<string, unknown>>({})
176
218
  const templateCollection = createWellPlateScreenCollection({
177
219
  samples: ['Control', 'Treatment'],
178
220
  compounds: { 'Drug A': [10, 1, 0.1] },
179
221
  })
180
222
  const templateWorkspace = useBioTemplateWorkspace(templateCollection)
181
- const templateValues = reactive<Record<string, unknown>>({ ...templateWorkspace.controls.initialValues })
223
+ const templateValues = ref<Record<string, unknown>>({ ...templateWorkspace.controls.initialValues })
182
224
  const templateWellPlateProps = templateWorkspace.componentProps
183
225
  .find(binding => binding.component === 'WellPlate')
184
226
  ?.propsObject ?? {}
@@ -190,8 +232,9 @@ function handleToggle(id: string, value: boolean) {
190
232
  function initSimpleToolkit() {
191
233
  return {
192
234
  activeView: 'analysis',
235
+ variant: 'analysis' as SidebarVariant,
193
236
  floating: false,
194
- width: '260px',
237
+ width: '20rem',
195
238
  side: 'left' as const,
196
239
  }
197
240
  }
@@ -276,14 +319,70 @@ function initSimpleToolkit() {
276
319
  <Variant title="Schema Driven Controls">
277
320
  <div style="padding: 2rem; height: 520px; position: relative; background: var(--bg-primary, #f1f5f9);">
278
321
  <AppSidebar
322
+ variant="analysis"
323
+ title="Peak Picking"
324
+ subtitle="Current experiment"
279
325
  :controls="schemaControls"
280
326
  active-view="analysis"
281
327
  v-model="controlValues"
282
- width="300px"
283
328
  />
284
329
  </div>
285
330
  </Variant>
286
331
 
332
+ <Variant title="Model Driven Analysis">
333
+ <div style="padding: 2rem; height: 560px; position: relative; background: var(--bg-primary, #f1f5f9);">
334
+ <AppSidebar
335
+ v-model="modelDrivenValues"
336
+ variant="analysis"
337
+ title="Peak Picking"
338
+ subtitle="Current experiment"
339
+ :model="modelDrivenSidebarModel"
340
+ />
341
+ </div>
342
+ </Variant>
343
+
344
+ <Variant title="Route Owned Shell">
345
+ <div style="padding: 2rem; height: 560px; display: flex; gap: 1rem; background: var(--bg-primary, #f1f5f9);">
346
+ <AppSidebar
347
+ variant="analysis"
348
+ title="Sequence"
349
+ subtitle="Acquisition run"
350
+ content-id="seqgen-sidebar"
351
+ show-when-empty
352
+ >
353
+ <div style="display: flex; flex-direction: column; gap: 0.75rem;">
354
+ <FormField label="File Prefix">
355
+ <BaseInput v-model="sequencePrefix" size="sm" />
356
+ </FormField>
357
+ <FormField label="Run Mode">
358
+ <BaseSelect v-model="runMode" :options="runModeOptions" size="sm" />
359
+ </FormField>
360
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
361
+ <BaseButton size="sm" variant="secondary">NEG</BaseButton>
362
+ <BaseButton size="sm" variant="secondary">VIAL</BaseButton>
363
+ </div>
364
+ </div>
365
+
366
+ <template #footer>
367
+ <div style="display: flex; flex-direction: column; gap: 0.5rem;">
368
+ <BaseButton variant="cta" style="width: 100%;">Generate Sequence</BaseButton>
369
+ <BaseButton variant="ghost" size="sm" style="width: 100%;">Clear Draft</BaseButton>
370
+ </div>
371
+ </template>
372
+ </AppSidebar>
373
+
374
+ <div style="flex: 1; min-width: 0; border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.5rem; background: var(--bg-card, #fff); padding: 1rem;">
375
+ <div style="display: flex; justify-content: space-between; gap: 1rem; align-items: center;">
376
+ <div>
377
+ <div style="font-size: 0.875rem; font-weight: 650; color: var(--text-primary, #111827);">MS queue</div>
378
+ <div style="font-size: 0.75rem; color: var(--text-muted, #6b7280);">{{ sequencePrefix }} · {{ runMode }}</div>
379
+ </div>
380
+ <BaseButton size="sm" variant="secondary">Preview CSV</BaseButton>
381
+ </div>
382
+ </div>
383
+ </div>
384
+ </Variant>
385
+
287
386
  <Variant title="Template Driven Controls">
288
387
  <div style="padding: 2rem; min-height: 560px; display: flex; gap: 1rem; background: var(--bg-primary, #f1f5f9);">
289
388
  <AppSidebar
@@ -335,6 +434,7 @@ function initSimpleToolkit() {
335
434
  </div>
336
435
  <AppSidebar
337
436
  :panels="simplePanels"
437
+ :variant="state.variant"
338
438
  :active-view="state.activeView"
339
439
  :floating="state.floating"
340
440
  :width="state.width"
@@ -365,6 +465,14 @@ function initSimpleToolkit() {
365
465
  title="Active View"
366
466
  :options="['analysis', 'results', 'settings'].map(v => ({ label: v, value: v }))"
367
467
  />
468
+ <HstSelect
469
+ v-model="state.variant"
470
+ title="Variant"
471
+ :options="[
472
+ { label: 'Analysis', value: 'analysis' },
473
+ { label: 'Default', value: 'default' },
474
+ ]"
475
+ />
368
476
  <HstCheckbox v-model="state.floating" title="Floating" />
369
477
  <HstText v-model="state.width" title="Width" />
370
478
  <HstSelect
@@ -6,7 +6,8 @@
6
6
  * the `panels` config and rendered as CollapsibleCards. Controls can be
7
7
  * provided through named slots or auto-rendered from FormBuilder schemas.
8
8
  *
9
- * When the active view has no matching panels, the sidebar hides entirely.
9
+ * When activeView is omitted, the first non-empty panel view is selected.
10
+ * When no view has matching panels, the sidebar hides entirely.
10
11
  *
11
12
  * @example
12
13
  * ```vue
@@ -24,7 +25,7 @@
24
25
  * <AppSidebar :controls="controls" :active-view="activeTab" v-model="values" />
25
26
  * ```
26
27
  */
27
- import { computed } from 'vue'
28
+ import { computed, ref, useSlots, type Slots } from 'vue'
28
29
  import type { PillNavItem, SidebarToolSection } from '../types'
29
30
  import type { FormEnhancements, FormSchema } from '../types/form-builder'
30
31
  import {
@@ -42,15 +43,23 @@ import CollapsibleCard from './CollapsibleCard.vue'
42
43
  import FormBuilder from './FormBuilder.vue'
43
44
 
44
45
  interface Props {
46
+ /** Optional chrome title rendered above generated sections. */
47
+ title?: string
48
+ /** Optional secondary chrome copy rendered below title. */
49
+ subtitle?: string
50
+ /** Optional compact badge/count rendered in the chrome header. */
51
+ badge?: string | number
52
+ /** Visual preset for common plugin sidebars. `analysis` preserves the LEAF-style MINT analysis sidebar design language. */
53
+ variant?: 'default' | 'analysis'
45
54
  /** Map of view IDs to their tool sections */
46
55
  panels?: Record<string, SidebarToolSection[]>
47
- /** Which view's panels to display */
56
+ /** Which view's panels to display. Defaults to the first non-empty panel view. */
48
57
  activeView?: string
49
- /** Floating variant with absolute positioning */
58
+ /** Floating variant with absolute positioning. Defaults to false for analysis variant. */
50
59
  floating?: boolean
51
60
  /** Compact layout: smaller headers, tighter spacing, no icon backgrounds */
52
61
  dense?: boolean
53
- /** Width when visible */
62
+ /** Width when visible. Defaults to 20rem for analysis variant, otherwise 280px. */
54
63
  width?: string
55
64
  /** Position sidebar on left or right side */
56
65
  side?: 'left' | 'right'
@@ -60,7 +69,7 @@ interface Props {
60
69
  forms?: Record<string, FormSchema>
61
70
  /** Generated view IDs from useControlSchema(). Consumed for clean v-bind ergonomics. */
62
71
  viewIds?: string[]
63
- /** Generated AppPillNav-compatible view items from useControlSchema(). Consumed for clean v-bind ergonomics. */
72
+ /** Generated AppTopBar pillNav-compatible view items from useControlSchema(). Consumed for clean v-bind ergonomics. */
64
73
  viewItems?: PillNavItem[]
65
74
  /** Default view ID used when activeView is omitted. */
66
75
  defaultView?: string
@@ -70,6 +79,10 @@ interface Props {
70
79
  controls?: ControlSchema
71
80
  /** Options passed to compact control schema generation, including shared initialValues. */
72
81
  controlOptions?: ControlWorkspaceOptions
82
+ /** DOM id for the scrollable content area. Use with Teleport when route/tab children own sidebar controls. */
83
+ contentId?: string
84
+ /** Render the sidebar shell even when no panel matches the active view. Useful for default-slot or Teleport-driven sidebars. */
85
+ showWhenEmpty?: boolean
73
86
  /** Shared values for auto-rendered section forms. Supports default v-model. */
74
87
  modelValue?: Record<string, unknown>
75
88
  /** Shared values for auto-rendered section forms */
@@ -86,14 +99,30 @@ interface Props {
86
99
  formReadonly?: boolean
87
100
  /** Size passed to auto-rendered section forms */
88
101
  formSize?: 'sm' | 'md' | 'lg'
102
+ /** Show a built-in collapse/expand button in the sidebar chrome. Defaults to true for analysis variant. */
103
+ collapsible?: boolean
104
+ /** Controlled collapsed state. */
105
+ collapsed?: boolean
106
+ /** Initial collapsed state when collapsed is uncontrolled. */
107
+ defaultCollapsed?: boolean
108
+ /** Width when collapsed. */
109
+ collapsedWidth?: string
110
+ /** Accessible label for the collapse action. */
111
+ collapseButtonLabel?: string
112
+ /** Accessible label for the expand action. */
113
+ expandButtonLabel?: string
89
114
  }
90
115
 
91
116
  const props = withDefaults(defineProps<Props>(), {
117
+ title: undefined,
118
+ subtitle: undefined,
119
+ badge: undefined,
120
+ variant: 'default',
92
121
  panels: () => ({}),
93
122
  activeView: '',
94
- floating: true,
123
+ floating: undefined,
95
124
  dense: false,
96
- width: '280px',
125
+ width: undefined,
97
126
  side: 'left',
98
127
  toggleState: () => ({}),
99
128
  forms: () => ({}),
@@ -102,6 +131,8 @@ const props = withDefaults(defineProps<Props>(), {
102
131
  defaultView: '',
103
132
  model: undefined,
104
133
  controlOptions: () => ({}),
134
+ contentId: undefined,
135
+ showWhenEmpty: false,
105
136
  modelValue: undefined,
106
137
  values: () => ({}),
107
138
  showFormActions: false,
@@ -109,8 +140,38 @@ const props = withDefaults(defineProps<Props>(), {
109
140
  formDisabled: false,
110
141
  formReadonly: false,
111
142
  formSize: 'sm',
143
+ collapsible: undefined,
144
+ collapsed: undefined,
145
+ defaultCollapsed: false,
146
+ collapsedWidth: '3rem',
147
+ collapseButtonLabel: 'Collapse sidebar',
148
+ expandButtonLabel: 'Expand sidebar',
112
149
  })
113
150
 
151
+ const emit = defineEmits<{
152
+ 'update:toggle': [sectionId: string, value: boolean]
153
+ 'update:modelValue': [values: Record<string, unknown>]
154
+ 'update:values': [values: Record<string, unknown>]
155
+ 'update:collapsed': [value: boolean]
156
+ 'form-submit': [sectionId: string, values: Record<string, unknown>]
157
+ 'form-cancel': [sectionId: string]
158
+ }>()
159
+
160
+ const slots: Slots = useSlots()
161
+ const internalCollapsed = ref(props.defaultCollapsed)
162
+ const collapsedModel = computed({
163
+ get: () => props.collapsed ?? internalCollapsed.value,
164
+ set: (value: boolean) => {
165
+ internalCollapsed.value = value
166
+ emit('update:collapsed', value)
167
+ },
168
+ })
169
+
170
+ const isAnalysisVariant = computed(() => props.variant === 'analysis')
171
+ const resolvedFloating = computed(() => props.floating ?? !isAnalysisVariant.value)
172
+ const resolvedWidth = computed(() => props.width ?? (isAnalysisVariant.value ? '20rem' : '280px'))
173
+ const resolvedCollapsible = computed(() => props.collapsible ?? isAnalysisVariant.value)
174
+
114
175
  const resolvedModel = computed<ControlModelBinding | undefined>(() => {
115
176
  if (props.model === undefined) return undefined
116
177
  return isControlModelBinding(props.model) ? props.model : defineControlModel(props.model)
@@ -159,7 +220,6 @@ const resolvedValues = computed<Record<string, unknown>>(() => ({
159
220
  const resolvedActiveView = computed(() => {
160
221
  if (props.activeView) return props.activeView
161
222
  if (props.defaultView) return props.defaultView
162
- if (!resolvedControls.value) return ''
163
223
  return firstVisibleViewId(resolvedPanels.value)
164
224
  })
165
225
 
@@ -168,28 +228,25 @@ const activeSections = computed<SidebarToolSection[]>(() => {
168
228
  return resolvedPanels.value[resolvedActiveView.value]
169
229
  })
170
230
 
171
- const isVisible = computed(() => activeSections.value.length > 0)
231
+ const isVisible = computed<boolean>(() =>
232
+ activeSections.value.length > 0 || props.showWhenEmpty || Boolean(slots.default),
233
+ )
172
234
 
173
- const sidebarClasses = computed(() => [
235
+ const sidebarClasses = computed<string[]>(() => [
174
236
  'mint-sidebar',
175
237
  `mint-sidebar--${props.side}`,
176
- props.floating ? 'mint-sidebar--floating' : 'mint-sidebar--static',
238
+ `mint-sidebar--${props.variant}`,
239
+ resolvedFloating.value ? 'mint-sidebar--floating' : 'mint-sidebar--static',
177
240
  props.dense ? 'mint-sidebar--dense' : '',
241
+ resolvedCollapsible.value ? 'mint-sidebar--collapsible' : '',
242
+ collapsedModel.value ? 'mint-sidebar--collapsed' : '',
178
243
  !isVisible.value ? 'mint-sidebar--hidden' : '',
179
244
  ])
180
245
 
181
- const sidebarStyle = computed(() => ({
182
- width: props.width,
246
+ const sidebarStyle = computed<Record<string, string>>(() => ({
247
+ width: collapsedModel.value ? props.collapsedWidth : resolvedWidth.value,
183
248
  }))
184
249
 
185
- const emit = defineEmits<{
186
- 'update:toggle': [sectionId: string, value: boolean]
187
- 'update:modelValue': [values: Record<string, unknown>]
188
- 'update:values': [values: Record<string, unknown>]
189
- 'form-submit': [sectionId: string, values: Record<string, unknown>]
190
- 'form-cancel': [sectionId: string]
191
- }>()
192
-
193
250
  function handleFormUpdate(values: Record<string, unknown>) {
194
251
  const nextValues = { ...resolvedValues.value, ...values }
195
252
  emit('update:modelValue', nextValues)
@@ -204,6 +261,14 @@ function handleFormCancel(sectionId: string) {
204
261
  emit('form-cancel', sectionId)
205
262
  }
206
263
 
264
+ function toggleCollapsed() {
265
+ collapsedModel.value = !collapsedModel.value
266
+ }
267
+
268
+ function expandCollapsed() {
269
+ collapsedModel.value = false
270
+ }
271
+
207
272
  function mergeSidebarPanels(
208
273
  generated: Record<string, SidebarToolSection[]>,
209
274
  explicit: Record<string, SidebarToolSection[]>,
@@ -242,13 +307,54 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
242
307
  :class="sidebarClasses"
243
308
  :style="sidebarStyle"
244
309
  >
245
- <!-- Header slot -->
246
- <div v-if="$slots.header" class="mint-sidebar__header">
247
- <slot name="header" />
310
+ <!-- Header slot / built-in chrome -->
311
+ <div
312
+ v-if="$slots.header || title || subtitle || badge !== undefined || resolvedCollapsible"
313
+ class="mint-sidebar__header"
314
+ >
315
+ <slot
316
+ name="header"
317
+ :collapsed="collapsedModel"
318
+ :toggle-collapsed="toggleCollapsed"
319
+ >
320
+ <div v-if="!collapsedModel" class="mint-sidebar__heading">
321
+ <div class="mint-sidebar__heading-copy">
322
+ <h2 v-if="title" class="mint-sidebar__title">{{ title }}</h2>
323
+ <p v-if="subtitle" class="mint-sidebar__subtitle">{{ subtitle }}</p>
324
+ </div>
325
+ <span v-if="badge !== undefined" class="mint-sidebar__badge">{{ badge }}</span>
326
+ </div>
327
+ <button
328
+ v-if="resolvedCollapsible"
329
+ type="button"
330
+ class="mint-sidebar__collapse-button"
331
+ :aria-label="collapsedModel ? expandButtonLabel : collapseButtonLabel"
332
+ :aria-expanded="!collapsedModel"
333
+ @click="toggleCollapsed"
334
+ >
335
+ <svg
336
+ class="mint-sidebar__collapse-icon"
337
+ :class="{ 'mint-sidebar__collapse-icon--collapsed': collapsedModel }"
338
+ viewBox="0 0 24 24"
339
+ fill="none"
340
+ stroke="currentColor"
341
+ stroke-width="2"
342
+ stroke-linecap="round"
343
+ stroke-linejoin="round"
344
+ aria-hidden="true"
345
+ >
346
+ <path d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
347
+ </svg>
348
+ </button>
349
+ </slot>
248
350
  </div>
249
351
 
250
352
  <!-- Tool sections -->
251
- <div class="mint-sidebar__sections">
353
+ <div
354
+ v-if="!collapsedModel"
355
+ :id="contentId"
356
+ class="mint-sidebar__sections"
357
+ >
252
358
  <CollapsibleCard
253
359
  v-for="section in activeSections"
254
360
  :key="section.id"
@@ -280,10 +386,24 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
280
386
  />
281
387
  </slot>
282
388
  </CollapsibleCard>
389
+
390
+ <slot
391
+ :sections="activeSections"
392
+ :active-view="resolvedActiveView"
393
+ :values="resolvedValues"
394
+ />
395
+ </div>
396
+
397
+ <div v-else-if="$slots.collapsed" class="mint-sidebar__collapsed">
398
+ <slot
399
+ name="collapsed"
400
+ :sections="activeSections"
401
+ :expand="expandCollapsed"
402
+ />
283
403
  </div>
284
404
 
285
405
  <!-- Footer slot -->
286
- <div v-if="$slots.footer" class="mint-sidebar__footer">
406
+ <div v-if="!collapsedModel && $slots.footer" class="mint-sidebar__footer">
287
407
  <slot name="footer" />
288
408
  </div>
289
409
  </aside>
@@ -231,10 +231,9 @@ const sampleAccountMenu: AccountMenuItem[] = [
231
231
  </div>
232
232
  </Variant>
233
233
 
234
- <Variant title="Classic · Page selector">
234
+ <Variant title="Navigation · Page selector">
235
235
  <div style="padding: 2rem;">
236
236
  <AppTopBar
237
- plugin-name="IC50 Calculator"
238
237
  title="Results"
239
238
  :page-selector="samplePageSelector"
240
239
  current-page-selector-id="plugins"
@@ -245,10 +244,9 @@ const sampleAccountMenu: AccountMenuItem[] = [
245
244
  </div>
246
245
  </Variant>
247
246
 
248
- <Variant title="Classic · Pill nav">
247
+ <Variant title="Navigation · Pill nav">
249
248
  <div style="padding: 2rem;">
250
249
  <AppTopBar
251
- plugin-name="Plate Analyzer"
252
250
  title="Experiment View"
253
251
  :pill-nav="samplePillNav.slice(0, 3)"
254
252
  current-pill-id="workspace"
@@ -268,7 +266,6 @@ const sampleAccountMenu: AccountMenuItem[] = [
268
266
  <div style="padding: 2rem;">
269
267
  <ExperimentProvider>
270
268
  <AppTopBar
271
- plugin-name="IC50 Calculator"
272
269
  title="Analysis"
273
270
  variant="card"
274
271
  :show-theme-toggle="true"