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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Phase 9 bulk-action toolbar. Surfaces only when selection is
|
||||
non-empty, so the unselected default state is unchanged. -->
|
||||
{#if selected.size > 0}
|
||||
<div
|
||||
class="mx-7 mb-3 flex items-center gap-3 px-4 py-2 rounded-lg border border-border-subtle bg-bg-elevated text-[12px]"
|
||||
role="toolbar"
|
||||
aria-label="Bulk actions for selected transcripts"
|
||||
>
|
||||
<span class="text-text-tertiary">{selected.size} selected</span>
|
||||
<button
|
||||
class="text-text hover:underline"
|
||||
onclick={selectAllVisible}
|
||||
>Select all</button>
|
||||
<button
|
||||
class="text-text-tertiary hover:text-text"
|
||||
onclick={clearSelection}
|
||||
>Clear</button>
|
||||
<div class="ml-auto flex gap-4">
|
||||
<button
|
||||
class="text-text hover:underline"
|
||||
onclick={bulkExport}
|
||||
>Export selected</button>
|
||||
<button
|
||||
class="text-danger hover:underline"
|
||||
onclick={bulkDelete}
|
||||
>Delete selected</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- History list -->
|
||||
<div class="flex-1 px-7 pb-4 min-h-0">
|
||||
<Card classes="h-full flex flex-col overflow-hidden">
|
||||
@@ -469,7 +563,7 @@
|
||||
<div class="absolute left-0 right-0" style="top: {top}px; min-height: {height}px">
|
||||
<!-- Compact row -->
|
||||
<div
|
||||
class="flex items-center gap-3 px-4 py-3 border-b border-border-subtle bg-bg-card hover:bg-hover cursor-pointer"
|
||||
class="flex items-center gap-3 px-4 py-3 border-b border-border-subtle bg-bg-card hover:bg-hover cursor-pointer {selected.has(item.id) ? 'bg-accent/5' : ''}"
|
||||
style="transition-duration: var(--duration-ui)"
|
||||
onclick={() => 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}
|
||||
>
|
||||
<!-- Phase 9 bulk-select checkbox. Stays unobtrusive
|
||||
until checked or hovered; e.stopPropagation keeps
|
||||
the click off the row-expand toggle. -->
|
||||
<input
|
||||
type="checkbox"
|
||||
class="w-3.5 h-3.5 flex-shrink-0 accent-accent cursor-pointer opacity-50 hover:opacity-100 {selected.has(item.id) ? 'opacity-100' : ''}"
|
||||
aria-label={`Select transcript ${item.title || item.id}`}
|
||||
checked={selected.has(item.id)}
|
||||
onclick={(e) => { e.stopPropagation(); toggleSelected(item.id); }}
|
||||
onkeydown={(e) => { if (e.key === " " || e.key === "Enter") e.stopPropagation(); }}
|
||||
/>
|
||||
|
||||
<!-- Play button (if audio exists) -->
|
||||
{#if item.audioPath}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user