@morscherlab/mint-sdk 1.2.0 → 1.2.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 (152) 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-DELMkUFR.js} +39 -16
  4. package/dist/SettingsModal-DELMkUFR.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/ChartContainer.test.d.ts +1 -0
  8. package/dist/__tests__/components/ChemicalFormula.test.d.ts +1 -0
  9. package/dist/__tests__/components/FormField.test.d.ts +1 -0
  10. package/dist/__tests__/components/LayoutResizeHandle.test.d.ts +1 -0
  11. package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
  12. package/dist/__tests__/components/SearchableSelect.test.d.ts +1 -0
  13. package/dist/__tests__/components/WellPlate.decoration.test.d.ts +1 -0
  14. package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
  15. package/dist/__tests__/composables/useManualLayoutResize.test.d.ts +1 -0
  16. package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
  17. package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
  18. package/dist/components/AdductText.vue.d.ts +17 -0
  19. package/dist/components/AppSidebar.vue.d.ts +7 -7
  20. package/dist/components/BasePill.vue.d.ts +3 -0
  21. package/dist/components/BaseTabs.vue.d.ts +1 -4
  22. package/dist/components/ChartContainer.vue.d.ts +19 -5
  23. package/dist/components/ChemicalFormula.vue.d.ts +28 -2
  24. package/dist/components/DataFrame.vue.d.ts +1 -1
  25. package/dist/components/FileUploader.vue.d.ts +3 -0
  26. package/dist/components/FormField.vue.d.ts +8 -1
  27. package/dist/components/LayoutResizeHandle.vue.d.ts +23 -0
  28. package/dist/components/PlotlyChart.vue.d.ts +34 -6
  29. package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
  30. package/dist/components/SearchableSelect.vue.d.ts +57 -0
  31. package/dist/components/WellPlate.vue.d.ts +5 -1
  32. package/dist/components/index.d.ts +3 -1
  33. package/dist/components/index.js +6 -6
  34. package/dist/{components-DIum5hkx.js → components-CxQVQCpP.js} +1215 -546
  35. package/dist/components-CxQVQCpP.js.map +1 -0
  36. package/dist/composables/index.d.ts +1 -0
  37. package/dist/composables/index.js +8 -8
  38. package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
  39. package/dist/composables/useManualLayoutResize.d.ts +56 -0
  40. package/dist/composables/useToast.d.ts +22 -1
  41. package/dist/{composables-mFXsTA4g.js → composables-DAqOU8Aq.js} +97 -8
  42. package/dist/composables-DAqOU8Aq.js.map +1 -0
  43. package/dist/index.js +11 -11
  44. package/dist/install.js +2 -2
  45. package/dist/styles.css +855 -320
  46. package/dist/templates/index.js +2 -2
  47. package/dist/{templates-CmGfmBRA.js → templates-NuaNHohp.js} +2 -2
  48. package/dist/{templates-CmGfmBRA.js.map → templates-NuaNHohp.js.map} +1 -1
  49. package/dist/types/componentLabTypes.d.ts +3 -0
  50. package/dist/types/components.d.ts +26 -1
  51. package/dist/types/form-builder.d.ts +2 -0
  52. package/dist/types/generated-ui.d.ts +2 -0
  53. package/dist/types/index.d.ts +1 -1
  54. package/dist/{useControlSchema-DldpxNOP.js → useControlSchema-vLiRPUmj.js} +181 -71
  55. package/dist/useControlSchema-vLiRPUmj.js.map +1 -0
  56. package/dist/{useExperimentSelector-CEcIaueJ.js → useExperimentSelector-G_CAOFTX.js} +2 -2
  57. package/dist/{useExperimentSelector-CEcIaueJ.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
  58. package/dist/{useFormBuilder-z5L_dUIy.js → useFormBuilder-D37ajgS8.js} +2 -2
  59. package/dist/{useFormBuilder-z5L_dUIy.js.map → useFormBuilder-D37ajgS8.js.map} +1 -1
  60. package/dist/{validation-uwoJGEb6.js → validation-ChMyixAf.js} +21 -9
  61. package/dist/validation-ChMyixAf.js.map +1 -0
  62. package/package.json +1 -1
  63. package/src/__tests__/components/AppSidebar.test.ts +84 -94
  64. package/src/__tests__/components/AppToastContainer.test.ts +55 -1
  65. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
  66. package/src/__tests__/components/ChartContainer.test.ts +24 -0
  67. package/src/__tests__/components/ChemicalFormula.test.ts +27 -0
  68. package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
  69. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
  70. package/src/__tests__/components/FormBuilder.test.ts +26 -0
  71. package/src/__tests__/components/FormField.test.ts +26 -0
  72. package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
  73. package/src/__tests__/components/LayoutResizeHandle.test.ts +19 -0
  74. package/src/__tests__/components/NumberInput.test.ts +149 -0
  75. package/src/__tests__/components/PlotlyChart.test.ts +48 -0
  76. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
  77. package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
  78. package/src/__tests__/components/RackEditor.test.ts +9 -0
  79. package/src/__tests__/components/SearchableSelect.test.ts +61 -0
  80. package/src/__tests__/components/WellPlate.decoration.test.ts +28 -0
  81. package/src/__tests__/composables/localFiles.test.ts +57 -0
  82. package/src/__tests__/composables/useManualLayoutResize.test.ts +21 -0
  83. package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
  84. package/src/__tests__/generatedUi.test.ts +111 -0
  85. package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
  86. package/src/components/AdductText.story.vue +15 -0
  87. package/src/components/AdductText.vue +40 -0
  88. package/src/components/AppLayout.story.vue +1 -1
  89. package/src/components/AppSidebar.story.vue +7 -10
  90. package/src/components/AppSidebar.vue +178 -58
  91. package/src/components/AppToastContainer.vue +65 -23
  92. package/src/components/BasePill.story.vue +2 -0
  93. package/src/components/BasePill.vue +4 -0
  94. package/src/components/BaseTabs.story.vue +0 -42
  95. package/src/components/BaseTabs.vue +3 -6
  96. package/src/components/ChartContainer.story.vue +21 -0
  97. package/src/components/ChartContainer.vue +27 -6
  98. package/src/components/ChemicalFormula.story.vue +7 -0
  99. package/src/components/ChemicalFormula.vue +40 -11
  100. package/src/components/FileUploader.vue +10 -1
  101. package/src/components/FormField.vue +10 -3
  102. package/src/components/GeneratedPathInput.vue +11 -7
  103. package/src/components/LayoutResizeHandle.story.vue +26 -0
  104. package/src/components/LayoutResizeHandle.vue +62 -0
  105. package/src/components/NumberInput.story.vue +32 -13
  106. package/src/components/NumberInput.vue +145 -56
  107. package/src/components/PlotlyChart.vue +77 -11
  108. package/src/components/PluginWorkspaceView.props.ts +3 -3
  109. package/src/components/RackEditor.vue +38 -32
  110. package/src/components/SearchableSelect.story.vue +24 -0
  111. package/src/components/SearchableSelect.vue +223 -0
  112. package/src/components/WellPlate.story.vue +16 -0
  113. package/src/components/WellPlate.vue +17 -8
  114. package/src/components/index.ts +3 -1
  115. package/src/components/internal/FormFieldRendererInternal.vue +17 -0
  116. package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
  117. package/src/composables/filePicker/localFiles.ts +21 -14
  118. package/src/composables/filePicker/usePickerSelection.ts +15 -4
  119. package/src/composables/index.ts +10 -0
  120. package/src/composables/pluginWorkspaceModel.ts +0 -3
  121. package/src/composables/useManualLayoutResize.ts +165 -0
  122. package/src/composables/usePluginWorkspace.ts +1 -2
  123. package/src/composables/useToast.ts +21 -5
  124. package/src/generated/schema.ts +58 -18
  125. package/src/styles/components/adduct-text.css +45 -0
  126. package/src/styles/components/app-sidebar.css +282 -121
  127. package/src/styles/components/chart-container.css +92 -0
  128. package/src/styles/components/chemical-formula.css +34 -4
  129. package/src/styles/components/form-builder.css +1 -1
  130. package/src/styles/components/form-field.css +28 -0
  131. package/src/styles/components/layout-resize-handle.css +60 -0
  132. package/src/styles/components/number-input.css +75 -91
  133. package/src/styles/components/pill.css +49 -51
  134. package/src/styles/components/rack-editor.css +31 -9
  135. package/src/styles/components/searchable-select.css +63 -0
  136. package/src/styles/components/segmented-control.css +16 -11
  137. package/src/styles/components/tabs.css +18 -34
  138. package/src/styles/components/toast.css +136 -27
  139. package/src/styles/components/well-plate.css +9 -0
  140. package/src/styles/index.css +3 -0
  141. package/src/styles/variables.css +25 -5
  142. package/src/types/componentLabTypes.ts +6 -0
  143. package/src/types/components.ts +30 -0
  144. package/src/types/form-builder.ts +2 -0
  145. package/src/types/generated-ui.ts +2 -0
  146. package/src/types/index.ts +5 -0
  147. package/dist/SettingsModal-BYR20I4c.js.map +0 -1
  148. package/dist/Skeleton-BAF3CKY7.js.map +0 -1
  149. package/dist/components-DIum5hkx.js.map +0 -1
  150. package/dist/composables-mFXsTA4g.js.map +0 -1
  151. package/dist/useControlSchema-DldpxNOP.js.map +0 -1
  152. package/dist/validation-uwoJGEb6.js.map +0 -1
@@ -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,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import ChartContainer from './ChartContainer.vue'
3
+ import IconButton from './IconButton.vue'
3
4
  </script>
4
5
 
5
6
  <template>
@@ -109,5 +110,25 @@ import ChartContainer from './ChartContainer.vue'
109
110
  </ChartContainer>
110
111
  </div>
111
112
  </Variant>
113
+
114
+ <Variant title="Workbench Frame">
115
+ <div style="height: 420px; border: 1px solid var(--border-color); border-radius: 0.5rem; overflow: hidden;">
116
+ <ChartContainer variant="frame" empty empty-message="Select a sample to populate this chart">
117
+ <template #header>
118
+ <strong style="font-size: 0.8125rem; color: var(--text-primary);">Extracted Ion Chromatogram</strong>
119
+ </template>
120
+ <template #toolbar>
121
+ <IconButton size="sm" label="Reset zoom">
122
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
123
+ <path d="M4 4v6h6M20 20v-6h-6M5.5 15a7 7 0 0 0 11.8 2M18.5 9A7 7 0 0 0 6.7 7" />
124
+ </svg>
125
+ </IconButton>
126
+ </template>
127
+ <template #footer>
128
+ Retention time (min)
129
+ </template>
130
+ </ChartContainer>
131
+ </div>
132
+ </Variant>
112
133
  </Story>
113
134
  </template>
@@ -9,27 +9,44 @@ interface Props {
9
9
  loading?: boolean
10
10
  empty?: boolean
11
11
  emptyMessage?: string
12
+ /** `frame` fills its parent and uses compact workbench chrome. */
13
+ variant?: 'card' | 'frame'
12
14
  }
13
15
 
14
16
  withDefaults(defineProps<Props>(), {
15
17
  loading: false,
16
18
  empty: false,
17
19
  emptyMessage: 'No data available',
20
+ variant: 'card',
18
21
  })
22
+
23
+ defineSlots<{
24
+ default?: () => unknown
25
+ header?: () => unknown
26
+ toolbar?: () => unknown
27
+ subhead?: () => unknown
28
+ empty?: () => unknown
29
+ legend?: () => unknown
30
+ footer?: () => unknown
31
+ }>()
19
32
  </script>
20
33
 
21
34
  <template>
22
- <div class="mint-chart">
23
- <div v-if="title || description || $slots.toolbar" class="mint-chart__header">
24
- <div v-if="title || description" class="mint-chart__header-text">
25
- <h3 v-if="title" class="mint-chart__title">{{ title }}</h3>
26
- <p v-if="description" class="mint-chart__description">{{ description }}</p>
27
- </div>
35
+ <div :class="['mint-chart', `mint-chart--${variant}`]">
36
+ <div v-if="title || description || $slots.header || $slots.toolbar" class="mint-chart__header">
37
+ <slot name="header">
38
+ <div v-if="title || description" class="mint-chart__header-text">
39
+ <h3 v-if="title" class="mint-chart__title">{{ title }}</h3>
40
+ <p v-if="description" class="mint-chart__description">{{ description }}</p>
41
+ </div>
42
+ </slot>
28
43
  <div v-if="$slots.toolbar" class="mint-chart__toolbar">
29
44
  <slot name="toolbar" />
30
45
  </div>
31
46
  </div>
32
47
 
48
+ <slot name="subhead" />
49
+
33
50
  <div class="mint-chart__body">
34
51
  <!-- Loading overlay -->
35
52
  <div v-if="loading" class="mint-chart__loading-overlay">
@@ -56,5 +73,9 @@ withDefaults(defineProps<Props>(), {
56
73
  <div v-if="$slots.legend" class="mint-chart__legend">
57
74
  <slot name="legend" />
58
75
  </div>
76
+
77
+ <div v-if="$slots.footer" class="mint-chart__footer">
78
+ <slot name="footer" />
79
+ </div>
59
80
  </div>
60
81
  </template>
@@ -98,5 +98,12 @@ const formulas = [
98
98
  </div>
99
99
  </div>
100
100
  </Variant>
101
+
102
+ <Variant title="Formula with Adduct">
103
+ <div style="display: flex; gap: 1rem; padding: 2rem; align-items: center;">
104
+ <ChemicalFormula formula="C6H12O6" adduct="[M+H]+" size="sm" />
105
+ <ChemicalFormula formula="C3H6O3" adduct="[M-H]-" size="sm" pill tone="secondary" />
106
+ </div>
107
+ </Variant>
101
108
  </Story>
102
109
  </template>
@@ -1,17 +1,32 @@
1
1
  <script setup lang="ts">
2
- /** Renders a chemical formula string (H₂O, C₆H₁₂O₆, Ca(OH)₂) with typographically correct subscripts and superscripts for charges. */
2
+ /** Renders a chemical formula and optional MS adduct with typographically correct subscripts and charges. */
3
+ import { computed } from 'vue'
3
4
  import { useChemicalFormula, type FormulaPart } from '../composables/useChemicalFormula'
5
+ import AdductText from './AdductText.vue'
4
6
 
5
7
  interface Props {
6
- formula: string
8
+ formula?: string | null
7
9
  inline?: boolean
10
+ adduct?: string | null
11
+ emptyText?: string
12
+ pill?: boolean
13
+ size?: 'xs' | 'sm' | 'inherit'
14
+ tone?: 'primary' | 'secondary' | 'muted'
8
15
  }
9
16
 
10
- withDefaults(defineProps<Props>(), {
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ formula: null,
11
19
  inline: true,
20
+ adduct: null,
21
+ emptyText: '-',
22
+ pill: false,
23
+ size: 'inherit',
24
+ tone: 'primary',
12
25
  })
13
26
 
14
27
  const { renderFormulaParts } = useChemicalFormula()
28
+ const formula = computed(() => props.formula?.trim() ?? '')
29
+ const title = computed(() => [formula.value, props.adduct?.trim()].filter(Boolean).join(' '))
15
30
 
16
31
  function getParts(f: string): FormulaPart[] {
17
32
  return renderFormulaParts(f)
@@ -21,15 +36,29 @@ function getParts(f: string): FormulaPart[] {
21
36
  <template>
22
37
  <component
23
38
  :is="inline ? 'span' : 'div'"
24
- :class="['mint-chem-formula', !inline ? 'mint-chem-formula--block' : '']"
39
+ :class="[
40
+ 'mint-chem-formula',
41
+ !inline ? 'mint-chem-formula--block' : '',
42
+ `mint-chem-formula--${size}`,
43
+ `mint-chem-formula--${tone}`,
44
+ { 'mint-chem-formula--pill': pill, 'mint-chem-formula--empty': !formula },
45
+ ]"
46
+ :title="title || undefined"
25
47
  >
26
- <template v-for="(part, i) in getParts(formula)" :key="i">
27
- <span v-if="part.type === 'element'" class="mint-chem-formula__element">{{ part.text }}</span>
28
- <span v-else-if="part.type === 'subscript'" class="mint-chem-formula__subscript">{{ part.text }}</span>
29
- <span v-else-if="part.type === 'superscript'" class="mint-chem-formula__superscript">{{ part.text }}</span>
30
- <span v-else-if="part.type === 'paren'" class="mint-chem-formula__paren">{{ part.text }}</span>
31
- <span v-else-if="part.type === 'dot'" class="mint-chem-formula__dot">{{ part.text }}</span>
32
- <span v-else-if="part.type === 'charge'" class="mint-chem-formula__charge">{{ part.text }}</span>
48
+ <template v-if="formula">
49
+ <span class="mint-chem-formula__formula">
50
+ <template v-for="(part, i) in getParts(formula)" :key="i">
51
+ <span v-if="part.type === 'element'" class="mint-chem-formula__element">{{ part.text }}</span>
52
+ <span v-else-if="part.type === 'subscript'" class="mint-chem-formula__subscript">{{ part.text }}</span>
53
+ <span v-else-if="part.type === 'superscript'" class="mint-chem-formula__superscript">{{ part.text }}</span>
54
+ <span v-else-if="part.type === 'paren'" class="mint-chem-formula__paren">{{ part.text }}</span>
55
+ <span v-else-if="part.type === 'dot'" class="mint-chem-formula__dot">{{ part.text }}</span>
56
+ <span v-else-if="part.type === 'charge'" class="mint-chem-formula__charge">{{ part.text }}</span>
57
+ </template>
58
+ </span>
59
+ <AdductText v-if="adduct" :value="adduct" size="inherit" />
60
+ <slot />
33
61
  </template>
62
+ <span v-else>{{ emptyText }}</span>
34
63
  </component>
35
64
  </template>
@@ -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
 
@@ -10,9 +10,13 @@ interface Props {
10
10
  showOptional?: boolean
11
11
  htmlFor?: string
12
12
  fieldId?: string
13
+ /** `row` puts the label left and the control in a fixed 7.5rem column. Use for numbers, toggles, short enums. */
14
+ layout?: 'stacked' | 'row'
13
15
  }
14
16
 
15
- const props = defineProps<Props>()
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ layout: 'stacked',
19
+ })
16
20
 
17
21
  const errorId = computed(() =>
18
22
  props.error && props.fieldId ? `${props.fieldId}-error` : undefined
@@ -26,7 +30,7 @@ const describedBy = computed(() => errorId.value ?? hintId.value)
26
30
  </script>
27
31
 
28
32
  <template>
29
- <div class="mint-form-field">
33
+ <div :class="['mint-form-field', layout === 'row' ? 'mint-form-field--row' : '']">
30
34
  <div v-if="label" class="mint-form-field__label-row">
31
35
  <label
32
36
  :for="htmlFor"
@@ -43,7 +47,10 @@ const describedBy = computed(() => errorId.value ?? hintId.value)
43
47
  </span>
44
48
  </div>
45
49
 
46
- <slot :described-by="describedBy" />
50
+ <div v-if="layout === 'row'" class="mint-form-field__control">
51
+ <slot :described-by="describedBy" />
52
+ </div>
53
+ <slot v-else :described-by="describedBy" />
47
54
 
48
55
  <p v-if="error" :id="errorId" class="mint-form-field__error" role="alert">
49
56
  {{ error }}
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, watch } from 'vue'
3
3
 
4
4
  import { createGeneratedAnalysisTransport } from '../composables/useGeneratedAnalysis'
5
5
  import {
@@ -32,6 +32,13 @@ const emit = defineEmits<{
32
32
  }>()
33
33
 
34
34
  const mode = ref<'file' | 'folder'>('file')
35
+ const selections = computed(() => Array.isArray(props.modelValue)
36
+ ? props.modelValue
37
+ : props.modelValue ? [props.modelValue] : [])
38
+ const selectedFiles = computed(() => selections.value.flatMap(selection => selection.files))
39
+ watch(selections, (current) => {
40
+ if (current.length) mode.value = current.every(selection => selection.rootName) ? 'folder' : 'file'
41
+ }, { immediate: true })
35
42
  const artifactOpen = ref(false)
36
43
  const artifactLoading = ref(false)
37
44
  const artifactError = ref('')
@@ -73,12 +80,7 @@ function selectFiles(
73
80
 
74
81
  function removeFiles(files: File[]): void {
75
82
  const removed = new Set(files)
76
- const current = Array.isArray(props.modelValue)
77
- ? props.modelValue
78
- : props.modelValue
79
- ? [props.modelValue]
80
- : []
81
- const remaining = current.flatMap((selection) => {
83
+ const remaining = selections.value.flatMap((selection) => {
82
84
  const indexes = selection.files
83
85
  .map((file, index) => removed.has(file) ? -1 : index)
84
86
  .filter(index => index >= 0)
@@ -186,6 +188,8 @@ function artifactFileReference(detail: AnalysisArtifactDetail): ArtifactFileRefe
186
188
  <FileUploader
187
189
  :key="mode"
188
190
  :mode="mode"
191
+ :files="selectedFiles"
192
+ :root-name="selections[0]?.rootName"
189
193
  :multiple="multiple && mode === 'file'"
190
194
  :disabled="disabled"
191
195
  @upload="selectFiles"
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import LayoutResizeHandle from './LayoutResizeHandle.vue'
4
+
5
+ const width = ref(320)
6
+ </script>
7
+
8
+ <template>
9
+ <Story title="Layout/LayoutResizeHandle">
10
+ <Variant title="Keyboard splitter">
11
+ <div style="display: flex; height: 16rem; padding: 2rem;">
12
+ <div :style="{ width: `${width}px`, background: 'var(--bg-secondary)', border: '1px solid var(--border-color)' }">
13
+ Panel width: {{ width }}px
14
+ </div>
15
+ <LayoutResizeHandle
16
+ v-model="width"
17
+ orientation="vertical"
18
+ label="Resize panel"
19
+ :min="240"
20
+ :max="480"
21
+ />
22
+ <div style="flex: 1; background: var(--bg-tertiary);" />
23
+ </div>
24
+ </Variant>
25
+ </Story>
26
+ </template>