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