CommandPalette: replace workspaceProjects with getCurrentUserProjects (no workspace dependency so it works outside WorkspaceProvider). Repos fetched per-project to preserve correct /repository/ns/repo routes. Keyboard shortcut correctly matches Ctrl+Alt+F / Cmd+Ctrl+F. sidebar-user: fix notification button layout — bell icon and label now on the same row instead of separate stacked elements.
155 lines
4.9 KiB
TypeScript
155 lines
4.9 KiB
TypeScript
'use client';
|
||
|
||
/**
|
||
* Renders a compact code reference block (file path + line range).
|
||
* Clicking navigates to the file in the repository browser.
|
||
*/
|
||
|
||
import { useState } from 'react';
|
||
import { FileCode2, ChevronDown, ChevronRight } from 'lucide-react';
|
||
import { cn } from '@/lib/utils';
|
||
import type { CodeRef } from '@/lib/code-ref-parser';
|
||
|
||
interface CodeReferenceProps {
|
||
ref: CodeRef;
|
||
/** API to fetch line content (optional — shows skeleton if not provided) */
|
||
getLineContent?: (filePath: string, startLine: number, endLine: number) => Promise<string[]>;
|
||
/** Called when the reference is clicked — navigate to file browser */
|
||
onClick?: (ref: CodeRef) => void;
|
||
/** Base URL for the repository file browser (e.g. /repository/ns/repo) */
|
||
repoBaseUrl?: string;
|
||
/** Branch name to use in the link */
|
||
branch?: string;
|
||
className?: string;
|
||
}
|
||
|
||
export function CodeReference({
|
||
ref,
|
||
getLineContent,
|
||
onClick,
|
||
repoBaseUrl,
|
||
branch = 'main',
|
||
className,
|
||
}: CodeReferenceProps) {
|
||
const [expanded, setExpanded] = useState(false);
|
||
const [lines, setLines] = useState<string[] | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const handleClick = () => {
|
||
if (onClick) {
|
||
onClick(ref);
|
||
return;
|
||
}
|
||
if (repoBaseUrl) {
|
||
// Navigate to file browser with line highlighted
|
||
window.location.href = `${repoBaseUrl}/blob/${branch}/${ref.filePath}#L${ref.startLine}`;
|
||
}
|
||
};
|
||
|
||
const loadLines = async () => {
|
||
if (!getLineContent || lines !== null) return;
|
||
setLoading(true);
|
||
try {
|
||
const content = await getLineContent(ref.filePath, ref.startLine, ref.endLine);
|
||
setLines(content);
|
||
} catch {
|
||
setLines([]);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const toggleExpand = () => {
|
||
setExpanded((prev) => {
|
||
if (!prev) loadLines();
|
||
return !prev;
|
||
});
|
||
};
|
||
|
||
const lineLabel = ref.endLine === ref.startLine
|
||
? `L${ref.startLine}`
|
||
: `L${ref.startLine}–L${ref.endLine}`;
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
'rounded-md border bg-muted/50 my-1.5 overflow-hidden',
|
||
className,
|
||
)}
|
||
>
|
||
{/* Header bar */}
|
||
<button
|
||
type="button"
|
||
className="w-full flex items-center gap-2 px-3 py-1.5 hover:bg-muted transition-colors text-left"
|
||
onClick={handleClick}
|
||
title={ref.filePath ? `View ${ref.filePath}` : `Jump to ${lineLabel}`}
|
||
>
|
||
<FileCode2 className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||
{ref.filePath && (
|
||
<span className="text-xs font-mono text-muted-foreground truncate">
|
||
{ref.filePath}
|
||
</span>
|
||
)}
|
||
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded text-muted-foreground">
|
||
{lineLabel}
|
||
</span>
|
||
</button>
|
||
|
||
{/* Code preview (optional) */}
|
||
{getLineContent && (
|
||
<>
|
||
<button
|
||
type="button"
|
||
className="w-full flex items-center gap-1 px-3 py-0.5 hover:bg-muted/70 transition-colors text-left"
|
||
onClick={toggleExpand}
|
||
>
|
||
{expanded ? (
|
||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||
) : (
|
||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||
)}
|
||
<span className="text-xs text-muted-foreground">
|
||
{expanded ? 'Hide preview' : 'Show preview'}
|
||
</span>
|
||
</button>
|
||
|
||
{expanded && (
|
||
<div className="border-t">
|
||
{loading ? (
|
||
<div className="px-3 py-2 space-y-1">
|
||
{Array.from({ length: Math.min(ref.endLine - ref.startLine + 1, 5) }).map((_, i) => (
|
||
<div key={i} className="flex gap-2">
|
||
<div className="h-3 w-6 bg-muted rounded animate-pulse" />
|
||
<div className="h-3 flex-1 bg-muted rounded animate-pulse" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : lines && lines.length > 0 ? (
|
||
<div className="py-1">
|
||
{lines.map((line, i) => {
|
||
const lineNum = ref.startLine + i;
|
||
return (
|
||
<div key={i} className="flex gap-0 px-1 hover:bg-muted/50">
|
||
<span className="select-none w-10 text-right pr-2 text-xs text-muted-foreground/50 font-mono shrink-0">
|
||
{lineNum}
|
||
</span>
|
||
<span className="text-xs font-mono leading-5 text-foreground/90 whitespace-pre">
|
||
{line || ' '}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<div className="px-3 py-2 text-xs text-muted-foreground italic">
|
||
No content available
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|