7.4 KiB
Simple Todo List — product specification
This is the contract the project is built against. It is written for the agent that scaffolds the repository, but it doubles as the human-readable brief: every requirement below is meant to be implementable and verifiable.
This is the minimal demo template. The goal is the smallest website that is genuinely useful, so the spec is deliberately short and the non-goals matter as much as the goals.
Values in angle brackets (<Project name>, <accent>) come from the project-creation dialog and
are recorded in the project record (.w4c/project.json → fields); replace them as you read.
1. Goal
A single static page that manages a todo list — add a todo, mark it done or undone, delete it,
clear the completed ones, filter by all / active / done — and remembers the list in the browser
between visits. It must be usable the moment it is served: no install step, no build step, no
backend and no account. <Project name> is the page title; <accent> is the single accent colour.
Non-goals (do not build)
- No backend, API, database or authentication.
- No framework, package manager, bundler or third-party runtime — plain HTML, CSS and JavaScript.
- No accounts, sharing, sync between devices or server persistence.
- No service worker / offline caching, no analytics, no ads.
- No admin panel, control panel, workflows or database layer.
2. The screen
One route, / (the static index.html). It contains:
- a header with
<Project name>; - a composer — a labelled text input plus an Add control (Enter also submits);
- a list of todos, each row: a checkbox/toggle ("done"), the title, a Delete control (visible or on row hover/focus — always reachable by keyboard);
- a footer with the count of remaining active todos, the filters (All / Active / Done) and a Clear completed action shown only when at least one todo is done;
- an explicit empty state when there are no todos (and a distinct message when the filter has no matches), and a hidden/disabled Delete/Checkbox state that never leaves a row inaccessible.
No routing beyond the single page; /404 is not required.
3. Data model
One entity, persisted as a JSON array in localStorage:
- Todo —
id(unique, e.g. a stringified timestamp orcrypto.randomUUID()),title(non-empty, trimmed),done(boolean),createdAt(ISO timestamp).
Storage contract: a single documented key holds the array (e.g. simple-todo.items). A missing,
empty or unparseable value is treated as an empty list — never throw. Unknown fields are ignored;
a malformed stored item is dropped, not rendered as a broken row. The filter selection is UI state
and does not need to persist (persisting just the filter is optional).
4. Key flows
- Add. The user types a non-empty title and presses Add (or Enter): the todo appears at the top of the list, the input clears and refocuses. Whitespace-only input is rejected (the Add control is disabled and the input stays unchanged).
- Toggle. Clicking the checkbox / row toggles
done; the title gets a struck-through done style and the remaining count updates. - Delete. The Delete control removes the row; the list and the empty state update immediately.
- Clear completed. Shown only when at least one todo is done; removes every done todo after
the built-in confirmation (a native
confirm()is acceptable) and updates the list. - Filter. All / Active / Done switches the visible rows; the active filter is visibly marked
and exposed to assistive tech (
aria-pressedor equivalent). The footer count reflects the active todos regardless of the filter. - Persist. Every mutation writes the list to
localStorage; reloading the page restores the list exactly (order, done state, titles) and the correct remaining count.
5. Functional requirements
- Input validation: titles are trimmed; empty/whitespace titles are never added; long titles wrap instead of overflowing; a sensible maximum is enforced (e.g. 200 characters, hinted to the user) rather than silently truncating stored data.
- Rendering: the list is rendered from the stored array (a single
render()function is enough); the DOM never accumulates stale rows across re-renders. - Counts: the "X active" count updates on every add / toggle / delete / clear / filter change.
- Keyboard: everything the mouse can do is reachable and operable with the keyboard; focus is visible and focus order follows the visual order. The composer supports Enter to add.
- Safe text: todo titles are inserted as text, never as HTML (no
innerHTMLwith user input), so a title like<img src=x onerror=alert(1)>renders as literal text. - No network: the page makes no requests; it works when opened directly from the filesystem.
6. Non-functional requirements
- Dependency-free: plain HTML + CSS + JavaScript. ES modules or one
<script>are both fine; apackage.json/ bundler is not. The page must run fromfile://and from a static server. - Performance: a single small page; first render well under 1 s on a normal machine; no render-blocking third-party assets.
- Responsive: usable from 360px wide up to desktop; no horizontal scroll at 390×844.
- Accessibility (AA): semantic landmarks (
header/main/footer), a real<label>for the composer input, the checkbox as an accessible control (or a button witharia-pressed), ≥ AA contrast for the<accent>colour and the done style, visible focus. - Resilience: corrupt/empty storage degrades to an empty list; no unhandled errors in the console during normal use.
- Security: no secrets; user input is rendered as text only.
7. Acceptance criteria (definition of done)
- The project is a static site: no
package.json, no bundler, no third-party runtime, no network calls. - Served at
/, the page loads with no console errors and shows the empty state on a fresh browser. - Adding, toggling, deleting and clearing todos all work; the remaining count is always correct.
- The All / Active / Done filter works, marks the active filter and shows a distinct no-match state.
- Reloading restores the list exactly from
localStorage; a corrupted stored value degrades to an empty list instead of throwing. - A title containing HTML is rendered as literal text (no injection).
- Whitespace-only titles cannot be added; long titles wrap and do not overflow.
- Everything is operable by keyboard alone, with visible focus and AA contrast.
- Usable at 390×844 with no horizontal scroll.
- The README quickstart (how to serve it, how to use it) works on a clean clone.
- The dev server (template
devServersection) serves the page at its port.
8. Suggested build order
This is a small project — keep it top-down and verify each step before the next:
- Skeleton —
index.htmlwith the header, composer, list and footer, styled with the accent colour; empty state visible. Serve it and confirm it loads. - Model + storage — the
Todoshape, load/save tolocalStorage(with the corrupt-value fallback), and rendering the list from the stored array. - Interactions — add, toggle, delete, clear-completed, filters, counts, keyboard support.
- Polish + quality — responsive layout, accessibility pass, injection-safe rendering, README quickstart, and walk every acceptance box.