From 10cecb01f54dcb8f8e7c48f9a57b32f481ba0f0f Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 7 Apr 2026 11:27:01 +0200 Subject: [PATCH] feat(a11y): respect prefers-reduced-motion for scroll-sync Uses scrollIntoView behavior 'instant' instead of 'smooth', skips CSS animations (static highlight instead), and extends timeout to 2s for reduced-motion users. Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/AnnotationLayer.svelte | 7 ++++++ .../components/TranscriptionReadView.svelte | 7 ++++++ .../src/routes/documents/[id]/+page.svelte | 25 +++++++++++++------ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/components/AnnotationLayer.svelte b/frontend/src/lib/components/AnnotationLayer.svelte index 4dbe4c28..dcaddf15 100644 --- a/frontend/src/lib/components/AnnotationLayer.svelte +++ b/frontend/src/lib/components/AnnotationLayer.svelte @@ -194,4 +194,11 @@ const containerStyle = $derived( .annotation-flash { animation: annotation-flash-anim 1.5s ease-out; } + +@media (prefers-reduced-motion: reduce) { + .annotation-flash { + animation: none; + outline: 3px solid rgba(0, 199, 177, 0.8); + } +} diff --git a/frontend/src/lib/components/TranscriptionReadView.svelte b/frontend/src/lib/components/TranscriptionReadView.svelte index e70077d0..e9876a58 100644 --- a/frontend/src/lib/components/TranscriptionReadView.svelte +++ b/frontend/src/lib/components/TranscriptionReadView.svelte @@ -48,4 +48,11 @@ let sorted = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder)); .flash-highlight { animation: flash 1.2s ease-out; } + +@media (prefers-reduced-motion: reduce) { + .flash-highlight { + animation: none; + background-color: rgba(0, 199, 177, 0.18); + } +} diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 2b1005a9..bc1b2890 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -56,6 +56,10 @@ let activeAnnotationId = $state(null); let highlightBlockId = $state(null); let flashAnnotationId = $state(null); +const prefersReducedMotion = $derived( + typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches +); + // ── Transcription blocks ───────────────────────────────────────────────────── let transcriptionBlocks = $state([]); @@ -162,16 +166,20 @@ async function handleAnnotationClick(annotationId: string) { const block = transcriptionBlocks.find((b) => b.annotationId === annotationId); if (block) { highlightBlockId = block.id; - setTimeout(() => { - highlightBlockId = null; - }, 1500); + setTimeout( + () => { + highlightBlockId = null; + }, + prefersReducedMotion ? 2000 : 1500 + ); } // Wait for DOM to render, then scroll to the matching block + const scrollBehavior = prefersReducedMotion ? 'instant' : 'smooth'; requestAnimationFrame(() => { if (block) { const el = document.querySelector(`[data-block-id="${block.id}"]`); - el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + el?.scrollIntoView({ behavior: scrollBehavior, block: 'nearest' }); } }); } @@ -179,9 +187,12 @@ async function handleAnnotationClick(annotationId: string) { function handleParagraphClick(annotationId: string) { activeAnnotationId = annotationId; flashAnnotationId = annotationId; - setTimeout(() => { - flashAnnotationId = null; - }, 1500); + setTimeout( + () => { + flashAnnotationId = null; + }, + prefersReducedMotion ? 2000 : 1500 + ); } // Load blocks when transcribe mode is entered and set default panel mode