@morscherlab/mint-sdk 1.2.0-beta.11 → 1.2.0-beta.13

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 (74) hide show
  1. package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-BIZKoRTT.js} +19 -12
  2. package/dist/ExperimentPopover-BIZKoRTT.js.map +1 -0
  3. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  4. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  5. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  6. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  7. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  8. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  9. package/dist/components/FilePicker.vue.d.ts +32 -0
  10. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  11. package/dist/components/index.d.ts +3 -0
  12. package/dist/components/index.js +3 -3
  13. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  14. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  15. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  16. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  17. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  18. package/dist/{components-CgAUzF8F.js → components-DuQRC810.js} +3727 -1521
  19. package/dist/components-DuQRC810.js.map +1 -0
  20. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  21. package/dist/composables/filePicker/usePickerNavigation.d.ts +95 -0
  22. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  23. package/dist/composables/filePicker/validation.d.ts +5 -0
  24. package/dist/composables/index.d.ts +2 -0
  25. package/dist/composables/index.js +3 -3
  26. package/dist/composables/useFileBrowser.d.ts +55 -0
  27. package/dist/composables/useFilePicker.d.ts +190 -0
  28. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  29. package/dist/{composables-B_pia3hO.js → composables-hY2Q2xPI.js} +289 -3
  30. package/dist/composables-hY2Q2xPI.js.map +1 -0
  31. package/dist/index.js +5 -5
  32. package/dist/install.js +1 -1
  33. package/dist/styles.css +2232 -1259
  34. package/dist/types/fileBrowserTypes.d.ts +77 -0
  35. package/dist/types/filePicker.d.ts +69 -0
  36. package/dist/types/index.d.ts +2 -0
  37. package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-nBbSn1TI.js} +46 -2
  38. package/dist/validation-nBbSn1TI.js.map +1 -0
  39. package/package.json +1 -1
  40. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  41. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  42. package/src/__tests__/components/FilePicker.state.test.ts +160 -0
  43. package/src/__tests__/components/FilePicker.test.ts +328 -0
  44. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  45. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  46. package/src/components/ExperimentPopover.vue +10 -2
  47. package/src/components/FileBrowserModal.story.vue +203 -0
  48. package/src/components/FileBrowserModal.vue +384 -0
  49. package/src/components/FilePicker.vue +301 -0
  50. package/src/components/fileBrowserPresentation.ts +39 -0
  51. package/src/components/index.ts +4 -0
  52. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  53. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  54. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  55. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  56. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  57. package/src/composables/filePicker/localFiles.ts +55 -0
  58. package/src/composables/filePicker/usePickerNavigation.ts +199 -0
  59. package/src/composables/filePicker/usePickerSelection.ts +242 -0
  60. package/src/composables/filePicker/validation.ts +40 -0
  61. package/src/composables/index.ts +7 -0
  62. package/src/composables/useFileBrowser.ts +314 -0
  63. package/src/composables/useFilePicker.ts +347 -0
  64. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  65. package/src/styles/components/file-browser-modal.css +489 -0
  66. package/src/styles/components/file-picker.css +501 -0
  67. package/src/styles/index.css +3 -0
  68. package/src/types/fileBrowserTypes.ts +85 -0
  69. package/src/types/filePicker.ts +65 -0
  70. package/src/types/index.ts +14 -0
  71. package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
  72. package/dist/components-CgAUzF8F.js.map +0 -1
  73. package/dist/composables-B_pia3hO.js.map +0 -1
  74. package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
@@ -0,0 +1,199 @@
1
+ import { computed, onScopeDispose, ref, shallowRef } from 'vue'
2
+ import type {
3
+ PickerAdapter,
4
+ PickerNode,
5
+ PickerRequest,
6
+ PickerTreeRow,
7
+ } from '../../types/filePicker'
8
+
9
+ export function usePickerNavigation(adapter: () => PickerAdapter, request: () => PickerRequest) {
10
+ const path = shallowRef('')
11
+ const trail = ref<{ path: string; label: string }[]>([])
12
+ const entries = shallowRef<PickerNode[]>([])
13
+ const nodes = ref(new Map<string, PickerNode>())
14
+ const children = shallowRef(new Map<string, PickerNode[]>())
15
+ const expanded = ref(new Set<string>())
16
+ const expanding = ref(new Set<string>())
17
+ const expansionErrors = ref(new Map<string, string>())
18
+ const loading = shallowRef(false)
19
+ const error = shallowRef('')
20
+ let epoch = 0
21
+ let navigation = 0
22
+ let controller: AbortController | undefined
23
+ const pending = new Map<string, Promise<PickerNode[]>>()
24
+ const childControllers = new Set<AbortController>()
25
+
26
+ function register(items: PickerNode[], parent?: string) {
27
+ for (const item of items)
28
+ nodes.value.set(item.path, {
29
+ ...item,
30
+ parentPath:
31
+ item.parentPath !== undefined
32
+ ? item.parentPath
33
+ : parent !== undefined
34
+ ? parent || null
35
+ : (nodes.value.get(item.path)?.parentPath ?? null),
36
+ })
37
+ }
38
+ function invalidate() {
39
+ ++epoch
40
+ ++navigation
41
+ controller?.abort()
42
+ childControllers.forEach((item) => item.abort())
43
+ childControllers.clear()
44
+ pending.clear()
45
+ children.value = new Map()
46
+ expanding.value.clear()
47
+ expansionErrors.value.clear()
48
+ }
49
+ function clear() {
50
+ invalidate()
51
+ nodes.value.clear()
52
+ expanded.value.clear()
53
+ expanding.value.clear()
54
+ expansionErrors.value.clear()
55
+ entries.value = []
56
+ loading.value = false
57
+ error.value = ''
58
+ }
59
+ async function loadChildren(folder: string, ancestors: string[] = []): Promise<PickerNode[]> {
60
+ if (ancestors.includes(folder)) throw new Error('The adapter returned a circular folder tree.')
61
+ const cached = children.value.get(folder)
62
+ if (cached) return cached
63
+ const existing = pending.get(folder)
64
+ if (existing) return existing
65
+ const token = epoch
66
+ const abort = new AbortController()
67
+ childControllers.add(abort)
68
+ const task = adapter()
69
+ .listChildren(folder, { ...request(), signal: abort.signal })
70
+ .then((items) => {
71
+ if (token !== epoch) throw new DOMException('Cancelled', 'AbortError')
72
+ const retained = new Set(items.map((item) => item.path))
73
+ function remove(path: string, seen = new Set<string>()) {
74
+ if (seen.has(path) || path === folder) return
75
+ seen.add(path)
76
+ for (const node of nodes.value.values())
77
+ if (node.parentPath === path) remove(node.path, seen)
78
+ nodes.value.delete(path)
79
+ children.value.delete(path)
80
+ }
81
+ for (const node of nodes.value.values())
82
+ if (node.parentPath === folder && !retained.has(node.path)) remove(node.path)
83
+ register(items, folder)
84
+ children.value = new Map(children.value).set(folder, items)
85
+ return items
86
+ })
87
+ .finally(() => {
88
+ childControllers.delete(abort)
89
+ if (token === epoch) pending.delete(folder)
90
+ })
91
+ pending.set(folder, task)
92
+ return task
93
+ }
94
+ async function load(query = '', recursive = false) {
95
+ const token = ++navigation
96
+ controller?.abort()
97
+ controller = new AbortController()
98
+ const options = { ...request(), signal: controller.signal }
99
+ loading.value = true
100
+ error.value = ''
101
+ entries.value = []
102
+ try {
103
+ const items = query.trim()
104
+ ? await adapter().search(query.trim(), { path: path.value, recursive }, options)
105
+ : path.value
106
+ ? await loadChildren(path.value)
107
+ : await adapter().listRoot(options)
108
+ if (token !== navigation) return
109
+ register(items, query.trim() ? undefined : path.value)
110
+ entries.value = items
111
+ } catch (cause) {
112
+ if (token === navigation)
113
+ error.value = cause instanceof Error ? cause.message : 'Could not read this location.'
114
+ } finally {
115
+ if (token === navigation) loading.value = false
116
+ }
117
+ }
118
+ async function expand(node: PickerNode) {
119
+ if (node.kind !== 'folder' || node.unavailableReason) return
120
+ if (expanded.value.has(node.path)) {
121
+ expanded.value.delete(node.path)
122
+ return
123
+ }
124
+ expanded.value.add(node.path)
125
+ expanding.value.add(node.path)
126
+ expansionErrors.value.delete(node.path)
127
+ const token = epoch
128
+ try {
129
+ await loadChildren(node.path)
130
+ } catch (cause) {
131
+ if (token === epoch)
132
+ expansionErrors.value.set(
133
+ node.path,
134
+ cause instanceof Error ? cause.message : 'Could not read this folder.',
135
+ )
136
+ } finally {
137
+ if (token === epoch) expanding.value.delete(node.path)
138
+ }
139
+ }
140
+ function enter(node: PickerNode) {
141
+ if (node.kind !== 'folder' || node.unavailableReason) return
142
+ const index = trail.value.findIndex((crumb) => crumb.path === node.path)
143
+ trail.value =
144
+ index >= 0
145
+ ? trail.value.slice(0, index + 1)
146
+ : [...trail.value, { path: node.path, label: node.name }]
147
+ path.value = node.path
148
+ }
149
+ function up(target?: string) {
150
+ const index =
151
+ target !== undefined
152
+ ? trail.value.findIndex((crumb) => crumb.path === target)
153
+ : trail.value.length - 2
154
+ trail.value = index < 0 ? [] : trail.value.slice(0, index + 1)
155
+ path.value = trail.value.at(-1)?.path ?? ''
156
+ }
157
+ const rows = computed(() => {
158
+ const result: PickerTreeRow[] = []
159
+ function visit(items: PickerNode[], depth: number, ancestors: Set<string>) {
160
+ for (const item of items) {
161
+ if (ancestors.has(item.path) || depth > 30) continue
162
+ const node = nodes.value.get(item.path) ?? item
163
+ result.push({
164
+ node,
165
+ depth,
166
+ expanded: expanded.value.has(item.path),
167
+ loading: expanding.value.has(item.path),
168
+ error: expansionErrors.value.get(item.path),
169
+ })
170
+ if (expanded.value.has(item.path))
171
+ visit(children.value.get(item.path) ?? [], depth + 1, new Set([...ancestors, item.path]))
172
+ }
173
+ }
174
+ visit(entries.value, 0, new Set())
175
+ return result
176
+ })
177
+ onScopeDispose(clear)
178
+ return {
179
+ path,
180
+ trail,
181
+ entries,
182
+ nodes,
183
+ children,
184
+ expanded,
185
+ expanding,
186
+ expansionErrors,
187
+ loading,
188
+ error,
189
+ rows,
190
+ clear,
191
+ invalidate,
192
+ register,
193
+ loadChildren,
194
+ load,
195
+ expand,
196
+ enter,
197
+ up,
198
+ }
199
+ }
@@ -0,0 +1,242 @@
1
+ import { computed, ref, shallowRef } from 'vue'
2
+ import type {
3
+ PickerCapability,
4
+ PickerExcluded,
5
+ PickerFile,
6
+ PickerNode,
7
+ PickerSelection,
8
+ PickerSelectionMode,
9
+ } from '../../types/filePicker'
10
+ import type { usePickerNavigation } from './usePickerNavigation'
11
+ import { pickerExclusion } from './validation'
12
+
13
+ type Navigation = ReturnType<typeof usePickerNavigation>
14
+ export function usePickerSelection(
15
+ nav: Navigation,
16
+ mode: () => PickerSelectionMode,
17
+ capability: () => PickerCapability | undefined,
18
+ skipBlanks: () => boolean,
19
+ ) {
20
+ const keys = ref(new Set<string>())
21
+ const dropped = ref(new Set<string>())
22
+ const focusPath = shallowRef('')
23
+ const resolving = shallowRef(false)
24
+ const resolutionError = shallowRef('')
25
+ let anchor = ''
26
+ let generation = 0
27
+
28
+ function ancestorSelected(node: PickerNode, candidates = keys.value): boolean {
29
+ const seen = new Set<string>()
30
+ let parent = node.parentPath
31
+ while (parent && !seen.has(parent)) {
32
+ if (candidates.has(parent)) return true
33
+ seen.add(parent)
34
+ parent = nav.nodes.value.get(parent)?.parentPath
35
+ }
36
+ return false
37
+ }
38
+ function exclusion(node: PickerNode) {
39
+ const own = pickerExclusion(node, capability(), skipBlanks())
40
+ if (own) return own
41
+ const seen = new Set<string>()
42
+ let parent = node.parentPath
43
+ while (parent && !seen.has(parent)) {
44
+ seen.add(parent)
45
+ const folder = nav.nodes.value.get(parent)
46
+ if (!folder) break
47
+ const inherited = pickerExclusion(folder, capability(), skipBlanks())
48
+ if (inherited) return { ...inherited, path: node.path, name: node.name }
49
+ parent = folder.parentPath
50
+ }
51
+ return null
52
+ }
53
+ function selected(node: PickerNode): boolean {
54
+ return (
55
+ !exclusion(node) &&
56
+ !dropped.value.has(node.path) &&
57
+ !ancestorSelected(node, dropped.value) &&
58
+ (keys.value.has(node.path) || (mode() === 'folder+files' && ancestorSelected(node)))
59
+ )
60
+ }
61
+ function selectable(node: PickerNode): boolean {
62
+ if (exclusion(node)) return false
63
+ return node.kind === 'folder'
64
+ ? mode() === 'folder' || mode() === 'folder+files'
65
+ : mode() !== 'folder'
66
+ }
67
+ function select(
68
+ node: PickerNode,
69
+ modifiers: { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; toggle?: boolean } = {},
70
+ ) {
71
+ if (exclusion(node)) return
72
+ focusPath.value = node.path
73
+ if (!selectable(node)) return
74
+ const multi = mode() === 'multi-file' || mode() === 'folder+files'
75
+ const additive = multi && (modifiers.metaKey || modifiers.ctrlKey)
76
+ if (multi && modifiers.shiftKey && anchor) {
77
+ const rows = nav.rows.value.map((row) => row.node)
78
+ const from = rows.findIndex((item) => item.path === anchor)
79
+ const to = rows.findIndex((item) => item.path === node.path)
80
+ if (from >= 0 && to >= 0) {
81
+ keys.value = new Set([
82
+ ...(additive ? keys.value : []),
83
+ ...rows
84
+ .slice(Math.min(from, to), Math.max(from, to) + 1)
85
+ .filter(selectable)
86
+ .map((item) => item.path),
87
+ ])
88
+ dropped.value.clear()
89
+ return
90
+ }
91
+ }
92
+ if (additive || (modifiers.toggle && (selected(node) || dropped.value.has(node.path)))) {
93
+ if (mode() === 'folder+files' && ancestorSelected(node)) {
94
+ if (dropped.value.has(node.path)) dropped.value.delete(node.path)
95
+ else dropped.value.add(node.path)
96
+ } else if (keys.value.has(node.path)) keys.value.delete(node.path)
97
+ else {
98
+ keys.value.add(node.path)
99
+ dropped.value.delete(node.path)
100
+ }
101
+ } else {
102
+ keys.value = new Set([node.path])
103
+ dropped.value.clear()
104
+ }
105
+ anchor = node.path
106
+ }
107
+ function reset(selection?: PickerSelection | null) {
108
+ ++generation
109
+ keys.value.clear()
110
+ dropped.value.clear()
111
+ resolving.value = false
112
+ resolutionError.value = ''
113
+ focusPath.value = ''
114
+ if (selection?.source === 'server') {
115
+ const files =
116
+ mode() === 'folder'
117
+ ? []
118
+ : mode() === 'single-file'
119
+ ? selection.files.slice(0, 1)
120
+ : selection.files
121
+ const folders =
122
+ mode() === 'folder'
123
+ ? selection.folders.slice(0, 1)
124
+ : mode() === 'folder+files'
125
+ ? selection.folders
126
+ : []
127
+ nav.register(files)
128
+ folders.forEach((path) => {
129
+ if (!nav.nodes.value.has(path))
130
+ nav.register([
131
+ { kind: 'folder', path, name: 'Selected folder', parentName: 'Selected location' },
132
+ ])
133
+ keys.value.add(path)
134
+ })
135
+ files.forEach((file) => keys.value.add(file.path))
136
+ selection.excluded
137
+ .filter((item) => item.reason === 'deselected')
138
+ .forEach((item) => dropped.value.add(item.path))
139
+ }
140
+ }
141
+ const roots = computed(() =>
142
+ [...keys.value]
143
+ .map((key) => nav.nodes.value.get(key))
144
+ .filter((node): node is PickerNode => !!node && !ancestorSelected(node)),
145
+ )
146
+ const chosen = computed(() => {
147
+ const files: PickerFile[] = []
148
+ const excluded: PickerExcluded[] = []
149
+ for (const node of nav.nodes.value.values()) {
150
+ if (!keys.value.has(node.path) && !ancestorSelected(node)) continue
151
+ const reason =
152
+ exclusion(node) ??
153
+ (dropped.value.has(node.path)
154
+ ? { path: node.path, name: node.name, reason: 'deselected' as const }
155
+ : null)
156
+ // A dropped folder excludes its descendants as well.
157
+ const droppedByParent = ancestorSelected(node, dropped.value)
158
+ if (reason || droppedByParent) {
159
+ if (node.kind === 'file' || reason?.reason === 'unreadable')
160
+ excluded.push(reason ?? { path: node.path, name: node.name, reason: 'deselected' })
161
+ } else if (node.kind === 'file') files.push(node)
162
+ }
163
+ return { files, excluded, bytes: files.reduce((total, node) => total + (node.bytes ?? 0), 0) }
164
+ })
165
+ async function resolve() {
166
+ const token = ++generation
167
+ resolving.value = true
168
+ resolutionError.value = ''
169
+ let count = 0
170
+ const seen = new Set<string>()
171
+ async function walk(node: PickerNode, ancestors: string[] = []): Promise<PickerFile[]> {
172
+ if (token !== generation) throw new DOMException('Cancelled', 'AbortError')
173
+ if (node.kind === 'file') return [node]
174
+ if (node.unavailableReason) return []
175
+ if (ancestors.includes(node.path))
176
+ throw new Error('The adapter returned a circular folder tree.')
177
+ if (seen.has(node.path)) return []
178
+ seen.add(node.path)
179
+ if (++count > 500 || ancestors.length > 30)
180
+ throw new Error('This selection is too large to validate. Choose a smaller folder.')
181
+ const children = await nav.loadChildren(node.path, ancestors)
182
+ const files: PickerFile[] = []
183
+ for (const child of children) files.push(...(await walk(child, [...ancestors, node.path])))
184
+ if (token !== generation) throw new DOMException('Cancelled', 'AbortError')
185
+ const formats: Record<string, number> = {}
186
+ files.forEach((file) => {
187
+ formats[file.format] = (formats[file.format] ?? 0) + 1
188
+ })
189
+ nav.register([
190
+ {
191
+ ...node,
192
+ fileCount: files.length,
193
+ bytes: files.reduce((sum, file) => sum + (file.bytes ?? 0), 0),
194
+ formats,
195
+ },
196
+ ])
197
+ return files
198
+ }
199
+ try {
200
+ for (const node of roots.value) await walk(node)
201
+ if (token === generation) {
202
+ // Resolved descendants inherit their folder, including restored selections.
203
+ for (const key of [...keys.value]) {
204
+ const node = nav.nodes.value.get(key)
205
+ if (node && ancestorSelected(node)) keys.value.delete(key)
206
+ }
207
+ }
208
+ } catch (cause) {
209
+ if (token === generation)
210
+ resolutionError.value =
211
+ cause instanceof Error ? cause.message : 'Could not validate the selection.'
212
+ } finally {
213
+ if (token === generation) resolving.value = false
214
+ }
215
+ }
216
+ const focused = computed(() => nav.nodes.value.get(focusPath.value) ?? roots.value.at(-1) ?? null)
217
+ function folderProgress(node: PickerNode): string {
218
+ if (node.kind !== 'folder' || node.fileCount == null) return ''
219
+ const allowed = chosen.value.files.filter((file) =>
220
+ ancestorSelected(file, new Set([node.path])),
221
+ ).length
222
+ return `${selected(node) ? allowed : 0} / ${node.fileCount}`
223
+ }
224
+ return {
225
+ keys,
226
+ dropped,
227
+ focusPath,
228
+ resolving,
229
+ resolutionError,
230
+ roots,
231
+ chosen,
232
+ focused,
233
+ select,
234
+ selected,
235
+ selectable,
236
+ exclusion,
237
+ reset,
238
+ resolve,
239
+ folderProgress,
240
+ ancestorSelected,
241
+ }
242
+ }
@@ -0,0 +1,40 @@
1
+ import type { PickerCapability, PickerExcluded, PickerNode } from '../../types/filePicker'
2
+
3
+ export function normalizePickerFormat(format: string): string {
4
+ return format.trim().replace(/^\./, '').toLowerCase()
5
+ }
6
+
7
+ export function pickerExclusion(
8
+ node: PickerNode,
9
+ capability?: PickerCapability,
10
+ skipBlanks = false,
11
+ ): PickerExcluded | null {
12
+ const base = { path: node.path, name: node.name }
13
+ if (node.unavailableReason) return { ...base, reason: 'unreadable' }
14
+ if (skipBlanks && capability?.isBlank?.(node)) return { ...base, reason: 'blank' }
15
+ if (node.kind === 'folder') return null
16
+ const formats = capability?.formats.map(normalizePickerFormat) ?? []
17
+ if (formats.length && !formats.some((format) => node.name.toLowerCase().endsWith(`.${format}`))) {
18
+ return { ...base, reason: 'unsupported' }
19
+ }
20
+ if (capability?.maxBytes != null && node.bytes != null && node.bytes > capability.maxBytes) {
21
+ return { ...base, reason: 'tooLarge' }
22
+ }
23
+ return null
24
+ }
25
+
26
+ export function pickerReason(excluded: PickerExcluded | null, node?: PickerNode): string {
27
+ if (!excluded) return ''
28
+ return {
29
+ unsupported: 'This format is not supported by this source.',
30
+ tooLarge: 'This file exceeds the size limit for this source.',
31
+ blank: 'Skipped by the blank-file rule.',
32
+ unreadable: node?.unavailableReason || 'This item cannot be read.',
33
+ deselected: 'Removed from the selection.',
34
+ }[excluded.reason]
35
+ }
36
+
37
+ export function pickerFormat(name: string): string {
38
+ const compound = /\.(mzml|mzxml)\.gz$/i.exec(name)
39
+ return compound ? `${compound[1]}.gz` : name.includes('.') ? name.split('.').pop()! : ''
40
+ }
@@ -174,6 +174,11 @@ export {
174
174
  type UseAnalysisArtifactsOptions,
175
175
  type UseAnalysisArtifactsReturn,
176
176
  } from './useAnalysisArtifacts'
177
+ export {
178
+ useFileBrowser,
179
+ type UseFileBrowserOptions,
180
+ type UseFileBrowserReturn,
181
+ } from './useFileBrowser'
177
182
  export {
178
183
  buildAnalysisArtifactTargetQuery,
179
184
  parseAnalysisArtifactTarget,
@@ -564,3 +569,5 @@ export {
564
569
  type UseMenuKeyboardOptions,
565
570
  type UseMenuKeyboardReturn,
566
571
  } from './useMenuKeyboard'
572
+
573
+ export { usePlatformFilePickerAdapter, encodePlatformPickerPath, decodePlatformPickerPath } from './usePlatformFilePickerAdapter'