/** * File Menu Component * * 文件自动补全菜单,支持键盘导航 */ import { useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { File, Folder } from 'lucide-react'; import { cn } from '../utils/cn.js'; export interface FileMenuItem { path: string; name: string; type: 'file' | 'directory'; extension?: string; } interface FileMenuProps { /** 文件列表 */ files: FileMenuItem[]; /** 是否显示 */ isOpen: boolean; /** 当前选中索引 */ selectedIndex: number; /** 选择文件回调 */ onSelect: (file: FileMenuItem) => void; /** 关闭菜单回调 */ onClose: () => void; /** 选中索引变化回调 */ onSelectedIndexChange: (index: number) => void; /** 是否正在加载 */ isLoading?: boolean; } // 文件图标颜色 function getFileIconColor(extension?: string): string { const colors: Record = { ts: 'text-blue-400', tsx: 'text-blue-400', js: 'text-yellow-400', jsx: 'text-yellow-400', json: 'text-yellow-500', md: 'text-fg-muted', css: 'text-pink-400', scss: 'text-pink-400', html: 'text-orange-400', py: 'text-green-400', go: 'text-cyan-400', rs: 'text-orange-500', vue: 'text-emerald-400', svelte: 'text-orange-500', }; return colors[extension || ''] || 'text-fg-muted'; } export function FileMenu({ files, isOpen, selectedIndex, onSelect, onClose, onSelectedIndexChange, isLoading = false, }: FileMenuProps) { const menuRef = useRef(null); const selectedRef = useRef(null); // 滚动选中项到可见区域 useEffect(() => { if (isOpen && selectedRef.current) { selectedRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth', }); } }, [selectedIndex, isOpen]); // 键盘导航 useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); onSelectedIndexChange(selectedIndex < files.length - 1 ? selectedIndex + 1 : 0); break; case 'ArrowUp': e.preventDefault(); onSelectedIndexChange(selectedIndex > 0 ? selectedIndex - 1 : files.length - 1); break; case 'Enter': e.preventDefault(); if (files[selectedIndex]) { onSelect(files[selectedIndex]); } break; case 'Escape': e.preventDefault(); onClose(); break; case 'Tab': e.preventDefault(); if (files[selectedIndex]) { onSelect(files[selectedIndex]); } break; } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, files, selectedIndex, onSelect, onClose, onSelectedIndexChange]); return ( {isOpen && (
{/* Header */}
Files {files.length > 0 && ({files.length})}
{/* Loading */} {isLoading && files.length === 0 && (
Searching files...
)} {/* Empty state */} {!isLoading && files.length === 0 && (
No files found
)} {/* File list */} {files.length > 0 && (
{files.map((file, index) => ( ))}
)} {/* Footer hint */}
navigate Tab select Esc close
)}
); }