feat(web): add keyboard shortcuts for tool navigation

Register global keyboard shortcuts: Cmd/Ctrl+K to focus search,
Cmd/Ctrl+/ for tools view, Cmd/Ctrl+Shift+D to toggle theme,
Cmd/Ctrl+Alt+1-8 for quick tool navigation. Uses platform-aware
mod key detection (Cmd on Mac, Ctrl on Windows/Linux).
This commit is contained in:
Siddharth Kumar Sah
2026-03-22 04:42:14 +08:00
parent 0cd3d71a63
commit 939bc2e001
2 changed files with 132 additions and 0 deletions
@@ -0,0 +1,15 @@
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
/**
* Wrapper that registers global keyboard shortcuts.
* Must be rendered inside a <BrowserRouter> so that
* useNavigate() works inside the hook.
*/
export function KeyboardShortcutProvider({
children,
}: {
children: React.ReactNode;
}) {
useKeyboardShortcuts();
return <>{children}</>;
}
@@ -0,0 +1,117 @@
import { useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useTheme } from "./use-theme";
/**
* Detect if the current platform uses Cmd (macOS) or Ctrl.
*/
function isMac(): boolean {
return typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
}
interface ShortcutDef {
/** Keys: "mod" = Cmd on Mac, Ctrl on others. Plus key names separated by "+" */
keys: string;
description: string;
action: () => void;
}
/**
* Check if a keyboard event matches a shortcut definition.
*/
function matchesShortcut(e: KeyboardEvent, keys: string): boolean {
const parts = keys.toLowerCase().split("+").map((s) => s.trim());
const mac = isMac();
const needsMod = parts.includes("mod");
const needsShift = parts.includes("shift");
const needsAlt = parts.includes("alt");
// The actual key is the last non-modifier part
const key = parts.filter((p) => !["mod", "shift", "alt"].includes(p))[0];
if (!key) return false;
const modPressed = mac ? e.metaKey : e.ctrlKey;
if (needsMod && !modPressed) return false;
if (!needsMod && modPressed) return false;
if (needsShift && !e.shiftKey) return false;
if (!needsShift && e.shiftKey) return false;
if (needsAlt && !e.altKey) return false;
if (!needsAlt && e.altKey) return false;
return e.key.toLowerCase() === key;
}
/**
* Hook that registers global keyboard shortcuts.
* Call once in the root App component.
*/
export function useKeyboardShortcuts() {
const navigate = useNavigate();
const { toggleTheme } = useTheme();
const focusSearchBar = useCallback(() => {
const searchInput = document.querySelector<HTMLInputElement>(
'input[placeholder*="Search"]'
);
if (searchInput) {
searchInput.focus();
searchInput.select();
}
}, []);
useEffect(() => {
const shortcuts: ShortcutDef[] = [
{ keys: "mod+k", description: "Focus search bar", action: focusSearchBar },
{ keys: "mod+/", description: "Go to tools", action: () => navigate("/") },
{ keys: "mod+shift+d", description: "Toggle theme", action: toggleTheme },
{ keys: "mod+alt+1", description: "Go to Resize", action: () => navigate("/resize") },
{ keys: "mod+alt+2", description: "Go to Crop", action: () => navigate("/crop") },
{ keys: "mod+alt+3", description: "Go to Compress", action: () => navigate("/compress") },
{ keys: "mod+alt+4", description: "Go to Convert", action: () => navigate("/convert") },
{ keys: "mod+alt+5", description: "Go to Remove Background", action: () => navigate("/remove-background") },
{ keys: "mod+alt+6", description: "Go to Watermark Text", action: () => navigate("/watermark-text") },
{ keys: "mod+alt+7", description: "Go to Strip Metadata", action: () => navigate("/strip-metadata") },
{ keys: "mod+alt+8", description: "Go to Image Info", action: () => navigate("/info") },
];
function handler(e: KeyboardEvent) {
// Don't intercept when typing in inputs/textareas (except Cmd+K for search)
const tag = (e.target as HTMLElement)?.tagName;
const isInput = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
for (const shortcut of shortcuts) {
if (matchesShortcut(e, shortcut.keys)) {
// Allow Cmd+K even in inputs (it focuses search)
if (isInput && shortcut.keys !== "mod+k") continue;
e.preventDefault();
e.stopPropagation();
shortcut.action();
return;
}
}
}
window.addEventListener("keydown", handler, { capture: true });
return () => window.removeEventListener("keydown", handler, { capture: true });
}, [navigate, toggleTheme, focusSearchBar]);
}
/**
* Returns a human-readable label for a keyboard shortcut.
*/
export function formatShortcut(keys: string): string {
const mac = isMac();
return keys
.split("+")
.map((k) => {
const lk = k.trim().toLowerCase();
if (lk === "mod") return mac ? "\u2318" : "Ctrl";
if (lk === "shift") return mac ? "\u21E7" : "Shift";
if (lk === "alt") return mac ? "\u2325" : "Alt";
if (lk === "/") return "/";
return k.trim().toUpperCase();
})
.join(mac ? "" : "+");
}