feat: keyboard shortcuts help modal (press ? to toggle)

This commit is contained in:
2026-01-29 11:06:30 +00:00
parent bfa6c87fce
commit 9279956e33
2 changed files with 97 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import { useState } from "react";
import { NavLink, Outlet } from "react-router-dom";
import { useCurrentUser } from "../hooks/useCurrentUser";
import { useTheme } from "../hooks/useTheme";
import { KeyboardShortcutsModal } from "./KeyboardShortcutsModal";
import { signOut } from "../lib/auth-client";
const navItems = [
@@ -160,6 +161,9 @@ export function DashboardLayout() {
>
Sign Out
</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>
</aside>
@@ -167,6 +171,8 @@ export function DashboardLayout() {
<main className="flex-1 md:ml-56 pt-14 md:pt-0">
<Outlet />
</main>
<KeyboardShortcutsModal />
</div>
);
}

View 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>
</>
);
}