From 2fb13f2c51406c3891c0452746fdf6de6ce9884d Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 28 Jul 2026 16:15:21 +0530 Subject: [PATCH] feat(shortcuts): add useKeyboardShortcuts hook, time range keys (1-4), Cmd+/, and ShortcutsModal updates --- src/components/GlobalKeyboardShortcuts.tsx | 66 ++++++++---------- src/components/ShortcutsModal.tsx | 13 ++-- src/lib/useKeyboardShortcuts.ts | 78 ++++++++++++++++++++++ 3 files changed, 112 insertions(+), 45 deletions(-) create mode 100644 src/lib/useKeyboardShortcuts.ts diff --git a/src/components/GlobalKeyboardShortcuts.tsx b/src/components/GlobalKeyboardShortcuts.tsx index c482604fc..31054e946 100644 --- a/src/components/GlobalKeyboardShortcuts.tsx +++ b/src/components/GlobalKeyboardShortcuts.tsx @@ -38,51 +38,39 @@ export default function GlobalKeyboardShortcuts() { if (activeElement.getAttribute("contenteditable") === "true") return; } - // Show shortcuts modal - if (e.key === "?") { - setIsOpen(true); + // Show shortcuts modal: ? or Cmd+/ / Ctrl+/ + if (e.key === "?" || ((e.metaKey || e.ctrlKey) && e.key === "/")) { + setIsOpen((prev) => !prev); e.preventDefault(); return; } - // Alt+T / Option+T to toggle theme - if (e.altKey && e.key.toLowerCase() === "t") { - keyboardToggleRef.current = true; - toggleTheme(); - e.preventDefault(); - return; - } - - // Toggle chart - if (e.key.toLowerCase() === "b") { - window.dispatchEvent(new Event("toggleChart")); - e.preventDefault(); - return; - } - - // G key handling (chord detection) - if (e.key.toLowerCase() === "g") { - gPressed = true; - setTimeout(() => { - gPressed = false; - }, 1000); - e.preventDefault(); - return; - } - - // G + D -> Dashboard - if (gPressed && e.key.toLowerCase() === "d") { - window.location.href = "/dashboard"; + // Time range shortcuts: 1 -> 7d, 2 -> 14d, 3 -> 30d, 4 -> 90d + const rangeMap: Record = { + "1": 7, + "2": 14, + "3": 30, + "4": 90, + }; + if (rangeMap[e.key]) { + const days = rangeMap[e.key]; + try { + localStorage.setItem("devtrack_dashboard_range", String(days)); + } catch { + // localStorage access failed + } + window.dispatchEvent( + new CustomEvent("timeRangeChange", { detail: days }) + ); + setAnnouncement(`Switched time range to ${days} days`); e.preventDefault(); return; } - // G + P -> Goals (scroll to goals section) - if (gPressed && e.key.toLowerCase() === "p") { - const goalSection = document.getElementById("goals-section"); - if (goalSection) { - goalSection.scrollIntoView({ behavior: "smooth", block: "center" }); - } + // Alt+T / Option+T to toggle theme + if (e.altKey && e.key.toLowerCase() === "t") { + keyboardToggleRef.current = true; + toggleTheme(); e.preventDefault(); return; } @@ -95,8 +83,8 @@ export default function GlobalKeyboardShortcuts() { return; } - // Reload page - if (e.key.toLowerCase() === "r") { + // Reload page / refresh data + if (e.key.toLowerCase() === "r" && !e.metaKey && !e.ctrlKey) { window.location.reload(); e.preventDefault(); return; diff --git a/src/components/ShortcutsModal.tsx b/src/components/ShortcutsModal.tsx index 382e71707..51c650bc4 100644 --- a/src/components/ShortcutsModal.tsx +++ b/src/components/ShortcutsModal.tsx @@ -14,13 +14,14 @@ interface ShortcutItem { } const SHORTCUTS: ShortcutItem[] = [ + { key: "1", action: "Switch to 7d range" }, + { key: "2", action: "Switch to 14d range" }, + { key: "3", action: "Switch to 30d range" }, + { key: "4", action: "Switch to 90d range" }, + { key: "R", action: "Refresh data" }, { key: "Alt + T", action: "Toggle theme" }, - { key: "B", action: "Toggle chart" }, - { key: "R", action: "Reload data" }, - { key: "G + D", action: "Go to Dashboard" }, - { key: "G + P", action: "Go to Goals" }, - { key: "Esc", action: "Close modal/dialog" }, - { key: "?", action: "Show shortcuts" }, + { key: "Esc", action: "Close modal / dialog" }, + { key: "? or Cmd+/", action: "Show shortcuts help" }, ]; export default function ShortcutsModal({ isOpen, diff --git a/src/lib/useKeyboardShortcuts.ts b/src/lib/useKeyboardShortcuts.ts new file mode 100644 index 000000000..f308d5fda --- /dev/null +++ b/src/lib/useKeyboardShortcuts.ts @@ -0,0 +1,78 @@ +import { useEffect } from "react"; + +export interface ShortcutHandlers { + onTimeRangeChange?: (days: number) => void; + onRefresh?: () => void; + onToggleHelp?: () => void; + onCloseModal?: () => void; + onToggleTheme?: () => void; +} + +/** + * Custom hook for keyboard-first navigation in DevTrack. + * Listens for global keydown events and triggers specified handlers. + * Ignores keypresses when focus is inside text input, textarea, or editable elements. + */ +export function useKeyboardShortcuts(handlers: ShortcutHandlers) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const activeElement = document.activeElement; + if (activeElement) { + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === "input" || tagName === "textarea" || tagName === "select") return; + if (activeElement.getAttribute("contenteditable") === "true") return; + } + + // Help modal: ? or Cmd+/ / Ctrl+/ + if ( + (e.key === "?" && !e.altKey) || + ((e.metaKey || e.ctrlKey) && e.key === "/") + ) { + handlers.onToggleHelp?.(); + e.preventDefault(); + return; + } + + // Escape -> close modal / panel + if (e.key === "Escape") { + handlers.onCloseModal?.(); + e.preventDefault(); + return; + } + + // Time range shortcuts: 1 -> 7d, 2 -> 14d, 3 -> 30d, 4 -> 90d + if (e.key === "1") { + handlers.onTimeRangeChange?.(7); + e.preventDefault(); + return; + } + if (e.key === "2") { + handlers.onTimeRangeChange?.(14); + e.preventDefault(); + return; + } + if (e.key === "3") { + handlers.onTimeRangeChange?.(30); + e.preventDefault(); + return; + } + if (e.key === "4") { + handlers.onTimeRangeChange?.(90); + e.preventDefault(); + return; + } + + // R -> refresh data + if (e.key.toLowerCase() === "r" && !e.metaKey && !e.ctrlKey && !e.altKey) { + handlers.onRefresh?.(); + e.preventDefault(); + return; + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handlers]); +}