diff --git a/README.md b/README.md index 94b4407..70a9f3a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Built with Svelte for speed and simplicity. No overwhelming features, no endless ### Core Functionality - ✅ **Add, complete, and delete tasks** with smooth animations -- ↩️ **Undo** - `Cmd/Ctrl+Z` brings back a deleted task, in its original position +- ↩️ **Undo** - Deleted something by mistake? An Undo button appears, and `Cmd/Ctrl+Z` works too. The task returns to its original position - 📅 **Today & Tomorrow lists** - Plan ahead with separate task lists - 🔄 **Unfinished work carries over** - When a new day begins, tasks you didn't finish move to Today; completed ones are cleared away - ✏️ **Edit a task** - Fix a typo without deleting and retyping it @@ -92,7 +92,7 @@ The date, storage, rollover, task and undo logic lives in `src/lib/` as plain mo - **Complete a task**: Click the checkbox next to the task - **Edit a task**: Click its text. Enter saves, Escape cancels, and clicking away saves too - **Delete a task**: Click the delete icon on the task (it shows on hover, and is always visible on touch), or press `Delete` with the task focused -- **Undo a delete**: Press `Cmd/Ctrl+Z`. The task returns to where it was +- **Undo a delete**: An Undo button appears for a few seconds after anything is removed. `Cmd/Ctrl+Z` does the same. Either way the task returns to where it was - **Move a task to Tomorrow**: Click the arrow on the task, or press `Alt+→`. From Tomorrow, the arrow points back and `Alt+←` returns it to Today - **Reorder tasks**: Drag with a mouse, long-press then drag on touch, or focus a task and press `Alt+↑`/`Alt+↓` - **Clear input**: Press Escape while the input is focused diff --git a/src/App.svelte b/src/App.svelte index 246e3ee..ebca6dc 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -62,6 +62,7 @@ if (index === -1) return; undoStack.push({ type: 'delete', task: tasks[index], index }); setTasks(taskOps.deleteTask(tasks, id)); + showUndo('Task deleted'); } function clearCompleted() { @@ -74,6 +75,7 @@ if (removed.length === 0) return; undoStack.push({ type: 'clearCompleted', removed }); setTasks(taskOps.clearCompleted(tasks)); + showUndo(removed.length === 1 ? '1 completed task cleared' : `${removed.length} completed tasks cleared`); } const viewingToday = $derived(selectedKey === todayKey); @@ -129,9 +131,35 @@ tasks = moveTaskToDay(storage, selectedKey, destination, taskId); } + /** + * Undo used to be reachable only by Cmd/Ctrl+Z, which nothing announced and + * which does not exist on a phone at all. This is the one moment it is worth + * saying something: right after work disappears. It carries the shortcut too, + * so a keyboard user learns it once, here, rather than from a README. + */ + let undoNotice = $state(null); + let undoNoticeTimer = null; + + const UNDO_NOTICE_MS = 7000; + const shortcutLabel = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) + ? '\u2318Z' + : 'Ctrl+Z'; + + function showUndo(message) { + undoNotice = message; + clearTimeout(undoNoticeTimer); + undoNoticeTimer = setTimeout(() => { undoNotice = null; }, UNDO_NOTICE_MS); + } + + function dismissUndo() { + clearTimeout(undoNoticeTimer); + undoNotice = null; + } + function undo() { const entry = undoStack.pop(); if (entry) setTasks(applyUndo(tasks, entry)); + dismissUndo(); } function toggleTheme() { @@ -736,3 +764,12 @@ + +{#if undoNotice} +
+ {undoNotice} + +
+{/if} diff --git a/src/style.css b/src/style.css index a85a91c..302b5f2 100644 --- a/src/style.css +++ b/src/style.css @@ -701,3 +701,80 @@ html { scroll-behavior: smooth; } + +/* Shown only after something is removed, and only long enough to act on. + It carries a real button because Cmd/Ctrl+Z does not exist on a phone, + so on touch this is the only way back. */ +.undo-toast { + position: fixed; + left: 50%; + transform: translateX(-50%); + bottom: calc(24px + env(safe-area-inset-bottom)); + z-index: 200; + display: flex; + align-items: center; + gap: 16px; + max-width: calc(100vw - 32px); + padding: 12px 12px 12px 18px; + border-radius: 12px; + background-color: var(--bg-surface); + border: 1px solid var(--border); + box-shadow: + 0 12px 28px rgba(0, 0, 0, 0.16), + 0 4px 8px rgba(0, 0, 0, 0.08); + font-size: 14px; + color: var(--text-primary); +} + +html.dark .undo-toast { + box-shadow: + 0 12px 28px rgba(0, 0, 0, 0.55), + 0 4px 8px rgba(0, 0, 0, 0.4); +} + +.undo-message { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.undo-action { + display: inline-flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + padding: 6px 12px; + border: none; + border-radius: 8px; + background: transparent; + font-family: inherit; + font-size: 14px; + font-weight: 500; + color: var(--accent-text); + cursor: pointer; + transition: background-color 200ms ease; +} + +.undo-action:hover { + background-color: var(--hover); +} + +/* Teaching the shortcut is the point, so it only shows where one exists. */ +.undo-key { + font-family: var(--font-family); + font-size: 12px; + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: 4px; + padding: 1px 5px; +} + +@media (pointer: coarse) { + .undo-key { + display: none; + } + + .undo-action { + min-height: 44px; + } +}