@brftech/filex-core 0.39.0 → 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.
@@ -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
- <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>
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
- 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'));
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
- if ((ev.ctrlKey || ev.metaKey) && (ev.key === 'k' || ev.key === 'K')) {
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 Ctrl+K command palette also runs commands.',
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 ? for the shortcut cheat-sheet and Ctrl+K for the command palette. Replay this tour any time via "Restart the tour" in the menu.',
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. Ctrl+K komut paleti ayrıca komut da çalıştırır.',
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
- '? tuşu kısayol kartını, Ctrl+K komut paletini açar. Bu turu menüdeki "Turu tekrar başlat" ile dilediğinde yeniden izleyebilirsin.',
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',
@@ -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
- .fe-ql-hint {
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)