@morscherlab/mint-sdk 1.2.0-beta.9 → 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 (215) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DHE9k8TP.js} +5 -5
  5. package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
  6. package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DOcCf0Vo.js} +41 -18
  7. package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
  8. package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-jhF9wUkU.js} +19 -9
  9. package/dist/Skeleton-jhF9wUkU.js.map +1 -0
  10. package/dist/__tests__/apiErrors.test.d.ts +1 -0
  11. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  12. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  14. package/dist/__tests__/components/FormField.test.d.ts +1 -0
  15. package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
  16. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  17. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  18. package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  20. package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
  21. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  22. package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
  23. package/dist/apiErrors.d.ts +26 -0
  24. package/dist/components/AppSidebar.vue.d.ts +7 -7
  25. package/dist/components/BaseInput.vue.d.ts +1 -1
  26. package/dist/components/BasePill.vue.d.ts +3 -0
  27. package/dist/components/BaseSelect.vue.d.ts +1 -1
  28. package/dist/components/BaseTabs.vue.d.ts +1 -4
  29. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  30. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  31. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  32. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  33. package/dist/components/DatePicker.vue.d.ts +1 -1
  34. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  35. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  36. package/dist/components/FilePicker.vue.d.ts +32 -0
  37. package/dist/components/FileUploader.vue.d.ts +8 -2
  38. package/dist/components/FormField.vue.d.ts +8 -1
  39. package/dist/components/FormulaInput.vue.d.ts +1 -1
  40. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  41. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  42. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  43. package/dist/components/NumberInput.vue.d.ts +1 -1
  44. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  45. package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
  46. package/dist/components/ResourceCard.vue.d.ts +1 -1
  47. package/dist/components/SecretInput.vue.d.ts +1 -1
  48. package/dist/components/SequenceInput.vue.d.ts +1 -1
  49. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  50. package/dist/components/TagsInput.vue.d.ts +1 -1
  51. package/dist/components/TimePicker.vue.d.ts +1 -1
  52. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  53. package/dist/components/UnitInput.vue.d.ts +1 -1
  54. package/dist/components/WellPlate.vue.d.ts +1 -1
  55. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  56. package/dist/components/index.d.ts +4 -0
  57. package/dist/components/index.js +8 -8
  58. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  59. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  60. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  61. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  62. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  63. package/dist/{components-CgAUzF8F.js → components-Cp8DcNrU.js} +4363 -1730
  64. package/dist/components-Cp8DcNrU.js.map +1 -0
  65. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  66. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  67. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  68. package/dist/composables/filePicker/validation.d.ts +5 -0
  69. package/dist/composables/index.d.ts +2 -0
  70. package/dist/composables/index.js +10 -10
  71. package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
  72. package/dist/composables/useApi.d.ts +14 -7
  73. package/dist/composables/useFileBrowser.d.ts +55 -0
  74. package/dist/composables/useFilePicker.d.ts +189 -0
  75. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  76. package/dist/composables/useToast.d.ts +22 -1
  77. package/dist/{composables-B_pia3hO.js → composables-D_4qqOlp.js} +300 -11
  78. package/dist/composables-D_4qqOlp.js.map +1 -0
  79. package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
  80. package/dist/errors-BLuqMee5.js.map +1 -0
  81. package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
  82. package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
  83. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  84. package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.js +15 -15
  87. package/dist/install.js +2 -2
  88. package/dist/stores/index.js +2 -2
  89. package/dist/styles.css +2957 -1708
  90. package/dist/templates/index.js +2 -2
  91. package/dist/{templates-Qq113pyZ.js → templates-Dd9bBSs6.js} +2 -2
  92. package/dist/{templates-Qq113pyZ.js.map → templates-Dd9bBSs6.js.map} +1 -1
  93. package/dist/types/components.d.ts +21 -0
  94. package/dist/types/fileBrowserTypes.d.ts +77 -0
  95. package/dist/types/filePicker.d.ts +77 -0
  96. package/dist/types/form-builder.d.ts +2 -0
  97. package/dist/types/generated-ui.d.ts +2 -0
  98. package/dist/types/index.d.ts +3 -1
  99. package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BB2GRPhW.js} +190 -105
  100. package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
  101. package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
  102. package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
  103. package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-G_CAOFTX.js} +4 -4
  104. package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
  105. package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-BnGSdSRG.js} +2 -2
  106. package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
  107. package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-CTxuzLIX.js} +70 -13
  108. package/dist/validation-CTxuzLIX.js.map +1 -0
  109. package/package.json +1 -1
  110. package/src/__tests__/apiErrors.test.ts +81 -0
  111. package/src/__tests__/components/AppSidebar.test.ts +84 -94
  112. package/src/__tests__/components/AppToastContainer.test.ts +55 -1
  113. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
  114. package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
  115. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
  116. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  117. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  118. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  119. package/src/__tests__/components/FilePicker.test.ts +487 -0
  120. package/src/__tests__/components/FormBuilder.test.ts +26 -0
  121. package/src/__tests__/components/FormField.test.ts +26 -0
  122. package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
  123. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  124. package/src/__tests__/components/NumberInput.test.ts +105 -0
  125. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  126. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
  127. package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
  128. package/src/__tests__/components/SegmentedControl.test.ts +19 -0
  129. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  130. package/src/__tests__/composables/localFiles.test.ts +57 -0
  131. package/src/__tests__/composables/useApi.test.ts +92 -9
  132. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  133. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  134. package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
  135. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  136. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  137. package/src/__tests__/generatedUi.test.ts +111 -0
  138. package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
  139. package/src/apiErrors.ts +85 -0
  140. package/src/components/AppLayout.story.vue +1 -1
  141. package/src/components/AppSidebar.story.vue +7 -10
  142. package/src/components/AppSidebar.vue +178 -58
  143. package/src/components/AppToastContainer.vue +65 -23
  144. package/src/components/BasePill.story.vue +2 -0
  145. package/src/components/BasePill.vue +4 -0
  146. package/src/components/BaseTabs.story.vue +0 -42
  147. package/src/components/BaseTabs.vue +3 -6
  148. package/src/components/ExperimentPopover.vue +10 -2
  149. package/src/components/FileBrowserModal.story.vue +203 -0
  150. package/src/components/FileBrowserModal.vue +384 -0
  151. package/src/components/FilePicker.story.vue +44 -0
  152. package/src/components/FilePicker.vue +284 -0
  153. package/src/components/FileUploader.vue +42 -15
  154. package/src/components/FormField.vue +10 -3
  155. package/src/components/GeneratedPathInput.vue +11 -7
  156. package/src/components/GeneratedPlotlyResult.vue +34 -69
  157. package/src/components/GeneratedResultRenderer.vue +0 -5
  158. package/src/components/NumberInput.story.vue +32 -13
  159. package/src/components/NumberInput.vue +116 -55
  160. package/src/components/PlotlyChart.story.vue +61 -0
  161. package/src/components/PlotlyChart.vue +233 -0
  162. package/src/components/PluginWorkspaceView.props.ts +3 -3
  163. package/src/components/SegmentedControl.vue +2 -2
  164. package/src/components/fileBrowserPresentation.ts +39 -0
  165. package/src/components/index.ts +5 -0
  166. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  167. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  168. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  169. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  170. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  171. package/src/components/internal/FormFieldRendererInternal.vue +17 -0
  172. package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
  173. package/src/composables/filePicker/localFiles.ts +65 -0
  174. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  175. package/src/composables/filePicker/usePickerSelection.ts +270 -0
  176. package/src/composables/filePicker/validation.ts +41 -0
  177. package/src/composables/index.ts +7 -0
  178. package/src/composables/pluginWorkspaceModel.ts +0 -3
  179. package/src/composables/useApi.ts +57 -13
  180. package/src/composables/useFileBrowser.ts +314 -0
  181. package/src/composables/useFilePicker.ts +366 -0
  182. package/src/composables/useGeneratedAnalysis.ts +10 -17
  183. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  184. package/src/composables/usePluginClient.ts +3 -0
  185. package/src/composables/usePluginWorkspace.ts +1 -2
  186. package/src/composables/useToast.ts +21 -5
  187. package/src/generated/schema.ts +58 -18
  188. package/src/index.ts +3 -0
  189. package/src/styles/components/app-sidebar.css +282 -121
  190. package/src/styles/components/file-browser-modal.css +489 -0
  191. package/src/styles/components/file-picker.css +502 -0
  192. package/src/styles/components/form-builder.css +1 -1
  193. package/src/styles/components/form-field.css +28 -0
  194. package/src/styles/components/icon-button.css +9 -0
  195. package/src/styles/components/number-input.css +73 -91
  196. package/src/styles/components/pill.css +49 -51
  197. package/src/styles/components/segmented-control.css +16 -11
  198. package/src/styles/components/tabs.css +18 -34
  199. package/src/styles/components/toast.css +136 -27
  200. package/src/styles/index.css +3 -0
  201. package/src/styles/variables.css +25 -5
  202. package/src/types/components.ts +23 -0
  203. package/src/types/fileBrowserTypes.ts +85 -0
  204. package/src/types/filePicker.ts +72 -0
  205. package/src/types/form-builder.ts +2 -0
  206. package/src/types/generated-ui.ts +2 -0
  207. package/src/types/index.ts +16 -0
  208. package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
  209. package/dist/SettingsModal-Dphh3RrI.js.map +0 -1
  210. package/dist/Skeleton-DxtwJ5gt.js.map +0 -1
  211. package/dist/components-CgAUzF8F.js.map +0 -1
  212. package/dist/composables-B_pia3hO.js.map +0 -1
  213. package/dist/errors-_W0-LbA6.js.map +0 -1
  214. package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
  215. package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
@@ -1,7 +1,20 @@
1
1
  <script setup lang="ts">
2
+ import { ref } from 'vue'
2
3
  import NumberInput from './NumberInput.vue'
3
4
 
4
5
  const sizes = ['sm', 'md', 'lg'] as const
6
+
7
+ const sizeValues = ref<Record<string, number | undefined>>({ sm: 42, md: 42, lg: 42 })
8
+ const samples = ref<number | undefined>(96)
9
+ const temperature = ref<number | undefined>(37)
10
+ const volume = ref<number | undefined>(100)
11
+ const overMax = ref<number | undefined>(999)
12
+ const empty = ref<number | undefined>(undefined)
13
+ const mass = ref<number | undefined>(12.5)
14
+ const ph = ref<number | undefined>(7.4)
15
+ const wells = ref<number | undefined>(96)
16
+ const sliderPh = ref<number | undefined>(7.4)
17
+ const percent = ref<number | undefined>(45)
5
18
  </script>
6
19
 
7
20
  <template>
@@ -39,6 +52,9 @@ const sizes = ['sm', 'md', 'lg'] as const
39
52
  <HstCheckbox v-model="state.disabled" title="Disabled" />
40
53
  <HstCheckbox v-model="state.error" title="Error" />
41
54
  </template>
55
+ <template #initState>
56
+ {{ { value: 42, min: 0, max: 100, step: 1, size: 'md', placeholder: '', disabled: false, error: false } }}
57
+ </template>
42
58
  </Variant>
43
59
 
44
60
  <Variant title="Sizes">
@@ -47,7 +63,7 @@ const sizes = ['sm', 'md', 'lg'] as const
47
63
  <p style="margin: 0 0 0.5rem; font-size: 0.75rem; color: var(--text-muted, #94a3b8); text-transform: uppercase; letter-spacing: 0.05em;">
48
64
  {{ size }}
49
65
  </p>
50
- <NumberInput :model-value="42" :size="size" />
66
+ <NumberInput v-model="sizeValues[size]" :size="size" />
51
67
  </div>
52
68
  </div>
53
69
  </Variant>
@@ -56,22 +72,22 @@ const sizes = ['sm', 'md', 'lg'] as const
56
72
  <div style="padding: 2rem; max-width: 300px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem;">
57
73
  <div>
58
74
  <p style="margin: 0 0 0.5rem; font-size: 0.875rem; color: var(--text-primary, #e2e8f0);">Sample count (0-384)</p>
59
- <NumberInput :model-value="96" :min="0" :max="384" :step="1" placeholder="Samples" />
75
+ <NumberInput v-model="samples" :min="0" :max="384" :step="1" placeholder="Samples" />
60
76
  </div>
61
77
  <div>
62
78
  <p style="margin: 0 0 0.5rem; font-size: 0.875rem; color: var(--text-primary, #e2e8f0);">Temperature (step 0.5)</p>
63
- <NumberInput :model-value="37" :min="0" :max="100" :step="0.5" />
79
+ <NumberInput v-model="temperature" :min="0" :max="100" :step="0.5" />
64
80
  </div>
65
81
  <div>
66
82
  <p style="margin: 0 0 0.5rem; font-size: 0.875rem; color: var(--text-primary, #e2e8f0);">Volume in uL (step 10)</p>
67
- <NumberInput :model-value="100" :min="0" :max="1000" :step="10" />
83
+ <NumberInput v-model="volume" :min="0" :max="1000" :step="10" />
68
84
  </div>
69
85
  </div>
70
86
  </Variant>
71
87
 
72
88
  <Variant title="Error State">
73
89
  <div style="padding: 2rem; max-width: 300px; margin: 0 auto;">
74
- <NumberInput :model-value="999" :max="100" error />
90
+ <NumberInput v-model="overMax" :max="100" error />
75
91
  </div>
76
92
  </Variant>
77
93
 
@@ -83,16 +99,16 @@ const sizes = ['sm', 'md', 'lg'] as const
83
99
 
84
100
  <Variant title="Empty with Placeholder">
85
101
  <div style="padding: 2rem; max-width: 300px; margin: 0 auto;">
86
- <NumberInput placeholder="Enter value..." />
102
+ <NumberInput v-model="empty" placeholder="Enter value..." />
87
103
  </div>
88
104
  </Variant>
89
105
 
90
- <Variant title="Hover-Reveal Steppers (default)">
106
+ <Variant title="Steppers (always visible)">
91
107
  <template #default="{ state }">
92
108
  <div style="padding: 2rem; max-width: 320px; display: flex; flex-direction: column; gap: 1rem;">
93
109
  <NumberInput v-model="state.value" :step="0.5" />
94
110
  <p style="margin: 0; font-size: 0.8125rem; color: var(--text-muted);">
95
- Hover or focus to reveal the ↑/↓ steppers. Rest = clean.
111
+ Two horizontal steppers, hairline-divided. Arrow keys, PageUp/PageDown also step.
96
112
  </p>
97
113
  </div>
98
114
  </template>
@@ -106,16 +122,19 @@ const sizes = ['sm', 'md', 'lg'] as const
106
122
 
107
123
  <Variant title="With unit suffix">
108
124
  <div style="padding: 2rem; max-width: 320px; display: flex; flex-direction: column; gap: 0.75rem;">
109
- <NumberInput :model-value="12.5" unit="mg" :step="0.1" />
110
- <NumberInput :model-value="7.4" unit="pH" :step="0.1" :min="0" :max="14" />
111
- <NumberInput :model-value="96" unit="wells" :step="1" />
125
+ <NumberInput v-model="mass" unit="mg" :step="0.1" />
126
+ <NumberInput v-model="ph" unit="pH" :step="0.1" :min="0" :max="14" />
127
+ <NumberInput v-model="wells" unit="wells" :step="1" />
112
128
  </div>
113
129
  </Variant>
114
130
 
115
131
  <Variant title="Slider mode (min + max provided)">
116
132
  <div style="padding: 2rem; max-width: 320px; display: flex; flex-direction: column; gap: 1rem;">
117
- <NumberInput :model-value="7.4" :min="0" :max="14" :step="0.1" unit="pH" />
118
- <NumberInput :model-value="45" :min="0" :max="100" :step="1" unit="%" />
133
+ <NumberInput v-model="sliderPh" :min="0" :max="14" :step="0.1" unit="pH" />
134
+ <NumberInput v-model="percent" :min="0" :max="100" :step="1" unit="%" />
135
+ <p style="margin: 0; font-size: 0.8125rem; color: var(--text-muted);">
136
+ Drag anywhere in the field (outside the text) to scrub. Home/End jump to min/max.
137
+ </p>
119
138
  </div>
120
139
  </Variant>
121
140
  </Story>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- /** Numeric input with increment/decrement buttons, optional unit label, and slider progress track. */
2
+ /** Numeric input with always-visible steppers, optional unit label, and (with min+max) an in-field drag-to-scrub range fill. */
3
3
  import { computed } from 'vue'
4
4
 
5
5
  interface Props {
@@ -33,7 +33,7 @@ const sliderPercent = computed(() => {
33
33
  if (min === undefined || max === undefined || props.modelValue === undefined) return 0
34
34
  const range = max - min
35
35
  if (range === 0) return 0
36
- return ((props.modelValue - min) / range) * 100
36
+ return Math.min(100, Math.max(0, ((props.modelValue - min) / range) * 100))
37
37
  })
38
38
 
39
39
  const canDecrement = computed(() => {
@@ -55,6 +55,17 @@ function clamp(value: number): number {
55
55
  return result
56
56
  }
57
57
 
58
+ /** Clamp and trim float noise (0.1 + 0.2 → 0.3) without losing real precision. */
59
+ function settle(value: number): number {
60
+ return Number(clamp(value).toFixed(10))
61
+ }
62
+
63
+ /** Snap to the step grid anchored at `min`, for pointer scrubbing. */
64
+ function snap(value: number): number {
65
+ const origin = props.min ?? 0
66
+ return settle(origin + Math.round((value - origin) / props.step) * props.step)
67
+ }
68
+
58
69
  function handleInput(event: Event) {
59
70
  const target = event.target as HTMLInputElement
60
71
  const value = target.value === '' ? undefined : Number(target.value)
@@ -65,24 +76,70 @@ function handleInput(event: Event) {
65
76
  }
66
77
  }
67
78
 
68
- function handleSliderInput(event: Event) {
69
- const target = event.target as HTMLInputElement
70
- const value = Number(target.value)
71
- if (!isNaN(value)) {
72
- emit('update:modelValue', clamp(value))
73
- }
79
+ function nudge(delta: number) {
80
+ if (props.disabled) return
81
+ if (delta < 0 && !canDecrement.value) return
82
+ if (delta > 0 && !canIncrement.value) return
83
+ const current = props.modelValue ?? (delta < 0 ? props.max ?? 0 : props.min ?? 0)
84
+ emit('update:modelValue', settle(current + delta))
74
85
  }
75
86
 
76
87
  function decrement() {
77
- if (props.disabled || !canDecrement.value) return
78
- const current = props.modelValue ?? (props.max ?? 0)
79
- emit('update:modelValue', clamp(current - props.step))
88
+ nudge(-props.step)
80
89
  }
81
90
 
82
91
  function increment() {
83
- if (props.disabled || !canIncrement.value) return
84
- const current = props.modelValue ?? (props.min ?? 0)
85
- emit('update:modelValue', clamp(current + props.step))
92
+ nudge(props.step)
93
+ }
94
+
95
+ // Home/End only apply on the slider surface: inside the text input they must
96
+ // keep moving the caret.
97
+ function handleKeydown(event: KeyboardEvent, allowJump = false) {
98
+ if (props.disabled) return
99
+ switch (event.key) {
100
+ case 'ArrowUp': increment(); break
101
+ case 'ArrowDown': decrement(); break
102
+ case 'PageUp': nudge(props.step * 10); break
103
+ case 'PageDown': nudge(-props.step * 10); break
104
+ case 'Home':
105
+ if (!allowJump || props.min === undefined) return
106
+ emit('update:modelValue', props.min)
107
+ break
108
+ case 'End':
109
+ if (!allowJump || props.max === undefined) return
110
+ emit('update:modelValue', props.max)
111
+ break
112
+ default:
113
+ return
114
+ }
115
+ event.preventDefault()
116
+ }
117
+
118
+ // Drag-to-scrub across the field. Pointerdown on the <input> itself is left
119
+ // alone so click-to-place-caret and double-click-to-select keep working.
120
+ function scrubTo(clientX: number, area: HTMLElement) {
121
+ const rect = area.getBoundingClientRect()
122
+ if (rect.width === 0 || props.min === undefined || props.max === undefined) return
123
+ const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width))
124
+ emit('update:modelValue', snap(props.min + ratio * (props.max - props.min)))
125
+ }
126
+
127
+ function handleScrubStart(event: PointerEvent) {
128
+ if (props.disabled || !isSliderMode.value) return
129
+ if ((event.target as HTMLElement).tagName === 'INPUT') return
130
+ const area = event.currentTarget as HTMLElement
131
+ event.preventDefault()
132
+ area.setPointerCapture?.(event.pointerId)
133
+ scrubTo(event.clientX, area)
134
+ const move = (e: PointerEvent) => scrubTo(e.clientX, area)
135
+ const stop = () => {
136
+ area.removeEventListener('pointermove', move)
137
+ area.removeEventListener('pointerup', stop)
138
+ area.removeEventListener('pointercancel', stop)
139
+ }
140
+ area.addEventListener('pointermove', move)
141
+ area.addEventListener('pointerup', stop)
142
+ area.addEventListener('pointercancel', stop)
86
143
  }
87
144
  </script>
88
145
 
@@ -97,62 +154,66 @@ function increment() {
97
154
  ]"
98
155
  >
99
156
  <div class="mint-number-input__field">
100
- <input
101
- type="number"
102
- :value="modelValue"
103
- :min="min"
104
- :max="max"
105
- :step="step"
106
- :disabled="disabled"
107
- :placeholder="placeholder"
108
- :class="[
109
- 'mint-number-input__input',
110
- `mint-number-input__input--${size}`,
111
- ]"
112
- @input="handleInput"
113
- />
114
-
115
- <span v-if="unit" class="mint-number-input__unit">{{ unit }}</span>
157
+ <div
158
+ class="mint-number-input__scrub"
159
+ :style="isSliderMode ? { '--mint-number-input-fill': `${sliderPercent}%` } : undefined"
160
+ :role="isSliderMode ? 'slider' : undefined"
161
+ :tabindex="isSliderMode && !disabled ? 0 : undefined"
162
+ :aria-valuenow="isSliderMode ? modelValue : undefined"
163
+ :aria-valuemin="isSliderMode ? min : undefined"
164
+ :aria-valuemax="isSliderMode ? max : undefined"
165
+ :aria-orientation="isSliderMode ? 'horizontal' : undefined"
166
+ @pointerdown="handleScrubStart"
167
+ @keydown.self="isSliderMode && handleKeydown($event, true)"
168
+ >
169
+ <template v-if="isSliderMode">
170
+ <span class="mint-number-input__wash" aria-hidden="true" />
171
+ <span class="mint-number-input__rule" aria-hidden="true" />
172
+ </template>
173
+ <input
174
+ type="number"
175
+ :value="modelValue"
176
+ :min="min"
177
+ :max="max"
178
+ :step="step"
179
+ :disabled="disabled"
180
+ :placeholder="placeholder"
181
+ :class="[
182
+ 'mint-number-input__input',
183
+ `mint-number-input__input--${size}`,
184
+ ]"
185
+ @input="handleInput"
186
+ @keydown="handleKeydown"
187
+ />
188
+ <span v-if="unit" class="mint-number-input__unit">{{ unit }}</span>
189
+ </div>
116
190
 
117
191
  <div class="mint-number-input__steppers" aria-hidden="true">
118
192
  <button
119
193
  type="button"
120
194
  tabindex="-1"
121
- aria-label="Increase value"
122
- :disabled="disabled || !canIncrement"
123
- class="mint-number-input__stepper mint-number-input__stepper--up"
124
- @click="increment"
195
+ aria-label="Decrease value"
196
+ :disabled="disabled || !canDecrement"
197
+ class="mint-number-input__stepper mint-number-input__stepper--down"
198
+ @click="decrement"
125
199
  >
126
- <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
127
- <path d="M1 5l4-4 4 4" />
200
+ <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
201
+ <path d="M2 5h6" />
128
202
  </svg>
129
203
  </button>
130
204
  <button
131
205
  type="button"
132
206
  tabindex="-1"
133
- aria-label="Decrease value"
134
- :disabled="disabled || !canDecrement"
135
- class="mint-number-input__stepper mint-number-input__stepper--down"
136
- @click="decrement"
207
+ aria-label="Increase value"
208
+ :disabled="disabled || !canIncrement"
209
+ class="mint-number-input__stepper mint-number-input__stepper--up"
210
+ @click="increment"
137
211
  >
138
- <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
139
- <path d="M1 1l4 4 4-4" />
212
+ <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
213
+ <path d="M5 2v6" /><path d="M2 5h6" />
140
214
  </svg>
141
215
  </button>
142
216
  </div>
143
217
  </div>
144
-
145
- <input
146
- v-if="isSliderMode"
147
- type="range"
148
- :value="modelValue ?? min"
149
- :min="min"
150
- :max="max"
151
- :step="step"
152
- :disabled="disabled"
153
- :style="{ '--mint-slider-fill': `${sliderPercent}%` }"
154
- class="mint-number-input__slider"
155
- @input="handleSliderInput"
156
- />
157
218
  </div>
158
219
  </template>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import type * as Plotly from 'plotly.js'
3
+
4
+ import PlotlyChart from './PlotlyChart.vue'
5
+
6
+ const data: Plotly.Data[] = [
7
+ {
8
+ type: 'scatter',
9
+ mode: 'lines+markers',
10
+ name: 'Control',
11
+ x: ['0 h', '6 h', '12 h', '24 h'],
12
+ y: [1, 1.3, 1.8, 2.1],
13
+ },
14
+ {
15
+ type: 'scatter',
16
+ mode: 'lines+markers',
17
+ name: 'Treatment',
18
+ x: ['0 h', '6 h', '12 h', '24 h'],
19
+ y: [1, 1.8, 3.1, 4.4],
20
+ },
21
+ ]
22
+
23
+ const layout: Partial<Plotly.Layout> = {
24
+ margin: { t: 24, r: 24, b: 56, l: 56 },
25
+ xaxis: { title: { text: 'Time' } },
26
+ yaxis: { title: { text: 'Relative abundance' } },
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <Story title="Data Display/PlotlyChart">
32
+ <Variant title="Native Plotly">
33
+ <div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
34
+ <PlotlyChart
35
+ title="Metabolite response"
36
+ description="Native Plotly traces with MINT theme and resize handling"
37
+ :data="data"
38
+ :layout="layout"
39
+ :config="{ toImageButtonOptions: { filename: 'metabolite-response' } }"
40
+ />
41
+ </div>
42
+ </Variant>
43
+
44
+ <Variant title="Empty">
45
+ <div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
46
+ <PlotlyChart
47
+ title="Metabolite response"
48
+ :data="[]"
49
+ empty
50
+ empty-message="No measurements are available."
51
+ />
52
+ </div>
53
+ </Variant>
54
+
55
+ <Variant title="Loading">
56
+ <div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
57
+ <PlotlyChart title="Loading measurements" :data="[]" loading />
58
+ </div>
59
+ </Variant>
60
+ </Story>
61
+ </template>
@@ -0,0 +1,233 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Render native Plotly data with MINT loading, empty, theme, and error states.
4
+ *
5
+ * Plotly is loaded only when the component is mounted, so importing the SDK does
6
+ * not add Plotly to the application's initial bundle.
7
+ *
8
+ * @example
9
+ * ```vue
10
+ * <PlotlyChart
11
+ * title="QC overview"
12
+ * :data="traces"
13
+ * :layout="{ xaxis: { title: { text: 'Sample' } } }"
14
+ * :config="{ toImageButtonOptions: { filename: 'qc-overview' } }"
15
+ * />
16
+ * ```
17
+ */
18
+ import type * as PlotlyType from 'plotly.js'
19
+ import { nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
20
+
21
+ import ChartContainer from './ChartContainer.vue'
22
+
23
+ interface Props {
24
+ data: PlotlyType.Data[]
25
+ layout?: Partial<PlotlyType.Layout>
26
+ config?: Partial<PlotlyType.Config>
27
+ title?: string
28
+ description?: string
29
+ loading?: boolean
30
+ empty?: boolean
31
+ emptyMessage?: string
32
+ ariaLabel?: string
33
+ }
34
+
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ layout: () => ({}),
37
+ config: () => ({}),
38
+ loading: false,
39
+ empty: false,
40
+ emptyMessage: 'No data available',
41
+ ariaLabel: 'Interactive Plotly chart',
42
+ })
43
+
44
+ const chartElement = ref<HTMLDivElement | null>(null)
45
+ const failed = ref(false)
46
+ let plotly: typeof PlotlyType | null = null
47
+ let resizeObserver: ResizeObserver | null = null
48
+ let themeObserver: MutationObserver | null = null
49
+ let renderedElement: HTMLDivElement | null = null
50
+ let rendering = false
51
+ let disposed = false
52
+ let revision = 0
53
+
54
+ async function loadPlotly(): Promise<typeof PlotlyType> {
55
+ const module = await import('plotly.js-dist-min')
56
+ return ('default' in module ? module.default : module) as unknown as typeof PlotlyType
57
+ }
58
+
59
+ function cssValue(name: string, fallback: string): string {
60
+ const value = getComputedStyle(chartElement.value ?? document.documentElement).getPropertyValue(name).trim()
61
+ return value || getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback
62
+ }
63
+
64
+ // Unwrap nested Vue proxies while copying Plotly's mutable input tree. Keep
65
+ // native dates/typed arrays and callbacks instead of round-tripping through JSON.
66
+ function clonePlotlyInput<T>(value: T): T {
67
+ const raw = toRaw(value)
68
+ if (Array.isArray(raw)) return raw.map(clonePlotlyInput) as T
69
+ if (raw !== null && typeof raw === 'object') {
70
+ if (Object.getPrototypeOf(raw) === Object.prototype) {
71
+ return Object.fromEntries(Object.entries(raw).map(([key, item]) => [key, clonePlotlyInput(item)])) as T
72
+ }
73
+ return structuredClone(raw)
74
+ }
75
+ return raw
76
+ }
77
+
78
+ function themedLayout(): Partial<PlotlyType.Layout> {
79
+ const textColor = cssValue('--text-primary', '#1E293B')
80
+ const gridColor = cssValue('--border-color', '#E5E7EB')
81
+ const layout = clonePlotlyInput(props.layout)
82
+ return {
83
+ autosize: true,
84
+ paper_bgcolor: 'rgba(0,0,0,0)',
85
+ plot_bgcolor: 'rgba(0,0,0,0)',
86
+ ...layout,
87
+ font: { color: textColor, family: cssValue('--font-sans', 'Arial, sans-serif'), ...layout.font },
88
+ xaxis: { gridcolor: gridColor, ...layout.xaxis },
89
+ yaxis: { gridcolor: gridColor, ...layout.yaxis },
90
+ }
91
+ }
92
+
93
+ function purgeChart(): void {
94
+ resizeObserver?.disconnect()
95
+ if (renderedElement) plotly?.purge(renderedElement)
96
+ renderedElement = null
97
+ }
98
+
99
+ async function renderChart(): Promise<void> {
100
+ revision++
101
+ if (rendering || disposed || props.loading || props.empty) return
102
+ rendering = true
103
+ try {
104
+ while (!disposed && !props.loading && !props.empty) {
105
+ const currentRevision = revision
106
+ failed.value = false
107
+ await nextTick()
108
+ if (disposed || props.loading || props.empty) break
109
+ try {
110
+ plotly ??= await loadPlotly()
111
+ if (disposed || props.loading || props.empty) break
112
+ if (currentRevision !== revision) continue
113
+ const element = chartElement.value
114
+ if (!element) break
115
+ renderedElement = element
116
+ // Plotly mutates traces and nested layout fields; keep caller-owned values intact.
117
+ await plotly.react(
118
+ element,
119
+ clonePlotlyInput(props.data),
120
+ themedLayout(),
121
+ { responsive: true, displaylogo: false, ...props.config },
122
+ )
123
+ if (disposed || props.loading || props.empty || element !== chartElement.value) {
124
+ purgeChart()
125
+ } else {
126
+ resizeObserver?.observe(element)
127
+ }
128
+ } catch {
129
+ purgeChart()
130
+ if (!disposed && currentRevision === revision) failed.value = true
131
+ }
132
+ if (currentRevision === revision) break
133
+ }
134
+ } finally {
135
+ rendering = false
136
+ if (disposed || props.loading || props.empty) purgeChart()
137
+ }
138
+ }
139
+
140
+ onMounted(() => {
141
+ if (typeof ResizeObserver !== 'undefined') {
142
+ resizeObserver = new ResizeObserver(() => {
143
+ if (rendering || !plotly || !renderedElement) return
144
+ // A tab can become hidden or unmount before Plotly's async resize finishes.
145
+ void Promise.resolve(plotly.Plots.resize(renderedElement)).catch(() => {})
146
+ })
147
+ }
148
+
149
+ if (typeof MutationObserver !== 'undefined') {
150
+ themeObserver = new MutationObserver(() => void renderChart())
151
+ themeObserver.observe(document.documentElement, {
152
+ attributes: true,
153
+ attributeFilter: ['class', 'style'],
154
+ })
155
+ }
156
+ void renderChart()
157
+ })
158
+
159
+ watch(
160
+ [() => props.data, () => props.layout, () => props.config, () => props.loading, () => props.empty],
161
+ () => {
162
+ if (props.loading || props.empty) {
163
+ resizeObserver?.disconnect()
164
+ if (!rendering) purgeChart()
165
+ }
166
+ void renderChart()
167
+ },
168
+ )
169
+
170
+ onBeforeUnmount(() => {
171
+ disposed = true
172
+ resizeObserver?.disconnect()
173
+ themeObserver?.disconnect()
174
+ // An in-flight react must finish before its resources can be purged completely.
175
+ if (!rendering) purgeChart()
176
+ })
177
+ </script>
178
+
179
+ <template>
180
+ <ChartContainer
181
+ :title="title"
182
+ :description="description"
183
+ :loading="loading"
184
+ :empty="empty"
185
+ :empty-message="emptyMessage"
186
+ >
187
+ <template v-if="$slots.toolbar" #toolbar>
188
+ <slot name="toolbar" />
189
+ </template>
190
+ <template v-if="$slots.empty" #empty>
191
+ <slot name="empty" />
192
+ </template>
193
+
194
+ <template v-if="failed">
195
+ <slot name="error">
196
+ <div class="mint-plotly-chart__error">
197
+ <p role="alert">Unable to display this chart.</p>
198
+ </div>
199
+ </slot>
200
+ </template>
201
+ <div
202
+ ref="chartElement"
203
+ v-show="!failed"
204
+ class="mint-plotly-chart__plot"
205
+ role="group"
206
+ :aria-label="ariaLabel"
207
+ />
208
+
209
+ <template v-if="$slots.legend" #legend>
210
+ <slot name="legend" />
211
+ </template>
212
+ </ChartContainer>
213
+ </template>
214
+
215
+ <style scoped>
216
+ .mint-plotly-chart__plot {
217
+ width: 100%;
218
+ min-height: 24rem;
219
+ }
220
+
221
+ .mint-plotly-chart__error {
222
+ margin: 0;
223
+ padding: 1rem;
224
+ border: 1px solid var(--mint-error-border);
225
+ border-radius: 0.75rem;
226
+ background: var(--mint-error-bg);
227
+ color: var(--mint-error-hover);
228
+ }
229
+
230
+ .mint-plotly-chart__error :deep(p) {
231
+ margin: 0;
232
+ }
233
+ </style>
@@ -97,9 +97,9 @@ export interface PluginWorkspaceViewProps {
97
97
  sidebarSubtitle?: string
98
98
  /** Optional compact badge/count rendered in the sidebar header. */
99
99
  sidebarBadge?: string | number
100
- /** AppSidebar visual preset. `analysis` matches the LEAF-style MINT analysis sidebar language. */
100
+ /** @deprecated No-op. AppSidebar has one look now; the former `analysis` preset is the default. */
101
101
  sidebarVariant?: PluginWorkspaceSidebarVariant
102
- /** Sidebar CSS width. Defaults to AppSidebar's variant width. */
102
+ /** Sidebar CSS width. Defaults to AppSidebar's width. */
103
103
  sidebarWidth?: string
104
104
  /** Sidebar position in AppLayout. */
105
105
  sidebarPosition?: 'left' | 'right'
@@ -113,7 +113,7 @@ export interface PluginWorkspaceViewProps {
113
113
  sidebarContentId?: string
114
114
  /** Compact AppSidebar density. */
115
115
  dense?: boolean
116
- /** Show a built-in collapse/expand button in the sidebar chrome. Defaults to AppSidebar's variant behavior. */
116
+ /** Show a built-in collapse/expand button in the sidebar chrome. Defaults to true. */
117
117
  sidebarCollapsible?: boolean
118
118
  /** Sidebar width when collapsed. */
119
119
  sidebarCollapsedWidth?: string
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- /** Renders a segmented button group for single-option selection, with simple or solid variants. */
2
+ /** Renders a segmented button group for single-option selection, defaulting to the card variant. */
3
3
  import { computed, type ComponentPublicInstance } from 'vue'
4
4
  import type { SegmentedOption, SegmentedOptionInput, SegmentedControlVariant, SegmentedControlSize } from '../types'
5
5
  import { normalizeOptionInput } from '../utils/options'
@@ -16,7 +16,7 @@ interface Props {
16
16
  }
17
17
 
18
18
  const props = withDefaults(defineProps<Props>(), {
19
- variant: 'simple',
19
+ variant: 'card',
20
20
  size: 'md',
21
21
  fullWidth: false,
22
22
  disabled: false,