Phase 05: Search - 3 plans in 2 waves - Wave 1: 05-01 (UI components), 05-02 (filtering logic) — parallel - Wave 2: 05-03 (integration + recent searches + "/" shortcut) - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
10 KiB
10 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-search | 03 | execute | 2 |
|
|
true |
|
Purpose: Wire together SearchBar, FilterBar, filterEntries, and highlightText to create a complete search experience. Add recent searches persistence and "/" keyboard shortcut. Output: Fully functional search and filtering on the main entries page.
<execution_context> @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-search/05-CONTEXT.md @.planning/phases/05-search/05-RESEARCH.md @.planning/phases/05-search/05-01-SUMMARY.md @.planning/phases/05-search/05-02-SUMMARY.md @src/routes/+page.svelte @src/lib/components/EntryList.svelte @src/lib/components/EntryCard.svelte Task 1: Create recent searches store and add "/" keyboard shortcut to SearchBar src/lib/stores/recentSearches.ts, src/lib/components/SearchBar.svelte 1. Create src/lib/stores/recentSearches.ts: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);
});
}
- Update SearchBar.svelte to add:
- "/" keyboard shortcut using native document.addEventListener in onMount
- Skip shortcut if already in input/textarea (check e.target)
- e.preventDefault() to prevent "/" from being typed
- Recent searches dropdown (show when input focused and empty)
- Clicking a recent search fills the input
- Call addRecentSearch when search is executed (on blur with value >= 2 chars)
Implementation notes for "/" shortcut:
- Use native document.addEventListener (NOT svelte:window) to avoid known Svelte 5 bug
- Return cleanup function from onMount to remove listener
- Check if target is HTMLInputElement or HTMLTextAreaElement before handling
Recent searches UI:
- Show dropdown below input when: focused AND inputValue is empty AND recentSearches.length > 0
- List shows "Recent searches" header + up to 5 items
- onmousedown (not onclick) to fire before onblur Pressing "/" focuses search when not in an input field; recent searches appear on focus Search input has "/" shortcut and shows recent searches dropdown
- Accept new props:
interface Props {
entries: EntryWithData[];
availableTags: Tag[];
filters: SearchFilters; // NEW
searchQuery: string; // NEW - for highlighting
}
- Apply filtering using $derived:
import { filterEntries } from '$lib/utils/filterEntries';
import { hasActiveFilters } from '$lib/types/search';
let filteredEntries = $derived(filterEntries(entries, filters));
let isFiltering = $derived(hasActiveFilters(filters));
- Modify display logic:
- When NOT filtering: Keep current pinned/unpinned separation
- When filtering: Flat list (no pinned section), just render all filteredEntries
- Pass searchQuery to each EntryCard for highlighting
- Update empty state:
- When no entries at all: "No entries yet" (existing)
- When filtering with no results: "No entries match your search" (new)
Implementation:
{#if filteredEntries.length === 0}
{#if isFiltering}
<div class="text-center py-12 text-gray-500">
<p class="text-lg">No entries match your search</p>
<p class="text-sm mt-1">Try adjusting your filters or search term</p>
</div>
{:else}
<!-- existing empty state -->
{/if}
{:else if isFiltering}
<!-- Flat list when filtering -->
<div class="divide-y divide-gray-100 md:divide-y-0 md:space-y-3">
{#each filteredEntries as entry (entry.id)}
<EntryCard {entry} {availableTags} {searchQuery} />
{/each}
</div>
{:else}
<!-- Pinned/unpinned separation when not filtering -->
<!-- existing code -->
{/if}
interface Props {
entry: EntryWithData;
availableTags: Tag[];
searchQuery?: string; // NEW, optional with default ''
}
let { entry, availableTags, searchQuery = '' }: Props = $props();
For title (collapsed view):
<h3 class="...">
{@html highlightText(entry.title || 'Untitled', searchQuery)}
</h3>
For content preview (collapsed view):
<p class="...">
{@html highlightText(entry.content, searchQuery)}
</p>
- Update src/routes/+page.svelte:
- Import SearchBar, FilterBar from components
- Import SearchFilters, defaultFilters from types/search
- Import recentSearches, addRecentSearch from stores
- Add filter state with $state(defaultFilters)
- Add searchQuery state bound to SearchBar
- Add FilterBar with onchange handler
- Place SearchBar above EntryList (visible at top, in header or just below)
- Place FilterBar below SearchBar
- Pass filters and searchQuery to EntryList
- Handle onSearch callback to add to recent searches
Layout in +page.svelte:
<header class="bg-white border-b border-gray-200 sticky top-0 z-10">
<div class="max-w-2xl mx-auto px-4 py-4">
<div class="flex items-center justify-between mb-3">
<h1 class="text-xl font-bold text-gray-900">TaskPlaner</h1>
<CompletedToggle />
</div>
<SearchBar bind:value={searchQuery} onSearch={handleSearch} {recentSearches} onSelectRecent={handleSelectRecent} />
</div>
</header>
<div class="max-w-2xl mx-auto px-4">
<FilterBar
{filters}
availableTags={data.allTags}
onchange={(f) => filters = f}
/>
</div>
<div class="max-w-2xl mx-auto px-4 py-4">
<EntryList
entries={data.entries}
availableTags={data.allTags}
{filters}
{searchQuery}
/>
</div>
<success_criteria>
- Search bar visible at top of page
- "/" key focuses search input
- Text search filters entries with 2-char minimum and debounce
- Recent searches show as quick picks (last 5)
- Type filter toggles between All/Tasks/Thoughts
- Tag filter with AND logic (multiple tags)
- Date range with presets and custom option
- Clear button appears when filters active
- Matching text highlighted with bold (not background)
- Flat list during search (no pinned separation)
- Friendly empty state message </success_criteria>