Files
pdf/frontend/src/components/SignatureModal.tsx
T

823 lines
35 KiB
TypeScript

import { CustomButton } from './custom/CustomButton';
import React, { useRef, useState, useEffect, useCallback } from 'react';
/* ─── Types ─────────────────────────────────────────────── */
interface SignatureModalProps {
open: boolean;
onClose: () => void;
onConfirm: (dataUrl: string, aspect: number) => void;
}
type Mode = 'draw' | 'type' | 'upload' | 'saved';
interface SavedSig {
id: string;
dataUrl: string;
aspect: number;
label: string;
createdAt: number;
}
/* ─── Constants ──────────────────────────────────────────── */
const STORAGE_KEY = 'pdf_saved_signatures';
const CANVAS_W = 960;
const CANVAS_H = 320;
const SIGNATURE_FONTS: { label: string; family: string; css: string }[] = [
{ label: 'Script', family: 'Great Vibes', css: '"Great Vibes", cursive' },
{ label: 'Elegant', family: 'Pacifico', css: '"Pacifico", cursive' },
{ label: 'Classic', family: 'Dancing Script', css: '"Dancing Script", cursive' },
{ label: 'Formal', family: 'Pinyon Script', css: '"Pinyon Script", cursive' },
{ label: 'Bold', family: 'Satisfy', css: '"Satisfy", cursive' },
{ label: 'Handwrite', family: 'Caveat', css: '"Caveat", cursive' },
];
const INK_COLORS = [
{ label: 'Black', value: '#0f172a' },
{ label: 'Navy', value: '#1e3a8a' },
{ label: 'Blue', value: '#2563eb' },
{ label: 'Ink', value: '#312e81' },
];
const THICKNESS_OPTIONS = [
{ label: 'Thin', value: 1.5 },
{ label: 'Medium', value: 2.8 },
{ label: 'Thick', value: 4.5 },
];
/* ─── Google Fonts loader ────────────────────────────────── */
function useGoogleFonts() {
useEffect(() => {
const id = 'sig-google-fonts';
if (document.getElementById(id)) return;
const link = document.createElement('link');
link.id = id;
link.rel = 'stylesheet';
link.href =
'https://fonts.googleapis.com/css2?family=Great+Vibes&family=Pacifico&family=Dancing+Script:wght@700&family=Pinyon+Script&family=Satisfy&family=Caveat:wght@700&display=swap';
document.head.appendChild(link);
}, []);
}
/* ─── Saved signatures helpers ───────────────────────────── */
function loadSaved(): SavedSig[] {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
catch { return []; }
}
function persistSaved(sigs: SavedSig[]) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(sigs.slice(0, 8))); } catch {}
}
/* ─── Point smoothing (Catmull-Rom) ─────────────────────── */
type Pt = { x: number; y: number; p: number };
function midPt(a: Pt, b: Pt): Pt {
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, p: (a.p + b.p) / 2 };
}
function strokePoints(ctx: CanvasRenderingContext2D, pts: Pt[], color: string, baseThickness: number) {
if (pts.length < 2) return;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = color;
for (let i = 1; i < pts.length; i++) {
const prev = pts[i - 1];
const curr = pts[i];
const mid = midPt(prev, curr);
const pressure = (prev.p + curr.p) / 2;
ctx.lineWidth = baseThickness * (0.6 + pressure * 0.8);
ctx.beginPath();
if (i === 1) {
ctx.moveTo(prev.x, prev.y);
ctx.lineTo(mid.x, mid.y);
} else {
const prevMid = midPt(pts[i - 2], prev);
ctx.moveTo(prevMid.x, prevMid.y);
ctx.quadraticCurveTo(prev.x, prev.y, mid.x, mid.y);
}
ctx.stroke();
}
}
/* ────────────────────────────────────────────────────────── */
export const SignatureModal: React.FC<SignatureModalProps> = ({ open, onClose, onConfirm }) => {
useGoogleFonts();
/* tabs */
const [mode, setMode] = useState<Mode>('draw');
/* draw */
const canvasRef = useRef<HTMLCanvasElement>(null);
const strokes = useRef<Pt[][]>([]);
const currentStroke = useRef<Pt[]>([]);
const drawing = useRef(false);
const hasInk = useRef(false);
const [inkColor, setInkColor] = useState(INK_COLORS[0].value);
const [thickness, setThickness] = useState(THICKNESS_OPTIONS[1].value);
/* type */
const [typed, setTyped] = useState('');
const [fontIdx, setFontIdx] = useState(0);
const [typeColor, setTypeColor] = useState(INK_COLORS[0].value);
/* upload */
const [uploaded, setUploaded] = useState<{ url: string; aspect: number } | null>(null);
const [dragging, setDragging] = useState(false);
/* saved */
const [savedSigs, setSavedSigs] = useState<SavedSig[]>([]);
/* load saved on open */
useEffect(() => { if (open) setSavedSigs(loadSaved()); }, [open]);
/* redraw canvas after color/thickness change */
const redrawAll = useCallback(() => {
const c = canvasRef.current;
if (!c) return;
const ctx = c.getContext('2d')!;
ctx.clearRect(0, 0, c.width, c.height);
for (const stroke of strokes.current) {
strokePoints(ctx, stroke, inkColor, thickness);
}
}, [inkColor, thickness]);
useEffect(() => { if (mode === 'draw') redrawAll(); }, [inkColor, thickness, mode, redrawAll]);
/* ── reset on close ── */
const resetLocal = () => {
setMode('draw');
setTyped('');
setUploaded(null);
hasInk.current = false;
strokes.current = [];
currentStroke.current = [];
const c = canvasRef.current;
if (c) c.getContext('2d')!.clearRect(0, 0, c.width, c.height);
};
const handleClose = () => { resetLocal(); onClose(); };
/* ── canvas clear ── */
const clearCanvas = () => {
strokes.current = [];
currentStroke.current = [];
hasInk.current = false;
const c = canvasRef.current;
if (c) c.getContext('2d')!.clearRect(0, 0, c.width, c.height);
};
/* ── pointer helpers ── */
const canvasPos = (e: React.PointerEvent): Pt => {
const c = canvasRef.current!;
const r = c.getBoundingClientRect();
return {
x: (e.clientX - r.left) * (c.width / r.width),
y: (e.clientY - r.top) * (c.height / r.height),
p: e.pressure > 0 ? e.pressure : 0.5,
};
};
const onDown = (e: React.PointerEvent) => {
drawing.current = true;
const pt = canvasPos(e);
currentStroke.current = [pt];
(e.target as Element).setPointerCapture(e.pointerId);
};
const onMove = (e: React.PointerEvent) => {
if (!drawing.current) return;
const pt = canvasPos(e);
currentStroke.current.push(pt);
const ctx = canvasRef.current!.getContext('2d')!;
const stroke = currentStroke.current;
strokePoints(ctx, stroke.slice(-3), inkColor, thickness);
hasInk.current = true;
};
const onUp = () => {
if (drawing.current && currentStroke.current.length > 0) {
strokes.current.push([...currentStroke.current]);
currentStroke.current = [];
}
drawing.current = false;
};
/* ── build final dataUrl from typed ── */
const buildTypedCanvas = () => {
const c = document.createElement('canvas');
c.width = 900; c.height = 240;
const ctx = c.getContext('2d')!;
ctx.clearRect(0, 0, c.width, c.height);
ctx.fillStyle = typeColor;
ctx.font = `bold 100px ${SIGNATURE_FONTS[fontIdx].css}`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(typed.trim() || 'Preview', c.width / 2, c.height / 2);
return { dataUrl: c.toDataURL('image/png'), aspect: c.width / c.height };
};
/* ── confirm ── */
const handleConfirm = () => {
let dataUrl = '';
let aspect = 3;
if (mode === 'draw') {
if (!hasInk.current) return;
const c = canvasRef.current!;
dataUrl = c.toDataURL('image/png');
aspect = c.width / c.height;
} else if (mode === 'type') {
if (!typed.trim()) return;
const res = buildTypedCanvas();
dataUrl = res.dataUrl; aspect = res.aspect;
} else if (mode === 'upload') {
if (!uploaded) return;
dataUrl = uploaded.url; aspect = uploaded.aspect;
} else if (mode === 'saved') {
return;
}
saveToStorage(dataUrl, aspect);
onConfirm(dataUrl, aspect);
resetLocal();
};
const saveToStorage = (dataUrl: string, aspect: number) => {
const prev = loadSaved();
const entry: SavedSig = {
id: `sig_${Date.now()}`,
dataUrl,
aspect,
label: mode === 'type' ? (typed.trim() || 'Signature') : 'Signature',
createdAt: Date.now(),
};
const updated = [entry, ...prev.filter((s) => s.dataUrl !== dataUrl)];
persistSaved(updated);
};
const handleUseSaved = (s: SavedSig) => {
saveToStorage(s.dataUrl, s.aspect); // bump to top
onConfirm(s.dataUrl, s.aspect);
resetLocal();
};
const handleDeleteSaved = (id: string) => {
const updated = savedSigs.filter((s) => s.id !== id);
setSavedSigs(updated);
persistSaved(updated);
};
/* ── upload handlers ── */
const processFile = (f: File) => {
const reader = new FileReader();
reader.onload = () => {
const url = reader.result as string;
const img = new Image();
img.onload = () => setUploaded({ url, aspect: img.width / img.height });
img.src = url;
};
reader.readAsDataURL(f);
};
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0];
if (f) processFile(f);
e.target.value = '';
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault(); setDragging(false);
const f = e.dataTransfer.files[0];
if (f && f.type.startsWith('image/')) processFile(f);
};
/* ── tab labels ── */
const tabs: { id: Mode; label: string; icon: React.ReactNode }[] = [
{
id: 'draw', label: 'Draw',
icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>,
},
{
id: 'type', label: 'Type',
icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>,
},
{
id: 'upload', label: 'Upload',
icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg>,
},
{
id: 'saved', label: 'Saved',
icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>,
},
];
/* ── Baseline guide on canvas ── */
useEffect(() => {
if (!open || mode !== 'draw') return;
const c = canvasRef.current;
if (!c || hasInk.current || strokes.current.length > 0) return;
const ctx = c.getContext('2d')!;
ctx.clearRect(0, 0, c.width, c.height);
// baseline guide
ctx.beginPath();
ctx.setLineDash([8, 10]);
ctx.strokeStyle = '#c7d2e0';
ctx.lineWidth = 1.5;
ctx.moveTo(60, c.height * 0.72);
ctx.lineTo(c.width - 60, c.height * 0.72);
ctx.stroke();
ctx.setLineDash([]);
}, [open, mode]);
/* ── Render ── */
if (!open) return null;
return (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4"
style={{ background: 'rgba(10,15,25,0.55)', backdropFilter: 'blur(4px)' }}
onMouseDown={handleClose}
>
<div
className="relative flex flex-col overflow-hidden"
style={{
width: 660,
maxHeight: '92vh',
borderRadius: 16,
background: '#ffffff',
boxShadow: '0 32px 80px rgba(10,15,30,0.28), 0 0 0 1px rgba(0,0,0,0.07)',
animation: 'sigModalIn 0.22s cubic-bezier(0.34,1.4,0.64,1)',
}}
onMouseDown={(e) => e.stopPropagation()}
>
{/* ── Header ── */}
<div
style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '18px 24px 16px',
borderBottom: '1px solid #edf0f5',
background: '#ffffff',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 34, height: 34, borderRadius: 8,
background: '#eef4ff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#2563eb" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
</div>
<div>
<h2 style={{ fontSize: 15, fontWeight: 700, color: '#0f172a', margin: 0, lineHeight: 1.2 }}>
Add Signature
</h2>
<p style={{ fontSize: 11, color: '#94a3b8', margin: 0, marginTop: 1 }}>
Draw, type, or upload your signature
</p>
</div>
</div>
<button
onClick={handleClose}
style={{
width: 30, height: 30, borderRadius: 8, border: '1px solid #edf0f5',
background: '#f8fafc', cursor: 'pointer', display: 'flex',
alignItems: 'center', justifyContent: 'center', color: '#64748b',
transition: 'all 0.15s',
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = '#f1f5f9'; (e.currentTarget as HTMLButtonElement).style.color = '#0f172a'; }}
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = '#f8fafc'; (e.currentTarget as HTMLButtonElement).style.color = '#64748b'; }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
{/* ── Tab bar ── */}
<div style={{ display: 'flex', gap: 4, padding: '12px 20px 0', background: '#f8fafc', borderBottom: '1px solid #edf0f5' }}>
{tabs.map((t) => (
<button
key={t.id}
onClick={() => setMode(t.id)}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 16px', borderRadius: '8px 8px 0 0',
border: 'none', cursor: 'pointer', fontSize: 13, fontWeight: 600,
transition: 'all 0.15s',
background: mode === t.id ? '#ffffff' : 'transparent',
color: mode === t.id ? '#2563eb' : '#64748b',
borderBottom: mode === t.id ? '2px solid #2563eb' : '2px solid transparent',
boxShadow: mode === t.id ? '0 -2px 12px rgba(37,99,235,0.07), inset 0 0 0 1px rgba(37,99,235,0.08)' : 'none',
position: 'relative', bottom: -1,
}}
>
{t.icon}
{t.label}
{t.id === 'saved' && savedSigs.length > 0 && (
<span style={{
background: '#2563eb', color: '#fff', borderRadius: 20,
fontSize: 9, fontWeight: 700, padding: '1px 5px', lineHeight: 1.6,
}}>
{savedSigs.length}
</span>
)}
</button>
))}
</div>
{/* ── Body ── */}
<div style={{ padding: '20px 24px', overflowY: 'auto', flex: 1, minHeight: 0 }}>
{/* ══ DRAW ══ */}
{mode === 'draw' && (
<div>
{/* Controls row */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
{/* Ink color */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Ink</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{INK_COLORS.map((c) => (
<button
key={c.value}
title={c.label}
onClick={() => setInkColor(c.value)}
style={{
width: 24, height: 24, borderRadius: '50%',
background: c.value, border: 'none', cursor: 'pointer',
outline: inkColor === c.value ? `2px solid #2563eb` : '2px solid transparent',
outlineOffset: 3, transition: 'all 0.12s',
}}
/>
))}
</div>
</div>
<div style={{ width: 1, height: 20, background: '#e2e8f0' }} />
{/* Thickness */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Thickness</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{THICKNESS_OPTIONS.map((o) => (
<button
key={o.value}
title={o.label}
onClick={() => setThickness(o.value)}
style={{
width: 32, height: 30, border: 'none', borderRadius: 6, cursor: 'pointer',
background: thickness === o.value ? '#eff6ff' : '#ffffff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: thickness === o.value ? 'inset 0 0 0 1.5px #2563eb' : 'inset 0 0 0 1px #e2e8f0',
transition: 'all 0.12s',
}}
>
<div style={{
width: 18, height: o.value * 0.9,
borderRadius: 99,
background: thickness === o.value ? '#2563eb' : '#64748b',
transition: 'all 0.12s',
}} />
</button>
))}
</div>
</div>
</div>
{/* Clear */}
<CustomButton variant="outline" size="sm" onClick={clearCanvas}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M3 6h18M19 6l-1 14H6L5 6M10 11v6M14 11v6M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/></svg>
Clear
</CustomButton>
</div>
{/* Canvas */}
<div style={{ position: 'relative', borderRadius: 12, overflow: 'hidden', boxShadow: '0 0 0 1.5px #e2e8f0, 0 4px 20px rgba(0,0,0,0.05)' }}>
<canvas
ref={canvasRef}
width={CANVAS_W}
height={CANVAS_H}
onPointerDown={onDown}
onPointerMove={onMove}
onPointerUp={onUp}
onPointerCancel={onUp}
style={{
width: '100%', height: 200,
display: 'block', cursor: 'crosshair',
background: '#f8fafc',
touchAction: 'none',
}}
/>
{!hasInk.current && strokes.current.length === 0 && (
<div style={{
position: 'absolute', inset: 0, pointerEvents: 'none',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexDirection: 'column', gap: 6,
}}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#c7d2e0" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
<p style={{ fontSize: 12, color: '#b8c5d6', fontWeight: 500, margin: 0 }}>Sign here</p>
</div>
)}
</div>
<p style={{ fontSize: 11, color: '#94a3b8', marginTop: 8, textAlign: 'center' }}>
Use mouse, stylus, or finger pressure sensitivity supported
</p>
</div>
)}
{/* ══ TYPE ══ */}
{mode === 'type' && (
<div>
{/* Name input */}
<input
autoFocus
value={typed}
onChange={(e) => setTyped(e.target.value)}
placeholder="Type your full name"
maxLength={60}
style={{
width: '100%', boxSizing: 'border-box',
padding: '11px 14px', borderRadius: 9,
border: '1.5px solid #e2e8f0', background: '#f8fafc',
fontSize: 14, fontWeight: 500, color: '#0f172a', outline: 'none',
transition: 'border-color 0.15s',
}}
onFocus={(e) => (e.currentTarget.style.borderColor = '#2563eb')}
onBlur={(e) => (e.currentTarget.style.borderColor = '#e2e8f0')}
/>
{/* Font picker */}
<div style={{ marginTop: 14 }}>
<p style={{ fontSize: 11, fontWeight: 600, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 8 }}>
Style
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{SIGNATURE_FONTS.map((f, i) => (
<button
key={f.family}
onClick={() => setFontIdx(i)}
style={{
padding: '10px 14px', borderRadius: 9,
border: fontIdx === i ? '1.5px solid #2563eb' : '1.5px solid #e2e8f0',
background: fontIdx === i ? '#eff6ff' : '#f8fafc',
cursor: 'pointer', textAlign: 'left', transition: 'all 0.15s',
display: 'flex', flexDirection: 'column', gap: 3,
}}
>
<span style={{ fontSize: 10, fontWeight: 600, color: fontIdx === i ? '#2563eb' : '#94a3b8', letterSpacing: '0.04em', textTransform: 'uppercase' }}>
{f.label}
</span>
<span style={{
fontFamily: f.css, fontSize: 26, color: typeColor,
lineHeight: 1.2, display: 'block', maxWidth: '100%',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{typed.trim() || 'Preview'}
</span>
</button>
))}
</div>
</div>
{/* Ink color */}
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Color</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{INK_COLORS.map((c) => (
<button
key={c.value}
title={c.label}
onClick={() => setTypeColor(c.value)}
style={{
width: 24, height: 24, borderRadius: '50%',
background: c.value, border: 'none', cursor: 'pointer',
outline: typeColor === c.value ? `2px solid #2563eb` : '2px solid transparent',
outlineOffset: 3, transition: 'all 0.12s',
}}
/>
))}
</div>
</div>
{/* Live preview */}
<div style={{
marginTop: 16, height: 90, borderRadius: 10,
background: 'linear-gradient(180deg,#f8faff 0%,#eef2fb 100%)',
border: '1.5px dashed #c7d7f5',
display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
}}>
<span style={{
fontFamily: SIGNATURE_FONTS[fontIdx].css,
fontSize: 54, color: typeColor, lineHeight: 1,
maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{typed.trim() || 'Your Signature'}
</span>
</div>
</div>
)}
{/* ══ UPLOAD ══ */}
{mode === 'upload' && (
<div>
<label
onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
minHeight: 220, borderRadius: 12, cursor: 'pointer',
border: `2px dashed ${dragging ? '#2563eb' : uploaded ? '#c7d7f5' : '#cbd5e1'}`,
background: dragging ? '#eff6ff' : uploaded ? '#f8faff' : '#f8fafc',
transition: 'all 0.18s', gap: 10,
}}
>
{uploaded ? (
<>
<img
src={uploaded.url}
alt="signature preview"
style={{ maxHeight: 160, maxWidth: '85%', objectFit: 'contain', borderRadius: 6 }}
/>
<span style={{ fontSize: 12, color: '#2563eb', fontWeight: 600, marginTop: 4 }}>
Click to replace
</span>
</>
) : (
<>
<div style={{
width: 52, height: 52, borderRadius: 14,
background: 'linear-gradient(135deg,#eff6ff,#e0e7ff)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2563eb" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/>
<path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/>
</svg>
</div>
<div style={{ textAlign: 'center' }}>
<p style={{ fontSize: 13.5, fontWeight: 700, color: '#0f172a', margin: 0 }}>
Drop image here or click to browse
</p>
<p style={{ fontSize: 11, color: '#94a3b8', margin: '4px 0 0' }}>
PNG with transparent background gives the best result
</p>
</div>
<span style={{
fontSize: 11, fontWeight: 600, color: '#2563eb',
padding: '6px 14px', borderRadius: 7, background: '#eff6ff',
border: '1px solid #bfdbfe',
}}>
Choose file
</span>
</>
)}
<input type="file" accept="image/*" onChange={handleFileInput} style={{ display: 'none' }} />
</label>
{uploaded && (
<button
onClick={() => setUploaded(null)}
style={{
marginTop: 10, width: '100%', padding: '7px 0', borderRadius: 8,
border: '1px solid #fee2e2', background: '#fff5f5', cursor: 'pointer',
fontSize: 12, fontWeight: 600, color: '#dc2626',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
transition: 'all 0.15s',
}}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M3 6h18M19 6l-1 14H6L5 6M10 11v6M14 11v6M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/></svg>
Remove image
</button>
)}
</div>
)}
{/* ══ SAVED ══ */}
{mode === 'saved' && (
<div>
{savedSigs.length === 0 ? (
<div style={{
minHeight: 200, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 10,
}}>
<div style={{
width: 52, height: 52, borderRadius: 14, background: '#f1f5f9',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
</svg>
</div>
<p style={{ fontSize: 13.5, fontWeight: 700, color: '#0f172a', margin: 0 }}>No saved signatures</p>
<p style={{ fontSize: 12, color: '#94a3b8', margin: 0 }}>Your signatures will appear here after use</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{savedSigs.map((s) => (
<div
key={s.id}
style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '10px 12px', borderRadius: 10,
border: '1.5px solid #edf0f5', background: '#f8fafc',
transition: 'border-color 0.15s',
}}
onMouseEnter={(e) => ((e.currentTarget as HTMLDivElement).style.borderColor = '#bfdbfe')}
onMouseLeave={(e) => ((e.currentTarget as HTMLDivElement).style.borderColor = '#edf0f5')}
>
{/* Thumbnail */}
<div style={{
width: 120, height: 52, borderRadius: 8, overflow: 'hidden',
background: '#fff', border: '1px solid #e2e8f0',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<img
src={s.dataUrl}
alt={s.label}
style={{ maxWidth: '95%', maxHeight: '90%', objectFit: 'contain' }}
/>
</div>
{/* Info */}
<div style={{ flex: 1, minWidth: 0 }}>
<p style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', margin: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{s.label}
</p>
<p style={{ fontSize: 11, color: '#94a3b8', margin: '2px 0 0' }}>
{new Date(s.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</p>
</div>
{/* Actions */}
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
<CustomButton
variant="primary"
size="sm"
onClick={() => handleUseSaved(s)}
>
Use
</CustomButton>
<CustomButton
variant="outline"
size="sm"
onClick={() => handleDeleteSaved(s.id)}
title="Delete"
style={{ padding: '0 8px' }}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M3 6h18M19 6l-1 14H6L5 6M10 11v6M14 11v6M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/></svg>
</CustomButton>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* ── Footer ── */}
{mode !== 'saved' && (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '14px 24px', borderTop: '1px solid #edf0f5',
background: '#f8fafc',
}}>
<p style={{ fontSize: 11, color: '#94a3b8', margin: 0 }}>
Visual signature placed as image overlay
</p>
<div style={{ display: 'flex', gap: 8 }}>
<CustomButton variant="outline" onClick={handleClose}>Cancel</CustomButton>
<CustomButton
variant="primary"
onClick={handleConfirm}
style={{ display: 'flex', alignItems: 'center', gap: 7 }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
Use Signature
</CustomButton>
</div>
</div>
)}
{/* Keyframe animation */}
<style>{`
@keyframes sigModalIn {
from { opacity: 0; transform: scale(0.94) translateY(12px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
`}</style>
</div>
</div>
);
};