From a4cca58289f552f309dab6ae29f85b4f80ebe9d8 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 7 May 2026 09:21:04 +0100 Subject: [PATCH] fix(history): replace native confirm() with inline two-click confirmation Native window.confirm() is OS-styled and breaks the warm-amber UI tone with an abrupt brutalist modal. The brand voice is "calm, informative, solution-first; never blame the user", and confirm() is none of those. Replaced both destructive paths (Clear All, Delete N selected) with a two-click inline pattern: first click arms the trigger and morphs it into "Delete X? [Confirm] [Cancel]" inline pills; auto-cancels after 4s so a user who walked away doesn't return to a primed delete button. No new modal, no new component, no new dependency. Both timers are cleared in onDestroy so unmounting the page mid-arm doesn't leak. --- src/lib/pages/HistoryPage.svelte | 86 ++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte index c88c2c6..8648112 100644 --- a/src/lib/pages/HistoryPage.svelte +++ b/src/lib/pages/HistoryPage.svelte @@ -64,8 +64,41 @@ let containerWidth = $state(0); let scrollTop = $state(0); + // Inline two-click confirmation state for destructive bulk actions. + // First click arms the button (morphs into "Confirm" + "Cancel"), a + // second click confirms. Auto-cancels after CONFIRM_TIMEOUT_MS so a + // user who walked away doesn't return to a primed delete button. + // Replaces the native window.confirm() dialog, which broke the warm + // UI tone with an OS-styled modal. + const CONFIRM_TIMEOUT_MS = 4000; + let clearAllArmed = $state(false); + let bulkDeleteArmed = $state(false); + let clearAllArmTimer: ReturnType | null = null; + let bulkDeleteArmTimer: ReturnType | null = null; + + function armClearAll() { + clearAllArmed = true; + if (clearAllArmTimer) clearTimeout(clearAllArmTimer); + clearAllArmTimer = setTimeout(() => { clearAllArmed = false; }, CONFIRM_TIMEOUT_MS); + } + function disarmClearAll() { + clearAllArmed = false; + if (clearAllArmTimer) { clearTimeout(clearAllArmTimer); clearAllArmTimer = null; } + } + function armBulkDelete() { + bulkDeleteArmed = true; + if (bulkDeleteArmTimer) clearTimeout(bulkDeleteArmTimer); + bulkDeleteArmTimer = setTimeout(() => { bulkDeleteArmed = false; }, CONFIRM_TIMEOUT_MS); + } + function disarmBulkDelete() { + bulkDeleteArmed = false; + if (bulkDeleteArmTimer) { clearTimeout(bulkDeleteArmTimer); bulkDeleteArmTimer = null; } + } + onDestroy(() => { stopPlayback(); + if (clearAllArmTimer) clearTimeout(clearAllArmTimer); + if (bulkDeleteArmTimer) clearTimeout(bulkDeleteArmTimer); }); function itemHasStar(h) { @@ -229,7 +262,7 @@ }); async function clearAll() { - if (!confirm("Delete all history? This can't be undone.")) return; + disarmClearAll(); const ids = history.map((entry) => entry.id); history.splice(0); expandedId = null; @@ -470,10 +503,7 @@ async function bulkDelete() { if (selected.size === 0) return; - const yes = confirm( - `Delete ${selected.size} transcript${selected.size === 1 ? "" : "s"}?`, - ); - if (!yes) return; + disarmBulkDelete(); for (const id of selected) deleteFromHistoryById(id); clearSelection(); } @@ -566,12 +596,24 @@ {/if} {#if history.length > 0} - + {#if clearAllArmed} + Delete all history? This can't be undone. + + + {:else} + + {/if} {/if} @@ -641,15 +683,27 @@ class="text-text-tertiary hover:text-text" onclick={clearSelection} >Clear -
+
- + {#if bulkDeleteArmed} + Delete {selected.size} {selected.size === 1 ? 'transcript' : 'transcripts'}? + + + {:else} + + {/if}
{/if}