@josephomills/esign 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/index.cjs +487 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +100 -0
- package/dist/ui/index.d.ts +100 -0
- package/dist/ui/index.js +477 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +12 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface SignaturePadProps {
|
|
4
|
+
primaryColor?: string;
|
|
5
|
+
/** Emits a PNG data URL of the current signature, or null when empty. */
|
|
6
|
+
onChange: (pngDataUrl: string | null) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Capture a signature by drawing (signature_pad, touch + mouse, DPI-aware) or by
|
|
10
|
+
* typing a name rendered in a script font. Both yield the same PNG data URL that
|
|
11
|
+
* the seal step stamps onto the document.
|
|
12
|
+
*/
|
|
13
|
+
declare function SignaturePad({ primaryColor, onChange }: SignaturePadProps): react.JSX.Element;
|
|
14
|
+
|
|
15
|
+
interface ConsentValue {
|
|
16
|
+
signerName: string;
|
|
17
|
+
consent: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface ConsentBlockProps {
|
|
20
|
+
value: ConsentValue;
|
|
21
|
+
onChange: (value: ConsentValue) => void;
|
|
22
|
+
primaryColor?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The electronic-signature intent record: a typed full legal name + an explicit
|
|
26
|
+
* consent checkbox. Together with the audit trail this is what makes the simple
|
|
27
|
+
* e-signature legally valid.
|
|
28
|
+
*/
|
|
29
|
+
declare function ConsentBlock({ value, onChange, primaryColor }: ConsentBlockProps): react.JSX.Element;
|
|
30
|
+
|
|
31
|
+
interface ViewerPlacement {
|
|
32
|
+
page: number;
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
w: number;
|
|
36
|
+
h: number;
|
|
37
|
+
}
|
|
38
|
+
interface PdfViewerProps {
|
|
39
|
+
/** Token-gated source PDF URL. */
|
|
40
|
+
url: string;
|
|
41
|
+
/** Optional signature-box hint, drawn on its page at normalized coords. */
|
|
42
|
+
placement?: ViewerPlacement;
|
|
43
|
+
/** Override the pdf.js worker (defaults to the unpkg .mjs for the bundled version). */
|
|
44
|
+
workerSrc?: string;
|
|
45
|
+
primaryColor?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render the PDF to canvases (pdf.js, responsive, DPI-aware) with the signature
|
|
49
|
+
* box overlaid where the manager placed it. Falls back to an <iframe> + download
|
|
50
|
+
* link if pdf.js fails to load — so the document is always readable.
|
|
51
|
+
*/
|
|
52
|
+
declare function PdfViewer({ url, placement, workerSrc, primaryColor }: PdfViewerProps): react.JSX.Element;
|
|
53
|
+
|
|
54
|
+
interface DesignerPlacement {
|
|
55
|
+
page: number;
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
w: number;
|
|
59
|
+
h: number;
|
|
60
|
+
}
|
|
61
|
+
interface FieldDesignerProps {
|
|
62
|
+
/** The chosen PDF's bytes (read client-side before upload). */
|
|
63
|
+
pdfData: Uint8Array;
|
|
64
|
+
value: DesignerPlacement | null;
|
|
65
|
+
onChange: (placement: DesignerPlacement) => void;
|
|
66
|
+
workerSrc?: string;
|
|
67
|
+
primaryColor?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Drag a signature box onto a pdf.js-rendered page. Emits a normalized
|
|
71
|
+
* placement `{ page, x, y, w, h }` (0–1 fractions, top-left origin) that the seal
|
|
72
|
+
* step converts to pdf coordinates — so it stays accurate at any size.
|
|
73
|
+
*/
|
|
74
|
+
declare function FieldDesigner({ pdfData, value, onChange, workerSrc, primaryColor, }: FieldDesignerProps): react.JSX.Element;
|
|
75
|
+
|
|
76
|
+
interface SigningBranding {
|
|
77
|
+
appName?: string;
|
|
78
|
+
logoUrl?: string;
|
|
79
|
+
primaryColor?: string;
|
|
80
|
+
}
|
|
81
|
+
interface SigningExperienceProps {
|
|
82
|
+
/** Token-gated source PDF URL (GET /api/esign/source/<token>). */
|
|
83
|
+
sourceUrl: string;
|
|
84
|
+
/** Submit endpoint (POST /sign/<token>/submit). */
|
|
85
|
+
submitUrl: string;
|
|
86
|
+
documentTitle: string;
|
|
87
|
+
placement?: ViewerPlacement;
|
|
88
|
+
branding?: SigningBranding;
|
|
89
|
+
/** Called after a successful signature submit (host navigates to /done). */
|
|
90
|
+
onSigned?: () => void;
|
|
91
|
+
workerSrc?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The full host-branded signer page body: read the PDF (placement highlighted),
|
|
95
|
+
* draw/type a signature, consent, and submit. Self-contained styling so it sits
|
|
96
|
+
* cleanly inside any host shell.
|
|
97
|
+
*/
|
|
98
|
+
declare function SigningExperience(props: SigningExperienceProps): react.JSX.Element;
|
|
99
|
+
|
|
100
|
+
export { ConsentBlock, type ConsentBlockProps, type ConsentValue, type DesignerPlacement, FieldDesigner, type FieldDesignerProps, PdfViewer, type PdfViewerProps, SignaturePad, type SignaturePadProps, type SigningBranding, SigningExperience, type SigningExperienceProps, type ViewerPlacement };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface SignaturePadProps {
|
|
4
|
+
primaryColor?: string;
|
|
5
|
+
/** Emits a PNG data URL of the current signature, or null when empty. */
|
|
6
|
+
onChange: (pngDataUrl: string | null) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Capture a signature by drawing (signature_pad, touch + mouse, DPI-aware) or by
|
|
10
|
+
* typing a name rendered in a script font. Both yield the same PNG data URL that
|
|
11
|
+
* the seal step stamps onto the document.
|
|
12
|
+
*/
|
|
13
|
+
declare function SignaturePad({ primaryColor, onChange }: SignaturePadProps): react.JSX.Element;
|
|
14
|
+
|
|
15
|
+
interface ConsentValue {
|
|
16
|
+
signerName: string;
|
|
17
|
+
consent: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface ConsentBlockProps {
|
|
20
|
+
value: ConsentValue;
|
|
21
|
+
onChange: (value: ConsentValue) => void;
|
|
22
|
+
primaryColor?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The electronic-signature intent record: a typed full legal name + an explicit
|
|
26
|
+
* consent checkbox. Together with the audit trail this is what makes the simple
|
|
27
|
+
* e-signature legally valid.
|
|
28
|
+
*/
|
|
29
|
+
declare function ConsentBlock({ value, onChange, primaryColor }: ConsentBlockProps): react.JSX.Element;
|
|
30
|
+
|
|
31
|
+
interface ViewerPlacement {
|
|
32
|
+
page: number;
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
w: number;
|
|
36
|
+
h: number;
|
|
37
|
+
}
|
|
38
|
+
interface PdfViewerProps {
|
|
39
|
+
/** Token-gated source PDF URL. */
|
|
40
|
+
url: string;
|
|
41
|
+
/** Optional signature-box hint, drawn on its page at normalized coords. */
|
|
42
|
+
placement?: ViewerPlacement;
|
|
43
|
+
/** Override the pdf.js worker (defaults to the unpkg .mjs for the bundled version). */
|
|
44
|
+
workerSrc?: string;
|
|
45
|
+
primaryColor?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render the PDF to canvases (pdf.js, responsive, DPI-aware) with the signature
|
|
49
|
+
* box overlaid where the manager placed it. Falls back to an <iframe> + download
|
|
50
|
+
* link if pdf.js fails to load — so the document is always readable.
|
|
51
|
+
*/
|
|
52
|
+
declare function PdfViewer({ url, placement, workerSrc, primaryColor }: PdfViewerProps): react.JSX.Element;
|
|
53
|
+
|
|
54
|
+
interface DesignerPlacement {
|
|
55
|
+
page: number;
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
w: number;
|
|
59
|
+
h: number;
|
|
60
|
+
}
|
|
61
|
+
interface FieldDesignerProps {
|
|
62
|
+
/** The chosen PDF's bytes (read client-side before upload). */
|
|
63
|
+
pdfData: Uint8Array;
|
|
64
|
+
value: DesignerPlacement | null;
|
|
65
|
+
onChange: (placement: DesignerPlacement) => void;
|
|
66
|
+
workerSrc?: string;
|
|
67
|
+
primaryColor?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Drag a signature box onto a pdf.js-rendered page. Emits a normalized
|
|
71
|
+
* placement `{ page, x, y, w, h }` (0–1 fractions, top-left origin) that the seal
|
|
72
|
+
* step converts to pdf coordinates — so it stays accurate at any size.
|
|
73
|
+
*/
|
|
74
|
+
declare function FieldDesigner({ pdfData, value, onChange, workerSrc, primaryColor, }: FieldDesignerProps): react.JSX.Element;
|
|
75
|
+
|
|
76
|
+
interface SigningBranding {
|
|
77
|
+
appName?: string;
|
|
78
|
+
logoUrl?: string;
|
|
79
|
+
primaryColor?: string;
|
|
80
|
+
}
|
|
81
|
+
interface SigningExperienceProps {
|
|
82
|
+
/** Token-gated source PDF URL (GET /api/esign/source/<token>). */
|
|
83
|
+
sourceUrl: string;
|
|
84
|
+
/** Submit endpoint (POST /sign/<token>/submit). */
|
|
85
|
+
submitUrl: string;
|
|
86
|
+
documentTitle: string;
|
|
87
|
+
placement?: ViewerPlacement;
|
|
88
|
+
branding?: SigningBranding;
|
|
89
|
+
/** Called after a successful signature submit (host navigates to /done). */
|
|
90
|
+
onSigned?: () => void;
|
|
91
|
+
workerSrc?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The full host-branded signer page body: read the PDF (placement highlighted),
|
|
95
|
+
* draw/type a signature, consent, and submit. Self-contained styling so it sits
|
|
96
|
+
* cleanly inside any host shell.
|
|
97
|
+
*/
|
|
98
|
+
declare function SigningExperience(props: SigningExperienceProps): react.JSX.Element;
|
|
99
|
+
|
|
100
|
+
export { ConsentBlock, type ConsentBlockProps, type ConsentValue, type DesignerPlacement, FieldDesigner, type FieldDesignerProps, PdfViewer, type PdfViewerProps, SignaturePad, type SignaturePadProps, type SigningBranding, SigningExperience, type SigningExperienceProps, type ViewerPlacement };
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import SignaturePadLib from 'signature_pad';
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/ui/SignaturePad.tsx
|
|
7
|
+
function SignaturePad({ primaryColor = "#4f46e5", onChange }) {
|
|
8
|
+
const [mode, setMode] = useState("draw");
|
|
9
|
+
const [typed, setTyped] = useState("");
|
|
10
|
+
const canvasRef = useRef(null);
|
|
11
|
+
const padRef = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (mode !== "draw") return;
|
|
14
|
+
const canvas = canvasRef.current;
|
|
15
|
+
if (!canvas) return;
|
|
16
|
+
const pad = new SignaturePadLib(canvas, { penColor: "#111827", minWidth: 0.8, maxWidth: 2.2 });
|
|
17
|
+
padRef.current = pad;
|
|
18
|
+
const resize = () => {
|
|
19
|
+
const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
|
20
|
+
const rect = canvas.getBoundingClientRect();
|
|
21
|
+
canvas.width = Math.floor(rect.width * ratio);
|
|
22
|
+
canvas.height = Math.floor(rect.height * ratio);
|
|
23
|
+
canvas.getContext("2d")?.scale(ratio, ratio);
|
|
24
|
+
pad.clear();
|
|
25
|
+
onChange(null);
|
|
26
|
+
};
|
|
27
|
+
pad.addEventListener(
|
|
28
|
+
"endStroke",
|
|
29
|
+
() => onChange(pad.isEmpty() ? null : pad.toDataURL("image/png"))
|
|
30
|
+
);
|
|
31
|
+
resize();
|
|
32
|
+
window.addEventListener("resize", resize);
|
|
33
|
+
return () => {
|
|
34
|
+
window.removeEventListener("resize", resize);
|
|
35
|
+
pad.off();
|
|
36
|
+
padRef.current = null;
|
|
37
|
+
};
|
|
38
|
+
}, [mode, onChange]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (mode !== "type") return;
|
|
41
|
+
const name = typed.trim();
|
|
42
|
+
if (!name) {
|
|
43
|
+
onChange(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const c = document.createElement("canvas");
|
|
47
|
+
c.width = 720;
|
|
48
|
+
c.height = 200;
|
|
49
|
+
const ctx = c.getContext("2d");
|
|
50
|
+
if (!ctx) return;
|
|
51
|
+
ctx.fillStyle = "#111827";
|
|
52
|
+
ctx.font = "72px 'Segoe Script', 'Brush Script MT', cursive";
|
|
53
|
+
ctx.textBaseline = "middle";
|
|
54
|
+
ctx.fillText(name, 24, 110);
|
|
55
|
+
onChange(c.toDataURL("image/png"));
|
|
56
|
+
}, [mode, typed, onChange]);
|
|
57
|
+
const tab = (m, label) => /* @__PURE__ */ jsx(
|
|
58
|
+
"button",
|
|
59
|
+
{
|
|
60
|
+
type: "button",
|
|
61
|
+
onClick: () => {
|
|
62
|
+
setMode(m);
|
|
63
|
+
onChange(null);
|
|
64
|
+
},
|
|
65
|
+
style: {
|
|
66
|
+
flex: 1,
|
|
67
|
+
padding: "8px 12px",
|
|
68
|
+
border: "none",
|
|
69
|
+
borderBottom: mode === m ? `2px solid ${primaryColor}` : "2px solid transparent",
|
|
70
|
+
background: "transparent",
|
|
71
|
+
fontWeight: mode === m ? 600 : 400,
|
|
72
|
+
color: mode === m ? primaryColor : "#6b7280",
|
|
73
|
+
cursor: "pointer"
|
|
74
|
+
},
|
|
75
|
+
children: label
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
return /* @__PURE__ */ jsxs("div", { style: { border: "1px solid #e5e7eb", borderRadius: 12, overflow: "hidden" }, children: [
|
|
79
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", borderBottom: "1px solid #f3f4f6" }, children: [
|
|
80
|
+
tab("draw", "Draw"),
|
|
81
|
+
tab("type", "Type")
|
|
82
|
+
] }),
|
|
83
|
+
/* @__PURE__ */ jsxs("div", { style: { position: "relative", background: "#fff" }, children: [
|
|
84
|
+
mode === "draw" ? /* @__PURE__ */ jsx(
|
|
85
|
+
"canvas",
|
|
86
|
+
{
|
|
87
|
+
ref: canvasRef,
|
|
88
|
+
style: { width: "100%", height: 180, touchAction: "none", display: "block" }
|
|
89
|
+
}
|
|
90
|
+
) : /* @__PURE__ */ jsx("div", { style: { padding: 16 }, children: /* @__PURE__ */ jsx(
|
|
91
|
+
"input",
|
|
92
|
+
{
|
|
93
|
+
value: typed,
|
|
94
|
+
onChange: (e) => setTyped(e.target.value),
|
|
95
|
+
placeholder: "Type your name",
|
|
96
|
+
style: {
|
|
97
|
+
width: "100%",
|
|
98
|
+
fontSize: 40,
|
|
99
|
+
fontFamily: "'Segoe Script', 'Brush Script MT', cursive",
|
|
100
|
+
border: "none",
|
|
101
|
+
borderBottom: "1px solid #e5e7eb",
|
|
102
|
+
outline: "none",
|
|
103
|
+
padding: "8px 0"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
) }),
|
|
107
|
+
/* @__PURE__ */ jsx(
|
|
108
|
+
"button",
|
|
109
|
+
{
|
|
110
|
+
type: "button",
|
|
111
|
+
onClick: () => {
|
|
112
|
+
padRef.current?.clear();
|
|
113
|
+
setTyped("");
|
|
114
|
+
onChange(null);
|
|
115
|
+
},
|
|
116
|
+
style: {
|
|
117
|
+
position: "absolute",
|
|
118
|
+
top: 8,
|
|
119
|
+
right: 8,
|
|
120
|
+
fontSize: 12,
|
|
121
|
+
color: "#6b7280",
|
|
122
|
+
background: "rgba(255,255,255,0.8)",
|
|
123
|
+
border: "1px solid #e5e7eb",
|
|
124
|
+
borderRadius: 6,
|
|
125
|
+
padding: "2px 8px",
|
|
126
|
+
cursor: "pointer"
|
|
127
|
+
},
|
|
128
|
+
children: "Clear"
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
] })
|
|
132
|
+
] });
|
|
133
|
+
}
|
|
134
|
+
function ConsentBlock({ value, onChange, primaryColor = "#4f46e5" }) {
|
|
135
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
136
|
+
/* @__PURE__ */ jsxs("label", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [
|
|
137
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 13, fontWeight: 600, color: "#374151" }, children: "Full legal name" }),
|
|
138
|
+
/* @__PURE__ */ jsx(
|
|
139
|
+
"input",
|
|
140
|
+
{
|
|
141
|
+
value: value.signerName,
|
|
142
|
+
onChange: (e) => onChange({ ...value, signerName: e.target.value }),
|
|
143
|
+
placeholder: "e.g. Ada Lovelace",
|
|
144
|
+
style: {
|
|
145
|
+
padding: "10px 12px",
|
|
146
|
+
border: "1px solid #d1d5db",
|
|
147
|
+
borderRadius: 8,
|
|
148
|
+
fontSize: 15,
|
|
149
|
+
outline: "none"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
] }),
|
|
154
|
+
/* @__PURE__ */ jsxs("label", { style: { display: "flex", gap: 10, alignItems: "flex-start", cursor: "pointer" }, children: [
|
|
155
|
+
/* @__PURE__ */ jsx(
|
|
156
|
+
"input",
|
|
157
|
+
{
|
|
158
|
+
type: "checkbox",
|
|
159
|
+
checked: value.consent,
|
|
160
|
+
onChange: (e) => onChange({ ...value, consent: e.target.checked }),
|
|
161
|
+
style: { marginTop: 3, accentColor: primaryColor, width: 16, height: 16 }
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 13, color: "#4b5563", lineHeight: 1.5 }, children: "I agree to sign this document electronically. I understand my electronic signature is legally binding and that an audit record (time, IP, and a tamper-evident hash) is kept with the signed document." })
|
|
165
|
+
] })
|
|
166
|
+
] });
|
|
167
|
+
}
|
|
168
|
+
function PdfViewer({ url, placement, workerSrc, primaryColor = "#4f46e5" }) {
|
|
169
|
+
const containerRef = useRef(null);
|
|
170
|
+
const [failed, setFailed] = useState(false);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
let cancelled = false;
|
|
173
|
+
void (async () => {
|
|
174
|
+
try {
|
|
175
|
+
const pdfjs = await import('pdfjs-dist');
|
|
176
|
+
pdfjs.GlobalWorkerOptions.workerSrc = workerSrc ?? `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
|
|
177
|
+
const doc = await pdfjs.getDocument({ url }).promise;
|
|
178
|
+
const container = containerRef.current;
|
|
179
|
+
if (!container || cancelled) return;
|
|
180
|
+
container.innerHTML = "";
|
|
181
|
+
const width = container.clientWidth || 640;
|
|
182
|
+
const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
|
183
|
+
for (let p = 1; p <= doc.numPages; p++) {
|
|
184
|
+
const page = await doc.getPage(p);
|
|
185
|
+
if (cancelled) return;
|
|
186
|
+
const base = page.getViewport({ scale: 1 });
|
|
187
|
+
const viewport = page.getViewport({ scale: width / base.width });
|
|
188
|
+
const canvas = document.createElement("canvas");
|
|
189
|
+
canvas.width = Math.floor(viewport.width * ratio);
|
|
190
|
+
canvas.height = Math.floor(viewport.height * ratio);
|
|
191
|
+
canvas.style.width = "100%";
|
|
192
|
+
canvas.style.display = "block";
|
|
193
|
+
canvas.style.borderRadius = "4px";
|
|
194
|
+
const wrap = document.createElement("div");
|
|
195
|
+
wrap.style.position = "relative";
|
|
196
|
+
wrap.style.marginBottom = "14px";
|
|
197
|
+
wrap.style.boxShadow = "0 1px 4px rgba(0,0,0,0.12)";
|
|
198
|
+
wrap.appendChild(canvas);
|
|
199
|
+
if (placement && placement.page === p) {
|
|
200
|
+
const box = document.createElement("div");
|
|
201
|
+
box.style.cssText = `position:absolute;left:${placement.x * 100}%;top:${placement.y * 100}%;width:${placement.w * 100}%;height:${placement.h * 100}%;border:2px dashed ${primaryColor};background:${primaryColor}1a;border-radius:4px;pointer-events:none;`;
|
|
202
|
+
const label = document.createElement("span");
|
|
203
|
+
label.textContent = "Signature";
|
|
204
|
+
label.style.cssText = `position:absolute;top:-18px;left:0;font-size:11px;font-weight:600;color:${primaryColor};`;
|
|
205
|
+
box.appendChild(label);
|
|
206
|
+
wrap.appendChild(box);
|
|
207
|
+
}
|
|
208
|
+
container.appendChild(wrap);
|
|
209
|
+
const ctx = canvas.getContext("2d");
|
|
210
|
+
if (!ctx) continue;
|
|
211
|
+
ctx.scale(ratio, ratio);
|
|
212
|
+
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
if (!cancelled) setFailed(true);
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
return () => {
|
|
219
|
+
cancelled = true;
|
|
220
|
+
};
|
|
221
|
+
}, [url, placement, workerSrc, primaryColor]);
|
|
222
|
+
if (failed) {
|
|
223
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
224
|
+
/* @__PURE__ */ jsx(
|
|
225
|
+
"iframe",
|
|
226
|
+
{
|
|
227
|
+
src: `${url}#toolbar=0`,
|
|
228
|
+
title: "Document",
|
|
229
|
+
style: { width: "100%", height: 520, border: "1px solid #e5e7eb", borderRadius: 8 }
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
/* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noreferrer", style: { fontSize: 13, color: primaryColor }, children: "Download to read" })
|
|
233
|
+
] });
|
|
234
|
+
}
|
|
235
|
+
return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%" } });
|
|
236
|
+
}
|
|
237
|
+
function FieldDesigner({
|
|
238
|
+
pdfData,
|
|
239
|
+
value,
|
|
240
|
+
onChange,
|
|
241
|
+
workerSrc,
|
|
242
|
+
primaryColor = "#4f46e5"
|
|
243
|
+
}) {
|
|
244
|
+
const [numPages, setNumPages] = useState(0);
|
|
245
|
+
const [page, setPage] = useState(value?.page ?? 1);
|
|
246
|
+
const [failed, setFailed] = useState(false);
|
|
247
|
+
const stageRef = useRef(null);
|
|
248
|
+
const canvasRef = useRef(null);
|
|
249
|
+
const docRef = useRef(null);
|
|
250
|
+
const [drag, setDrag] = useState(null);
|
|
251
|
+
const startRef = useRef(null);
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
let cancelled = false;
|
|
254
|
+
void (async () => {
|
|
255
|
+
try {
|
|
256
|
+
const pdfjs = await import('pdfjs-dist');
|
|
257
|
+
pdfjs.GlobalWorkerOptions.workerSrc = workerSrc ?? `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
|
|
258
|
+
const doc = await pdfjs.getDocument({ data: pdfData.slice() }).promise;
|
|
259
|
+
if (cancelled) return;
|
|
260
|
+
docRef.current = doc;
|
|
261
|
+
setNumPages(doc.numPages);
|
|
262
|
+
setPage((p) => Math.min(p, doc.numPages));
|
|
263
|
+
} catch {
|
|
264
|
+
if (!cancelled) setFailed(true);
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
return () => {
|
|
268
|
+
cancelled = true;
|
|
269
|
+
};
|
|
270
|
+
}, [pdfData, workerSrc]);
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
let cancelled = false;
|
|
273
|
+
void (async () => {
|
|
274
|
+
const doc = docRef.current;
|
|
275
|
+
const canvas = canvasRef.current;
|
|
276
|
+
if (!doc || !canvas) return;
|
|
277
|
+
const pageObj = await doc.getPage(page);
|
|
278
|
+
if (cancelled) return;
|
|
279
|
+
const width = stageRef.current?.clientWidth || 640;
|
|
280
|
+
const base = pageObj.getViewport({ scale: 1 });
|
|
281
|
+
const viewport = pageObj.getViewport({ scale: width / base.width });
|
|
282
|
+
const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
|
283
|
+
canvas.width = Math.floor(viewport.width * ratio);
|
|
284
|
+
canvas.height = Math.floor(viewport.height * ratio);
|
|
285
|
+
canvas.style.width = "100%";
|
|
286
|
+
canvas.style.display = "block";
|
|
287
|
+
const ctx = canvas.getContext("2d");
|
|
288
|
+
if (!ctx) return;
|
|
289
|
+
ctx.scale(ratio, ratio);
|
|
290
|
+
await pageObj.render({ canvasContext: ctx, viewport, canvas }).promise;
|
|
291
|
+
})();
|
|
292
|
+
return () => {
|
|
293
|
+
cancelled = true;
|
|
294
|
+
};
|
|
295
|
+
}, [page, numPages]);
|
|
296
|
+
function rel(e) {
|
|
297
|
+
const rect = stageRef.current.getBoundingClientRect();
|
|
298
|
+
return {
|
|
299
|
+
x: Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1),
|
|
300
|
+
y: Math.min(Math.max((e.clientY - rect.top) / rect.height, 0), 1)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function onDown(e) {
|
|
304
|
+
startRef.current = rel(e);
|
|
305
|
+
setDrag({ ...startRef.current, w: 0, h: 0 });
|
|
306
|
+
}
|
|
307
|
+
function onMove(e) {
|
|
308
|
+
if (!startRef.current) return;
|
|
309
|
+
const p = rel(e);
|
|
310
|
+
const s = startRef.current;
|
|
311
|
+
setDrag({ x: Math.min(s.x, p.x), y: Math.min(s.y, p.y), w: Math.abs(p.x - s.x), h: Math.abs(p.y - s.y) });
|
|
312
|
+
}
|
|
313
|
+
function onUp() {
|
|
314
|
+
if (drag && drag.w > 0.02 && drag.h > 0.01) {
|
|
315
|
+
onChange({ page, x: drag.x, y: drag.y, w: drag.w, h: drag.h });
|
|
316
|
+
}
|
|
317
|
+
startRef.current = null;
|
|
318
|
+
setDrag(null);
|
|
319
|
+
}
|
|
320
|
+
const box = drag ?? (value?.page === page ? value : null);
|
|
321
|
+
if (failed) {
|
|
322
|
+
return /* @__PURE__ */ jsx("div", { style: { color: "#b91c1c", fontSize: 14 }, children: "Could not render this PDF for placement. Please check the file is a valid, unencrypted PDF." });
|
|
323
|
+
}
|
|
324
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
325
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }, children: [
|
|
326
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 13, color: "#6b7280" }, children: "Drag a box where the signature goes." }),
|
|
327
|
+
numPages > 1 ? /* @__PURE__ */ jsxs("label", { style: { marginLeft: "auto", fontSize: 13, color: "#374151" }, children: [
|
|
328
|
+
"Page",
|
|
329
|
+
" ",
|
|
330
|
+
/* @__PURE__ */ jsx("select", { value: page, onChange: (e) => setPage(Number(e.target.value)), children: Array.from({ length: numPages }, (_, i) => /* @__PURE__ */ jsx("option", { value: i + 1, children: i + 1 }, i)) })
|
|
331
|
+
] }) : null
|
|
332
|
+
] }),
|
|
333
|
+
/* @__PURE__ */ jsxs(
|
|
334
|
+
"div",
|
|
335
|
+
{
|
|
336
|
+
ref: stageRef,
|
|
337
|
+
onMouseDown: onDown,
|
|
338
|
+
onMouseMove: onMove,
|
|
339
|
+
onMouseUp: onUp,
|
|
340
|
+
onMouseLeave: onUp,
|
|
341
|
+
style: {
|
|
342
|
+
position: "relative",
|
|
343
|
+
cursor: "crosshair",
|
|
344
|
+
userSelect: "none",
|
|
345
|
+
border: "1px solid #e5e7eb",
|
|
346
|
+
borderRadius: 6,
|
|
347
|
+
overflow: "hidden"
|
|
348
|
+
},
|
|
349
|
+
children: [
|
|
350
|
+
/* @__PURE__ */ jsx("canvas", { ref: canvasRef }),
|
|
351
|
+
box ? /* @__PURE__ */ jsx(
|
|
352
|
+
"div",
|
|
353
|
+
{
|
|
354
|
+
style: {
|
|
355
|
+
position: "absolute",
|
|
356
|
+
left: `${box.x * 100}%`,
|
|
357
|
+
top: `${box.y * 100}%`,
|
|
358
|
+
width: `${box.w * 100}%`,
|
|
359
|
+
height: `${box.h * 100}%`,
|
|
360
|
+
border: `2px solid ${primaryColor}`,
|
|
361
|
+
background: `${primaryColor}22`,
|
|
362
|
+
borderRadius: 3,
|
|
363
|
+
pointerEvents: "none"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
) : null
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
] });
|
|
371
|
+
}
|
|
372
|
+
function SigningExperience(props) {
|
|
373
|
+
const primary = props.branding?.primaryColor ?? "#4f46e5";
|
|
374
|
+
const [signaturePng, setSignaturePng] = useState(null);
|
|
375
|
+
const [consent, setConsent] = useState({ signerName: "", consent: false });
|
|
376
|
+
const [submitting, setSubmitting] = useState(false);
|
|
377
|
+
const [error, setError] = useState(null);
|
|
378
|
+
const canSubmit = !!signaturePng && consent.consent && consent.signerName.trim().length > 0 && !submitting;
|
|
379
|
+
async function submit() {
|
|
380
|
+
if (!canSubmit) return;
|
|
381
|
+
setSubmitting(true);
|
|
382
|
+
setError(null);
|
|
383
|
+
try {
|
|
384
|
+
const res = await fetch(props.submitUrl, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers: { "content-type": "application/json" },
|
|
387
|
+
body: JSON.stringify({
|
|
388
|
+
signaturePng,
|
|
389
|
+
signerName: consent.signerName.trim(),
|
|
390
|
+
consent: true
|
|
391
|
+
})
|
|
392
|
+
});
|
|
393
|
+
if (!res.ok) {
|
|
394
|
+
const body = await res.json().catch(() => null);
|
|
395
|
+
throw new Error(body?.error?.message ?? "Could not submit your signature.");
|
|
396
|
+
}
|
|
397
|
+
props.onSigned?.();
|
|
398
|
+
} catch (e) {
|
|
399
|
+
setError(e instanceof Error ? e.message : "Something went wrong.");
|
|
400
|
+
} finally {
|
|
401
|
+
setSubmitting(false);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return /* @__PURE__ */ jsxs(
|
|
405
|
+
"div",
|
|
406
|
+
{
|
|
407
|
+
style: {
|
|
408
|
+
maxWidth: 760,
|
|
409
|
+
margin: "0 auto",
|
|
410
|
+
padding: "24px 16px 64px",
|
|
411
|
+
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
|
412
|
+
color: "#111827"
|
|
413
|
+
},
|
|
414
|
+
children: [
|
|
415
|
+
/* @__PURE__ */ jsxs("header", { style: { display: "flex", alignItems: "center", gap: 12, marginBottom: 8 }, children: [
|
|
416
|
+
props.branding?.logoUrl ? /* @__PURE__ */ jsx("img", { src: props.branding.logoUrl, alt: "", style: { height: 32 } }) : null,
|
|
417
|
+
props.branding?.appName ? /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, color: "#374151" }, children: props.branding.appName }) : null
|
|
418
|
+
] }),
|
|
419
|
+
/* @__PURE__ */ jsx("h1", { style: { fontSize: 22, margin: "8px 0 4px" }, children: props.documentTitle }),
|
|
420
|
+
/* @__PURE__ */ jsx("p", { style: { color: "#6b7280", fontSize: 14, marginTop: 0 }, children: "Review the document below, then sign \u2014 no account or login needed." }),
|
|
421
|
+
/* @__PURE__ */ jsx("section", { style: { margin: "16px 0" }, children: /* @__PURE__ */ jsx(
|
|
422
|
+
PdfViewer,
|
|
423
|
+
{
|
|
424
|
+
url: props.sourceUrl,
|
|
425
|
+
placement: props.placement,
|
|
426
|
+
workerSrc: props.workerSrc,
|
|
427
|
+
primaryColor: primary
|
|
428
|
+
}
|
|
429
|
+
) }),
|
|
430
|
+
/* @__PURE__ */ jsxs("section", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
|
|
431
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
432
|
+
/* @__PURE__ */ jsx("label", { style: { fontSize: 13, fontWeight: 600, color: "#374151" }, children: "Your signature" }),
|
|
433
|
+
/* @__PURE__ */ jsx("div", { style: { marginTop: 6 }, children: /* @__PURE__ */ jsx(SignaturePad, { primaryColor: primary, onChange: setSignaturePng }) })
|
|
434
|
+
] }),
|
|
435
|
+
/* @__PURE__ */ jsx(ConsentBlock, { value: consent, onChange: setConsent, primaryColor: primary }),
|
|
436
|
+
error ? /* @__PURE__ */ jsx(
|
|
437
|
+
"div",
|
|
438
|
+
{
|
|
439
|
+
style: {
|
|
440
|
+
background: "#fef2f2",
|
|
441
|
+
border: "1px solid #fecaca",
|
|
442
|
+
color: "#b91c1c",
|
|
443
|
+
borderRadius: 8,
|
|
444
|
+
padding: "10px 12px",
|
|
445
|
+
fontSize: 14
|
|
446
|
+
},
|
|
447
|
+
children: error
|
|
448
|
+
}
|
|
449
|
+
) : null,
|
|
450
|
+
/* @__PURE__ */ jsx(
|
|
451
|
+
"button",
|
|
452
|
+
{
|
|
453
|
+
type: "button",
|
|
454
|
+
onClick: submit,
|
|
455
|
+
disabled: !canSubmit,
|
|
456
|
+
style: {
|
|
457
|
+
padding: "14px 16px",
|
|
458
|
+
borderRadius: 10,
|
|
459
|
+
border: "none",
|
|
460
|
+
background: canSubmit ? primary : "#c7c9d1",
|
|
461
|
+
color: "#fff",
|
|
462
|
+
fontSize: 16,
|
|
463
|
+
fontWeight: 600,
|
|
464
|
+
cursor: canSubmit ? "pointer" : "not-allowed"
|
|
465
|
+
},
|
|
466
|
+
children: submitting ? "Signing\u2026" : "Sign document"
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
] })
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export { ConsentBlock, FieldDesigner, PdfViewer, SignaturePad, SigningExperience };
|
|
476
|
+
//# sourceMappingURL=index.js.map
|
|
477
|
+
//# sourceMappingURL=index.js.map
|