# 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)