diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index a0f7a2666..28aa660e2 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1978,9 +1978,23 @@ "title": "Draw your signature", "clear": "Clear" }, + "canvas": { + "heading": "Draw your signature", + "clickToOpen": "Click to open the drawing canvas", + "modalTitle": "Draw your signature", + "colorLabel": "Colour", + "penSizeLabel": "Pen size", + "penSizePlaceholder": "Size", + "clear": "Clear canvas", + "colorPickerTitle": "Choose stroke colour" + }, "text": { "name": "Signer Name", - "placeholder": "Enter your full name" + "placeholder": "Enter your full name", + "fontLabel": "Font", + "fontSizeLabel": "Font size", + "fontSizePlaceholder": "Type or select font size (8-200)", + "colorLabel": "Text colour" }, "clear": "Clear", "add": "Add", @@ -2003,6 +2017,11 @@ "steps": { "configure": "Configure Signature" }, + "step": { + "createDesc": "Choose how you want to create the signature", + "place": "Place & save", + "placeDesc": "Position the signature on your PDF" + }, "type": { "title": "Signature Type", "draw": "Draw", @@ -2019,11 +2038,16 @@ "title": "How to add signature", "canvas": "After drawing your signature in the canvas, close the modal then click anywhere on the PDF to place it.", "image": "After uploading your signature image above, click anywhere on the PDF to place it.", - "text": "After entering your name above, click anywhere on the PDF to place your signature." + "text": "After entering your name above, click anywhere on the PDF to place your signature.", + "paused": "Placement paused", + "resumeHint": "Resume placement to click and add your signature.", + "noSignature": "Create a signature above to enable placement tools." }, "mode": { "move": "Move Signature", - "place": "Place Signature" + "place": "Place Signature", + "pause": "Pause placement", + "resume": "Resume placement" }, "updateAndPlace": "Update and Place", "activate": "Activate Signature Placement", @@ -4622,6 +4646,9 @@ "processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images." } }, + "colorPicker": { + "title": "Choose colour" + }, "common": { "previous": "Previous", "next": "Next", @@ -4637,7 +4664,8 @@ "used": "used", "available": "available", "cancel": "Cancel", - "preview": "Preview" + "preview": "Preview", + "done": "Done" }, "config": { "overview": { diff --git a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx index c61b61cfd..ea093b5be 100644 --- a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx +++ b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Stack, Alert, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { DrawingControls } from '@app/components/annotation/shared/DrawingControls'; import { ColorPicker } from '@app/components/annotation/shared/ColorPicker'; import { usePDFAnnotation } from '@app/components/annotation/providers/PDFAnnotationProvider'; +import { useSignature } from '@app/contexts/SignatureContext'; export interface AnnotationToolConfig { enableDrawing?: boolean; @@ -32,10 +33,34 @@ export const BaseAnnotationTool: React.FC = ({ undo, redo } = usePDFAnnotation(); + const { historyApiRef } = useSignature(); const [selectedColor, setSelectedColor] = useState('#000000'); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [signatureData, setSignatureData] = useState(null); + const [historyAvailability, setHistoryAvailability] = useState({ canUndo: false, canRedo: false }); + const historyApiInstance = historyApiRef.current; + + useEffect(() => { + if (!historyApiInstance) { + setHistoryAvailability({ canUndo: false, canRedo: false }); + return; + } + + const updateAvailability = () => { + setHistoryAvailability({ + canUndo: historyApiInstance.canUndo?.() ?? false, + canRedo: historyApiInstance.canRedo?.() ?? false, + }); + }; + + const unsubscribe = historyApiInstance.subscribe?.(updateAvailability); + updateAvailability(); + + return () => { + unsubscribe?.(); + }; + }, [historyApiInstance]); const handleSignatureDataChange = (data: string | null) => { setSignatureData(data); @@ -54,6 +79,8 @@ export const BaseAnnotationTool: React.FC = ({ = ({ /> ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/shared/ColorPicker.tsx b/frontend/src/core/components/annotation/shared/ColorPicker.tsx index 40bb363b4..04ae501bb 100644 --- a/frontend/src/core/components/annotation/shared/ColorPicker.tsx +++ b/frontend/src/core/components/annotation/shared/ColorPicker.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; interface ColorPickerProps { isOpen: boolean; @@ -14,13 +15,16 @@ export const ColorPicker: React.FC = ({ onClose, selectedColor, onColorChange, - title = "Choose Color" + title }) => { + const { t } = useTranslation(); + const resolvedTitle = title ?? t('colorPicker.title', 'Choose colour'); + return ( @@ -36,7 +40,7 @@ export const ColorPicker: React.FC = ({ /> @@ -64,4 +68,4 @@ export const ColorSwatchButton: React.FC = ({ onClick={onClick} /> ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx b/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx index e8600e0a2..52908edbc 100644 --- a/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx +++ b/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx @@ -1,5 +1,6 @@ -import React, { useRef, useState } from 'react'; -import { Paper, Button, Modal, Stack, Text, Popover, ColorPicker as MantineColorPicker } from '@mantine/core'; +import React, { useEffect, useRef, useState } from 'react'; +import { Paper, Button, Modal, Stack, Text, Group } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import { ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker'; import PenSizeSelector from '@app/components/tools/sign/PenSizeSelector'; import SignaturePad from 'signature_pad'; @@ -20,6 +21,7 @@ interface DrawingCanvasProps { modalWidth?: number; modalHeight?: number; additionalButtons?: React.ReactNode; + initialSignatureData?: string; } export const DrawingCanvas: React.FC = ({ @@ -34,12 +36,14 @@ export const DrawingCanvas: React.FC = ({ disabled = false, width = 400, height = 150, + initialSignatureData, }) => { + const { t } = useTranslation(); const previewCanvasRef = useRef(null); const modalCanvasRef = useRef(null); const padRef = useRef(null); const [modalOpen, setModalOpen] = useState(false); - const [colorPickerOpen, setColorPickerOpen] = useState(false); + const [savedSignatureData, setSavedSignatureData] = useState(null); const initPad = (canvas: HTMLCanvasElement) => { if (!padRef.current) { @@ -55,6 +59,18 @@ export const DrawingCanvas: React.FC = ({ minDistance: 5, velocityFilterWeight: 0.7, }); + + // Restore saved signature data if it exists + if (savedSignatureData) { + const img = new Image(); + img.onload = () => { + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + } + }; + img.src = savedSignatureData; + } } }; @@ -104,36 +120,35 @@ export const DrawingCanvas: React.FC = ({ return trimmedCanvas.toDataURL('image/png'); }; + const renderPreview = (dataUrl: string) => { + const canvas = previewCanvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const img = new Image(); + img.onload = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + const scale = Math.min(canvas.width / img.width, canvas.height / img.height); + const scaledWidth = img.width * scale; + const scaledHeight = img.height * scale; + const x = (canvas.width - scaledWidth) / 2; + const y = (canvas.height - scaledHeight) / 2; + + ctx.drawImage(img, x, y, scaledWidth, scaledHeight); + }; + img.src = dataUrl; + }; + const closeModal = () => { if (padRef.current && !padRef.current.isEmpty()) { const canvas = modalCanvasRef.current; if (canvas) { const trimmedPng = trimCanvas(canvas); + const untrimmedPng = canvas.toDataURL('image/png'); + setSavedSignatureData(untrimmedPng); // Save untrimmed for restoration onSignatureDataChange(trimmedPng); - - // Update preview canvas with proper aspect ratio - const img = new Image(); - img.onload = () => { - if (previewCanvasRef.current) { - const ctx = previewCanvasRef.current.getContext('2d'); - if (ctx) { - ctx.clearRect(0, 0, previewCanvasRef.current.width, previewCanvasRef.current.height); - - // Calculate scaling to fit within preview canvas while maintaining aspect ratio - const scale = Math.min( - previewCanvasRef.current.width / img.width, - previewCanvasRef.current.height / img.height - ); - const scaledWidth = img.width * scale; - const scaledHeight = img.height * scale; - const x = (previewCanvasRef.current.width - scaledWidth) / 2; - const y = (previewCanvasRef.current.height - scaledHeight) / 2; - - ctx.drawImage(img, x, y, scaledWidth, scaledHeight); - } - } - }; - img.src = trimmedPng; + renderPreview(trimmedPng); if (onDrawingComplete) { onDrawingComplete(); @@ -157,6 +172,7 @@ export const DrawingCanvas: React.FC = ({ ctx.clearRect(0, 0, previewCanvasRef.current.width, previewCanvasRef.current.height); } } + setSavedSignatureData(null); // Clear saved signature onSignatureDataChange(null); }; @@ -173,67 +189,70 @@ export const DrawingCanvas: React.FC = ({ } }; + useEffect(() => { + updatePenColor(selectedColor); + }, [selectedColor]); + + useEffect(() => { + updatePenSize(penSize); + }, [penSize]); + + useEffect(() => { + const canvas = previewCanvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + if (!initialSignatureData) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + return; + } + + renderPreview(initialSignatureData); + }, [initialSignatureData]); + return ( <> - Draw your signature - + {t('sign.canvas.heading', 'Draw your signature')} + - Click to open drawing canvas + {t('sign.canvas.clickToOpen', 'Click to open the drawing canvas')} - + -
-
- Color - - -
- setColorPickerOpen(!colorPickerOpen)} - /> -
-
- - { - onColorSwatchClick(); - updatePenColor(color); - }} - swatches={['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc']} - /> - -
-
-
- Pen Size + + + + {t('sign.canvas.colorLabel', 'Colour')} + + + + + + {t('sign.canvas.penSizeLabel', 'Pen size')} + = ({ updatePenSize(size); }} onInputChange={onPenSizeInputChange} - placeholder="Size" + placeholder={t('sign.canvas.penSizePlaceholder', 'Size')} size="compact-sm" - style={{ width: '60px' }} + style={{ width: '80px' }} /> -
-
+
+ = ({ backgroundColor: 'white', width: '100%', maxWidth: '800px', - height: '400px', + height: '25rem', cursor: 'crosshair', }} /> @@ -271,10 +290,10 @@ export const DrawingCanvas: React.FC = ({
diff --git a/frontend/src/core/components/annotation/shared/DrawingControls.tsx b/frontend/src/core/components/annotation/shared/DrawingControls.tsx index 62c7c615f..3c28a594e 100644 --- a/frontend/src/core/components/annotation/shared/DrawingControls.tsx +++ b/frontend/src/core/components/annotation/shared/DrawingControls.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Group, Button } from '@mantine/core'; +import { Group, Button, ActionIcon, Tooltip } from '@mantine/core'; import { useTranslation } from 'react-i18next'; +import { LocalIcon } from '@app/components/shared/LocalIcon'; interface DrawingControlsProps { onUndo?: () => void; @@ -8,8 +9,11 @@ interface DrawingControlsProps { onPlaceSignature?: () => void; hasSignatureData?: boolean; disabled?: boolean; + canUndo?: boolean; + canRedo?: boolean; showPlaceButton?: boolean; placeButtonText?: string; + additionalControls?: React.ReactNode; } export const DrawingControls: React.FC = ({ @@ -18,30 +22,48 @@ export const DrawingControls: React.FC = ({ onPlaceSignature, hasSignatureData = false, disabled = false, + canUndo = true, + canRedo = true, showPlaceButton = true, - placeButtonText = "Update and Place" + placeButtonText = "Update and Place", + additionalControls, }) => { const { t } = useTranslation(); + const undoDisabled = disabled || !canUndo; + const redoDisabled = disabled || !canRedo; return ( - - {/* Undo/Redo Controls */} - - + + {onUndo && ( + + + + + + )} + {onRedo && ( + + + + + + )} + + {additionalControls} {/* Place Signature Button */} {showPlaceButton && onPlaceSignature && ( @@ -50,11 +72,11 @@ export const DrawingControls: React.FC = ({ color="blue" onClick={onPlaceSignature} disabled={disabled || !hasSignatureData} - flex={1} + ml="auto" > {placeButtonText} )} ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx index aca7430ce..c700d4d05 100644 --- a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx +++ b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx @@ -34,12 +34,18 @@ export const TextInputWithFont: React.FC = ({ const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString()); const fontSizeCombobox = useCombobox(); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); + const [colorInput, setColorInput] = useState(textColor); // Sync font size input with prop changes useEffect(() => { setFontSizeInput(fontSize.toString()); }, [fontSize]); + // Sync color input with prop changes + useEffect(() => { + setColorInput(textColor); + }, [textColor]); + const fontOptions = [ { value: 'Helvetica', label: 'Helvetica' }, { value: 'Times-Roman', label: 'Times' }, @@ -50,10 +56,15 @@ export const TextInputWithFont: React.FC = ({ const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48', '56', '64', '72', '80', '96', '112', '128', '144', '160', '176', '192', '200']; + // Validate hex color + const isValidHexColor = (color: string): boolean => { + return /^#[0-9A-Fa-f]{6}$/.test(color); + }; + return ( onTextChange(e.target.value)} @@ -63,7 +74,7 @@ export const TextInputWithFont: React.FC = ({ {/* Font Selection */}