From f5362a58508efe151865eb69662109a8ba80205c Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 14 Apr 2026 10:52:07 +0200 Subject: [PATCH] feat(annotations): add AnnotationEditOverlay component with resize handles and drag Co-Authored-By: Claude Sonnet 4.6 --- .../components/AnnotationEditOverlay.svelte | 259 ++++++++++++++++++ .../AnnotationEditOverlay.svelte.test.ts | 50 ++++ 2 files changed, 309 insertions(+) create mode 100644 frontend/src/lib/components/AnnotationEditOverlay.svelte create mode 100644 frontend/src/lib/components/AnnotationEditOverlay.svelte.test.ts diff --git a/frontend/src/lib/components/AnnotationEditOverlay.svelte b/frontend/src/lib/components/AnnotationEditOverlay.svelte new file mode 100644 index 00000000..45605f13 --- /dev/null +++ b/frontend/src/lib/components/AnnotationEditOverlay.svelte @@ -0,0 +1,259 @@ + + +
+ {m.annotation_edit_mode_active()} +
+ + + + handlePointerDown(e, 'move')} + /> + + {#if dragState} + + {/if} + + {#each handles as handle (handle.id)} + handlePointerDown(e, 'handle', handle.id)} + > + + + + {/each} + + + diff --git a/frontend/src/lib/components/AnnotationEditOverlay.svelte.test.ts b/frontend/src/lib/components/AnnotationEditOverlay.svelte.test.ts new file mode 100644 index 00000000..1afa47eb --- /dev/null +++ b/frontend/src/lib/components/AnnotationEditOverlay.svelte.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import AnnotationEditOverlay from './AnnotationEditOverlay.svelte'; +import type { Annotation } from '$lib/types'; + +const annotation: Annotation = { + id: 'ann-1', + documentId: 'doc-1', + pageNumber: 1, + x: 0.1, + y: 0.2, + width: 0.3, + height: 0.4, + color: '#00c7b1', + createdAt: '2026-01-01T00:00:00Z' +}; + +describe('AnnotationEditOverlay', () => { + it('renders 8 handle elements', async () => { + render(AnnotationEditOverlay, { annotation }); + + const handles = document.querySelectorAll('[data-handle]'); + expect(handles).toHaveLength(8); + }); + + it('each handle has a 44x44 hit area', async () => { + render(AnnotationEditOverlay, { annotation }); + + const hitAreas = document.querySelectorAll('[data-handle-hit]'); + expect(hitAreas).toHaveLength(8); + hitAreas.forEach((el) => { + expect(el.getAttribute('width')).toBe('44'); + expect(el.getAttribute('height')).toBe('44'); + }); + }); + + it('renders a move area covering the full box', async () => { + render(AnnotationEditOverlay, { annotation }); + + const moveArea = document.querySelector('[data-move-area]'); + expect(moveArea).not.toBeNull(); + }); + + it('renders an aria-live region for screen reader announcement', async () => { + render(AnnotationEditOverlay, { annotation }); + + const liveRegion = document.querySelector('[aria-live="polite"]'); + expect(liveRegion).not.toBeNull(); + }); +});