feat: keyboard shortcuts help modal (press ? to toggle)
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { NavLink, Outlet } from "react-router-dom";
|
import { NavLink, Outlet } from "react-router-dom";
|
||||||
import { useCurrentUser } from "../hooks/useCurrentUser";
|
import { useCurrentUser } from "../hooks/useCurrentUser";
|
||||||
import { useTheme } from "../hooks/useTheme";
|
import { useTheme } from "../hooks/useTheme";
|
||||||
|
import { KeyboardShortcutsModal } from "./KeyboardShortcutsModal";
|
||||||
import { signOut } from "../lib/auth-client";
|
import { signOut } from "../lib/auth-client";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -160,6 +161,9 @@ export function DashboardLayout() {
|
|||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
|
<div className="hidden sm:block text-[10px] text-gray-600 px-3 pt-2 mt-1 border-t border-gray-800">
|
||||||
|
Press <kbd className="px-1 py-0.5 bg-gray-800 border border-gray-700 rounded text-[10px] font-mono">?</kbd> for shortcuts
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -167,6 +171,8 @@ export function DashboardLayout() {
|
|||||||
<main className="flex-1 md:ml-56 pt-14 md:pt-0">
|
<main className="flex-1 md:ml-56 pt-14 md:pt-0">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<KeyboardShortcutsModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
91
frontend/src/components/KeyboardShortcutsModal.tsx
Normal file
91
frontend/src/components/KeyboardShortcutsModal.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const shortcuts = [
|
||||||
|
{ keys: ["Ctrl", "N"], action: "Create new task", context: "Queue" },
|
||||||
|
{ keys: ["Esc"], action: "Close panel / Cancel edit", context: "Global" },
|
||||||
|
{ keys: ["?"], action: "Show keyboard shortcuts", context: "Global" },
|
||||||
|
{ keys: ["Ctrl", "Enter"], action: "Submit note", context: "Task Detail" },
|
||||||
|
{ keys: ["Enter"], action: "Add subtask / Save tag", context: "Task Detail" },
|
||||||
|
{ keys: [","], action: "Add tag", context: "Task Detail" },
|
||||||
|
{ keys: ["Backspace"], action: "Remove last tag (when empty)", context: "Task Detail" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function KeyboardShortcutsModal() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
// Don't trigger if user is typing in an input
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") return;
|
||||||
|
if (target.isContentEditable) return;
|
||||||
|
|
||||||
|
if (e.key === "?") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen((prev) => !prev);
|
||||||
|
}
|
||||||
|
if (e.key === "Escape" && open) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKey);
|
||||||
|
return () => window.removeEventListener("keydown", handleKey);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[60]" onClick={() => setOpen(false)} />
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-[61] p-4" onClick={() => setOpen(false)}>
|
||||||
|
<div
|
||||||
|
className="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 w-full max-w-md overflow-hidden"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="px-6 py-4 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100">⌨️ Keyboard Shortcuts</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="p-1.5 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="px-6 py-4 space-y-3 max-h-[60vh] overflow-y-auto">
|
||||||
|
{shortcuts.map((shortcut, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{shortcut.keys.map((key, j) => (
|
||||||
|
<span key={j}>
|
||||||
|
<kbd className="px-2 py-1 text-xs font-mono font-semibold text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm">
|
||||||
|
{key}
|
||||||
|
</kbd>
|
||||||
|
{j < shortcut.keys.length - 1 && (
|
||||||
|
<span className="text-gray-400 dark:text-gray-500 mx-0.5">+</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-right">
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{shortcut.action}</span>
|
||||||
|
<span className="text-[10px] text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800 px-1.5 py-0.5 rounded">
|
||||||
|
{shortcut.context}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="px-6 py-3 border-t border-gray-100 dark:border-gray-800 text-center">
|
||||||
|
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Press <kbd className="px-1.5 py-0.5 text-[10px] font-mono bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded">?</kbd> to toggle
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user