@morscherlab/mint-sdk 1.2.0 → 1.2.1

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 (99) hide show
  1. package/dist/{ExperimentSelectorModal-DCXmwKNS.js → ExperimentSelectorModal-DHE9k8TP.js} +3 -3
  2. package/dist/{ExperimentSelectorModal-DCXmwKNS.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
  3. package/dist/{SettingsModal-BYR20I4c.js → SettingsModal-DOcCf0Vo.js} +39 -16
  4. package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
  5. package/dist/{Skeleton-BAF3CKY7.js → Skeleton-jhF9wUkU.js} +17 -7
  6. package/dist/Skeleton-jhF9wUkU.js.map +1 -0
  7. package/dist/__tests__/components/FormField.test.d.ts +1 -0
  8. package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
  9. package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
  10. package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
  11. package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
  12. package/dist/components/AppSidebar.vue.d.ts +7 -7
  13. package/dist/components/BasePill.vue.d.ts +3 -0
  14. package/dist/components/BaseTabs.vue.d.ts +1 -4
  15. package/dist/components/FileUploader.vue.d.ts +3 -0
  16. package/dist/components/FormField.vue.d.ts +8 -1
  17. package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
  18. package/dist/components/index.js +5 -5
  19. package/dist/{components-DIum5hkx.js → components-Cp8DcNrU.js} +472 -259
  20. package/dist/components-Cp8DcNrU.js.map +1 -0
  21. package/dist/composables/index.js +7 -7
  22. package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
  23. package/dist/composables/useToast.d.ts +22 -1
  24. package/dist/{composables-mFXsTA4g.js → composables-D_4qqOlp.js} +6 -6
  25. package/dist/{composables-mFXsTA4g.js.map → composables-D_4qqOlp.js.map} +1 -1
  26. package/dist/index.js +10 -10
  27. package/dist/install.js +2 -2
  28. package/dist/styles.css +560 -303
  29. package/dist/templates/index.js +2 -2
  30. package/dist/{templates-CmGfmBRA.js → templates-Dd9bBSs6.js} +2 -2
  31. package/dist/{templates-CmGfmBRA.js.map → templates-Dd9bBSs6.js.map} +1 -1
  32. package/dist/types/components.d.ts +21 -0
  33. package/dist/types/form-builder.d.ts +2 -0
  34. package/dist/types/generated-ui.d.ts +2 -0
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/{useControlSchema-DldpxNOP.js → useControlSchema-BB2GRPhW.js} +151 -71
  37. package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
  38. package/dist/{useExperimentSelector-CEcIaueJ.js → useExperimentSelector-G_CAOFTX.js} +2 -2
  39. package/dist/{useExperimentSelector-CEcIaueJ.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
  40. package/dist/{useFormBuilder-z5L_dUIy.js → useFormBuilder-BnGSdSRG.js} +2 -2
  41. package/dist/{useFormBuilder-z5L_dUIy.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
  42. package/dist/{validation-uwoJGEb6.js → validation-CTxuzLIX.js} +21 -9
  43. package/dist/validation-CTxuzLIX.js.map +1 -0
  44. package/package.json +1 -1
  45. package/src/__tests__/components/AppSidebar.test.ts +84 -94
  46. package/src/__tests__/components/AppToastContainer.test.ts +55 -1
  47. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
  48. package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
  49. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
  50. package/src/__tests__/components/FormBuilder.test.ts +26 -0
  51. package/src/__tests__/components/FormField.test.ts +26 -0
  52. package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
  53. package/src/__tests__/components/NumberInput.test.ts +105 -0
  54. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
  55. package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
  56. package/src/__tests__/composables/localFiles.test.ts +57 -0
  57. package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
  58. package/src/__tests__/generatedUi.test.ts +111 -0
  59. package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
  60. package/src/components/AppLayout.story.vue +1 -1
  61. package/src/components/AppSidebar.story.vue +7 -10
  62. package/src/components/AppSidebar.vue +178 -58
  63. package/src/components/AppToastContainer.vue +65 -23
  64. package/src/components/BasePill.story.vue +2 -0
  65. package/src/components/BasePill.vue +4 -0
  66. package/src/components/BaseTabs.story.vue +0 -42
  67. package/src/components/BaseTabs.vue +3 -6
  68. package/src/components/FileUploader.vue +10 -1
  69. package/src/components/FormField.vue +10 -3
  70. package/src/components/GeneratedPathInput.vue +11 -7
  71. package/src/components/NumberInput.story.vue +32 -13
  72. package/src/components/NumberInput.vue +116 -55
  73. package/src/components/PluginWorkspaceView.props.ts +3 -3
  74. package/src/components/internal/FormFieldRendererInternal.vue +17 -0
  75. package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
  76. package/src/composables/filePicker/localFiles.ts +21 -14
  77. package/src/composables/filePicker/usePickerSelection.ts +15 -4
  78. package/src/composables/pluginWorkspaceModel.ts +0 -3
  79. package/src/composables/usePluginWorkspace.ts +1 -2
  80. package/src/composables/useToast.ts +21 -5
  81. package/src/generated/schema.ts +58 -18
  82. package/src/styles/components/app-sidebar.css +282 -121
  83. package/src/styles/components/form-builder.css +1 -1
  84. package/src/styles/components/form-field.css +28 -0
  85. package/src/styles/components/number-input.css +73 -91
  86. package/src/styles/components/pill.css +49 -51
  87. package/src/styles/components/segmented-control.css +16 -11
  88. package/src/styles/components/tabs.css +18 -34
  89. package/src/styles/components/toast.css +136 -27
  90. package/src/styles/variables.css +25 -5
  91. package/src/types/components.ts +23 -0
  92. package/src/types/form-builder.ts +2 -0
  93. package/src/types/generated-ui.ts +2 -0
  94. package/src/types/index.ts +2 -0
  95. package/dist/SettingsModal-BYR20I4c.js.map +0 -1
  96. package/dist/Skeleton-BAF3CKY7.js.map +0 -1
  97. package/dist/components-DIum5hkx.js.map +0 -1
  98. package/dist/useControlSchema-DldpxNOP.js.map +0 -1
  99. package/dist/validation-uwoJGEb6.js.map +0 -1
@@ -3,8 +3,9 @@
3
3
  * AppSidebar - Context-sensitive toolkit panel
4
4
  *
5
5
  * Shows tool sections relevant to the active view. Sections are defined via
6
- * the `panels` config and rendered as CollapsibleCards. Controls can be
7
- * provided through named slots or auto-rendered from FormBuilder schemas.
6
+ * the `panels` config and rendered as flat groups (header row + hairline,
7
+ * no nested card). Controls can be provided through named slots or
8
+ * auto-rendered from FormBuilder schemas.
8
9
  *
9
10
  * When activeView is omitted, the first non-empty panel view is selected.
10
11
  * When no view has matching panels, the sidebar hides entirely.
@@ -15,7 +16,7 @@
15
16
  * ```
16
17
  */
17
18
  import { computed, ref, useSlots, type Slots } from 'vue'
18
- import type { PillNavItem, SidebarToolSection } from '../types'
19
+ import type { PillNavItem, SidebarToolSection, SidebarToolSectionAction } from '../types'
19
20
  import type { FormEnhancements, FormSchema } from '../types/form-builder'
20
21
  import {
21
22
  controlsToSectionFormSchemas,
@@ -28,7 +29,6 @@ import {
28
29
  type ControlSchema,
29
30
  type ControlWorkspaceOptions,
30
31
  } from '../composables/useControlSchema'
31
- import CollapsibleCard from './CollapsibleCard.vue'
32
32
  import FormBuilder from './FormBuilder.vue'
33
33
 
34
34
  interface Props {
@@ -38,17 +38,17 @@ interface Props {
38
38
  subtitle?: string
39
39
  /** Optional compact badge/count rendered in the chrome header. */
40
40
  badge?: string | number
41
- /** Visual preset for common plugin sidebars. `analysis` preserves the LEAF-style MINT analysis sidebar design language. */
42
- variant?: 'default' | 'analysis'
43
41
  /** Map of view IDs to their tool sections */
44
42
  panels?: Record<string, SidebarToolSection[]>
45
43
  /** Which view's panels to display. Defaults to the first non-empty panel view. */
46
44
  activeView?: string
47
- /** Floating variant with absolute positioning. Defaults to false for analysis variant. */
45
+ /** Floating shell with absolute positioning. Prefer letting `AppLayout` own this; it stays for standalone use. */
48
46
  floating?: boolean
49
- /** Compact layout: smaller headers, tighter spacing, no icon backgrounds */
47
+ /** Section density. `compact` (default) is the 36px header row; `comfortable` restores the roomier spacing. */
48
+ density?: 'compact' | 'comfortable'
49
+ /** @deprecated Compact is now the default. Pass `density="comfortable"` for the roomier layout. */
50
50
  dense?: boolean
51
- /** Width when visible. Defaults to 20rem for analysis variant, otherwise 280px. */
51
+ /** Width when visible. */
52
52
  width?: string
53
53
  /** Position sidebar on left or right side */
54
54
  side?: 'left' | 'right'
@@ -88,7 +88,7 @@ interface Props {
88
88
  formReadonly?: boolean
89
89
  /** Size passed to auto-rendered section forms */
90
90
  formSize?: 'sm' | 'md' | 'lg'
91
- /** Show a built-in collapse/expand button in the sidebar chrome. Defaults to true for analysis variant. */
91
+ /** Show a built-in collapse/expand button in the sidebar chrome. */
92
92
  collapsible?: boolean
93
93
  /** Controlled collapsed state. */
94
94
  collapsed?: boolean
@@ -106,12 +106,12 @@ const props = withDefaults(defineProps<Props>(), {
106
106
  title: undefined,
107
107
  subtitle: undefined,
108
108
  badge: undefined,
109
- variant: 'default',
110
109
  panels: () => ({}),
111
110
  activeView: '',
112
- floating: undefined,
113
- dense: false,
114
- width: undefined,
111
+ floating: true,
112
+ density: undefined,
113
+ dense: undefined,
114
+ width: '280px',
115
115
  side: 'left',
116
116
  toggleState: () => ({}),
117
117
  forms: () => ({}),
@@ -129,7 +129,7 @@ const props = withDefaults(defineProps<Props>(), {
129
129
  formDisabled: false,
130
130
  formReadonly: false,
131
131
  formSize: 'sm',
132
- collapsible: undefined,
132
+ collapsible: false,
133
133
  collapsed: undefined,
134
134
  defaultCollapsed: false,
135
135
  collapsedWidth: '3rem',
@@ -157,10 +157,20 @@ const collapsedModel = computed({
157
157
  },
158
158
  })
159
159
 
160
- const isAnalysisVariant = computed(() => props.variant === 'analysis')
161
- const resolvedFloating = computed(() => props.floating ?? !isAnalysisVariant.value)
162
- const resolvedWidth = computed(() => props.width ?? (isAnalysisVariant.value ? '20rem' : '280px'))
163
- const resolvedCollapsible = computed(() => props.collapsible ?? isAnalysisVariant.value)
160
+ const resolvedDensity = computed(() =>
161
+ props.density ?? (props.dense === false ? 'comfortable' : 'compact'),
162
+ )
163
+
164
+ // Per-section open state; falls back to `defaultOpen` until the user toggles.
165
+ const openState = ref<Record<string, boolean>>({})
166
+
167
+ function isSectionOpen(section: SidebarToolSection): boolean {
168
+ return openState.value[section.id] ?? section.defaultOpen !== false
169
+ }
170
+
171
+ function toggleSection(section: SidebarToolSection) {
172
+ openState.value[section.id] = !isSectionOpen(section)
173
+ }
164
174
 
165
175
  const resolvedModel = computed<ControlModelBinding | undefined>(() => {
166
176
  return resolveControlModel(props.model)
@@ -224,16 +234,15 @@ const isVisible = computed<boolean>(() =>
224
234
  const sidebarClasses = computed<string[]>(() => [
225
235
  'mint-sidebar',
226
236
  `mint-sidebar--${props.side}`,
227
- `mint-sidebar--${props.variant}`,
228
- resolvedFloating.value ? 'mint-sidebar--floating' : 'mint-sidebar--static',
229
- props.dense ? 'mint-sidebar--dense' : '',
230
- resolvedCollapsible.value ? 'mint-sidebar--collapsible' : '',
237
+ props.floating ? 'mint-sidebar--floating' : 'mint-sidebar--static',
238
+ resolvedDensity.value === 'comfortable' ? 'mint-sidebar--comfortable' : '',
239
+ props.collapsible ? 'mint-sidebar--collapsible' : '',
231
240
  collapsedModel.value ? 'mint-sidebar--collapsed' : '',
232
241
  !isVisible.value ? 'mint-sidebar--hidden' : '',
233
242
  ])
234
243
 
235
244
  const sidebarStyle = computed<Record<string, string>>(() => ({
236
- width: collapsedModel.value ? props.collapsedWidth : resolvedWidth.value,
245
+ width: collapsedModel.value ? props.collapsedWidth : props.width,
237
246
  }))
238
247
 
239
248
  function handleFormUpdate(values: Record<string, unknown>) {
@@ -282,6 +291,17 @@ function badgeToneClass(section: SidebarToolSection): string {
282
291
  return `mint-sidebar__rail-badge--${section.badgeTone ?? 'cta'}`
283
292
  }
284
293
 
294
+ function tagToneClass(section: SidebarToolSection): string {
295
+ return `mint-sidebar__tag--${section.badgeTone ?? 'cta'}`
296
+ }
297
+
298
+ function actionClasses(action: SidebarToolSectionAction): string[] {
299
+ return [
300
+ 'mint-sidebar__group-action',
301
+ action.tone ? `mint-sidebar__group-action--${action.tone}` : '',
302
+ ]
303
+ }
304
+
285
305
  function mergeSidebarPanels(
286
306
  generated: Record<string, SidebarToolSection[]>,
287
307
  explicit: Record<string, SidebarToolSection[]>,
@@ -319,7 +339,7 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
319
339
  >
320
340
  <!-- Header slot / built-in chrome -->
321
341
  <div
322
- v-if="$slots.header || title || subtitle || badge !== undefined || resolvedCollapsible"
342
+ v-if="$slots.header || title || subtitle || badge !== undefined || collapsible"
323
343
  class="mint-sidebar__header"
324
344
  >
325
345
  <slot
@@ -335,7 +355,7 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
335
355
  <span v-if="badge !== undefined" class="mint-sidebar__badge">{{ badge }}</span>
336
356
  </div>
337
357
  <button
338
- v-if="resolvedCollapsible"
358
+ v-if="collapsible"
339
359
  type="button"
340
360
  class="mint-sidebar__collapse-button"
341
361
  :aria-label="collapsedModel ? expandButtonLabel : collapseButtonLabel"
@@ -365,41 +385,141 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
365
385
  :id="contentId"
366
386
  class="mint-sidebar__sections"
367
387
  >
368
- <CollapsibleCard
388
+ <div
369
389
  v-for="section in activeSections"
370
390
  :key="section.id"
371
- :title="section.label"
372
- :subtitle="section.subtitle"
373
- :icon="section.icon"
374
- :icon-color="section.iconColor"
375
- :icon-bg="section.iconBg"
376
- :badge="section.badge"
377
- :badge-tone="section.badgeTone"
378
- :actions="section.actions"
379
- :dense="dense"
380
- :default-open="section.defaultOpen !== false"
381
- :show-toggle="section.showToggle"
382
- :toggle-value="toggleState[section.id] ?? false"
383
- @action="handleSectionAction(section.id, $event)"
384
- @update:toggle-value="emit('update:toggle', section.id, $event)"
391
+ :class="['mint-sidebar__group', isSectionOpen(section) ? 'mint-sidebar__group--open' : 'mint-sidebar__group--closed']"
385
392
  >
386
- <slot :name="`section-${section.id}`">
387
- <FormBuilder
388
- v-if="resolvedForms[section.id]"
389
- :model-value="resolvedValues"
390
- :schema="resolvedForms[section.id]"
391
- :enhancements="formEnhancements"
392
- :loading="formLoading"
393
- :disabled="formDisabled"
394
- :readonly="formReadonly"
395
- :size="formSize"
396
- :show-actions="showFormActions"
397
- @update:model-value="handleFormUpdate"
398
- @submit="handleFormSubmit(section.id, $event)"
399
- @cancel="handleFormCancel(section.id)"
400
- />
401
- </slot>
402
- </CollapsibleCard>
393
+ <div class="mint-sidebar__group-head">
394
+ <button
395
+ type="button"
396
+ class="mint-sidebar__group-toggle"
397
+ :aria-expanded="isSectionOpen(section)"
398
+ @click="toggleSection(section)"
399
+ >
400
+ <span
401
+ v-if="section.icon"
402
+ class="mint-sidebar__group-icon"
403
+ :style="railIconStyle(section)"
404
+ >
405
+ <svg
406
+ v-if="isSvgIcon(section.icon)"
407
+ class="mint-sidebar__group-svg"
408
+ viewBox="0 0 24 24"
409
+ fill="none"
410
+ stroke="currentColor"
411
+ stroke-width="2"
412
+ stroke-linecap="round"
413
+ stroke-linejoin="round"
414
+ aria-hidden="true"
415
+ >
416
+ <template v-if="Array.isArray(section.icon)">
417
+ <path v-for="(d, i) in section.icon" :key="i" :d="d" />
418
+ </template>
419
+ <path v-else :d="section.icon" />
420
+ </svg>
421
+ <span v-else class="mint-sidebar__group-text-icon">{{ section.icon }}</span>
422
+ </span>
423
+ <span class="mint-sidebar__group-copy">
424
+ <span class="mint-sidebar__group-label">{{ section.label }}</span>
425
+ <span v-if="section.subtitle" class="mint-sidebar__group-subtitle">{{ section.subtitle }}</span>
426
+ </span>
427
+ </button>
428
+
429
+ <div class="mint-sidebar__group-tail">
430
+ <span
431
+ v-if="section.badge !== undefined"
432
+ :class="['mint-sidebar__tag', tagToneClass(section)]"
433
+ >
434
+ {{ section.badge }}
435
+ </span>
436
+
437
+ <button
438
+ v-for="action in section.actions ?? []"
439
+ :key="action.id"
440
+ type="button"
441
+ :class="actionClasses(action)"
442
+ :style="action.iconColor ? { color: action.iconColor } : undefined"
443
+ :disabled="action.disabled"
444
+ :aria-label="action.label"
445
+ :title="action.label"
446
+ @click="handleSectionAction(section.id, action.id)"
447
+ >
448
+ <svg
449
+ v-if="action.icon && isSvgIcon(action.icon)"
450
+ class="mint-sidebar__group-action-icon"
451
+ viewBox="0 0 24 24"
452
+ fill="none"
453
+ stroke="currentColor"
454
+ stroke-width="2"
455
+ stroke-linecap="round"
456
+ stroke-linejoin="round"
457
+ aria-hidden="true"
458
+ >
459
+ <template v-if="Array.isArray(action.icon)">
460
+ <path v-for="(d, i) in action.icon" :key="i" :d="d" />
461
+ </template>
462
+ <path v-else :d="action.icon" />
463
+ </svg>
464
+ <span v-else-if="action.icon">{{ action.icon }}</span>
465
+ </button>
466
+
467
+ <button
468
+ v-if="section.showToggle"
469
+ type="button"
470
+ role="switch"
471
+ :aria-checked="toggleState[section.id] ?? false"
472
+ :aria-label="`Enable ${section.label}`"
473
+ :class="['mint-sidebar__switch', (toggleState[section.id] ?? false) ? 'mint-sidebar__switch--on' : '']"
474
+ @click="emit('update:toggle', section.id, !(toggleState[section.id] ?? false))"
475
+ >
476
+ <span class="mint-sidebar__switch-knob" />
477
+ </button>
478
+
479
+ <button
480
+ type="button"
481
+ class="mint-sidebar__chevron-button"
482
+ :aria-label="isSectionOpen(section) ? 'Collapse section' : 'Expand section'"
483
+ :aria-expanded="isSectionOpen(section)"
484
+ @click="toggleSection(section)"
485
+ >
486
+ <svg
487
+ :class="['mint-sidebar__chevron', isSectionOpen(section) ? 'mint-sidebar__chevron--open' : '']"
488
+ viewBox="0 0 24 24"
489
+ fill="none"
490
+ stroke="currentColor"
491
+ stroke-width="2"
492
+ stroke-linecap="round"
493
+ stroke-linejoin="round"
494
+ aria-hidden="true"
495
+ >
496
+ <path d="m6 9 6 6 6-6" />
497
+ </svg>
498
+ </button>
499
+ </div>
500
+ </div>
501
+
502
+ <Transition name="collapse">
503
+ <div v-show="isSectionOpen(section)" class="mint-sidebar__group-body">
504
+ <slot :name="`section-${section.id}`">
505
+ <FormBuilder
506
+ v-if="resolvedForms[section.id]"
507
+ :model-value="resolvedValues"
508
+ :schema="resolvedForms[section.id]"
509
+ :enhancements="formEnhancements"
510
+ :loading="formLoading"
511
+ :disabled="formDisabled"
512
+ :readonly="formReadonly"
513
+ :size="formSize"
514
+ :show-actions="showFormActions"
515
+ @update:model-value="handleFormUpdate"
516
+ @submit="handleFormSubmit(section.id, $event)"
517
+ @cancel="handleFormCancel(section.id)"
518
+ />
519
+ </slot>
520
+ </div>
521
+ </Transition>
522
+ </div>
403
523
 
404
524
  <slot
405
525
  :sections="activeSections"
@@ -1,17 +1,25 @@
1
1
  <script setup lang="ts">
2
- /** Preferred toast container driven by useToast, with auto-dismiss progress bar and click-to-close on each notification. */
2
+ /** Preferred toast container driven by useToast: title/detail, up to two actions, TTL or indeterminate bar, click-to-close. */
3
3
  import { useToast } from '../composables/useToast'
4
- import type { Toast } from '../types'
4
+ import type { Toast, ToastAction } from '../types'
5
5
 
6
6
  const { toasts, dismiss } = useToast()
7
7
 
8
- // Semantic accents come from the shared tokens rather than a fourth copy of
9
- // the same four hexes.
10
- const accentColors: Record<Toast['type'], string> = {
11
- success: 'var(--mint-success)',
12
- error: 'var(--mint-error)',
13
- warning: 'var(--mint-warning)',
14
- info: 'var(--mint-info)',
8
+ function title(toast: Toast): string {
9
+ return toast.title ?? toast.message
10
+ }
11
+
12
+ function actions(toast: Toast): ToastAction[] {
13
+ return (toast.actions ?? []).slice(0, 2)
14
+ }
15
+
16
+ function progress(toast: Toast): NonNullable<Toast['progress']> {
17
+ return toast.progress ?? 'ttl'
18
+ }
19
+
20
+ function runAction(toast: Toast, action: ToastAction) {
21
+ action.onClick()
22
+ dismiss(toast.id)
15
23
  }
16
24
  </script>
17
25
 
@@ -22,36 +30,70 @@ const accentColors: Record<Toast['type'], string> = {
22
30
  <div
23
31
  v-for="toast in toasts"
24
32
  :key="toast.id"
25
- :class="['mint-toast__item', `mint-toast__item--${toast.type}`]"
26
- :style="{
27
- borderLeftColor: accentColors[toast.type],
28
- }"
29
- role="alert"
33
+ :class="[
34
+ 'mint-toast__item',
35
+ `mint-toast__item--${toast.type}`,
36
+ (toast.count ?? 1) > 1 ? 'mint-toast__item--stacked' : '',
37
+ ]"
38
+ :role="toast.type === 'error' || toast.type === 'warning' ? 'alert' : 'status'"
30
39
  @click="dismiss(toast.id)"
31
40
  >
32
41
  <!-- Lucide circle-check -->
33
- <svg v-if="toast.type === 'success'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :style="{ color: accentColors[toast.type] }">
42
+ <svg v-if="toast.type === 'success'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
34
43
  <circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" />
35
44
  </svg>
36
45
  <!-- Lucide circle-x -->
37
- <svg v-else-if="toast.type === 'error'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :style="{ color: accentColors[toast.type] }">
46
+ <svg v-else-if="toast.type === 'error'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
38
47
  <circle cx="12" cy="12" r="10" /><path d="m15 9-6 6" /><path d="m9 9 6 6" />
39
48
  </svg>
40
49
  <!-- Lucide triangle-alert -->
41
- <svg v-else-if="toast.type === 'warning'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :style="{ color: accentColors[toast.type] }">
50
+ <svg v-else-if="toast.type === 'warning'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
42
51
  <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /><path d="M12 9v4" /><path d="M12 17h.01" />
43
52
  </svg>
44
53
  <!-- Lucide info -->
45
- <svg v-else class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :style="{ color: accentColors[toast.type] }">
54
+ <svg v-else class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
46
55
  <circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" />
47
56
  </svg>
48
- <span class="mint-toast__message">{{ toast.message }}</span>
57
+
58
+ <div class="mint-toast__body">
59
+ <div class="mint-toast__title">
60
+ <span class="mint-toast__message">{{ title(toast) }}</span>
61
+ <span v-if="(toast.count ?? 1) > 1" class="mint-toast__count">×{{ toast.count }}</span>
62
+ </div>
63
+ <div v-if="toast.detail" class="mint-toast__detail">{{ toast.detail }}</div>
64
+ <div v-if="actions(toast).length" class="mint-toast__actions">
65
+ <button
66
+ v-for="action in actions(toast)"
67
+ :key="action.label"
68
+ type="button"
69
+ :class="['mint-toast__action', action.primary ? 'mint-toast__action--primary' : '']"
70
+ @click.stop="runAction(toast, action)"
71
+ >
72
+ {{ action.label }}
73
+ </button>
74
+ </div>
75
+ </div>
76
+
77
+ <button
78
+ v-if="toast.dismissible !== false"
79
+ type="button"
80
+ class="mint-toast__close"
81
+ aria-label="Dismiss"
82
+ @click.stop="dismiss(toast.id)"
83
+ >
84
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true">
85
+ <path d="M18 6 6 18" /><path d="m6 6 12 12" />
86
+ </svg>
87
+ </button>
88
+
49
89
  <span
90
+ v-if="progress(toast) === 'ttl'"
50
91
  class="mint-toast__progress"
51
- :style="{
52
- backgroundColor: accentColors[toast.type],
53
- animationDuration: `${toast.duration ?? 3500}ms`,
54
- }"
92
+ :style="{ animationDuration: `${toast.duration ?? 3500}ms` }"
93
+ />
94
+ <span
95
+ v-else-if="progress(toast) === 'indeterminate'"
96
+ class="mint-toast__progress mint-toast__progress--indeterminate"
55
97
  />
56
98
  </div>
57
99
  </TransitionGroup>
@@ -16,6 +16,7 @@ const sizes: PillSize[] = ['sm', 'md', 'lg']
16
16
  :size="state.size"
17
17
  :removable="state.removable"
18
18
  :disabled="state.disabled"
19
+ :dot="state.dot"
19
20
  @remove="() => console.log('Remove clicked')"
20
21
  >
21
22
  {{ state.label }}
@@ -37,6 +38,7 @@ const sizes: PillSize[] = ['sm', 'md', 'lg']
37
38
  />
38
39
  <HstCheckbox v-model="state.removable" title="Removable" />
39
40
  <HstCheckbox v-model="state.disabled" title="Disabled" />
41
+ <HstCheckbox v-model="state.dot" title="Status dot" />
40
42
  </template>
41
43
  </Variant>
42
44
 
@@ -28,6 +28,8 @@ interface Props {
28
28
  disabled?: boolean
29
29
  /** Show icon slot */
30
30
  icon?: boolean
31
+ /** Leading status dot in the pill's tone color */
32
+ dot?: boolean
31
33
  }
32
34
 
33
35
  const props = withDefaults(defineProps<Props>(), {
@@ -37,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
37
39
  removable: false,
38
40
  disabled: false,
39
41
  icon: false,
42
+ dot: false,
40
43
  })
41
44
 
42
45
  /**
@@ -64,6 +67,7 @@ function handleRemove(event: MouseEvent) {
64
67
  { 'mint-pill--disabled': disabled, 'mint-pill--with-icon': icon },
65
68
  ]"
66
69
  >
70
+ <span v-if="dot" class="mint-pill__dot" aria-hidden="true" />
67
71
  <span v-if="icon" class="mint-pill__icon">
68
72
  <slot name="icon" />
69
73
  </span>
@@ -1,9 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { ref } from 'vue'
3
2
  import BaseTabs from './BaseTabs.vue'
4
3
  import type { TabItem } from '../types'
5
4
 
6
- const activeTab = ref('overview')
7
5
 
8
6
  const basicTabs: TabItem[] = [
9
7
  { id: 'overview', label: 'Overview' },
@@ -25,12 +23,9 @@ const tabsWithIcons: TabItem[] = [
25
23
  { id: 'analysis', label: 'Analysis', icon: '\u{1F52C}' },
26
24
  ]
27
25
 
28
- const variants = ['underline', 'pills'] as const
29
-
30
26
  function initPlayground() {
31
27
  return {
32
28
  activeTab: 'overview',
33
- variant: 'underline' as const,
34
29
  }
35
30
  }
36
31
  </script>
@@ -43,7 +38,6 @@ function initPlayground() {
43
38
  <BaseTabs
44
39
  v-model="state.activeTab"
45
40
  :tabs="basicTabs"
46
- :variant="state.variant"
47
41
  />
48
42
  <p style="margin-top: 1rem; font-size: 0.875rem; color: var(--text-muted, #94a3b8);">
49
43
  Active tab: <code>{{ state.activeTab }}</code>
@@ -52,11 +46,6 @@ function initPlayground() {
52
46
  </template>
53
47
 
54
48
  <template #controls="{ state }">
55
- <HstSelect
56
- v-model="state.variant"
57
- title="Variant"
58
- :options="variants.map(v => ({ label: v, value: v }))"
59
- />
60
49
  <HstSelect
61
50
  v-model="state.activeTab"
62
51
  title="Active Tab"
@@ -65,27 +54,11 @@ function initPlayground() {
65
54
  </template>
66
55
  </Variant>
67
56
 
68
- <Variant title="All Variants">
69
- <div style="padding: 2rem; display: flex; flex-direction: column; gap: 2rem;">
70
- <div v-for="variant in variants" :key="variant">
71
- <p style="margin: 0 0 0.5rem; font-size: 0.75rem; color: var(--text-muted, #94a3b8); text-transform: uppercase; letter-spacing: 0.05em;">
72
- {{ variant }}
73
- </p>
74
- <BaseTabs
75
- v-model="activeTab"
76
- :tabs="basicTabs"
77
- :variant="variant"
78
- />
79
- </div>
80
- </div>
81
- </Variant>
82
-
83
57
  <Variant title="With Badges">
84
58
  <div style="padding: 2rem;">
85
59
  <BaseTabs
86
60
  model-value="inbox"
87
61
  :tabs="tabsWithBadges"
88
- variant="underline"
89
62
  />
90
63
  </div>
91
64
  </Variant>
@@ -95,7 +68,6 @@ function initPlayground() {
95
68
  <BaseTabs
96
69
  model-value="plate"
97
70
  :tabs="tabsWithIcons"
98
- variant="pills"
99
71
  />
100
72
  </div>
101
73
  </Variant>
@@ -105,23 +77,9 @@ function initPlayground() {
105
77
  <BaseTabs
106
78
  model-value="inbox"
107
79
  :tabs="tabsWithBadges"
108
- variant="underline"
109
80
  />
110
81
  </div>
111
82
  </Variant>
112
83
 
113
- <Variant title="Pills — matches SegmentedControl.card">
114
- <div style="padding: 2rem; display: flex; justify-content: center;">
115
- <BaseTabs
116
- v-model="activeTab"
117
- variant="pills"
118
- :tabs="[
119
- { id: 'overview', label: 'Overview' },
120
- { id: 'samples', label: 'Samples' },
121
- { id: 'results', label: 'Results' },
122
- ]"
123
- />
124
- </div>
125
- </Variant>
126
84
  </Story>
127
85
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- /** Renders a tab bar with underline or pills variant, badges, icons, and disabled tab support. */
2
+ /** Renders an underline tab bar with mono count badges, icons, and disabled tab support. */
3
3
  import { computed, type ComponentPublicInstance } from 'vue'
4
4
  import type { TabItem, TabItemInput } from '../types'
5
5
  import { normalizeItemInput } from '../utils/items'
@@ -8,12 +8,9 @@ import { useRovingFocus } from '../composables/useRovingFocus'
8
8
  interface Props {
9
9
  modelValue: string
10
10
  tabs: TabItemInput[]
11
- variant?: 'underline' | 'pills'
12
11
  }
13
12
 
14
- const props = withDefaults(defineProps<Props>(), {
15
- variant: 'underline',
16
- })
13
+ const props = defineProps<Props>()
17
14
 
18
15
  const emit = defineEmits<{
19
16
  'update:modelValue': [value: string]
@@ -55,7 +52,7 @@ const { handleKeydown } = useRovingFocus({
55
52
  </script>
56
53
 
57
54
  <template>
58
- <div :class="['mint-tabs', `mint-tabs--${variant}`]" role="tablist">
55
+ <div class="mint-tabs mint-tabs--underline" role="tablist">
59
56
  <button
60
57
  v-for="tab in normalizedTabs"
61
58
  :id="`mint-tab-${tab.id}`"
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  /** Drag-and-drop file upload zone with type filtering, size validation, and folder mode. */
3
- import { ref, computed } from 'vue'
3
+ import { ref, computed, watch } from 'vue'
4
4
 
5
5
  interface Props {
6
6
  accept?: string
@@ -11,6 +11,9 @@ interface Props {
11
11
  mode?: 'file' | 'folder' | 'file+folder'
12
12
  /** Hide the selection list when a parent presents the files itself. */
13
13
  showFiles?: boolean
14
+ /** Restore selections owned by a parent form; omitted for internal selection state. */
15
+ files?: File[]
16
+ rootName?: string
14
17
  }
15
18
 
16
19
  const props = withDefaults(defineProps<Props>(), {
@@ -32,6 +35,12 @@ const inputRef = ref<HTMLInputElement>()
32
35
  const selectedFiles = ref<File[]>([])
33
36
  const folderName = ref<string | null>(null)
34
37
 
38
+ watch(() => [props.files, props.rootName] as const, ([files, rootName]) => {
39
+ if (files === undefined) return
40
+ selectedFiles.value = [...files]
41
+ folderName.value = rootName ?? extractFolderName(files)
42
+ }, { immediate: true })
43
+
35
44
  const isFolder = computed(() => props.mode === 'folder')
36
45
  const isMixed = computed(() => props.mode === 'file+folder')
37
46