@morscherlab/mint-sdk 1.0.0-rc.5 → 1.0.0-rc.7

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 (55) hide show
  1. package/dist/components/AppSidebar.vue.d.ts +6 -3
  2. package/dist/components/BaseCheckbox.vue.d.ts +6 -1
  3. package/dist/components/BasePill.vue.d.ts +1 -1
  4. package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
  5. package/dist/components/BaseToggle.vue.d.ts +6 -1
  6. package/dist/components/CollapsibleCard.vue.d.ts +9 -0
  7. package/dist/components/ControlWorkspaceView.vue.d.ts +1 -1
  8. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
  9. package/dist/components/PluginWorkspaceView.vue.d.ts +5 -1
  10. package/dist/components/index.js +2 -2
  11. package/dist/{components-DtHA2bgp.js → components-Blx4MG--.js} +565 -1477
  12. package/dist/components-Blx4MG--.js.map +1 -0
  13. package/dist/composables/controlSchemaTypes.d.ts +7 -1
  14. package/dist/composables/index.js +3 -3
  15. package/dist/{composables-Dlg8jenH.js → composables-CHDjDIQT.js} +2 -2
  16. package/dist/{composables-Dlg8jenH.js.map → composables-CHDjDIQT.js.map} +1 -1
  17. package/dist/index.js +4 -4
  18. package/dist/install.js +2 -2
  19. package/dist/styles.css +867 -126
  20. package/dist/templates/index.js +1 -1
  21. package/dist/{templates-DtdUvJ4c.js → templates-DSbHJC4v.js} +1351 -120
  22. package/dist/templates-DSbHJC4v.js.map +1 -0
  23. package/dist/types/components.d.ts +12 -0
  24. package/dist/types/form-builder.d.ts +3 -0
  25. package/dist/types/index.d.ts +1 -1
  26. package/dist/{useProtocolTemplates-Bm5vyH4_.js → useProtocolTemplates-BbPOYPzO.js} +2 -2
  27. package/dist/{useProtocolTemplates-Bm5vyH4_.js.map → useProtocolTemplates-BbPOYPzO.js.map} +1 -1
  28. package/package.json +1 -1
  29. package/src/__tests__/components/AppSidebar.test.ts +67 -0
  30. package/src/__tests__/components/CollapsibleCard.test.ts +47 -0
  31. package/src/__tests__/components/FormBuilder.test.ts +57 -0
  32. package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +2 -0
  33. package/src/components/AppSidebar.story.vue +79 -6
  34. package/src/components/AppSidebar.vue +72 -0
  35. package/src/components/BaseCheckbox.story.vue +27 -0
  36. package/src/components/BaseCheckbox.vue +63 -1
  37. package/src/components/BaseToggle.story.vue +27 -0
  38. package/src/components/BaseToggle.vue +66 -1
  39. package/src/components/CollapsibleCard.vue +123 -45
  40. package/src/components/FormBuilder.story.vue +13 -0
  41. package/src/components/PluginWorkspaceView.shell.ts +1 -0
  42. package/src/components/PluginWorkspaceView.vue +3 -0
  43. package/src/components/internal/FormFieldRendererInternal.vue +23 -5
  44. package/src/composables/controlSchemaAdapters.ts +3 -0
  45. package/src/composables/controlSchemaFormFields.ts +3 -0
  46. package/src/composables/controlSchemaTypes.ts +8 -0
  47. package/src/styles/components/app-sidebar.css +134 -6
  48. package/src/styles/components/checkbox.css +87 -0
  49. package/src/styles/components/collapsible-card.css +154 -14
  50. package/src/styles/components/toggle.css +80 -0
  51. package/src/types/components.ts +21 -0
  52. package/src/types/form-builder.ts +3 -0
  53. package/src/types/index.ts +2 -0
  54. package/dist/components-DtHA2bgp.js.map +0 -1
  55. package/dist/templates-DtdUvJ4c.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.0.0-rc.5",
3
+ "version": "1.0.0-rc.7",
4
4
  "description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -238,6 +238,46 @@ describe('AppSidebar', () => {
238
238
  expect(wrapper.findAllComponents(CollapsibleCard)).toHaveLength(0)
239
239
  })
240
240
 
241
+ it('renders a default collapsed rail from active sections', () => {
242
+ const wrapper = mount(AppSidebar, {
243
+ props: {
244
+ panels: {
245
+ analysis: [
246
+ {
247
+ id: 'filters',
248
+ label: 'Feature Filters',
249
+ icon: 'M3 4.5h18m-15 6h12m-9 6h6',
250
+ badge: 2,
251
+ badgeTone: 'warning',
252
+ },
253
+ ],
254
+ },
255
+ activeView: 'analysis',
256
+ collapsible: true,
257
+ collapsed: true,
258
+ },
259
+ })
260
+
261
+ expect(wrapper.find('.mint-sidebar__rail-item').exists()).toBe(true)
262
+ expect(wrapper.find('.mint-sidebar__rail-svg').exists()).toBe(true)
263
+ expect(wrapper.find('.mint-sidebar__rail-badge').text()).toBe('2')
264
+ expect(wrapper.find('.mint-sidebar__rail-badge').classes()).toContain('mint-sidebar__rail-badge--warning')
265
+ })
266
+
267
+ it('expands when a default collapsed rail item is clicked', async () => {
268
+ const wrapper = mount(AppSidebar, {
269
+ props: {
270
+ panels: samplePanels,
271
+ activeView: 'analysis',
272
+ collapsible: true,
273
+ defaultCollapsed: true,
274
+ },
275
+ })
276
+
277
+ await wrapper.get('.mint-sidebar__rail-item').trigger('click')
278
+ expect(wrapper.emitted('update:collapsed')).toEqual([[false]])
279
+ })
280
+
241
281
  it('renders an empty analysis shell for route-owned sidebar content', () => {
242
282
  const wrapper = mount(AppSidebar, {
243
283
  props: {
@@ -297,6 +337,33 @@ describe('AppSidebar', () => {
297
337
  expect(cards[0].props('icon')).toBe('⚙')
298
338
  })
299
339
 
340
+ it('passes section badges and actions to CollapsibleCard and re-emits actions', async () => {
341
+ const wrapper = mount(AppSidebar, {
342
+ props: {
343
+ panels: {
344
+ analysis: [
345
+ {
346
+ id: 'filters',
347
+ label: 'Feature Filters',
348
+ badge: 4,
349
+ badgeTone: 'cta',
350
+ actions: [{ id: 'reset', label: 'Reset filters' }],
351
+ },
352
+ ],
353
+ },
354
+ activeView: 'analysis',
355
+ },
356
+ global: { components: { CollapsibleCard } },
357
+ })
358
+
359
+ const card = wrapper.findComponent(CollapsibleCard)
360
+ expect(card.props('badge')).toBe(4)
361
+ expect(card.props('badgeTone')).toBe('cta')
362
+
363
+ await card.vm.$emit('action', 'reset')
364
+ expect(wrapper.emitted('section-action')).toEqual([['filters', 'reset']])
365
+ })
366
+
300
367
  it('should respect defaultOpen on sections', () => {
301
368
  const wrapper = mount(AppSidebar, {
302
369
  props: { panels: samplePanels, activeView: 'analysis' },
@@ -423,6 +423,53 @@ describe('CollapsibleCard', () => {
423
423
  })
424
424
  })
425
425
 
426
+ describe('badges and header actions', () => {
427
+ it('renders a compact badge in the header controls area', () => {
428
+ const wrapper = mount(CollapsibleCard, {
429
+ props: { title: 'Feature Filters', badge: 3, badgeTone: 'warning' },
430
+ })
431
+
432
+ const badge = wrapper.get('.mint-collapsible-card__badge')
433
+ expect(badge.text()).toBe('3')
434
+ expect(badge.classes()).toContain('mint-collapsible-card__badge--warning')
435
+ })
436
+
437
+ it('renders icon-only header actions and emits their ids', async () => {
438
+ const wrapper = mount(CollapsibleCard, {
439
+ props: {
440
+ title: 'Feature Filters',
441
+ actions: [
442
+ {
443
+ id: 'reset',
444
+ label: 'Reset filters',
445
+ icon: 'M16.023 9.348h4.992M2.985 19.644v-4.992',
446
+ tone: 'neutral',
447
+ },
448
+ ],
449
+ },
450
+ })
451
+
452
+ const action = wrapper.get('.mint-collapsible-card__action')
453
+ expect(action.attributes('aria-label')).toBe('Reset filters')
454
+ expect(wrapper.find('.mint-collapsible-card__action-icon').exists()).toBe(true)
455
+
456
+ await action.trigger('click')
457
+ expect(wrapper.emitted('action')).toEqual([['reset']])
458
+ })
459
+
460
+ it('does not emit disabled actions', async () => {
461
+ const wrapper = mount(CollapsibleCard, {
462
+ props: {
463
+ title: 'Feature Filters',
464
+ actions: [{ id: 'reset', label: 'Reset filters', disabled: true }],
465
+ },
466
+ })
467
+
468
+ await wrapper.get('.mint-collapsible-card__action').trigger('click')
469
+ expect(wrapper.emitted('action')).toBeUndefined()
470
+ })
471
+ })
472
+
426
473
  describe('content slot', () => {
427
474
  it('should render default slot content', () => {
428
475
  const wrapper = mount(CollapsibleCard, {
@@ -3,6 +3,7 @@ import { nextTick } from 'vue'
3
3
  import { describe, expect, it } from 'vitest'
4
4
  import FormBuilder from '../../components/FormBuilder.vue'
5
5
  import FormActions from '../../components/FormActions.vue'
6
+ import Tooltip from '../../components/Tooltip.vue'
6
7
  import { defineControlModel, useControlWorkspace, type ControlSchema } from '../../composables/useControlSchema'
7
8
  import type { FormSchema } from '../../types/form-builder'
8
9
 
@@ -286,6 +287,62 @@ describe('FormBuilder', () => {
286
287
  expect(wrapper.find('.mint-form-actions').exists()).toBe(false)
287
288
  })
288
289
 
290
+ it('renders boolean fields as compact choice rows', async () => {
291
+ const schema: FormSchema = {
292
+ sections: [
293
+ {
294
+ id: 'display',
295
+ title: 'Display',
296
+ fields: [
297
+ {
298
+ name: 'showOutliers',
299
+ label: 'Show outliers',
300
+ type: 'toggle',
301
+ defaultValue: true,
302
+ hint: 'Keep flagged peaks visible',
303
+ icon: ['M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'],
304
+ },
305
+ {
306
+ name: 'includeMetadata',
307
+ label: 'Include metadata',
308
+ type: 'checkbox',
309
+ defaultValue: false,
310
+ hint: 'Add method context',
311
+ icon: ['M12 3c4.418 0 8 1.343 8 3s-3.582 3-8 3-8-1.343-8-3z'],
312
+ },
313
+ ],
314
+ },
315
+ ],
316
+ }
317
+
318
+ const wrapper = mount(FormBuilder, {
319
+ props: { schema, showActions: false },
320
+ })
321
+
322
+ const toggle = wrapper.get('.mint-toggle--row')
323
+ const checkbox = wrapper.get('.mint-checkbox--row')
324
+
325
+ expect(toggle.text()).toContain('Show outliers')
326
+ expect(toggle.text()).not.toContain('Keep flagged peaks visible')
327
+ expect(toggle.find('.mint-toggle__row-svg').exists()).toBe(true)
328
+ expect(wrapper.findAll('.mint-form-field__label')).toHaveLength(0)
329
+
330
+ expect(checkbox.text()).toContain('Include metadata')
331
+ expect(checkbox.text()).not.toContain('Add method context')
332
+ expect(checkbox.find('.mint-checkbox__row-svg').exists()).toBe(true)
333
+ expect(wrapper.findAllComponents(Tooltip).map(tooltip => tooltip.props('text'))).toEqual([
334
+ 'Keep flagged peaks visible',
335
+ 'Add method context',
336
+ ])
337
+
338
+ await toggle.trigger('click')
339
+
340
+ expect(wrapper.emitted('update:modelValue')?.at(-1)?.[0]).toMatchObject({
341
+ showOutliers: false,
342
+ includeMetadata: false,
343
+ })
344
+ })
345
+
289
346
  it('renders default actions by default', () => {
290
347
  const wrapper = mount(FormBuilder, {
291
348
  props: { controls },
@@ -12,6 +12,7 @@ describe('PluginWorkspaceView shell helpers', () => {
12
12
  actions: undefined,
13
13
  'settings-tab-analysis': undefined,
14
14
  header: undefined,
15
+ 'collapsed-footer': undefined,
15
16
  'section-import': undefined,
16
17
  unrelated: undefined,
17
18
  }
@@ -22,6 +23,7 @@ describe('PluginWorkspaceView shell helpers', () => {
22
23
  ])
23
24
  expect(getPluginWorkspaceSidebarSlotNames(slots)).toEqual([
24
25
  'header',
26
+ 'collapsed-footer',
25
27
  'section-import',
26
28
  ])
27
29
  })
@@ -4,6 +4,7 @@ import AppSidebar from './AppSidebar.vue'
4
4
  import BaseSlider from './BaseSlider.vue'
5
5
  import BaseSelect from './BaseSelect.vue'
6
6
  import BaseToggle from './BaseToggle.vue'
7
+ import BaseCheckbox from './BaseCheckbox.vue'
7
8
  import BaseButton from './BaseButton.vue'
8
9
  import BaseInput from './BaseInput.vue'
9
10
  import NumberInput from './NumberInput.vue'
@@ -25,6 +26,10 @@ const icons = {
25
26
  zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8',
26
27
  route: ['M18 6l-6 6-4-4-6 6'],
27
28
  settings: ['M12 2L2 7l10 5 10-5-10-5z', 'M2 17l10 5 10-5', 'M2 12l10 5 10-5'],
29
+ reset: ['M16.023 9.348h4.992v-.001', 'M2.985 19.644v-4.992', 'M4.031 9.865a8.25 8.25 0 0113.803-3.7'],
30
+ eye: ['M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z', 'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'],
31
+ chart: ['M3 3v18h18', 'M7 15l3-3 3 2 4-6'],
32
+ database: ['M12 3c4.418 0 8 1.343 8 3s-3.582 3-8 3-8-1.343-8-3 3.582-3 8-3z', 'M4 6v6c0 1.657 3.582 3 8 3s8-1.343 8-3V6', 'M4 12v6c0 1.657 3.582 3 8 3s8-1.343 8-3v-6'],
28
33
  }
29
34
 
30
35
  /* --- Sequence builder panels --- */
@@ -46,6 +51,8 @@ const sequencePanels: Record<string, SidebarToolSection[]> = {
46
51
  iconColor: '#6366f1',
47
52
  iconBg: '#e0e7ff',
48
53
  showToggle: true,
54
+ badge: 'on',
55
+ badgeTone: 'success',
49
56
  },
50
57
  {
51
58
  id: 'randomize',
@@ -55,6 +62,11 @@ const sequencePanels: Record<string, SidebarToolSection[]> = {
55
62
  iconColor: '#6366f1',
56
63
  iconBg: '#e0e7ff',
57
64
  showToggle: true,
65
+ badge: 3,
66
+ badgeTone: 'cta',
67
+ actions: [
68
+ { id: 'reset-randomize', label: 'Reset randomization', icon: icons.reset, tone: 'neutral' },
69
+ ],
58
70
  defaultOpen: false,
59
71
  },
60
72
  {
@@ -128,6 +140,7 @@ const methods = [
128
140
  ]
129
141
  const showOutliers = ref(true)
130
142
  const logScale = ref(false)
143
+ const includeMetadata = ref(true)
131
144
  const polarity = ref('negative')
132
145
  const container = ref('vial')
133
146
  const expNumber = ref(1)
@@ -169,6 +182,10 @@ const schemaControls = defineControls({
169
182
  showOutliers: {
170
183
  label: 'Show outliers',
171
184
  default: true,
185
+ hint: 'Keep flagged points visible',
186
+ icon: icons.eye,
187
+ iconColor: '#0ea5e9',
188
+ iconBg: '#e0f2fe',
172
189
  section: 'display',
173
190
  sectionLabel: 'Display',
174
191
  sectionSubtitle: 'Result filters',
@@ -206,7 +223,14 @@ const modelDrivenSidebarModel = defineControlModel({
206
223
  defaultOpen: false,
207
224
  },
208
225
  controls: {
209
- showOutliers: true,
226
+ showOutliers: {
227
+ label: 'Show outliers',
228
+ default: true,
229
+ hint: 'Keep flagged points visible',
230
+ icon: icons.eye,
231
+ iconColor: '#0ea5e9',
232
+ iconBg: '#e0f2fe',
233
+ },
210
234
  minPeakArea: { type: 'number', default: 1000, min: 0 },
211
235
  },
212
236
  },
@@ -301,7 +325,15 @@ function initSimpleToolkit() {
301
325
  </div>
302
326
  </template>
303
327
  <template #section-method>
304
- <BaseToggle model-value label="Use custom method paths" />
328
+ <BaseToggle
329
+ model-value
330
+ variant="row"
331
+ label="Use custom method paths"
332
+ description="Override the default acquisition directory"
333
+ :icon="icons.route"
334
+ icon-color="#8b5cf6"
335
+ icon-bg="#ede9fe"
336
+ />
305
337
  <BaseSelect
306
338
  model-value=""
307
339
  placeholder="-- Select Method --"
@@ -445,13 +477,46 @@ function initSimpleToolkit() {
445
477
  <BaseSelect v-model="method" label="Method" :options="methods" />
446
478
  </template>
447
479
  <template #section-filters>
448
- <BaseToggle v-model="showOutliers" label="Exclude outliers" />
480
+ <BaseToggle
481
+ v-model="showOutliers"
482
+ variant="row"
483
+ label="Exclude outliers"
484
+ description="Hide flagged injections from summaries"
485
+ :icon="icons.eye"
486
+ icon-color="#0ea5e9"
487
+ icon-bg="#e0f2fe"
488
+ />
449
489
  </template>
450
490
  <template #section-display>
451
- <BaseToggle v-model="showOutliers" label="Show outliers" />
452
- <BaseToggle v-model="logScale" label="Log scale" />
491
+ <BaseToggle
492
+ v-model="showOutliers"
493
+ variant="row"
494
+ label="Show outliers"
495
+ description="Keep flagged peaks visible"
496
+ :icon="icons.eye"
497
+ icon-color="#0ea5e9"
498
+ icon-bg="#e0f2fe"
499
+ />
500
+ <BaseToggle
501
+ v-model="logScale"
502
+ variant="row"
503
+ label="Log scale"
504
+ description="Compress high-intensity spread"
505
+ :icon="icons.chart"
506
+ icon-color="#8b5cf6"
507
+ icon-bg="#ede9fe"
508
+ />
453
509
  </template>
454
510
  <template #section-export>
511
+ <BaseCheckbox
512
+ v-model="includeMetadata"
513
+ variant="row"
514
+ label="Include metadata"
515
+ description="Add method and sample context"
516
+ :icon="icons.database"
517
+ icon-color="#10b981"
518
+ icon-bg="#d1fae5"
519
+ />
455
520
  <BaseButton size="sm" variant="secondary">Export CSV</BaseButton>
456
521
  <BaseButton size="sm" variant="secondary">Export PNG</BaseButton>
457
522
  </template>
@@ -499,7 +564,15 @@ function initSimpleToolkit() {
499
564
  <BaseSelect v-model="method" label="Method" :options="methods" />
500
565
  </template>
501
566
  <template #section-filters>
502
- <BaseToggle v-model="showOutliers" label="Exclude outliers" />
567
+ <BaseToggle
568
+ v-model="showOutliers"
569
+ variant="row"
570
+ label="Exclude outliers"
571
+ description="Hide flagged injections from summaries"
572
+ :icon="icons.eye"
573
+ icon-color="#0ea5e9"
574
+ icon-bg="#e0f2fe"
575
+ />
503
576
  </template>
504
577
  <template #footer>
505
578
  <BaseButton size="sm" variant="ghost" style="width: 100%;">Reset Defaults</BaseButton>
@@ -153,6 +153,7 @@ const emit = defineEmits<{
153
153
  'update:modelValue': [values: Record<string, unknown>]
154
154
  'update:values': [values: Record<string, unknown>]
155
155
  'update:collapsed': [value: boolean]
156
+ 'section-action': [sectionId: string, actionId: string]
156
157
  'form-submit': [sectionId: string, values: Record<string, unknown>]
157
158
  'form-cancel': [sectionId: string]
158
159
  }>()
@@ -268,6 +269,30 @@ function expandCollapsed() {
268
269
  collapsedModel.value = false
269
270
  }
270
271
 
272
+ function handleSectionAction(sectionId: string, actionId: string) {
273
+ emit('section-action', sectionId, actionId)
274
+ }
275
+
276
+ function isSvgIcon(icon?: string | string[]): boolean {
277
+ if (!icon) return false
278
+ return Array.isArray(icon) || icon.startsWith('M') || icon.startsWith('m')
279
+ }
280
+
281
+ function sectionInitial(section: SidebarToolSection): string {
282
+ return section.label.trim().charAt(0).toUpperCase() || '?'
283
+ }
284
+
285
+ function railIconStyle(section: SidebarToolSection): Record<string, string> {
286
+ return {
287
+ backgroundColor: section.iconBg ?? 'var(--color-primary-soft)',
288
+ color: section.iconColor ?? 'var(--color-primary)',
289
+ }
290
+ }
291
+
292
+ function badgeToneClass(section: SidebarToolSection): string {
293
+ return `mint-sidebar__rail-badge--${section.badgeTone ?? 'cta'}`
294
+ }
295
+
271
296
  function mergeSidebarPanels(
272
297
  generated: Record<string, SidebarToolSection[]>,
273
298
  explicit: Record<string, SidebarToolSection[]>,
@@ -359,10 +384,14 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
359
384
  :icon="section.icon"
360
385
  :icon-color="section.iconColor"
361
386
  :icon-bg="section.iconBg"
387
+ :badge="section.badge"
388
+ :badge-tone="section.badgeTone"
389
+ :actions="section.actions"
362
390
  :dense="dense"
363
391
  :default-open="section.defaultOpen !== false"
364
392
  :show-toggle="section.showToggle"
365
393
  :toggle-value="toggleState[section.id] ?? false"
394
+ @action="handleSectionAction(section.id, $event)"
366
395
  @update:toggle-value="emit('update:toggle', section.id, $event)"
367
396
  >
368
397
  <slot :name="`section-${section.id}`">
@@ -398,10 +427,53 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
398
427
  />
399
428
  </div>
400
429
 
430
+ <div v-else class="mint-sidebar__collapsed mint-sidebar__collapsed--default">
431
+ <button
432
+ v-for="section in activeSections"
433
+ :key="section.id"
434
+ type="button"
435
+ class="mint-sidebar__rail-item"
436
+ :aria-label="`Expand ${section.label}`"
437
+ :title="section.label"
438
+ @click="expandCollapsed"
439
+ >
440
+ <span class="mint-sidebar__rail-icon" :style="railIconStyle(section)">
441
+ <svg
442
+ v-if="section.icon && isSvgIcon(section.icon)"
443
+ class="mint-sidebar__rail-svg"
444
+ viewBox="0 0 24 24"
445
+ fill="none"
446
+ stroke="currentColor"
447
+ stroke-width="2"
448
+ stroke-linecap="round"
449
+ stroke-linejoin="round"
450
+ aria-hidden="true"
451
+ >
452
+ <template v-if="Array.isArray(section.icon)">
453
+ <path v-for="(d, i) in section.icon" :key="i" :d="d" />
454
+ </template>
455
+ <path v-else :d="section.icon" />
456
+ </svg>
457
+ <span v-else class="mint-sidebar__rail-text-icon">
458
+ {{ section.icon || sectionInitial(section) }}
459
+ </span>
460
+ </span>
461
+ <span
462
+ v-if="section.badge !== undefined"
463
+ :class="['mint-sidebar__rail-badge', badgeToneClass(section)]"
464
+ >
465
+ {{ section.badge }}
466
+ </span>
467
+ </button>
468
+ </div>
469
+
401
470
  <!-- Footer slot -->
402
471
  <div v-if="!collapsedModel && $slots.footer" class="mint-sidebar__footer">
403
472
  <slot name="footer" />
404
473
  </div>
474
+ <div v-else-if="collapsedModel && $slots['collapsed-footer']" class="mint-sidebar__footer mint-sidebar__footer--collapsed">
475
+ <slot name="collapsed-footer" />
476
+ </div>
405
477
  </aside>
406
478
  </template>
407
479
 
@@ -4,6 +4,10 @@ import BaseCheckbox from './BaseCheckbox.vue'
4
4
 
5
5
  const sizes = ['sm', 'md', 'lg'] as const
6
6
  const checked = ref(true)
7
+ const icons = {
8
+ database: ['M12 3c4.418 0 8 1.343 8 3s-3.582 3-8 3-8-1.343-8-3z', 'M4 6v6c0 1.657 3.582 3 8 3s8-1.343 8-3V6', 'M4 12v6c0 1.657 3.582 3 8 3s8-1.343 8-3v-6'],
9
+ file: ['M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z', 'M14 2v6h6', 'M16 13H8', 'M16 17H8'],
10
+ }
7
11
  </script>
8
12
 
9
13
  <template>
@@ -53,6 +57,29 @@ const checked = ref(true)
53
57
  </div>
54
58
  </Variant>
55
59
 
60
+ <Variant title="Choice Rows">
61
+ <div style="padding: 2rem; max-width: 360px; display: flex; flex-direction: column; gap: 0.5rem;">
62
+ <BaseCheckbox
63
+ v-model="checked"
64
+ variant="row"
65
+ label="Include metadata"
66
+ description="Add method and sample context"
67
+ :icon="icons.database"
68
+ icon-color="#10b981"
69
+ icon-bg="#d1fae5"
70
+ />
71
+ <BaseCheckbox
72
+ :model-value="false"
73
+ variant="row"
74
+ label="Attach report"
75
+ description="Include the rendered summary PDF"
76
+ :icon="icons.file"
77
+ icon-color="#6366f1"
78
+ icon-bg="#e0e7ff"
79
+ />
80
+ </div>
81
+ </Variant>
82
+
56
83
  <Variant title="Disabled States">
57
84
  <div style="padding: 2rem; display: flex; flex-direction: column; gap: 1rem;">
58
85
  <BaseCheckbox :model-value="false" label="Disabled unchecked" disabled />
@@ -1,16 +1,24 @@
1
1
  <script setup lang="ts">
2
2
  /** Renders a styled checkbox with an optional label, size variants, and disabled state. */
3
+ import Tooltip from './Tooltip.vue'
4
+
3
5
  interface Props {
4
6
  modelValue?: boolean
5
7
  label?: string
8
+ description?: string
9
+ icon?: string | string[]
10
+ iconColor?: string
11
+ iconBg?: string
6
12
  disabled?: boolean
7
13
  size?: 'sm' | 'md' | 'lg'
14
+ variant?: 'default' | 'row'
8
15
  }
9
16
 
10
17
  const props = withDefaults(defineProps<Props>(), {
11
18
  modelValue: false,
12
19
  disabled: false,
13
20
  size: 'md',
21
+ variant: 'default',
14
22
  })
15
23
 
16
24
  const emit = defineEmits<{
@@ -21,15 +29,69 @@ function handleChange(event: Event) {
21
29
  const target = event.target as HTMLInputElement
22
30
  emit('update:modelValue', target.checked)
23
31
  }
32
+
33
+ function isSvgIcon(icon?: string | string[]): boolean {
34
+ if (!icon) return false
35
+ return Array.isArray(icon) || icon.startsWith('M') || icon.startsWith('m')
36
+ }
37
+
38
+ function iconStyle(): Record<string, string> {
39
+ return {
40
+ color: props.iconColor ?? (props.modelValue ? 'var(--color-primary)' : 'var(--text-muted)'),
41
+ }
42
+ }
24
43
  </script>
25
44
 
26
45
  <template>
27
46
  <label
28
47
  :class="[
29
48
  'mint-checkbox',
49
+ `mint-checkbox--${props.variant}`,
30
50
  props.disabled ? 'mint-checkbox--disabled' : '',
51
+ props.modelValue ? 'mint-checkbox--checked' : '',
31
52
  ]"
32
53
  >
54
+ <span
55
+ v-if="props.variant === 'row' && props.icon"
56
+ class="mint-checkbox__row-icon"
57
+ :style="iconStyle()"
58
+ aria-hidden="true"
59
+ >
60
+ <svg
61
+ v-if="isSvgIcon(props.icon)"
62
+ class="mint-checkbox__row-svg"
63
+ viewBox="0 0 24 24"
64
+ fill="none"
65
+ stroke="currentColor"
66
+ stroke-width="2"
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
69
+ >
70
+ <template v-if="Array.isArray(props.icon)">
71
+ <path v-for="(d, i) in props.icon" :key="i" :d="d" />
72
+ </template>
73
+ <path v-else :d="props.icon" />
74
+ </svg>
75
+ <span v-else class="mint-checkbox__row-text-icon">{{ props.icon }}</span>
76
+ </span>
77
+ <span
78
+ v-if="props.variant === 'row' && props.label"
79
+ class="mint-checkbox__copy"
80
+ >
81
+ <Tooltip
82
+ v-if="props.description"
83
+ :text="props.description"
84
+ position="right"
85
+ max-width="16rem"
86
+ >
87
+ <span :class="['mint-checkbox__label', `mint-checkbox__label--${props.size}`, 'mint-checkbox__label--tooltip']">
88
+ {{ props.label }}
89
+ </span>
90
+ </Tooltip>
91
+ <span v-else :class="['mint-checkbox__label', `mint-checkbox__label--${props.size}`]">
92
+ {{ props.label }}
93
+ </span>
94
+ </span>
33
95
  <div class="mint-checkbox__input-wrapper">
34
96
  <input
35
97
  type="checkbox"
@@ -58,7 +120,7 @@ function handleChange(event: Event) {
58
120
  </svg>
59
121
  </div>
60
122
  </div>
61
- <span v-if="props.label" :class="['mint-checkbox__label', `mint-checkbox__label--${props.size}`]">
123
+ <span v-if="props.label && props.variant !== 'row'" :class="['mint-checkbox__label', `mint-checkbox__label--${props.size}`]">
62
124
  {{ props.label }}
63
125
  </span>
64
126
  </label>
@@ -5,6 +5,10 @@ import BaseToggle from './BaseToggle.vue'
5
5
  const sizes = ['sm', 'md', 'lg'] as const
6
6
  const toggleOn = ref(true)
7
7
  const toggleOff = ref(false)
8
+ const icons = {
9
+ eye: ['M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z', 'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'],
10
+ chart: ['M3 3v18h18', 'M7 15l3-3 3 2 4-6'],
11
+ }
8
12
  </script>
9
13
 
10
14
  <template>
@@ -54,6 +58,29 @@ const toggleOff = ref(false)
54
58
  </div>
55
59
  </Variant>
56
60
 
61
+ <Variant title="Choice Rows">
62
+ <div style="padding: 2rem; max-width: 360px; display: flex; flex-direction: column; gap: 0.5rem;">
63
+ <BaseToggle
64
+ v-model="toggleOn"
65
+ variant="row"
66
+ label="Show outliers"
67
+ description="Keep flagged peaks visible"
68
+ :icon="icons.eye"
69
+ icon-color="#0ea5e9"
70
+ icon-bg="#e0f2fe"
71
+ />
72
+ <BaseToggle
73
+ v-model="toggleOff"
74
+ variant="row"
75
+ label="Log scale"
76
+ description="Compress high-intensity spread"
77
+ :icon="icons.chart"
78
+ icon-color="#8b5cf6"
79
+ icon-bg="#ede9fe"
80
+ />
81
+ </div>
82
+ </Variant>
83
+
57
84
  <Variant title="Without Label">
58
85
  <div style="padding: 2rem; display: flex; gap: 1.5rem; align-items: center;">
59
86
  <BaseToggle v-model="toggleOn" size="sm" />