From c26d82c26a3b3e7148966019c89cea30db2913dd Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 24 Apr 2026 23:51:27 +0100 Subject: [PATCH] feat(phase9): History bulk select + bulk export Slim leading checkbox on every row, tinted-row state when selected. Bulk-action toolbar appears only when selection is non-empty: select all (visible), clear, export selected (via exportTranscriptsToDir), delete selected (single confirm). Esc clears selection. Cmd/Ctrl+A selects all visible when focus is inside the list and not in a text input. Stop-propagation on the checkbox keeps the click off the row-expand toggle. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/pages/HistoryPage.svelte | 108 ++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte index d1b88c5..9b8c54d 100644 --- a/src/lib/pages/HistoryPage.svelte +++ b/src/lib/pages/HistoryPage.svelte @@ -44,6 +44,9 @@ let showStarredOnly = $state(false); let activeTagFilter = $state(null); // null = all; string = tag value let expandedId = $state(null); + // Phase 9 bulk selection. Selection state is frontend-only and resets + // on page refresh by design. + let selected = $state(new Set()); let playingId = $state(null); let audioEl = $state(null); let currentTime = $state(0); @@ -348,6 +351,67 @@ await saveTranscriptAsMarkdown(item); } + // Phase 9 bulk selection handlers. + function toggleSelected(id) { + const next = new Set(selected); + if (next.has(id)) next.delete(id); + else next.add(id); + selected = next; + } + + function clearSelection() { + selected = new Set(); + } + + function selectAllVisible() { + selected = new Set(filtered.map((i) => i.id)); + } + + let selectedItems = $derived(history.filter((i) => selected.has(i.id))); + + async function bulkExport() { + const items = selectedItems; + if (items.length === 0) return; + await exportTranscriptsToDir(items); + clearSelection(); + } + + async function bulkDelete() { + if (selected.size === 0) return; + const yes = confirm( + `Delete ${selected.size} transcript${selected.size === 1 ? "" : "s"}?`, + ); + if (!yes) return; + for (const id of selected) deleteFromHistory(id); + clearSelection(); + } + + // Phase 9 keyboard shortcuts: Escape clears selection, Cmd/Ctrl+A + // selects all visible when the History list owns focus. The + // listEl?.contains check prevents hijacking Cmd+A inside text inputs + // outside the list (e.g. the search box, inline title input). + $effect(() => { + function onKey(e) { + if (e.key === "Escape" && selected.size > 0) { + clearSelection(); + e.preventDefault(); + return; + } + if ((e.key === "a" || e.key === "A") && (e.metaKey || e.ctrlKey)) { + const target = document.activeElement; + const inText = + target && + (target.tagName === "INPUT" || target.tagName === "TEXTAREA"); + if (!inText && listEl && listEl.contains(target)) { + selectAllVisible(); + e.preventDefault(); + } + } + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }); + async function openEditor(item) { try { localStorage.setItem("kon_viewer_item", JSON.stringify(item)); @@ -454,6 +518,36 @@ {/if} + + {#if selected.size > 0} + + {/if} +
@@ -469,7 +563,7 @@
toggleExpand(item.id)} onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleExpand(item.id); } }} @@ -477,6 +571,18 @@ tabindex="0" aria-expanded={expandedId === item.id} > + + { e.stopPropagation(); toggleSelected(item.id); }} + onkeydown={(e) => { if (e.key === " " || e.key === "Enter") e.stopPropagation(); }} + /> + {#if item.audioPath}