KeyboardShortcut.tsx67 lines · main
| 1 | import { cn } from '../../lib/utils' |
| 2 | |
| 3 | const getIsMac = () => { |
| 4 | if (typeof navigator === 'undefined') return false |
| 5 | |
| 6 | if (navigator.userAgent && navigator.userAgent.includes('Mac')) { |
| 7 | return true |
| 8 | } |
| 9 | |
| 10 | if (navigator.platform) { |
| 11 | return navigator.platform.startsWith('Mac') || navigator.platform === 'iPhone' |
| 12 | } |
| 13 | return false |
| 14 | } |
| 15 | |
| 16 | const KEY_SYMBOLS: Record<string, string | ((isMac: boolean) => string)> = { |
| 17 | Meta: (isMac) => (isMac ? '⌘' : 'Ctrl'), |
| 18 | Alt: (isMac) => (isMac ? '⌥' : 'Alt'), |
| 19 | Shift: '⇧', |
| 20 | Enter: '↵', |
| 21 | ArrowUp: '↑', |
| 22 | ArrowDown: '↓', |
| 23 | ArrowLeft: '←', |
| 24 | ArrowRight: '→', |
| 25 | Esc: 'Esc', // ⎋ symbol not recognisable enough |
| 26 | Escape: 'Esc', // Match above |
| 27 | Tab: 'Tab', // ⇥ symbol not recognisable enough |
| 28 | } |
| 29 | |
| 30 | type KeyboardShortcutProps = { |
| 31 | keys: string[] |
| 32 | variant?: 'pill' | 'inline' |
| 33 | className?: string |
| 34 | } |
| 35 | |
| 36 | const resolveKeyLabel = (key: string, isMac: boolean) => { |
| 37 | const symbol = KEY_SYMBOLS[key] |
| 38 | const resolvedKey = typeof symbol === 'function' ? symbol(isMac) : (symbol ?? key) |
| 39 | |
| 40 | return resolvedKey.length === 1 ? resolvedKey.toUpperCase() : resolvedKey |
| 41 | } |
| 42 | |
| 43 | const formatShortcutLabel = (keys: string[]) => { |
| 44 | if (keys.length <= 1) return keys.join('') |
| 45 | |
| 46 | return keys.every((key) => key.length === 1) ? keys.join('') : keys.join(' ') |
| 47 | } |
| 48 | |
| 49 | export const KeyboardShortcut = ({ keys, variant = 'pill', className }: KeyboardShortcutProps) => { |
| 50 | const isMac = getIsMac() |
| 51 | const resolvedKeys = keys.map((key) => resolveKeyLabel(key, isMac)) |
| 52 | const shortcutLabel = formatShortcutLabel(resolvedKeys) |
| 53 | |
| 54 | return ( |
| 55 | <span |
| 56 | className={cn( |
| 57 | 'inline-flex whitespace-nowrap shrink-0', |
| 58 | variant === 'pill' |
| 59 | ? 'items-center text-[11px] leading-none tracking-[-0.025em] text-foreground-light bg-surface-200/50 dark:bg-surface-100/50 rounded px-[5px] py-[3px] border border-border-muted' |
| 60 | : 'items-baseline text-[11px] leading-[inherit] text-foreground/40', |
| 61 | className |
| 62 | )} |
| 63 | > |
| 64 | {shortcutLabel} |
| 65 | </span> |
| 66 | ) |
| 67 | } |