Files
familienarchiv/frontend/src/lib/components/AnnotationLayer.svelte
Marcel d389dc2023 feat(annotations): dim non-active annotations when a block is focused
When activeAnnotationId is set, the active annotation stays at full
opacity with a highlight box-shadow, while all other annotations fade
to 30% opacity (300ms ease transition). When no block is focused,
all annotations show at full opacity.

Prop chain: activeAnnotationId flows from PdfViewer → AnnotationLayer.

2 new tests (RED/GREEN):
- dims non-active annotations when activeAnnotationId is set
- shows all at full opacity when no activeAnnotationId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:26:02 +02:00

176 lines
4.8 KiB
Svelte

<script lang="ts">
import type { Annotation } from '$lib/types';
type DrawRect = {
x: number;
y: number;
width: number;
height: number;
};
let {
annotations = [],
canDraw,
color,
blockNumbers = {},
activeAnnotationId = null,
onDraw,
onAnnotationClick
}: {
annotations: Annotation[];
canDraw: boolean;
color: string;
blockNumbers?: Record<string, number>;
activeAnnotationId?: string | null;
onDraw: (rect: DrawRect) => void;
onAnnotationClick?: (id: string) => void;
} = $props();
let drawStart = $state<{ x: number; y: number } | null>(null);
let drawRect = $state<DrawRect | null>(null);
function hexToRgba(hex: string, alpha: number): string {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function getNormalizedCoords(event: PointerEvent, element: HTMLElement): { x: number; y: number } {
const rect = element.getBoundingClientRect();
return {
x: (event.clientX - rect.left) / rect.width,
y: (event.clientY - rect.top) / rect.height
};
}
function handlePointerDown(event: PointerEvent) {
if (!canDraw) return;
if ((event.target as HTMLElement).closest('[data-annotation]')) return;
const container = event.currentTarget as HTMLElement;
container.setPointerCapture(event.pointerId);
const coords = getNormalizedCoords(event, container);
drawStart = coords;
drawRect = { x: coords.x, y: coords.y, width: 0, height: 0 };
}
function handlePointerMove(event: PointerEvent) {
if (!canDraw || !drawStart) return;
const container = event.currentTarget as HTMLElement;
const coords = getNormalizedCoords(event, container);
const x = Math.min(drawStart.x, coords.x);
const y = Math.min(drawStart.y, coords.y);
const width = Math.abs(coords.x - drawStart.x);
const height = Math.abs(coords.y - drawStart.y);
drawRect = { x, y, width, height };
}
function handlePointerUp(event: PointerEvent) {
if (!canDraw || !drawStart || !drawRect) return;
const container = event.currentTarget as HTMLElement;
const coords = getNormalizedCoords(event, container);
const x = Math.min(drawStart.x, coords.x);
const y = Math.min(drawStart.y, coords.y);
const width = Math.abs(coords.x - drawStart.x);
const height = Math.abs(coords.y - drawStart.y);
if (width > 0.01 && height > 0.01) {
onDraw({ x, y, width, height });
}
drawStart = null;
drawRect = null;
}
let hoveredId = $state<string | null>(null);
const containerStyle = $derived(
`position: absolute; top: 0; left: 0; width: 100%; height: 100%;${canDraw ? ' cursor: crosshair; touch-action: none;' : ''}`
);
</script>
<div
style={containerStyle}
role="presentation"
onpointerdown={handlePointerDown}
onpointermove={handlePointerMove}
onpointerup={handlePointerUp}
>
{#each annotations as annotation (annotation.id)}
<div
data-testid="annotation-{annotation.id}"
data-annotation
role="button"
tabindex="0"
aria-label="Block anzeigen"
onclick={() => onAnnotationClick?.(annotation.id)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onAnnotationClick?.(annotation.id);
}}
onpointerenter={() => (hoveredId = annotation.id)}
onpointerleave={() => (hoveredId = null)}
style="
position: absolute;
left: {annotation.x * 100}%;
top: {annotation.y * 100}%;
width: {annotation.width * 100}%;
height: {annotation.height * 100}%;
background-color: {hexToRgba(annotation.color, hoveredId === annotation.id || annotation.id === activeAnnotationId ? 0.5 : 0.3)};
box-shadow: {annotation.id === activeAnnotationId ? `inset 0 0 0 2px ${hexToRgba(annotation.color, 0.8)}` : hoveredId === annotation.id ? `inset 0 0 0 2px ${hexToRgba(annotation.color, 0.8)}` : 'none'};
opacity: {activeAnnotationId && annotation.id !== activeAnnotationId ? 0.3 : 1};
pointer-events: auto;
cursor: pointer;
transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.3s ease;
"
>
{#if blockNumbers[annotation.id]}
<div
style="
position: absolute;
top: -8px;
left: -8px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: {annotation.color};
color: white;
font-size: 11px;
font-family: sans-serif;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
"
>
{blockNumbers[annotation.id]}
</div>
{/if}
</div>
{/each}
{#if drawRect && drawRect.width > 0}
<div
style="
position: absolute;
left: {drawRect.x * 100}%;
top: {drawRect.y * 100}%;
width: {drawRect.width * 100}%;
height: {drawRect.height * 100}%;
border: 2px dashed {color};
opacity: 0.3;
pointer-events: none;
"
></div>
{/if}
</div>