'use client' import * as React from 'react' import type { ContentEditorSlashCommand } from '@admin/utils/editor/slash-commands' import type { SlashMenuState } from '@admin/utils/editor/slash-menu-state' import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from '@admin/components/ui/combobox' import { cn } from '@admin/utils/shared/cn' export function SlashCommandMenu({ activeIndex, commands, onActiveIndexChange, onClose, onCommandSelect, onSearchChange, slashMenu, searchQuery }: { activeIndex: number commands: ContentEditorSlashCommand[] onActiveIndexChange: (index: number) => void onClose: () => void onCommandSelect: (command: ContentEditorSlashCommand) => void onSearchChange: (query: string) => void slashMenu: SlashMenuState | null searchQuery: string }) { const activeItemRef = React.useRef(null) const searchInputRef = React.useRef(null) const activeCommandId = commands[activeIndex]?.id const handleSearchKeyDown = (event: React.KeyboardEvent) => { const stopComboboxKeyHandling = () => { const baseUIEvent = event as React.KeyboardEvent & { preventBaseUIHandler?: () => void } event.preventDefault() event.stopPropagation() baseUIEvent.preventBaseUIHandler?.() } if (event.key === 'ArrowDown') { stopComboboxKeyHandling() onActiveIndexChange(commands.length > 0 ? (activeIndex + 1) % commands.length : 0) return } if (event.key === 'ArrowUp') { stopComboboxKeyHandling() onActiveIndexChange( commands.length > 0 ? (activeIndex - 1 + commands.length) % commands.length : 0 ) return } if (event.key === 'Escape') { stopComboboxKeyHandling() onClose() return } if (event.key === 'Enter' || event.key === 'Tab') { const command = commands[activeIndex] if (!command) return stopComboboxKeyHandling() onCommandSelect(command) } } React.useEffect(() => { if (!slashMenu) return const focusSearchInput = () => { searchInputRef.current?.focus({ preventScroll: true }) searchInputRef.current?.select() } focusSearchInput() const frame = requestAnimationFrame(focusSearchInput) return () => cancelAnimationFrame(frame) }, [slashMenu]) React.useEffect(() => { if (!activeCommandId) return activeItemRef.current?.scrollIntoView({ block: 'nearest' }) }, [activeCommandId]) if (!slashMenu) return null const slashAnchor = { getBoundingClientRect: () => slashMenu.rect } return ( autoHighlight filter={null} filteredItems={commands} inputValue={searchQuery} itemToStringLabel={(command) => command.title} itemToStringValue={(command) => command.id} items={commands} onInputValueChange={onSearchChange} onItemHighlighted={(command) => { if (!command) return const nextIndex = commands.findIndex((item) => item.id === command.id) if (nextIndex >= 0) onActiveIndexChange(nextIndex) }} onOpenChange={(isOpen) => { if (!isOpen) onClose() }} onValueChange={(command) => { if (command) onCommandSelect(command) }} open value={null} > No commands found {commands.map((command, index) => { const Icon = command.Icon const isActive = index === activeIndex return ( { if (isActive) activeItemRef.current = node }} onMouseDown={(event) => event.preventDefault()} onMouseEnter={() => onActiveIndexChange(index)} value={command} className={cn( 'border border-transparent p-2 data-highlighted:border-border! data-highlighted:bg-card!', { 'border-border bg-card text-foreground shadow-input': isActive } )} > {command.title} {command.description} ) })} ) }