Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 27 additions & 39 deletions src/components/GlobalKeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number> = {
"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;
}
Expand All @@ -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;
Expand Down
13 changes: 7 additions & 6 deletions src/components/ShortcutsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
78 changes: 78 additions & 0 deletions src/lib/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
@@ -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]);
}
Loading