import { useState, useEffect } from 'react' import { Copy, Download } from 'lucide-react' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { getRawUrl } from '@/lib/gitApi' import type { GitBlob } from '@/lib/gitApi' interface FileViewerProps { blob: GitBlob ref: string loading?: boolean } // Syntax-highlighted file viewer with line numbers, copy, and download buttons. // highlight.js is loaded lazily (dynamic import) so it doesn't bloat the initial bundle. export function FileViewer({ blob, ref, loading }: FileViewerProps) { const [highlighted, setHighlighted] = useState<{ html: string; lineCount: number } | null>(null) useEffect(() => { if (blob.isBinary || !blob.content) { setHighlighted({ html: '', lineCount: 0 }) return } setHighlighted(null) let cancelled = false import('highlight.js').then(({ default: hljs }) => { if (cancelled) return const ext = blob.path.split('.').pop() ?? '' const result = hljs.getLanguage(ext) ? hljs.highlight(blob.content, { language: ext }) : hljs.highlightAuto(blob.content) setHighlighted({ html: result.value, lineCount: blob.content.split('\n').length, }) }) return () => { cancelled = true } }, [blob]) if (loading || highlighted === null) return const { html, lineCount } = highlighted function copyToClipboard() { navigator.clipboard.writeText(blob.content) } return (
{/* Metadata bar */}
{lineCount.toLocaleString()} lines · {formatBytes(blob.size)}
{blob.isBinary ? (
Binary file — {formatBytes(blob.size)}
) : ( // Line numbers are a fixed column; code scrolls horizontally independently. // Keeping them in separate divs avoids having to split highlighted HTML by line.
{Array.from({ length: lineCount }, (_, i) => (
{i + 1}
))}
            
          
)}
) } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / 1024 / 1024).toFixed(1)} MB` } function FileViewerSkeleton() { return (
{Array.from({ length: 20 }).map((_, i) => ( ))}
{Array.from({ length: 20 }).map((_, i) => ( ))}
) }