@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
@@ -0,0 +1,284 @@
1
+ <script setup lang="ts">
2
+ /** Adapter-driven file picker with Finder-style selection, disclosure and metadata preview. */
3
+ import { ref } from 'vue'
4
+ import BaseButton from './BaseButton.vue'
5
+ import BaseInput from './BaseInput.vue'
6
+ import IconButton from './IconButton.vue'
7
+ import FileUploader from './FileUploader.vue'
8
+ import FileBrowserDialogInternal from './internal/FileBrowserDialogInternal.vue'
9
+ import FilePickerTreeInternal from './internal/FilePickerTreeInternal.vue'
10
+ import FilePickerPreviewInternal from './internal/FilePickerPreviewInternal.vue'
11
+ import { formatBytes } from './fileBrowserPresentation'
12
+ import { useFilePicker } from '../composables/useFilePicker'
13
+ import { pickerReason } from '../composables/filePicker/validation'
14
+ import type {
15
+ PickerAdapter,
16
+ PickerCapabilities,
17
+ PickerNode,
18
+ PickerSelection,
19
+ PickerSelectionMode,
20
+ PickerSource,
21
+ } from '../types/filePicker'
22
+
23
+ interface Props {
24
+ open: boolean
25
+ adapter: PickerAdapter
26
+ selectionMode?: PickerSelectionMode
27
+ sources?: PickerSource[]
28
+ capabilities?: PickerCapabilities
29
+ initialPath?: string
30
+ initialSelection?: PickerSelection | null
31
+ title?: string
32
+ /** Host-owned visibility/selection filter; backend must still enforce access. */
33
+ systemFilter?: (node: PickerNode, source: PickerSource) => boolean
34
+ }
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ selectionMode: 'folder+files',
37
+ sources: () => ['server'],
38
+ capabilities: () => ({}),
39
+ initialPath: '',
40
+ initialSelection: null,
41
+ title: 'Choose data',
42
+ })
43
+ const emit = defineEmits<{
44
+ 'update:open': [open: boolean]
45
+ select: [selection: PickerSelection]
46
+ cancel: []
47
+ }>()
48
+ const uploadError = ref('')
49
+ const {
50
+ previewNode,
51
+ sourceNames,
52
+ sourceIcons,
53
+ allowedSources,
54
+ source,
55
+ query,
56
+ recursive,
57
+ skipBlanks,
58
+ reviewing,
59
+ nav,
60
+ selection,
61
+ capability,
62
+ path,
63
+ trail,
64
+ loading,
65
+ error,
66
+ rows,
67
+ roots,
68
+ chosen,
69
+ focused,
70
+ resolving,
71
+ resolutionError,
72
+ searchArea,
73
+ confirmLabel,
74
+ folderCount,
75
+ canConfirm,
76
+ switchSource,
77
+ selectNode,
78
+ enter,
79
+ up,
80
+ refresh,
81
+ selectParent,
82
+ chooseLocal,
83
+ cancel,
84
+ confirm,
85
+ keyboard,
86
+ } = useFilePicker(props, emit)
87
+ </script>
88
+
89
+ <template>
90
+ <FileBrowserDialogInternal
91
+ :open="open"
92
+ :label="title"
93
+ size="full"
94
+ variant="picker"
95
+ @close="cancel"
96
+ >
97
+ <template #header>
98
+ <div v-if="source === 'server'" ref="searchArea" class="mint-picker__search">
99
+ <svg
100
+ viewBox="0 0 24 24"
101
+ fill="none"
102
+ stroke="currentColor"
103
+ stroke-width="1.8"
104
+ aria-hidden="true"
105
+ >
106
+ <circle cx="10.5" cy="10.5" r="7" />
107
+ <path d="m16 16 5 5" />
108
+ </svg>
109
+ <BaseInput
110
+ v-model="query"
111
+ aria-label="Search folders and files"
112
+ placeholder="Search folders and files…"
113
+ />
114
+ <kbd>/</kbd>
115
+ </div>
116
+ <h2 v-else class="mint-file-browser__title mint-picker__local-title">{{ title }}</h2>
117
+ <div
118
+ v-if="allowedSources.length > 1"
119
+ class="mint-picker__sources"
120
+ role="group"
121
+ aria-label="Data source"
122
+ :style="{ '--picker-source-index': allowedSources.indexOf(source) }"
123
+ >
124
+ <span class="mint-picker__source-indicator" aria-hidden="true" />
125
+ <IconButton
126
+ v-for="item in allowedSources"
127
+ :key="item"
128
+ size="sm"
129
+ variant="ghost"
130
+ :label="sourceNames[item]"
131
+ :aria-pressed="item === source"
132
+ @click="switchSource(item)"
133
+ >
134
+ <svg
135
+ viewBox="0 0 24 24"
136
+ fill="none"
137
+ stroke="currentColor"
138
+ stroke-width="1.8"
139
+ aria-hidden="true"
140
+ >
141
+ <path :d="sourceIcons[item]" />
142
+ </svg>
143
+ </IconButton>
144
+ </div>
145
+ </template>
146
+ <div :key="source" class="mint-picker mint-picker__source-panel" @keydown="keyboard">
147
+ <div v-if="source === 'server'" class="mint-picker__pathbar">
148
+ <IconButton
149
+ label="Up one level"
150
+ size="sm"
151
+ variant="secondary"
152
+ :disabled="!path"
153
+ @click="up()"
154
+ ><svg
155
+ viewBox="0 0 24 24"
156
+ fill="none"
157
+ stroke="currentColor"
158
+ stroke-width="2"
159
+ aria-hidden="true"
160
+ >
161
+ <path d="M11 17l-5-5 5-5M6 12h13" /></svg
162
+ ></IconButton>
163
+ <nav aria-label="Breadcrumb" class="mint-picker__breadcrumbs">
164
+ <BaseButton variant="ghost" size="sm" @click="up('')">{{
165
+ sourceNames[source]
166
+ }}</BaseButton>
167
+ <template v-for="crumb in trail" :key="crumb.path"
168
+ ><span>/</span
169
+ ><BaseButton variant="ghost" size="sm" @click="up(crumb.path)">{{
170
+ crumb.label
171
+ }}</BaseButton></template
172
+ >
173
+ </nav>
174
+ <BaseButton
175
+ variant="ghost"
176
+ size="sm"
177
+ class="mint-picker__scope"
178
+ :aria-pressed="recursive"
179
+ @click="recursive = !recursive"
180
+ >{{ recursive ? 'Subfolders' : 'This folder' }}</BaseButton
181
+ >
182
+ <IconButton size="sm" label="Refresh location" :disabled="loading" @click="refresh()"
183
+ ><svg
184
+ viewBox="0 0 24 24"
185
+ fill="none"
186
+ stroke="currentColor"
187
+ stroke-width="1.8"
188
+ aria-hidden="true"
189
+ >
190
+ <path d="M20 7v5h-5M4 17v-5h5M6 8a7 7 0 0 1 12-2l2 3M4 15l2 3a7 7 0 0 0 12-2" /></svg
191
+ ></IconButton>
192
+ </div>
193
+ <div v-if="!allowedSources.length" class="mint-picker__empty">
194
+ No data sources are enabled.
195
+ </div>
196
+ <template v-else>
197
+ <div v-if="source === 'localFile'" class="mint-picker__local-upload">
198
+ <FileUploader
199
+ size="lg"
200
+ :mode="selectionMode === 'folder' ? 'folder' : selectionMode === 'single-file' ? 'file' : 'file+folder'"
201
+ :multiple="selectionMode !== 'single-file'"
202
+ :show-files="false"
203
+ @upload="(...args) => { uploadError = ''; chooseLocal(...args) }"
204
+ @error="uploadError = $event"
205
+ />
206
+ <p v-if="uploadError || error || resolutionError" role="alert">
207
+ {{ uploadError || error || resolutionError }}
208
+ </p>
209
+ </div>
210
+ <div v-else class="mint-picker__workspace">
211
+ <FilePickerTreeInternal
212
+ :empty-title="
213
+ path
214
+ ? 'No readable files'
215
+ : 'No server locations configured'
216
+ "
217
+ :rows="rows"
218
+ :loading="loading"
219
+ :error="error"
220
+ :query="query"
221
+ :mode="selectionMode"
222
+ :selected="selection.selected"
223
+ :reason="(node) => pickerReason(selection.exclusion(node), node)"
224
+ :progress="selection.folderProgress"
225
+ :focus-path="selection.focusPath.value"
226
+ @select="selectNode"
227
+ @enter="enter"
228
+ @expand="nav.expand"
229
+ @up="up"
230
+ @retry="refresh"
231
+ @parent="selectParent"
232
+ />
233
+ <FilePickerPreviewInternal
234
+ :node="previewNode"
235
+ :total-count="focused?.kind === 'folder' ? focused.fileCount : undefined"
236
+ :source="source"
237
+ :exclusions="chosen.excluded"
238
+ :busy="resolving"
239
+ :error="resolutionError"
240
+ :reviewing="reviewing"
241
+ @review="reviewing = !reviewing"
242
+ @retry="selection.resolve"
243
+ />
244
+ </div>
245
+ </template>
246
+ </div>
247
+ <template #footer>
248
+ <div class="mint-picker__footer">
249
+ <div class="mint-picker__receipt" role="status">
250
+ <span v-if="folderCount" class="mint-picker__badge"
251
+ >{{ folderCount }} folder{{ folderCount === 1 ? '' : 's' }}</span
252
+ >
253
+ <span v-if="chosen.files.length" class="mint-picker__badge"
254
+ >{{ chosen.files.length }} {{ chosen.files.length === 1 ? 'file' : 'files' }}</span
255
+ >
256
+ <span v-if="roots.length">{{ resolving ? 'Checking…' : formatBytes(chosen.bytes) }}</span>
257
+ <span v-else>No selection</span>
258
+ <BaseButton
259
+ v-if="chosen.excluded.length && source === 'server'"
260
+ variant="ghost"
261
+ size="sm"
262
+ @click="reviewing = !reviewing"
263
+ >{{ chosen.excluded.length }} skipped</BaseButton
264
+ >
265
+ <span v-else-if="chosen.excluded.length">{{ chosen.excluded.length }} skipped</span>
266
+ <BaseButton
267
+ v-if="capability?.isBlank"
268
+ size="sm"
269
+ variant="ghost"
270
+ :aria-pressed="skipBlanks"
271
+ @click="skipBlanks = !skipBlanks"
272
+ >Skip blanks {{ skipBlanks ? 'on' : 'off' }}</BaseButton
273
+ >
274
+ </div>
275
+ <div class="mint-picker__actions">
276
+ <BaseButton variant="ghost" size="sm" @click="cancel">Cancel</BaseButton
277
+ ><BaseButton size="sm" :disabled="!canConfirm" @click="confirm">{{
278
+ confirmLabel
279
+ }}</BaseButton>
280
+ </div>
281
+ </div>
282
+ </template>
283
+ </FileBrowserDialogInternal>
284
+ </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
@@ -8,7 +8,12 @@ interface Props {
8
8
  maxSize?: number
9
9
  disabled?: boolean
10
10
  size?: 'sm' | 'md' | 'lg'
11
- mode?: 'file' | 'folder'
11
+ mode?: 'file' | 'folder' | 'file+folder'
12
+ /** Hide the selection list when a parent presents the files itself. */
13
+ showFiles?: boolean
14
+ /** Restore selections owned by a parent form; omitted for internal selection state. */
15
+ files?: File[]
16
+ rootName?: string
12
17
  }
13
18
 
14
19
  const props = withDefaults(defineProps<Props>(), {
@@ -16,6 +21,7 @@ const props = withDefaults(defineProps<Props>(), {
16
21
  disabled: false,
17
22
  size: 'md',
18
23
  mode: 'file',
24
+ showFiles: true,
19
25
  })
20
26
 
21
27
  const emit = defineEmits<{
@@ -29,10 +35,17 @@ const inputRef = ref<HTMLInputElement>()
29
35
  const selectedFiles = ref<File[]>([])
30
36
  const folderName = ref<string | null>(null)
31
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
+
32
44
  const isFolder = computed(() => props.mode === 'folder')
45
+ const isMixed = computed(() => props.mode === 'file+folder')
33
46
 
34
47
  const acceptLabel = computed(() => {
35
- if (!props.accept) return 'any file'
48
+ if (!props.accept) return isFolder.value ? 'a folder' : 'any file'
36
49
  return props.accept.split(',').map(a => a.trim().replace('.', '').toUpperCase()).join(', ')
37
50
  })
38
51
 
@@ -154,15 +167,19 @@ function extractFolderName(files: File[]): string | null {
154
167
  return null
155
168
  }
156
169
 
157
- function handleDrop(event: DragEvent) {
170
+ async function handleDrop(event: DragEvent) {
158
171
  event.preventDefault()
159
172
  isDragOver.value = false
160
173
  if (props.disabled) return
161
174
 
162
- if (isFolder.value && event.dataTransfer?.items) {
163
- handleFolderDrop(event.dataTransfer.items)
164
- } else if (event.dataTransfer?.files) {
165
- handleFiles(event.dataTransfer.files)
175
+ try {
176
+ if (props.mode !== 'file' && event.dataTransfer?.items?.length) {
177
+ await handleFolderDrop(event.dataTransfer.items)
178
+ } else if (event.dataTransfer?.files) {
179
+ handleFiles(event.dataTransfer.files)
180
+ }
181
+ } catch (cause) {
182
+ emit('error', cause instanceof Error ? cause.message : 'Could not read the dropped folder.')
166
183
  }
167
184
  }
168
185
 
@@ -171,8 +188,12 @@ async function handleFolderDrop(items: DataTransferItemList) {
171
188
  const relativePaths: string[] = []
172
189
  let folderNameFromDrop: string | undefined
173
190
 
174
- for (const item of Array.from(items)) {
191
+ // Capture live drag data before awaiting: the browser protects it after the drop handler.
192
+ const dropped = Array.from(items, (item) => {
175
193
  const entry = item.webkitGetAsEntry?.()
194
+ return { entry, file: entry ? null : item.getAsFile?.() }
195
+ })
196
+ for (const { entry, file: droppedFile } of dropped) {
176
197
  if (entry?.isDirectory) {
177
198
  folderNameFromDrop = entry.name
178
199
  await readDirectory(
@@ -181,8 +202,10 @@ async function handleFolderDrop(items: DataTransferItemList) {
181
202
  relativePaths,
182
203
  entry.name,
183
204
  )
184
- } else if (entry?.isFile) {
185
- const file = await getFile(entry as FileSystemFileEntry)
205
+ } else {
206
+ const file = entry?.isFile
207
+ ? await getFile(entry as FileSystemFileEntry)
208
+ : droppedFile
186
209
  if (file) {
187
210
  files.push(file)
188
211
  relativePaths.push(file.name)
@@ -291,7 +314,11 @@ function formatFileSize(bytes: number): string {
291
314
  <div
292
315
  role="button"
293
316
  tabindex="0"
294
- :aria-label="disabled ? 'File upload disabled' : 'Click or drag files to upload'"
317
+ :aria-label="disabled
318
+ ? 'File upload disabled'
319
+ : isMixed
320
+ ? 'Drop files or a folder, or click to choose files'
321
+ : isFolder ? 'Click or drag a folder to upload' : 'Click or drag files to upload'"
295
322
  :aria-disabled="disabled"
296
323
  :class="[
297
324
  'mint-file-uploader__dropzone',
@@ -345,7 +372,7 @@ function formatFileSize(bytes: number): string {
345
372
  </svg>
346
373
 
347
374
  <p class="mint-file-uploader__text">
348
- Drop {{ acceptLabel }} or click
375
+ {{ isMixed ? 'Drop files or a folder, or click to choose files' : `Drop ${acceptLabel} or click` }}
349
376
  </p>
350
377
  </div>
351
378
  </div>
@@ -356,7 +383,7 @@ function formatFileSize(bytes: number): string {
356
383
  </div>
357
384
 
358
385
  <!-- Folder summary display -->
359
- <div v-if="isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
386
+ <div v-if="showFiles && isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
360
387
  <div class="mint-file-uploader__folder-info">
361
388
  <div class="mint-file-uploader__folder-icon-wrapper">
362
389
  <svg class="mint-file-uploader__folder-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
@@ -382,7 +409,7 @@ function formatFileSize(bytes: number): string {
382
409
 
383
410
  <!-- File list display -->
384
411
  <TransitionGroup
385
- v-if="!isFolder && selectedFiles.length > 0"
412
+ v-if="showFiles && !isFolder && selectedFiles.length > 0"
386
413
  tag="ul"
387
414
  name="mint-file-uploader-item"
388
415
  class="mint-file-uploader__list"
@@ -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"
@@ -1,87 +1,52 @@
1
1
  <script setup lang="ts">
2
- import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
2
  import type * as PlotlyType from 'plotly.js'
3
+ import { computed, toRaw } from 'vue'
4
4
 
5
- import ChartContainer from './ChartContainer.vue'
5
+ import PlotlyChart from './PlotlyChart.vue'
6
6
 
7
7
  const props = defineProps<{
8
8
  spec: Record<string, unknown>
9
9
  fallback?: { media_type: string; data: string }
10
10
  }>()
11
11
 
12
- const chartElement = ref<HTMLDivElement | null>(null)
13
- const failed = ref(false)
14
- let plotly: typeof PlotlyType | null = null
15
-
16
- async function loadPlotly(): Promise<typeof PlotlyType> {
17
- const module = await import('plotly.js-dist-min')
18
- return ('default' in module ? module.default : module) as unknown as typeof PlotlyType
19
- }
20
-
21
- async function renderChart(): Promise<void> {
22
- if (!chartElement.value) return
23
- failed.value = false
24
- await nextTick()
25
- try {
26
- const renderer = plotly ?? await loadPlotly()
27
- plotly = renderer
28
- const data = Array.isArray(props.spec.data) ? props.spec.data : []
29
- const layout = props.spec.layout && typeof props.spec.layout === 'object'
30
- ? props.spec.layout
31
- : {}
32
- await renderer.react(
33
- chartElement.value,
34
- data as PlotlyType.Data[],
35
- layout as Partial<PlotlyType.Layout>,
36
- { responsive: true, displaylogo: false },
37
- )
38
- failed.value = false
39
- } catch {
40
- failed.value = true
41
- }
42
- }
43
-
44
- onMounted(renderChart)
45
- watch(() => props.spec, async () => {
46
- await nextTick()
47
- await renderChart()
48
- })
49
- onBeforeUnmount(() => {
50
- if (plotly && chartElement.value) plotly.purge(chartElement.value)
12
+ const data = computed<PlotlyType.Data[]>(() => {
13
+ const traces = toRaw(props.spec).data
14
+ return Array.isArray(traces) ? [...toRaw(traces)] as PlotlyType.Data[] : []
51
15
  })
16
+ const layout = computed<Partial<PlotlyType.Layout>>(() => (
17
+ props.spec.layout && typeof props.spec.layout === 'object'
18
+ ? props.spec.layout as Partial<PlotlyType.Layout>
19
+ : {}
20
+ ))
21
+ const config = computed<Partial<PlotlyType.Config>>(() => (
22
+ props.spec.config && typeof props.spec.config === 'object'
23
+ ? props.spec.config as Partial<PlotlyType.Config>
24
+ : {}
25
+ ))
52
26
  </script>
53
27
 
54
28
  <template>
55
- <ChartContainer title="Interactive chart">
56
- <img
57
- v-if="failed && fallback"
58
- class="mint-generated-result__image"
59
- :src="`data:${fallback.media_type};base64,${fallback.data}`"
60
- alt="Static Plotly chart fallback"
61
- >
62
- <p
63
- v-else-if="failed"
64
- class="mint-generated-result__plotly-error"
65
- role="alert"
66
- >
67
- Unable to display this chart.
68
- </p>
69
- <div
70
- ref="chartElement"
71
- v-show="!failed"
72
- class="mint-generated-result__plotly"
73
- aria-label="Interactive Plotly chart"
74
- />
75
- </ChartContainer>
29
+ <PlotlyChart
30
+ title="Interactive chart"
31
+ :data="data"
32
+ :layout="layout"
33
+ :config="config"
34
+ >
35
+ <template v-if="fallback" #error>
36
+ <img
37
+ class="mint-generated-result__image"
38
+ :src="`data:${fallback.media_type};base64,${fallback.data}`"
39
+ alt="Static Plotly chart fallback"
40
+ >
41
+ </template>
42
+ </PlotlyChart>
76
43
  </template>
77
44
 
78
45
  <style scoped>
79
- .mint-generated-result__plotly-error {
80
- margin: 0;
81
- padding: 1rem;
82
- border: 1px solid var(--mint-error-border);
83
- border-radius: 0.75rem;
84
- background: var(--mint-error-bg);
85
- color: var(--mint-error);
46
+ .mint-generated-result__image {
47
+ display: block;
48
+ max-width: 100%;
49
+ height: auto;
50
+ margin: 0 auto;
86
51
  }
87
52
  </style>
@@ -128,11 +128,6 @@ function humanize(value: string): string {
128
128
  margin: 0 auto;
129
129
  }
130
130
 
131
- .mint-generated-result__plotly {
132
- width: 100%;
133
- min-height: 24rem;
134
- }
135
-
136
131
  .mint-generated-result__artifact {
137
132
  display: flex;
138
133
  align-items: center;