@brftech/filex-core 0.4.2 → 0.5.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.
Files changed (51) hide show
  1. package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-DKW-HyXT.js} +2 -2
  2. package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-DKW-HyXT.js.map} +1 -1
  3. package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-D2HmP2yr.js} +2 -2
  4. package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-D2HmP2yr.js.map} +1 -1
  5. package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-DPNvOFCs.js} +2 -2
  6. package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-DPNvOFCs.js.map} +1 -1
  7. package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-ByqjNuNL.js} +2 -2
  8. package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-ByqjNuNL.js.map} +1 -1
  9. package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-2iONhbuJ.js} +2 -2
  10. package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-2iONhbuJ.js.map} +1 -1
  11. package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-CmiU7hIT.js} +2 -2
  12. package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-CmiU7hIT.js.map} +1 -1
  13. package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Inhl2yN9.js} +2 -2
  14. package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Inhl2yN9.js.map} +1 -1
  15. package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-C2AFFEze.js} +2 -2
  16. package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-C2AFFEze.js.map} +1 -1
  17. package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-YdW14q_n.js} +2 -2
  18. package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-YdW14q_n.js.map} +1 -1
  19. package/dist/filex-core.js +47 -25
  20. package/dist/filex-core.umd.cjs +40 -40
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-eARevpz0.js +9952 -0
  23. package/dist/index-eARevpz0.js.map +1 -0
  24. package/dist/index.d.ts +363 -3
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +269 -1
  28. package/src/components/CommandPalette.vue +12 -0
  29. package/src/components/ContextMenu.vue +114 -8
  30. package/src/components/GridView.vue +33 -1
  31. package/src/components/ListView.vue +39 -6
  32. package/src/components/OnboardingTour.vue +332 -0
  33. package/src/components/OperationsCenter.vue +284 -0
  34. package/src/components/PendingOpsTray.vue +50 -66
  35. package/src/components/QuickLook.vue +142 -0
  36. package/src/components/ShortcutSettings.vue +270 -0
  37. package/src/components/ShortcutsHelp.vue +30 -9
  38. package/src/components/ThemeGallery.vue +253 -0
  39. package/src/components/Toolbar.vue +52 -6
  40. package/src/components/UploadProgress.vue +49 -57
  41. package/src/composables/useKeyboardShortcuts.ts +321 -137
  42. package/src/composables/useOperations.ts +291 -0
  43. package/src/index.ts +45 -0
  44. package/src/lib/dragGhost.ts +41 -0
  45. package/src/lib/themes.ts +505 -0
  46. package/src/locales/en.ts +92 -0
  47. package/src/locales/tr.ts +92 -0
  48. package/src/modals/Modal.vue +41 -5
  49. package/src/styles/base.css +672 -0
  50. package/dist/index-BLocdbmc.js +0 -7993
  51. package/dist/index-BLocdbmc.js.map +0 -1
@@ -1,12 +1,26 @@
1
1
  /**
2
2
  * useKeyboardShortcuts — binds FileExplorer's keyboard affordances.
3
3
  *
4
+ * wiring:c2 — rewritten as a REGISTRY: every action is declared once
5
+ * (id + default combo + i18n label + category) and the user may remap
6
+ * the customizable ones. Overrides persist in localStorage under
7
+ * `filex.shortcuts` as `{ "<actionId>": "<combo>" }` — only deviations
8
+ * from the defaults are stored; an empty string means "unbound".
9
+ *
10
+ * Canonical combo format (also the display format): modifiers in
11
+ * `Ctrl+Alt+Shift` order followed by a single key token, joined with
12
+ * `+`. Key tokens: uppercase letters/digits, symbols as typed (`/`,
13
+ * `?`), `F1`–`F12`, `Space`, `Enter`, `Esc`, `Del`, `Backspace`, `Tab`,
14
+ * `Home`, `End`, `PgUp`, `PgDn` and arrows as `↑ ↓ ← →`. Meta (Cmd) is
15
+ * folded into `Ctrl` so one saved combo works on macOS too. For
16
+ * printable symbols Shift is omitted (`?` already encodes it).
17
+ *
4
18
  * Activated whenever the explorer is mounted (only one instance per
5
19
  * page is expected). Skips events that originate inside form controls
6
20
  * so the user can type filenames in the search box, modals, etc.
7
21
  */
8
22
 
9
- import { onMounted, onBeforeUnmount, type Ref } from 'vue';
23
+ import { computed, onMounted, onBeforeUnmount, ref, type ComputedRef, type Ref } from 'vue';
10
24
 
11
25
  export interface ShortcutHandlers {
12
26
  onDelete?: () => void;
@@ -18,47 +32,292 @@ export interface ShortcutHandlers {
18
32
  onOpen?: () => void; // Enter
19
33
  onClose?: () => void; // Escape
20
34
  onFocusSearch?: () => void; // /
21
- onDuplicate?: () => void; // Ctrl+D
35
+ onDuplicate?: () => void; // (unwired — kept for API compat)
22
36
  onPathJump?: () => void; // Cmd+K / Ctrl+K
23
37
  onGoUp?: () => void; // Alt+Up / Backspace (when nothing selected)
24
38
  onShowHelp?: () => void; // ? (Shift+/ on most layouts)
25
39
  onToggleInspector?: () => void; // i (koru:k1 details panel)
40
+ onQuickLook?: () => void; // Space (wiring:c2 quick-look overlay)
26
41
  hasSelection?: () => boolean; // disambiguates Backspace
27
42
  }
28
43
 
29
- /**
30
- * SHORTCUTS — the single source of truth for the shortcut cheat-sheet
31
- * (ShortcutsHelp.vue). Only shortcuts actually wired by FileExplorer are
32
- * listed; labels/groups resolve through the locale catalogue so the help
33
- * dialog stays tr/en aware. `keys` entries are display combos — each one
34
- * renders as its own <kbd>, alternatives joined visually with "/".
35
- */
36
- export interface ShortcutDef {
37
- keys: string[];
44
+ // --------------------------------------------------------------------
45
+ // Registry
46
+ // --------------------------------------------------------------------
47
+
48
+ /** One remappable action in the shortcut registry. */
49
+ export interface ShortcutActionDef {
50
+ /** Stable id — also the localStorage override key. */
51
+ id: string;
52
+ /** Default combo in canonical form. */
53
+ defaultCombo: string;
54
+ /**
55
+ * Extra built-in combos that also trigger the action but are NOT
56
+ * remappable (e.g. Backspace on go-up, which carries the
57
+ * selection-dependent dual behaviour). Shown in the help sheet and
58
+ * reserved in conflict detection.
59
+ */
60
+ fixedCombos?: string[];
38
61
  labelKey: string;
39
62
  groupKey: string;
63
+ /** false → the combo can't be remapped (Esc). Default true. */
64
+ customizable?: boolean;
65
+ /** false → don't preventDefault on match (Enter / Esc). Default true. */
66
+ prevent?: boolean;
67
+ /** true → fires even when focus sits in a form control (Esc). */
68
+ inForms?: boolean;
40
69
  }
41
70
 
42
- export const SHORTCUTS: ShortcutDef[] = [
71
+ /** Registry order = help/settings display order. */
72
+ export const SHORTCUT_ACTIONS: ShortcutActionDef[] = [
43
73
  // Navigation
44
- { keys: ['Ctrl+K'], labelKey: 'shortcuts.palette', groupKey: 'shortcuts.group.nav' },
45
- { keys: ['/'], labelKey: 'shortcuts.search', groupKey: 'shortcuts.group.nav' },
46
- { keys: ['Alt+↑', 'Backspace'], labelKey: 'shortcuts.go_up', groupKey: 'shortcuts.group.nav' },
47
- { keys: ['Enter'], labelKey: 'shortcuts.open', groupKey: 'shortcuts.group.nav' },
48
- { keys: ['Esc'], labelKey: 'shortcuts.close', groupKey: 'shortcuts.group.nav' },
49
- { keys: ['I'], labelKey: 'shortcuts.inspector', groupKey: 'shortcuts.group.nav' } /* koru:k1 */,
50
- { keys: ['?'], labelKey: 'shortcuts.help', groupKey: 'shortcuts.group.nav' },
74
+ { id: 'palette', defaultCombo: 'Ctrl+K', labelKey: 'shortcuts.palette', groupKey: 'shortcuts.group.nav' },
75
+ { id: 'search', defaultCombo: '/', labelKey: 'shortcuts.search', groupKey: 'shortcuts.group.nav' },
76
+ { id: 'go-up', defaultCombo: 'Alt+↑', fixedCombos: ['Backspace'], labelKey: 'shortcuts.go_up', groupKey: 'shortcuts.group.nav' },
77
+ { id: 'open', defaultCombo: 'Enter', prevent: false, labelKey: 'shortcuts.open', groupKey: 'shortcuts.group.nav' },
78
+ { id: 'quicklook', defaultCombo: 'Space', labelKey: 'shortcuts.quicklook', groupKey: 'shortcuts.group.nav' },
79
+ { id: 'close', defaultCombo: 'Esc', customizable: false, prevent: false, inForms: true, labelKey: 'shortcuts.close', groupKey: 'shortcuts.group.nav' },
80
+ { id: 'inspector', defaultCombo: 'I', labelKey: 'shortcuts.inspector', groupKey: 'shortcuts.group.nav' } /* koru:k1 */,
81
+ { id: 'help', defaultCombo: '?', labelKey: 'shortcuts.help', groupKey: 'shortcuts.group.nav' },
51
82
  // Selection
52
- { keys: ['Ctrl+A'], labelKey: 'shortcuts.select_all', groupKey: 'shortcuts.group.selection' },
83
+ { id: 'select-all', defaultCombo: 'Ctrl+A', labelKey: 'shortcuts.select_all', groupKey: 'shortcuts.group.selection' },
53
84
  // File operations
54
- { keys: ['F2'], labelKey: 'shortcuts.rename', groupKey: 'shortcuts.group.file' },
55
- { keys: ['Del'], labelKey: 'shortcuts.delete', groupKey: 'shortcuts.group.file' },
56
- { keys: ['Ctrl+X'], labelKey: 'shortcuts.cut', groupKey: 'shortcuts.group.file' },
57
- { keys: ['Ctrl+C'], labelKey: 'shortcuts.copy', groupKey: 'shortcuts.group.file' },
58
- { keys: ['Ctrl+V'], labelKey: 'shortcuts.paste', groupKey: 'shortcuts.group.file' },
85
+ { id: 'rename', defaultCombo: 'F2', labelKey: 'shortcuts.rename', groupKey: 'shortcuts.group.file' },
86
+ { id: 'delete', defaultCombo: 'Del', labelKey: 'shortcuts.delete', groupKey: 'shortcuts.group.file' },
87
+ { id: 'cut', defaultCombo: 'Ctrl+X', labelKey: 'shortcuts.cut', groupKey: 'shortcuts.group.file' },
88
+ { id: 'copy', defaultCombo: 'Ctrl+C', labelKey: 'shortcuts.copy', groupKey: 'shortcuts.group.file' },
89
+ { id: 'paste', defaultCombo: 'Ctrl+V', labelKey: 'shortcuts.paste', groupKey: 'shortcuts.group.file' },
59
90
  ];
60
91
 
92
+ /** action id → ShortcutHandlers callback name. */
93
+ const HANDLER_KEY: Record<string, keyof ShortcutHandlers> = {
94
+ palette: 'onPathJump',
95
+ search: 'onFocusSearch',
96
+ 'go-up': 'onGoUp',
97
+ open: 'onOpen',
98
+ quicklook: 'onQuickLook',
99
+ close: 'onClose',
100
+ inspector: 'onToggleInspector',
101
+ help: 'onShowHelp',
102
+ 'select-all': 'onSelectAll',
103
+ rename: 'onRename',
104
+ delete: 'onDelete',
105
+ cut: 'onCut',
106
+ copy: 'onCopy',
107
+ paste: 'onPaste',
108
+ };
109
+
110
+ // --------------------------------------------------------------------
111
+ // Overrides (localStorage `filex.shortcuts`)
112
+ // --------------------------------------------------------------------
113
+
114
+ const SHORTCUTS_LS_KEY = 'filex.shortcuts';
115
+
116
+ function readOverrides(): Record<string, string> {
117
+ try {
118
+ const raw = localStorage.getItem(SHORTCUTS_LS_KEY);
119
+ if (!raw) return {};
120
+ const parsed = JSON.parse(raw) as unknown;
121
+ if (!parsed || typeof parsed !== 'object') return {};
122
+ const out: Record<string, string> = {};
123
+ for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
124
+ if (typeof v === 'string' && SHORTCUT_ACTIONS.some((a) => a.id === k)) out[k] = v;
125
+ }
126
+ return out;
127
+ } catch {
128
+ return {};
129
+ }
130
+ }
131
+
132
+ // Module-level so the key handler, the "?" help sheet and the settings
133
+ // modal all observe the same reactive mapping.
134
+ const overrides = ref<Record<string, string>>(readOverrides());
135
+
136
+ function writeOverrides(): void {
137
+ try {
138
+ if (Object.keys(overrides.value).length === 0) localStorage.removeItem(SHORTCUTS_LS_KEY);
139
+ else localStorage.setItem(SHORTCUTS_LS_KEY, JSON.stringify(overrides.value));
140
+ } catch {
141
+ /* private mode / quota */
142
+ }
143
+ }
144
+
145
+ function defOf(id: string): ShortcutActionDef | undefined {
146
+ return SHORTCUT_ACTIONS.find((a) => a.id === id);
147
+ }
148
+
149
+ /** Current combo for an action ('' = unbound). */
150
+ export function effectiveCombo(id: string): string {
151
+ const o = overrides.value[id];
152
+ if (o !== undefined) return o;
153
+ return defOf(id)?.defaultCombo ?? '';
154
+ }
155
+
156
+ /** Set (or clear back to default) a user override. '' unbinds. */
157
+ export function setShortcutOverride(id: string, combo: string): void {
158
+ const def = defOf(id);
159
+ if (!def || def.customizable === false) return;
160
+ const next = { ...overrides.value };
161
+ if (combo === def.defaultCombo) delete next[id];
162
+ else next[id] = combo;
163
+ overrides.value = next;
164
+ writeOverrides();
165
+ }
166
+
167
+ export function resetShortcut(id: string): void {
168
+ if (!(id in overrides.value)) return;
169
+ const next = { ...overrides.value };
170
+ delete next[id];
171
+ overrides.value = next;
172
+ writeOverrides();
173
+ }
174
+
175
+ export function resetAllShortcuts(): void {
176
+ overrides.value = {};
177
+ writeOverrides();
178
+ }
179
+
180
+ export interface ShortcutConflict {
181
+ id: string;
182
+ /** true → the clash is with a fixed (non-remappable) combo. */
183
+ fixed: boolean;
184
+ }
185
+
186
+ /**
187
+ * Does `combo` already trigger another action? Checks both effective
188
+ * combos and the reserved fixed combos (Backspace, Esc). Returns null
189
+ * when free.
190
+ */
191
+ export function findShortcutConflict(combo: string, excludeId: string): ShortcutConflict | null {
192
+ if (!combo) return null;
193
+ for (const def of SHORTCUT_ACTIONS) {
194
+ if (def.id === excludeId) continue;
195
+ if (def.fixedCombos?.includes(combo)) return { id: def.id, fixed: true };
196
+ if (effectiveCombo(def.id) === combo) return { id: def.id, fixed: def.customizable === false };
197
+ }
198
+ return null;
199
+ }
200
+
201
+ // --------------------------------------------------------------------
202
+ // Event → canonical combo
203
+ // --------------------------------------------------------------------
204
+
205
+ const KEY_TOKEN_MAP: Record<string, string> = {
206
+ ' ': 'Space',
207
+ Spacebar: 'Space',
208
+ Escape: 'Esc',
209
+ Delete: 'Del',
210
+ ArrowUp: '↑',
211
+ ArrowDown: '↓',
212
+ ArrowLeft: '←',
213
+ ArrowRight: '→',
214
+ PageUp: 'PgUp',
215
+ PageDown: 'PgDn',
216
+ };
217
+
218
+ const MODIFIER_KEYS = new Set(['Control', 'Alt', 'Shift', 'Meta', 'AltGraph', 'CapsLock', 'NumLock', 'ScrollLock', 'Fn', 'Dead']);
219
+
220
+ function keyToken(e: KeyboardEvent): string | null {
221
+ const k = e.key;
222
+ if (!k || MODIFIER_KEYS.has(k)) return null;
223
+ const mapped = KEY_TOKEN_MAP[k];
224
+ if (mapped) return mapped;
225
+ if (k.length === 1) {
226
+ const upper = k.toUpperCase();
227
+ // Letters normalize to uppercase; symbols pass through as typed.
228
+ return upper;
229
+ }
230
+ return k; // 'Enter', 'F2', 'Tab', 'Home', 'End', 'Backspace', …
231
+ }
232
+
233
+ /**
234
+ * Canonical combo for a keyboard event, or null for a bare modifier
235
+ * press. Meta folds into Ctrl; Shift is dropped for printable symbols
236
+ * because the produced character already encodes it (`?` not `Shift+?`).
237
+ */
238
+ export function comboFromEvent(e: KeyboardEvent): string | null {
239
+ const key = keyToken(e);
240
+ if (!key) return null;
241
+ const parts: string[] = [];
242
+ if (e.ctrlKey || e.metaKey) parts.push('Ctrl');
243
+ if (e.altKey) parts.push('Alt');
244
+ const printableSymbol = key.length === 1 && key.toUpperCase() === key.toLowerCase();
245
+ if (e.shiftKey && !printableSymbol) parts.push('Shift');
246
+ parts.push(key);
247
+ return parts.join('+');
248
+ }
249
+
250
+ // --------------------------------------------------------------------
251
+ // Reactive views (help sheet + settings modal)
252
+ // --------------------------------------------------------------------
253
+
254
+ export interface ShortcutView {
255
+ id: string;
256
+ /** Current (possibly remapped) combo, '' = unbound. */
257
+ combo: string;
258
+ /** Non-remappable extra combos (display + reservation only). */
259
+ fixedCombos: string[];
260
+ /** Display combos: [combo, ...fixedCombos] minus empties. */
261
+ keys: string[];
262
+ labelKey: string;
263
+ groupKey: string;
264
+ customizable: boolean;
265
+ overridden: boolean;
266
+ }
267
+
268
+ /** Reactive, override-aware view of the registry (registry order). */
269
+ export function useShortcutList(): ComputedRef<ShortcutView[]> {
270
+ return computed(() =>
271
+ SHORTCUT_ACTIONS.map((def) => {
272
+ const combo = effectiveCombo(def.id);
273
+ const fixed = def.fixedCombos ?? [];
274
+ return {
275
+ id: def.id,
276
+ combo,
277
+ fixedCombos: fixed,
278
+ keys: [combo, ...fixed].filter((c) => !!c),
279
+ labelKey: def.labelKey,
280
+ groupKey: def.groupKey,
281
+ customizable: def.customizable !== false,
282
+ overridden: overrides.value[def.id] !== undefined,
283
+ };
284
+ }),
285
+ );
286
+ }
287
+
288
+ /**
289
+ * @deprecated Legacy static cheat-sheet shape (pre-registry). Kept for
290
+ * API compatibility; use `useShortcutList()` for the live, remap-aware
291
+ * list.
292
+ */
293
+ export interface ShortcutDef {
294
+ keys: string[];
295
+ labelKey: string;
296
+ groupKey: string;
297
+ }
298
+
299
+ /** @deprecated See {@link useShortcutList}. Defaults only. */
300
+ export const SHORTCUTS: ShortcutDef[] = SHORTCUT_ACTIONS.map((def) => ({
301
+ keys: [def.defaultCombo, ...(def.fixedCombos ?? [])].filter((c) => !!c),
302
+ labelKey: def.labelKey,
303
+ groupKey: def.groupKey,
304
+ }));
305
+
306
+ // --------------------------------------------------------------------
307
+ // Binding
308
+ // --------------------------------------------------------------------
309
+
61
310
  export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers: ShortcutHandlers) {
311
+ // combo → action def, rebuilt when overrides change.
312
+ const comboMap = computed(() => {
313
+ const m = new Map<string, ShortcutActionDef>();
314
+ for (const def of SHORTCUT_ACTIONS) {
315
+ const c = effectiveCombo(def.id);
316
+ if (c) m.set(c, def);
317
+ }
318
+ return m;
319
+ });
320
+
62
321
  function onKey(e: KeyboardEvent) {
63
322
  const root = rootEl.value;
64
323
  if (!root) return;
@@ -67,125 +326,50 @@ export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers:
67
326
  // want `Delete` while editing a filename, `/` while typing in the
68
327
  // search box, etc. Escape always goes through so modals can close.
69
328
  const target = e.target as HTMLElement | null;
70
- if (
329
+ const inForm = !!(
71
330
  target &&
72
331
  (target.tagName === 'INPUT' ||
73
332
  target.tagName === 'TEXTAREA' ||
333
+ target.tagName === 'SELECT' ||
74
334
  target.isContentEditable)
75
- ) {
76
- if (e.key !== 'Escape') return;
335
+ );
336
+
337
+ const combo = comboFromEvent(e);
338
+ if (!combo) return;
339
+
340
+ // Backspace keeps its FIXED dual behaviour: delete when something is
341
+ // selected (file-manager convention), parent-dir navigation otherwise.
342
+ // Not remappable — the selection-dependent branch doesn't fit the
343
+ // one-combo-one-action registry model.
344
+ if (combo === 'Backspace') {
345
+ if (inForm) return;
346
+ if (handlers.hasSelection?.() && handlers.onDelete) {
347
+ e.preventDefault();
348
+ handlers.onDelete();
349
+ } else if (handlers.onGoUp) {
350
+ e.preventDefault();
351
+ handlers.onGoUp();
352
+ }
353
+ return;
77
354
  }
78
355
 
79
- const ctrl = e.ctrlKey || e.metaKey;
80
-
81
- switch (e.key) {
82
- case 'Delete':
83
- if (handlers.onDelete) {
84
- e.preventDefault();
85
- handlers.onDelete();
86
- }
87
- break;
88
- case 'Backspace':
89
- // Backspace = delete when something is selected (file-manager
90
- // convention), parent-dir navigation otherwise. Without this
91
- // disambiguation Backspace was firing onDelete with an empty
92
- // selection and nothing happened, leaving users wondering why
93
- // the obvious 'go back' key did nothing.
94
- if (handlers.hasSelection?.() && handlers.onDelete) {
95
- e.preventDefault();
96
- handlers.onDelete();
97
- } else if (handlers.onGoUp) {
98
- e.preventDefault();
99
- handlers.onGoUp();
100
- }
101
- break;
102
- case 'F2':
103
- if (handlers.onRename) {
104
- e.preventDefault();
105
- handlers.onRename();
106
- }
107
- break;
108
- case 'Enter':
109
- if (handlers.onOpen) {
110
- handlers.onOpen();
111
- }
112
- break;
113
- case 'Escape':
114
- if (handlers.onClose) {
115
- handlers.onClose();
116
- }
117
- break;
118
- case '/':
119
- if (handlers.onFocusSearch && !ctrl) {
120
- e.preventDefault();
121
- handlers.onFocusSearch();
122
- }
123
- break;
124
- case '?':
125
- if (handlers.onShowHelp && !ctrl && !e.altKey) {
126
- e.preventDefault();
127
- handlers.onShowHelp();
128
- }
129
- break;
130
- case 'a':
131
- case 'A':
132
- if (ctrl && handlers.onSelectAll) {
133
- e.preventDefault();
134
- handlers.onSelectAll();
135
- }
136
- break;
137
- case 'x':
138
- case 'X':
139
- if (ctrl && handlers.onCut) {
140
- e.preventDefault();
141
- handlers.onCut();
142
- }
143
- break;
144
- case 'c':
145
- case 'C':
146
- if (ctrl && handlers.onCopy) {
147
- e.preventDefault();
148
- handlers.onCopy();
149
- }
150
- break;
151
- case 'v':
152
- case 'V':
153
- if (ctrl && handlers.onPaste) {
154
- e.preventDefault();
155
- handlers.onPaste();
156
- }
157
- break;
158
- case 'd':
159
- case 'D':
160
- if (ctrl && handlers.onDuplicate) {
161
- e.preventDefault();
162
- handlers.onDuplicate();
163
- }
164
- break;
165
- /* koru:k1 — plain `i` toggles the inspector (details) panel. Ctrl+I /
166
- Alt+I stay untouched so browser/OS combos keep working. */
167
- case 'i':
168
- case 'I':
169
- if (!ctrl && !e.altKey && handlers.onToggleInspector) {
170
- e.preventDefault();
171
- handlers.onToggleInspector();
172
- }
173
- break;
174
- case 'k':
175
- case 'K':
176
- if (ctrl && handlers.onPathJump) {
177
- e.preventDefault();
178
- handlers.onPathJump();
179
- }
180
- break;
181
- case 'ArrowUp':
182
- // Alt+Up = parent dir (matches Finder / Files / Explorer).
183
- if (e.altKey && handlers.onGoUp) {
184
- e.preventDefault();
185
- handlers.onGoUp();
186
- }
187
- break;
356
+ const def = comboMap.value.get(combo);
357
+ if (!def) return;
358
+ if (inForm && !def.inForms) return;
359
+ // Space on a focused button/link must keep its native activation
360
+ // (quick-look would otherwise fire alongside the click).
361
+ if (
362
+ combo === 'Space' &&
363
+ target &&
364
+ typeof target.closest === 'function' &&
365
+ target.closest('button, a, select, summary, [role="button"], [role="menuitem"]')
366
+ ) {
367
+ return;
188
368
  }
369
+ const fn = handlers[HANDLER_KEY[def.id]] as (() => void) | undefined;
370
+ if (!fn) return;
371
+ if (def.prevent !== false) e.preventDefault();
372
+ fn();
189
373
  }
190
374
 
191
375
  onMounted(() => window.addEventListener('keydown', onKey));