refactor: move document domain core to lib/document/

Moves ~25 components, utils (search, filename, groupDocuments,
documentStatusLabel, validateFile), bulkSelection store, and
TranscriptionSection sub-component. Fixes broken relative imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-05 13:56:36 +02:00
parent 422e86fbf1
commit e7f8aa5894
77 changed files with 47 additions and 47 deletions

View File

@@ -0,0 +1,154 @@
<script lang="ts">
import { tick } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
export interface FileEntry {
id: string;
/** Present in upload mode only. Edit mode entries reference an existing
* document by `documentId` and have no local file blob. */
file?: File;
/** Present in edit mode only — the server-side document UUID being edited. */
documentId?: string;
title: string;
status: 'idle' | 'error';
previewUrl: string;
}
let {
files,
activeId,
onSelect,
onRemove
}: {
files: FileEntry[];
activeId: string;
onSelect: (id: string) => void;
onRemove: (id: string) => void;
} = $props();
let trackEl = $state<HTMLDivElement | null>(null);
let listEl = $state<HTMLUListElement | null>(null);
const activeAnnouncement = $derived(files.find((f) => f.id === activeId)?.title ?? '');
function scrollPrev() {
trackEl?.scrollBy({ left: -120, behavior: 'smooth' });
}
function scrollNext() {
trackEl?.scrollBy({ left: 120, behavior: 'smooth' });
}
async function handleRemove(entry: FileEntry, index: number) {
const targetId = index > 0 ? files[index - 1].id : (files[index + 1]?.id ?? null);
onRemove(entry.id);
if (targetId) {
await tick();
(listEl?.querySelector<HTMLElement>(`[data-chip-id="${targetId}"]`) ?? null)?.focus();
}
}
$effect(() => {
if (!listEl) return;
const node = listEl;
function handleKeyDown(event: KeyboardEvent) {
const buttons = Array.from(node.querySelectorAll<HTMLElement>('[data-chip-id]'));
if (buttons.length === 0) return;
const focusedIndex = buttons.indexOf(document.activeElement as HTMLElement);
if (focusedIndex === -1) return;
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
event.preventDefault();
const nextIndex = (focusedIndex + 1) % buttons.length;
buttons[nextIndex].focus();
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
event.preventDefault();
const prevIndex = (focusedIndex - 1 + buttons.length) % buttons.length;
buttons[prevIndex].focus();
}
}
node.addEventListener('keydown', handleKeyDown);
return () => node.removeEventListener('keydown', handleKeyDown);
});
</script>
<div aria-live="polite" aria-atomic="true" class="sr-only">{activeAnnouncement}</div>
<div
data-testid="file-switcher-strip"
class="flex h-11 shrink-0 items-center gap-1 border-t border-line bg-pdf-ctrl px-2"
>
<button
type="button"
aria-label={m.bulk_switcher_prev()}
onclick={scrollPrev}
class="flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-sm text-sm text-ink-3 hover:bg-black/10 hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
></button
>
<!-- Gradient fade overlays signal hidden overflow to pointer-only users -->
<div class="relative flex flex-1 overflow-hidden">
<div
class="pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-gradient-to-r from-pdf-ctrl to-transparent"
></div>
<div
class="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-gradient-to-l from-pdf-ctrl to-transparent"
></div>
<div bind:this={trackEl} class="flex flex-1 gap-1 overflow-x-auto" style="scrollbar-width:none">
<ul bind:this={listEl} role="list" class="flex flex-row gap-1 py-1">
{#each files as entry, i (entry.id)}
<li role="listitem" class="inline-flex shrink-0 items-center">
<button
type="button"
tabindex="0"
aria-current={entry.id === activeId ? 'true' : undefined}
data-status={entry.status}
data-chip-id={entry.id}
onclick={() => onSelect(entry.id)}
class={[
'inline-flex cursor-pointer items-center gap-1 rounded-[2px] px-1.5 py-0.5 text-xs font-bold transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent',
entry.id === activeId
? 'bg-accent text-primary'
: 'bg-black/[0.06] text-ink-2 hover:bg-black/10',
entry.status === 'error'
? '!border !border-dashed !border-red-400 !bg-red-50/80 !text-red-700'
: ''
].join(' ')}
>
<span
class={[
'rounded-[2px] px-0.5 text-[11px] font-extrabold opacity-85',
entry.id === activeId ? 'bg-black/20' : 'bg-black/10'
].join(' ')}
>{i + 1}</span
>
<span class="max-w-[8rem] truncate" title={entry.title}>{entry.title}</span>
{#if entry.status === 'error'}
<span class="sr-only">{m.bulk_file_error_chip_label()}</span>
<span aria-hidden="true" class="ml-0.5 font-extrabold text-red-600">!</span>
{/if}
</button>
<button
type="button"
aria-label={m.bulk_remove_file()}
data-remove-id={entry.id}
onclick={() => handleRemove(entry, i)}
class="ml-0.5 flex h-[44px] w-[44px] items-center justify-center text-base text-ink-3 hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
×
</button>
</li>
{/each}
</ul>
</div>
</div>
<button
type="button"
aria-label={m.bulk_switcher_next()}
onclick={scrollNext}
class="flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-sm text-sm text-ink-3 hover:bg-black/10 hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
></button
>
</div>