Files
taskplaner/.planning/phases/02-core-crud/02-03-PLAN.md
Thomas Richter 457cd407a8 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
2026-01-29 05:27:31 +01:00

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
02-02
src/lib/components/EntryCard.svelte
src/lib/components/CompletedToggle.svelte
src/routes/+page.svelte
src/routes/+page.server.ts
true
truths artifacts key_links
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
path provides contains
src/lib/components/EntryCard.svelte Expandable entry with inline editing expanded
path provides min_lines
src/lib/components/CompletedToggle.svelte Toggle for showing completed tasks 15
path provides contains
src/routes/+page.svelte Main page with completed toggle CompletedToggle
from to via pattern
src/lib/components/EntryCard.svelte src/routes/+page.server.ts form action ?/update action=.*?/update
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.

<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:

  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
<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
Task 2: Create CompletedToggle component and wire up showCompleted src/lib/components/CompletedToggle.svelte, src/routes/+page.svelte, src/routes/+page.server.ts **CompletedToggle.svelte** - Toggle for showing/hiding completed tasks:
<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
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) ✓

<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>
After completion, create `.planning/phases/02-core-crud/02-03-SUMMARY.md`