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
+15
View File
@@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+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; _stop_game?: () => void;
_restart_game?: () => void; _restart_game?: () => void;
locateFile: (path: string) => string; locateFile: (path: string) => string;
preRun: unknown[]; preRun: Array<() => void>;
// Emscripten lifecycle hooks // Emscripten lifecycle hooks
onAbort?: (what: unknown) => void; onAbort?: (what: unknown) => void;
onRuntimeInitialized?: () => void; onRuntimeInitialized?: () => void;
+19 -34
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { browser } from '$app/environment';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { onNavigate } from '$app/navigation'; import { onNavigate } from '$app/navigation';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-svelte'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-svelte';
@@ -10,11 +11,11 @@
IconDeviceGamepad3, IconDeviceGamepad3,
IconTrophy IconTrophy
} from '@tabler/icons-svelte'; } from '@tabler/icons-svelte';
import NavLink from '$lib/components/NavLink.svelte';
let { children } = $props(); let { children } = $props();
let opened = $state(false); let opened = $state(false);
let mounted = $state(false);
// Keys that the game uses - only these should reach SDL/Emscripten on the play page // Keys that the game uses - only these should reach SDL/Emscripten on the play page
const GAME_KEYS = new Set([ const GAME_KEYS = new Set([
@@ -41,8 +42,6 @@
]); ]);
onMount(() => { onMount(() => {
mounted = true;
// Global keyboard filter to prevent SDL/Emscripten from capturing keys. // Global keyboard filter to prevent SDL/Emscripten from capturing keys.
// SDL's handlers persist globally even after navigating away from the play page. // 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. // This filter ensures browser shortcuts (F5, F12, Ctrl+R, etc.) always work.
@@ -152,7 +151,7 @@
<svelte:head> <svelte:head>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<title>Pac-Man</title> <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> </svelte:head>
<div class="bg-black text-yellow-400 h-screen flex flex-col overflow-hidden"> <div class="bg-black text-yellow-400 h-screen flex flex-col overflow-hidden">
@@ -168,15 +167,7 @@
</button> </button>
<div class="flex items-center gap-8"> <div class="flex items-center gap-8">
<a <NavLink href="/leaderboard" icon={IconTrophy} label="Leaderboard" active={isActive('/leaderboard')} />
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>
<a <a
href="/" href="/"
@@ -197,15 +188,7 @@
</h1> </h1>
</a> </a>
<a <NavLink href="/download" icon={IconDownload} label="Download" active={isActive('/download')} />
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>
</div> </div>
<div class="absolute right-4 hidden sm:flex gap-4 items-center"> <div class="absolute right-4 hidden sm:flex gap-4 items-center">
@@ -223,7 +206,7 @@
</div> </div>
</header> </header>
{#if mounted} {#if browser}
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
defer defer
options={{ options={{
@@ -247,8 +230,18 @@
{#if opened} {#if opened}
<div class="fixed inset-0 z-30"> <div class="fixed inset-0 z-30">
<!-- svelte-ignore a11y_click_events_have_key_events --> <div
<div role="button" tabindex="-1" class="absolute inset-0 bg-black/60" onclick={close}></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 <div
class="absolute left-0 top-0 h-full w-72 max-w-[80vw] bg-black border-r border-yellow-400/25 p-4" 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>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
{#each links as link} {#each links as link}
<a <NavLink href={link.href} icon={link.icon} label={link.label} active={isActive(link.href)} size={28} />
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>
{/each} {/each}
</div> </div>
</div> </div>
+16 -16
View File
@@ -12,23 +12,22 @@
let loadError = $state<LoadingError | null>(null); let loadError = $state<LoadingError | null>(null);
let timeoutId: ReturnType<typeof setTimeout> | null = null; let timeoutId: ReturnType<typeof setTimeout> | null = null;
// Fade out loading overlay when game becomes ready // Manage loading overlay fade and timeout cleanup
$effect(() => { $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) { if (gameReady && loadingVisible) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
loadingVisible = false; loadingVisible = false;
}, LOADING_FADE_DURATION); }, LOADING_FADE_DURATION);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
});
// Clear timeout when game is ready or error occurs
$effect(() => {
if (gameReady || loadError) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
} }
}); });
@@ -64,11 +63,7 @@
// Restart game when returning to this page // Restart game when returning to this page
afterNavigate(() => { afterNavigate(() => {
requestAnimationFrame(() => {
setTimeout(() => {
restartGame(); restartGame();
}, 0);
});
}); });
function restartGame() { function restartGame() {
@@ -209,14 +204,19 @@
} }
</script> </script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="flex justify-center items-center h-full pt-4"> <div class="flex justify-center items-center h-full pt-4">
<div <div
role="button" role="button"
tabindex="-1" tabindex="0"
class="relative block aspect-[5/6]" class="relative block aspect-[5/6]"
style="height: min(calc(100vh - 96px), calc((100vw - 32px) * 6 / 5));" style="height: min(calc(100vh - 96px), calc((100vw - 32px) * 6 / 5));"
onclick={handleInteraction} 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> <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="page-container">
<div class="space-y-6"> <div class="space-y-6">
<div class="card"> <div class="card">