Files
taskplaner/.planning/phases/05-search/05-03-PLAN.md
Thomas Richter f6144f4edf 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>
2026-01-31 14:15:38 +01:00

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
05-01
05-02
src/routes/+page.svelte
src/lib/components/EntryList.svelte
src/lib/components/EntryCard.svelte
src/lib/stores/recentSearches.ts
true
truths artifacts key_links
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
path provides contains
src/routes/+page.svelte Integrated search UI at page level SearchBar
path provides contains
src/lib/components/EntryList.svelte Filtered entry rendering with flat list mode filterEntries
path provides contains
src/lib/components/EntryCard.svelte Highlighted text display highlightText
path provides exports
src/lib/stores/recentSearches.ts Persisted recent searches store
recentSearches
addRecentSearch
from to via pattern
src/routes/+page.svelte src/lib/components/SearchBar.svelte component import import SearchBar
from to via pattern
src/lib/components/EntryList.svelte src/lib/utils/filterEntries.ts $derived filtering filterEntries.*filters
from to via pattern
src/lib/components/EntryCard.svelte src/lib/utils/highlightText.ts {@html} rendering {@html.*highlightText
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.

<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);
  });
}
  1. 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
Task 2: Integrate search/filter into EntryList with flat list mode src/lib/components/EntryList.svelte Modify EntryList.svelte to:
  1. Accept new props:
interface Props {
  entries: EntryWithData[];
  availableTags: Tag[];
  filters: SearchFilters;         // NEW
  searchQuery: string;            // NEW - for highlighting
}
  1. 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));
  1. 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
  1. 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}
EntryList filters entries and shows flat list during search EntryList applies filters via $derived and displays flat list when filtering is active Task 3: Add highlighting to EntryCard and integrate search UI in +page.svelte src/lib/components/EntryCard.svelte, src/routes/+page.svelte 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)
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>
  1. 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>
Full search flow works: type in search, see highlighted results, use filters Search and filters are fully integrated with highlighting in search results - `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

<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>
After completion, create `.planning/phases/05-search/05-03-SUMMARY.md`