@morscherlab/mint-sdk 1.2.0-beta.9 → 1.2.0

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/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-DCXmwKNS.js} +5 -5
  5. package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DCXmwKNS.js.map} +1 -1
  6. package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-BYR20I4c.js} +5 -5
  7. package/dist/{SettingsModal-Dphh3RrI.js.map → SettingsModal-BYR20I4c.js.map} +1 -1
  8. package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-BAF3CKY7.js} +3 -3
  9. package/dist/{Skeleton-DxtwJ5gt.js.map → Skeleton-BAF3CKY7.js.map} +1 -1
  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/PlotlyChart.test.d.ts +1 -0
  15. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  16. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  17. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  18. package/dist/apiErrors.d.ts +26 -0
  19. package/dist/components/BaseInput.vue.d.ts +1 -1
  20. package/dist/components/BaseSelect.vue.d.ts +1 -1
  21. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  22. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  23. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  24. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  25. package/dist/components/DatePicker.vue.d.ts +1 -1
  26. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  27. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  28. package/dist/components/FilePicker.vue.d.ts +32 -0
  29. package/dist/components/FileUploader.vue.d.ts +5 -2
  30. package/dist/components/FormulaInput.vue.d.ts +1 -1
  31. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  32. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  33. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  34. package/dist/components/NumberInput.vue.d.ts +1 -1
  35. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  36. package/dist/components/ResourceCard.vue.d.ts +1 -1
  37. package/dist/components/SecretInput.vue.d.ts +1 -1
  38. package/dist/components/SequenceInput.vue.d.ts +1 -1
  39. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  40. package/dist/components/TagsInput.vue.d.ts +1 -1
  41. package/dist/components/TimePicker.vue.d.ts +1 -1
  42. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  43. package/dist/components/UnitInput.vue.d.ts +1 -1
  44. package/dist/components/WellPlate.vue.d.ts +1 -1
  45. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  46. package/dist/components/index.d.ts +4 -0
  47. package/dist/components/index.js +8 -8
  48. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  49. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  50. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  51. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  52. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  53. package/dist/{components-CgAUzF8F.js → components-DIum5hkx.js} +4286 -1866
  54. package/dist/components-DIum5hkx.js.map +1 -0
  55. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  56. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  57. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  58. package/dist/composables/filePicker/validation.d.ts +5 -0
  59. package/dist/composables/index.d.ts +2 -0
  60. package/dist/composables/index.js +10 -10
  61. package/dist/composables/useApi.d.ts +14 -7
  62. package/dist/composables/useFileBrowser.d.ts +55 -0
  63. package/dist/composables/useFilePicker.d.ts +189 -0
  64. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  65. package/dist/{composables-B_pia3hO.js → composables-mFXsTA4g.js} +300 -11
  66. package/dist/composables-mFXsTA4g.js.map +1 -0
  67. package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
  68. package/dist/errors-BLuqMee5.js.map +1 -0
  69. package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
  70. package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
  71. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  72. package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.js +15 -15
  75. package/dist/install.js +2 -2
  76. package/dist/stores/index.js +2 -2
  77. package/dist/styles.css +2369 -1377
  78. package/dist/templates/index.js +2 -2
  79. package/dist/{templates-Qq113pyZ.js → templates-CmGfmBRA.js} +2 -2
  80. package/dist/{templates-Qq113pyZ.js.map → templates-CmGfmBRA.js.map} +1 -1
  81. package/dist/types/fileBrowserTypes.d.ts +77 -0
  82. package/dist/types/filePicker.d.ts +77 -0
  83. package/dist/types/index.d.ts +2 -0
  84. package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-DldpxNOP.js} +42 -37
  85. package/dist/useControlSchema-DldpxNOP.js.map +1 -0
  86. package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
  87. package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
  88. package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-CEcIaueJ.js} +4 -4
  89. package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
  90. package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-z5L_dUIy.js} +2 -2
  91. package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
  92. package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-uwoJGEb6.js} +53 -8
  93. package/dist/validation-uwoJGEb6.js.map +1 -0
  94. package/package.json +1 -1
  95. package/src/__tests__/apiErrors.test.ts +81 -0
  96. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  97. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  98. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  99. package/src/__tests__/components/FilePicker.test.ts +487 -0
  100. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  101. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  102. package/src/__tests__/components/SegmentedControl.test.ts +19 -0
  103. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  104. package/src/__tests__/composables/useApi.test.ts +92 -9
  105. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  106. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  107. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  108. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  109. package/src/apiErrors.ts +85 -0
  110. package/src/components/ExperimentPopover.vue +10 -2
  111. package/src/components/FileBrowserModal.story.vue +203 -0
  112. package/src/components/FileBrowserModal.vue +384 -0
  113. package/src/components/FilePicker.story.vue +44 -0
  114. package/src/components/FilePicker.vue +284 -0
  115. package/src/components/FileUploader.vue +32 -14
  116. package/src/components/GeneratedPlotlyResult.vue +34 -69
  117. package/src/components/GeneratedResultRenderer.vue +0 -5
  118. package/src/components/PlotlyChart.story.vue +61 -0
  119. package/src/components/PlotlyChart.vue +233 -0
  120. package/src/components/SegmentedControl.vue +2 -2
  121. package/src/components/fileBrowserPresentation.ts +39 -0
  122. package/src/components/index.ts +5 -0
  123. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  124. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  125. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  126. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  127. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  128. package/src/composables/filePicker/localFiles.ts +58 -0
  129. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  130. package/src/composables/filePicker/usePickerSelection.ts +259 -0
  131. package/src/composables/filePicker/validation.ts +41 -0
  132. package/src/composables/index.ts +7 -0
  133. package/src/composables/useApi.ts +57 -13
  134. package/src/composables/useFileBrowser.ts +314 -0
  135. package/src/composables/useFilePicker.ts +366 -0
  136. package/src/composables/useGeneratedAnalysis.ts +10 -17
  137. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  138. package/src/composables/usePluginClient.ts +3 -0
  139. package/src/index.ts +3 -0
  140. package/src/styles/components/file-browser-modal.css +489 -0
  141. package/src/styles/components/file-picker.css +502 -0
  142. package/src/styles/components/icon-button.css +9 -0
  143. package/src/styles/index.css +3 -0
  144. package/src/types/fileBrowserTypes.ts +85 -0
  145. package/src/types/filePicker.ts +72 -0
  146. package/src/types/index.ts +14 -0
  147. package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
  148. package/dist/components-CgAUzF8F.js.map +0 -1
  149. package/dist/composables-B_pia3hO.js.map +0 -1
  150. package/dist/errors-_W0-LbA6.js.map +0 -1
  151. package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
  152. 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>
@@ -8,7 +8,9 @@ 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
12
14
  }
13
15
 
14
16
  const props = withDefaults(defineProps<Props>(), {
@@ -16,6 +18,7 @@ const props = withDefaults(defineProps<Props>(), {
16
18
  disabled: false,
17
19
  size: 'md',
18
20
  mode: 'file',
21
+ showFiles: true,
19
22
  })
20
23
 
21
24
  const emit = defineEmits<{
@@ -30,9 +33,10 @@ const selectedFiles = ref<File[]>([])
30
33
  const folderName = ref<string | null>(null)
31
34
 
32
35
  const isFolder = computed(() => props.mode === 'folder')
36
+ const isMixed = computed(() => props.mode === 'file+folder')
33
37
 
34
38
  const acceptLabel = computed(() => {
35
- if (!props.accept) return 'any file'
39
+ if (!props.accept) return isFolder.value ? 'a folder' : 'any file'
36
40
  return props.accept.split(',').map(a => a.trim().replace('.', '').toUpperCase()).join(', ')
37
41
  })
38
42
 
@@ -154,15 +158,19 @@ function extractFolderName(files: File[]): string | null {
154
158
  return null
155
159
  }
156
160
 
157
- function handleDrop(event: DragEvent) {
161
+ async function handleDrop(event: DragEvent) {
158
162
  event.preventDefault()
159
163
  isDragOver.value = false
160
164
  if (props.disabled) return
161
165
 
162
- if (isFolder.value && event.dataTransfer?.items) {
163
- handleFolderDrop(event.dataTransfer.items)
164
- } else if (event.dataTransfer?.files) {
165
- handleFiles(event.dataTransfer.files)
166
+ try {
167
+ if (props.mode !== 'file' && event.dataTransfer?.items?.length) {
168
+ await handleFolderDrop(event.dataTransfer.items)
169
+ } else if (event.dataTransfer?.files) {
170
+ handleFiles(event.dataTransfer.files)
171
+ }
172
+ } catch (cause) {
173
+ emit('error', cause instanceof Error ? cause.message : 'Could not read the dropped folder.')
166
174
  }
167
175
  }
168
176
 
@@ -171,8 +179,12 @@ async function handleFolderDrop(items: DataTransferItemList) {
171
179
  const relativePaths: string[] = []
172
180
  let folderNameFromDrop: string | undefined
173
181
 
174
- for (const item of Array.from(items)) {
182
+ // Capture live drag data before awaiting: the browser protects it after the drop handler.
183
+ const dropped = Array.from(items, (item) => {
175
184
  const entry = item.webkitGetAsEntry?.()
185
+ return { entry, file: entry ? null : item.getAsFile?.() }
186
+ })
187
+ for (const { entry, file: droppedFile } of dropped) {
176
188
  if (entry?.isDirectory) {
177
189
  folderNameFromDrop = entry.name
178
190
  await readDirectory(
@@ -181,8 +193,10 @@ async function handleFolderDrop(items: DataTransferItemList) {
181
193
  relativePaths,
182
194
  entry.name,
183
195
  )
184
- } else if (entry?.isFile) {
185
- const file = await getFile(entry as FileSystemFileEntry)
196
+ } else {
197
+ const file = entry?.isFile
198
+ ? await getFile(entry as FileSystemFileEntry)
199
+ : droppedFile
186
200
  if (file) {
187
201
  files.push(file)
188
202
  relativePaths.push(file.name)
@@ -291,7 +305,11 @@ function formatFileSize(bytes: number): string {
291
305
  <div
292
306
  role="button"
293
307
  tabindex="0"
294
- :aria-label="disabled ? 'File upload disabled' : 'Click or drag files to upload'"
308
+ :aria-label="disabled
309
+ ? 'File upload disabled'
310
+ : isMixed
311
+ ? 'Drop files or a folder, or click to choose files'
312
+ : isFolder ? 'Click or drag a folder to upload' : 'Click or drag files to upload'"
295
313
  :aria-disabled="disabled"
296
314
  :class="[
297
315
  'mint-file-uploader__dropzone',
@@ -345,7 +363,7 @@ function formatFileSize(bytes: number): string {
345
363
  </svg>
346
364
 
347
365
  <p class="mint-file-uploader__text">
348
- Drop {{ acceptLabel }} or click
366
+ {{ isMixed ? 'Drop files or a folder, or click to choose files' : `Drop ${acceptLabel} or click` }}
349
367
  </p>
350
368
  </div>
351
369
  </div>
@@ -356,7 +374,7 @@ function formatFileSize(bytes: number): string {
356
374
  </div>
357
375
 
358
376
  <!-- Folder summary display -->
359
- <div v-if="isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
377
+ <div v-if="showFiles && isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
360
378
  <div class="mint-file-uploader__folder-info">
361
379
  <div class="mint-file-uploader__folder-icon-wrapper">
362
380
  <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 +400,7 @@ function formatFileSize(bytes: number): string {
382
400
 
383
401
  <!-- File list display -->
384
402
  <TransitionGroup
385
- v-if="!isFolder && selectedFiles.length > 0"
403
+ v-if="showFiles && !isFolder && selectedFiles.length > 0"
386
404
  tag="ul"
387
405
  name="mint-file-uploader-item"
388
406
  class="mint-file-uploader__list"
@@ -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;
@@ -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>