feat(ui): wire panelMode state with read/edit view switching

Adds TranscriptionPanelHeader and TranscriptionReadView to the
document detail page. Default mode is 'read' when blocks exist,
'edit' otherwise. Annotations dimmed in read mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-07 11:21:15 +02:00
parent 306eef2e95
commit e089192d7a
3 changed files with 69 additions and 15 deletions

View File

@@ -20,6 +20,7 @@ type Props = {
blockNumbers?: Record<string, number>;
annotationReloadKey?: number;
activeAnnotationId: string | null;
annotationsDimmed?: boolean;
onAnnotationClick: (id: string) => void;
onTranscriptionDraw?: (rect: DrawRect) => void;
};
@@ -33,6 +34,7 @@ let {
blockNumbers = {},
annotationReloadKey = 0,
activeAnnotationId = $bindable(),
annotationsDimmed = false,
onAnnotationClick,
onTranscriptionDraw
}: Props = $props();
@@ -90,6 +92,7 @@ let {
blockNumbers={blockNumbers}
annotationReloadKey={annotationReloadKey}
bind:activeAnnotationId={activeAnnotationId}
annotationsDimmed={annotationsDimmed}
onAnnotationClick={onAnnotationClick}
onTranscriptionDraw={onTranscriptionDraw}
documentFileHash={doc.fileHash ?? null}

View File

@@ -16,7 +16,8 @@ let {
activeAnnotationId = $bindable<string | null>(null),
onAnnotationClick,
onTranscriptionDraw,
documentFileHash
documentFileHash,
annotationsDimmed = false
}: {
url: string;
documentId?: string;
@@ -27,6 +28,7 @@ let {
onAnnotationClick?: (id: string) => void;
onTranscriptionDraw?: (rect: DrawRect) => void;
documentFileHash?: string | null;
annotationsDimmed?: boolean;
} = $props();
let pdfDoc = $state<PDFDocumentProxy | null>(null);
@@ -456,6 +458,7 @@ function zoomOut() {
color={TRANSCRIPTION_COLOR}
blockNumbers={blockNumbers}
activeAnnotationId={activeAnnotationId}
dimmed={annotationsDimmed}
onDraw={handleDraw}
onAnnotationClick={handleAnnotationClick}
/>

View File

@@ -3,6 +3,8 @@ import { onMount } from 'svelte';
import DocumentTopBar from '$lib/components/DocumentTopBar.svelte';
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
import TranscriptionEditView from '$lib/components/TranscriptionEditView.svelte';
import TranscriptionReadView from '$lib/components/TranscriptionReadView.svelte';
import TranscriptionPanelHeader from '$lib/components/TranscriptionPanelHeader.svelte';
import type { TranscriptionBlockData } from '$lib/types';
let { data } = $props();
@@ -49,7 +51,9 @@ async function loadFile(id: string) {
// ── Mode state ───────────────────────────────────────────────────────────────
let transcribeMode = $state(false);
let panelMode = $state<'read' | 'edit'>('read');
let activeAnnotationId = $state<string | null>(null);
let highlightBlockId = $state<string | null>(null);
// ── Transcription blocks ─────────────────────────────────────────────────────
@@ -64,6 +68,17 @@ const blockNumbers = $derived(
)
);
const hasBlocks = $derived(transcriptionBlocks.length > 0);
const lastEditedAt = $derived.by(() => {
if (transcriptionBlocks.length === 0) return null;
const dates = transcriptionBlocks
.filter((b) => b.updatedAt)
.map((b) => new Date(b.updatedAt!).getTime());
if (dates.length === 0) return null;
return new Date(Math.max(...dates)).toISOString();
});
async function loadTranscriptionBlocks() {
if (!doc?.id) return;
try {
@@ -142,9 +157,17 @@ async function handleAnnotationClick(annotationId: string) {
await loadTranscriptionBlocks();
}
// Wait for DOM to render the blocks, then scroll to the matching one
// In read mode, highlight the matching paragraph
const block = transcriptionBlocks.find((b) => b.annotationId === annotationId);
if (block) {
highlightBlockId = block.id;
setTimeout(() => {
highlightBlockId = null;
}, 1500);
}
// Wait for DOM to render, then scroll to the matching block
requestAnimationFrame(() => {
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' });
@@ -152,10 +175,16 @@ async function handleAnnotationClick(annotationId: string) {
});
}
// Load blocks when transcribe mode is entered
function handleParagraphClick(annotationId: string) {
activeAnnotationId = annotationId;
}
// Load blocks when transcribe mode is entered and set default panel mode
$effect(() => {
if (transcribeMode) {
loadTranscriptionBlocks();
loadTranscriptionBlocks().then(() => {
panelMode = transcriptionBlocks.length > 0 ? 'read' : 'edit';
});
}
});
@@ -211,6 +240,7 @@ onMount(() => {
transcribeMode={transcribeMode}
blockNumbers={blockNumbers}
annotationReloadKey={annotationReloadKey}
annotationsDimmed={transcribeMode && panelMode === 'read'}
bind:activeAnnotationId={activeAnnotationId}
onAnnotationClick={handleAnnotationClick}
onTranscriptionDraw={createBlockFromDraw}
@@ -219,18 +249,36 @@ onMount(() => {
{#if transcribeMode}
<div
class="shrink-0 border-t border-line md:w-[400px] md:border-t-0 md:border-l lg:w-[480px]"
class="flex shrink-0 flex-col border-t border-line md:w-[400px] md:border-t-0 md:border-l lg:w-[480px]"
>
<TranscriptionEditView
documentId={doc.id}
blocks={transcriptionBlocks}
canComment={canWrite}
currentUserId={currentUserId}
activeAnnotationId={activeAnnotationId}
onBlockFocus={handleBlockFocus}
onSaveBlock={saveBlock}
onDeleteBlock={deleteBlock}
<TranscriptionPanelHeader
mode={panelMode}
hasBlocks={hasBlocks}
blockCount={transcriptionBlocks.length}
lastEditedAt={lastEditedAt}
onModeChange={(m) => (panelMode = m)}
onClose={() => (transcribeMode = false)}
/>
<div class="flex-1 overflow-y-auto">
{#if panelMode === 'read'}
<TranscriptionReadView
blocks={transcriptionBlocks}
highlightBlockId={highlightBlockId}
onParagraphClick={handleParagraphClick}
/>
{:else}
<TranscriptionEditView
documentId={doc.id}
blocks={transcriptionBlocks}
canComment={canWrite}
currentUserId={currentUserId}
activeAnnotationId={activeAnnotationId}
onBlockFocus={handleBlockFocus}
onSaveBlock={saveBlock}
onDeleteBlock={deleteBlock}
/>
{/if}
</div>
</div>
{/if}
</div>