This commit is contained in:
Reece 2025-11-14 13:57:37 +00:00
parent c8bf43ea6b
commit c4f8c42a5a
8 changed files with 35 additions and 54 deletions

View File

@ -2256,34 +2256,7 @@
}, },
"clear": "Clear", "clear": "Clear",
"add": "Add", "add": "Add",
"saved": { "saved": "Saved Signatures",
"heading": "Saved signatures",
"description": "Reuse saved signatures at any time.",
"emptyTitle": "No saved signatures yet",
"emptyDescription": "Draw, upload, or type a signature above, then use \"Save to library\" to keep up to {{max}} favourites ready to use.",
"type": {
"canvas": "Drawing",
"image": "Upload",
"text": "Text"
},
"limitTitle": "Limit reached",
"limitDescription": "Remove a saved signature before adding new ones (max {{max}}).",
"carouselPosition": "{{current}} of {{total}}",
"prev": "Previous",
"next": "Next",
"delete": "Remove",
"label": "Label",
"defaultLabel": "Signature",
"defaultCanvasLabel": "Drawing signature",
"defaultImageLabel": "Uploaded signature",
"defaultTextLabel": "Typed signature",
"saveButton": "Save signature",
"saveUnavailable": "Create a signature first to save it.",
"noChanges": "Current signature is already saved.",
"status": {
"saved": "Saved"
}
},
"save": "Save Signature", "save": "Save Signature",
"applySignatures": "Apply Signatures", "applySignatures": "Apply Signatures",
"personalSigs": "Personal Signatures", "personalSigs": "Personal Signatures",
@ -2318,7 +2291,6 @@
"title": "How to add signature", "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.", "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.", "image": "After uploading your signature image above, click anywhere on the PDF to place it.",
"saved": "Select a saved signature above, then 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."
}, },
"mode": { "mode": {

View File

@ -283,7 +283,7 @@ export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
touchAction: 'none', touchAction: 'none',
backgroundColor: 'white', backgroundColor: 'white',
width: '100%', width: '100%',
maxWidth: '800px', maxWidth: '50rem',
height: '25rem', height: '25rem',
cursor: 'crosshair', cursor: 'crosshair',
}} }}

View File

@ -31,7 +31,6 @@ export const SavedSignaturesSection = ({
const [labelDrafts, setLabelDrafts] = useState<Record<string, string>>({}); const [labelDrafts, setLabelDrafts] = useState<Record<string, string>>({});
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const activeSignature = signatures[activeIndex]; const activeSignature = signatures[activeIndex];
const activeSignatureRef = useRef<SavedSignature | null>(activeSignature ?? null);
const appliedSignatureIdRef = useRef<string | null>(null); const appliedSignatureIdRef = useRef<string | null>(null);
const onUseSignatureRef = useRef(onUseSignature); const onUseSignatureRef = useRef(onUseSignature);
@ -39,10 +38,6 @@ export const SavedSignaturesSection = ({
onUseSignatureRef.current = onUseSignature; onUseSignatureRef.current = onUseSignature;
}, [onUseSignature]); }, [onUseSignature]);
useEffect(() => {
activeSignatureRef.current = activeSignature ?? null;
}, [activeSignature]);
useEffect(() => { useEffect(() => {
setLabelDrafts(prev => { setLabelDrafts(prev => {
const nextDrafts: Record<string, string> = {}; const nextDrafts: Record<string, string> = {};
@ -187,19 +182,18 @@ export const SavedSignaturesSection = ({
}; };
useEffect(() => { useEffect(() => {
const signature = activeSignatureRef.current; if (!activeSignature || disabled) {
if (!signature || disabled) {
appliedSignatureIdRef.current = null; appliedSignatureIdRef.current = null;
return; return;
} }
if (appliedSignatureIdRef.current === signature.id) { if (appliedSignatureIdRef.current === activeSignature.id) {
return; return;
} }
appliedSignatureIdRef.current = signature.id; appliedSignatureIdRef.current = activeSignature.id;
onUseSignatureRef.current(signature); onUseSignatureRef.current(activeSignature);
}, [activeSignature?.id, disabled]); }, [activeSignature, disabled]);
return ( return (
<Stack gap="sm"> <Stack gap="sm">

View File

@ -588,7 +588,9 @@ const SignSettings = ({
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
onActivateSignaturePlacement?.(); onActivateSignaturePlacement?.();
}, PLACEMENT_ACTIVATION_DELAY); }, PLACEMENT_ACTIVATION_DELAY);
return () => window.clearTimeout(timer); return () => {
window.clearTimeout(timer);
};
} }
onActivateSignaturePlacement?.(); onActivateSignaturePlacement?.();
@ -625,7 +627,9 @@ const SignSettings = ({
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const timer = window.setTimeout(trigger, PLACEMENT_ACTIVATION_DELAY); const timer = window.setTimeout(trigger, PLACEMENT_ACTIVATION_DELAY);
return () => window.clearTimeout(timer); return () => {
window.clearTimeout(timer);
};
} }
trigger(); trigger();
@ -648,7 +652,9 @@ const SignSettings = ({
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
onActivateSignaturePlacement?.(); onActivateSignaturePlacement?.();
}, FILE_SWITCH_ACTIVATION_DELAY); }, FILE_SWITCH_ACTIVATION_DELAY);
return () => window.clearTimeout(timer); return () => {
window.clearTimeout(timer);
};
} }
onActivateSignaturePlacement?.(); onActivateSignaturePlacement?.();

View File

@ -4,6 +4,7 @@ import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { useSignature } from '@app/contexts/SignatureContext'; import { useSignature } from '@app/contexts/SignatureContext';
import { uuidV4 } from '@embedpdf/models'; import { uuidV4 } from '@embedpdf/models';
import type { HistoryAPI } from '@app/components/viewer/viewerTypes'; import type { HistoryAPI } from '@app/components/viewer/viewerTypes';
import { ANNOTATION_RECREATION_DELAY_MS, ANNOTATION_VERIFICATION_DELAY_MS } from '@app/core/constants/app';
export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) { export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) {
const { provides: historyApi } = useHistoryCapability(); const { provides: historyApi } = useHistoryCapability();
@ -59,7 +60,7 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
data: storedImageData, data: storedImageData,
appearance: storedImageData, appearance: storedImageData,
}); });
}, 50); }, ANNOTATION_RECREATION_DELAY_MS);
} catch (restoreError) { } catch (restoreError) {
console.error('HistoryAPI: Failed to restore cropped signature:', restoreError); console.error('HistoryAPI: Failed to restore cropped signature:', restoreError);
} }
@ -103,12 +104,12 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
// Small delay to ensure deletion completes // Small delay to ensure deletion completes
setTimeout(() => { setTimeout(() => {
annotationApi.createAnnotation(event.pageIndex, restoredData); annotationApi.createAnnotation(event.pageIndex, restoredData);
}, 50); }, ANNOTATION_RECREATION_DELAY_MS);
} catch (error) { } catch (error) {
console.error('HistoryAPI: Failed to restore annotation:', error); console.error('HistoryAPI: Failed to restore annotation:', error);
} }
} }
}, 100); }, ANNOTATION_VERIFICATION_DELAY_MS);
} }
} }
}; };

View File

@ -14,6 +14,14 @@ const MIN_SIGNATURE_DIMENSION = 12;
// This provides a good balance between visual fidelity and performance/memory usage. // This provides a good balance between visual fidelity and performance/memory usage.
const TEXT_OVERSAMPLE_FACTOR = 2; const TEXT_OVERSAMPLE_FACTOR = 2;
type TextStampImageResult = {
dataUrl: string;
pixelWidth: number;
pixelHeight: number;
displayWidth: number;
displayHeight: number;
};
const extractDataUrl = (value: unknown, depth = 0, visited: Set<unknown> = new Set()): string | undefined => { const extractDataUrl = (value: unknown, depth = 0, visited: Set<unknown> = new Set()): string | undefined => {
if (!value || depth > 6) return undefined; if (!value || depth > 6) return undefined;
@ -48,7 +56,7 @@ const extractDataUrl = (value: unknown, depth = 0, visited: Set<unknown> = new S
const createTextStampImage = ( const createTextStampImage = (
config: SignParameters, config: SignParameters,
displaySize?: { width: number; height: number } | null displaySize?: { width: number; height: number } | null
): { dataUrl: string; pixelWidth: number; pixelHeight: number; displayWidth: number; displayHeight: number } | null => { ): TextStampImageResult | null => {
const text = (config.signerName ?? '').trim(); const text = (config.signerName ?? '').trim();
if (!text) { if (!text) {
return null; return null;

View File

@ -3,6 +3,11 @@
// When no subpath, use empty string instead of '.' to avoid relative path issues // When no subpath, use empty string instead of '.' to avoid relative path issues
export const BASE_PATH = (import.meta.env.BASE_URL || '/').replace(/\/$/, '').replace(/^\.$/, ''); export const BASE_PATH = (import.meta.env.BASE_URL || '/').replace(/\/$/, '').replace(/^\.$/, '');
// EmbedPDF needs time to remove annotations internally before a recreation runs.
// Without the buffer we occasionally end up with duplicate annotations or stale image data.
export const ANNOTATION_RECREATION_DELAY_MS = 50;
export const ANNOTATION_VERIFICATION_DELAY_MS = 100;
/** For in-app navigations when you must touch window.location (rare). */ /** For in-app navigations when you must touch window.location (rare). */
export const withBasePath = (path: string): string => { export const withBasePath = (path: string): string => {
const clean = path.startsWith('/') ? path : `/${path}`; const clean = path.startsWith('/') ? path : `/${path}`;

View File

@ -97,12 +97,7 @@ const writeToStorage = (entries: SavedSignature[]) => {
} }
}; };
const generateId = () => { const generateId = () => crypto.randomUUID();
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `sig_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
};
export const useSavedSignatures = () => { export const useSavedSignatures = () => {
const [savedSignatures, setSavedSignatures] = useState<SavedSignature[]>(() => readFromStorage()); const [savedSignatures, setSavedSignatures] = useState<SavedSignature[]>(() => readFromStorage());