sourcebot/packages/web/src/features/fileTree/components/fileTreeItemComponent.tsx

107 lines
3.6 KiB
TypeScript
Raw Normal View History

2025-06-06 19:38:16 +00:00
'use client';
import { FileTreeItem } from "../actions";
import { useMemo, useEffect, useRef } from "react";
import { getIconForFile, getIconForFolder } from "vscode-icons-js";
import { Icon } from '@iconify/react';
import clsx from "clsx";
import scrollIntoView from 'scroll-into-view-if-needed';
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
export const FileTreeItemComponent = ({
node,
isActive,
depth,
isCollapsed = false,
isCollapseChevronVisible = true,
onClick,
onMouseEnter,
parentRef,
}: {
node: FileTreeItem,
isActive: boolean,
depth: number,
isCollapsed?: boolean,
isCollapseChevronVisible?: boolean,
onClick: () => void,
onMouseEnter: () => void,
parentRef: React.RefObject<HTMLDivElement>,
}) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isActive && ref.current) {
scrollIntoView(ref.current, {
scrollMode: 'if-needed',
block: 'center',
behavior: 'instant',
// We only want to scroll if the element is hidden vertically
// in the parent element.
boundary: () => {
if (!parentRef.current || !ref.current) {
return false;
}
const rect = ref.current.getBoundingClientRect();
const parentRect = parentRef.current.getBoundingClientRect();
const completelyAbove = rect.bottom <= parentRect.top;
const completelyBelow = rect.top >= parentRect.bottom;
return completelyAbove || completelyBelow;
}
});
}
}, [isActive, parentRef]);
const iconName = useMemo(() => {
if (node.type === 'tree') {
const icon = getIconForFolder(node.name);
if (icon) {
const iconName = `vscode-icons:${icon.substring(0, icon.indexOf('.')).replaceAll('_', '-')}`;
return iconName;
}
} else if (node.type === 'blob') {
const icon = getIconForFile(node.name);
if (icon) {
const iconName = `vscode-icons:${icon.substring(0, icon.indexOf('.')).replaceAll('_', '-')}`;
return iconName;
}
}
return "vscode-icons:file-type-unknown";
}, [node.name, node.type]);
return (
<div
ref={ref}
className={clsx("flex flex-row gap-1 items-center hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer p-0.5", {
'bg-accent': isActive,
})}
style={{ paddingLeft: `${depth * 16}px` }}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
onClick();
}
}}
onClick={onClick}
onMouseEnter={onMouseEnter}
>
<div
className="flex flex-row gap-1 cursor-pointer w-4 h-4 flex-shrink-0"
>
{isCollapseChevronVisible && (
isCollapsed ? (
<ChevronRightIcon className="w-4 h-4 flex-shrink-0" />
) : (
<ChevronDownIcon className="w-4 h-4 flex-shrink-0" />
)
)}
</div>
<Icon icon={iconName} className="w-4 h-4 flex-shrink-0" />
<span className="text-sm">{node.name}</span>
</div>
)
}