formlift 0.1.0 → 0.2.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.
@@ -1,30 +1,29 @@
1
1
  import {
2
2
  ELEMENT_TOOLS,
3
- NO_FILTER,
4
3
  boxStyle,
5
- choicesOf,
4
+ builtinFilters,
6
5
  deletePage,
7
6
  detectDocument,
8
7
  duplicate,
9
8
  duplicateElement,
9
+ filterQuestions,
10
10
  fitInPage,
11
11
  fitTextWidth,
12
12
  insertBlankPage,
13
- matches,
14
13
  moveBy,
15
14
  movePage,
16
15
  newElement,
17
16
  newField,
18
- pickedOf,
19
17
  reorder,
20
18
  replaceLine,
21
19
  resizeBy,
20
+ resolveFilters,
22
21
  snap,
23
22
  toLines,
24
23
  toPage,
25
24
  toQuestions,
26
25
  withField
27
- } from "../chunk-BQAVPA7Q.js";
26
+ } from "../chunk-NUKLGHKS.js";
28
27
  import {
29
28
  FIELD_TYPES,
30
29
  FONTS,
@@ -56,14 +55,10 @@ import {
56
55
  Undo2,
57
56
  Wand2
58
57
  } from "lucide-react";
59
- import {
60
- useCallback as useCallback2,
61
- useEffect as useEffect3,
62
- useImperativeHandle,
63
- useMemo,
64
- useRef as useRef4,
65
- useState as useState7
66
- } from "react";
58
+ import { useEffect as useEffect5, useImperativeHandle, useRef as useRef6, useState as useState9 } from "react";
59
+
60
+ // src/react/ui/slots.tsx
61
+ import { createContext, useContext } from "react";
67
62
 
68
63
  // src/react/ui/Button.tsx
69
64
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -103,10 +98,147 @@ function Button({
103
98
  );
104
99
  }
105
100
 
101
+ // src/react/ui/Checkbox.tsx
102
+ import { jsx as jsx2 } from "react/jsx-runtime";
103
+ function Checkbox({
104
+ label,
105
+ className = "",
106
+ ...rest
107
+ }) {
108
+ return /* @__PURE__ */ jsx2(
109
+ "input",
110
+ {
111
+ type: "checkbox",
112
+ "aria-label": label,
113
+ className: `h-[18px] w-[18px] flex-none cursor-pointer accent-primary ${className}`,
114
+ ...rest
115
+ }
116
+ );
117
+ }
118
+
119
+ // src/react/ui/Chip.tsx
120
+ import { X } from "lucide-react";
121
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
122
+ function Chip({
123
+ label,
124
+ count,
125
+ active,
126
+ onClick
127
+ }) {
128
+ return /* @__PURE__ */ jsxs2(
129
+ "button",
130
+ {
131
+ type: "button",
132
+ "aria-pressed": !!active,
133
+ onClick,
134
+ className: `inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-[12.5px] whitespace-nowrap transition-colors ${active ? "border-primary bg-primary-tint font-semibold text-primary" : "border-line-strong bg-white text-ink-2 hover:border-faint hover:bg-surface"}`,
135
+ children: [
136
+ label,
137
+ count != null && /* @__PURE__ */ jsx3(
138
+ "span",
139
+ {
140
+ className: `rounded-full px-1.5 text-[11px] font-semibold ${active ? "bg-primary text-white" : "bg-hover text-muted"}`,
141
+ children: count
142
+ }
143
+ )
144
+ ]
145
+ }
146
+ );
147
+ }
148
+ function ChipGroup({
149
+ title,
150
+ options,
151
+ value,
152
+ onChange
153
+ }) {
154
+ return /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": title, className: "flex flex-wrap items-center gap-1.5", children: [
155
+ title && /* @__PURE__ */ jsx3("span", { className: "mr-1 min-w-28 text-[11.5px] max-sm:basis-full font-semibold tracking-wide text-muted uppercase", children: title }),
156
+ options.map((o) => /* @__PURE__ */ jsx3(
157
+ Chip,
158
+ {
159
+ label: o.label,
160
+ count: o.count,
161
+ active: value === o.value,
162
+ onClick: () => onChange(value === o.value ? null : o.value)
163
+ },
164
+ o.value
165
+ ))
166
+ ] });
167
+ }
168
+ function RemovableChip({ label, onRemove }) {
169
+ return /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary-tint py-0.5 pr-1 pl-2.5 text-[12px] font-medium text-primary", children: [
170
+ label,
171
+ /* @__PURE__ */ jsx3(
172
+ "button",
173
+ {
174
+ type: "button",
175
+ "aria-label": `Remove ${label}`,
176
+ onClick: onRemove,
177
+ className: "inline-flex h-4.5 w-4.5 cursor-pointer items-center justify-center rounded-full hover:bg-primary hover:text-white",
178
+ children: /* @__PURE__ */ jsx3(X, { size: 12 })
179
+ }
180
+ )
181
+ ] });
182
+ }
183
+
184
+ // src/react/ui/FormField.tsx
185
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
186
+ var inputClass = "w-full rounded-md border border-line-strong bg-white px-3 py-2 text-[13.5px] outline-none focus:border-primary";
187
+ function FormField({
188
+ label,
189
+ required,
190
+ error,
191
+ children
192
+ }) {
193
+ return /* @__PURE__ */ jsxs3("label", { className: "block text-[13.5px]", children: [
194
+ /* @__PURE__ */ jsxs3("span", { className: "mb-1 block font-semibold", children: [
195
+ label,
196
+ " ",
197
+ required && /* @__PURE__ */ jsx4("span", { className: "text-danger", children: "*" })
198
+ ] }),
199
+ children,
200
+ error && /* @__PURE__ */ jsx4("small", { className: "text-danger", children: error })
201
+ ] });
202
+ }
203
+ var TextInput = ({ className = "", ...rest }) => /* @__PURE__ */ jsx4("input", { className: `${inputClass} ${className}`, ...rest });
204
+ var Select = ({ className = "", ...rest }) => /* @__PURE__ */ jsx4(
205
+ "select",
206
+ {
207
+ className: `rounded-md border border-line-strong bg-white px-2.5 py-1.5 text-[13px] outline-none ${className}`,
208
+ ...rest
209
+ }
210
+ );
211
+ var TextArea = ({ className = "", ...rest }) => /* @__PURE__ */ jsx4("textarea", { className: `${inputClass} resize-none ${className}`, ...rest });
212
+
213
+ // src/react/ui/IconButton.tsx
214
+ import { jsx as jsx5 } from "react/jsx-runtime";
215
+ function IconButton({
216
+ title,
217
+ icon: Icon,
218
+ danger,
219
+ iconSize = 18,
220
+ light,
221
+ className = "",
222
+ ...rest
223
+ }) {
224
+ const tone = light ? "text-white opacity-90 hover:bg-white/15 hover:opacity-100" : `text-muted hover:bg-line-soft ${danger ? "hover:text-danger" : "hover:text-primary"}`;
225
+ return /* @__PURE__ */ jsx5(
226
+ "button",
227
+ {
228
+ type: "button",
229
+ title,
230
+ "aria-label": title,
231
+ className: `inline-flex cursor-pointer items-center justify-center rounded p-1.5 transition disabled:cursor-not-allowed disabled:opacity-40 ${tone} ${className}`,
232
+ ...rest,
233
+ children: /* @__PURE__ */ jsx5(Icon, { size: iconSize })
234
+ }
235
+ );
236
+ }
237
+
106
238
  // src/react/ui/Modal.tsx
107
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
239
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
108
240
  function Modal({ children, onClose, width }) {
109
- return /* @__PURE__ */ jsx2("div", { className: "fixed inset-0 z-[1200] flex items-center justify-center bg-[rgba(15,23,42,0.45)]", onClick: onClose, children: /* @__PURE__ */ jsx2(
241
+ return /* @__PURE__ */ jsx6("div", { className: "fixed inset-0 z-[1200] flex items-center justify-center bg-[rgba(15,23,42,0.45)]", onClick: onClose, children: /* @__PURE__ */ jsx6(
110
242
  "div",
111
243
  {
112
244
  role: "dialog",
@@ -121,9 +253,9 @@ var ModalHeader = ({
121
253
  title,
122
254
  onClose,
123
255
  disabled
124
- }) => /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between border-b border-line px-5 py-4", children: [
125
- /* @__PURE__ */ jsx2("h3", { className: "m-0 text-[17px] font-semibold", children: title }),
126
- /* @__PURE__ */ jsx2(
256
+ }) => /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between border-b border-line px-5 py-4", children: [
257
+ /* @__PURE__ */ jsx6("h3", { className: "m-0 text-[17px] font-semibold", children: title }),
258
+ /* @__PURE__ */ jsx6(
127
259
  "button",
128
260
  {
129
261
  type: "button",
@@ -135,8 +267,8 @@ var ModalHeader = ({
135
267
  }
136
268
  )
137
269
  ] });
138
- var ModalBody = ({ children }) => /* @__PURE__ */ jsx2("div", { className: "space-y-4 px-5 py-4 text-[13.5px]", children });
139
- var ModalFooter = ({ children }) => /* @__PURE__ */ jsx2("div", { className: "flex justify-end gap-2 border-t border-line px-5 py-3", children });
270
+ var ModalBody = ({ children }) => /* @__PURE__ */ jsx6("div", { className: "space-y-4 px-5 py-4 text-[13.5px]", children });
271
+ var ModalFooter = ({ children }) => /* @__PURE__ */ jsx6("div", { className: "flex justify-end gap-2 border-t border-line px-5 py-3", children });
140
272
  function ConfirmModal({
141
273
  children,
142
274
  confirmLabel = "Yes",
@@ -145,57 +277,91 @@ function ConfirmModal({
145
277
  onConfirm,
146
278
  onCancel
147
279
  }) {
148
- return /* @__PURE__ */ jsxs2(Modal, { onClose: onCancel, width: "w-[420px]", children: [
149
- /* @__PURE__ */ jsx2("div", { className: "px-6 pt-7 pb-6 text-sm", children }),
150
- /* @__PURE__ */ jsxs2(ModalFooter, { children: [
151
- /* @__PURE__ */ jsx2(Button, { variant: "secondary", onClick: onCancel, children: cancelLabel }),
152
- /* @__PURE__ */ jsx2(Button, { variant: danger ? "danger" : "primary", onClick: onConfirm, children: confirmLabel })
280
+ return /* @__PURE__ */ jsxs4(Modal, { onClose: onCancel, width: "w-[420px]", children: [
281
+ /* @__PURE__ */ jsx6("div", { className: "px-6 pt-7 pb-6 text-sm", children }),
282
+ /* @__PURE__ */ jsxs4(ModalFooter, { children: [
283
+ /* @__PURE__ */ jsx6(Button, { variant: "secondary", onClick: onCancel, children: cancelLabel }),
284
+ /* @__PURE__ */ jsx6(Button, { variant: danger ? "danger" : "primary", onClick: onConfirm, children: confirmLabel })
153
285
  ] })
154
286
  ] });
155
287
  }
156
288
 
157
- // src/react/ui/IconButton.tsx
158
- import { jsx as jsx3 } from "react/jsx-runtime";
159
- function IconButton({
160
- title,
161
- icon: Icon,
162
- danger,
163
- iconSize = 18,
164
- light,
165
- className = "",
166
- ...rest
167
- }) {
168
- const tone = light ? "text-white opacity-90 hover:bg-white/15 hover:opacity-100" : `text-muted hover:bg-line-soft ${danger ? "hover:text-danger" : "hover:text-primary"}`;
169
- return /* @__PURE__ */ jsx3(
170
- "button",
171
- {
172
- type: "button",
173
- title,
174
- "aria-label": title,
175
- className: `inline-flex cursor-pointer items-center justify-center rounded p-1.5 transition disabled:cursor-not-allowed disabled:opacity-40 ${tone} ${className}`,
176
- ...rest,
177
- children: /* @__PURE__ */ jsx3(Icon, { size: iconSize })
178
- }
179
- );
180
- }
181
-
182
289
  // src/react/ui/Panel.tsx
183
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
184
- var PanelHeading = ({ children, actions }) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between bg-heading px-3.5 py-[11px] text-[13px] font-bold tracking-[0.02em] text-white", children: [
290
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
291
+ var PanelHeading = ({ children, actions }) => /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between bg-heading px-3.5 py-[11px] text-[13px] font-bold tracking-[0.02em] text-white", children: [
185
292
  children,
186
- actions && /* @__PURE__ */ jsx4("span", { className: "flex items-center gap-1 tracking-normal", children: actions })
293
+ actions && /* @__PURE__ */ jsx7("span", { className: "flex items-center gap-1 tracking-normal", children: actions })
187
294
  ] });
188
- var EmptyText = ({ children, className = "" }) => /* @__PURE__ */ jsx4("div", { className: `m-0 px-3.5 py-3 text-[13px] text-muted ${className}`, children });
295
+ var EmptyText = ({ children, className = "" }) => /* @__PURE__ */ jsx7("div", { className: `m-0 px-3.5 py-3 text-[13px] text-muted ${className}`, children });
296
+
297
+ // src/react/ui/Popover.tsx
298
+ import { useRef, useState } from "react";
299
+
300
+ // src/react/ui/useOutsideClose.ts
301
+ import { useEffect } from "react";
302
+ function useOutsideClose(ref, open, onClose) {
303
+ useEffect(() => {
304
+ if (!open) return;
305
+ const onClick = (e) => ref.current && e.composedPath().includes(ref.current) || onClose();
306
+ const onKey = (e) => e.key === "Escape" && onClose();
307
+ document.addEventListener("mousedown", onClick);
308
+ document.addEventListener("keydown", onKey);
309
+ return () => {
310
+ document.removeEventListener("mousedown", onClick);
311
+ document.removeEventListener("keydown", onKey);
312
+ };
313
+ }, [ref, open, onClose]);
314
+ }
315
+
316
+ // src/react/ui/Popover.tsx
317
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
318
+ function Popover({
319
+ label,
320
+ icon: Icon,
321
+ badge,
322
+ align = "left",
323
+ children
324
+ }) {
325
+ const [open, setOpen] = useState(false);
326
+ const box2 = useRef(null);
327
+ useOutsideClose(box2, open, () => setOpen(false));
328
+ return /* @__PURE__ */ jsxs6("div", { ref: box2, className: "relative", children: [
329
+ /* @__PURE__ */ jsxs6(
330
+ "button",
331
+ {
332
+ type: "button",
333
+ "aria-expanded": open,
334
+ "aria-haspopup": "dialog",
335
+ onClick: () => setOpen((o) => !o),
336
+ className: `inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-3 py-1.5 text-[13px] font-medium ${open || badge ? "border-primary bg-primary-soft text-primary" : "border-line-strong bg-white text-ink-2 hover:border-faint"}`,
337
+ children: [
338
+ Icon && /* @__PURE__ */ jsx8(Icon, { size: 15 }),
339
+ label,
340
+ !!badge && /* @__PURE__ */ jsx8("span", { className: "inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[11px] font-bold text-white", children: badge })
341
+ ]
342
+ }
343
+ ),
344
+ open && /* @__PURE__ */ jsx8(
345
+ "div",
346
+ {
347
+ role: "dialog",
348
+ "aria-label": label,
349
+ className: `absolute z-30 mt-1.5 w-[min(560px,calc(100vw-24px))] max-sm:fixed max-sm:inset-x-3 max-sm:max-h-[70dvh] max-sm:w-auto max-sm:overflow-y-auto rounded-lg border border-line bg-white p-4 shadow-xl ${align === "right" ? "right-0" : "left-0"}`,
350
+ children
351
+ }
352
+ )
353
+ ] });
354
+ }
189
355
 
190
356
  // src/react/ui/SegmentedTabs.tsx
191
- import { jsx as jsx5 } from "react/jsx-runtime";
357
+ import { jsx as jsx9 } from "react/jsx-runtime";
192
358
  function SegmentedTabs({
193
359
  options,
194
360
  value,
195
361
  onChange,
196
362
  size = "md"
197
363
  }) {
198
- return /* @__PURE__ */ jsx5("div", { role: "tablist", className: "inline-flex rounded-full border border-brand p-0.5", children: options.map((o) => /* @__PURE__ */ jsx5(
364
+ return /* @__PURE__ */ jsx9("div", { role: "tablist", className: "inline-flex rounded-full border border-brand p-0.5", children: options.map((o) => /* @__PURE__ */ jsx9(
199
365
  "button",
200
366
  {
201
367
  type: "button",
@@ -209,59 +375,163 @@ function SegmentedTabs({
209
375
  )) });
210
376
  }
211
377
 
212
- // src/react/ui/Checkbox.tsx
213
- import { jsx as jsx6 } from "react/jsx-runtime";
214
- function Checkbox({
215
- label,
216
- className = "",
217
- ...rest
218
- }) {
219
- return /* @__PURE__ */ jsx6(
220
- "input",
221
- {
222
- type: "checkbox",
223
- "aria-label": label,
224
- className: `h-[18px] w-[18px] flex-none cursor-pointer accent-primary ${className}`,
225
- ...rest
226
- }
227
- );
228
- }
229
-
230
- // src/react/ui/FormField.tsx
231
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
232
- var inputClass = "w-full rounded-md border border-line-strong bg-white px-3 py-2 text-[13.5px] outline-none focus:border-primary";
233
- function FormField({
234
- label,
235
- required,
236
- error,
237
- children
378
+ // src/react/ui/SignaturePad.tsx
379
+ import { useRef as useRef2, useState as useState2 } from "react";
380
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
381
+ function SignaturePad({
382
+ onSave,
383
+ onCancel,
384
+ maxWidth = 600
238
385
  }) {
239
- return /* @__PURE__ */ jsxs4("label", { className: "block text-[13.5px]", children: [
240
- /* @__PURE__ */ jsxs4("span", { className: "mb-1 block font-semibold", children: [
241
- label,
242
- " ",
243
- required && /* @__PURE__ */ jsx7("span", { className: "text-danger", children: "*" })
244
- ] }),
245
- children,
246
- error && /* @__PURE__ */ jsx7("small", { className: "text-danger", children: error })
386
+ const canvas = useRef2(null);
387
+ const last = useRef2(null);
388
+ const [empty, setEmpty] = useState2(true);
389
+ const point = (e) => {
390
+ const r = e.currentTarget.getBoundingClientRect();
391
+ return {
392
+ x: (e.clientX - r.left) / r.width * e.currentTarget.width,
393
+ y: (e.clientY - r.top) / r.height * e.currentTarget.height
394
+ };
395
+ };
396
+ const draw = (e) => {
397
+ const ctx = canvas.current?.getContext("2d");
398
+ if (!ctx || !last.current) return;
399
+ const p = point(e);
400
+ ctx.lineWidth = 3;
401
+ ctx.lineCap = "round";
402
+ ctx.strokeStyle = "#111827";
403
+ ctx.beginPath();
404
+ ctx.moveTo(last.current.x, last.current.y);
405
+ ctx.lineTo(p.x, p.y);
406
+ ctx.stroke();
407
+ last.current = p;
408
+ setEmpty(false);
409
+ };
410
+ const clear = () => {
411
+ const c = canvas.current;
412
+ c?.getContext("2d")?.clearRect(0, 0, c.width, c.height);
413
+ setEmpty(true);
414
+ };
415
+ const save = () => {
416
+ const c = canvas.current;
417
+ if (!c) return;
418
+ const scale = Math.min(1, maxWidth / c.width);
419
+ const out = document.createElement("canvas");
420
+ out.width = Math.round(c.width * scale);
421
+ out.height = Math.round(c.height * scale);
422
+ out.getContext("2d")?.drawImage(c, 0, 0, out.width, out.height);
423
+ onSave(out.toDataURL("image/png"));
424
+ };
425
+ return /* @__PURE__ */ jsxs7("div", { className: "space-y-3", children: [
426
+ /* @__PURE__ */ jsx10(
427
+ "canvas",
428
+ {
429
+ ref: canvas,
430
+ width: 800,
431
+ height: 260,
432
+ "aria-label": "Signature drawing area",
433
+ className: "h-[200px] w-full touch-none rounded-md border border-dashed border-line-strong bg-surface",
434
+ onPointerDown: (e) => {
435
+ e.currentTarget.setPointerCapture(e.pointerId);
436
+ last.current = point(e);
437
+ },
438
+ onPointerMove: draw,
439
+ onPointerUp: () => last.current = null
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsxs7("div", { className: "flex justify-end gap-2", children: [
443
+ /* @__PURE__ */ jsx10(Button, { variant: "secondary", onClick: clear, children: "Clear" }),
444
+ /* @__PURE__ */ jsx10(Button, { variant: "secondary", onClick: onCancel, children: "Cancel" }),
445
+ /* @__PURE__ */ jsx10(Button, { disabled: empty, onClick: save, children: "Use signature" })
446
+ ] })
247
447
  ] });
248
448
  }
249
- var TextInput = ({ className = "", ...rest }) => /* @__PURE__ */ jsx7("input", { className: `${inputClass} ${className}`, ...rest });
250
- var Select = ({ className = "", ...rest }) => /* @__PURE__ */ jsx7(
251
- "select",
449
+
450
+ // src/react/ui/Tag.tsx
451
+ import { jsx as jsx11 } from "react/jsx-runtime";
452
+ var tones = {
453
+ success: "bg-success/15 text-success",
454
+ info: "bg-heading/20 text-primary",
455
+ muted: "bg-hover text-muted"
456
+ };
457
+ var Tag = ({ tone = "info", children }) => /* @__PURE__ */ jsx11(
458
+ "span",
252
459
  {
253
- className: `rounded-md border border-line-strong bg-white px-2.5 py-1.5 text-[13px] outline-none ${className}`,
254
- ...rest
460
+ className: `inline-block rounded-full px-2.5 py-0.5 text-[11px] font-semibold whitespace-nowrap ${tones[tone]}`,
461
+ children
255
462
  }
256
463
  );
257
- var TextArea = ({ className = "", ...rest }) => /* @__PURE__ */ jsx7("textarea", { className: `${inputClass} resize-none ${className}`, ...rest });
464
+
465
+ // src/react/ui/slots.tsx
466
+ import { jsx as jsx12 } from "react/jsx-runtime";
467
+ var base = {
468
+ Button,
469
+ IconButton,
470
+ Checkbox,
471
+ FormField,
472
+ TextInput,
473
+ Select,
474
+ TextArea,
475
+ Modal,
476
+ ModalHeader,
477
+ ModalBody,
478
+ ModalFooter,
479
+ ConfirmModal,
480
+ PanelHeading,
481
+ EmptyText,
482
+ SegmentedTabs,
483
+ Chip,
484
+ ChipGroup,
485
+ RemovableChip,
486
+ Popover,
487
+ Tag,
488
+ SignaturePad
489
+ };
490
+ var defaultComponents = base;
491
+ var UIContext = createContext({});
492
+ function FormliftProvider({
493
+ components,
494
+ children
495
+ }) {
496
+ const outer = useContext(UIContext);
497
+ return /* @__PURE__ */ jsx12(UIContext.Provider, { value: components ? { ...outer, ...components } : outer, children });
498
+ }
499
+ function slot(name) {
500
+ const Slot = (props) => {
501
+ const C = useContext(UIContext)[name] ?? base[name];
502
+ return /* @__PURE__ */ jsx12(C, { ...props });
503
+ };
504
+ Slot.displayName = `formlift.${name}`;
505
+ return Slot;
506
+ }
507
+ var Button2 = slot("Button");
508
+ var IconButton2 = slot("IconButton");
509
+ var Checkbox2 = slot("Checkbox");
510
+ var FormField2 = slot("FormField");
511
+ var TextInput2 = slot("TextInput");
512
+ var Select2 = slot("Select");
513
+ var TextArea2 = slot("TextArea");
514
+ var Modal2 = slot("Modal");
515
+ var ModalHeader2 = slot("ModalHeader");
516
+ var ModalBody2 = slot("ModalBody");
517
+ var ModalFooter2 = slot("ModalFooter");
518
+ var ConfirmModal2 = slot("ConfirmModal");
519
+ var PanelHeading2 = slot("PanelHeading");
520
+ var EmptyText2 = slot("EmptyText");
521
+ var SegmentedTabs2 = slot("SegmentedTabs");
522
+ var Chip2 = slot("Chip");
523
+ var ChipGroup2 = slot("ChipGroup");
524
+ var RemovableChip2 = slot("RemovableChip");
525
+ var Popover2 = slot("Popover");
526
+ var Tag2 = slot("Tag");
527
+ var SignaturePad2 = slot("SignaturePad");
258
528
 
259
529
  // src/react/components/ElementProperties.tsx
260
- import { Fragment, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
530
+ import { Fragment, jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
261
531
  var num = (s) => s.trim() === "" || !Number.isFinite(Number(s)) ? void 0 : Number(s);
262
532
  var pct = (n) => Math.round(n * 1e3) / 10;
263
- var Section = ({ title, children }) => /* @__PURE__ */ jsxs5("section", { className: "space-y-3 border-b border-line px-3.5 py-3", children: [
264
- /* @__PURE__ */ jsx8("h4", { className: "m-0 text-[12px] font-bold tracking-wide text-muted uppercase", children: title }),
533
+ var Section = ({ title, children }) => /* @__PURE__ */ jsxs8("section", { className: "space-y-3 border-b border-line px-3.5 py-3", children: [
534
+ /* @__PURE__ */ jsx13("h4", { className: "m-0 text-[12px] font-bold tracking-wide text-muted uppercase", children: title }),
265
535
  children
266
536
  ] });
267
537
  function ColorField({
@@ -270,8 +540,8 @@ function ColorField({
270
540
  onChange,
271
541
  allowNone
272
542
  }) {
273
- return /* @__PURE__ */ jsx8(FormField, { label, children: /* @__PURE__ */ jsxs5("span", { className: "flex items-center gap-2", children: [
274
- /* @__PURE__ */ jsx8(
543
+ return /* @__PURE__ */ jsx13(FormField2, { label, children: /* @__PURE__ */ jsxs8("span", { className: "flex items-center gap-2", children: [
544
+ /* @__PURE__ */ jsx13(
275
545
  "input",
276
546
  {
277
547
  type: "color",
@@ -281,9 +551,9 @@ function ColorField({
281
551
  onChange: (e) => onChange(e.target.value)
282
552
  }
283
553
  ),
284
- allowNone && /* @__PURE__ */ jsxs5("label", { className: "inline-flex items-center gap-1.5 text-[12.5px]", children: [
285
- /* @__PURE__ */ jsx8(
286
- Checkbox,
554
+ allowNone && /* @__PURE__ */ jsxs8("label", { className: "inline-flex items-center gap-1.5 text-[12.5px]", children: [
555
+ /* @__PURE__ */ jsx13(
556
+ Checkbox2,
287
557
  {
288
558
  label: `No ${label.toLowerCase()}`,
289
559
  checked: !value,
@@ -312,15 +582,15 @@ function ElementProperties({
312
582
  set({ position: fitted });
313
583
  }
314
584
  };
315
- return /* @__PURE__ */ jsxs5("div", { className: "text-[13px]", children: [
316
- e.type === "text" && /* @__PURE__ */ jsxs5(Section, { title: "Text", children: [
317
- e.replaces != null && /* @__PURE__ */ jsxs5("p", { className: "m-0 rounded bg-primary-soft px-2 py-1 text-[12px] text-ink-2", children: [
585
+ return /* @__PURE__ */ jsxs8("div", { className: "text-[13px]", children: [
586
+ e.type === "text" && /* @__PURE__ */ jsxs8(Section, { title: "Text", children: [
587
+ e.replaces != null && /* @__PURE__ */ jsxs8("p", { className: "m-0 rounded bg-primary-soft px-2 py-1 text-[12px] text-ink-2", children: [
318
588
  "Replaces the original: \u201C",
319
589
  e.replaces,
320
590
  "\u201D"
321
591
  ] }),
322
- /* @__PURE__ */ jsx8(
323
- TextArea,
592
+ /* @__PURE__ */ jsx13(
593
+ TextArea2,
324
594
  {
325
595
  rows: 4,
326
596
  "aria-label": "Text",
@@ -330,18 +600,18 @@ function ElementProperties({
330
600
  onChange: (ev) => set({ text: ev.target.value })
331
601
  }
332
602
  ),
333
- /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-2", children: [
334
- /* @__PURE__ */ jsx8(FormField, { label: "Font", children: /* @__PURE__ */ jsx8(
335
- Select,
603
+ /* @__PURE__ */ jsxs8("div", { className: "grid grid-cols-2 gap-2", children: [
604
+ /* @__PURE__ */ jsx13(FormField2, { label: "Font", children: /* @__PURE__ */ jsx13(
605
+ Select2,
336
606
  {
337
607
  className: "w-full",
338
608
  value: e.font ?? "sans",
339
609
  onChange: (ev) => set({ font: ev.target.value }),
340
- children: FONTS.map((f) => /* @__PURE__ */ jsx8("option", { value: f, children: { sans: "Sans (Helvetica)", serif: "Serif (Times)", mono: "Mono (Courier)" }[f] }, f))
610
+ children: FONTS.map((f) => /* @__PURE__ */ jsx13("option", { value: f, children: { sans: "Sans (Helvetica)", serif: "Serif (Times)", mono: "Mono (Courier)" }[f] }, f))
341
611
  }
342
612
  ) }),
343
- /* @__PURE__ */ jsx8(FormField, { label: "Size (pt)", children: /* @__PURE__ */ jsx8(
344
- TextInput,
613
+ /* @__PURE__ */ jsx13(FormField2, { label: "Size (pt)", children: /* @__PURE__ */ jsx13(
614
+ TextInput2,
345
615
  {
346
616
  type: "number",
347
617
  min: 3,
@@ -355,31 +625,31 @@ function ElementProperties({
355
625
  }
356
626
  ) })
357
627
  ] }),
358
- /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-4", children: [
359
- ["bold", "italic"].map((k) => /* @__PURE__ */ jsxs5("label", { className: "inline-flex items-center gap-1.5", children: [
360
- /* @__PURE__ */ jsx8(Checkbox, { label: k, checked: !!e[k], onChange: (ev) => set({ [k]: ev.target.checked || void 0 }) }),
628
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-4", children: [
629
+ ["bold", "italic"].map((k) => /* @__PURE__ */ jsxs8("label", { className: "inline-flex items-center gap-1.5", children: [
630
+ /* @__PURE__ */ jsx13(Checkbox2, { label: k, checked: !!e[k], onChange: (ev) => set({ [k]: ev.target.checked || void 0 }) }),
361
631
  k === "bold" ? "Bold" : "Italic"
362
632
  ] }, k)),
363
- /* @__PURE__ */ jsxs5(
364
- Select,
633
+ /* @__PURE__ */ jsxs8(
634
+ Select2,
365
635
  {
366
636
  "aria-label": "Alignment",
367
637
  value: e.align ?? "left",
368
638
  onChange: (ev) => set({ align: ev.target.value }),
369
639
  children: [
370
- /* @__PURE__ */ jsx8("option", { value: "left", children: "Left" }),
371
- /* @__PURE__ */ jsx8("option", { value: "center", children: "Centre" }),
372
- /* @__PURE__ */ jsx8("option", { value: "right", children: "Right" })
640
+ /* @__PURE__ */ jsx13("option", { value: "left", children: "Left" }),
641
+ /* @__PURE__ */ jsx13("option", { value: "center", children: "Centre" }),
642
+ /* @__PURE__ */ jsx13("option", { value: "right", children: "Right" })
373
643
  ]
374
644
  }
375
645
  )
376
646
  ] }),
377
- /* @__PURE__ */ jsx8(ColorField, { label: "Text colour", value: e.color ?? "#000000", onChange: (v) => set({ color: v }) })
647
+ /* @__PURE__ */ jsx13(ColorField, { label: "Text colour", value: e.color ?? "#000000", onChange: (v) => set({ color: v }) })
378
648
  ] }),
379
- /* @__PURE__ */ jsxs5(Section, { title: "Colours", children: [
380
- e.type !== "line" && /* @__PURE__ */ jsx8(ColorField, { label: "Background", allowNone: true, value: e.fill, onChange: (v) => set({ fill: v }) }),
381
- e.type !== "text" && /* @__PURE__ */ jsxs5(Fragment, { children: [
382
- /* @__PURE__ */ jsx8(
649
+ /* @__PURE__ */ jsxs8(Section, { title: "Colours", children: [
650
+ e.type !== "line" && /* @__PURE__ */ jsx13(ColorField, { label: "Background", allowNone: true, value: e.fill, onChange: (v) => set({ fill: v }) }),
651
+ e.type !== "text" && /* @__PURE__ */ jsxs8(Fragment, { children: [
652
+ /* @__PURE__ */ jsx13(
383
653
  ColorField,
384
654
  {
385
655
  label: e.type === "line" ? "Line colour" : "Border",
@@ -388,8 +658,8 @@ function ElementProperties({
388
658
  onChange: (v) => set({ stroke: v })
389
659
  }
390
660
  ),
391
- /* @__PURE__ */ jsx8(FormField, { label: "Line width (pt)", children: /* @__PURE__ */ jsx8(
392
- TextInput,
661
+ /* @__PURE__ */ jsx13(FormField2, { label: "Line width (pt)", children: /* @__PURE__ */ jsx13(
662
+ TextInput2,
393
663
  {
394
664
  type: "number",
395
665
  min: 0,
@@ -403,11 +673,11 @@ function ElementProperties({
403
673
  }
404
674
  ) })
405
675
  ] }),
406
- e.fill === "#ffffff" && /* @__PURE__ */ jsx8("p", { className: "m-0 text-[12px] text-muted", children: "A white background hides the original under it." })
676
+ e.fill === "#ffffff" && /* @__PURE__ */ jsx13("p", { className: "m-0 text-[12px] text-muted", children: "A white background hides the original under it." })
407
677
  ] }),
408
- /* @__PURE__ */ jsx8(Section, { title: "Position (% of page)", children: /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-2", children: [
409
- /* @__PURE__ */ jsx8(FormField, { label: "Page", children: /* @__PURE__ */ jsx8(
410
- TextInput,
678
+ /* @__PURE__ */ jsx13(Section, { title: "Position (% of page)", children: /* @__PURE__ */ jsxs8("div", { className: "grid grid-cols-2 gap-2", children: [
679
+ /* @__PURE__ */ jsx13(FormField2, { label: "Page", children: /* @__PURE__ */ jsx13(
680
+ TextInput2,
411
681
  {
412
682
  type: "number",
413
683
  min: 1,
@@ -419,9 +689,9 @@ function ElementProperties({
419
689
  }
420
690
  }
421
691
  ) }),
422
- /* @__PURE__ */ jsx8("span", {}),
423
- ["x", "y", "width", "height"].map((k) => /* @__PURE__ */ jsx8(FormField, { label: { x: "Left", y: "Top", width: "Width", height: "Height" }[k], children: /* @__PURE__ */ jsx8(
424
- TextInput,
692
+ /* @__PURE__ */ jsx13("span", {}),
693
+ ["x", "y", "width", "height"].map((k) => /* @__PURE__ */ jsx13(FormField2, { label: { x: "Left", y: "Top", width: "Width", height: "Height" }[k], children: /* @__PURE__ */ jsx13(
694
+ TextInput2,
425
695
  {
426
696
  type: "number",
427
697
  step: 0.1,
@@ -435,82 +705,8 @@ function ElementProperties({
435
705
 
436
706
  // src/react/components/FieldControl.tsx
437
707
  import { Paperclip, PenLine } from "lucide-react";
438
- import { useRef as useRef2, useState as useState2 } from "react";
439
-
440
- // src/react/ui/SignaturePad.tsx
441
- import { useRef, useState } from "react";
442
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
443
- function SignaturePad({
444
- onSave,
445
- onCancel,
446
- maxWidth = 600
447
- }) {
448
- const canvas = useRef(null);
449
- const last = useRef(null);
450
- const [empty, setEmpty] = useState(true);
451
- const point = (e) => {
452
- const r = e.currentTarget.getBoundingClientRect();
453
- return {
454
- x: (e.clientX - r.left) / r.width * e.currentTarget.width,
455
- y: (e.clientY - r.top) / r.height * e.currentTarget.height
456
- };
457
- };
458
- const draw = (e) => {
459
- const ctx = canvas.current?.getContext("2d");
460
- if (!ctx || !last.current) return;
461
- const p = point(e);
462
- ctx.lineWidth = 3;
463
- ctx.lineCap = "round";
464
- ctx.strokeStyle = "#111827";
465
- ctx.beginPath();
466
- ctx.moveTo(last.current.x, last.current.y);
467
- ctx.lineTo(p.x, p.y);
468
- ctx.stroke();
469
- last.current = p;
470
- setEmpty(false);
471
- };
472
- const clear = () => {
473
- const c = canvas.current;
474
- c?.getContext("2d")?.clearRect(0, 0, c.width, c.height);
475
- setEmpty(true);
476
- };
477
- const save = () => {
478
- const c = canvas.current;
479
- if (!c) return;
480
- const scale = Math.min(1, maxWidth / c.width);
481
- const out = document.createElement("canvas");
482
- out.width = Math.round(c.width * scale);
483
- out.height = Math.round(c.height * scale);
484
- out.getContext("2d")?.drawImage(c, 0, 0, out.width, out.height);
485
- onSave(out.toDataURL("image/png"));
486
- };
487
- return /* @__PURE__ */ jsxs6("div", { className: "space-y-3", children: [
488
- /* @__PURE__ */ jsx9(
489
- "canvas",
490
- {
491
- ref: canvas,
492
- width: 800,
493
- height: 260,
494
- "aria-label": "Signature drawing area",
495
- className: "h-[200px] w-full touch-none rounded-md border border-dashed border-line-strong bg-surface",
496
- onPointerDown: (e) => {
497
- e.currentTarget.setPointerCapture(e.pointerId);
498
- last.current = point(e);
499
- },
500
- onPointerMove: draw,
501
- onPointerUp: () => last.current = null
502
- }
503
- ),
504
- /* @__PURE__ */ jsxs6("div", { className: "flex justify-end gap-2", children: [
505
- /* @__PURE__ */ jsx9(Button, { variant: "secondary", onClick: clear, children: "Clear" }),
506
- /* @__PURE__ */ jsx9(Button, { variant: "secondary", onClick: onCancel, children: "Cancel" }),
507
- /* @__PURE__ */ jsx9(Button, { disabled: empty, onClick: save, children: "Use signature" })
508
- ] })
509
- ] });
510
- }
511
-
512
- // src/react/components/FieldControl.tsx
513
- import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
708
+ import { useRef as useRef3, useState as useState3 } from "react";
709
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
514
710
  var box = "h-full w-full rounded-sm border bg-primary-soft/70 px-1 text-[clamp(8px,62cqh,15px)] text-navy outline-none focus:border-primary focus:bg-white disabled:cursor-not-allowed";
515
711
  function pngFile(dataUrl, name) {
516
712
  const bin = atob(dataUrl.slice(dataUrl.indexOf(",") + 1));
@@ -527,12 +723,12 @@ function SheetInput({
527
723
  onDone
528
724
  }) {
529
725
  if (field.type === "radio")
530
- return /* @__PURE__ */ jsx10("div", { role: "radiogroup", "aria-label": field.label || field.key, className: "flex flex-col gap-1", children: (field.options ?? []).map((o) => /* @__PURE__ */ jsxs7(
726
+ return /* @__PURE__ */ jsx14("div", { role: "radiogroup", "aria-label": field.label || field.key, className: "flex flex-col gap-1", children: (field.options ?? []).map((o) => /* @__PURE__ */ jsxs9(
531
727
  "label",
532
728
  {
533
729
  className: "flex min-h-11 cursor-pointer items-center gap-3 rounded-md border border-line px-3 text-base",
534
730
  children: [
535
- /* @__PURE__ */ jsx10(
731
+ /* @__PURE__ */ jsx14(
536
732
  "input",
537
733
  {
538
734
  type: "radio",
@@ -551,7 +747,7 @@ function SheetInput({
551
747
  o
552
748
  )) });
553
749
  if (field.type === "textarea")
554
- return /* @__PURE__ */ jsx10(
750
+ return /* @__PURE__ */ jsx14(
555
751
  "textarea",
556
752
  {
557
753
  autoFocus: true,
@@ -563,7 +759,7 @@ function SheetInput({
563
759
  onChange: (e) => onChange(e.target.value)
564
760
  }
565
761
  );
566
- return /* @__PURE__ */ jsx10(
762
+ return /* @__PURE__ */ jsx14(
567
763
  "input",
568
764
  {
569
765
  autoFocus: true,
@@ -592,9 +788,9 @@ function FieldControl({
592
788
  signatureSrc,
593
789
  compact
594
790
  }) {
595
- const [signing, setSigning] = useState2(false);
596
- const [editing, setEditing] = useState2(false);
597
- const fileInput = useRef2(null);
791
+ const [signing, setSigning] = useState3(false);
792
+ const [editing, setEditing] = useState3(false);
793
+ const fileInput = useRef3(null);
598
794
  const border = error ? "border-danger" : "border-primary/40";
599
795
  const common = {
600
796
  "aria-label": field.label || field.key,
@@ -605,31 +801,31 @@ function FieldControl({
605
801
  const text = typeof value === "string" ? value : "";
606
802
  if (compact && !readOnly && SHEET_TYPES.includes(field.type)) {
607
803
  const name = field.label || field.key;
608
- return /* @__PURE__ */ jsxs7(Fragment2, { children: [
609
- /* @__PURE__ */ jsx10(
804
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
805
+ /* @__PURE__ */ jsx14(
610
806
  "button",
611
807
  {
612
808
  type: "button",
613
809
  ...common,
614
810
  onClick: () => setEditing(true),
615
811
  className: `${box} ${border} flex cursor-pointer overflow-hidden text-left leading-tight ${field.type === "textarea" ? "items-start py-0.5 text-[clamp(8px,14cqh,15px)]" : "items-center"}`,
616
- children: /* @__PURE__ */ jsx10("span", { className: `min-w-0 ${field.type === "textarea" ? "line-clamp-[8] whitespace-pre-wrap" : "truncate"}`, children: text || /* @__PURE__ */ jsx10("span", { className: "text-faint", children: field.placeholder || "" }) })
812
+ children: /* @__PURE__ */ jsx14("span", { className: `min-w-0 ${field.type === "textarea" ? "line-clamp-[8] whitespace-pre-wrap" : "truncate"}`, children: text || /* @__PURE__ */ jsx14("span", { className: "text-faint", children: field.placeholder || "" }) })
617
813
  }
618
814
  ),
619
- editing && /* @__PURE__ */ jsxs7(Modal, { onClose: () => setEditing(false), width: "w-[480px]", children: [
620
- /* @__PURE__ */ jsx10(ModalHeader, { title: name, onClose: () => setEditing(false) }),
621
- /* @__PURE__ */ jsxs7(ModalBody, { children: [
622
- /* @__PURE__ */ jsx10(SheetInput, { field, text, onChange, onDone: () => setEditing(false) }),
623
- field.helpText && /* @__PURE__ */ jsx10("p", { className: "mt-2 mb-0 text-[13px] text-muted", children: field.helpText }),
624
- error && /* @__PURE__ */ jsx10("p", { className: "mt-2 mb-0 text-[13px] text-danger", children: error })
815
+ editing && /* @__PURE__ */ jsxs9(Modal2, { onClose: () => setEditing(false), width: "w-[480px]", children: [
816
+ /* @__PURE__ */ jsx14(ModalHeader2, { title: name, onClose: () => setEditing(false) }),
817
+ /* @__PURE__ */ jsxs9(ModalBody2, { children: [
818
+ /* @__PURE__ */ jsx14(SheetInput, { field, text, onChange, onDone: () => setEditing(false) }),
819
+ field.helpText && /* @__PURE__ */ jsx14("p", { className: "mt-2 mb-0 text-[13px] text-muted", children: field.helpText }),
820
+ error && /* @__PURE__ */ jsx14("p", { className: "mt-2 mb-0 text-[13px] text-danger", children: error })
625
821
  ] }),
626
- /* @__PURE__ */ jsx10(ModalFooter, { children: /* @__PURE__ */ jsx10(Button, { onClick: () => setEditing(false), children: "Done" }) })
822
+ /* @__PURE__ */ jsx14(ModalFooter2, { children: /* @__PURE__ */ jsx14(Button2, { onClick: () => setEditing(false), children: "Done" }) })
627
823
  ] })
628
824
  ] });
629
825
  }
630
826
  switch (field.type) {
631
827
  case "textarea":
632
- return /* @__PURE__ */ jsx10(
828
+ return /* @__PURE__ */ jsx14(
633
829
  "textarea",
634
830
  {
635
831
  ...common,
@@ -641,19 +837,19 @@ function FieldControl({
641
837
  }
642
838
  );
643
839
  case "dropdown":
644
- return /* @__PURE__ */ jsxs7("select", { ...common, className: `${box} ${border}`, value: text, onChange: (e) => onChange(e.target.value), children: [
645
- /* @__PURE__ */ jsx10("option", { value: "", children: field.placeholder || "" }),
646
- (field.options ?? []).map((o) => /* @__PURE__ */ jsx10("option", { children: o }, o))
840
+ return /* @__PURE__ */ jsxs9("select", { ...common, className: `${box} ${border}`, value: text, onChange: (e) => onChange(e.target.value), children: [
841
+ /* @__PURE__ */ jsx14("option", { value: "", children: field.placeholder || "" }),
842
+ (field.options ?? []).map((o) => /* @__PURE__ */ jsx14("option", { children: o }, o))
647
843
  ] });
648
844
  case "radio":
649
- return /* @__PURE__ */ jsx10(
845
+ return /* @__PURE__ */ jsx14(
650
846
  "div",
651
847
  {
652
848
  role: "radiogroup",
653
849
  ...common,
654
850
  className: `flex h-full w-full flex-wrap items-center gap-x-3 rounded-sm border px-1 text-[clamp(8px,55cqh,14px)] ${border} bg-primary-soft/50`,
655
- children: (field.options ?? []).map((o) => /* @__PURE__ */ jsxs7("label", { className: "inline-flex cursor-pointer items-center gap-1", children: [
656
- /* @__PURE__ */ jsx10(
851
+ children: (field.options ?? []).map((o) => /* @__PURE__ */ jsxs9("label", { className: "inline-flex cursor-pointer items-center gap-1", children: [
852
+ /* @__PURE__ */ jsx14(
657
853
  "input",
658
854
  {
659
855
  type: "radio",
@@ -669,11 +865,11 @@ function FieldControl({
669
865
  }
670
866
  );
671
867
  case "checkbox":
672
- return /* @__PURE__ */ jsx10(
868
+ return /* @__PURE__ */ jsx14(
673
869
  "label",
674
870
  {
675
871
  className: `flex h-full w-full items-center justify-center rounded-sm border ${border} bg-primary-soft/50`,
676
- children: /* @__PURE__ */ jsx10(
872
+ children: /* @__PURE__ */ jsx14(
677
873
  "input",
678
874
  {
679
875
  type: "checkbox",
@@ -686,31 +882,31 @@ function FieldControl({
686
882
  }
687
883
  );
688
884
  case "signature":
689
- return /* @__PURE__ */ jsxs7(Fragment2, { children: [
690
- /* @__PURE__ */ jsx10(
885
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
886
+ /* @__PURE__ */ jsx14(
691
887
  "button",
692
888
  {
693
889
  type: "button",
694
890
  ...common,
695
891
  onClick: () => setSigning(true),
696
892
  className: `flex h-full w-full cursor-pointer items-center justify-center rounded-sm border ${border} bg-primary-soft/50 disabled:cursor-default`,
697
- children: text ? /* @__PURE__ */ jsx10(
893
+ children: text ? /* @__PURE__ */ jsx14(
698
894
  "img",
699
895
  {
700
896
  src: text.startsWith("data:") ? text : signatureSrc,
701
897
  alt: "Signature",
702
898
  className: "max-h-full max-w-full object-contain"
703
899
  }
704
- ) : /* @__PURE__ */ jsxs7("span", { className: "inline-flex items-center gap-1 text-[clamp(8px,45cqh,13px)] text-primary", children: [
705
- /* @__PURE__ */ jsx10(PenLine, { size: 12 }),
900
+ ) : /* @__PURE__ */ jsxs9("span", { className: "inline-flex items-center gap-1 text-[clamp(8px,45cqh,13px)] text-primary", children: [
901
+ /* @__PURE__ */ jsx14(PenLine, { size: 12 }),
706
902
  " Sign"
707
903
  ] })
708
904
  }
709
905
  ),
710
- signing && /* @__PURE__ */ jsxs7(Modal, { onClose: () => setSigning(false), width: "w-[640px]", children: [
711
- /* @__PURE__ */ jsx10(ModalHeader, { title: `Sign: ${field.label || field.key}`, onClose: () => setSigning(false) }),
712
- /* @__PURE__ */ jsx10(ModalBody, { children: /* @__PURE__ */ jsx10(
713
- SignaturePad,
906
+ signing && /* @__PURE__ */ jsxs9(Modal2, { onClose: () => setSigning(false), width: "w-[640px]", children: [
907
+ /* @__PURE__ */ jsx14(ModalHeader2, { title: `Sign: ${field.label || field.key}`, onClose: () => setSigning(false) }),
908
+ /* @__PURE__ */ jsx14(ModalBody2, { children: /* @__PURE__ */ jsx14(
909
+ SignaturePad2,
714
910
  {
715
911
  onCancel: () => setSigning(false),
716
912
  onSave: (url) => {
@@ -723,8 +919,8 @@ function FieldControl({
723
919
  ] })
724
920
  ] });
725
921
  case "file":
726
- return /* @__PURE__ */ jsxs7("div", { className: `flex h-full w-full items-center gap-1 rounded-sm border px-1 ${border} bg-primary-soft/50`, children: [
727
- /* @__PURE__ */ jsx10(
922
+ return /* @__PURE__ */ jsxs9("div", { className: `flex h-full w-full items-center gap-1 rounded-sm border px-1 ${border} bg-primary-soft/50`, children: [
923
+ /* @__PURE__ */ jsx14(
728
924
  "input",
729
925
  {
730
926
  ref: fileInput,
@@ -737,7 +933,7 @@ function FieldControl({
737
933
  }
738
934
  }
739
935
  ),
740
- value && /* @__PURE__ */ jsx10(
936
+ value && /* @__PURE__ */ jsx14(
741
937
  "button",
742
938
  {
743
939
  type: "button",
@@ -746,7 +942,7 @@ function FieldControl({
746
942
  children: fileName || "Attached file"
747
943
  }
748
944
  ),
749
- !readOnly && /* @__PURE__ */ jsxs7(
945
+ !readOnly && /* @__PURE__ */ jsxs9(
750
946
  "button",
751
947
  {
752
948
  type: "button",
@@ -754,7 +950,7 @@ function FieldControl({
754
950
  onClick: () => fileInput.current?.click(),
755
951
  className: "inline-flex flex-none cursor-pointer items-center gap-1 text-[clamp(8px,50cqh,13px)] text-primary",
756
952
  children: [
757
- /* @__PURE__ */ jsx10(Paperclip, { size: 12 }),
953
+ /* @__PURE__ */ jsx14(Paperclip, { size: 12 }),
758
954
  " ",
759
955
  value ? "Replace" : "Attach"
760
956
  ]
@@ -763,7 +959,7 @@ function FieldControl({
763
959
  ] });
764
960
  default: {
765
961
  const type = INPUT_TYPE[field.type] ?? "text";
766
- return /* @__PURE__ */ jsx10(
962
+ return /* @__PURE__ */ jsx14(
767
963
  "input",
768
964
  {
769
965
  ...common,
@@ -780,9 +976,13 @@ function FieldControl({
780
976
  }
781
977
  }
782
978
  }
979
+ function AnswerControl({ components, ...props }) {
980
+ const C = components?.fields?.[props.field.type] ?? components?.FieldControl ?? FieldControl;
981
+ return /* @__PURE__ */ jsx14(C, { ...props });
982
+ }
783
983
 
784
984
  // src/react/components/FieldProperties.tsx
785
- import { useState as useState3 } from "react";
985
+ import { useState as useState4 } from "react";
786
986
 
787
987
  // src/react/constants.ts
788
988
  var TYPE_LABEL = {
@@ -802,8 +1002,8 @@ var ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2];
802
1002
  var BASE_SCALE = 96 / 72;
803
1003
 
804
1004
  // src/react/components/ConditionBuilder.tsx
805
- import { Plus, X } from "lucide-react";
806
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1005
+ import { Plus, X as X2 } from "lucide-react";
1006
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
807
1007
  var OP_LABEL = {
808
1008
  equals: "equals",
809
1009
  notEquals: "does not equal",
@@ -826,50 +1026,50 @@ function ConditionBuilder({
826
1026
  const setRule = (i, r) => onChange({ ...value, rules: value.rules.map((x, j) => j === i ? r : x) });
827
1027
  const remove = (i) => onChange({ ...value, rules: value.rules.filter((_, j) => j !== i) });
828
1028
  const newRule = () => ({ fieldKey: fields[0]?.key ?? "", op: "equals", value: "" });
829
- return /* @__PURE__ */ jsxs8("div", { className: `space-y-2 ${depth ? "rounded-md border border-line bg-surface p-2" : ""}`, children: [
830
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 text-[12px]", children: [
1029
+ return /* @__PURE__ */ jsxs10("div", { className: `space-y-2 ${depth ? "rounded-md border border-line bg-surface p-2" : ""}`, children: [
1030
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 text-[12px]", children: [
831
1031
  "Match",
832
- /* @__PURE__ */ jsxs8(
833
- Select,
1032
+ /* @__PURE__ */ jsxs10(
1033
+ Select2,
834
1034
  {
835
1035
  "aria-label": "Match all or any",
836
1036
  value: value.logic,
837
1037
  onChange: (e) => onChange({ ...value, logic: e.target.value }),
838
1038
  children: [
839
- /* @__PURE__ */ jsx11("option", { value: "and", children: "ALL (AND)" }),
840
- /* @__PURE__ */ jsx11("option", { value: "or", children: "ANY (OR)" })
1039
+ /* @__PURE__ */ jsx15("option", { value: "and", children: "ALL (AND)" }),
1040
+ /* @__PURE__ */ jsx15("option", { value: "or", children: "ANY (OR)" })
841
1041
  ]
842
1042
  }
843
1043
  )
844
1044
  ] }),
845
1045
  value.rules.map(
846
- (r, i) => isGroup(r) ? /* @__PURE__ */ jsxs8("div", { className: "flex items-start gap-1", children: [
847
- /* @__PURE__ */ jsx11("div", { className: "flex-1", children: /* @__PURE__ */ jsx11(ConditionBuilder, { value: r, onChange: (c) => setRule(i, c), fields, depth: depth + 1 }) }),
848
- /* @__PURE__ */ jsx11(IconButton, { icon: X, title: "Remove group", danger: true, iconSize: 14, onClick: () => remove(i) })
849
- ] }, i) : /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-1", children: [
850
- /* @__PURE__ */ jsxs8(
851
- Select,
1046
+ (r, i) => isGroup(r) ? /* @__PURE__ */ jsxs10("div", { className: "flex items-start gap-1", children: [
1047
+ /* @__PURE__ */ jsx15("div", { className: "flex-1", children: /* @__PURE__ */ jsx15(ConditionBuilder, { value: r, onChange: (c) => setRule(i, c), fields, depth: depth + 1 }) }),
1048
+ /* @__PURE__ */ jsx15(IconButton2, { icon: X2, title: "Remove group", danger: true, iconSize: 14, onClick: () => remove(i) })
1049
+ ] }, i) : /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-center gap-1", children: [
1050
+ /* @__PURE__ */ jsxs10(
1051
+ Select2,
852
1052
  {
853
1053
  "aria-label": "Field",
854
1054
  value: r.fieldKey,
855
1055
  onChange: (e) => setRule(i, { ...r, fieldKey: e.target.value }),
856
1056
  children: [
857
- !fields.some((f) => f.key === r.fieldKey) && /* @__PURE__ */ jsx11("option", { value: r.fieldKey, children: r.fieldKey || "\u2014" }),
858
- fields.map((f) => /* @__PURE__ */ jsx11("option", { value: f.key, children: f.label || f.key }, f.id))
1057
+ !fields.some((f) => f.key === r.fieldKey) && /* @__PURE__ */ jsx15("option", { value: r.fieldKey, children: r.fieldKey || "\u2014" }),
1058
+ fields.map((f) => /* @__PURE__ */ jsx15("option", { value: f.key, children: f.label || f.key }, f.id))
859
1059
  ]
860
1060
  }
861
1061
  ),
862
- /* @__PURE__ */ jsx11(
863
- Select,
1062
+ /* @__PURE__ */ jsx15(
1063
+ Select2,
864
1064
  {
865
1065
  "aria-label": "Operator",
866
1066
  value: r.op,
867
1067
  onChange: (e) => setRule(i, { ...r, op: e.target.value }),
868
- children: OPERATORS.map((op) => /* @__PURE__ */ jsx11("option", { value: op, children: OP_LABEL[op] }, op))
1068
+ children: OPERATORS.map((op) => /* @__PURE__ */ jsx15("option", { value: op, children: OP_LABEL[op] }, op))
869
1069
  }
870
1070
  ),
871
- r.op !== "isEmpty" && r.op !== "isNotEmpty" && /* @__PURE__ */ jsx11(
872
- TextInput,
1071
+ r.op !== "isEmpty" && r.op !== "isNotEmpty" && /* @__PURE__ */ jsx15(
1072
+ TextInput2,
873
1073
  {
874
1074
  "aria-label": "Value",
875
1075
  className: "!w-24 !py-1",
@@ -877,8 +1077,8 @@ function ConditionBuilder({
877
1077
  onChange: (e) => setRule(i, { ...r, value: e.target.value })
878
1078
  }
879
1079
  ),
880
- r.op === "between" && /* @__PURE__ */ jsx11(
881
- TextInput,
1080
+ r.op === "between" && /* @__PURE__ */ jsx15(
1081
+ TextInput2,
882
1082
  {
883
1083
  "aria-label": "Upper value",
884
1084
  className: "!w-24 !py-1",
@@ -886,12 +1086,12 @@ function ConditionBuilder({
886
1086
  onChange: (e) => setRule(i, { ...r, value2: e.target.value })
887
1087
  }
888
1088
  ),
889
- /* @__PURE__ */ jsx11(IconButton, { icon: X, title: "Remove rule", danger: true, iconSize: 14, onClick: () => remove(i) })
1089
+ /* @__PURE__ */ jsx15(IconButton2, { icon: X2, title: "Remove rule", danger: true, iconSize: 14, onClick: () => remove(i) })
890
1090
  ] }, i)
891
1091
  ),
892
- /* @__PURE__ */ jsxs8("div", { className: "flex gap-2", children: [
893
- /* @__PURE__ */ jsx11(
894
- Button,
1092
+ /* @__PURE__ */ jsxs10("div", { className: "flex gap-2", children: [
1093
+ /* @__PURE__ */ jsx15(
1094
+ Button2,
895
1095
  {
896
1096
  size: "sm",
897
1097
  variant: "secondary",
@@ -901,8 +1101,8 @@ function ConditionBuilder({
901
1101
  children: "Rule"
902
1102
  }
903
1103
  ),
904
- depth < 2 && /* @__PURE__ */ jsx11(
905
- Button,
1104
+ depth < 2 && /* @__PURE__ */ jsx15(
1105
+ Button2,
906
1106
  {
907
1107
  size: "sm",
908
1108
  variant: "secondary",
@@ -917,11 +1117,11 @@ function ConditionBuilder({
917
1117
  }
918
1118
 
919
1119
  // src/react/components/FieldProperties.tsx
920
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1120
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
921
1121
  var num2 = (s) => s.trim() === "" || !Number.isFinite(Number(s)) ? void 0 : Number(s);
922
1122
  var pct2 = (n) => Math.round(n * 1e3) / 10;
923
- var Section2 = ({ title, children }) => /* @__PURE__ */ jsxs9("section", { className: "space-y-3 border-b border-line px-3.5 py-3", children: [
924
- /* @__PURE__ */ jsx12("h4", { className: "m-0 text-[12px] font-bold tracking-wide text-muted uppercase", children: title }),
1123
+ var Section2 = ({ title, children }) => /* @__PURE__ */ jsxs11("section", { className: "space-y-3 border-b border-line px-3.5 py-3", children: [
1124
+ /* @__PURE__ */ jsx16("h4", { className: "m-0 text-[12px] font-bold tracking-wide text-muted uppercase", children: title }),
925
1125
  children
926
1126
  ] });
927
1127
  var CONDITIONS = [
@@ -937,56 +1137,56 @@ function FieldProperties({
937
1137
  }) {
938
1138
  const set = (patch) => onChange({ ...field, ...patch });
939
1139
  const others = fields.filter((f) => f.id !== field.id);
940
- const [optionsText, setOptionsText] = useState3((field.options ?? []).join("\n"));
1140
+ const [optionsText, setOptionsText] = useState4((field.options ?? []).join("\n"));
941
1141
  const hasOptions = field.type === "dropdown" || field.type === "radio";
942
1142
  const textual = !["checkbox", "file", "signature", "dropdown", "radio"].includes(field.type);
943
1143
  const setPos = (k, v) => {
944
1144
  const n = num2(v);
945
1145
  if (n != null) set({ position: fitInPage({ ...field.position, [k]: n / 100 }) });
946
1146
  };
947
- return /* @__PURE__ */ jsxs9("div", { className: "text-[13px]", children: [
948
- /* @__PURE__ */ jsxs9(Section2, { title: "Field", children: [
949
- field.source === "ai" && /* @__PURE__ */ jsxs9("p", { className: "m-0 rounded bg-folder/15 px-2 py-1 text-[12px]", children: [
1147
+ return /* @__PURE__ */ jsxs11("div", { className: "text-[13px]", children: [
1148
+ /* @__PURE__ */ jsxs11(Section2, { title: "Field", children: [
1149
+ field.source === "ai" && /* @__PURE__ */ jsxs11("p", { className: "m-0 rounded bg-folder/15 px-2 py-1 text-[12px]", children: [
950
1150
  "Suggested automatically",
951
1151
  field.confidence != null && ` (${Math.round(field.confidence * 100)}% sure)`,
952
1152
  ". Check it."
953
1153
  ] }),
954
- /* @__PURE__ */ jsxs9("p", { className: "m-0 font-mono text-[11px] text-faint", children: [
1154
+ /* @__PURE__ */ jsxs11("p", { className: "m-0 font-mono text-[11px] text-faint", children: [
955
1155
  "ID ",
956
1156
  field.id
957
1157
  ] }),
958
- /* @__PURE__ */ jsx12(FormField, { label: "Label", children: /* @__PURE__ */ jsx12(TextInput, { value: field.label, onChange: (e) => set({ label: e.target.value }) }) }),
959
- /* @__PURE__ */ jsx12(FormField, { label: "Key (automatic, from the label)", children: /* @__PURE__ */ jsx12(TextInput, { value: field.key, readOnly: true, className: "text-faint" }) }),
960
- /* @__PURE__ */ jsx12(FormField, { label: "Type", children: /* @__PURE__ */ jsx12(Select, { className: "w-full", value: field.type, onChange: (e) => set({ type: e.target.value }), children: FIELD_TYPES.map((t) => /* @__PURE__ */ jsx12("option", { value: t, children: TYPE_LABEL[t] }, t)) }) }),
961
- textual && /* @__PURE__ */ jsx12(FormField, { label: "Placeholder", children: /* @__PURE__ */ jsx12(
962
- TextInput,
1158
+ /* @__PURE__ */ jsx16(FormField2, { label: "Label", children: /* @__PURE__ */ jsx16(TextInput2, { value: field.label, onChange: (e) => set({ label: e.target.value }) }) }),
1159
+ /* @__PURE__ */ jsx16(FormField2, { label: "Key (automatic, from the label)", children: /* @__PURE__ */ jsx16(TextInput2, { value: field.key, readOnly: true, className: "text-faint" }) }),
1160
+ /* @__PURE__ */ jsx16(FormField2, { label: "Type", children: /* @__PURE__ */ jsx16(Select2, { className: "w-full", value: field.type, onChange: (e) => set({ type: e.target.value }), children: FIELD_TYPES.map((t) => /* @__PURE__ */ jsx16("option", { value: t, children: TYPE_LABEL[t] }, t)) }) }),
1161
+ textual && /* @__PURE__ */ jsx16(FormField2, { label: "Placeholder", children: /* @__PURE__ */ jsx16(
1162
+ TextInput2,
963
1163
  {
964
1164
  value: field.placeholder ?? "",
965
1165
  onChange: (e) => set({ placeholder: e.target.value || void 0 })
966
1166
  }
967
1167
  ) }),
968
- field.type !== "file" && field.type !== "signature" && /* @__PURE__ */ jsx12(FormField, { label: field.type === "checkbox" ? "Checked by default" : "Default value", children: field.type === "checkbox" ? /* @__PURE__ */ jsx12(
969
- Checkbox,
1168
+ field.type !== "file" && field.type !== "signature" && /* @__PURE__ */ jsx16(FormField2, { label: field.type === "checkbox" ? "Checked by default" : "Default value", children: field.type === "checkbox" ? /* @__PURE__ */ jsx16(
1169
+ Checkbox2,
970
1170
  {
971
1171
  label: "Checked by default",
972
1172
  checked: field.defaultValue === "true",
973
1173
  onChange: (e) => set({ defaultValue: e.target.checked ? "true" : void 0 })
974
1174
  }
975
- ) : /* @__PURE__ */ jsx12(
976
- TextInput,
1175
+ ) : /* @__PURE__ */ jsx16(
1176
+ TextInput2,
977
1177
  {
978
1178
  value: field.defaultValue ?? "",
979
1179
  onChange: (e) => set({ defaultValue: e.target.value || void 0 })
980
1180
  }
981
1181
  ) }),
982
- /* @__PURE__ */ jsx12(FormField, { label: "Help text (shown as tooltip)", children: /* @__PURE__ */ jsx12(TextInput, { value: field.helpText ?? "", onChange: (e) => set({ helpText: e.target.value || void 0 }) }) }),
983
- /* @__PURE__ */ jsx12("div", { className: "flex flex-wrap gap-4", children: ["required", "readOnly", "hidden"].map((k) => /* @__PURE__ */ jsxs9("label", { className: "inline-flex items-center gap-1.5", children: [
984
- /* @__PURE__ */ jsx12(Checkbox, { label: k, checked: !!field[k], onChange: (e) => set({ [k]: e.target.checked || void 0 }) }),
1182
+ /* @__PURE__ */ jsx16(FormField2, { label: "Help text (shown as tooltip)", children: /* @__PURE__ */ jsx16(TextInput2, { value: field.helpText ?? "", onChange: (e) => set({ helpText: e.target.value || void 0 }) }) }),
1183
+ /* @__PURE__ */ jsx16("div", { className: "flex flex-wrap gap-4", children: ["required", "readOnly", "hidden"].map((k) => /* @__PURE__ */ jsxs11("label", { className: "inline-flex items-center gap-1.5", children: [
1184
+ /* @__PURE__ */ jsx16(Checkbox2, { label: k, checked: !!field[k], onChange: (e) => set({ [k]: e.target.checked || void 0 }) }),
985
1185
  { required: "Required", readOnly: "Read-only", hidden: "Hidden" }[k]
986
1186
  ] }, k)) })
987
1187
  ] }),
988
- hasOptions && /* @__PURE__ */ jsx12(Section2, { title: "Options (one per line)", children: /* @__PURE__ */ jsx12(
989
- TextArea,
1188
+ hasOptions && /* @__PURE__ */ jsx16(Section2, { title: "Options (one per line)", children: /* @__PURE__ */ jsx16(
1189
+ TextArea2,
990
1190
  {
991
1191
  rows: 4,
992
1192
  value: optionsText,
@@ -1002,10 +1202,10 @@ function FieldProperties({
1002
1202
  }
1003
1203
  }
1004
1204
  ) }),
1005
- textual && /* @__PURE__ */ jsxs9(Section2, { title: "Validation", children: [
1006
- /* @__PURE__ */ jsxs9("div", { className: "grid grid-cols-2 gap-2", children: [
1007
- /* @__PURE__ */ jsx12(FormField, { label: "Min length", children: /* @__PURE__ */ jsx12(
1008
- TextInput,
1205
+ textual && /* @__PURE__ */ jsxs11(Section2, { title: "Validation", children: [
1206
+ /* @__PURE__ */ jsxs11("div", { className: "grid grid-cols-2 gap-2", children: [
1207
+ /* @__PURE__ */ jsx16(FormField2, { label: "Min length", children: /* @__PURE__ */ jsx16(
1208
+ TextInput2,
1009
1209
  {
1010
1210
  type: "number",
1011
1211
  min: 0,
@@ -1013,8 +1213,8 @@ function FieldProperties({
1013
1213
  onChange: (e) => set({ minLength: num2(e.target.value) })
1014
1214
  }
1015
1215
  ) }),
1016
- /* @__PURE__ */ jsx12(FormField, { label: "Max length", children: /* @__PURE__ */ jsx12(
1017
- TextInput,
1216
+ /* @__PURE__ */ jsx16(FormField2, { label: "Max length", children: /* @__PURE__ */ jsx16(
1217
+ TextInput2,
1018
1218
  {
1019
1219
  type: "number",
1020
1220
  min: 0,
@@ -1022,17 +1222,17 @@ function FieldProperties({
1022
1222
  onChange: (e) => set({ maxLength: num2(e.target.value) })
1023
1223
  }
1024
1224
  ) }),
1025
- field.type === "number" && /* @__PURE__ */ jsxs9(Fragment3, { children: [
1026
- /* @__PURE__ */ jsx12(FormField, { label: "Min value", children: /* @__PURE__ */ jsx12(
1027
- TextInput,
1225
+ field.type === "number" && /* @__PURE__ */ jsxs11(Fragment3, { children: [
1226
+ /* @__PURE__ */ jsx16(FormField2, { label: "Min value", children: /* @__PURE__ */ jsx16(
1227
+ TextInput2,
1028
1228
  {
1029
1229
  type: "number",
1030
1230
  value: field.min ?? "",
1031
1231
  onChange: (e) => set({ min: num2(e.target.value) })
1032
1232
  }
1033
1233
  ) }),
1034
- /* @__PURE__ */ jsx12(FormField, { label: "Max value", children: /* @__PURE__ */ jsx12(
1035
- TextInput,
1234
+ /* @__PURE__ */ jsx16(FormField2, { label: "Max value", children: /* @__PURE__ */ jsx16(
1235
+ TextInput2,
1036
1236
  {
1037
1237
  type: "number",
1038
1238
  value: field.max ?? "",
@@ -1041,8 +1241,8 @@ function FieldProperties({
1041
1241
  ) })
1042
1242
  ] })
1043
1243
  ] }),
1044
- /* @__PURE__ */ jsx12(FormField, { label: "Regex (whole value must match)", children: /* @__PURE__ */ jsx12(
1045
- TextInput,
1244
+ /* @__PURE__ */ jsx16(FormField2, { label: "Regex (whole value must match)", children: /* @__PURE__ */ jsx16(
1245
+ TextInput2,
1046
1246
  {
1047
1247
  className: "font-mono",
1048
1248
  value: field.pattern ?? "",
@@ -1052,14 +1252,14 @@ function FieldProperties({
1052
1252
  ] }),
1053
1253
  CONDITIONS.map(([key, title]) => {
1054
1254
  const cond = field[key];
1055
- return /* @__PURE__ */ jsx12(Section2, { title, children: cond ? /* @__PURE__ */ jsx12(
1255
+ return /* @__PURE__ */ jsx16(Section2, { title, children: cond ? /* @__PURE__ */ jsx16(
1056
1256
  ConditionBuilder,
1057
1257
  {
1058
1258
  value: cond,
1059
1259
  fields: others,
1060
1260
  onChange: (c) => set({ [key]: c.rules.length ? c : void 0 })
1061
1261
  }
1062
- ) : /* @__PURE__ */ jsx12(
1262
+ ) : /* @__PURE__ */ jsx16(
1063
1263
  "button",
1064
1264
  {
1065
1265
  type: "button",
@@ -1070,9 +1270,9 @@ function FieldProperties({
1070
1270
  }
1071
1271
  ) }, key);
1072
1272
  }),
1073
- /* @__PURE__ */ jsx12(Section2, { title: "Position (% of page)", children: /* @__PURE__ */ jsxs9("div", { className: "grid grid-cols-2 gap-2", children: [
1074
- /* @__PURE__ */ jsx12(FormField, { label: "Page", children: /* @__PURE__ */ jsx12(
1075
- TextInput,
1273
+ /* @__PURE__ */ jsx16(Section2, { title: "Position (% of page)", children: /* @__PURE__ */ jsxs11("div", { className: "grid grid-cols-2 gap-2", children: [
1274
+ /* @__PURE__ */ jsx16(FormField2, { label: "Page", children: /* @__PURE__ */ jsx16(
1275
+ TextInput2,
1076
1276
  {
1077
1277
  type: "number",
1078
1278
  min: 1,
@@ -1085,9 +1285,9 @@ function FieldProperties({
1085
1285
  }
1086
1286
  }
1087
1287
  ) }),
1088
- /* @__PURE__ */ jsx12("span", {}),
1089
- ["x", "y", "width", "height"].map((k) => /* @__PURE__ */ jsx12(FormField, { label: { x: "Left", y: "Top", width: "Width", height: "Height" }[k], children: /* @__PURE__ */ jsx12(
1090
- TextInput,
1288
+ /* @__PURE__ */ jsx16("span", {}),
1289
+ ["x", "y", "width", "height"].map((k) => /* @__PURE__ */ jsx16(FormField2, { label: { x: "Left", y: "Top", width: "Width", height: "Height" }[k], children: /* @__PURE__ */ jsx16(
1290
+ TextInput2,
1091
1291
  {
1092
1292
  type: "number",
1093
1293
  step: 0.1,
@@ -1100,7 +1300,7 @@ function FieldProperties({
1100
1300
  }
1101
1301
 
1102
1302
  // src/react/components/LayoutElementView.tsx
1103
- import { jsx as jsx13 } from "react/jsx-runtime";
1303
+ import { jsx as jsx17 } from "react/jsx-runtime";
1104
1304
  var FAMILY = {
1105
1305
  sans: "Helvetica, Arial, sans-serif",
1106
1306
  serif: '"Times New Roman", Times, serif',
@@ -1114,9 +1314,9 @@ function LayoutElementView({
1114
1314
  style
1115
1315
  }) {
1116
1316
  const stroke = ptToCqh(e.strokeWidth ?? 1, pageHeight);
1117
- const base = { ...boxStyle({ page: e.page, ...e.position }), ...style };
1317
+ const base2 = { ...boxStyle({ page: e.page, ...e.position }), ...style };
1118
1318
  if (e.type === "line")
1119
- return /* @__PURE__ */ jsx13("svg", { className: `absolute overflow-visible ${className}`, style: base, "aria-hidden": true, children: /* @__PURE__ */ jsx13(
1319
+ return /* @__PURE__ */ jsx17("svg", { className: `absolute overflow-visible ${className}`, style: base2, "aria-hidden": true, children: /* @__PURE__ */ jsx17(
1120
1320
  "line",
1121
1321
  {
1122
1322
  x1: "0",
@@ -1127,12 +1327,12 @@ function LayoutElementView({
1127
1327
  style: { strokeWidth: stroke }
1128
1328
  }
1129
1329
  ) });
1130
- return /* @__PURE__ */ jsx13(
1330
+ return /* @__PURE__ */ jsx17(
1131
1331
  "div",
1132
1332
  {
1133
1333
  className: `absolute overflow-hidden whitespace-pre-wrap ${className}`,
1134
1334
  style: {
1135
- ...base,
1335
+ ...base2,
1136
1336
  background: e.fill,
1137
1337
  border: e.type === "box" && e.stroke ? `${stroke} solid ${e.stroke}` : void 0,
1138
1338
  color: e.color ?? "#000000",
@@ -1151,7 +1351,7 @@ function LayoutElementView({
1151
1351
 
1152
1352
  // src/react/components/PagesPanel.tsx
1153
1353
  import { ArrowDown, ArrowUp, FilePlus2, Trash2 } from "lucide-react";
1154
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1354
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
1155
1355
  function PagesPanel({
1156
1356
  pages,
1157
1357
  counts,
@@ -1160,36 +1360,36 @@ function PagesPanel({
1160
1360
  onInsertBlank,
1161
1361
  onGoTo
1162
1362
  }) {
1163
- return /* @__PURE__ */ jsxs10("div", { className: "text-[12.5px]", children: [
1164
- /* @__PURE__ */ jsxs10(
1363
+ return /* @__PURE__ */ jsxs12("div", { className: "text-[12.5px]", children: [
1364
+ /* @__PURE__ */ jsxs12(
1165
1365
  "button",
1166
1366
  {
1167
1367
  type: "button",
1168
1368
  className: "flex w-full cursor-pointer items-center gap-1.5 border-b border-line-soft px-3 py-2 text-primary hover:bg-primary-soft",
1169
1369
  onClick: () => onInsertBlank(0),
1170
1370
  children: [
1171
- /* @__PURE__ */ jsx14(FilePlus2, { size: 14 }),
1371
+ /* @__PURE__ */ jsx18(FilePlus2, { size: 14 }),
1172
1372
  " Blank page at the start"
1173
1373
  ]
1174
1374
  }
1175
1375
  ),
1176
- /* @__PURE__ */ jsx14("ol", { className: "m-0 list-none p-0", children: pages.map((p, i) => {
1376
+ /* @__PURE__ */ jsx18("ol", { className: "m-0 list-none p-0", children: pages.map((p, i) => {
1177
1377
  const n = i + 1;
1178
- return /* @__PURE__ */ jsxs10("li", { className: "flex items-center gap-1 border-b border-line-soft px-2 py-1", children: [
1179
- /* @__PURE__ */ jsxs10(
1378
+ return /* @__PURE__ */ jsxs12("li", { className: "flex items-center gap-1 border-b border-line-soft px-2 py-1", children: [
1379
+ /* @__PURE__ */ jsxs12(
1180
1380
  "button",
1181
1381
  {
1182
1382
  type: "button",
1183
1383
  className: "min-w-0 flex-1 cursor-pointer truncate text-left",
1184
1384
  onClick: () => onGoTo(n),
1185
1385
  children: [
1186
- /* @__PURE__ */ jsxs10("b", { children: [
1386
+ /* @__PURE__ */ jsxs12("b", { children: [
1187
1387
  "Page ",
1188
1388
  n
1189
1389
  ] }),
1190
1390
  " ",
1191
- /* @__PURE__ */ jsx14("span", { className: "text-muted", children: p.source ? `\xB7 original p.${p.source}` : "\xB7 blank" }),
1192
- counts[n] ? /* @__PURE__ */ jsxs10("span", { className: "text-faint", children: [
1391
+ /* @__PURE__ */ jsx18("span", { className: "text-muted", children: p.source ? `\xB7 original p.${p.source}` : "\xB7 blank" }),
1392
+ counts[n] ? /* @__PURE__ */ jsxs12("span", { className: "text-faint", children: [
1193
1393
  " \xB7 ",
1194
1394
  counts[n],
1195
1395
  " items"
@@ -1197,8 +1397,8 @@ function PagesPanel({
1197
1397
  ]
1198
1398
  }
1199
1399
  ),
1200
- /* @__PURE__ */ jsx14(
1201
- IconButton,
1400
+ /* @__PURE__ */ jsx18(
1401
+ IconButton2,
1202
1402
  {
1203
1403
  icon: ArrowUp,
1204
1404
  title: "Move page up",
@@ -1207,8 +1407,8 @@ function PagesPanel({
1207
1407
  onClick: () => onMove(n, -1)
1208
1408
  }
1209
1409
  ),
1210
- /* @__PURE__ */ jsx14(
1211
- IconButton,
1410
+ /* @__PURE__ */ jsx18(
1411
+ IconButton2,
1212
1412
  {
1213
1413
  icon: ArrowDown,
1214
1414
  title: "Move page down",
@@ -1217,8 +1417,8 @@ function PagesPanel({
1217
1417
  onClick: () => onMove(n, 1)
1218
1418
  }
1219
1419
  ),
1220
- /* @__PURE__ */ jsx14(
1221
- IconButton,
1420
+ /* @__PURE__ */ jsx18(
1421
+ IconButton2,
1222
1422
  {
1223
1423
  icon: FilePlus2,
1224
1424
  title: "Add blank page after",
@@ -1226,8 +1426,8 @@ function PagesPanel({
1226
1426
  onClick: () => onInsertBlank(n)
1227
1427
  }
1228
1428
  ),
1229
- /* @__PURE__ */ jsx14(
1230
- IconButton,
1429
+ /* @__PURE__ */ jsx18(
1430
+ IconButton2,
1231
1431
  {
1232
1432
  icon: Trash2,
1233
1433
  title: "Delete page",
@@ -1243,7 +1443,7 @@ function PagesPanel({
1243
1443
  }
1244
1444
 
1245
1445
  // src/react/components/PdfPages.tsx
1246
- import { useEffect, useRef as useRef3, useState as useState4 } from "react";
1446
+ import { useEffect as useEffect2, useRef as useRef4, useState as useState5 } from "react";
1247
1447
 
1248
1448
  // src/react/pdfjs.ts
1249
1449
  var workerSrc;
@@ -1263,7 +1463,7 @@ var loadPdfJs = () => pdfjs ??= import("pdfjs-dist").then((lib) => {
1263
1463
  });
1264
1464
 
1265
1465
  // src/react/components/PdfPages.tsx
1266
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1466
+ import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1267
1467
  async function readPdf(src) {
1268
1468
  const v = typeof src === "function" ? await src() : src;
1269
1469
  if (typeof v === "string" || v instanceof URL) {
@@ -1282,14 +1482,14 @@ function PdfPages({
1282
1482
  onLoaded,
1283
1483
  onError
1284
1484
  }) {
1285
- const [doc, setDoc] = useState4(null);
1286
- const [sizes2, setSizes] = useState4([]);
1287
- const loadRef = useRef3(load);
1288
- const callbacks = useRef3({ onLoaded, onError });
1289
- useEffect(() => {
1485
+ const [doc, setDoc] = useState5(null);
1486
+ const [sizes2, setSizes] = useState5([]);
1487
+ const loadRef = useRef4(load);
1488
+ const callbacks = useRef4({ onLoaded, onError });
1489
+ useEffect2(() => {
1290
1490
  callbacks.current = { onLoaded, onError };
1291
1491
  });
1292
- useEffect(() => {
1492
+ useEffect2(() => {
1293
1493
  let live = true;
1294
1494
  let opened = null;
1295
1495
  Promise.all([loadPdfJs(), readPdf(loadRef.current)]).then(([lib, data]) => lib.getDocument({ data, ...{ isEvalSupported: false } }).promise).then(async (pdf) => {
@@ -1309,11 +1509,11 @@ function PdfPages({
1309
1509
  void opened?.destroy();
1310
1510
  };
1311
1511
  }, []);
1312
- if (!doc) return /* @__PURE__ */ jsx15("p", { className: "p-6 text-center text-[13px] text-muted", children: "Loading document\u2026" });
1512
+ if (!doc) return /* @__PURE__ */ jsx19("p", { className: "p-6 text-center text-[13px] text-muted", children: "Loading document\u2026" });
1313
1513
  const shown = pages ?? sizes2.map((s, i) => ({ source: i + 1, ...s }));
1314
1514
  return (
1315
1515
  // min-w-full + w-max: centred when narrower than the panel, fully scrollable (left edge too) when wider.
1316
- /* @__PURE__ */ jsx15("div", { className: "flex w-max min-w-full flex-col gap-4 p-4", children: shown.map((p, i) => /* @__PURE__ */ jsx15(
1516
+ /* @__PURE__ */ jsx19("div", { className: "flex w-max min-w-full flex-col gap-4 p-4", children: shown.map((p, i) => /* @__PURE__ */ jsx19(
1317
1517
  PdfPage,
1318
1518
  {
1319
1519
  doc,
@@ -1337,20 +1537,20 @@ function PdfPage({
1337
1537
  withText,
1338
1538
  overlay
1339
1539
  }) {
1340
- const box2 = useRef3(null);
1341
- const canvas = useRef3(null);
1342
- const [visible, setVisible] = useState4(false);
1343
- const [lines, setLines] = useState4(null);
1540
+ const box2 = useRef4(null);
1541
+ const canvas = useRef4(null);
1542
+ const [visible, setVisible] = useState5(false);
1543
+ const [lines, setLines] = useState5(null);
1344
1544
  const width = size.width * BASE_SCALE * zoom;
1345
1545
  const height = size.height * BASE_SCALE * zoom;
1346
- useEffect(() => {
1546
+ useEffect2(() => {
1347
1547
  const el = box2.current;
1348
1548
  if (!el || typeof IntersectionObserver === "undefined") return setVisible(true);
1349
1549
  const io = new IntersectionObserver(([e]) => e.isIntersecting && setVisible(true), { rootMargin: "600px" });
1350
1550
  io.observe(el);
1351
1551
  return () => io.disconnect();
1352
1552
  }, []);
1353
- useEffect(() => {
1553
+ useEffect2(() => {
1354
1554
  if (!visible || !source || !canvas.current) return;
1355
1555
  let task = null;
1356
1556
  let live = true;
@@ -1370,7 +1570,7 @@ function PdfPage({
1370
1570
  task?.cancel();
1371
1571
  };
1372
1572
  }, [doc, source, visible, zoom]);
1373
- useEffect(() => {
1573
+ useEffect2(() => {
1374
1574
  if (!withText || !visible || !source || lines?.source === source) return;
1375
1575
  let live = true;
1376
1576
  void doc.getPage(source).then(async (page) => {
@@ -1388,7 +1588,7 @@ function PdfPage({
1388
1588
  live = false;
1389
1589
  };
1390
1590
  }, [doc, source, visible, withText, lines]);
1391
- return /* @__PURE__ */ jsxs11(
1591
+ return /* @__PURE__ */ jsxs13(
1392
1592
  "div",
1393
1593
  {
1394
1594
  ref: box2,
@@ -1396,8 +1596,8 @@ function PdfPage({
1396
1596
  className: "relative mx-auto flex-none bg-white shadow-[0_1px_6px_rgba(15,23,42,0.18)]",
1397
1597
  style: { width, height },
1398
1598
  children: [
1399
- source && /* @__PURE__ */ jsx15("canvas", { ref: canvas, className: "absolute inset-0 h-full w-full", "aria-label": `Page ${pageNo}` }),
1400
- /* @__PURE__ */ jsx15("div", { className: "absolute inset-0 [container-type:size]", children: overlay?.(pageNo, withText && lines?.source === source ? lines.lines : void 0) })
1599
+ source && /* @__PURE__ */ jsx19("canvas", { ref: canvas, className: "absolute inset-0 h-full w-full", "aria-label": `Page ${pageNo}` }),
1600
+ /* @__PURE__ */ jsx19("div", { className: "absolute inset-0 [container-type:size]", children: overlay?.(pageNo, withText && lines?.source === source ? lines.lines : void 0) })
1401
1601
  ]
1402
1602
  }
1403
1603
  );
@@ -1405,28 +1605,28 @@ function PdfPage({
1405
1605
 
1406
1606
  // src/react/components/ZoomControls.tsx
1407
1607
  import { Minus, Plus as Plus2 } from "lucide-react";
1408
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1608
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1409
1609
  function ZoomControls({ zoom, onChange }) {
1410
1610
  const lower = [...ZOOMS].reverse().find((z) => z < zoom - 0.01);
1411
1611
  const higher = ZOOMS.find((z) => z > zoom + 0.01);
1412
- return /* @__PURE__ */ jsxs12("div", { className: "inline-flex items-center gap-1 text-[13px]", children: [
1413
- /* @__PURE__ */ jsx16(IconButton, { icon: Minus, title: "Zoom out", disabled: lower == null, onClick: () => onChange(lower) }),
1414
- /* @__PURE__ */ jsxs12("span", { className: "w-12 text-center tabular-nums", children: [
1612
+ return /* @__PURE__ */ jsxs14("div", { className: "inline-flex items-center gap-1 text-[13px]", children: [
1613
+ /* @__PURE__ */ jsx20(IconButton2, { icon: Minus, title: "Zoom out", disabled: lower == null, onClick: () => onChange(lower) }),
1614
+ /* @__PURE__ */ jsxs14("span", { className: "w-12 text-center tabular-nums", children: [
1415
1615
  Math.round(zoom * 100),
1416
1616
  "%"
1417
1617
  ] }),
1418
- /* @__PURE__ */ jsx16(IconButton, { icon: Plus2, title: "Zoom in", disabled: higher == null, onClick: () => onChange(higher) })
1618
+ /* @__PURE__ */ jsx20(IconButton2, { icon: Plus2, title: "Zoom in", disabled: higher == null, onClick: () => onChange(higher) })
1419
1619
  ] });
1420
1620
  }
1421
1621
 
1422
1622
  // src/react/hooks/useFitZoom.ts
1423
- import { useEffect as useEffect2, useState as useState5 } from "react";
1623
+ import { useEffect as useEffect3, useState as useState6 } from "react";
1424
1624
  var PADDING = 32;
1425
1625
  function useFitZoom(pageWidthPt) {
1426
- const [el, ref] = useState5(null);
1427
- const [fit, setFit] = useState5(1);
1428
- const [chosen, setZoom] = useState5(null);
1429
- useEffect2(() => {
1626
+ const [el, ref] = useState6(null);
1627
+ const [fit, setFit] = useState6(1);
1628
+ const [chosen, setZoom] = useState6(null);
1629
+ useEffect3(() => {
1430
1630
  if (!el) return;
1431
1631
  const measure = () => {
1432
1632
  const room = el.clientWidth - PADDING;
@@ -1443,8 +1643,8 @@ function useFitZoom(pageWidthPt) {
1443
1643
  function useCompact() {
1444
1644
  const query = "(max-width: 767px), (pointer: coarse)";
1445
1645
  const get = () => typeof window !== "undefined" && !!window.matchMedia?.(query).matches;
1446
- const [compact, setCompact] = useState5(get);
1447
- useEffect2(() => {
1646
+ const [compact, setCompact] = useState6(get);
1647
+ useEffect3(() => {
1448
1648
  const m = window.matchMedia?.(query);
1449
1649
  if (!m) return;
1450
1650
  const on = () => setCompact(m.matches);
@@ -1454,11 +1654,14 @@ function useCompact() {
1454
1654
  return compact;
1455
1655
  }
1456
1656
 
1657
+ // src/react/hooks/useFormEditor.ts
1658
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo, useRef as useRef5, useState as useState8 } from "react";
1659
+
1457
1660
  // src/react/hooks/useUndo.ts
1458
- import { useCallback, useState as useState6 } from "react";
1661
+ import { useCallback, useState as useState7 } from "react";
1459
1662
  var LIMIT = 100;
1460
1663
  function useUndo(initial) {
1461
- const [state, setState] = useState6({ past: [], present: initial, future: [] });
1664
+ const [state, setState] = useState7({ past: [], present: initial, future: [] });
1462
1665
  const set = useCallback((next, record = true) => {
1463
1666
  setState((s) => {
1464
1667
  const value = typeof next === "function" ? next(s.present) : next;
@@ -1495,51 +1698,39 @@ function useUndo(initial) {
1495
1698
  };
1496
1699
  }
1497
1700
 
1498
- // src/react/FormEditor.tsx
1499
- import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1701
+ // src/react/hooks/useFormEditor.ts
1500
1702
  var GRID = 5e-3;
1501
- var DRAG_TYPE = "application/x-doc-item";
1502
1703
  var EMPTY = { fields: [], layout: null };
1503
1704
  var isDefault = (l, sizes2) => !l.elements.length && l.pages.length === sizes2.length && l.pages.every((p, i) => p.source === i + 1);
1504
- function FormEditor({
1505
- pdf: pdfSource,
1705
+ function useFormEditor({
1506
1706
  initialValue = EMPTY,
1507
1707
  onSave,
1508
1708
  onChange,
1509
- title,
1510
- onBack,
1511
- toolbar,
1512
- banner,
1513
- tabs = [],
1514
1709
  autoDetect,
1710
+ onDetected,
1711
+ keyboard = true,
1712
+ warnUnsaved = true,
1713
+ fieldTypes = FIELD_TYPES,
1714
+ elementTools = Object.keys(ELEMENT_TOOLS),
1515
1715
  notify = () => {
1516
1716
  },
1517
- onError: notifyError = (e) => console.error(e),
1518
- className = "h-full",
1519
- ref
1717
+ onError: notifyError = (e) => console.error(e)
1520
1718
  }) {
1521
- const [sizes2, setSizes] = useState7(null);
1719
+ const [sizes2, setSizes] = useState8(null);
1522
1720
  const history = useUndo(initialValue);
1523
1721
  const { value: doc, set } = history;
1524
- const [saved, setSaved] = useState7(initialValue);
1525
- const [selection, setSelection] = useState7(null);
1526
- const [tab, setTab] = useState7("props");
1527
- const [drawer, setDrawer] = useState7(null);
1528
- const [armed, setArmed] = useState7(null);
1529
- const compact = useCompact();
1530
- const [snapOn, setSnapOn] = useState7(true);
1531
- const [editText, setEditText] = useState7(false);
1532
- const [preview, setPreview] = useState7(false);
1533
- const [previewAnswers, setPreviewAnswers] = useState7({});
1534
- const [busy, setBusy] = useState7(false);
1535
- const [confirmDeletePage, setConfirmDeletePage] = useState7(null);
1536
- const [loadError, setLoadError] = useState7("");
1537
- const [pdf, setPdf] = useState7(null);
1538
- const [detecting, setDetecting] = useState7(null);
1539
- const autoDetected = useRef4(false);
1540
- const drag = useRef4(null);
1541
- const scroller = useRef4(null);
1542
- const { zoom, setZoom, ref: fitRef } = useFitZoom(Math.max(...(sizes2 ?? [{ width: 595 }]).map((p) => p.width)));
1722
+ const [saved, setSaved] = useState8(initialValue);
1723
+ const [selection, setSelection] = useState8(null);
1724
+ const [armed, setArmed] = useState8(null);
1725
+ const [snapOn, setSnapOn] = useState8(true);
1726
+ const [editText, setEditText] = useState8(false);
1727
+ const [preview, setPreviewState] = useState8(false);
1728
+ const [previewAnswers, setPreviewAnswers] = useState8({});
1729
+ const [busy, setBusy] = useState8(false);
1730
+ const [pdf, setPdf] = useState8(null);
1731
+ const [detecting, setDetecting] = useState8(null);
1732
+ const autoDetected = useRef5(false);
1733
+ const drag = useRef5(null);
1543
1734
  const { fields } = doc;
1544
1735
  const layout = doc.layout ?? (sizes2 ? defaultLayout(sizes2) : null);
1545
1736
  const pageCount = layout?.pages.length ?? 1;
@@ -1551,20 +1742,20 @@ function FormEditor({
1551
1742
  () => sizes2 ? [...doc.layout ? validateLayout(doc.layout, sizes2.length) : [], ...validateFields(fields, pageCount)] : [],
1552
1743
  [sizes2, doc.layout, fields, pageCount]
1553
1744
  );
1554
- const onChangeRef = useRef4(onChange);
1555
- useEffect3(() => {
1745
+ const onChangeRef = useRef5(onChange);
1746
+ useEffect4(() => {
1556
1747
  onChangeRef.current = onChange;
1557
1748
  });
1558
- const first = useRef4(true);
1559
- useEffect3(() => {
1749
+ const first = useRef5(true);
1750
+ useEffect4(() => {
1560
1751
  if (first.current) first.current = false;
1561
1752
  else onChangeRef.current?.(doc);
1562
1753
  }, [doc]);
1563
1754
  const withDoc = useCallback2(
1564
1755
  (fn, record = true) => set((cur) => {
1565
- const base = cur.layout ?? (sizes2 ? defaultLayout(sizes2) : null);
1566
- if (!base) return cur;
1567
- return fn({ fields: cur.fields, layout: base });
1756
+ const base2 = cur.layout ?? (sizes2 ? defaultLayout(sizes2) : null);
1757
+ if (!base2) return cur;
1758
+ return fn({ fields: cur.fields, layout: base2 });
1568
1759
  }, record),
1569
1760
  [set, sizes2]
1570
1761
  );
@@ -1576,10 +1767,6 @@ function FormEditor({
1576
1767
  (fn, record = true) => withDoc((d) => ({ ...d, layout: { ...d.layout, elements: fn(d.layout.elements) } }), record),
1577
1768
  [withDoc]
1578
1769
  );
1579
- const select = (s) => {
1580
- setSelection(s);
1581
- if (s) setTab("props");
1582
- };
1583
1770
  const updateField = useCallback2((f) => setFields((l) => withField(l, f)), [setFields]);
1584
1771
  const updateElement = useCallback2(
1585
1772
  (e) => {
@@ -1605,7 +1792,19 @@ function FormEditor({
1605
1792
  setSelection({ kind: "element", id: copy.id });
1606
1793
  }
1607
1794
  }, [selectedField, selectedElement, fields, setFields, setElements]);
1608
- useEffect3(() => {
1795
+ const nudge = useCallback2(
1796
+ (dx, dy) => {
1797
+ if (selectedField) updateField({ ...selectedField, position: moveBy(selectedField.position, dx, dy) });
1798
+ if (selectedElement) {
1799
+ const { page: _p, ...position } = moveBy({ page: selectedElement.page, ...selectedElement.position }, dx, dy);
1800
+ void _p;
1801
+ updateElement({ ...selectedElement, position });
1802
+ }
1803
+ },
1804
+ [selectedField, selectedElement, updateField, updateElement]
1805
+ );
1806
+ useEffect4(() => {
1807
+ if (!keyboard) return;
1609
1808
  const onKey = (e) => {
1610
1809
  const t = e.target;
1611
1810
  if (preview || t.closest("input, textarea, select, [contenteditable]")) return;
@@ -1627,42 +1826,27 @@ function FormEditor({
1627
1826
  else if (e.key.startsWith("Arrow")) {
1628
1827
  e.preventDefault();
1629
1828
  const step = e.shiftKey ? 0.01 : 2e-3;
1630
- const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
1631
- const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
1632
- if (selectedField) updateField({ ...selectedField, position: moveBy(selectedField.position, dx, dy) });
1633
- if (selectedElement) {
1634
- const { page: _p, ...position } = moveBy({ page: selectedElement.page, ...selectedElement.position }, dx, dy);
1635
- void _p;
1636
- updateElement({ ...selectedElement, position });
1637
- }
1829
+ nudge(
1830
+ e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0,
1831
+ e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0
1832
+ );
1638
1833
  }
1639
1834
  };
1640
1835
  window.addEventListener("keydown", onKey);
1641
1836
  return () => window.removeEventListener("keydown", onKey);
1642
- }, [
1643
- preview,
1644
- armed,
1645
- selection,
1646
- selectedField,
1647
- selectedElement,
1648
- history,
1649
- removeSelected,
1650
- duplicateSelected,
1651
- updateField,
1652
- updateElement
1653
- ]);
1654
- useEffect3(() => {
1655
- if (!dirty) return;
1837
+ }, [keyboard, preview, armed, selection, history, removeSelected, duplicateSelected, nudge]);
1838
+ useEffect4(() => {
1839
+ if (!dirty || !warnUnsaved) return;
1656
1840
  const warn = (e) => e.preventDefault();
1657
1841
  window.addEventListener("beforeunload", warn);
1658
1842
  return () => window.removeEventListener("beforeunload", warn);
1659
- }, [dirty]);
1843
+ }, [dirty, warnUnsaved]);
1660
1844
  function startDrag(e, sel, start, mode) {
1661
1845
  if (e.button !== 0) return;
1662
1846
  e.stopPropagation();
1663
1847
  const pageEl = e.currentTarget.closest("[data-page]");
1664
1848
  if (!pageEl) return;
1665
- select(sel);
1849
+ setSelection(sel);
1666
1850
  history.checkpoint();
1667
1851
  drag.current = { sel, mode, x: e.clientX, y: e.clientY, start, rect: pageEl.getBoundingClientRect() };
1668
1852
  e.currentTarget.setPointerCapture(e.pointerId);
@@ -1698,31 +1882,35 @@ function FormEditor({
1698
1882
  false
1699
1883
  );
1700
1884
  }
1701
- const endDrag = () => drag.current = null;
1885
+ const endDrag = () => {
1886
+ drag.current = null;
1887
+ };
1702
1888
  function placeNew(key, page, clientX, clientY, rect) {
1703
1889
  const [kind, what] = key.split(":");
1704
1890
  const { x, y } = toPage(clientX, clientY, rect);
1705
- if (kind === "field" && FIELD_TYPES.includes(what)) {
1891
+ if (kind === "field" && fieldTypes.includes(what)) {
1706
1892
  const f = newField(what, page, x, y, fields);
1707
1893
  setFields((l) => [...l, f]);
1708
- select({ kind: "field", id: f.id });
1894
+ setSelection({ kind: "field", id: f.id });
1709
1895
  return true;
1710
1896
  }
1711
- if (kind === "element" && what in ELEMENT_TOOLS) {
1897
+ if (kind === "element" && elementTools.includes(what)) {
1712
1898
  const el = newElement(what, page, x, y);
1713
1899
  setElements((l) => [...l, el]);
1714
- select({ kind: "element", id: el.id });
1900
+ setSelection({ kind: "element", id: el.id });
1715
1901
  return true;
1716
1902
  }
1717
1903
  return false;
1718
1904
  }
1719
- function dropNew(e, page) {
1720
- const key = e.dataTransfer.getData(DRAG_TYPE);
1721
- if (placeNew(key, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect())) e.preventDefault();
1722
- }
1723
- function goToPage(n) {
1724
- scroller.current?.querySelector(`[data-page="${n}"]`)?.scrollIntoView({ block: "start" });
1905
+ function editLine(line, page) {
1906
+ const el = replaceLine(line, page);
1907
+ setElements((l) => [...l, el]);
1908
+ setSelection({ kind: "element", id: el.id });
1725
1909
  }
1910
+ const onDetectedRef = useRef5(onDetected);
1911
+ useEffect4(() => {
1912
+ onDetectedRef.current = onDetected;
1913
+ });
1726
1914
  const detect = useCallback2(
1727
1915
  async (auto = false) => {
1728
1916
  if (!pdf || !layout || detecting) return;
@@ -1740,7 +1928,7 @@ function FormEditor({
1740
1928
  );
1741
1929
  if (found.length) {
1742
1930
  setFields((l) => [...l, ...found]);
1743
- setTab("fields");
1931
+ onDetectedRef.current?.(found);
1744
1932
  }
1745
1933
  notify(
1746
1934
  found.length ? `${found.length} fields suggested${stop.signal.aborted ? " (stopped early)" : ""}. Check the dashed ones, then Save.` : auto ? "No fields found automatically. Drag them in from the left." : "No new fields found"
@@ -1753,7 +1941,7 @@ function FormEditor({
1753
1941
  },
1754
1942
  [pdf, layout, detecting, fields, setFields, notify, notifyError]
1755
1943
  );
1756
- useEffect3(() => {
1944
+ useEffect4(() => {
1757
1945
  if (autoDetected.current || !pdf || !layout) return;
1758
1946
  const fresh = autoDetect ?? (!initialValue.fields.length && !initialValue.layout);
1759
1947
  if (!fresh) {
@@ -1786,33 +1974,152 @@ function FormEditor({
1786
1974
  setBusy(false);
1787
1975
  }
1788
1976
  }
1789
- const saveRef = useRef4(save);
1790
- useEffect3(() => {
1791
- saveRef.current = save;
1977
+ const counts = {};
1978
+ for (const f of fields) counts[f.position.page] = (counts[f.position.page] ?? 0) + 1;
1979
+ for (const el of layout?.elements ?? []) counts[el.page] = (counts[el.page] ?? 0) + 1;
1980
+ return {
1981
+ doc,
1982
+ fields,
1983
+ /** The layout as shown (the original pages until changed); null until the PDF has loaded. */
1984
+ layout,
1985
+ pageCount,
1986
+ /** Page size in PDF points, for drawing layout text at the right size. */
1987
+ pageHeight: (page) => layout?.pages[page - 1]?.height ?? 842,
1988
+ /** Fields + layout items per page. */
1989
+ counts,
1990
+ dirty,
1991
+ /** Validation messages; save() refuses while there are any. */
1992
+ problems,
1993
+ busy,
1994
+ save,
1995
+ undo: history.undo,
1996
+ redo: history.redo,
1997
+ canUndo: history.canUndo,
1998
+ canRedo: history.canRedo,
1999
+ /** Changes the document; `record: false` = no undo step (live drags). */
2000
+ setFields,
2001
+ setElements,
2002
+ updateField,
2003
+ updateElement,
2004
+ reorderField: (id, step) => setFields((l) => reorder(l, id, step)),
2005
+ movePage: (n, step) => withDoc((d) => movePage(d, n, step)),
2006
+ insertBlankPage: (n) => withDoc((d) => insertBlankPage(d, n)),
2007
+ deletePage: (n) => {
2008
+ setSelection(null);
2009
+ withDoc((d) => deletePage(d, n));
2010
+ },
2011
+ selection,
2012
+ setSelection,
2013
+ selectedField,
2014
+ selectedElement,
2015
+ removeSelected,
2016
+ duplicateSelected,
2017
+ nudge,
2018
+ startDrag,
2019
+ onDrag,
2020
+ endDrag,
2021
+ /** Tools for a palette: drag with `dataTransfer` / tap to arm, then placeNew. */
2022
+ fieldTools: fieldTypes.map((t) => `field:${t}`),
2023
+ elementTools: elementTools.map((t) => `element:${t}`),
2024
+ armed,
2025
+ setArmed,
2026
+ placeNew,
2027
+ editText,
2028
+ setEditText,
2029
+ editLine,
2030
+ snapOn,
2031
+ setSnapOn,
2032
+ preview,
2033
+ /** Preview starts with empty answers; leaving it ends Edit text. */
2034
+ setPreview: (on) => {
2035
+ setPreviewState(on);
2036
+ setPreviewAnswers({});
2037
+ setEditText(false);
2038
+ },
2039
+ previewAnswers,
2040
+ setPreviewAnswers,
2041
+ previewStates: fieldStates(fields, previewAnswers),
2042
+ pdf,
2043
+ /** Pass to PdfPages' onLoaded. */
2044
+ onPdfLoaded: (s, d) => {
2045
+ setSizes(s);
2046
+ setPdf(d);
2047
+ },
2048
+ pageSizes: sizes2,
2049
+ detect,
2050
+ /** Progress while detecting; `stop.abort()` keeps what was found so far. */
2051
+ detecting
2052
+ };
2053
+ }
2054
+
2055
+ // src/react/FormEditor.tsx
2056
+ import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
2057
+ var DRAG_TYPE = "application/x-doc-item";
2058
+ function FormEditor({
2059
+ pdf: pdfSource,
2060
+ title,
2061
+ onBack,
2062
+ toolbar,
2063
+ banner,
2064
+ tabs = [],
2065
+ components,
2066
+ hide = [],
2067
+ className = "h-full",
2068
+ ref,
2069
+ ...options
2070
+ }) {
2071
+ const shows = (p) => !hide.includes(p);
2072
+ const panelTabs = [
2073
+ ...shows("propertiesTab") ? ["props"] : [],
2074
+ ...shows("fieldsTab") ? ["fields"] : [],
2075
+ ...shows("pagesTab") ? ["pages"] : [],
2076
+ ...tabs.map((t) => t.id)
2077
+ ];
2078
+ const [tab, setTabState] = useState9(panelTabs[0] ?? "props");
2079
+ const setTab = (t) => panelTabs.includes(t) && setTabState(t);
2080
+ const ed = useFormEditor({
2081
+ ...options,
2082
+ onDetected: (found) => {
2083
+ setTab("fields");
2084
+ options.onDetected?.(found);
2085
+ }
2086
+ });
2087
+ const { fields, layout, pageCount, selection, selectedField, selectedElement, preview, armed, snapOn, editText } = ed;
2088
+ const [drawer, setDrawer] = useState9(null);
2089
+ const compact = useCompact();
2090
+ const [confirmDeletePage, setConfirmDeletePage] = useState9(null);
2091
+ const [loadError, setLoadError] = useState9("");
2092
+ const scroller = useRef6(null);
2093
+ const { zoom, setZoom, ref: fitRef } = useFitZoom(Math.max(...(ed.pageSizes ?? [{ width: 595 }]).map((p) => p.width)));
2094
+ useEffect5(() => {
2095
+ if (selection) setTab("props");
2096
+ }, [selection]);
2097
+ function goToPage(n) {
2098
+ scroller.current?.querySelector(`[data-page="${n}"]`)?.scrollIntoView({ block: "start" });
2099
+ }
2100
+ const saveRef = useRef6(ed.save);
2101
+ useEffect5(() => {
2102
+ saveRef.current = ed.save;
1792
2103
  });
1793
2104
  useImperativeHandle(
1794
2105
  ref,
1795
2106
  () => ({
1796
- save: () => dirty ? saveRef.current() : Promise.resolve(problems.length === 0),
1797
- dirty,
1798
- value: doc
2107
+ save: () => ed.dirty ? saveRef.current() : Promise.resolve(ed.problems.length === 0),
2108
+ dirty: ed.dirty,
2109
+ value: ed.doc
1799
2110
  }),
1800
- [dirty, doc, problems]
2111
+ [ed.dirty, ed.doc, ed.problems]
1801
2112
  );
1802
- if (loadError) return /* @__PURE__ */ jsx17("p", { className: "fl-root p-6 text-center text-sm text-danger", children: loadError });
1803
- const previewStates = fieldStates(fields, previewAnswers);
1804
- const counts = {};
1805
- for (const f of fields) counts[f.position.page] = (counts[f.position.page] ?? 0) + 1;
1806
- for (const el of layout?.elements ?? []) counts[el.page] = (counts[el.page] ?? 0) + 1;
1807
- const pageHeight = (page) => layout?.pages[page - 1]?.height ?? 842;
1808
- const toolItem = (key, label) => /* @__PURE__ */ jsx17(
2113
+ if (loadError) return /* @__PURE__ */ jsx21("p", { className: "fl-root p-6 text-center text-sm text-danger", children: loadError });
2114
+ const { previewStates, previewAnswers, counts, pageHeight } = ed;
2115
+ const toolItem = (key, label) => /* @__PURE__ */ jsx21(
1809
2116
  "button",
1810
2117
  {
1811
2118
  type: "button",
1812
2119
  draggable: true,
1813
2120
  onDragStart: (e) => e.dataTransfer.setData(DRAG_TYPE, key),
1814
2121
  onClick: () => {
1815
- setArmed((a) => a === key ? null : key);
2122
+ ed.setArmed(armed === key ? null : key);
1816
2123
  setDrawer(null);
1817
2124
  },
1818
2125
  "aria-pressed": armed === key,
@@ -1822,11 +2129,11 @@ function FormEditor({
1822
2129
  key
1823
2130
  );
1824
2131
  const armedLabel = armed ? armed.startsWith("field:") ? TYPE_LABEL[armed.slice(6)] : ELEMENT_TOOLS[armed.slice(8)]?.label : "";
1825
- return /* @__PURE__ */ jsxs13("div", { className: `fl-root flex min-h-[640px] flex-col bg-white text-ink ${className}`, children: [
1826
- /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap items-center gap-2 border-b border-line bg-white px-4 py-2.5", children: [
1827
- onBack && /* @__PURE__ */ jsx17(Button, { variant: "secondary", size: "sm", icon: ArrowLeft, onClick: onBack, children: "Back" }),
1828
- !preview && /* @__PURE__ */ jsx17(
1829
- Button,
2132
+ return /* @__PURE__ */ jsx21(FormliftProvider, { components, children: /* @__PURE__ */ jsxs15("div", { className: `fl-root flex min-h-[640px] flex-col bg-white text-ink ${className}`, children: [
2133
+ /* @__PURE__ */ jsxs15("div", { className: "flex flex-wrap items-center gap-2 border-b border-line bg-white px-4 py-2.5", children: [
2134
+ onBack && shows("back") && /* @__PURE__ */ jsx21(Button2, { variant: "secondary", size: "sm", icon: ArrowLeft, onClick: onBack, children: "Back" }),
2135
+ !preview && shows("tools") && /* @__PURE__ */ jsx21(
2136
+ Button2,
1830
2137
  {
1831
2138
  variant: drawer === "tools" ? "primary" : "secondary",
1832
2139
  size: "sm",
@@ -1836,8 +2143,8 @@ function FormEditor({
1836
2143
  children: "Add"
1837
2144
  }
1838
2145
  ),
1839
- /* @__PURE__ */ jsx17(
1840
- Button,
2146
+ shows("panel") && /* @__PURE__ */ jsx21(
2147
+ Button2,
1841
2148
  {
1842
2149
  variant: drawer === "panel" ? "primary" : "secondary",
1843
2150
  size: "sm",
@@ -1847,127 +2154,119 @@ function FormEditor({
1847
2154
  children: selection ? "Properties" : "Panel"
1848
2155
  }
1849
2156
  ),
1850
- title != null && /* @__PURE__ */ jsx17("h2", { className: "m-0 min-w-0 truncate text-[17px] font-semibold", children: title }),
1851
- /* @__PURE__ */ jsx17("span", { className: "text-[12px] text-muted", children: dirty ? "Unsaved changes" : "All changes saved" }),
1852
- /* @__PURE__ */ jsxs13("div", { className: "ml-auto flex flex-wrap items-center gap-1.5", children: [
1853
- /* @__PURE__ */ jsx17(
1854
- IconButton,
1855
- {
1856
- icon: Undo2,
1857
- title: "Undo (Ctrl+Z)",
1858
- disabled: !history.canUndo || preview,
1859
- onClick: history.undo
1860
- }
1861
- ),
1862
- /* @__PURE__ */ jsx17(
1863
- IconButton,
1864
- {
1865
- icon: Redo2,
1866
- title: "Redo (Ctrl+Y)",
1867
- disabled: !history.canRedo || preview,
1868
- onClick: history.redo
1869
- }
1870
- ),
1871
- /* @__PURE__ */ jsx17(
1872
- IconButton,
2157
+ title != null && shows("title") && /* @__PURE__ */ jsx21("h2", { className: "m-0 min-w-0 truncate text-[17px] font-semibold", children: title }),
2158
+ shows("status") && /* @__PURE__ */ jsx21("span", { className: "text-[12px] text-muted", children: ed.dirty ? "Unsaved changes" : "All changes saved" }),
2159
+ /* @__PURE__ */ jsxs15("div", { className: "ml-auto flex flex-wrap items-center gap-1.5", children: [
2160
+ shows("undo") && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2161
+ /* @__PURE__ */ jsx21(IconButton2, { icon: Undo2, title: "Undo (Ctrl+Z)", disabled: !ed.canUndo || preview, onClick: ed.undo }),
2162
+ /* @__PURE__ */ jsx21(IconButton2, { icon: Redo2, title: "Redo (Ctrl+Y)", disabled: !ed.canRedo || preview, onClick: ed.redo })
2163
+ ] }),
2164
+ shows("snap") && /* @__PURE__ */ jsx21(
2165
+ IconButton2,
1873
2166
  {
1874
2167
  icon: Grid3x3,
1875
2168
  title: snapOn ? "Snap to grid: on" : "Snap to grid: off",
1876
2169
  className: snapOn ? "!text-primary" : "",
1877
- onClick: () => setSnapOn((s) => !s)
2170
+ onClick: () => ed.setSnapOn(!snapOn)
1878
2171
  }
1879
2172
  ),
1880
- /* @__PURE__ */ jsx17(ZoomControls, { zoom, onChange: setZoom }),
1881
- !preview && /* @__PURE__ */ jsx17(
1882
- Button,
2173
+ shows("zoom") && /* @__PURE__ */ jsx21(ZoomControls, { zoom, onChange: setZoom }),
2174
+ !preview && shows("editText") && /* @__PURE__ */ jsx21(
2175
+ Button2,
1883
2176
  {
1884
2177
  variant: editText ? "primary" : "secondary",
1885
2178
  size: "sm",
1886
2179
  icon: Type,
1887
2180
  disabled: !layout,
1888
2181
  title: "Click any line of the document to change its text",
1889
- onClick: () => setEditText((v) => !v),
1890
- children: /* @__PURE__ */ jsx17("span", { className: "max-sm:hidden", children: "Edit text" })
2182
+ onClick: () => ed.setEditText(!editText),
2183
+ children: /* @__PURE__ */ jsx21("span", { className: "max-sm:hidden", children: "Edit text" })
1891
2184
  }
1892
2185
  ),
1893
- !preview && /* @__PURE__ */ jsx17(
1894
- Button,
2186
+ !preview && shows("autoDetect") && /* @__PURE__ */ jsx21(
2187
+ Button2,
1895
2188
  {
1896
2189
  variant: "secondary",
1897
2190
  size: "sm",
1898
2191
  icon: Wand2,
1899
- disabled: !pdf || !layout || !!detecting,
2192
+ disabled: !ed.pdf || !layout || !!ed.detecting,
1900
2193
  title: "Find answer lines, checkboxes and table cells and suggest fields for them",
1901
- onClick: () => void detect(),
1902
- children: /* @__PURE__ */ jsx17("span", { className: "max-sm:hidden", children: "Auto-detect" })
2194
+ onClick: () => void ed.detect(),
2195
+ children: /* @__PURE__ */ jsx21("span", { className: "max-sm:hidden", children: "Auto-detect" })
1903
2196
  }
1904
2197
  ),
1905
- /* @__PURE__ */ jsx17(
1906
- Button,
2198
+ shows("preview") && /* @__PURE__ */ jsx21(
2199
+ Button2,
1907
2200
  {
1908
2201
  variant: "secondary",
1909
2202
  size: "sm",
1910
2203
  icon: preview ? Pencil : Eye,
1911
2204
  title: preview ? "Back to editing" : "Preview",
1912
- onClick: () => {
1913
- setPreview((p) => !p);
1914
- setPreviewAnswers({});
1915
- setEditText(false);
1916
- },
1917
- children: /* @__PURE__ */ jsx17("span", { className: "max-sm:hidden", children: preview ? "Edit" : "Preview" })
2205
+ onClick: () => ed.setPreview(!preview),
2206
+ children: /* @__PURE__ */ jsx21("span", { className: "max-sm:hidden", children: preview ? "Edit" : "Preview" })
1918
2207
  }
1919
2208
  ),
1920
- /* @__PURE__ */ jsx17(Button, { size: "sm", icon: Save, title: "Save", disabled: !dirty || busy, onClick: () => void save(), children: /* @__PURE__ */ jsx17("span", { className: "max-sm:hidden", children: "Save" }) }),
2209
+ shows("save") && /* @__PURE__ */ jsx21(Button2, { size: "sm", icon: Save, title: "Save", disabled: !ed.dirty || ed.busy, onClick: () => void ed.save(), children: /* @__PURE__ */ jsx21("span", { className: "max-sm:hidden", children: "Save" }) }),
1921
2210
  toolbar
1922
2211
  ] })
1923
2212
  ] }),
1924
2213
  banner,
1925
- detecting && /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-3 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2", role: "status", children: [
2214
+ ed.detecting && /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-3 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2", role: "status", children: [
1926
2215
  "Detecting fields\u2026 page ",
1927
- detecting.done,
2216
+ ed.detecting.done,
1928
2217
  " of ",
1929
- detecting.total,
1930
- /* @__PURE__ */ jsx17("div", { className: "h-1.5 w-40 overflow-hidden rounded bg-white", children: /* @__PURE__ */ jsx17(
2218
+ ed.detecting.total,
2219
+ /* @__PURE__ */ jsx21("div", { className: "h-1.5 w-40 overflow-hidden rounded bg-white", children: /* @__PURE__ */ jsx21(
1931
2220
  "div",
1932
2221
  {
1933
2222
  className: "h-full bg-folder",
1934
- style: { width: `${100 * detecting.done / Math.max(1, detecting.total)}%` }
2223
+ style: { width: `${100 * ed.detecting.done / Math.max(1, ed.detecting.total)}%` }
1935
2224
  }
1936
2225
  ) }),
1937
- /* @__PURE__ */ jsx17(
2226
+ /* @__PURE__ */ jsx21(
1938
2227
  "button",
1939
2228
  {
1940
2229
  type: "button",
1941
2230
  className: "cursor-pointer text-primary hover:underline",
1942
- onClick: () => detecting.stop.abort(),
2231
+ onClick: () => ed.detecting?.stop.abort(),
1943
2232
  children: "Stop"
1944
2233
  }
1945
2234
  )
1946
2235
  ] }),
1947
- armed && !preview && /* @__PURE__ */ jsxs13("p", { className: "m-0 flex items-center gap-3 bg-primary-soft px-4 py-1.5 text-[12.5px] text-ink-2", role: "status", children: [
2236
+ armed && !preview && /* @__PURE__ */ jsxs15("p", { className: "m-0 flex items-center gap-3 bg-primary-soft px-4 py-1.5 text-[12.5px] text-ink-2", role: "status", children: [
1948
2237
  "Tap the page where the ",
1949
2238
  armedLabel,
1950
2239
  " should go.",
1951
- /* @__PURE__ */ jsx17("button", { type: "button", className: "cursor-pointer text-primary hover:underline", onClick: () => setArmed(null), children: "Cancel" })
2240
+ /* @__PURE__ */ jsx21(
2241
+ "button",
2242
+ {
2243
+ type: "button",
2244
+ className: "cursor-pointer text-primary hover:underline",
2245
+ onClick: () => ed.setArmed(null),
2246
+ children: "Cancel"
2247
+ }
2248
+ )
1952
2249
  ] }),
1953
- editText && /* @__PURE__ */ jsx17("p", { className: "m-0 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2", children: "Edit text: click a line of the document to replace it with an editable copy (same place and size). The uploaded file itself never changes." }),
1954
- /* @__PURE__ */ jsxs13("div", { className: "relative flex min-h-0 flex-1", children: [
1955
- !preview && /* @__PURE__ */ jsxs13(
2250
+ editText && /* @__PURE__ */ jsx21("p", { className: "m-0 bg-folder/15 px-4 py-1.5 text-[12.5px] text-ink-2", children: "Edit text: click a line of the document to replace it with an editable copy (same place and size). The uploaded file itself never changes." }),
2251
+ /* @__PURE__ */ jsxs15("div", { className: "relative flex min-h-0 flex-1", children: [
2252
+ !preview && shows("tools") && /* @__PURE__ */ jsxs15(
1956
2253
  "aside",
1957
2254
  {
1958
2255
  className: `w-[170px] flex-none overflow-y-auto border-r border-line bg-white max-lg:absolute max-lg:inset-y-0 max-lg:left-0 max-lg:z-30 max-lg:w-[220px] max-lg:shadow-xl ${drawer === "tools" ? "" : "max-lg:hidden"}`,
1959
2256
  children: [
1960
- /* @__PURE__ */ jsx17(PanelHeading, { children: "FIELDS" }),
1961
- /* @__PURE__ */ jsx17("p", { className: "m-0 px-3 pt-2 text-[11.5px] text-muted", children: "Drag onto the document, or tap one, then the page" }),
1962
- /* @__PURE__ */ jsx17("div", { className: "space-y-1 p-2", children: FIELD_TYPES.map((t) => toolItem(`field:${t}`, TYPE_LABEL[t])) }),
1963
- /* @__PURE__ */ jsx17(PanelHeading, { children: "LAYOUT" }),
1964
- /* @__PURE__ */ jsx17("div", { className: "space-y-1 p-2", children: Object.keys(ELEMENT_TOOLS).map(
1965
- (t) => toolItem(`element:${t}`, ELEMENT_TOOLS[t].label)
1966
- ) })
2257
+ shows("fieldTools") && ed.fieldTools.length > 0 && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2258
+ /* @__PURE__ */ jsx21(PanelHeading2, { children: "FIELDS" }),
2259
+ /* @__PURE__ */ jsx21("p", { className: "m-0 px-3 pt-2 text-[11.5px] text-muted", children: "Drag onto the document, or tap one, then the page" }),
2260
+ /* @__PURE__ */ jsx21("div", { className: "space-y-1 p-2", children: ed.fieldTools.map((k) => toolItem(k, TYPE_LABEL[k.slice(6)])) })
2261
+ ] }),
2262
+ shows("layoutTools") && ed.elementTools.length > 0 && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2263
+ /* @__PURE__ */ jsx21(PanelHeading2, { children: "LAYOUT" }),
2264
+ /* @__PURE__ */ jsx21("div", { className: "space-y-1 p-2", children: ed.elementTools.map((k) => toolItem(k, ELEMENT_TOOLS[k.slice(8)].label)) })
2265
+ ] })
1967
2266
  ]
1968
2267
  }
1969
2268
  ),
1970
- /* @__PURE__ */ jsx17(
2269
+ /* @__PURE__ */ jsx21(
1971
2270
  "div",
1972
2271
  {
1973
2272
  ref: (n) => {
@@ -1976,22 +2275,19 @@ function FormEditor({
1976
2275
  },
1977
2276
  className: "min-w-0 flex-1 overflow-auto bg-hover",
1978
2277
  onPointerDown: () => {
1979
- if (!preview) setSelection(null);
2278
+ if (!preview) ed.setSelection(null);
1980
2279
  setDrawer(null);
1981
2280
  },
1982
- children: /* @__PURE__ */ jsx17(
2281
+ children: /* @__PURE__ */ jsx21(
1983
2282
  PdfPages,
1984
2283
  {
1985
2284
  load: pdfSource,
1986
2285
  zoom,
1987
2286
  pages: layout?.pages,
1988
2287
  withText: editText && !preview,
1989
- onLoaded: (s, doc2) => {
1990
- setSizes(s);
1991
- setPdf(doc2);
1992
- },
2288
+ onLoaded: ed.onPdfLoaded,
1993
2289
  onError: () => setLoadError("The document could not be shown"),
1994
- overlay: (page, lines) => /* @__PURE__ */ jsxs13(
2290
+ overlay: (page, lines) => /* @__PURE__ */ jsxs15(
1995
2291
  "div",
1996
2292
  {
1997
2293
  className: "absolute inset-0",
@@ -2000,14 +2296,18 @@ function FormEditor({
2000
2296
  backgroundSize: "2% 2%"
2001
2297
  } : void 0,
2002
2298
  onDragOver: (e) => !preview && e.preventDefault(),
2003
- onDrop: (e) => dropNew(e, page),
2299
+ onDrop: (e) => {
2300
+ const key = e.dataTransfer.getData(DRAG_TYPE);
2301
+ if (ed.placeNew(key, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect()))
2302
+ e.preventDefault();
2303
+ },
2004
2304
  onClick: (e) => {
2005
2305
  if (!armed || preview) return;
2006
- placeNew(armed, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect());
2007
- setArmed(null);
2306
+ ed.placeNew(armed, page, e.clientX, e.clientY, e.currentTarget.getBoundingClientRect());
2307
+ ed.setArmed(null);
2008
2308
  },
2009
2309
  children: [
2010
- lines?.map((line, i) => /* @__PURE__ */ jsx17(
2310
+ lines?.map((line, i) => /* @__PURE__ */ jsx21(
2011
2311
  "button",
2012
2312
  {
2013
2313
  type: "button",
@@ -2015,16 +2315,12 @@ function FormEditor({
2015
2315
  className: "absolute cursor-text rounded-[1px] outline-1 outline-folder/60 hover:bg-folder/25 hover:outline-folder",
2016
2316
  style: boxStyle({ page, ...line }),
2017
2317
  onPointerDown: (e) => e.stopPropagation(),
2018
- onClick: () => {
2019
- const el = replaceLine(line, page);
2020
- setElements((l) => [...l, el]);
2021
- select({ kind: "element", id: el.id });
2022
- }
2318
+ onClick: () => ed.editLine(line, page)
2023
2319
  },
2024
2320
  i
2025
2321
  )),
2026
2322
  layout?.elements.filter((el) => el.page === page).map(
2027
- (el) => preview ? /* @__PURE__ */ jsx17(LayoutElementView, { element: el, pageHeight: pageHeight(page) }, el.id) : /* @__PURE__ */ jsxs13(
2323
+ (el) => preview ? /* @__PURE__ */ jsx21(LayoutElementView, { element: el, pageHeight: pageHeight(page) }, el.id) : /* @__PURE__ */ jsxs15(
2028
2324
  "div",
2029
2325
  {
2030
2326
  role: "button",
@@ -2032,12 +2328,12 @@ function FormEditor({
2032
2328
  "aria-label": `${el.type} ${el.text ?? ""}`,
2033
2329
  className: `absolute cursor-move touch-none select-none ${selection?.id === el.id ? "z-10 outline-2 outline-primary" : "hover:outline-1 hover:outline-primary/50"}`,
2034
2330
  style: boxStyle({ page, ...el.position, height: Math.max(el.position.height, 4e-3) }),
2035
- onPointerDown: (e) => startDrag(e, { kind: "element", id: el.id }, { page, ...el.position }, "move"),
2036
- onPointerMove: onDrag,
2037
- onPointerUp: endDrag,
2331
+ onPointerDown: (e) => ed.startDrag(e, { kind: "element", id: el.id }, { page, ...el.position }, "move"),
2332
+ onPointerMove: ed.onDrag,
2333
+ onPointerUp: ed.endDrag,
2038
2334
  onClick: (e) => e.stopPropagation(),
2039
2335
  children: [
2040
- /* @__PURE__ */ jsx17(
2336
+ /* @__PURE__ */ jsx21(
2041
2337
  LayoutElementView,
2042
2338
  {
2043
2339
  element: el,
@@ -2045,14 +2341,14 @@ function FormEditor({
2045
2341
  style: { left: 0, top: 0, width: "100%", height: "100%" }
2046
2342
  }
2047
2343
  ),
2048
- selection?.id === el.id && /* @__PURE__ */ jsx17(
2344
+ selection?.id === el.id && /* @__PURE__ */ jsx21(
2049
2345
  "span",
2050
2346
  {
2051
2347
  "aria-label": "Resize",
2052
2348
  className: "absolute -right-1 -bottom-1 h-2.5 w-2.5 cursor-se-resize rounded-sm border border-white bg-primary",
2053
- onPointerDown: (e) => startDrag(e, { kind: "element", id: el.id }, { page, ...el.position }, "resize"),
2054
- onPointerMove: onDrag,
2055
- onPointerUp: endDrag
2349
+ onPointerDown: (e) => ed.startDrag(e, { kind: "element", id: el.id }, { page, ...el.position }, "resize"),
2350
+ onPointerMove: ed.onDrag,
2351
+ onPointerUp: ed.endDrag
2056
2352
  }
2057
2353
  )
2058
2354
  ]
@@ -2061,16 +2357,17 @@ function FormEditor({
2061
2357
  )
2062
2358
  ),
2063
2359
  fields.filter((f) => f.position.page === page).map(
2064
- (f) => preview ? previewStates[f.key]?.visible && /* @__PURE__ */ jsx17("div", { className: "absolute [container-type:size]", style: boxStyle(f.position), children: /* @__PURE__ */ jsx17(
2065
- FieldControl,
2360
+ (f) => preview ? previewStates[f.key]?.visible && /* @__PURE__ */ jsx21("div", { className: "absolute [container-type:size]", style: boxStyle(f.position), children: /* @__PURE__ */ jsx21(
2361
+ AnswerControl,
2066
2362
  {
2363
+ components,
2067
2364
  field: f,
2068
2365
  value: previewAnswers[f.key],
2069
2366
  readOnly: previewStates[f.key].readOnly,
2070
2367
  compact,
2071
- onChange: (v) => setPreviewAnswers((a) => ({ ...a, [f.key]: v }))
2368
+ onChange: (v) => ed.setPreviewAnswers((a) => ({ ...a, [f.key]: v }))
2072
2369
  }
2073
- ) }, f.id) : /* @__PURE__ */ jsxs13(
2370
+ ) }, f.id) : /* @__PURE__ */ jsxs15(
2074
2371
  "div",
2075
2372
  {
2076
2373
  role: "button",
@@ -2079,23 +2376,23 @@ function FormEditor({
2079
2376
  title: `${f.label || f.key} (${TYPE_LABEL[f.type]})`,
2080
2377
  className: `absolute cursor-move touch-none rounded-sm border text-[10px] select-none [container-type:size] ${selection?.id === f.id ? "z-10 border-primary bg-primary/20 ring-2 ring-primary/40" : f.source === "ai" ? "border-dashed border-folder bg-folder/15" : "border-primary/60 bg-primary/10 hover:bg-primary/15"}`,
2081
2378
  style: boxStyle(f.position),
2082
- onPointerDown: (e) => startDrag(e, { kind: "field", id: f.id }, f.position, "move"),
2083
- onPointerMove: onDrag,
2084
- onPointerUp: endDrag,
2379
+ onPointerDown: (e) => ed.startDrag(e, { kind: "field", id: f.id }, f.position, "move"),
2380
+ onPointerMove: ed.onDrag,
2381
+ onPointerUp: ed.endDrag,
2085
2382
  onClick: (e) => e.stopPropagation(),
2086
2383
  children: [
2087
- /* @__PURE__ */ jsxs13("span", { className: "pointer-events-none block truncate px-0.5 text-[clamp(7px,55cqh,12px)] leading-tight text-primary", children: [
2384
+ /* @__PURE__ */ jsxs15("span", { className: "pointer-events-none block truncate px-0.5 text-[clamp(7px,55cqh,12px)] leading-tight text-primary", children: [
2088
2385
  f.label || f.key,
2089
- f.required && /* @__PURE__ */ jsx17("b", { className: "text-danger", children: " *" })
2386
+ f.required && /* @__PURE__ */ jsx21("b", { className: "text-danger", children: " *" })
2090
2387
  ] }),
2091
- selection?.id === f.id && /* @__PURE__ */ jsx17(
2388
+ selection?.id === f.id && /* @__PURE__ */ jsx21(
2092
2389
  "span",
2093
2390
  {
2094
2391
  "aria-label": "Resize",
2095
2392
  className: "absolute -right-1 -bottom-1 h-2.5 w-2.5 cursor-se-resize rounded-sm border border-white bg-primary",
2096
- onPointerDown: (e) => startDrag(e, { kind: "field", id: f.id }, f.position, "resize"),
2097
- onPointerMove: onDrag,
2098
- onPointerUp: endDrag
2393
+ onPointerDown: (e) => ed.startDrag(e, { kind: "field", id: f.id }, f.position, "resize"),
2394
+ onPointerMove: ed.onDrag,
2395
+ onPointerUp: ed.endDrag
2099
2396
  }
2100
2397
  )
2101
2398
  ]
@@ -2110,121 +2407,130 @@ function FormEditor({
2110
2407
  )
2111
2408
  }
2112
2409
  ),
2113
- /* @__PURE__ */ jsxs13(
2410
+ shows("panel") && panelTabs.length > 0 && /* @__PURE__ */ jsxs15(
2114
2411
  "aside",
2115
2412
  {
2116
2413
  className: `flex w-[350px] flex-none flex-col border-l border-line bg-white max-lg:absolute max-lg:inset-y-0 max-lg:right-0 max-lg:z-30 max-lg:w-[min(350px,92vw)] max-lg:shadow-xl ${drawer === "panel" ? "" : "max-lg:hidden"}`,
2117
2414
  children: [
2118
- /* @__PURE__ */ jsx17("div", { className: "overflow-x-auto border-b border-line p-2 [&_button]:px-2.5", children: /* @__PURE__ */ jsx17(
2119
- SegmentedTabs,
2415
+ /* @__PURE__ */ jsx21("div", { className: "overflow-x-auto border-b border-line p-2 [&_button]:px-2.5", children: /* @__PURE__ */ jsx21(
2416
+ SegmentedTabs2,
2120
2417
  {
2121
2418
  options: [
2122
2419
  { value: "props", label: "Properties" },
2123
2420
  { value: "fields", label: `Fields (${fields.length})` },
2124
2421
  { value: "pages", label: `Pages (${pageCount})` },
2125
2422
  ...tabs.map((t) => ({ value: t.id, label: t.label }))
2126
- ],
2423
+ ].filter((o) => panelTabs.includes(o.value)),
2127
2424
  value: tab,
2128
2425
  onChange: setTab
2129
2426
  }
2130
2427
  ) }),
2131
- problems.length > 0 && /* @__PURE__ */ jsx17("ul", { className: "m-0 border-b border-danger/30 bg-danger/5 py-2 pr-3 pl-7 text-[12px] text-danger", children: problems.slice(0, 6).map((p) => /* @__PURE__ */ jsx17("li", { children: p }, p)) }),
2132
- /* @__PURE__ */ jsxs13("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
2133
- tab === "props" && /* @__PURE__ */ jsxs13(Fragment4, { children: [
2134
- /* @__PURE__ */ jsx17(
2135
- PanelHeading,
2428
+ ed.problems.length > 0 && shows("problems") && /* @__PURE__ */ jsx21("ul", { className: "m-0 border-b border-danger/30 bg-danger/5 py-2 pr-3 pl-7 text-[12px] text-danger", children: ed.problems.slice(0, 6).map((p) => /* @__PURE__ */ jsx21("li", { children: p }, p)) }),
2429
+ /* @__PURE__ */ jsxs15("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
2430
+ tab === "props" && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2431
+ /* @__PURE__ */ jsx21(
2432
+ PanelHeading2,
2136
2433
  {
2137
- actions: selection && /* @__PURE__ */ jsxs13(Fragment4, { children: [
2138
- /* @__PURE__ */ jsx17(
2139
- IconButton,
2434
+ actions: selection && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2435
+ /* @__PURE__ */ jsx21(
2436
+ IconButton2,
2140
2437
  {
2141
2438
  icon: Copy,
2142
2439
  title: "Duplicate (Ctrl+D)",
2143
2440
  light: true,
2144
2441
  iconSize: 16,
2145
- onClick: duplicateSelected
2442
+ onClick: ed.duplicateSelected
2146
2443
  }
2147
2444
  ),
2148
- /* @__PURE__ */ jsx17(IconButton, { icon: Trash22, title: "Delete (Del)", light: true, iconSize: 16, onClick: removeSelected })
2445
+ /* @__PURE__ */ jsx21(
2446
+ IconButton2,
2447
+ {
2448
+ icon: Trash22,
2449
+ title: "Delete (Del)",
2450
+ light: true,
2451
+ iconSize: 16,
2452
+ onClick: ed.removeSelected
2453
+ }
2454
+ )
2149
2455
  ] }),
2150
2456
  children: selectedField ? "FIELD" : selectedElement ? selectedElement.type.toUpperCase() : "PROPERTIES"
2151
2457
  }
2152
2458
  ),
2153
- selectedField ? /* @__PURE__ */ jsx17(
2459
+ selectedField ? /* @__PURE__ */ jsx21(
2154
2460
  FieldProperties,
2155
2461
  {
2156
2462
  field: selectedField,
2157
2463
  fields,
2158
2464
  pageCount,
2159
- onChange: updateField
2465
+ onChange: ed.updateField
2160
2466
  },
2161
2467
  selectedField.id
2162
- ) : selectedElement ? /* @__PURE__ */ jsx17(
2468
+ ) : selectedElement ? /* @__PURE__ */ jsx21(
2163
2469
  ElementProperties,
2164
2470
  {
2165
2471
  element: selectedElement,
2166
2472
  pageCount,
2167
- onChange: updateElement
2473
+ onChange: ed.updateElement
2168
2474
  },
2169
2475
  selectedElement.id
2170
- ) : /* @__PURE__ */ jsxs13(EmptyText, { children: [
2476
+ ) : /* @__PURE__ */ jsxs15(EmptyText2, { children: [
2171
2477
  "Select something on the document, drag a field or layout item in from the left, or use",
2172
2478
  " ",
2173
- /* @__PURE__ */ jsx17("b", { children: "Edit text" }),
2479
+ /* @__PURE__ */ jsx21("b", { children: "Edit text" }),
2174
2480
  " to change the document's own text."
2175
2481
  ] })
2176
2482
  ] }),
2177
- tab === "fields" && /* @__PURE__ */ jsxs13(Fragment4, { children: [
2178
- /* @__PURE__ */ jsx17(PanelHeading, { children: "FILL ORDER" }),
2179
- !fields.length && /* @__PURE__ */ jsx17(EmptyText, { children: "No fields yet." }),
2180
- /* @__PURE__ */ jsx17("ol", { className: "m-0 list-none p-0", children: fields.map((f, i) => /* @__PURE__ */ jsxs13(
2483
+ tab === "fields" && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2484
+ /* @__PURE__ */ jsx21(PanelHeading2, { children: "FILL ORDER" }),
2485
+ !fields.length && /* @__PURE__ */ jsx21(EmptyText2, { children: "No fields yet." }),
2486
+ /* @__PURE__ */ jsx21("ol", { className: "m-0 list-none p-0", children: fields.map((f, i) => /* @__PURE__ */ jsxs15(
2181
2487
  "li",
2182
2488
  {
2183
2489
  className: `flex items-center gap-1 border-b border-line-soft px-2 py-1 text-[12.5px] ${selection?.id === f.id ? "bg-primary-tint" : ""}`,
2184
2490
  children: [
2185
- /* @__PURE__ */ jsxs13(
2491
+ /* @__PURE__ */ jsxs15(
2186
2492
  "button",
2187
2493
  {
2188
2494
  type: "button",
2189
2495
  className: "min-w-0 flex-1 cursor-pointer truncate text-left",
2190
2496
  onClick: () => {
2191
- select({ kind: "field", id: f.id });
2497
+ ed.setSelection({ kind: "field", id: f.id });
2192
2498
  goToPage(f.position.page);
2193
2499
  },
2194
2500
  children: [
2195
- /* @__PURE__ */ jsxs13("span", { className: "text-faint", children: [
2501
+ /* @__PURE__ */ jsxs15("span", { className: "text-faint", children: [
2196
2502
  "p",
2197
2503
  f.position.page
2198
2504
  ] }),
2199
2505
  " ",
2200
2506
  f.label || f.key,
2201
2507
  " ",
2202
- /* @__PURE__ */ jsxs13("span", { className: "text-muted", children: [
2508
+ /* @__PURE__ */ jsxs15("span", { className: "text-muted", children: [
2203
2509
  "\xB7 ",
2204
2510
  TYPE_LABEL[f.type]
2205
2511
  ] }),
2206
- f.source === "ai" && /* @__PURE__ */ jsx17("span", { className: "text-folder", children: " \xB7 AI" })
2512
+ f.source === "ai" && /* @__PURE__ */ jsx21("span", { className: "text-folder", children: " \xB7 AI" })
2207
2513
  ]
2208
2514
  }
2209
2515
  ),
2210
- /* @__PURE__ */ jsx17(
2211
- IconButton,
2516
+ /* @__PURE__ */ jsx21(
2517
+ IconButton2,
2212
2518
  {
2213
2519
  icon: ArrowUp2,
2214
2520
  title: "Move up",
2215
2521
  iconSize: 13,
2216
2522
  disabled: i === 0,
2217
- onClick: () => setFields((l) => reorder(l, f.id, -1))
2523
+ onClick: () => ed.reorderField(f.id, -1)
2218
2524
  }
2219
2525
  ),
2220
- /* @__PURE__ */ jsx17(
2221
- IconButton,
2526
+ /* @__PURE__ */ jsx21(
2527
+ IconButton2,
2222
2528
  {
2223
2529
  icon: ArrowDown2,
2224
2530
  title: "Move down",
2225
2531
  iconSize: 13,
2226
2532
  disabled: i === fields.length - 1,
2227
- onClick: () => setFields((l) => reorder(l, f.id, 1))
2533
+ onClick: () => ed.reorderField(f.id, 1)
2228
2534
  }
2229
2535
  )
2230
2536
  ]
@@ -2232,25 +2538,25 @@ function FormEditor({
2232
2538
  f.id
2233
2539
  )) })
2234
2540
  ] }),
2235
- tab === "pages" && (layout ? /* @__PURE__ */ jsx17(
2541
+ tab === "pages" && (layout ? /* @__PURE__ */ jsx21(
2236
2542
  PagesPanel,
2237
2543
  {
2238
2544
  pages: layout.pages,
2239
2545
  counts,
2240
2546
  onGoTo: goToPage,
2241
- onMove: (n, step) => withDoc((d) => movePage(d, n, step)),
2242
- onInsertBlank: (n) => withDoc((d) => insertBlankPage(d, n)),
2243
- onDelete: (n) => counts[n] ? setConfirmDeletePage(n) : withDoc((d) => deletePage(d, n))
2547
+ onMove: ed.movePage,
2548
+ onInsertBlank: ed.insertBlankPage,
2549
+ onDelete: (n) => counts[n] ? setConfirmDeletePage(n) : ed.deletePage(n)
2244
2550
  }
2245
- ) : /* @__PURE__ */ jsx17(EmptyText, { children: "Loading pages\u2026" })),
2551
+ ) : /* @__PURE__ */ jsx21(EmptyText2, { children: "Loading pages\u2026" })),
2246
2552
  tabs.find((t) => t.id === tab)?.content
2247
2553
  ] })
2248
2554
  ]
2249
2555
  }
2250
2556
  )
2251
2557
  ] }),
2252
- confirmDeletePage != null && /* @__PURE__ */ jsxs13(
2253
- ConfirmModal,
2558
+ confirmDeletePage != null && /* @__PURE__ */ jsxs15(
2559
+ ConfirmModal2,
2254
2560
  {
2255
2561
  danger: true,
2256
2562
  confirmLabel: "Delete page",
@@ -2258,8 +2564,7 @@ function FormEditor({
2258
2564
  onConfirm: () => {
2259
2565
  const n = confirmDeletePage;
2260
2566
  setConfirmDeletePage(null);
2261
- setSelection(null);
2262
- withDoc((d) => deletePage(d, n));
2567
+ ed.deletePage(n);
2263
2568
  },
2264
2569
  children: [
2265
2570
  "Delete page ",
@@ -2270,158 +2575,88 @@ function FormEditor({
2270
2575
  ]
2271
2576
  }
2272
2577
  )
2273
- ] });
2578
+ ] }) });
2274
2579
  }
2275
2580
 
2276
2581
  // src/react/FormFiller.tsx
2277
2582
  import { ArrowLeft as ArrowLeft2, Download, Paperclip as Paperclip2, Save as Save2, Send } from "lucide-react";
2278
- import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
2583
+ import { useEffect as useEffect8, useState as useState13 } from "react";
2279
2584
 
2280
2585
  // src/react/components/QuestionList.tsx
2281
2586
  import { Search, SlidersHorizontal } from "lucide-react";
2282
- import { Fragment as Fragment5, useMemo as useMemo2, useState as useState9 } from "react";
2587
+ import { Fragment as Fragment5, useEffect as useEffect6, useState as useState11 } from "react";
2283
2588
 
2284
- // src/react/ui/Chip.tsx
2285
- import { X as X2 } from "lucide-react";
2286
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2287
- function Chip({
2288
- label,
2289
- count,
2290
- active,
2291
- onClick
2292
- }) {
2293
- return /* @__PURE__ */ jsxs14(
2294
- "button",
2295
- {
2296
- type: "button",
2297
- "aria-pressed": !!active,
2298
- onClick,
2299
- className: `inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-[12.5px] whitespace-nowrap transition-colors ${active ? "border-primary bg-primary-tint font-semibold text-primary" : "border-line-strong bg-white text-ink-2 hover:border-faint hover:bg-surface"}`,
2300
- children: [
2301
- label,
2302
- count != null && /* @__PURE__ */ jsx18(
2303
- "span",
2304
- {
2305
- className: `rounded-full px-1.5 text-[11px] font-semibold ${active ? "bg-primary text-white" : "bg-hover text-muted"}`,
2306
- children: count
2307
- }
2308
- )
2309
- ]
2310
- }
2589
+ // src/react/hooks/useQuestionFilter.ts
2590
+ import { useMemo as useMemo2, useState as useState10 } from "react";
2591
+ var defaultFilters = builtinFilters({ type: (t) => TYPE_LABEL[t] });
2592
+ var byId = (id) => {
2593
+ if (id === "tags") return defaultFilters.find((s) => typeof s === "function");
2594
+ return defaultFilters.find((s) => typeof s !== "function" && s.id === id);
2595
+ };
2596
+ function useQuestionFilter(questions, answers, { filters = defaultFilters, search: searchOn = true, initial, predicate, onChange } = {}) {
2597
+ const [values, setValues] = useState10(initial?.values ?? {});
2598
+ const [search, setSearchState] = useState10(initial?.search ?? "");
2599
+ const [seen, setSeen] = useState10(answers);
2600
+ const [version, setVersion] = useState10(0);
2601
+ const update = (nextSearch, nextValues) => {
2602
+ setSearchState(nextSearch);
2603
+ setValues(nextValues);
2604
+ setSeen(answers);
2605
+ setVersion((v) => v + 1);
2606
+ onChange?.({ search: nextSearch, values: nextValues });
2607
+ };
2608
+ const key = filters.map((f) => typeof f === "string" ? f : typeof f === "function" ? "fn" : f.id).join();
2609
+ const defs = useMemo2(
2610
+ () => resolveFilters(
2611
+ filters.map((f) => typeof f === "string" ? byId(f) : f).filter((f) => !!f),
2612
+ questions
2613
+ ),
2614
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- re-resolve when the list's ids change, not on every render
2615
+ [key, questions]
2311
2616
  );
2312
- }
2313
- function ChipGroup({
2314
- title,
2315
- options,
2316
- value,
2317
- onChange
2318
- }) {
2319
- return /* @__PURE__ */ jsxs14("div", { role: "group", "aria-label": title, className: "flex flex-wrap items-center gap-1.5", children: [
2320
- title && /* @__PURE__ */ jsx18("span", { className: "mr-1 min-w-28 text-[11.5px] max-sm:basis-full font-semibold tracking-wide text-muted uppercase", children: title }),
2321
- options.map((o) => /* @__PURE__ */ jsx18(
2322
- Chip,
2323
- {
2324
- label: o.label,
2325
- count: o.count,
2326
- active: value === o.value,
2327
- onClick: () => onChange(value === o.value ? null : o.value)
2328
- },
2329
- o.value
2330
- ))
2331
- ] });
2332
- }
2333
- function RemovableChip({ label, onRemove }) {
2334
- return /* @__PURE__ */ jsxs14("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary-tint py-0.5 pr-1 pl-2.5 text-[12px] font-medium text-primary", children: [
2335
- label,
2336
- /* @__PURE__ */ jsx18(
2337
- "button",
2338
- {
2339
- type: "button",
2340
- "aria-label": `Remove ${label}`,
2341
- onClick: onRemove,
2342
- className: "inline-flex h-4.5 w-4.5 cursor-pointer items-center justify-center rounded-full hover:bg-primary hover:text-white",
2343
- children: /* @__PURE__ */ jsx18(X2, { size: 12 })
2344
- }
2345
- )
2346
- ] });
2347
- }
2348
-
2349
- // src/react/ui/Popover.tsx
2350
- import { useRef as useRef5, useState as useState8 } from "react";
2351
-
2352
- // src/react/ui/useOutsideClose.ts
2353
- import { useEffect as useEffect4 } from "react";
2354
- function useOutsideClose(ref, open, onClose) {
2355
- useEffect4(() => {
2356
- if (!open) return;
2357
- const onClick = (e) => ref.current && e.composedPath().includes(ref.current) || onClose();
2358
- const onKey = (e) => e.key === "Escape" && onClose();
2359
- document.addEventListener("mousedown", onClick);
2360
- document.addEventListener("keydown", onKey);
2361
- return () => {
2362
- document.removeEventListener("mousedown", onClick);
2363
- document.removeEventListener("keydown", onKey);
2617
+ const list = useMemo2(() => {
2618
+ const shown = filterQuestions(questions, seen, defs, values, searchOn ? search : "");
2619
+ return predicate ? shown.filter((q) => predicate(q, seen)) : shown;
2620
+ }, [questions, seen, defs, values, search, searchOn, predicate]);
2621
+ const groups = defs.map((d) => {
2622
+ const options = d.options(questions, answers);
2623
+ return {
2624
+ id: d.id,
2625
+ title: d.title,
2626
+ section: d.section,
2627
+ options,
2628
+ available: options.length > 0,
2629
+ value: values[d.id] ?? null,
2630
+ set: (v) => update(search, { ...values, [d.id]: v })
2364
2631
  };
2365
- }, [ref, open, onClose]);
2366
- }
2367
-
2368
- // src/react/ui/Popover.tsx
2369
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
2370
- function Popover({
2371
- label,
2372
- icon: Icon,
2373
- badge,
2374
- align = "left",
2375
- children
2376
- }) {
2377
- const [open, setOpen] = useState8(false);
2378
- const box2 = useRef5(null);
2379
- useOutsideClose(box2, open, () => setOpen(false));
2380
- return /* @__PURE__ */ jsxs15("div", { ref: box2, className: "relative", children: [
2381
- /* @__PURE__ */ jsxs15(
2382
- "button",
2383
- {
2384
- type: "button",
2385
- "aria-expanded": open,
2386
- "aria-haspopup": "dialog",
2387
- onClick: () => setOpen((o) => !o),
2388
- className: `inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-3 py-1.5 text-[13px] font-medium ${open || badge ? "border-primary bg-primary-soft text-primary" : "border-line-strong bg-white text-ink-2 hover:border-faint"}`,
2389
- children: [
2390
- Icon && /* @__PURE__ */ jsx19(Icon, { size: 15 }),
2391
- label,
2392
- !!badge && /* @__PURE__ */ jsx19("span", { className: "inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[11px] font-bold text-white", children: badge })
2393
- ]
2394
- }
2395
- ),
2396
- open && /* @__PURE__ */ jsx19(
2397
- "div",
2398
- {
2399
- role: "dialog",
2400
- "aria-label": label,
2401
- className: `absolute z-30 mt-1.5 w-[min(560px,calc(100vw-24px))] max-sm:fixed max-sm:inset-x-3 max-sm:max-h-[70dvh] max-sm:w-auto max-sm:overflow-y-auto rounded-lg border border-line bg-white p-4 shadow-xl ${align === "right" ? "right-0" : "left-0"}`,
2402
- children
2403
- }
2404
- )
2405
- ] });
2632
+ });
2633
+ const picked = groups.filter((g) => g.value != null);
2634
+ return {
2635
+ groups,
2636
+ /** One group by id (undefined when not in `filters`). */
2637
+ group: (id) => groups.find((g) => g.id === id),
2638
+ /** The search text; null when search is off. */
2639
+ search: searchOn ? search : null,
2640
+ setSearch: (text) => update(text, values),
2641
+ values,
2642
+ /** Filters in use, as chips to take off one by one. */
2643
+ inUse: picked.map((g) => ({
2644
+ id: g.id,
2645
+ label: `${g.title}: ${g.options.find((o) => o.value === g.value)?.label ?? g.value}`,
2646
+ clear: () => g.set(null)
2647
+ })),
2648
+ active: !!search || picked.length > 0,
2649
+ clear: () => update("", {}),
2650
+ /** The questions that match, in form order. */
2651
+ list,
2652
+ total: questions.length,
2653
+ /** Bumps on every change (reset paging / scroll on it). */
2654
+ version
2655
+ };
2406
2656
  }
2407
2657
 
2408
- // src/react/ui/Tag.tsx
2409
- import { jsx as jsx20 } from "react/jsx-runtime";
2410
- var tones = {
2411
- success: "bg-success/15 text-success",
2412
- info: "bg-heading/20 text-primary",
2413
- muted: "bg-hover text-muted"
2414
- };
2415
- var Tag = ({ tone = "info", children }) => /* @__PURE__ */ jsx20(
2416
- "span",
2417
- {
2418
- className: `inline-block rounded-full px-2.5 py-0.5 text-[11px] font-semibold whitespace-nowrap ${tones[tone]}`,
2419
- children
2420
- }
2421
- );
2422
-
2423
2658
  // src/react/components/QuestionList.tsx
2424
- import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
2659
+ import { Fragment as Fragment6, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
2425
2660
  var PAGE = 100;
2426
2661
  var SIZE = {
2427
2662
  checkbox: "h-7 w-7 lg:h-8 lg:w-8",
@@ -2435,216 +2670,103 @@ var wide = (t) => t === "textarea" || t === "radio" || t === "file";
2435
2670
  function QuestionList({
2436
2671
  questions,
2437
2672
  answers,
2438
- control
2673
+ control,
2674
+ filter: external,
2675
+ filters,
2676
+ search,
2677
+ hideUnavailable = true,
2678
+ toolbar = true,
2679
+ renderToolbar
2439
2680
  }) {
2440
- const [filter, setFilter] = useState9(NO_FILTER);
2441
- const [shown, setShown] = useState9(PAGE);
2442
- const [seen, setSeen] = useState9(answers);
2443
- const set = (patch) => {
2444
- setFilter((f) => ({ ...f, ...patch }));
2445
- setSeen(answers);
2446
- setShown(PAGE);
2447
- };
2448
- const list = useMemo2(() => questions.filter((q) => matches(q, seen, filter)), [questions, seen, filter]);
2449
- const count = (f) => questions.filter(f).length;
2450
- const types = useMemo2(() => [...new Set(questions.flatMap((q) => q.parts.map((p) => p.field.type)))], [questions]);
2451
- const categories = useMemo2(
2452
- () => [...new Set(questions.map((q) => q.category).filter((c) => !!c))],
2453
- [questions]
2454
- );
2455
- const tagValues = useMemo2(() => {
2456
- const m = /* @__PURE__ */ new Map();
2457
- for (const q of questions)
2458
- for (const [k, v] of Object.entries(q.tags ?? {})) m.set(k, (m.get(k) ?? /* @__PURE__ */ new Set()).add(v));
2459
- return [...m].map(
2460
- ([k, vs]) => [k, [...vs].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }))]
2461
- );
2462
- }, [questions]);
2463
- const has = (re) => questions.some((q) => q.parts.some((p) => re.test(p.name) || re.test(p.field.label)));
2464
- const progress = [
2465
- ["answered", "Answered", "Answered", "Not answered"],
2466
- ["remarks", "Remarks", "Written", "Empty"],
2467
- ["comments", "Comments", "Written", "Empty"]
2468
- ].filter(([key]) => key === "answered" || has(key === "remarks" ? /remark/i : /comment/i));
2469
- const setTag = (key, value) => {
2470
- const tags = { ...filter.tags };
2471
- if (value == null) delete tags[key];
2472
- else tags[key] = value;
2473
- set({ tags });
2474
- };
2475
- const choiceCount = useMemo2(() => {
2476
- const m = /* @__PURE__ */ new Map();
2477
- for (const q of questions) for (const c of new Set(choicesOf(q))) m.set(c, (m.get(c) ?? 0) + 1);
2478
- return [...m].filter(([, n]) => n >= 2).slice(0, 40);
2479
- }, [questions]);
2480
- const [choiceMode, setChoiceMode] = useState9("offered");
2481
- const choiceValue = choiceMode === "picked" ? filter.picked : filter.choices;
2482
- const pickedCount = useMemo2(() => {
2483
- const m = /* @__PURE__ */ new Map();
2484
- for (const q of questions) for (const c of new Set(pickedOf(q, answers))) m.set(c, (m.get(c) ?? 0) + 1);
2485
- return m;
2486
- }, [questions, answers]);
2487
- const inUse = [
2488
- ...progress.filter(([key]) => filter[key] !== "all").map(([key, title, yes, no]) => ({
2489
- label: `${title}: ${filter[key] === "filled" ? yes : no}`,
2490
- clear: () => set({ [key]: "all" })
2491
- })),
2492
- ...filter.type !== "all" ? [{ label: `Type: ${TYPE_LABEL[filter.type]}`, clear: () => set({ type: "all" }) }] : [],
2493
- ...Object.entries(filter.tags).map(([k, v]) => ({ label: `${k}: ${v}`, clear: () => setTag(k, null) })),
2494
- ...filter.choices !== "all" ? [{ label: `Has option: ${filter.choices}`, clear: () => set({ choices: "all" }) }] : [],
2495
- ...filter.picked !== "all" ? [{ label: `Answered: ${filter.picked}`, clear: () => set({ picked: "all" }) }] : []
2496
- ];
2497
- const active = JSON.stringify(filter) !== JSON.stringify(NO_FILTER);
2681
+ const own = useQuestionFilter(questions, answers, { filters, search });
2682
+ const fs = external ?? own;
2683
+ const { list } = fs;
2684
+ const [shown, setShown] = useState11(PAGE);
2685
+ useEffect6(() => setShown(PAGE), [fs.version]);
2686
+ const groups = fs.groups.filter((g) => g.available || !hideUnavailable);
2687
+ const bar = groups.filter((g) => g.section === "bar");
2688
+ const menu = groups.filter((g) => g.section !== "bar");
2689
+ const sections = [...new Set(menu.map((g) => g.section ?? ""))];
2498
2690
  return /* @__PURE__ */ jsxs16("div", { className: "w-full p-3 sm:p-4 lg:px-6", children: [
2499
- /* @__PURE__ */ jsxs16("div", { className: "sticky top-0 z-10 -mx-3 mb-3 flex flex-col gap-2 border-b border-line bg-white/95 px-3 py-2.5 shadow-sm backdrop-blur sm:-mx-4 sm:px-4 lg:-mx-6 lg:px-6", children: [
2691
+ renderToolbar ? renderToolbar(fs) : toolbar && /* @__PURE__ */ jsxs16("div", { className: "sticky top-0 z-10 -mx-3 mb-3 flex flex-col gap-2 border-b border-line bg-white/95 px-3 py-2.5 shadow-sm backdrop-blur sm:-mx-4 sm:px-4 lg:-mx-6 lg:px-6", children: [
2500
2692
  /* @__PURE__ */ jsxs16("div", { className: "flex flex-wrap items-center gap-2", children: [
2501
- /* @__PURE__ */ jsxs16("label", { className: "relative min-w-0 flex-1 basis-full sm:basis-72", children: [
2502
- /* @__PURE__ */ jsx21(Search, { size: 15, className: "absolute top-1/2 left-3 -translate-y-1/2 text-muted" }),
2503
- /* @__PURE__ */ jsx21(
2693
+ fs.search != null && /* @__PURE__ */ jsxs16("label", { className: "relative min-w-0 flex-1 basis-full sm:basis-72", children: [
2694
+ /* @__PURE__ */ jsx22(Search, { size: 15, className: "absolute top-1/2 left-3 -translate-y-1/2 text-muted" }),
2695
+ /* @__PURE__ */ jsx22(
2504
2696
  "input",
2505
2697
  {
2506
2698
  type: "search",
2507
2699
  "aria-label": "Search questions",
2508
2700
  placeholder: "Search questions, sections, answers\u2026",
2509
- value: filter.search,
2510
- onChange: (e) => set({ search: e.target.value }),
2701
+ value: fs.search,
2702
+ onChange: (e) => fs.setSearch(e.target.value),
2511
2703
  className: "w-full rounded-full border border-line-strong bg-surface py-1.5 pr-3 pl-9 text-[13px] outline-none focus:border-primary focus:bg-white max-sm:text-base"
2512
2704
  }
2513
2705
  )
2514
2706
  ] }),
2515
- /* @__PURE__ */ jsx21(Popover, { label: "Filters", icon: SlidersHorizontal, badge: inUse.length, align: "right", children: /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-4", children: [
2516
- /* @__PURE__ */ jsx21(FilterSection, { title: "Progress", children: progress.map(([key, title, yes, no]) => /* @__PURE__ */ jsx21(
2517
- ChipGroup,
2518
- {
2519
- title,
2520
- value: filter[key] === "all" ? null : filter[key],
2521
- onChange: (v) => set({ [key]: v ?? "all" }),
2522
- options: [
2523
- { value: "filled", label: yes },
2524
- { value: "empty", label: no }
2525
- ]
2526
- },
2527
- key
2528
- )) }),
2529
- choiceCount.length > 0 && /* @__PURE__ */ jsx21(
2530
- FilterSection,
2531
- {
2532
- title: "Answers",
2533
- action: /* @__PURE__ */ jsx21(
2534
- SegmentedTabs,
2535
- {
2536
- size: "sm",
2537
- value: choiceMode,
2538
- onChange: (m) => {
2539
- setChoiceMode(m);
2540
- const c = filter.choices !== "all" ? filter.choices : filter.picked;
2541
- if (c !== "all")
2542
- set(m === "picked" ? { picked: c, choices: "all" } : { choices: c, picked: "all" });
2543
- },
2544
- options: [
2545
- { value: "offered", label: "Has option" },
2546
- { value: "picked", label: "Answered" }
2547
- ]
2548
- }
2549
- ),
2550
- children: /* @__PURE__ */ jsx21(
2551
- ChipGroup,
2552
- {
2553
- value: choiceValue === "all" ? null : choiceValue,
2554
- onChange: (v) => set(choiceMode === "picked" ? { picked: v ?? "all" } : { choices: v ?? "all" }),
2555
- options: choiceCount.map(([c, n]) => ({
2556
- value: c,
2557
- label: c,
2558
- count: choiceMode === "picked" ? pickedCount.get(c) ?? 0 : n
2559
- }))
2560
- }
2561
- )
2562
- }
2563
- ),
2564
- tagValues.length > 0 && /* @__PURE__ */ jsx21(FilterSection, { title: "Question details", children: tagValues.map(([key, values]) => /* @__PURE__ */ jsx21(
2565
- ChipGroup,
2566
- {
2567
- title: key,
2568
- value: filter.tags[key] ?? null,
2569
- onChange: (v) => setTag(key, v),
2570
- options: values.map((v) => ({ value: v, label: v, count: count((q) => q.tags?.[key] === v) }))
2571
- },
2572
- key
2573
- )) }),
2574
- types.length > 1 && /* @__PURE__ */ jsx21(FilterSection, { title: "Answer type", children: /* @__PURE__ */ jsx21(
2575
- ChipGroup,
2576
- {
2577
- value: filter.type === "all" ? null : filter.type,
2578
- onChange: (v) => set({ type: v ?? "all" }),
2579
- options: types.map((t) => ({
2580
- value: t,
2581
- label: TYPE_LABEL[t],
2582
- count: count((q) => q.parts.some((p) => p.field.type === t))
2583
- }))
2584
- }
2585
- ) })
2586
- ] }) }),
2707
+ menu.length > 0 && /* @__PURE__ */ jsx22(Popover2, { label: "Filters", icon: SlidersHorizontal, badge: fs.inUse.length, align: "right", children: /* @__PURE__ */ jsx22("div", { className: "flex flex-col gap-4", children: sections.map((sec) => /* @__PURE__ */ jsx22(FilterSection, { title: sec, children: menu.filter((g) => (g.section ?? "") === sec).map((g) => /* @__PURE__ */ jsx22(
2708
+ ChipGroup2,
2709
+ {
2710
+ title: g.title,
2711
+ value: g.value,
2712
+ onChange: g.set,
2713
+ options: g.options
2714
+ },
2715
+ g.id
2716
+ )) }, sec)) }) }),
2587
2717
  /* @__PURE__ */ jsxs16("span", { className: "text-[12.5px] text-muted sm:ml-auto", "aria-live": "polite", children: [
2588
- /* @__PURE__ */ jsx21("b", { className: "font-semibold text-ink-2", children: list.length }),
2718
+ /* @__PURE__ */ jsx22("b", { className: "font-semibold text-ink-2", children: list.length }),
2589
2719
  " of ",
2590
- questions.length,
2720
+ fs.total,
2591
2721
  " questions"
2592
2722
  ] }),
2593
- active && /* @__PURE__ */ jsx21(Button, { variant: "secondary", size: "sm", onClick: () => set(NO_FILTER), children: "Clear all" })
2723
+ fs.active && /* @__PURE__ */ jsx22(Button2, { variant: "secondary", size: "sm", onClick: fs.clear, children: "Clear all" })
2594
2724
  ] }),
2595
- categories.length > 0 && /* @__PURE__ */ jsxs16("div", { role: "group", "aria-label": "Category", className: "flex flex-wrap items-center gap-1.5", children: [
2596
- /* @__PURE__ */ jsx21("span", { className: "mr-1 text-[11.5px] font-semibold tracking-wide text-muted uppercase", children: "Category" }),
2597
- /* @__PURE__ */ jsx21(
2598
- Chip,
2599
- {
2600
- label: "All",
2601
- count: questions.length,
2602
- active: filter.category === "all",
2603
- onClick: () => set({ category: "all" })
2604
- }
2605
- ),
2606
- categories.map((c) => /* @__PURE__ */ jsx21(
2607
- Chip,
2725
+ bar.map((g) => /* @__PURE__ */ jsxs16("div", { role: "group", "aria-label": g.title, className: "flex flex-wrap items-center gap-1.5", children: [
2726
+ /* @__PURE__ */ jsx22("span", { className: "mr-1 text-[11.5px] font-semibold tracking-wide text-muted uppercase", children: g.title }),
2727
+ /* @__PURE__ */ jsx22(Chip2, { label: "All", count: fs.total, active: g.value == null, onClick: () => g.set(null) }),
2728
+ g.options.map((o) => /* @__PURE__ */ jsx22(
2729
+ Chip2,
2608
2730
  {
2609
- label: c,
2610
- count: count((q) => q.category === c),
2611
- active: filter.category === c,
2612
- onClick: () => set({ category: filter.category === c ? "all" : c })
2731
+ label: o.label,
2732
+ count: o.count,
2733
+ active: g.value === o.value,
2734
+ onClick: () => g.set(g.value === o.value ? null : o.value)
2613
2735
  },
2614
- c
2736
+ o.value
2615
2737
  ))
2616
- ] }),
2617
- inUse.length > 0 && /* @__PURE__ */ jsx21("div", { className: "flex flex-wrap items-center gap-1.5", children: inUse.map((f) => /* @__PURE__ */ jsx21(RemovableChip, { label: f.label, onRemove: f.clear }, f.label)) })
2738
+ ] }, g.id)),
2739
+ fs.inUse.length > 0 && /* @__PURE__ */ jsx22("div", { className: "flex flex-wrap items-center gap-1.5", children: fs.inUse.map((f) => /* @__PURE__ */ jsx22(RemovableChip2, { label: f.label, onRemove: f.clear }, f.id)) })
2618
2740
  ] }),
2619
- list.length === 0 && /* @__PURE__ */ jsx21(EmptyText, { children: "No question matches these filters." }),
2620
- /* @__PURE__ */ jsx21("ul", { className: "m-0 flex list-none flex-col gap-2 p-0", children: list.slice(0, shown).map((q, i, all) => /* @__PURE__ */ jsxs16(Fragment5, { children: [
2741
+ list.length === 0 && /* @__PURE__ */ jsx22(EmptyText2, { children: "No question matches these filters." }),
2742
+ /* @__PURE__ */ jsx22("ul", { className: "m-0 flex list-none flex-col gap-2 p-0", children: list.slice(0, shown).map((q, i, all) => /* @__PURE__ */ jsxs16(Fragment5, { children: [
2621
2743
  q.section && q.section !== all[i - 1]?.section && /* @__PURE__ */ jsxs16("li", { className: "mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-b border-line pb-1", children: [
2622
- /* @__PURE__ */ jsx21("span", { className: "text-[13.5px] font-semibold text-primary lg:text-[15px]", children: q.section }),
2623
- Object.entries(q.tags ?? {}).map(([k, v]) => /* @__PURE__ */ jsxs16(Tag, { tone: "muted", children: [
2744
+ /* @__PURE__ */ jsx22("span", { className: "text-[13.5px] font-semibold text-primary lg:text-[15px]", children: q.section }),
2745
+ Object.entries(q.tags ?? {}).map(([k, v]) => /* @__PURE__ */ jsxs16(Tag2, { tone: "muted", children: [
2624
2746
  k,
2625
2747
  ": ",
2626
2748
  v
2627
2749
  ] }, k))
2628
2750
  ] }),
2629
- q.intro && (q.intro !== all[i - 1]?.intro || q.section !== all[i - 1]?.section) && /* @__PURE__ */ jsx21("li", { className: "rounded-md border-l-4 border-primary bg-primary-soft px-3 py-2 text-[13px] font-medium text-ink-2 lg:text-[14px]", children: q.intro }),
2751
+ q.intro && (q.intro !== all[i - 1]?.intro || q.section !== all[i - 1]?.section) && /* @__PURE__ */ jsx22("li", { className: "rounded-md border-l-4 border-primary bg-primary-soft px-3 py-2 text-[13px] font-medium text-ink-2 lg:text-[14px]", children: q.intro }),
2630
2752
  /* @__PURE__ */ jsxs16("li", { className: "rounded-lg border border-line bg-white p-3", children: [
2631
2753
  /* @__PURE__ */ jsxs16("div", { className: "mb-2 flex items-start gap-2", children: [
2632
- /* @__PURE__ */ jsx21("p", { className: "m-0 min-w-0 flex-1 text-[13.5px] font-semibold whitespace-pre-line text-ink-2 lg:text-[15px]", children: q.title }),
2633
- q.category && /* @__PURE__ */ jsx21(Tag, { tone: "muted", children: q.category }),
2634
- /* @__PURE__ */ jsxs16(Tag, { tone: "muted", children: [
2754
+ /* @__PURE__ */ jsx22("p", { className: "m-0 min-w-0 flex-1 text-[13.5px] font-semibold whitespace-pre-line text-ink-2 lg:text-[15px]", children: q.title }),
2755
+ q.category && /* @__PURE__ */ jsx22(Tag2, { tone: "muted", children: q.category }),
2756
+ /* @__PURE__ */ jsxs16(Tag2, { tone: "muted", children: [
2635
2757
  "p.",
2636
2758
  q.page
2637
2759
  ] })
2638
2760
  ] }),
2639
- q.columns ? /* @__PURE__ */ jsx21(TableGrid, { q, control }) : /* @__PURE__ */ jsxs16(Fragment6, { children: [
2640
- q.rows && /* @__PURE__ */ jsx21(OptionGrid, { rows: q.rows, parts: q.parts.slice(0, gridSize(q)), control }),
2641
- q.parts.length > gridSize(q) && /* @__PURE__ */ jsx21("div", { className: `flex flex-wrap items-end gap-x-4 gap-y-2 ${q.rows ? "mt-3" : ""}`, children: q.parts.slice(gridSize(q)).map(({ field, name }) => /* @__PURE__ */ jsxs16(
2761
+ q.columns ? /* @__PURE__ */ jsx22(TableGrid, { q, control }) : /* @__PURE__ */ jsxs16(Fragment6, { children: [
2762
+ q.rows && /* @__PURE__ */ jsx22(OptionGrid, { rows: q.rows, parts: q.parts.slice(0, gridSize(q)), control }),
2763
+ q.parts.length > gridSize(q) && /* @__PURE__ */ jsx22("div", { className: `flex flex-wrap items-end gap-x-4 gap-y-2 ${q.rows ? "mt-3" : ""}`, children: q.parts.slice(gridSize(q)).map(({ field, name }) => /* @__PURE__ */ jsxs16(
2642
2764
  "div",
2643
2765
  {
2644
2766
  className: `flex min-w-0 flex-col gap-1 ${wide(field.type) ? "basis-full" : ""} ${field.type === "checkbox" ? "flex-row-reverse items-center justify-end gap-2" : "max-sm:basis-full"}`,
2645
2767
  children: [
2646
- name && /* @__PURE__ */ jsx21("span", { className: "text-[12px] text-muted lg:text-[13.5px]", children: name }),
2647
- /* @__PURE__ */ jsx21(
2768
+ name && /* @__PURE__ */ jsx22("span", { className: "text-[12px] text-muted lg:text-[13.5px]", children: name }),
2769
+ /* @__PURE__ */ jsx22(
2648
2770
  "div",
2649
2771
  {
2650
2772
  className: `relative [container-type:size] ${SIZE[field.type] ?? "h-9 w-full sm:w-80 lg:h-10 lg:w-96"}`,
@@ -2658,7 +2780,7 @@ function QuestionList({
2658
2780
  ] })
2659
2781
  ] })
2660
2782
  ] }, q.id)) }),
2661
- list.length > shown && /* @__PURE__ */ jsx21("div", { className: "mt-3 text-center", children: /* @__PURE__ */ jsxs16(Button, { variant: "secondary", size: "sm", onClick: () => setShown((n) => n + PAGE), children: [
2783
+ list.length > shown && /* @__PURE__ */ jsx22("div", { className: "mt-3 text-center", children: /* @__PURE__ */ jsxs16(Button2, { variant: "secondary", size: "sm", onClick: () => setShown((n) => n + PAGE), children: [
2662
2784
  "Show more (",
2663
2785
  list.length - shown,
2664
2786
  " left)"
@@ -2671,7 +2793,7 @@ function OptionGrid({
2671
2793
  control
2672
2794
  }) {
2673
2795
  const starts = new Set(rows.map((_, i) => rows.slice(0, i).reduce((a, b) => a + b, 0)));
2674
- return /* @__PURE__ */ jsx21(
2796
+ return /* @__PURE__ */ jsx22(
2675
2797
  "div",
2676
2798
  {
2677
2799
  className: "grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-[repeat(var(--cols),minmax(0,1fr))]",
@@ -2681,8 +2803,8 @@ function OptionGrid({
2681
2803
  {
2682
2804
  className: `flex min-w-0 items-center gap-2 text-[13px] text-ink-2 lg:text-[14.5px] ${starts.has(i) ? "sm:col-start-1" : ""}`,
2683
2805
  children: [
2684
- /* @__PURE__ */ jsx21("div", { className: "relative h-6 w-6 shrink-0 [container-type:size] lg:h-7 lg:w-7", children: control(field) }),
2685
- /* @__PURE__ */ jsx21(
2806
+ /* @__PURE__ */ jsx22("div", { className: "relative h-6 w-6 shrink-0 [container-type:size] lg:h-7 lg:w-7", children: control(field) }),
2807
+ /* @__PURE__ */ jsx22(
2686
2808
  "span",
2687
2809
  {
2688
2810
  className: "min-w-0 cursor-pointer break-words",
@@ -2699,13 +2821,13 @@ function OptionGrid({
2699
2821
  }
2700
2822
  function TableGrid({ q, control }) {
2701
2823
  const rows = q.rows.map((n, i) => q.parts.slice(q.rows.slice(0, i).reduce((a, b) => a + b, 0)).slice(0, n));
2702
- return /* @__PURE__ */ jsx21("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs16("table", { className: "w-full border-separate border-spacing-x-2 border-spacing-y-1.5 text-left", children: [
2703
- /* @__PURE__ */ jsx21("thead", { className: q.columns.some(Boolean) ? "" : "sr-only", children: /* @__PURE__ */ jsxs16("tr", { children: [
2704
- q.rowNames && /* @__PURE__ */ jsx21("th", {}),
2705
- q.columns.map((c, i) => /* @__PURE__ */ jsx21("th", { scope: "col", className: "min-w-32 px-0 text-[12px] font-medium text-muted lg:text-[13.5px]", children: c }, i))
2824
+ return /* @__PURE__ */ jsx22("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs16("table", { className: "w-full border-separate border-spacing-x-2 border-spacing-y-1.5 text-left", children: [
2825
+ /* @__PURE__ */ jsx22("thead", { className: q.columns.some(Boolean) ? "" : "sr-only", children: /* @__PURE__ */ jsxs16("tr", { children: [
2826
+ q.rowNames && /* @__PURE__ */ jsx22("th", {}),
2827
+ q.columns.map((c, i) => /* @__PURE__ */ jsx22("th", { scope: "col", className: "min-w-32 px-0 text-[12px] font-medium text-muted lg:text-[13.5px]", children: c }, i))
2706
2828
  ] }) }),
2707
- /* @__PURE__ */ jsx21("tbody", { children: rows.map((row, r) => /* @__PURE__ */ jsxs16("tr", { children: [
2708
- q.rowNames && /* @__PURE__ */ jsx21(
2829
+ /* @__PURE__ */ jsx22("tbody", { children: rows.map((row, r) => /* @__PURE__ */ jsxs16("tr", { children: [
2830
+ q.rowNames && /* @__PURE__ */ jsx22(
2709
2831
  "th",
2710
2832
  {
2711
2833
  scope: "row",
@@ -2713,7 +2835,7 @@ function TableGrid({ q, control }) {
2713
2835
  children: q.rowNames[r]
2714
2836
  }
2715
2837
  ),
2716
- row.map(({ field }) => /* @__PURE__ */ jsx21("td", { className: "p-0 align-middle", children: /* @__PURE__ */ jsx21(
2838
+ row.map(({ field }) => /* @__PURE__ */ jsx22("td", { className: "p-0 align-middle", children: /* @__PURE__ */ jsx22(
2717
2839
  "div",
2718
2840
  {
2719
2841
  className: `relative [container-type:size] ${field.type === "checkbox" ? "h-7 w-7" : `${field.type === "textarea" ? "h-16" : "h-9 lg:h-10"} w-full min-w-32`}`,
@@ -2726,25 +2848,15 @@ function TableGrid({ q, control }) {
2726
2848
  function FilterSection({ title, action, children }) {
2727
2849
  return /* @__PURE__ */ jsxs16("section", { className: "flex flex-col gap-2 border-b border-line-soft pb-3 last:border-0 last:pb-0", children: [
2728
2850
  /* @__PURE__ */ jsxs16("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
2729
- /* @__PURE__ */ jsx21("h4", { className: "m-0 text-[13px] font-semibold text-ink", children: title }),
2851
+ /* @__PURE__ */ jsx22("h4", { className: "m-0 text-[13px] font-semibold text-ink", children: title }),
2730
2852
  action
2731
2853
  ] }),
2732
2854
  children
2733
2855
  ] });
2734
2856
  }
2735
2857
 
2736
- // src/react/FormFiller.tsx
2737
- import { Fragment as Fragment7, jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
2738
- var humanSize = (n) => {
2739
- if (!n) return "";
2740
- const units = ["B", "KB", "MB", "GB"];
2741
- let i = 0;
2742
- while (n >= 1024 && i < units.length - 1) {
2743
- n /= 1024;
2744
- i++;
2745
- }
2746
- return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
2747
- };
2858
+ // src/react/hooks/useFormFiller.ts
2859
+ import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo3, useRef as useRef7, useState as useState12 } from "react";
2748
2860
  function download(blob, name) {
2749
2861
  const url = URL.createObjectURL(blob);
2750
2862
  const a = document.createElement("a");
@@ -2753,11 +2865,9 @@ function download(blob, name) {
2753
2865
  a.click();
2754
2866
  setTimeout(() => URL.revokeObjectURL(url), 1e3);
2755
2867
  }
2756
- function FormFiller({
2757
- pdf: pdfSource,
2868
+ function useFormFiller({
2758
2869
  fields,
2759
2870
  layout = null,
2760
- title,
2761
2871
  initialAnswers,
2762
2872
  files: initialFiles = [],
2763
2873
  readOnly = false,
@@ -2766,36 +2876,29 @@ function FormFiller({
2766
2876
  autosave = 2500,
2767
2877
  onUpload,
2768
2878
  loadFile,
2769
- onBack,
2770
- toolbar,
2879
+ toQuestions: toQuestions2 = toQuestions,
2771
2880
  notify = () => {
2772
2881
  },
2773
- onError: notifyError = (e) => console.error(e),
2774
- className = "h-full"
2882
+ onError: notifyError = (e) => console.error(e)
2775
2883
  }) {
2776
- const [answers, setAnswers] = useState10(() => initialAnswers ?? defaultAnswers(fields));
2777
- const [files, setFiles] = useState10(initialFiles);
2778
- const [signatures, setSignatures] = useState10({});
2779
- const [errors, setErrors] = useState10({});
2780
- const [pdfWidth, setPdfWidth] = useState10(595);
2781
- const pageWidth = Math.max(...layout?.pages.map((p) => p.width) ?? [pdfWidth]);
2782
- const { zoom, setZoom, ref: fitRef } = useFitZoom(pageWidth);
2783
- const compact = useCompact();
2784
- const [saveState, setSaveState] = useState10("saved");
2785
- const [hasSaved, setHasSaved] = useState10(!!initialAnswers);
2786
- const [confirmSubmit, setConfirmSubmit] = useState10(false);
2787
- const [loadError, setLoadError] = useState10("");
2788
- const queue = useRef6(Promise.resolve());
2789
- const latest = useRef6(answers);
2790
- useEffect5(() => {
2884
+ const [answers, setAnswers] = useState12(() => initialAnswers ?? defaultAnswers(fields));
2885
+ const [files, setFiles] = useState12(initialFiles);
2886
+ const [signatures, setSignatures] = useState12({});
2887
+ const [errors, setErrors] = useState12({});
2888
+ const [saveState, setSaveState] = useState12("saved");
2889
+ const [hasSaved, setHasSaved] = useState12(!!initialAnswers);
2890
+ const [pdf, setPdf] = useState12(null);
2891
+ const [pdfSizes, setPdfSizes] = useState12([]);
2892
+ const queue = useRef7(Promise.resolve());
2893
+ const latest = useRef7(answers);
2894
+ useEffect7(() => {
2791
2895
  latest.current = answers;
2792
2896
  }, [answers]);
2793
2897
  const states = useMemo3(() => fieldStates(fields, answers), [fields, answers]);
2794
- const [view, setView] = useState10("document");
2795
- const [pdf, setPdf] = useState10(null);
2796
- const [pageText, setPageText] = useState10(null);
2797
- useEffect5(() => {
2798
- if (view !== "list" || !pdf || pageText) return;
2898
+ const [wantText, setWantText] = useState12(false);
2899
+ const [pageText, setPageText] = useState12(null);
2900
+ useEffect7(() => {
2901
+ if (!wantText || !pdf || pageText) return;
2799
2902
  let live = true;
2800
2903
  (async () => {
2801
2904
  const out = {};
@@ -2808,7 +2911,7 @@ function FormFiller({
2808
2911
  return () => {
2809
2912
  live = false;
2810
2913
  };
2811
- }, [view, pdf, pageText]);
2914
+ }, [wantText, pdf, pageText]);
2812
2915
  const visibleKeys = fields.filter((f) => states[f.key]?.visible).map((f) => f.key).join(",");
2813
2916
  const questions = useMemo3(() => {
2814
2917
  const keys = new Set(visibleKeys.split(","));
@@ -2816,13 +2919,13 @@ function FormFiller({
2816
2919
  const source = layout ? layout.pages[page - 1]?.source : page;
2817
2920
  return source ? pageText?.[source] : void 0;
2818
2921
  };
2819
- return toQuestions(
2922
+ return toQuestions2(
2820
2923
  fields.filter((f) => keys.has(f.key)),
2821
2924
  textOf
2822
2925
  );
2823
- }, [fields, layout, visibleKeys, pageText]);
2824
- const loading = useRef6(/* @__PURE__ */ new Set());
2825
- useEffect5(() => {
2926
+ }, [fields, layout, visibleKeys, pageText, toQuestions2]);
2927
+ const loading = useRef7(/* @__PURE__ */ new Set());
2928
+ useEffect7(() => {
2826
2929
  if (!loadFile) return;
2827
2930
  for (const f of fields) {
2828
2931
  const v = answers[f.key];
@@ -2832,11 +2935,11 @@ function FormFiller({
2832
2935
  loadFile(v).then((blob) => setSignatures((m) => ({ ...m, [v]: URL.createObjectURL(blob) }))).catch(() => loading.current.delete(v));
2833
2936
  }
2834
2937
  }, [fields, answers, signatures, loadFile]);
2835
- const urls = useRef6(signatures);
2836
- useEffect5(() => {
2938
+ const urls = useRef7(signatures);
2939
+ useEffect7(() => {
2837
2940
  urls.current = signatures;
2838
2941
  }, [signatures]);
2839
- useEffect5(() => () => Object.values(urls.current).forEach((u) => URL.revokeObjectURL(u)), []);
2942
+ useEffect7(() => () => Object.values(urls.current).forEach((u) => URL.revokeObjectURL(u)), []);
2840
2943
  const save = useCallback3(
2841
2944
  (submit2 = false) => {
2842
2945
  if (!onSave) return Promise.resolve(true);
@@ -2860,7 +2963,7 @@ function FormFiller({
2860
2963
  },
2861
2964
  [onSave, notifyError]
2862
2965
  );
2863
- useEffect5(() => {
2966
+ useEffect7(() => {
2864
2967
  if (readOnly || autosave === false || !onSave || saveState !== "dirty") return;
2865
2968
  const t = setTimeout(() => void save(), autosave);
2866
2969
  return () => clearTimeout(t);
@@ -2889,135 +2992,248 @@ function FormFiller({
2889
2992
  }
2890
2993
  }
2891
2994
  const fetchFile = (id, then) => loadFile?.(id).then(then, () => notifyError("Unable to open the file"));
2995
+ const openFile = (id) => fetchFile(id, (blob) => {
2996
+ const url = URL.createObjectURL(blob);
2997
+ window.open(url, "_blank");
2998
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
2999
+ });
3000
+ const downloadFile = (id, name) => fetchFile(id, (blob) => download(blob, name));
2892
3001
  async function submit() {
2893
- setConfirmSubmit(false);
2894
3002
  const found = validateAnswers(fields, answers);
2895
3003
  setErrors(found);
2896
- if (Object.keys(found).length) return notifyError("Some answers need attention");
2897
- if (await save(true)) {
2898
- notify("Submitted");
2899
- onSubmitted?.(latest.current);
3004
+ if (Object.keys(found).length) {
3005
+ notifyError("Some answers need attention");
3006
+ return false;
2900
3007
  }
3008
+ if (!await save(true)) return false;
3009
+ notify("Submitted");
3010
+ onSubmitted?.(latest.current);
3011
+ return true;
2901
3012
  }
2902
- if (loadError) return /* @__PURE__ */ jsx22("p", { className: "fl-root p-6 text-center text-sm text-danger", children: loadError });
2903
3013
  const fileOf = (id) => files.find((x) => x.id === id);
2904
- const attachments = files.filter(
2905
- (x) => fields.some((f) => f.type === "file" && f.key === x.fieldKey && answers[f.key] === x.id)
2906
- );
2907
- const errorList = fields.filter((f) => errors[f.key]);
3014
+ return {
3015
+ answers,
3016
+ /** Visible / required / readOnly per field key, from the conditions and current answers. */
3017
+ states,
3018
+ /** Field key → message. */
3019
+ errors,
3020
+ setErrors,
3021
+ /** Fields with an error, in form order. */
3022
+ errorList: fields.filter((f) => errors[f.key]),
3023
+ change,
3024
+ save,
3025
+ submit,
3026
+ saveState,
3027
+ hasSaved,
3028
+ readOnly,
3029
+ files,
3030
+ fileOf,
3031
+ /** Files of upload fields that are answers now. */
3032
+ attachments: files.filter(
3033
+ (x) => fields.some((f) => f.type === "file" && f.key === x.fieldKey && answers[f.key] === x.id)
3034
+ ),
3035
+ attach,
3036
+ canUpload: !!onUpload,
3037
+ canLoadFiles: !!loadFile,
3038
+ openFile,
3039
+ downloadFile,
3040
+ /** Image src of a signature answer (stored file or data URL). */
3041
+ signatureSrc: (f) => signatures[String(answers[f.key])],
3042
+ /** Visible fields grouped as questions (tables, option groups), titled from the PDF text once loaded. */
3043
+ questions,
3044
+ /** Reads the PDF text so `questions` get real titles (call when you first show them). */
3045
+ loadPageText: () => setWantText(true),
3046
+ pdf,
3047
+ pdfSizes,
3048
+ /** Pass to PdfPages' onLoaded. */
3049
+ onPdfLoaded: (sizes2, doc) => {
3050
+ setPdf(doc);
3051
+ setPdfSizes(sizes2);
3052
+ }
3053
+ };
3054
+ }
3055
+
3056
+ // src/react/FormFiller.tsx
3057
+ import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
3058
+ var humanSize = (n) => {
3059
+ if (!n) return "";
3060
+ const units = ["B", "KB", "MB", "GB"];
3061
+ let i = 0;
3062
+ while (n >= 1024 && i < units.length - 1) {
3063
+ n /= 1024;
3064
+ i++;
3065
+ }
3066
+ return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
3067
+ };
3068
+ function FormFiller({
3069
+ pdf: pdfSource,
3070
+ title,
3071
+ onBack,
3072
+ toolbar,
3073
+ components,
3074
+ hide = [],
3075
+ views = ["document", "list"],
3076
+ listFilters,
3077
+ listSearch,
3078
+ hideUnavailableFilters,
3079
+ renderFilters,
3080
+ confirmSubmit: askFirst = true,
3081
+ className = "h-full",
3082
+ ...options
3083
+ }) {
3084
+ const shows = (p) => !hide.includes(p);
3085
+ const ff = useFormFiller(options);
3086
+ const { fields, readOnly = false, onSave } = options;
3087
+ const layout = options.layout ?? null;
3088
+ const { answers, states, errors } = ff;
3089
+ const pdfWidth = ff.pdfSizes.length ? Math.max(...ff.pdfSizes.map((p) => p.width)) : 595;
3090
+ const { zoom, setZoom, ref: fitRef } = useFitZoom(Math.max(...layout?.pages.map((p) => p.width) ?? [pdfWidth]));
3091
+ const compact = useCompact();
3092
+ const [confirmSubmit, setConfirmSubmit] = useState13(false);
3093
+ const [loadError, setLoadError] = useState13("");
3094
+ const [view, setView] = useState13(views[0] ?? "document");
3095
+ const { loadPageText } = ff;
3096
+ useEffect8(() => {
3097
+ if (view === "list") loadPageText();
3098
+ }, [view, loadPageText]);
3099
+ if (loadError) return /* @__PURE__ */ jsx23("p", { className: "fl-root p-6 text-center text-sm text-danger", children: loadError });
2908
3100
  const control = (f) => /* @__PURE__ */ jsxs17(Fragment7, { children: [
2909
- /* @__PURE__ */ jsx22(
2910
- FieldControl,
3101
+ /* @__PURE__ */ jsx23(
3102
+ AnswerControl,
2911
3103
  {
3104
+ components,
2912
3105
  field: f,
2913
3106
  value: answers[f.key],
2914
- readOnly: readOnly || states[f.key].readOnly || f.type === "file" && !onUpload,
3107
+ readOnly: readOnly || states[f.key].readOnly || f.type === "file" && !ff.canUpload,
2915
3108
  compact,
2916
3109
  error: errors[f.key],
2917
- onChange: (v) => change(f, v),
2918
- fileName: fileOf(answers[f.key])?.fileName,
2919
- signatureSrc: signatures[String(answers[f.key])],
2920
- onFile: onUpload ? (file) => void attach(f, file) : void 0,
2921
- onOpenFile: () => fetchFile(String(answers[f.key]), (blob) => {
2922
- const url = URL.createObjectURL(blob);
2923
- window.open(url, "_blank");
2924
- setTimeout(() => URL.revokeObjectURL(url), 6e4);
2925
- })
3110
+ onChange: (v) => ff.change(f, v),
3111
+ fileName: ff.fileOf(answers[f.key])?.fileName,
3112
+ signatureSrc: ff.signatureSrc(f),
3113
+ onFile: ff.canUpload ? (file) => void ff.attach(f, file) : void 0,
3114
+ onOpenFile: () => ff.openFile(String(answers[f.key]))
2926
3115
  }
2927
3116
  ),
2928
- states[f.key].required && !readOnly && /* @__PURE__ */ jsx22("span", { className: "pointer-events-none absolute -top-1.5 -right-1.5 text-[12px] font-bold text-danger", children: "*" })
3117
+ states[f.key].required && !readOnly && shows("requiredMark") && /* @__PURE__ */ jsx23("span", { className: "pointer-events-none absolute -top-1.5 -right-1.5 text-[12px] font-bold text-danger", children: "*" })
2929
3118
  ] });
2930
- return /* @__PURE__ */ jsxs17("div", { className: `fl-root flex min-h-[min(600px,100dvh)] flex-col bg-white text-ink ${className}`, children: [
3119
+ return /* @__PURE__ */ jsx23(FormliftProvider, { components, children: /* @__PURE__ */ jsxs17("div", { className: `fl-root flex min-h-[min(600px,100dvh)] flex-col bg-white text-ink ${className}`, children: [
2931
3120
  /* @__PURE__ */ jsxs17("div", { className: "flex flex-wrap items-center gap-3 border-b border-line bg-white px-4 py-2.5", children: [
2932
- onBack && /* @__PURE__ */ jsx22(Button, { variant: "secondary", size: "sm", icon: ArrowLeft2, onClick: onBack, children: "Back" }),
2933
- title != null && /* @__PURE__ */ jsx22("h2", { className: "m-0 min-w-0 flex-1 truncate text-[17px] font-semibold max-sm:text-[15px] sm:flex-none", children: title }),
2934
- /* @__PURE__ */ jsx22(
2935
- SegmentedTabs,
3121
+ onBack && shows("back") && /* @__PURE__ */ jsx23(Button2, { variant: "secondary", size: "sm", icon: ArrowLeft2, onClick: onBack, children: "Back" }),
3122
+ title != null && shows("title") && /* @__PURE__ */ jsx23("h2", { className: "m-0 min-w-0 flex-1 truncate text-[17px] font-semibold max-sm:text-[15px] sm:flex-none", children: title }),
3123
+ views.length > 1 && shows("views") && /* @__PURE__ */ jsx23(
3124
+ SegmentedTabs2,
2936
3125
  {
2937
- options: [
2938
- { value: "document", label: "Document" },
2939
- { value: "list", label: "List" }
2940
- ],
3126
+ options: views.map((v) => ({ value: v, label: v === "document" ? "Document" : "List" })),
2941
3127
  value: view,
2942
3128
  onChange: setView
2943
3129
  }
2944
3130
  ),
2945
3131
  /* @__PURE__ */ jsxs17("div", { className: "ml-auto flex flex-wrap items-center gap-2 max-sm:w-full max-sm:justify-end", children: [
2946
- view === "document" && /* @__PURE__ */ jsx22(ZoomControls, { zoom, onChange: setZoom }),
3132
+ view === "document" && shows("zoom") && /* @__PURE__ */ jsx23(ZoomControls, { zoom, onChange: setZoom }),
2947
3133
  !readOnly && onSave && /* @__PURE__ */ jsxs17(Fragment7, { children: [
2948
- /* @__PURE__ */ jsx22("span", { className: "text-right text-[12px] text-muted sm:w-24", "aria-live": "polite", children: {
2949
- saved: hasSaved ? "Draft saved" : "",
3134
+ shows("saveStatus") && /* @__PURE__ */ jsx23("span", { className: "text-right text-[12px] text-muted sm:w-24", "aria-live": "polite", children: {
3135
+ saved: ff.hasSaved ? "Draft saved" : "",
2950
3136
  dirty: "Unsaved",
2951
3137
  saving: "Saving\u2026",
2952
3138
  error: "Not saved"
2953
- }[saveState] }),
2954
- /* @__PURE__ */ jsx22(Button, { variant: "secondary", size: "sm", icon: Save2, title: "Save draft", onClick: () => void save(), children: /* @__PURE__ */ jsx22("span", { className: "max-sm:hidden", children: "Save draft" }) }),
2955
- /* @__PURE__ */ jsx22(Button, { size: "sm", icon: Send, onClick: () => setConfirmSubmit(true), children: "Submit" })
3139
+ }[ff.saveState] }),
3140
+ shows("saveDraft") && /* @__PURE__ */ jsx23(Button2, { variant: "secondary", size: "sm", icon: Save2, title: "Save draft", onClick: () => void ff.save(), children: /* @__PURE__ */ jsx23("span", { className: "max-sm:hidden", children: "Save draft" }) }),
3141
+ shows("submit") && /* @__PURE__ */ jsx23(Button2, { size: "sm", icon: Send, onClick: () => askFirst ? setConfirmSubmit(true) : void ff.submit(), children: "Submit" })
2956
3142
  ] }),
2957
3143
  toolbar
2958
3144
  ] })
2959
3145
  ] }),
2960
- attachments.length > 0 && /* @__PURE__ */ jsxs17("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-line bg-white px-4 py-1.5 text-[12.5px]", children: [
3146
+ ff.attachments.length > 0 && shows("attachments") && /* @__PURE__ */ jsxs17("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-line bg-white px-4 py-1.5 text-[12.5px]", children: [
2961
3147
  /* @__PURE__ */ jsxs17("span", { className: "inline-flex items-center gap-1 font-semibold text-ink-2", children: [
2962
- /* @__PURE__ */ jsx22(Paperclip2, { size: 13 }),
3148
+ /* @__PURE__ */ jsx23(Paperclip2, { size: 13 }),
2963
3149
  " Attachments"
2964
3150
  ] }),
2965
- attachments.map((x) => /* @__PURE__ */ jsxs17(
3151
+ ff.attachments.map((x) => /* @__PURE__ */ jsxs17(
2966
3152
  "button",
2967
3153
  {
2968
3154
  type: "button",
2969
3155
  title: `Download ${x.fileName}`,
2970
- disabled: !loadFile,
3156
+ disabled: !ff.canLoadFiles,
2971
3157
  className: "inline-flex max-w-full cursor-pointer items-center gap-1 text-primary hover:underline disabled:cursor-default",
2972
- onClick: () => fetchFile(x.id, (blob) => download(blob, x.fileName)),
3158
+ onClick: () => ff.downloadFile(x.id, x.fileName),
2973
3159
  children: [
2974
- /* @__PURE__ */ jsx22("span", { className: "truncate", children: x.fileName }),
3160
+ /* @__PURE__ */ jsx23("span", { className: "truncate", children: x.fileName }),
2975
3161
  x.fileSize ? /* @__PURE__ */ jsxs17("span", { className: "text-muted", children: [
2976
3162
  "(",
2977
3163
  humanSize(x.fileSize),
2978
3164
  ")"
2979
3165
  ] }) : null,
2980
- loadFile && /* @__PURE__ */ jsx22(Download, { size: 12 })
3166
+ ff.canLoadFiles && /* @__PURE__ */ jsx23(Download, { size: 12 })
2981
3167
  ]
2982
3168
  },
2983
3169
  x.id
2984
3170
  ))
2985
3171
  ] }),
2986
- errorList.length > 0 && /* @__PURE__ */ jsx22("ul", { className: "m-0 border-b border-danger/30 bg-danger/5 px-6 py-2 text-[13px] text-danger", children: errorList.map((f) => /* @__PURE__ */ jsxs17("li", { children: [
3172
+ ff.errorList.length > 0 && shows("errors") && /* @__PURE__ */ jsx23("ul", { className: "m-0 border-b border-danger/30 bg-danger/5 px-6 py-2 text-[13px] text-danger", children: ff.errorList.map((f) => /* @__PURE__ */ jsxs17("li", { children: [
2987
3173
  "Page ",
2988
3174
  f.position.page,
2989
3175
  ": ",
2990
3176
  errors[f.key]
2991
3177
  ] }, f.key)) }),
2992
- view === "list" && /* @__PURE__ */ jsx22("div", { className: "flex-1 overflow-auto bg-hover", children: /* @__PURE__ */ jsx22(QuestionList, { questions, answers, control }) }),
2993
- /* @__PURE__ */ jsx22("div", { ref: fitRef, className: `flex-1 overflow-auto bg-hover ${view === "list" ? "hidden" : ""}`, children: /* @__PURE__ */ jsx22(
3178
+ view === "list" && /* @__PURE__ */ jsx23("div", { className: "flex-1 overflow-auto bg-hover", children: /* @__PURE__ */ jsx23(
3179
+ QuestionList,
3180
+ {
3181
+ questions: ff.questions,
3182
+ answers,
3183
+ control,
3184
+ filters: listFilters,
3185
+ search: listSearch,
3186
+ hideUnavailable: hideUnavailableFilters,
3187
+ renderToolbar: renderFilters
3188
+ }
3189
+ ) }),
3190
+ /* @__PURE__ */ jsx23("div", { ref: fitRef, className: `flex-1 overflow-auto bg-hover ${view === "list" ? "hidden" : ""}`, children: /* @__PURE__ */ jsx23(
2994
3191
  PdfPages,
2995
3192
  {
2996
3193
  load: pdfSource,
2997
3194
  zoom,
2998
3195
  pages: layout?.pages,
2999
- onLoaded: (sizes2, doc) => {
3000
- setPdf(doc);
3001
- if (sizes2.length) setPdfWidth(Math.max(...sizes2.map((p) => p.width)));
3002
- },
3196
+ onLoaded: ff.onPdfLoaded,
3003
3197
  onError: () => setLoadError("The document could not be shown"),
3004
3198
  overlay: (page) => [
3005
3199
  // The admin's text / boxes / lines first, the answer controls on top.
3006
- ...(layout?.elements ?? []).filter((el) => el.page === page).map((el) => /* @__PURE__ */ jsx22(LayoutElementView, { element: el, pageHeight: layout.pages[page - 1]?.height ?? 842 }, el.id)),
3007
- ...fields.filter((f) => f.position.page === page && states[f.key]?.visible).map((f) => /* @__PURE__ */ jsx22("div", { className: "absolute [container-type:size]", style: boxStyle(f.position), children: control(f) }, f.id))
3200
+ ...(layout?.elements ?? []).filter((el) => el.page === page).map((el) => /* @__PURE__ */ jsx23(LayoutElementView, { element: el, pageHeight: layout.pages[page - 1]?.height ?? 842 }, el.id)),
3201
+ ...fields.filter((f) => f.position.page === page && states[f.key]?.visible).map((f) => /* @__PURE__ */ jsx23("div", { className: "absolute [container-type:size]", style: boxStyle(f.position), children: control(f) }, f.id))
3008
3202
  ]
3009
3203
  }
3010
3204
  ) }),
3011
- confirmSubmit && /* @__PURE__ */ jsx22(ConfirmModal, { confirmLabel: "Submit", onConfirm: () => void submit(), onCancel: () => setConfirmSubmit(false), children: "Submit this form? You cannot change the answers afterwards." })
3012
- ] });
3205
+ confirmSubmit && /* @__PURE__ */ jsx23(
3206
+ ConfirmModal2,
3207
+ {
3208
+ confirmLabel: "Submit",
3209
+ onConfirm: () => {
3210
+ setConfirmSubmit(false);
3211
+ void ff.submit();
3212
+ },
3213
+ onCancel: () => setConfirmSubmit(false),
3214
+ children: "Submit this form? You cannot change the answers afterwards."
3215
+ }
3216
+ )
3217
+ ] }) });
3013
3218
  }
3014
3219
  export {
3220
+ AnswerControl,
3015
3221
  FieldControl,
3016
3222
  FormEditor,
3017
3223
  FormFiller,
3224
+ FormliftProvider,
3225
+ LayoutElementView,
3018
3226
  PdfPages,
3227
+ QuestionList,
3019
3228
  TYPE_LABEL,
3229
+ defaultComponents,
3230
+ defaultFilters,
3020
3231
  readPdf,
3021
- setPdfWorker
3232
+ setPdfWorker,
3233
+ useFitZoom,
3234
+ useFormEditor,
3235
+ useFormFiller,
3236
+ useQuestionFilter,
3237
+ useUndo
3022
3238
  };
3023
3239
  //# sourceMappingURL=index.js.map