BLOG-58 feat: implement terminal UI components, refactor Navbar, and update styles
All checks were successful
Frontend CI / build (push) Successful in 1m31s

This commit is contained in:
SquidSpirit 2025-07-23 04:20:53 +08:00
parent 98802dc862
commit 2a7d0f9878
11 changed files with 206 additions and 18 deletions

11
.vscode/settings.json vendored
View File

@ -1,12 +1,3 @@
{
"cSpell.words": [
"actix",
"chrono",
"dotenv",
"rustls",
"serde",
"sqlx",
"squidspirit"
]
"typescript.preferences.importModuleSpecifier": "non-relative"
}

View File

@ -8,6 +8,18 @@
@theme {
--font-sans: 'Noto Sans TC', sans-serif;
--font-mono: 'HackNerdMono', 'Noto Sans Mono', monospace;
--color-true-gray-50: #fafafa;
--color-true-gray-100: #f5f5f5;
--color-true-gray-200: #e5e5e5;
--color-true-gray-300: #d4d4d4;
--color-true-gray-400: #a3a3a3;
--color-true-gray-500: #737373;
--color-true-gray-600: #525252;
--color-true-gray-700: #404040;
--color-true-gray-800: #262626;
--color-true-gray-900: #171717;
--spacing-toolbar-height: 4rem;
--spacing-content-height: calc(100vh - var(--spacing-toolbar-height));
}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import Action from '$lib/common/framework/ui/Action.svelte';
import NavbarAction from '$lib/common/framework/ui/NavbarAction.svelte';
</script>
<div class="border-b border-gray-300">
@ -12,7 +12,7 @@
<span class="text-2xl font-black text-gray-800">魚之魷魂</span>
</a>
<div class="flex flex-row items-center gap-x-6">
<Action label="首頁" link="/" isSelected={page.url.pathname === '/'} />
<NavbarAction label="首頁" link="/" isSelected={page.url.pathname === '/'} />
</div>
</div>
</div>

View File

@ -1,5 +1,13 @@
<script lang="ts">
let { label, link, isSelected } = $props();
let {
label,
link,
isSelected
}: {
label: string;
link: string;
isSelected: boolean;
} = $props();
</script>
<div class="rounded px-1.5 {isSelected ? 'bg-blue-600' : 'bg-transparent'}">

View File

@ -0,0 +1,74 @@
<script lang="ts">
import TerminalLastLine from '$lib/home/framework/ui/TerminalLastLine.svelte';
import TerminalNormalLine from '$lib/home/framework/ui/TerminalNormalLine.svelte';
import { onDestroy, onMount } from 'svelte';
const lines = [
'大家好,我是 Squid 魷魚',
'身為一位軟體工程師',
'平常最喜歡埋首於程式碼的世界',
'鑽研各種新奇有趣的技術',
'在這裡',
'我會分享我的技術筆記、開發心得',
'還有各式各樣實用工具的評測與介紹',
'一起探索數位世界的無限可能吧!'
];
let isReady: boolean = $state(false);
let currentIndex: number = $state(0);
let element: HTMLDivElement | null = null;
let observer: IntersectionObserver | null = null;
onMount(() => {
if (element == null) {
return;
}
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
isReady = true;
observer?.disconnect();
}
});
},
{ threshold: 1 }
);
observer.observe(element);
});
onDestroy(() => {
observer?.disconnect();
observer = null;
});
function onLastLineComplete() {
if (currentIndex < lines.length - 1) {
currentIndex++;
}
}
</script>
<div
class="mx-auto flex max-w-screen-xl flex-col items-center justify-center gap-y-2.5 px-4 py-32 md:gap-y-8 md:px-24 md:py-32"
>
<div
bind:this={element}
class="border-true-gray-800 bg-true-gray-700 flex w-full flex-col gap-y-1.5 rounded-2xl border-4 p-4 pb-28 font-mono font-medium text-gray-50 shadow-lg transition-opacity duration-300 md:gap-y-2.5 md:rounded-3xl md:border-8 md:p-8 md:pb-32 md:text-xl md:shadow-xl {isReady
? 'opacity-100'
: 'opacity-0'}"
>
{#each lines.slice(0, currentIndex) as line, index (index)}
<TerminalNormalLine text={line} />
{/each}
{#if isReady}
{#key currentIndex}
<TerminalLastLine text={lines[currentIndex]} onComplete={onLastLineComplete} />
{/key}
{/if}
</div>
</div>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
let isVisible: boolean = $state(true);
let interval: ReturnType<typeof setInterval> | null = null;
onMount(() => {
interval = setInterval(() => {
toggleVisibility();
setTimeout(toggleVisibility, 500);
}, 1000);
});
onDestroy(() => {
if (interval != null) {
clearInterval(interval);
interval = null;
}
});
function toggleVisibility() {
isVisible = !isVisible;
}
</script>
<span class="transition-opacity duration-200 {isVisible ? 'opacity-100' : 'opacity-0'}"></span>

View File

@ -0,0 +1,66 @@
<script lang="ts">
import TerminalCursor from '$lib/home/framework/ui/TerminalCursor.svelte';
import { onDestroy, onMount } from 'svelte';
let { text, onComplete: onCompleted }: { text: string; onComplete: () => void } = $props();
let timeText: string = $state('');
let showingText: string = $state('');
let textUpdateInterval: ReturnType<typeof setInterval> | null = null;
let timeUpdateInterval: ReturnType<typeof setInterval> | null = null;
onMount(() => {
setTimeout(() => {
textUpdateInterval = setInterval(() => {
if (showingText.length < text.length) {
showingText += text[showingText.length];
} else {
clearInterval(textUpdateInterval!);
setTimeout(onCompleted, 300);
}
}, 50);
}, 300);
timeUpdateInterval = setInterval(() => {
const now = new Date();
timeText = now.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}, 1000);
});
onDestroy(() => {
if (textUpdateInterval != null) {
clearInterval(textUpdateInterval);
textUpdateInterval = null;
}
if (timeUpdateInterval != null) {
clearInterval(timeUpdateInterval);
timeUpdateInterval = null;
}
});
</script>
<div class="flex w-full flex-col pt-1.5 leading-5 md:pt-2.5 md:leading-7">
<div class="flex w-full flex-row flex-nowrap items-center gap-x-1.5 text-nowrap md:gap-x-2">
<span>
╭─  squid{' '}
<span class="text-blue-500">
~<span class="max-md:hidden">/Documents/blog</span>
</span>
</span>
<div class="h-0.5 w-full bg-gray-50"></div>
<span>{timeText}</span>
</div>
<div class="flex w-full flex-row gap-x-1.5 md:gap-x-2">
<span>
╰─<span class="text-green-400"></span>
</span>
<span>{showingText}</span>
<TerminalCursor />
</div>
</div>

View File

@ -0,0 +1,8 @@
<script lang="ts">
const { text }: { text: string } = $props();
</script>
<div class="flex w-full flex-row gap-x-1.5 md:gap-x-2">
<span class="text-green-400"></span>
<span>{text}</span>
</div>

View File

@ -1,9 +1,9 @@
<script lang="ts">
import SelfTags from '$lib/home/framework/ui/SelfTags.svelte';
import TitleScreenAnimatedTags from '$lib/home/framework/ui/TitleScreenAnimatedTags.svelte';
</script>
<div
class="mx-auto flex min-h-content-height max-w-screen-xl flex-col justify-center gap-y-2.5 px-4 md:gap-y-8 md:px-6"
class="min-h-content-height mx-auto flex max-w-screen-xl flex-col justify-center gap-y-2.5 px-4 md:gap-y-8 md:px-6"
>
<h2 class="text-3xl font-bold text-gray-800 md:text-6xl">Hello 大家好!</h2>
<h1 class="flex flex-row items-center gap-x-2 text-4xl font-extrabold text-gray-800 md:text-7xl">
@ -13,5 +13,5 @@
</div>
<span>魷魚</span>
</h1>
<SelfTags />
<TitleScreenAnimatedTags />
</div>

View File

@ -1,7 +1,9 @@
<script>
import FirstView from '$lib/home/framework/ui/FirstView.svelte';
import Terminal from '$lib/home/framework/ui/Terminal.svelte';
import TitleScreen from '$lib/home/framework/ui/TitleScreen.svelte';
</script>
<div>
<FirstView />
<TitleScreen />
<Terminal />
</div>