# Design Document: Todo web app (local)

> **Status: Accepted**  
> **Accepted by:** Michael  
> **Accepted on:** 2026-04-29

## Overview

This design specifies a **client-only** single-page application: React UI, **IndexedDB** persistence via **Dexie**, and **@dnd-kit** for reordering. The app satisfies [Requirements 1–6 in `requirements.md`](requirements.md): add todos with validation, toggle completion, delete, drag-and-reorder with persisted order, empty and list views, and durability across refresh and browser restart without a project backend.

## Key design decisions

| Decision                                                                 | Rationale                                                                                                                      | Requirements      |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----------------- |
| **Vite + React + TypeScript**                                            | Fast dev/build; typed UI and data layer.                                                                                       | 5, 6              |
| **MUI** (`@mui/material` + Emotion peers per MUI install docs)           | Consistent components for layout, inputs, list, buttons, and completion affordances.                                           | 1.1–1.3, 2, 3, 5  |
| **Dexie** on **IndexedDB**                                               | Typed, migratable local store; survives refresh/restart; no custom server.                                                     | 4.3, 5.4, 6.1–6.3 |
| **@dnd-kit** (`@dnd-kit/core` + `@dnd-kit/sortable`) with MUI list rows  | MUI has no built-in sortable list; dnd-kit is a common, accessible-friendly pairing.                                           | 4.1, 4.2          |
| **`sortOrder` (integer) per todo**                                       | Single column encodes list order; **reorder = rewrite contiguous `0…n-1` in one transaction** to keep order unique and simple. | 4                 |
| **Max text length: 2 000 characters** (after trim)                       | Exceeds the minimum 500 in requirements; overflow via `input` + validation + user-visible error.                               | 1.3               |
| **Optimistic UI for toggle/delete/add/reorder; await persistence after** | Perceived speed; design requires clearing optimistic state on DB error and surfacing a short error.                            | 2, 3, 4, 6        |
| **No Vitest/Jest** in this experiment                                    | Excluded from scope; manual verification per correctness properties.                                                           | (project scope)   |

## Architecture

High-level layering: **presentational and container components** call **hooks and a small repository** that read/write **Dexie**; drag-and-drop updates local React state, then the repository **persists the new `sortOrder` set** in one transaction.

```mermaid
flowchart TB
  subgraph UI["React UI (MUI)"]
    A[TodoApp shell]
    B[AddTodoForm]
    C[TodoSortableList]
    D[DragOverlay / row chrome]
  end
  subgraph Logic["Data & DnD"]
    E[useTodoList hook]
    F[DnDContext + sortable]
    G[todoRepository]
  end
  subgraph Store["Local persistence"]
    H[(IndexedDB / Dexie)]
  end
  A --> B
  A --> C
  C --> F
  F --> E
  B --> E
  E --> G
  G --> H
```

**Flow after reorder (direct manipulation):** the user drags a row → **dnd-kit** computes the new order → the hook updates **in-memory list order** → `todoRepository.reorder(idsInOrder)` assigns `sortOrder = 0..n-1` and **writes in a single Dexie transaction** → on success, state matches store; on failure, revert to last known good list and show a non-blocking error (e.g. MUI `Snackbar` or `Alert`).

## Sequence: initial load and reorder persist

```mermaid
sequenceDiagram
  participant U as User
  participant R as React / hook
  participant Repo as todoRepository
  participant IDB as Dexie (IndexedDB)

  U->>R: open app
  R->>Repo: getAllOrdered()
  Repo->>IDB: read all todos, ORDER BY sortOrder
  IDB-->>Repo: rows
  Repo-->>R: sorted Todo items
  R-->>U: render list (or empty state)

  U->>R: end drag (new order)
  R->>R: apply optimistic reorder
  R->>Repo: reorder(newIdSequence)
  Repo->>IDB: transaction: update sortOrder for all affected
  IDB-->>Repo: commit
  Repo-->>R: ok
  R-->>U: list matches new order
```

## Layer and component responsibilities

| Layer / unit                       | Responsibility                                                                                                                                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`TodoApp` (or `App`)**           | Theme provider, layout (`Container`), error surface, single-page shell.                                                                                             |
| **`AddTodoForm`**                  | `TextField`, trim + length check, disabled submit or inline helper text when empty/over limit; calls hook `add(text)`.                                              |
| **`TodoSortableList` + `TodoRow`** | Renders `SortableContext` and sortable items; each row: drag handle (or full-row as pointer sensor per implementation), checkbox toggle, delete, completed styling. |
| **`useTodoList`**                  | Holds list state; on mount calls `getAllOrdered`; implements add, toggle, delete, reorder; coordinates optimistic updates and error rollback.                       |
| **`todoRepository`**               | Dexie `Table` access: CRUD, `reorder(orderedIds: string[])` with transactional `sortOrder` updates, no business UI.                                                 |

**Accessibility (DnD):** use **dnd-kit** `KeyboardSensor` + `PointerSensor` (with `PointerSensor` activation constraints if using handles) so keyboard users can move focus and activate sortable move patterns where supported; ensure each row’s toggle and delete are reachable and labeled; list semantics (`list` / `listitem` or equivalent) where MUI list props allow.

## Data contracts

### Database (Dexie) — table `todos`

| Field       | Type                | Description                                                     |
| ----------- | ------------------- | --------------------------------------------------------------- |
| `id`        | `string`            | Opaque unique id (e.g. `crypto.randomUUID()`).                  |
| `text`      | `string`            | User text; stored trimmed to max length on write.               |
| `completed` | `boolean`           | Completion flag.                                                |
| `sortOrder` | `number` (integer)  | **Contiguous 0…n-1 in sync with list order** after any reorder. |
| `createdAt` | `string` (ISO-8601) | Optional; useful for future features; not required for core AC. |
| `updatedAt` | `string` (ISO-8601) | Optional.                                                       |

**Invariants (after any successful write that affects order):** every todo in the table has a unique `sortOrder` in `0..n-1` where `n` is the row count.

### TypeScript (domain + DB row)

```ts
// Domain shape used in the UI layer
export type Todo = {
  id: string;
  text: string;
  completed: boolean;
  sortOrder: number;
};

// Dexie version schema (illustrative)
// todos: 'id, sortOrder' — query by order via sortOrder index
```

`getAllOrdered()` **SHALL** return rows sorted by `sortOrder` ascending. New items receive `sortOrder = max(sortOrder) + 1` or `0` if the list is empty, then a reorder can normalize; alternatively append with `n` and immediately **normalize** in the same user action to keep the invariant—implementation must preserve invariant after add.

**Add behavior:** on successful add, assign `sortOrder` so the new item is last (e.g. `n`) then **re-normalize** to `0..n` in the same transaction as the insert, or insert with the next index without gaps if gap-free invariant is maintained by definition.

**Delete behavior:** after delete, **re-number** `sortOrder` to `0..n-1` in one transaction for remaining rows.

## Error handling

- **Validation (empty / whitespace, over limit):** block submit; show MUI helper text or disabled state (satisfies requirement 1.2–1.3 at UI level). No DB write.
- **IndexedDB / Dexie failures:** on read failure, show empty or error state with retry; on write after optimistic UI, **revert** optimistic data from last successful read and show a short message; do not leave the UI in a state that contradicts durable storage for longer than the error message lifetime.

## Correctness properties

### Property 1: Persisted order matches last successful reorder

_For any_ user session, _after_ a drag ends and persistence completes, _if_ the user only refreshes (no other mutations), the list order **SHALL** match that order when read back from `Local_Persistence`.

**Validates: Requirements 4.1, 4.2, 4.3, 6.1**

### Property 2: CRUD durability

_For any_ sequence of add, toggle, and delete operations that all complete without error, _if_ the user then refreshes or reopens the browser (same profile, data not cleared), the set of **Todo_Items** (text, completed, and relative order) **SHALL** match the last successful persisted state.

**Validates: Requirements 1.1, 2, 3, 6.1, 6.2**

### Property 3: Invariant on `sortOrder`

_At any_ committed state of the `todos` table, the multiset of `sortOrder` values is exactly `{0, 1, …, n-1}` for `n` items.

**Validates: Requirements 4.3** (enables a single unambiguous list order for restore)

### Property 4: Rapid operations

_For any_ rapid sequence of toggles, deletes, or reorders, the final persisted state (after all in-flight operations settle or the last one wins per transaction ordering) **SHALL** be consistent: no partial reorder with duplicate or missing `sortOrder` after commit.

**Validates: Requirements 2, 3, 4** (no corrupt order index)

## Testing and verification

Automated **unit and integration tests are out of scope** for this experiment. Manual checks should cover: add/validation, toggle, delete, drag reorder + refresh, and offline refresh (airplane mode) to confirm no dependency on a backend. Future work could add Vitest + fake IndexedDB.

## Traceability matrix (requirements → design sections)

| Requirements          | Design anchor                                                                                         |
| --------------------- | ----------------------------------------------------------------------------------------------------- |
| 1 (add)               | [Key design decisions](#key-design-decisions), [Data contracts](#data-contracts), `AddTodoForm`       |
| 2 (toggle)            | [Layer](#layer-and-component-responsibilities), `TodoRow`                                             |
| 3 (delete)            | Same, `reorder` / delete in repository                                                                |
| 4 (reorder + persist) | [Architecture](#architecture), [Sequence](#sequence-initial-load-and-reorder-persist), Property 1 & 3 |
| 5 (SPA, list, empty)  | [Overview](#overview), `TodoApp`                                                                      |
| 6 (local durability)  | [Key design decisions](#key-design-decisions), Property 2                                             |
