@unlev/exeq 0.2.1 → 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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/pdf-builder/DesignerView.tsx
2
- import { useState as useState3, useCallback as useCallback3, useEffect as useEffect2, useRef as useRef3 } from "react";
2
+ import { useState as useState5, useCallback as useCallback4, useEffect as useEffect2, useRef as useRef4 } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
 
5
5
  // src/types/pdf-builder.ts
@@ -139,45 +139,95 @@ async function renderPdfPages(source, scale = 2) {
139
139
  canvas.height = viewport.height;
140
140
  const ctx = canvas.getContext("2d");
141
141
  await page.render({ canvasContext: ctx, viewport }).promise;
142
+ const unscaledViewport = page.getViewport({ scale: 1 });
142
143
  pages.push({
143
144
  pageNumber: i,
144
145
  dataUrl: canvas.toDataURL("image/png"),
145
146
  width: viewport.width,
146
- height: viewport.height
147
+ height: viewport.height,
148
+ pdfWidth: unscaledViewport.width,
149
+ pdfHeight: unscaledViewport.height
147
150
  });
148
151
  }
149
152
  return pages;
150
153
  }
151
154
 
152
155
  // src/components/pdf-builder/PdfViewer.tsx
153
- import { useRef, useCallback } from "react";
156
+ import { useRef, useCallback, useState } from "react";
154
157
  import { jsx, jsxs } from "react/jsx-runtime";
155
158
  function PdfViewer({
156
159
  pages,
157
160
  fields,
158
- selectedFieldId,
161
+ selectedFieldIds,
159
162
  onSelectField,
160
163
  onFieldMove,
161
164
  onFieldResize,
162
165
  onPageClick,
163
166
  onDropField,
167
+ onGroupMove,
168
+ onMoveEnd,
164
169
  mode,
165
170
  currentSigner,
166
- renderFieldContent
171
+ renderFieldContent,
172
+ zoom = 1,
173
+ onMarqueeSelect
167
174
  }) {
168
175
  const containerRef = useRef(null);
169
- const handlePageClick = useCallback((e, pageIndex) => {
176
+ const [guides, setGuides] = useState([]);
177
+ const [marquee, setMarquee] = useState(null);
178
+ const marqueeRef = useRef(null);
179
+ const handlePageMouseDown = useCallback((e, pageIndex) => {
170
180
  const target = e.target;
171
181
  if (target.closest(".field-overlay")) return;
172
- if (onPageClick) {
173
- const rect = e.currentTarget.getBoundingClientRect();
174
- const x = (e.clientX - rect.left) / rect.width * 100;
175
- const y = (e.clientY - rect.top) / rect.height * 100;
176
- onPageClick(pageIndex, x, y);
177
- } else {
178
- onSelectField(null);
179
- }
180
- }, [onPageClick, onSelectField]);
182
+ if (mode !== "designer") return;
183
+ e.preventDefault();
184
+ const rect = e.currentTarget.getBoundingClientRect();
185
+ const x = (e.clientX - rect.left) / rect.width * 100;
186
+ const y = (e.clientY - rect.top) / rect.height * 100;
187
+ marqueeRef.current = { startX: x, startY: y, page: pageIndex, didDrag: false };
188
+ const handleMouseMove = (ev) => {
189
+ if (!marqueeRef.current) return;
190
+ const mx = (ev.clientX - rect.left) / rect.width * 100;
191
+ const my = (ev.clientY - rect.top) / rect.height * 100;
192
+ const dx = Math.abs(mx - marqueeRef.current.startX);
193
+ const dy = Math.abs(my - marqueeRef.current.startY);
194
+ if (dx > 1 || dy > 1) {
195
+ marqueeRef.current.didDrag = true;
196
+ setMarquee({
197
+ page: pageIndex,
198
+ x1: Math.min(marqueeRef.current.startX, mx),
199
+ y1: Math.min(marqueeRef.current.startY, my),
200
+ x2: Math.max(marqueeRef.current.startX, mx),
201
+ y2: Math.max(marqueeRef.current.startY, my)
202
+ });
203
+ }
204
+ };
205
+ const handleMouseUp = (ev) => {
206
+ window.removeEventListener("mousemove", handleMouseMove);
207
+ window.removeEventListener("mouseup", handleMouseUp);
208
+ if (marqueeRef.current?.didDrag && onMarqueeSelect) {
209
+ const mx1 = Math.min(marqueeRef.current.startX, (ev.clientX - rect.left) / rect.width * 100);
210
+ const my1 = Math.min(marqueeRef.current.startY, (ev.clientY - rect.top) / rect.height * 100);
211
+ const mx2 = Math.max(marqueeRef.current.startX, (ev.clientX - rect.left) / rect.width * 100);
212
+ const my2 = Math.max(marqueeRef.current.startY, (ev.clientY - rect.top) / rect.height * 100);
213
+ const pageFields = fields.filter((f) => f.page === pageIndex);
214
+ const hits = pageFields.filter((f) => {
215
+ return f.x < mx2 && f.x + f.width > mx1 && f.y < my2 && f.y + f.height > my1;
216
+ }).map((f) => f.id);
217
+ onMarqueeSelect(hits);
218
+ } else if (!marqueeRef.current?.didDrag) {
219
+ if (onPageClick) {
220
+ onPageClick(pageIndex, x, y);
221
+ } else {
222
+ onSelectField(null);
223
+ }
224
+ }
225
+ marqueeRef.current = null;
226
+ setMarquee(null);
227
+ };
228
+ window.addEventListener("mousemove", handleMouseMove);
229
+ window.addEventListener("mouseup", handleMouseUp);
230
+ }, [mode, fields, onPageClick, onSelectField, onMarqueeSelect]);
181
231
  const handleDragOver = useCallback((e) => {
182
232
  if (e.dataTransfer.types.includes("application/exeq-field-type")) {
183
233
  e.preventDefault();
@@ -193,14 +243,14 @@ function PdfViewer({
193
243
  const y = (e.clientY - rect.top) / rect.height * 100;
194
244
  onDropField(pageIndex, x, y, fieldType);
195
245
  }, [onDropField]);
196
- return /* @__PURE__ */ jsx("div", { className: "pdf-viewer", ref: containerRef, children: pages.map((page, pageIndex) => {
246
+ return /* @__PURE__ */ jsx("div", { className: "pdf-viewer-zoom-wrapper", ref: containerRef, style: zoom !== 1 ? { width: `${zoom * 100}%`, minHeight: "min-content" } : void 0, children: /* @__PURE__ */ jsx("div", { className: "pdf-viewer", style: zoom !== 1 ? { transform: `scale(${zoom})`, transformOrigin: "top left", width: `${100 / zoom}%` } : void 0, children: pages.map((page, pageIndex) => {
197
247
  const pageFields = fields.filter((f) => f.page === pageIndex);
198
248
  return /* @__PURE__ */ jsxs(
199
249
  "div",
200
250
  {
201
251
  className: "pdf-page",
202
252
  style: { aspectRatio: `${page.width} / ${page.height}` },
203
- onClick: (e) => handlePageClick(e, pageIndex),
253
+ onMouseDown: (e) => handlePageMouseDown(e, pageIndex),
204
254
  onDragOver: handleDragOver,
205
255
  onDrop: (e) => handleDrop(e, pageIndex),
206
256
  "data-page": pageIndex,
@@ -218,72 +268,181 @@ function PdfViewer({
218
268
  FieldOverlayItem,
219
269
  {
220
270
  field,
221
- isSelected: field.id === selectedFieldId,
222
- onSelect: () => onSelectField(field.id),
271
+ isSelected: selectedFieldIds.has(field.id),
272
+ isMultiSelected: selectedFieldIds.size > 1 && selectedFieldIds.has(field.id),
273
+ onSelect: (e) => onSelectField(field.id, e),
223
274
  onMove: onFieldMove,
224
275
  onResize: onFieldResize,
276
+ onGroupMove: selectedFieldIds.size > 1 && selectedFieldIds.has(field.id) ? onGroupMove : void 0,
277
+ onMoveEnd,
278
+ otherFields: mode === "designer" ? pageFields.filter((f) => f.id !== field.id && !selectedFieldIds.has(f.id)) : void 0,
279
+ setGuides: mode === "designer" ? setGuides : void 0,
280
+ pageIndex,
281
+ selectedIds: selectedFieldIds,
225
282
  mode,
226
283
  currentSigner,
227
284
  renderContent: renderFieldContent
228
285
  },
229
286
  field.id
230
- ))
287
+ )),
288
+ guides.filter((g) => g.page === pageIndex).map((g, i) => g.x !== void 0 ? /* @__PURE__ */ jsx("div", { className: "snap-guide snap-guide-v", style: { left: `${g.x}%` } }, `gx${i}`) : /* @__PURE__ */ jsx("div", { className: "snap-guide snap-guide-h", style: { top: `${g.y}%` } }, `gy${i}`)),
289
+ marquee && marquee.page === pageIndex && /* @__PURE__ */ jsx("div", { className: "marquee-rect", style: {
290
+ left: `${marquee.x1}%`,
291
+ top: `${marquee.y1}%`,
292
+ width: `${marquee.x2 - marquee.x1}%`,
293
+ height: `${marquee.y2 - marquee.y1}%`
294
+ } })
231
295
  ]
232
296
  },
233
297
  pageIndex
234
298
  );
235
- }) });
299
+ }) }) });
300
+ }
301
+ var SNAP_THRESHOLD = 1.2;
302
+ function snapAndGuide(field, others, page) {
303
+ const fx = field.x, fy = field.y, fw = field.width, fh = field.height;
304
+ const fcx = fx + fw / 2, fcy = fy + fh / 2;
305
+ let bestSnapX = null;
306
+ let bestSnapY = null;
307
+ for (const o of others) {
308
+ const ox = o.x, oy = o.y, ow = o.width, oh = o.height;
309
+ const ocx = ox + ow / 2, ocy = oy + oh / 2;
310
+ const xCandidates = [
311
+ [fx, ox, ox],
312
+ // left ↔ left
313
+ [fx + fw, ox + ow, ox + ow],
314
+ // right ↔ right
315
+ [fcx, ocx, ocx],
316
+ // center ↔ center
317
+ [fx, ox + ow, ox + ow],
318
+ // left ↔ right
319
+ [fx + fw, ox, ox]
320
+ // right ↔ left
321
+ ];
322
+ for (const [fe, te, gx] of xCandidates) {
323
+ const d = Math.abs(fe - te);
324
+ if (d < SNAP_THRESHOLD && (!bestSnapX || d < bestSnapX.dist)) {
325
+ bestSnapX = { offset: te - fe, guideX: gx, dist: d };
326
+ }
327
+ }
328
+ const yCandidates = [
329
+ [fy, oy, oy],
330
+ // top ↔ top
331
+ [fy + fh, oy + oh, oy + oh],
332
+ // bottom ↔ bottom
333
+ [fcy, ocy, ocy],
334
+ // center ↔ center
335
+ [fy, oy + oh, oy + oh],
336
+ // top ↔ bottom
337
+ [fy + fh, oy, oy]
338
+ // bottom ↔ top
339
+ ];
340
+ for (const [fe, te, gy] of yCandidates) {
341
+ const d = Math.abs(fe - te);
342
+ if (d < SNAP_THRESHOLD && (!bestSnapY || d < bestSnapY.dist)) {
343
+ bestSnapY = { offset: te - fe, guideY: gy, dist: d };
344
+ }
345
+ }
346
+ }
347
+ const snappedX = bestSnapX ? fx + bestSnapX.offset : fx;
348
+ const snappedY = bestSnapY ? fy + bestSnapY.offset : fy;
349
+ const guides = [];
350
+ if (bestSnapX) guides.push({ page, x: bestSnapX.guideX });
351
+ if (bestSnapY) guides.push({ page, y: bestSnapY.guideY });
352
+ return {
353
+ x: Math.max(0, Math.min(100 - fw, snappedX)),
354
+ y: Math.max(0, Math.min(100 - fh, snappedY)),
355
+ guides
356
+ };
236
357
  }
237
358
  function FieldOverlayItem({
238
359
  field,
239
360
  isSelected,
361
+ isMultiSelected,
240
362
  onSelect,
241
363
  onMove,
242
364
  onResize,
365
+ onGroupMove,
366
+ onMoveEnd,
367
+ otherFields,
368
+ setGuides,
369
+ pageIndex,
370
+ selectedIds,
243
371
  mode,
244
372
  currentSigner,
245
373
  renderContent
246
374
  }) {
247
375
  const overlayRef = useRef(null);
248
376
  const dragStartRef = useRef(null);
377
+ const dragPosRef = useRef({ x: field.x, y: field.y });
378
+ const didDragRef = useRef(false);
249
379
  const resizeStartRef = useRef(null);
250
380
  const isRedact = field.type === "blackout" || field.type === "whiteout";
251
381
  const isEditable = mode === "designer" || mode === "signer" && field.assignee === currentSigner;
252
382
  const isInactiveSigner = mode === "signer" && !isRedact && field.assignee !== currentSigner;
253
- const color = isInactiveSigner ? "#cccccc" : getSignerColor(field.assignee);
383
+ const isFilled = mode === "signer" && isEditable && !isRedact && (field.type === "checkbox" ? true : field.formula ? true : !!field.value);
384
+ const color = isInactiveSigner ? "#cccccc" : isFilled ? "#22c55e" : getSignerColor(field.assignee);
254
385
  const isPreFilled = !isEditable && !!field.value;
255
386
  const handleMouseDown = useCallback((e) => {
256
387
  if (mode !== "designer" || !onMove) return;
257
388
  e.preventDefault();
258
389
  e.stopPropagation();
259
- onSelect();
390
+ const alreadyInSelection = selectedIds.has(field.id) && selectedIds.size > 1;
391
+ if (!alreadyInSelection) {
392
+ onSelect(e);
393
+ }
394
+ if (field.locked) return;
260
395
  const pageEl = overlayRef.current?.closest(".pdf-page");
261
396
  if (!pageEl) return;
397
+ didDragRef.current = false;
262
398
  dragStartRef.current = {
263
399
  startX: e.clientX,
264
400
  startY: e.clientY,
265
401
  fieldX: field.x,
266
402
  fieldY: field.y
267
403
  };
268
- const handleMouseMove = (e2) => {
404
+ const handleMouseMove = (ev) => {
269
405
  if (!dragStartRef.current) return;
406
+ didDragRef.current = true;
270
407
  const rect = pageEl.getBoundingClientRect();
271
- const dx = (e2.clientX - dragStartRef.current.startX) / rect.width * 100;
272
- const dy = (e2.clientY - dragStartRef.current.startY) / rect.height * 100;
273
- const newX = Math.max(0, Math.min(100 - field.width, dragStartRef.current.fieldX + dx));
274
- const newY = Math.max(0, Math.min(100 - field.height, dragStartRef.current.fieldY + dy));
275
- onMove(field.id, field.page, newX, newY);
408
+ const dx = (ev.clientX - dragStartRef.current.startX) / rect.width * 100;
409
+ const dy = (ev.clientY - dragStartRef.current.startY) / rect.height * 100;
410
+ if (isMultiSelected && onGroupMove) {
411
+ onGroupMove(Array.from(selectedIds), dx, dy);
412
+ dragStartRef.current.startX = ev.clientX;
413
+ dragStartRef.current.startY = ev.clientY;
414
+ } else {
415
+ const newX = Math.max(0, Math.min(100 - field.width, dragStartRef.current.fieldX + dx));
416
+ const newY = Math.max(0, Math.min(100 - field.height, dragStartRef.current.fieldY + dy));
417
+ dragPosRef.current = { x: newX, y: newY };
418
+ onMove(field.id, field.page, newX, newY);
419
+ if (ev.shiftKey && otherFields && setGuides && pageIndex !== void 0) {
420
+ const { guides: g } = snapAndGuide({ x: newX, y: newY, width: field.width, height: field.height }, otherFields, pageIndex);
421
+ setGuides(g);
422
+ } else {
423
+ setGuides?.([]);
424
+ }
425
+ }
276
426
  };
277
- const handleMouseUp = () => {
427
+ const handleMouseUp = (ev) => {
428
+ if (ev.shiftKey && didDragRef.current && otherFields && onMove && pageIndex !== void 0 && !isMultiSelected) {
429
+ const pos = dragPosRef.current;
430
+ const snap = snapAndGuide({ x: pos.x, y: pos.y, width: field.width, height: field.height }, otherFields, pageIndex);
431
+ if (snap.x !== pos.x || snap.y !== pos.y) {
432
+ onMove(field.id, field.page, snap.x, snap.y);
433
+ }
434
+ }
278
435
  dragStartRef.current = null;
279
436
  window.removeEventListener("mousemove", handleMouseMove);
280
437
  window.removeEventListener("mouseup", handleMouseUp);
438
+ setGuides?.([]);
439
+ onMoveEnd?.();
281
440
  };
282
441
  window.addEventListener("mousemove", handleMouseMove);
283
442
  window.addEventListener("mouseup", handleMouseUp);
284
- }, [field, mode, onMove, onSelect]);
443
+ }, [field, mode, onMove, onSelect, isMultiSelected, onGroupMove, onMoveEnd, selectedIds, otherFields, setGuides, pageIndex]);
285
444
  const handleResizeMouseDown = useCallback((e) => {
286
- if (mode !== "designer" || !onResize) return;
445
+ if (mode !== "designer" || !onResize || field.locked) return;
287
446
  e.preventDefault();
288
447
  e.stopPropagation();
289
448
  const pageEl = overlayRef.current?.closest(".pdf-page");
@@ -307,15 +466,16 @@ function FieldOverlayItem({
307
466
  resizeStartRef.current = null;
308
467
  window.removeEventListener("mousemove", handleMouseMove);
309
468
  window.removeEventListener("mouseup", handleMouseUp);
469
+ onMoveEnd?.();
310
470
  };
311
471
  window.addEventListener("mousemove", handleMouseMove);
312
472
  window.addEventListener("mouseup", handleMouseUp);
313
- }, [field, mode, onResize]);
473
+ }, [field, mode, onResize, onMoveEnd]);
314
474
  return /* @__PURE__ */ jsxs(
315
475
  "div",
316
476
  {
317
477
  ref: overlayRef,
318
- className: `field-overlay ${isSelected ? "selected" : ""} ${isEditable ? "editable" : "readonly"} ${isPreFilled ? "prefilled" : ""} ${isRedact ? "redact" : ""} ${isInactiveSigner ? "inactive-signer" : ""}`,
478
+ className: `field-overlay ${isSelected ? "selected" : ""} ${isEditable ? "editable" : "readonly"} ${isPreFilled ? "prefilled" : ""} ${isRedact ? "redact" : ""} ${isInactiveSigner ? "inactive-signer" : ""} ${field.locked ? "locked" : ""}`,
319
479
  style: {
320
480
  left: `${field.x}%`,
321
481
  top: `${field.y}%`,
@@ -324,18 +484,18 @@ function FieldOverlayItem({
324
484
  borderColor: isRedact ? field.type === "blackout" ? "#666" : "#bbb" : color,
325
485
  borderStyle: isRedact ? "dashed" : "solid",
326
486
  backgroundColor: isRedact ? field.type === "blackout" ? "#000000" : "#ffffff" : isInactiveSigner ? "#f5f5f5" : isSelected ? `${color}22` : `${color}11`,
327
- cursor: mode === "designer" ? "move" : isInactiveSigner ? "default" : "default",
487
+ cursor: mode === "designer" ? field.locked ? "not-allowed" : "move" : "default",
328
488
  pointerEvents: mode === "signer" && (isRedact || isInactiveSigner) ? "none" : void 0
329
489
  },
330
490
  onClick: (e) => {
331
491
  e.stopPropagation();
332
- onSelect();
492
+ if (!didDragRef.current) onSelect(e);
333
493
  },
334
494
  onMouseDown: handleMouseDown,
335
495
  children: [
336
496
  mode === "designer" && !isRedact && /* @__PURE__ */ jsx("div", { className: "field-overlay-label", style: { backgroundColor: color }, children: field.label }),
337
497
  renderContent ? renderContent(field) : /* @__PURE__ */ jsx("div", { className: "field-overlay-placeholder", children: field.value || field.placeholder }),
338
- mode === "designer" && isSelected && /* @__PURE__ */ jsx(
498
+ mode === "designer" && isSelected && !field.locked && /* @__PURE__ */ jsx(
339
499
  "div",
340
500
  {
341
501
  className: "field-resize-handle",
@@ -348,127 +508,320 @@ function FieldOverlayItem({
348
508
  }
349
509
 
350
510
  // src/components/pdf-builder/FieldPropertyPanel.tsx
351
- import { useState } from "react";
352
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
511
+ import { useState as useState2 } from "react";
512
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
353
513
  var INK_COLORS = [
354
514
  { value: "#000000", label: "Black" },
355
515
  { value: "#1a56db", label: "Blue" }
356
516
  ];
357
- function FieldPropertyPanel({ field, signerRoles, onUpdate, onDelete }) {
517
+ function FieldPropertyPanel({ field, signerRoles, onUpdate, onDelete, prefillContent, pageSize }) {
358
518
  const color = getSignerColor(field.assignee);
359
519
  const isRedactField = field.type === "blackout" || field.type === "whiteout";
360
520
  const isTextField = field.type === "text" || field.type === "signed-date" || field.type === "dropdown";
361
521
  const showInkColor = field.type === "text" || field.type === "dropdown" || field.type === "signature" || field.type === "initials" || field.type === "signed-date";
362
- const [newOption, setNewOption] = useState("");
522
+ const [newOption, setNewOption] = useState2("");
363
523
  return /* @__PURE__ */ jsxs2("div", { className: "field-property-panel", children: [
364
524
  /* @__PURE__ */ jsxs2("div", { className: "panel-header", children: [
365
- /* @__PURE__ */ jsx2("h3", { style: { color }, children: field.label }),
366
- /* @__PURE__ */ jsx2("button", { onClick: () => onDelete(field.id), className: "panel-delete-btn", children: "Delete" })
367
- ] }),
368
- /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
369
- /* @__PURE__ */ jsx2("label", { children: "Label" }),
370
525
  /* @__PURE__ */ jsx2(
371
526
  "input",
372
527
  {
373
- type: "text",
528
+ className: "panel-header-label",
529
+ style: { color },
374
530
  value: field.label,
375
531
  onChange: (e) => onUpdate(field.id, { label: e.target.value })
376
532
  }
377
- )
378
- ] }),
379
- !isRedactField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
380
- /* @__PURE__ */ jsx2("label", { children: "Field Type" }),
381
- /* @__PURE__ */ jsxs2(
382
- "select",
383
- {
384
- value: field.type,
385
- onChange: (e) => onUpdate(field.id, { type: e.target.value }),
386
- children: [
387
- /* @__PURE__ */ jsx2("option", { value: "text", children: "Text" }),
388
- /* @__PURE__ */ jsx2("option", { value: "dropdown", children: "Dropdown" }),
389
- /* @__PURE__ */ jsx2("option", { value: "signature", children: "Signature" }),
390
- /* @__PURE__ */ jsx2("option", { value: "signed-date", children: "Signed Date" }),
391
- /* @__PURE__ */ jsx2("option", { value: "checkbox", children: "Checkbox" }),
392
- /* @__PURE__ */ jsx2("option", { value: "initials", children: "Initials" })
393
- ]
394
- }
395
- )
396
- ] }),
397
- field.type === "text" && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
398
- /* @__PURE__ */ jsx2("label", { children: "Text Type" }),
399
- /* @__PURE__ */ jsxs2(
400
- "select",
401
- {
402
- value: field.textSubtype || "freeform",
403
- onChange: (e) => onUpdate(field.id, { textSubtype: e.target.value }),
404
- children: [
405
- /* @__PURE__ */ jsx2("option", { value: "freeform", children: "Freeform" }),
406
- /* @__PURE__ */ jsx2("option", { value: "number", children: "Number" }),
407
- /* @__PURE__ */ jsx2("option", { value: "date", children: "Date" }),
408
- /* @__PURE__ */ jsx2("option", { value: "email", children: "Email" }),
409
- /* @__PURE__ */ jsx2("option", { value: "phone", children: "Phone" })
410
- ]
411
- }
412
- )
533
+ ),
534
+ /* @__PURE__ */ jsxs2("div", { className: "panel-header-actions", children: [
535
+ /* @__PURE__ */ jsx2(
536
+ "button",
537
+ {
538
+ onClick: () => onUpdate(field.id, { locked: !field.locked }),
539
+ className: `panel-icon-btn ${field.locked ? "active" : ""}`,
540
+ title: field.locked ? "Unlock" : "Lock",
541
+ children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "currentColor", children: field.locked ? /* @__PURE__ */ jsxs2(Fragment, { children: [
542
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "6", width: "10", height: "7", rx: "1.5" }),
543
+ /* @__PURE__ */ jsx2("path", { d: "M4.5 6V4.5a2.5 2.5 0 015 0V6", fill: "none", stroke: "currentColor", strokeWidth: "1.5" })
544
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
545
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "6", width: "10", height: "7", rx: "1.5" }),
546
+ /* @__PURE__ */ jsx2("path", { d: "M4.5 6V4.5a2.5 2.5 0 015 0", fill: "none", stroke: "currentColor", strokeWidth: "1.5" })
547
+ ] }) })
548
+ }
549
+ ),
550
+ /* @__PURE__ */ jsx2("button", { onClick: () => onDelete(field.id), className: "panel-icon-btn panel-icon-btn-danger", title: "Delete", children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M5 1h4v1H5V1zM2 3v1h1v8.5A1.5 1.5 0 004.5 14h5a1.5 1.5 0 001.5-1.5V4h1V3H2zm2 1h6v8.5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5V4z" }) }) })
551
+ ] })
413
552
  ] }),
414
- !isRedactField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
415
- /* @__PURE__ */ jsx2("label", { children: "Assigned To" }),
553
+ !isRedactField && /* @__PURE__ */ jsxs2("div", { className: "panel-section", children: [
554
+ /* @__PURE__ */ jsx2("div", { className: "panel-section-heading", children: "Assigned To" }),
416
555
  /* @__PURE__ */ jsx2(
417
556
  "select",
418
557
  {
558
+ className: "panel-assignee-select",
419
559
  value: field.assignee,
420
560
  onChange: (e) => onUpdate(field.id, { assignee: e.target.value }),
561
+ style: { borderColor: color, color },
421
562
  children: signerRoles.map((role) => /* @__PURE__ */ jsx2("option", { value: role, style: { color: getSignerColor(role) }, children: role }, role))
422
563
  }
423
564
  )
424
565
  ] }),
425
- !isRedactField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
426
- /* @__PURE__ */ jsx2("label", { children: "Placeholder" }),
427
- /* @__PURE__ */ jsx2(
428
- "input",
429
- {
430
- type: "text",
431
- value: field.placeholder,
432
- onChange: (e) => onUpdate(field.id, { placeholder: e.target.value })
433
- }
434
- )
566
+ /* @__PURE__ */ jsxs2("div", { className: "panel-section", children: [
567
+ /* @__PURE__ */ jsx2("div", { className: "panel-section-heading", children: "Content" }),
568
+ !isRedactField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
569
+ /* @__PURE__ */ jsx2("label", { children: "Field Type" }),
570
+ /* @__PURE__ */ jsxs2("select", { value: field.type, onChange: (e) => onUpdate(field.id, { type: e.target.value }), children: [
571
+ /* @__PURE__ */ jsx2("option", { value: "text", children: "Text" }),
572
+ /* @__PURE__ */ jsx2("option", { value: "dropdown", children: "Dropdown" }),
573
+ /* @__PURE__ */ jsx2("option", { value: "signature", children: "Signature" }),
574
+ /* @__PURE__ */ jsx2("option", { value: "signed-date", children: "Signed Date" }),
575
+ /* @__PURE__ */ jsx2("option", { value: "checkbox", children: "Checkbox" }),
576
+ /* @__PURE__ */ jsx2("option", { value: "initials", children: "Initials" })
577
+ ] })
578
+ ] }),
579
+ field.type === "text" && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
580
+ /* @__PURE__ */ jsx2("label", { children: "Text Type" }),
581
+ /* @__PURE__ */ jsxs2("select", { value: field.textSubtype || "freeform", onChange: (e) => onUpdate(field.id, { textSubtype: e.target.value }), children: [
582
+ /* @__PURE__ */ jsx2("option", { value: "freeform", children: "Freeform" }),
583
+ /* @__PURE__ */ jsx2("option", { value: "number", children: "Number" }),
584
+ /* @__PURE__ */ jsx2("option", { value: "date", children: "Date" }),
585
+ /* @__PURE__ */ jsx2("option", { value: "email", children: "Email" }),
586
+ /* @__PURE__ */ jsx2("option", { value: "phone", children: "Phone" })
587
+ ] })
588
+ ] }),
589
+ !isRedactField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
590
+ /* @__PURE__ */ jsx2("label", { children: "Placeholder" }),
591
+ /* @__PURE__ */ jsx2("input", { type: "text", value: field.placeholder, onChange: (e) => onUpdate(field.id, { placeholder: e.target.value }) })
592
+ ] }),
593
+ !isRedactField && field.type !== "checkbox" && /* @__PURE__ */ jsx2("div", { className: "panel-field", children: /* @__PURE__ */ jsxs2("label", { className: "panel-checkbox-label", children: [
594
+ /* @__PURE__ */ jsx2("input", { type: "checkbox", checked: field.required, onChange: (e) => onUpdate(field.id, { required: e.target.checked }) }),
595
+ "Required"
596
+ ] }) }),
597
+ isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
598
+ /* @__PURE__ */ jsx2("label", { children: "Formula" }),
599
+ /* @__PURE__ */ jsx2("input", { type: "text", value: field.formula || "", onChange: (e) => onUpdate(field.id, { formula: e.target.value || void 0 }), placeholder: "e.g. {{Date Field | month}}" }),
600
+ field.formula && /* @__PURE__ */ jsx2("span", { className: "panel-hint", children: "Auto-computed. Signer cannot edit." })
601
+ ] }),
602
+ field.type === "text" && /* @__PURE__ */ jsxs2("div", { className: "panel-field-row", children: [
603
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
604
+ /* @__PURE__ */ jsx2("label", { children: "Min Chars" }),
605
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "0", max: "9999", value: field.minLength || 0, onChange: (e) => onUpdate(field.id, { minLength: Number(e.target.value) }) })
606
+ ] }),
607
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
608
+ /* @__PURE__ */ jsx2("label", { children: "Max Chars" }),
609
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "0", max: "9999", value: field.maxLength || 0, onChange: (e) => onUpdate(field.id, { maxLength: Number(e.target.value) }) })
610
+ ] })
611
+ ] }),
612
+ (field.type === "text" || field.type === "dropdown") && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
613
+ /* @__PURE__ */ jsx2("label", { children: field.type === "dropdown" ? "Options" : "Predefined Options" }),
614
+ /* @__PURE__ */ jsxs2("div", { className: "panel-options-list", children: [
615
+ (field.options || []).map((opt, i) => /* @__PURE__ */ jsxs2("div", { className: "panel-option-item", children: [
616
+ /* @__PURE__ */ jsx2("span", { children: opt }),
617
+ /* @__PURE__ */ jsx2("button", { className: "panel-option-remove", onClick: () => {
618
+ const updated = (field.options || []).filter((_, j) => j !== i);
619
+ onUpdate(field.id, { options: updated.length > 0 ? updated : void 0 });
620
+ }, children: "\xD7" })
621
+ ] }, i)),
622
+ /* @__PURE__ */ jsxs2("div", { className: "panel-option-add", children: [
623
+ /* @__PURE__ */ jsx2("input", { type: "text", value: newOption, onChange: (e) => setNewOption(e.target.value), onKeyDown: (e) => {
624
+ if (e.key === "Enter" && newOption.trim()) {
625
+ onUpdate(field.id, { options: [...field.options || [], newOption.trim()] });
626
+ setNewOption("");
627
+ }
628
+ }, placeholder: "Add option..." }),
629
+ /* @__PURE__ */ jsx2("button", { onClick: () => {
630
+ if (newOption.trim()) {
631
+ onUpdate(field.id, { options: [...field.options || [], newOption.trim()] });
632
+ setNewOption("");
633
+ }
634
+ }, children: "+" })
635
+ ] })
636
+ ] }),
637
+ field.type === "dropdown" && (field.options?.length ?? 0) === 0 && /* @__PURE__ */ jsx2("span", { className: "panel-hint", style: { color: "#c44" }, children: "Add at least one option" }),
638
+ field.type === "text" && (field.options?.length ?? 0) > 0 && /* @__PURE__ */ jsx2("span", { className: "panel-hint", children: "Signer will choose from these options via dropdown" })
639
+ ] }),
640
+ prefillContent
435
641
  ] }),
436
- isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
437
- /* @__PURE__ */ jsx2("label", { children: "Formula" }),
438
- /* @__PURE__ */ jsx2(
439
- "input",
642
+ (isTextField || showInkColor) && /* @__PURE__ */ jsxs2("div", { className: "panel-section", children: [
643
+ /* @__PURE__ */ jsx2("div", { className: "panel-section-heading", children: "Style" }),
644
+ isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
645
+ /* @__PURE__ */ jsx2("label", { children: "Font" }),
646
+ /* @__PURE__ */ jsx2("select", { value: field.fontFamily || "Helvetica", onChange: (e) => onUpdate(field.id, { fontFamily: e.target.value }), children: FONT_FAMILIES.map((f) => /* @__PURE__ */ jsx2("option", { value: f.value, children: f.label }, f.value)) })
647
+ ] }),
648
+ isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
649
+ /* @__PURE__ */ jsx2("label", { children: "Font Size (pt)" }),
650
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "6", max: "72", value: field.fontSize, onChange: (e) => onUpdate(field.id, { fontSize: Number(e.target.value) }) })
651
+ ] }),
652
+ isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field-row", children: [
653
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
654
+ /* @__PURE__ */ jsx2("label", { children: "Letter Spacing" }),
655
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "0", max: "20", step: "0.5", value: field.letterSpacing || 0, onChange: (e) => onUpdate(field.id, { letterSpacing: Number(e.target.value) }) })
656
+ ] }),
657
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
658
+ /* @__PURE__ */ jsx2("label", { children: "Line Height" }),
659
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "0.8", max: "3", step: "0.1", value: field.lineHeight || 1.2, onChange: (e) => onUpdate(field.id, { lineHeight: Number(e.target.value) }) })
660
+ ] })
661
+ ] }),
662
+ showInkColor && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
663
+ /* @__PURE__ */ jsx2("label", { children: "Ink Color" }),
664
+ /* @__PURE__ */ jsx2("div", { className: "ink-color-picker", children: INK_COLORS.map((c) => /* @__PURE__ */ jsx2("button", { className: `ink-color-swatch ${(field.inkColor || "#000000") === c.value ? "active" : ""}`, style: { backgroundColor: c.value }, onClick: () => onUpdate(field.id, { inkColor: c.value }), title: c.label }, c.value)) })
665
+ ] })
666
+ ] }),
667
+ /* @__PURE__ */ jsxs2("div", { className: "panel-section", children: [
668
+ /* @__PURE__ */ jsx2("div", { className: "panel-section-heading", children: "Position & Size" }),
669
+ (() => {
670
+ const pw = pageSize?.width || 612;
671
+ const ph = pageSize?.height || 792;
672
+ const toPt = (pct, dim) => Math.round(pct / 100 * dim * 10) / 10;
673
+ const fromPt = (pt, dim) => pt / dim * 100;
674
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
675
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field-row", children: [
676
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
677
+ /* @__PURE__ */ jsx2("label", { children: "X (pt)" }),
678
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "0", step: "0.5", value: toPt(field.x, pw), onChange: (e) => onUpdate(field.id, { x: fromPt(Number(e.target.value), pw) }) })
679
+ ] }),
680
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
681
+ /* @__PURE__ */ jsx2("label", { children: "Y (pt)" }),
682
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "0", step: "0.5", value: toPt(field.y, ph), onChange: (e) => onUpdate(field.id, { y: fromPt(Number(e.target.value), ph) }) })
683
+ ] })
684
+ ] }),
685
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field-row", children: [
686
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
687
+ /* @__PURE__ */ jsx2("label", { children: "Width (pt)" }),
688
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "1", step: "0.5", value: toPt(field.width, pw), onChange: (e) => onUpdate(field.id, { width: fromPt(Number(e.target.value), pw) }) })
689
+ ] }),
690
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
691
+ /* @__PURE__ */ jsx2("label", { children: "Height (pt)" }),
692
+ /* @__PURE__ */ jsx2("input", { type: "number", min: "1", step: "0.5", value: toPt(field.height, ph), onChange: (e) => onUpdate(field.id, { height: fromPt(Number(e.target.value), ph) }) })
693
+ ] })
694
+ ] }),
695
+ /* @__PURE__ */ jsxs2("div", { className: "panel-hint", children: [
696
+ pw,
697
+ " \xD7 ",
698
+ ph,
699
+ " pt \xB7 Page ",
700
+ field.page + 1,
701
+ " \xB7 ",
702
+ field.id.slice(0, 8)
703
+ ] })
704
+ ] });
705
+ })()
706
+ ] })
707
+ ] });
708
+ }
709
+ function sharedValue(items) {
710
+ if (items.length === 0) return void 0;
711
+ return items.every((v) => v === items[0]) ? items[0] : void 0;
712
+ }
713
+ function BulkPropertyPanel({ fields, signerRoles, onUpdate, onAlign, onDelete }) {
714
+ const count = fields.length;
715
+ const hasText = fields.some((f) => f.type === "text" || f.type === "dropdown" || f.type === "signed-date");
716
+ const allNonRedact = fields.every((f) => f.type !== "blackout" && f.type !== "whiteout");
717
+ const sharedAssignee = sharedValue(fields.map((f) => f.assignee));
718
+ const sharedRequired = sharedValue(fields.map((f) => f.required));
719
+ const sharedFontSize = sharedValue(fields.map((f) => f.fontSize));
720
+ const sharedFontFamily = sharedValue(fields.map((f) => f.fontFamily || "Helvetica"));
721
+ const sharedInkColor = sharedValue(fields.map((f) => f.inkColor || "#000000"));
722
+ const sharedLetterSpacing = sharedValue(fields.map((f) => f.letterSpacing || 0));
723
+ const sharedLineHeight = sharedValue(fields.map((f) => f.lineHeight || 1.2));
724
+ return /* @__PURE__ */ jsxs2("div", { className: "field-property-panel", children: [
725
+ /* @__PURE__ */ jsxs2("div", { className: "panel-header", children: [
726
+ /* @__PURE__ */ jsxs2("h3", { children: [
727
+ count,
728
+ " fields selected"
729
+ ] }),
730
+ /* @__PURE__ */ jsx2("button", { onClick: onDelete, className: "panel-delete-btn", children: "Delete All" })
731
+ ] }),
732
+ /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
733
+ /* @__PURE__ */ jsx2("label", { children: "Align" }),
734
+ /* @__PURE__ */ jsxs2("div", { className: "align-buttons", children: [
735
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("left"), title: "Align left edges", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
736
+ /* @__PURE__ */ jsx2("rect", { x: "1", y: "0", width: "1.5", height: "14", fill: "currentColor" }),
737
+ /* @__PURE__ */ jsx2("rect", { x: "4", y: "2", width: "8", height: "3", fill: "currentColor", opacity: "0.5" }),
738
+ /* @__PURE__ */ jsx2("rect", { x: "4", y: "7", width: "5", height: "3", fill: "currentColor", opacity: "0.5" })
739
+ ] }) }),
740
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("center-h"), title: "Align centers horizontally", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
741
+ /* @__PURE__ */ jsx2("rect", { x: "6.25", y: "0", width: "1.5", height: "14", fill: "currentColor" }),
742
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "2", width: "10", height: "3", fill: "currentColor", opacity: "0.5" }),
743
+ /* @__PURE__ */ jsx2("rect", { x: "3.5", y: "7", width: "7", height: "3", fill: "currentColor", opacity: "0.5" })
744
+ ] }) }),
745
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("right"), title: "Align right edges", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
746
+ /* @__PURE__ */ jsx2("rect", { x: "11.5", y: "0", width: "1.5", height: "14", fill: "currentColor" }),
747
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "2", width: "8", height: "3", fill: "currentColor", opacity: "0.5" }),
748
+ /* @__PURE__ */ jsx2("rect", { x: "5", y: "7", width: "5", height: "3", fill: "currentColor", opacity: "0.5" })
749
+ ] }) }),
750
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("top"), title: "Align top edges", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
751
+ /* @__PURE__ */ jsx2("rect", { x: "0", y: "1", width: "14", height: "1.5", fill: "currentColor" }),
752
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "4", width: "3", height: "8", fill: "currentColor", opacity: "0.5" }),
753
+ /* @__PURE__ */ jsx2("rect", { x: "7", y: "4", width: "3", height: "5", fill: "currentColor", opacity: "0.5" })
754
+ ] }) }),
755
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("center-v"), title: "Align centers vertically", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
756
+ /* @__PURE__ */ jsx2("rect", { x: "0", y: "6.25", width: "14", height: "1.5", fill: "currentColor" }),
757
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "1", width: "3", height: "12", fill: "currentColor", opacity: "0.5" }),
758
+ /* @__PURE__ */ jsx2("rect", { x: "7", y: "3", width: "3", height: "8", fill: "currentColor", opacity: "0.5" })
759
+ ] }) }),
760
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("bottom"), title: "Align bottom edges", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
761
+ /* @__PURE__ */ jsx2("rect", { x: "0", y: "11.5", width: "14", height: "1.5", fill: "currentColor" }),
762
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "2", width: "3", height: "8", fill: "currentColor", opacity: "0.5" }),
763
+ /* @__PURE__ */ jsx2("rect", { x: "7", y: "5", width: "3", height: "5", fill: "currentColor", opacity: "0.5" })
764
+ ] }) })
765
+ ] }),
766
+ count > 2 && /* @__PURE__ */ jsxs2(Fragment, { children: [
767
+ /* @__PURE__ */ jsx2("label", { style: { marginTop: "0.4rem" }, children: "Distribute" }),
768
+ /* @__PURE__ */ jsxs2("div", { className: "align-buttons", children: [
769
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("distribute-h"), title: "Distribute horizontally", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
770
+ /* @__PURE__ */ jsx2("rect", { x: "0", y: "0", width: "1.5", height: "14", fill: "currentColor" }),
771
+ /* @__PURE__ */ jsx2("rect", { x: "12.5", y: "0", width: "1.5", height: "14", fill: "currentColor" }),
772
+ /* @__PURE__ */ jsx2("rect", { x: "5", y: "3", width: "4", height: "8", fill: "currentColor", opacity: "0.5" })
773
+ ] }) }),
774
+ /* @__PURE__ */ jsx2("button", { onClick: () => onAlign("distribute-v"), title: "Distribute vertically", children: /* @__PURE__ */ jsxs2("svg", { width: "14", height: "14", viewBox: "0 0 14 14", children: [
775
+ /* @__PURE__ */ jsx2("rect", { x: "0", y: "0", width: "14", height: "1.5", fill: "currentColor" }),
776
+ /* @__PURE__ */ jsx2("rect", { x: "0", y: "12.5", width: "14", height: "1.5", fill: "currentColor" }),
777
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "5", width: "8", height: "4", fill: "currentColor", opacity: "0.5" })
778
+ ] }) })
779
+ ] })
780
+ ] })
781
+ ] }),
782
+ allNonRedact && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
783
+ /* @__PURE__ */ jsx2("label", { children: "Assigned To" }),
784
+ /* @__PURE__ */ jsxs2(
785
+ "select",
440
786
  {
441
- type: "text",
442
- value: field.formula || "",
443
- onChange: (e) => onUpdate(field.id, { formula: e.target.value || void 0 }),
444
- placeholder: "e.g. {{Date Field | month}}"
787
+ value: sharedAssignee || "",
788
+ onChange: (e) => onUpdate({ assignee: e.target.value }),
789
+ children: [
790
+ !sharedAssignee && /* @__PURE__ */ jsx2("option", { value: "", disabled: true, children: "Mixed" }),
791
+ signerRoles.map((role) => /* @__PURE__ */ jsx2("option", { value: role, style: { color: getSignerColor(role) }, children: role }, role))
792
+ ]
445
793
  }
446
- ),
447
- field.formula && /* @__PURE__ */ jsx2("span", { className: "panel-hint", children: "This field auto-computes from other fields. Signer cannot edit it." })
794
+ )
448
795
  ] }),
449
- !isRedactField && field.type !== "checkbox" && /* @__PURE__ */ jsx2("div", { className: "panel-field", children: /* @__PURE__ */ jsxs2("label", { className: "panel-checkbox-label", children: [
796
+ allNonRedact && /* @__PURE__ */ jsx2("div", { className: "panel-field", children: /* @__PURE__ */ jsxs2("label", { className: "panel-checkbox-label", children: [
450
797
  /* @__PURE__ */ jsx2(
451
798
  "input",
452
799
  {
453
800
  type: "checkbox",
454
- checked: field.required,
455
- onChange: (e) => onUpdate(field.id, { required: e.target.checked })
801
+ checked: sharedRequired ?? false,
802
+ ref: (el) => {
803
+ if (el) el.indeterminate = sharedRequired === void 0;
804
+ },
805
+ onChange: (e) => onUpdate({ required: e.target.checked })
456
806
  }
457
807
  ),
458
808
  "Required"
459
809
  ] }) }),
460
- isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
810
+ hasText && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
461
811
  /* @__PURE__ */ jsx2("label", { children: "Font" }),
462
- /* @__PURE__ */ jsx2(
812
+ /* @__PURE__ */ jsxs2(
463
813
  "select",
464
814
  {
465
- value: field.fontFamily || "Helvetica",
466
- onChange: (e) => onUpdate(field.id, { fontFamily: e.target.value }),
467
- children: FONT_FAMILIES.map((f) => /* @__PURE__ */ jsx2("option", { value: f.value, children: f.label }, f.value))
815
+ value: sharedFontFamily || "",
816
+ onChange: (e) => onUpdate({ fontFamily: e.target.value }),
817
+ children: [
818
+ !sharedFontFamily && /* @__PURE__ */ jsx2("option", { value: "", disabled: true, children: "Mixed" }),
819
+ FONT_FAMILIES.map((f) => /* @__PURE__ */ jsx2("option", { value: f.value, children: f.label }, f.value))
820
+ ]
468
821
  }
469
822
  )
470
823
  ] }),
471
- isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
824
+ hasText && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
472
825
  /* @__PURE__ */ jsx2("label", { children: "Font Size (pt)" }),
473
826
  /* @__PURE__ */ jsx2(
474
827
  "input",
@@ -476,12 +829,13 @@ function FieldPropertyPanel({ field, signerRoles, onUpdate, onDelete }) {
476
829
  type: "number",
477
830
  min: "6",
478
831
  max: "72",
479
- value: field.fontSize,
480
- onChange: (e) => onUpdate(field.id, { fontSize: Number(e.target.value) })
832
+ value: sharedFontSize ?? "",
833
+ placeholder: "Mixed",
834
+ onChange: (e) => onUpdate({ fontSize: Number(e.target.value) })
481
835
  }
482
836
  )
483
837
  ] }),
484
- isTextField && /* @__PURE__ */ jsxs2("div", { className: "panel-field-row", children: [
838
+ hasText && /* @__PURE__ */ jsxs2("div", { className: "panel-field-row", children: [
485
839
  /* @__PURE__ */ jsxs2("div", { className: "panel-field panel-field-half", children: [
486
840
  /* @__PURE__ */ jsx2("label", { children: "Letter Spacing" }),
487
841
  /* @__PURE__ */ jsx2(
@@ -491,8 +845,9 @@ function FieldPropertyPanel({ field, signerRoles, onUpdate, onDelete }) {
491
845
  min: "0",
492
846
  max: "20",
493
847
  step: "0.5",
494
- value: field.letterSpacing || 0,
495
- onChange: (e) => onUpdate(field.id, { letterSpacing: Number(e.target.value) })
848
+ value: sharedLetterSpacing ?? "",
849
+ placeholder: "Mixed",
850
+ onChange: (e) => onUpdate({ letterSpacing: Number(e.target.value) })
496
851
  }
497
852
  )
498
853
  ] }),
@@ -505,93 +860,31 @@ function FieldPropertyPanel({ field, signerRoles, onUpdate, onDelete }) {
505
860
  min: "0.8",
506
861
  max: "3",
507
862
  step: "0.1",
508
- value: field.lineHeight || 1.2,
509
- onChange: (e) => onUpdate(field.id, { lineHeight: Number(e.target.value) })
863
+ value: sharedLineHeight ?? "",
864
+ placeholder: "Mixed",
865
+ onChange: (e) => onUpdate({ lineHeight: Number(e.target.value) })
510
866
  }
511
867
  )
512
868
  ] })
513
869
  ] }),
514
- field.type === "text" && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
515
- /* @__PURE__ */ jsx2("label", { children: "Max Characters (0 = unlimited)" }),
516
- /* @__PURE__ */ jsx2(
517
- "input",
518
- {
519
- type: "number",
520
- min: "0",
521
- max: "9999",
522
- value: field.maxLength || 0,
523
- onChange: (e) => onUpdate(field.id, { maxLength: Number(e.target.value) })
524
- }
525
- )
526
- ] }),
527
- showInkColor && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
870
+ allNonRedact && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
528
871
  /* @__PURE__ */ jsx2("label", { children: "Ink Color" }),
529
872
  /* @__PURE__ */ jsx2("div", { className: "ink-color-picker", children: INK_COLORS.map((c) => /* @__PURE__ */ jsx2(
530
873
  "button",
531
874
  {
532
- className: `ink-color-swatch ${(field.inkColor || "#000000") === c.value ? "active" : ""}`,
875
+ className: `ink-color-swatch ${sharedInkColor === c.value ? "active" : ""}`,
533
876
  style: { backgroundColor: c.value },
534
- onClick: () => onUpdate(field.id, { inkColor: c.value }),
877
+ onClick: () => onUpdate({ inkColor: c.value }),
535
878
  title: c.label
536
879
  },
537
880
  c.value
538
881
  )) })
539
- ] }),
540
- (field.type === "text" || field.type === "dropdown") && /* @__PURE__ */ jsxs2("div", { className: "panel-field", children: [
541
- /* @__PURE__ */ jsx2("label", { children: field.type === "dropdown" ? "Options" : "Predefined Options" }),
542
- /* @__PURE__ */ jsxs2("div", { className: "panel-options-list", children: [
543
- (field.options || []).map((opt, i) => /* @__PURE__ */ jsxs2("div", { className: "panel-option-item", children: [
544
- /* @__PURE__ */ jsx2("span", { children: opt }),
545
- /* @__PURE__ */ jsx2(
546
- "button",
547
- {
548
- className: "panel-option-remove",
549
- onClick: () => {
550
- const updated = (field.options || []).filter((_, j) => j !== i);
551
- onUpdate(field.id, { options: updated.length > 0 ? updated : void 0 });
552
- },
553
- children: "\xD7"
554
- }
555
- )
556
- ] }, i)),
557
- /* @__PURE__ */ jsxs2("div", { className: "panel-option-add", children: [
558
- /* @__PURE__ */ jsx2(
559
- "input",
560
- {
561
- type: "text",
562
- value: newOption,
563
- onChange: (e) => setNewOption(e.target.value),
564
- onKeyDown: (e) => {
565
- if (e.key === "Enter" && newOption.trim()) {
566
- onUpdate(field.id, { options: [...field.options || [], newOption.trim()] });
567
- setNewOption("");
568
- }
569
- },
570
- placeholder: "Add option..."
571
- }
572
- ),
573
- /* @__PURE__ */ jsx2(
574
- "button",
575
- {
576
- onClick: () => {
577
- if (newOption.trim()) {
578
- onUpdate(field.id, { options: [...field.options || [], newOption.trim()] });
579
- setNewOption("");
580
- }
581
- },
582
- children: "+"
583
- }
584
- )
585
- ] })
586
- ] }),
587
- field.type === "dropdown" && (field.options?.length ?? 0) === 0 && /* @__PURE__ */ jsx2("span", { className: "panel-hint", style: { color: "#c44" }, children: "Add at least one option" }),
588
- field.type === "text" && (field.options?.length ?? 0) > 0 && /* @__PURE__ */ jsx2("span", { className: "panel-hint", children: "Signer will choose from these options via dropdown" })
589
882
  ] })
590
883
  ] });
591
884
  }
592
885
 
593
886
  // src/components/pdf-builder/SignatureCanvas.tsx
594
- import { useRef as useRef2, useState as useState2, useCallback as useCallback2, useEffect } from "react";
887
+ import { useRef as useRef2, useState as useState3, useCallback as useCallback2, useEffect } from "react";
595
888
  import { getStroke } from "perfect-freehand";
596
889
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
597
890
  function getSvgPathFromStroke(stroke) {
@@ -616,9 +909,9 @@ function SignatureCanvas({
616
909
  inkColor = "#000000"
617
910
  }) {
618
911
  const canvasRef = useRef2(null);
619
- const [paths, setPaths] = useState2([]);
620
- const [currentPath, setCurrentPath] = useState2(null);
621
- const [isEmpty, setIsEmpty] = useState2(!initialValue);
912
+ const [paths, setPaths] = useState3([]);
913
+ const [currentPath, setCurrentPath] = useState3(null);
914
+ const [isEmpty, setIsEmpty] = useState3(!initialValue);
622
915
  useEffect(() => {
623
916
  if (initialValue && canvasRef.current) {
624
917
  const ctx = canvasRef.current.getContext("2d");
@@ -730,8 +1023,77 @@ function isValidApiKey(key) {
730
1023
  return VALID_KEYS.has(key);
731
1024
  }
732
1025
 
1026
+ // src/hooks/useHistory.ts
1027
+ import { useState as useState4, useCallback as useCallback3, useRef as useRef3 } from "react";
1028
+ function useHistory(initialState, maxHistory = 50) {
1029
+ const [state, setState] = useState4({
1030
+ past: [],
1031
+ present: initialState,
1032
+ future: []
1033
+ });
1034
+ const skipRef = useRef3(false);
1035
+ const set = useCallback3((updater) => {
1036
+ setState((prev) => {
1037
+ const newPresent = typeof updater === "function" ? updater(prev.present) : updater;
1038
+ if (newPresent === prev.present) return prev;
1039
+ if (skipRef.current) {
1040
+ skipRef.current = false;
1041
+ return { ...prev, present: newPresent };
1042
+ }
1043
+ return {
1044
+ past: [...prev.past.slice(-maxHistory), prev.present],
1045
+ present: newPresent,
1046
+ future: []
1047
+ };
1048
+ });
1049
+ }, [maxHistory]);
1050
+ const setWithoutHistory = useCallback3((updater) => {
1051
+ skipRef.current = true;
1052
+ set(updater);
1053
+ }, [set]);
1054
+ const snapshot = useCallback3(() => {
1055
+ setState((prev) => ({
1056
+ past: [...prev.past.slice(-maxHistory), prev.present],
1057
+ present: prev.present,
1058
+ future: []
1059
+ }));
1060
+ }, [maxHistory]);
1061
+ const undo = useCallback3(() => {
1062
+ setState((prev) => {
1063
+ if (prev.past.length === 0) return prev;
1064
+ const previous = prev.past[prev.past.length - 1];
1065
+ return {
1066
+ past: prev.past.slice(0, -1),
1067
+ present: previous,
1068
+ future: [prev.present, ...prev.future]
1069
+ };
1070
+ });
1071
+ }, []);
1072
+ const redo = useCallback3(() => {
1073
+ setState((prev) => {
1074
+ if (prev.future.length === 0) return prev;
1075
+ const next = prev.future[0];
1076
+ return {
1077
+ past: [...prev.past, prev.present],
1078
+ present: next,
1079
+ future: prev.future.slice(1)
1080
+ };
1081
+ });
1082
+ }, []);
1083
+ return {
1084
+ state: state.present,
1085
+ set,
1086
+ setWithoutHistory,
1087
+ snapshot,
1088
+ undo,
1089
+ redo,
1090
+ canUndo: state.past.length > 0,
1091
+ canRedo: state.future.length > 0
1092
+ };
1093
+ }
1094
+
733
1095
  // src/components/pdf-builder/DesignerView.tsx
734
- import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1096
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
735
1097
  var FIELD_TYPE_META = [
736
1098
  { type: "text", label: "Text", icon: "T" },
737
1099
  { type: "dropdown", label: "Dropdown", icon: "\u25BE" },
@@ -760,23 +1122,27 @@ function DesignerView({
760
1122
  ] })
761
1123
  ] }) });
762
1124
  }
763
- const [pages, setPages] = useState3([]);
764
- const [fields, setFields] = useState3(initialTemplate?.fields ?? []);
765
- const [selectedFieldId, setSelectedFieldId] = useState3(null);
766
- const [signerRoles, setSignerRoles] = useState3(initialTemplate?.signerRoles ?? [...DEFAULT_SIGNER_ROLES]);
767
- const [activeRole, setActiveRole] = useState3("Sender");
768
- const [activeFieldType, setActiveFieldType] = useState3("text");
769
- const [loading, setLoading] = useState3(false);
770
- const [pdfSource, setPdfSource] = useState3(null);
771
- const [rightTab, setRightTab] = useState3("properties");
772
- const [isAddingRole, setIsAddingRole] = useState3(false);
773
- const [newRoleName, setNewRoleName] = useState3("");
774
- const [draggingFieldType, setDraggingFieldType] = useState3(null);
775
- const [panelWidth, setPanelWidth] = useState3(380);
776
- const [clipboardField, setClipboardField] = useState3(null);
777
- const dragGhostRef = useRef3(null);
778
- const resizingRef = useRef3(false);
779
- const lastStylesRef = useRef3({});
1125
+ const [pages, setPages] = useState5([]);
1126
+ const { state: fields, set: setFields, setWithoutHistory: setFieldsSilent, snapshot: snapshotFields, undo: undoFields, redo: redoFields, canUndo, canRedo } = useHistory(initialTemplate?.fields ?? []);
1127
+ const [selectedFieldIds, setSelectedFieldIds] = useState5(/* @__PURE__ */ new Set());
1128
+ const [signerRoles, setSignerRoles] = useState5(initialTemplate?.signerRoles ?? [...DEFAULT_SIGNER_ROLES]);
1129
+ const [activeRole, setActiveRole] = useState5("Sender");
1130
+ const [activeFieldType, setActiveFieldType] = useState5("text");
1131
+ const [loading, setLoading] = useState5(false);
1132
+ const [pdfSource, setPdfSource] = useState5(null);
1133
+ const [rightTab, setRightTab] = useState5("properties");
1134
+ const [isAddingRole, setIsAddingRole] = useState5(false);
1135
+ const [newRoleName, setNewRoleName] = useState5("");
1136
+ const [draggingFieldType, setDraggingFieldType] = useState5(null);
1137
+ const [panelWidth, setPanelWidth] = useState5(380);
1138
+ const [zoom, setZoom] = useState5(1);
1139
+ const [isPanning, setIsPanning] = useState5(false);
1140
+ const panRef = useRef4(null);
1141
+ const pdfAreaRef = useRef4(null);
1142
+ const [clipboardFields, setClipboardFields] = useState5([]);
1143
+ const dragGhostRef = useRef4(null);
1144
+ const resizingRef = useRef4(false);
1145
+ const lastStylesRef = useRef4({});
780
1146
  useEffect2(() => {
781
1147
  const pdfUrl = initialPdfUrl || initialTemplate?.pdfUrl;
782
1148
  if (pdfUrl) {
@@ -801,7 +1167,7 @@ function DesignerView({
801
1167
  window.addEventListener("message", handleMessage);
802
1168
  return () => window.removeEventListener("message", handleMessage);
803
1169
  }, [initialPdfUrl, initialTemplate]);
804
- const loadPdf = useCallback3(async (source) => {
1170
+ const loadPdf = useCallback4(async (source) => {
805
1171
  setLoading(true);
806
1172
  try {
807
1173
  const rendered = await renderPdfPages(source);
@@ -813,7 +1179,7 @@ function DesignerView({
813
1179
  setLoading(false);
814
1180
  }
815
1181
  }, []);
816
- const handleFileUpload = useCallback3((e) => {
1182
+ const handleFileUpload = useCallback4((e) => {
817
1183
  const file = e.target.files?.[0];
818
1184
  if (!file) return;
819
1185
  const reader = new FileReader();
@@ -822,19 +1188,22 @@ function DesignerView({
822
1188
  };
823
1189
  reader.readAsArrayBuffer(file);
824
1190
  }, [loadPdf]);
825
- const handlePageClick = useCallback3((page, x, y) => {
1191
+ const handlePageClick = useCallback4((page, x, y) => {
826
1192
  const field = createField(activeFieldType, activeRole, page, x, y, fields);
827
1193
  const sticky = lastStylesRef.current[activeFieldType];
828
1194
  const styledField = sticky ? { ...field, ...sticky } : field;
829
1195
  setFields((prev) => [...prev, styledField]);
830
- setSelectedFieldId(styledField.id);
1196
+ setSelectedFieldIds(/* @__PURE__ */ new Set([styledField.id]));
831
1197
  setRightTab("properties");
832
1198
  }, [activeFieldType, activeRole, fields]);
833
- const handleFieldMove = useCallback3((id, page, x, y) => {
834
- setFields((prev) => prev.map((f) => f.id === id ? { ...f, page, x, y } : f));
835
- }, []);
836
- const handleFieldResize = useCallback3((id, width, height) => {
837
- setFields((prev) => {
1199
+ const handleFieldMove = useCallback4((id, page, x, y) => {
1200
+ setFieldsSilent((prev) => prev.map((f) => {
1201
+ if (f.id !== id || f.locked) return f;
1202
+ return { ...f, page, x, y };
1203
+ }));
1204
+ }, [setFieldsSilent]);
1205
+ const handleFieldResize = useCallback4((id, width, height) => {
1206
+ setFieldsSilent((prev) => {
838
1207
  const field = prev.find((f) => f.id === id);
839
1208
  if (field) {
840
1209
  const existing = lastStylesRef.current[field.type] || {};
@@ -842,8 +1211,8 @@ function DesignerView({
842
1211
  }
843
1212
  return prev.map((f) => f.id === id ? { ...f, width, height } : f);
844
1213
  });
845
- }, []);
846
- const handleFieldUpdate = useCallback3((id, updates) => {
1214
+ }, [setFieldsSilent]);
1215
+ const handleFieldUpdate = useCallback4((id, updates) => {
847
1216
  setFields((prev) => {
848
1217
  if (updates.label !== void 0) {
849
1218
  const otherLabels = prev.filter((f) => f.id !== id).map((f) => f.label);
@@ -863,11 +1232,125 @@ function DesignerView({
863
1232
  return updated;
864
1233
  });
865
1234
  }, []);
866
- const handleFieldDelete = useCallback3((id) => {
1235
+ const handleBulkUpdate = useCallback4((updates) => {
1236
+ setFields((prev) => prev.map((f) => selectedFieldIds.has(f.id) ? { ...f, ...updates } : f));
1237
+ }, [selectedFieldIds]);
1238
+ const handleAlign = useCallback4((type) => {
1239
+ setFields((prev) => {
1240
+ const selected = prev.filter((f) => selectedFieldIds.has(f.id));
1241
+ if (selected.length < 2) return prev;
1242
+ let updates = {};
1243
+ if (type === "left") {
1244
+ const minX = Math.min(...selected.map((f) => f.x));
1245
+ selected.forEach((f) => {
1246
+ updates[f.id] = { x: minX };
1247
+ });
1248
+ } else if (type === "right") {
1249
+ const maxRight = Math.max(...selected.map((f) => f.x + f.width));
1250
+ selected.forEach((f) => {
1251
+ updates[f.id] = { x: maxRight - f.width };
1252
+ });
1253
+ } else if (type === "top") {
1254
+ const minY = Math.min(...selected.map((f) => f.y));
1255
+ selected.forEach((f) => {
1256
+ updates[f.id] = { y: minY };
1257
+ });
1258
+ } else if (type === "bottom") {
1259
+ const maxBottom = Math.max(...selected.map((f) => f.y + f.height));
1260
+ selected.forEach((f) => {
1261
+ updates[f.id] = { y: maxBottom - f.height };
1262
+ });
1263
+ } else if (type === "center-h") {
1264
+ const centers = selected.map((f) => f.x + f.width / 2);
1265
+ const avgCenter = centers.reduce((a, b) => a + b, 0) / centers.length;
1266
+ selected.forEach((f) => {
1267
+ updates[f.id] = { x: avgCenter - f.width / 2 };
1268
+ });
1269
+ } else if (type === "center-v") {
1270
+ const centers = selected.map((f) => f.y + f.height / 2);
1271
+ const avgCenter = centers.reduce((a, b) => a + b, 0) / centers.length;
1272
+ selected.forEach((f) => {
1273
+ updates[f.id] = { y: avgCenter - f.height / 2 };
1274
+ });
1275
+ } else if (type === "distribute-h") {
1276
+ const sorted = [...selected].sort((a, b) => a.x - b.x);
1277
+ const first = sorted[0], last = sorted[sorted.length - 1];
1278
+ const totalSpan = last.x + last.width - first.x;
1279
+ const totalFieldWidth = sorted.reduce((s, f) => s + f.width, 0);
1280
+ const gap = (totalSpan - totalFieldWidth) / (sorted.length - 1);
1281
+ let cx = first.x;
1282
+ sorted.forEach((f) => {
1283
+ updates[f.id] = { x: cx };
1284
+ cx += f.width + gap;
1285
+ });
1286
+ } else if (type === "distribute-v") {
1287
+ const sorted = [...selected].sort((a, b) => a.y - b.y);
1288
+ const first = sorted[0], last = sorted[sorted.length - 1];
1289
+ const totalSpan = last.y + last.height - first.y;
1290
+ const totalFieldHeight = sorted.reduce((s, f) => s + f.height, 0);
1291
+ const gap = (totalSpan - totalFieldHeight) / (sorted.length - 1);
1292
+ let cy = first.y;
1293
+ sorted.forEach((f) => {
1294
+ updates[f.id] = { y: cy };
1295
+ cy += f.height + gap;
1296
+ });
1297
+ }
1298
+ return prev.map((f) => updates[f.id] ? { ...f, ...updates[f.id] } : f);
1299
+ });
1300
+ }, [selectedFieldIds]);
1301
+ const handleFieldDelete = useCallback4((id) => {
1302
+ const field = fields.find((f) => f.id === id);
1303
+ if (field?.locked) return;
867
1304
  setFields((prev) => prev.filter((f) => f.id !== id));
868
- if (selectedFieldId === id) setSelectedFieldId(null);
869
- }, [selectedFieldId]);
870
- const handleAddRole = useCallback3(() => {
1305
+ setSelectedFieldIds((prev) => {
1306
+ const next = new Set(prev);
1307
+ next.delete(id);
1308
+ return next;
1309
+ });
1310
+ }, []);
1311
+ const handleSelectField = useCallback4((id, e) => {
1312
+ if (!id) {
1313
+ setSelectedFieldIds(/* @__PURE__ */ new Set());
1314
+ return;
1315
+ }
1316
+ if (e && (e.metaKey || e.ctrlKey)) {
1317
+ setSelectedFieldIds((prev) => {
1318
+ const next = new Set(prev);
1319
+ if (next.has(id)) next.delete(id);
1320
+ else next.add(id);
1321
+ return next;
1322
+ });
1323
+ } else if (e && e.shiftKey && selectedFieldIds.size > 0) {
1324
+ const lastId = Array.from(selectedFieldIds).pop();
1325
+ const sorted = sortedFields.map((f) => f.id);
1326
+ const a = sorted.indexOf(lastId);
1327
+ const b = sorted.indexOf(id);
1328
+ if (a >= 0 && b >= 0) {
1329
+ const start = Math.min(a, b);
1330
+ const end = Math.max(a, b);
1331
+ const range = sorted.slice(start, end + 1);
1332
+ setSelectedFieldIds((prev) => {
1333
+ const next = new Set(prev);
1334
+ range.forEach((fid) => next.add(fid));
1335
+ return next;
1336
+ });
1337
+ } else {
1338
+ setSelectedFieldIds(/* @__PURE__ */ new Set([id]));
1339
+ }
1340
+ } else {
1341
+ setSelectedFieldIds(/* @__PURE__ */ new Set([id]));
1342
+ }
1343
+ setRightTab("properties");
1344
+ }, [selectedFieldIds]);
1345
+ const handleGroupMove = useCallback4((ids, dx, dy) => {
1346
+ setFieldsSilent((prev) => prev.map((f) => {
1347
+ if (!ids.includes(f.id) || f.locked) return f;
1348
+ const newX = Math.max(0, Math.min(100 - f.width, f.x + dx));
1349
+ const newY = Math.max(0, Math.min(100 - f.height, f.y + dy));
1350
+ return { ...f, x: newX, y: newY };
1351
+ }));
1352
+ }, [setFieldsSilent]);
1353
+ const handleAddRole = useCallback4(() => {
871
1354
  const name = newRoleName.trim();
872
1355
  if (name && !signerRoles.includes(name)) {
873
1356
  setSignerRoles((prev) => [...prev, name]);
@@ -875,12 +1358,12 @@ function DesignerView({
875
1358
  setNewRoleName("");
876
1359
  setIsAddingRole(false);
877
1360
  }, [newRoleName, signerRoles]);
878
- const handleRemoveRole = useCallback3((role) => {
1361
+ const handleRemoveRole = useCallback4((role) => {
879
1362
  setSignerRoles((prev) => prev.filter((r) => r !== role));
880
1363
  setFields((prev) => prev.map((f) => f.assignee === role ? { ...f, assignee: signerRoles[0] } : f));
881
1364
  if (activeRole === role) setActiveRole(signerRoles[0]);
882
1365
  }, [signerRoles, activeRole]);
883
- const handleExport = useCallback3(() => {
1366
+ const handleExport = useCallback4(() => {
884
1367
  const template = {
885
1368
  fields,
886
1369
  signerRoles,
@@ -900,7 +1383,7 @@ function DesignerView({
900
1383
  }
901
1384
  window.parent?.postMessage({ type: "template-saved", template }, "*");
902
1385
  }, [fields, signerRoles, pdfSource, onSave]);
903
- const handlePaletteDragStart = useCallback3((e, type) => {
1386
+ const handlePaletteDragStart = useCallback4((e, type) => {
904
1387
  setDraggingFieldType(type);
905
1388
  e.dataTransfer.setData("application/exeq-field-type", type);
906
1389
  e.dataTransfer.effectAllowed = "copy";
@@ -917,69 +1400,157 @@ function DesignerView({
917
1400
  e.dataTransfer.setDragImage(ghost, 40, 16);
918
1401
  dragGhostRef.current = ghost;
919
1402
  }, [activeRole]);
920
- const handlePaletteDragEnd = useCallback3(() => {
1403
+ const handlePaletteDragEnd = useCallback4(() => {
921
1404
  setDraggingFieldType(null);
922
1405
  if (dragGhostRef.current) {
923
1406
  document.body.removeChild(dragGhostRef.current);
924
1407
  dragGhostRef.current = null;
925
1408
  }
926
1409
  }, []);
927
- const handleDropOnPage = useCallback3((page, x, y, fieldType) => {
1410
+ const handleDropOnPage = useCallback4((page, x, y, fieldType) => {
928
1411
  const field = createField(fieldType, activeRole, page, x, y, fields);
929
1412
  const sticky = lastStylesRef.current[fieldType];
930
1413
  const styledField = sticky ? { ...field, ...sticky } : field;
931
1414
  setFields((prev) => [...prev, styledField]);
932
- setSelectedFieldId(styledField.id);
1415
+ setSelectedFieldIds(/* @__PURE__ */ new Set([styledField.id]));
933
1416
  setRightTab("properties");
934
1417
  }, [activeRole, fields]);
935
1418
  useEffect2(() => {
936
1419
  const handleKeyDown = (e) => {
937
1420
  if (e.key !== "Delete" && e.key !== "Backspace") return;
938
- if (!selectedFieldId) return;
1421
+ if (selectedFieldIds.size === 0) return;
939
1422
  const tag = (document.activeElement?.tagName || "").toLowerCase();
940
1423
  if (tag === "input" || tag === "textarea" || tag === "select") return;
941
1424
  if (document.activeElement?.isContentEditable) return;
942
1425
  e.preventDefault();
943
- if (window.confirm("Delete this field?")) {
944
- handleFieldDelete(selectedFieldId);
945
- }
1426
+ setFields((prev) => prev.filter((f) => !selectedFieldIds.has(f.id) || f.locked));
1427
+ setSelectedFieldIds((prev) => {
1428
+ const remaining = /* @__PURE__ */ new Set();
1429
+ fields.forEach((f) => {
1430
+ if (selectedFieldIds.has(f.id) && f.locked) remaining.add(f.id);
1431
+ });
1432
+ return remaining;
1433
+ });
946
1434
  };
947
1435
  window.addEventListener("keydown", handleKeyDown);
948
1436
  return () => window.removeEventListener("keydown", handleKeyDown);
949
- }, [selectedFieldId, handleFieldDelete]);
1437
+ }, [selectedFieldIds, fields]);
950
1438
  useEffect2(() => {
951
1439
  const handleKeyDown = (e) => {
952
1440
  const tag = (document.activeElement?.tagName || "").toLowerCase();
953
1441
  if (tag === "input" || tag === "textarea" || tag === "select") return;
954
1442
  if (document.activeElement?.isContentEditable) return;
955
1443
  const isMod = e.metaKey || e.ctrlKey;
956
- if (isMod && e.key === "c" && selectedFieldId) {
957
- const field = fields.find((f) => f.id === selectedFieldId);
958
- if (field) {
1444
+ if (isMod && e.key === "a") {
1445
+ e.preventDefault();
1446
+ setSelectedFieldIds(new Set(fields.map((f) => f.id)));
1447
+ return;
1448
+ }
1449
+ if (isMod && e.key === "z" && !e.shiftKey) {
1450
+ e.preventDefault();
1451
+ undoFields();
1452
+ return;
1453
+ }
1454
+ if (isMod && (e.key === "y" || e.key === "z" && e.shiftKey)) {
1455
+ e.preventDefault();
1456
+ redoFields();
1457
+ return;
1458
+ }
1459
+ if (isMod && e.key === "c" && selectedFieldIds.size > 0) {
1460
+ const selected = fields.filter((f) => selectedFieldIds.has(f.id));
1461
+ if (selected.length > 0) {
959
1462
  e.preventDefault();
960
- setClipboardField(field);
1463
+ setClipboardFields(selected);
961
1464
  }
962
1465
  }
963
- if (isMod && e.key === "v" && clipboardField) {
1466
+ if (isMod && e.key === "v" && clipboardFields.length > 0) {
964
1467
  e.preventDefault();
965
- const existingLabels = fields.map((f) => f.label);
966
- const newField = {
967
- ...clipboardField,
968
- id: generateId(),
969
- label: uniqueLabel(clipboardField.label, existingLabels),
970
- x: clipboardField.x + 2,
971
- y: clipboardField.y + 2,
972
- value: ""
973
- };
974
- setFields((prev) => [...prev, newField]);
975
- setSelectedFieldId(newField.id);
1468
+ let existingLabels = fields.map((f) => f.label);
1469
+ const newIds = /* @__PURE__ */ new Set();
1470
+ const newFields = clipboardFields.map((cf) => {
1471
+ const label = uniqueLabel(cf.label, existingLabels);
1472
+ existingLabels.push(label);
1473
+ const id = generateId();
1474
+ newIds.add(id);
1475
+ return { ...cf, id, label, x: cf.x + 2, y: cf.y + 2, value: "" };
1476
+ });
1477
+ setFields((prev) => [...prev, ...newFields]);
1478
+ setSelectedFieldIds(newIds);
976
1479
  setRightTab("properties");
977
1480
  }
978
1481
  };
979
1482
  window.addEventListener("keydown", handleKeyDown);
980
1483
  return () => window.removeEventListener("keydown", handleKeyDown);
981
- }, [selectedFieldId, fields, clipboardField]);
982
- const handleResizeStart = useCallback3((e) => {
1484
+ }, [selectedFieldIds, fields, clipboardFields]);
1485
+ useEffect2(() => {
1486
+ const handleKeyDown = (e) => {
1487
+ const isMod = e.metaKey || e.ctrlKey;
1488
+ if (isMod && (e.key === "=" || e.key === "+")) {
1489
+ e.preventDefault();
1490
+ setZoom((z) => Math.min(3, z + 0.25));
1491
+ }
1492
+ if (isMod && e.key === "-") {
1493
+ e.preventDefault();
1494
+ setZoom((z) => Math.max(0.75, z - 0.25));
1495
+ }
1496
+ if (isMod && e.key === "0") {
1497
+ e.preventDefault();
1498
+ setZoom(1);
1499
+ }
1500
+ if (e.key === " ") {
1501
+ const tag = (document.activeElement?.tagName || "").toLowerCase();
1502
+ if (tag === "input" || tag === "textarea" || tag === "select") return;
1503
+ e.preventDefault();
1504
+ e.stopPropagation();
1505
+ setIsPanning(true);
1506
+ }
1507
+ };
1508
+ const handleKeyUp = (e) => {
1509
+ if (e.key === " ") {
1510
+ e.preventDefault();
1511
+ setIsPanning(false);
1512
+ panRef.current = null;
1513
+ }
1514
+ };
1515
+ window.addEventListener("keydown", handleKeyDown, { capture: true });
1516
+ window.addEventListener("keyup", handleKeyUp, { capture: true });
1517
+ return () => {
1518
+ window.removeEventListener("keydown", handleKeyDown, { capture: true });
1519
+ window.removeEventListener("keyup", handleKeyUp, { capture: true });
1520
+ };
1521
+ }, []);
1522
+ useEffect2(() => {
1523
+ const el = pdfAreaRef.current;
1524
+ if (!el) return;
1525
+ const handleWheel = (e) => {
1526
+ if (e.ctrlKey || e.metaKey) {
1527
+ e.preventDefault();
1528
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
1529
+ setZoom((z) => Math.max(0.75, Math.min(3, z + delta)));
1530
+ }
1531
+ };
1532
+ el.addEventListener("wheel", handleWheel, { passive: false });
1533
+ return () => el.removeEventListener("wheel", handleWheel);
1534
+ }, [pages.length]);
1535
+ const handlePanMouseDown = useCallback4((e) => {
1536
+ if (!isPanning || !pdfAreaRef.current) return;
1537
+ e.preventDefault();
1538
+ panRef.current = {
1539
+ startX: e.clientX,
1540
+ startY: e.clientY,
1541
+ scrollLeft: pdfAreaRef.current.scrollLeft,
1542
+ scrollTop: pdfAreaRef.current.scrollTop
1543
+ };
1544
+ }, [isPanning]);
1545
+ const handlePanMouseMove = useCallback4((e) => {
1546
+ if (!panRef.current || !pdfAreaRef.current) return;
1547
+ pdfAreaRef.current.scrollLeft = panRef.current.scrollLeft - (e.clientX - panRef.current.startX);
1548
+ pdfAreaRef.current.scrollTop = panRef.current.scrollTop - (e.clientY - panRef.current.startY);
1549
+ }, []);
1550
+ const handlePanMouseUp = useCallback4(() => {
1551
+ panRef.current = null;
1552
+ }, []);
1553
+ const handleResizeStart = useCallback4((e) => {
983
1554
  e.preventDefault();
984
1555
  resizingRef.current = true;
985
1556
  const startX = e.clientX;
@@ -1003,8 +1574,8 @@ function DesignerView({
1003
1574
  if (Math.abs(a.y - b.y) > bandThreshold) return a.y - b.y;
1004
1575
  return a.x - b.x;
1005
1576
  });
1006
- const selectedField = fields.find((f) => f.id === selectedFieldId) || null;
1007
- const renderFieldContent = useCallback3((field) => {
1577
+ const selectedField = selectedFieldIds.size === 1 ? fields.find((f) => f.id === Array.from(selectedFieldIds)[0]) || null : null;
1578
+ const renderFieldContent = useCallback4((field) => {
1008
1579
  if (field.type === "blackout" || field.type === "whiteout") {
1009
1580
  return null;
1010
1581
  }
@@ -1030,7 +1601,7 @@ function DesignerView({
1030
1601
  }
1031
1602
  );
1032
1603
  }, [fields]);
1033
- const headerButtons = pages.length > 0 ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1604
+ const headerButtons = pages.length > 0 ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1034
1605
  /* @__PURE__ */ jsxs4("label", { className: "header-btn header-btn-outline", children: [
1035
1606
  "Change PDF",
1036
1607
  /* @__PURE__ */ jsx4("input", { type: "file", accept: ".pdf", onChange: handleFileUpload, hidden: true })
@@ -1108,41 +1679,91 @@ function DesignerView({
1108
1679
  type
1109
1680
  ))
1110
1681
  ] }),
1111
- /* @__PURE__ */ jsxs4("div", { className: "designer-pdf-area", children: [
1112
- loading && /* @__PURE__ */ jsx4("div", { className: "loading-indicator", children: "Loading PDF..." }),
1113
- !pages.length && !loading && /* @__PURE__ */ jsxs4("div", { className: "empty-state", children: [
1114
- /* @__PURE__ */ jsx4("h2", { children: "Open a PDF to get started" }),
1115
- /* @__PURE__ */ jsx4("p", { children: "Select a PDF from your device to begin. Your file stays on your computer \u2014 nothing is uploaded." }),
1116
- /* @__PURE__ */ jsxs4("label", { className: "upload-btn upload-btn-large", children: [
1117
- "Select PDF",
1118
- /* @__PURE__ */ jsx4("input", { type: "file", accept: ".pdf", onChange: handleFileUpload, hidden: true })
1119
- ] })
1120
- ] }),
1121
- pages.length > 0 && /* @__PURE__ */ jsx4(
1122
- PdfViewer,
1123
- {
1124
- pages,
1125
- fields,
1126
- selectedFieldId,
1127
- onSelectField: (id) => {
1128
- setSelectedFieldId(id);
1129
- if (id) setRightTab("properties");
1130
- },
1131
- onFieldMove: handleFieldMove,
1132
- onFieldResize: handleFieldResize,
1133
- onPageClick: handlePageClick,
1134
- onDropField: handleDropOnPage,
1135
- mode: "designer",
1136
- renderFieldContent
1137
- }
1138
- )
1139
- ] }),
1140
- pages.length > 0 && /* @__PURE__ */ jsxs4(Fragment, { children: [
1682
+ /* @__PURE__ */ jsxs4(
1683
+ "div",
1684
+ {
1685
+ className: `designer-pdf-area ${isPanning ? "panning" : ""}`,
1686
+ ref: pdfAreaRef,
1687
+ onMouseDown: isPanning ? handlePanMouseDown : void 0,
1688
+ onMouseMove: isPanning ? handlePanMouseMove : void 0,
1689
+ onMouseUp: isPanning ? handlePanMouseUp : void 0,
1690
+ onMouseLeave: isPanning ? handlePanMouseUp : void 0,
1691
+ children: [
1692
+ loading && /* @__PURE__ */ jsx4("div", { className: "loading-indicator", children: "Loading PDF..." }),
1693
+ !pages.length && !loading && /* @__PURE__ */ jsxs4("div", { className: "empty-state", children: [
1694
+ /* @__PURE__ */ jsx4("h2", { children: "Open a PDF to get started" }),
1695
+ /* @__PURE__ */ jsx4("p", { children: "Select a PDF from your device to begin. Your file stays on your computer \u2014 nothing is uploaded." }),
1696
+ /* @__PURE__ */ jsxs4("label", { className: "upload-btn upload-btn-large", children: [
1697
+ "Select PDF",
1698
+ /* @__PURE__ */ jsx4("input", { type: "file", accept: ".pdf", onChange: handleFileUpload, hidden: true })
1699
+ ] })
1700
+ ] }),
1701
+ pages.length > 0 && /* @__PURE__ */ jsx4(
1702
+ PdfViewer,
1703
+ {
1704
+ pages,
1705
+ fields,
1706
+ selectedFieldIds,
1707
+ onSelectField: handleSelectField,
1708
+ onFieldMove: handleFieldMove,
1709
+ onFieldResize: handleFieldResize,
1710
+ onGroupMove: handleGroupMove,
1711
+ onMoveEnd: snapshotFields,
1712
+ onPageClick: handlePageClick,
1713
+ onDropField: handleDropOnPage,
1714
+ onMarqueeSelect: (ids) => setSelectedFieldIds(new Set(ids)),
1715
+ mode: "designer",
1716
+ renderFieldContent,
1717
+ zoom
1718
+ }
1719
+ )
1720
+ ]
1721
+ }
1722
+ ),
1723
+ pages.length > 0 && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1141
1724
  /* @__PURE__ */ jsx4("div", { className: "panel-resize-handle", onMouseDown: handleResizeStart }),
1142
1725
  /* @__PURE__ */ jsxs4("div", { className: "designer-panel", style: { width: panelWidth }, children: [
1143
- /* @__PURE__ */ jsxs4("div", { className: "signer-role-indicator", children: [
1144
- /* @__PURE__ */ jsx4("span", { className: "signer-role-indicator-label", children: "Editing as" }),
1145
- /* @__PURE__ */ jsx4("strong", { children: activeRole })
1726
+ /* @__PURE__ */ jsxs4("div", { className: "designer-panel-header", children: [
1727
+ /* @__PURE__ */ jsxs4("div", { className: "signer-role-indicator", children: [
1728
+ /* @__PURE__ */ jsx4("span", { className: "signer-role-indicator-label", children: "Editing as" }),
1729
+ /* @__PURE__ */ jsx4("strong", { children: activeRole })
1730
+ ] }),
1731
+ /* @__PURE__ */ jsxs4("div", { className: "zoom-slider", children: [
1732
+ /* @__PURE__ */ jsx4("button", { onClick: () => setZoom((z) => Math.max(0.75, z - 0.25)), disabled: zoom <= 0.75, children: "\u2212" }),
1733
+ /* @__PURE__ */ jsx4(
1734
+ "input",
1735
+ {
1736
+ type: "range",
1737
+ min: "75",
1738
+ max: "300",
1739
+ step: "25",
1740
+ value: Math.round(zoom * 100),
1741
+ onChange: (e) => setZoom(Number(e.target.value) / 100)
1742
+ }
1743
+ ),
1744
+ /* @__PURE__ */ jsx4("button", { onClick: () => setZoom((z) => Math.min(3, z + 0.25)), disabled: zoom >= 3, children: "+" }),
1745
+ /* @__PURE__ */ jsx4("div", { className: "zoom-dropdown-wrapper", children: /* @__PURE__ */ jsxs4(
1746
+ "select",
1747
+ {
1748
+ className: "zoom-dropdown",
1749
+ value: Math.round(zoom * 100),
1750
+ onChange: (e) => setZoom(Number(e.target.value) / 100),
1751
+ children: [
1752
+ /* @__PURE__ */ jsx4("option", { value: "75", children: "75%" }),
1753
+ /* @__PURE__ */ jsx4("option", { value: "100", children: "100%" }),
1754
+ /* @__PURE__ */ jsx4("option", { value: "125", children: "125%" }),
1755
+ /* @__PURE__ */ jsx4("option", { value: "150", children: "150%" }),
1756
+ /* @__PURE__ */ jsx4("option", { value: "200", children: "200%" }),
1757
+ /* @__PURE__ */ jsx4("option", { value: "250", children: "250%" }),
1758
+ /* @__PURE__ */ jsx4("option", { value: "300", children: "300%" }),
1759
+ ![75, 100, 125, 150, 200, 250, 300].includes(Math.round(zoom * 100)) && /* @__PURE__ */ jsxs4("option", { value: Math.round(zoom * 100), children: [
1760
+ Math.round(zoom * 100),
1761
+ "%"
1762
+ ] })
1763
+ ]
1764
+ }
1765
+ ) })
1766
+ ] })
1146
1767
  ] }),
1147
1768
  /* @__PURE__ */ jsxs4("div", { className: "panel-tabs", children: [
1148
1769
  /* @__PURE__ */ jsx4(
@@ -1166,76 +1787,163 @@ function DesignerView({
1166
1787
  )
1167
1788
  ] }),
1168
1789
  /* @__PURE__ */ jsxs4("div", { className: "panel-tab-content", children: [
1169
- rightTab === "properties" && /* @__PURE__ */ jsx4(Fragment, { children: selectedField ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1170
- /* @__PURE__ */ jsx4(
1171
- FieldPropertyPanel,
1172
- {
1173
- field: selectedField,
1174
- signerRoles,
1175
- onUpdate: handleFieldUpdate,
1176
- onDelete: handleFieldDelete
1790
+ rightTab === "properties" && /* @__PURE__ */ jsx4(Fragment2, { children: selectedFieldIds.size > 1 ? /* @__PURE__ */ jsx4(
1791
+ BulkPropertyPanel,
1792
+ {
1793
+ fields: fields.filter((f) => selectedFieldIds.has(f.id)),
1794
+ signerRoles,
1795
+ onUpdate: handleBulkUpdate,
1796
+ onAlign: handleAlign,
1797
+ onDelete: () => {
1798
+ setFields((prev) => prev.filter((f) => !selectedFieldIds.has(f.id) || f.locked));
1799
+ setSelectedFieldIds((prev) => {
1800
+ const remaining = /* @__PURE__ */ new Set();
1801
+ fields.forEach((f) => {
1802
+ if (prev.has(f.id) && f.locked) remaining.add(f.id);
1803
+ });
1804
+ return remaining;
1805
+ });
1177
1806
  }
1178
- ),
1179
- selectedField.type !== "blackout" && selectedField.type !== "whiteout" && /* @__PURE__ */ jsxs4("div", { className: "prefill-section", children: [
1180
- /* @__PURE__ */ jsx4("h4", { children: "Pre-fill Value" }),
1181
- selectedField.type === "signature" || selectedField.type === "initials" ? /* @__PURE__ */ jsx4(
1182
- SignatureCanvas,
1183
- {
1184
- width: panelWidth - 40,
1185
- height: selectedField.type === "initials" ? 100 : 150,
1186
- onSign: (dataUrl) => handleFieldUpdate(selectedField.id, { value: dataUrl }),
1187
- initialValue: selectedField.value,
1188
- inkColor: selectedField.inkColor
1189
- }
1190
- ) : selectedField.type === "checkbox" ? /* @__PURE__ */ jsxs4("label", { className: "panel-checkbox-label", children: [
1191
- /* @__PURE__ */ jsx4(
1192
- "input",
1807
+ }
1808
+ ) : selectedField ? /* @__PURE__ */ jsx4(Fragment2, { children: /* @__PURE__ */ jsx4(
1809
+ FieldPropertyPanel,
1810
+ {
1811
+ field: selectedField,
1812
+ signerRoles,
1813
+ onUpdate: handleFieldUpdate,
1814
+ onDelete: handleFieldDelete,
1815
+ pageSize: pages[selectedField.page] ? { width: pages[selectedField.page].pdfWidth, height: pages[selectedField.page].pdfHeight } : void 0,
1816
+ prefillContent: selectedField.type !== "blackout" && selectedField.type !== "whiteout" ? /* @__PURE__ */ jsxs4("div", { className: "prefill-section", children: [
1817
+ /* @__PURE__ */ jsx4("label", { children: "Pre-fill Value" }),
1818
+ selectedField.type === "signature" || selectedField.type === "initials" ? /* @__PURE__ */ jsx4(
1819
+ SignatureCanvas,
1193
1820
  {
1194
- type: "checkbox",
1195
- checked: selectedField.value === "true",
1196
- onChange: (e) => handleFieldUpdate(selectedField.id, { value: e.target.checked ? "true" : "" })
1821
+ width: panelWidth - 40,
1822
+ height: selectedField.type === "initials" ? 100 : 150,
1823
+ onSign: (dataUrl) => handleFieldUpdate(selectedField.id, { value: dataUrl }),
1824
+ initialValue: selectedField.value,
1825
+ inkColor: selectedField.inkColor
1197
1826
  }
1198
- ),
1199
- "Checked"
1200
- ] }) : /* @__PURE__ */ jsx4(
1201
- "input",
1202
- {
1203
- type: selectedField.textSubtype === "email" ? "email" : selectedField.textSubtype === "number" ? "number" : selectedField.textSubtype === "phone" ? "tel" : selectedField.textSubtype === "date" ? "date" : "text",
1204
- value: selectedField.value,
1205
- onChange: (e) => handleFieldUpdate(selectedField.id, { value: e.target.value }),
1206
- placeholder: `Pre-fill ${selectedField.label}`,
1207
- className: "prefill-input",
1208
- style: { color: selectedField.inkColor || "#000000" }
1209
- }
1210
- )
1211
- ] })
1212
- ] }) : /* @__PURE__ */ jsx4("div", { className: "panel-empty", children: "Click on the PDF to place a field, or select an existing field to edit its properties." }) }),
1213
- rightTab === "fields" && /* @__PURE__ */ jsx4(Fragment, { children: fields.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "panel-empty", children: "No fields yet. Drag a field from the left palette onto the PDF, or click on the PDF to place one." }) : /* @__PURE__ */ jsx4("div", { className: "field-list", children: sortedFields.map((f) => /* @__PURE__ */ jsxs4(
1214
- "div",
1215
- {
1216
- className: `field-list-item ${f.id === selectedFieldId ? "active" : ""}`,
1217
- onClick: () => {
1218
- setSelectedFieldId(f.id);
1219
- setRightTab("properties");
1220
- },
1221
- children: [
1222
- /* @__PURE__ */ jsx4(
1223
- "span",
1827
+ ) : selectedField.type === "checkbox" ? /* @__PURE__ */ jsxs4("label", { className: "panel-checkbox-label", children: [
1828
+ /* @__PURE__ */ jsx4(
1829
+ "input",
1830
+ {
1831
+ type: "checkbox",
1832
+ checked: selectedField.value === "true",
1833
+ onChange: (e) => handleFieldUpdate(selectedField.id, { value: e.target.checked ? "true" : "" })
1834
+ }
1835
+ ),
1836
+ "Checked"
1837
+ ] }) : /* @__PURE__ */ jsx4(
1838
+ "input",
1224
1839
  {
1225
- className: "field-list-dot",
1226
- style: { backgroundColor: getSignerColor(f.assignee) }
1840
+ type: selectedField.textSubtype === "email" ? "email" : selectedField.textSubtype === "number" ? "number" : selectedField.textSubtype === "phone" ? "tel" : selectedField.textSubtype === "date" ? "date" : "text",
1841
+ value: selectedField.value,
1842
+ onChange: (e) => handleFieldUpdate(selectedField.id, { value: e.target.value }),
1843
+ placeholder: `Pre-fill ${selectedField.label}`,
1844
+ className: "prefill-input",
1845
+ style: { color: selectedField.inkColor || "#000000" }
1227
1846
  }
1228
- ),
1229
- /* @__PURE__ */ jsx4("span", { className: "field-list-name", children: f.label }),
1230
- /* @__PURE__ */ jsxs4("span", { className: "field-list-page", children: [
1231
- "p",
1232
- f.page + 1
1233
- ] }),
1234
- f.required && /* @__PURE__ */ jsx4("span", { className: "field-list-required", children: "*" })
1235
- ]
1236
- },
1237
- f.id
1238
- )) }) })
1847
+ )
1848
+ ] }) : void 0
1849
+ }
1850
+ ) }) : /* @__PURE__ */ jsx4("div", { className: "panel-empty", children: "Click on the PDF to place a field, or select an existing field to edit its properties." }) }),
1851
+ rightTab === "fields" && /* @__PURE__ */ jsx4(Fragment2, { children: fields.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "panel-empty", children: "No fields yet. Drag a field from the left palette onto the PDF, or click on the PDF to place one." }) : /* @__PURE__ */ jsx4("div", { className: "field-list", children: sortedFields.map((f, idx) => {
1852
+ const isSel = selectedFieldIds.has(f.id);
1853
+ const isSingleSel = isSel && selectedFieldIds.size === 1;
1854
+ const isTextLike = f.type === "text" || f.type === "dropdown" || f.type === "signed-date";
1855
+ return /* @__PURE__ */ jsxs4(
1856
+ "div",
1857
+ {
1858
+ className: `field-list-item ${isSel ? "active" : ""}`,
1859
+ onClick: (e) => {
1860
+ if (e.target.tagName === "INPUT") return;
1861
+ if (e.metaKey || e.ctrlKey) {
1862
+ setSelectedFieldIds((prev) => {
1863
+ const next = new Set(prev);
1864
+ if (next.has(f.id)) next.delete(f.id);
1865
+ else next.add(f.id);
1866
+ return next;
1867
+ });
1868
+ } else if (e.shiftKey && selectedFieldIds.size > 0) {
1869
+ const ids = sortedFields.map((sf) => sf.id);
1870
+ const lastId = Array.from(selectedFieldIds).pop();
1871
+ const a = ids.indexOf(lastId);
1872
+ const b = ids.indexOf(f.id);
1873
+ if (a >= 0 && b >= 0) {
1874
+ const start = Math.min(a, b);
1875
+ const end = Math.max(a, b);
1876
+ setSelectedFieldIds((prev) => {
1877
+ const next = new Set(prev);
1878
+ ids.slice(start, end + 1).forEach((id) => next.add(id));
1879
+ return next;
1880
+ });
1881
+ }
1882
+ } else {
1883
+ setSelectedFieldIds(/* @__PURE__ */ new Set([f.id]));
1884
+ }
1885
+ },
1886
+ children: [
1887
+ /* @__PURE__ */ jsx4(
1888
+ "span",
1889
+ {
1890
+ className: "field-list-dot",
1891
+ style: { backgroundColor: getSignerColor(f.assignee) }
1892
+ }
1893
+ ),
1894
+ /* @__PURE__ */ jsx4("span", { className: "field-list-name", children: f.label }),
1895
+ f.locked && /* @__PURE__ */ jsx4("span", { className: "field-list-lock", title: "Locked", children: "\u{1F512}" }),
1896
+ /* @__PURE__ */ jsxs4("span", { className: "field-list-page", children: [
1897
+ "p",
1898
+ f.page + 1
1899
+ ] }),
1900
+ f.required && /* @__PURE__ */ jsx4("span", { className: "field-list-required", children: "*" }),
1901
+ isSingleSel && isTextLike && /* @__PURE__ */ jsx4(
1902
+ "input",
1903
+ {
1904
+ className: "field-list-input",
1905
+ type: "text",
1906
+ value: f.value,
1907
+ placeholder: f.placeholder,
1908
+ maxLength: f.maxLength || void 0,
1909
+ autoFocus: true,
1910
+ onChange: (e) => handleFieldUpdate(f.id, { value: e.target.value }),
1911
+ onKeyDown: (e) => {
1912
+ if (e.key === "Tab") {
1913
+ e.preventDefault();
1914
+ const dir = e.shiftKey ? -1 : 1;
1915
+ const nextIdx = idx + dir;
1916
+ if (nextIdx >= 0 && nextIdx < sortedFields.length) {
1917
+ setSelectedFieldIds(/* @__PURE__ */ new Set([sortedFields[nextIdx].id]));
1918
+ }
1919
+ }
1920
+ },
1921
+ onClick: (e) => e.stopPropagation()
1922
+ }
1923
+ ),
1924
+ isSingleSel && f.type === "checkbox" && /* @__PURE__ */ jsx4("label", { className: "field-list-checkbox", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx4(
1925
+ "input",
1926
+ {
1927
+ type: "checkbox",
1928
+ checked: f.value === "true",
1929
+ onChange: (e) => handleFieldUpdate(f.id, { value: e.target.checked ? "true" : "" }),
1930
+ onKeyDown: (e) => {
1931
+ if (e.key === "Tab") {
1932
+ e.preventDefault();
1933
+ const dir = e.shiftKey ? -1 : 1;
1934
+ const nextIdx = idx + dir;
1935
+ if (nextIdx >= 0 && nextIdx < sortedFields.length) {
1936
+ setSelectedFieldIds(/* @__PURE__ */ new Set([sortedFields[nextIdx].id]));
1937
+ }
1938
+ }
1939
+ }
1940
+ }
1941
+ ) })
1942
+ ]
1943
+ },
1944
+ f.id
1945
+ );
1946
+ }) }) })
1239
1947
  ] }),
1240
1948
  /* @__PURE__ */ jsxs4("div", { className: "powered-by", children: [
1241
1949
  "Powered by ",
@@ -1248,7 +1956,7 @@ function DesignerView({
1248
1956
  }
1249
1957
 
1250
1958
  // src/components/pdf-builder/SignerView.tsx
1251
- import { useState as useState5, useCallback as useCallback4, useEffect as useEffect3, useRef as useRef4 } from "react";
1959
+ import { useState as useState7, useCallback as useCallback5, useEffect as useEffect3, useRef as useRef5 } from "react";
1252
1960
 
1253
1961
  // src/utils/pdfFiller.ts
1254
1962
  import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
@@ -1385,7 +2093,7 @@ async function postPdfToCallback(bytes, callbackUrl, filename) {
1385
2093
  }
1386
2094
 
1387
2095
  // src/components/pdf-builder/FieldNavigator.tsx
1388
- import { useState as useState4 } from "react";
2096
+ import { useState as useState6 } from "react";
1389
2097
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1390
2098
  function isFieldFilled(f) {
1391
2099
  if (!f.required) return true;
@@ -1400,7 +2108,7 @@ function FieldNavigator({
1400
2108
  onComplete,
1401
2109
  completeLabel = "Complete"
1402
2110
  }) {
1403
- const [showIncomplete, setShowIncomplete] = useState4(false);
2111
+ const [showIncomplete, setShowIncomplete] = useState6(false);
1404
2112
  const currentIndex = fields.findIndex((f) => f.id === currentFieldId);
1405
2113
  const hasPrev = currentIndex > 0;
1406
2114
  const hasNext = currentIndex < fields.length - 1;
@@ -1565,7 +2273,7 @@ function resolveAllFormulas(fields, customTransforms) {
1565
2273
  }
1566
2274
 
1567
2275
  // src/components/pdf-builder/SignerView.tsx
1568
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2276
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1569
2277
  function SignerView({
1570
2278
  apiKey,
1571
2279
  initialPdfUrl,
@@ -1576,7 +2284,12 @@ function SignerView({
1576
2284
  initialValues,
1577
2285
  submitLabel,
1578
2286
  signerOrder: signerOrderProp,
1579
- transforms
2287
+ transforms,
2288
+ onChange,
2289
+ onSignerComplete,
2290
+ includeAuditTrail,
2291
+ exportFormat,
2292
+ onExport
1580
2293
  } = {}) {
1581
2294
  if (!isValidApiKey(apiKey)) {
1582
2295
  return /* @__PURE__ */ jsx6("div", { className: "signer-layout", children: /* @__PURE__ */ jsxs6("div", { className: "empty-state", children: [
@@ -1588,23 +2301,24 @@ function SignerView({
1588
2301
  ] })
1589
2302
  ] }) });
1590
2303
  }
1591
- const [pages, setPages] = useState5([]);
1592
- const [fields, setFields] = useState5([]);
1593
- const [selectedFieldId, setSelectedFieldId] = useState5(null);
1594
- const [signerRoles, setSignerRoles] = useState5([]);
1595
- const [currentSignerIndex, setCurrentSignerIndex] = useState5(() => {
2304
+ const [pages, setPages] = useState7([]);
2305
+ const [fields, setFields] = useState7([]);
2306
+ const [selectedFieldId, setSelectedFieldId] = useState7(null);
2307
+ const [signerRoles, setSignerRoles] = useState7([]);
2308
+ const [currentSignerIndex, setCurrentSignerIndex] = useState7(() => {
1596
2309
  if (initialSigner && signerOrderProp) {
1597
2310
  const idx = signerOrderProp.indexOf(initialSigner);
1598
2311
  return idx >= 0 ? idx : 0;
1599
2312
  }
1600
2313
  return 0;
1601
2314
  });
1602
- const initializedRef = useRef4(false);
1603
- const [loading, setLoading] = useState5(false);
1604
- const [submitting, setSubmitting] = useState5(false);
1605
- const [pdfSource, setPdfSource] = useState5(null);
1606
- const [callbackUrl, setCallbackUrl] = useState5(initialCallbackUrl || "");
1607
- const containerRef = useRef4(null);
2315
+ const initializedRef = useRef5(false);
2316
+ const auditLogRef = useRef5([]);
2317
+ const [loading, setLoading] = useState7(false);
2318
+ const [submitting, setSubmitting] = useState7(false);
2319
+ const [pdfSource, setPdfSource] = useState7(null);
2320
+ const [callbackUrl, setCallbackUrl] = useState7(initialCallbackUrl || "");
2321
+ const containerRef = useRef5(null);
1608
2322
  const signerOrder = signerOrderProp || (signerRoles.length > 0 ? [...signerRoles.filter((r) => r !== "Sender"), ...signerRoles.filter((r) => r === "Sender")] : [initialSigner || "Signer 1"]);
1609
2323
  const signer = signerOrder[currentSignerIndex] || signerOrder[0] || "Signer 1";
1610
2324
  const isLastSigner = currentSignerIndex >= signerOrder.length - 1;
@@ -1694,7 +2408,7 @@ function SignerView({
1694
2408
  window.addEventListener("message", handleMessage);
1695
2409
  return () => window.removeEventListener("message", handleMessage);
1696
2410
  }, [initialTemplate, initialPdfUrl]);
1697
- const loadPdf = useCallback4(async (source) => {
2411
+ const loadPdf = useCallback5(async (source) => {
1698
2412
  setLoading(true);
1699
2413
  try {
1700
2414
  const rendered = await renderPdfPages(source);
@@ -1714,13 +2428,22 @@ function SignerView({
1714
2428
  });
1715
2429
  const selectedField = fields.find((f) => f.id === selectedFieldId) || null;
1716
2430
  const isFieldEditable = selectedField ? selectedField.assignee === signer && selectedField.type !== "blackout" && selectedField.type !== "whiteout" && !selectedField.formula : false;
1717
- const handleFieldUpdate = useCallback4((id, value) => {
2431
+ const handleFieldUpdate = useCallback5((id, value) => {
1718
2432
  setFields((prev) => prev.map((f) => f.id === id ? { ...f, value } : f));
1719
2433
  }, []);
1720
- const handleFieldPropertyUpdate = useCallback4((id, updates) => {
2434
+ const handleFieldPropertyUpdate = useCallback5((id, updates) => {
1721
2435
  setFields((prev) => prev.map((f) => f.id === id ? { ...f, ...updates } : f));
1722
2436
  }, []);
1723
- const handleNavigate = useCallback4((fieldId) => {
2437
+ const fieldValuesKey = fields.map((f) => `${f.id}:${f.value}`).join("|");
2438
+ useEffect3(() => {
2439
+ if (!onChange) return;
2440
+ const values = {};
2441
+ fields.forEach((f) => {
2442
+ if (f.type !== "blackout" && f.type !== "whiteout") values[f.label] = f.value || "";
2443
+ });
2444
+ onChange(values);
2445
+ }, [fieldValuesKey]);
2446
+ const handleNavigate = useCallback5((fieldId) => {
1724
2447
  setSelectedFieldId(fieldId);
1725
2448
  const field = fields.find((f) => f.id === fieldId);
1726
2449
  if (field && containerRef.current) {
@@ -1733,10 +2456,23 @@ function SignerView({
1733
2456
  const allRequiredFilled = editableFields.every((f) => {
1734
2457
  if (!f.required) return true;
1735
2458
  if (f.type === "checkbox") return true;
1736
- return !!f.value;
2459
+ if (!f.value) return false;
2460
+ if (f.minLength && f.value.length < f.minLength) return false;
2461
+ return true;
1737
2462
  });
1738
- const handleAdvanceOrSubmit = useCallback4(async () => {
2463
+ const getFieldValues = useCallback5(() => {
2464
+ const values = {};
2465
+ fields.forEach((f) => {
2466
+ if (f.type !== "blackout" && f.type !== "whiteout") values[f.label] = f.value || "";
2467
+ });
2468
+ return values;
2469
+ }, [fields]);
2470
+ const handleAdvanceOrSubmit = useCallback5(async () => {
1739
2471
  if (!allRequiredFilled) return;
2472
+ auditLogRef.current.push({ signer, completedAt: (/* @__PURE__ */ new Date()).toISOString() });
2473
+ if (onSignerComplete) {
2474
+ onSignerComplete(signer, getFieldValues());
2475
+ }
1740
2476
  if (!isLastSigner) {
1741
2477
  setCurrentSignerIndex((prev) => prev + 1);
1742
2478
  setSelectedFieldId(null);
@@ -1750,8 +2486,40 @@ function SignerView({
1750
2486
  setSubmitting(true);
1751
2487
  try {
1752
2488
  const finalFields = resolveAllFormulas(fields, transforms);
1753
- const pdfBytes = await generateFilledPdf(pdfSource, finalFields);
2489
+ let pdfBytes;
2490
+ if (includeAuditTrail) {
2491
+ const { PDFDocument: PDFDocument2, StandardFonts: StandardFonts2, rgb: rgb2 } = await import("pdf-lib");
2492
+ const basePdf = await generateFilledPdf(pdfSource, finalFields);
2493
+ const pdfDoc = await PDFDocument2.load(basePdf);
2494
+ const font = await pdfDoc.embedFont(StandardFonts2.Helvetica);
2495
+ const boldFont = await pdfDoc.embedFont(StandardFonts2.HelveticaBold);
2496
+ const auditPage = pdfDoc.addPage([612, 792]);
2497
+ let cy = 742;
2498
+ auditPage.drawText("Signing Audit Trail", { x: 50, y: cy, size: 18, font: boldFont, color: rgb2(0, 0, 0) });
2499
+ cy -= 30;
2500
+ auditPage.drawText(`Document completed: ${(/* @__PURE__ */ new Date()).toLocaleString()}`, { x: 50, y: cy, size: 10, font, color: rgb2(0.4, 0.4, 0.4) });
2501
+ cy -= 30;
2502
+ for (const entry of auditLogRef.current) {
2503
+ auditPage.drawText(`${entry.signer}`, { x: 50, y: cy, size: 12, font: boldFont, color: rgb2(0, 0, 0) });
2504
+ auditPage.drawText(`Completed: ${new Date(entry.completedAt).toLocaleString()}`, { x: 200, y: cy, size: 10, font, color: rgb2(0.3, 0.3, 0.3) });
2505
+ cy -= 22;
2506
+ }
2507
+ pdfBytes = await pdfDoc.save();
2508
+ } else {
2509
+ pdfBytes = await generateFilledPdf(pdfSource, finalFields);
2510
+ }
1754
2511
  const blob = new Blob([pdfBytes.slice().buffer], { type: "application/pdf" });
2512
+ if (onExport && exportFormat) {
2513
+ const values = getFieldValues();
2514
+ if (exportFormat === "json") {
2515
+ onExport(JSON.stringify(values, null, 2), "json");
2516
+ } else {
2517
+ const headers = Object.keys(values);
2518
+ const row = headers.map((h) => `"${(values[h] || "").replace(/"/g, '""')}"`);
2519
+ onExport(`${headers.join(",")}
2520
+ ${row.join(",")}`, "csv");
2521
+ }
2522
+ }
1755
2523
  if (callbackUrl) {
1756
2524
  await postPdfToCallback(pdfBytes, callbackUrl, "signed-document.pdf");
1757
2525
  }
@@ -1767,8 +2535,8 @@ function SignerView({
1767
2535
  } finally {
1768
2536
  setSubmitting(false);
1769
2537
  }
1770
- }, [pdfSource, fields, callbackUrl, allRequiredFilled, onComplete, isLastSigner]);
1771
- const renderFieldContent = useCallback4((field) => {
2538
+ }, [pdfSource, fields, callbackUrl, allRequiredFilled, onComplete, isLastSigner, signer, onSignerComplete, includeAuditTrail, exportFormat, onExport, getFieldValues, transforms]);
2539
+ const renderFieldContent = useCallback5((field) => {
1772
2540
  if (field.type === "blackout" || field.type === "whiteout") {
1773
2541
  return null;
1774
2542
  }
@@ -1872,8 +2640,8 @@ function SignerView({
1872
2640
  {
1873
2641
  pages,
1874
2642
  fields,
1875
- selectedFieldId,
1876
- onSelectField: setSelectedFieldId,
2643
+ selectedFieldIds: new Set(selectedFieldId ? [selectedFieldId] : []),
2644
+ onSelectField: (id) => setSelectedFieldId(id),
1877
2645
  mode: "signer",
1878
2646
  currentSigner: signer,
1879
2647
  renderFieldContent
@@ -1905,7 +2673,7 @@ function SignerView({
1905
2673
  initialValue: selectedField.value
1906
2674
  }
1907
2675
  ),
1908
- (selectedField.type === "text" || selectedField.type === "dropdown") && /* @__PURE__ */ jsxs6(Fragment2, { children: [
2676
+ (selectedField.type === "text" || selectedField.type === "dropdown") && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1909
2677
  selectedField.type === "dropdown" || selectedField.options && selectedField.options.length > 0 ? /* @__PURE__ */ jsxs6(
1910
2678
  "select",
1911
2679
  {
@@ -1986,7 +2754,7 @@ function SignerView({
1986
2754
  }
1987
2755
 
1988
2756
  // src/components/pdf-builder/SignerRoleSelector.tsx
1989
- import { useState as useState6 } from "react";
2757
+ import { useState as useState8 } from "react";
1990
2758
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1991
2759
  function SignerRoleSelector({
1992
2760
  roles,
@@ -1995,8 +2763,8 @@ function SignerRoleSelector({
1995
2763
  onAddRole,
1996
2764
  onRemoveRole
1997
2765
  }) {
1998
- const [isAdding, setIsAdding] = useState6(false);
1999
- const [newRoleName, setNewRoleName] = useState6("");
2766
+ const [isAdding, setIsAdding] = useState8(false);
2767
+ const [newRoleName, setNewRoleName] = useState8("");
2000
2768
  const handleAdd = () => {
2001
2769
  if (newRoleName.trim()) {
2002
2770
  onAddRole(newRoleName.trim());