282 lines
11 KiB
TypeScript
282 lines
11 KiB
TypeScript
"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,
|
||
}
|