@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
@@ -0,0 +1,308 @@
1
+ <script setup lang="ts">
2
+ /** Render generated SDK component bindings such as WellPlate, DoseCalculator, DataFrame, and PlateMapEditor. */
3
+ import { computed, type Component } from 'vue'
4
+ import DataFrame from './DataFrame.vue'
5
+ import DoseCalculator from './DoseCalculator.vue'
6
+ import ExperimentTimeline from './ExperimentTimeline.vue'
7
+ import PlateMapEditor from './PlateMapEditor.vue'
8
+ import ReagentList from './ReagentList.vue'
9
+ import SampleSelector from './SampleSelector.vue'
10
+ import WellPlate from './WellPlate.vue'
11
+
12
+ export interface ComponentBindingRendererBinding {
13
+ id?: string
14
+ component: string
15
+ props?: Record<string, unknown> | readonly string[]
16
+ propsObject?: Record<string, unknown>
17
+ description?: string
18
+ template_id?: string
19
+ }
20
+
21
+ type ComponentBindingRendererLayout = 'grid' | 'stack'
22
+
23
+ interface Props {
24
+ /** Single generated SDK component binding to render. */
25
+ binding?: ComponentBindingRendererBinding
26
+ /** Generated SDK component bindings to render. */
27
+ bindings?: ComponentBindingRendererBinding[]
28
+ /** Optional allow-list of component names, for example ['WellPlate', 'DataFrame']. */
29
+ include?: string[]
30
+ /** Optional deny-list of component names. */
31
+ exclude?: string[]
32
+ /** Compact child component sizing. */
33
+ dense?: boolean
34
+ /** Prefer preview-safe props for editable components. */
35
+ readonly?: boolean
36
+ /** Show component/template labels above each rendered component. */
37
+ showHeaders?: boolean
38
+ /** Show binding descriptions in each header. */
39
+ showDescriptions?: boolean
40
+ /** Grid or vertical stack layout. */
41
+ layout?: ComponentBindingRendererLayout
42
+ /** Message shown when no binding can be rendered. */
43
+ emptyText?: string
44
+ }
45
+
46
+ type RenderableBinding = ComponentBindingRendererBinding & {
47
+ id: string
48
+ componentImpl: Component
49
+ normalizedProps: Record<string, unknown>
50
+ }
51
+
52
+ const props = withDefaults(defineProps<Props>(), {
53
+ binding: undefined,
54
+ bindings: () => [],
55
+ include: () => [],
56
+ exclude: () => [],
57
+ dense: false,
58
+ readonly: true,
59
+ showHeaders: true,
60
+ showDescriptions: true,
61
+ layout: 'grid',
62
+ emptyText: 'No renderable SDK component bindings.',
63
+ })
64
+
65
+ const componentRegistry: Record<string, Component> = {
66
+ DataFrame,
67
+ DoseCalculator,
68
+ ExperimentTimeline,
69
+ PlateMapEditor,
70
+ ReagentList,
71
+ SampleSelector,
72
+ WellPlate,
73
+ }
74
+
75
+ const inputBindings = computed<ComponentBindingRendererBinding[]>(() => [
76
+ ...(props.binding ? [props.binding] : []),
77
+ ...props.bindings,
78
+ ])
79
+
80
+ const renderableBindings = computed<RenderableBinding[]>(() =>
81
+ inputBindings.value
82
+ .filter(binding => isIncluded(binding.component))
83
+ .map((binding, index) => {
84
+ const componentImpl = componentRegistry[binding.component]
85
+ if (!componentImpl) return null
86
+ return {
87
+ ...binding,
88
+ id: binding.id ?? `${binding.component}-${index + 1}`,
89
+ componentImpl,
90
+ normalizedProps: normalizeProps(binding),
91
+ }
92
+ })
93
+ .filter((binding): binding is RenderableBinding => binding !== null)
94
+ )
95
+
96
+ const rendererClasses = computed(() => [
97
+ 'mint-component-binding-renderer',
98
+ `mint-component-binding-renderer--${props.layout}`,
99
+ props.dense ? 'mint-component-binding-renderer--dense' : '',
100
+ ])
101
+
102
+ function isIncluded(componentName: string): boolean {
103
+ if (props.include.length > 0 && !props.include.includes(componentName)) return false
104
+ return !props.exclude.includes(componentName)
105
+ }
106
+
107
+ function baseProps(binding: ComponentBindingRendererBinding): Record<string, unknown> {
108
+ if (binding.propsObject) return { ...binding.propsObject }
109
+ if (isRecord(binding.props)) return { ...binding.props }
110
+ return {}
111
+ }
112
+
113
+ function normalizeProps(binding: ComponentBindingRendererBinding): Record<string, unknown> {
114
+ const base = baseProps(binding)
115
+
116
+ switch (binding.component) {
117
+ case 'WellPlate':
118
+ return {
119
+ ...base,
120
+ readonly: props.readonly || Boolean(base.readonly),
121
+ size: props.dense ? 'md' : (base.size ?? 'fill'),
122
+ }
123
+ case 'PlateMapEditor':
124
+ return {
125
+ ...base,
126
+ size: props.dense ? 'md' : (base.size ?? 'fill'),
127
+ showToolbar: props.readonly ? false : (base.showToolbar ?? true),
128
+ showSidebar: props.readonly ? false : (base.showSidebar ?? true),
129
+ allowAddPlates: props.readonly ? false : (base.allowAddPlates ?? true),
130
+ allowAddSamples: props.readonly ? false : (base.allowAddSamples ?? true),
131
+ }
132
+ case 'DataFrame':
133
+ return {
134
+ ...base,
135
+ size: props.dense ? 'sm' : (base.size ?? 'md'),
136
+ maxHeight: base.maxHeight ?? (props.dense ? '280px' : undefined),
137
+ }
138
+ case 'ExperimentTimeline':
139
+ return {
140
+ ...base,
141
+ editable: props.readonly ? false : base.editable,
142
+ size: props.dense ? 'sm' : (base.size ?? 'md'),
143
+ }
144
+ case 'ReagentList':
145
+ return {
146
+ ...base,
147
+ readonly: props.readonly || Boolean(base.readonly),
148
+ }
149
+ case 'SampleSelector':
150
+ return {
151
+ ...base,
152
+ enableGrouping: props.readonly ? false : (base.enableGrouping ?? true),
153
+ enableSmartGroup: props.readonly ? false : (base.enableSmartGroup ?? true),
154
+ }
155
+ default:
156
+ return base
157
+ }
158
+ }
159
+
160
+ function isRecord(value: unknown): value is Record<string, unknown> {
161
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
162
+ }
163
+ </script>
164
+
165
+ <template>
166
+ <div :class="rendererClasses">
167
+ <section
168
+ v-for="renderableBinding in renderableBindings"
169
+ :key="renderableBinding.id"
170
+ class="mint-component-binding-renderer__item"
171
+ :data-component-binding-id="renderableBinding.id"
172
+ :data-template-id="renderableBinding.template_id"
173
+ :data-template-component="renderableBinding.component"
174
+ >
175
+ <header
176
+ v-if="showHeaders"
177
+ class="mint-component-binding-renderer__header"
178
+ >
179
+ <div class="mint-component-binding-renderer__title-group">
180
+ <p class="mint-component-binding-renderer__component">
181
+ {{ renderableBinding.component }}
182
+ </p>
183
+ <p
184
+ v-if="renderableBinding.template_id || renderableBinding.id"
185
+ class="mint-component-binding-renderer__source"
186
+ >
187
+ {{ renderableBinding.template_id ?? renderableBinding.id }}
188
+ </p>
189
+ </div>
190
+ <p
191
+ v-if="showDescriptions && renderableBinding.description"
192
+ class="mint-component-binding-renderer__description"
193
+ >
194
+ {{ renderableBinding.description }}
195
+ </p>
196
+ </header>
197
+
198
+ <div class="mint-component-binding-renderer__body">
199
+ <component
200
+ :is="renderableBinding.componentImpl"
201
+ v-bind="renderableBinding.normalizedProps"
202
+ />
203
+ </div>
204
+ </section>
205
+
206
+ <p
207
+ v-if="renderableBindings.length === 0"
208
+ class="mint-component-binding-renderer__empty"
209
+ >
210
+ {{ emptyText }}
211
+ </p>
212
+ </div>
213
+ </template>
214
+
215
+ <style scoped>
216
+ .mint-component-binding-renderer {
217
+ display: grid;
218
+ gap: 1rem;
219
+ min-width: 0;
220
+ }
221
+
222
+ .mint-component-binding-renderer--grid {
223
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr));
224
+ }
225
+
226
+ .mint-component-binding-renderer--stack {
227
+ grid-template-columns: minmax(0, 1fr);
228
+ }
229
+
230
+ .mint-component-binding-renderer--dense {
231
+ gap: 0.75rem;
232
+ }
233
+
234
+ .mint-component-binding-renderer__item {
235
+ min-width: 0;
236
+ overflow: hidden;
237
+ border: 1px solid var(--border-color, #e5e7eb);
238
+ border-radius: 0.5rem;
239
+ background: var(--bg-card, #ffffff);
240
+ box-shadow: var(--shadow-sm, 0 1px 2px rgb(15 23 42 / 0.06));
241
+ }
242
+
243
+ .mint-component-binding-renderer__header {
244
+ display: flex;
245
+ align-items: flex-start;
246
+ justify-content: space-between;
247
+ gap: 1rem;
248
+ padding: 0.75rem 1rem;
249
+ border-bottom: 1px solid var(--border-color, #e5e7eb);
250
+ background: var(--bg-secondary, #f8fafc);
251
+ }
252
+
253
+ .mint-component-binding-renderer__title-group {
254
+ min-width: 0;
255
+ }
256
+
257
+ .mint-component-binding-renderer__component {
258
+ margin: 0;
259
+ color: var(--text-primary, #0f172a);
260
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
261
+ font-size: 0.8125rem;
262
+ font-weight: 600;
263
+ }
264
+
265
+ .mint-component-binding-renderer__source,
266
+ .mint-component-binding-renderer__description,
267
+ .mint-component-binding-renderer__empty {
268
+ margin: 0;
269
+ color: var(--text-secondary, #475569);
270
+ font-size: 0.75rem;
271
+ line-height: 1.4;
272
+ }
273
+
274
+ .mint-component-binding-renderer__description {
275
+ max-width: 24rem;
276
+ text-align: right;
277
+ }
278
+
279
+ .mint-component-binding-renderer__body {
280
+ min-width: 0;
281
+ padding: 1rem;
282
+ }
283
+
284
+ .mint-component-binding-renderer--dense .mint-component-binding-renderer__header,
285
+ .mint-component-binding-renderer--dense .mint-component-binding-renderer__body {
286
+ padding: 0.75rem;
287
+ }
288
+
289
+ .mint-component-binding-renderer__empty {
290
+ padding: 1rem;
291
+ border: 1px dashed var(--border-color, #e5e7eb);
292
+ border-radius: 0.5rem;
293
+ background: var(--bg-secondary, #f8fafc);
294
+ text-align: center;
295
+ }
296
+
297
+ @media (max-width: 720px) {
298
+ .mint-component-binding-renderer__header {
299
+ display: block;
300
+ }
301
+
302
+ .mint-component-binding-renderer__description {
303
+ max-width: none;
304
+ margin-top: 0.25rem;
305
+ text-align: left;
306
+ }
307
+ }
308
+ </style>
@@ -11,6 +11,8 @@ import {
11
11
  type ControlComponentPropsMap,
12
12
  } from '../composables/useControlSchema'
13
13
 
14
+ type SidebarVariant = 'analysis' | 'default'
15
+
14
16
  const controls = defineControls({
15
17
  threshold: {
16
18
  type: 'number',
@@ -137,8 +139,11 @@ const MockResultChart = defineComponent({
137
139
  function initState() {
138
140
  return {
139
141
  title: 'Analysis Workspace',
142
+ sidebarTitle: 'Analysis Controls',
143
+ sidebarSubtitle: 'Current run',
140
144
  sidebarPosition: 'left',
141
- navigation: 'pill',
145
+ sidebarVariant: 'analysis' as SidebarVariant,
146
+ responsiveSidebar: true,
142
147
  floating: false,
143
148
  dense: true,
144
149
  showSettings: true,
@@ -157,8 +162,11 @@ function initState() {
157
162
  <ControlWorkspaceView
158
163
  :workspace="workspace"
159
164
  :title="state.title"
165
+ :sidebar-title="state.sidebarTitle"
166
+ :sidebar-subtitle="state.sidebarSubtitle"
160
167
  :sidebar-position="state.sidebarPosition"
161
- :navigation="state.navigation"
168
+ :sidebar-variant="state.sidebarVariant"
169
+ :responsive-sidebar="state.responsiveSidebar"
162
170
  :floating="state.floating"
163
171
  :dense="state.dense"
164
172
  :show-settings="state.showSettings"
@@ -171,6 +179,8 @@ function initState() {
171
179
 
172
180
  <template #controls="{ state }">
173
181
  <HstText v-model="state.title" title="Title" />
182
+ <HstText v-model="state.sidebarTitle" title="Sidebar title" />
183
+ <HstText v-model="state.sidebarSubtitle" title="Sidebar subtitle" />
174
184
  <HstSelect
175
185
  v-model="state.sidebarPosition"
176
186
  title="Sidebar"
@@ -180,13 +190,14 @@ function initState() {
180
190
  ]"
181
191
  />
182
192
  <HstSelect
183
- v-model="state.navigation"
184
- title="Navigation"
193
+ v-model="state.sidebarVariant"
194
+ title="Sidebar variant"
185
195
  :options="[
186
- { label: 'Pill', value: 'pill' },
187
- { label: 'Tabs', value: 'tabs' },
196
+ { label: 'Analysis', value: 'analysis' },
197
+ { label: 'Default', value: 'default' },
188
198
  ]"
189
199
  />
200
+ <HstCheckbox v-model="state.responsiveSidebar" title="Responsive sidebar" />
190
201
  <HstCheckbox v-model="state.floating" title="Floating" />
191
202
  <HstCheckbox v-model="state.dense" title="Dense" />
192
203
  <HstCheckbox v-model="state.showSettings" title="Settings" />
@@ -228,16 +239,16 @@ function initState() {
228
239
  v-model="doseDesignValues"
229
240
  :model="doseDesignModel"
230
241
  title="Dose Design Workspace"
231
- v-slot="{ componentPropsById }"
242
+ v-slot="{ componentBindingsById }"
232
243
  >
233
244
  <div class="dose-design-demo">
234
245
  <WellPlate
235
- v-bind="componentPropsById.plate"
246
+ v-bind="componentBindingsById.plate.props"
236
247
  size="fill"
237
248
  selection-mode="multiple"
238
249
  :heatmap="{ enabled: true, min: 0, max: 100, colorScale: 'viridis' }"
239
250
  />
240
- <DoseCalculator v-bind="componentPropsById.dose" />
251
+ <DoseCalculator v-bind="componentBindingsById.dose.props" />
241
252
  </div>
242
253
  </ControlWorkspaceView>
243
254
  </div>
@@ -1,9 +1,10 @@
1
1
  <script setup lang="ts">
2
- /** Page shell that turns a simple controls data model into AppTopBar, AppSidebar, and FormBuilder. */
2
+ /** Complete control workspace component that turns a simple controls data model into AppTopBar, AppSidebar, and FormBuilder forms. */
3
3
  import { computed, effectScope, onScopeDispose, shallowRef, toRaw, watch, type EffectScope } from 'vue'
4
4
  import type { TopBarVariant } from '../types'
5
5
  import type { FormEnhancements } from '../types/form-builder'
6
6
  import type {
7
+ ControlComponentBindingsConfig,
7
8
  ControlComponentPropsByIdMap,
8
9
  ControlComponentPropsMap,
9
10
  ControlModel,
@@ -18,13 +19,15 @@ import AppSidebar from './AppSidebar.vue'
18
19
  import AppTopBar from './AppTopBar.vue'
19
20
  import FormBuilder from './FormBuilder.vue'
20
21
 
21
- type ControlWorkspaceNavigation = 'tabs' | 'pill'
22
+ type ControlWorkspaceSidebarVariant = 'default' | 'analysis'
22
23
  type ResolvedControlWorkspace = UseControlWorkspaceReturn<ControlSchema>
23
24
 
24
25
  interface ControlWorkspaceDefaultSlotProps {
25
26
  workspace: ResolvedControlWorkspace
26
27
  bindings: ResolvedControlWorkspace['bindings']
27
28
  values: ResolvedControlWorkspace['values']
29
+ componentBindings: ReturnType<ResolvedControlWorkspace['getComponentBindings']>
30
+ componentBindingsById: ReturnType<ResolvedControlWorkspace['getComponentBindingsById']>
28
31
  componentProps: ReturnType<ResolvedControlWorkspace['getComponentProps']>
29
32
  componentPropsById: ReturnType<ResolvedControlWorkspace['getComponentPropsById']>
30
33
  }
@@ -32,7 +35,7 @@ interface ControlWorkspaceDefaultSlotProps {
32
35
  interface ControlWorkspaceTopbarSlotProps {
33
36
  workspace: ResolvedControlWorkspace
34
37
  bindings: ResolvedControlWorkspace['bindings']
35
- topBar: ResolvedControlWorkspace['topBar']
38
+ topBar: ResolvedControlWorkspace['bindings']['topBar']['value']
36
39
  }
37
40
 
38
41
  interface ControlWorkspaceSidebarSlotProps {
@@ -62,12 +65,20 @@ interface Props {
62
65
  subtitle?: string
63
66
  /** AppTopBar visual variant. */
64
67
  topBarVariant?: TopBarVariant
65
- /** AppTopBar generated navigation style. "pill" uses the preferred AppPillNav surface; "tabs" keeps the legacy prop surface. */
66
- navigation?: ControlWorkspaceNavigation
67
68
  /** AppSidebar/AppLayout sidebar width. */
68
69
  sidebarWidth?: string
70
+ /** AppSidebar visual preset. `analysis` matches the LEAF-style MINT analysis sidebar design language. */
71
+ sidebarVariant?: ControlWorkspaceSidebarVariant
72
+ /** Convert the sidebar into an SDK-owned mobile overlay below the AppLayout breakpoint. */
73
+ responsiveSidebar?: boolean
69
74
  /** Sidebar position in AppLayout. */
70
75
  sidebarPosition?: 'left' | 'right'
76
+ /** Optional AppSidebar chrome title for LEAF-style plugin workbenches. */
77
+ sidebarTitle?: string
78
+ /** Optional AppSidebar chrome subtitle for active experiment/run context. */
79
+ sidebarSubtitle?: string
80
+ /** Optional compact badge/count rendered in the AppSidebar chrome header. */
81
+ sidebarBadge?: string | number
71
82
  /** Floating AppLayout style. */
72
83
  floating?: boolean
73
84
  /** Compact AppSidebar density. */
@@ -78,6 +89,8 @@ interface Props {
78
89
  showFormActions?: boolean
79
90
  /** Runtime FormBuilder enhancements passed to generated forms. */
80
91
  formEnhancements?: FormEnhancements<Record<string, unknown>>
92
+ /** Optional SDK component bindings exposed to the default slot with resolved props. */
93
+ componentBindings?: ControlComponentBindingsConfig
81
94
  /** Optional mapping from workspace values to component props exposed to the default slot. */
82
95
  componentProps?: ControlComponentPropsMap
83
96
  /** Optional named mappings from workspace values to component props exposed to the default slot. */
@@ -120,14 +133,19 @@ const props = withDefaults(defineProps<Props>(), {
120
133
  title: 'Workspace',
121
134
  subtitle: undefined,
122
135
  topBarVariant: 'card',
123
- navigation: 'pill',
124
136
  sidebarWidth: '320px',
137
+ sidebarVariant: 'analysis',
138
+ responsiveSidebar: true,
125
139
  sidebarPosition: 'left',
140
+ sidebarTitle: undefined,
141
+ sidebarSubtitle: undefined,
142
+ sidebarBadge: undefined,
126
143
  floating: false,
127
144
  dense: true,
128
145
  showSettings: true,
129
146
  showFormActions: false,
130
147
  formEnhancements: undefined,
148
+ componentBindings: undefined,
131
149
  componentProps: undefined,
132
150
  componentPropsById: undefined,
133
151
  formLoading: false,
@@ -177,6 +195,12 @@ function createGeneratedWorkspace(seedValues: Record<string, unknown> = {}) {
177
195
 
178
196
  const generatedWorkspace = shallowRef<UseControlWorkspaceReturn<ControlSchema>>(createGeneratedWorkspace())
179
197
  const resolvedWorkspace = computed<UseControlWorkspaceReturn<ControlSchema>>(() => props.workspace ?? generatedWorkspace.value)
198
+ const resolvedComponentBindings = computed(() =>
199
+ resolvedWorkspace.value.getComponentBindings(props.componentBindings ?? resolvedModel.value?.componentBindings)
200
+ )
201
+ const resolvedComponentBindingsById = computed(() =>
202
+ resolvedWorkspace.value.getComponentBindingsById(props.componentBindings ?? resolvedModel.value?.componentBindings)
203
+ )
180
204
  const resolvedComponentProps = computed(() =>
181
205
  resolvedWorkspace.value.getComponentProps(props.componentProps ?? resolvedModel.value?.componentProps)
182
206
  )
@@ -185,9 +209,12 @@ const resolvedComponentPropsById = computed(() =>
185
209
  )
186
210
  const resolvedBindings = computed<ResolvedControlWorkspace['bindings']>(() => ({
187
211
  ...resolvedWorkspace.value.bindings,
212
+ componentBindings: resolvedComponentBindings,
213
+ componentBindingsById: resolvedComponentBindingsById,
188
214
  componentProps: resolvedComponentProps,
189
215
  componentPropsById: resolvedComponentPropsById,
190
216
  }))
217
+ const resolvedTopBarSlot = computed(() => resolvedWorkspace.value.bindings.topBar.value)
191
218
 
192
219
  watch(
193
220
  [() => props.model, () => props.controls, () => props.controlOptions, () => props.initialValues],
@@ -265,6 +292,7 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
265
292
  class="mint-control-workspace-view"
266
293
  :sidebar-position="sidebarPosition"
267
294
  :sidebar-width="sidebarWidth"
295
+ :responsive-sidebar="responsiveSidebar"
268
296
  :floating="floating"
269
297
  >
270
298
  <template #topbar>
@@ -272,19 +300,16 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
272
300
  name="topbar"
273
301
  :workspace="resolvedWorkspace"
274
302
  :bindings="resolvedBindings"
275
- :top-bar="resolvedWorkspace.topBar"
303
+ :top-bar="resolvedTopBarSlot"
276
304
  >
277
305
  <AppTopBar
278
306
  :title="title"
279
307
  :subtitle="subtitle"
280
308
  :variant="topBarVariant"
281
- :tabs="navigation === 'tabs' ? resolvedWorkspace.topBar.tabs : undefined"
282
- :current-tab-id="navigation === 'tabs' ? resolvedWorkspace.topBar.currentTabId : undefined"
283
- :pill-nav="navigation === 'pill' ? resolvedWorkspace.pillNav.items : undefined"
284
- :current-pill-id="navigation === 'pill' ? resolvedWorkspace.pillNav.currentItemId : undefined"
309
+ :pill-nav="resolvedWorkspace.pillNav.items"
310
+ :current-pill-id="resolvedWorkspace.pillNav.currentItemId"
285
311
  :show-settings="showSettings"
286
312
  :settings-config="resolvedWorkspace.topBarSettings.settingsConfig"
287
- @tab-select="resolvedWorkspace.topBar.onTabSelect"
288
313
  @pill-select="resolvedWorkspace.pillNav.onSelect"
289
314
  @settings-values-change="resolvedWorkspace.topBarSettings.onSettingsValuesChange"
290
315
  />
@@ -300,6 +325,10 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
300
325
  >
301
326
  <AppSidebar
302
327
  v-bind="resolvedWorkspace.sidebar"
328
+ :title="sidebarTitle"
329
+ :subtitle="sidebarSubtitle"
330
+ :badge="sidebarBadge"
331
+ :variant="sidebarVariant"
303
332
  :floating="false"
304
333
  :dense="dense"
305
334
  :width="sidebarWidth"
@@ -316,6 +345,8 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
316
345
  :workspace="resolvedWorkspace"
317
346
  :bindings="resolvedBindings"
318
347
  :values="resolvedWorkspace.values"
348
+ :component-bindings="resolvedComponentBindings"
349
+ :component-bindings-by-id="resolvedComponentBindingsById"
319
350
  :component-props="resolvedComponentProps"
320
351
  :component-props-by-id="resolvedComponentPropsById"
321
352
  >
@@ -4,7 +4,7 @@ import { ref, computed, watch } from 'vue'
4
4
  import { useCalendarGrid, type CalendarGridDay } from '../composables/useCalendarGrid'
5
5
  import { useDropdownState } from '../composables/useDropdownState'
6
6
  import { useEventListener } from '../composables/useEventListener'
7
- import CalendarGridPanel from './CalendarGridPanel.vue'
7
+ import CalendarGridPanelInternal from './internal/CalendarGridPanelInternal.vue'
8
8
 
9
9
  interface Props {
10
10
  modelValue?: string
@@ -159,7 +159,7 @@ useEventListener(() => window, 'resize', handleScrollOrResize)
159
159
  aria-modal="true"
160
160
  aria-label="Date picker"
161
161
  >
162
- <CalendarGridPanel
162
+ <CalendarGridPanelInternal
163
163
  :week-days="weekDays"
164
164
  :month-year="monthYear"
165
165
  :days="calendarDays"
@@ -4,7 +4,7 @@ import { computed, watch, nextTick } from 'vue'
4
4
  import { useCalendarGrid, type CalendarGridDay } from '../composables/useCalendarGrid'
5
5
  import { useDropdownState } from '../composables/useDropdownState'
6
6
  import { formatTime, formatTimeSlot, generateTimeSlots, toMinutes } from '../composables/useTimeUtils'
7
- import CalendarGridPanel from './CalendarGridPanel.vue'
7
+ import CalendarGridPanelInternal from './internal/CalendarGridPanelInternal.vue'
8
8
 
9
9
  interface Props {
10
10
  modelValue?: string
@@ -210,7 +210,7 @@ watch(isOpen, (open) => {
210
210
  >
211
211
  <!-- Calendar section -->
212
212
  <div class="mint-datetime-picker__calendar-section">
213
- <CalendarGridPanel
213
+ <CalendarGridPanelInternal
214
214
  :week-days="weekDays"
215
215
  :month-year="monthYear"
216
216
  :days="calendarDays"
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import DoseDesignWorkspaceView from './DoseDesignWorkspaceView.vue'
4
+ import type { DoseDesignControlModelOptions } from '../composables/useControlSchema'
5
+
6
+ type SidebarVariant = 'analysis' | 'default'
7
+
8
+ const initState = () => ({
9
+ title: 'Dose Design Workspace',
10
+ includeMolecularWeight: true,
11
+ plateFormat: 96,
12
+ selectedWells: ['A1', 'A2', 'B1', 'B2'],
13
+ sidebarTitle: 'Dose Controls',
14
+ sidebarSubtitle: 'Plate setup',
15
+ sidebarVariant: 'analysis' as SidebarVariant,
16
+ responsiveSidebar: true,
17
+ showSettings: true,
18
+ })
19
+
20
+ const state = ref(initState())
21
+ const values = ref<Record<string, unknown>>({})
22
+
23
+ function doseOptions(): DoseDesignControlModelOptions {
24
+ return {
25
+ selectedWells: state.value.selectedWells,
26
+ plateFormat: state.value.plateFormat,
27
+ includeMolecularWeight: state.value.includeMolecularWeight,
28
+ }
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <Story title="Workflow/DoseDesignWorkspaceView" :init-state="initState">
34
+ <Variant title="Playground">
35
+ <div style="min-height: 720px; background: var(--bg-secondary, #f8fafc);">
36
+ <DoseDesignWorkspaceView
37
+ v-model="values"
38
+ :title="state.title"
39
+ :dose-design-options="doseOptions()"
40
+ :sidebar-title="state.sidebarTitle"
41
+ :sidebar-subtitle="state.sidebarSubtitle"
42
+ :sidebar-variant="state.sidebarVariant"
43
+ :responsive-sidebar="state.responsiveSidebar"
44
+ :show-settings="state.showSettings"
45
+ :well-plate-props="{
46
+ heatmap: { enabled: true, min: 0, max: 100, colorScale: 'viridis' },
47
+ }"
48
+ />
49
+ </div>
50
+ </Variant>
51
+
52
+ <template #controls>
53
+ <HstText v-model="state.title" title="Title" />
54
+ <HstText v-model="state.sidebarTitle" title="Sidebar title" />
55
+ <HstText v-model="state.sidebarSubtitle" title="Sidebar subtitle" />
56
+ <HstCheckbox v-model="state.includeMolecularWeight" title="Molecular weight" />
57
+ <HstCheckbox v-model="state.responsiveSidebar" title="Responsive sidebar" />
58
+ <HstCheckbox v-model="state.showSettings" title="Settings" />
59
+ <HstSelect
60
+ v-model="state.sidebarVariant"
61
+ title="Sidebar variant"
62
+ :options="[
63
+ { label: 'Analysis', value: 'analysis' },
64
+ { label: 'Default', value: 'default' },
65
+ ]"
66
+ />
67
+ <HstSelect
68
+ v-model="state.plateFormat"
69
+ title="Plate format"
70
+ :options="[
71
+ { label: '96 wells', value: 96 },
72
+ { label: '384 wells', value: 384 },
73
+ ]"
74
+ />
75
+ </template>
76
+ </Story>
77
+ </template>