Phase 02: Core CRUD - 4 plans in 4 waves - 3 parallel-ready (Wave 1-3 sequential), 1 checkpoint - Ready for execution
418 lines
13 KiB
Markdown
418 lines
13 KiB
Markdown
---
|
|
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>
|