Repositioning from top:-8px/right:-8px to top:4px/right:4px ensures the 44px touch target stays fully within the annotation shape. Annotations drawn near the top or right edge of the PDF page no longer risk the button being obscured or inaccessible. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
4.7 KiB
Svelte
195 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import type { Annotation } from '$lib/types';
|
|
import AnnotationEditOverlay from './AnnotationEditOverlay.svelte';
|
|
|
|
let {
|
|
annotation,
|
|
isHovered,
|
|
isActive,
|
|
faded = false,
|
|
dimmed = false,
|
|
blockNumber = undefined,
|
|
isFlashing = false,
|
|
isResizable = false,
|
|
showDelete = false,
|
|
onDeleteRequest,
|
|
onclick,
|
|
onpointerenter,
|
|
onpointerleave
|
|
}: {
|
|
annotation: Annotation;
|
|
isHovered: boolean;
|
|
isActive: boolean;
|
|
faded?: boolean;
|
|
dimmed?: boolean;
|
|
blockNumber?: number | undefined;
|
|
isFlashing?: boolean;
|
|
isResizable?: boolean;
|
|
showDelete?: boolean;
|
|
onDeleteRequest?: () => void;
|
|
onclick: () => void;
|
|
onpointerenter: () => void;
|
|
onpointerleave: () => void;
|
|
} = $props();
|
|
|
|
const deleteVisible = $derived(showDelete && (isHovered || isActive));
|
|
|
|
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})`;
|
|
}
|
|
|
|
let clipPath = $derived.by(() => {
|
|
if (!annotation.polygon || annotation.polygon.length !== 4) return 'none';
|
|
const points = annotation.polygon
|
|
.map(([px, py]) => {
|
|
const cx = ((px - annotation.x) / annotation.width) * 100;
|
|
const cy = ((py - annotation.y) / annotation.height) * 100;
|
|
return `${cx}% ${cy}%`;
|
|
})
|
|
.join(', ');
|
|
return `polygon(${points})`;
|
|
});
|
|
|
|
let bgAlpha = $derived(dimmed ? 0.3 : isHovered || isActive ? 0.5 : 0.3);
|
|
|
|
let boxShadow = $derived.by(() => {
|
|
if (dimmed) return 'none';
|
|
if (isActive || isHovered) return `inset 0 0 0 2px ${hexToRgba(annotation.color, 0.8)}`;
|
|
return 'none';
|
|
});
|
|
|
|
let opacity = $derived(dimmed ? 1 : faded ? 0.3 : 1);
|
|
|
|
let shapeStyle = $derived(
|
|
`position: absolute;` +
|
|
` left: ${annotation.x * 100}%;` +
|
|
` top: ${annotation.y * 100}%;` +
|
|
` width: ${annotation.width * 100}%;` +
|
|
` height: ${annotation.height * 100}%;` +
|
|
` background-color: ${hexToRgba(annotation.color, bgAlpha)};` +
|
|
` box-shadow: ${boxShadow};` +
|
|
` opacity: ${opacity};` +
|
|
` pointer-events: auto;` +
|
|
` cursor: pointer;` +
|
|
` transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.3s ease;` +
|
|
(clipPath !== 'none' ? ` clip-path: ${clipPath};` : '')
|
|
);
|
|
</script>
|
|
|
|
<div
|
|
data-testid="annotation-{annotation.id}"
|
|
data-annotation
|
|
class:annotation-flash={isFlashing}
|
|
role="button"
|
|
tabindex="0"
|
|
aria-label="Block anzeigen"
|
|
onclick={onclick}
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') onclick();
|
|
if (e.key === 'Delete' && showDelete) onDeleteRequest?.();
|
|
}}
|
|
onpointerenter={onpointerenter}
|
|
onpointerleave={onpointerleave}
|
|
style={shapeStyle}
|
|
>
|
|
{#if !dimmed && blockNumber}
|
|
<div
|
|
style="
|
|
position: absolute;
|
|
top: -8px;
|
|
left: -8px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background-color: {annotation.color};
|
|
color: white;
|
|
font-size: 12px;
|
|
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);
|
|
"
|
|
>
|
|
{blockNumber}
|
|
</div>
|
|
{/if}
|
|
{#if deleteVisible}
|
|
<button
|
|
data-testid="annotation-delete-{annotation.id}"
|
|
type="button"
|
|
aria-label="Löschen"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
onDeleteRequest?.();
|
|
}}
|
|
style="
|
|
position: absolute;
|
|
top: 4px;
|
|
right: 4px;
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
background-color: #fff;
|
|
border: 1px solid var(--color-error, #e53e3e);
|
|
color: var(--color-error, #e53e3e);
|
|
cursor: pointer;
|
|
pointer-events: auto;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
|
z-index: 10;
|
|
"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
{/if}
|
|
{#if isResizable}
|
|
<AnnotationEditOverlay annotation={annotation} />
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes annotation-flash-anim {
|
|
0% {
|
|
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 80%, transparent);
|
|
outline-offset: 0px;
|
|
}
|
|
100% {
|
|
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 0%, transparent);
|
|
outline-offset: 2px;
|
|
}
|
|
}
|
|
|
|
.annotation-flash {
|
|
animation: annotation-flash-anim 1.5s ease-out;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.annotation-flash {
|
|
animation: none;
|
|
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 80%, transparent);
|
|
}
|
|
}
|
|
</style>
|