feat(05-01): create SearchBar component with debounced input

- Debounced value binding (300ms delay via $effect cleanup)
- Only trigger search when query >= 2 chars OR cleared to empty
- Bindable value prop for parent integration
- '/' keyboard shortcut to focus search (GitHub-style)
- Native event listener to avoid Svelte 5 keydown bug
This commit is contained in:
Thomas Richter
2026-01-31 17:12:25 +01:00
parent 8f544a9989
commit b7a982c104

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { onMount } from 'svelte';
interface Props {
value: string;
}
let { value = $bindable() }: Props = $props();
// Internal state for immediate user input
let inputValue = $state(value);
let searchInput: HTMLInputElement;
// Debounce: update bound value 300ms after user stops typing
$effect(() => {
const timeout = setTimeout(() => {
// Only trigger when >= 2 chars OR cleared to empty
if (inputValue.length >= 2 || inputValue.length === 0) {
value = inputValue;
}
}, 300);
return () => clearTimeout(timeout);
});
// Sync inputValue if parent changes value externally
$effect(() => {
if (value !== inputValue && (value === '' || inputValue === '')) {
inputValue = value;
}
});
// "/" keyboard shortcut - use native listener to avoid Svelte 5 bug
onMount(() => {
function handleKeydown(e: KeyboardEvent) {
// Skip if already in an input or textarea
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (e.key === '/') {
e.preventDefault();
searchInput?.focus();
}
}
document.addEventListener('keydown', handleKeydown);
return () => document.removeEventListener('keydown', handleKeydown);
});
</script>
<div class="relative">
<input
bind:this={searchInput}
bind:value={inputValue}
type="text"
placeholder='Search entries... (press "/")'
class="w-full px-4 py-2 border border-gray-200 rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>