eleven / ocho Public

web: to-do — tap a row to check off, newest on top (#203)

Point 8: tapping anywhere on a to-do row toggles done/reopen, not just the
little circle. The row carries the handler; the check button keeps no handler
of its own (so the click bubbles up) but stays a real, labelled <button> for
keyboard use; the remove × stops propagation so it deletes without toggling.

Point 3: new to-dos land at the top — the fold still appends (snapshot-safe,
idempotent), we just render the list reversed.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paul Campbell f72999c23a62dc2441f2b94367d7071b095f97da parent 847647f
2 files changed, +16 −4
  • changelog/2026-07-05-todo-tap-and-newest.md +1 −0
  • web/static/apps/todo.js +15 −4
diff --git a/changelog/2026-07-05-todo-tap-and-newest.md b/changelog/2026-07-05-todo-tap-and-newest.md
new file mode 100644
index 0000000..1256a05
--- /dev/null
+++ b/changelog/2026-07-05-todo-tap-and-newest.md
@@ -0,0 +1 @@
+To-do got quicker: tap anywhere on a to-do to check it off (or tap again to reopen) — no more aiming for the little circle. New to-dos now land at the top of the list.
diff --git a/web/static/apps/todo.js b/web/static/apps/todo.js
index 26caecd..6e9db15 100644
--- a/web/static/apps/todo.js
+++ b/web/static/apps/todo.js
@@ -62,7 +62,8 @@ export const app = defineApp({
.todo-add-btn { flex: 0 0 auto; }
.todo-count { font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: 0.02em; }
.todo-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
-.todo-item { display: flex; align-items: center; gap: 10px; padding: 6px 2px; }
+.todo-item { display: flex; align-items: center; gap: 10px; padding: 6px 8px; margin: 0 -8px; border-radius: 8px; cursor: var(--ui-cursor, pointer); }
+.todo-item:hover { background: var(--bubble-them); }
.todo-check {
flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;
border: 1.5px solid var(--border); background: transparent; color: var(--accent-ink);
@@ -114,16 +115,23 @@ export const app = defineApp({
const list = document.createElement("ul");
list.className = "todo-list";
- for (const item of items) {
+ // Newest on top: the fold appends new items, so render the list reversed.
+ for (const item of [...items].reverse()) {
const li = document.createElement("li");
li.className = "todo-item" + (item.done ? " done" : "");
+ // Tapping anywhere on the row checks it off (or reopens it) — the whole item
+ // is the target, not just the little circle. Remove (×) stops the click from
+ // also toggling (see its handler below).
+ li.onclick = () => ctx.emit({ type: item.done ? "todo.reopen" : "todo.done", id: item.id });
const check = document.createElement("button");
check.type = "button";
check.className = "todo-check";
check.setAttribute("aria-label", item.done ? "Reopen" : "Mark done");
check.textContent = item.done ? "✓" : "";
- check.onclick = () => ctx.emit({ type: item.done ? "todo.reopen" : "todo.done", id: item.id });
+ // No own handler: the click bubbles to the row's toggle. Keeping it a real
+ // <button> means it stays keyboard-focusable and labelled, so Space/Enter
+ // on the circle checks the item off too.
const text = document.createElement("span");
text.className = "todo-text";
@@ -134,7 +142,10 @@ export const app = defineApp({
rm.className = "todo-remove";
rm.setAttribute("aria-label", "Remove");
rm.textContent = "×";
- rm.onclick = () => ctx.emit({ type: "todo.remove", id: item.id });
+ rm.onclick = (e) => {
+ e.stopPropagation(); // remove the item — don't also toggle the row
+ ctx.emit({ type: "todo.remove", id: item.id });
+ };
li.append(check, text, rm);
list.appendChild(li);