KeyboardShortcut.tsx67 lines · main
1import { cn } from '../../lib/utils'
2
3const 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
16const 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
30type KeyboardShortcutProps = {
31 keys: string[]
32 variant?: 'pill' | 'inline'
33 className?: string
34}
35
36const 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
43const 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
49export 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}