Files
metabuilder/hooks/useKeyboardShortcuts.ts
johndoe6345789 5aabff44cd refactor(fakemui): flatten QML components directory structure and update documentation
Directory Restructuring:
- qml/qml-components/qml-components/* → qml/components/ (flattens nesting)
- All 104 QML files moved with git history preserved
- Eliminates redundant qml-components nesting

Documentation Updates:
- ARCHITECTURE.md: Updated qml/components references (2 locations)
- GETTING_STARTED.md: Updated qml/components path (1 location, end of file)
- README.md: Updated qml/components references (3 locations)
- CODE_REVIEW.md: Updated qml/components file paths (4 locations)
- docs/ARCHITECTURE.md: Complete refactor with qml/components paths

Verification:
-  No remaining qml-components/ references in documentation
-  All 104 QML files present in flattened structure
-  Directory structure verified (12 component categories)
-  First-class directory naming convention

Structure Post-Refactor:
qml/
├── components/
│   ├── atoms/ (16 files)
│   ├── core/ (11 files)
│   ├── data-display/ (10 files)
│   ├── feedback/ (11 files)
│   ├── form/ (19 files)
│   ├── lab/ (11 files)
│   ├── layout/ (12 files)
│   ├── navigation/ (12 files)
│   ├── surfaces/ (7 files)
│   ├── theming/ (4 files)
│   └── utils/ (13 files)
├── hybrid/
└── widgets/

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-23 19:54:21 +00:00

174 lines
4.8 KiB
TypeScript

/**
* useKeyboardShortcuts Hook
* Unified keyboard shortcut handling with meta key support
*
* Features:
* - Register shortcuts with meta keys (cmd/ctrl, shift, alt, etc)
* - Automatic platform detection (macOS vs Windows/Linux)
* - Prevent default browser shortcuts
* - Multiple callbacks per shortcut
* - Clean unregistration and complete clearing
* - Debouncing and preventDefault handling
*
* @example
* const { registerShortcut, unregister, clearAll } = useKeyboardShortcuts()
*
* // Simple shortcut
* registerShortcut({
* key: 's',
* ctrl: true,
* onPress: () => console.log('Save triggered')
* })
*
* // macOS-specific shortcut
* registerShortcut({
* key: 's',
* cmd: true,
* onPress: () => console.log('Save on Mac'),
* preventDefault: true
* })
*
* // Multiple keys
* registerShortcut({
* key: 'Enter',
* ctrl: true,
* shift: true,
* onPress: () => console.log('Ctrl+Shift+Enter')
* })
*
* @example
* // Cleanup on unmount
* useEffect(() => {
* const id = registerShortcut({
* key: 'Escape',
* onPress: () => setOpen(false)
* })
* return () => unregister(id)
* }, [])
*/
import { useEffect, useRef, useCallback } from 'react'
export interface KeyboardShortcut {
/** Key to listen for (e.g., 's', 'Enter', 'ArrowUp') */
key: string
/** Trigger on Ctrl key (Windows/Linux) */
ctrl?: boolean
/** Trigger on Cmd key (macOS) */
cmd?: boolean
/** Trigger on Shift key */
shift?: boolean
/** Trigger on Alt key */
alt?: boolean
/** Callback when shortcut is triggered */
onPress: () => void
/** Whether to preventDefault on keydown */
preventDefault?: boolean
/** Debounce delay in ms */
debounce?: number
}
export interface UseKeyboardShortcutsReturn {
/** Register a new keyboard shortcut, returns ID for unregistration */
registerShortcut: (shortcut: KeyboardShortcut) => string
/** Unregister a specific shortcut by ID */
unregister: (id: string) => void
/** Clear all registered shortcuts */
clearAll: () => void
}
const isMac = () => {
if (typeof window === 'undefined') return false
return /Mac|iPhone|iPad|iPod/.test(navigator.platform || '')
}
export function useKeyboardShortcuts(): UseKeyboardShortcutsReturn {
const shortcutsRef = useRef<Map<string, KeyboardShortcut & { timeoutId?: NodeJS.Timeout }>>(
new Map()
)
const platformIsMac = useRef(isMac())
const registerShortcut = useCallback((shortcut: KeyboardShortcut): string => {
const id = `shortcut-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
shortcutsRef.current.set(id, shortcut)
return id
}, [])
const unregister = useCallback((id: string) => {
const shortcut = shortcutsRef.current.get(id)
if (shortcut?.timeoutId) {
clearTimeout(shortcut.timeoutId)
}
shortcutsRef.current.delete(id)
}, [])
const clearAll = useCallback(() => {
shortcutsRef.current.forEach((shortcut) => {
if (shortcut.timeoutId) {
clearTimeout(shortcut.timeoutId)
}
})
shortcutsRef.current.clear()
}, [])
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
shortcutsRef.current.forEach((shortcut, id) => {
const isKeyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase()
// Determine if meta key matches (cmd on Mac, ctrl on Windows/Linux)
const isMetaMatch = (() => {
if (shortcut.cmd) {
return platformIsMac.current ? event.metaKey : false
}
if (shortcut.ctrl) {
return platformIsMac.current ? event.metaKey : event.ctrlKey
}
return !shortcut.cmd && !shortcut.ctrl
})()
const isShiftMatch = shortcut.shift ? event.shiftKey : true
const isAltMatch = shortcut.alt ? event.altKey : true
if (isKeyMatch && isMetaMatch && isShiftMatch && isAltMatch) {
if (shortcut.preventDefault) {
event.preventDefault()
}
// Handle debouncing
if (shortcut.debounce) {
const current = shortcutsRef.current.get(id)
if (current?.timeoutId) {
clearTimeout(current.timeoutId)
}
const timeoutId = setTimeout(() => {
shortcut.onPress()
}, shortcut.debounce)
shortcutsRef.current.set(id, { ...shortcut, timeoutId })
} else {
shortcut.onPress()
}
}
})
}
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
// Cleanup debounce timeouts
shortcutsRef.current.forEach((shortcut) => {
if (shortcut.timeoutId) {
clearTimeout(shortcut.timeoutId)
}
})
}
}, [])
return {
registerShortcut,
unregister,
clearAll,
}
}