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:
@@ -1,15 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { addRecentSearch } from '$lib/stores/recentSearches';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
recentSearches?: string[];
|
||||
}
|
||||
|
||||
let { value = $bindable() }: Props = $props();
|
||||
let { value = $bindable(), recentSearches = [] }: Props = $props();
|
||||
|
||||
// Internal state for immediate user input
|
||||
let inputValue = $state(value);
|
||||
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
|
||||
$effect(() => {
|
||||
@@ -47,14 +53,53 @@
|
||||
document.addEventListener('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>
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
bind:this={searchInput}
|
||||
bind:value={inputValue}
|
||||
onfocus={handleFocus}
|
||||
onblur={handleBlur}
|
||||
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"
|
||||
/>
|
||||
|
||||
{#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>
|
||||
|
||||
16
src/lib/stores/recentSearches.ts
Normal file
16
src/lib/stores/recentSearches.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user