@ssi-lib/pad-signature
v1.0.2
Published
Pad de firma manuscrita para React: canvas táctil, theme por CSS vars y resultado en data URL / base64.
Readme
@ssi-lib/pad-signature
Pad de firma manuscrita para React (ESM + CJS + tipos). Canvas táctil con trazo Bézier (signature_pad), alto DPI, theme por CSS custom properties y resultado en data URL / base64.
El CSS se inyecta al importar el paquete — no hace falta import './styles.css'.
Instalación
npm i @ssi-lib/pad-signaturePeer deps: react / react-dom (>=18).
1. Lo más simple
Un pad vacío. Sin label, sin chrome extra: el usuario firma y onChange entrega el resultado.
import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';
export function FirmaMinima() {
const [firma, setFirma] = useState<SignatureResult | null>(null);
return <PadSignature onChange={setFirma} />;
}onChange se dispara al terminar un trazo y al limpiar. Si no hay firma, result.empty === true y dataUrl es ''.
2. Label, ayuda y required
<PadSignature
label="Firma"
placeholder="Firme dentro del recuadro"
required
onChange={setFirma}
/>Sin copy default: si no pasás label / placeholder, no se muestra texto. required solo pinta el * — la validación la hacés vos (ver ejemplo 5).
3. Mostrar la firma
import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';
export function FirmaConPreview() {
const [firma, setFirma] = useState<SignatureResult | null>(null);
return (
<>
<PadSignature
label="Firma"
placeholder="Firme dentro del recuadro"
onChange={setFirma}
/>
{firma && !firma.empty && <img src={firma.dataUrl} alt="Firma" />}
</>
);
}4. Controlado
Pasá value (data URL) para que el padre mande. Útil para hidratar, resetear o sincronizar con un store.
import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';
export function FirmaControlada() {
const [firma, setFirma] = useState<SignatureResult | null>(null);
return (
<>
<PadSignature value={firma?.dataUrl ?? ''} onChange={setFirma} />
<button type="button" onClick={() => setFirma(null)}>
Reset
</button>
</>
);
}Sin value, el pad es no controlado. Para una firma inicial sin controlar después:
<PadSignature defaultValue={dataUrlGuardado} onChange={setFirma} />5. Validación al continuar
El pad no bloquea solo: required es visual. El host decide cuándo mostrar el error.
import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';
export function FirmaConValidacion() {
const [firma, setFirma] = useState<SignatureResult | null>(null);
const [intentado, setIntentado] = useState(false);
const vacia = !firma || firma.empty;
return (
<>
<PadSignature
label="Firma"
placeholder="Firme dentro del recuadro"
required
error="La firma es obligatoria"
showError={intentado && vacia}
onChange={setFirma}
/>
<button type="button" onClick={() => setIntentado(true)} disabled={vacia && intentado}>
Continuar
</button>
</>
);
}showError pinta el mensaje y el borde --ssi-pad-error-color.
6. Disabled y i18n del botón limpiar
<PadSignature
label="Signature"
placeholder="Sign inside the box"
disabled={readonly}
clearLabel="Clear"
onChange={setFirma}
/>clearLabel no tiene i18n interno: pasalo ya traducido. El botón limpiar solo aparece cuando hay firma (showClear default true).
<PadSignature showClear={false} onChange={setFirma} />7. JPEG, grosor y alto
<PadSignature
label="Firma"
height={220}
mimeType="image/jpeg"
quality={0.82}
minWidth={2}
maxWidth={4}
onChange={setFirma}
/>JPEG necesita fondo opaco (theme.background o el default #ffffff). PNG es el default. SVG: mimeType="image/svg+xml".
8. Form nativo (name)
El pad escribe un <input type="hidden"> con el data URL, para un <form> clásico.
export function FirmaEnForm() {
return (
<form method="post" action="/contratos">
<PadSignature name="signature" label="Firma" />
<button type="submit">Enviar</button>
</form>
);
}9. Theme (objeto)
<PadSignature
label="Firma"
height={220}
theme={{
background: '#fbf9fd',
pen: '#5c349e',
border: '#e7e1ef',
radius: '16px',
labelColor: '#2d2d2c',
hintColor: '#726c7c',
clearColor: '#5c349e',
errorColor: '#c0392b',
}}
onChange={setFirma}
/>theme.background y theme.pen también van al motor del canvas (no son solo CSS). El resto es chrome.
10. Theme (CSS del host)
<PadSignature className="mi-firma" onChange={setFirma} />.mi-firma {
--ssi-pad-bg: #111;
--ssi-pad-pen: #f2f2f2;
--ssi-pad-border: #333;
--ssi-pad-height: 200px;
--ssi-pad-radius: 8px;
}11. Design system propio (classNames)
<PadSignature
label="Firma"
classNames={{
root: 'Field',
label: 'Field-label',
hint: 'Field-hint',
surface: 'Field-control',
canvas: 'Field-canvas',
clear: 'Field-action',
error: 'Field-error',
}}
onChange={setFirma}
/>Las clases de la lib (ssi-pad-*) siguen ahí: las tuyas se suman. Estados en el root: .is-disabled, .is-invalid, .is-empty, .has-value.
12. Ref — leer / limpiar / hidratar
import { useRef } from 'react';
import { PadSignature, type PadSignatureHandle } from '@ssi-lib/pad-signature';
export function FirmaConRef() {
const ref = useRef<PadSignatureHandle>(null);
const onSubmit = () => {
if (ref.current?.isEmpty()) return;
const result = ref.current?.toResult();
console.log(result?.dataUrl);
};
return (
<>
<PadSignature ref={ref} label="Firma" />
<button type="button" onClick={() => ref.current?.clear()}>
Limpiar
</button>
<button type="button" onClick={onSubmit}>
Guardar
</button>
</>
);
}También: toDataURL('image/jpeg', 0.8) y await ref.current.fromDataURL(dataUrl).
13. Migrar desde omega-pad-signature
Omega guardaba el split item.type + item.value. Acá es el mismo split en SignatureResult.
import { joinSignature, PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';
export function FirmaDesdeOmega({ item }: { item: { type: string; value: string } }) {
const inicial = joinSignature(item.type, item.value);
const onChange = (result: SignatureResult) => {
item.type = result.type;
item.value = result.base64;
item.validForm = !result.empty;
};
return <PadSignature defaultValue={inicial} label={item.lbl} onChange={onChange} />;
}14. Hook sin chrome (useSignaturePad)
Si el layout lo armás vos:
import { useSignaturePad } from '@ssi-lib/pad-signature';
export function MiPad() {
const pad = useSignaturePad({
penColor: '#111',
backgroundColor: '#fff',
onChange: console.log,
});
return (
<div>
<canvas ref={pad.canvasRef} style={{ width: '100%', height: 180, touchAction: 'none' }} />
<button type="button" onClick={pad.clear} disabled={pad.empty}>
Limpiar
</button>
</div>
);
}El hook se ocupa del DPR, ResizeObserver, disabled y del value controlado. El <canvas> tiene que tener tamaño CSS (width/height) para que el backing store se calcule bien.
15. Completo — onboarding de contrato
Controlado + validación + theme + preview + JPEG.
import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';
export function FirmaContrato() {
const [firma, setFirma] = useState<SignatureResult | null>(null);
const [intentado, setIntentado] = useState(false);
const vacia = !firma || firma.empty;
const onContinuar = () => {
setIntentado(true);
if (vacia) return;
enviar(firma.dataUrl);
};
return (
<section>
<PadSignature
label="Firma del titular"
placeholder="Firme dentro del recuadro"
required
value={firma?.dataUrl ?? ''}
onChange={setFirma}
error="La firma es obligatoria"
showError={intentado && vacia}
height={200}
mimeType="image/jpeg"
quality={0.82}
theme={{
pen: '#111111',
background: '#ffffff',
border: '#e7e1ef',
radius: '12px',
clearColor: '#5c349e',
}}
/>
{firma && !firma.empty && (
<p>
{firma.mimeType} · {firma.base64.length} chars
</p>
)}
<button type="button" onClick={onContinuar}>
Continuar
</button>
</section>
);
}
function enviar(_dataUrl: string) {}Resultado (SignatureResult)
interface SignatureResult {
dataUrl: string; // data:image/png;base64,...
type: string; // data:image/png;base64 (omega item.type)
base64: string; // payload (omega item.value)
mimeType: string; // image/png
empty: boolean;
}Helpers: joinSignature(type, value), splitDataUrl(dataUrl), EMPTY_SIGNATURE.
Props de PadSignature
| Prop | Tipo | Default | Descripción |
| --- | --- | --- | --- |
| label | string | — | Título. Sin copy default. |
| placeholder | string | — | Texto de ayuda. Sin copy default. |
| value | string | — | Data URL. Si se pasa, el pad es controlado. |
| defaultValue | string | — | Data URL inicial (no controlado). |
| onChange | (result) => void | — | Al trazar y al limpiar. |
| onBegin / onEnd | () => void / (result) => void | — | Inicio / fin de un trazo. |
| disabled | boolean | false | Bloquea el canvas y el botón limpiar. |
| required | boolean | false | Muestra * junto al label. |
| error / showError | string / boolean | — | Mensaje y visibilidad. Pinta borde de error. |
| showClear | boolean | true | Botón limpiar cuando hay firma. |
| clearLabel | string | 'Limpiar' | Texto del botón. Pasalo ya traducido. |
| height | number \| string | 180 | Alto del recuadro. Gana sobre theme.height. |
| mimeType | 'image/png' \| 'image/jpeg' \| 'image/svg+xml' | 'image/png' | Formato de toDataURL. JPEG pide fondo opaco. |
| quality | number | — | Calidad JPEG 0–1. |
| minWidth / maxWidth | number | 1.5 / 3.5 | Grosor del trazo. |
| theme | PadSignatureTheme | — | CSS vars + color de pluma/fondo. |
| className / classNames | string / slots | — | Root y piezas (root, label, hint, surface, canvas, clear, error, …). |
| name | string | — | <input type="hidden"> con el data URL, para forms nativos. |
Theme — variables CSS
Tres capas, de menor a mayor especificidad: CSS del host → theme → classNames / className.
| Var | Default | Qué pinta |
| --- | --- | --- |
| --ssi-pad-bg | #ffffff | Fondo del canvas |
| --ssi-pad-pen | #111111 | Color del trazo (theme.pen también va al motor) |
| --ssi-pad-border | #e7e1ef | Borde del recuadro |
| --ssi-pad-border-width | 1px | Grosor del borde |
| --ssi-pad-radius | 12px | Radio del recuadro |
| --ssi-pad-height | 180px | Alto del recuadro |
| --ssi-pad-gap | 8px | Espacio entre label, pad y error |
| --ssi-pad-padding | 0px | Padding del root |
| --ssi-pad-label-color / -size / -weight | inherit / 0.875rem / 600 | Label |
| --ssi-pad-hint-color / -size | #726c7c / 0.8125rem | Placeholder |
| --ssi-pad-error-color | #c0392b | Error y * de required |
| --ssi-pad-clear-color / -size | #5c349e / 0.8125rem | Botón limpiar |
| --ssi-pad-disabled-opacity | 0.55 | Estado disabled |
CSS (clases)
Prefijo ssi-pad-. Estados en el root: .is-disabled, .is-invalid, .is-empty, .has-value.
| Clase | Elemento |
| --- | --- |
| .ssi-pad | Root |
| .ssi-pad-head | Label + botón limpiar |
| .ssi-pad-label / .ssi-pad-hint | Título y ayuda |
| .ssi-pad-clear | Botón limpiar |
| .ssi-pad-surface | Recuadro (borde, radio, alto) |
| .ssi-pad-canvas | <canvas> |
| .ssi-pad-error | Mensaje de error |
Desarrollo
npm install
npm run build
npm run typecheckPreview local: library-hub/ui-preview-library → ruta /pad-signature.
