'use client'; import { cn, getCodeHostInfoForRepo } from "@/lib/utils"; import Image from "next/image"; import { getBrowsePath } from "../browse/hooks/utils"; import { ChevronRight, MoreHorizontal } from "lucide-react"; import { useCallback, useState, useMemo, useRef, useEffect } from "react"; import { useToast } from "@/components/hooks/use-toast"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { CopyIconButton } from "./copyIconButton"; import Link from "next/link"; import { useDomain } from "@/hooks/useDomain"; import { CodeHostType } from "@sourcebot/db"; interface FileHeaderProps { path: string; pathHighlightRange?: { from: number; to: number; } pathType?: 'blob' | 'tree'; repo: { name: string; codeHostType: CodeHostType; displayName?: string; webUrl?: string; }, branchDisplayName?: string; branchDisplayTitle?: string; isCodeHostIconVisible?: boolean; isFileIconVisible?: boolean; repoNameClassName?: string; } interface BreadcrumbSegment { name: string; fullPath: string; isLastSegment: boolean; highlightRange?: { from: number; to: number; }; } export const PathHeader = ({ repo, path, pathHighlightRange, branchDisplayName, branchDisplayTitle, pathType = 'blob', isCodeHostIconVisible = true, isFileIconVisible = true, repoNameClassName, }: FileHeaderProps) => { const info = getCodeHostInfoForRepo({ name: repo.name, codeHostType: repo.codeHostType, displayName: repo.displayName, webUrl: repo.webUrl, }); const { toast } = useToast(); const containerRef = useRef(null); const breadcrumbsRef = useRef(null); const [visibleSegmentCount, setVisibleSegmentCount] = useState(null); const domain = useDomain(); // Create breadcrumb segments from file path const breadcrumbSegments = useMemo(() => { const pathParts = path.split('/').filter(Boolean); const segments: BreadcrumbSegment[] = []; let currentPath = ''; pathParts.forEach((part, index) => { currentPath = currentPath ? `${currentPath}/${part}` : part; const isLastSegment = index === pathParts.length - 1; // Calculate highlight range for this segment if it exists let segmentHighlight: { from: number; to: number } | undefined; if (pathHighlightRange) { const segmentStart = path.indexOf(part, currentPath.length - part.length); const segmentEnd = segmentStart + part.length; // Check if highlight overlaps with this segment if (pathHighlightRange.from < segmentEnd && pathHighlightRange.to > segmentStart) { segmentHighlight = { from: Math.max(0, pathHighlightRange.from - segmentStart), to: Math.min(part.length, pathHighlightRange.to - segmentStart) }; } } segments.push({ name: part, fullPath: currentPath, isLastSegment, highlightRange: segmentHighlight }); }); return segments; }, [path, pathHighlightRange]); // Calculate which segments should be visible based on available space useEffect(() => { const measureSegments = () => { if (!containerRef.current || !breadcrumbsRef.current) return; const containerWidth = containerRef.current.offsetWidth; const availableWidth = containerWidth - 175; // Reserve space for copy button and padding // Create a temporary element to measure segment widths const tempElement = document.createElement('div'); tempElement.style.position = 'absolute'; tempElement.style.visibility = 'hidden'; tempElement.style.whiteSpace = 'nowrap'; tempElement.className = 'font-mono text-sm'; document.body.appendChild(tempElement); let totalWidth = 0; let visibleCount = breadcrumbSegments.length; // Start from the end (most important segments) and work backwards for (let i = breadcrumbSegments.length - 1; i >= 0; i--) { const segment = breadcrumbSegments[i]; tempElement.textContent = segment.name; const segmentWidth = tempElement.offsetWidth; const separatorWidth = i < breadcrumbSegments.length - 1 ? 16 : 0; // ChevronRight width if (totalWidth + segmentWidth + separatorWidth > availableWidth && i > 0) { // If adding this segment would overflow and it's not the last segment visibleCount = breadcrumbSegments.length - i; // Add width for ellipsis dropdown (approximately 24px) if (visibleCount < breadcrumbSegments.length) { totalWidth += 40; // Ellipsis button + separator } break; } totalWidth += segmentWidth + separatorWidth; } document.body.removeChild(tempElement); setVisibleSegmentCount(visibleCount); }; measureSegments(); const resizeObserver = new ResizeObserver(measureSegments); if (containerRef.current) { resizeObserver.observe(containerRef.current); } return () => resizeObserver.disconnect(); }, [breadcrumbSegments]); const hiddenSegments = useMemo(() => { if (visibleSegmentCount === null || visibleSegmentCount >= breadcrumbSegments.length) { return []; } return breadcrumbSegments.slice(0, breadcrumbSegments.length - visibleSegmentCount); }, [breadcrumbSegments, visibleSegmentCount]); const visibleSegments = useMemo(() => { if (visibleSegmentCount === null) { return breadcrumbSegments; } return breadcrumbSegments.slice(breadcrumbSegments.length - visibleSegmentCount); }, [breadcrumbSegments, visibleSegmentCount]); const onCopyPath = useCallback(() => { navigator.clipboard.writeText(path); toast({ description: "✅ Copied to clipboard" }); return true; }, [path, toast]); const renderSegmentWithHighlight = (segment: BreadcrumbSegment) => { if (!segment.highlightRange) { return segment.name; } const { from, to } = segment.highlightRange; return ( <> {segment.name.slice(0, from)} {segment.name.slice(from, to)} {segment.name.slice(to)} ); }; return (
{isCodeHostIconVisible && ( <> {info.codeHostName} )} {info?.displayName} {branchDisplayName && (

@ {`${branchDisplayName}`}

)} ·
{hiddenSegments.length > 0 && ( <> {hiddenSegments.map((segment) => ( {renderSegmentWithHighlight(segment)} ))} )} {visibleSegments.map((segment, index) => (
{(isFileIconVisible && index === visibleSegments.length - 1) && ( )} {renderSegmentWithHighlight(segment)} {index < visibleSegments.length - 1 && ( )}
))}
) }