@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.
Files changed (43) hide show
  1. package/README.md +2 -0
  2. package/dist/{ArchiveViewer-BRASXNIS.js → ArchiveViewer-wg4uE-3C.js} +2 -2
  3. package/dist/{ArchiveViewer-BRASXNIS.js.map → ArchiveViewer-wg4uE-3C.js.map} +1 -1
  4. package/dist/{CsvViewer-CgiLiHWw.js → CsvViewer-57uz4UKk.js} +2 -2
  5. package/dist/{CsvViewer-CgiLiHWw.js.map → CsvViewer-57uz4UKk.js.map} +1 -1
  6. package/dist/{DrawioViewer-D2I_uEng.js → DrawioViewer-BfL9Hbyp.js} +2 -2
  7. package/dist/{DrawioViewer-D2I_uEng.js.map → DrawioViewer-BfL9Hbyp.js.map} +1 -1
  8. package/dist/{EpubViewer-6nVPRR6R.js → EpubViewer-CYG2CV_3.js} +2 -2
  9. package/dist/{EpubViewer-6nVPRR6R.js.map → EpubViewer-CYG2CV_3.js.map} +1 -1
  10. package/dist/{IpynbViewer-B7p9hxFx.js → IpynbViewer-DJw34-wV.js} +2 -2
  11. package/dist/{IpynbViewer-B7p9hxFx.js.map → IpynbViewer-DJw34-wV.js.map} +1 -1
  12. package/dist/{MermaidViewer-O9vGvp4y.js → MermaidViewer-C86jjm27.js} +2 -2
  13. package/dist/{MermaidViewer-O9vGvp4y.js.map → MermaidViewer-C86jjm27.js.map} +1 -1
  14. package/dist/{PsdViewer-C6EaEAxF.js → PsdViewer-DGHIqv-x.js} +2 -2
  15. package/dist/{PsdViewer-C6EaEAxF.js.map → PsdViewer-DGHIqv-x.js.map} +1 -1
  16. package/dist/{TiffViewer-Dod4uIXI.js → TiffViewer-gct9Ub3o.js} +2 -2
  17. package/dist/{TiffViewer-Dod4uIXI.js.map → TiffViewer-gct9Ub3o.js.map} +1 -1
  18. package/dist/{Viewer3D-B3kLZrwO.js → Viewer3D-BVt4l2a9.js} +2 -2
  19. package/dist/{Viewer3D-B3kLZrwO.js.map → Viewer3D-BVt4l2a9.js.map} +1 -1
  20. package/dist/filex-core.js +1 -1
  21. package/dist/filex-core.umd.cjs +38 -38
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/index-BeUoODQq.js +6871 -0
  24. package/dist/index-BeUoODQq.js.map +1 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/style.css +1 -1
  27. package/package.json +1 -1
  28. package/src/FileExplorer.vue +393 -31
  29. package/src/components/Breadcrumb.vue +100 -5
  30. package/src/components/CommandPalette.vue +299 -0
  31. package/src/components/GridView.vue +8 -13
  32. package/src/components/ListView.vue +173 -20
  33. package/src/components/ShortcutsHelp.vue +59 -0
  34. package/src/components/Toolbar.vue +65 -1
  35. package/src/composables/useKeyboardShortcuts.ts +38 -0
  36. package/src/composables/useRealtime.ts +9 -1
  37. package/src/lib/fileIcons.ts +127 -0
  38. package/src/locales/en.ts +54 -0
  39. package/src/locales/tr.ts +54 -0
  40. package/src/styles/base.css +565 -0
  41. package/src/styles/variables.css +70 -0
  42. package/dist/index-DL6_eaM3.js +0 -5888
  43. 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 crumbs"
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 === crumbs.length - 1 }"
343
+ :class="{ 'is-last': i === tailCrumbs.length - 1 }"
249
344
  type="button"
250
- :aria-current="i === crumbs.length - 1 ? 'page' : undefined"
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 < crumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">›</span>
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
- function iconFor(n: FileNode): string {
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
- if (n.type === 'dir') return '📁';
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">{{ iconFor(n) }}</span>
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) }}