# Svelte Todo List - Implementation Plan

## Overview

This plan builds a Svelte todo list application with localStorage persistence, filtering, and full test coverage. We use Vite + Svelte + TypeScript and Vitest for testing. Each task follows TDD: write a failing test, watch it fail, implement, watch it pass, commit.

## File Structure

| File | Responsibility |
|------|---------------|
| `src/lib/storage.ts` | Read/write todos array to localStorage with JSON serialization |
| `src/lib/storage.test.ts` | Tests for storage read/write |
| `src/lib/store.ts` | Svelte writable store wrapping todos; add/toggle/delete/clearCompleted helpers; syncs to storage |
| `src/lib/store.test.ts` | Tests for store mutations |
| `src/lib/TodoInput.svelte` | Text input + Add button; emits `add` event |
| `src/lib/TodoItem.svelte` | Single todo row: checkbox, text, delete button; emits `toggle`/`delete` |
| `src/lib/TodoList.svelte` | Renders list of TodoItem; empty-state message |
| `src/lib/FilterBar.svelte` | Items-left count, filter buttons, clear-completed button |
| `src/App.svelte` | Wires store + components; holds active filter state |
| `src/main.ts` | App entry point (generated by scaffold) |

## Data Model

```typescript
// src/lib/types.ts
export interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export type Filter = 'all' | 'active' | 'completed';
```

---

### Task 0: Project Scaffold

**Files:** `package.json`, `vite.config.ts`, `tsconfig.json`, `vitest.config.ts`, `src/main.ts`

- [ ] Scaffold a Vite Svelte+TS project in the current directory:

```bash
npm create vite@latest . -- --template svelte-ts
```

If the directory is non-empty and the tool refuses, scaffold into a temp dir and move files:

```bash
npm create vite@latest tmp-scaffold -- --template svelte-ts && \
  cp -r tmp-scaffold/. . && rm -rf tmp-scaffold
```

- [ ] Install dependencies plus testing tools:

```bash
npm install
npm install -D vitest jsdom @testing-library/svelte @testing-library/jest-dom
```

- [ ] Create `vitest.config.ts` in the project root:

```typescript
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte({ hot: false })],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./vitest.setup.ts'],
  },
});
```

- [ ] Create `vitest.setup.ts` in the project root:

```typescript
import '@testing-library/jest-dom';
```

- [ ] Add a `test` script to `package.json` (inside the `"scripts"` object):

```json
"test": "vitest run",
"test:watch": "vitest"
```

- [ ] Verify the dev build works:

```bash
npm run build
```

Expected output: ends with a line like `✓ built in <time>ms` and no errors.

- [ ] Verify the test runner starts (no tests yet is fine):

```bash
npm run test
```

Expected output: `No test files found` (exit code may be non-zero; that is acceptable at this stage).

- [ ] Commit:

```bash
git add -A && git commit -m "Scaffold Vite Svelte+TS project with Vitest"
```

---

### Task 1: Types

**Files:** `src/lib/types.ts`

- [ ] Create `src/lib/types.ts`:

```typescript
export interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export type Filter = 'all' | 'active' | 'completed';
```

- [ ] Verify TypeScript accepts it:

```bash
npx tsc --noEmit
```

Expected output: no output, exit code 0.

- [ ] Commit:

```bash
git add -A && git commit -m "Add Todo and Filter types"
```

---

### Task 2: Storage Module

**Files:** `src/lib/storage.ts`, `src/lib/storage.test.ts`

- [ ] Write the failing test `src/lib/storage.test.ts`:

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { loadTodos, saveTodos } from './storage';
import type { Todo } from './types';

const sample: Todo[] = [
  { id: '1', text: 'Buy groceries', completed: false },
  { id: '2', text: 'Walk the dog', completed: true },
];

describe('storage', () => {
  beforeEach(() => {
    localStorage.clear();
  });

  it('returns empty array when nothing stored', () => {
    expect(loadTodos()).toEqual([]);
  });

  it('saves and loads todos', () => {
    saveTodos(sample);
    expect(loadTodos()).toEqual(sample);
  });

  it('returns empty array when stored value is invalid JSON', () => {
    localStorage.setItem('svelte-todos', 'not-json');
    expect(loadTodos()).toEqual([]);
  });

  it('returns empty array when stored value is not an array', () => {
    localStorage.setItem('svelte-todos', '{"foo":1}');
    expect(loadTodos()).toEqual([]);
  });
});
```

- [ ] Run it to see it fail:

```bash
npm run test -- storage
```

Expected output: fails with `Failed to resolve import "./storage"` or `loadTodos is not a function`.

- [ ] Implement `src/lib/storage.ts`:

```typescript
import type { Todo } from './types';

const KEY = 'svelte-todos';

export function loadTodos(): Todo[] {
  const raw = localStorage.getItem(KEY);
  if (raw === null) return [];
  try {
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed as Todo[];
  } catch {
    return [];
  }
}

export function saveTodos(todos: Todo[]): void {
  localStorage.setItem(KEY, JSON.stringify(todos));
}
```

- [ ] Run it to see it pass:

```bash
npm run test -- storage
```

Expected output: `4 passed`.

- [ ] Commit:

```bash
git add -A && git commit -m "Add localStorage persistence module"
```

---

### Task 3: Todos Store

**Files:** `src/lib/store.ts`, `src/lib/store.test.ts`

- [ ] Write the failing test `src/lib/store.test.ts`:

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { get } from 'svelte/store';
import { todos, addTodo, toggleTodo, deleteTodo, clearCompleted } from './store';

describe('todos store', () => {
  beforeEach(() => {
    localStorage.clear();
    todos.set([]);
  });

  it('starts empty', () => {
    expect(get(todos)).toEqual([]);
  });

  it('adds a todo with generated id and completed false', () => {
    addTodo('Buy groceries');
    const list = get(todos);
    expect(list).toHaveLength(1);
    expect(list[0].text).toBe('Buy groceries');
    expect(list[0].completed).toBe(false);
    expect(typeof list[0].id).toBe('string');
    expect(list[0].id.length).toBeGreaterThan(0);
  });

  it('does not add blank or whitespace-only todos', () => {
    addTodo('   ');
    expect(get(todos)).toHaveLength(0);
  });

  it('trims whitespace from added text', () => {
    addTodo('  hello  ');
    expect(get(todos)[0].text).toBe('hello');
  });

  it('toggles completion', () => {
    addTodo('task');
    const id = get(todos)[0].id;
    toggleTodo(id);
    expect(get(todos)[0].completed).toBe(true);
    toggleTodo(id);
    expect(get(todos)[0].completed).toBe(false);
  });

  it('deletes a todo', () => {
    addTodo('task');
    const id = get(todos)[0].id;
    deleteTodo(id);
    expect(get(todos)).toHaveLength(0);
  });

  it('clears completed todos only', () => {
    addTodo('a');
    addTodo('b');
    const [first] = get(todos);
    toggleTodo(first.id);
    clearCompleted();
    const list = get(todos);
    expect(list).toHaveLength(1);
    expect(list[0].text).toBe('b');
  });

  it('persists to localStorage on change', () => {
    addTodo('persisted');
    const raw = localStorage.getItem('svelte-todos');
    expect(raw).toContain('persisted');
  });
});
```

- [ ] Run it to see it fail:

```bash
npm run test -- store
```

Expected output: fails with `Failed to resolve import "./store"`.

- [ ] Implement `src/lib/store.ts`:

```typescript
import { writable } from 'svelte/store';
import type { Todo } from './types';
import { loadTodos, saveTodos } from './storage';

export const todos = writable<Todo[]>(loadTodos());

todos.subscribe((value) => {
  saveTodos(value);
});

export function addTodo(text: string): void {
  const trimmed = text.trim();
  if (trimmed === '') return;
  const todo: Todo = {
    id: crypto.randomUUID(),
    text: trimmed,
    completed: false,
  };
  todos.update((list) => [...list, todo]);
}

export function toggleTodo(id: string): void {
  todos.update((list) =>
    list.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
  );
}

export function deleteTodo(id: string): void {
  todos.update((list) => list.filter((t) => t.id !== id));
}

export function clearCompleted(): void {
  todos.update((list) => list.filter((t) => !t.completed));
}
```

- [ ] Run it to see it pass:

```bash
npm run test -- store
```

Expected output: `8 passed`.

> Note: `crypto.randomUUID` is available in jsdom (Node 19+) and modern browsers. If your Node version lacks it, the test will error on `addTodo`; in that case upgrade Node to 19+.

- [ ] Commit:

```bash
git add -A && git commit -m "Add todos store with persistence"
```

---

### Task 4: TodoInput Component

**Files:** `src/lib/TodoInput.svelte`, `src/lib/TodoInput.test.ts`

- [ ] Write the failing test `src/lib/TodoInput.test.ts`:

```typescript
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import TodoInput from './TodoInput.svelte';

describe('TodoInput', () => {
  it('dispatches add event with text on button click', async () => {
    const { getByRole, component } = render(TodoInput);
    const handler = vi.fn();
    component.$on('add', (e) => handler(e.detail));

    const input = getByRole('textbox') as HTMLInputElement;
    await fireEvent.input(input, { target: { value: 'New task' } });
    await fireEvent.click(getByRole('button', { name: /add/i }));

    expect(handler).toHaveBeenCalledWith('New task');
  });

  it('dispatches add event on Enter key', async () => {
    const { getByRole, component } = render(TodoInput);
    const handler = vi.fn();
    component.$on('add', (e) => handler(e.detail));

    const input = getByRole('textbox') as HTMLInputElement;
    await fireEvent.input(input, { target: { value: 'Via enter' } });
    await fireEvent.keyDown(input, { key: 'Enter' });

    expect(handler).toHaveBeenCalledWith('Via enter');
  });

  it('clears input after adding', async () => {
    const { getByRole } = render(TodoInput);
    const input = getByRole('textbox') as HTMLInputElement;
    await fireEvent.input(input, { target: { value: 'Task' } });
    await fireEvent.click(getByRole('button', { name: /add/i }));
    expect(input.value).toBe('');
  });
});
```

- [ ] Run it to see it fail:

```bash
npm run test -- TodoInput
```

Expected output: fails resolving `./TodoInput.svelte`.

- [ ] Implement `src/lib/TodoInput.svelte`:

```svelte
<script lang="ts">
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher<{ add: string }>();
  let text = '';

  function submit() {
    dispatch('add', text);
    text = '';
  }

  function onKeyDown(e: KeyboardEvent) {
    if (e.key === 'Enter') {
      submit();
    }
  }
</script>

<div class="todo-input">
  <input
    type="text"
    placeholder="What needs to be done?"
    bind:value={text}
    on:keydown={onKeyDown}
  />
  <button on:click={submit}>Add</button>
</div>

<style>
  .todo-input {
    display: flex;
    gap: 0.5rem;
  }
  input {
    flex: 1;
    padding: 0.5rem;
  }
</style>
```

- [ ] Run it to see it pass:

```bash
npm run test -- TodoInput
```

Expected output: `3 passed`.

- [ ] Commit:

```bash
git add -A && git commit -m "Add TodoInput component"
```

---

### Task 5: TodoItem Component

**Files:** `src/lib/TodoItem.svelte`, `src/lib/TodoItem.test.ts`

- [ ] Write the failing test `src/lib/TodoItem.test.ts`:

```typescript
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import TodoItem from './TodoItem.svelte';
import type { Todo } from './types';

const todo: Todo = { id: 'abc', text: 'Walk the dog', completed: false };

describe('TodoItem', () => {
  it('renders the todo text', () => {
    const { getByText } = render(TodoItem, { props: { todo } });
    expect(getByText('Walk the dog')).toBeInTheDocument();
  });

  it('checkbox reflects completed state', () => {
    const { getByRole } = render(TodoItem, {
      props: { todo: { ...todo, completed: true } },
    });
    expect((getByRole('checkbox') as HTMLInputElement).checked).toBe(true);
  });

  it('dispatches toggle with id when checkbox clicked', async () => {
    const { getByRole, component } = render(TodoItem, { props: { todo } });
    const handler = vi.fn();
    component.$on('toggle', (e) => handler(e.detail));
    await fireEvent.click(getByRole('checkbox'));
    expect(handler).toHaveBeenCalledWith('abc');
  });

  it('dispatches delete with id when delete button clicked', async () => {
    const { getByRole, component } = render(TodoItem, { props: { todo } });
    const handler = vi.fn();
    component.$on('delete', (e) => handler(e.detail));
    await fireEvent.click(getByRole('button', { name: /delete/i }));
    expect(handler).toHaveBeenCalledWith('abc');
  });
});
```

- [ ] Run it to see it fail:

```bash
npm run test -- TodoItem
```

Expected output: fails resolving `./TodoItem.svelte`.

- [ ] Implement `src/lib/TodoItem.svelte`:

```svelte
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  import type { Todo } from './types';

  export let todo: Todo;

  const dispatch = createEventDispatcher<{ toggle: string; delete: string }>();
</script>

<li class="todo-item" class:completed={todo.completed}>
  <input
    type="checkbox"
    checked={todo.completed}
    on:change={() => dispatch('toggle', todo.id)}
  />
  <span class="text">{todo.text}</span>
  <button
    class="delete"
    aria-label="Delete todo"
    on:click={() => dispatch('delete', todo.id)}>x</button
  >
</li>

<style>
  .todo-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0;
  }
  .text {
    flex: 1;
  }
  .completed .text {
    text-decoration: line-through;
    opacity: 0.6;
  }
</style>
```

- [ ] Run it to see it pass:

```bash
npm run test -- TodoItem
```

Expected output: `4 passed`.

- [ ] Commit:

```bash
git add -A && git commit -m "Add TodoItem component"
```

---

### Task 6: TodoList Component

**Files:** `src/lib/TodoList.svelte`, `src/lib/TodoList.test.ts`

- [ ] Write the failing test `src/lib/TodoList.test.ts`:

```typescript
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import TodoList from './TodoList.svelte';
import type { Todo } from './types';

const todos: Todo[] = [
  { id: '1', text: 'A', completed: false },
  { id: '2', text: 'B', completed: true },
];

describe('TodoList', () => {
  it('renders each todo', () => {
    const { getByText } = render(TodoList, { props: { todos } });
    expect(getByText('A')).toBeInTheDocument();
    expect(getByText('B')).toBeInTheDocument();
  });

  it('shows empty-state message when no todos', () => {
    const { getByText } = render(TodoList, { props: { todos: [] } });
    expect(getByText(/nothing here/i)).toBeInTheDocument();
  });

  it('forwards toggle event from item', async () => {
    const { getAllByRole, component } = render(TodoList, { props: { todos } });
    const handler = vi.fn();
    component.$on('toggle', (e) => handler(e.detail));
    await fireEvent.click(getAllByRole('checkbox')[0]);
    expect(handler).toHaveBeenCalledWith('1');
  });

  it('forwards delete event from item', async () => {
    const { getAllByRole, component } = render(TodoList, { props: { todos } });
    const handler = vi.fn();
    component.$on('delete', (e) => handler(e.detail));
    await fireEvent.click(getAllByRole('button', { name: /delete/i })[1]);
    expect(handler).toHaveBeenCalledWith('2');
  });
});
```

- [ ] Run it to see it fail:

```bash
npm run test -- TodoList
```

Expected output: fails resolving `./TodoList.svelte`.

- [ ] Implement `src/lib/TodoList.svelte`:

```svelte
<script lang="ts">
  import TodoItem from './TodoItem.svelte';
  import type { Todo } from './types';

  export let todos: Todo[];
</script>

{#if todos.length === 0}
  <p class="empty">Nothing here yet — add your first todo!</p>
{:else}
  <ul class="todo-list">
    {#each todos as todo (todo.id)}
      <TodoItem {todo} on:toggle on:delete />
    {/each}
  </ul>
{/if}

<style>
  .todo-list {
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .empty {
    text-align: center;
    color: #888;
    padding: 1rem 0;
  }
</style>
```

> Note: `on:toggle on:delete` without a handler forwards the child events to this component's parent.

- [ ] Run it to see it pass:

```bash
npm run test -- TodoList
```

Expected output: `4 passed`.

- [ ] Commit:

```bash
git add -A && git commit -m "Add TodoList component with empty state"
```

---

### Task 7: FilterBar Component

**Files:** `src/lib/FilterBar.svelte`, `src/lib/FilterBar.test.ts`

- [ ] Write the failing test `src/lib/FilterBar.test.ts`:

```typescript
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import FilterBar from './FilterBar.svelte';

describe('FilterBar', () => {
  it('shows singular item count', () => {
    const { getByText } = render(FilterBar, {
      props: { remaining: 1, filter: 'all' },
    });
    expect(getByText('1 item left')).toBeInTheDocument();
  });

  it('shows plural item count', () => {
    const { getByText } = render(FilterBar, {
      props: { remaining: 2, filter: 'all' },
    });
    expect(getByText('2 items left')).toBeInTheDocument();
  });

  it('dispatches filter event when a filter button clicked', async () => {
    const { getByRole, component } = render(FilterBar, {
      props: { remaining: 0, filter: 'all' },
    });
    const handler = vi.fn();
    component.$on('filter', (e) => handler(e.detail));
    await fireEvent.click(getByRole('button', { name: /active/i }));
    expect(handler).toHaveBeenCalledWith('active');
  });

  it('marks the current filter button active', () => {
    const { getByRole } = render(FilterBar, {
      props: { remaining: 0, filter: 'completed' },
    });
    expect(getByRole('button', { name: /completed/i })).toHaveClass('selected');
  });

  it('dispatches clearCompleted event', async () => {
    const { getByRole, component } = render(FilterBar, {
      props: { remaining: 0, filter: 'all' },
    });
    const handler = vi.fn();
    component.$on('clearCompleted', handler);
    await fireEvent.click(getByRole('button', { name: /clear/i }));
    expect(handler).toHaveBeenCalled();
  });
});
```

- [ ] Run it to see it fail:

```bash
npm run test -- FilterBar
```

Expected output: fails resolving `./FilterBar.svelte`.

- [ ] Implement `src/lib/FilterBar.svelte`:

```svelte
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  import type { Filter } from './types';

  export let remaining: number;
  export let filter: Filter;

  const dispatch = createEventDispatcher<{
    filter: Filter;
    clearCompleted: void;
  }>();

  const filters: Filter[] = ['all', 'active', 'completed'];

  function label(f: Filter): string {
    return f.charAt(0).toUpperCase() + f.slice(1);
  }
</script>

<div class="filter-bar">
  <span class="count">{remaining} {remaining === 1 ? 'item' : 'items'} left</span>

  <div class="filters">
    {#each filters as f}
      <button
        class:selected={filter === f}
        on:click={() => dispatch('filter', f)}>{label(f)}</button
      >
    {/each}
  </div>

  <button class="clear" on:click={() => dispatch('clearCompleted')}>Clear ✓</button>
</div>

<style>
  .filter-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    flex-wrap: wrap;
  }
  .filters button.selected {
    font-weight: bold;
    text-decoration: underline;
  }
</style>
```

- [ ] Run it to see it pass:

```bash
npm run test -- FilterBar
```

Expected output: `5 passed`.

- [ ] Commit:

```bash
git add -A && git commit -m "Add FilterBar component"
```

---

### Task 8: App Integration

**Files:** `src/App.svelte`, `src/App.test.ts`

- [ ] Write the failing test `src/App.test.ts`:

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import App from './App.svelte';
import { todos } from './lib/store';

async function addTodo(getByRole: any, text: string) {
  const input = getByRole('textbox') as HTMLInputElement;
  await fireEvent.input(input, { target: { value: text } });
  await fireEvent.click(getByRole('button', { name: /^add$/i }));
}

describe('App', () => {
  beforeEach(() => {
    localStorage.clear();
    todos.set([]);
  });

  it('adds a todo and shows it', async () => {
    const { getByRole, getByText } = render(App);
    await addTodo(getByRole, 'Buy milk');
    expect(getByText('Buy milk')).toBeInTheDocument();
  });

  it('shows remaining count', async () => {
    const { getByRole, getByText } = render(App);
    await addTodo(getByRole, 'One');
    await addTodo(getByRole, 'Two');
    expect(getByText('2 items left')).toBeInTheDocument();
  });

  it('toggling reduces remaining count', async () => {
    const { getByRole, getAllByRole, getByText } = render(App);
    await addTodo(getByRole, 'One');
    await fireEvent.click(getAllByRole('checkbox')[0]);
    expect(getByText('0 items left')).toBeInTheDocument();
  });

  it('Active filter hides completed todos', async () => {
    const { getByRole, getAllByRole, queryByText } = render(App);
    await addTodo(getByRole, 'Active task');
    await addTodo(getByRole, 'Done task');
    // complete the second todo
    await fireEvent.click(getAllByRole('checkbox')[1]);
    await fireEvent.click(getByRole('button', { name: /active/i }));
    expect(queryByText('Active task')).toBeInTheDocument();
    expect(queryByText('Done task')).not.toBeInTheDocument();
  });

  it('Completed filter shows only completed todos', async () => {
    const { getByRole, getAllByRole, queryByText } = render(App);
    await addTodo(getByRole, 'Active task');
    await addTodo(getByRole, 'Done task');
    await fireEvent.click(getAllByRole('checkbox')[1]);
    await fireEvent.click(getByRole('button', { name: /completed/i }));
    expect(queryByText('Done task')).toBeInTheDocument();
    expect(queryByText('Active task')).not.toBeInTheDocument();
  });

  it('clear completed removes completed todos', async () => {
    const { getByRole, getAllByRole, queryByText } = render(App);
    await addTodo(getByRole, 'Keep me');
    await addTodo(getByRole, 'Remove me');
    await fireEvent.click(getAllByRole('checkbox')[1]);
    await fireEvent.click(getByRole('button', { name: /clear/i }));
    expect(queryByText('Keep me')).toBeInTheDocument();
    expect(queryByText('Remove me')).not.toBeInTheDocument();
  });

  it('deleting removes the todo', async () => {
    const { getByRole, getAllByRole, queryByText } = render(App);
    await addTodo(getByRole, 'Delete me');
    await fireEvent.click(getAllByRole('button', { name: /delete/i })[0]);
    expect(queryByText('Delete me')).not.toBeInTheDocument();
  });
});
```

- [ ] Run it to see it fail:

```bash
npm run test -- App
```

Expected output: fails — the scaffolded `App.svelte` does not contain todo UI.

- [ ] Replace `src/App.svelte` entirely:

```svelte
<script lang="ts">
  import TodoInput from './lib/TodoInput.svelte';
  import TodoList from './lib/TodoList.svelte';
  import FilterBar from './lib/FilterBar.svelte';
  import { todos, addTodo, toggleTodo, deleteTodo, clearCompleted } from './lib/store';
  import type { Filter } from './lib/types';

  let filter: Filter = 'all';

  $: visible = $todos.filter((t) => {
    if (filter === 'active') return !t.completed;
    if (filter === 'completed') return t.completed;
    return true;
  });

  $: remaining = $todos.filter((t) => !t.completed).length;
</script>

<main>
  <h1>Svelte Todos</h1>

  <TodoInput on:add={(e) => addTodo(e.detail)} />

  <TodoList
    todos={visible}
    on:toggle={(e) => toggleTodo(e.detail)}
    on:delete={(e) => deleteTodo(e.detail)}
  />

  <FilterBar
    {remaining}
    {filter}
    on:filter={(e) => (filter = e.detail)}
    on:clearCompleted={clearCompleted}
  />
</main>

<style>
  main {
    max-width: 500px;
    margin: 2rem auto;
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
    font-family: system-ui, sans-serif;
  }
  h1 {
    font-size: 1.5rem;
    text-align: center;
  }
</style>
```

- [ ] Run it to see it pass:

```bash
npm run test -- App
```

Expected output: `7 passed`.

- [ ] Commit:

```bash
git add -A && git commit -m "Wire components together in App"
```

---

### Task 9: Full Suite & Build Verification

**Files:** none (verification only)

- [ ] Run the entire test suite:

```bash
npm run test
```

Expected output: all test files pass; total `35 passed` (4 storage + 8 store + 3 TodoInput + 4 TodoItem + 4 TodoList + 5 FilterBar + 7 App).

- [ ] Type-check the whole project:

```bash
npx tsc --noEmit
```

Expected output: no output, exit code 0.

- [ ] Build the production bundle:

```bash
npm run build
```

Expected output: ends with `✓ built in <time>ms`, no errors.

- [ ] Manually verify persistence (optional but recommended):

```bash
npm run dev
```

Open the printed localhost URL, add a few todos, toggle and filter them, then refresh the page. Confirm the todos persist. Stop the server with Ctrl+C.

- [ ] Commit any final cleanup if needed:

```bash
git add -A && git commit -m "Verify full suite and build" --allow-empty
```

---

## Self-Review

**Spec coverage check:**

1. ✅ Add via Enter or Add button — Task 4 tests both; App test `adds a todo`.
2. ✅ Toggle completion — Task 5 + App `toggling reduces remaining count`.
3. ✅ Delete via X — Task 5 + App `deleting removes the todo`.
4. ✅ Filter subsets — App `Active filter` and `Completed filter` tests.
5. ✅ "X items left" — FilterBar singular/plural tests + App `shows remaining count`.
6. ✅ Clear completed — store test + App `clear completed removes` test.
7. ✅ Persist across refresh — storage tests + store persistence test + manual verify in Task 9.
8. ✅ Empty state — TodoList `shows empty-state message`.
9. ✅ All tests pass — Task 9 full suite.

**Placeholder scan:** No TODOs, no stub functions, no `// implement later`. All code blocks are complete.

**Type consistency check:**
- `Todo` / `Filter` defined once in `types.ts` and imported everywhere (DRY).
- Event dispatch generics match what App consumes: `TodoInput` dispatches `add: string`; `TodoItem`/`TodoList` dispatch `toggle: string` / `delete: string`; `FilterBar` dispatches `filter: Filter` / `clearCompleted: void`. App handlers consume `e.detail` accordingly.
- `remaining: number` and `filter: Filter` props on FilterBar match App's reactive `$:` bindings.

**Fixes applied inline:** FilterBar's `clearCompleted` event uses `void` detail; the App test for it listens without `.detail`, and the App binds `on:clearCompleted={clearCompleted}` directly (no detail needed) — consistent.