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:
218
.planning/phases/02-core-crud/02-01-PLAN.md
Normal file
218
.planning/phases/02-core-crud/02-01-PLAN.md
Normal 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>
|
||||
368
.planning/phases/02-core-crud/02-02-PLAN.md
Normal file
368
.planning/phases/02-core-crud/02-02-PLAN.md
Normal 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>
|
||||
417
.planning/phases/02-core-crud/02-03-PLAN.md
Normal file
417
.planning/phases/02-core-crud/02-03-PLAN.md
Normal 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>
|
||||
255
.planning/phases/02-core-crud/02-04-PLAN.md
Normal file
255
.planning/phases/02-core-crud/02-04-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user