diff --git a/.planning/phases/02-core-crud/02-RESEARCH.md b/.planning/phases/02-core-crud/02-RESEARCH.md new file mode 100644 index 0000000..b441fcf --- /dev/null +++ b/.planning/phases/02-core-crud/02-RESEARCH.md @@ -0,0 +1,605 @@ +# Phase 2: Core CRUD - Research + +**Researched:** 2026-01-29 +**Domain:** SvelteKit CRUD UI, Mobile-First Design, Accessibility +**Confidence:** HIGH + +## Summary + +This phase builds a fully functional CRUD interface for entries (tasks and thoughts) on top of the existing foundation. The research covers SvelteKit form actions, Svelte 5 runes for reactive UI, mobile-first responsive design with Tailwind v4, swipe gestures for mobile delete, debounced auto-save, and accessibility requirements for touch targets and font sizes. + +The standard approach is to use SvelteKit form actions with progressive enhancement (`use:enhance`) for all CRUD operations, Svelte 5 runes (`$state`, `$derived`) for component state, and Tailwind v4's mobile-first breakpoints. Swipe gestures can be implemented with the `svelte-gestures` library (Svelte 5 compatible), and user preferences (like last-used entry type) should persist via `svelte-persisted-store` or a simple localStorage wrapper. + +**Primary recommendation:** Build progressively enhanced forms with `use:enhance`, use inline editing with debounced auto-save (300-500ms), implement swipe-to-delete with `svelte-gestures`, and ensure 44x44px minimum touch targets with 16px+ base font size. + +## Standard Stack + +### Core (Already Installed) + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| SvelteKit | ^2.50.1 | Full-stack framework | Form actions, SSR, routing | +| Svelte | ^5.48.2 | UI framework | Runes for reactivity | +| Tailwind CSS | ^4.1.18 | Styling | Mobile-first, utility classes | +| Drizzle ORM | ^0.45.1 | Database queries | Type-safe filters, orderBy | + +### Supporting (To Add) + +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| svelte-gestures | ^5.x | Touch gestures | Swipe-to-delete on mobile | +| svelte-persisted-store | latest | localStorage persistence | Sticky user preferences | + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| svelte-gestures | @svelte-put/swipeable | Less comprehensive but simpler for just swipe | +| svelte-persisted-store | Custom localStorage wrapper | More control but more code | +| use:enhance | fetch API | Progressive enhancement lost | + +**Installation:** +```bash +npm install svelte-gestures svelte-persisted-store +``` + +## Architecture Patterns + +### Recommended Project Structure + +``` +src/ +├── lib/ +│ ├── components/ +│ │ ├── EntryList.svelte # Entry list with ordering +│ │ ├── EntryCard.svelte # Single entry (expandable) +│ │ ├── QuickCapture.svelte # Bottom capture bar +│ │ └── CompletedToggle.svelte # Show/hide completed +│ ├── stores/ +│ │ └── preferences.svelte.ts # User preferences (last type, show completed) +│ └── server/ +│ └── db/ # Existing repository +├── routes/ +│ ├── +page.svelte # Main view +│ ├── +page.server.ts # Load + form actions +│ └── +layout.svelte # Global layout +└── app.css # Tailwind + base styles +``` + +### Pattern 1: SvelteKit Form Actions with Progressive Enhancement + +**What:** Server-side form handlers that work without JavaScript, enhanced with `use:enhance`. +**When to use:** All create, update, delete operations. + +```typescript +// +page.server.ts +import { fail } from '@sveltejs/kit'; +import { entryRepository } from '$lib/server/db/repository'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + const entries = entryRepository.getAll(); + return { entries }; +}; + +export const actions: Actions = { + create: async ({ request }) => { + const data = await request.formData(); + const title = data.get('title')?.toString() || ''; + const content = data.get('content')?.toString() || ''; + const type = data.get('type')?.toString() as 'task' | 'thought' || 'thought'; + + if (!content.trim()) { + return fail(400, { error: 'Content is required', title, type }); + } + + const entry = entryRepository.create({ title, content, type }); + return { success: true, entryId: entry.id }; + }, + + update: async ({ request }) => { + const data = await request.formData(); + const id = data.get('id')?.toString(); + if (!id) return fail(400, { error: 'Missing entry ID' }); + + const updates: Record = {}; + for (const [key, value] of data.entries()) { + if (key !== 'id') updates[key] = value; + } + + const entry = entryRepository.update(id, updates); + if (!entry) return fail(404, { error: 'Entry not found' }); + return { success: true }; + }, + + delete: async ({ request }) => { + const data = await request.formData(); + const id = data.get('id')?.toString(); + if (!id) return fail(400, { error: 'Missing entry ID' }); + + const deleted = entryRepository.delete(id); + if (!deleted) return fail(404, { error: 'Entry not found' }); + return { success: true }; + }, + + toggleComplete: async ({ request }) => { + const data = await request.formData(); + const id = data.get('id')?.toString(); + const currentStatus = data.get('status')?.toString(); + if (!id) return fail(400, { error: 'Missing entry ID' }); + + const newStatus = currentStatus === 'done' ? 'open' : 'done'; + entryRepository.update(id, { status: newStatus }); + return { success: true }; + } +}; +``` + +```svelte + + + +
+ + + + +
+``` + +### Pattern 2: Inline Expand with Svelte 5 Runes + +**What:** Clicking an entry expands it in place for editing. +**When to use:** Entry editing flow. + +```svelte + + +
+ + + {#if expanded} +
+ + + +
+ {/if} +
+``` + +### Pattern 3: User Preferences with Persisted Store + +**What:** Remember last-used entry type and completed filter state. +**When to use:** Sticky preferences across sessions. + +```typescript +// src/lib/stores/preferences.svelte.ts +import { persisted } from 'svelte-persisted-store'; + +export const preferences = persisted('taskplaner-preferences', { + lastEntryType: 'thought' as 'task' | 'thought', + showCompleted: false +}); +``` + +```svelte + +``` + +### Pattern 4: Swipe-to-Delete with svelte-gestures + +**What:** Mobile-friendly delete via horizontal swipe. +**When to use:** Entry deletion on touch devices. + +```svelte + + +
({ + minSwipeDistance: 100, + timeframe: 300, + touchAction: 'pan-y' + }))} + class="relative overflow-hidden" + style:transform="translateX({swipeOffset}px)" +> + +
+``` + +### Anti-Patterns to Avoid + +- **Direct database calls in components:** Always go through form actions or API routes. +- **Non-debounced auto-save:** Will flood the database with writes. +- **Pixel-based font sizes:** Use rem for accessibility (user can scale). +- **Small touch targets:** Under 44x44px causes usability issues on mobile. +- **Missing form progressive enhancement:** Forms should work without JS. + +## Don't Hand-Roll + +Problems that look simple but have existing solutions: + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Debounce function | Custom debounce | lodash.debounce or inline setTimeout | Edge cases with rapid input | +| Touch gestures | Pointer event handlers | svelte-gestures | Cross-browser/device complexity | +| localStorage sync | Custom wrapper | svelte-persisted-store | Tab sync, SSR safety, error handling | +| Form validation display | Custom error state | SvelteKit `form` prop | Built-in, progressive enhancement | +| CSS transitions | JS animations | Svelte transitions (slide, fade) | CSS-based, non-blocking | + +**Key insight:** Form handling and state persistence have many edge cases. Libraries handle SSR hydration, race conditions, and browser quirks that custom code misses. + +## Common Pitfalls + +### Pitfall 1: Breaking Reactivity with Destructuring + +**What goes wrong:** Destructuring reactive objects breaks the proxy. +**Why it happens:** Values are evaluated at destructure time, not read time. +**How to avoid:** Access properties directly or use `$derived`. + +```typescript +// BAD - loses reactivity +let { title, content } = entry; + +// GOOD - stays reactive +let title = $derived(entry.title); +let content = $derived(entry.content); +``` + +**Warning signs:** UI not updating when data changes. + +### Pitfall 2: Auto-Save Without Debounce + +**What goes wrong:** Every keystroke triggers a database write. +**Why it happens:** Input events fire on every character. +**How to avoid:** Always debounce auto-save (300-500ms recommended). + +```typescript +let timeout: ReturnType; +function debouncedSave(data: FormData) { + clearTimeout(timeout); + timeout = setTimeout(() => saveToServer(data), 400); +} +``` + +**Warning signs:** High CPU usage, sluggish UI, database locks. + +### Pitfall 3: Mobile-First Confusion + +**What goes wrong:** Styles only apply on larger screens, not mobile. +**Why it happens:** Using `sm:` prefix for mobile styles. +**How to avoid:** Unprefixed = mobile, breakpoint prefixes = larger screens. + +```html + +
+ + +
+``` + +**Warning signs:** Layout broken on mobile devices. + +### Pitfall 4: localStorage on Server + +**What goes wrong:** Server-side render fails with "localStorage is not defined". +**Why it happens:** SvelteKit renders on server first. +**How to avoid:** Use `browser` check or svelte-persisted-store. + +```typescript +import { browser } from '$app/environment'; + +const stored = browser ? localStorage.getItem('key') : null; +``` + +**Warning signs:** Hydration errors, SSR crashes. + +### Pitfall 5: Touch Targets Too Small + +**What goes wrong:** Users can't tap buttons reliably on mobile. +**Why it happens:** Desktop-sized click targets. +**How to avoid:** Minimum 44x44px (WCAG AAA) or at least 24x24px (AA). + +```html + + + + + +``` + +**Warning signs:** User complaints, high error rates on mobile. + +## Code Examples + +### Quick Capture Component + +```svelte + + + +
{ + return async ({ result, update }) => { + if (result.type === 'success') { + title = ''; + content = ''; + // Scroll to new entry, highlight it + } + await update(); + }; + }} + onsubmit={handleSubmit} + class="fixed bottom-0 left-0 right-0 bg-white border-t shadow-lg p-4" + style="padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem);" +> +
+ +
+ +
+ + +
+
+
+
+``` + +### Entry List with Ordering + +```typescript +// Extended repository method for phase 2 +// src/lib/server/db/repository.ts + +import { eq, desc, asc, and, ne } from 'drizzle-orm'; + +// Add to EntryRepository interface: +getOrdered(options?: { + showCompleted?: boolean; +}): Entry[] { + const conditions = options?.showCompleted + ? undefined + : ne(entries.status, 'done'); + + // Tasks first (type='task'), then thoughts, newest first within each group + return db.select() + .from(entries) + .where(conditions) + .orderBy( + // Tasks before thoughts (asc puts 'task' before 'thought' alphabetically) + asc(entries.type), + // Newest first within each type + desc(entries.createdAt) + ) + .all(); +} +``` + +### Mobile-Responsive Entry Card + +```svelte + +
+
+ {#if entry.type === 'task'} + + {:else} + + T + + {/if} + +
+

+ {entry.title || 'Untitled'} +

+

+ {entry.content} +

+
+
+
+``` + +### Base Font Size Configuration + +```css +/* src/app.css */ +@import 'tailwindcss'; + +/* Ensure minimum 16px base for accessibility */ +html { + font-size: 100%; /* 16px default, respects user browser settings */ +} + +/* All text uses rem, so user can scale */ +body { + font-size: 1rem; /* 16px minimum */ + line-height: 1.5; +} + +/* Safe area padding for bottom capture bar */ +.safe-bottom { + padding-bottom: env(safe-area-inset-bottom, 0px); +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Svelte stores for component state | `$state` rune | Svelte 5 (Oct 2024) | Simpler, more explicit reactivity | +| `$:` reactive statements | `$derived` rune | Svelte 5 | Computed values are memoized | +| `export let` for props | `$props()` rune | Svelte 5 | Better TypeScript inference | +| tailwind.config.js breakpoints | CSS `@theme` variables | Tailwind v4 | Configuration in CSS | +| `createEventDispatcher` | Callback props | Svelte 5 | Direct function passing | + +**Deprecated/outdated:** +- Svelte stores for local state: Use `$state` rune instead +- `$:` reactive declarations: Use `$derived` for computed values +- Tailwind v3 config files: v4 uses CSS-based `@theme` configuration + +## Open Questions + +1. **svelte-persisted-store Svelte 5 compatibility** + - What we know: Library is actively maintained (22 releases, TypeScript support) + - What's unclear: Explicit Svelte 5 runes compatibility not documented + - Recommendation: Test during implementation. Fallback: simple custom wrapper with `browser` check + +2. **Swipe gesture on desktop with mouse** + - What we know: svelte-gestures supports pointer events + - What's unclear: How swipe-to-delete should work with mouse drag + - Recommendation: Show delete button on hover for desktop, swipe for touch + +3. **Optimistic UI updates** + - What we know: `use:enhance` can return custom handlers + - What's unclear: Best pattern for optimistic delete with rollback + - Recommendation: Use simple approach first (wait for server), add optimistic if UX suffers + +## Sources + +### Primary (HIGH confidence) +- [SvelteKit Form Actions](https://svelte.dev/docs/kit/form-actions) - Official documentation +- [Svelte 5 $state Rune](https://svelte.dev/docs/svelte/$state) - Official documentation +- [Tailwind CSS v4 Responsive Design](https://tailwindcss.com/docs/responsive-design) - Official documentation +- [Drizzle ORM Operators](https://orm.drizzle.team/docs/operators) - Official documentation + +### Secondary (MEDIUM confidence) +- [svelte-gestures GitHub](https://github.com/Rezi/svelte-gestures) - Library documentation, Svelte 5 support confirmed +- [svelte-persisted-store](https://github.com/joshnuss/svelte-persisted-store) - Library documentation +- [WCAG 2.5.8 Target Size](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) - W3C specification + +### Tertiary (LOW confidence) +- [CSS env() Safe Area Insets](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/env) - MDN documentation +- Community patterns for debounce auto-save + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - Using existing dependencies, well-documented +- Architecture: HIGH - SvelteKit form actions are official pattern +- Pitfalls: HIGH - Common issues well-documented in community +- Gesture library: MEDIUM - Confirmed Svelte 5 support, less community usage data + +**Research date:** 2026-01-29 +**Valid until:** 2026-02-28 (30 days - stable technologies)