@brftech/filex-core 0.1.56 → 0.1.61

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 (82) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +108 -108
  3. package/dist/{ArchiveViewer-CJdaw5x3.js → ArchiveViewer-CbUUEREI.js} +20 -20
  4. package/dist/ArchiveViewer-CbUUEREI.js.map +1 -0
  5. package/dist/{CsvViewer-QYx_tePQ.js → CsvViewer-72rDsQJX.js} +3 -3
  6. package/dist/CsvViewer-72rDsQJX.js.map +1 -0
  7. package/dist/{DrawioViewer-GWt0TyB7.js → DrawioViewer-DSyMwGvg.js} +7 -7
  8. package/dist/DrawioViewer-DSyMwGvg.js.map +1 -0
  9. package/dist/{EpubViewer-Je0ywklQ.js → EpubViewer-DvDa17d7.js} +3 -3
  10. package/dist/EpubViewer-DvDa17d7.js.map +1 -0
  11. package/dist/{IpynbViewer-TsB4gqP-.js → IpynbViewer-DibIkdsz.js} +3 -3
  12. package/dist/IpynbViewer-DibIkdsz.js.map +1 -0
  13. package/dist/{MermaidViewer-CXfeDS-7.js → MermaidViewer-BoFLtGvl.js} +3 -3
  14. package/dist/MermaidViewer-BoFLtGvl.js.map +1 -0
  15. package/dist/{PsdViewer-VtPpxKVe.js → PsdViewer-K2iZkwax.js} +3 -3
  16. package/dist/PsdViewer-K2iZkwax.js.map +1 -0
  17. package/dist/{TiffViewer-OINn9bbb.js → TiffViewer-JzDzbqpO.js} +3 -3
  18. package/dist/TiffViewer-JzDzbqpO.js.map +1 -0
  19. package/dist/{Viewer3D-xvvyiWHd.js → Viewer3D-WEMYCP8l.js} +7 -7
  20. package/dist/Viewer3D-WEMYCP8l.js.map +1 -0
  21. package/dist/filex-core.js +1 -1
  22. package/dist/filex-core.umd.cjs +4 -4
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/{index-CmSYzt6L.js → index-B5YT-m5Q.js} +209 -209
  25. package/dist/index-B5YT-m5Q.js.map +1 -0
  26. package/dist/style.css +1 -1
  27. package/dist/useViewerFetch-czqbd2Lj.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/components/Breadcrumb.vue +282 -282
  30. package/src/components/ContextMenu.vue +152 -152
  31. package/src/components/GridView.vue +166 -166
  32. package/src/components/ListView.vue +212 -212
  33. package/src/components/PendingOpsTray.vue +91 -91
  34. package/src/components/RecentlyOpened.vue +155 -155
  35. package/src/components/StarButton.vue +101 -101
  36. package/src/components/TagPicker.vue +184 -184
  37. package/src/components/Toolbar.vue +220 -220
  38. package/src/components/UploadProgress.vue +78 -78
  39. package/src/composables/useFileApi.ts +692 -692
  40. package/src/composables/useKeyboardShortcuts.ts +144 -144
  41. package/src/composables/useLocale.ts +60 -60
  42. package/src/composables/useMonacoLoader.ts +98 -98
  43. package/src/composables/usePendingOps.ts +200 -200
  44. package/src/composables/useSelection.ts +81 -81
  45. package/src/composables/useUploadChunked.ts +250 -250
  46. package/src/composables/useViewerFetch.ts +58 -58
  47. package/src/index.ts +80 -80
  48. package/src/locales/en.ts +122 -122
  49. package/src/locales/index.ts +12 -12
  50. package/src/locales/tr.ts +122 -122
  51. package/src/modals/DeleteConfirmModal.vue +32 -32
  52. package/src/modals/Modal.vue +104 -104
  53. package/src/modals/NewFolderModal.vue +64 -64
  54. package/src/modals/PermissionsModal.vue +815 -815
  55. package/src/modals/PreviewModal.vue +1070 -1070
  56. package/src/modals/RenameModal.vue +58 -58
  57. package/src/modals/ShareModal.vue +135 -135
  58. package/src/styles/base.css +1224 -1224
  59. package/src/styles/variables.css +90 -90
  60. package/src/types/FileNode.ts +136 -136
  61. package/src/types/index.ts +27 -27
  62. package/src/types/peers.d.ts +45 -45
  63. package/src/viewers/ArchiveViewer.vue +186 -186
  64. package/src/viewers/CsvViewer.vue +319 -319
  65. package/src/viewers/DrawioViewer.vue +243 -243
  66. package/src/viewers/EpubViewer.vue +307 -307
  67. package/src/viewers/IpynbViewer.vue +365 -365
  68. package/src/viewers/MermaidViewer.vue +259 -259
  69. package/src/viewers/PdfViewer.vue +562 -562
  70. package/src/viewers/PsdViewer.vue +264 -264
  71. package/src/viewers/TiffViewer.vue +273 -273
  72. package/src/viewers/Viewer3D.vue +120 -120
  73. package/dist/ArchiveViewer-CJdaw5x3.js.map +0 -1
  74. package/dist/CsvViewer-QYx_tePQ.js.map +0 -1
  75. package/dist/DrawioViewer-GWt0TyB7.js.map +0 -1
  76. package/dist/EpubViewer-Je0ywklQ.js.map +0 -1
  77. package/dist/IpynbViewer-TsB4gqP-.js.map +0 -1
  78. package/dist/MermaidViewer-CXfeDS-7.js.map +0 -1
  79. package/dist/PsdViewer-VtPpxKVe.js.map +0 -1
  80. package/dist/TiffViewer-OINn9bbb.js.map +0 -1
  81. package/dist/Viewer3D-xvvyiWHd.js.map +0 -1
  82. package/dist/index-CmSYzt6L.js.map +0 -1
@@ -1,282 +1,282 @@
1
- <script setup lang="ts">
2
- /**
3
- * Breadcrumb — adapter-aware path crumbs.
4
- *
5
- * Dirname arrives as `<adapter>://<rel>` (e.g. `s3-test://aa/bb`). The
6
- * first crumb is ALWAYS the storage root — its label is the adapter
7
- * name itself (`s3-test`) and clicking it lands you at `<adapter>://`,
8
- * NOT at the first sub-folder you happened to be inside. The remaining
9
- * crumbs walk down the path one segment at a time.
10
- *
11
- * Examples:
12
- * `s3-test://` → [s3-test]
13
- * `s3-test://aa` → [s3-test] › [aa]
14
- * `s3-test://aa/bb` → [s3-test] › [aa] › [bb]
15
- *
16
- * `rootLabel` overrides the adapter name when the embedder wants a
17
- * branded label ('Files', 'My Drive', etc). Defaults to the adapter.
18
- *
19
- * The ✏ button swaps the crumbs for a free-form input; Enter
20
- * navigates, Escape cancels.
21
- */
22
- import { computed, nextTick, ref } from 'vue';
23
- import type { LocaleCode } from '../types/ExplorerConfig';
24
- import { useLocale } from '../composables/useLocale';
25
-
26
- const props = defineProps<{
27
- dirname: string;
28
- adapter: string;
29
- rootLabel: string;
30
- locale: LocaleCode;
31
- /**
32
- * Multi-storage mode — when true the breadcrumb prepends a global
33
- * "/" crumb so the user can pop all the way back to the storage
34
- * picker. Storage name then occupies crumb #1 instead of #0.
35
- */
36
- multiStorageRoot?: boolean;
37
- /**
38
- * Root confinement (qualified `<adapter>://<rel>`). When set the breadcrumb
39
- * treats this folder as `/`: crumbs above it are hidden, the root crumb is
40
- * the confined folder's own name, and the ✏ path editor shows/accepts paths
41
- * relative to it (`/`, `/sub`). Pairs with FileExplorer's rootPath clamp.
42
- */
43
- rootPath?: string;
44
- }>();
45
-
46
- // Confined root parsed into its adapter-relative form ("projeler/acme") and
47
- // the trailing folder name used as the "/" crumb label.
48
- const confinedRel = computed(() => {
49
- if (!props.rootPath) return '';
50
- const r = props.rootPath;
51
- const i = r.indexOf('://');
52
- return (i >= 0 ? r.slice(i + 3) : r).replace(/^\/+|\/+$/g, '');
53
- });
54
- const floorLabel = computed(() => {
55
- const rel = confinedRel.value;
56
- if (!rel) return props.rootLabel || props.adapter;
57
- const parts = rel.split('/');
58
- return parts[parts.length - 1] || (props.rootLabel || props.adapter);
59
- });
60
-
61
- const { t } = useLocale(() => props.locale);
62
-
63
- const emit = defineEmits<{
64
- (e: 'navigate', adapterPath: string): void;
65
- (e: 'copy-path', adapterPath: string): void;
66
- (e: 'crumb-context', payload: { x: number; y: number; adapterPath: string; label: string }): void;
67
- (e: 'crumb-drop', adapterPath: string, ev: DragEvent): void;
68
- }>();
69
-
70
- interface Crumb {
71
- label: string;
72
- adapterPath: string;
73
- }
74
-
75
- const crumbs = computed<Crumb[]>(() => {
76
- const adapterPrefix = `${props.adapter}://`;
77
- const raw = props.dirname.startsWith(adapterPrefix)
78
- ? props.dirname.slice(adapterPrefix.length)
79
- : props.dirname;
80
- const parts = raw.split('/').filter(Boolean);
81
-
82
- const out: Crumb[] = [];
83
-
84
- // Confined: the root folder is the top crumb (its own name), and only the
85
- // path BELOW it is walked — segments above the confinement are never shown.
86
- if (props.rootPath) {
87
- const floor = confinedRel.value;
88
- const cur = raw.replace(/^\/+|\/+$/g, '');
89
- const below = floor && (cur === floor || cur.startsWith(floor + '/'))
90
- ? cur.slice(floor.length).replace(/^\/+/, '')
91
- : '';
92
- out.push({ label: floorLabel.value, adapterPath: props.rootPath });
93
- let acc = floor;
94
- for (const part of below.split('/').filter(Boolean)) {
95
- acc = acc ? `${acc}/${part}` : part;
96
- out.push({ label: part === '.trash' ? t('node.trash') : part, adapterPath: `${adapterPrefix}${acc}` });
97
- }
98
- return out;
99
- }
100
-
101
- if (props.multiStorageRoot) {
102
- // Global "/" crumb — clicking it pops back to the storage picker
103
- // (FileExplorer treats the empty string as the virtual root).
104
- out.push({ label: '/', adapterPath: '' });
105
- if (props.adapter) {
106
- // Storage segment — wire form `<adapter>://` lands at storage
107
- // root regardless of where we are now.
108
- out.push({ label: props.rootLabel || props.adapter, adapterPath: adapterPrefix });
109
- }
110
- } else {
111
- // Single-storage mode: the storage name IS the root, no "/"
112
- // above it.
113
- out.push({
114
- label: props.rootLabel || props.adapter,
115
- adapterPath: adapterPrefix,
116
- });
117
- }
118
-
119
- let acc = '';
120
- for (const part of parts) {
121
- acc = acc ? `${acc}/${part}` : part;
122
- const label = part === '.trash' ? t('node.trash') : part;
123
- out.push({ label, adapterPath: `${adapterPrefix}${acc}` });
124
- }
125
- return out;
126
- });
127
-
128
- function onClick(crumb: Crumb) {
129
- emit('navigate', crumb.adapterPath);
130
- }
131
-
132
- function onContext(ev: MouseEvent, crumb: Crumb) {
133
- ev.preventDefault();
134
- ev.stopPropagation();
135
- emit('crumb-context', {
136
- x: ev.clientX,
137
- y: ev.clientY,
138
- adapterPath: crumb.adapterPath,
139
- label: crumb.label,
140
- });
141
- }
142
-
143
- function onCrumbDragOver(ev: DragEvent) {
144
- if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
145
- ev.preventDefault();
146
- ev.stopPropagation();
147
- if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
148
- }
149
-
150
- function onCrumbDrop(ev: DragEvent, crumb: Crumb) {
151
- if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
152
- ev.preventDefault();
153
- ev.stopPropagation();
154
- emit('crumb-drop', crumb.adapterPath, ev);
155
- }
156
-
157
- const editing = ref(false);
158
- const pathDraft = ref('');
159
- const pathInput = ref<HTMLInputElement | null>(null);
160
-
161
- function startEdit() {
162
- const adapterPrefix = `${props.adapter}://`;
163
- const rel = props.dirname.startsWith(adapterPrefix)
164
- ? props.dirname.slice(adapterPrefix.length)
165
- : props.dirname;
166
- // Confined: edit relative to the root folder — `/` is the root, `/sub` below.
167
- if (props.rootPath) {
168
- const floor = confinedRel.value;
169
- const cur = rel.replace(/^\/+|\/+$/g, '');
170
- const below = floor && (cur === floor || cur.startsWith(floor + '/'))
171
- ? cur.slice(floor.length).replace(/^\/+/, '')
172
- : '';
173
- pathDraft.value = below ? `/${below}` : '/';
174
- editing.value = true;
175
- void nextTick(() => {
176
- pathInput.value?.focus();
177
- pathInput.value?.select();
178
- });
179
- return;
180
- }
181
- // Multi-storage: show `/<storage>/<rel>` so the user can edit the
182
- // whole tree (`/main/foo`, `/s3-test/example`, just `/` for root).
183
- if (props.multiStorageRoot) {
184
- if (!props.adapter) {
185
- pathDraft.value = '/';
186
- } else {
187
- const trimmed = rel.replace(/^\/+|\/+$/g, '');
188
- pathDraft.value = trimmed
189
- ? `/${props.adapter}/${trimmed}`
190
- : `/${props.adapter}`;
191
- }
192
- } else {
193
- pathDraft.value = rel;
194
- }
195
- editing.value = true;
196
- void nextTick(() => {
197
- pathInput.value?.focus();
198
- pathInput.value?.select();
199
- });
200
- }
201
-
202
- function submitPath() {
203
- const raw = pathDraft.value.trim();
204
- editing.value = false;
205
-
206
- // Confined: the typed path is relative to the root folder; prepend the floor.
207
- if (props.rootPath) {
208
- const sub = raw.replace(/^\/+|\/+$/g, '');
209
- const floor = confinedRel.value;
210
- const rel = sub ? (floor ? `${floor}/${sub}` : sub) : floor;
211
- emit('navigate', rel ? `${props.adapter}://${rel}` : `${props.adapter}://`);
212
- return;
213
- }
214
-
215
- if (props.multiStorageRoot) {
216
- // Strip leading/trailing slashes. First segment = storage.
217
- const clean = raw.replace(/^\/+|\/+$/g, '');
218
- if (!clean) {
219
- // navigate to global root by emitting empty wire path; the
220
- // FileExplorer treats it as virtual storage list.
221
- emit('navigate', '');
222
- return;
223
- }
224
- const slash = clean.indexOf('/');
225
- const adapter = slash === -1 ? clean : clean.slice(0, slash);
226
- const rel = slash === -1 ? '' : clean.slice(slash + 1);
227
- emit('navigate', rel ? `${adapter}://${rel}` : `${adapter}://`);
228
- return;
229
- }
230
-
231
- const v = raw.replace(/^\/+|\/+$/g, '');
232
- if (!v) return;
233
- emit('navigate', `${props.adapter}://${v}`);
234
- }
235
-
236
- function cancelEdit() {
237
- editing.value = false;
238
- }
239
- </script>
240
-
241
- <template>
242
- <nav class="fe-breadcrumb" aria-label="Breadcrumb">
243
- <template v-if="!editing">
244
- <button
245
- v-for="(c, i) in crumbs"
246
- :key="c.adapterPath"
247
- class="fe-breadcrumb__crumb"
248
- :class="{ 'is-last': i === crumbs.length - 1 }"
249
- type="button"
250
- :aria-current="i === crumbs.length - 1 ? 'page' : undefined"
251
- @click="onClick(c)"
252
- @contextmenu="onContext($event, c)"
253
- @dragover="onCrumbDragOver"
254
- @drop="onCrumbDrop($event, c)"
255
- >
256
- <span>{{ c.label }}</span>
257
- <span v-if="i < crumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">›</span>
258
- </button>
259
- <button
260
- type="button"
261
- class="fe-breadcrumb__edit"
262
- :title="t('breadcrumb.go_to_path')"
263
- :aria-label="t('breadcrumb.go_to_path')"
264
- @click="startEdit"
265
- >✏</button>
266
- </template>
267
- <template v-else>
268
- <input
269
- ref="pathInput"
270
- v-model="pathDraft"
271
- type="text"
272
- class="fe-breadcrumb__input"
273
- :placeholder="t('breadcrumb.path_placeholder')"
274
- spellcheck="false"
275
- autocomplete="off"
276
- @keydown.enter.prevent="submitPath"
277
- @keydown.escape.prevent="cancelEdit"
278
- @blur="cancelEdit"
279
- />
280
- </template>
281
- </nav>
282
- </template>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Breadcrumb — adapter-aware path crumbs.
4
+ *
5
+ * Dirname arrives as `<adapter>://<rel>` (e.g. `s3-test://aa/bb`). The
6
+ * first crumb is ALWAYS the storage root — its label is the adapter
7
+ * name itself (`s3-test`) and clicking it lands you at `<adapter>://`,
8
+ * NOT at the first sub-folder you happened to be inside. The remaining
9
+ * crumbs walk down the path one segment at a time.
10
+ *
11
+ * Examples:
12
+ * `s3-test://` → [s3-test]
13
+ * `s3-test://aa` → [s3-test] › [aa]
14
+ * `s3-test://aa/bb` → [s3-test] › [aa] › [bb]
15
+ *
16
+ * `rootLabel` overrides the adapter name when the embedder wants a
17
+ * branded label ('Files', 'My Drive', etc). Defaults to the adapter.
18
+ *
19
+ * The ✏ button swaps the crumbs for a free-form input; Enter
20
+ * navigates, Escape cancels.
21
+ */
22
+ import { computed, nextTick, ref } from 'vue';
23
+ import type { LocaleCode } from '../types/ExplorerConfig';
24
+ import { useLocale } from '../composables/useLocale';
25
+
26
+ const props = defineProps<{
27
+ dirname: string;
28
+ adapter: string;
29
+ rootLabel: string;
30
+ locale: LocaleCode;
31
+ /**
32
+ * Multi-storage mode — when true the breadcrumb prepends a global
33
+ * "/" crumb so the user can pop all the way back to the storage
34
+ * picker. Storage name then occupies crumb #1 instead of #0.
35
+ */
36
+ multiStorageRoot?: boolean;
37
+ /**
38
+ * Root confinement (qualified `<adapter>://<rel>`). When set the breadcrumb
39
+ * treats this folder as `/`: crumbs above it are hidden, the root crumb is
40
+ * the confined folder's own name, and the ✏ path editor shows/accepts paths
41
+ * relative to it (`/`, `/sub`). Pairs with FileExplorer's rootPath clamp.
42
+ */
43
+ rootPath?: string;
44
+ }>();
45
+
46
+ // Confined root parsed into its adapter-relative form ("projeler/acme") and
47
+ // the trailing folder name used as the "/" crumb label.
48
+ const confinedRel = computed(() => {
49
+ if (!props.rootPath) return '';
50
+ const r = props.rootPath;
51
+ const i = r.indexOf('://');
52
+ return (i >= 0 ? r.slice(i + 3) : r).replace(/^\/+|\/+$/g, '');
53
+ });
54
+ const floorLabel = computed(() => {
55
+ const rel = confinedRel.value;
56
+ if (!rel) return props.rootLabel || props.adapter;
57
+ const parts = rel.split('/');
58
+ return parts[parts.length - 1] || (props.rootLabel || props.adapter);
59
+ });
60
+
61
+ const { t } = useLocale(() => props.locale);
62
+
63
+ const emit = defineEmits<{
64
+ (e: 'navigate', adapterPath: string): void;
65
+ (e: 'copy-path', adapterPath: string): void;
66
+ (e: 'crumb-context', payload: { x: number; y: number; adapterPath: string; label: string }): void;
67
+ (e: 'crumb-drop', adapterPath: string, ev: DragEvent): void;
68
+ }>();
69
+
70
+ interface Crumb {
71
+ label: string;
72
+ adapterPath: string;
73
+ }
74
+
75
+ const crumbs = computed<Crumb[]>(() => {
76
+ const adapterPrefix = `${props.adapter}://`;
77
+ const raw = props.dirname.startsWith(adapterPrefix)
78
+ ? props.dirname.slice(adapterPrefix.length)
79
+ : props.dirname;
80
+ const parts = raw.split('/').filter(Boolean);
81
+
82
+ const out: Crumb[] = [];
83
+
84
+ // Confined: the root folder is the top crumb (its own name), and only the
85
+ // path BELOW it is walked — segments above the confinement are never shown.
86
+ if (props.rootPath) {
87
+ const floor = confinedRel.value;
88
+ const cur = raw.replace(/^\/+|\/+$/g, '');
89
+ const below = floor && (cur === floor || cur.startsWith(floor + '/'))
90
+ ? cur.slice(floor.length).replace(/^\/+/, '')
91
+ : '';
92
+ out.push({ label: floorLabel.value, adapterPath: props.rootPath });
93
+ let acc = floor;
94
+ for (const part of below.split('/').filter(Boolean)) {
95
+ acc = acc ? `${acc}/${part}` : part;
96
+ out.push({ label: part === '.trash' ? t('node.trash') : part, adapterPath: `${adapterPrefix}${acc}` });
97
+ }
98
+ return out;
99
+ }
100
+
101
+ if (props.multiStorageRoot) {
102
+ // Global "/" crumb — clicking it pops back to the storage picker
103
+ // (FileExplorer treats the empty string as the virtual root).
104
+ out.push({ label: '/', adapterPath: '' });
105
+ if (props.adapter) {
106
+ // Storage segment — wire form `<adapter>://` lands at storage
107
+ // root regardless of where we are now.
108
+ out.push({ label: props.rootLabel || props.adapter, adapterPath: adapterPrefix });
109
+ }
110
+ } else {
111
+ // Single-storage mode: the storage name IS the root, no "/"
112
+ // above it.
113
+ out.push({
114
+ label: props.rootLabel || props.adapter,
115
+ adapterPath: adapterPrefix,
116
+ });
117
+ }
118
+
119
+ let acc = '';
120
+ for (const part of parts) {
121
+ acc = acc ? `${acc}/${part}` : part;
122
+ const label = part === '.trash' ? t('node.trash') : part;
123
+ out.push({ label, adapterPath: `${adapterPrefix}${acc}` });
124
+ }
125
+ return out;
126
+ });
127
+
128
+ function onClick(crumb: Crumb) {
129
+ emit('navigate', crumb.adapterPath);
130
+ }
131
+
132
+ function onContext(ev: MouseEvent, crumb: Crumb) {
133
+ ev.preventDefault();
134
+ ev.stopPropagation();
135
+ emit('crumb-context', {
136
+ x: ev.clientX,
137
+ y: ev.clientY,
138
+ adapterPath: crumb.adapterPath,
139
+ label: crumb.label,
140
+ });
141
+ }
142
+
143
+ function onCrumbDragOver(ev: DragEvent) {
144
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
145
+ ev.preventDefault();
146
+ ev.stopPropagation();
147
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
148
+ }
149
+
150
+ function onCrumbDrop(ev: DragEvent, crumb: Crumb) {
151
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
152
+ ev.preventDefault();
153
+ ev.stopPropagation();
154
+ emit('crumb-drop', crumb.adapterPath, ev);
155
+ }
156
+
157
+ const editing = ref(false);
158
+ const pathDraft = ref('');
159
+ const pathInput = ref<HTMLInputElement | null>(null);
160
+
161
+ function startEdit() {
162
+ const adapterPrefix = `${props.adapter}://`;
163
+ const rel = props.dirname.startsWith(adapterPrefix)
164
+ ? props.dirname.slice(adapterPrefix.length)
165
+ : props.dirname;
166
+ // Confined: edit relative to the root folder — `/` is the root, `/sub` below.
167
+ if (props.rootPath) {
168
+ const floor = confinedRel.value;
169
+ const cur = rel.replace(/^\/+|\/+$/g, '');
170
+ const below = floor && (cur === floor || cur.startsWith(floor + '/'))
171
+ ? cur.slice(floor.length).replace(/^\/+/, '')
172
+ : '';
173
+ pathDraft.value = below ? `/${below}` : '/';
174
+ editing.value = true;
175
+ void nextTick(() => {
176
+ pathInput.value?.focus();
177
+ pathInput.value?.select();
178
+ });
179
+ return;
180
+ }
181
+ // Multi-storage: show `/<storage>/<rel>` so the user can edit the
182
+ // whole tree (`/main/foo`, `/s3-test/example`, just `/` for root).
183
+ if (props.multiStorageRoot) {
184
+ if (!props.adapter) {
185
+ pathDraft.value = '/';
186
+ } else {
187
+ const trimmed = rel.replace(/^\/+|\/+$/g, '');
188
+ pathDraft.value = trimmed
189
+ ? `/${props.adapter}/${trimmed}`
190
+ : `/${props.adapter}`;
191
+ }
192
+ } else {
193
+ pathDraft.value = rel;
194
+ }
195
+ editing.value = true;
196
+ void nextTick(() => {
197
+ pathInput.value?.focus();
198
+ pathInput.value?.select();
199
+ });
200
+ }
201
+
202
+ function submitPath() {
203
+ const raw = pathDraft.value.trim();
204
+ editing.value = false;
205
+
206
+ // Confined: the typed path is relative to the root folder; prepend the floor.
207
+ if (props.rootPath) {
208
+ const sub = raw.replace(/^\/+|\/+$/g, '');
209
+ const floor = confinedRel.value;
210
+ const rel = sub ? (floor ? `${floor}/${sub}` : sub) : floor;
211
+ emit('navigate', rel ? `${props.adapter}://${rel}` : `${props.adapter}://`);
212
+ return;
213
+ }
214
+
215
+ if (props.multiStorageRoot) {
216
+ // Strip leading/trailing slashes. First segment = storage.
217
+ const clean = raw.replace(/^\/+|\/+$/g, '');
218
+ if (!clean) {
219
+ // navigate to global root by emitting empty wire path; the
220
+ // FileExplorer treats it as virtual storage list.
221
+ emit('navigate', '');
222
+ return;
223
+ }
224
+ const slash = clean.indexOf('/');
225
+ const adapter = slash === -1 ? clean : clean.slice(0, slash);
226
+ const rel = slash === -1 ? '' : clean.slice(slash + 1);
227
+ emit('navigate', rel ? `${adapter}://${rel}` : `${adapter}://`);
228
+ return;
229
+ }
230
+
231
+ const v = raw.replace(/^\/+|\/+$/g, '');
232
+ if (!v) return;
233
+ emit('navigate', `${props.adapter}://${v}`);
234
+ }
235
+
236
+ function cancelEdit() {
237
+ editing.value = false;
238
+ }
239
+ </script>
240
+
241
+ <template>
242
+ <nav class="fe-breadcrumb" aria-label="Breadcrumb">
243
+ <template v-if="!editing">
244
+ <button
245
+ v-for="(c, i) in crumbs"
246
+ :key="c.adapterPath"
247
+ class="fe-breadcrumb__crumb"
248
+ :class="{ 'is-last': i === crumbs.length - 1 }"
249
+ type="button"
250
+ :aria-current="i === crumbs.length - 1 ? 'page' : undefined"
251
+ @click="onClick(c)"
252
+ @contextmenu="onContext($event, c)"
253
+ @dragover="onCrumbDragOver"
254
+ @drop="onCrumbDrop($event, c)"
255
+ >
256
+ <span>{{ c.label }}</span>
257
+ <span v-if="i < crumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">›</span>
258
+ </button>
259
+ <button
260
+ type="button"
261
+ class="fe-breadcrumb__edit"
262
+ :title="t('breadcrumb.go_to_path')"
263
+ :aria-label="t('breadcrumb.go_to_path')"
264
+ @click="startEdit"
265
+ >✏</button>
266
+ </template>
267
+ <template v-else>
268
+ <input
269
+ ref="pathInput"
270
+ v-model="pathDraft"
271
+ type="text"
272
+ class="fe-breadcrumb__input"
273
+ :placeholder="t('breadcrumb.path_placeholder')"
274
+ spellcheck="false"
275
+ autocomplete="off"
276
+ @keydown.enter.prevent="submitPath"
277
+ @keydown.escape.prevent="cancelEdit"
278
+ @blur="cancelEdit"
279
+ />
280
+ </template>
281
+ </nav>
282
+ </template>