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:
Thomas Richter
2026-01-31 14:15:38 +01:00
parent 9d4f9bb67f
commit f6144f4edf
4 changed files with 731 additions and 4 deletions

View File

@@ -105,12 +105,12 @@ Plans:
3. User can filter entries by date range 3. User can filter entries by date range
4. User can filter to show only tasks or only thoughts 4. User can filter to show only tasks or only thoughts
5. Search results show relevant matches with highlighting 5. Search results show relevant matches with highlighting
**Plans**: TBD **Plans**: 3 plans
Plans: Plans:
- [ ] 05-01: TBD - [ ] 05-01-PLAN.md — SearchBar and FilterBar components with type definitions
- [ ] 05-02: TBD - [ ] 05-02-PLAN.md — Filtering logic and text highlighting utilities
- [ ] 05-03: TBD - [ ] 05-03-PLAN.md — Integration with recent searches and "/" keyboard shortcut
### Phase 6: Deployment ### Phase 6: Deployment
**Goal**: Application runs in Docker with persistent data and easy configuration **Goal**: Application runs in Docker with persistent data and easy configuration

View File

@@ -0,0 +1,221 @@
---
phase: 05-search
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/components/SearchBar.svelte
- src/lib/components/FilterBar.svelte
- src/lib/types/search.ts
autonomous: true
must_haves:
truths:
- "Search input is visible at top of page"
- "Filter controls (type, tags, date) are visible alongside search"
- "User can type in search input"
- "User can interact with filter controls"
artifacts:
- path: "src/lib/components/SearchBar.svelte"
provides: "Text search input with debounced value binding"
min_lines: 40
- path: "src/lib/components/FilterBar.svelte"
provides: "Filter controls for type, tags, and date range"
min_lines: 60
- path: "src/lib/types/search.ts"
provides: "SearchFilters interface and default values"
exports: ["SearchFilters", "defaultFilters", "hasActiveFilters"]
key_links:
- from: "src/lib/components/SearchBar.svelte"
to: "parent component"
via: "$bindable value prop"
pattern: "value = \\$bindable"
- from: "src/lib/components/FilterBar.svelte"
to: "parent component"
via: "onchange callback prop"
pattern: "onchange.*SearchFilters"
---
<objective>
Create the search and filter UI components with type definitions.
Purpose: Establish the visual search/filter interface that users interact with. These components emit filter state changes but do not perform filtering themselves.
Output: SearchBar.svelte, FilterBar.svelte, and search.ts type definitions ready for integration.
</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
@src/lib/components/EntryList.svelte
@src/lib/stores/preferences.svelte.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Create search types and utility functions</name>
<files>src/lib/types/search.ts</files>
<action>
Create the SearchFilters interface and related utilities:
```typescript
export interface SearchFilters {
query: string;
tags: string[];
type: 'task' | 'thought' | 'all';
dateRange: {
start: string | null;
end: string | null;
};
}
export const defaultFilters: SearchFilters = {
query: '',
tags: [],
type: 'all',
dateRange: { start: null, end: null }
};
export function hasActiveFilters(filters: SearchFilters): boolean {
return (
filters.query.length >= 2 ||
filters.tags.length > 0 ||
filters.type !== 'all' ||
filters.dateRange.start !== null ||
filters.dateRange.end !== null
);
}
// Date preset helper for quick date range selection
export function getDatePreset(preset: 'today' | 'week' | 'month'): { start: string; end: string } {
const now = new Date();
const end = now.toISOString().split('T')[0];
let start: Date;
switch (preset) {
case 'today':
start = now;
break;
case 'week':
start = new Date(now);
start.setDate(start.getDate() - 7);
break;
case 'month':
start = new Date(now);
start.setMonth(start.getMonth() - 1);
break;
}
return {
start: start.toISOString().split('T')[0],
end
};
}
```
</action>
<verify>TypeScript compiles without errors: `npx tsc --noEmit`</verify>
<done>SearchFilters interface exported and importable from other files</done>
</task>
<task type="auto">
<name>Task 2: Create SearchBar component with debounced input</name>
<files>src/lib/components/SearchBar.svelte</files>
<action>
Create SearchBar.svelte with:
- Text input with placeholder 'Search entries... (press "/")'
- Debounced value binding (300ms delay, using $effect cleanup pattern)
- Only trigger search when query >= 2 characters OR when cleared to empty
- Bindable value prop for parent to receive debounced search term
- Styling: full width, px-4 py-2, border rounded-lg, focus ring
Props interface:
```typescript
interface Props {
value: string; // bindable - the debounced search query
}
```
Implementation notes:
- Use internal $state for inputValue (immediate user typing)
- Use $effect with setTimeout/clearTimeout for debounce
- Update the bound value only after debounce completes
- Match existing Tailwind styling patterns from other components
</action>
<verify>Component renders without errors when imported</verify>
<done>SearchBar accepts text input and exposes debounced value via $bindable prop</done>
</task>
<task type="auto">
<name>Task 3: Create FilterBar component with type, tag, and date controls</name>
<files>src/lib/components/FilterBar.svelte</files>
<action>
Create FilterBar.svelte with:
1. Type filter - three-state toggle (All / Tasks / Thoughts):
- Use button group with active state highlighting
- Default: 'all'
2. Tag filter - multi-select from available tags:
- Re-use existing Svelecte pattern from TagInput.svelte
- Multiple selection enabled
- AND logic (handled by filtering logic in Plan 02)
3. Date range filter:
- Quick preset buttons: Today, This week, This month
- Custom range: two native date inputs (start/end)
- Use getDatePreset helper from search.ts
4. Clear/Reset button:
- Only visible when hasActiveFilters() returns true
- Resets all filters to defaultFilters
Props interface:
```typescript
interface Props {
filters: SearchFilters;
availableTags: Tag[];
onchange: (filters: SearchFilters) => void;
}
```
Layout:
- Horizontal flex wrap on desktop
- Stack vertically on mobile (use flex-wrap and gap)
- Each filter group visually separated
Styling notes:
- Match existing component patterns (border-gray-200, rounded-lg, focus:ring-2)
- Active type button: bg-blue-100 text-blue-700
- Inactive type button: bg-white text-gray-600 hover:bg-gray-50
</action>
<verify>Component renders with all filter controls visible and interactive</verify>
<done>FilterBar displays type toggle, tag selector, date range controls, and clear button (when filters active)</done>
</task>
</tasks>
<verification>
- `npm run check` passes (Svelte and TypeScript)
- SearchBar and FilterBar import without errors
- Type definitions are correct and exported
</verification>
<success_criteria>
- SearchBar.svelte renders with debounced text input
- FilterBar.svelte renders with all filter controls (type, tags, date range)
- Clear button appears only when filters are active
- All components follow existing project patterns
</success_criteria>
<output>
After completion, create `.planning/phases/05-search/05-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,206 @@
---
phase: 05-search
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/utils/filterEntries.ts
- src/lib/utils/highlightText.ts
autonomous: true
must_haves:
truths:
- "Entries can be filtered by text search (title + content)"
- "Entries can be filtered by tag (AND logic for multiple tags)"
- "Entries can be filtered by entry type (task/thought)"
- "Entries can be filtered by date range"
- "Matching text can be highlighted in search results"
artifacts:
- path: "src/lib/utils/filterEntries.ts"
provides: "Pure function to filter entries by SearchFilters"
exports: ["filterEntries"]
min_lines: 30
- path: "src/lib/utils/highlightText.ts"
provides: "Function to wrap matching text in mark tags"
exports: ["highlightText"]
min_lines: 15
key_links:
- from: "src/lib/utils/filterEntries.ts"
to: "SearchFilters type"
via: "import"
pattern: "import.*SearchFilters.*from.*search"
- from: "src/lib/utils/highlightText.ts"
to: "mark element"
via: "string replacement"
pattern: "<mark.*class=.*font-bold"
---
<objective>
Create the filtering logic and text highlighting utilities.
Purpose: Implement pure functions that filter entries based on SearchFilters criteria and highlight matching text. These are decoupled from UI components for testability.
Output: filterEntries.ts and highlightText.ts utilities ready for use in the integrated search view.
</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
@src/lib/server/db/schema.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Create filterEntries utility function</name>
<files>src/lib/utils/filterEntries.ts</files>
<action>
Create filterEntries.ts with a pure function that filters entries:
```typescript
import type { SearchFilters } from '$lib/types/search';
import type { Entry, Tag } from '$lib/server/db/schema';
interface EntryWithTags extends Entry {
tags: Tag[];
}
export function filterEntries(
entries: EntryWithTags[],
filters: SearchFilters
): EntryWithTags[] {
let result = entries;
// Text search (title + content) - minimum 2 characters
if (filters.query.length >= 2) {
const query = filters.query.toLowerCase();
result = result.filter(e =>
(e.title?.toLowerCase().includes(query)) ||
e.content.toLowerCase().includes(query)
);
}
// Tag filter - AND logic (must have ALL selected tags)
if (filters.tags.length > 0) {
result = result.filter(e =>
filters.tags.every(tagName =>
e.tags.some(t => t.name.toLowerCase() === tagName.toLowerCase())
)
);
}
// Type filter
if (filters.type !== 'all') {
result = result.filter(e => e.type === filters.type);
}
// Date range filter (using createdAt)
if (filters.dateRange.start) {
result = result.filter(e => e.createdAt >= filters.dateRange.start!);
}
if (filters.dateRange.end) {
// Include the end date by comparing to end of day
const endOfDay = filters.dateRange.end + 'T23:59:59';
result = result.filter(e => e.createdAt <= endOfDay);
}
return result;
}
```
Implementation notes:
- Case-insensitive text search (lowercase comparison)
- Case-insensitive tag matching (tags stored with original case but matched insensitively per 04-01 decision)
- AND logic for tags (every selected tag must be present)
- Date comparison works with ISO strings (schema uses ISO format)
- End date is inclusive (compare to end of day)
</action>
<verify>TypeScript compiles: `npx tsc --noEmit`</verify>
<done>filterEntries function filters entries by query, tags, type, and date range</done>
</task>
<task type="auto">
<name>Task 2: Create highlightText utility function</name>
<files>src/lib/utils/highlightText.ts</files>
<action>
Create highlightText.ts for search result highlighting:
```typescript
/**
* Wraps matching text in <mark> tags for highlighting.
* Returns HTML string to be used with {@html} in Svelte.
*
* @param text - The text to search within
* @param query - The search query to highlight
* @returns HTML string with matches wrapped in <mark> tags
*/
export function highlightText(text: string, query: string): string {
// Don't highlight if query is too short
if (!query || query.length < 2) return escapeHtml(text);
// Escape HTML in the original text first
const escaped = escapeHtml(text);
// Escape regex special characters in query
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
// Wrap matches in <mark> with bold styling (no background per CONTEXT.md)
return escaped.replace(regex, '<mark class="font-bold bg-transparent">$1</mark>');
}
/**
* Escapes HTML special characters to prevent XSS.
*/
function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, char => map[char]);
}
```
Security notes:
- Always escape HTML before highlighting to prevent XSS
- The escapeHtml function runs BEFORE regex replacement
- Result is safe to use with {@html} directive
Styling notes:
- Uses font-bold for emphasis (per CONTEXT.md: "bold, not background highlight")
- bg-transparent ensures no background color on <mark>
</action>
<verify>TypeScript compiles: `npx tsc --noEmit`</verify>
<done>highlightText function wraps matching text in styled mark tags with XSS protection</done>
</task>
</tasks>
<verification>
- `npm run check` passes
- filterEntries correctly filters by each criterion
- highlightText properly escapes HTML and highlights matches
</verification>
<success_criteria>
- filterEntries handles all filter types (query, tags, type, dateRange)
- Tag filtering uses AND logic (entry must have ALL selected tags)
- Text search is case-insensitive with 2-character minimum
- highlightText escapes HTML to prevent XSS
- Highlighted text uses bold styling without background
</success_criteria>
<output>
After completion, create `.planning/phases/05-search/05-02-SUMMARY.md`
</output>

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