docs(02): create phase plan

Phase 02: Core CRUD
- 4 plans in 4 waves
- 3 parallel-ready (Wave 1-3 sequential), 1 checkpoint
- Ready for execution
This commit is contained in:
Thomas Richter
2026-01-29 05:27:31 +01:00
parent 4036457391
commit 457cd407a8
5 changed files with 1263 additions and 5 deletions

View File

@@ -50,13 +50,13 @@ Plans:
6. Quick capture input is visible on main view with one-click submission
7. UI is usable on mobile devices with adequate touch targets
8. Text is readable for older eyes (minimum 16px base font)
**Plans**: TBD
**Plans**: 4 plans
Plans:
- [ ] 02-01: TBD
- [ ] 02-02: TBD
- [ ] 02-03: TBD
- [ ] 02-04: TBD
- [ ] 02-01-PLAN.md — Form actions for CRUD operations and accessible base styling
- [ ] 02-02-PLAN.md — Entry list, entry cards, and quick capture components
- [ ] 02-03-PLAN.md — Inline editing with expand/collapse, auto-save, and completed toggle
- [ ] 02-04-PLAN.md — Swipe-to-delete gesture and mobile UX verification
### Phase 3: Images
**Goal**: Users can attach, view, and manage images on entries from any device

View File

@@ -0,0 +1,218 @@
---
phase: 02-core-crud
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/routes/+page.server.ts
- src/lib/server/db/repository.ts
- src/app.css
autonomous: true
must_haves:
truths:
- "Form actions accept POST requests for create, update, delete, toggleComplete"
- "Repository has getOrdered method returning tasks-first, newest-within-type"
- "Base font size is 16px (1rem) respecting browser settings"
artifacts:
- path: "src/routes/+page.server.ts"
provides: "SvelteKit form actions for CRUD operations"
exports: ["actions", "load"]
- path: "src/lib/server/db/repository.ts"
provides: "Extended repository with getOrdered method"
contains: "getOrdered"
- path: "src/app.css"
provides: "Base styling for accessibility"
contains: "font-size: 100%"
key_links:
- from: "src/routes/+page.server.ts"
to: "src/lib/server/db/repository.ts"
via: "entryRepository import"
pattern: "import.*entryRepository.*from.*repository"
---
<objective>
Create SvelteKit form actions for all CRUD operations and establish base styling for accessibility.
Purpose: Provides the server-side API that all UI components will use, ensuring progressive enhancement (forms work without JavaScript) and establishing accessible typography.
Output: Form actions (create, update, delete, toggleComplete), extended repository with ordering, base CSS with 16px minimum font.
</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/02-core-crud/02-CONTEXT.md
@.planning/phases/02-core-crud/02-RESEARCH.md
@src/lib/server/db/repository.ts
@src/lib/server/db/schema.ts
@src/routes/+page.server.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend repository with getOrdered method</name>
<files>src/lib/server/db/repository.ts</files>
<action>
Add a `getOrdered` method to EntryRepository interface and SQLiteEntryRepository implementation:
```typescript
getOrdered(options?: { showCompleted?: boolean }): Entry[]
```
Behavior:
- If showCompleted is false (default), exclude entries where status='done'
- Order by: type ASC (task before thought alphabetically), then createdAt DESC (newest first)
- Use drizzle-orm operators: `asc`, `ne` (import from drizzle-orm)
Implementation pattern from research:
```typescript
import { eq, desc, asc, ne } from 'drizzle-orm';
getOrdered(options: { showCompleted?: boolean } = {}): Entry[] {
const { showCompleted = false } = options;
let query = db.select().from(entries);
if (!showCompleted) {
query = query.where(ne(entries.status, 'done'));
}
return query
.orderBy(asc(entries.type), desc(entries.createdAt))
.all();
}
```
</action>
<verify>
Run TypeScript check: `npm run check` passes without errors.
Test manually: Start dev server, database should still work with existing getAll method.
</verify>
<done>Repository has getOrdered method that filters completed entries and orders tasks-first, newest-within-type</done>
</task>
<task type="auto">
<name>Task 2: Create form actions for CRUD operations</name>
<files>src/routes/+page.server.ts</files>
<action>
Replace current page.server.ts with full CRUD form actions. Keep the existing load function but modify to use getOrdered.
Actions to implement:
1. **create**: Accept title (optional), content (required), type (task|thought, default thought)
- Validate content not empty
- Return { success: true, entryId } on success
- Return fail(400, { error, title, content, type }) on validation failure
2. **update**: Accept id (required), plus any of: title, content, type, status
- Return fail(400) if no id
- Return fail(404) if entry not found
- Return { success: true } on success
3. **delete**: Accept id (required)
- Return fail(400) if no id
- Return fail(404) if entry not found
- Return { success: true } on success
4. **toggleComplete**: Accept id (required)
- Toggle status between 'open' and 'done'
- Return fail(400) if no id
- Return { success: true } on success
Import `fail` from '@sveltejs/kit'.
Import `entryRepository` from '$lib/server/db/repository'.
Update load function:
- Use getOrdered() instead of getAll()
- Pass showCompleted: false for now (will be dynamic later)
</action>
<verify>
Run `npm run check` - TypeScript passes.
Test with curl:
```bash
# Create
curl -X POST http://localhost:5173/?/create -d "content=Test entry&type=task"
# Should return redirect (SvelteKit form action behavior)
```
</verify>
<done>All four form actions (create, update, delete, toggleComplete) exist and handle validation/errors correctly</done>
</task>
<task type="auto">
<name>Task 3: Establish accessible base styling</name>
<files>src/app.css</files>
<action>
Update app.css to establish accessible base typography:
```css
@import 'tailwindcss';
/* Ensure minimum 16px base for accessibility (UX-02) */
html {
font-size: 100%; /* 16px default, respects user browser settings */
}
body {
font-size: 1rem; /* 16px minimum */
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Safe area padding for mobile devices with notches */
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* Utility for adequate touch targets (44x44px minimum per WCAG) */
.touch-target {
min-height: 44px;
min-width: 44px;
}
```
This ensures:
- Base font respects user's browser zoom settings (100% = 16px default)
- All text uses rem, so user scaling works
- Mobile safe areas handled for bottom capture bar
- Touch target utility class available
</action>
<verify>
Start dev server: `npm run dev`
Open in browser, inspect html element - font-size should be 100%.
Inspect body - font-size should be 1rem (16px computed).
</verify>
<done>Base CSS establishes 16px minimum font, safe area insets, and touch target utility</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. **TypeScript**: `npm run check` passes
2. **Dev server**: `npm run dev` starts without errors
3. **Repository**: getOrdered method exists and returns entries in correct order
4. **Form actions**: All four actions exist in +page.server.ts
5. **CSS**: Base font is 16px (1rem), safe-bottom and touch-target classes defined
</verification>
<success_criteria>
- Repository has getOrdered(options?) method with showCompleted filter and tasks-first ordering
- Form actions handle create (with validation), update, delete, and toggleComplete
- Base CSS ensures 16px minimum font and provides mobile-friendly utilities
- TypeScript compilation passes
- All requirements groundwork laid: CORE-01 through CORE-06, UX-02
</success_criteria>
<output>
After completion, create `.planning/phases/02-core-crud/02-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,368 @@
---
phase: 02-core-crud
plan: 02
type: execute
wave: 2
depends_on: [02-01]
files_modified:
- src/lib/components/EntryList.svelte
- src/lib/components/EntryCard.svelte
- src/lib/components/QuickCapture.svelte
- src/lib/stores/preferences.svelte.ts
- src/routes/+page.svelte
- package.json
autonomous: true
must_haves:
truths:
- "User sees a list of entries on the main page"
- "User can type in quick capture bar and create an entry"
- "Entries show type indicator (task vs thought)"
- "Quick capture bar is fixed at bottom of screen"
artifacts:
- path: "src/lib/components/EntryList.svelte"
provides: "Entry list rendering with ordering"
min_lines: 20
- path: "src/lib/components/EntryCard.svelte"
provides: "Single entry display with type indicator"
min_lines: 30
- path: "src/lib/components/QuickCapture.svelte"
provides: "Bottom capture bar with form"
min_lines: 40
- path: "src/lib/stores/preferences.svelte.ts"
provides: "Persisted user preferences store"
contains: "lastEntryType"
- path: "src/routes/+page.svelte"
provides: "Main page composing components"
contains: "EntryList"
key_links:
- from: "src/routes/+page.svelte"
to: "src/lib/components/EntryList.svelte"
via: "component import"
pattern: "import EntryList"
- from: "src/lib/components/QuickCapture.svelte"
to: "src/routes/+page.server.ts"
via: "form action ?/create"
pattern: "action=.*\\?/create"
---
<objective>
Build the main UI components: entry list, entry cards, and quick capture bar.
Purpose: Users can see their entries and create new ones via the quick capture bar fixed at the bottom of the screen.
Output: EntryList, EntryCard, QuickCapture components plus preferences store. Main page renders all entries with quick capture.
</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/02-core-crud/02-CONTEXT.md
@.planning/phases/02-core-crud/02-RESEARCH.md
@.planning/phases/02-core-crud/02-01-SUMMARY.md
@src/routes/+page.server.ts
@src/lib/server/db/schema.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Install dependencies and create preferences store</name>
<files>package.json, src/lib/stores/preferences.svelte.ts</files>
<action>
Install svelte-persisted-store for sticky preferences:
```bash
npm install svelte-persisted-store
```
Create preferences store at src/lib/stores/preferences.svelte.ts:
```typescript
import { persisted } from 'svelte-persisted-store';
export const preferences = persisted('taskplaner-preferences', {
lastEntryType: 'thought' as 'task' | 'thought',
showCompleted: false
});
```
This store:
- Persists to localStorage automatically
- Remembers last used entry type for quick capture
- Tracks show/hide completed preference
Create the stores directory if it doesn't exist.
</action>
<verify>
npm install completes without errors.
TypeScript check: `npm run check` passes.
</verify>
<done>svelte-persisted-store installed, preferences store created with lastEntryType and showCompleted</done>
</task>
<task type="auto">
<name>Task 2: Create EntryCard and EntryList components</name>
<files>src/lib/components/EntryCard.svelte, src/lib/components/EntryList.svelte</files>
<action>
Create src/lib/components directory if needed.
**EntryCard.svelte** - Single entry display:
```svelte
<script lang="ts">
import type { Entry } from '$lib/server/db/schema';
interface Props {
entry: Entry;
}
let { entry }: Props = $props();
</script>
<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'}
<form method="POST" action="?/toggleComplete" class="flex items-center">
<input type="hidden" name="id" value={entry.id} />
<button
type="submit"
class="w-6 h-6 rounded border-2 border-gray-300 flex items-center justify-center touch-target
{entry.status === 'done' ? 'bg-green-500 border-green-500' : 'hover:border-gray-400'}"
aria-label={entry.status === 'done' ? 'Mark as incomplete' : 'Mark as complete'}
>
{#if entry.status === 'done'}
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{/if}
</button>
</form>
{:else}
<span class="w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
<span class="text-purple-600 text-xs font-medium">T</span>
</span>
{/if}
<div class="flex-1 min-w-0">
<h3 class="font-medium text-gray-900 text-base md:text-lg {entry.status === 'done' ? 'line-through text-gray-400' : ''}">
{entry.title || 'Untitled'}
</h3>
<p class="text-gray-600 text-sm md:text-base line-clamp-2 {entry.status === 'done' ? 'text-gray-400' : ''}">
{entry.content}
</p>
</div>
<span class="text-xs px-2 py-1 rounded-full {entry.type === 'task' ? 'bg-blue-100 text-blue-700' : 'bg-purple-100 text-purple-700'}">
{entry.type}
</span>
</div>
</article>
```
**EntryList.svelte** - List of entries:
```svelte
<script lang="ts">
import type { Entry } from '$lib/server/db/schema';
import EntryCard from './EntryCard.svelte';
interface Props {
entries: Entry[];
}
let { entries }: Props = $props();
</script>
{#if entries.length === 0}
<div class="text-center py-12 text-gray-500">
<p class="text-lg">No entries yet</p>
<p class="text-sm mt-1">Use the capture bar below to add your first entry</p>
</div>
{:else}
<div class="divide-y divide-gray-100 md:divide-y-0 md:space-y-3">
{#each entries as entry (entry.id)}
<EntryCard {entry} />
{/each}
</div>
{/if}
```
Key features:
- Type indicator: checkbox for tasks, purple "T" badge for thoughts
- Type badge in corner (blue for task, purple for thought)
- Completed tasks have strikethrough
- Mobile compact (border-b), desktop cards (rounded, shadow)
- Touch-friendly checkbox (44px touch target via touch-target class)
</action>
<verify>
TypeScript check: `npm run check` passes.
Files exist: src/lib/components/EntryCard.svelte, src/lib/components/EntryList.svelte
</verify>
<done>EntryCard shows entry with type indicator and completion toggle. EntryList renders all entries.</done>
</task>
<task type="auto">
<name>Task 3: Create QuickCapture component and integrate into main page</name>
<files>src/lib/components/QuickCapture.svelte, src/routes/+page.svelte</files>
<action>
**QuickCapture.svelte** - Bottom capture bar:
```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<'task' | 'thought'>('thought');
// Initialize from preferences (client-side only)
$effect(() => {
if (typeof window !== 'undefined') {
type = $preferences.lastEntryType;
}
});
</script>
<form
method="POST"
action="?/create"
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'success') {
// Update preference
$preferences.lastEntryType = type;
// Clear form
title = '';
content = '';
// Let SvelteKit refresh data
}
await update();
};
}}
class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg safe-bottom"
>
<div class="max-w-2xl mx-auto p-4">
<div class="flex flex-col gap-2">
<input
name="title"
bind:value={title}
placeholder="Title (optional)"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<div class="flex gap-2">
<textarea
name="content"
bind:value={content}
placeholder="What's on your mind?"
required
rows="1"
class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-base resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
<div class="flex flex-col gap-1">
<select
name="type"
bind:value={type}
class="px-2 py-1 border border-gray-200 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<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 touch-target font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Add
</button>
</div>
</div>
</div>
</div>
</form>
```
**+page.svelte** - Main page:
```svelte
<script lang="ts">
import EntryList from '$lib/components/EntryList.svelte';
import QuickCapture from '$lib/components/QuickCapture.svelte';
let { data } = $props();
</script>
<svelte:head>
<title>TaskPlaner</title>
</svelte:head>
<main class="min-h-screen bg-gray-50 pb-40">
<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">
<h1 class="text-xl font-bold text-gray-900">TaskPlaner</h1>
</div>
</header>
<div class="max-w-2xl mx-auto px-4 py-4">
<EntryList entries={data.entries} />
</div>
<QuickCapture />
</main>
```
Key features:
- Fixed bottom capture bar with safe area insets
- Type selector remembers last used type
- Clear form on successful submit
- Adequate padding at bottom (pb-40) so entries aren't hidden by capture bar
- Progressive enhancement: works without JS, enhanced with use:enhance
</action>
<verify>
Start dev server: `npm run dev`
Open http://localhost:5173
- Entry list should display (may be empty)
- Quick capture bar should be fixed at bottom
- Creating an entry should work and appear in list
- Type preference should stick across page refreshes
</verify>
<done>Quick capture bar creates entries. Main page shows entry list with proper bottom padding. Type preference persists.</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. **TypeScript**: `npm run check` passes
2. **Dev server**: `npm run dev` starts without errors
3. **UI visible**: Entry list renders, quick capture bar at bottom
4. **Create entry**: Fill in content, click Add - entry appears in list
5. **Type indicator**: Tasks show checkbox, thoughts show purple badge
6. **Task toggle**: Clicking checkbox marks task complete (strikethrough)
7. **Type preference**: Select "task", create entry, refresh page - type selector still shows "task"
Requirements satisfied:
- CORE-01: Can create entry ✓
- CORE-04: Can specify type ✓
- CORE-05: Can mark task complete ✓
- CAPT-01, CAPT-02, CAPT-03: Quick capture works ✓
</verification>
<success_criteria>
- EntryList and EntryCard components render entries with type indicators
- QuickCapture component creates entries via form action
- Type preference persists in localStorage
- Main page composes all components with proper layout
- TypeScript compilation passes
</success_criteria>
<output>
After completion, create `.planning/phases/02-core-crud/02-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,417 @@
---
phase: 02-core-crud
plan: 03
type: execute
wave: 3
depends_on: [02-02]
files_modified:
- src/lib/components/EntryCard.svelte
- src/lib/components/CompletedToggle.svelte
- src/routes/+page.svelte
- src/routes/+page.server.ts
autonomous: true
must_haves:
truths:
- "Clicking an entry expands it to show edit fields"
- "Editing title or content auto-saves after typing stops"
- "User can toggle show/hide completed tasks"
- "User can change entry type after creation"
artifacts:
- path: "src/lib/components/EntryCard.svelte"
provides: "Expandable entry with inline editing"
contains: "expanded"
- path: "src/lib/components/CompletedToggle.svelte"
provides: "Toggle for showing completed tasks"
min_lines: 15
- path: "src/routes/+page.svelte"
provides: "Main page with completed toggle"
contains: "CompletedToggle"
key_links:
- from: "src/lib/components/EntryCard.svelte"
to: "src/routes/+page.server.ts"
via: "form action ?/update"
pattern: "action=.*\\?/update"
---
<objective>
Implement inline editing with expand/collapse and auto-save, plus completed tasks toggle.
Purpose: Users can click an entry to expand and edit it in place. Changes save automatically as they type (debounced). Completed tasks can be shown/hidden.
Output: Expandable EntryCard with auto-save, CompletedToggle component, updated page with toggle 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/02-core-crud/02-CONTEXT.md
@.planning/phases/02-core-crud/02-RESEARCH.md
@.planning/phases/02-core-crud/02-02-SUMMARY.md
@src/lib/components/EntryCard.svelte
@src/routes/+page.svelte
@src/routes/+page.server.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Add expand/collapse and inline editing to EntryCard</name>
<files>src/lib/components/EntryCard.svelte</files>
<action>
Update EntryCard to support expand/collapse with inline editing and debounced auto-save.
Key changes:
1. Add `expanded` state variable
2. When collapsed, show title + truncated content (current behavior)
3. When expanded, show editable fields: title input, content textarea, type selector
4. Auto-save changes with 400ms debounce using fetch to ?/update
5. Use Svelte's slide transition for smooth expand/collapse
```svelte
<script lang="ts">
import type { Entry } from '$lib/server/db/schema';
import { enhance } from '$app/forms';
import { slide } from 'svelte/transition';
interface Props {
entry: Entry;
}
let { entry }: Props = $props();
// Expand/collapse state
let expanded = $state(false);
// Edit state - initialize from entry
let editTitle = $state(entry.title || '');
let editContent = $state(entry.content);
let editType = $state(entry.type);
// Debounced auto-save
let saveTimeout: ReturnType<typeof setTimeout>;
let isSaving = $state(false);
async function debouncedSave() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
isSaving = true;
const formData = new FormData();
formData.append('id', entry.id);
formData.append('title', editTitle);
formData.append('content', editContent);
formData.append('type', editType);
try {
await fetch('?/update', {
method: 'POST',
body: formData
});
} finally {
isSaving = false;
}
}, 400);
}
function handleInput() {
debouncedSave();
}
function toggleExpand() {
if (expanded) {
// Collapsing - reset edit state to current entry values
editTitle = entry.title || '';
editContent = entry.content;
editType = entry.type;
}
expanded = !expanded;
}
</script>
<article class="p-4 border-b border-gray-100 md:border md:rounded-lg md:shadow-sm md:mb-3 bg-white">
<!-- Collapsed view - clickable header -->
<button
type="button"
onclick={toggleExpand}
class="w-full text-left flex items-start gap-3"
>
{#if entry.type === 'task'}
<form method="POST" action="?/toggleComplete" use:enhance onclick={(e) => e.stopPropagation()}>
<input type="hidden" name="id" value={entry.id} />
<button
type="submit"
class="w-6 h-6 rounded border-2 border-gray-300 flex items-center justify-center touch-target
{entry.status === 'done' ? 'bg-green-500 border-green-500' : 'hover:border-gray-400'}"
aria-label={entry.status === 'done' ? 'Mark as incomplete' : 'Mark as complete'}
>
{#if entry.status === 'done'}
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{/if}
</button>
</form>
{:else}
<span class="w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
<span class="text-purple-600 text-xs font-medium">T</span>
</span>
{/if}
<div class="flex-1 min-w-0">
<h3 class="font-medium text-gray-900 text-base md:text-lg {entry.status === 'done' ? 'line-through text-gray-400' : ''}">
{entry.title || 'Untitled'}
</h3>
{#if !expanded}
<p class="text-gray-600 text-sm md:text-base line-clamp-2 {entry.status === 'done' ? 'text-gray-400' : ''}">
{entry.content}
</p>
{/if}
</div>
<div class="flex items-center gap-2">
{#if isSaving}
<span class="text-xs text-gray-400">Saving...</span>
{/if}
<span class="text-xs px-2 py-1 rounded-full {entry.type === 'task' ? 'bg-blue-100 text-blue-700' : 'bg-purple-100 text-purple-700'}">
{entry.type}
</span>
<svg
class="w-5 h-5 text-gray-400 transform transition-transform {expanded ? 'rotate-180' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
<!-- Expanded view - edit fields -->
{#if expanded}
<div transition:slide={{ duration: 200 }} class="mt-4 pl-9 space-y-3">
<div>
<label for="edit-title-{entry.id}" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input
id="edit-title-{entry.id}"
bind:value={editTitle}
oninput={handleInput}
placeholder="Add a title..."
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label for="edit-content-{entry.id}" class="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
id="edit-content-{entry.id}"
bind:value={editContent}
oninput={handleInput}
rows="4"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-base resize-y focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
</div>
<div class="flex items-center justify-between">
<div>
<label for="edit-type-{entry.id}" class="block text-sm font-medium text-gray-700 mb-1">Type</label>
<select
id="edit-type-{entry.id}"
bind:value={editType}
onchange={handleInput}
class="px-3 py-2 border border-gray-200 rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="thought">Thought</option>
<option value="task">Task</option>
</select>
</div>
<form method="POST" action="?/delete" use:enhance>
<input type="hidden" name="id" value={entry.id} />
<button
type="submit"
class="px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg touch-target"
onclick={(e) => {
if (!confirm('Delete this entry?')) {
e.preventDefault();
}
}}
>
Delete
</button>
</form>
</div>
</div>
{/if}
</article>
```
Features:
- Click anywhere on header to expand/collapse
- Checkbox click doesn't trigger expand (stopPropagation)
- Smooth slide transition
- 400ms debounce on input changes
- "Saving..." indicator during save
- Delete with confirmation dialog
- Type can be changed after creation
</action>
<verify>
Start dev server: `npm run dev`
- Click an entry - should expand
- Edit title or content - should see "Saving..." briefly
- Click again - should collapse
- Change type - should save
- Delete - should prompt for confirmation
</verify>
<done>EntryCard expands/collapses with inline editing and 400ms debounced auto-save</done>
</task>
<task type="auto">
<name>Task 2: Create CompletedToggle component and wire up showCompleted</name>
<files>src/lib/components/CompletedToggle.svelte, src/routes/+page.svelte, src/routes/+page.server.ts</files>
<action>
**CompletedToggle.svelte** - Toggle for showing/hiding completed tasks:
```svelte
<script lang="ts">
import { preferences } from '$lib/stores/preferences.svelte';
let showCompleted = $state(false);
// Initialize from preferences (client-side only)
$effect(() => {
if (typeof window !== 'undefined') {
showCompleted = $preferences.showCompleted;
}
});
function handleChange(e: Event) {
const target = e.target as HTMLInputElement;
showCompleted = target.checked;
$preferences.showCompleted = showCompleted;
}
</script>
<label class="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input
type="checkbox"
checked={showCompleted}
onchange={handleChange}
class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
Show completed
</label>
```
**Update +page.server.ts** - Accept showCompleted parameter:
Modify the load function to accept a URL search param:
```typescript
export const load: PageServerLoad = async ({ url }) => {
const showCompleted = url.searchParams.get('showCompleted') === 'true';
const entries = entryRepository.getOrdered({ showCompleted });
return { entries, showCompleted };
};
```
**Update +page.svelte** - Add toggle and use invalidateAll for refresh:
```svelte
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/state';
import EntryList from '$lib/components/EntryList.svelte';
import QuickCapture from '$lib/components/QuickCapture.svelte';
import CompletedToggle from '$lib/components/CompletedToggle.svelte';
import { preferences } from '$lib/stores/preferences.svelte';
let { data } = $props();
// Sync URL with preference on mount
$effect(() => {
if (typeof window !== 'undefined') {
const urlShowCompleted = $page.url.searchParams.get('showCompleted') === 'true';
const prefShowCompleted = $preferences.showCompleted;
if (urlShowCompleted !== prefShowCompleted) {
const url = new URL($page.url);
if (prefShowCompleted) {
url.searchParams.set('showCompleted', 'true');
} else {
url.searchParams.delete('showCompleted');
}
goto(url.toString(), { replaceState: true, invalidateAll: true });
}
}
});
</script>
<svelte:head>
<title>TaskPlaner</title>
</svelte:head>
<main class="min-h-screen bg-gray-50 pb-40">
<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 flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900">TaskPlaner</h1>
<CompletedToggle />
</div>
</header>
<div class="max-w-2xl mx-auto px-4 py-4">
<EntryList entries={data.entries} />
</div>
<QuickCapture />
</main>
```
Note: The preference syncs with URL so the server can filter correctly. When toggle changes, URL updates and data reloads.
</action>
<verify>
Start dev server: `npm run dev`
- Create a task, mark it complete
- Should disappear from list
- Check "Show completed" - completed task should appear
- Refresh page - toggle state should persist
</verify>
<done>CompletedToggle shows/hides completed tasks, state persists in preferences and URL</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. **TypeScript**: `npm run check` passes
2. **Expand/collapse**: Click entry to expand, click again to collapse
3. **Inline editing**: Title, content, type can be edited in expanded view
4. **Auto-save**: Editing shows "Saving..." then saves (no need to click save)
5. **Delete**: Delete button in expanded view with confirmation
6. **Completed toggle**: Checkbox in header shows/hides completed tasks
7. **Persistence**: Toggle state persists across page refreshes
Requirements satisfied:
- CORE-02: Edit entry ✓
- CORE-03: Delete entry ✓ (with confirmation)
- CORE-06: Add notes (append to content) ✓
</verification>
<success_criteria>
- EntryCard expands on click to show edit fields
- Changes auto-save with 400ms debounce
- Delete button with confirmation dialog
- CompletedToggle shows/hides completed tasks
- Toggle state persists in localStorage
- TypeScript compilation passes
</success_criteria>
<output>
After completion, create `.planning/phases/02-core-crud/02-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,255 @@
---
phase: 02-core-crud
plan: 04
type: execute
wave: 4
depends_on: [02-03]
files_modified:
- src/lib/components/EntryCard.svelte
- package.json
autonomous: false
must_haves:
truths:
- "User can swipe left on mobile to delete an entry"
- "Touch targets are at least 44x44px"
- "UI is usable on mobile devices (tested)"
- "Delete via swipe shows confirmation before deleting"
artifacts:
- path: "src/lib/components/EntryCard.svelte"
provides: "Swipe-to-delete functionality"
contains: "svelte-gestures"
- path: "package.json"
provides: "svelte-gestures dependency"
contains: "svelte-gestures"
key_links:
- from: "src/lib/components/EntryCard.svelte"
to: "src/routes/+page.server.ts"
via: "form action ?/delete"
pattern: "action=.*\\?/delete"
---
<objective>
Implement swipe-to-delete gesture for mobile and verify overall mobile UX.
Purpose: Mobile users can swipe left to delete entries (common mobile pattern). Final verification ensures all touch targets meet WCAG guidelines and UI works well on mobile.
Output: Swipe-to-delete on EntryCard, verified mobile-friendly UI.
</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/02-core-crud/02-CONTEXT.md
@.planning/phases/02-core-crud/02-RESEARCH.md
@.planning/phases/02-core-crud/02-03-SUMMARY.md
@src/lib/components/EntryCard.svelte
</context>
<tasks>
<task type="auto">
<name>Task 1: Install svelte-gestures and implement swipe-to-delete</name>
<files>package.json, src/lib/components/EntryCard.svelte</files>
<action>
Install svelte-gestures:
```bash
npm install svelte-gestures
```
Update EntryCard.svelte to add swipe-to-delete functionality:
1. Import the swipe action from svelte-gestures
2. Wrap the entry in a swipeable container
3. On left swipe, show confirmation then delete
4. Add visual feedback (red background revealing during swipe)
5. On desktop, keep the delete button in expanded view (swipe is touch-only)
Add to the script section:
```typescript
import { swipe } from 'svelte-gestures';
// Swipe state
let swipeOffset = $state(0);
let isConfirmingDelete = $state(false);
function handleSwipe(event: CustomEvent) {
const { direction } = event.detail;
if (direction === 'left') {
// Show confirmation
isConfirmingDelete = true;
}
}
function handleSwipeMove(event: CustomEvent) {
// Visual feedback during swipe
const { offsetX } = event.detail;
if (offsetX < 0) {
swipeOffset = Math.max(offsetX, -100); // Cap at -100px
}
}
function handleSwipeEnd() {
if (!isConfirmingDelete) {
swipeOffset = 0;
}
}
function cancelDelete() {
isConfirmingDelete = false;
swipeOffset = 0;
}
async function confirmDelete() {
const formData = new FormData();
formData.append('id', entry.id);
await fetch('?/delete', {
method: 'POST',
body: formData
});
// Refresh page to show updated list
window.location.reload();
}
```
Update the template to wrap in a swipeable container:
```svelte
<div class="relative overflow-hidden">
<!-- Delete background revealed during swipe -->
<div class="absolute inset-y-0 right-0 w-24 bg-red-500 flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
<!-- Swipeable entry card -->
<article
use:swipe={{ timeframe: 300, minSwipeDistance: 60, touchAction: 'pan-y' }}
onswipe={handleSwipe}
style="transform: translateX({swipeOffset}px); transition: {swipeOffset === 0 ? 'transform 0.2s' : 'none'}"
class="p-4 border-b border-gray-100 md:border md:rounded-lg md:shadow-sm md:mb-3 bg-white relative"
>
<!-- ... existing entry content ... -->
</article>
<!-- Delete confirmation overlay -->
{#if isConfirmingDelete}
<div
class="absolute inset-0 bg-white/95 flex items-center justify-center gap-4 z-10"
transition:slide={{ duration: 150 }}
>
<button
onclick={cancelDelete}
class="px-4 py-2 text-gray-600 border border-gray-300 rounded-lg touch-target"
>
Cancel
</button>
<button
onclick={confirmDelete}
class="px-4 py-2 bg-red-600 text-white rounded-lg touch-target"
>
Delete
</button>
</div>
{/if}
</div>
```
Key behaviors:
- Swipe left reveals red delete icon
- If swipe distance > 60px, show confirmation overlay
- Confirmation has Cancel and Delete buttons
- On desktop, swipe doesn't trigger (touch-only)
- Delete button in expanded view still works for desktop users
</action>
<verify>
npm install completes.
TypeScript check: `npm run check` passes.
Start dev server: `npm run dev`
Open in browser dev tools mobile view:
- Swipe left on an entry
- Red background should reveal
- Release after 60px+ swipe shows confirmation
- Cancel returns to normal, Delete removes entry
</verify>
<done>Swipe-to-delete works on mobile with confirmation overlay</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Complete Phase 2 UI with:
- Entry list showing tasks and thoughts
- Quick capture bar at bottom
- Inline editing with auto-save
- Swipe-to-delete on mobile
- Show/hide completed toggle
- Mobile-responsive layout
</what-built>
<how-to-verify>
1. **Desktop verification** (http://localhost:5173):
- Create a task via quick capture - should appear in list
- Create a thought - should appear with purple indicator
- Click an entry - should expand with edit fields
- Edit title/content - should auto-save (see "Saving...")
- Mark a task complete - checkbox should fill, task may disappear
- Check "Show completed" - completed task reappears
- Delete an entry via expanded view - should prompt then remove
2. **Mobile verification** (use browser dev tools mobile mode OR actual phone):
- Quick capture bar should be at bottom, thumb-friendly
- Type selector and Add button should be reachable
- Entries should be tappable to expand
- Swipe left on an entry - red trash icon reveals
- Complete swipe - confirmation appears
- Tap Delete - entry removed
- Touch targets feel adequately sized (not too small)
- Text is readable (not tiny)
3. **Font/Accessibility**:
- Base text should be at least 16px
- No text smaller than 14px anywhere
- Buttons and checkboxes are easy to tap
</how-to-verify>
<resume-signal>Type "approved" if all checks pass, or describe any issues found</resume-signal>
</task>
</tasks>
<verification>
After all tasks complete:
1. **TypeScript**: `npm run check` passes
2. **Swipe gesture**: Works on mobile/touch devices
3. **Touch targets**: All interactive elements >= 44x44px
4. **Font sizes**: Base 16px, no text under 14px
5. **Mobile layout**: Compact list, bottom capture bar
6. **Desktop layout**: Card style entries, adequate spacing
Requirements satisfied:
- UX-01: Mobile-friendly ✓
- UX-02: Readable fonts ✓
- UX-03: Cross-browser (SvelteKit handles) ✓
</verification>
<success_criteria>
- Swipe-to-delete works on mobile with confirmation
- All touch targets meet WCAG 44x44px minimum
- UI verified working on both desktop and mobile
- User approves the UI at checkpoint
- All Phase 2 requirements complete
</success_criteria>
<output>
After completion, create `.planning/phases/02-core-crud/02-04-SUMMARY.md`
</output>