Files
metabuilder/hooks/QUICK_START.md
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

8.6 KiB

Keyboard & Event Hooks - Quick Start

Fast reference for the 4 new hooks added to the /hooks folder.

Import Statements

import { useKeyboardShortcuts } from '@metabuilder/hooks'
import { useClickOutside } from '@metabuilder/hooks'
import { useHotkeys } from '@metabuilder/hooks'
import { useEventListener } from '@metabuilder/hooks'

1. useKeyboardShortcuts

Register keyboard shortcuts with automatic platform detection.

const { registerShortcut, unregister, clearAll } = useKeyboardShortcuts()

// Register shortcut
useEffect(() => {
  const id = registerShortcut({
    key: 's',
    ctrl: true,           // Cmd on Mac, Ctrl on Windows
    shift: false,
    alt: false,
    onPress: () => save(),
    preventDefault: true,
    debounce: 0           // Optional: ms delay
  })

  return () => unregister(id)
}, [])

Common patterns:

// Save
registerShortcut({ key: 's', ctrl: true, onPress: save, preventDefault: true })

// Open search
registerShortcut({ key: 'f', ctrl: true, onPress: openSearch, preventDefault: true })

// Close dialog
registerShortcut({ key: 'Escape', onPress: closeDialog })

// Navigate up/down
registerShortcut({ key: 'ArrowUp', onPress: () => selectPrev() })
registerShortcut({ key: 'ArrowDown', onPress: () => selectNext() })

2. useClickOutside

Detect clicks outside an element to close modals, dropdowns, etc.

const { ref, isOpen, setIsOpen, toggle } = useClickOutside<HTMLDivElement>({
  onClickOutside: () => console.log('Closed'),
  excludeRefs: [triggerButtonRef],  // Don't close on these refs
  includeTouch: true,                // Include touch events
  delayMs: 0                         // Delay before closing
})

return (
  <>
    <button ref={triggerButtonRef} onClick={() => toggle()}>
      Open
    </button>
    {isOpen && (
      <div ref={ref}>
        {/* Clicks outside this div close it */}
      </div>
    )}
  </>
)

Common patterns:

// Simple dropdown
const { ref, isOpen, setIsOpen } = useClickOutside()

// Modal that can't be closed by clicking trigger button
const { ref, isOpen, setIsOpen } = useClickOutside({
  excludeRefs: [triggerButtonRef]
})

// With callback
const { ref, isOpen, setIsOpen } = useClickOutside({
  onClickOutside: () => console.log('Closed from outside')
})

3. useHotkeys

Global hotkey registration with combo key support (ctrl+shift+k).

const hotkeys = useHotkeys()

useEffect(() => {
  // Register hotkey
  const id = hotkeys.register('ctrl+s', () => {
    save()
  }, {
    preventDefault: true,    // Prevent browser default
    enabled: true,           // Enable/disable
    debounceMs: 0           // Optional: delay
  })

  // Cleanup
  return () => hotkeys.unregister(id)
}, [hotkeys])

Common patterns:

// Save: Ctrl+S (Mac: Cmd+S)
hotkeys.register('ctrl+s', save, { preventDefault: true })

// Find: Ctrl+F
hotkeys.register('ctrl+f', openFind, { preventDefault: true })

// Replace: Ctrl+H
hotkeys.register('ctrl+h', openReplace, { preventDefault: true })

// Format: Ctrl+Shift+I
hotkeys.register('ctrl+shift+i', format, { preventDefault: true })

// Close all hotkeys at once
useEffect(() => {
  return () => hotkeys.unregisterAll()
}, [hotkeys])

Combo key formats:

  • 'ctrl+s' - Ctrl+S (or Cmd on Mac)
  • 'shift+enter' - Shift+Enter
  • 'ctrl+shift+k' - Ctrl+Shift+K
  • 'alt+1' - Alt+1
  • 'cmd+opt+i' - Cmd+Opt+I (Mac specific)

4. useEventListener

Generic event listener with automatic cleanup.

const { add, remove, removeAll } = useEventListener()

useEffect(() => {
  // Add listener and get cleanup function
  const cleanup = add(
    window,
    'resize',
    (e: UIEvent) => {
      console.log('Resized')
    },
    {
      passive: true,    // Better performance for scroll/touch
      capture: false,   // Use capture phase
      once: false       // Auto-remove after first trigger
    }
  )

  // Return cleanup function
  return cleanup
}, [add])

Common patterns:

// Window resize
add(window, 'resize', (e: UIEvent) => updateSize(), { passive: true })

// Window scroll
add(window, 'scroll', (e: Event) => updateScroll(), { passive: true })

// Element input
add(inputRef.current, 'input', (e: Event) => {
  const target = e.target as HTMLInputElement
  updateValue(target.value)
})

// Document click
add(document, 'click', (e: MouseEvent) => {
  console.log('Clicked at', e.clientX, e.clientY)
})

// Multiple listeners
useEffect(() => {
  add(window, 'resize', handleResize)
  add(window, 'scroll', handleScroll)
  add(document, 'click', handleClick)

  return removeAll  // Clean up all at once
}, [add, removeAll])

Real-World Examples

Example 1: Command Palette

function CommandPalette() {
  const [isOpen, setIsOpen] = useState(false)
  const { registerShortcut } = useKeyboardShortcuts()

  useEffect(() => {
    const id = registerShortcut({
      key: 'k',
      ctrl: true,
      onPress: () => setIsOpen(true),
      preventDefault: true
    })

    const closeId = registerShortcut({
      key: 'Escape',
      onPress: () => setIsOpen(false)
    })

    return () => {
      unregister(id)
      unregister(closeId)
    }
  }, [registerShortcut])

  return (
    <>
      <button>Cmd+K</button>
      {isOpen && <CommandPaletteModal />}
    </>
  )
}

Example 2: Dropdown Menu

function DropdownMenu() {
  const triggerRef = useRef<HTMLButtonElement>(null)
  const dropdown = useClickOutside<HTMLDivElement>({
    excludeRefs: [triggerRef]
  })

  return (
    <>
      <button ref={triggerRef} onClick={() => dropdown.toggle()}>
        Menu
      </button>
      {dropdown.isOpen && (
        <div ref={dropdown.ref}>
          <a href="/profile">Profile</a>
          <a href="/settings">Settings</a>
          <a href="/logout">Logout</a>
        </div>
      )}
    </>
  )
}

Example 3: IDE Editor Shortcuts

function CodeEditor() {
  const hotkeys = useHotkeys()

  useEffect(() => {
    hotkeys.register('ctrl+s', saveFile, { preventDefault: true })
    hotkeys.register('ctrl+f', openFind, { preventDefault: true })
    hotkeys.register('ctrl+h', openReplace, { preventDefault: true })
    hotkeys.register('ctrl+shift+i', formatCode, { preventDefault: true })
    hotkeys.register('Escape', closeDialogs)

    return () => hotkeys.unregisterAll()
  }, [hotkeys])

  return <Editor />
}

Example 4: Responsive Layout

function ResponsiveLayout() {
  const [size, setSize] = useState({ width: 0, height: 0 })
  const { add } = useEventListener()

  useEffect(() => {
    return add(window, 'resize', () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }, { passive: true })
  }, [add])

  return (
    <div>
      {size.width < 768 && <MobileLayout />}
      {size.width >= 768 && <DesktopLayout />}
    </div>
  )
}

Type Safety

All hooks have full TypeScript support:

// useKeyboardShortcuts
interface KeyboardShortcut {
  key: string
  ctrl?: boolean
  cmd?: boolean
  shift?: boolean
  alt?: boolean
  onPress: () => void
  preventDefault?: boolean
  debounce?: number
}

// useClickOutside<T extends HTMLElement>
interface UseClickOutsideOptions {
  onClickOutside?: () => void
  excludeRefs?: React.RefObject<HTMLElement>[]
  includeTouch?: boolean
  delayMs?: number
}

// useHotkeys
interface HotkeysOptions {
  preventDefault?: boolean
  enabled?: boolean
  debounceMs?: number
}

// useEventListener
type EventHandler<T extends Event = Event> = (event: T) => void
interface EventListenerOptions extends AddEventListenerOptions {
  passive?: boolean
  capture?: boolean
  once?: boolean
}

Common Gotchas

  1. Always cleanup: Return cleanup function from useEffect

    // ✓ Good
    useEffect(() => {
      const id = registerShortcut(...)
      return () => unregister(id)
    }, [])
    
  2. Exclude refs matter: Remember to exclude trigger button

    // ✓ Good - won't close when clicking button
    useClickOutside({ excludeRefs: [triggerRef] })
    
  3. Passive listeners can't preventDefault:

    // ✗ Won't work - passive listeners can't prevent default
    add(window, 'scroll', (e) => e.preventDefault(), { passive: true })
    
  4. Platform detection is automatic:

    // ✓ Good - automatically uses Cmd on Mac, Ctrl on Windows
    registerShortcut({ key: 's', ctrl: true, onPress: save })
    

Full Documentation

See KEYBOARD_EVENT_HOOKS.md for comprehensive documentation with more examples, best practices, performance tips, and troubleshooting guide.