@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,39 @@
1
+ import type { FileEntry } from '../types/fileBrowserTypes'
2
+
3
+ export const FILE_BROWSER_ICONS = {
4
+ disk: 'M4 5h16v14H4zM8 9h.01M4 12h16',
5
+ folder: 'M4 7h6l2 2h8v10H4z',
6
+ file: 'M14 3v5h5M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z',
7
+ }
8
+
9
+ export function formatCount(value: number): string {
10
+ // Thin spaces group digits without the comma/period ambiguity across locales.
11
+ return value.toLocaleString('en-US').replace(/,/g, ' ')
12
+ }
13
+
14
+ export function formatSize(entry: FileEntry): string {
15
+ if (entry.kind === 'directory') {
16
+ return entry.child_count === null || entry.child_count === undefined
17
+ ? '—'
18
+ : `${entry.child_count} item${entry.child_count === 1 ? '' : 's'}`
19
+ }
20
+ return formatBytes(entry.size_bytes ?? null)
21
+ }
22
+
23
+ export function formatBytes(bytes: number | null): string {
24
+ if (bytes === null) return '—'
25
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
26
+ let value = bytes
27
+ let unit = 0
28
+ while (value >= 1024 && unit < units.length - 1) {
29
+ value /= 1024
30
+ unit += 1
31
+ }
32
+ const rounded = value >= 100 || unit === 0 ? Math.round(value) : Number(value.toFixed(1))
33
+ return `${rounded} ${units[unit]}`
34
+ }
35
+
36
+ export function entryType(entry: FileEntry): string {
37
+ if (entry.kind === 'directory') return 'Folder'
38
+ return entry.extension ? entry.extension.replace('.', '').toUpperCase() : 'File'
39
+ }
@@ -99,6 +99,7 @@ export { default as SampleHierarchyTree } from './SampleHierarchyTree.vue'
99
99
  export { default as ProtocolStepEditor } from './ProtocolStepEditor.vue'
100
100
 
101
101
  // Scientific display components
102
+ export { default as PlotlyChart } from './PlotlyChart.vue'
102
103
  export { default as ScientificNumber } from './ScientificNumber.vue'
103
104
  /** Renders a chemical formula string (H₂O, C₆H₁₂O₆, Ca(OH)₂) with typographically correct subscripts and superscripts for charges. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; no platform replacement is planned. */
104
105
  export { default as ChemicalFormula } from './ChemicalFormula.vue'
@@ -142,7 +143,11 @@ export { default as SectionCard } from './SectionCard.vue'
142
143
  // Experiment / analysis components
143
144
  export { default as ExperimentSelectorModal } from './ExperimentSelectorModal.vue'
144
145
  export { default as ArtifactSelectorModal } from './ArtifactSelectorModal.vue'
146
+ /** Modal picker over the read-only server mounts, recording paths rather than copying data. */
147
+ export { default as FileBrowserModal } from './FileBrowserModal.vue'
145
148
  export { default as GeneratedPathInput } from './GeneratedPathInput.vue'
146
149
  export { default as ArtifactSaveDialog } from './ArtifactSaveDialog.vue'
147
150
  export { default as ExperimentPopover } from './ExperimentPopover.vue'
148
151
  export { default as FitPanel } from './FitPanel.vue'
152
+
153
+ export { default as FilePicker } from './FilePicker.vue'
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ import { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'
3
+ import type { ModalSize } from '../../types'
4
+ import { useFocusTrap } from '../../composables/useFocusTrap'
5
+ import IconButton from '../IconButton.vue'
6
+
7
+ const props = defineProps<{ open: boolean; label: string; size: ModalSize; variant?: 'picker' }>()
8
+ const emit = defineEmits<{ close: [] }>()
9
+ const containerRef = shallowRef<HTMLElement | null>(null)
10
+ const active = shallowRef(false)
11
+ useFocusTrap({ containerRef, active })
12
+
13
+ // Activate after mounting so an initially open picker traps focus too.
14
+ onMounted(() => {
15
+ active.value = props.open
16
+ })
17
+ watch(
18
+ () => props.open,
19
+ (open) => {
20
+ active.value = open
21
+ },
22
+ )
23
+ watch(
24
+ active,
25
+ (open, _, onCleanup) => {
26
+ if (!open) return
27
+ const overflow = document.body.style.overflow
28
+ document.body.style.overflow = 'hidden'
29
+ onCleanup(() => {
30
+ document.body.style.overflow = overflow
31
+ })
32
+ },
33
+ { flush: 'sync' },
34
+ )
35
+ onBeforeUnmount(() => {
36
+ active.value = false
37
+ })
38
+ </script>
39
+
40
+ <template>
41
+ <Teleport to="body">
42
+ <div v-if="open" class="mint-file-browser-overlay">
43
+ <section
44
+ ref="containerRef"
45
+ class="mint-file-browser-dialog"
46
+ :class="[
47
+ `mint-file-browser-dialog--${size}`,
48
+ variant === 'picker' ? 'mint-picker-dialog' : '',
49
+ ]"
50
+ role="dialog"
51
+ aria-modal="true"
52
+ :aria-label="label"
53
+ tabindex="-1"
54
+ @keydown.esc.stop.prevent="emit('close')"
55
+ >
56
+ <header class="mint-file-browser-dialog__header">
57
+ <slot name="header" />
58
+ <IconButton size="sm" label="Close modal" @click="emit('close')">
59
+ <svg
60
+ viewBox="0 0 24 24"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ stroke-width="2"
64
+ aria-hidden="true"
65
+ >
66
+ <path d="M6 6l12 12M18 6L6 18" />
67
+ </svg>
68
+ </IconButton>
69
+ </header>
70
+ <slot />
71
+ <footer class="mint-file-browser-dialog__footer"><slot name="footer" /></footer>
72
+ </section>
73
+ </div>
74
+ </Teleport>
75
+ </template>
@@ -0,0 +1,197 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { FileEntry, FileSelection } from '../../types/fileBrowserTypes'
4
+ import {
5
+ FILE_BROWSER_ICONS as ICON,
6
+ formatCount,
7
+ formatSize,
8
+ entryType,
9
+ } from '../fileBrowserPresentation'
10
+ import EmptyState from '../EmptyState.vue'
11
+ import BaseButton from '../BaseButton.vue'
12
+ import IconButton from '../IconButton.vue'
13
+ import Skeleton from '../Skeleton.vue'
14
+ const props = defineProps<{
15
+ mountId: string
16
+ entries: FileEntry[]
17
+ selected: FileSelection[]
18
+ loading: boolean
19
+ error?: string | null
20
+ dimNonMatching: boolean
21
+ totalCount: number | null
22
+ matchCount: number | null
23
+ truncated: boolean
24
+ hasMounts: boolean
25
+ hasAvailableMounts: boolean
26
+ search: string
27
+ }>()
28
+ const emit = defineEmits<{ toggle: [entry: FileEntry]; open: [entry: FileEntry]; refresh: [] }>()
29
+ const emptyTitle = computed(() =>
30
+ !props.hasMounts
31
+ ? 'No server mounts configured'
32
+ : !props.hasAvailableMounts
33
+ ? 'Server mounts are offline'
34
+ : props.search
35
+ ? 'No matching files or folders'
36
+ : 'This folder is empty',
37
+ )
38
+ const emptyDescription = computed(() =>
39
+ !props.hasMounts
40
+ ? 'Ask your administrator to configure a read-only data mount.'
41
+ : !props.hasAvailableMounts
42
+ ? 'Refresh when the data mount is available again.'
43
+ : props.search
44
+ ? 'Try a different search or clear the search field.'
45
+ : 'Choose another folder from the mount list or breadcrumbs.',
46
+ )
47
+ function isSelected(entry: FileEntry) {
48
+ return props.selected.some((item) => item.mount_id === props.mountId && item.path === entry.path)
49
+ }
50
+ function isDimmed(entry: FileEntry) {
51
+ return props.dimNonMatching && entry.matches_type_rules === false
52
+ }
53
+ function entryIcon(entry: FileEntry) {
54
+ return entry.kind === 'directory' ? ICON.folder : ICON.file
55
+ }
56
+ function handleRowClick(entry: FileEntry) {
57
+ if (!props.loading && !props.error && props.mountId && entry.kind === 'directory')
58
+ emit('open', entry)
59
+ }
60
+ function toggleEntry(entry: FileEntry) {
61
+ if (!props.loading && !props.error && props.mountId) emit('toggle', entry)
62
+ }
63
+ function folderNote(entry: FileEntry) {
64
+ if (entry.kind !== 'directory' || !isSelected(entry)) return ''
65
+ return entry.child_count == null
66
+ ? 'whole folder'
67
+ : `whole folder, ${entry.child_count} item${entry.child_count === 1 ? '' : 's'}`
68
+ }
69
+ const statusLine = computed(() => {
70
+ if (props.loading) return 'Loading folder…'
71
+ if (props.error || !props.mountId) return ''
72
+ const total = props.totalCount ?? props.entries.length
73
+ const parts = [`${formatCount(total)} item${total === 1 ? '' : 's'}`]
74
+ if (props.entries.length !== total) parts.push(`${formatCount(props.entries.length)} shown`)
75
+ if (props.matchCount !== null && props.matchCount !== total)
76
+ parts.push(`${formatCount(props.matchCount)} match the type rules`)
77
+ if (props.truncated) parts.push('listing capped')
78
+ return parts.join(' · ')
79
+ })
80
+ </script>
81
+ <template>
82
+ <div class="mint-file-browser__listing" :aria-busy="loading">
83
+ <div class="mint-file-browser__head">
84
+ <span class="mint-file-browser__col-check" />
85
+ <span class="mint-file-browser__col-icon" />
86
+ <span class="mint-file-browser__col-name">Name</span>
87
+ <span class="mint-file-browser__col-size">Size</span>
88
+ <span class="mint-file-browser__col-type">Type</span>
89
+ <span class="mint-file-browser__col-open" />
90
+ </div>
91
+
92
+ <div v-if="loading" class="mint-file-browser__skeleton">
93
+ <div v-for="n in 6" :key="n" class="mint-file-browser__skeleton-row">
94
+ <Skeleton width="60%" height="14px" />
95
+ <Skeleton width="60px" height="10px" />
96
+ </div>
97
+ </div>
98
+
99
+ <div v-else-if="error" class="mint-file-browser__error" role="alert">
100
+ <span>{{ error }}</span>
101
+ <BaseButton
102
+ variant="secondary"
103
+ size="sm"
104
+ class="mint-file-browser__retry"
105
+ aria-label="Retry folder listing"
106
+ @click="emit('refresh')"
107
+ >
108
+ Try again
109
+ </BaseButton>
110
+ </div>
111
+
112
+ <EmptyState
113
+ v-else-if="!entries.length"
114
+ :title="emptyTitle"
115
+ :description="emptyDescription"
116
+ size="sm"
117
+ />
118
+
119
+ <div v-else class="mint-file-browser__rows">
120
+ <div
121
+ v-for="entry in entries"
122
+ :key="entry.path"
123
+ class="mint-file-browser__row"
124
+ :class="{
125
+ 'mint-file-browser__row--selected': isSelected(entry),
126
+ 'mint-file-browser__row--dimmed': isDimmed(entry),
127
+ 'mint-file-browser__row--dir': entry.kind === 'directory',
128
+ }"
129
+ @click="handleRowClick(entry)"
130
+ >
131
+ <input
132
+ class="mint-file-browser__checkbox"
133
+ type="checkbox"
134
+ :checked="isSelected(entry)"
135
+ :title="entry.kind === 'directory' ? 'Select the whole folder' : 'Select this file'"
136
+ :aria-label="
137
+ entry.kind === 'directory'
138
+ ? `Select the whole ${entry.name} folder`
139
+ : `Select ${entry.name}`
140
+ "
141
+ @click.stop
142
+ @change="toggleEntry(entry)"
143
+ />
144
+ <span
145
+ class="mint-file-browser__row-icon"
146
+ :class="`mint-file-browser__row-icon--${entry.kind}`"
147
+ >
148
+ <svg
149
+ class="mint-file-browser__icon"
150
+ viewBox="0 0 24 24"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ stroke-width="1.8"
154
+ stroke-linecap="round"
155
+ stroke-linejoin="round"
156
+ >
157
+ <path :d="entryIcon(entry)" />
158
+ </svg>
159
+ </span>
160
+ <span class="mint-file-browser__name-cell">
161
+ <span class="mint-file-browser__name" :title="entry.name">{{ entry.name }}</span>
162
+ <span v-if="folderNote(entry)" class="mint-file-browser__note-pill">Whole folder</span>
163
+ </span>
164
+ <span class="mint-file-browser__size">{{ formatSize(entry) }}</span>
165
+ <span class="mint-file-browser__type">{{ entryType(entry) }}</span>
166
+ <!-- The chevron is the keyboard route into a folder: the row
167
+ itself is a div, so this button is what Tab can reach. -->
168
+ <IconButton
169
+ v-if="entry.kind === 'directory'"
170
+ size="sm"
171
+ class="mint-file-browser__open"
172
+ :label="`Open ${entry.name}`"
173
+ @click.stop="handleRowClick(entry)"
174
+ >
175
+ <svg
176
+ class="mint-file-browser__icon"
177
+ viewBox="0 0 24 24"
178
+ fill="none"
179
+ stroke="currentColor"
180
+ stroke-width="2.5"
181
+ stroke-linecap="round"
182
+ stroke-linejoin="round"
183
+ >
184
+ <path d="M9 6l6 6-6 6" />
185
+ </svg>
186
+ </IconButton>
187
+ <span v-else class="mint-file-browser__open" aria-hidden="true" />
188
+ </div>
189
+ </div>
190
+
191
+ <div class="mint-file-browser__status" role="status" aria-live="polite">
192
+ <span>{{ statusLine }}</span>
193
+ <div class="mint-file-browser__status-spacer" />
194
+ <span class="mint-file-browser__legend">Read-only · Select paths, not copies</span>
195
+ </div>
196
+ </div>
197
+ </template>
@@ -0,0 +1,136 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import BaseButton from '../BaseButton.vue'
4
+ import BaseInput from '../BaseInput.vue'
5
+ import IconButton from '../IconButton.vue'
6
+ import type {
7
+ FileBrowserSortKey,
8
+ FilePathCrumb,
9
+ FileBrowserLocation,
10
+ } from '../../types/fileBrowserTypes'
11
+ const props = defineProps<{
12
+ mountId: string
13
+ path: string
14
+ parent: string | null
15
+ breadcrumbs: FilePathCrumb[]
16
+ search: string
17
+ sort: FileBrowserSortKey
18
+ typeRules: string[]
19
+ loading: boolean
20
+ error?: string | null
21
+ }>()
22
+ const emit = defineEmits<{
23
+ navigate: [location: FileBrowserLocation]
24
+ selectFolder: [location: FileBrowserLocation]
25
+ 'update:search': [value: string]
26
+ 'update:sort': [value: FileBrowserSortKey]
27
+ refresh: []
28
+ }>()
29
+ const typeRuleLabel = computed(() => props.typeRules.join(' '))
30
+ function handleBack() {
31
+ if (props.parent !== null && !props.loading && !props.error)
32
+ emit('navigate', { mount_id: props.mountId, path: props.parent })
33
+ }
34
+ function handleCrumb(crumb: FilePathCrumb) {
35
+ emit('navigate', { mount_id: props.mountId, path: crumb.path })
36
+ }
37
+ function cycleSort() {
38
+ const order: FileBrowserSortKey[] = ['name', 'size', 'modified']
39
+ emit('update:sort', order[(order.indexOf(props.sort) + 1) % order.length]!)
40
+ }
41
+ </script>
42
+ <template>
43
+ <div class="mint-file-browser__toolbar">
44
+ <div class="mint-file-browser__pathbar">
45
+ <IconButton
46
+ size="sm"
47
+ class="mint-file-browser__back"
48
+ label="Go to parent folder"
49
+ :disabled="parent === null || loading || !!error"
50
+ @click="handleBack"
51
+ >
52
+ <svg
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ aria-hidden="true"
58
+ >
59
+ <path d="M12 19V5m-6 6 6-6 6 6" />
60
+ </svg>
61
+ </IconButton>
62
+ <nav class="mint-file-browser__crumbs" aria-label="Breadcrumb">
63
+ <template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
64
+ <span v-if="index" class="mint-file-browser__crumb-sep" aria-hidden="true">/</span>
65
+ <BaseButton
66
+ variant="ghost"
67
+ size="sm"
68
+ class="mint-file-browser__crumb"
69
+ :class="{ 'mint-file-browser__crumb--current': index === breadcrumbs.length - 1 }"
70
+ :aria-current="index === breadcrumbs.length - 1 ? 'location' : undefined"
71
+ @click="handleCrumb(crumb)"
72
+ >{{ crumb.label }}</BaseButton
73
+ >
74
+ </template>
75
+ <span v-if="!breadcrumbs.length" class="mint-file-browser__selection-hint">{{
76
+ path || 'Choose a location'
77
+ }}</span>
78
+ </nav>
79
+ <IconButton
80
+ size="sm"
81
+ label="Refresh folder listing"
82
+ :disabled="loading"
83
+ @click="emit('refresh')"
84
+ >
85
+ <svg
86
+ viewBox="0 0 24 24"
87
+ fill="none"
88
+ stroke="currentColor"
89
+ stroke-width="2"
90
+ aria-hidden="true"
91
+ >
92
+ <path d="M20 7v5h-5M4 17v-5h5M6 8a7 7 0 0 1 12-2l2 3M4 15l2 3a7 7 0 0 0 12-2" />
93
+ </svg>
94
+ </IconButton>
95
+ </div>
96
+ <div class="mint-file-browser__tools">
97
+ <BaseInput
98
+ :model-value="search"
99
+ type="search"
100
+ size="sm"
101
+ class="mint-file-browser__search"
102
+ placeholder="Search this folder…"
103
+ aria-label="Search this folder"
104
+ @update:model-value="emit('update:search', String($event ?? ''))"
105
+ />
106
+ <span v-if="typeRuleLabel" class="mint-file-browser__rules" title="Matching file types">{{
107
+ typeRuleLabel
108
+ }}</span>
109
+ <IconButton
110
+ size="sm"
111
+ class="mint-file-browser__sort"
112
+ :label="`Sorted by ${sort}. Click to change.`"
113
+ @click="cycleSort"
114
+ >
115
+ <svg
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ stroke="currentColor"
119
+ stroke-width="2"
120
+ aria-hidden="true"
121
+ >
122
+ <path d="M4 6h10M4 12h16M4 18h7" />
123
+ </svg>
124
+ </IconButton>
125
+ <BaseButton
126
+ variant="secondary"
127
+ size="sm"
128
+ class="mint-file-browser__select-folder"
129
+ :title="`Select ${path || 'this folder'} itself as the input`"
130
+ :disabled="!mountId || loading || !!error"
131
+ @click="emit('selectFolder', { mount_id: mountId, path })"
132
+ >Select this folder</BaseButton
133
+ >
134
+ </div>
135
+ </div>
136
+ </template>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import BaseButton from '../BaseButton.vue'
3
+ import { FILE_BROWSER_ICONS as ICON, formatBytes } from '../fileBrowserPresentation'
4
+ import type { PickerNode, PickerExcluded, PickerSource } from '../../types/filePicker'
5
+ import { pickerReason } from '../../composables/filePicker/validation'
6
+
7
+ defineProps<{
8
+ totalCount?: number | null
9
+ node: PickerNode | null
10
+ source: PickerSource
11
+ exclusions: PickerExcluded[]
12
+ busy: boolean
13
+ error: string
14
+ reviewing: boolean
15
+ }>()
16
+ const emit = defineEmits<{ review: []; retry: [] }>()
17
+ </script>
18
+ <template>
19
+ <aside class="mint-picker__preview" aria-label="Selection preview">
20
+ <div class="mint-picker__preview-heading">{{ reviewing ? 'Skipped files' : 'Preview' }}</div>
21
+ <div v-if="reviewing" class="mint-picker__preview-content">
22
+ <BaseButton variant="ghost" size="sm" @click="emit('review')">Back to preview</BaseButton>
23
+ <div
24
+ v-for="item in exclusions"
25
+ :key="item.path"
26
+ class="mint-picker__exclusion"
27
+ :title="item.path"
28
+ >
29
+ <strong>{{ item.name }}</strong
30
+ ><span>{{ pickerReason(item) }}</span>
31
+ </div>
32
+ </div>
33
+ <div v-else-if="node" class="mint-picker__preview-content">
34
+ <div class="mint-picker__preview-icon">
35
+ <svg
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ stroke-width="1.7"
40
+ aria-hidden="true"
41
+ >
42
+ <path :d="node.kind === 'folder' ? ICON.folder : ICON.file" />
43
+ </svg>
44
+ </div>
45
+ <strong class="mint-picker__preview-name">{{ node.name }}</strong>
46
+ <dl class="mint-picker__metadata">
47
+ <div>
48
+ <dt>Location</dt>
49
+ <dd :title="node.parentName || node.path">{{ node.parentName || node.path }}</dd>
50
+ </div>
51
+ <div>
52
+ <dt>Size</dt>
53
+ <dd>{{ node.bytes == null ? 'Not available' : formatBytes(node.bytes) }}</dd>
54
+ </div>
55
+ <div v-if="node.kind === 'folder'">
56
+ <dt>Files</dt>
57
+ <dd>
58
+ {{ node.fileCount ?? 'Not counted yet'
59
+ }}<template v-if="totalCount != null && totalCount !== node.fileCount">
60
+ of {{ totalCount }}</template
61
+ >
62
+ </dd>
63
+ </div>
64
+ <div>
65
+ <dt>Format</dt>
66
+ <dd v-if="node.kind === 'file'">{{ node.format.toUpperCase() || 'Unknown' }}</dd>
67
+ <dd v-else>
68
+ <span v-for="(count, format) in node.formats" :key="format"
69
+ >{{ format.toUpperCase() }} ×{{ count }}<br /></span
70
+ ><span v-if="!node.formats">Not counted yet</span>
71
+ </dd>
72
+ </div>
73
+ <div>
74
+ <dt>Source</dt>
75
+ <dd>
76
+ {{ source === 'localFile' ? 'Local files' : 'Server' }}
77
+ </dd>
78
+ </div>
79
+ </dl>
80
+ <BaseButton
81
+ v-if="exclusions.length"
82
+ variant="ghost"
83
+ size="sm"
84
+ class="mint-picker__skipped"
85
+ @click="emit('review')"
86
+ >{{ exclusions.length }} skipped · Review</BaseButton
87
+ >
88
+ <div v-if="error" class="mint-picker__preview-error" role="alert">
89
+ {{ error
90
+ }}<BaseButton variant="secondary" size="sm" @click="emit('retry')">Try again</BaseButton>
91
+ </div>
92
+ <span v-else class="mint-picker__ready" role="status">{{
93
+ busy ? 'Checking selection…' : '✓ Metadata available'
94
+ }}</span>
95
+ </div>
96
+ <div v-else class="mint-picker__preview-empty">
97
+ Select a file or folder to preview its details.
98
+ </div>
99
+ </aside>
100
+ </template>