Website Redesign 7
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { themeColors } from '$lib/stores/theme';
|
||||
|
||||
interface Props {
|
||||
lines?: TerminalLine[];
|
||||
typeSpeed?: number;
|
||||
startDelay?: number;
|
||||
showCursor?: boolean;
|
||||
autoType?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
interface TerminalLine {
|
||||
type: 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info';
|
||||
content: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
lines = [],
|
||||
typeSpeed = 30,
|
||||
startDelay = 500,
|
||||
showCursor = true,
|
||||
autoType = true,
|
||||
class: className = ''
|
||||
}: Props = $props();
|
||||
|
||||
let displayedLines = $state<{ line: TerminalLine; text: string; complete: boolean }[]>([]);
|
||||
let currentLineIndex = $state(0);
|
||||
let currentCharIndex = $state(0);
|
||||
let isTyping = $state(false);
|
||||
let terminalElement: HTMLDivElement;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function typeText() {
|
||||
if (!autoType || lines.length === 0) {
|
||||
displayedLines = lines.map(line => ({ line, text: line.content, complete: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
isTyping = true;
|
||||
await sleep(startDelay);
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
displayedLines = [...displayedLines, { line, text: '', complete: false }];
|
||||
currentLineIndex = i;
|
||||
|
||||
if (line.delay) {
|
||||
await sleep(line.delay);
|
||||
}
|
||||
|
||||
// Type out the line character by character
|
||||
for (let j = 0; j <= line.content.length; j++) {
|
||||
currentCharIndex = j;
|
||||
displayedLines[i] = {
|
||||
line,
|
||||
text: line.content.slice(0, j),
|
||||
complete: j === line.content.length
|
||||
};
|
||||
|
||||
// Scroll to bottom
|
||||
if (terminalElement) {
|
||||
terminalElement.scrollTop = terminalElement.scrollHeight;
|
||||
}
|
||||
|
||||
if (j < line.content.length) {
|
||||
await sleep(line.type === 'output' ? typeSpeed / 3 : typeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
displayedLines[i].complete = true;
|
||||
|
||||
// Pause between lines
|
||||
if (i < lines.length - 1) {
|
||||
await sleep(150);
|
||||
}
|
||||
}
|
||||
|
||||
isTyping = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
typeText();
|
||||
});
|
||||
|
||||
function getLinePrefix(type: TerminalLine['type']): string {
|
||||
switch (type) {
|
||||
case 'command':
|
||||
case 'prompt':
|
||||
return '';
|
||||
case 'error':
|
||||
return '✗ ';
|
||||
case 'success':
|
||||
return '✓ ';
|
||||
case 'info':
|
||||
return 'ℹ ';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="terminal {className}"
|
||||
style="
|
||||
--terminal-bg: {$themeColors.terminal};
|
||||
--terminal-text: {$themeColors.text};
|
||||
--terminal-muted: {$themeColors.textMuted};
|
||||
--terminal-border: {$themeColors.border};
|
||||
--terminal-prompt: {$themeColors.terminalPrompt};
|
||||
--terminal-user: {$themeColors.terminalUser};
|
||||
--terminal-path: {$themeColors.terminalPath};
|
||||
--terminal-primary: {$themeColors.primary};
|
||||
--terminal-accent: {$themeColors.accent};
|
||||
"
|
||||
bind:this={terminalElement}
|
||||
>
|
||||
<!-- Terminal Header -->
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-buttons">
|
||||
<span class="btn close"></span>
|
||||
<span class="btn minimize"></span>
|
||||
<span class="btn maximize"></span>
|
||||
</div>
|
||||
<div class="terminal-title">
|
||||
<span class="terminal-icon">🐧</span>
|
||||
<span>arch@terminal: ~</span>
|
||||
</div>
|
||||
<div class="terminal-spacer"></div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Body -->
|
||||
<div class="terminal-body">
|
||||
{#each displayedLines as { line, text, complete }, i}
|
||||
<div class="terminal-line {line.type}">
|
||||
{#if line.type === 'command' || line.type === 'prompt'}
|
||||
<span class="prompt">
|
||||
<span class="user">user@arch</span><span class="separator">:</span><span class="path">~</span><span class="symbol">$</span>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="content">
|
||||
{getLinePrefix(line.type)}{text}
|
||||
</span>
|
||||
{#if showCursor && i === currentLineIndex && !complete && isTyping}
|
||||
<span class="cursor"></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if showCursor && !isTyping && displayedLines.length > 0}
|
||||
<div class="terminal-line prompt">
|
||||
<span class="prompt">
|
||||
<span class="user">user@arch</span><span class="separator">:</span><span class="path">~</span><span class="symbol">$</span>
|
||||
</span>
|
||||
<span class="cursor"></span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.terminal {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
background: var(--terminal-bg);
|
||||
border: 1px solid var(--terminal-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
||||
animation: terminalFadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes terminalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.terminal-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.terminal-buttons .btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.terminal-buttons .btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.terminal-buttons .close {
|
||||
background: #ff5f56;
|
||||
}
|
||||
|
||||
.terminal-buttons .minimize {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
|
||||
.terminal-buttons .maximize {
|
||||
background: #27ca40;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--terminal-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.terminal-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.terminal-spacer {
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
padding: 1rem 1.25rem;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.25rem;
|
||||
animation: lineSlideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes lineSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.prompt {
|
||||
display: inline-flex;
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.prompt .user {
|
||||
color: var(--terminal-user);
|
||||
}
|
||||
|
||||
.prompt .separator {
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
.prompt .path {
|
||||
color: var(--terminal-path);
|
||||
}
|
||||
|
||||
.prompt .symbol {
|
||||
color: var(--terminal-text);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--terminal-text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.terminal-line.output .content {
|
||||
color: var(--terminal-muted);
|
||||
}
|
||||
|
||||
.terminal-line.error .content {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.terminal-line.success .content {
|
||||
color: #51cf66;
|
||||
}
|
||||
|
||||
.terminal-line.info .content {
|
||||
color: var(--terminal-primary);
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1.2em;
|
||||
background: var(--terminal-primary);
|
||||
animation: cursorBlink 1s step-end infinite;
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes cursorBlink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.terminal-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.terminal-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.terminal-body::-webkit-scrollbar-thumb {
|
||||
background: var(--terminal-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.terminal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--terminal-muted);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user