From f6144f4edfbdb9ee1987ea24fc5f0898fd76d63c Mon Sep 17 00:00:00 2001 From: Thomas Richter Date: Sat, 31 Jan 2026 14:15:38 +0100 Subject: [PATCH] docs(05): create phase plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .planning/ROADMAP.md | 8 +- .planning/phases/05-search/05-01-PLAN.md | 221 +++++++++++++++++ .planning/phases/05-search/05-02-PLAN.md | 206 ++++++++++++++++ .planning/phases/05-search/05-03-PLAN.md | 300 +++++++++++++++++++++++ 4 files changed, 731 insertions(+), 4 deletions(-) create mode 100644 .planning/phases/05-search/05-01-PLAN.md create mode 100644 .planning/phases/05-search/05-02-PLAN.md create mode 100644 .planning/phases/05-search/05-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f57da9f..ef69014 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -105,12 +105,12 @@ Plans: 3. User can filter entries by date range 4. User can filter to show only tasks or only thoughts 5. Search results show relevant matches with highlighting -**Plans**: TBD +**Plans**: 3 plans Plans: -- [ ] 05-01: TBD -- [ ] 05-02: TBD -- [ ] 05-03: TBD +- [ ] 05-01-PLAN.md — SearchBar and FilterBar components with type definitions +- [ ] 05-02-PLAN.md — Filtering logic and text highlighting utilities +- [ ] 05-03-PLAN.md — Integration with recent searches and "/" keyboard shortcut ### Phase 6: Deployment **Goal**: Application runs in Docker with persistent data and easy configuration diff --git a/.planning/phases/05-search/05-01-PLAN.md b/.planning/phases/05-search/05-01-PLAN.md new file mode 100644 index 0000000..70d5f35 --- /dev/null +++ b/.planning/phases/05-search/05-01-PLAN.md @@ -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" +--- + + +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. + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Task 1: Create search types and utility functions + src/lib/types/search.ts + +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 + }; +} +``` + + TypeScript compiles without errors: `npx tsc --noEmit` + SearchFilters interface exported and importable from other files + + + + Task 2: Create SearchBar component with debounced input + src/lib/components/SearchBar.svelte + +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 + + Component renders without errors when imported + SearchBar accepts text input and exposes debounced value via $bindable prop + + + + Task 3: Create FilterBar component with type, tag, and date controls + src/lib/components/FilterBar.svelte + +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 + + Component renders with all filter controls visible and interactive + FilterBar displays type toggle, tag selector, date range controls, and clear button (when filters active) + + + + + +- `npm run check` passes (Svelte and TypeScript) +- SearchBar and FilterBar import without errors +- Type definitions are correct and exported + + + +- 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 + + + +After completion, create `.planning/phases/05-search/05-01-SUMMARY.md` + diff --git a/.planning/phases/05-search/05-02-PLAN.md b/.planning/phases/05-search/05-02-PLAN.md new file mode 100644 index 0000000..db80b28 --- /dev/null +++ b/.planning/phases/05-search/05-02-PLAN.md @@ -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: " +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. + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Task 1: Create filterEntries utility function + src/lib/utils/filterEntries.ts + +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) + + TypeScript compiles: `npx tsc --noEmit` + filterEntries function filters entries by query, tags, type, and date range + + + + Task 2: Create highlightText utility function + src/lib/utils/highlightText.ts + +Create highlightText.ts for search result highlighting: + +```typescript +/** + * Wraps matching text in 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 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 with bold styling (no background per CONTEXT.md) + return escaped.replace(regex, '$1'); +} + +/** + * Escapes HTML special characters to prevent XSS. + */ +function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + 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 + + TypeScript compiles: `npx tsc --noEmit` + highlightText function wraps matching text in styled mark tags with XSS protection + + + + + +- `npm run check` passes +- filterEntries correctly filters by each criterion +- highlightText properly escapes HTML and highlights matches + + + +- 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 + + + +After completion, create `.planning/phases/05-search/05-02-SUMMARY.md` + diff --git a/.planning/phases/05-search/05-03-PLAN.md b/.planning/phases/05-search/05-03-PLAN.md new file mode 100644 index 0000000..5d7cf86 --- /dev/null +++ b/.planning/phases/05-search/05-03-PLAN.md @@ -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" +--- + + +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. + + + +@/home/tho/.claude/get-shit-done/workflows/execute-plan.md +@/home/tho/.claude/get-shit-done/templates/summary.md + + + +@.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: + +```typescript +import { persisted } from 'svelte-persisted-store'; + +const MAX_RECENT = 5; + +export const recentSearches = persisted('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 + + 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: +```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} +
+

No entries match your search

+

Try adjusting your filters or search term

+
+ {:else} + + {/if} +{:else if isFiltering} + +
+ {#each filteredEntries as entry (entry.id)} + + {/each} +
+{:else} + + +{/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) + +```typescript +interface Props { + entry: EntryWithData; + availableTags: Tag[]; + searchQuery?: string; // NEW, optional with default '' +} + +let { entry, availableTags, searchQuery = '' }: Props = $props(); +``` + +For title (collapsed view): +```svelte +

+ {@html highlightText(entry.title || 'Untitled', searchQuery)} +

+``` + +For content preview (collapsed view): +```svelte +

+ {@html highlightText(entry.content, searchQuery)} +

+``` + +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 +
+
+
+

TaskPlaner

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