Files
2026-05-01 21:39:30 +08:00

110 lines
2.9 KiB
TypeScript

'use client';
import { Button } from '../ui/button';
import { cn } from '../../src/lib/utils';
import { CheckIcon, CopyIcon } from 'lucide-react';
import { type ComponentProps, createContext, type HTMLAttributes, useContext, useState } from 'react';
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
code: string;
language?: string;
showLineNumbers?: boolean;
};
type CodeBlockContextType = {
code: string;
};
const CodeBlockContext = createContext<CodeBlockContextType>({
code: '',
});
export const CodeBlock = ({
code,
language,
showLineNumbers = false,
className,
children,
...props
}: CodeBlockProps) => {
const lines = code.split('\n');
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
'group relative w-full overflow-hidden rounded-md border bg-background text-foreground',
className,
)}
{...props}
>
<div className="relative">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<tbody>
{lines.map((line, i) => (
<tr key={i} className="border-0">
{showLineNumbers && (
<td className="w-10 select-none pr-4 text-right align-top text-muted-foreground text-xs">
{i + 1}
</td>
)}
<td className="p-0">
<pre className="m-0 p-0 text-sm whitespace-pre font-mono">
<code className="text-sm">{line || '\u00A0'}</code>
</pre>
</td>
</tr>
))}
</tbody>
</table>
</div>
{children && <div className="absolute top-2 right-2 flex items-center gap-2">{children}</div>}
</div>
</div>
</CodeBlockContext.Provider>
);
};
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void;
onError?: (error: Error) => void;
timeout?: number;
};
export const CodeBlockCopyButton = ({
onCopy,
onError,
timeout = 2000,
children,
className,
...props
}: CodeBlockCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const { code } = useContext(CodeBlockContext);
const copyToClipboard = async () => {
if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) {
onError?.(new Error('Clipboard API not available'));
return;
}
try {
await navigator.clipboard.writeText(code);
setIsCopied(true);
onCopy?.();
setTimeout(() => setIsCopied(false), timeout);
} catch (error) {
onError?.(error as Error);
}
};
const Icon = isCopied ? CheckIcon : CopyIcon;
return (
<Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props}>
{children ?? <Icon size={14} />}
</Button>
);
};