@josephomills/esign 0.2.2 → 0.3.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.
@@ -0,0 +1,351 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var SignaturePadLib = require('signature_pad');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var SignaturePadLib__default = /*#__PURE__*/_interopDefault(SignaturePadLib);
11
+
12
+ // src/ui/SignaturePad.tsx
13
+ function SignaturePad({ primaryColor = "#4f46e5", onChange }) {
14
+ const [mode, setMode] = react.useState("draw");
15
+ const [typed, setTyped] = react.useState("");
16
+ const canvasRef = react.useRef(null);
17
+ const padRef = react.useRef(null);
18
+ react.useEffect(() => {
19
+ if (mode !== "draw") return;
20
+ const canvas = canvasRef.current;
21
+ if (!canvas) return;
22
+ const pad = new SignaturePadLib__default.default(canvas, { penColor: "#111827", minWidth: 0.8, maxWidth: 2.2 });
23
+ padRef.current = pad;
24
+ const resize = () => {
25
+ const ratio = Math.max(window.devicePixelRatio || 1, 1);
26
+ const rect = canvas.getBoundingClientRect();
27
+ canvas.width = Math.floor(rect.width * ratio);
28
+ canvas.height = Math.floor(rect.height * ratio);
29
+ canvas.getContext("2d")?.scale(ratio, ratio);
30
+ pad.clear();
31
+ onChange(null);
32
+ };
33
+ pad.addEventListener(
34
+ "endStroke",
35
+ () => onChange(pad.isEmpty() ? null : pad.toDataURL("image/png"))
36
+ );
37
+ resize();
38
+ window.addEventListener("resize", resize);
39
+ return () => {
40
+ window.removeEventListener("resize", resize);
41
+ pad.off();
42
+ padRef.current = null;
43
+ };
44
+ }, [mode, onChange]);
45
+ react.useEffect(() => {
46
+ if (mode !== "type") return;
47
+ const name = typed.trim();
48
+ if (!name) {
49
+ onChange(null);
50
+ return;
51
+ }
52
+ const c = document.createElement("canvas");
53
+ c.width = 720;
54
+ c.height = 200;
55
+ const ctx = c.getContext("2d");
56
+ if (!ctx) return;
57
+ ctx.fillStyle = "#111827";
58
+ ctx.font = "72px 'Segoe Script', 'Brush Script MT', cursive";
59
+ ctx.textBaseline = "middle";
60
+ ctx.fillText(name, 24, 110);
61
+ onChange(c.toDataURL("image/png"));
62
+ }, [mode, typed, onChange]);
63
+ const tab = (m, label) => /* @__PURE__ */ jsxRuntime.jsx(
64
+ "button",
65
+ {
66
+ type: "button",
67
+ onClick: () => {
68
+ setMode(m);
69
+ onChange(null);
70
+ },
71
+ style: {
72
+ flex: 1,
73
+ padding: "8px 12px",
74
+ border: "none",
75
+ borderBottom: mode === m ? `2px solid ${primaryColor}` : "2px solid transparent",
76
+ background: "transparent",
77
+ fontWeight: mode === m ? 600 : 400,
78
+ color: mode === m ? primaryColor : "#6b7280",
79
+ cursor: "pointer"
80
+ },
81
+ children: label
82
+ }
83
+ );
84
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { border: "1px solid #e5e7eb", borderRadius: 12, overflow: "hidden" }, children: [
85
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", borderBottom: "1px solid #f3f4f6" }, children: [
86
+ tab("draw", "Draw"),
87
+ tab("type", "Type")
88
+ ] }),
89
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", background: "#fff" }, children: [
90
+ mode === "draw" ? /* @__PURE__ */ jsxRuntime.jsx(
91
+ "canvas",
92
+ {
93
+ ref: canvasRef,
94
+ style: { width: "100%", height: 180, touchAction: "none", display: "block" }
95
+ }
96
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 16 }, children: /* @__PURE__ */ jsxRuntime.jsx(
97
+ "input",
98
+ {
99
+ value: typed,
100
+ onChange: (e) => setTyped(e.target.value),
101
+ placeholder: "Type your name",
102
+ style: {
103
+ width: "100%",
104
+ fontSize: 40,
105
+ fontFamily: "'Segoe Script', 'Brush Script MT', cursive",
106
+ border: "none",
107
+ borderBottom: "1px solid #e5e7eb",
108
+ outline: "none",
109
+ padding: "8px 0"
110
+ }
111
+ }
112
+ ) }),
113
+ /* @__PURE__ */ jsxRuntime.jsx(
114
+ "button",
115
+ {
116
+ type: "button",
117
+ onClick: () => {
118
+ padRef.current?.clear();
119
+ setTyped("");
120
+ onChange(null);
121
+ },
122
+ style: {
123
+ position: "absolute",
124
+ top: 8,
125
+ right: 8,
126
+ fontSize: 12,
127
+ color: "#6b7280",
128
+ background: "rgba(255,255,255,0.8)",
129
+ border: "1px solid #e5e7eb",
130
+ borderRadius: 6,
131
+ padding: "2px 8px",
132
+ cursor: "pointer"
133
+ },
134
+ children: "Clear"
135
+ }
136
+ )
137
+ ] })
138
+ ] });
139
+ }
140
+ function ConsentBlock({ value, onChange, primaryColor = "#4f46e5" }) {
141
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
142
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [
143
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 13, fontWeight: 600, color: "#374151" }, children: "Full legal name" }),
144
+ /* @__PURE__ */ jsxRuntime.jsx(
145
+ "input",
146
+ {
147
+ value: value.signerName,
148
+ onChange: (e) => onChange({ ...value, signerName: e.target.value }),
149
+ placeholder: "e.g. Ada Lovelace",
150
+ style: {
151
+ padding: "10px 12px",
152
+ border: "1px solid #d1d5db",
153
+ borderRadius: 8,
154
+ fontSize: 15,
155
+ outline: "none"
156
+ }
157
+ }
158
+ )
159
+ ] }),
160
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", gap: 10, alignItems: "flex-start", cursor: "pointer" }, children: [
161
+ /* @__PURE__ */ jsxRuntime.jsx(
162
+ "input",
163
+ {
164
+ type: "checkbox",
165
+ checked: value.consent,
166
+ onChange: (e) => onChange({ ...value, consent: e.target.checked }),
167
+ style: { marginTop: 3, accentColor: primaryColor, width: 16, height: 16 }
168
+ }
169
+ ),
170
+ /* @__PURE__ */ jsxRuntime.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." })
171
+ ] })
172
+ ] });
173
+ }
174
+ function PdfViewer({ url, placement, workerSrc, primaryColor = "#4f46e5" }) {
175
+ const containerRef = react.useRef(null);
176
+ const [failed, setFailed] = react.useState(false);
177
+ react.useEffect(() => {
178
+ let cancelled = false;
179
+ void (async () => {
180
+ try {
181
+ const pdfjs = await import('pdfjs-dist');
182
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc ?? `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
183
+ const doc = await pdfjs.getDocument({ url }).promise;
184
+ const container = containerRef.current;
185
+ if (!container || cancelled) return;
186
+ container.innerHTML = "";
187
+ const width = container.clientWidth || 640;
188
+ const ratio = Math.max(window.devicePixelRatio || 1, 1);
189
+ for (let p = 1; p <= doc.numPages; p++) {
190
+ const page = await doc.getPage(p);
191
+ if (cancelled) return;
192
+ const base = page.getViewport({ scale: 1 });
193
+ const viewport = page.getViewport({ scale: width / base.width });
194
+ const canvas = document.createElement("canvas");
195
+ canvas.width = Math.floor(viewport.width * ratio);
196
+ canvas.height = Math.floor(viewport.height * ratio);
197
+ canvas.style.width = "100%";
198
+ canvas.style.display = "block";
199
+ canvas.style.borderRadius = "4px";
200
+ const wrap = document.createElement("div");
201
+ wrap.style.position = "relative";
202
+ wrap.style.marginBottom = "14px";
203
+ wrap.style.boxShadow = "0 1px 4px rgba(0,0,0,0.12)";
204
+ wrap.appendChild(canvas);
205
+ if (placement && placement.page === p) {
206
+ const box = document.createElement("div");
207
+ 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;`;
208
+ const label = document.createElement("span");
209
+ label.textContent = "Signature";
210
+ label.style.cssText = `position:absolute;top:-18px;left:0;font-size:11px;font-weight:600;color:${primaryColor};`;
211
+ box.appendChild(label);
212
+ wrap.appendChild(box);
213
+ }
214
+ container.appendChild(wrap);
215
+ const ctx = canvas.getContext("2d");
216
+ if (!ctx) continue;
217
+ ctx.scale(ratio, ratio);
218
+ await page.render({ canvasContext: ctx, viewport, canvas }).promise;
219
+ }
220
+ } catch {
221
+ if (!cancelled) setFailed(true);
222
+ }
223
+ })();
224
+ return () => {
225
+ cancelled = true;
226
+ };
227
+ }, [url, placement, workerSrc, primaryColor]);
228
+ if (failed) {
229
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
230
+ /* @__PURE__ */ jsxRuntime.jsx(
231
+ "iframe",
232
+ {
233
+ src: `${url}#toolbar=0`,
234
+ title: "Document",
235
+ style: { width: "100%", height: 520, border: "1px solid #e5e7eb", borderRadius: 8 }
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: url, target: "_blank", rel: "noreferrer", style: { fontSize: 13, color: primaryColor }, children: "Download to read" })
239
+ ] });
240
+ }
241
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, style: { width: "100%" } });
242
+ }
243
+ function SigningExperience(props) {
244
+ const primary = props.branding?.primaryColor ?? "#4f46e5";
245
+ const [signaturePng, setSignaturePng] = react.useState(null);
246
+ const [consent, setConsent] = react.useState({ signerName: "", consent: false });
247
+ const [submitting, setSubmitting] = react.useState(false);
248
+ const [error, setError] = react.useState(null);
249
+ const canSubmit = !!signaturePng && consent.consent && consent.signerName.trim().length > 0 && !submitting;
250
+ async function submit() {
251
+ if (!canSubmit) return;
252
+ setSubmitting(true);
253
+ setError(null);
254
+ try {
255
+ const res = await fetch(props.submitUrl, {
256
+ method: "POST",
257
+ headers: { "content-type": "application/json" },
258
+ body: JSON.stringify({
259
+ signaturePng,
260
+ signerName: consent.signerName.trim(),
261
+ consent: true
262
+ })
263
+ });
264
+ if (!res.ok) {
265
+ const body = await res.json().catch(() => null);
266
+ throw new Error(body?.error?.message ?? "Could not submit your signature.");
267
+ }
268
+ props.onSigned?.();
269
+ } catch (e) {
270
+ setError(e instanceof Error ? e.message : "Something went wrong.");
271
+ } finally {
272
+ setSubmitting(false);
273
+ }
274
+ }
275
+ return /* @__PURE__ */ jsxRuntime.jsxs(
276
+ "div",
277
+ {
278
+ style: {
279
+ maxWidth: 760,
280
+ margin: "0 auto",
281
+ padding: "24px 16px 64px",
282
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
283
+ color: "#111827"
284
+ },
285
+ children: [
286
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { style: { display: "flex", alignItems: "center", gap: 12, marginBottom: 8 }, children: [
287
+ props.branding?.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: props.branding.logoUrl, alt: "", style: { height: 32 } }) : null,
288
+ props.branding?.appName ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 600, color: "#374151" }, children: props.branding.appName }) : null
289
+ ] }),
290
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: 22, margin: "8px 0 4px" }, children: props.documentTitle }),
291
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#6b7280", fontSize: 14, marginTop: 0 }, children: "Review the document below, then sign \u2014 no account or login needed." }),
292
+ /* @__PURE__ */ jsxRuntime.jsx("section", { style: { margin: "16px 0" }, children: /* @__PURE__ */ jsxRuntime.jsx(
293
+ PdfViewer,
294
+ {
295
+ url: props.sourceUrl,
296
+ placement: props.placement,
297
+ workerSrc: props.workerSrc,
298
+ primaryColor: primary
299
+ }
300
+ ) }),
301
+ /* @__PURE__ */ jsxRuntime.jsxs("section", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
302
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
303
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: { fontSize: 13, fontWeight: 600, color: "#374151" }, children: "Your signature" }),
304
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 6 }, children: /* @__PURE__ */ jsxRuntime.jsx(SignaturePad, { primaryColor: primary, onChange: setSignaturePng }) })
305
+ ] }),
306
+ /* @__PURE__ */ jsxRuntime.jsx(ConsentBlock, { value: consent, onChange: setConsent, primaryColor: primary }),
307
+ error ? /* @__PURE__ */ jsxRuntime.jsx(
308
+ "div",
309
+ {
310
+ style: {
311
+ background: "#fef2f2",
312
+ border: "1px solid #fecaca",
313
+ color: "#b91c1c",
314
+ borderRadius: 8,
315
+ padding: "10px 12px",
316
+ fontSize: 14
317
+ },
318
+ children: error
319
+ }
320
+ ) : null,
321
+ /* @__PURE__ */ jsxRuntime.jsx(
322
+ "button",
323
+ {
324
+ type: "button",
325
+ onClick: submit,
326
+ disabled: !canSubmit,
327
+ style: {
328
+ padding: "14px 16px",
329
+ borderRadius: 10,
330
+ border: "none",
331
+ background: canSubmit ? primary : "#c7c9d1",
332
+ color: "#fff",
333
+ fontSize: 16,
334
+ fontWeight: 600,
335
+ cursor: canSubmit ? "pointer" : "not-allowed"
336
+ },
337
+ children: submitting ? "Signing\u2026" : "Sign document"
338
+ }
339
+ )
340
+ ] })
341
+ ]
342
+ }
343
+ );
344
+ }
345
+
346
+ exports.ConsentBlock = ConsentBlock;
347
+ exports.PdfViewer = PdfViewer;
348
+ exports.SignaturePad = SignaturePad;
349
+ exports.SigningExperience = SigningExperience;
350
+ //# sourceMappingURL=index.cjs.map
351
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/ui/SignaturePad.tsx","../../src/ui/ConsentBlock.tsx","../../src/ui/PdfViewer.tsx","../../src/ui/SigningExperience.tsx"],"names":["useState","useRef","useEffect","SignaturePadLib","jsx","jsxs"],"mappings":"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","file":"index.cjs","sourcesContent":["import SignaturePadLib from \"signature_pad\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport interface SignaturePadProps {\n primaryColor?: string;\n /** Emits a PNG data URL of the current signature, or null when empty. */\n onChange: (pngDataUrl: string | null) => void;\n}\n\n/**\n * Capture a signature by drawing (signature_pad, touch + mouse, DPI-aware) or by\n * typing a name rendered in a script font. Both yield the same PNG data URL that\n * the seal step stamps onto the document.\n */\nexport function SignaturePad({ primaryColor = \"#4f46e5\", onChange }: SignaturePadProps) {\n const [mode, setMode] = useState<\"draw\" | \"type\">(\"draw\");\n const [typed, setTyped] = useState(\"\");\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const padRef = useRef<SignaturePadLib | null>(null);\n\n useEffect(() => {\n if (mode !== \"draw\") return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n const pad = new SignaturePadLib(canvas, { penColor: \"#111827\", minWidth: 0.8, maxWidth: 2.2 });\n padRef.current = pad;\n const resize = () => {\n const ratio = Math.max(window.devicePixelRatio || 1, 1);\n const rect = canvas.getBoundingClientRect();\n canvas.width = Math.floor(rect.width * ratio);\n canvas.height = Math.floor(rect.height * ratio);\n canvas.getContext(\"2d\")?.scale(ratio, ratio);\n pad.clear();\n onChange(null);\n };\n pad.addEventListener(\"endStroke\", () =>\n onChange(pad.isEmpty() ? null : pad.toDataURL(\"image/png\")),\n );\n resize();\n window.addEventListener(\"resize\", resize);\n return () => {\n window.removeEventListener(\"resize\", resize);\n pad.off();\n padRef.current = null;\n };\n }, [mode, onChange]);\n\n useEffect(() => {\n if (mode !== \"type\") return;\n const name = typed.trim();\n if (!name) {\n onChange(null);\n return;\n }\n const c = document.createElement(\"canvas\");\n c.width = 720;\n c.height = 200;\n const ctx = c.getContext(\"2d\");\n if (!ctx) return;\n ctx.fillStyle = \"#111827\";\n ctx.font = \"72px 'Segoe Script', 'Brush Script MT', cursive\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(name, 24, 110);\n onChange(c.toDataURL(\"image/png\"));\n }, [mode, typed, onChange]);\n\n const tab = (m: \"draw\" | \"type\", label: string) => (\n <button\n type=\"button\"\n onClick={() => {\n setMode(m);\n onChange(null);\n }}\n style={{\n flex: 1,\n padding: \"8px 12px\",\n border: \"none\",\n borderBottom: mode === m ? `2px solid ${primaryColor}` : \"2px solid transparent\",\n background: \"transparent\",\n fontWeight: mode === m ? 600 : 400,\n color: mode === m ? primaryColor : \"#6b7280\",\n cursor: \"pointer\",\n }}\n >\n {label}\n </button>\n );\n\n return (\n <div style={{ border: \"1px solid #e5e7eb\", borderRadius: 12, overflow: \"hidden\" }}>\n <div style={{ display: \"flex\", borderBottom: \"1px solid #f3f4f6\" }}>\n {tab(\"draw\", \"Draw\")}\n {tab(\"type\", \"Type\")}\n </div>\n <div style={{ position: \"relative\", background: \"#fff\" }}>\n {mode === \"draw\" ? (\n <canvas\n ref={canvasRef}\n style={{ width: \"100%\", height: 180, touchAction: \"none\", display: \"block\" }}\n />\n ) : (\n <div style={{ padding: 16 }}>\n <input\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n placeholder=\"Type your name\"\n style={{\n width: \"100%\",\n fontSize: 40,\n fontFamily: \"'Segoe Script', 'Brush Script MT', cursive\",\n border: \"none\",\n borderBottom: \"1px solid #e5e7eb\",\n outline: \"none\",\n padding: \"8px 0\",\n }}\n />\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => {\n padRef.current?.clear();\n setTyped(\"\");\n onChange(null);\n }}\n style={{\n position: \"absolute\",\n top: 8,\n right: 8,\n fontSize: 12,\n color: \"#6b7280\",\n background: \"rgba(255,255,255,0.8)\",\n border: \"1px solid #e5e7eb\",\n borderRadius: 6,\n padding: \"2px 8px\",\n cursor: \"pointer\",\n }}\n >\n Clear\n </button>\n </div>\n </div>\n );\n}\n","export interface ConsentValue {\n signerName: string;\n consent: boolean;\n}\n\nexport interface ConsentBlockProps {\n value: ConsentValue;\n onChange: (value: ConsentValue) => void;\n primaryColor?: string;\n}\n\n/**\n * The electronic-signature intent record: a typed full legal name + an explicit\n * consent checkbox. Together with the audit trail this is what makes the simple\n * e-signature legally valid.\n */\nexport function ConsentBlock({ value, onChange, primaryColor = \"#4f46e5\" }: ConsentBlockProps) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\n <label style={{ display: \"flex\", flexDirection: \"column\", gap: 4 }}>\n <span style={{ fontSize: 13, fontWeight: 600, color: \"#374151\" }}>Full legal name</span>\n <input\n value={value.signerName}\n onChange={(e) => onChange({ ...value, signerName: e.target.value })}\n placeholder=\"e.g. Ada Lovelace\"\n style={{\n padding: \"10px 12px\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 15,\n outline: \"none\",\n }}\n />\n </label>\n <label style={{ display: \"flex\", gap: 10, alignItems: \"flex-start\", cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={value.consent}\n onChange={(e) => onChange({ ...value, consent: e.target.checked })}\n style={{ marginTop: 3, accentColor: primaryColor, width: 16, height: 16 }}\n />\n <span style={{ fontSize: 13, color: \"#4b5563\", lineHeight: 1.5 }}>\n I agree to sign this document electronically. I understand my electronic\n signature is legally binding and that an audit record (time, IP, and a\n tamper-evident hash) is kept with the signed document.\n </span>\n </label>\n </div>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\n\nexport interface ViewerPlacement {\n page: number;\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\nexport interface PdfViewerProps {\n /** Token-gated source PDF URL. */\n url: string;\n /** Optional signature-box hint, drawn on its page at normalized coords. */\n placement?: ViewerPlacement;\n /** Override the pdf.js worker (defaults to the unpkg .mjs for the bundled version). */\n workerSrc?: string;\n primaryColor?: string;\n}\n\n/**\n * Render the PDF to canvases (pdf.js, responsive, DPI-aware) with the signature\n * box overlaid where the manager placed it. Falls back to an <iframe> + download\n * link if pdf.js fails to load — so the document is always readable.\n */\nexport function PdfViewer({ url, placement, workerSrc, primaryColor = \"#4f46e5\" }: PdfViewerProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [failed, setFailed] = useState(false);\n\n useEffect(() => {\n let cancelled = false;\n void (async () => {\n try {\n const pdfjs = await import(\"pdfjs-dist\");\n pdfjs.GlobalWorkerOptions.workerSrc =\n workerSrc ?? `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;\n const doc = await pdfjs.getDocument({ url }).promise;\n const container = containerRef.current;\n if (!container || cancelled) return;\n container.innerHTML = \"\";\n const width = container.clientWidth || 640;\n const ratio = Math.max(window.devicePixelRatio || 1, 1);\n\n for (let p = 1; p <= doc.numPages; p++) {\n const page = await doc.getPage(p);\n if (cancelled) return;\n const base = page.getViewport({ scale: 1 });\n const viewport = page.getViewport({ scale: width / base.width });\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(viewport.width * ratio);\n canvas.height = Math.floor(viewport.height * ratio);\n canvas.style.width = \"100%\";\n canvas.style.display = \"block\";\n canvas.style.borderRadius = \"4px\";\n\n const wrap = document.createElement(\"div\");\n wrap.style.position = \"relative\";\n wrap.style.marginBottom = \"14px\";\n wrap.style.boxShadow = \"0 1px 4px rgba(0,0,0,0.12)\";\n wrap.appendChild(canvas);\n\n if (placement && placement.page === p) {\n const box = document.createElement(\"div\");\n box.style.cssText = `position:absolute;left:${placement.x * 100}%;top:${\n placement.y * 100\n }%;width:${placement.w * 100}%;height:${placement.h * 100}%;border:2px dashed ${primaryColor};background:${primaryColor}1a;border-radius:4px;pointer-events:none;`;\n const label = document.createElement(\"span\");\n label.textContent = \"Signature\";\n label.style.cssText = `position:absolute;top:-18px;left:0;font-size:11px;font-weight:600;color:${primaryColor};`;\n box.appendChild(label);\n wrap.appendChild(box);\n }\n\n container.appendChild(wrap);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) continue;\n ctx.scale(ratio, ratio);\n await page.render({ canvasContext: ctx, viewport, canvas }).promise;\n }\n } catch {\n if (!cancelled) setFailed(true);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [url, placement, workerSrc, primaryColor]);\n\n if (failed) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>\n <iframe\n src={`${url}#toolbar=0`}\n title=\"Document\"\n style={{ width: \"100%\", height: 520, border: \"1px solid #e5e7eb\", borderRadius: 8 }}\n />\n <a href={url} target=\"_blank\" rel=\"noreferrer\" style={{ fontSize: 13, color: primaryColor }}>\n Download to read\n </a>\n </div>\n );\n }\n\n return <div ref={containerRef} style={{ width: \"100%\" }} />;\n}\n","import { useState } from \"react\";\n\nimport { ConsentBlock, type ConsentValue } from \"./ConsentBlock\";\nimport { PdfViewer, type ViewerPlacement } from \"./PdfViewer\";\nimport { SignaturePad } from \"./SignaturePad\";\n\nexport interface SigningBranding {\n appName?: string;\n logoUrl?: string;\n primaryColor?: string;\n}\n\nexport interface SigningExperienceProps {\n /** Token-gated source PDF URL (GET /api/esign/source/<token>). */\n sourceUrl: string;\n /** Submit endpoint (POST /sign/<token>/submit). */\n submitUrl: string;\n documentTitle: string;\n placement?: ViewerPlacement;\n branding?: SigningBranding;\n /** Called after a successful signature submit (host navigates to /done). */\n onSigned?: () => void;\n workerSrc?: string;\n}\n\n/**\n * The full host-branded signer page body: read the PDF (placement highlighted),\n * draw/type a signature, consent, and submit. Self-contained styling so it sits\n * cleanly inside any host shell.\n */\nexport function SigningExperience(props: SigningExperienceProps) {\n const primary = props.branding?.primaryColor ?? \"#4f46e5\";\n const [signaturePng, setSignaturePng] = useState<string | null>(null);\n const [consent, setConsent] = useState<ConsentValue>({ signerName: \"\", consent: false });\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit =\n !!signaturePng && consent.consent && consent.signerName.trim().length > 0 && !submitting;\n\n async function submit() {\n if (!canSubmit) return;\n setSubmitting(true);\n setError(null);\n try {\n const res = await fetch(props.submitUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n signaturePng,\n signerName: consent.signerName.trim(),\n consent: true,\n }),\n });\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as {\n error?: { message?: string };\n } | null;\n throw new Error(body?.error?.message ?? \"Could not submit your signature.\");\n }\n props.onSigned?.();\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Something went wrong.\");\n } finally {\n setSubmitting(false);\n }\n }\n\n return (\n <div\n style={{\n maxWidth: 760,\n margin: \"0 auto\",\n padding: \"24px 16px 64px\",\n fontFamily:\n \"system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif\",\n color: \"#111827\",\n }}\n >\n <header style={{ display: \"flex\", alignItems: \"center\", gap: 12, marginBottom: 8 }}>\n {props.branding?.logoUrl ? (\n <img src={props.branding.logoUrl} alt=\"\" style={{ height: 32 }} />\n ) : null}\n {props.branding?.appName ? (\n <span style={{ fontWeight: 600, color: \"#374151\" }}>{props.branding.appName}</span>\n ) : null}\n </header>\n\n <h1 style={{ fontSize: 22, margin: \"8px 0 4px\" }}>{props.documentTitle}</h1>\n <p style={{ color: \"#6b7280\", fontSize: 14, marginTop: 0 }}>\n Review the document below, then sign — no account or login needed.\n </p>\n\n <section style={{ margin: \"16px 0\" }}>\n <PdfViewer\n url={props.sourceUrl}\n placement={props.placement}\n workerSrc={props.workerSrc}\n primaryColor={primary}\n />\n </section>\n\n <section style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>\n <div>\n <label style={{ fontSize: 13, fontWeight: 600, color: \"#374151\" }}>Your signature</label>\n <div style={{ marginTop: 6 }}>\n <SignaturePad primaryColor={primary} onChange={setSignaturePng} />\n </div>\n </div>\n\n <ConsentBlock value={consent} onChange={setConsent} primaryColor={primary} />\n\n {error ? (\n <div\n style={{\n background: \"#fef2f2\",\n border: \"1px solid #fecaca\",\n color: \"#b91c1c\",\n borderRadius: 8,\n padding: \"10px 12px\",\n fontSize: 14,\n }}\n >\n {error}\n </div>\n ) : null}\n\n <button\n type=\"button\"\n onClick={submit}\n disabled={!canSubmit}\n style={{\n padding: \"14px 16px\",\n borderRadius: 10,\n border: \"none\",\n background: canSubmit ? primary : \"#c7c9d1\",\n color: \"#fff\",\n fontSize: 16,\n fontWeight: 600,\n cursor: canSubmit ? \"pointer\" : \"not-allowed\",\n }}\n >\n {submitting ? \"Signing…\" : \"Sign document\"}\n </button>\n </section>\n </div>\n );\n}\n"]}
@@ -0,0 +1,78 @@
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 SigningBranding {
55
+ appName?: string;
56
+ logoUrl?: string;
57
+ primaryColor?: string;
58
+ }
59
+ interface SigningExperienceProps {
60
+ /** Token-gated source PDF URL (GET /api/esign/source/<token>). */
61
+ sourceUrl: string;
62
+ /** Submit endpoint (POST /sign/<token>/submit). */
63
+ submitUrl: string;
64
+ documentTitle: string;
65
+ placement?: ViewerPlacement;
66
+ branding?: SigningBranding;
67
+ /** Called after a successful signature submit (host navigates to /done). */
68
+ onSigned?: () => void;
69
+ workerSrc?: string;
70
+ }
71
+ /**
72
+ * The full host-branded signer page body: read the PDF (placement highlighted),
73
+ * draw/type a signature, consent, and submit. Self-contained styling so it sits
74
+ * cleanly inside any host shell.
75
+ */
76
+ declare function SigningExperience(props: SigningExperienceProps): react.JSX.Element;
77
+
78
+ export { ConsentBlock, type ConsentBlockProps, type ConsentValue, PdfViewer, type PdfViewerProps, SignaturePad, type SignaturePadProps, type SigningBranding, SigningExperience, type SigningExperienceProps, type ViewerPlacement };
@@ -0,0 +1,78 @@
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 SigningBranding {
55
+ appName?: string;
56
+ logoUrl?: string;
57
+ primaryColor?: string;
58
+ }
59
+ interface SigningExperienceProps {
60
+ /** Token-gated source PDF URL (GET /api/esign/source/<token>). */
61
+ sourceUrl: string;
62
+ /** Submit endpoint (POST /sign/<token>/submit). */
63
+ submitUrl: string;
64
+ documentTitle: string;
65
+ placement?: ViewerPlacement;
66
+ branding?: SigningBranding;
67
+ /** Called after a successful signature submit (host navigates to /done). */
68
+ onSigned?: () => void;
69
+ workerSrc?: string;
70
+ }
71
+ /**
72
+ * The full host-branded signer page body: read the PDF (placement highlighted),
73
+ * draw/type a signature, consent, and submit. Self-contained styling so it sits
74
+ * cleanly inside any host shell.
75
+ */
76
+ declare function SigningExperience(props: SigningExperienceProps): react.JSX.Element;
77
+
78
+ export { ConsentBlock, type ConsentBlockProps, type ConsentValue, PdfViewer, type PdfViewerProps, SignaturePad, type SignaturePadProps, type SigningBranding, SigningExperience, type SigningExperienceProps, type ViewerPlacement };
@@ -0,0 +1,342 @@
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 SigningExperience(props) {
238
+ const primary = props.branding?.primaryColor ?? "#4f46e5";
239
+ const [signaturePng, setSignaturePng] = useState(null);
240
+ const [consent, setConsent] = useState({ signerName: "", consent: false });
241
+ const [submitting, setSubmitting] = useState(false);
242
+ const [error, setError] = useState(null);
243
+ const canSubmit = !!signaturePng && consent.consent && consent.signerName.trim().length > 0 && !submitting;
244
+ async function submit() {
245
+ if (!canSubmit) return;
246
+ setSubmitting(true);
247
+ setError(null);
248
+ try {
249
+ const res = await fetch(props.submitUrl, {
250
+ method: "POST",
251
+ headers: { "content-type": "application/json" },
252
+ body: JSON.stringify({
253
+ signaturePng,
254
+ signerName: consent.signerName.trim(),
255
+ consent: true
256
+ })
257
+ });
258
+ if (!res.ok) {
259
+ const body = await res.json().catch(() => null);
260
+ throw new Error(body?.error?.message ?? "Could not submit your signature.");
261
+ }
262
+ props.onSigned?.();
263
+ } catch (e) {
264
+ setError(e instanceof Error ? e.message : "Something went wrong.");
265
+ } finally {
266
+ setSubmitting(false);
267
+ }
268
+ }
269
+ return /* @__PURE__ */ jsxs(
270
+ "div",
271
+ {
272
+ style: {
273
+ maxWidth: 760,
274
+ margin: "0 auto",
275
+ padding: "24px 16px 64px",
276
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
277
+ color: "#111827"
278
+ },
279
+ children: [
280
+ /* @__PURE__ */ jsxs("header", { style: { display: "flex", alignItems: "center", gap: 12, marginBottom: 8 }, children: [
281
+ props.branding?.logoUrl ? /* @__PURE__ */ jsx("img", { src: props.branding.logoUrl, alt: "", style: { height: 32 } }) : null,
282
+ props.branding?.appName ? /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, color: "#374151" }, children: props.branding.appName }) : null
283
+ ] }),
284
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: 22, margin: "8px 0 4px" }, children: props.documentTitle }),
285
+ /* @__PURE__ */ jsx("p", { style: { color: "#6b7280", fontSize: 14, marginTop: 0 }, children: "Review the document below, then sign \u2014 no account or login needed." }),
286
+ /* @__PURE__ */ jsx("section", { style: { margin: "16px 0" }, children: /* @__PURE__ */ jsx(
287
+ PdfViewer,
288
+ {
289
+ url: props.sourceUrl,
290
+ placement: props.placement,
291
+ workerSrc: props.workerSrc,
292
+ primaryColor: primary
293
+ }
294
+ ) }),
295
+ /* @__PURE__ */ jsxs("section", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
296
+ /* @__PURE__ */ jsxs("div", { children: [
297
+ /* @__PURE__ */ jsx("label", { style: { fontSize: 13, fontWeight: 600, color: "#374151" }, children: "Your signature" }),
298
+ /* @__PURE__ */ jsx("div", { style: { marginTop: 6 }, children: /* @__PURE__ */ jsx(SignaturePad, { primaryColor: primary, onChange: setSignaturePng }) })
299
+ ] }),
300
+ /* @__PURE__ */ jsx(ConsentBlock, { value: consent, onChange: setConsent, primaryColor: primary }),
301
+ error ? /* @__PURE__ */ jsx(
302
+ "div",
303
+ {
304
+ style: {
305
+ background: "#fef2f2",
306
+ border: "1px solid #fecaca",
307
+ color: "#b91c1c",
308
+ borderRadius: 8,
309
+ padding: "10px 12px",
310
+ fontSize: 14
311
+ },
312
+ children: error
313
+ }
314
+ ) : null,
315
+ /* @__PURE__ */ jsx(
316
+ "button",
317
+ {
318
+ type: "button",
319
+ onClick: submit,
320
+ disabled: !canSubmit,
321
+ style: {
322
+ padding: "14px 16px",
323
+ borderRadius: 10,
324
+ border: "none",
325
+ background: canSubmit ? primary : "#c7c9d1",
326
+ color: "#fff",
327
+ fontSize: 16,
328
+ fontWeight: 600,
329
+ cursor: canSubmit ? "pointer" : "not-allowed"
330
+ },
331
+ children: submitting ? "Signing\u2026" : "Sign document"
332
+ }
333
+ )
334
+ ] })
335
+ ]
336
+ }
337
+ );
338
+ }
339
+
340
+ export { ConsentBlock, PdfViewer, SignaturePad, SigningExperience };
341
+ //# sourceMappingURL=index.js.map
342
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/ui/SignaturePad.tsx","../../src/ui/ConsentBlock.tsx","../../src/ui/PdfViewer.tsx","../../src/ui/SigningExperience.tsx"],"names":["jsxs","jsx","useRef","useState","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import SignaturePadLib from \"signature_pad\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport interface SignaturePadProps {\n primaryColor?: string;\n /** Emits a PNG data URL of the current signature, or null when empty. */\n onChange: (pngDataUrl: string | null) => void;\n}\n\n/**\n * Capture a signature by drawing (signature_pad, touch + mouse, DPI-aware) or by\n * typing a name rendered in a script font. Both yield the same PNG data URL that\n * the seal step stamps onto the document.\n */\nexport function SignaturePad({ primaryColor = \"#4f46e5\", onChange }: SignaturePadProps) {\n const [mode, setMode] = useState<\"draw\" | \"type\">(\"draw\");\n const [typed, setTyped] = useState(\"\");\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const padRef = useRef<SignaturePadLib | null>(null);\n\n useEffect(() => {\n if (mode !== \"draw\") return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n const pad = new SignaturePadLib(canvas, { penColor: \"#111827\", minWidth: 0.8, maxWidth: 2.2 });\n padRef.current = pad;\n const resize = () => {\n const ratio = Math.max(window.devicePixelRatio || 1, 1);\n const rect = canvas.getBoundingClientRect();\n canvas.width = Math.floor(rect.width * ratio);\n canvas.height = Math.floor(rect.height * ratio);\n canvas.getContext(\"2d\")?.scale(ratio, ratio);\n pad.clear();\n onChange(null);\n };\n pad.addEventListener(\"endStroke\", () =>\n onChange(pad.isEmpty() ? null : pad.toDataURL(\"image/png\")),\n );\n resize();\n window.addEventListener(\"resize\", resize);\n return () => {\n window.removeEventListener(\"resize\", resize);\n pad.off();\n padRef.current = null;\n };\n }, [mode, onChange]);\n\n useEffect(() => {\n if (mode !== \"type\") return;\n const name = typed.trim();\n if (!name) {\n onChange(null);\n return;\n }\n const c = document.createElement(\"canvas\");\n c.width = 720;\n c.height = 200;\n const ctx = c.getContext(\"2d\");\n if (!ctx) return;\n ctx.fillStyle = \"#111827\";\n ctx.font = \"72px 'Segoe Script', 'Brush Script MT', cursive\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(name, 24, 110);\n onChange(c.toDataURL(\"image/png\"));\n }, [mode, typed, onChange]);\n\n const tab = (m: \"draw\" | \"type\", label: string) => (\n <button\n type=\"button\"\n onClick={() => {\n setMode(m);\n onChange(null);\n }}\n style={{\n flex: 1,\n padding: \"8px 12px\",\n border: \"none\",\n borderBottom: mode === m ? `2px solid ${primaryColor}` : \"2px solid transparent\",\n background: \"transparent\",\n fontWeight: mode === m ? 600 : 400,\n color: mode === m ? primaryColor : \"#6b7280\",\n cursor: \"pointer\",\n }}\n >\n {label}\n </button>\n );\n\n return (\n <div style={{ border: \"1px solid #e5e7eb\", borderRadius: 12, overflow: \"hidden\" }}>\n <div style={{ display: \"flex\", borderBottom: \"1px solid #f3f4f6\" }}>\n {tab(\"draw\", \"Draw\")}\n {tab(\"type\", \"Type\")}\n </div>\n <div style={{ position: \"relative\", background: \"#fff\" }}>\n {mode === \"draw\" ? (\n <canvas\n ref={canvasRef}\n style={{ width: \"100%\", height: 180, touchAction: \"none\", display: \"block\" }}\n />\n ) : (\n <div style={{ padding: 16 }}>\n <input\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n placeholder=\"Type your name\"\n style={{\n width: \"100%\",\n fontSize: 40,\n fontFamily: \"'Segoe Script', 'Brush Script MT', cursive\",\n border: \"none\",\n borderBottom: \"1px solid #e5e7eb\",\n outline: \"none\",\n padding: \"8px 0\",\n }}\n />\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => {\n padRef.current?.clear();\n setTyped(\"\");\n onChange(null);\n }}\n style={{\n position: \"absolute\",\n top: 8,\n right: 8,\n fontSize: 12,\n color: \"#6b7280\",\n background: \"rgba(255,255,255,0.8)\",\n border: \"1px solid #e5e7eb\",\n borderRadius: 6,\n padding: \"2px 8px\",\n cursor: \"pointer\",\n }}\n >\n Clear\n </button>\n </div>\n </div>\n );\n}\n","export interface ConsentValue {\n signerName: string;\n consent: boolean;\n}\n\nexport interface ConsentBlockProps {\n value: ConsentValue;\n onChange: (value: ConsentValue) => void;\n primaryColor?: string;\n}\n\n/**\n * The electronic-signature intent record: a typed full legal name + an explicit\n * consent checkbox. Together with the audit trail this is what makes the simple\n * e-signature legally valid.\n */\nexport function ConsentBlock({ value, onChange, primaryColor = \"#4f46e5\" }: ConsentBlockProps) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\n <label style={{ display: \"flex\", flexDirection: \"column\", gap: 4 }}>\n <span style={{ fontSize: 13, fontWeight: 600, color: \"#374151\" }}>Full legal name</span>\n <input\n value={value.signerName}\n onChange={(e) => onChange({ ...value, signerName: e.target.value })}\n placeholder=\"e.g. Ada Lovelace\"\n style={{\n padding: \"10px 12px\",\n border: \"1px solid #d1d5db\",\n borderRadius: 8,\n fontSize: 15,\n outline: \"none\",\n }}\n />\n </label>\n <label style={{ display: \"flex\", gap: 10, alignItems: \"flex-start\", cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={value.consent}\n onChange={(e) => onChange({ ...value, consent: e.target.checked })}\n style={{ marginTop: 3, accentColor: primaryColor, width: 16, height: 16 }}\n />\n <span style={{ fontSize: 13, color: \"#4b5563\", lineHeight: 1.5 }}>\n I agree to sign this document electronically. I understand my electronic\n signature is legally binding and that an audit record (time, IP, and a\n tamper-evident hash) is kept with the signed document.\n </span>\n </label>\n </div>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\n\nexport interface ViewerPlacement {\n page: number;\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\nexport interface PdfViewerProps {\n /** Token-gated source PDF URL. */\n url: string;\n /** Optional signature-box hint, drawn on its page at normalized coords. */\n placement?: ViewerPlacement;\n /** Override the pdf.js worker (defaults to the unpkg .mjs for the bundled version). */\n workerSrc?: string;\n primaryColor?: string;\n}\n\n/**\n * Render the PDF to canvases (pdf.js, responsive, DPI-aware) with the signature\n * box overlaid where the manager placed it. Falls back to an <iframe> + download\n * link if pdf.js fails to load — so the document is always readable.\n */\nexport function PdfViewer({ url, placement, workerSrc, primaryColor = \"#4f46e5\" }: PdfViewerProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [failed, setFailed] = useState(false);\n\n useEffect(() => {\n let cancelled = false;\n void (async () => {\n try {\n const pdfjs = await import(\"pdfjs-dist\");\n pdfjs.GlobalWorkerOptions.workerSrc =\n workerSrc ?? `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;\n const doc = await pdfjs.getDocument({ url }).promise;\n const container = containerRef.current;\n if (!container || cancelled) return;\n container.innerHTML = \"\";\n const width = container.clientWidth || 640;\n const ratio = Math.max(window.devicePixelRatio || 1, 1);\n\n for (let p = 1; p <= doc.numPages; p++) {\n const page = await doc.getPage(p);\n if (cancelled) return;\n const base = page.getViewport({ scale: 1 });\n const viewport = page.getViewport({ scale: width / base.width });\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(viewport.width * ratio);\n canvas.height = Math.floor(viewport.height * ratio);\n canvas.style.width = \"100%\";\n canvas.style.display = \"block\";\n canvas.style.borderRadius = \"4px\";\n\n const wrap = document.createElement(\"div\");\n wrap.style.position = \"relative\";\n wrap.style.marginBottom = \"14px\";\n wrap.style.boxShadow = \"0 1px 4px rgba(0,0,0,0.12)\";\n wrap.appendChild(canvas);\n\n if (placement && placement.page === p) {\n const box = document.createElement(\"div\");\n box.style.cssText = `position:absolute;left:${placement.x * 100}%;top:${\n placement.y * 100\n }%;width:${placement.w * 100}%;height:${placement.h * 100}%;border:2px dashed ${primaryColor};background:${primaryColor}1a;border-radius:4px;pointer-events:none;`;\n const label = document.createElement(\"span\");\n label.textContent = \"Signature\";\n label.style.cssText = `position:absolute;top:-18px;left:0;font-size:11px;font-weight:600;color:${primaryColor};`;\n box.appendChild(label);\n wrap.appendChild(box);\n }\n\n container.appendChild(wrap);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) continue;\n ctx.scale(ratio, ratio);\n await page.render({ canvasContext: ctx, viewport, canvas }).promise;\n }\n } catch {\n if (!cancelled) setFailed(true);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [url, placement, workerSrc, primaryColor]);\n\n if (failed) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>\n <iframe\n src={`${url}#toolbar=0`}\n title=\"Document\"\n style={{ width: \"100%\", height: 520, border: \"1px solid #e5e7eb\", borderRadius: 8 }}\n />\n <a href={url} target=\"_blank\" rel=\"noreferrer\" style={{ fontSize: 13, color: primaryColor }}>\n Download to read\n </a>\n </div>\n );\n }\n\n return <div ref={containerRef} style={{ width: \"100%\" }} />;\n}\n","import { useState } from \"react\";\n\nimport { ConsentBlock, type ConsentValue } from \"./ConsentBlock\";\nimport { PdfViewer, type ViewerPlacement } from \"./PdfViewer\";\nimport { SignaturePad } from \"./SignaturePad\";\n\nexport interface SigningBranding {\n appName?: string;\n logoUrl?: string;\n primaryColor?: string;\n}\n\nexport interface SigningExperienceProps {\n /** Token-gated source PDF URL (GET /api/esign/source/<token>). */\n sourceUrl: string;\n /** Submit endpoint (POST /sign/<token>/submit). */\n submitUrl: string;\n documentTitle: string;\n placement?: ViewerPlacement;\n branding?: SigningBranding;\n /** Called after a successful signature submit (host navigates to /done). */\n onSigned?: () => void;\n workerSrc?: string;\n}\n\n/**\n * The full host-branded signer page body: read the PDF (placement highlighted),\n * draw/type a signature, consent, and submit. Self-contained styling so it sits\n * cleanly inside any host shell.\n */\nexport function SigningExperience(props: SigningExperienceProps) {\n const primary = props.branding?.primaryColor ?? \"#4f46e5\";\n const [signaturePng, setSignaturePng] = useState<string | null>(null);\n const [consent, setConsent] = useState<ConsentValue>({ signerName: \"\", consent: false });\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit =\n !!signaturePng && consent.consent && consent.signerName.trim().length > 0 && !submitting;\n\n async function submit() {\n if (!canSubmit) return;\n setSubmitting(true);\n setError(null);\n try {\n const res = await fetch(props.submitUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n signaturePng,\n signerName: consent.signerName.trim(),\n consent: true,\n }),\n });\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as {\n error?: { message?: string };\n } | null;\n throw new Error(body?.error?.message ?? \"Could not submit your signature.\");\n }\n props.onSigned?.();\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Something went wrong.\");\n } finally {\n setSubmitting(false);\n }\n }\n\n return (\n <div\n style={{\n maxWidth: 760,\n margin: \"0 auto\",\n padding: \"24px 16px 64px\",\n fontFamily:\n \"system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif\",\n color: \"#111827\",\n }}\n >\n <header style={{ display: \"flex\", alignItems: \"center\", gap: 12, marginBottom: 8 }}>\n {props.branding?.logoUrl ? (\n <img src={props.branding.logoUrl} alt=\"\" style={{ height: 32 }} />\n ) : null}\n {props.branding?.appName ? (\n <span style={{ fontWeight: 600, color: \"#374151\" }}>{props.branding.appName}</span>\n ) : null}\n </header>\n\n <h1 style={{ fontSize: 22, margin: \"8px 0 4px\" }}>{props.documentTitle}</h1>\n <p style={{ color: \"#6b7280\", fontSize: 14, marginTop: 0 }}>\n Review the document below, then sign — no account or login needed.\n </p>\n\n <section style={{ margin: \"16px 0\" }}>\n <PdfViewer\n url={props.sourceUrl}\n placement={props.placement}\n workerSrc={props.workerSrc}\n primaryColor={primary}\n />\n </section>\n\n <section style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>\n <div>\n <label style={{ fontSize: 13, fontWeight: 600, color: \"#374151\" }}>Your signature</label>\n <div style={{ marginTop: 6 }}>\n <SignaturePad primaryColor={primary} onChange={setSignaturePng} />\n </div>\n </div>\n\n <ConsentBlock value={consent} onChange={setConsent} primaryColor={primary} />\n\n {error ? (\n <div\n style={{\n background: \"#fef2f2\",\n border: \"1px solid #fecaca\",\n color: \"#b91c1c\",\n borderRadius: 8,\n padding: \"10px 12px\",\n fontSize: 14,\n }}\n >\n {error}\n </div>\n ) : null}\n\n <button\n type=\"button\"\n onClick={submit}\n disabled={!canSubmit}\n style={{\n padding: \"14px 16px\",\n borderRadius: 10,\n border: \"none\",\n background: canSubmit ? primary : \"#c7c9d1\",\n color: \"#fff\",\n fontSize: 16,\n fontWeight: 600,\n cursor: canSubmit ? \"pointer\" : \"not-allowed\",\n }}\n >\n {submitting ? \"Signing…\" : \"Sign document\"}\n </button>\n </section>\n </div>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephomills/esign",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Reusable, host-agnostic document e-signing: no-login signer links, drag-placed signatures, Level-1 cryptographically-sealed PDFs with a hash-chained audit trail, document versioning, group targeting via a SubjectsPort, coverage detection, and stats. Host injects storage, persistence, notifications, auth, and subject listing via configureEsign().",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mills",
@@ -38,6 +38,11 @@
38
38
  "import": "./dist/prisma/index.js",
39
39
  "require": "./dist/prisma/index.cjs"
40
40
  },
41
+ "./ui": {
42
+ "types": "./dist/ui/index.d.ts",
43
+ "import": "./dist/ui/index.js",
44
+ "require": "./dist/ui/index.cjs"
45
+ },
41
46
  "./prisma-fragment": "./prisma/esign-models.prisma"
42
47
  },
43
48
  "scripts": {
@@ -66,13 +71,19 @@
66
71
  },
67
72
  "dependencies": {
68
73
  "pdf-lib": "^1.17.1",
74
+ "pdfjs-dist": "^6.0.227",
75
+ "signature_pad": "^5.1.3",
69
76
  "zod": "^3.23.0"
70
77
  },
71
78
  "devDependencies": {
72
79
  "@types/node": "^22.10.10",
80
+ "@types/react": "^19.2.17",
81
+ "@types/react-dom": "^19.2.3",
73
82
  "@typescript-eslint/eslint-plugin": "^8.60.0",
74
83
  "@typescript-eslint/parser": "^8.60.0",
75
84
  "eslint": "^9.39.4",
85
+ "react": "^19.2.7",
86
+ "react-dom": "^19.2.7",
76
87
  "tsup": "^8.5.1",
77
88
  "typescript": "^5.7.3",
78
89
  "vitest": "^3.0.5"