refactor(web): extract NavLink component and add Prettier config

This commit is contained in:
2025-12-30 02:42:36 -06:00
parent de7c656b61
commit a89a210c78
6 changed files with 81 additions and 56 deletions
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts">
import type { ComponentType } from 'svelte';
let {
href,
icon,
label,
active = false,
size = 18
}: {
href: string;
icon: ComponentType;
label: string;
active?: boolean;
size?: number;
} = $props();
function navLinkClass(active: boolean): string {
return `flex items-center gap-1.5 tracking-wide transition-colors duration-200 ${
active ? 'text-white' : 'text-gray-500 hover:text-gray-300'
}`;
}
</script>
<a {href} class={navLinkClass(active)}>
<icon {size}></icon>
<span>{label}</span>
</a>
+1 -1
View File
@@ -6,7 +6,7 @@ export interface PacmanModule {
_stop_game?: () => void;
_restart_game?: () => void;
locateFile: (path: string) => string;
preRun: unknown[];
preRun: Array<() => void>;
// Emscripten lifecycle hooks
onAbort?: (what: unknown) => void;
onRuntimeInitialized?: () => void;
+19 -34
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import '../app.css';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import { onNavigate } from '$app/navigation';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-svelte';
@@ -10,11 +11,11 @@
IconDeviceGamepad3,
IconTrophy
} from '@tabler/icons-svelte';
import NavLink from '$lib/components/NavLink.svelte';
let { children } = $props();
let opened = $state(false);
let mounted = $state(false);
// Keys that the game uses - only these should reach SDL/Emscripten on the play page
const GAME_KEYS = new Set([
@@ -41,8 +42,6 @@
]);
onMount(() => {
mounted = true;
// Global keyboard filter to prevent SDL/Emscripten from capturing keys.
// SDL's handlers persist globally even after navigating away from the play page.
// This filter ensures browser shortcuts (F5, F12, Ctrl+R, etc.) always work.
@@ -152,7 +151,7 @@
<svelte:head>
<link rel="icon" href="/favicon.ico" />
<title>Pac-Man</title>
<meta name="description" content="A Pac-Man game built with Rust and React." />
<meta name="description" content="A Pac-Man game built with Rust and Svelte." />
</svelte:head>
<div class="bg-black text-yellow-400 h-screen flex flex-col overflow-hidden">
@@ -168,15 +167,7 @@
</button>
<div class="flex items-center gap-8">
<a
href="/leaderboard"
class="flex items-center gap-1.5 tracking-wide transition-colors duration-200 {isActive('/leaderboard')
? 'text-white'
: 'text-gray-500 hover:text-gray-300'}"
>
<IconTrophy size={18} />
<span>Leaderboard</span>
</a>
<NavLink href="/leaderboard" icon={IconTrophy} label="Leaderboard" active={isActive('/leaderboard')} />
<a
href="/"
@@ -197,15 +188,7 @@
</h1>
</a>
<a
href="/download"
class="flex items-center gap-1.5 tracking-wide transition-colors duration-200 {isActive('/download')
? 'text-white'
: 'text-gray-500 hover:text-gray-300'}"
>
<IconDownload size={18} />
<span>Download</span>
</a>
<NavLink href="/download" icon={IconDownload} label="Download" active={isActive('/download')} />
</div>
<div class="absolute right-4 hidden sm:flex gap-4 items-center">
@@ -223,7 +206,7 @@
</div>
</header>
{#if mounted}
{#if browser}
<OverlayScrollbarsComponent
defer
options={{
@@ -247,8 +230,18 @@
{#if opened}
<div class="fixed inset-0 z-30">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div role="button" tabindex="-1" class="absolute inset-0 bg-black/60" onclick={close}></div>
<div
role="button"
tabindex="0"
class="absolute inset-0 bg-black/60"
onclick={close}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
close();
}
}}
></div>
<div
class="absolute left-0 top-0 h-full w-72 max-w-[80vw] bg-black border-r border-yellow-400/25 p-4"
>
@@ -264,15 +257,7 @@
</div>
<div class="flex flex-col gap-3">
{#each links as link}
<a
href={link.href}
class="flex items-center gap-1.5 tracking-wide transition-colors duration-200 {isActive(link.href)
? 'text-white'
: 'text-gray-500 hover:text-gray-300'}"
>
<link.icon size={28} />
<span>{link.label}</span>
</a>
<NavLink href={link.href} icon={link.icon} label={link.label} active={isActive(link.href)} size={28} />
{/each}
</div>
</div>
+18 -18
View File
@@ -12,23 +12,22 @@
let loadError = $state<LoadingError | null>(null);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
// Fade out loading overlay when game becomes ready
$effect(() => {
if (gameReady && loadingVisible) {
const timer = setTimeout(() => {
loadingVisible = false;
}, LOADING_FADE_DURATION);
return () => clearTimeout(timer);
}
});
// Clear timeout when game is ready or error occurs
// Manage loading overlay fade and timeout cleanup
$effect(() => {
if (gameReady || loadError) {
// Clear loading timeout when ready or error
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
// Fade out loading overlay when ready
if (gameReady && loadingVisible) {
const timer = setTimeout(() => {
loadingVisible = false;
}, LOADING_FADE_DURATION);
return () => clearTimeout(timer);
}
}
});
@@ -64,11 +63,7 @@
// Restart game when returning to this page
afterNavigate(() => {
requestAnimationFrame(() => {
setTimeout(() => {
restartGame();
}, 0);
});
restartGame();
});
function restartGame() {
@@ -209,14 +204,19 @@
}
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="flex justify-center items-center h-full pt-4">
<div
role="button"
tabindex="-1"
tabindex="0"
class="relative block aspect-[5/6]"
style="height: min(calc(100vh - 96px), calc((100vw - 32px) * 6 / 5));"
onclick={handleInteraction}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleInteraction();
}
}}
>
<canvas id="canvas" tabindex="-1" class="w-full h-full" onclick={focusCanvas}></canvas>
-3
View File
@@ -1,6 +1,3 @@
<script lang="ts">
</script>
<div class="page-container">
<div class="space-y-6">
<div class="card">