docs(05): create phase plan
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>
This commit is contained in:
300
.planning/phases/05-search/05-03-PLAN.md
Normal file
300
.planning/phases/05-search/05-03-PLAN.md
Normal file
@@ -0,0 +1,300 @@
|
||||
---
|
||||
phase: 05-search
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["05-01", "05-02"]
|
||||
files_modified:
|
||||
- src/routes/+page.svelte
|
||||
- src/lib/components/EntryList.svelte
|
||||
- src/lib/components/EntryCard.svelte
|
||||
- src/lib/stores/recentSearches.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can search entries by typing in search bar"
|
||||
- "User can filter entries using filter controls"
|
||||
- "Search results show matching text highlighted with bold"
|
||||
- "Pinned entries appear in flat list during search/filter (not separated)"
|
||||
- "Empty state shows friendly message when no matches"
|
||||
- "Pressing '/' key focuses search input"
|
||||
- "Recent searches appear as quick picks"
|
||||
- "Clear button resets all filters"
|
||||
artifacts:
|
||||
- path: "src/routes/+page.svelte"
|
||||
provides: "Integrated search UI at page level"
|
||||
contains: "SearchBar"
|
||||
- path: "src/lib/components/EntryList.svelte"
|
||||
provides: "Filtered entry rendering with flat list mode"
|
||||
contains: "filterEntries"
|
||||
- path: "src/lib/components/EntryCard.svelte"
|
||||
provides: "Highlighted text display"
|
||||
contains: "highlightText"
|
||||
- path: "src/lib/stores/recentSearches.ts"
|
||||
provides: "Persisted recent searches store"
|
||||
exports: ["recentSearches", "addRecentSearch"]
|
||||
key_links:
|
||||
- from: "src/routes/+page.svelte"
|
||||
to: "src/lib/components/SearchBar.svelte"
|
||||
via: "component import"
|
||||
pattern: "import SearchBar"
|
||||
- from: "src/lib/components/EntryList.svelte"
|
||||
to: "src/lib/utils/filterEntries.ts"
|
||||
via: "$derived filtering"
|
||||
pattern: "filterEntries.*filters"
|
||||
- from: "src/lib/components/EntryCard.svelte"
|
||||
to: "src/lib/utils/highlightText.ts"
|
||||
via: "{@html} rendering"
|
||||
pattern: "\\{@html.*highlightText"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Integrate search components, filtering logic, and highlighting into the main page.
|
||||
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/tho/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create recent searches store and add "/" keyboard shortcut to SearchBar</name>
|
||||
<files>src/lib/stores/recentSearches.ts, src/lib/components/SearchBar.svelte</files>
|
||||
<action>
|
||||
1. Create src/lib/stores/recentSearches.ts:
|
||||
|
||||
```typescript
|
||||
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);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
2. 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
|
||||
</action>
|
||||
<verify>Pressing "/" focuses search when not in an input field; recent searches appear on focus</verify>
|
||||
<done>Search input has "/" shortcut and shows recent searches dropdown</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Integrate search/filter into EntryList with flat list mode</name>
|
||||
<files>src/lib/components/EntryList.svelte</files>
|
||||
<action>
|
||||
Modify EntryList.svelte to:
|
||||
|
||||
1. Accept new props:
|
||||
```typescript
|
||||
interface Props {
|
||||
entries: EntryWithData[];
|
||||
availableTags: Tag[];
|
||||
filters: SearchFilters; // NEW
|
||||
searchQuery: string; // NEW - for highlighting
|
||||
}
|
||||
```
|
||||
|
||||
2. Apply filtering using $derived:
|
||||
```typescript
|
||||
import { filterEntries } from '$lib/utils/filterEntries';
|
||||
import { hasActiveFilters } from '$lib/types/search';
|
||||
|
||||
let filteredEntries = $derived(filterEntries(entries, filters));
|
||||
let isFiltering = $derived(hasActiveFilters(filters));
|
||||
```
|
||||
|
||||
3. 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
|
||||
|
||||
4. Update empty state:
|
||||
- When no entries at all: "No entries yet" (existing)
|
||||
- When filtering with no results: "No entries match your search" (new)
|
||||
|
||||
Implementation:
|
||||
```svelte
|
||||
{#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}
|
||||
```
|
||||
</action>
|
||||
<verify>EntryList filters entries and shows flat list during search</verify>
|
||||
<done>EntryList applies filters via $derived and displays flat list when filtering is active</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Add highlighting to EntryCard and integrate search UI in +page.svelte</name>
|
||||
<files>src/lib/components/EntryCard.svelte, src/routes/+page.svelte</files>
|
||||
<action>
|
||||
1. Update EntryCard.svelte:
|
||||
- Add new prop: searchQuery: string (default '')
|
||||
- Import highlightText from '$lib/utils/highlightText'
|
||||
- Use {@html highlightText(entry.title, searchQuery)} for title display
|
||||
- Use {@html highlightText(entry.content, searchQuery)} for content preview
|
||||
- Only apply highlighting in collapsed view (where preview text shows)
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
entry: EntryWithData;
|
||||
availableTags: Tag[];
|
||||
searchQuery?: string; // NEW, optional with default ''
|
||||
}
|
||||
|
||||
let { entry, availableTags, searchQuery = '' }: Props = $props();
|
||||
```
|
||||
|
||||
For title (collapsed view):
|
||||
```svelte
|
||||
<h3 class="...">
|
||||
{@html highlightText(entry.title || 'Untitled', searchQuery)}
|
||||
</h3>
|
||||
```
|
||||
|
||||
For content preview (collapsed view):
|
||||
```svelte
|
||||
<p class="...">
|
||||
{@html highlightText(entry.content, searchQuery)}
|
||||
</p>
|
||||
```
|
||||
|
||||
2. 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:
|
||||
```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>
|
||||
```
|
||||
</action>
|
||||
<verify>Full search flow works: type in search, see highlighted results, use filters</verify>
|
||||
<done>Search and filters are fully integrated with highlighting in search results</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npm run check` passes
|
||||
- `npm run dev` starts and search/filter UI is visible
|
||||
- Typing in search filters entries (after 2 chars, debounced)
|
||||
- Matching text is highlighted with bold
|
||||
- Pressing "/" focuses search input (when not already in input)
|
||||
- Recent searches appear on focus
|
||||
- Filters work correctly (type, tags, date range)
|
||||
- Clear button resets all filters
|
||||
- Pinned entries appear in flat list during search (no separation)
|
||||
- Empty state shows "No entries match your search" when filtering yields nothing
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-search/05-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user