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 containerWidth = $state(0);
|
||||||
let scrollTop = $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(() => {
|
onDestroy(() => {
|
||||||
stopPlayback();
|
stopPlayback();
|
||||||
|
if (clearAllArmTimer) clearTimeout(clearAllArmTimer);
|
||||||
|
if (bulkDeleteArmTimer) clearTimeout(bulkDeleteArmTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
function itemHasStar(h) {
|
function itemHasStar(h) {
|
||||||
@@ -229,7 +262,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function clearAll() {
|
async function clearAll() {
|
||||||
if (!confirm("Delete all history? This can't be undone.")) return;
|
disarmClearAll();
|
||||||
const ids = history.map((entry) => entry.id);
|
const ids = history.map((entry) => entry.id);
|
||||||
history.splice(0);
|
history.splice(0);
|
||||||
expandedId = null;
|
expandedId = null;
|
||||||
@@ -470,10 +503,7 @@
|
|||||||
|
|
||||||
async function bulkDelete() {
|
async function bulkDelete() {
|
||||||
if (selected.size === 0) return;
|
if (selected.size === 0) return;
|
||||||
const yes = confirm(
|
disarmBulkDelete();
|
||||||
`Delete ${selected.size} transcript${selected.size === 1 ? "" : "s"}?`,
|
|
||||||
);
|
|
||||||
if (!yes) return;
|
|
||||||
for (const id of selected) deleteFromHistoryById(id);
|
for (const id of selected) deleteFromHistoryById(id);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
@@ -566,13 +596,25 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if history.length > 0}
|
{#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
|
<button
|
||||||
class="btn-md rounded-lg text-text-tertiary hover:text-danger hover:bg-hover"
|
class="btn-md rounded-lg text-text-tertiary hover:text-danger hover:bg-hover"
|
||||||
onclick={clearAll}
|
onclick={armClearAll}
|
||||||
>
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
@@ -641,15 +683,27 @@
|
|||||||
class="text-text-tertiary hover:text-text"
|
class="text-text-tertiary hover:text-text"
|
||||||
onclick={clearSelection}
|
onclick={clearSelection}
|
||||||
>Clear</button>
|
>Clear</button>
|
||||||
<div class="ml-auto flex gap-4">
|
<div class="ml-auto flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
class="text-text hover:underline"
|
class="text-text hover:underline"
|
||||||
onclick={bulkExport}
|
onclick={bulkExport}
|
||||||
>Export selected</button>
|
>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
|
<button
|
||||||
class="text-danger hover:underline"
|
class="text-danger hover:underline"
|
||||||
onclick={bulkDelete}
|
onclick={armBulkDelete}
|
||||||
>Delete selected</button>
|
>Delete selected</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user