@brftech/filex-core 0.1.82 → 0.1.84
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 +2 -0
- package/dist/{ArchiveViewer-BRASXNIS.js → ArchiveViewer-wg4uE-3C.js} +2 -2
- package/dist/{ArchiveViewer-BRASXNIS.js.map → ArchiveViewer-wg4uE-3C.js.map} +1 -1
- package/dist/{CsvViewer-CgiLiHWw.js → CsvViewer-57uz4UKk.js} +2 -2
- package/dist/{CsvViewer-CgiLiHWw.js.map → CsvViewer-57uz4UKk.js.map} +1 -1
- package/dist/{DrawioViewer-D2I_uEng.js → DrawioViewer-BfL9Hbyp.js} +2 -2
- package/dist/{DrawioViewer-D2I_uEng.js.map → DrawioViewer-BfL9Hbyp.js.map} +1 -1
- package/dist/{EpubViewer-6nVPRR6R.js → EpubViewer-CYG2CV_3.js} +2 -2
- package/dist/{EpubViewer-6nVPRR6R.js.map → EpubViewer-CYG2CV_3.js.map} +1 -1
- package/dist/{IpynbViewer-B7p9hxFx.js → IpynbViewer-DJw34-wV.js} +2 -2
- package/dist/{IpynbViewer-B7p9hxFx.js.map → IpynbViewer-DJw34-wV.js.map} +1 -1
- package/dist/{MermaidViewer-O9vGvp4y.js → MermaidViewer-C86jjm27.js} +2 -2
- package/dist/{MermaidViewer-O9vGvp4y.js.map → MermaidViewer-C86jjm27.js.map} +1 -1
- package/dist/{PsdViewer-C6EaEAxF.js → PsdViewer-DGHIqv-x.js} +2 -2
- package/dist/{PsdViewer-C6EaEAxF.js.map → PsdViewer-DGHIqv-x.js.map} +1 -1
- package/dist/{TiffViewer-Dod4uIXI.js → TiffViewer-gct9Ub3o.js} +2 -2
- package/dist/{TiffViewer-Dod4uIXI.js.map → TiffViewer-gct9Ub3o.js.map} +1 -1
- package/dist/{Viewer3D-B3kLZrwO.js → Viewer3D-BVt4l2a9.js} +2 -2
- package/dist/{Viewer3D-B3kLZrwO.js.map → Viewer3D-BVt4l2a9.js.map} +1 -1
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +38 -38
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BeUoODQq.js +6871 -0
- package/dist/index-BeUoODQq.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +393 -31
- package/src/components/Breadcrumb.vue +100 -5
- package/src/components/CommandPalette.vue +299 -0
- package/src/components/GridView.vue +8 -13
- package/src/components/ListView.vue +173 -20
- package/src/components/ShortcutsHelp.vue +59 -0
- package/src/components/Toolbar.vue +65 -1
- package/src/composables/useKeyboardShortcuts.ts +38 -0
- package/src/composables/useRealtime.ts +9 -1
- package/src/lib/fileIcons.ts +127 -0
- package/src/locales/en.ts +54 -0
- package/src/locales/tr.ts +54 -0
- package/src/styles/base.css +565 -0
- package/src/styles/variables.css +70 -0
- package/dist/index-DL6_eaM3.js +0 -5888
- package/dist/index-DL6_eaM3.js.map +0 -1
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* The ✏ button swaps the crumbs for a free-form input; Enter
|
|
20
20
|
* navigates, Escape cancels.
|
|
21
21
|
*/
|
|
22
|
-
import { computed, nextTick, ref } from 'vue';
|
|
22
|
+
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
23
23
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
24
24
|
import { useLocale } from '../composables/useLocale';
|
|
25
25
|
|
|
@@ -125,6 +125,64 @@ const crumbs = computed<Crumb[]>(() => {
|
|
|
125
125
|
return out;
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
// ------------------------------------------------------------------
|
|
129
|
+
// Overflow collapse (cila:c / I5) — when the trail grows past the
|
|
130
|
+
// threshold, show `root › … › parent › current`; the middle crumbs move
|
|
131
|
+
// into a dropdown. Visible crumbs keep their full click/drag-drop/context
|
|
132
|
+
// behavior; dropdown entries only navigate.
|
|
133
|
+
// ------------------------------------------------------------------
|
|
134
|
+
const OVERFLOW_THRESHOLD = 4;
|
|
135
|
+
const collapsed = computed(() => crumbs.value.length > OVERFLOW_THRESHOLD);
|
|
136
|
+
const leadCrumbs = computed<Crumb[]>(() =>
|
|
137
|
+
collapsed.value ? crumbs.value.slice(0, 1) : crumbs.value,
|
|
138
|
+
);
|
|
139
|
+
const middleCrumbs = computed<Crumb[]>(() =>
|
|
140
|
+
collapsed.value ? crumbs.value.slice(1, -2) : [],
|
|
141
|
+
);
|
|
142
|
+
const tailCrumbs = computed<Crumb[]>(() =>
|
|
143
|
+
collapsed.value ? crumbs.value.slice(-2) : [],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const overflowOpen = ref(false);
|
|
147
|
+
const moreWrapEl = ref<HTMLElement | null>(null);
|
|
148
|
+
|
|
149
|
+
function onOverflowPick(crumb: Crumb) {
|
|
150
|
+
overflowOpen.value = false;
|
|
151
|
+
emit('navigate', crumb.adapterPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onDocClick(e: MouseEvent) {
|
|
155
|
+
const wrap = moreWrapEl.value;
|
|
156
|
+
if (wrap && e.target instanceof Node && wrap.contains(e.target)) return;
|
|
157
|
+
overflowOpen.value = false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function onDocKeydown(e: KeyboardEvent) {
|
|
161
|
+
if (e.key === 'Escape') {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
overflowOpen.value = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
watch(overflowOpen, (v) => {
|
|
168
|
+
if (v) {
|
|
169
|
+
document.addEventListener('click', onDocClick, true);
|
|
170
|
+
document.addEventListener('keydown', onDocKeydown, true);
|
|
171
|
+
} else {
|
|
172
|
+
document.removeEventListener('click', onDocClick, true);
|
|
173
|
+
document.removeEventListener('keydown', onDocKeydown, true);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// Navigation rebuilds the trail — a stale open menu would list crumbs of
|
|
177
|
+
// the previous folder.
|
|
178
|
+
watch(crumbs, () => {
|
|
179
|
+
overflowOpen.value = false;
|
|
180
|
+
});
|
|
181
|
+
onBeforeUnmount(() => {
|
|
182
|
+
document.removeEventListener('click', onDocClick, true);
|
|
183
|
+
document.removeEventListener('keydown', onDocKeydown, true);
|
|
184
|
+
});
|
|
185
|
+
|
|
128
186
|
function onClick(crumb: Crumb) {
|
|
129
187
|
emit('navigate', crumb.adapterPath);
|
|
130
188
|
}
|
|
@@ -242,19 +300,56 @@ function cancelEdit() {
|
|
|
242
300
|
<nav class="fe-breadcrumb" aria-label="Breadcrumb">
|
|
243
301
|
<template v-if="!editing">
|
|
244
302
|
<button
|
|
245
|
-
v-for="(c, i) in
|
|
303
|
+
v-for="(c, i) in leadCrumbs"
|
|
304
|
+
:key="c.adapterPath"
|
|
305
|
+
class="fe-breadcrumb__crumb"
|
|
306
|
+
:class="{ 'is-last': !collapsed && i === leadCrumbs.length - 1 }"
|
|
307
|
+
type="button"
|
|
308
|
+
:aria-current="!collapsed && i === leadCrumbs.length - 1 ? 'page' : undefined"
|
|
309
|
+
@click="onClick(c)"
|
|
310
|
+
@contextmenu="onContext($event, c)"
|
|
311
|
+
@dragover="onCrumbDragOver"
|
|
312
|
+
@drop="onCrumbDrop($event, c)"
|
|
313
|
+
>
|
|
314
|
+
<span>{{ c.label }}</span>
|
|
315
|
+
<span v-if="collapsed || i < leadCrumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">›</span>
|
|
316
|
+
</button>
|
|
317
|
+
<span v-if="middleCrumbs.length" ref="moreWrapEl" class="fe-breadcrumb__more-wrap">
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
class="fe-breadcrumb__more"
|
|
321
|
+
aria-haspopup="menu"
|
|
322
|
+
:aria-expanded="overflowOpen"
|
|
323
|
+
:aria-label="t('breadcrumb.more')"
|
|
324
|
+
:title="t('breadcrumb.more')"
|
|
325
|
+
@click="overflowOpen = !overflowOpen"
|
|
326
|
+
>…</button>
|
|
327
|
+
<span class="fe-breadcrumb__sep" aria-hidden="true">›</span>
|
|
328
|
+
<div v-if="overflowOpen" class="fe-breadcrumb__menu" role="menu">
|
|
329
|
+
<button
|
|
330
|
+
v-for="c in middleCrumbs"
|
|
331
|
+
:key="c.adapterPath"
|
|
332
|
+
type="button"
|
|
333
|
+
class="fe-breadcrumb__menu-item"
|
|
334
|
+
role="menuitem"
|
|
335
|
+
@click="onOverflowPick(c)"
|
|
336
|
+
>{{ c.label }}</button>
|
|
337
|
+
</div>
|
|
338
|
+
</span>
|
|
339
|
+
<button
|
|
340
|
+
v-for="(c, i) in tailCrumbs"
|
|
246
341
|
:key="c.adapterPath"
|
|
247
342
|
class="fe-breadcrumb__crumb"
|
|
248
|
-
:class="{ 'is-last': i ===
|
|
343
|
+
:class="{ 'is-last': i === tailCrumbs.length - 1 }"
|
|
249
344
|
type="button"
|
|
250
|
-
:aria-current="i ===
|
|
345
|
+
:aria-current="i === tailCrumbs.length - 1 ? 'page' : undefined"
|
|
251
346
|
@click="onClick(c)"
|
|
252
347
|
@contextmenu="onContext($event, c)"
|
|
253
348
|
@dragover="onCrumbDragOver"
|
|
254
349
|
@drop="onCrumbDrop($event, c)"
|
|
255
350
|
>
|
|
256
351
|
<span>{{ c.label }}</span>
|
|
257
|
-
<span v-if="i <
|
|
352
|
+
<span v-if="i < tailCrumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">›</span>
|
|
258
353
|
</button>
|
|
259
354
|
<button
|
|
260
355
|
type="button"
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CommandPalette — Ctrl/Cmd+K quick launcher.
|
|
4
|
+
*
|
|
5
|
+
* Sources (no new backend calls — everything comes from props):
|
|
6
|
+
* 1. Files/folders of the CURRENT listing (`files` prop), filtered by a
|
|
7
|
+
* tiny scored-includes matcher (no fuzzy-search dependency).
|
|
8
|
+
* 2. Commands mapped 1:1 onto existing FileExplorer actions, surfaced as
|
|
9
|
+
* individual emits so the host wires each to code it already has.
|
|
10
|
+
* 3. Path jump: any query containing `/` offers a "go to path" entry.
|
|
11
|
+
*
|
|
12
|
+
* Keyboard: ↑/↓ move, Enter selects, Esc closes; the listener sits on the
|
|
13
|
+
* document in CAPTURE phase so it wins over useKeyboardShortcuts' window
|
|
14
|
+
* handler (otherwise Enter would also fire the explorer's own onOpen).
|
|
15
|
+
*/
|
|
16
|
+
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
17
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
18
|
+
import type { FileNode, ViewMode } from '../types/FileNode';
|
|
19
|
+
import { useLocale } from '../composables/useLocale';
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
open: boolean;
|
|
23
|
+
locale: LocaleCode;
|
|
24
|
+
files: FileNode[];
|
|
25
|
+
viewMode: ViewMode;
|
|
26
|
+
/** Gates the mutating commands (new folder / upload). */
|
|
27
|
+
canWrite?: boolean;
|
|
28
|
+
/** Gates the "up one level" command. */
|
|
29
|
+
canGoUp?: boolean;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(e: 'close'): void;
|
|
34
|
+
(e: 'open-node', node: FileNode): void;
|
|
35
|
+
(e: 'navigate', path: string): void;
|
|
36
|
+
(e: 'new-folder'): void;
|
|
37
|
+
(e: 'upload'): void;
|
|
38
|
+
(e: 'toggle-view'): void;
|
|
39
|
+
(e: 'open-trash'): void;
|
|
40
|
+
(e: 'refresh'): void;
|
|
41
|
+
(e: 'go-up'): void;
|
|
42
|
+
}>();
|
|
43
|
+
|
|
44
|
+
const { t, nodeDisplayName } = useLocale(() => props.locale);
|
|
45
|
+
|
|
46
|
+
const query = ref('');
|
|
47
|
+
const active = ref(0);
|
|
48
|
+
const inputEl = ref<HTMLInputElement | null>(null);
|
|
49
|
+
const listEl = ref<HTMLElement | null>(null);
|
|
50
|
+
|
|
51
|
+
type PaletteItem =
|
|
52
|
+
| { kind: 'goto'; id: string; label: string; icon: string; path: string }
|
|
53
|
+
| { kind: 'file'; id: string; label: string; icon: string; node: FileNode }
|
|
54
|
+
| { kind: 'command'; id: string; label: string; icon: string; command: string };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Scored includes: -1 = no match; otherwise earlier + prefix matches rank
|
|
58
|
+
* higher and shorter names get a small edge. Good enough without a fuzzy
|
|
59
|
+
* dependency.
|
|
60
|
+
*/
|
|
61
|
+
function matchScore(label: string, q: string): number {
|
|
62
|
+
if (!q) return 0;
|
|
63
|
+
const n = label.toLocaleLowerCase();
|
|
64
|
+
const s = q.toLocaleLowerCase();
|
|
65
|
+
const i = n.indexOf(s);
|
|
66
|
+
if (i === -1) return -1;
|
|
67
|
+
let sc = 100 - Math.min(i, 50);
|
|
68
|
+
if (i === 0) sc += 40;
|
|
69
|
+
sc -= Math.min(n.length, 40) / 4;
|
|
70
|
+
return sc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const gotoItems = computed<PaletteItem[]>(() => {
|
|
74
|
+
const q = query.value.trim();
|
|
75
|
+
if (!q.includes('/')) return [];
|
|
76
|
+
const clean = q.replace(/^\/+|\/+$/g, '');
|
|
77
|
+
if (!clean) return [];
|
|
78
|
+
return [
|
|
79
|
+
{ kind: 'goto', id: `goto:${clean}`, label: `${t('palette.goto')}: ${clean}`, icon: '➜', path: clean },
|
|
80
|
+
];
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const fileItems = computed<PaletteItem[]>(() => {
|
|
84
|
+
const q = query.value.trim();
|
|
85
|
+
const scored = props.files
|
|
86
|
+
.map((f) => ({ f, name: nodeDisplayName(f), s: matchScore(nodeDisplayName(f), q) }))
|
|
87
|
+
.filter((r) => r.s >= 0);
|
|
88
|
+
if (q) scored.sort((a, b) => b.s - a.s);
|
|
89
|
+
return scored.slice(0, 8).map((r) => ({
|
|
90
|
+
kind: 'file' as const,
|
|
91
|
+
id: `file:${r.f.path}`,
|
|
92
|
+
label: r.name,
|
|
93
|
+
icon: r.f.type === 'dir' ? '📁' : '📄',
|
|
94
|
+
node: r.f,
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const commandItems = computed<PaletteItem[]>(() => {
|
|
99
|
+
const q = query.value.trim();
|
|
100
|
+
const defs: Array<{ command: string; label: string; icon: string; enabled: boolean }> = [
|
|
101
|
+
{ command: 'new-folder', label: t('toolbar.new_folder'), icon: '📁', enabled: props.canWrite !== false },
|
|
102
|
+
{ command: 'upload', label: t('toolbar.upload'), icon: '⬆', enabled: props.canWrite !== false },
|
|
103
|
+
{ command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : '☰', enabled: true },
|
|
104
|
+
{ command: 'open-trash', label: t('cmd.trash'), icon: '🗑', enabled: true },
|
|
105
|
+
{ command: 'refresh', label: t('toolbar.refresh'), icon: '⟳', enabled: true },
|
|
106
|
+
{ command: 'go-up', label: t('toolbar.go_up'), icon: '↑', enabled: props.canGoUp !== false },
|
|
107
|
+
];
|
|
108
|
+
return defs
|
|
109
|
+
.filter((d) => d.enabled && matchScore(d.label, q) >= 0)
|
|
110
|
+
.map((d) => ({ kind: 'command' as const, id: `cmd:${d.command}`, label: d.label, icon: d.icon, command: d.command }));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const flat = computed<PaletteItem[]>(() => [
|
|
114
|
+
...gotoItems.value,
|
|
115
|
+
...fileItems.value,
|
|
116
|
+
...commandItems.value,
|
|
117
|
+
]);
|
|
118
|
+
const fileOffset = computed(() => gotoItems.value.length);
|
|
119
|
+
const cmdOffset = computed(() => gotoItems.value.length + fileItems.value.length);
|
|
120
|
+
|
|
121
|
+
watch(query, () => {
|
|
122
|
+
active.value = 0;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function move(delta: number) {
|
|
126
|
+
const n = flat.value.length;
|
|
127
|
+
if (!n) return;
|
|
128
|
+
active.value = (active.value + delta + n) % n;
|
|
129
|
+
void nextTick(() => {
|
|
130
|
+
listEl.value?.querySelector('.is-active')?.scrollIntoView({ block: 'nearest' });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function choose(item?: PaletteItem) {
|
|
135
|
+
const it = item ?? flat.value[active.value];
|
|
136
|
+
if (!it) return;
|
|
137
|
+
emit('close');
|
|
138
|
+
if (it.kind === 'file') {
|
|
139
|
+
emit('open-node', it.node);
|
|
140
|
+
} else if (it.kind === 'goto') {
|
|
141
|
+
emit('navigate', it.path);
|
|
142
|
+
} else {
|
|
143
|
+
switch (it.command) {
|
|
144
|
+
case 'new-folder': emit('new-folder'); break;
|
|
145
|
+
case 'upload': emit('upload'); break;
|
|
146
|
+
case 'toggle-view': emit('toggle-view'); break;
|
|
147
|
+
case 'open-trash': emit('open-trash'); break;
|
|
148
|
+
case 'refresh': emit('refresh'); break;
|
|
149
|
+
case 'go-up': emit('go-up'); break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onDocKeydown(e: KeyboardEvent) {
|
|
155
|
+
if (!props.open) return;
|
|
156
|
+
switch (e.key) {
|
|
157
|
+
case 'ArrowDown':
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
move(1);
|
|
161
|
+
break;
|
|
162
|
+
case 'ArrowUp':
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
e.stopPropagation();
|
|
165
|
+
move(-1);
|
|
166
|
+
break;
|
|
167
|
+
case 'Enter':
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
e.stopPropagation();
|
|
170
|
+
choose();
|
|
171
|
+
break;
|
|
172
|
+
case 'Escape':
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
e.stopPropagation();
|
|
175
|
+
emit('close');
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
watch(
|
|
181
|
+
() => props.open,
|
|
182
|
+
(v) => {
|
|
183
|
+
if (v) {
|
|
184
|
+
query.value = '';
|
|
185
|
+
active.value = 0;
|
|
186
|
+
document.addEventListener('keydown', onDocKeydown, true);
|
|
187
|
+
void nextTick(() => inputEl.value?.focus());
|
|
188
|
+
} else {
|
|
189
|
+
document.removeEventListener('keydown', onDocKeydown, true);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
onBeforeUnmount(() => document.removeEventListener('keydown', onDocKeydown, true));
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
<template>
|
|
198
|
+
<transition name="fe-modal">
|
|
199
|
+
<div
|
|
200
|
+
v-if="open"
|
|
201
|
+
class="fe-modal__backdrop fe-cmdp__backdrop"
|
|
202
|
+
role="presentation"
|
|
203
|
+
@click="emit('close')"
|
|
204
|
+
>
|
|
205
|
+
<div
|
|
206
|
+
class="fe-cmdp"
|
|
207
|
+
role="dialog"
|
|
208
|
+
aria-modal="true"
|
|
209
|
+
:aria-label="t('palette.placeholder')"
|
|
210
|
+
@click.stop
|
|
211
|
+
>
|
|
212
|
+
<div class="fe-cmdp__inputwrap">
|
|
213
|
+
<svg
|
|
214
|
+
class="fe-cmdp__glyph"
|
|
215
|
+
width="16"
|
|
216
|
+
height="16"
|
|
217
|
+
viewBox="0 0 16 16"
|
|
218
|
+
fill="none"
|
|
219
|
+
stroke="currentColor"
|
|
220
|
+
stroke-width="1.5"
|
|
221
|
+
stroke-linecap="round"
|
|
222
|
+
aria-hidden="true"
|
|
223
|
+
>
|
|
224
|
+
<circle cx="7" cy="7" r="4.5" />
|
|
225
|
+
<path d="m10.5 10.5 3.5 3.5" />
|
|
226
|
+
</svg>
|
|
227
|
+
<input
|
|
228
|
+
ref="inputEl"
|
|
229
|
+
v-model="query"
|
|
230
|
+
type="text"
|
|
231
|
+
class="fe-cmdp__input"
|
|
232
|
+
:placeholder="t('palette.placeholder')"
|
|
233
|
+
autocomplete="off"
|
|
234
|
+
spellcheck="false"
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div ref="listEl" class="fe-cmdp__list" role="listbox">
|
|
239
|
+
<button
|
|
240
|
+
v-for="(it, i) in gotoItems"
|
|
241
|
+
:key="it.id"
|
|
242
|
+
type="button"
|
|
243
|
+
class="fe-cmdp__item"
|
|
244
|
+
:class="{ 'is-active': i === active }"
|
|
245
|
+
role="option"
|
|
246
|
+
:aria-selected="i === active"
|
|
247
|
+
@mouseenter="active = i"
|
|
248
|
+
@click="choose(it)"
|
|
249
|
+
>
|
|
250
|
+
<span class="fe-cmdp__icon" aria-hidden="true">{{ it.icon }}</span>
|
|
251
|
+
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
252
|
+
</button>
|
|
253
|
+
|
|
254
|
+
<template v-if="fileItems.length">
|
|
255
|
+
<div class="fe-cmdp__group">{{ t('palette.files') }}</div>
|
|
256
|
+
<button
|
|
257
|
+
v-for="(it, i) in fileItems"
|
|
258
|
+
:key="it.id"
|
|
259
|
+
type="button"
|
|
260
|
+
class="fe-cmdp__item"
|
|
261
|
+
:class="{ 'is-active': fileOffset + i === active }"
|
|
262
|
+
role="option"
|
|
263
|
+
:aria-selected="fileOffset + i === active"
|
|
264
|
+
@mouseenter="active = fileOffset + i"
|
|
265
|
+
@click="choose(it)"
|
|
266
|
+
>
|
|
267
|
+
<span class="fe-cmdp__icon" aria-hidden="true">{{ it.icon }}</span>
|
|
268
|
+
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
269
|
+
</button>
|
|
270
|
+
</template>
|
|
271
|
+
|
|
272
|
+
<template v-if="commandItems.length">
|
|
273
|
+
<div class="fe-cmdp__group">{{ t('palette.commands') }}</div>
|
|
274
|
+
<button
|
|
275
|
+
v-for="(it, i) in commandItems"
|
|
276
|
+
:key="it.id"
|
|
277
|
+
type="button"
|
|
278
|
+
class="fe-cmdp__item"
|
|
279
|
+
:class="{ 'is-active': cmdOffset + i === active }"
|
|
280
|
+
role="option"
|
|
281
|
+
:aria-selected="cmdOffset + i === active"
|
|
282
|
+
@mouseenter="active = cmdOffset + i"
|
|
283
|
+
@click="choose(it)"
|
|
284
|
+
>
|
|
285
|
+
<span class="fe-cmdp__icon" aria-hidden="true">{{ it.icon }}</span>
|
|
286
|
+
<span class="fe-cmdp__label">{{ it.label }}</span>
|
|
287
|
+
</button>
|
|
288
|
+
</template>
|
|
289
|
+
|
|
290
|
+
<div v-if="flat.length === 0" class="fe-cmdp__empty">
|
|
291
|
+
{{ t('palette.empty') }}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div class="fe-cmdp__hint">{{ t('palette.hint') }}</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</transition>
|
|
299
|
+
</template>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { FileNode } from '../types/FileNode';
|
|
6
6
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
7
7
|
import { useLocale } from '../composables/useLocale';
|
|
8
|
+
import { fileIconSvg } from '../lib/fileIcons';
|
|
8
9
|
|
|
9
10
|
const props = defineProps<{
|
|
10
11
|
files: FileNode[];
|
|
@@ -101,20 +102,12 @@ function parentDir(path: string): string {
|
|
|
101
102
|
return stripped.slice(0, idx);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
// Special rows keep their emoji (trash/storage are not file-TYPE icons);
|
|
106
|
+
// everything else renders the SVG icon set from lib/fileIcons.
|
|
107
|
+
function specialEmojiFor(n: FileNode): string | null {
|
|
105
108
|
if (n.basename === '.trash') return '🗑';
|
|
106
109
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
107
|
-
|
|
108
|
-
const e = (n.extension || '').toLowerCase();
|
|
109
|
-
if (['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'svg'].includes(e)) return '🖼';
|
|
110
|
-
if (['mp4', 'webm', 'mov', 'mkv'].includes(e)) return '🎞';
|
|
111
|
-
if (['mp3', 'wav', 'flac', 'ogg'].includes(e)) return '🎵';
|
|
112
|
-
if (e === 'pdf') return '📕';
|
|
113
|
-
if (['doc', 'docx', 'odt'].includes(e)) return '📄';
|
|
114
|
-
if (['xls', 'xlsx', 'csv'].includes(e)) return '📊';
|
|
115
|
-
if (['ppt', 'pptx'].includes(e)) return '📽';
|
|
116
|
-
if (['zip', 'tar', 'gz', '7z'].includes(e)) return '🗜';
|
|
117
|
-
return '📎';
|
|
110
|
+
return null;
|
|
118
111
|
}
|
|
119
112
|
</script>
|
|
120
113
|
|
|
@@ -156,7 +149,9 @@ function iconFor(n: FileNode): string {
|
|
|
156
149
|
loading="lazy"
|
|
157
150
|
draggable="false"
|
|
158
151
|
/>
|
|
159
|
-
<span v-else class="fe-grid__icon">{{
|
|
152
|
+
<span v-else-if="specialEmojiFor(n)" class="fe-grid__icon">{{ specialEmojiFor(n) }}</span>
|
|
153
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
154
|
+
<span v-else class="fe-grid__icon fe-grid__icon--svg" v-html="fileIconSvg(n)"></span>
|
|
160
155
|
</div>
|
|
161
156
|
<div class="fe-grid__label" :title="n.basename">
|
|
162
157
|
{{ nodeDisplayName(n) }}
|