// Phantom — shared visual components // ANSI colors, ASCII art, banners, prompts, separators // Neon violet/cyan on dark — hacker-in-hoodie aesthetic // ── ANSI helpers (local — no dependency on phantom.mjs) ── function fg(name) { const colors = { black:30, red:31, green:32, yellow:33, blue:34, magenta:35, cyan:36, white:37, dim:2 }; const n = colors[name] ?? 37; return `\x1b[${n}m`; } const _R = "\x1b[0m"; const _B = "\x1b[1m"; const _D = "\x1b[2m"; // ── Hooded figure logo (cyberpunk neon) ───────────────── export function renderLogo(opts = {}) { const { mode = "", // "non-interactive", "v0.2.0" tools = 0, wide = false, } = opts; const M = fg("magenta"); const C = fg("cyan"); const G = fg("green"); const Y = fg("yellow"); const dim = fg("dim"); const _r = _R; const b = _B; const d = _D; // Shadow figlet logo — no box-drawing borders, clean const lines = [ ` ${M}${b} _ \\ | | \\ \\ |__ __| _ \\ \\ |${_r}`, ` ${M}${b} | | | | _ \\ \\ | | | | |\\/ |${_r}`, ` ${M}${b} ___/ ___ | ___ \\ |\\ | | | | | |${_r}`, ` ${M}${b} _| _| _|_/ _\\_| \\_| _| \\___/ _| _|${_r}`, ` ${C}${b}P${_r} ${M}${b}H${_r} ${Y}${b}A${_r} ${C}${b}N${_r} ${M}${b}T${_r} ${Y}${b}O${_r} ${C}${b}M${_r}`, ]; if (tools) lines.push(` ${dim}${tools} tools${_r}`); return lines.join("\n"); } // ── Simple banner (small terminals / DesktopUI) ── export function renderBanner(text = "PHANTOM", subtitle = "") { const C = fg("cyan"); const dim = fg("dim"); const _r = _R; const b = _B; return `${C}${b}${text}${_r}${subtitle ? ` ${dim}${subtitle}${_r}` : ""}`; } // ── Prompt symbols ──────────────────────────────── export const prompt = { ghost: `${fg("green")}👻${_R} `, bolt: `${fg("cyan")}⚡${_R} `, hash: `${fg("magenta")}#${_R} `, arrow: `${fg("green")}→${_R} `, dim: `${fg("dim")}⧩${_R} `, }; // ── Separator line ────────────────────────────────── export function separator(char = "─", len = 50) { return `${fg("dim")}${char.repeat(len)}${_R}`; } // ── Status icons ──────────────────────────────────── // ── Animated spinner ────────────────────────────────── // Minimal frame-based spinner; no external deps. export function createSpinner(startTime, opts = {}) { const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; let i = 0, timer = null; const write = opts.write || ((s) => process.stdout.write(s)); const fmt = (ts) => { const s = Math.floor((Date.now() - ts) / 1000); return s > 3600 ? `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m` : s > 60 ? `${Math.floor(s/60)}m ${s%60}s` : `${s}s`; }; return { start(msg) { if (timer) return; timer = setInterval(() => { const f = frames[i = (i + 1) % frames.length]; write(`\r${fg("cyan")}${_B}${f}${_R} [${fmt(startTime)}] ${msg}`); }, 80); }, stop() { if (!timer) return; clearInterval(timer); timer = null; write('\r\x1b[K'); }, update(msg) { const f = timer ? frames[i] : frames[0]; write(`\r${fg("cyan")}${_B}${f}${_R} [${fmt(startTime)}] ${msg}`); } }; } export const icons = { ok: `${fg("green")}✓${_R}`, err: `${fg("red")}✕${_R}`, warn: `${fg("yellow")}⚠${_R}`, info: `${fg("cyan")}◈${_R}`, bullet: `${fg("magenta")}◆${_R}`, arrow: `${fg("green")}→${_R}`, }; // ── Chat border — wraps multi-line text in a bordered box ── // width = total width (defaults to terminal cols - 2) // title = optional top-left label // color = border color function (default dim) export function chatBorder(text, opts = {}) { const { width, title, color = fg("dim") } = opts; const cols = width || (process.stdout.columns || 80) - 4; const c = color; const _r = _R; const lines = text.split("\n"); const out = []; const top = title ? `${c}╔═ ${_B}${title}${_R}${c} ${"═".repeat(Math.max(0, cols - title.length - 4))}╗${_r}` : `${c}╔${"═".repeat(cols + 2)}╗${_r}`; out.push(top); for (const line of lines) { const wrapped = line.length > cols ? line.substring(0, cols - 1) + "…" : line; const pad = " ".repeat(Math.max(0, cols - wrapped.length + 2)); out.push(`${c}║${_r} ${wrapped}${pad}${c}║${_r}`); } out.push(`${c}╚${"═".repeat(cols + 2)}╝${_r}`); return out.join("\n"); }