@josephomills/esign 0.4.0 → 0.5.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.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,14 @@ 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 [drag, setDrag] = useState(null);
254
+ const [live, setLive] = useState(null);
247
255
  const stageRef = useRef(null);
248
256
  const canvasRef = useRef(null);
249
257
  const docRef = useRef(null);
250
- const [drag, setDrag] = useState(null);
251
- const startRef = useRef(null);
252
258
  useEffect(() => {
253
259
  let cancelled = false;
254
260
  void (async () => {
@@ -276,7 +282,7 @@ function FieldDesigner({
276
282
  if (!doc || !canvas) return;
277
283
  const pageObj = await doc.getPage(page);
278
284
  if (cancelled) return;
279
- const width = stageRef.current?.clientWidth || 640;
285
+ const width = stageRef.current?.clientWidth || 480;
280
286
  const base = pageObj.getViewport({ scale: 1 });
281
287
  const viewport = pageObj.getViewport({ scale: width / base.width });
282
288
  const ratio = Math.max(window.devicePixelRatio || 1, 1);
@@ -294,79 +300,307 @@ function FieldDesigner({
294
300
  };
295
301
  }, [page, numPages]);
296
302
  function rel(e) {
297
- const rect = stageRef.current.getBoundingClientRect();
303
+ const r = stageRef.current.getBoundingClientRect();
298
304
  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)
305
+ x: clamp((e.clientX - r.left) / r.width, 0, 1),
306
+ y: clamp((e.clientY - r.top) / r.height, 0, 1)
301
307
  };
302
308
  }
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
+ function onPointerMove(e) {
310
+ if (!drag) return;
309
311
  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
+ let b;
313
+ if (drag.mode === "new") {
314
+ b = {
315
+ x: Math.min(drag.start.x, p.x),
316
+ y: Math.min(drag.start.y, p.y),
317
+ w: Math.abs(p.x - drag.start.x),
318
+ h: Math.abs(p.y - drag.start.y)
319
+ };
320
+ } else if (drag.mode === "move") {
321
+ const dx = p.x - drag.start.x;
322
+ const dy = p.y - drag.start.y;
323
+ b = {
324
+ x: clamp(drag.orig.x + dx, 0, 1 - drag.orig.w),
325
+ y: clamp(drag.orig.y + dy, 0, 1 - drag.orig.h),
326
+ w: drag.orig.w,
327
+ h: drag.orig.h
328
+ };
329
+ } else {
330
+ let x1 = drag.orig.x;
331
+ let y1 = drag.orig.y;
332
+ let x2 = drag.orig.x + drag.orig.w;
333
+ let y2 = drag.orig.y + drag.orig.h;
334
+ if (drag.handle[0] === "n") y1 = p.y;
335
+ else y2 = p.y;
336
+ if (drag.handle[1] === "w") x1 = p.x;
337
+ else x2 = p.x;
338
+ b = { x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x2 - x1), h: Math.abs(y2 - y1) };
339
+ }
340
+ setLive(b);
312
341
  }
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 });
342
+ function onPointerUp() {
343
+ if (drag && live && live.w >= MIN_W && live.h >= MIN_H) {
344
+ if (drag.mode === "new") {
345
+ const f = {
346
+ id: newId(),
347
+ label: value.length === 0 ? "Signature" : `Signature ${value.length + 1}`,
348
+ page,
349
+ ...live
350
+ };
351
+ onChange([...value, f]);
352
+ setSelectedId(f.id);
353
+ } else {
354
+ onChange(value.map((x) => x.id === drag.id ? { ...x, ...live } : x));
355
+ }
316
356
  }
317
- startRef.current = null;
318
357
  setDrag(null);
358
+ setLive(null);
319
359
  }
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." });
360
+ function removeField(id) {
361
+ onChange(value.filter((f) => f.id !== id));
362
+ if (selectedId === id) setSelectedId(null);
323
363
  }
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(
364
+ function rename(id, label) {
365
+ onChange(value.map((f) => f.id === id ? { ...f, label } : f));
366
+ }
367
+ const handle = (id, geom, c) => {
368
+ const cx = c[1] === "w" ? geom.x : geom.x + geom.w;
369
+ const cy = c[0] === "n" ? geom.y : geom.y + geom.h;
370
+ const cursor = c === "nw" || c === "se" ? "nwse-resize" : "nesw-resize";
371
+ return /* @__PURE__ */ jsx(
334
372
  "div",
335
373
  {
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"
374
+ onPointerDown: (e) => {
375
+ e.stopPropagation();
376
+ setDrag({ mode: "resize", id, handle: c, orig: geom });
348
377
  },
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"
378
+ style: {
379
+ position: "absolute",
380
+ left: `${cx * 100}%`,
381
+ top: `${cy * 100}%`,
382
+ width: 13,
383
+ height: 13,
384
+ marginLeft: -7,
385
+ marginTop: -7,
386
+ background: "#fff",
387
+ border: `2px solid ${primaryColor}`,
388
+ borderRadius: 2,
389
+ cursor,
390
+ touchAction: "none"
391
+ }
392
+ },
393
+ c
394
+ );
395
+ };
396
+ if (failed) {
397
+ 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." });
398
+ }
399
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 16, alignItems: "flex-start" }, children: [
400
+ /* @__PURE__ */ jsxs("div", { style: { flex: "1 1 340px", minWidth: 260 }, children: [
401
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }, children: [
402
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 13, color: "#6b7280" }, children: "Drag a box where a signature goes." }),
403
+ numPages > 1 ? /* @__PURE__ */ jsxs("label", { style: { marginLeft: "auto", fontSize: 13, color: "#374151" }, children: [
404
+ "Page",
405
+ " ",
406
+ /* @__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)) })
407
+ ] }) : null
408
+ ] }),
409
+ /* @__PURE__ */ jsxs(
410
+ "div",
411
+ {
412
+ ref: stageRef,
413
+ onPointerDown: (e) => setDrag({ mode: "new", start: rel(e) }),
414
+ onPointerMove,
415
+ onPointerUp,
416
+ onPointerLeave: onPointerUp,
417
+ style: {
418
+ position: "relative",
419
+ cursor: "crosshair",
420
+ userSelect: "none",
421
+ touchAction: "none",
422
+ border: "1px solid #e5e7eb",
423
+ borderRadius: 6,
424
+ overflow: "hidden"
425
+ },
426
+ children: [
427
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef }),
428
+ value.filter((f) => f.page === page).map((f) => {
429
+ const dragging = drag && drag.mode !== "new" && drag.id === f.id;
430
+ const geom = dragging && live ? live : f;
431
+ const isSel = f.id === selectedId;
432
+ return /* @__PURE__ */ jsxs("div", { children: [
433
+ /* @__PURE__ */ jsx(
434
+ "div",
435
+ {
436
+ onPointerDown: (e) => {
437
+ e.stopPropagation();
438
+ setSelectedId(f.id);
439
+ setDrag({ mode: "move", id: f.id, start: rel(e), orig: geom });
440
+ },
441
+ style: {
442
+ position: "absolute",
443
+ left: `${geom.x * 100}%`,
444
+ top: `${geom.y * 100}%`,
445
+ width: `${geom.w * 100}%`,
446
+ height: `${geom.h * 100}%`,
447
+ border: `2px ${isSel ? "solid" : "dashed"} ${primaryColor}`,
448
+ background: `${primaryColor}${isSel ? "22" : "14"}`,
449
+ borderRadius: 3,
450
+ cursor: "move",
451
+ display: "flex",
452
+ alignItems: "center",
453
+ justifyContent: "center",
454
+ overflow: "hidden",
455
+ touchAction: "none"
456
+ },
457
+ children: /* @__PURE__ */ jsxs(
458
+ "span",
459
+ {
460
+ style: {
461
+ fontSize: 12,
462
+ fontWeight: 600,
463
+ color: primaryColor,
464
+ opacity: 0.85,
465
+ whiteSpace: "nowrap",
466
+ padding: "0 4px"
467
+ },
468
+ children: [
469
+ "\u270D ",
470
+ f.label || "Signature"
471
+ ]
472
+ }
473
+ )
474
+ }
475
+ ),
476
+ isSel ? /* @__PURE__ */ jsxs(Fragment, { children: [
477
+ ["nw", "ne", "sw", "se"].map((c) => handle(f.id, geom, c)),
478
+ /* @__PURE__ */ jsx(
479
+ "button",
480
+ {
481
+ type: "button",
482
+ "aria-label": `Remove ${f.label}`,
483
+ onPointerDown: (e) => e.stopPropagation(),
484
+ onClick: (e) => {
485
+ e.stopPropagation();
486
+ removeField(f.id);
487
+ },
488
+ style: {
489
+ position: "absolute",
490
+ left: `${(geom.x + geom.w) * 100}%`,
491
+ top: `${geom.y * 100}%`,
492
+ transform: "translate(-50%, -50%)",
493
+ marginTop: -18,
494
+ width: 22,
495
+ height: 22,
496
+ borderRadius: "50%",
497
+ background: primaryColor,
498
+ color: "#fff",
499
+ border: "2px solid #fff",
500
+ boxShadow: "0 1px 3px rgba(0,0,0,0.3)",
501
+ fontSize: 14,
502
+ lineHeight: "16px",
503
+ cursor: "pointer",
504
+ padding: 0,
505
+ touchAction: "none"
506
+ },
507
+ children: "\xD7"
508
+ }
509
+ )
510
+ ] }) : null
511
+ ] }, f.id);
512
+ }),
513
+ drag?.mode === "new" && live ? /* @__PURE__ */ jsx(
514
+ "div",
515
+ {
516
+ style: {
517
+ position: "absolute",
518
+ left: `${live.x * 100}%`,
519
+ top: `${live.y * 100}%`,
520
+ width: `${live.w * 100}%`,
521
+ height: `${live.h * 100}%`,
522
+ border: `2px dashed ${primaryColor}`,
523
+ background: `${primaryColor}22`,
524
+ borderRadius: 3,
525
+ pointerEvents: "none"
526
+ }
364
527
  }
365
- }
366
- ) : null
367
- ]
368
- }
369
- )
528
+ ) : null
529
+ ]
530
+ }
531
+ )
532
+ ] }),
533
+ /* @__PURE__ */ jsxs("div", { style: { flex: "1 1 200px", minWidth: 180, maxWidth: 300 }, children: [
534
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, fontWeight: 600, color: "#374151", margin: "0 0 8px" }, children: [
535
+ "Signature fields (",
536
+ value.length,
537
+ ")"
538
+ ] }),
539
+ 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(
540
+ "div",
541
+ {
542
+ onClick: () => {
543
+ setSelectedId(f.id);
544
+ setPage(f.page);
545
+ },
546
+ style: {
547
+ display: "flex",
548
+ alignItems: "center",
549
+ gap: 6,
550
+ padding: "6px 8px",
551
+ borderRadius: 8,
552
+ border: `1px solid ${f.id === selectedId ? primaryColor : "#e5e7eb"}`,
553
+ background: f.id === selectedId ? `${primaryColor}0f` : "#fff",
554
+ cursor: "pointer"
555
+ },
556
+ children: [
557
+ /* @__PURE__ */ jsx(
558
+ "input",
559
+ {
560
+ value: f.label,
561
+ onClick: (e) => e.stopPropagation(),
562
+ onChange: (e) => rename(f.id, e.target.value),
563
+ placeholder: "Field name",
564
+ style: {
565
+ flex: 1,
566
+ minWidth: 0,
567
+ border: "none",
568
+ outline: "none",
569
+ background: "transparent",
570
+ fontSize: 13
571
+ }
572
+ }
573
+ ),
574
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, color: "#9ca3af" }, children: [
575
+ "p",
576
+ f.page
577
+ ] }),
578
+ /* @__PURE__ */ jsx(
579
+ "button",
580
+ {
581
+ type: "button",
582
+ "aria-label": `Remove ${f.label}`,
583
+ onClick: (e) => {
584
+ e.stopPropagation();
585
+ removeField(f.id);
586
+ },
587
+ style: {
588
+ border: "none",
589
+ background: "transparent",
590
+ color: "#9ca3af",
591
+ cursor: "pointer",
592
+ fontSize: 16,
593
+ lineHeight: 1,
594
+ padding: 0
595
+ },
596
+ children: "\xD7"
597
+ }
598
+ )
599
+ ]
600
+ },
601
+ f.id
602
+ )) })
603
+ ] })
370
604
  ] });
371
605
  }
372
606
  function SigningExperience(props) {
@@ -422,7 +656,7 @@ function SigningExperience(props) {
422
656
  PdfViewer,
423
657
  {
424
658
  url: props.sourceUrl,
425
- placement: props.placement,
659
+ placements: props.placements,
426
660
  workerSrc: props.workerSrc,
427
661
  primaryColor: primary
428
662
  }