refactor: move document transcription, annotation, viewer sub-packages
- transcription/: TranscriptionBlock, Column, EditView, PanelHeader, ReadView, Section + transcriptionMarkers, blockConflictMerge, saveBlockWithConflictRetry + useBlockAutoSave, useBlockDragDrop hooks - annotation/: AnnotationLayer, AnnotationShape, AnnotationEditOverlay - viewer/: PdfViewer, PdfControls + useFileLoader, usePdfRenderer hooks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
194
frontend/src/lib/document/annotation/AnnotationShape.svelte
Normal file
194
frontend/src/lib/document/annotation/AnnotationShape.svelte
Normal file
@@ -0,0 +1,194 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user