'use client'; import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react'; export interface TerminalLine { type: 'command' | 'output' | 'error' | 'success' | 'warning' | 'info'; content: string; prompt?: string; delay?: number; } export interface TerminalOutputProps extends HTMLAttributes { lines: TerminalLine[]; prompt?: string; variant?: 'default' | 'hacker' | 'blood' | 'cyber'; showHeader?: boolean; title?: string; animated?: boolean; animationDelay?: number; typingCursor?: boolean; scanlines?: boolean; glowing?: boolean; showInput?: boolean; inputPlaceholder?: string; onCommand?: (command: string) => void; autoScroll?: boolean; } const variantStyles = { default: { border: 'border-gray-800', header: 'bg-gray-900', prompt: 'text-green-400', command: 'text-gray-100', output: 'text-gray-500' }, hacker: { border: 'border-green-500', header: 'bg-green-950', prompt: 'text-green-400', command: 'text-green-400', output: 'text-green-700' }, blood: { border: 'border-rose-500', header: 'bg-rose-950', prompt: 'text-rose-500', command: 'text-rose-300', output: 'text-rose-700' }, cyber: { border: 'border-cyan-500', header: 'bg-cyan-950', prompt: 'text-cyan-400', command: 'text-cyan-200', output: 'text-cyan-700' }, }; const typeColors = { error: 'text-rose-500', success: 'text-green-400', warning: 'text-amber-500', info: 'text-cyan-400' }; export const TerminalOutput = forwardRef( ( { lines, prompt = '❯', variant = 'default', showHeader = true, title = 'terminal', animated = false, animationDelay = 100, typingCursor = false, scanlines = false, glowing = false, showInput = false, inputPlaceholder = 'Type a command...', onCommand, autoScroll = true, className = '', ...props }, ref ) => { const [visibleLines, setVisibleLines] = useState(animated ? 0 : lines.length); const [inputValue, setInputValue] = useState(''); const contentRef = useRef(null); const styles = variantStyles[variant]; useEffect(() => { if (animated) { setVisibleLines(0); let index = 0; const showLine = () => { if (index < lines.length) { const line = lines[index]; setTimeout(() => { setVisibleLines(index + 1); index++; showLine(); }, line.delay ?? animationDelay); } }; showLine(); } else { setVisibleLines(lines.length); } }, [lines, animated, animationDelay]); useEffect(() => { if (autoScroll && contentRef.current) { contentRef.current.scrollTop = contentRef.current.scrollHeight; } }, [visibleLines, autoScroll]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (inputValue.trim()) { onCommand?.(inputValue); setInputValue(''); } }; return (
{showHeader && (
{title}
)}
{lines.slice(0, visibleLines).map((line, i) => { const isLastCommand = i === visibleLines - 1 && line.type === 'command' && typingCursor; const outputColor = line.type !== 'command' && line.type !== 'output' ? typeColors[line.type] : styles.output; return (
{line.type === 'command' ? ( <> {line.prompt || prompt} {line.content} {isLastCommand && } ) : ( {line.content} )}
); })} {showInput && (
{prompt} setInputValue(e.target.value)} placeholder={inputPlaceholder} className="flex-1 bg-transparent border-none text-gray-100 text-sm outline-none caret-green-400 placeholder:text-gray-700" autoFocus />
)}
); } ); TerminalOutput.displayName = 'TerminalOutput'; export default TerminalOutput;