feat(05-03): create recent searches store and add dropdown UI

- Add recentSearches persisted store with 5-item limit
- Add addRecentSearch() function for deduplication
- Update SearchBar with recentSearches dropdown
- Show dropdown on focus when input empty
- Use onmousedown to handle selection before blur
- Add to recent searches on blur with >= 2 chars
This commit is contained in:
Thomas Richter
2026-01-31 17:17:03 +01:00
parent 6090d78824
commit af61b10a59
2 changed files with 62 additions and 1 deletions

View File

@@ -1,15 +1,21 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { addRecentSearch } from '$lib/stores/recentSearches';
interface Props { interface Props {
value: string; value: string;
recentSearches?: string[];
} }
let { value = $bindable() }: Props = $props(); let { value = $bindable(), recentSearches = [] }: Props = $props();
// Internal state for immediate user input // Internal state for immediate user input
let inputValue = $state(value); let inputValue = $state(value);
let searchInput: HTMLInputElement; let searchInput: HTMLInputElement;
let isFocused = $state(false);
// Show dropdown when focused, empty input, and have recent searches
let showRecentSearches = $derived(isFocused && inputValue === '' && recentSearches.length > 0);
// Debounce: update bound value 300ms after user stops typing // Debounce: update bound value 300ms after user stops typing
$effect(() => { $effect(() => {
@@ -47,14 +53,53 @@
document.addEventListener('keydown', handleKeydown); document.addEventListener('keydown', handleKeydown);
return () => document.removeEventListener('keydown', handleKeydown); return () => document.removeEventListener('keydown', handleKeydown);
}); });
function handleFocus() {
isFocused = true;
}
function handleBlur() {
isFocused = false;
// Add to recent searches on blur if value is long enough
if (inputValue.length >= 2) {
addRecentSearch(inputValue);
}
}
function selectRecentSearch(search: string) {
inputValue = search;
value = search;
isFocused = false;
}
</script> </script>
<div class="relative"> <div class="relative">
<input <input
bind:this={searchInput} bind:this={searchInput}
bind:value={inputValue} bind:value={inputValue}
onfocus={handleFocus}
onblur={handleBlur}
type="text" type="text"
placeholder='Search entries... (press "/")' 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" 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"
/> />
{#if showRecentSearches}
<div
class="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-20"
>
<div class="px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wide border-b border-gray-100">
Recent searches
</div>
{#each recentSearches as search}
<button
type="button"
onmousedown={() => selectRecentSearch(search)}
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none"
>
{search}
</button>
{/each}
</div>
{/if}
</div> </div>

View File

@@ -0,0 +1,16 @@
import { persisted } from 'svelte-persisted-store';
const MAX_RECENT = 5;
export const recentSearches = persisted<string[]>('taskplaner-recent-searches', []);
export function addRecentSearch(query: string): void {
if (!query || query.length < 2) return;
recentSearches.update((searches) => {
// Remove if already exists (will re-add at front)
const filtered = searches.filter((s) => s.toLowerCase() !== query.toLowerCase());
// Add to front, limit to MAX_RECENT
return [query, ...filtered].slice(0, MAX_RECENT);
});
}