veza/apps/web/desy/components/ui/terminal.tsx
2026-01-22 17:23:11 +01:00

282 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
/* ═══════════════════════════════════════════════════════════════════════════
TERMINAL WINDOW
═══════════════════════════════════════════════════════════════════════════ */
interface TerminalProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string
}
const Terminal = React.forwardRef<HTMLDivElement, TerminalProps>(
({ className, title = "terminal", children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"rounded-lg border overflow-hidden font-mono",
"bg-terminal border-[var(--terminal-green)]",
className
)}
{...props}
>
{/* Header */}
<div className="flex items-center gap-2 px-4 py-2 bg-[rgba(0,255,0,0.1)] border-b border-[var(--terminal-green)]">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-[#ff5f56]" />
<div className="w-3 h-3 rounded-full bg-[#ffbd2e]" />
<div className="w-3 h-3 rounded-full bg-[#27c93f]" />
</div>
<span className="flex-1 text-center text-xs text-[var(--terminal-green)] opacity-70">
{title}
</span>
</div>
{/* Body */}
<div className="p-4 min-h-[200px] text-[var(--terminal-green)]" style={{ textShadow: "0 0 10px rgba(0,255,0,0.3)" }}>
{children}
</div>
</div>
)
}
)
Terminal.displayName = "Terminal"
/* ═══════════════════════════════════════════════════════════════════════════
TERMINAL LINE
═══════════════════════════════════════════════════════════════════════════ */
interface TerminalLineProps extends React.HTMLAttributes<HTMLDivElement> {
prompt?: string
command?: string
output?: string
isTyping?: boolean
}
const TerminalLine = React.forwardRef<HTMLDivElement, TerminalLineProps>(
({ className, prompt = "$", command, output, isTyping = false, ...props }, ref) => {
return (
<div ref={ref} className={cn("mb-2", className)} {...props}>
{command && (
<div className="flex items-center gap-2">
<span className="text-[var(--cyan-500)]">{prompt}</span>
<span>{command}</span>
{isTyping && (
<span className="inline-block w-2 h-4 bg-[var(--terminal-green)] animate-[terminal-blink_1s_step-end_infinite]" />
)}
</div>
)}
{output && (
<div className="mt-1 opacity-80 whitespace-pre-wrap">{output}</div>
)}
</div>
)
}
)
TerminalLine.displayName = "TerminalLine"
/* ═══════════════════════════════════════════════════════════════════════════
CODE BLOCK
═══════════════════════════════════════════════════════════════════════════ */
interface CodeBlockProps extends React.HTMLAttributes<HTMLPreElement> {
language?: string
filename?: string
showLineNumbers?: boolean
}
const CodeBlock = React.forwardRef<HTMLPreElement, CodeBlockProps>(
({ className, language, filename, showLineNumbers = false, children, ...props }, ref) => {
const lines = typeof children === 'string' ? children.split('\n') : []
return (
<div className="rounded-lg border border-border overflow-hidden">
{(language || filename) && (
<div className="flex items-center justify-between px-4 py-2 bg-muted border-b border-border">
{filename && (
<span className="text-xs font-mono text-muted-foreground">{filename}</span>
)}
{language && (
<span className="text-[10px] uppercase tracking-wider text-muted-foreground bg-background px-2 py-0.5 rounded">
{language}
</span>
)}
</div>
)}
<pre
ref={ref}
className={cn(
"p-4 overflow-x-auto text-sm font-mono bg-[var(--void-900)]",
className
)}
{...props}
>
{showLineNumbers && typeof children === 'string' ? (
<code>
{lines.map((line, i) => (
<div key={i} className="flex">
<span className="w-8 text-muted-foreground select-none text-right pr-4">
{i + 1}
</span>
<span>{line}</span>
</div>
))}
</code>
) : (
<code>{children}</code>
)}
</pre>
</div>
)
}
)
CodeBlock.displayName = "CodeBlock"
/* ═══════════════════════════════════════════════════════════════════════════
COMMAND PROMPT INPUT
═══════════════════════════════════════════════════════════════════════════ */
interface CommandInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
prompt?: string
onCommand?: (command: string) => void
}
const CommandInput = React.forwardRef<HTMLInputElement, CommandInputProps>(
({ className, prompt = ">", onCommand, ...props }, ref) => {
const [value, setValue] = React.useState("")
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && value.trim()) {
onCommand?.(value.trim())
setValue("")
}
}
return (
<div className={cn(
"flex items-center gap-2 px-3 py-2 font-mono text-sm",
"bg-[var(--matrix)] border border-[var(--terminal-green)] rounded",
"text-[var(--terminal-green)]",
className
)}>
<span className="text-[var(--cyan-500)]">{prompt}</span>
<input
ref={ref}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
className="flex-1 bg-transparent outline-none placeholder:text-[var(--terminal-green)]/40"
style={{ textShadow: "0 0 10px rgba(0,255,0,0.3)" }}
{...props}
/>
<span className="w-2 h-4 bg-[var(--terminal-green)] animate-[terminal-blink_1s_step-end_infinite]" />
</div>
)
}
)
CommandInput.displayName = "CommandInput"
/* ═══════════════════════════════════════════════════════════════════════════
MATRIX RAIN (Decorative)
═══════════════════════════════════════════════════════════════════════════ */
interface MatrixRainProps extends React.HTMLAttributes<HTMLDivElement> {
density?: number
}
const MatrixRain = React.forwardRef<HTMLDivElement, MatrixRainProps>(
({ className, density = 20, ...props }, ref) => {
const characters = "アイウエオカキクケコサシスセソタチツテトナニヌネハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789"
return (
<div
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<div className="absolute inset-0 bg-[var(--matrix)]">
{Array.from({ length: density }).map((_, i) => (
<div
key={i}
className="absolute top-0 text-[var(--terminal-green)] font-mono text-xs opacity-70"
style={{
left: `${(i / density) * 100}%`,
animation: `matrix-fall ${3 + Math.random() * 5}s linear infinite`,
animationDelay: `${Math.random() * 3}s`,
}}
>
{Array.from({ length: 20 }).map((_, j) => (
<div key={j} style={{ opacity: 1 - j * 0.05 }}>
{characters[Math.floor(Math.random() * characters.length)]}
</div>
))}
</div>
))}
</div>
<style>{`
@keyframes matrix-fall {
0% { transform: translateY(-100%); }
100% { transform: translateY(100vh); }
}
`}</style>
</div>
)
}
)
MatrixRain.displayName = "MatrixRain"
/* ═══════════════════════════════════════════════════════════════════════════
ASCII ART BOX
═══════════════════════════════════════════════════════════════════════════ */
interface AsciiBoxProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string
variant?: "single" | "double" | "rounded"
}
const AsciiBox = React.forwardRef<HTMLDivElement, AsciiBoxProps>(
({ className, title, variant = "single", children, ...props }, ref) => {
const chars = {
single: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" },
double: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" },
rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" },
}
const c = chars[variant]
return (
<div
ref={ref}
className={cn("font-mono text-sm text-[var(--terminal-green)]", className)}
style={{ textShadow: "0 0 5px rgba(0,255,0,0.2)" }}
{...props}
>
<div>
{c.tl}{c.h.repeat(title ? title.length + 2 : 30)}{c.tr}
</div>
{title && (
<div>{c.v} {title} {c.v}</div>
)}
<div className="whitespace-pre-wrap px-2">
{children}
</div>
<div>
{c.bl}{c.h.repeat(title ? title.length + 2 : 30)}{c.br}
</div>
</div>
)
}
)
AsciiBox.displayName = "AsciiBox"
export {
Terminal,
TerminalLine,
CodeBlock,
CommandInput,
MatrixRain,
AsciiBox,
}