152 lines
5.3 KiB
TypeScript
152 lines
5.3 KiB
TypeScript
|
|
import { useEffect } from 'react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
import { X } from 'lucide-react';
|
||
|
|
|
||
|
|
interface ShortcutGroup {
|
||
|
|
title: string;
|
||
|
|
shortcuts: { keys: string[]; description: string }[];
|
||
|
|
}
|
||
|
|
|
||
|
|
const shortcutGroups: ShortcutGroup[] = [
|
||
|
|
{
|
||
|
|
title: 'General',
|
||
|
|
shortcuts: [
|
||
|
|
{ keys: ['Ctrl', 'K'], description: 'Open search' },
|
||
|
|
{ keys: ['?'], description: 'Show keyboard shortcuts' },
|
||
|
|
{ keys: ['Esc'], description: 'Close dialog / panel' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Playback',
|
||
|
|
shortcuts: [
|
||
|
|
{ keys: ['Space'], description: 'Play / Pause' },
|
||
|
|
{ keys: ['N'], description: 'Next track' },
|
||
|
|
{ keys: ['P'], description: 'Previous track' },
|
||
|
|
{ keys: ['M'], description: 'Toggle mute' },
|
||
|
|
{ keys: ['↑'], description: 'Volume up' },
|
||
|
|
{ keys: ['↓'], description: 'Volume down' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Navigation',
|
||
|
|
shortcuts: [
|
||
|
|
{ keys: ['G', 'H'], description: 'Go to Home' },
|
||
|
|
{ keys: ['G', 'L'], description: 'Go to Library' },
|
||
|
|
{ keys: ['G', 'S'], description: 'Go to Settings' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
interface KeyboardShortcutsPanelProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function KeyboardShortcutsPanel({
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
}: KeyboardShortcutsPanelProps) {
|
||
|
|
useEffect(() => {
|
||
|
|
if (!isOpen) return;
|
||
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||
|
|
if (e.key === 'Escape' || e.key === '?') {
|
||
|
|
e.preventDefault();
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
window.addEventListener('keydown', handleKeyDown);
|
||
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||
|
|
}, [isOpen, onClose]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AnimatePresence>
|
||
|
|
{isOpen && (
|
||
|
|
<>
|
||
|
|
{/* Backdrop */}
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
exit={{ opacity: 0 }}
|
||
|
|
transition={{ duration: 0.15 }}
|
||
|
|
onClick={onClose}
|
||
|
|
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm"
|
||
|
|
/>
|
||
|
|
{/* Panel */}
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
|
|
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||
|
|
className="fixed inset-x-4 top-[10%] bottom-[10%] z-50 mx-auto max-w-2xl overflow-y-auto rounded-2xl border border-border bg-background/95 backdrop-blur-md p-6 shadow-2xl sm:inset-x-auto"
|
||
|
|
role="dialog"
|
||
|
|
aria-modal="true"
|
||
|
|
aria-label="Keyboard Shortcuts"
|
||
|
|
>
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-center justify-between mb-6">
|
||
|
|
<h2 className="text-xl font-semibold text-foreground">
|
||
|
|
Keyboard Shortcuts
|
||
|
|
</h2>
|
||
|
|
<button
|
||
|
|
onClick={onClose}
|
||
|
|
className="p-2 rounded-lg hover:bg-muted transition-colors"
|
||
|
|
aria-label="Close"
|
||
|
|
>
|
||
|
|
<X className="h-5 w-5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Groups */}
|
||
|
|
<div className="space-y-6">
|
||
|
|
{shortcutGroups.map((group) => (
|
||
|
|
<div key={group.title}>
|
||
|
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-3">
|
||
|
|
{group.title}
|
||
|
|
</h3>
|
||
|
|
<div className="space-y-1">
|
||
|
|
{group.shortcuts.map((shortcut) => (
|
||
|
|
<div
|
||
|
|
key={shortcut.description}
|
||
|
|
className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-muted/50 transition-colors"
|
||
|
|
>
|
||
|
|
<span className="text-sm text-foreground">
|
||
|
|
{shortcut.description}
|
||
|
|
</span>
|
||
|
|
<div className="flex items-center gap-1">
|
||
|
|
{shortcut.keys.map((key, i) => (
|
||
|
|
<span key={i}>
|
||
|
|
<kbd className="inline-flex h-6 min-w-6 items-center justify-center rounded-md border border-border bg-muted px-1.5 text-[11px] font-medium text-muted-foreground">
|
||
|
|
{key}
|
||
|
|
</kbd>
|
||
|
|
{i < shortcut.keys.length - 1 && (
|
||
|
|
<span className="mx-0.5 text-xs text-muted-foreground">
|
||
|
|
+
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Footer */}
|
||
|
|
<div className="mt-6 pt-4 border-t border-border">
|
||
|
|
<p className="text-xs text-muted-foreground text-center">
|
||
|
|
Press{' '}
|
||
|
|
<kbd className="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1 text-[10px] font-medium">
|
||
|
|
?
|
||
|
|
</kbd>{' '}
|
||
|
|
to toggle this panel
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
);
|
||
|
|
}
|