@brftech/filex-core 0.40.0 β 0.41.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.
- package/README.md +32 -25
- package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
- package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
- package/dist/CsvViewer-CyuwENI_.js +141 -0
- package/dist/CsvViewer-CyuwENI_.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js β DrawioViewer-BIYsTX97.js} +48 -42
- package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
- package/dist/EpubViewer-K9PjCMmS.js +163 -0
- package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
- package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
- package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
- package/dist/MermaidViewer-Bp73aId8.js +137 -0
- package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
- package/dist/PsdViewer-C2eLivw1.js +122 -0
- package/dist/PsdViewer-C2eLivw1.js.map +1 -0
- package/dist/TiffViewer-DsHg5gcz.js +142 -0
- package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
- package/dist/Viewer3D-C_dc2HN9.js +75 -0
- package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
- package/dist/filex-core.js +218 -20361
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-vxMZpYb-.js +26700 -0
- package/dist/index-vxMZpYb-.js.map +1 -0
- package/dist/index.d.ts +4212 -862
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2154 -506
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +39 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +951 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +172 -56
- package/src/components/GridView.vue +331 -82
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1173 -194
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +26 -1
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +6 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +135 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +35 -14
- package/src/components/Toolbar.vue +953 -465
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +89 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useLocale.ts +382 -26
- package/src/composables/useRowTouch.ts +95 -0
- package/src/index.ts +177 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +31 -2
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +151 -116
- package/src/lib/timezone.ts +501 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +416 -26
- package/src/locales/tr.ts +409 -26
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +557 -476
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5409 -8
- package/src/styles/variables.css +139 -65
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
|
@@ -24,6 +24,7 @@ import { hasInternalDrag } from '../lib/dragOut';
|
|
|
24
24
|
import { virtualSegmentLabel } from '../lib/listing';
|
|
25
25
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
26
26
|
import { useLocale } from '../composables/useLocale';
|
|
27
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
27
28
|
|
|
28
29
|
const props = defineProps<{
|
|
29
30
|
dirname: string;
|
|
@@ -43,6 +44,17 @@ const props = defineProps<{
|
|
|
43
44
|
* relative to it (`/`, `/sub`). Pairs with FileExplorer's rootPath clamp.
|
|
44
45
|
*/
|
|
45
46
|
rootPath?: string;
|
|
47
|
+
/**
|
|
48
|
+
* gorunum:v2-topbar β what is INSIDE the folder the last crumb names, for
|
|
49
|
+
* the chevron the reference shell puts there ("Subfolders"). Supplied by
|
|
50
|
+
* the parent, because the listing it comes from is the parent's: a second
|
|
51
|
+
* fetch from here would be a second answer to "what is in this folder".
|
|
52
|
+
*
|
|
53
|
+
* β The affordance is drawn whenever the array is PRESENT, empty or not β
|
|
54
|
+
* a control that appears and disappears as you walk the tree is a control
|
|
55
|
+
* nobody learns. An empty folder says so in the menu instead.
|
|
56
|
+
*/
|
|
57
|
+
subfolders?: { label: string; adapterPath: string }[];
|
|
46
58
|
}>();
|
|
47
59
|
|
|
48
60
|
// Confined root parsed into its adapter-relative form ("projeler/acme") and
|
|
@@ -76,8 +88,13 @@ interface Crumb {
|
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
const crumbs = computed<Crumb[]>(() => {
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
/* β Empty adapter β EMPTY prefix, not a bare `://`. A virtual view
|
|
92
|
+
(`.starred`, `.tag~invoices`) belongs to no storage, so the pane hands us
|
|
93
|
+
the sentinel with no adapter at all; gluing `://` onto it would make every
|
|
94
|
+
crumb's `adapterPath` read `://.starred`, which navigates nowhere β the
|
|
95
|
+
view's own address is the bare sentinel. */
|
|
96
|
+
const adapterPrefix = props.adapter ? `${props.adapter}://` : '';
|
|
97
|
+
const raw = adapterPrefix && props.dirname.startsWith(adapterPrefix)
|
|
81
98
|
? props.dirname.slice(adapterPrefix.length)
|
|
82
99
|
: props.dirname;
|
|
83
100
|
const parts = raw.split('/').filter(Boolean);
|
|
@@ -96,7 +113,14 @@ const crumbs = computed<Crumb[]>(() => {
|
|
|
96
113
|
let acc = floor;
|
|
97
114
|
for (const part of below.split('/').filter(Boolean)) {
|
|
98
115
|
acc = acc ? `${acc}/${part}` : part;
|
|
99
|
-
|
|
116
|
+
/* β THE SAME resolver the unconfined branch below uses. This line used to
|
|
117
|
+
carry a private `part === '.trash' ? t('node.trash') : part` β a second
|
|
118
|
+
copy of the sentinel mapping that knew about exactly one sentinel, so a
|
|
119
|
+
confined embed printed `.starred`, `.shared`, `.recent` and
|
|
120
|
+
`.tag~invoices` raw while the panel three inches away named them
|
|
121
|
+
properly. One resolver, and a sentinel added to `lib/listing` reaches
|
|
122
|
+
both branches on the day it lands. */
|
|
123
|
+
out.push({ label: virtualSegmentLabel(part, t) || part, adapterPath: `${adapterPrefix}${acc}` });
|
|
100
124
|
}
|
|
101
125
|
return out;
|
|
102
126
|
}
|
|
@@ -110,9 +134,12 @@ const crumbs = computed<Crumb[]>(() => {
|
|
|
110
134
|
// root regardless of where we are now.
|
|
111
135
|
out.push({ label: props.rootLabel || props.adapter, adapterPath: adapterPrefix });
|
|
112
136
|
}
|
|
113
|
-
} else {
|
|
137
|
+
} else if (props.adapter || props.rootLabel) {
|
|
114
138
|
// Single-storage mode: the storage name IS the root, no "/"
|
|
115
139
|
// above it.
|
|
140
|
+
// β β¦and with NEITHER a storage nor a branded label there is nothing to
|
|
141
|
+
// name: that is a virtual view in a single-storage explorer, where the
|
|
142
|
+
// crumb would otherwise be an empty, clickable box in front of "Starred".
|
|
116
143
|
out.push({
|
|
117
144
|
label: props.rootLabel || props.adapter,
|
|
118
145
|
adapterPath: adapterPrefix,
|
|
@@ -139,40 +166,83 @@ const crumbs = computed<Crumb[]>(() => {
|
|
|
139
166
|
// into a dropdown. Visible crumbs keep their full click/drag-drop/context
|
|
140
167
|
// behavior; dropdown entries only navigate.
|
|
141
168
|
// ------------------------------------------------------------------
|
|
169
|
+
/* gorunum:v2-topbar β the home crumb.
|
|
170
|
+
*
|
|
171
|
+
* β It REPLACES the root crumb, it is not added beside it. In multi-storage
|
|
172
|
+
* mode `crumbs[0]` is a bare "/" whose only job is "back to the storage
|
|
173
|
+
* list" β exactly what a house glyph says, and better than a slash nobody
|
|
174
|
+
* reads as a button. Drawing both would be two controls for one destination,
|
|
175
|
+
* which is the duplicate this pass exists to remove.
|
|
176
|
+
* β Only that "/" crumb is swallowed. A single-storage explorer's first crumb
|
|
177
|
+
* is the storage's NAME and a confined one's is the folder's β information a
|
|
178
|
+
* tooltip on a house cannot carry, so those keep their text. */
|
|
179
|
+
const homeCrumb = computed<Crumb | null>(() => {
|
|
180
|
+
const first = crumbs.value[0];
|
|
181
|
+
if (!first) return null;
|
|
182
|
+
return props.multiStorageRoot && first.adapterPath === '' ? first : null;
|
|
183
|
+
});
|
|
184
|
+
/** The crumbs drawn as text β everything the home button did not swallow. */
|
|
185
|
+
const trail = computed<Crumb[]>(() =>
|
|
186
|
+
homeCrumb.value ? crumbs.value.slice(1) : crumbs.value,
|
|
187
|
+
);
|
|
188
|
+
/** True while the home button is the current folder (nothing below it). */
|
|
189
|
+
const homeIsCurrent = computed(() => !!homeCrumb.value && trail.value.length === 0);
|
|
190
|
+
|
|
142
191
|
const OVERFLOW_THRESHOLD = 4;
|
|
143
|
-
const collapsed = computed(() =>
|
|
192
|
+
const collapsed = computed(() => trail.value.length > OVERFLOW_THRESHOLD);
|
|
144
193
|
const leadCrumbs = computed<Crumb[]>(() =>
|
|
145
|
-
collapsed.value ?
|
|
194
|
+
collapsed.value ? trail.value.slice(0, 1) : trail.value,
|
|
146
195
|
);
|
|
147
196
|
const middleCrumbs = computed<Crumb[]>(() =>
|
|
148
|
-
collapsed.value ?
|
|
197
|
+
collapsed.value ? trail.value.slice(1, -2) : [],
|
|
149
198
|
);
|
|
150
199
|
const tailCrumbs = computed<Crumb[]>(() =>
|
|
151
|
-
collapsed.value ?
|
|
200
|
+
collapsed.value ? trail.value.slice(-2) : [],
|
|
152
201
|
);
|
|
153
202
|
|
|
154
203
|
const overflowOpen = ref(false);
|
|
155
204
|
const moreWrapEl = ref<HTMLElement | null>(null);
|
|
205
|
+
/* gorunum:v2-topbar β the "Subfolders" chevron's own menu. Separate state
|
|
206
|
+
from the overflow "β¦", because the two open different lists (crumbs ABOVE
|
|
207
|
+
this folder vs folders INSIDE it) and only ever one at a time. */
|
|
208
|
+
const subOpen = ref(false);
|
|
209
|
+
const subWrapEl = ref<HTMLElement | null>(null);
|
|
156
210
|
|
|
157
|
-
|
|
211
|
+
const anyMenuOpen = computed(() => overflowOpen.value || subOpen.value);
|
|
212
|
+
|
|
213
|
+
function closeMenus() {
|
|
158
214
|
overflowOpen.value = false;
|
|
215
|
+
subOpen.value = false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function onOverflowPick(crumb: Crumb) {
|
|
219
|
+
closeMenus();
|
|
159
220
|
emit('navigate', crumb.adapterPath);
|
|
160
221
|
}
|
|
161
222
|
|
|
162
|
-
function
|
|
163
|
-
const wrap = moreWrapEl.value;
|
|
164
|
-
if (wrap && e.target instanceof Node && wrap.contains(e.target)) return;
|
|
223
|
+
function toggleSub() {
|
|
165
224
|
overflowOpen.value = false;
|
|
225
|
+
subOpen.value = !subOpen.value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function onDocClick(e: MouseEvent) {
|
|
229
|
+
const t = e.target;
|
|
230
|
+
if (!(t instanceof Node)) {
|
|
231
|
+
closeMenus();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (moreWrapEl.value?.contains(t) || subWrapEl.value?.contains(t)) return;
|
|
235
|
+
closeMenus();
|
|
166
236
|
}
|
|
167
237
|
|
|
168
238
|
function onDocKeydown(e: KeyboardEvent) {
|
|
169
239
|
if (e.key === 'Escape') {
|
|
170
240
|
e.stopPropagation();
|
|
171
|
-
|
|
241
|
+
closeMenus();
|
|
172
242
|
}
|
|
173
243
|
}
|
|
174
244
|
|
|
175
|
-
watch(
|
|
245
|
+
watch(anyMenuOpen, (v) => {
|
|
176
246
|
if (v) {
|
|
177
247
|
document.addEventListener('click', onDocClick, true);
|
|
178
248
|
document.addEventListener('keydown', onDocKeydown, true);
|
|
@@ -182,10 +252,8 @@ watch(overflowOpen, (v) => {
|
|
|
182
252
|
}
|
|
183
253
|
});
|
|
184
254
|
// Navigation rebuilds the trail β a stale open menu would list crumbs of
|
|
185
|
-
// the previous folder.
|
|
186
|
-
watch(crumbs,
|
|
187
|
-
overflowOpen.value = false;
|
|
188
|
-
});
|
|
255
|
+
// the previous folder (and subfolders of the one we just left).
|
|
256
|
+
watch(crumbs, closeMenus);
|
|
189
257
|
onBeforeUnmount(() => {
|
|
190
258
|
document.removeEventListener('click', onDocClick, true);
|
|
191
259
|
document.removeEventListener('keydown', onDocKeydown, true);
|
|
@@ -195,6 +263,19 @@ function onClick(crumb: Crumb) {
|
|
|
195
263
|
emit('navigate', crumb.adapterPath);
|
|
196
264
|
}
|
|
197
265
|
|
|
266
|
+
/* The home button's three handlers, null-guarded here rather than in the
|
|
267
|
+
* template: `v-if="homeCrumb"` does not narrow the ref inside an event
|
|
268
|
+
* binding, and a `!` in markup is an assertion no reader can check. */
|
|
269
|
+
function onHomeClick() {
|
|
270
|
+
if (homeCrumb.value) onClick(homeCrumb.value);
|
|
271
|
+
}
|
|
272
|
+
function onHomeContext(ev: MouseEvent) {
|
|
273
|
+
if (homeCrumb.value) onContext(ev, homeCrumb.value);
|
|
274
|
+
}
|
|
275
|
+
function onHomeDrop(ev: DragEvent) {
|
|
276
|
+
if (homeCrumb.value) onCrumbDrop(ev, homeCrumb.value);
|
|
277
|
+
}
|
|
278
|
+
|
|
198
279
|
function onContext(ev: MouseEvent, crumb: Crumb) {
|
|
199
280
|
ev.preventDefault();
|
|
200
281
|
ev.stopPropagation();
|
|
@@ -307,6 +388,53 @@ function cancelEdit() {
|
|
|
307
388
|
<template>
|
|
308
389
|
<nav class="fe-breadcrumb" aria-label="Breadcrumb">
|
|
309
390
|
<template v-if="!editing">
|
|
391
|
+
<!-- gorunum:v2-topbar β the root crumb. It IS `crumbs[0]` (see the
|
|
392
|
+
script), drawn as a house instead of a "/" nobody reads as a
|
|
393
|
+
button; it is never an extra control beside the root.
|
|
394
|
+
|
|
395
|
+
β β It is named `breadcrumb.root`, NOT `breadcrumb.home`, and that
|
|
396
|
+
was a real collision rather than a synonym. This button goes to
|
|
397
|
+
`adapterPath: ''` β the top of the tree: the DRIVES listing where
|
|
398
|
+
several storages are visible, the storage root where one is. The
|
|
399
|
+
panel's own "Home" row goes somewhere else entirely (the overview
|
|
400
|
+
with Storages Β· Recent Β· Starred). Two controls, one word, two
|
|
401
|
+
destinations β and the word belonged to the other one. It mattered
|
|
402
|
+
more from 2026-09-13, when "My files" stopped being drawn for a
|
|
403
|
+
multi-storage deployment: this button became the only door to that
|
|
404
|
+
listing, so the one thing it must not do is point at a different
|
|
405
|
+
row in the panel.
|
|
406
|
+
|
|
407
|
+
`breadcrumb.root` rather than a new string because the product
|
|
408
|
+
ALREADY names this destination: FileExplorer labels the tab you
|
|
409
|
+
land on with exactly this key ("Root" / "KΓΆk"). Hovering the house
|
|
410
|
+
and reading the tab now say the same word instead of two.
|
|
411
|
+
|
|
412
|
+
β Relabelled, NOT repointed. Sending it to the Home overview would
|
|
413
|
+
break two things that are measured: `crumbs[0]` is this crumb, and
|
|
414
|
+
`trail` is `crumbs.slice(1)`, so the root would lose its only crumb
|
|
415
|
+
and (post-"My files") its last door; and `goUp()`/Alt+β from a
|
|
416
|
+
storage root loads `''`, so Up and the crumb would then disagree
|
|
417
|
+
about what sits above a storage.
|
|
418
|
+
β `data-testid` stays `breadcrumb-home`: it is a selector other
|
|
419
|
+
surfaces already use, and renaming it buys nothing. -->
|
|
420
|
+
<button
|
|
421
|
+
v-if="homeCrumb"
|
|
422
|
+
type="button"
|
|
423
|
+
class="fe-breadcrumb__crumb fe-breadcrumb__home"
|
|
424
|
+
:class="{ 'is-last': homeIsCurrent }"
|
|
425
|
+
:aria-current="homeIsCurrent ? 'page' : undefined"
|
|
426
|
+
:title="t('breadcrumb.root')"
|
|
427
|
+
:aria-label="t('breadcrumb.root')"
|
|
428
|
+
data-testid="breadcrumb-home"
|
|
429
|
+
@click="onHomeClick"
|
|
430
|
+
@contextmenu="onHomeContext"
|
|
431
|
+
@dragover="onCrumbDragOver"
|
|
432
|
+
@drop="onHomeDrop"
|
|
433
|
+
>
|
|
434
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons -->
|
|
435
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('home')"></span>
|
|
436
|
+
<span v-if="!homeIsCurrent" class="fe-breadcrumb__sep" aria-hidden="true">βΊ</span>
|
|
437
|
+
</button>
|
|
310
438
|
<button
|
|
311
439
|
v-for="(c, i) in leadCrumbs"
|
|
312
440
|
:key="c.adapterPath"
|
|
@@ -359,13 +487,57 @@ function cancelEdit() {
|
|
|
359
487
|
<span>{{ c.label }}</span>
|
|
360
488
|
<span v-if="i < tailCrumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">βΊ</span>
|
|
361
489
|
</button>
|
|
490
|
+
<!-- gorunum:v2-topbar β "Subfolders": the chevron the reference shell
|
|
491
|
+
hangs on the LAST crumb, opening what is inside the folder you are
|
|
492
|
+
standing in. Drawn whenever the parent supplies the list, empty or
|
|
493
|
+
not (see the prop's note). -->
|
|
494
|
+
<span v-if="subfolders" ref="subWrapEl" class="fe-breadcrumb__sub-wrap">
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
class="fe-breadcrumb__sub"
|
|
498
|
+
aria-haspopup="menu"
|
|
499
|
+
:aria-expanded="subOpen"
|
|
500
|
+
:title="t('breadcrumb.subfolders')"
|
|
501
|
+
:aria-label="t('breadcrumb.subfolders')"
|
|
502
|
+
data-testid="breadcrumb-subfolders"
|
|
503
|
+
@click="toggleSub"
|
|
504
|
+
>
|
|
505
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons -->
|
|
506
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('subfolders')"></span>
|
|
507
|
+
</button>
|
|
508
|
+
<div v-if="subOpen" class="fe-breadcrumb__menu" role="menu">
|
|
509
|
+
<button
|
|
510
|
+
v-for="c in subfolders"
|
|
511
|
+
:key="c.adapterPath"
|
|
512
|
+
type="button"
|
|
513
|
+
class="fe-breadcrumb__menu-item"
|
|
514
|
+
role="menuitem"
|
|
515
|
+
@click="onOverflowPick(c)"
|
|
516
|
+
>{{ c.label }}</button>
|
|
517
|
+
<p v-if="!subfolders.length" class="fe-breadcrumb__menu-empty">
|
|
518
|
+
{{ t('breadcrumb.subfolders.empty') }}
|
|
519
|
+
</p>
|
|
520
|
+
</div>
|
|
521
|
+
</span>
|
|
522
|
+
<!-- gorunum:v1 β "type a path and go". The glyph was the β emoji, which
|
|
523
|
+
this row draws from whichever emoji font the machine has: on a
|
|
524
|
+
headless Chromium and on a Linux box without one it comes out as a
|
|
525
|
+
stray dash beside the crumbs. An inline SVG is the same mark
|
|
526
|
+
everywhere and inherits the row's colour.
|
|
527
|
+
gorunum:v2-topbar β the mark now comes from lib/actionIcons with
|
|
528
|
+
every other glyph in the row; the reference shell has no path
|
|
529
|
+
editor at all, so this is ours and it stays. -->
|
|
362
530
|
<button
|
|
363
531
|
type="button"
|
|
364
532
|
class="fe-breadcrumb__edit"
|
|
365
533
|
:title="t('breadcrumb.go_to_path')"
|
|
366
534
|
:aria-label="t('breadcrumb.go_to_path')"
|
|
535
|
+
data-testid="breadcrumb-edit-path"
|
|
367
536
|
@click="startEdit"
|
|
368
|
-
|
|
537
|
+
>
|
|
538
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons -->
|
|
539
|
+
<span class="fe-icon" aria-hidden="true" v-html="actionIconSvg('edit-path')"></span>
|
|
540
|
+
</button>
|
|
369
541
|
</template>
|
|
370
542
|
<template v-else>
|
|
371
543
|
<input
|
|
@@ -26,6 +26,8 @@ import type { LocaleCode } from '../types/ExplorerConfig';
|
|
|
26
26
|
import type { FileNode, ViewMode } from '../types/FileNode';
|
|
27
27
|
import type { GlobalSearchHit } from '../composables/useFileApi';
|
|
28
28
|
import { matchedInContent, snippetSegments } from '../lib/snippet';
|
|
29
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
30
|
+
import { fileIconTile } from '../lib/fileIcons';
|
|
29
31
|
import { useLocale } from '../composables/useLocale';
|
|
30
32
|
|
|
31
33
|
const props = defineProps<{
|
|
@@ -93,6 +95,41 @@ type PaletteItem =
|
|
|
93
95
|
type HitItem = Extract<PaletteItem, { kind: 'hit' }>;
|
|
94
96
|
type SavedGroupItem = Extract<PaletteItem, { kind: 'saved' | 'save' }>;
|
|
95
97
|
|
|
98
|
+
/* === ikon:emoji β the palette's marks =================================
|
|
99
|
+
*
|
|
100
|
+
* `icon` used to be the glyph itself and the template printed it as TEXT:
|
|
101
|
+
* `β π π β¬ β¦ π β³ β π¨ β¨ π β§ β« πΎ π`, i.e. two colour-emoji sets, a
|
|
102
|
+
* geometric-shapes set and an arrows set, in one 20px column β beside a
|
|
103
|
+
* context menu and a toolbar that draw the same actions from
|
|
104
|
+
* `lib/actionIcons`. It is a KEY now, resolved the way ContextMenu resolves
|
|
105
|
+
* a row's key, and the two groups that list FILES resolve through
|
|
106
|
+
* `lib/fileIcons` instead, so a result row in the palette wears the same
|
|
107
|
+
* tile as the same file in the listing behind it.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/** `name.ext` β `ext`, for a search hit (which carries no `extension`). */
|
|
111
|
+
function extOf(name: string): string {
|
|
112
|
+
const dot = name.lastIndexOf('.');
|
|
113
|
+
return dot > 0 ? name.slice(dot + 1) : '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The row's mark. Static markup from our own two icon modules β never a
|
|
118
|
+
* string that came off the wire, which is what lets the template v-html it.
|
|
119
|
+
*/
|
|
120
|
+
function iconHtml(it: PaletteItem): string {
|
|
121
|
+
if (it.kind === 'file') return fileIconTile(it.node);
|
|
122
|
+
if (it.kind === 'hit') {
|
|
123
|
+
const name = String(it.hit.name ?? '');
|
|
124
|
+
return fileIconTile({
|
|
125
|
+
type: it.hit.type === 'dir' ? 'dir' : 'file',
|
|
126
|
+
extension: extOf(name),
|
|
127
|
+
basename: name,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return actionIconSvg(it.icon);
|
|
131
|
+
}
|
|
132
|
+
|
|
96
133
|
/**
|
|
97
134
|
* Scored includes: -1 = no match; otherwise earlier + prefix matches rank
|
|
98
135
|
* higher and shorter names get a small edge. Good enough without a fuzzy
|
|
@@ -116,7 +153,7 @@ const gotoItems = computed<PaletteItem[]>(() => {
|
|
|
116
153
|
const clean = q.replace(/^\/+|\/+$/g, '');
|
|
117
154
|
if (!clean) return [];
|
|
118
155
|
return [
|
|
119
|
-
{ kind: 'goto', id: `goto:${clean}`, label: `${t('palette.goto')}: ${clean}`, icon: '
|
|
156
|
+
{ kind: 'goto', id: `goto:${clean}`, label: `${t('palette.goto')}: ${clean}`, icon: 'goto', path: clean },
|
|
120
157
|
];
|
|
121
158
|
});
|
|
122
159
|
|
|
@@ -130,7 +167,7 @@ const fileItems = computed<PaletteItem[]>(() => {
|
|
|
130
167
|
kind: 'file' as const,
|
|
131
168
|
id: `file:${r.f.path}`,
|
|
132
169
|
label: r.name,
|
|
133
|
-
icon:
|
|
170
|
+
icon: '', // file rows draw their real tile β see iconHtml()
|
|
134
171
|
node: r.f,
|
|
135
172
|
}));
|
|
136
173
|
});
|
|
@@ -138,19 +175,21 @@ const fileItems = computed<PaletteItem[]>(() => {
|
|
|
138
175
|
const commandItems = computed<PaletteItem[]>(() => {
|
|
139
176
|
const q = query.value.trim();
|
|
140
177
|
const defs: Array<{ command: string; label: string; icon: string; enabled: boolean }> = [
|
|
141
|
-
{ command: 'new-folder', label: t('toolbar.new_folder'), icon: '
|
|
142
|
-
{ command: 'upload', label: t('toolbar.upload'), icon: '
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{ command: '
|
|
146
|
-
{ command: '
|
|
178
|
+
{ command: 'new-folder', label: t('toolbar.new_folder'), icon: 'new-folder', enabled: props.canWrite !== false },
|
|
179
|
+
{ command: 'upload', label: t('toolbar.upload'), icon: 'upload', enabled: props.canWrite !== false },
|
|
180
|
+
/* wiring:d2 β the mark is the NEXT mode (listβgridβgalleryβlist), which is
|
|
181
|
+
what pressing the row gets you; the three keys are the ViewSwitcher's own. */
|
|
182
|
+
{ command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? 'view-grid' : props.viewMode === 'grid' ? 'view-gallery' : 'view-list', enabled: true },
|
|
183
|
+
{ command: 'open-trash', label: t('cmd.trash'), icon: 'open-trash', enabled: true },
|
|
184
|
+
{ command: 'refresh', label: t('toolbar.refresh'), icon: 'refresh', enabled: true },
|
|
185
|
+
{ command: 'go-up', label: t('toolbar.go_up'), icon: 'go-up', enabled: props.canGoUp !== false },
|
|
147
186
|
/* wiring:int */
|
|
148
|
-
{ command: 'open-theme', label: t('theme.menu'), icon: '
|
|
149
|
-
{ command: 'open-shortcut-settings', label: t('shortcuts.settings.menu'), icon: '
|
|
150
|
-
{ command: 'start-tour', label: t('tour.restart'), icon: '
|
|
187
|
+
{ command: 'open-theme', label: t('theme.menu'), icon: 'theme', enabled: true },
|
|
188
|
+
{ command: 'open-shortcut-settings', label: t('shortcuts.settings.menu'), icon: 'shortcut-settings', enabled: true },
|
|
189
|
+
{ command: 'start-tour', label: t('tour.restart'), icon: 'tour', enabled: true },
|
|
151
190
|
/* wiring:d1 β tabs + split */
|
|
152
|
-
{ command: 'tab-new', label: t('cmd.tab_new'), icon: '
|
|
153
|
-
{ command: 'split-toggle', label: t('cmd.split_toggle'), icon: '
|
|
191
|
+
{ command: 'tab-new', label: t('cmd.tab_new'), icon: 'tab-new', enabled: true },
|
|
192
|
+
{ command: 'split-toggle', label: t('cmd.split_toggle'), icon: 'split', enabled: props.splitEnabled !== false },
|
|
154
193
|
];
|
|
155
194
|
return defs
|
|
156
195
|
.filter((d) => d.enabled && matchScore(d.label, q) >= 0)
|
|
@@ -210,7 +249,7 @@ const hitItems = computed<HitItem[]>(() =>
|
|
|
210
249
|
kind: 'hit' as const,
|
|
211
250
|
id: `hit:${h.storage_id ?? ''}:${h.path ?? ''}:${h.id ?? ''}`,
|
|
212
251
|
label: String(h.name ?? h.path ?? ''),
|
|
213
|
-
icon:
|
|
252
|
+
icon: '', // hit rows draw their real tile β see iconHtml()
|
|
214
253
|
hit: h,
|
|
215
254
|
crumb: hitCrumb(h),
|
|
216
255
|
inContent: matchedInContent(h.matched),
|
|
@@ -264,13 +303,13 @@ const savedItems = computed<SavedGroupItem[]>(() => {
|
|
|
264
303
|
const out: SavedGroupItem[] = [];
|
|
265
304
|
// "Save this query" command β only when there IS a query and it isn't saved yet.
|
|
266
305
|
if (q && !savedSearches.value.includes(q)) {
|
|
267
|
-
out.push({ kind: 'save', id: `save:${q}`, label: `${t('palette.save')}: ${q}`, icon: '
|
|
306
|
+
out.push({ kind: 'save', id: `save:${q}`, label: `${t('palette.save')}: ${q}`, icon: 'save-search', query: q });
|
|
268
307
|
}
|
|
269
308
|
const scored = savedSearches.value
|
|
270
309
|
.map((s) => ({ s, sc: matchScore(s, q) }))
|
|
271
310
|
.filter((r) => r.sc >= 0 || !q);
|
|
272
311
|
for (const r of scored) {
|
|
273
|
-
out.push({ kind: 'saved', id: `saved:${r.s}`, label: r.s, icon: '
|
|
312
|
+
out.push({ kind: 'saved', id: `saved:${r.s}`, label: r.s, icon: 'bookmark', query: r.s });
|
|
274
313
|
}
|
|
275
314
|
return out;
|
|
276
315
|
});
|
|
@@ -457,7 +496,8 @@ onBeforeUnmount(() => {
|
|
|
457
496
|
@mouseenter="active = i"
|
|
458
497
|
@click="choose(it)"
|
|
459
498
|
>
|
|
460
|
-
|
|
499
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons + lib/fileIcons -->
|
|
500
|
+
<span class="fe-cmdp__icon" aria-hidden="true" v-html="iconHtml(it)"></span>
|
|
461
501
|
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
462
502
|
</button>
|
|
463
503
|
|
|
@@ -474,7 +514,8 @@ onBeforeUnmount(() => {
|
|
|
474
514
|
@mouseenter="active = fileOffset + i"
|
|
475
515
|
@click="choose(it)"
|
|
476
516
|
>
|
|
477
|
-
|
|
517
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons + lib/fileIcons -->
|
|
518
|
+
<span class="fe-cmdp__icon" aria-hidden="true" v-html="iconHtml(it)"></span>
|
|
478
519
|
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
479
520
|
</button>
|
|
480
521
|
</template>
|
|
@@ -496,7 +537,8 @@ onBeforeUnmount(() => {
|
|
|
496
537
|
@mouseenter="active = hitOffset + i"
|
|
497
538
|
@click="choose(it)"
|
|
498
539
|
>
|
|
499
|
-
|
|
540
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons + lib/fileIcons -->
|
|
541
|
+
<span class="fe-cmdp__icon" aria-hidden="true" v-html="iconHtml(it)"></span>
|
|
500
542
|
<span class="fe-cmdp__hitbody">
|
|
501
543
|
<span class="fe-cmdp__hitline">
|
|
502
544
|
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
@@ -529,7 +571,8 @@ onBeforeUnmount(() => {
|
|
|
529
571
|
@click="choose(it)"
|
|
530
572
|
@keydown.enter.prevent="choose(it)"
|
|
531
573
|
>
|
|
532
|
-
|
|
574
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons + lib/fileIcons -->
|
|
575
|
+
<span class="fe-cmdp__icon" aria-hidden="true" v-html="iconHtml(it)"></span>
|
|
533
576
|
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
534
577
|
<button
|
|
535
578
|
v-if="it.kind === 'saved'"
|
|
@@ -538,7 +581,7 @@ onBeforeUnmount(() => {
|
|
|
538
581
|
:title="t('palette.saved.delete')"
|
|
539
582
|
:aria-label="t('palette.saved.delete')"
|
|
540
583
|
@click.stop="removeSavedSearch(it.query)"
|
|
541
|
-
|
|
584
|
+
><!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons --><span aria-hidden="true" v-html="actionIconSvg('delete')"></span></button>
|
|
542
585
|
</div>
|
|
543
586
|
</template>
|
|
544
587
|
|
|
@@ -555,7 +598,8 @@ onBeforeUnmount(() => {
|
|
|
555
598
|
@mouseenter="active = cmdOffset + i"
|
|
556
599
|
@click="choose(it)"
|
|
557
600
|
>
|
|
558
|
-
|
|
601
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons + lib/fileIcons -->
|
|
602
|
+
<span class="fe-cmdp__icon" aria-hidden="true" v-html="iconHtml(it)"></span>
|
|
559
603
|
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
560
604
|
</button>
|
|
561
605
|
</template>
|
|
@@ -26,6 +26,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
|
26
26
|
import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
|
|
27
27
|
import type { StorageRow } from '../types/Connections';
|
|
28
28
|
import { useLocale } from '../composables/useLocale';
|
|
29
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
29
30
|
import { useConnections, connectionsOrigin } from '../composables/useConnections';
|
|
30
31
|
import {
|
|
31
32
|
buildGuide,
|
|
@@ -68,6 +69,7 @@ const {
|
|
|
68
69
|
storages,
|
|
69
70
|
visible,
|
|
70
71
|
me,
|
|
72
|
+
publicUrl,
|
|
71
73
|
loading,
|
|
72
74
|
loaded,
|
|
73
75
|
error,
|
|
@@ -274,7 +276,7 @@ const protocols = guideProtocols();
|
|
|
274
276
|
const protocol = ref(protocols[0] ?? 'webdav');
|
|
275
277
|
const guideStorage = ref<string>('');
|
|
276
278
|
|
|
277
|
-
const origin = computed(() => connectionsOrigin(props.config));
|
|
279
|
+
const origin = computed(() => connectionsOrigin(props.config, publicUrl.value));
|
|
278
280
|
|
|
279
281
|
/**
|
|
280
282
|
* What the S3 key panel published: the caller's own key, the endpoint the
|
|
@@ -387,7 +389,8 @@ watch(
|
|
|
387
389
|
:aria-label="t('conn.close')"
|
|
388
390
|
@click="emit('close')"
|
|
389
391
|
>
|
|
390
|
-
|
|
392
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons -->
|
|
393
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
391
394
|
</button>
|
|
392
395
|
</header>
|
|
393
396
|
|
|
@@ -670,6 +673,7 @@ watch(
|
|
|
670
673
|
v-if="protocol === 'ftps' || protocol === 'webdav' || protocol === 'mount'"
|
|
671
674
|
:config="config"
|
|
672
675
|
:protocol="protocol"
|
|
676
|
+
:host="hostOf(origin)"
|
|
673
677
|
/>
|
|
674
678
|
|
|
675
679
|
<ConnectionGuideView v-if="guide" :guide="guide" :locale="locale" />
|
|
@@ -11,13 +11,35 @@ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
|
11
11
|
import type { FileNode } from '../types/FileNode';
|
|
12
12
|
import { useLocale } from '../composables/useLocale';
|
|
13
13
|
import { menuShortcutHint } from '../composables/useKeyboardShortcuts';
|
|
14
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
14
15
|
|
|
15
16
|
export interface ContextAction {
|
|
16
17
|
key: string;
|
|
17
18
|
label: string;
|
|
19
|
+
/**
|
|
20
|
+
* gorunum:v1-icons β OPTIONAL icon override. Rows normally need nothing
|
|
21
|
+
* here: the glyph is looked up by `key` in lib/actionIcons. Set this to
|
|
22
|
+
* β’ another icon key, when one action has two faces (`star` / `unstar`);
|
|
23
|
+
* β’ any literal string (an emoji), when an embedder wants its own mark β
|
|
24
|
+
* an unknown name draws no SVG and the string is printed as-is.
|
|
25
|
+
*/
|
|
18
26
|
icon?: string;
|
|
19
27
|
danger?: boolean;
|
|
20
28
|
disabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* tasi:m1 β WHY, in one sentence, for a row that is greyed out.
|
|
31
|
+
*
|
|
32
|
+
* β A disabled control has to explain itself or it is indistinguishable from
|
|
33
|
+
* a broken one. Sharing works on one item at a time, so "PaylaΕ / Δ°zinler"
|
|
34
|
+
* greys out above a multi-selection β grey with a reason reads as a rule,
|
|
35
|
+
* grey without one reads as a bug, and HIDING it (which is what this row did
|
|
36
|
+
* until 2026-09-13) reads as the feature not existing.
|
|
37
|
+
*
|
|
38
|
+
* Rendered as the native `title` on both surfaces. Absent leaves the
|
|
39
|
+
* tooltip each surface already builds β the label plus its shortcut in the
|
|
40
|
+
* selection bar, nothing in the menu.
|
|
41
|
+
*/
|
|
42
|
+
title?: string;
|
|
21
43
|
hidden?: boolean;
|
|
22
44
|
divider?: boolean;
|
|
23
45
|
/**
|
|
@@ -61,6 +83,15 @@ function hintFor(a: ContextAction): string {
|
|
|
61
83
|
return menuShortcutHint(a.shortcutId ?? a.key);
|
|
62
84
|
}
|
|
63
85
|
|
|
86
|
+
/* gorunum:v1-icons β the row's glyph. `icon` is consulted FIRST because that
|
|
87
|
+
* is where a two-faced action names its variant (`star` vs `unstar`) and where
|
|
88
|
+
* an embedder puts its own mark; the action's own key is the default. An
|
|
89
|
+
* unrecognised name returns '' and the template falls back to printing the
|
|
90
|
+
* string, so a host that still passes an emoji keeps it. */
|
|
91
|
+
function iconFor(a: ContextAction): string {
|
|
92
|
+
return actionIconSvg(a.icon || a.key);
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
const open = ref(false);
|
|
65
96
|
const x = ref(0);
|
|
66
97
|
const y = ref(0);
|
|
@@ -328,10 +359,13 @@ defineExpose({ show, hide });
|
|
|
328
359
|
class="fe-ctx__item fe-sheet__item"
|
|
329
360
|
:class="{ 'is-danger': a.danger, 'is-disabled': a.disabled }"
|
|
330
361
|
:disabled="a.disabled"
|
|
362
|
+
:title="a.title"
|
|
331
363
|
role="menuitem"
|
|
332
364
|
@click="pick(a)"
|
|
333
365
|
>
|
|
334
|
-
|
|
366
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons -->
|
|
367
|
+
<span v-if="iconFor(a)" class="fe-ctx__icon" aria-hidden="true" v-html="iconFor(a)"></span>
|
|
368
|
+
<span v-else-if="a.icon" class="fe-ctx__icon" aria-hidden="true">{{ a.icon }}</span>
|
|
335
369
|
<span class="fe-ctx__label">{{ a.label }}</span>
|
|
336
370
|
<span v-if="hintFor(a)" class="fe-ctx__key" aria-hidden="true">{{ hintFor(a) }}</span>
|
|
337
371
|
</button>
|
|
@@ -356,10 +390,13 @@ defineExpose({ show, hide });
|
|
|
356
390
|
class="fe-ctx__item"
|
|
357
391
|
:class="{ 'is-danger': a.danger, 'is-disabled': a.disabled }"
|
|
358
392
|
:disabled="a.disabled"
|
|
393
|
+
:title="a.title"
|
|
359
394
|
role="menuitem"
|
|
360
395
|
@click="pick(a)"
|
|
361
396
|
>
|
|
362
|
-
|
|
397
|
+
<!-- eslint-disable-next-line vue/no-v-html β static markup from lib/actionIcons -->
|
|
398
|
+
<span v-if="iconFor(a)" class="fe-ctx__icon" aria-hidden="true" v-html="iconFor(a)"></span>
|
|
399
|
+
<span v-else-if="a.icon" class="fe-ctx__icon" aria-hidden="true">{{ a.icon }}</span>
|
|
363
400
|
<span class="fe-ctx__label">{{ a.label }}</span>
|
|
364
401
|
<!-- tus:t1 β the key that does this from the keyboard, read from
|
|
365
402
|
the registry so a remap reaches it. Empty for a verb with no
|