import React, { useState, useRef, useEffect, useMemo } from 'react' import { Search } from 'lucide-react' // --- Types --- export interface MenuItem { id: string label: string shortcut?: string action?: () => void disabled?: boolean separator?: boolean // renders a divider line submenu?: MenuItem[] } export interface MenuDefinition { id: string label: string items: MenuItem[] } interface MenuBarProps { menus: MenuDefinition[] rightContent?: React.ReactNode } // --- Component --- export function MenuBar({ menus, rightContent }: MenuBarProps) { const [openMenuId, setOpenMenuId] = useState(null) const [hoverMenuId, setHoverMenuId] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [highlightedResult, setHighlightedResult] = useState(0) const menuBarRef = useRef(null) const searchInputRef = useRef(null) // The active open menu (follow hover once a menu is open) const activeMenuId = openMenuId ? (hoverMenuId || openMenuId) : null // Close menus on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (menuBarRef.current && !menuBarRef.current.contains(e.target as Node)) { setOpenMenuId(null) setHoverMenuId(null) } } if (openMenuId) { document.addEventListener('mousedown', handleClickOutside) } return () => document.removeEventListener('mousedown', handleClickOutside) }, [openMenuId]) // Close on Escape useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { setOpenMenuId(null) setHoverMenuId(null) setSearchQuery('') } } document.addEventListener('keydown', handleKey) return () => document.removeEventListener('keydown', handleKey) }, []) const searchResults = useMemo(() => { const query = searchQuery.trim().toLowerCase() if (!query) return [] const results: { menuLabel: string; item: MenuItem }[] = [] for (const menu of menus) { for (const item of menu.items) { if (item.separator) continue if (item.label.toLowerCase().includes(query)) { results.push({ menuLabel: menu.label, item }) } if (item.submenu) { for (const sub of item.submenu) { if (sub.separator) continue if (sub.label.toLowerCase().includes(query)) { results.push({ menuLabel: `${menu.label} > ${item.label}`, item: sub }) } } } } } return results }, [menus, searchQuery]) useEffect(() => { setHighlightedResult(prev => { if (searchResults.length === 0) return 0 return Math.min(prev, searchResults.length - 1) }) }, [searchResults.length]) const handleItemClick = (item: MenuItem) => { if (item.disabled || !item.action) return item.action() setOpenMenuId(null) setHoverMenuId(null) } const handleSearchResultClick = (item: MenuItem) => { if (item.disabled || !item.action) return item.action() setSearchQuery('') setOpenMenuId(null) setHoverMenuId(null) } // Handle search keyboard navigation const handleSearchKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault() setHighlightedResult(prev => Math.min(prev + 1, searchResults.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setHighlightedResult(prev => Math.max(prev - 1, 0)) } else if (e.key === 'Enter' && searchResults[highlightedResult]) { e.preventDefault() handleSearchResultClick(searchResults[highlightedResult].item) } } const renderMenuItem = (item: MenuItem, index: number) => { if (item.separator) { return
} return ( ) } return (
{menus.map(menu => { const isActive = activeMenuId === menu.id const isHelpMenu = menu.id === 'help' return (
{/* Dropdown */} {isActive && (
{/* Help menu has search */} {isHelpMenu && (
setSearchQuery(e.target.value)} onKeyDown={handleSearchKeyDown} className="flex-1 bg-transparent text-[13px] text-white placeholder-zinc-500 outline-none" autoFocus />
{/* Search results */} {searchQuery && (
{searchResults.length === 0 ? (
No results
) : ( searchResults.map((result, i) => ( )) )}
)}
)} {/* Regular menu items */} {menu.items.map((item, i) => renderMenuItem(item, i))}
)}
) })}
{rightContent && (
{rightContent}
)}
) }