diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index b1668c8dd..6a605aab4 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -91,7 +91,10 @@ const EmbedPdfViewerContent = ({ // Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations const shouldEnableAnnotations = isSignatureMode || isAnnotateTool || isAnnotationMode || isAnnotationsVisible; const isPlacementOverlayActive = Boolean( - isSignatureMode && shouldEnableAnnotations && isPlacementMode && signatureConfig + (isSignatureMode || (isAnnotateTool && signatureConfig?.signatureType === 'image')) && + shouldEnableAnnotations && + isPlacementMode && + signatureConfig ); // Track which file tab is active diff --git a/frontend/src/core/tools/Annotate.tsx b/frontend/src/core/tools/Annotate.tsx index d63723dc4..4137c4fef 100644 --- a/frontend/src/core/tools/Annotate.tsx +++ b/frontend/src/core/tools/Annotate.tsx @@ -1,13 +1,13 @@ import { useEffect, useMemo, useState, useContext, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { Alert, Text, Group, ActionIcon, Stack, Divider, Slider, Box, Tooltip as MantineTooltip, Button, TextInput, NumberInput, Tooltip } from '@mantine/core'; +import { Alert, Text, Group, ActionIcon, Stack, Divider, Slider, Box, Tooltip as MantineTooltip, Button, TextInput, Textarea, NumberInput, Tooltip } from '@mantine/core'; import { createToolFlow } from '@app/components/tools/shared/createToolFlow'; import { useNavigation } from '@app/contexts/NavigationContext'; import { useFileSelection, useFileContext } from '@app/contexts/FileContext'; import { BaseToolProps } from '@app/types/tool'; import { useSignature } from '@app/contexts/SignatureContext'; -import { ViewerContext } from '@app/contexts/ViewerContext'; +import { ViewerContext, useViewer } from '@app/contexts/ViewerContext'; import { ColorPicker, ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker'; import { ImageUploader } from '@app/components/annotation/shared/ImageUploader'; import LocalIcon from '@app/components/shared/LocalIcon'; @@ -18,8 +18,18 @@ const Annotate = (_props: BaseToolProps) => { const { setToolAndWorkbench } = useNavigation(); const { selectedFiles } = useFileSelection(); const { selectors } = useFileContext(); - const { signatureApiRef, historyApiRef, undo, redo } = useSignature(); + const { + signatureApiRef, + historyApiRef, + undo, + redo, + setSignatureConfig, + setPlacementMode, + placementPreviewSize, + activateSignaturePlacementMode, + } = useSignature(); const viewerContext = useContext(ViewerContext); + const { getZoomState, registerImmediateZoomUpdate } = useViewer(); const [activeTool, setActiveTool] = useState('highlight'); const [inkColor, setInkColor] = useState('#1f2933'); @@ -54,6 +64,28 @@ const Annotate = (_props: BaseToolProps) => { const [historyAvailability, setHistoryAvailability] = useState({ canUndo: false, canRedo: false }); const manualToolSwitch = useRef(false); + // Zoom tracking for stamp size conversion + const [currentZoom, setCurrentZoom] = useState(() => getZoomState()?.currentZoom ?? 1); + + useEffect(() => { + return registerImmediateZoomUpdate((newZoom) => { + setCurrentZoom(newZoom); + }); + }, [registerImmediateZoomUpdate]); + + // CSS to PDF size conversion accounting for zoom + const cssToPdfSize = useCallback( + (size: { width: number; height: number }) => { + const zoom = currentZoom || 1; + const factor = 1 / zoom; + return { + width: size.width * factor, + height: size.height * factor, + }; + }, + [currentZoom] + ); + const buildToolOptions = useCallback((toolId: AnnotationToolId, includeMetadata: boolean = true) => { const metadata = includeMetadata ? { customData: { @@ -135,6 +167,12 @@ const Annotate = (_props: BaseToolProps) => { }, [viewerContext?.isAnnotationMode, signatureApiRef, activeTool, buildToolOptions]); const activateAnnotationTool = (toolId: AnnotationToolId) => { + // If leaving stamp tool, clean up placement mode + if (activeTool === 'stamp' && toolId !== 'stamp') { + setPlacementMode(false); + setSignatureConfig(null); + } + viewerContext?.setAnnotationMode(true); // Mark as manual tool switch to prevent auto-switch back @@ -174,6 +212,19 @@ const Annotate = (_props: BaseToolProps) => { } }, [activeTool, buildToolOptions, signatureApiRef, stampImageData]); + // Sync preview size from overlay to annotation engine + useEffect(() => { + // When preview size changes, update stamp annotation sizing + // The SignatureAPIBridge will use placementPreviewSize from SignatureContext + // and apply the converted size to the stamp tool automatically + if (activeTool === 'stamp' && placementPreviewSize && stampImageData) { + // Just update the image source; size is handled by SignatureAPIBridge + signatureApiRef?.current?.setAnnotationStyle?.('stamp', { + imageSrc: stampImageData, + }); + } + }, [placementPreviewSize, activeTool, stampImageData, signatureApiRef]); + // Allow exiting multi-point tools with Escape (e.g., polyline) useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -195,9 +246,9 @@ const Annotate = (_props: BaseToolProps) => { const interval = setInterval(() => { const ann = signatureApiRef?.current?.getSelectedAnnotation?.(); const annId = ann?.object?.id ?? null; - setSelectedAnn(ann || null); - // Only reset drafts when selection changes + // Only update state when selection actually changes if (annId !== selectedAnnId) { + setSelectedAnn(ann || null); setSelectedAnnId(annId); if (ann?.object?.contents !== undefined) { setSelectedTextDraft(ann.object.contents ?? ''); @@ -327,11 +378,19 @@ const Annotate = (_props: BaseToolProps) => { reader.readAsDataURL(file); }); setStampImageData(dataUrl); + + // Configure SignatureContext for placement preview + setSignatureConfig({ + signatureType: 'image', + signatureData: dataUrl, + }); + setIsAnnotationPaused(false); - // Activate stamp tool with the image after a small delay + // Activate placement mode with delay setTimeout(() => { viewerContext?.setAnnotationMode(true); + setPlacementMode(true); // This shows the preview overlay signatureApiRef?.current?.setAnnotationStyle?.('stamp', { imageSrc: dataUrl }); signatureApiRef?.current?.activateAnnotationTool?.('stamp', { imageSrc: dataUrl }); }, 150); @@ -340,6 +399,8 @@ const Annotate = (_props: BaseToolProps) => { } } else { setStampImageData(undefined); + setPlacementMode(false); + setSignatureConfig(null); } }} disabled={false} @@ -610,9 +671,41 @@ const Annotate = (_props: BaseToolProps) => { }} /> - { + // Explicitly handle Enter key to insert newlines + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + e.stopPropagation(); + const target = e.currentTarget; + const start = target.selectionStart; + const end = target.selectionEnd; + const val = selectedTextDraft; + // Use \r\n for PDF compatibility + const newVal = val.substring(0, start) + '\r\n' + val.substring(end); + setSelectedTextDraft(newVal); + // Update cursor position after state update + setTimeout(() => { + target.selectionStart = target.selectionEnd = start + 2; + }, 0); + // Trigger annotation update + if (selectedUpdateTimer.current) { + clearTimeout(selectedUpdateTimer.current); + } + selectedUpdateTimer.current = setTimeout(() => { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { contents: newVal, textColor: selectedAnn.object?.textColor ?? textColor } + ); + }, 120); + } + }} onChange={(e) => { const val = e.currentTarget.value; setSelectedTextDraft(val); @@ -628,36 +721,66 @@ const Annotate = (_props: BaseToolProps) => { }, 120); }} /> - { - const size = typeof val === 'number' ? val : 14; - setSelectedFontSize(size); - signatureApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { fontSize: size } - ); - }} - /> - {t('annotation.opacity', 'Opacity')} + {t('annotation.fontSize', 'Font size')} { + min={8} + max={32} + value={selectedFontSize} + onChange={(size) => { + setSelectedFontSize(size); signatureApiRef?.current?.updateAnnotation?.( selectedAnn.object?.pageIndex ?? 0, selectedAnn.object?.id, - { opacity: value / 100 } + { fontSize: size } ); }} /> + + {t('annotation.textAlignment', 'Text Alignment')} + + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { textAlign: 'left' } + ); + }} + size="md" + > + + + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { textAlign: 'center' } + ); + }} + size="md" + > + + + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { textAlign: 'right' } + ); + }} + size="md" + > + + + + ); }