refactor: Move search modal to layout root for proper z-index stacking
- Extract SearchModal component from SearchBar for root-level rendering - Add isModalOpen state to search store with open/close methods - Simplify SearchBar to trigger button only - Update Modal with proper overflow handling and scroll-to-close - Fix layout background to use void color 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
146
src/lib/components/layout/SearchModal.svelte
Normal file
146
src/lib/components/layout/SearchModal.svelte
Normal file
@@ -0,0 +1,146 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Search, Command } from 'lucide-svelte';
|
||||
import { search } from '$lib/stores';
|
||||
import { fly, fade } from 'svelte/transition';
|
||||
|
||||
let query = $state('');
|
||||
let searchInput = $state<HTMLInputElement | null>(null);
|
||||
|
||||
// Focus input when modal opens
|
||||
$effect(() => {
|
||||
if ($search.isModalOpen) {
|
||||
setTimeout(() => searchInput?.focus(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
search.closeModal();
|
||||
query = '';
|
||||
};
|
||||
|
||||
const handleBackdropClick = (e: MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && $search.isModalOpen) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if ($search.isModalOpen) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!query.trim()) return;
|
||||
|
||||
search.addRecentSearch(query);
|
||||
goto(`/matches?search=${encodeURIComponent(query)}`);
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleRecentClick = (recentQuery: string) => {
|
||||
query = recentQuery;
|
||||
handleSearch(new Event('submit'));
|
||||
};
|
||||
|
||||
const handleClearRecent = () => {
|
||||
search.clearRecentSearches();
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} onscroll={handleScroll} />
|
||||
|
||||
{#if $search.isModalOpen}
|
||||
<div
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center overflow-y-auto p-4"
|
||||
transition:fade={{ duration: 200 }}
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Escape') handleClose();
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Search"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div class="pointer-events-none absolute inset-0 z-0 bg-black/70 backdrop-blur-sm"></div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div
|
||||
class="relative z-10 my-auto max-h-[90vh] w-full max-w-4xl overflow-y-auto rounded-xl border border-neon-blue/20 bg-void shadow-2xl"
|
||||
style="box-shadow: 0 0 50px rgba(0, 212, 255, 0.1);"
|
||||
transition:fly={{ y: -20, duration: 300 }}
|
||||
>
|
||||
<!-- Content -->
|
||||
<div class="space-y-4 p-6">
|
||||
<form onsubmit={handleSearch}>
|
||||
<label
|
||||
class="flex items-center gap-3 rounded-lg border border-neon-blue/30 bg-void-light/50 px-4 py-3 transition-colors focus-within:border-neon-blue focus-within:ring-1 focus-within:ring-neon-blue"
|
||||
>
|
||||
<Search class="h-5 w-5 text-white/50" aria-hidden="true" />
|
||||
<input
|
||||
bind:this={searchInput}
|
||||
bind:value={query}
|
||||
type="text"
|
||||
class="grow bg-transparent text-white placeholder:text-white/40 focus:outline-none"
|
||||
placeholder="Search matches, players, share codes..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<kbd
|
||||
class="flex items-center gap-0.5 rounded border border-neon-blue/30 bg-void px-1.5 py-0.5 text-xs text-white/50"
|
||||
>
|
||||
<Command class="h-3 w-3" aria-hidden="true" />
|
||||
<span>K</span>
|
||||
</kbd>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<!-- Recent Searches -->
|
||||
{#if $search.recentSearches.length > 0}
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-white/60">Recent Searches</h3>
|
||||
<button
|
||||
class="rounded px-2 py-1 text-xs text-white/50 transition-colors hover:bg-neon-red/10 hover:text-neon-red focus:outline-none focus-visible:ring-1 focus-visible:ring-neon-red"
|
||||
onclick={handleClearRecent}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each $search.recentSearches as recent}
|
||||
<button
|
||||
class="flex items-center gap-2 rounded-full border border-neon-blue/30 px-3 py-1.5 text-sm text-white/70 transition-colors hover:border-neon-blue hover:bg-neon-blue/10 hover:text-neon-blue focus:outline-none focus-visible:ring-2 focus-visible:ring-neon-blue"
|
||||
onclick={() => handleRecentClick(recent)}
|
||||
>
|
||||
<Search class="h-3 w-3" aria-hidden="true" />
|
||||
{recent}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Search Tips -->
|
||||
<div class="rounded-lg border border-neon-blue/10 bg-neon-blue/5 p-4">
|
||||
<h4 class="mb-2 text-sm font-semibold text-white">Search Tips</h4>
|
||||
<ul class="space-y-1 text-xs text-white/50">
|
||||
<li>Search by player name or Steam ID</li>
|
||||
<li>Enter share code to find specific match</li>
|
||||
<li>Use map name to filter matches (e.g., "de_dust2")</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user