- Move api.server.ts, errors.ts, types.ts, utils.ts, relativeTime.ts to lib/shared/ - Move person relationship components to lib/person/relationship/ - Move Stammbaum components to lib/person/genealogy/ - Move HelpPopover to lib/shared/primitives/ - Update all import paths across routes, specs, and lib files - Update vi.mock() paths in server-project test files - Remove now-empty legacy directories (components/, hooks/, server/, etc.) - Update vite.config.ts coverage include paths for new structure - Update frontend/CLAUDE.md to reflect domain-based lib/ layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
140 lines
3.8 KiB
Svelte
140 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import type { Annotation } from '$lib/shared/types';
|
|
import AnnotationShape from './AnnotationShape.svelte';
|
|
|
|
type DrawRect = {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
let {
|
|
annotations = [],
|
|
canDraw,
|
|
color,
|
|
blockNumbers = {},
|
|
activeAnnotationId = null,
|
|
dimmed = false,
|
|
flashAnnotationId = null,
|
|
onDraw,
|
|
onAnnotationClick,
|
|
onDeleteRequest
|
|
}: {
|
|
annotations: Annotation[];
|
|
canDraw: boolean;
|
|
color: string;
|
|
blockNumbers?: Record<string, number>;
|
|
activeAnnotationId?: string | null;
|
|
dimmed?: boolean;
|
|
flashAnnotationId?: string | null;
|
|
onDraw: (rect: DrawRect) => void;
|
|
onAnnotationClick?: (id: string) => void;
|
|
onDeleteRequest?: (annotationId: string) => void;
|
|
} = $props();
|
|
|
|
let drawStart = $state<{ x: number; y: number } | null>(null);
|
|
let drawRect = $state<DrawRect | null>(null);
|
|
|
|
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)}
|
|
<AnnotationShape
|
|
annotation={annotation}
|
|
isHovered={hoveredId === annotation.id}
|
|
isActive={annotation.id === activeAnnotationId}
|
|
isResizable={canDraw && annotation.id === activeAnnotationId && !annotation.polygon}
|
|
faded={!dimmed && !!activeAnnotationId && annotation.id !== activeAnnotationId}
|
|
dimmed={dimmed}
|
|
blockNumber={blockNumbers[annotation.id]}
|
|
isFlashing={flashAnnotationId === annotation.id}
|
|
showDelete={canDraw}
|
|
onDeleteRequest={() => onDeleteRequest?.(annotation.id)}
|
|
onclick={() => onAnnotationClick?.(annotation.id)}
|
|
onpointerenter={() => (hoveredId = annotation.id)}
|
|
onpointerleave={() => (hoveredId = null)}
|
|
/>
|
|
{/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>
|