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>
This commit is contained in:
Marcel
2026-04-05 23:26:02 +02:00
parent b4212f5e86
commit d389dc2023
3 changed files with 36 additions and 3 deletions

View File

@@ -13,6 +13,7 @@ let {
canDraw, canDraw,
color, color,
blockNumbers = {}, blockNumbers = {},
activeAnnotationId = null,
onDraw, onDraw,
onAnnotationClick onAnnotationClick
}: { }: {
@@ -20,6 +21,7 @@ let {
canDraw: boolean; canDraw: boolean;
color: string; color: string;
blockNumbers?: Record<string, number>; blockNumbers?: Record<string, number>;
activeAnnotationId?: string | null;
onDraw: (rect: DrawRect) => void; onDraw: (rect: DrawRect) => void;
onAnnotationClick?: (id: string) => void; onAnnotationClick?: (id: string) => void;
} = $props(); } = $props();
@@ -121,11 +123,12 @@ const containerStyle = $derived(
top: {annotation.y * 100}%; top: {annotation.y * 100}%;
width: {annotation.width * 100}%; width: {annotation.width * 100}%;
height: {annotation.height * 100}%; height: {annotation.height * 100}%;
background-color: {hexToRgba(annotation.color, hoveredId === annotation.id ? 0.5 : 0.3)}; background-color: {hexToRgba(annotation.color, hoveredId === annotation.id || annotation.id === activeAnnotationId ? 0.5 : 0.3)};
box-shadow: {hoveredId === annotation.id ? `inset 0 0 0 2px ${hexToRgba(annotation.color, 0.8)}` : 'none'}; 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; pointer-events: auto;
cursor: pointer; cursor: pointer;
transition: background-color 0.15s ease, box-shadow 0.15s ease; transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.3s ease;
" "
> >
{#if blockNumbers[annotation.id]} {#if blockNumbers[annotation.id]}

View File

@@ -69,6 +69,35 @@ describe('AnnotationLayer', () => {
expect(container.getAttribute('style')).not.toContain('cursor: crosshair'); expect(container.getAttribute('style')).not.toContain('cursor: crosshair');
}); });
it('dims non-active annotations when activeAnnotationId is set', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1'), makeAnnotation('ann-2')],
canDraw: false,
color: '#00C7B1',
activeAnnotationId: 'ann-1',
onDraw: () => {}
});
const active = page.getByTestId('annotation-ann-1').element();
const dimmed = page.getByTestId('annotation-ann-2').element();
expect(active.style.opacity).toBe('1');
expect(dimmed.style.opacity).toBe('0.3');
});
it('shows all annotations at full opacity when no activeAnnotationId', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1'), makeAnnotation('ann-2')],
canDraw: false,
color: '#00C7B1',
onDraw: () => {}
});
const el1 = page.getByTestId('annotation-ann-1').element();
const el2 = page.getByTestId('annotation-ann-2').element();
expect(el1.style.opacity).toBe('1');
expect(el2.style.opacity).toBe('1');
});
it('does not show delete buttons (annotations owned by blocks)', async () => { it('does not show delete buttons (annotations owned by blocks)', async () => {
render(AnnotationLayer, { render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1')], annotations: [makeAnnotation('ann-1')],

View File

@@ -447,6 +447,7 @@ function zoomOut() {
canDraw={transcribeMode} canDraw={transcribeMode}
color={TRANSCRIPTION_COLOR} color={TRANSCRIPTION_COLOR}
blockNumbers={blockNumbers} blockNumbers={blockNumbers}
activeAnnotationId={activeAnnotationId}
onDraw={handleDraw} onDraw={handleDraw}
onAnnotationClick={handleAnnotationClick} onAnnotationClick={handleAnnotationClick}
/> />