@josephomills/esign 0.4.0 → 0.5.1

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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import SignaturePadLib from 'signature_pad';
3
3
  import { useState, useRef, useEffect } from 'react';
4
- import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/ui/SignaturePad.tsx
7
7
  function SignaturePad({ primaryColor = "#4f46e5", onChange }) {
@@ -165,7 +165,7 @@ function ConsentBlock({ value, onChange, primaryColor = "#4f46e5" }) {
165
165
  ] })
166
166
  ] });
167
167
  }
168
- function PdfViewer({ url, placement, workerSrc, primaryColor = "#4f46e5" }) {
168
+ function PdfViewer({ url, placements, workerSrc, primaryColor = "#4f46e5" }) {
169
169
  const containerRef = useRef(null);
170
170
  const [failed, setFailed] = useState(false);
171
171
  useEffect(() => {
@@ -196,12 +196,13 @@ function PdfViewer({ url, placement, workerSrc, primaryColor = "#4f46e5" }) {
196
196
  wrap.style.marginBottom = "14px";
197
197
  wrap.style.boxShadow = "0 1px 4px rgba(0,0,0,0.12)";
198
198
  wrap.appendChild(canvas);
199
- if (placement && placement.page === p) {
199
+ for (const pl of placements ?? []) {
200
+ if (pl.page !== p) continue;
200
201
  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
+ box.style.cssText = `position:absolute;left:${pl.x * 100}%;top:${pl.y * 100}%;width:${pl.w * 100}%;height:${pl.h * 100}%;border:2px dashed ${primaryColor};background:${primaryColor}1a;border-radius:4px;pointer-events:none;display:flex;align-items:center;justify-content:center;overflow:hidden;`;
202
203
  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};`;
204
+ label.textContent = "\u270D Sign here";
205
+ label.style.cssText = `font-size:11px;font-weight:600;color:${primaryColor};opacity:0.85;white-space:nowrap;`;
205
206
  box.appendChild(label);
206
207
  wrap.appendChild(box);
207
208
  }
@@ -218,7 +219,7 @@ function PdfViewer({ url, placement, workerSrc, primaryColor = "#4f46e5" }) {
218
219
  return () => {
219
220
  cancelled = true;
220
221
  };
221
- }, [url, placement, workerSrc, primaryColor]);
222
+ }, [url, placements, workerSrc, primaryColor]);
222
223
  if (failed) {
223
224
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
224
225
  /* @__PURE__ */ jsx(
@@ -234,6 +235,10 @@ function PdfViewer({ url, placement, workerSrc, primaryColor = "#4f46e5" }) {
234
235
  }
235
236
  return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%" } });
236
237
  }
238
+ var clamp = (v, min, max) => Math.min(Math.max(v, min), max);
239
+ var MIN_W = 0.04;
240
+ var MIN_H = 0.02;
241
+ var newId = () => typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
237
242
  function FieldDesigner({
238
243
  pdfData,
239
244
  value,
@@ -242,13 +247,15 @@ function FieldDesigner({
242
247
  primaryColor = "#4f46e5"
243
248
  }) {
244
249
  const [numPages, setNumPages] = useState(0);
245
- const [page, setPage] = useState(value?.page ?? 1);
250
+ const [page, setPage] = useState(value[0]?.page ?? 1);
246
251
  const [failed, setFailed] = useState(false);
252
+ const [selectedId, setSelectedId] = useState(value[0]?.id ?? null);
253
+ const [drawMode, setDrawMode] = useState(false);
254
+ const [drag, setDrag] = useState(null);
255
+ const [live, setLive] = useState(null);
247
256
  const stageRef = useRef(null);
248
257
  const canvasRef = useRef(null);
249
258
  const docRef = useRef(null);
250
- const [drag, setDrag] = useState(null);
251
- const startRef = useRef(null);
252
259
  useEffect(() => {
253
260
  let cancelled = false;
254
261
  void (async () => {
@@ -276,7 +283,7 @@ function FieldDesigner({
276
283
  if (!doc || !canvas) return;
277
284
  const pageObj = await doc.getPage(page);
278
285
  if (cancelled) return;
279
- const width = stageRef.current?.clientWidth || 640;
286
+ const width = stageRef.current?.clientWidth || 480;
280
287
  const base = pageObj.getViewport({ scale: 1 });
281
288
  const viewport = pageObj.getViewport({ scale: width / base.width });
282
289
  const ratio = Math.max(window.devicePixelRatio || 1, 1);
@@ -294,79 +301,340 @@ function FieldDesigner({
294
301
  };
295
302
  }, [page, numPages]);
296
303
  function rel(e) {
297
- const rect = stageRef.current.getBoundingClientRect();
304
+ const r = stageRef.current.getBoundingClientRect();
298
305
  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)
306
+ x: clamp((e.clientX - r.left) / r.width, 0, 1),
307
+ y: clamp((e.clientY - r.top) / r.height, 0, 1)
301
308
  };
302
309
  }
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;
310
+ function onPointerMove(e) {
311
+ if (!drag) return;
309
312
  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) });
313
+ let b;
314
+ if (drag.mode === "new") {
315
+ b = {
316
+ x: Math.min(drag.start.x, p.x),
317
+ y: Math.min(drag.start.y, p.y),
318
+ w: Math.abs(p.x - drag.start.x),
319
+ h: Math.abs(p.y - drag.start.y)
320
+ };
321
+ } else if (drag.mode === "move") {
322
+ const dx = p.x - drag.start.x;
323
+ const dy = p.y - drag.start.y;
324
+ b = {
325
+ x: clamp(drag.orig.x + dx, 0, 1 - drag.orig.w),
326
+ y: clamp(drag.orig.y + dy, 0, 1 - drag.orig.h),
327
+ w: drag.orig.w,
328
+ h: drag.orig.h
329
+ };
330
+ } else {
331
+ let x1 = drag.orig.x;
332
+ let y1 = drag.orig.y;
333
+ let x2 = drag.orig.x + drag.orig.w;
334
+ let y2 = drag.orig.y + drag.orig.h;
335
+ if (drag.handle[0] === "n") y1 = p.y;
336
+ else y2 = p.y;
337
+ if (drag.handle[1] === "w") x1 = p.x;
338
+ else x2 = p.x;
339
+ b = { x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x2 - x1), h: Math.abs(y2 - y1) };
340
+ }
341
+ setLive(b);
312
342
  }
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 });
343
+ function onPointerUp() {
344
+ if (drag && live && live.w >= MIN_W && live.h >= MIN_H) {
345
+ if (drag.mode === "new") {
346
+ const f = {
347
+ id: newId(),
348
+ label: value.length === 0 ? "Signature" : `Signature ${value.length + 1}`,
349
+ page,
350
+ ...live
351
+ };
352
+ onChange([...value, f]);
353
+ setSelectedId(f.id);
354
+ setDrawMode(false);
355
+ } else {
356
+ onChange(value.map((x) => x.id === drag.id ? { ...x, ...live } : x));
357
+ }
316
358
  }
317
- startRef.current = null;
318
359
  setDrag(null);
360
+ setLive(null);
319
361
  }
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." });
362
+ function removeField(id) {
363
+ onChange(value.filter((f) => f.id !== id));
364
+ if (selectedId === id) setSelectedId(null);
323
365
  }
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(
366
+ function rename(id, label) {
367
+ onChange(value.map((f) => f.id === id ? { ...f, label } : f));
368
+ }
369
+ const handle = (id, geom, c) => {
370
+ const cx = c[1] === "w" ? geom.x : geom.x + geom.w;
371
+ const cy = c[0] === "n" ? geom.y : geom.y + geom.h;
372
+ const cursor = c === "nw" || c === "se" ? "nwse-resize" : "nesw-resize";
373
+ return /* @__PURE__ */ jsx(
334
374
  "div",
335
375
  {
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"
376
+ onPointerDown: (e) => {
377
+ e.stopPropagation();
378
+ setDrag({ mode: "resize", id, handle: c, orig: geom });
348
379
  },
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"
380
+ style: {
381
+ position: "absolute",
382
+ left: `${cx * 100}%`,
383
+ top: `${cy * 100}%`,
384
+ width: 13,
385
+ height: 13,
386
+ marginLeft: -7,
387
+ marginTop: -7,
388
+ background: "#fff",
389
+ border: `2px solid ${primaryColor}`,
390
+ borderRadius: 2,
391
+ cursor,
392
+ touchAction: "none"
393
+ }
394
+ },
395
+ c
396
+ );
397
+ };
398
+ if (failed) {
399
+ 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." });
400
+ }
401
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 16, alignItems: "flex-start" }, children: [
402
+ /* @__PURE__ */ jsxs("div", { style: { flex: "1 1 340px", minWidth: 260 }, children: [
403
+ /* @__PURE__ */ jsxs(
404
+ "div",
405
+ {
406
+ style: {
407
+ display: "flex",
408
+ alignItems: "center",
409
+ gap: 8,
410
+ marginBottom: 8,
411
+ flexWrap: "wrap"
412
+ },
413
+ children: [
414
+ /* @__PURE__ */ jsx(
415
+ "button",
416
+ {
417
+ type: "button",
418
+ onClick: () => setDrawMode((d) => !d),
419
+ style: {
420
+ fontSize: 13,
421
+ fontWeight: 600,
422
+ padding: "5px 12px",
423
+ borderRadius: 6,
424
+ border: `1px solid ${primaryColor}`,
425
+ background: drawMode ? primaryColor : "#fff",
426
+ color: drawMode ? "#fff" : primaryColor,
427
+ cursor: "pointer"
428
+ },
429
+ children: drawMode ? "Now drag on the page\u2026" : "+ Add field"
364
430
  }
365
- }
366
- ) : null
367
- ]
368
- }
369
- )
431
+ ),
432
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#6b7280" }, children: "or drag on the page (desktop)." }),
433
+ numPages > 1 ? /* @__PURE__ */ jsxs("label", { style: { marginLeft: "auto", fontSize: 13, color: "#374151" }, children: [
434
+ "Page",
435
+ " ",
436
+ /* @__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)) })
437
+ ] }) : null
438
+ ]
439
+ }
440
+ ),
441
+ /* @__PURE__ */ jsxs(
442
+ "div",
443
+ {
444
+ ref: stageRef,
445
+ onPointerDown: (e) => {
446
+ if (drawMode || e.pointerType === "mouse") setDrag({ mode: "new", start: rel(e) });
447
+ },
448
+ onPointerMove,
449
+ onPointerUp,
450
+ onPointerLeave: onPointerUp,
451
+ style: {
452
+ position: "relative",
453
+ cursor: drawMode ? "crosshair" : "default",
454
+ userSelect: "none",
455
+ touchAction: drawMode ? "none" : "auto",
456
+ border: "1px solid #e5e7eb",
457
+ borderRadius: 6,
458
+ overflow: "hidden"
459
+ },
460
+ children: [
461
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef }),
462
+ value.filter((f) => f.page === page).map((f) => {
463
+ const dragging = drag && drag.mode !== "new" && drag.id === f.id;
464
+ const geom = dragging && live ? live : f;
465
+ const isSel = f.id === selectedId;
466
+ return /* @__PURE__ */ jsxs("div", { children: [
467
+ /* @__PURE__ */ jsx(
468
+ "div",
469
+ {
470
+ onPointerDown: (e) => {
471
+ e.stopPropagation();
472
+ setSelectedId(f.id);
473
+ setDrag({ mode: "move", id: f.id, start: rel(e), orig: geom });
474
+ },
475
+ style: {
476
+ position: "absolute",
477
+ left: `${geom.x * 100}%`,
478
+ top: `${geom.y * 100}%`,
479
+ width: `${geom.w * 100}%`,
480
+ height: `${geom.h * 100}%`,
481
+ border: `2px ${isSel ? "solid" : "dashed"} ${primaryColor}`,
482
+ background: `${primaryColor}${isSel ? "22" : "14"}`,
483
+ borderRadius: 3,
484
+ cursor: "move",
485
+ display: "flex",
486
+ alignItems: "center",
487
+ justifyContent: "center",
488
+ overflow: "hidden",
489
+ touchAction: "none"
490
+ },
491
+ children: /* @__PURE__ */ jsxs(
492
+ "span",
493
+ {
494
+ style: {
495
+ fontSize: 12,
496
+ fontWeight: 600,
497
+ color: primaryColor,
498
+ opacity: 0.85,
499
+ whiteSpace: "nowrap",
500
+ padding: "0 4px"
501
+ },
502
+ children: [
503
+ "\u270D ",
504
+ f.label || "Signature"
505
+ ]
506
+ }
507
+ )
508
+ }
509
+ ),
510
+ isSel ? /* @__PURE__ */ jsxs(Fragment, { children: [
511
+ ["nw", "ne", "sw", "se"].map((c) => handle(f.id, geom, c)),
512
+ /* @__PURE__ */ jsx(
513
+ "button",
514
+ {
515
+ type: "button",
516
+ "aria-label": `Remove ${f.label}`,
517
+ onPointerDown: (e) => e.stopPropagation(),
518
+ onClick: (e) => {
519
+ e.stopPropagation();
520
+ removeField(f.id);
521
+ },
522
+ style: {
523
+ position: "absolute",
524
+ left: `${(geom.x + geom.w) * 100}%`,
525
+ top: `${geom.y * 100}%`,
526
+ transform: "translate(-50%, -50%)",
527
+ marginTop: -18,
528
+ width: 22,
529
+ height: 22,
530
+ borderRadius: "50%",
531
+ background: primaryColor,
532
+ color: "#fff",
533
+ border: "2px solid #fff",
534
+ boxShadow: "0 1px 3px rgba(0,0,0,0.3)",
535
+ fontSize: 14,
536
+ lineHeight: "16px",
537
+ cursor: "pointer",
538
+ padding: 0,
539
+ touchAction: "none"
540
+ },
541
+ children: "\xD7"
542
+ }
543
+ )
544
+ ] }) : null
545
+ ] }, f.id);
546
+ }),
547
+ drag?.mode === "new" && live ? /* @__PURE__ */ jsx(
548
+ "div",
549
+ {
550
+ style: {
551
+ position: "absolute",
552
+ left: `${live.x * 100}%`,
553
+ top: `${live.y * 100}%`,
554
+ width: `${live.w * 100}%`,
555
+ height: `${live.h * 100}%`,
556
+ border: `2px dashed ${primaryColor}`,
557
+ background: `${primaryColor}22`,
558
+ borderRadius: 3,
559
+ pointerEvents: "none"
560
+ }
561
+ }
562
+ ) : null
563
+ ]
564
+ }
565
+ )
566
+ ] }),
567
+ /* @__PURE__ */ jsxs("div", { style: { flex: "1 1 200px", minWidth: 180, maxWidth: 300 }, children: [
568
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, fontWeight: 600, color: "#374151", margin: "0 0 8px" }, children: [
569
+ "Signature fields (",
570
+ value.length,
571
+ ")"
572
+ ] }),
573
+ value.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: "#6b7280" }, children: "Draw a box on the page to add a field. Add as many as you need." }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: value.map((f) => /* @__PURE__ */ jsxs(
574
+ "div",
575
+ {
576
+ onClick: () => {
577
+ setSelectedId(f.id);
578
+ setPage(f.page);
579
+ },
580
+ style: {
581
+ display: "flex",
582
+ alignItems: "center",
583
+ gap: 6,
584
+ padding: "6px 8px",
585
+ borderRadius: 8,
586
+ border: `1px solid ${f.id === selectedId ? primaryColor : "#e5e7eb"}`,
587
+ background: f.id === selectedId ? `${primaryColor}0f` : "#fff",
588
+ cursor: "pointer"
589
+ },
590
+ children: [
591
+ /* @__PURE__ */ jsx(
592
+ "input",
593
+ {
594
+ value: f.label,
595
+ onClick: (e) => e.stopPropagation(),
596
+ onChange: (e) => rename(f.id, e.target.value),
597
+ placeholder: "Field name",
598
+ style: {
599
+ flex: 1,
600
+ minWidth: 0,
601
+ border: "none",
602
+ outline: "none",
603
+ background: "transparent",
604
+ fontSize: 13
605
+ }
606
+ }
607
+ ),
608
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, color: "#9ca3af" }, children: [
609
+ "p",
610
+ f.page
611
+ ] }),
612
+ /* @__PURE__ */ jsx(
613
+ "button",
614
+ {
615
+ type: "button",
616
+ "aria-label": `Remove ${f.label}`,
617
+ onClick: (e) => {
618
+ e.stopPropagation();
619
+ removeField(f.id);
620
+ },
621
+ style: {
622
+ border: "none",
623
+ background: "transparent",
624
+ color: "#9ca3af",
625
+ cursor: "pointer",
626
+ fontSize: 16,
627
+ lineHeight: 1,
628
+ padding: 0
629
+ },
630
+ children: "\xD7"
631
+ }
632
+ )
633
+ ]
634
+ },
635
+ f.id
636
+ )) })
637
+ ] })
370
638
  ] });
371
639
  }
372
640
  function SigningExperience(props) {
@@ -422,7 +690,7 @@ function SigningExperience(props) {
422
690
  PdfViewer,
423
691
  {
424
692
  url: props.sourceUrl,
425
- placement: props.placement,
693
+ placements: props.placements,
426
694
  workerSrc: props.workerSrc,
427
695
  primaryColor: primary
428
696
  }