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