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.
This commit is contained in:
@@ -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<typeof setTimeout> | null = null;
|
||||
let bulkDeleteArmTimer: ReturnType<typeof setTimeout> | 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,13 +596,25 @@
|
||||
</button>
|
||||
{/if}
|
||||
{#if history.length > 0}
|
||||
{#if clearAllArmed}
|
||||
<span class="text-[12px] text-text-secondary">Delete all history? This can't be undone.</span>
|
||||
<button
|
||||
class="btn-md rounded-lg bg-danger/10 border border-danger/30 text-danger hover:bg-danger/20"
|
||||
onclick={clearAll}
|
||||
>Confirm</button>
|
||||
<button
|
||||
class="btn-md rounded-lg text-text-tertiary hover:text-text hover:bg-hover"
|
||||
onclick={disarmClearAll}
|
||||
>Cancel</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn-md rounded-lg text-text-tertiary hover:text-danger hover:bg-hover"
|
||||
onclick={clearAll}
|
||||
onclick={armClearAll}
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
@@ -641,15 +683,27 @@
|
||||
class="text-text-tertiary hover:text-text"
|
||||
onclick={clearSelection}
|
||||
>Clear</button>
|
||||
<div class="ml-auto flex gap-4">
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<button
|
||||
class="text-text hover:underline"
|
||||
onclick={bulkExport}
|
||||
>Export selected</button>
|
||||
{#if bulkDeleteArmed}
|
||||
<span class="text-text-secondary">Delete {selected.size} {selected.size === 1 ? 'transcript' : 'transcripts'}?</span>
|
||||
<button
|
||||
class="text-danger font-medium hover:underline"
|
||||
onclick={bulkDelete}
|
||||
>Confirm</button>
|
||||
<button
|
||||
class="text-text-tertiary hover:text-text hover:underline"
|
||||
onclick={disarmBulkDelete}
|
||||
>Cancel</button>
|
||||
{:else}
|
||||
<button
|
||||
class="text-danger hover:underline"
|
||||
onclick={bulkDelete}
|
||||
onclick={armBulkDelete}
|
||||
>Delete selected</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user