'use client'; import { getCodeHostInfoForRepo } from "@/lib/utils"; import { LaptopIcon } from "@radix-ui/react-icons"; import clsx from "clsx"; import Image from "next/image"; import Link from "next/link"; import { useBrowseNavigation } from "../browse/hooks/useBrowseNavigation"; import { Copy, CheckCircle2, ChevronRight, MoreHorizontal } from "lucide-react"; import { useCallback, useState, useMemo, useRef, useEffect } from "react"; import { useToast } from "@/components/hooks/use-toast"; import { usePrefetchFolderContents } from "@/hooks/usePrefetchFolderContents"; import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; interface FileHeaderProps { path: string; pathHighlightRange?: { from: number; to: number; } pathType?: 'blob' | 'tree'; repo: { name: string; codeHostType: string; displayName?: string; webUrl?: string; }, branchDisplayName?: string; branchDisplayTitle?: string; } interface BreadcrumbSegment { name: string; fullPath: string; isLastSegment: boolean; highlightRange?: { from: number; to: number; }; } export const PathHeader = ({ repo, path, pathHighlightRange, branchDisplayName, branchDisplayTitle, pathType = 'blob', }: FileHeaderProps) => { const info = getCodeHostInfoForRepo({ name: repo.name, codeHostType: repo.codeHostType, displayName: repo.displayName, webUrl: repo.webUrl, }); const { navigateToPath } = useBrowseNavigation(); const { toast } = useToast(); const [copied, setCopied] = useState(false); const { prefetchFolderContents } = usePrefetchFolderContents(); const { prefetchFileSource } = usePrefetchFileSource(); const containerRef = useRef(null); const breadcrumbsRef = useRef(null); const [visibleSegmentCount, setVisibleSegmentCount] = useState(null); // 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); setCopied(true); toast({ description: "✅ Copied to clipboard" }); setTimeout(() => setCopied(false), 1500); }, [path, toast]); const onBreadcrumbClick = useCallback((segment: BreadcrumbSegment) => { navigateToPath({ repoName: repo.name, path: segment.fullPath, pathType: segment.isLastSegment ? pathType : 'tree', revisionName: branchDisplayName, }); }, [repo.name, branchDisplayName, navigateToPath, pathType]); const onBreadcrumbMouseEnter = useCallback((segment: BreadcrumbSegment) => { if (segment.isLastSegment && pathType === 'blob') { prefetchFileSource(repo.name, branchDisplayName ?? 'HEAD', segment.fullPath); } else { prefetchFolderContents(repo.name, branchDisplayName ?? 'HEAD', segment.fullPath); } }, [ repo.name, branchDisplayName, prefetchFolderContents, pathType, prefetchFileSource, ]); 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 (
{info?.icon ? ( {info.codeHostName} ): ( )} {info?.displayName} {branchDisplayName && (

@ {`${branchDisplayName}`}

)} ·
{hiddenSegments.length > 0 && ( <> {hiddenSegments.map((segment) => ( onBreadcrumbClick(segment)} onMouseEnter={() => onBreadcrumbMouseEnter(segment)} className="font-mono text-sm cursor-pointer" > {renderSegmentWithHighlight(segment)} ))} )} {visibleSegments.map((segment, index) => (
onBreadcrumbClick(segment)} onMouseEnter={() => onBreadcrumbMouseEnter(segment)} > {renderSegmentWithHighlight(segment)} {index < visibleSegments.length - 1 && ( )}
))}
) }