Skip to content

Commit 4d751c2

Browse files
nullcoderclaude
andauthored
feat: implement global keyboard shortcuts (#101)
* feat: implement global keyboard shortcuts (#72) - Add useGlobalShortcuts hook for managing keyboard shortcuts - Implement KeyboardShortcutsHelp component with help dialog - Add Separator component from shadcn/ui - Integrate keyboard shortcuts help in Header (Cmd+/ to open) - Add comprehensive tests for keyboard shortcuts functionality - Create demo page for keyboard shortcuts Keyboard shortcuts implemented: - Cmd/Ctrl + / : Open help dialog - Cmd/Ctrl + K : Create new gist (navigation) - Cmd/Ctrl + S : Save gist (when applicable) - Cmd/Ctrl + Shift + C : Copy share link - Escape : Close modals/dialogs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: update TODO and tracking docs for completed keyboard shortcuts --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1e97b70 commit 4d751c2

File tree

12 files changed

+1097
-18
lines changed

12 files changed

+1097
-18
lines changed

app/demo/keyboard-shortcuts/page.tsx

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6+
import {
7+
KeyboardShortcutsHelp,
8+
KeyboardShortcut,
9+
} from "@/components/keyboard-shortcuts-help";
10+
import { useGlobalShortcuts, getPlatformKeyName } from "@/lib/hooks";
11+
import { toast } from "sonner";
12+
import { Keyboard, Save, Send, HelpCircle, X } from "lucide-react";
13+
14+
export default function KeyboardShortcutsDemo() {
15+
const [showHelp, setShowHelp] = useState(false);
16+
const [lastAction, setLastAction] = useState<string>("");
17+
const [shortcutsEnabled, setShortcutsEnabled] = useState(true);
18+
19+
const platformKey = getPlatformKeyName();
20+
21+
// Set up global shortcuts
22+
useGlobalShortcuts({
23+
onSave: async () => {
24+
setLastAction("Save triggered!");
25+
toast.success("Save shortcut triggered", {
26+
description: `${platformKey} + S was pressed`,
27+
});
28+
},
29+
onSubmit: async () => {
30+
setLastAction("Submit triggered!");
31+
toast.success("Submit shortcut triggered", {
32+
description: `${platformKey} + Enter was pressed`,
33+
});
34+
},
35+
onEscape: () => {
36+
setLastAction("Escape triggered!");
37+
toast.info("Escape key pressed", {
38+
description: "This would close dialogs",
39+
});
40+
},
41+
onHelp: () => {
42+
setLastAction("Help triggered!");
43+
setShowHelp(true);
44+
},
45+
enabled: shortcutsEnabled,
46+
});
47+
48+
return (
49+
<div className="container mx-auto max-w-4xl px-4 py-12">
50+
<div className="space-y-8">
51+
<div className="space-y-4 text-center">
52+
<h1 className="text-4xl font-bold">Keyboard Shortcuts Demo</h1>
53+
<p className="text-muted-foreground text-lg">
54+
Test the keyboard shortcuts implementation
55+
</p>
56+
</div>
57+
58+
<Card>
59+
<CardHeader>
60+
<CardTitle className="flex items-center gap-2">
61+
<Keyboard className="h-5 w-5" />
62+
Global Shortcuts Control
63+
</CardTitle>
64+
</CardHeader>
65+
<CardContent className="space-y-4">
66+
<div className="flex items-center justify-between">
67+
<div>
68+
<p className="font-medium">Shortcuts Enabled</p>
69+
<p className="text-muted-foreground text-sm">
70+
Toggle to enable/disable all keyboard shortcuts
71+
</p>
72+
</div>
73+
<Button
74+
variant={shortcutsEnabled ? "default" : "outline"}
75+
onClick={() => setShortcutsEnabled(!shortcutsEnabled)}
76+
>
77+
{shortcutsEnabled ? "Enabled" : "Disabled"}
78+
</Button>
79+
</div>
80+
81+
<div className="bg-muted/50 rounded-lg border p-4">
82+
<p className="mb-2 text-sm font-medium">Last Action:</p>
83+
<p className="font-mono text-lg">
84+
{lastAction || "No action triggered yet"}
85+
</p>
86+
</div>
87+
</CardContent>
88+
</Card>
89+
90+
<Card>
91+
<CardHeader>
92+
<CardTitle>Available Shortcuts</CardTitle>
93+
</CardHeader>
94+
<CardContent className="space-y-6">
95+
<div>
96+
<h3 className="mb-3 font-semibold">Try these shortcuts:</h3>
97+
<div className="space-y-3">
98+
<div className="flex items-center justify-between">
99+
<div className="flex items-center gap-3">
100+
<Save className="text-muted-foreground h-4 w-4" />
101+
<span>Save action</span>
102+
</div>
103+
<KeyboardShortcut keys={`${platformKey} + S`} />
104+
</div>
105+
<div className="flex items-center justify-between">
106+
<div className="flex items-center gap-3">
107+
<Send className="text-muted-foreground h-4 w-4" />
108+
<span>Submit action</span>
109+
</div>
110+
<KeyboardShortcut keys={`${platformKey} + Enter`} />
111+
</div>
112+
<div className="flex items-center justify-between">
113+
<div className="flex items-center gap-3">
114+
<X className="text-muted-foreground h-4 w-4" />
115+
<span>Escape/Cancel</span>
116+
</div>
117+
<KeyboardShortcut keys="Escape" />
118+
</div>
119+
<div className="flex items-center justify-between">
120+
<div className="flex items-center gap-3">
121+
<HelpCircle className="text-muted-foreground h-4 w-4" />
122+
<span>Show help</span>
123+
</div>
124+
<KeyboardShortcut keys={`${platformKey} + ?`} />
125+
</div>
126+
</div>
127+
</div>
128+
129+
<div className="border-t pt-4">
130+
<Button onClick={() => setShowHelp(true)} className="w-full">
131+
<Keyboard className="mr-2 h-4 w-4" />
132+
Show All Keyboard Shortcuts
133+
</Button>
134+
</div>
135+
</CardContent>
136+
</Card>
137+
138+
<Card>
139+
<CardHeader>
140+
<CardTitle>Implementation Example</CardTitle>
141+
</CardHeader>
142+
<CardContent>
143+
<div className="bg-muted overflow-x-auto rounded-lg p-4 font-mono text-sm">
144+
<pre>{`import { useGlobalShortcuts } from "@/lib/hooks";
145+
146+
function MyComponent() {
147+
useGlobalShortcuts({
148+
onSave: async () => {
149+
// Handle save action
150+
await saveData();
151+
toast.success("Saved!");
152+
},
153+
onSubmit: async () => {
154+
// Handle submit action
155+
await submitForm();
156+
},
157+
onEscape: () => {
158+
// Close dialogs, cancel actions
159+
closeModal();
160+
},
161+
onHelp: () => {
162+
// Show help dialog
163+
setShowHelp(true);
164+
},
165+
enabled: true, // Can be toggled
166+
});
167+
}`}</pre>
168+
</div>
169+
</CardContent>
170+
</Card>
171+
172+
<Card>
173+
<CardHeader>
174+
<CardTitle>Platform Detection</CardTitle>
175+
</CardHeader>
176+
<CardContent className="space-y-4">
177+
<p className="text-muted-foreground text-sm">
178+
The keyboard shortcuts automatically detect your platform and show
179+
the appropriate keys.
180+
</p>
181+
<div className="grid grid-cols-2 gap-4">
182+
<div className="rounded-lg border p-4">
183+
<p className="mb-2 font-medium">Your Platform</p>
184+
<p className="font-mono text-lg">
185+
{typeof navigator !== "undefined"
186+
? navigator.platform
187+
: "Unknown"}
188+
</p>
189+
</div>
190+
<div className="rounded-lg border p-4">
191+
<p className="mb-2 font-medium">Command Key</p>
192+
<p className="font-mono text-lg">{platformKey}</p>
193+
</div>
194+
</div>
195+
</CardContent>
196+
</Card>
197+
</div>
198+
199+
{/* Keyboard Shortcuts Dialog */}
200+
<KeyboardShortcutsHelp open={showHelp} onOpenChange={setShowHelp} />
201+
</div>
202+
);
203+
}

components/header.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState } from "react";
44
import Link from "next/link";
5-
import { Menu, Ghost } from "lucide-react";
5+
import { Menu, Ghost, Keyboard } from "lucide-react";
66
import { GithubIcon } from "@/components/icons/github-icon";
77
import {
88
NavigationMenu,
@@ -21,10 +21,24 @@ import {
2121
} from "@/components/ui/sheet";
2222
import { Button } from "@/components/ui/button";
2323
import { ThemeToggle } from "@/components/theme-toggle";
24+
import { KeyboardShortcutsHelp } from "@/components/keyboard-shortcuts-help";
25+
import { useGlobalShortcuts } from "@/lib/hooks";
2426
import { cn } from "@/lib/utils";
2527

2628
export function Header() {
2729
const [isOpen, setIsOpen] = useState(false);
30+
const [showShortcuts, setShowShortcuts] = useState(false);
31+
32+
// Set up global shortcuts
33+
useGlobalShortcuts({
34+
onHelp: () => setShowShortcuts(true),
35+
onEscape: () => {
36+
// Close mobile menu if open
37+
if (isOpen) setIsOpen(false);
38+
// Close shortcuts dialog if open
39+
if (showShortcuts) setShowShortcuts(false);
40+
},
41+
});
2842

2943
return (
3044
<header className="bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur">
@@ -78,6 +92,17 @@ export function Header() {
7892
<span>GitHub</span>
7993
</a>
8094
</NavigationMenuItem>
95+
<NavigationMenuItem>
96+
<Button
97+
variant="ghost"
98+
size="icon"
99+
onClick={() => setShowShortcuts(true)}
100+
aria-label="Show keyboard shortcuts"
101+
title="Keyboard shortcuts"
102+
>
103+
<Keyboard className="h-4 w-4" />
104+
</Button>
105+
</NavigationMenuItem>
81106
</NavigationMenuList>
82107
</NavigationMenu>
83108
<ThemeToggle />
@@ -130,6 +155,19 @@ export function Header() {
130155
About
131156
</Link>
132157
</SheetClose>
158+
<SheetClose asChild>
159+
<Button
160+
variant="ghost"
161+
className="hover:bg-accent hover:text-accent-foreground flex w-full items-center justify-start gap-3 rounded-lg px-3 py-3 text-base font-medium transition-colors"
162+
onClick={() => {
163+
setShowShortcuts(true);
164+
setIsOpen(false);
165+
}}
166+
>
167+
<Keyboard className="h-5 w-5" aria-hidden="true" />
168+
<span>Keyboard Shortcuts</span>
169+
</Button>
170+
</SheetClose>
133171
<div className="bg-border my-2 h-px" />
134172
<a
135173
href="https://github.com/nullcoder/ghostpaste"
@@ -146,6 +184,12 @@ export function Header() {
146184
</Sheet>
147185
</div>
148186
</div>
187+
188+
{/* Keyboard Shortcuts Dialog */}
189+
<KeyboardShortcutsHelp
190+
open={showShortcuts}
191+
onOpenChange={setShowShortcuts}
192+
/>
149193
</header>
150194
);
151195
}

0 commit comments

Comments
 (0)