docs(02): research phase domain
Phase 02: Core CRUD - Standard stack identified (SvelteKit form actions, svelte-gestures) - Architecture patterns documented (progressive enhancement, inline editing) - Pitfalls catalogued (reactivity, debounce, mobile-first) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
605
.planning/phases/02-core-crud/02-RESEARCH.md
Normal file
605
.planning/phases/02-core-crud/02-RESEARCH.md
Normal file
@@ -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<string, unknown> = {};
|
||||
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
|
||||
<!-- +page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { data, form } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/create" use:enhance>
|
||||
<input name="title" placeholder="Title (optional)" />
|
||||
<textarea name="content" required></textarea>
|
||||
<select name="type">
|
||||
<option value="thought">Thought</option>
|
||||
<option value="task">Task</option>
|
||||
</select>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 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
|
||||
<script lang="ts">
|
||||
import type { Entry } from '$lib/server/db/schema';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
entry: Entry;
|
||||
onUpdate: (id: string, updates: Partial<Entry>) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
let { entry, onUpdate, onDelete }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
let editTitle = $state(entry.title || '');
|
||||
let editContent = $state(entry.content);
|
||||
|
||||
// Debounced auto-save
|
||||
let saveTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleInput() {
|
||||
clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(() => {
|
||||
onUpdate(entry.id, { title: editTitle, content: editContent });
|
||||
}, 400);
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="border-b border-gray-200">
|
||||
<button
|
||||
onclick={() => expanded = !expanded}
|
||||
class="w-full text-left p-4"
|
||||
>
|
||||
<h3 class="font-medium text-gray-900">{entry.title || 'Untitled'}</h3>
|
||||
{#if !expanded}
|
||||
<p class="text-gray-600 truncate">{entry.content}</p>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if expanded}
|
||||
<div transition:slide={{ duration: 200 }} class="px-4 pb-4">
|
||||
<input
|
||||
bind:value={editTitle}
|
||||
oninput={handleInput}
|
||||
class="w-full text-lg font-medium mb-2"
|
||||
placeholder="Title"
|
||||
/>
|
||||
<textarea
|
||||
bind:value={editContent}
|
||||
oninput={handleInput}
|
||||
class="w-full min-h-32"
|
||||
></textarea>
|
||||
<button onclick={() => onDelete(entry.id)} class="text-red-600">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
```
|
||||
|
||||
### 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
|
||||
<script lang="ts">
|
||||
import { preferences } from '$lib/stores/preferences.svelte';
|
||||
|
||||
// Use in quick capture
|
||||
let selectedType = $state($preferences.lastEntryType);
|
||||
|
||||
function handleSubmit() {
|
||||
// Update preference when creating entry
|
||||
$preferences.lastEntryType = selectedType;
|
||||
// ... submit form
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Pattern 4: Swipe-to-Delete with svelte-gestures
|
||||
|
||||
**What:** Mobile-friendly delete via horizontal swipe.
|
||||
**When to use:** Entry deletion on touch devices.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||
|
||||
let swipeOffset = $state(0);
|
||||
let isDeleting = $state(false);
|
||||
|
||||
function handleSwipe(event: SwipeCustomEvent) {
|
||||
if (event.detail.direction === 'left') {
|
||||
isDeleting = true;
|
||||
// Trigger delete after animation
|
||||
setTimeout(() => onDelete(entry.id), 300);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
{...useSwipe(handleSwipe, () => ({
|
||||
minSwipeDistance: 100,
|
||||
timeframe: 300,
|
||||
touchAction: 'pan-y'
|
||||
}))}
|
||||
class="relative overflow-hidden"
|
||||
style:transform="translateX({swipeOffset}px)"
|
||||
>
|
||||
<!-- Entry content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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<typeof setTimeout>;
|
||||
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
|
||||
<!-- BAD: sm:text-center only applies at 640px+ -->
|
||||
<div class="sm:text-center">
|
||||
|
||||
<!-- GOOD: center on mobile, left-align on larger -->
|
||||
<div class="text-center md:text-left">
|
||||
```
|
||||
|
||||
**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
|
||||
<!-- BAD: icon-only button -->
|
||||
<button class="p-1"><Icon /></button>
|
||||
|
||||
<!-- GOOD: adequate padding -->
|
||||
<button class="p-3 min-h-11 min-w-11"><Icon /></button>
|
||||
```
|
||||
|
||||
**Warning signs:** User complaints, high error rates on mobile.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Quick Capture Component
|
||||
|
||||
```svelte
|
||||
<!-- src/lib/components/QuickCapture.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { preferences } from '$lib/stores/preferences.svelte';
|
||||
|
||||
let title = $state('');
|
||||
let content = $state('');
|
||||
let type = $state($preferences.lastEntryType);
|
||||
|
||||
function handleSubmit() {
|
||||
$preferences.lastEntryType = type;
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/create"
|
||||
use:enhance={() => {
|
||||
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);"
|
||||
>
|
||||
<div class="max-w-2xl mx-auto flex flex-col gap-2">
|
||||
<input
|
||||
name="title"
|
||||
bind:value={title}
|
||||
placeholder="Title (optional)"
|
||||
class="w-full px-3 py-2 border rounded-lg text-base"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<textarea
|
||||
name="content"
|
||||
bind:value={content}
|
||||
placeholder="What's on your mind?"
|
||||
required
|
||||
class="flex-1 px-3 py-2 border rounded-lg text-base min-h-12 resize-none"
|
||||
></textarea>
|
||||
<div class="flex flex-col gap-1">
|
||||
<select
|
||||
name="type"
|
||||
bind:value={type}
|
||||
class="px-2 py-1 border rounded text-sm"
|
||||
>
|
||||
<option value="thought">Thought</option>
|
||||
<option value="task">Task</option>
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg min-h-11"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 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
|
||||
<!-- Compact on mobile, card on desktop -->
|
||||
<article class="
|
||||
p-4 border-b border-gray-100
|
||||
md:border md:rounded-lg md:shadow-sm md:mb-3
|
||||
">
|
||||
<div class="flex items-start gap-3">
|
||||
{#if entry.type === 'task'}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entry.status === 'done'}
|
||||
class="mt-1 w-5 h-5 rounded"
|
||||
/>
|
||||
{:else}
|
||||
<span class="mt-1 w-5 h-5 rounded-full bg-purple-100 flex items-center justify-center">
|
||||
<span class="text-purple-600 text-xs">T</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-medium text-gray-900 text-base md:text-lg">
|
||||
{entry.title || 'Untitled'}
|
||||
</h3>
|
||||
<p class="text-gray-600 text-sm md:text-base line-clamp-2">
|
||||
{entry.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
### 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)
|
||||
Reference in New Issue
Block a user