@brftech/filex-core 0.39.0 → 0.40.0

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.
@@ -2029,9 +2029,39 @@ useKeyboardShortcuts(rootEl, {
2029
2029
  onTabNext: () => nextTab(),
2030
2030
  onTabPrev: () => prevTab(),
2031
2031
  /* /wiring:d1 */
2032
+ /* tus:t1 — the menu verbs, routed through the SAME dispatcher the right-click
2033
+ * menu and the toolbar use (dispatchItemAction), so a key and a click cannot
2034
+ * drift apart. The ones that act on a selection do nothing without one, which
2035
+ * is what the menu does too. */
2036
+ onNewFolder: () => {
2037
+ showNewFolder.value = true;
2038
+ },
2039
+ onUpload: () => triggerUpload(),
2040
+ onRefresh: () => void load(),
2041
+ onDownload: () => void dispatchItemAction('download', shortcutTargets()),
2042
+ onPreview: () => void dispatchItemAction('preview', shortcutTargets()),
2043
+ onShare: () => void dispatchItemAction('access', shortcutTargets()),
2044
+ onTags: () => void dispatchItemAction('tags', shortcutTargets()),
2045
+ onConvert: () => void dispatchItemAction('convert', shortcutTargets()),
2046
+ onOpenTab: () => void dispatchItemAction('open-tab', shortcutTargets()),
2047
+ onCopyPath: () => {
2048
+ const n = shortcutTargets()[0];
2049
+ if (n) void onCopyPath(n.path);
2050
+ },
2051
+ onCopyId: () => void dispatchItemAction('copy-id', shortcutTargets()),
2052
+ onRestore: () => void dispatchItemAction('restore', shortcutTargets()),
2053
+ /* /tus:t1 */
2032
2054
  hasSelection: () => !selection.isEmpty.value,
2033
2055
  });
2034
2056
 
2057
+ /* tus:t1 — which rows a keyboard verb acts on: the active pane's selection when
2058
+ * the split pane has focus, otherwise the main listing's. Same rule the delete
2059
+ * and rename shortcuts have followed since wiring:d1. */
2060
+ function shortcutTargets(): FileNode[] {
2061
+ if (paneIsActive.value) return splitPaneRef.value?.selectedNodes() ?? [];
2062
+ return selection.nodes.value;
2063
+ }
2064
+
2035
2065
  // --------------------------------------------------------------------
2036
2066
  // Actions
2037
2067
  // --------------------------------------------------------------------
@@ -10,6 +10,7 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
10
10
  import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
11
11
  import type { FileNode } from '../types/FileNode';
12
12
  import { useLocale } from '../composables/useLocale';
13
+ import { menuShortcutHint } from '../composables/useKeyboardShortcuts';
13
14
 
14
15
  export interface ContextAction {
15
16
  key: string;
@@ -19,6 +20,12 @@ export interface ContextAction {
19
20
  disabled?: boolean;
20
21
  hidden?: boolean;
21
22
  divider?: boolean;
23
+ /**
24
+ * tus:t1 — registry action id whose key this row should print. Optional:
25
+ * rows whose `key` already matches the registry (rename, delete, copy…)
26
+ * are resolved from the shared map and need nothing here.
27
+ */
28
+ shortcutId?: string;
22
29
  }
23
30
 
24
31
  const props = defineProps<{
@@ -48,6 +55,12 @@ const emit = defineEmits<{
48
55
 
49
56
  const { t } = useLocale(() => props.locale); // bag:b4 — sheet aria labels need t()
50
57
 
58
+ /* tus:t1 — a row's key label. `shortcutId` wins when an embedder sets one;
59
+ * otherwise the row's own key is looked up in the shared menu map. */
60
+ function hintFor(a: ContextAction): string {
61
+ return menuShortcutHint(a.shortcutId ?? a.key);
62
+ }
63
+
51
64
  const open = ref(false);
52
65
  const x = ref(0);
53
66
  const y = ref(0);
@@ -320,6 +333,7 @@ defineExpose({ show, hide });
320
333
  >
321
334
  <span v-if="a.icon" class="fe-ctx__icon" aria-hidden="true">{{ a.icon }}</span>
322
335
  <span class="fe-ctx__label">{{ a.label }}</span>
336
+ <span v-if="hintFor(a)" class="fe-ctx__key" aria-hidden="true">{{ hintFor(a) }}</span>
323
337
  </button>
324
338
  </template>
325
339
  </div>
@@ -347,6 +361,10 @@ defineExpose({ show, hide });
347
361
  >
348
362
  <span v-if="a.icon" class="fe-ctx__icon" aria-hidden="true">{{ a.icon }}</span>
349
363
  <span class="fe-ctx__label">{{ a.label }}</span>
364
+ <!-- tus:t1 — the key that does this from the keyboard, read from
365
+ the registry so a remap reaches it. Empty for a verb with no
366
+ binding, and the span then does not render at all. -->
367
+ <span v-if="hintFor(a)" class="fe-ctx__key" aria-hidden="true">{{ hintFor(a) }}</span>
350
368
  </button>
351
369
  </template>
352
370
  </div>
@@ -19,6 +19,7 @@
19
19
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
20
20
  import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
21
21
  import { useLocale } from '../composables/useLocale';
22
+ import { shortcutHint } from '../composables/useKeyboardShortcuts';
22
23
 
23
24
  const props = defineProps<{
24
25
  open: boolean;
@@ -34,6 +35,15 @@ const emit = defineEmits<{
34
35
 
35
36
  const { t } = useLocale(() => props.locale);
36
37
 
38
+ /* The tour teaches two keys by name, and both are remappable. Read them
39
+ * from the registry so the sentence stays true for whoever is taking the
40
+ * tour — a walkthrough that names the wrong key is worse than one that
41
+ * names none. */
42
+ const hintCombos = computed(() => ({
43
+ palette: shortcutHint('palette'),
44
+ help: shortcutHint('help'),
45
+ }));
46
+
37
47
  // ------------------------------------------------------------------
38
48
  // Step definitions. `target` returns the element to spotlight (null =
39
49
  // centered card, e.g. the closing shortcuts step). Buttons without a
@@ -299,7 +309,7 @@ const themeClass = computed(() => `fe-ctx-backdrop--theme-${props.theme || 'auto
299
309
  {{ t('tour.progress', { n: stepIdx + 1, m: total }) }}
300
310
  </p>
301
311
  <h3 class="fe-tour__title">{{ t(step.titleKey) }}</h3>
302
- <p class="fe-tour__desc">{{ t(step.descKey) }}</p>
312
+ <p class="fe-tour__desc">{{ t(step.descKey, hintCombos) }}</p>
303
313
  <div class="fe-tour__dots" aria-hidden="true">
304
314
  <span
305
315
  v-for="(s, i) in activeSteps"
@@ -6,11 +6,15 @@
6
6
  * mode), so every viewer the modal knows (image/video/pdf/office/code/
7
7
  * 3D/archive/…) works in quick-look for free. What this layer adds:
8
8
  *
9
- * - Space closes again (macOS Quick Look convention)
9
+ * - the quick-look key closes the peek again (Space by default — macOS
10
+ * Quick Look convention), and the open key promotes it into the full
11
+ * open flow (Enter by default, emit 'open-full'). Both come from the
12
+ * shortcut registry rather than from a comparison against the default
13
+ * key, so a remap reaches the overlay too
10
14
  * - ← ↑ / → ↓ ask the host to move the selection; the host keeps the
11
- * `file` prop in sync so the preview follows the selection
12
- * - Enter promotes the peek into the full open flow (emit 'open-full')
13
- * - a small floating hint bar with the key legend
15
+ * `file` prop in sync so the preview follows the selection. These are
16
+ * the peek's own arrows, not registry actions
17
+ * - a small floating hint bar naming those keys, read from the registry
14
18
  *
15
19
  * Keys are handled in window CAPTURE phase with stopPropagation so the
16
20
  * global shortcut registry never double-handles them; events that
@@ -18,10 +22,11 @@
18
22
  * input) are left alone. Esc is untouched — PreviewModal's own Modal
19
23
  * already closes on it.
20
24
  */
21
- import { onBeforeUnmount, watch } from 'vue';
25
+ import { computed, onBeforeUnmount, watch } from 'vue';
22
26
  import type { FileNode } from '../types/FileNode';
23
27
  import type { LocaleCode } from '../types/ExplorerConfig';
24
28
  import { useLocale } from '../composables/useLocale';
29
+ import { eventMatchesShortcut, shortcutHint } from '../composables/useKeyboardShortcuts';
25
30
  import PreviewModal from '../modals/PreviewModal.vue';
26
31
 
27
32
  const props = defineProps<{
@@ -61,16 +66,38 @@ function inFormControl(target: EventTarget | null): boolean {
61
66
  );
62
67
  }
63
68
 
69
+ /**
70
+ * The peek's own arrows. These are NOT registry actions — they only
71
+ * mean anything while the overlay is up — so they are declared once
72
+ * here and the hint bar prints this same constant. Two copies of a key
73
+ * name is how a legend starts lying about the key it names.
74
+ */
75
+ const NAV_KEYS = ['↑', '↓'] as const;
76
+
64
77
  function onKeydown(e: KeyboardEvent) {
65
78
  if (!props.open) return;
66
- if (e.ctrlKey || e.metaKey || e.altKey) return;
67
79
  if (inFormControl(e.target)) return;
80
+
81
+ // ⚠ The registry first, and BEFORE the modifier bail-out: the user may
82
+ // have remapped quick-look onto a combo that carries Ctrl or Alt. The
83
+ // old version compared `e.key` to a hardcoded ' ' / 'Enter', so after a
84
+ // remap the peek opened on the new key and still closed on the old one
85
+ // — and the hint bar named the old one.
86
+ if (eventMatchesShortcut(e, 'quicklook')) {
87
+ e.preventDefault();
88
+ e.stopPropagation();
89
+ emit('close');
90
+ return;
91
+ }
92
+ if (eventMatchesShortcut(e, 'open')) {
93
+ e.preventDefault();
94
+ e.stopPropagation();
95
+ emit('open-full');
96
+ return;
97
+ }
98
+
99
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
68
100
  switch (e.key) {
69
- case ' ':
70
- e.preventDefault();
71
- e.stopPropagation();
72
- emit('close');
73
- break;
74
101
  case 'ArrowRight':
75
102
  case 'ArrowDown':
76
103
  e.preventDefault();
@@ -83,14 +110,23 @@ function onKeydown(e: KeyboardEvent) {
83
110
  e.stopPropagation();
84
111
  emit('nav', -1);
85
112
  break;
86
- case 'Enter':
87
- e.preventDefault();
88
- e.stopPropagation();
89
- emit('open-full');
90
- break;
91
113
  }
92
114
  }
93
115
 
116
+ /**
117
+ * The legend, read from the live registry. A segment whose action the
118
+ * user has unbound disappears rather than printing an empty key cap.
119
+ */
120
+ const hintSegments = computed(() => {
121
+ const segs: Array<{ id: string; keys: string[]; label: string }> = [];
122
+ const close = shortcutHint('quicklook');
123
+ if (close) segs.push({ id: 'quicklook', keys: [close], label: t('quicklook.hint_close') });
124
+ segs.push({ id: 'nav', keys: [...NAV_KEYS], label: t('quicklook.hint_nav') });
125
+ const open = shortcutHint('open');
126
+ if (open) segs.push({ id: 'open', keys: [open], label: t('quicklook.hint_open') });
127
+ return segs;
128
+ });
129
+
94
130
  watch(
95
131
  () => props.open,
96
132
  (open) => {
@@ -122,21 +158,37 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true));
122
158
  :viewer-base-url="viewerBaseUrl"
123
159
  @close="emit('close')"
124
160
  />
125
- <transition name="fe-toast">
126
- <div
127
- v-if="open"
128
- class="fe fe-ql-hint"
129
- :class="{
130
- 'fe--theme-light': theme === 'light',
131
- 'fe--theme-dark': theme === 'dark',
132
- }"
133
- aria-hidden="true"
134
- >
135
- <kbd class="fe-kbd">Space</kbd> {{ t('quicklook.hint_close') }}
136
- <span class="fe-ql-hint__sep">·</span>
137
- <kbd class="fe-kbd">↑</kbd><kbd class="fe-kbd">↓</kbd> {{ t('quicklook.hint_nav') }}
138
- <span class="fe-ql-hint__sep">·</span>
139
- <kbd class="fe-kbd">Enter</kbd> {{ t('quicklook.hint_open') }}
140
- </div>
141
- </transition>
161
+ <!--
162
+ Teleported under <body> for the same reason as ContextMenu and
163
+ OnboardingTour: the hint carries the `fe` root class (that is how it
164
+ reaches the `--fe-*` theme variables), so while it sits inside the
165
+ explorer tree ANY host rule that reaches `.fe` by descendant also
166
+ reaches the hint — and a host selector is always more specific than
167
+ our own single class. web/src/views/Explore.vue sizes the embedded
168
+ explorer with `.explore-host[data-v-…] .fe { height: 100% }`; that
169
+ stretched the hint pill to the full viewport height (issue #22).
170
+ Teleporting takes it out of every host container, so only the
171
+ package's own rules can reach it.
172
+ -->
173
+ <Teleport to="body">
174
+ <transition name="fe-toast">
175
+ <div
176
+ v-if="open"
177
+ class="fe fe-ql-hint"
178
+ :class="{
179
+ 'fe--theme-light': theme === 'light',
180
+ 'fe--theme-dark': theme === 'dark',
181
+ }"
182
+ aria-hidden="true"
183
+ >
184
+ <template v-for="(seg, i) in hintSegments" :key="seg.id">
185
+ <span v-if="i > 0" class="fe-ql-hint__sep">·</span>
186
+ <span class="fe-ql-hint__seg">
187
+ <kbd v-for="k in seg.keys" :key="k" class="fe-kbd">{{ k }}</kbd>
188
+ {{ seg.label }}
189
+ </span>
190
+ </template>
191
+ </div>
192
+ </transition>
193
+ </Teleport>
142
194
  </template>
@@ -24,6 +24,7 @@ import {
24
24
  comboFromEvent,
25
25
  effectiveCombo,
26
26
  findShortcutConflict,
27
+ isReservedCombo,
27
28
  resetAllShortcuts,
28
29
  resetShortcut,
29
30
  setShortcutOverride,
@@ -66,6 +67,8 @@ const anyOverridden = computed(() => list.value.some((s) => s.overridden));
66
67
 
67
68
  const capturingId = ref<string | null>(null);
68
69
  const conflict = ref<{ id: string; combo: string; conflictId: string; fixed: boolean } | null>(null);
70
+ /* tus:t1 — the user pressed something the browser keeps for itself. */
71
+ const reserved = ref<{ id: string; combo: string } | null>(null);
69
72
 
70
73
  function labelOf(id: string): string {
71
74
  const def = SHORTCUT_ACTIONS.find((a) => a.id === id);
@@ -74,6 +77,7 @@ function labelOf(id: string): string {
74
77
 
75
78
  function beginCapture(id: string) {
76
79
  conflict.value = null;
80
+ reserved.value = null;
77
81
  capturingId.value = id;
78
82
  }
79
83
 
@@ -84,6 +88,13 @@ function cancelCapture() {
84
88
  function attemptAssign(id: string, combo: string) {
85
89
  capturingId.value = null;
86
90
  if (combo === effectiveCombo(id)) return; // unchanged
91
+ /* tus:t1 — a combo the browser takes before the page sees it would be a key
92
+ * that does nothing where the user is standing. Refuse it and say why,
93
+ * rather than storing a binding that never fires. */
94
+ if (isReservedCombo(combo)) {
95
+ reserved.value = { id, combo };
96
+ return;
97
+ }
87
98
  const clash = findShortcutConflict(combo, id);
88
99
  if (clash) {
89
100
  conflict.value = { id, combo, conflictId: clash.id, fixed: clash.fixed };
@@ -104,13 +115,19 @@ function dismissConflict() {
104
115
  conflict.value = null;
105
116
  }
106
117
 
118
+ function dismissReserved() {
119
+ reserved.value = null;
120
+ }
121
+
107
122
  function onReset(id: string) {
108
123
  conflict.value = null;
124
+ reserved.value = null;
109
125
  resetShortcut(id);
110
126
  }
111
127
 
112
128
  function onResetAll() {
113
129
  conflict.value = null;
130
+ reserved.value = null;
114
131
  capturingId.value = null;
115
132
  resetAllShortcuts();
116
133
  }
@@ -145,6 +162,7 @@ watch(
145
162
  window.removeEventListener('keydown', onWindowKeydown, true);
146
163
  capturingId.value = null;
147
164
  conflict.value = null;
165
+ reserved.value = null;
148
166
  }
149
167
  },
150
168
  );
@@ -201,6 +219,14 @@ function onClose() {
201
219
  </span>
202
220
  <template v-for="(combo, i) in s.keys" v-else :key="combo">
203
221
  <kbd class="fe-kbd">{{ combo }}</kbd>
222
+ <!-- tus:t1 — this combo never reaches a browser tab. Say so
223
+ on the row rather than letting the user discover it by
224
+ pressing the key and watching nothing happen. -->
225
+ <span
226
+ v-if="isReservedCombo(combo)"
227
+ class="fe-shortset__reserved"
228
+ :title="t('shortcuts.settings.reserved_title')"
229
+ >{{ t('shortcuts.settings.reserved_badge') }}</span>
204
230
  <span v-if="i < s.keys.length - 1" class="fe-shortcuts__or" aria-hidden="true">/</span>
205
231
  </template>
206
232
  </template>
@@ -260,6 +286,17 @@ function onClose() {
260
286
  </button>
261
287
  </span>
262
288
  </div>
289
+ <div v-if="reserved && reserved.id === s.id" class="fe-shortset__conflict" role="alert">
290
+ <span class="fe-shortset__conflict-msg">
291
+ <kbd class="fe-kbd">{{ reserved.combo }}</kbd>
292
+ {{ t('shortcuts.settings.reserved') }}
293
+ </span>
294
+ <span class="fe-shortset__conflict-actions">
295
+ <button type="button" class="fe-btn fe-shortset__btn" @click="dismissReserved">
296
+ {{ t('shortcuts.settings.conflict_cancel') }}
297
+ </button>
298
+ </span>
299
+ </div>
263
300
  </div>
264
301
  </section>
265
302
  </div>
@@ -17,6 +17,7 @@ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
17
17
  import ContextMenu, { type ContextAction } from './ContextMenu.vue';
18
18
  import ViewSwitcher from './ViewSwitcher.vue';
19
19
  import { useLocale } from '../composables/useLocale';
20
+ import { eventMatchesShortcut, menuShortcutHint, shortcutHint } from '../composables/useKeyboardShortcuts';
20
21
 
21
22
  export type SelectionMode = 'none' | 'single-file' | 'single-dir' | 'multi';
22
23
 
@@ -381,10 +382,20 @@ const moreActions = computed<ContextAction[]>(() => {
381
382
  * what the hint says, and Ctrl+K from anywhere else still opens the palette
382
383
  * as it has since cila:c.
383
384
  */
384
- const macLike =
385
- typeof navigator !== 'undefined' &&
386
- /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || '');
387
- const paletteCombo = computed(() => (macLike ? '⌘ K' : 'Ctrl K'));
385
+ /* Read from the registry, not written here. This chip used to print a
386
+ * hardcoded `⌘ K` / `Ctrl K`, which stopped being true the moment anyone
387
+ * remapped the palette in the shortcut settings — the chip then named a
388
+ * key that did nothing, on the one control whose whole job is to teach
389
+ * that key. */
390
+ const paletteCombo = computed(() => shortcutHint('palette'));
391
+
392
+ /* tus:t1 — a button's tooltip names its key. Same source as the right-click
393
+ * menu's key column, so the two can only ever say the same thing, and a verb
394
+ * with no binding gets its plain label back rather than an empty bracket. */
395
+ function withKey(label: string, key: string): string {
396
+ const combo = menuShortcutHint(key);
397
+ return combo ? `${label} (${combo})` : label;
398
+ }
388
399
 
389
400
  const drivePlaceholder = computed(() =>
390
401
  props.scopeLabel
@@ -396,7 +407,9 @@ const drivePlaceholder = computed(() =>
396
407
  * keystrokes aimed at an input — which is correct, and it would otherwise
397
408
  * make the hint on this very field a lie. */
398
409
  function onSearchKeydown(ev: KeyboardEvent) {
399
- if ((ev.ctrlKey || ev.metaKey) && (ev.key === 'k' || ev.key === 'K')) {
410
+ // The registry decides, so the chip beside this field and the key that
411
+ // actually escalates stay the same key after a remap.
412
+ if (eventMatchesShortcut(ev, 'palette')) {
400
413
  ev.preventDefault();
401
414
  ev.stopPropagation();
402
415
  emit('open-palette', localSearch.value);
@@ -504,7 +517,7 @@ function onMoreSelect(a: ContextAction) {
504
517
  v-if="canGoUp"
505
518
  type="button"
506
519
  class="fe-btn fe-btn--icon-only"
507
- :title="t('toolbar.go_up')"
520
+ :title="withKey(t('toolbar.go_up'), 'go-up')"
508
521
  :aria-label="t('toolbar.go_up')"
509
522
  @click="emit('go-up')"
510
523
  >
@@ -518,7 +531,7 @@ function onMoreSelect(a: ContextAction) {
518
531
  v-if="shell !== 'drive' && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
519
532
  type="button"
520
533
  class="fe-btn fe-btn--primary"
521
- :title="t('toolbar.new_folder')"
534
+ :title="withKey(t('toolbar.new_folder'), 'new-folder')"
522
535
  @click="emit('new-folder')"
523
536
  >
524
537
  <span class="fe-icon">📁</span>
@@ -532,7 +545,7 @@ function onMoreSelect(a: ContextAction) {
532
545
  class="fe-btn fe-btn--fold"
533
546
  :class="{ 'fe-btn--danger': a.danger, 'is-disabled': a.disabled }"
534
547
  :disabled="a.disabled"
535
- :title="a.label"
548
+ :title="withKey(a.label, a.key)"
536
549
  @click="fire(a.key)"
537
550
  >
538
551
  <span class="fe-icon">{{ a.icon }}</span>
@@ -573,7 +586,7 @@ function onMoreSelect(a: ContextAction) {
573
586
  v-if="pasteEnabled && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
574
587
  type="button"
575
588
  class="fe-btn"
576
- :title="t('ctx.paste')"
589
+ :title="withKey(t('ctx.paste'), 'paste')"
577
590
  @click="fire('paste')"
578
591
  >
579
592
  <span class="fe-icon">📋</span>
@@ -636,7 +649,7 @@ function onMoreSelect(a: ContextAction) {
636
649
  v-if="shell !== 'drive' && !atVirtualRoot && canWrite !== false"
637
650
  type="button"
638
651
  class="fe-btn fe-btn--icon-only"
639
- :title="t('toolbar.upload')"
652
+ :title="withKey(t('toolbar.upload'), 'upload')"
640
653
  :aria-label="t('toolbar.upload') /* wiring:c4 */"
641
654
  @click="emit('upload')"
642
655
  >
@@ -645,7 +658,7 @@ function onMoreSelect(a: ContextAction) {
645
658
  <button
646
659
  type="button"
647
660
  class="fe-btn fe-btn--icon-only"
648
- :title="t('toolbar.refresh')"
661
+ :title="withKey(t('toolbar.refresh'), 'refresh')"
649
662
  :aria-label="t('toolbar.refresh') /* wiring:c4 */"
650
663
  @click="emit('refresh')"
651
664
  >
@@ -717,7 +730,7 @@ function onMoreSelect(a: ContextAction) {
717
730
  class="fe-btn fe-btn--icon-only fe-toolbar__inspector"
718
731
  :class="{ 'is-active': inspectorOpen }"
719
732
  :aria-pressed="!!inspectorOpen"
720
- :title="t('toolbar.inspector')"
733
+ :title="withKey(t('toolbar.inspector'), 'inspector')"
721
734
  :aria-label="t('toolbar.inspector')"
722
735
  @click="emit('toggle-inspector')"
723
736
  >
@@ -917,7 +930,7 @@ function onMoreSelect(a: ContextAction) {
917
930
  v-if="canGoUp"
918
931
  type="button"
919
932
  class="fe-btn fe-btn--icon-only"
920
- :title="t('toolbar.go_up')"
933
+ :title="withKey(t('toolbar.go_up'), 'go-up')"
921
934
  :aria-label="t('toolbar.go_up')"
922
935
  @click="emit('go-up')"
923
936
  >
@@ -930,7 +943,7 @@ function onMoreSelect(a: ContextAction) {
930
943
  type="button"
931
944
  class="fe-btn fe-btn--icon-only fe-toolbar__search-toggle"
932
945
  :class="{ 'is-active': !!localSearch }"
933
- :title="t('toolbar.search')"
946
+ :title="withKey(t('toolbar.search'), 'search')"
934
947
  :aria-label="t('toolbar.search')"
935
948
  @click="openSearch"
936
949
  >
@@ -940,7 +953,7 @@ function onMoreSelect(a: ContextAction) {
940
953
  v-if="!atVirtualRoot && canWrite !== false"
941
954
  type="button"
942
955
  class="fe-btn fe-btn--icon-only"
943
- :title="t('toolbar.upload')"
956
+ :title="withKey(t('toolbar.upload'), 'upload')"
944
957
  :aria-label="t('toolbar.upload')"
945
958
  @click="emit('upload')"
946
959
  >