feat: Expandable metadata drawer + transcription system (#175, #176) #178

Merged
marcel merged 47 commits from feat/issue-175-176-metadata-drawer-transcription into main 2026-04-06 11:31:11 +02:00
6 changed files with 76 additions and 378 deletions
Showing only changes of commit f3c29ffe58 - Show all commits

View File

@@ -10,29 +10,18 @@ type DrawRect = {
let {
annotations = [],
canAnnotate,
canDraw,
color,
dimColor,
onDraw,
onDelete,
commentCounts,
onAnnotationClick
}: {
annotations: Annotation[];
canAnnotate: boolean;
canDraw: boolean;
color: string;
dimColor?: string;
onDraw: (rect: { x: number; y: number; width: number; height: number }) => void;
onDelete: (id: string) => void;
commentCounts?: Record<string, number>;
onDraw: (rect: DrawRect) => void;
onAnnotationClick?: (id: string) => void;
} = $props();
function isDimmed(annotation: Annotation): boolean {
if (!dimColor) return false;
return annotation.color.toLowerCase() === dimColor.toLowerCase();
}
let drawStart = $state<{ x: number; y: number } | null>(null);
let drawRect = $state<DrawRect | null>(null);
@@ -52,7 +41,7 @@ function getNormalizedCoords(event: PointerEvent, element: HTMLElement): { x: nu
}
function handlePointerDown(event: PointerEvent) {
if (!canAnnotate) return;
if (!canDraw) return;
if ((event.target as HTMLElement).closest('[data-annotation]')) return;
@@ -65,7 +54,7 @@ function handlePointerDown(event: PointerEvent) {
}
function handlePointerMove(event: PointerEvent) {
if (!canAnnotate || !drawStart) return;
if (!canDraw || !drawStart) return;
const container = event.currentTarget as HTMLElement;
const coords = getNormalizedCoords(event, container);
@@ -79,7 +68,7 @@ function handlePointerMove(event: PointerEvent) {
}
function handlePointerUp(event: PointerEvent) {
if (!canAnnotate || !drawStart || !drawRect) return;
if (!canDraw || !drawStart || !drawRect) return;
const container = event.currentTarget as HTMLElement;
const coords = getNormalizedCoords(event, container);
@@ -100,7 +89,7 @@ function handlePointerUp(event: PointerEvent) {
let hoveredId = $state<string | null>(null);
const containerStyle = $derived(
`position: absolute; top: 0; left: 0; width: 100%; height: 100%;${canAnnotate ? ' cursor: crosshair; touch-action: none;' : ''}`
`position: absolute; top: 0; left: 0; width: 100%; height: 100%;${canDraw ? ' cursor: crosshair; touch-action: none;' : ''}`
);
</script>
@@ -117,9 +106,11 @@ const containerStyle = $derived(
data-annotation
role="button"
tabindex="0"
aria-label="Kommentare anzeigen"
aria-label="Block anzeigen"
onclick={() => onAnnotationClick?.(annotation.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') onAnnotationClick?.(annotation.id); }}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onAnnotationClick?.(annotation.id);
}}
onpointerenter={() => (hoveredId = annotation.id)}
onpointerleave={() => (hoveredId = null)}
style="
@@ -130,73 +121,11 @@ const containerStyle = $derived(
height: {annotation.height * 100}%;
background-color: {hexToRgba(annotation.color, hoveredId === annotation.id ? 0.5 : 0.3)};
box-shadow: {hoveredId === annotation.id ? `inset 0 0 0 2px ${hexToRgba(annotation.color, 0.8)}` : 'none'};
opacity: {isDimmed(annotation) ? 0.3 : 1};
pointer-events: {isDimmed(annotation) ? 'none' : 'auto'};
transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.3s ease;
{onAnnotationClick && !canAnnotate ? 'cursor: pointer;' : ''}
pointer-events: auto;
cursor: pointer;
transition: background-color 0.15s ease, box-shadow 0.15s ease;
"
>
{#if canAnnotate}
<button
aria-label="Annotation löschen"
onclick={(e) => {
e.stopPropagation();
const count = commentCounts?.[annotation.id] ?? 0;
if (count > 0) {
const msg =
count === 1
? 'Diese Annotation hat 1 Kommentar. Beim Löschen wird er ebenfalls entfernt. Fortfahren?'
: `Diese Annotation hat ${count} Kommentare. Beim Löschen werden sie ebenfalls entfernt. Fortfahren?`;
if (!window.confirm(msg)) return;
}
onDelete(annotation.id);
}}
style="
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
background-color: #ef4444;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
padding: 0;
pointer-events: auto;
">×</button
>
{/if}
{#if (commentCounts?.[annotation.id] ?? 0) > 0}
<div
style="
position: absolute;
bottom: -10px;
right: -10px;
background-color: #002850;
color: white;
font-size: 11px;
font-family: sans-serif;
font-weight: 600;
padding: 2px 6px;
border-radius: 999px;
min-width: 20px;
text-align: center;
white-space: nowrap;
pointer-events: none;
line-height: 18px;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
"
>
{commentCounts?.[annotation.id]}
</div>
{/if}
</div>
></div>
{/each}
{#if drawRect && drawRect.width > 0}

View File

@@ -18,7 +18,7 @@ type Annotation = {
createdAt: string;
};
function makeAnnotation(id = 'ann-1'): Annotation {
function makeAnnotation(id = 'ann-1', color = '#00C7B1'): Annotation {
return {
id,
documentId: 'doc-1',
@@ -27,7 +27,7 @@ function makeAnnotation(id = 'ann-1'): Annotation {
y: 0.1,
width: 0.3,
height: 0.2,
color: '#ff0000',
color,
createdAt: new Date().toISOString()
};
}
@@ -36,87 +36,48 @@ describe('AnnotationLayer', () => {
it('renders a colored element for each annotation', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1'), makeAnnotation('ann-2')],
canAnnotate: false,
color: '#ff0000',
onDraw: () => {},
onDelete: () => {}
canDraw: false,
color: '#00C7B1',
onDraw: () => {}
});
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
await expect.element(page.getByTestId('annotation-ann-2')).toBeInTheDocument();
});
it('shows a delete button for each annotation when canAnnotate is true', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1')],
canAnnotate: true,
color: '#ff0000',
onDraw: () => {},
onDelete: () => {}
});
await expect
.element(page.getByRole('button', { name: /annotation löschen/i }))
.toBeInTheDocument();
});
it('does not show delete buttons when canAnnotate is false', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1')],
canAnnotate: false,
color: '#ff0000',
onDraw: () => {},
onDelete: () => {}
});
expect(page.getByRole('button', { name: /annotation löschen/i }).query()).toBeNull();
});
it('dims annotations matching dimColor', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1')],
canAnnotate: false,
color: '#00C7B1',
dimColor: '#ff0000',
onDraw: () => {},
onDelete: () => {}
});
const el = page.getByTestId('annotation-ann-1');
await expect.element(el).toBeInTheDocument();
const style = el.element().style;
expect(style.opacity).toBe('0.3');
expect(style.pointerEvents).toBe('none');
});
it('does not dim annotations that do not match dimColor', async () => {
const ann = makeAnnotation('ann-1');
ann.color = '#00C7B1';
render(AnnotationLayer, {
annotations: [ann],
canAnnotate: false,
color: '#00C7B1',
dimColor: '#ff0000',
onDraw: () => {},
onDelete: () => {}
});
const el = page.getByTestId('annotation-ann-1');
await expect.element(el).toBeInTheDocument();
const style = el.element().style;
expect(style.opacity).toBe('1');
});
it('has crosshair cursor when canAnnotate is true', async () => {
it('has crosshair cursor when canDraw is true', async () => {
render(AnnotationLayer, {
annotations: [],
canAnnotate: true,
canDraw: true,
color: '#00C7B1',
onDraw: () => {},
onDelete: () => {}
onDraw: () => {}
});
const container = document.querySelector('[role="presentation"]')!;
expect(container.getAttribute('style')).toContain('cursor: crosshair');
});
it('does not have crosshair cursor when canDraw is false', async () => {
render(AnnotationLayer, {
annotations: [],
canDraw: false,
color: '#00C7B1',
onDraw: () => {}
});
const container = document.querySelector('[role="presentation"]')!;
expect(container.getAttribute('style')).not.toContain('cursor: crosshair');
});
it('does not show delete buttons (annotations owned by blocks)', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1')],
canDraw: true,
color: '#00C7B1',
onDraw: () => {}
});
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
expect(page.getByRole('button', { name: /löschen/i }).query()).toBeNull();
});
});

View File

@@ -4,7 +4,6 @@ import { slide } from 'svelte/transition';
import { formatDate } from '$lib/utils/personFormat';
import { clickOutside } from '$lib/actions/clickOutside';
import PersonChipRow from './PersonChipRow.svelte';
import AnnotateHintStrip from './AnnotateHintStrip.svelte';
import OverflowPillButton from './OverflowPillButton.svelte';
import DocumentMetadataDrawer from './DocumentMetadataDrawer.svelte';
@@ -28,20 +27,11 @@ type Doc = {
type Props = {
doc: Doc;
canWrite: boolean;
canAnnotate: boolean;
fileUrl: string;
annotateMode: boolean;
transcribeMode: boolean;
};
let {
doc,
canWrite,
canAnnotate,
fileUrl,
annotateMode = $bindable(),
transcribeMode = $bindable()
}: Props = $props();
let { doc, canWrite, fileUrl, transcribeMode = $bindable() }: Props = $props();
let detailsOpen = $state(false);
@@ -56,55 +46,10 @@ const longDate = $derived(doc.documentDate ? formatDate(doc.documentDate, 'long'
let mobileMenuOpen = $state(false);
</script>
{#snippet annotateBtn(mobile: boolean)}
<button
onclick={() => {
annotateMode = true;
if (mobile) mobileMenuOpen = false;
}}
aria-label={m.doc_panel_annotate()}
aria-pressed={false}
class={mobile
? 'flex w-full items-center gap-2 rounded px-3 py-2 text-left text-[16px] text-ink transition hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary'
: 'hidden items-center gap-1.5 rounded border border-primary px-3 py-1.5 font-sans text-[16px] font-medium text-ink transition hover:bg-primary hover:text-primary-fg focus-visible:ring-2 focus-visible:ring-primary md:flex'}
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Note/Note-Add-MD.svg"
alt=""
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
{m.doc_panel_annotate()}
</button>
{/snippet}
{#snippet annotateStopBtn(mobile: boolean)}
<button
onclick={() => {
annotateMode = false;
if (mobile) mobileMenuOpen = false;
}}
aria-label={m.doc_panel_annotate_stop()}
aria-pressed={true}
class={mobile
? 'flex w-full items-center gap-2 rounded bg-primary px-3 py-2 text-left text-[16px] text-primary-fg transition focus-visible:ring-2 focus-visible:ring-primary'
: 'flex items-center gap-1.5 rounded bg-primary px-3 py-1.5 font-sans text-[16px] font-medium text-primary-fg transition focus-visible:ring-2 focus-visible:ring-primary'}
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Note/Note-Add-MD.svg"
alt=""
aria-hidden="true"
class="h-5 w-5 shrink-0 invert"
/>
{m.doc_panel_annotate_stop()}
</button>
{/snippet}
{#snippet transcribeBtn(mobile: boolean)}
<button
onclick={() => {
transcribeMode = true;
annotateMode = false;
if (mobile) mobileMenuOpen = false;
}}
aria-label={m.transcription_mode_label()}
@@ -256,7 +201,7 @@ let mobileMenuOpen = $state(false);
<!-- Action buttons -->
<div class="flex shrink-0 items-center gap-1.5 pr-3 font-sans">
{#if canWrite && isPdf && !transcribeMode && !annotateMode}
{#if canWrite && isPdf && !transcribeMode}
{@render transcribeBtn(false)}
{/if}
@@ -264,15 +209,7 @@ let mobileMenuOpen = $state(false);
{@render transcribeStopBtn(false)}
{/if}
{#if canAnnotate && isPdf && !annotateMode && !transcribeMode}
{@render annotateBtn(false)}
{/if}
{#if canAnnotate && isPdf && annotateMode}
{@render annotateStopBtn(false)}
{/if}
{#if canWrite && !annotateMode && !transcribeMode}
{#if canWrite && !transcribeMode}
<a
href="/documents/{doc.id}/edit"
aria-label={m.btn_edit()}
@@ -288,12 +225,12 @@ let mobileMenuOpen = $state(false);
</a>
{/if}
{#if doc.filePath && !annotateMode}
{#if doc.filePath && !transcribeMode}
{@render downloadLink(false)}
{/if}
<!-- Kebab menu — mobile only, contains actions hidden below md -->
{#if (canAnnotate && isPdf) || doc.filePath}
{#if (canWrite && isPdf) || doc.filePath}
<div
role="group"
class="relative md:hidden"
@@ -321,14 +258,10 @@ let mobileMenuOpen = $state(false);
role="menu"
class="absolute top-full right-0 z-50 mt-1 min-w-[200px] rounded-md border border-line bg-surface p-2 shadow-lg"
>
{#if canWrite && isPdf && !transcribeMode && !annotateMode}
{#if canWrite && isPdf && !transcribeMode}
{@render transcribeBtn(true)}
{/if}
{#if canAnnotate && isPdf && !annotateMode && !transcribeMode}
{@render annotateBtn(true)}
{/if}
{#if doc.filePath}
{@render downloadLink(true)}
{/if}
@@ -339,9 +272,6 @@ let mobileMenuOpen = $state(false);
</div>
</div>
<!-- Hint strip — only when annotateMode, only at ≥768px -->
<AnnotateHintStrip annotateMode={annotateMode} />
<!-- Metadata drawer -->
{#if detailsOpen}
<div transition:slide={{ duration: 200 }}>

View File

@@ -16,10 +16,8 @@ type Props = {
fileUrl: string;
isLoading: boolean;
error: string;
annotateMode: boolean;
transcribeMode?: boolean;
activeAnnotationId: string | null;
activeAnnotationPage: number | null;
onAnnotationClick: (id: string) => void;
onTranscriptionDraw?: (rect: DrawRect) => void;
};
@@ -29,10 +27,8 @@ let {
fileUrl,
isLoading,
error,
annotateMode = $bindable(),
transcribeMode = false,
activeAnnotationId = $bindable(),
activeAnnotationPage = $bindable(),
onAnnotationClick,
onTranscriptionDraw
}: Props = $props();
@@ -86,10 +82,8 @@ let {
<PdfViewer
url={fileUrl}
documentId={doc.id}
bind:annotateMode={annotateMode}
transcribeMode={transcribeMode}
bind:activeAnnotationId={activeAnnotationId}
bind:activeAnnotationPage={activeAnnotationPage}
onAnnotationClick={onAnnotationClick}
onTranscriptionDraw={onTranscriptionDraw}
documentFileHash={doc.fileHash ?? null}

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist';
import AnnotationLayer from './AnnotationLayer.svelte';
import type { Annotation } from '$lib/types';
@@ -11,20 +10,16 @@ type DrawRect = { x: number; y: number; width: number; height: number; pageNumbe
let {
url,
documentId = '',
annotateMode = $bindable(false),
transcribeMode = false,
activeAnnotationId = $bindable<string | null>(null),
activeAnnotationPage = $bindable<number | null>(null),
onAnnotationClick,
onTranscriptionDraw,
documentFileHash
}: {
url: string;
documentId?: string;
annotateMode?: boolean;
transcribeMode?: boolean;
activeAnnotationId?: string | null;
activeAnnotationPage?: number | null;
onAnnotationClick?: (id: string) => void;
onTranscriptionDraw?: (rect: DrawRect) => void;
documentFileHash?: string | null;
@@ -51,13 +46,9 @@ let pdfjsLib: typeof import('pdfjs-dist') | null = null;
let pdfjsReady = $state(false);
let annotations = $state<Annotation[]>([]);
let annotateColor = $state('#ffff00');
let commentCounts = new SvelteMap<string, number>();
let showAnnotations = $state(true);
const TRANSCRIPTION_COLOR = '#00C7B1';
const drawingEnabled = $derived(annotateMode || transcribeMode);
const drawColor = $derived(transcribeMode ? TRANSCRIPTION_COLOR : annotateColor);
const visibleAnnotations = $derived(
annotations.filter((a) => !a.fileHash || !documentFileHash || a.fileHash === documentFileHash)
@@ -174,30 +165,12 @@ async function prerender(doc: PDFDocumentProxy, pageNum: number) {
}
}
async function loadCommentCounts(docId: string, anns: Annotation[]) {
await Promise.all(
anns.map(async (a) => {
try {
const res = await fetch(`/api/documents/${docId}/annotations/${a.id}/comments`);
if (res.ok) {
const threads = (await res.json()) as Array<{ replies: unknown[] }>;
const total = threads.reduce((sum, t) => sum + 1 + t.replies.length, 0);
commentCounts.set(a.id, total);
}
} catch {
// ignore
}
})
);
}
async function loadAnnotations(docId: string) {
if (!docId) return;
try {
const res = await fetch(`/api/documents/${docId}/annotations`);
if (res.ok) {
annotations = await res.json();
await loadCommentCounts(docId, annotations);
}
} catch {
// ignore
@@ -205,57 +178,13 @@ async function loadAnnotations(docId: string) {
}
async function handleDraw(rect: { x: number; y: number; width: number; height: number }) {
if (!documentId) return;
if (transcribeMode) {
await onTranscriptionDraw?.({ ...rect, pageNumber: currentPage });
await loadAnnotations(documentId);
return;
}
try {
const res = await fetch(`/api/documents/${documentId}/annotations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pageNumber: currentPage,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
color: annotateColor
})
});
if (res.ok) {
const created: Annotation = await res.json();
annotations = [...annotations, created];
activeAnnotationId = created.id;
activeAnnotationPage = created.pageNumber;
onAnnotationClick?.(created.id);
}
} catch {
// ignore
}
}
async function handleAnnotationDelete(annotationId: string) {
if (!documentId) return;
try {
const res = await fetch(`/api/documents/${documentId}/annotations/${annotationId}`, {
method: 'DELETE'
});
if (res.ok) {
annotations = annotations.filter((a) => a.id !== annotationId);
}
} catch {
// ignore
}
if (!documentId || !transcribeMode) return;
await onTranscriptionDraw?.({ ...rect, pageNumber: currentPage });
await loadAnnotations(documentId);
}
function handleAnnotationClick(id: string) {
activeAnnotationId = id;
const ann = annotations.find((a) => a.id === id);
activeAnnotationPage = ann?.pageNumber ?? null;
onAnnotationClick?.(id);
}
@@ -283,7 +212,7 @@ $effect(() => {
});
$effect(() => {
if (annotateMode) showAnnotations = true;
if (transcribeMode) showAnnotations = true;
});
function prevPage() {
@@ -429,16 +358,6 @@ function zoomOut() {
</button>
</div>
<!-- Color picker (shown in annotate mode) -->
{#if annotateMode}
<input
type="color"
bind:value={annotateColor}
aria-label="Farbe wählen"
class="h-6 w-6 cursor-pointer rounded border-0 bg-transparent p-0"
title="Farbe wählen"
/>
{/if}
<!-- Annotation visibility toggle (shown when annotations exist) -->
{#if annotations.length > 0}
<button
@@ -503,12 +422,9 @@ function zoomOut() {
{#if showAnnotations}
<AnnotationLayer
annotations={visibleAnnotations.filter((a) => a.pageNumber === currentPage)}
canAnnotate={drawingEnabled}
color={drawColor}
dimColor={transcribeMode ? '#ffff00' : annotateMode ? TRANSCRIPTION_COLOR : undefined}
canDraw={transcribeMode}
color={TRANSCRIPTION_COLOR}
onDraw={handleDraw}
onDelete={handleAnnotationDelete}
commentCounts={Object.fromEntries(commentCounts)}
onAnnotationClick={handleAnnotationClick}
/>
{/if}

View File

@@ -1,25 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/state';
import DocumentTopBar from '$lib/components/DocumentTopBar.svelte';
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
import AnnotationSidePanel from '$lib/components/AnnotationSidePanel.svelte';
import TranscriptionEditView from '$lib/components/TranscriptionEditView.svelte';
import type { TranscriptionBlockData } from '$lib/types';
let { data } = $props();
const targetCommentId = $derived(page.url.searchParams.get('commentId'));
const targetAnnotationId = $derived(page.url.searchParams.get('annotationId'));
const doc = $derived(data.document);
const canWrite = $derived(data.canWrite ?? false);
const canComment = $derived((data.canAnnotate || data.canWrite) ?? false);
const canAdmin = $derived(
(data.user?.groups as Array<{ permissions: string[] }> | undefined)?.some((g) =>
g.permissions.includes('ADMIN')
) ?? false
);
const currentUserId = $derived((data.user?.id as string | undefined) ?? null);
// ── File loading ──────────────────────────────────────────────────────────────
@@ -57,12 +46,10 @@ async function loadFile(id: string) {
}
}
// ── Mode state (mutually exclusive) ──────────────────────────────────────────
// ── Mode state ───────────────────────────────────────────────────────────────
let annotateMode = $state(false);
let transcribeMode = $state(false);
let activeAnnotationId = $state<string | null>(null);
let activeAnnotationPage = $state<number | null>(null);
// ── Transcription blocks ─────────────────────────────────────────────────────
@@ -137,6 +124,18 @@ function handleBlockFocus(blockId: string) {
}
}
function handleAnnotationClick(annotationId: string) {
activeAnnotationId = annotationId;
// In transcribe mode, focus the block that owns this annotation
if (transcribeMode) {
const block = transcriptionBlocks.find((b) => b.annotationId === annotationId);
if (block) {
const el = document.querySelector(`[data-block-id="${block.id}"]`);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
}
// Load blocks when transcribe mode is entered
$effect(() => {
if (transcribeMode) {
@@ -151,10 +150,6 @@ let navHeight = $state(0);
onMount(() => {
navHeight = document.querySelector('header')?.getBoundingClientRect().height ?? 0;
if (targetAnnotationId) {
activeAnnotationId = targetAnnotationId;
}
if (doc?.id) {
localStorage.setItem(
'familienarchiv.lastVisited',
@@ -163,13 +158,8 @@ onMount(() => {
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (transcribeMode) {
transcribeMode = false;
} else if (activeAnnotationId) {
activeAnnotationId = null;
activeAnnotationPage = null;
}
if (e.key === 'Escape' && transcribeMode) {
transcribeMode = false;
}
}
document.addEventListener('keydown', onKeyDown);
@@ -189,9 +179,7 @@ onMount(() => {
<DocumentTopBar
doc={doc}
canWrite={canWrite}
canAnnotate={data.canAnnotate ?? false}
fileUrl={fileUrl}
bind:annotateMode={annotateMode}
bind:transcribeMode={transcribeMode}
/>
@@ -202,39 +190,19 @@ onMount(() => {
fileUrl={fileUrl}
isLoading={isLoading}
error={fileError}
bind:annotateMode={annotateMode}
transcribeMode={transcribeMode}
bind:activeAnnotationId={activeAnnotationId}
bind:activeAnnotationPage={activeAnnotationPage}
onAnnotationClick={(id) => {
activeAnnotationId = id;
}}
onAnnotationClick={handleAnnotationClick}
onTranscriptionDraw={createBlockFromDraw}
/>
</div>
{#if !transcribeMode}
<AnnotationSidePanel
documentId={doc.id}
activeAnnotationId={activeAnnotationId}
activeAnnotationPage={activeAnnotationPage}
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
targetCommentId={targetAnnotationId ? targetCommentId : null}
onClose={() => {
activeAnnotationId = null;
activeAnnotationPage = null;
}}
/>
{/if}
{#if transcribeMode}
<div class="w-[400px] shrink-0 border-l border-line lg:w-[480px]">
<TranscriptionEditView
documentId={doc.id}
blocks={transcriptionBlocks}
canComment={canComment}
canComment={canWrite}
currentUserId={currentUserId}
onBlockFocus={handleBlockFocus}
onSaveBlock={saveBlock}