feat(ui): add bidirectional scroll-sync with flash animations
Paragraph click flashes the PDF annotation outline (1.5s fade). Annotation click highlights the paragraph with a background flash. Both directions scroll the target into view. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ let {
|
|||||||
blockNumbers = {},
|
blockNumbers = {},
|
||||||
activeAnnotationId = null,
|
activeAnnotationId = null,
|
||||||
dimmed = false,
|
dimmed = false,
|
||||||
|
flashAnnotationId = null,
|
||||||
onDraw,
|
onDraw,
|
||||||
onAnnotationClick
|
onAnnotationClick
|
||||||
}: {
|
}: {
|
||||||
@@ -24,6 +25,7 @@ let {
|
|||||||
blockNumbers?: Record<string, number>;
|
blockNumbers?: Record<string, number>;
|
||||||
activeAnnotationId?: string | null;
|
activeAnnotationId?: string | null;
|
||||||
dimmed?: boolean;
|
dimmed?: boolean;
|
||||||
|
flashAnnotationId?: string | null;
|
||||||
onDraw: (rect: DrawRect) => void;
|
onDraw: (rect: DrawRect) => void;
|
||||||
onAnnotationClick?: (id: string) => void;
|
onAnnotationClick?: (id: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -110,6 +112,7 @@ const containerStyle = $derived(
|
|||||||
<div
|
<div
|
||||||
data-testid="annotation-{annotation.id}"
|
data-testid="annotation-{annotation.id}"
|
||||||
data-annotation
|
data-annotation
|
||||||
|
class:annotation-flash={flashAnnotationId === annotation.id}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Block anzeigen"
|
aria-label="Block anzeigen"
|
||||||
@@ -175,3 +178,20 @@ const containerStyle = $derived(
|
|||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes annotation-flash-anim {
|
||||||
|
0% {
|
||||||
|
outline: 3px solid rgba(0, 199, 177, 0.8);
|
||||||
|
outline-offset: 0px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
outline: 3px solid rgba(0, 199, 177, 0);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation-flash {
|
||||||
|
animation: annotation-flash-anim 1.5s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Props = {
|
|||||||
annotationReloadKey?: number;
|
annotationReloadKey?: number;
|
||||||
activeAnnotationId: string | null;
|
activeAnnotationId: string | null;
|
||||||
annotationsDimmed?: boolean;
|
annotationsDimmed?: boolean;
|
||||||
|
flashAnnotationId?: string | null;
|
||||||
onAnnotationClick: (id: string) => void;
|
onAnnotationClick: (id: string) => void;
|
||||||
onTranscriptionDraw?: (rect: DrawRect) => void;
|
onTranscriptionDraw?: (rect: DrawRect) => void;
|
||||||
};
|
};
|
||||||
@@ -35,6 +36,7 @@ let {
|
|||||||
annotationReloadKey = 0,
|
annotationReloadKey = 0,
|
||||||
activeAnnotationId = $bindable(),
|
activeAnnotationId = $bindable(),
|
||||||
annotationsDimmed = false,
|
annotationsDimmed = false,
|
||||||
|
flashAnnotationId = null,
|
||||||
onAnnotationClick,
|
onAnnotationClick,
|
||||||
onTranscriptionDraw
|
onTranscriptionDraw
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@@ -93,6 +95,7 @@ let {
|
|||||||
annotationReloadKey={annotationReloadKey}
|
annotationReloadKey={annotationReloadKey}
|
||||||
bind:activeAnnotationId={activeAnnotationId}
|
bind:activeAnnotationId={activeAnnotationId}
|
||||||
annotationsDimmed={annotationsDimmed}
|
annotationsDimmed={annotationsDimmed}
|
||||||
|
flashAnnotationId={flashAnnotationId}
|
||||||
onAnnotationClick={onAnnotationClick}
|
onAnnotationClick={onAnnotationClick}
|
||||||
onTranscriptionDraw={onTranscriptionDraw}
|
onTranscriptionDraw={onTranscriptionDraw}
|
||||||
documentFileHash={doc.fileHash ?? null}
|
documentFileHash={doc.fileHash ?? null}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ let {
|
|||||||
onAnnotationClick,
|
onAnnotationClick,
|
||||||
onTranscriptionDraw,
|
onTranscriptionDraw,
|
||||||
documentFileHash,
|
documentFileHash,
|
||||||
annotationsDimmed = false
|
annotationsDimmed = false,
|
||||||
|
flashAnnotationId = null
|
||||||
}: {
|
}: {
|
||||||
url: string;
|
url: string;
|
||||||
documentId?: string;
|
documentId?: string;
|
||||||
@@ -29,6 +30,7 @@ let {
|
|||||||
onTranscriptionDraw?: (rect: DrawRect) => void;
|
onTranscriptionDraw?: (rect: DrawRect) => void;
|
||||||
documentFileHash?: string | null;
|
documentFileHash?: string | null;
|
||||||
annotationsDimmed?: boolean;
|
annotationsDimmed?: boolean;
|
||||||
|
flashAnnotationId?: string | null;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let pdfDoc = $state<PDFDocumentProxy | null>(null);
|
let pdfDoc = $state<PDFDocumentProxy | null>(null);
|
||||||
@@ -459,6 +461,7 @@ function zoomOut() {
|
|||||||
blockNumbers={blockNumbers}
|
blockNumbers={blockNumbers}
|
||||||
activeAnnotationId={activeAnnotationId}
|
activeAnnotationId={activeAnnotationId}
|
||||||
dimmed={annotationsDimmed}
|
dimmed={annotationsDimmed}
|
||||||
|
flashAnnotationId={flashAnnotationId}
|
||||||
onDraw={handleDraw}
|
onDraw={handleDraw}
|
||||||
onAnnotationClick={handleAnnotationClick}
|
onAnnotationClick={handleAnnotationClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ let transcribeMode = $state(false);
|
|||||||
let panelMode = $state<'read' | 'edit'>('read');
|
let panelMode = $state<'read' | 'edit'>('read');
|
||||||
let activeAnnotationId = $state<string | null>(null);
|
let activeAnnotationId = $state<string | null>(null);
|
||||||
let highlightBlockId = $state<string | null>(null);
|
let highlightBlockId = $state<string | null>(null);
|
||||||
|
let flashAnnotationId = $state<string | null>(null);
|
||||||
|
|
||||||
// ── Transcription blocks ─────────────────────────────────────────────────────
|
// ── Transcription blocks ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -177,6 +178,10 @@ async function handleAnnotationClick(annotationId: string) {
|
|||||||
|
|
||||||
function handleParagraphClick(annotationId: string) {
|
function handleParagraphClick(annotationId: string) {
|
||||||
activeAnnotationId = annotationId;
|
activeAnnotationId = annotationId;
|
||||||
|
flashAnnotationId = annotationId;
|
||||||
|
setTimeout(() => {
|
||||||
|
flashAnnotationId = null;
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load blocks when transcribe mode is entered and set default panel mode
|
// Load blocks when transcribe mode is entered and set default panel mode
|
||||||
@@ -241,6 +246,7 @@ onMount(() => {
|
|||||||
blockNumbers={blockNumbers}
|
blockNumbers={blockNumbers}
|
||||||
annotationReloadKey={annotationReloadKey}
|
annotationReloadKey={annotationReloadKey}
|
||||||
annotationsDimmed={transcribeMode && panelMode === 'read'}
|
annotationsDimmed={transcribeMode && panelMode === 'read'}
|
||||||
|
flashAnnotationId={flashAnnotationId}
|
||||||
bind:activeAnnotationId={activeAnnotationId}
|
bind:activeAnnotationId={activeAnnotationId}
|
||||||
onAnnotationClick={handleAnnotationClick}
|
onAnnotationClick={handleAnnotationClick}
|
||||||
onTranscriptionDraw={createBlockFromDraw}
|
onTranscriptionDraw={createBlockFromDraw}
|
||||||
|
|||||||
Reference in New Issue
Block a user