@morscherlab/mint-sdk 1.0.0-alpha.9 → 1.0.0-beta.2

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 (81) hide show
  1. package/dist/__tests__/components/PluginIcon.test.d.ts +1 -0
  2. package/dist/components/AppTopBar.vue.d.ts +2 -0
  3. package/dist/components/BaseButton.vue.d.ts +1 -1
  4. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  5. package/dist/components/BaseInput.vue.d.ts +1 -1
  6. package/dist/components/BasePill.vue.d.ts +1 -1
  7. package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
  8. package/dist/components/BaseSelect.vue.d.ts +1 -1
  9. package/dist/components/BaseSlider.vue.d.ts +1 -1
  10. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  11. package/dist/components/BaseToggle.vue.d.ts +1 -1
  12. package/dist/components/ColorSlider.vue.d.ts +1 -1
  13. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  14. package/dist/components/DatePicker.vue.d.ts +1 -1
  15. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  16. package/dist/components/Divider.vue.d.ts +1 -1
  17. package/dist/components/DropdownButton.vue.d.ts +1 -1
  18. package/dist/components/FileUploader.vue.d.ts +1 -1
  19. package/dist/components/FormulaInput.vue.d.ts +1 -1
  20. package/dist/components/IconButton.vue.d.ts +1 -1
  21. package/dist/components/LoadingSpinner.vue.d.ts +1 -1
  22. package/dist/components/MultiSelect.vue.d.ts +1 -1
  23. package/dist/components/NumberInput.vue.d.ts +1 -1
  24. package/dist/components/PluginIcon.vue.d.ts +11 -0
  25. package/dist/components/ProgressBar.vue.d.ts +1 -1
  26. package/dist/components/ReagentEditor.vue.d.ts +1 -1
  27. package/dist/components/ResourceCard.vue.d.ts +1 -1
  28. package/dist/components/SampleSelector.vue.d.ts +1 -1
  29. package/dist/components/ScientificNumber.vue.d.ts +1 -1
  30. package/dist/components/SegmentedControl.vue.d.ts +1 -1
  31. package/dist/components/SettingsModal.vue.d.ts +22 -2
  32. package/dist/components/TagsInput.vue.d.ts +1 -1
  33. package/dist/components/TimePicker.vue.d.ts +1 -1
  34. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  35. package/dist/components/UnitInput.vue.d.ts +1 -1
  36. package/dist/components/WellPlate.vue.d.ts +1 -1
  37. package/dist/components/index.d.ts +1 -0
  38. package/dist/components/index.js +3 -3
  39. package/dist/{components-CzbQQPCb.js → components-_XqPEhP9.js} +572 -362
  40. package/dist/components-_XqPEhP9.js.map +1 -0
  41. package/dist/composables/index.js +2 -2
  42. package/dist/composables/usePlatformContext.d.ts +3 -0
  43. package/dist/{composables-BXklV5ii.js → composables-tiZqLu1M.js} +2 -2
  44. package/dist/{composables-BXklV5ii.js.map → composables-tiZqLu1M.js.map} +1 -1
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +4 -4
  47. package/dist/install.js +2 -2
  48. package/dist/stores/auth.d.ts +1 -1
  49. package/dist/styles.css +896 -553
  50. package/dist/types/components.d.ts +39 -0
  51. package/dist/types/index.d.ts +1 -1
  52. package/dist/types/platform.d.ts +1 -0
  53. package/dist/{useScheduleDrag-CxBeqYcu.js → useScheduleDrag-CA9sGNJG.js} +4000 -4000
  54. package/dist/useScheduleDrag-CA9sGNJG.js.map +1 -0
  55. package/package.json +1 -1
  56. package/src/__tests__/components/AppTopBar.test.ts +31 -13
  57. package/src/__tests__/components/PluginIcon.test.ts +119 -0
  58. package/src/components/AppTopBar.vue +32 -27
  59. package/src/components/PluginIcon.story.vue +71 -0
  60. package/src/components/PluginIcon.vue +88 -0
  61. package/src/components/SettingsModal.story.vue +337 -45
  62. package/src/components/SettingsModal.vue +251 -64
  63. package/src/components/index.ts +1 -0
  64. package/src/index.ts +4 -0
  65. package/src/styles/components/app-pill-nav.css +1 -2
  66. package/src/styles/components/app-top-bar.css +1 -2
  67. package/src/styles/components/button.css +3 -7
  68. package/src/styles/components/dropdown-button.css +4 -4
  69. package/src/styles/components/input.css +4 -5
  70. package/src/styles/components/number-input.css +3 -3
  71. package/src/styles/components/plugin-icon.css +38 -0
  72. package/src/styles/components/segmented-control.css +4 -7
  73. package/src/styles/components/settings-modal.css +184 -0
  74. package/src/styles/components/tabs.css +1 -2
  75. package/src/styles/components/textarea.css +4 -5
  76. package/src/styles/components/unit-input.css +3 -3
  77. package/src/types/components.ts +42 -0
  78. package/src/types/index.ts +3 -0
  79. package/src/types/platform.ts +1 -0
  80. package/dist/components-CzbQQPCb.js.map +0 -1
  81. package/dist/useScheduleDrag-CxBeqYcu.js.map +0 -1
@@ -1,11 +1,24 @@
1
1
  <script setup lang="ts">
2
- import { ref } from 'vue'
2
+ import { reactive, ref } from 'vue'
3
3
  import SettingsModal from './SettingsModal.vue'
4
- import type { SettingsTab } from '../types'
4
+ import FormField from './FormField.vue'
5
+ import BaseInput from './BaseInput.vue'
6
+ import BaseSelect from './BaseSelect.vue'
7
+ import BaseCheckbox from './BaseCheckbox.vue'
8
+ import BaseToggle from './BaseToggle.vue'
9
+ import NumberInput from './NumberInput.vue'
10
+ import type {
11
+ SettingsTab,
12
+ SettingsModalLayout,
13
+ SelectOption,
14
+ SettingsModalSchema,
15
+ } from '../types'
5
16
 
6
17
  const basicOpen = ref(false)
7
18
  const customOpen = ref(false)
8
19
  const appearanceOnlyOpen = ref(false)
20
+ const verticalOpen = ref(false)
21
+ const schemaOpen = ref(false)
9
22
 
10
23
  const customTabs: SettingsTab[] = [
11
24
  { id: 'general', label: 'General' },
@@ -13,7 +26,190 @@ const customTabs: SettingsTab[] = [
13
26
  { id: 'advanced', label: 'Advanced' },
14
27
  ]
15
28
 
29
+ // Reactive form state for the "With Custom Tabs" variant
30
+ const customForm = reactive({
31
+ pluginName: 'IC50 Calculator',
32
+ defaultModel: '4pl',
33
+ emailComplete: true,
34
+ emailQc: false,
35
+ inApp: true,
36
+ maxIterations: 1000,
37
+ threshold: 0.0001,
38
+ })
39
+
40
+ const modelOptions: SelectOption<string>[] = [
41
+ { value: '4pl', label: '4-Parameter Logistic' },
42
+ { value: '3pl', label: '3-Parameter Logistic' },
43
+ { value: 'linear', label: 'Linear' },
44
+ ]
45
+
46
+ // Inline SVG icon strings — passed via tab.icon and rendered with v-html.
47
+ // Using raw strings keeps the story self-contained without an icon-component dep.
48
+ const iconGeneral = `<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`
49
+ const iconModel = `<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`
50
+ const iconCurve = `<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>`
51
+ const iconQc = `<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`
52
+ const iconBell = `<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>`
53
+ const iconCode = `<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`
54
+
55
+ const verticalTabs: SettingsTab[] = [
56
+ { id: 'general', label: 'General', icon: iconGeneral, description: 'Plugin name, defaults, locale' },
57
+ { id: 'model', label: 'Model Parameters', icon: iconModel, description: 'Curve choice and bounds' },
58
+ { id: 'fitting', label: 'Curve Fitting', icon: iconCurve, description: 'Optimizer, weights, transforms' },
59
+ { id: 'qc', label: 'Quality Control', icon: iconQc, description: 'Outlier detection and thresholds' },
60
+ { id: 'notifications', label: 'Notifications', icon: iconBell, description: 'Email and in-app alerts' },
61
+ { id: 'advanced', label: 'Advanced', icon: iconCode, description: 'Numerics, debug, experimental' },
62
+ ]
63
+
64
+ // Reactive form state for the "Vertical Layout" variant
65
+ const vForm = reactive({
66
+ pluginName: 'IC50 Calculator',
67
+ locale: 'en-US',
68
+ curveModel: '4pl',
69
+ bottom: 0,
70
+ top: 100,
71
+ optimizer: 'lm',
72
+ logTransform: true,
73
+ inverseVariance: false,
74
+ outlier: 'grubbs',
75
+ minR2: 0.95,
76
+ emailComplete: true,
77
+ emailQc: false,
78
+ inApp: true,
79
+ slack: false,
80
+ maxIterations: 1000,
81
+ threshold: 0.0001,
82
+ bayesian: false,
83
+ })
84
+
85
+ const localeOptions: SelectOption<string>[] = [
86
+ { value: 'en-US', label: 'English (US)' },
87
+ { value: 'en-GB', label: 'English (UK)' },
88
+ { value: 'de-DE', label: 'Deutsch' },
89
+ ]
90
+
91
+ const curveModelOptions: SelectOption<string>[] = [
92
+ { value: '4pl', label: '4-Parameter Logistic (4PL)' },
93
+ { value: '3pl', label: '3-Parameter Logistic (3PL)' },
94
+ { value: '5pl', label: '5-Parameter Logistic (5PL)' },
95
+ { value: 'linear', label: 'Linear' },
96
+ ]
97
+
98
+ const optimizerOptions: SelectOption<string>[] = [
99
+ { value: 'lm', label: 'Levenberg-Marquardt' },
100
+ { value: 'trf', label: 'Trust Region Reflective' },
101
+ { value: 'nm', label: 'Nelder-Mead' },
102
+ ]
103
+
104
+ const outlierOptions: SelectOption<string>[] = [
105
+ { value: 'grubbs', label: "Grubbs' test (α=0.05)" },
106
+ { value: 'iqr', label: 'Tukey IQR' },
107
+ { value: 'none', label: 'None' },
108
+ ]
109
+
110
+ // ─────────── Schema-driven (recommended) ───────────
111
+ // Plugin authors describe parameters once; the modal auto-renders every field
112
+ // via the SDK's FormFieldRenderer registry. Compare to the "Vertical Layout"
113
+ // variant above, which builds the same surface manually.
114
+ const schemaSettings = ref<Record<string, unknown>>({
115
+ pluginName: 'IC50 Calculator',
116
+ locale: 'en-US',
117
+ curveModel: '4pl',
118
+ bottom: 0,
119
+ top: 100,
120
+ optimizer: 'lm',
121
+ logTransform: true,
122
+ inverseVariance: false,
123
+ outlier: 'grubbs',
124
+ minR2: 0.95,
125
+ emailComplete: true,
126
+ emailQc: false,
127
+ inApp: true,
128
+ slack: false,
129
+ maxIterations: 1000,
130
+ threshold: 0.0001,
131
+ bayesian: false,
132
+ })
133
+
134
+ const settingsSchema: SettingsModalSchema = {
135
+ groups: [
136
+ {
137
+ id: 'general',
138
+ label: 'General',
139
+ icon: iconGeneral,
140
+ description: 'Plugin name, defaults, locale',
141
+ fields: [
142
+ { name: 'pluginName', label: 'Plugin Display Name', type: 'text', hint: "Shown in the platform's plugin list and breadcrumbs." },
143
+ { name: 'locale', label: 'Locale', type: 'select', props: { options: localeOptions } },
144
+ ],
145
+ },
146
+ {
147
+ id: 'model',
148
+ label: 'Model Parameters',
149
+ icon: iconModel,
150
+ description: 'Curve choice and bounds',
151
+ columns: 2,
152
+ fields: [
153
+ { name: 'curveModel', label: 'Default Curve Model', type: 'select', colSpan: 2, props: { options: curveModelOptions } },
154
+ { name: 'bottom', label: 'Bottom bound', type: 'number', props: { step: 1 } },
155
+ { name: 'top', label: 'Top bound', type: 'number', props: { step: 1 } },
156
+ ],
157
+ },
158
+ {
159
+ id: 'fitting',
160
+ label: 'Curve Fitting',
161
+ icon: iconCurve,
162
+ description: 'Optimizer, weights, transforms',
163
+ fields: [
164
+ { name: 'optimizer', label: 'Optimizer', type: 'select', props: { options: optimizerOptions } },
165
+ { name: 'logTransform', label: 'Apply log-dose transform', type: 'toggle' },
166
+ { name: 'inverseVariance', label: 'Weight by inverse variance', type: 'toggle' },
167
+ ],
168
+ },
169
+ {
170
+ id: 'qc',
171
+ label: 'Quality Control',
172
+ icon: iconQc,
173
+ description: 'Outlier detection and thresholds',
174
+ fields: [
175
+ { name: 'outlier', label: 'Outlier method', type: 'select', props: { options: outlierOptions } },
176
+ {
177
+ name: 'minR2',
178
+ label: 'Min R² to accept fit',
179
+ type: 'number',
180
+ hint: 'Fits below this threshold are flagged for review.',
181
+ props: { step: 0.01, min: 0, max: 1 },
182
+ },
183
+ ],
184
+ },
185
+ {
186
+ id: 'notifications',
187
+ label: 'Notifications',
188
+ icon: iconBell,
189
+ description: 'Email and in-app alerts',
190
+ fields: [
191
+ { name: 'emailComplete', label: 'Email on analysis complete', type: 'checkbox' },
192
+ { name: 'emailQc', label: 'Email on QC failure', type: 'checkbox' },
193
+ { name: 'inApp', label: 'In-app notifications', type: 'checkbox' },
194
+ { name: 'slack', label: 'Slack mentions', type: 'checkbox' },
195
+ ],
196
+ },
197
+ {
198
+ id: 'advanced',
199
+ label: 'Advanced',
200
+ icon: iconCode,
201
+ description: 'Numerics, debug, experimental',
202
+ fields: [
203
+ { name: 'maxIterations', label: 'Max iterations', type: 'number', props: { step: 100, min: 1 } },
204
+ { name: 'threshold', label: 'Convergence threshold', type: 'number', props: { step: 0.0001 } },
205
+ { name: 'bayesian', label: 'Enable experimental Bayesian fitting', type: 'toggle' },
206
+ ],
207
+ },
208
+ ],
209
+ }
210
+
16
211
  const sizes: Array<'md' | 'lg' | 'xl'> = ['md', 'lg', 'xl']
212
+ const layouts: SettingsModalLayout[] = ['horizontal', 'vertical']
17
213
  </script>
18
214
 
19
215
  <template>
@@ -33,6 +229,7 @@ const sizes: Array<'md' | 'lg' | 'xl'> = ['md', 'lg', 'xl']
33
229
  :title="state.title"
34
230
  :show-appearance="state.showAppearance"
35
231
  :size="state.size"
232
+ :layout="state.layout"
36
233
  />
37
234
  </div>
38
235
  </template>
@@ -45,6 +242,11 @@ const sizes: Array<'md' | 'lg' | 'xl'> = ['md', 'lg', 'xl']
45
242
  title="Size"
46
243
  :options="sizes.map(s => ({ label: s, value: s }))"
47
244
  />
245
+ <HstSelect
246
+ v-model="state.layout"
247
+ title="Layout"
248
+ :options="layouts.map(l => ({ label: l, value: l }))"
249
+ />
48
250
  </template>
49
251
  </Variant>
50
252
 
@@ -83,63 +285,153 @@ const sizes: Array<'md' | 'lg' | 'xl'> = ['md', 'lg', 'xl']
83
285
  >
84
286
  <template #tab-general>
85
287
  <div style="display: flex; flex-direction: column; gap: 1rem;">
86
- <div>
87
- <label style="display: block; font-size: 0.875rem; font-weight: 500; color: var(--text-primary, #1e293b); margin-bottom: 0.25rem;">Plugin Name</label>
88
- <input
89
- type="text"
90
- value="IC50 Calculator"
91
- style="width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; font-size: 0.875rem; background: var(--bg-primary, #fff); color: var(--text-primary, #1e293b); box-sizing: border-box;"
92
- />
93
- </div>
94
- <div>
95
- <label style="display: block; font-size: 0.875rem; font-weight: 500; color: var(--text-primary, #1e293b); margin-bottom: 0.25rem;">Default Model</label>
96
- <select style="width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; font-size: 0.875rem; background: var(--bg-primary, #fff); color: var(--text-primary, #1e293b); box-sizing: border-box;">
97
- <option>4-Parameter Logistic</option>
98
- <option>3-Parameter Logistic</option>
99
- <option>Linear</option>
100
- </select>
101
- </div>
288
+ <FormField label="Plugin Name">
289
+ <BaseInput v-model="customForm.pluginName" />
290
+ </FormField>
291
+ <FormField label="Default Model">
292
+ <BaseSelect v-model="customForm.defaultModel" :options="modelOptions" />
293
+ </FormField>
102
294
  </div>
103
295
  </template>
104
296
  <template #tab-notifications>
105
- <div style="display: flex; flex-direction: column; gap: 0.75rem;">
106
- <label style="display: flex; align-items: center; gap: 0.5rem; color: var(--text-primary, #1e293b); font-size: 0.875rem;">
107
- <input type="checkbox" checked /> Email on analysis complete
108
- </label>
109
- <label style="display: flex; align-items: center; gap: 0.5rem; color: var(--text-primary, #1e293b); font-size: 0.875rem;">
110
- <input type="checkbox" /> Email on QC failure
111
- </label>
112
- <label style="display: flex; align-items: center; gap: 0.5rem; color: var(--text-primary, #1e293b); font-size: 0.875rem;">
113
- <input type="checkbox" checked /> In-app notifications
114
- </label>
297
+ <div style="display: flex; flex-direction: column; gap: 0.625rem;">
298
+ <BaseCheckbox v-model="customForm.emailComplete" label="Email on analysis complete" />
299
+ <BaseCheckbox v-model="customForm.emailQc" label="Email on QC failure" />
300
+ <BaseCheckbox v-model="customForm.inApp" label="In-app notifications" />
115
301
  </div>
116
302
  </template>
117
303
  <template #tab-advanced>
118
304
  <div style="display: flex; flex-direction: column; gap: 1rem;">
119
- <p style="margin: 0; font-size: 0.875rem; color: var(--text-muted, #94a3b8);">
305
+ <p style="margin: 0; font-size: 0.8125rem; color: var(--text-muted);">
120
306
  Advanced settings for curve fitting and data processing.
121
307
  </p>
122
- <div>
123
- <label style="display: block; font-size: 0.875rem; font-weight: 500; color: var(--text-primary, #1e293b); margin-bottom: 0.25rem;">Max Iterations</label>
124
- <input
125
- type="number"
126
- value="1000"
127
- style="width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; font-size: 0.875rem; background: var(--bg-primary, #fff); color: var(--text-primary, #1e293b); box-sizing: border-box;"
128
- />
129
- </div>
130
- <div>
131
- <label style="display: block; font-size: 0.875rem; font-weight: 500; color: var(--text-primary, #1e293b); margin-bottom: 0.25rem;">Convergence Threshold</label>
132
- <input
133
- type="number"
134
- value="0.0001"
135
- step="0.0001"
136
- style="width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; font-size: 0.875rem; background: var(--bg-primary, #fff); color: var(--text-primary, #1e293b); box-sizing: border-box;"
137
- />
308
+ <FormField label="Max Iterations">
309
+ <NumberInput v-model="customForm.maxIterations" :step="100" :min="1" />
310
+ </FormField>
311
+ <FormField label="Convergence Threshold">
312
+ <NumberInput v-model="customForm.threshold" :step="0.0001" />
313
+ </FormField>
314
+ </div>
315
+ </template>
316
+ </SettingsModal>
317
+ </div>
318
+ </Variant>
319
+
320
+ <Variant title="Vertical Layout (Complex Plugin)">
321
+ <div style="padding: 2rem; display: flex; align-items: center; justify-content: center;">
322
+ <button
323
+ type="button"
324
+ style="padding: 0.5rem 1rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; background: var(--bg-card, #fff); color: var(--text-primary, #1e293b); cursor: pointer; font-size: 0.875rem;"
325
+ @click="verticalOpen = true"
326
+ >
327
+ Open Settings (Vertical)
328
+ </button>
329
+ <SettingsModal
330
+ v-model="verticalOpen"
331
+ title="IC50 Plugin Settings"
332
+ :tabs="verticalTabs"
333
+ :show-appearance="true"
334
+ size="xl"
335
+ layout="vertical"
336
+ >
337
+ <template #tab-general>
338
+ <div style="display: flex; flex-direction: column; gap: 1rem;">
339
+ <FormField label="Plugin Display Name" hint="Shown in the platform's plugin list and breadcrumbs.">
340
+ <BaseInput v-model="vForm.pluginName" />
341
+ </FormField>
342
+ <FormField label="Locale">
343
+ <BaseSelect v-model="vForm.locale" :options="localeOptions" />
344
+ </FormField>
345
+ </div>
346
+ </template>
347
+
348
+ <template #tab-model>
349
+ <div style="display: flex; flex-direction: column; gap: 1rem;">
350
+ <FormField label="Default Curve Model">
351
+ <BaseSelect v-model="vForm.curveModel" :options="curveModelOptions" />
352
+ </FormField>
353
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;">
354
+ <FormField label="Bottom bound">
355
+ <NumberInput v-model="vForm.bottom" :step="1" />
356
+ </FormField>
357
+ <FormField label="Top bound">
358
+ <NumberInput v-model="vForm.top" :step="1" />
359
+ </FormField>
138
360
  </div>
139
361
  </div>
140
362
  </template>
363
+
364
+ <template #tab-fitting>
365
+ <div style="display: flex; flex-direction: column; gap: 1rem;">
366
+ <FormField label="Optimizer">
367
+ <BaseSelect v-model="vForm.optimizer" :options="optimizerOptions" />
368
+ </FormField>
369
+ <BaseToggle v-model="vForm.logTransform" label="Apply log-dose transform" />
370
+ <BaseToggle v-model="vForm.inverseVariance" label="Weight by inverse variance" />
371
+ </div>
372
+ </template>
373
+
374
+ <template #tab-qc>
375
+ <div style="display: flex; flex-direction: column; gap: 1rem;">
376
+ <FormField label="Outlier method">
377
+ <BaseSelect v-model="vForm.outlier" :options="outlierOptions" />
378
+ </FormField>
379
+ <FormField label="Min R² to accept fit" hint="Fits below this threshold are flagged for review.">
380
+ <NumberInput v-model="vForm.minR2" :step="0.01" :min="0" :max="1" />
381
+ </FormField>
382
+ </div>
383
+ </template>
384
+
385
+ <template #tab-notifications>
386
+ <div style="display: flex; flex-direction: column; gap: 0.625rem;">
387
+ <BaseCheckbox v-model="vForm.emailComplete" label="Email on analysis complete" />
388
+ <BaseCheckbox v-model="vForm.emailQc" label="Email on QC failure" />
389
+ <BaseCheckbox v-model="vForm.inApp" label="In-app notifications" />
390
+ <BaseCheckbox v-model="vForm.slack" label="Slack mentions" />
391
+ </div>
392
+ </template>
393
+
394
+ <template #tab-advanced>
395
+ <div style="display: flex; flex-direction: column; gap: 1rem;">
396
+ <FormField label="Max iterations">
397
+ <NumberInput v-model="vForm.maxIterations" :step="100" :min="1" />
398
+ </FormField>
399
+ <FormField label="Convergence threshold">
400
+ <NumberInput v-model="vForm.threshold" :step="0.0001" />
401
+ </FormField>
402
+ <BaseToggle v-model="vForm.bayesian" label="Enable experimental Bayesian fitting" />
403
+ </div>
404
+ </template>
141
405
  </SettingsModal>
142
406
  </div>
143
407
  </Variant>
408
+
409
+ <Variant title="Schema-Driven (Auto-Render)">
410
+ <div style="padding: 2rem; display: flex; flex-direction: column; gap: 1rem; align-items: center;">
411
+ <button
412
+ type="button"
413
+ style="padding: 0.5rem 1rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; background: var(--bg-card, #fff); color: var(--text-primary, #1e293b); cursor: pointer; font-size: 0.875rem;"
414
+ @click="schemaOpen = true"
415
+ >
416
+ Open Settings (from schema)
417
+ </button>
418
+ <p style="margin: 0; font-size: 0.75rem; color: var(--text-muted); max-width: 32rem; text-align: center; line-height: 1.5;">
419
+ Plugin authors declare a <code style="font-size: 0.6875rem; padding: 0.125rem 0.25rem; background: var(--bg-tertiary); border-radius: 0.25rem;">SettingsModalSchema</code> once; the modal auto-renders SDK form components for every field. No per-tab template needed.
420
+ </p>
421
+ <details style="font-size: 0.75rem; color: var(--text-secondary); max-width: 36rem; width: 100%;">
422
+ <summary style="cursor: pointer; user-select: none;">Live values</summary>
423
+ <pre style="margin: 0.5rem 0 0; padding: 0.75rem; background: var(--bg-tertiary); border-radius: 0.375rem; font-size: 0.6875rem; overflow-x: auto;">{{ JSON.stringify(schemaSettings, null, 2) }}</pre>
424
+ </details>
425
+ <SettingsModal
426
+ v-model="schemaOpen"
427
+ v-model:values="schemaSettings"
428
+ title="IC50 Plugin Settings"
429
+ :schema="settingsSchema"
430
+ :show-appearance="true"
431
+ size="xl"
432
+ layout="vertical"
433
+ />
434
+ </div>
435
+ </Variant>
144
436
  </Story>
145
437
  </template>