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">
|
<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>
|
||||||
|
|||||||
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