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:
@@ -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
|
||||||
|
|||||||
221
.planning/phases/05-search/05-01-PLAN.md
Normal file
221
.planning/phases/05-search/05-01-PLAN.md
Normal 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>
|
||||||
206
.planning/phases/05-search/05-02-PLAN.md
Normal file
206
.planning/phases/05-search/05-02-PLAN.md
Normal 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> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
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>
|
||||||
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