@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.
- package/README.md +9 -0
- package/dist/filex-core.js +7339 -7014
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +49 -49
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +84 -11
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +30 -0
- package/src/components/ContextMenu.vue +18 -0
- package/src/components/OnboardingTour.vue +11 -1
- package/src/components/QuickLook.vue +85 -33
- package/src/components/ShortcutSettings.vue +37 -0
- package/src/components/Toolbar.vue +28 -15
- package/src/composables/useKeyboardShortcuts.ts +175 -0
- package/src/index.ts +12 -0
- package/src/lib/themes.ts +96 -2
- package/src/locales/en.ts +21 -2
- package/src/locales/tr.ts +21 -2
- package/src/styles/base.css +42 -2
- package/src/styles/variables.css +6 -0
package/src/FileExplorer.vue
CHANGED
|
@@ -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
|
-
* -
|
|
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
|
-
*
|
|
13
|
-
* - a small floating hint bar
|
|
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
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
>
|