mirror of
https://github.com/aculix/negotium.git
synced 2026-09-11 07:28:17 +00:00
feat: offer undo at the moment something is removed
Undo existed but nothing said so. The only route was Cmd/Ctrl+Z, which was documented in a README nobody reads mid-task, and which does not exist on a phone at all. On touch there was simply no way to get a deleted task back. Deleting a task or clearing completed ones now brings up a short-lived bar with a real Undo button, so touch has a route for the first time. It also shows the keyboard shortcut, which is the point: someone learns it once, here, at the only moment they actually care. It uses the existing tokens, sits out of the way at the bottom, clears itself after seven seconds and goes as soon as it is used. The shortcut badge is hidden where no keyboard exists rather than advertising a chord nobody can press.
This commit is contained in:
@@ -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 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{#if undoNotice}
|
||||
<div class="undo-toast" role="status" aria-live="polite" transition:fly={{ y: reduceMotion ? 0 : 12, duration: reduceMotion ? 0 : 200, easing: cubicOut }}>
|
||||
<span class="undo-message">{undoNotice}</span>
|
||||
<button class="undo-action" onclick={undo}>
|
||||
Undo <kbd class="undo-key">{shortcutLabel}</kbd>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user