Files
Netcatty/components/ui/code-textarea.tsx
2026-06-02 01:15:36 +08:00

100 lines
3.1 KiB
TypeScript

import * as React from "react";
import { cn } from "@/lib/utils.ts";
export interface CodeTextareaProps
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "spellCheck"> {
showLineNumbers?: boolean;
/** Applied to the outer bordered container when line numbers are shown. */
wrapperClassName?: string;
}
function countLines(value: string): number {
if (!value) return 1;
return value.split("\n").length;
}
const CodeTextarea = React.forwardRef<HTMLTextAreaElement, CodeTextareaProps>(
({ className, wrapperClassName, value, showLineNumbers = true, onScroll, ...props }, ref) => {
const gutterRef = React.useRef<HTMLDivElement>(null);
const text = typeof value === "string" ? value : String(value ?? "");
const lineCount = countLines(text);
const lineNumbers = React.useMemo(
() => Array.from({ length: lineCount }, (_, i) => i + 1),
[lineCount],
);
const gutterWidthCh = Math.max(2, String(lineCount).length) + 1;
const handleScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
if (gutterRef.current) {
gutterRef.current.scrollTop = e.currentTarget.scrollTop;
}
onScroll?.(e);
};
const editorClass = cn(
"w-full flex-1 resize-none border-0 bg-transparent px-2 py-2 font-mono text-xs leading-5",
"placeholder:text-muted-foreground focus-visible:outline-none",
"whitespace-pre overflow-auto",
className,
);
if (!showLineNumbers) {
return (
<textarea
ref={ref}
value={value}
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs leading-5",
"placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
"whitespace-pre",
className,
)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
data-gramm="false"
onScroll={onScroll}
{...props}
/>
);
}
return (
<div
className={cn(
"flex w-full overflow-hidden rounded-md border border-input bg-background transition-colors",
"focus-within:border-ring",
wrapperClassName,
)}
>
<div
ref={gutterRef}
aria-hidden
className="shrink-0 overflow-hidden border-r border-border/60 bg-muted/30 py-2 pl-2 pr-1.5 select-none"
style={{ width: `${gutterWidthCh}ch` }}
>
<pre className="font-mono text-[11px] leading-5 text-muted-foreground text-right m-0">
{lineNumbers.join("\n")}
</pre>
</div>
<textarea
ref={ref}
value={value}
className={editorClass}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
data-gramm="false"
onScroll={handleScroll}
{...props}
/>
</div>
);
},
);
CodeTextarea.displayName = "CodeTextarea";
export { CodeTextarea };