import React, { useEffect, useMemo, useState } from 'react'; import { Box } from '@mantine/core'; import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; import { buildSignaturePreview, SignaturePreview } from '@app/utils/signaturePreview'; import { useSignature } from '@app/contexts/SignatureContext'; import { MAX_PREVIEW_WIDTH_RATIO, MAX_PREVIEW_HEIGHT_RATIO, MAX_PREVIEW_WIDTH_REM, MAX_PREVIEW_HEIGHT_REM, MIN_SIGNATURE_DIMENSION_REM, OVERLAY_EDGE_PADDING_REM, } from '@app/constants/signConstants'; // Convert rem to pixels using browser's base font size (typically 16px) const remToPx = (rem: number) => rem * parseFloat(getComputedStyle(document.documentElement).fontSize); interface SignaturePlacementOverlayProps { containerRef: React.RefObject; isActive: boolean; signatureConfig: SignParameters | null; } export const SignaturePlacementOverlay: React.FC = ({ containerRef, isActive, signatureConfig, }) => { const [preview, setPreview] = useState(null); const [cursor, setCursor] = useState<{ x: number; y: number } | null>(null); const { setPlacementPreviewSize } = useSignature(); useEffect(() => { let cancelled = false; const buildPreview = async () => { try { const value = await buildSignaturePreview(signatureConfig ?? null); if (!cancelled) { setPreview(value); } } catch (error) { console.error('Failed to build signature preview:', error); if (!cancelled) { setPreview(null); } } }; buildPreview(); return () => { cancelled = true; }; }, [signatureConfig]); useEffect(() => { const element = containerRef.current; if (!isActive || !element) { setCursor(null); return; } const handleMove = (event: MouseEvent) => { const rect = element.getBoundingClientRect(); setCursor({ x: event.clientX - rect.left, y: event.clientY - rect.top, }); }; const handleLeave = () => setCursor(null); element.addEventListener('mousemove', handleMove); element.addEventListener('mouseleave', handleLeave); return () => { element.removeEventListener('mousemove', handleMove); element.removeEventListener('mouseleave', handleLeave); }; }, [containerRef, isActive]); const scaledSize = useMemo(() => { if (!preview || !containerRef.current) { return null; } const container = containerRef.current; const containerWidth = container.clientWidth || 1; const containerHeight = container.clientHeight || 1; const maxWidth = Math.min(containerWidth * MAX_PREVIEW_WIDTH_RATIO, remToPx(MAX_PREVIEW_WIDTH_REM)); const maxHeight = Math.min(containerHeight * MAX_PREVIEW_HEIGHT_RATIO, remToPx(MAX_PREVIEW_HEIGHT_REM)); const scale = Math.min( 1, maxWidth / Math.max(preview.width, 1), maxHeight / Math.max(preview.height, 1) ); return { width: Math.max(remToPx(MIN_SIGNATURE_DIMENSION_REM), preview.width * scale), height: Math.max(remToPx(MIN_SIGNATURE_DIMENSION_REM), preview.height * scale), }; }, [preview, containerRef]); useEffect(() => { if (!isActive || !scaledSize) { setPlacementPreviewSize(null); } else { setPlacementPreviewSize(scaledSize); } }, [isActive, scaledSize, setPlacementPreviewSize]); useEffect(() => { return () => { setPlacementPreviewSize(null); }; }, [setPlacementPreviewSize]); const display = useMemo(() => { if (!preview || !scaledSize || !cursor || !containerRef.current) { return null; } const container = containerRef.current; const containerWidth = container.clientWidth || 1; const containerHeight = container.clientHeight || 1; const width = scaledSize.width; const height = scaledSize.height; const edgePadding = remToPx(OVERLAY_EDGE_PADDING_REM); const clampedLeft = Math.max(edgePadding, Math.min(cursor.x - width / 2, containerWidth - width - edgePadding)); const clampedTop = Math.max(edgePadding, Math.min(cursor.y - height / 2, containerHeight - height - edgePadding)); return { left: clampedLeft, top: clampedTop, width, height, dataUrl: preview.dataUrl, }; }, [preview, scaledSize, cursor, containerRef]); if (!isActive || !display || !preview) { return null; } return ( ); };