mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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>
221 lines
5.7 KiB
TypeScript
221 lines
5.7 KiB
TypeScript
/**
|
|
* useHotkeys Hook
|
|
* Global hotkey registration with combo key support
|
|
*
|
|
* Features:
|
|
* - Support for combo keys (e.g., 'ctrl+shift+k', 'cmd+s')
|
|
* - Scoped hotkey management per component
|
|
* - Handler registration function
|
|
* - Automatic cleanup on unmount
|
|
* - Platform-aware (Mac vs Windows/Linux)
|
|
* - Prevent default behavior
|
|
* - Multiple handlers for same hotkey
|
|
*
|
|
* @example
|
|
* // Basic hotkey
|
|
* const hotkeys = useHotkeys()
|
|
*
|
|
* hotkeys.register('ctrl+s', () => {
|
|
* console.log('Save shortcut triggered')
|
|
* })
|
|
*
|
|
* @example
|
|
* // Platform-specific hotkeys
|
|
* const hotkeys = useHotkeys()
|
|
*
|
|
* // Use cmd on Mac, ctrl on Windows
|
|
* const saveKey = navigator.platform.includes('Mac') ? 'cmd+s' : 'ctrl+s'
|
|
*
|
|
* hotkeys.register(saveKey, () => saveDocument())
|
|
* hotkeys.register('Escape', () => closeDialog())
|
|
*
|
|
* @example
|
|
* // With options
|
|
* const hotkeys = useHotkeys()
|
|
*
|
|
* const id = hotkeys.register('ctrl+shift+k', () => {
|
|
* openCommandPalette()
|
|
* }, {
|
|
* preventDefault: true,
|
|
* enabled: isComponentVisible
|
|
* })
|
|
*
|
|
* // Unregister later
|
|
* useEffect(() => {
|
|
* return () => hotkeys.unregister(id)
|
|
* }, [])
|
|
*
|
|
* @example
|
|
* // Combo key parsing
|
|
* // Supports: 'ctrl', 'cmd', 'shift', 'alt', 'meta'
|
|
* // Case-insensitive key names
|
|
* hotkeys.register('ctrl+shift+enter', handleSubmit)
|
|
* hotkeys.register('cmd+opt+k', handleSearch) // opt = alt on Mac
|
|
*/
|
|
|
|
import { useEffect, useRef, useCallback } from 'react'
|
|
|
|
export interface HotkeysOptions {
|
|
/** Prevent default behavior on hotkey press */
|
|
preventDefault?: boolean
|
|
/** Whether this hotkey is currently enabled */
|
|
enabled?: boolean
|
|
/** Debounce delay in milliseconds */
|
|
debounceMs?: number
|
|
}
|
|
|
|
export interface UseHotkeysReturn {
|
|
/** Register a hotkey combo, returns ID for later unregistration */
|
|
register: (combo: string, handler: () => void, options?: HotkeysOptions) => string
|
|
/** Unregister a hotkey by ID */
|
|
unregister: (id: string) => void
|
|
/** Unregister all hotkeys for this instance */
|
|
unregisterAll: () => void
|
|
}
|
|
|
|
interface ParsedCombo {
|
|
ctrl: boolean
|
|
cmd: boolean
|
|
shift: boolean
|
|
alt: boolean
|
|
meta: boolean
|
|
key: string
|
|
}
|
|
|
|
interface RegisteredHotkey {
|
|
combo: ParsedCombo
|
|
handler: () => void
|
|
options: HotkeysOptions
|
|
lastTriggeredAt: number
|
|
}
|
|
|
|
const parseCombo = (combo: string): ParsedCombo => {
|
|
const parts = combo.toLowerCase().split('+')
|
|
const parsed: ParsedCombo = {
|
|
ctrl: false,
|
|
cmd: false,
|
|
shift: false,
|
|
alt: false,
|
|
meta: false,
|
|
key: '',
|
|
}
|
|
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const part = parts[i].trim()
|
|
if (part === 'ctrl' || part === 'control') parsed.ctrl = true
|
|
if (part === 'cmd' || part === 'command') parsed.cmd = true
|
|
if (part === 'shift') parsed.shift = true
|
|
if (part === 'alt' || part === 'option' || part === 'opt') parsed.alt = true
|
|
if (part === 'meta') parsed.meta = true
|
|
}
|
|
|
|
parsed.key = parts[parts.length - 1].trim()
|
|
return parsed
|
|
}
|
|
|
|
const isMac = () => {
|
|
if (typeof window === 'undefined') return false
|
|
return /Mac|iPhone|iPad|iPod/.test(navigator.platform || '')
|
|
}
|
|
|
|
export function useHotkeys(): UseHotkeysReturn {
|
|
const hotkeysRef = useRef<Map<string, RegisteredHotkey>>(new Map())
|
|
const platformIsMacRef = useRef(isMac())
|
|
|
|
const register = useCallback(
|
|
(combo: string, handler: () => void, options: HotkeysOptions = {}): string => {
|
|
const id = `hotkey-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
const parsedCombo = parseCombo(combo)
|
|
|
|
hotkeysRef.current.set(id, {
|
|
combo: parsedCombo,
|
|
handler,
|
|
options: {
|
|
preventDefault: false,
|
|
enabled: true,
|
|
debounceMs: 0,
|
|
...options,
|
|
},
|
|
lastTriggeredAt: 0,
|
|
})
|
|
|
|
return id
|
|
},
|
|
[]
|
|
)
|
|
|
|
const unregister = useCallback((id: string) => {
|
|
hotkeysRef.current.delete(id)
|
|
}, [])
|
|
|
|
const unregisterAll = useCallback(() => {
|
|
hotkeysRef.current.clear()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
hotkeysRef.current.forEach((hotkey) => {
|
|
if (!hotkey.options.enabled) return
|
|
|
|
const { combo, handler, options, lastTriggeredAt } = hotkey
|
|
|
|
// Check key match
|
|
const isKeyMatch = event.key.toLowerCase() === combo.key.toLowerCase()
|
|
|
|
// Determine if ctrl/cmd is pressed (cross-platform)
|
|
const isMetaPressed = (() => {
|
|
if (combo.cmd && combo.ctrl) {
|
|
return platformIsMacRef.current ? event.metaKey : event.ctrlKey
|
|
}
|
|
if (combo.cmd) {
|
|
return platformIsMacRef.current ? event.metaKey : false
|
|
}
|
|
if (combo.ctrl) {
|
|
return platformIsMacRef.current ? event.metaKey : event.ctrlKey
|
|
}
|
|
return true // No meta key required
|
|
})()
|
|
|
|
// Check shift
|
|
const isShiftMatch = combo.shift ? event.shiftKey : true
|
|
|
|
// Check alt (option on Mac)
|
|
const isAltMatch = combo.alt ? event.altKey : true
|
|
|
|
// Check meta
|
|
const isMetaMatch = combo.meta ? event.metaKey : true
|
|
|
|
if (isKeyMatch && isMetaPressed && isShiftMatch && isAltMatch && isMetaMatch) {
|
|
// Check debounce
|
|
const now = Date.now()
|
|
if (
|
|
options.debounceMs &&
|
|
now - lastTriggeredAt < options.debounceMs
|
|
) {
|
|
return
|
|
}
|
|
|
|
if (options.preventDefault) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
hotkey.lastTriggeredAt = now
|
|
handler()
|
|
}
|
|
})
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyDown)
|
|
}
|
|
}, [])
|
|
|
|
return {
|
|
register,
|
|
unregister,
|
|
unregisterAll,
|
|
}
|
|
}
|