import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { AnimatePresence, motion, type Variants } from 'framer-motion'; import { memo, useEffect, useRef, useState } from 'react'; import type { FileMap } from '~/lib/stores/files'; import { classNames } from '~/utils/classNames'; import { WORK_DIR } from '~/utils/constants'; import { cubicEasingFn } from '~/utils/easings'; import { renderLogger } from '~/utils/logger'; import FileTree from './FileTree'; const WORK_DIR_REGEX = new RegExp(`^${WORK_DIR.split('/').slice(0, -1).join('/').replaceAll('/', '\\/')}/`); interface FileBreadcrumbProps { files?: FileMap; pathSegments?: string[]; onFileSelect?: (filePath: string) => void; } const contextMenuVariants = { open: { y: 0, opacity: 1, transition: { duration: 0.15, ease: cubicEasingFn, }, }, close: { y: 6, opacity: 0, transition: { duration: 0.15, ease: cubicEasingFn, }, }, } satisfies Variants; export const FileBreadcrumb = memo<FileBreadcrumbProps>(({ files, pathSegments = [], onFileSelect }) => { renderLogger.trace('FileBreadcrumb'); const [activeIndex, setActiveIndex] = useState<number | null>(null); const contextMenuRef = useRef<HTMLDivElement | null>(null); const segmentRefs = useRef<(HTMLSpanElement | null)[]>([]); const handleSegmentClick = (index: number) => { setActiveIndex((prevIndex) => (prevIndex === index ? null : index)); }; useEffect(() => { const handleOutsideClick = (event: MouseEvent) => { if ( activeIndex !== null && !contextMenuRef.current?.contains(event.target as Node) && !segmentRefs.current.some((ref) => ref?.contains(event.target as Node)) ) { setActiveIndex(null); } }; document.addEventListener('mousedown', handleOutsideClick); return () => { document.removeEventListener('mousedown', handleOutsideClick); }; }, [activeIndex]); if (files === undefined || pathSegments.length === 0) { return null; } return ( <div className="flex"> {pathSegments.map((segment, index) => { const isLast = index === pathSegments.length - 1; const path = pathSegments.slice(0, index).join('/'); if (!WORK_DIR_REGEX.test(path)) { return null; } const isActive = activeIndex === index; return ( <div key={index} className="relative flex items-center"> <DropdownMenu.Root open={isActive} modal={false}> <DropdownMenu.Trigger asChild> <span ref={(ref) => (segmentRefs.current[index] = ref)} className={classNames('flex items-center gap-1.5 cursor-pointer shrink-0', { 'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary': !isActive, 'text-bolt-elements-textPrimary underline': isActive, 'pr-4': isLast, })} onClick={() => handleSegmentClick(index)} > {isLast && <div className="i-ph:file-duotone" />} {segment} </span> </DropdownMenu.Trigger> {index > 0 && !isLast && <span className="i-ph:caret-right inline-block mx-1" />} <AnimatePresence> {isActive && ( <DropdownMenu.Portal> <DropdownMenu.Content className="z-file-tree-breadcrumb" asChild align="start" side="bottom" avoidCollisions={false} > <motion.div ref={contextMenuRef} initial="close" animate="open" exit="close" variants={contextMenuVariants} > <div className="rounded-lg overflow-hidden"> <div className="max-h-[50vh] min-w-[300px] overflow-scroll bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor shadow-sm rounded-lg"> <FileTree files={files} hideRoot rootFolder={path} collapsed allowFolderSelection selectedFile={`${path}/${segment}`} onFileSelect={(filePath) => { setActiveIndex(null); onFileSelect?.(filePath); }} /> </div> </div> <DropdownMenu.Arrow className="fill-bolt-elements-borderColor" /> </motion.div> </DropdownMenu.Content> </DropdownMenu.Portal> )} </AnimatePresence> </DropdownMenu.Root> </div> ); })} </div> ); });