feat(frontend): make keyboard shortcuts layout-independent

Command keys were matched by character (e.key), so on a non-Latin layout
(e.g. Russian) the physical g/f/e/p/x/j/k keys emitted Cyrillic letters
and nothing fired. Letter and digit commands now match by physical
position (e.code: KeyG, Digit1, Slash, …) across the global nav, the file
grid, and the viewer, so the same physical keys work on any layout. Named
keys (arrows, Enter, Esc, Delete), the Mod combos, and the filter's
literal operators (& | ! ( )) stay on e.key, where character matching is
correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:55:39 +03:00
parent 19ec96c544
commit 94d100675e
3 changed files with 59 additions and 37 deletions
+31 -17
View File
@@ -112,33 +112,54 @@
if (activeFileId || tagEditorOpen || poolPickerOpen || confirmDeleteFiles) return;
if (isFormTarget(e.target) || e.metaKey || e.ctrlKey || e.altKey) return;
// Navigation / named keys — same on every layout.
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
moveFocus(1);
break;
return;
case 'ArrowLeft':
e.preventDefault();
moveFocus(-1);
break;
return;
case 'ArrowDown':
e.preventDefault();
moveFocus(gridCols());
break;
return;
case 'ArrowUp':
e.preventDefault();
moveFocus(-gridCols());
break;
return;
case 'Enter': {
const f = focusedFile();
if (f) {
e.preventDefault();
openFile(f);
}
break;
return;
}
case ' ':
case 'x': {
case ' ': {
const f = focusedFile();
if (f?.id) {
e.preventDefault();
selectionStore.toggle(f.id);
}
return;
}
case 'Delete':
if ($selectionActive || focusedFile()) {
e.preventDefault();
ensureSelectedFocused();
confirmDeleteFiles = true;
}
return;
}
// Letter / symbol commands matched by physical position, so they fire the
// same on a non-Latin layout.
if (e.shiftKey) return;
switch (e.code) {
case 'KeyX': {
const f = focusedFile();
if (f?.id) {
e.preventDefault();
@@ -146,28 +167,21 @@
}
break;
}
case 'e':
case 'KeyE':
if ($selectionActive || focusedFile()) {
e.preventDefault();
ensureSelectedFocused();
openTagEditor();
}
break;
case 'p':
case 'KeyP':
if ($selectionActive || focusedFile()) {
e.preventDefault();
ensureSelectedFocused();
void openPoolPicker();
}
break;
case 'Delete':
if ($selectionActive || focusedFile()) {
e.preventDefault();
ensureSelectedFocused();
confirmDeleteFiles = true;
}
break;
case '/':
case 'Slash':
e.preventDefault();
openFilterAndFocus();
break;