Website Redesign 7

This commit is contained in:
2025-11-28 02:40:12 +00:00
parent 9b9a201c3e
commit 96e2d0650c
72 changed files with 7504 additions and 1231 deletions
+350
View File
@@ -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>