argobox/src/components/Terminal.astro

395 lines
11 KiB
Plaintext

---
// Terminal.astro
// A component that displays terminal-like interface with animated commands and outputs
export interface Props {
title?: string;
height?: string;
showTitleBar?: boolean;
showPrompt?: boolean;
commands?: {
prompt: string;
command: string;
output?: string[];
delay?: number;
}[];
}
const {
title = "terminal",
height = "auto",
showTitleBar = true,
showPrompt = true,
commands = []
} = Astro.props;
// Make the last command have the typing effect
const lastIndex = commands.length - 1;
---
<div class="terminal-box">
{showTitleBar && (
<div class="terminal-header">
<div class="terminal-dots">
<div class="terminal-dot terminal-dot-red"></div>
<div class="terminal-dot terminal-dot-yellow"></div>
<div class="terminal-dot terminal-dot-green"></div>
</div>
<div class="terminal-title">{title}</div>
<div class="terminal-actions">
<button class="terminal-button terminal-button-minimize" aria-label="Minimize">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<button class="terminal-button terminal-button-maximize" aria-label="Maximize">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 3 21 3 21 9"></polyline>
<polyline points="9 21 3 21 3 15"></polyline>
<line x1="21" y1="3" x2="14" y2="10"></line>
<line x1="3" y1="21" x2="10" y2="14"></line>
</svg>
</button>
</div>
</div>
)}
<div class="terminal-content" style={`height: ${height};`}>
{commands.map((cmd, index) => (
<div class="terminal-block">
<div class="terminal-line">
<span class="terminal-prompt">{cmd.prompt}</span>
<span class={index === lastIndex ? "terminal-command terminal-typing" : "terminal-command"} data-delay={cmd.delay || 50}>
{cmd.command}
</span>
</div>
{cmd.output && cmd.output.length > 0 && (
<div class="terminal-output">
{cmd.output.map((line) => (
<div class="terminal-output-line" set:html={line} />
))}
</div>
)}
</div>
))}
<div class="terminal-cursor"></div>
</div>
</div>
<style>
.terminal-box {
width: 100%;
height: 340px;
background: var(--bg-secondary, #0d1529);
border-radius: 10px;
border: 1px solid var(--border-primary, rgba(255, 255, 255, 0.1));
box-shadow: 0 0 30px rgba(6, 182, 212, 0.1);
font-family: var(--font-mono, 'JetBrains Mono', monospace);
font-size: 0.9rem;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* Header */
.terminal-header {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid var(--border-secondary, rgba(255, 255, 255, 0.05));
height: 40px;
}
.terminal-dots {
display: flex;
gap: 0.5rem;
}
.terminal-dot {
width: 12px;
height: 12px;
border-radius: 50%;
transition: all 0.3s ease;
}
.terminal-dot-red {
background: #ef4444;
}
.terminal-dot-yellow {
background: #eab308;
}
.terminal-dot-green {
background: #22c55e;
}
.terminal-title {
margin-left: auto;
margin-right: auto;
color: var(--text-secondary, #a0aec0);
font-size: 0.8rem;
}
.terminal-actions {
display: flex;
gap: 0.5rem;
}
.terminal-button {
background: transparent;
border: none;
color: var(--text-secondary, #a0aec0);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border-radius: 2px;
transition: all 0.2s ease;
}
.terminal-button:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary, #e2e8f0);
}
/* Content */
.terminal-content {
flex: 1;
color: var(--text-secondary, #a0aec0);
overflow-y: auto;
padding: 0.5rem 1.5rem 1.5rem;
opacity: 0.9;
position: relative;
}
.terminal-block {
margin-bottom: 1rem;
}
.terminal-line {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.terminal-prompt {
color: var(--accent-primary, #06b6d4);
margin-right: 0.5rem;
font-weight: bold;
white-space: nowrap;
}
.terminal-command {
color: var(--text-primary, #e2e8f0);
word-break: break-word;
}
.terminal-output {
margin-top: 0.5rem;
margin-bottom: 1rem;
padding-left: 1.25rem;
color: var(--text-secondary, #a0aec0);
font-size: 0.85rem;
line-height: 1.5;
}
.terminal-output-line {
margin-bottom: 0.25rem;
}
/* Highlight syntax in output */
.terminal-output :global(.highlight) {
color: var(--accent-primary, #06b6d4);
}
.terminal-output :global(.success) {
color: #22c55e;
}
.terminal-output :global(.warning) {
color: #eab308;
}
.terminal-output :global(.error) {
color: #ef4444;
}
/* Blinking cursor */
.terminal-cursor {
position: absolute;
display: inline-block;
width: 8px;
height: 16px;
background: var(--accent-primary, #06b6d4);
animation: blink 1s infinite;
bottom: 2rem;
left: calc(1.5rem + 200px); /* Adjustable using JS */
opacity: 0;
}
/* Typing effect */
.terminal-typing {
position: relative;
white-space: pre-wrap;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Scrollbar styling */
.terminal-content::-webkit-scrollbar {
width: 6px;
}
.terminal-content::-webkit-scrollbar-track {
background: rgba(15, 23, 42, 0.3);
}
.terminal-content::-webkit-scrollbar-thumb {
background: rgba(226, 232, 240, 0.2);
border-radius: 3px;
}
.terminal-content::-webkit-scrollbar-thumb:hover {
background: rgba(226, 232, 240, 0.3);
}
/* Fixed dot hovered appearance */
.terminal-box:hover .terminal-dot-red {
background: #f87171;
}
.terminal-box:hover .terminal-dot-yellow {
background: #fbbf24;
}
.terminal-box:hover .terminal-dot-green {
background: #34d399;
}
</style>
<script>
// Terminal typing effect
document.addEventListener('DOMContentLoaded', function() {
// Typing effect for commands
const typingElements = document.querySelectorAll('.terminal-typing');
typingElements.forEach((typingElement, elementIndex) => {
const text = typingElement.textContent || '';
const delay = parseInt(typingElement.getAttribute('data-delay') || '50', 10);
// Clear the element
typingElement.textContent = '';
let i = 0;
// Random delay before starting to type (sequential if there are multiple)
setTimeout(() => {
function typeWriter() {
if (i < text.length) {
typingElement.textContent += text.charAt(i);
i++;
// Random typing speed for realistic effect
const randomVariation = Math.random() * 30 - 15; // -15 to +15ms variation
const speed = delay + randomVariation;
setTimeout(typeWriter, speed);
} else {
// When done typing, scroll terminal content to bottom
const terminalContent = typingElement.closest('.terminal-content');
if (terminalContent) {
terminalContent.scrollTop = terminalContent.scrollHeight;
}
// Add blinking cursor after the last command
if (elementIndex === typingElements.length - 1) {
const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor');
if (cursor) {
const rect = typingElement.getBoundingClientRect();
if (terminalContent) {
const parentRect = terminalContent.getBoundingClientRect();
// Position cursor after the last character
cursor.style.opacity = '1';
cursor.style.left = `${rect.left - parentRect.left + typingElement.offsetWidth}px`;
cursor.style.top = `${rect.top - parentRect.top}px`;
cursor.style.height = `${rect.height}px`;
}
}
}
}
}
typeWriter();
}, 1000 * elementIndex); // Sequential delay for multiple typing elements
});
// Button interactions
const minButtons = document.querySelectorAll('.terminal-button-minimize');
const maxButtons = document.querySelectorAll('.terminal-button-maximize');
minButtons.forEach(button => {
button.addEventListener('click', () => {
const terminalBox = button.closest('.terminal-box');
if (terminalBox) {
terminalBox.classList.toggle('minimized');
if (terminalBox.classList.contains('minimized')) {
const content = terminalBox.querySelector('.terminal-content');
if (content) {
terminalBox.dataset.prevHeight = terminalBox.style.height;
terminalBox.style.height = '40px';
content.style.display = 'none';
}
} else {
const content = terminalBox.querySelector('.terminal-content');
if (content) {
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
content.style.display = 'block';
}
}
}
});
});
maxButtons.forEach(button => {
button.addEventListener('click', () => {
const terminalBox = button.closest('.terminal-box');
if (terminalBox) {
terminalBox.classList.toggle('maximized');
if (terminalBox.classList.contains('maximized')) {
const content = terminalBox.querySelector('.terminal-content');
terminalBox.dataset.prevHeight = terminalBox.style.height;
terminalBox.dataset.prevWidth = terminalBox.style.width;
terminalBox.dataset.prevPosition = terminalBox.style.position;
terminalBox.style.position = 'fixed';
terminalBox.style.top = '0';
terminalBox.style.left = '0';
terminalBox.style.width = '100%';
terminalBox.style.height = '100%';
terminalBox.style.zIndex = '9999';
terminalBox.style.borderRadius = '0';
} else {
terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative';
terminalBox.style.width = terminalBox.dataset.prevWidth || '100%';
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
terminalBox.style.zIndex = 'auto';
terminalBox.style.borderRadius = '10px';
terminalBox.style.top = 'auto';
terminalBox.style.left = 'auto';
}
}
});
});
});
</script>