Phase 02: Core CRUD - 4 plans in 4 waves - 3 parallel-ready (Wave 1-3 sequential), 1 checkpoint - Ready for execution
13 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-core-crud | 03 | execute | 3 |
|
|
true |
|
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.
<execution_context> @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md </execution_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 Task 1: Add expand/collapse and inline editing to EntryCard src/lib/components/EntryCard.svelte Update EntryCard to support expand/collapse with inline editing and debounced auto-save.Key changes:
- Add
expandedstate variable - When collapsed, show title + truncated content (current behavior)
- When expanded, show editable fields: title input, content textarea, type selector
- Auto-save changes with 400ms debounce using fetch to ?/update
- Use Svelte's slide transition for smooth expand/collapse
<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
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 EntryCard expands/collapses with inline editing and 400ms debounced auto-save
<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:
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:
<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.
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 CompletedToggle shows/hides completed tasks, state persists in preferences and URL
- TypeScript:
npm run checkpasses - Expand/collapse: Click entry to expand, click again to collapse
- Inline editing: Title, content, type can be edited in expanded view
- Auto-save: Editing shows "Saving..." then saves (no need to click save)
- Delete: Delete button in expanded view with confirmation
- Completed toggle: Checkbox in header shows/hides completed tasks
- 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) ✓
<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>