refactor: move document transcription, annotation, viewer sub-packages

- transcription/: TranscriptionBlock, Column, EditView, PanelHeader, ReadView,
  Section + transcriptionMarkers, blockConflictMerge, saveBlockWithConflictRetry
  + useBlockAutoSave, useBlockDragDrop hooks
- annotation/: AnnotationLayer, AnnotationShape, AnnotationEditOverlay
- viewer/: PdfViewer, PdfControls + useFileLoader, usePdfRenderer hooks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-05 14:01:39 +02:00
parent e7f8aa5894
commit 1e656d2db4
43 changed files with 32 additions and 27 deletions

View File

@@ -0,0 +1,63 @@
import type { PersonMention, TranscriptionBlockData } from '$lib/types';
import {
BlockConflictResolvedError,
mergeBlockOnConflict
} from '$lib/document/transcription/blockConflictMerge';
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
type Args = {
fetchImpl: typeof fetch;
documentId: string;
blockId: string;
text: string;
mentionedPersons: PersonMention[];
};
/**
* Persists a transcription block edit, with built-in handling for the
* rename-mid-edit conflict (B12b).
*
* - 200/204 → resolves with the server's updated block.
* - 409 → refetches the latest server block, merges it with the
* transcriber's unsaved input via mergeBlockOnConflict, and
* throws BlockConflictResolvedError carrying the merged
* snapshot. The caller is responsible for updating local
* state with `err.merged` before surfacing the error.
* - other → throws Error('Save failed').
*
* Validates both ids against the UUID pattern before any fetch fires
* (Sina #5505 — defence-in-depth path-injection guard).
*/
export async function saveBlockWithConflictRetry(args: Args): Promise<TranscriptionBlockData> {
const { fetchImpl, documentId, blockId, text, mentionedPersons } = args;
if (!UUID_RE.test(documentId) || !UUID_RE.test(blockId)) {
throw new Error(`Invalid id for save: doc=${documentId} block=${blockId}`);
}
const url = `/api/documents/${documentId}/transcription-blocks/${blockId}`;
const res = await fetchImpl(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, mentionedPersons })
});
if (res.status === 409) {
const fresh = await fetchImpl(url);
if (!fresh.ok) {
throw new BlockConflictResolvedError(blockId);
}
const serverBlock = (await fresh.json()) as TranscriptionBlockData;
const merged = mergeBlockOnConflict({
serverBlock,
localText: text,
localMentions: mentionedPersons
});
const err = new BlockConflictResolvedError(blockId);
(err as BlockConflictResolvedError & { merged: TranscriptionBlockData }).merged = merged;
throw err;
}
if (!res.ok) throw new Error('Save failed');
return (await res.json()) as TranscriptionBlockData;
}