mirror of
https://github.com/stackblitz/bolt.new
synced 2024-11-27 22:42:21 +00:00
149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
});
|