@morscherlab/mint-sdk 1.0.0-beta.3 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +9 -2
  2. package/dist/__tests__/composables/experiment-utils.test.d.ts +1 -0
  3. package/dist/__tests__/composables/useApi.test.d.ts +1 -0
  4. package/dist/components/AppContainer.vue.d.ts +1 -1
  5. package/dist/components/AppLayout.vue.d.ts +20 -1
  6. package/dist/components/AppSidebar.vue.d.ts +56 -4
  7. package/dist/components/AppTopBar.vue.d.ts +7 -25
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +3 -1
  9. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -0
  10. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +5 -0
  11. package/dist/components/ComponentBindingRenderer.vue.d.ts +44 -0
  12. package/dist/components/ControlWorkspaceView.vue.d.ts +24 -7
  13. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +149 -0
  14. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  15. package/dist/components/FormBuilder.vue.d.ts +9 -9
  16. package/dist/components/PlateMapEditor.vue.d.ts +1 -1
  17. package/dist/components/PluginWorkspaceView.vue.d.ts +310 -0
  18. package/dist/components/SettingsModal.vue.d.ts +1 -1
  19. package/dist/components/WellPlate.vue.d.ts +2 -2
  20. package/dist/components/index.d.ts +3 -12
  21. package/dist/components/index.js +3 -3
  22. package/dist/components/{AppPageSelector.vue.d.ts → internal/AppPageSelectorInternal.vue.d.ts} +1 -1
  23. package/dist/components/{AppPillNav.vue.d.ts → internal/AppPillNavInternal.vue.d.ts} +3 -1
  24. package/dist/components/{CalendarGridPanel.vue.d.ts → internal/CalendarGridPanelInternal.vue.d.ts} +1 -1
  25. package/dist/components/internal/FormSectionRenderer.vue.d.ts +4 -4
  26. package/dist/components/{WellEditPopup.vue.d.ts → internal/WellEditPopupInternal.vue.d.ts} +1 -1
  27. package/dist/{components-D_Sr0adg.js → components-BkGF4B4y.js} +4484 -3967
  28. package/dist/components-BkGF4B4y.js.map +1 -0
  29. package/dist/composables/experiment-utils.d.ts +8 -0
  30. package/dist/composables/index.d.ts +5 -7
  31. package/dist/composables/index.js +4 -4
  32. package/dist/composables/useAppExperiment.d.ts +31 -2
  33. package/dist/composables/useBioTemplateComponents.d.ts +5 -3
  34. package/dist/composables/useBioTemplatePackWorkspace.d.ts +3 -2
  35. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +6 -5
  36. package/dist/composables/useBioTemplateWorkspace.d.ts +5 -4
  37. package/dist/composables/useControlSchema.d.ts +43 -21
  38. package/dist/composables/usePluginClient.d.ts +5 -2
  39. package/dist/{composables-C3dpXQN5.js → composables-CHsME9H1.js} +40 -28
  40. package/dist/composables-CHsME9H1.js.map +1 -0
  41. package/dist/index.d.ts +5 -12
  42. package/dist/index.js +5 -5
  43. package/dist/install.js +2 -2
  44. package/dist/styles.css +3625 -3651
  45. package/dist/templates/componentBindings.d.ts +13 -0
  46. package/dist/templates/index.d.ts +3 -3
  47. package/dist/templates/index.js +2 -2
  48. package/dist/{templates-50NPjaxL.js → templates-B5jmTWuk.js} +111 -56
  49. package/dist/templates-B5jmTWuk.js.map +1 -0
  50. package/dist/types/components.d.ts +6 -25
  51. package/dist/types/index.d.ts +1 -1
  52. package/dist/{useScheduleDrag-D4oWdh41.js → useScheduleDrag-BgzpQT53.js} +160 -117
  53. package/dist/useScheduleDrag-BgzpQT53.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/__tests__/components/ActionItem.test.ts +6 -6
  56. package/src/__tests__/components/AppLayout.test.ts +44 -0
  57. package/src/__tests__/components/AppPageSelector.test.ts +8 -8
  58. package/src/__tests__/components/AppPillNav.test.ts +53 -6
  59. package/src/__tests__/components/AppSidebar.test.ts +126 -0
  60. package/src/__tests__/components/AppToastContainer.test.ts +0 -11
  61. package/src/__tests__/components/AppTopBar.test.ts +182 -119
  62. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +7 -1
  63. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +15 -1
  64. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +26 -1
  65. package/src/__tests__/components/CalendarGridPanel.test.ts +3 -3
  66. package/src/__tests__/components/ComponentBindingRenderer.test.ts +161 -0
  67. package/src/__tests__/components/ControlWorkspaceView.test.ts +134 -63
  68. package/src/__tests__/components/DateTimePicker.test.ts +2 -2
  69. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
  70. package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
  71. package/src/__tests__/composables/experiment-utils.test.ts +30 -0
  72. package/src/__tests__/composables/useApi.test.ts +30 -0
  73. package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
  74. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +6 -3
  75. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +6 -6
  76. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +6 -1
  77. package/src/__tests__/composables/useControlSchema.test.ts +150 -36
  78. package/src/__tests__/composables/usePluginClient.test.ts +99 -2
  79. package/src/__tests__/docs/frontendDocsCatalog.test.ts +120 -25
  80. package/src/__tests__/templates/templates.test.ts +12 -0
  81. package/src/components/AppAvatarMenu.vue +3 -3
  82. package/src/components/AppLayout.story.vue +39 -0
  83. package/src/components/AppLayout.vue +83 -2
  84. package/src/components/AppPluginSwitcher.vue +5 -5
  85. package/src/components/AppSidebar.story.vue +113 -5
  86. package/src/components/AppSidebar.vue +144 -24
  87. package/src/components/AppTopBar.story.vue +2 -5
  88. package/src/components/AppTopBar.vue +35 -425
  89. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +2 -2
  90. package/src/components/BioTemplateExperimentWorkspaceView.vue +6 -0
  91. package/src/components/BioTemplatePackWorkspaceView.story.vue +4 -4
  92. package/src/components/BioTemplatePackWorkspaceView.vue +1 -0
  93. package/src/components/BioTemplatePresetWorkspaceView.story.vue +14 -2
  94. package/src/components/BioTemplatePresetWorkspaceView.vue +11 -2
  95. package/src/components/BioTemplateRenderer.vue +15 -227
  96. package/src/components/ComponentBindingRenderer.story.vue +57 -0
  97. package/src/components/ComponentBindingRenderer.vue +308 -0
  98. package/src/components/ControlWorkspaceView.story.vue +20 -9
  99. package/src/components/ControlWorkspaceView.vue +43 -12
  100. package/src/components/DatePicker.vue +2 -2
  101. package/src/components/DateTimePicker.vue +2 -2
  102. package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
  103. package/src/components/DoseDesignWorkspaceView.vue +255 -0
  104. package/src/components/ExperimentPopover.vue +2 -6
  105. package/src/components/ExperimentSelectorModal.vue +6 -5
  106. package/src/components/FormBuilder.story.vue +190 -0
  107. package/src/components/PluginWorkspaceView.story.vue +334 -0
  108. package/src/components/PluginWorkspaceView.vue +708 -0
  109. package/src/components/SettingsModal.story.vue +87 -0
  110. package/src/components/WellPlate.vue +2 -2
  111. package/src/components/index.ts +3 -12
  112. package/src/components/{AppPageSelector.vue → internal/AppPageSelectorInternal.vue} +9 -9
  113. package/src/components/internal/AppPillNavInternal.vue +194 -0
  114. package/src/components/{CalendarGridPanel.vue → internal/CalendarGridPanelInternal.vue} +1 -1
  115. package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +3 -3
  116. package/src/composables/experiment-utils.ts +26 -0
  117. package/src/composables/index.ts +21 -7
  118. package/src/composables/useApi.ts +9 -2
  119. package/src/composables/useAppExperiment.ts +85 -13
  120. package/src/composables/useBioTemplateComponents.ts +12 -0
  121. package/src/composables/useBioTemplatePackWorkspace.ts +6 -2
  122. package/src/composables/useBioTemplatePresetWorkspace.ts +10 -21
  123. package/src/composables/useBioTemplateWorkspace.ts +6 -4
  124. package/src/composables/useControlSchema.ts +157 -69
  125. package/src/composables/usePluginClient.ts +50 -9
  126. package/src/index.ts +6 -563
  127. package/src/styles/components/app-layout.css +82 -0
  128. package/src/styles/components/app-pill-nav.css +70 -0
  129. package/src/styles/components/app-sidebar.css +119 -0
  130. package/src/styles/components/app-top-bar.css +0 -235
  131. package/src/styles/index.css +0 -1
  132. package/src/templates/componentBindings.ts +38 -0
  133. package/src/templates/index.ts +4 -0
  134. package/src/types/components.ts +6 -31
  135. package/src/types/index.ts +2 -6
  136. package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
  137. package/dist/components/FormFieldRenderer.vue.d.ts +0 -28
  138. package/dist/components/FormSection.vue.d.ts +0 -30
  139. package/dist/components/GroupingModal.vue.d.ts +0 -12
  140. package/dist/components/SettingsButton.vue.d.ts +0 -30
  141. package/dist/components/ToastNotification.vue.d.ts +0 -2
  142. package/dist/components-D_Sr0adg.js.map +0 -1
  143. package/dist/composables/usePluginApi.d.ts +0 -22
  144. package/dist/composables-C3dpXQN5.js.map +0 -1
  145. package/dist/templates-50NPjaxL.js.map +0 -1
  146. package/dist/useScheduleDrag-D4oWdh41.js.map +0 -1
  147. package/src/__tests__/components/FormCompatibility.test.ts +0 -94
  148. package/src/__tests__/components/GroupingModal.test.ts +0 -73
  149. package/src/__tests__/components/SettingsButton.test.ts +0 -44
  150. package/src/__tests__/composables/usePluginApi.test.ts +0 -81
  151. package/src/components/AppPillNav.vue +0 -71
  152. package/src/components/FormFieldRenderer.vue +0 -35
  153. package/src/components/FormSection.vue +0 -37
  154. package/src/components/GroupingModal.story.vue +0 -52
  155. package/src/components/GroupingModal.vue +0 -61
  156. package/src/components/SettingsButton.story.vue +0 -58
  157. package/src/components/SettingsButton.vue +0 -64
  158. package/src/components/ToastNotification.vue +0 -9
  159. package/src/composables/usePluginApi.ts +0 -32
  160. package/src/styles/components/settings-button.css +0 -31
  161. /package/dist/__tests__/components/{FormCompatibility.test.d.ts → ComponentBindingRenderer.test.d.ts} +0 -0
  162. /package/dist/__tests__/components/{GroupingModal.test.d.ts → DoseDesignWorkspaceView.test.d.ts} +0 -0
  163. /package/dist/__tests__/components/{SettingsButton.test.d.ts → PluginWorkspaceView.test.d.ts} +0 -0
  164. /package/dist/components/{ActionItem.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +0 -0
  165. /package/src/components/{ActionItem.vue → internal/ActionItemInternal.vue} +0 -0
@@ -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
@@ -24,7 +24,7 @@
24
24
  * <AppSidebar :controls="controls" :active-view="activeTab" v-model="values" />
25
25
  * ```
26
26
  */
27
- import { computed } from 'vue'
27
+ import { computed, ref, useSlots, type Slots } from 'vue'
28
28
  import type { PillNavItem, SidebarToolSection } from '../types'
29
29
  import type { FormEnhancements, FormSchema } from '../types/form-builder'
30
30
  import {
@@ -42,15 +42,23 @@ import CollapsibleCard from './CollapsibleCard.vue'
42
42
  import FormBuilder from './FormBuilder.vue'
43
43
 
44
44
  interface Props {
45
+ /** Optional chrome title rendered above generated sections. */
46
+ title?: string
47
+ /** Optional secondary chrome copy rendered below title. */
48
+ subtitle?: string
49
+ /** Optional compact badge/count rendered in the chrome header. */
50
+ badge?: string | number
51
+ /** Visual preset for common plugin sidebars. `analysis` preserves the LEAF-style MINT analysis sidebar design language. */
52
+ variant?: 'default' | 'analysis'
45
53
  /** Map of view IDs to their tool sections */
46
54
  panels?: Record<string, SidebarToolSection[]>
47
55
  /** Which view's panels to display */
48
56
  activeView?: string
49
- /** Floating variant with absolute positioning */
57
+ /** Floating variant with absolute positioning. Defaults to false for analysis variant. */
50
58
  floating?: boolean
51
59
  /** Compact layout: smaller headers, tighter spacing, no icon backgrounds */
52
60
  dense?: boolean
53
- /** Width when visible */
61
+ /** Width when visible. Defaults to 20rem for analysis variant, otherwise 280px. */
54
62
  width?: string
55
63
  /** Position sidebar on left or right side */
56
64
  side?: 'left' | 'right'
@@ -60,7 +68,7 @@ interface Props {
60
68
  forms?: Record<string, FormSchema>
61
69
  /** Generated view IDs from useControlSchema(). Consumed for clean v-bind ergonomics. */
62
70
  viewIds?: string[]
63
- /** Generated AppPillNav-compatible view items from useControlSchema(). Consumed for clean v-bind ergonomics. */
71
+ /** Generated AppTopBar pillNav-compatible view items from useControlSchema(). Consumed for clean v-bind ergonomics. */
64
72
  viewItems?: PillNavItem[]
65
73
  /** Default view ID used when activeView is omitted. */
66
74
  defaultView?: string
@@ -70,6 +78,10 @@ interface Props {
70
78
  controls?: ControlSchema
71
79
  /** Options passed to compact control schema generation, including shared initialValues. */
72
80
  controlOptions?: ControlWorkspaceOptions
81
+ /** DOM id for the scrollable content area. Use with Teleport when route/tab children own sidebar controls. */
82
+ contentId?: string
83
+ /** Render the sidebar shell even when no panel matches the active view. Useful for default-slot or Teleport-driven sidebars. */
84
+ showWhenEmpty?: boolean
73
85
  /** Shared values for auto-rendered section forms. Supports default v-model. */
74
86
  modelValue?: Record<string, unknown>
75
87
  /** Shared values for auto-rendered section forms */
@@ -86,14 +98,30 @@ interface Props {
86
98
  formReadonly?: boolean
87
99
  /** Size passed to auto-rendered section forms */
88
100
  formSize?: 'sm' | 'md' | 'lg'
101
+ /** Show a built-in collapse/expand button in the sidebar chrome. Defaults to true for analysis variant. */
102
+ collapsible?: boolean
103
+ /** Controlled collapsed state. */
104
+ collapsed?: boolean
105
+ /** Initial collapsed state when collapsed is uncontrolled. */
106
+ defaultCollapsed?: boolean
107
+ /** Width when collapsed. */
108
+ collapsedWidth?: string
109
+ /** Accessible label for the collapse action. */
110
+ collapseButtonLabel?: string
111
+ /** Accessible label for the expand action. */
112
+ expandButtonLabel?: string
89
113
  }
90
114
 
91
115
  const props = withDefaults(defineProps<Props>(), {
116
+ title: undefined,
117
+ subtitle: undefined,
118
+ badge: undefined,
119
+ variant: 'default',
92
120
  panels: () => ({}),
93
121
  activeView: '',
94
- floating: true,
122
+ floating: undefined,
95
123
  dense: false,
96
- width: '280px',
124
+ width: undefined,
97
125
  side: 'left',
98
126
  toggleState: () => ({}),
99
127
  forms: () => ({}),
@@ -102,6 +130,8 @@ const props = withDefaults(defineProps<Props>(), {
102
130
  defaultView: '',
103
131
  model: undefined,
104
132
  controlOptions: () => ({}),
133
+ contentId: undefined,
134
+ showWhenEmpty: false,
105
135
  modelValue: undefined,
106
136
  values: () => ({}),
107
137
  showFormActions: false,
@@ -109,8 +139,38 @@ const props = withDefaults(defineProps<Props>(), {
109
139
  formDisabled: false,
110
140
  formReadonly: false,
111
141
  formSize: 'sm',
142
+ collapsible: undefined,
143
+ collapsed: undefined,
144
+ defaultCollapsed: false,
145
+ collapsedWidth: '3rem',
146
+ collapseButtonLabel: 'Collapse sidebar',
147
+ expandButtonLabel: 'Expand sidebar',
112
148
  })
113
149
 
150
+ const emit = defineEmits<{
151
+ 'update:toggle': [sectionId: string, value: boolean]
152
+ 'update:modelValue': [values: Record<string, unknown>]
153
+ 'update:values': [values: Record<string, unknown>]
154
+ 'update:collapsed': [value: boolean]
155
+ 'form-submit': [sectionId: string, values: Record<string, unknown>]
156
+ 'form-cancel': [sectionId: string]
157
+ }>()
158
+
159
+ const slots: Slots = useSlots()
160
+ const internalCollapsed = ref(props.defaultCollapsed)
161
+ const collapsedModel = computed({
162
+ get: () => props.collapsed ?? internalCollapsed.value,
163
+ set: (value: boolean) => {
164
+ internalCollapsed.value = value
165
+ emit('update:collapsed', value)
166
+ },
167
+ })
168
+
169
+ const isAnalysisVariant = computed(() => props.variant === 'analysis')
170
+ const resolvedFloating = computed(() => props.floating ?? !isAnalysisVariant.value)
171
+ const resolvedWidth = computed(() => props.width ?? (isAnalysisVariant.value ? '20rem' : '280px'))
172
+ const resolvedCollapsible = computed(() => props.collapsible ?? isAnalysisVariant.value)
173
+
114
174
  const resolvedModel = computed<ControlModelBinding | undefined>(() => {
115
175
  if (props.model === undefined) return undefined
116
176
  return isControlModelBinding(props.model) ? props.model : defineControlModel(props.model)
@@ -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"