veza/apps/web/src/components/ui/KeyboardShortcutsPanel.tsx
senke d829e228a8 feat(ui): profile page premium polish + keyboard shortcuts panel
Profile page:
- Hero: gradient upgrade, animated shimmer sweep, pulsing glow orb, bottom fade
- Header card: avatar ring glow, stats with icons (data-driven), tabular-nums
- Tabs: stagger animation on grid items, tab trigger transitions
- Skeleton: consistent with loaded state styling
- Page entry animation (fade-in)

Keyboard shortcuts panel (Discord-style):
- New KeyboardShortcutsPanel component with framer-motion animations
- Groups: General, Playback, Navigation
- Styled kbd badges with semantic tokens
- ARIA: role=dialog, aria-modal, aria-label
- Replaces old KeyboardShortcutsHelp component
- Fix: ? key handler no longer blocked by !e.shiftKey guard

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 00:15:50 +01:00

151 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>
);
}