import type { ReactElement, CSSProperties } from "react"; export interface TerminalLine { type: "prompt" | "command" | "output" | "error" | "warning" | "success"; content: string; } export interface TerminalTab { id: string; label: string; } export interface TerminalPanelProps { open: boolean; onClose: () => void; tabs?: TerminalTab[]; activeTab?: string; onTabChange?: (id: string) => void; lines?: TerminalLine[]; className?: string; } const defaultTabs: TerminalTab[] = [ { id: "main", label: "main" }, { id: "build", label: "build" }, { id: "logs", label: "logs" }, ]; const blinkKeyframes = ` @keyframes ms-terminal-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } `; let blinkStyleInjected = false; function ensureBlinkStyle(): void { if (blinkStyleInjected || typeof document === "undefined") return; const styleEl = document.createElement("style"); styleEl.textContent = blinkKeyframes; document.head.appendChild(styleEl); blinkStyleInjected = true; } function getLineColor(type: TerminalLine["type"]): string { switch (type) { case "prompt": return "var(--success)"; case "command": return "var(--text-2)"; case "output": return "var(--muted)"; case "error": return "var(--danger)"; case "warning": return "var(--warn)"; case "success": return "var(--success)"; default: return "var(--muted)"; } } export function TerminalPanel({ open, onClose, tabs, activeTab, onTabChange, lines = [], className = "", }: TerminalPanelProps): ReactElement { ensureBlinkStyle(); const resolvedTabs = tabs ?? defaultTabs; const resolvedActiveTab = activeTab ?? resolvedTabs[0]?.id ?? ""; const panelStyle: CSSProperties = { background: "var(--bg-deep)", borderTop: "1px solid var(--border)", overflow: "hidden", flexShrink: 0, display: "flex", flexDirection: "column", height: open ? 280 : 0, transition: "height 0.3s ease", }; const headerStyle: CSSProperties = { display: "flex", alignItems: "center", gap: 10, padding: "6px 16px", borderBottom: "1px solid var(--border)", flexShrink: 0, }; const tabBarStyle: CSSProperties = { display: "flex", gap: 2, }; const actionsStyle: CSSProperties = { marginLeft: "auto", display: "flex", gap: 4, }; const bodyStyle: CSSProperties = { flex: 1, overflowY: "auto", padding: "10px 16px", fontFamily: "var(--mono)", fontSize: "0.78rem", lineHeight: 1.6, }; const cursorStyle: CSSProperties = { display: "inline-block", width: 7, height: 14, background: "var(--success)", marginLeft: 2, animation: "ms-terminal-blink 1s step-end infinite", verticalAlign: "text-bottom", }; return (
{/* Header */}
{/* Tab bar */}
{resolvedTabs.map((tab) => { const isActive = tab.id === resolvedActiveTab; const tabStyle: CSSProperties = { padding: "3px 10px", borderRadius: 4, fontSize: "0.75rem", fontFamily: "var(--mono)", color: isActive ? "var(--success)" : "var(--muted)", cursor: "pointer", background: isActive ? "var(--surface)" : "transparent", border: "none", outline: "none", }; return ( ); })}
{/* Action buttons */}
{/* Body */}
{lines.map((line, index) => { const isLast = index === lines.length - 1; const lineStyle: CSSProperties = { display: "flex", gap: 8, }; const contentStyle: CSSProperties = { color: getLineColor(line.type), }; return (
{line.content} {isLast &&
); })} {/* Show cursor even when no lines */} {lines.length === 0 && (
)}
); }