T5/Phase 3: SPA shell + navigation
- TanStack Router with type-safe file-based routes - Sidebar: collapsible, workspace switcher, 12 nav links, user menu - Topbar: search, quick-add, notifications, user avatar - cmdk command palette: nav + create + search + @mentions - Keyboard shortcuts: Cmd+K palette, g+t/h/p/n/c/g/s navigation, ? help - Theme: dark default, accent picker (slate/blue/green/purple/orange), persisted to localStorage - Auth: login -> /, logout -> /login, 401 redirect - Placeholder pages for all 13 nav routes (real in T6/T7) - Typed API client with useApiQuery/useApiMutation hooks
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useKeyboardShortcutsStore } from "@/lib/stores/use-keyboard-shortcuts-store";
|
||||
|
||||
export function useKeyboardShortcuts() {
|
||||
const navigate = useNavigate();
|
||||
const { enabled } = useKeyboardShortcutsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
let pendingKey = "";
|
||||
let pendingTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Never override native behavior inside controls or modal UI
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.tagName === "SELECT" ||
|
||||
target.tagName === "BUTTON" ||
|
||||
target.isContentEditable ||
|
||||
target.closest('[role="dialog"], [role="menu"], [role="listbox"], [role="combobox"]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cmd+K / Ctrl+K — open command palette
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
document.dispatchEvent(new CustomEvent("open-command-palette"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ? — show shortcuts help
|
||||
if (e.key === "?" && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
document.dispatchEvent(new CustomEvent("open-shortcuts-help"));
|
||||
return;
|
||||
}
|
||||
|
||||
// / — focus search (when not in an input)
|
||||
if (e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
document.dispatchEvent(new CustomEvent("open-command-palette"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if modifier keys are pressed
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
// Handle two-key combos (G then letter)
|
||||
if (pendingKey) {
|
||||
clearTimeout(pendingTimeout);
|
||||
pendingKey = "";
|
||||
|
||||
const navMap: Record<string, string> = {
|
||||
d: "/",
|
||||
t: "/tasks",
|
||||
h: "/habits",
|
||||
p: "/projects",
|
||||
n: "/notes",
|
||||
c: "/calendar",
|
||||
g: "/graph",
|
||||
s: "/settings",
|
||||
};
|
||||
|
||||
if (navMap[key]) {
|
||||
e.preventDefault();
|
||||
navigate({ to: navMap[key] });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Single key shortcuts
|
||||
switch (key) {
|
||||
case "g":
|
||||
pendingKey = "g";
|
||||
pendingTimeout = setTimeout(() => {
|
||||
pendingKey = "";
|
||||
}, 1000);
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [navigate, enabled]);
|
||||
}
|
||||
Reference in New Issue
Block a user