@brftech/filex-core 0.38.2 → 0.39.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 +9 -0
- package/dist/filex-core.js +6889 -6847
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +45 -45
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +33 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/OnboardingTour.vue +11 -1
- package/src/components/QuickLook.vue +77 -29
- package/src/components/Toolbar.vue +10 -5
- package/src/composables/useKeyboardShortcuts.ts +53 -0
- package/src/index.ts +7 -0
- package/src/locales/en.ts +2 -2
- package/src/locales/tr.ts +2 -2
- package/src/styles/base.css +16 -2
|
@@ -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"
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
* input) are left alone. Esc is untouched — PreviewModal's own Modal
|
|
19
19
|
* already closes on it.
|
|
20
20
|
*/
|
|
21
|
-
import { onBeforeUnmount, watch } from 'vue';
|
|
21
|
+
import { computed, onBeforeUnmount, watch } from 'vue';
|
|
22
22
|
import type { FileNode } from '../types/FileNode';
|
|
23
23
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
24
24
|
import { useLocale } from '../composables/useLocale';
|
|
25
|
+
import { eventMatchesShortcut, shortcutHint } from '../composables/useKeyboardShortcuts';
|
|
25
26
|
import PreviewModal from '../modals/PreviewModal.vue';
|
|
26
27
|
|
|
27
28
|
const props = defineProps<{
|
|
@@ -61,16 +62,38 @@ function inFormControl(target: EventTarget | null): boolean {
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The peek's own arrows. These are NOT registry actions — they only
|
|
67
|
+
* mean anything while the overlay is up — so they are declared once
|
|
68
|
+
* here and the hint bar prints this same constant. Two copies of a key
|
|
69
|
+
* name is how a legend starts lying about the key it names.
|
|
70
|
+
*/
|
|
71
|
+
const NAV_KEYS = ['↑', '↓'] as const;
|
|
72
|
+
|
|
64
73
|
function onKeydown(e: KeyboardEvent) {
|
|
65
74
|
if (!props.open) return;
|
|
66
|
-
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
67
75
|
if (inFormControl(e.target)) return;
|
|
76
|
+
|
|
77
|
+
// ⚠ The registry first, and BEFORE the modifier bail-out: the user may
|
|
78
|
+
// have remapped quick-look onto a combo that carries Ctrl or Alt. The
|
|
79
|
+
// old version compared `e.key` to a hardcoded ' ' / 'Enter', so after a
|
|
80
|
+
// remap the peek opened on the new key and still closed on the old one
|
|
81
|
+
// — and the hint bar named the old one.
|
|
82
|
+
if (eventMatchesShortcut(e, 'quicklook')) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
emit('close');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (eventMatchesShortcut(e, 'open')) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
emit('open-full');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
68
96
|
switch (e.key) {
|
|
69
|
-
case ' ':
|
|
70
|
-
e.preventDefault();
|
|
71
|
-
e.stopPropagation();
|
|
72
|
-
emit('close');
|
|
73
|
-
break;
|
|
74
97
|
case 'ArrowRight':
|
|
75
98
|
case 'ArrowDown':
|
|
76
99
|
e.preventDefault();
|
|
@@ -83,14 +106,23 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
83
106
|
e.stopPropagation();
|
|
84
107
|
emit('nav', -1);
|
|
85
108
|
break;
|
|
86
|
-
case 'Enter':
|
|
87
|
-
e.preventDefault();
|
|
88
|
-
e.stopPropagation();
|
|
89
|
-
emit('open-full');
|
|
90
|
-
break;
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The legend, read from the live registry. A segment whose action the
|
|
114
|
+
* user has unbound disappears rather than printing an empty key cap.
|
|
115
|
+
*/
|
|
116
|
+
const hintSegments = computed(() => {
|
|
117
|
+
const segs: Array<{ id: string; keys: string[]; label: string }> = [];
|
|
118
|
+
const close = shortcutHint('quicklook');
|
|
119
|
+
if (close) segs.push({ id: 'quicklook', keys: [close], label: t('quicklook.hint_close') });
|
|
120
|
+
segs.push({ id: 'nav', keys: [...NAV_KEYS], label: t('quicklook.hint_nav') });
|
|
121
|
+
const open = shortcutHint('open');
|
|
122
|
+
if (open) segs.push({ id: 'open', keys: [open], label: t('quicklook.hint_open') });
|
|
123
|
+
return segs;
|
|
124
|
+
});
|
|
125
|
+
|
|
94
126
|
watch(
|
|
95
127
|
() => props.open,
|
|
96
128
|
(open) => {
|
|
@@ -122,21 +154,37 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true));
|
|
|
122
154
|
:viewer-base-url="viewerBaseUrl"
|
|
123
155
|
@close="emit('close')"
|
|
124
156
|
/>
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
<!--
|
|
158
|
+
Teleported under <body> for the same reason as ContextMenu and
|
|
159
|
+
OnboardingTour: the hint carries the `fe` root class (that is how it
|
|
160
|
+
reaches the `--fe-*` theme variables), so while it sits inside the
|
|
161
|
+
explorer tree ANY host rule that reaches `.fe` by descendant also
|
|
162
|
+
reaches the hint — and a host selector is always more specific than
|
|
163
|
+
our own single class. web/src/views/Explore.vue sizes the embedded
|
|
164
|
+
explorer with `.explore-host[data-v-…] .fe { height: 100% }`; that
|
|
165
|
+
stretched the hint pill to the full viewport height (issue #22).
|
|
166
|
+
Teleporting takes it out of every host container, so only the
|
|
167
|
+
package's own rules can reach it.
|
|
168
|
+
-->
|
|
169
|
+
<Teleport to="body">
|
|
170
|
+
<transition name="fe-toast">
|
|
171
|
+
<div
|
|
172
|
+
v-if="open"
|
|
173
|
+
class="fe fe-ql-hint"
|
|
174
|
+
:class="{
|
|
175
|
+
'fe--theme-light': theme === 'light',
|
|
176
|
+
'fe--theme-dark': theme === 'dark',
|
|
177
|
+
}"
|
|
178
|
+
aria-hidden="true"
|
|
179
|
+
>
|
|
180
|
+
<template v-for="(seg, i) in hintSegments" :key="seg.id">
|
|
181
|
+
<span v-if="i > 0" class="fe-ql-hint__sep">·</span>
|
|
182
|
+
<span class="fe-ql-hint__seg">
|
|
183
|
+
<kbd v-for="k in seg.keys" :key="k" class="fe-kbd">{{ k }}</kbd>
|
|
184
|
+
{{ seg.label }}
|
|
185
|
+
</span>
|
|
186
|
+
</template>
|
|
187
|
+
</div>
|
|
188
|
+
</transition>
|
|
189
|
+
</Teleport>
|
|
142
190
|
</template>
|
|
@@ -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, shortcutHint } from '../composables/useKeyboardShortcuts';
|
|
20
21
|
|
|
21
22
|
export type SelectionMode = 'none' | 'single-file' | 'single-dir' | 'multi';
|
|
22
23
|
|
|
@@ -381,10 +382,12 @@ 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'));
|
|
388
391
|
|
|
389
392
|
const drivePlaceholder = computed(() =>
|
|
390
393
|
props.scopeLabel
|
|
@@ -396,7 +399,9 @@ const drivePlaceholder = computed(() =>
|
|
|
396
399
|
* keystrokes aimed at an input — which is correct, and it would otherwise
|
|
397
400
|
* make the hint on this very field a lie. */
|
|
398
401
|
function onSearchKeydown(ev: KeyboardEvent) {
|
|
399
|
-
|
|
402
|
+
// The registry decides, so the chip beside this field and the key that
|
|
403
|
+
// actually escalates stay the same key after a remap.
|
|
404
|
+
if (eventMatchesShortcut(ev, 'palette')) {
|
|
400
405
|
ev.preventDefault();
|
|
401
406
|
ev.stopPropagation();
|
|
402
407
|
emit('open-palette', localSearch.value);
|
|
@@ -314,6 +314,59 @@ export function useShortcutList(): ComputedRef<ShortcutView[]> {
|
|
|
314
314
|
);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// --------------------------------------------------------------------
|
|
318
|
+
// Hint surfaces
|
|
319
|
+
// --------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Is this a Mac-style keyboard? Only affects how a combo is PRINTED —
|
|
323
|
+
* the canonical form folds Meta into Ctrl, so one saved combo works on
|
|
324
|
+
* every platform and only the label differs.
|
|
325
|
+
*/
|
|
326
|
+
export function isMacLike(): boolean {
|
|
327
|
+
return (
|
|
328
|
+
typeof navigator !== 'undefined' &&
|
|
329
|
+
/mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || '')
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Canonical combo → what a human should see. '' stays ''. */
|
|
334
|
+
export function comboLabel(combo: string): string {
|
|
335
|
+
if (!combo) return '';
|
|
336
|
+
return isMacLike() ? combo.replace(/\bCtrl\b/g, '⌘') : combo;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The one way a hint may name a key.
|
|
341
|
+
*
|
|
342
|
+
* ⚠ Never write a key into a hint by hand — not in a template, not in a
|
|
343
|
+
* locale string. Every combo in this registry is remappable, so a
|
|
344
|
+
* hardcoded "Ctrl+K" is true only until someone opens the shortcut
|
|
345
|
+
* settings, and then it is a label that tells the user to press a key
|
|
346
|
+
* that does nothing. Call this instead and interpolate the result;
|
|
347
|
+
* `web/tests/ui/shortcutHints.test.ts` fails the build on a literal.
|
|
348
|
+
*
|
|
349
|
+
* Returns '' when the action is unbound, so a caller can drop the whole
|
|
350
|
+
* segment rather than print an empty key cap.
|
|
351
|
+
*/
|
|
352
|
+
export function shortcutHint(id: string): string {
|
|
353
|
+
return comboLabel(effectiveCombo(id));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Does this event fire the given registry action? For overlays that
|
|
358
|
+
* handle their own keys (the quick-look peek) rather than going through
|
|
359
|
+
* the global binder — without this they keep answering to the DEFAULT
|
|
360
|
+
* key after the user has remapped the action.
|
|
361
|
+
*/
|
|
362
|
+
export function eventMatchesShortcut(e: KeyboardEvent, id: string): boolean {
|
|
363
|
+
const combo = effectiveCombo(id);
|
|
364
|
+
if (!combo) return false;
|
|
365
|
+
const fired = comboFromEvent(e);
|
|
366
|
+
if (fired === combo) return true;
|
|
367
|
+
return defOf(id)?.fixedCombos?.includes(fired ?? '') ?? false;
|
|
368
|
+
}
|
|
369
|
+
|
|
317
370
|
/**
|
|
318
371
|
* @deprecated Legacy static cheat-sheet shape (pre-registry). Kept for
|
|
319
372
|
* API compatibility; use `useShortcutList()` for the live, remap-aware
|
package/src/index.ts
CHANGED
|
@@ -139,6 +139,13 @@ export {
|
|
|
139
139
|
resetAllShortcuts,
|
|
140
140
|
findShortcutConflict,
|
|
141
141
|
comboFromEvent,
|
|
142
|
+
/* Hint surfaces: the only sanctioned way to NAME a key on screen. A hint
|
|
143
|
+
* written by hand is true until the user remaps that action, and then the
|
|
144
|
+
* product is telling them to press something that does nothing. */
|
|
145
|
+
shortcutHint,
|
|
146
|
+
comboLabel,
|
|
147
|
+
eventMatchesShortcut,
|
|
148
|
+
isMacLike,
|
|
142
149
|
} from './composables/useKeyboardShortcuts';
|
|
143
150
|
export type {
|
|
144
151
|
ShortcutActionDef,
|
package/src/locales/en.ts
CHANGED
|
@@ -366,7 +366,7 @@ export const en: Record<string, string> = {
|
|
|
366
366
|
'Pick files with this button — or simply drag & drop them anywhere in the window.',
|
|
367
367
|
'tour.step.search.title': 'Search',
|
|
368
368
|
'tour.step.search.desc':
|
|
369
|
-
'This box searches the whole storage by name — separators and one typo are forgiven, so "invoice 2026" finds invoice_2026.pdf. Add tag:invoice to filter by tag. The
|
|
369
|
+
'This box searches the whole storage by name — separators and one typo are forgiven, so "invoice 2026" finds invoice_2026.pdf. Add tag:invoice to filter by tag. The {palette} command palette also runs commands.',
|
|
370
370
|
'tour.step.view.title': 'Switch views',
|
|
371
371
|
'tour.step.view.desc':
|
|
372
372
|
'Toggle between the list and grid layout; your choice is remembered.',
|
|
@@ -375,7 +375,7 @@ export const en: Record<string, string> = {
|
|
|
375
375
|
'Right-click any file and use "Share / Permissions" to create a link with an optional PIN and expiry.',
|
|
376
376
|
'tour.step.help.title': 'Shortcuts',
|
|
377
377
|
'tour.step.help.desc':
|
|
378
|
-
'Press
|
|
378
|
+
'Press {help} for the shortcut cheat-sheet and {palette} for the command palette. Replay this tour any time via "Restart the tour" in the menu.',
|
|
379
379
|
'error.hint': 'This may be a temporary network or server problem. Please try again.',
|
|
380
380
|
'error.details': 'Technical details',
|
|
381
381
|
'col.star': 'Star',
|
package/src/locales/tr.ts
CHANGED
|
@@ -366,7 +366,7 @@ export const tr: Record<string, string> = {
|
|
|
366
366
|
'Bu düğmeyle dosya seçebilirsin; dosyaları doğrudan pencereye sürükleyip bırakmak da çalışır.',
|
|
367
367
|
'tour.step.search.title': 'Arama',
|
|
368
368
|
'tour.step.search.desc':
|
|
369
|
-
'Bu kutu deponun tamamında ada göre arar; ayraçları ve bir harflik yazım hatasını affeder, yani "fatura 2026" yazdığında fatura_2026.pdf gelir. tag:fatura yazarak etikete göre süzebilirsin.
|
|
369
|
+
'Bu kutu deponun tamamında ada göre arar; ayraçları ve bir harflik yazım hatasını affeder, yani "fatura 2026" yazdığında fatura_2026.pdf gelir. tag:fatura yazarak etikete göre süzebilirsin. {palette} komut paleti ayrıca komut da çalıştırır.',
|
|
370
370
|
'tour.step.view.title': 'Görünümü değiştir',
|
|
371
371
|
'tour.step.view.desc':
|
|
372
372
|
'Liste ve ızgara görünümü arasında geçiş yapabilirsin; seçimin hatırlanır.',
|
|
@@ -375,7 +375,7 @@ export const tr: Record<string, string> = {
|
|
|
375
375
|
'Bir dosyaya sağ tıklayıp "Paylaş / İzinler" ile bağlantı oluşturabilir, PIN ve son kullanma süresi ekleyebilirsin.',
|
|
376
376
|
'tour.step.help.title': 'Kısayollar',
|
|
377
377
|
'tour.step.help.desc':
|
|
378
|
-
'
|
|
378
|
+
'{help} tuşu kısayol kartını, {palette} komut paletini açar. Bu turu menüdeki "Turu tekrar başlat" ile dilediğinde yeniden izleyebilirsin.',
|
|
379
379
|
'error.hint': 'Bağlantı ya da sunucu kaynaklı geçici bir sorun olabilir. Yeniden deneyin.',
|
|
380
380
|
'error.details': 'Teknik ayrıntılar',
|
|
381
381
|
'col.star': 'Yıldız',
|
package/src/styles/base.css
CHANGED
|
@@ -2622,8 +2622,15 @@ filex-explorer {
|
|
|
2622
2622
|
/* Quick-look hint bar — floats above the PreviewModal backdrop (z 70).
|
|
2623
2623
|
* Carries the `.fe` class for the theme-variable cascade, so the `.fe`
|
|
2624
2624
|
* root layout defaults (column flex, min-height 420px, 100% height)
|
|
2625
|
-
* must be explicitly neutralised here.
|
|
2626
|
-
|
|
2625
|
+
* must be explicitly neutralised here.
|
|
2626
|
+
*
|
|
2627
|
+
* Written as `.fe.fe-ql-hint` on purpose: a single class ties with the
|
|
2628
|
+
* `.fe` root rule and would be decided by source order alone, which is
|
|
2629
|
+
* not something a host embed can be trusted to preserve. The hint is
|
|
2630
|
+
* also Teleported under <body> (QuickLook.vue) so a host container's
|
|
2631
|
+
* own `.fe` override cannot reach it by descendant — issue #22, where
|
|
2632
|
+
* the pill grew to the full viewport height inside the admin UI. */
|
|
2633
|
+
.fe.fe-ql-hint {
|
|
2627
2634
|
position: fixed;
|
|
2628
2635
|
left: 50%;
|
|
2629
2636
|
bottom: 14px;
|
|
@@ -2653,6 +2660,13 @@ filex-explorer {
|
|
|
2653
2660
|
.fe-ql-hint__sep {
|
|
2654
2661
|
opacity: 0.5;
|
|
2655
2662
|
}
|
|
2663
|
+
/* One key-cap group plus its label, kept together so the pill can only
|
|
2664
|
+
* break between segments. */
|
|
2665
|
+
.fe-ql-hint__seg {
|
|
2666
|
+
display: inline-flex;
|
|
2667
|
+
align-items: center;
|
|
2668
|
+
gap: 4px;
|
|
2669
|
+
}
|
|
2656
2670
|
/* /wiring:c2 */
|
|
2657
2671
|
/* =========================================================
|
|
2658
2672
|
* wiring:c3 — Operations center (unified upload + ops-queue surface)
|