From f3c29ffe58c422582fa6dd53e9f84c3d95e7d21e Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 5 Apr 2026 21:17:27 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20remove=20legacy=20annotate=20mode?= =?UTF-8?q?=20=E2=80=94=20transcription=20replaces=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The yellow annotation+comment system is now redundant. Transcription blocks handle the same use case (mark region → discuss) but better, because they also produce a transcription. Removed: - annotateMode state and all wiring through page/topbar/viewer/pdfviewer - Annotate/Stop annotate buttons from DocumentTopBar - AnnotateHintStrip import and rendering - AnnotationSidePanel from document detail page - canAnnotate prop from DocumentTopBar - Color picker from PdfViewer - Comment count badges and loadCommentCounts from PdfViewer - Delete button from AnnotationLayer (blocks own annotation lifecycle) - dimColor prop from AnnotationLayer Simplified: - AnnotationLayer: only canDraw + color + onDraw + onAnnotationClick - PdfViewer: only draws in transcribeMode with turquoise - Clicking annotation in transcribe mode scrolls to corresponding block - canComment derived from canWrite (no longer needs canAnnotate) Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/AnnotationLayer.svelte | 101 +++-------------- .../components/AnnotationLayer.svelte.spec.ts | 103 ++++++------------ .../src/lib/components/DocumentTopBar.svelte | 82 +------------- .../src/lib/components/DocumentViewer.svelte | 6 - frontend/src/lib/components/PdfViewer.svelte | 96 +--------------- .../src/routes/documents/[id]/+page.svelte | 66 +++-------- 6 files changed, 76 insertions(+), 378 deletions(-) diff --git a/frontend/src/lib/components/AnnotationLayer.svelte b/frontend/src/lib/components/AnnotationLayer.svelte index 5d103b11..2db06063 100644 --- a/frontend/src/lib/components/AnnotationLayer.svelte +++ b/frontend/src/lib/components/AnnotationLayer.svelte @@ -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; + 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(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(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;' : ''}` ); @@ -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} - - {/if} - {#if (commentCounts?.[annotation.id] ?? 0) > 0} -
- {commentCounts?.[annotation.id]} -
- {/if} - + > {/each} {#if drawRect && drawRect.width > 0} diff --git a/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts b/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts index cc82e084..78befeca 100644 --- a/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts +++ b/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts @@ -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(); + }); }); diff --git a/frontend/src/lib/components/DocumentTopBar.svelte b/frontend/src/lib/components/DocumentTopBar.svelte index b7b06b89..40874939 100644 --- a/frontend/src/lib/components/DocumentTopBar.svelte +++ b/frontend/src/lib/components/DocumentTopBar.svelte @@ -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); -{#snippet annotateBtn(mobile: boolean)} - -{/snippet} - -{#snippet annotateStopBtn(mobile: boolean)} - -{/snippet} - {#snippet transcribeBtn(mobile: boolean)} - - {#if annotateMode} - - {/if} {#if annotations.length > 0}