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,8 +1,10 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, Ref } from 'react';
2
+ import { ReactNode, PointerEvent, ButtonHTMLAttributes, InputHTMLAttributes, ComponentType, Ref, CSSProperties } from 'react';
3
3
  import { PDFDocumentProxy } from 'pdfjs-dist';
4
- import { g as LayoutPage, F as FieldDef, L as Layout, b as Answers, A as AnswerValue, a as FieldType } from '../form-logic-DT9Bfwfz.js';
5
- import { T as TextLine } from '../layout-ExhBeO6b.js';
4
+ import { g as LayoutPage, F as FieldDef, L as Layout, a as FieldType, f as LayoutElement, P as Position, b as Answers, A as AnswerValue } from '../form-logic-DT9Bfwfz.js';
5
+ import { T as TextLine, E as ElementTool, P as PageText, Q as Question, F as FilterOption, a as FilterSource, b as FilterValues } from '../filters-Yzd4tG8r.js';
6
+ import * as pdfjs_dist_types_src_display_api from 'pdfjs-dist/types/src/display/api';
7
+ import { LucideIcon } from 'lucide-react';
6
8
 
7
9
  /** A PDF: URL, Blob / File, bytes, or a (possibly async) function returning one of those. */
8
10
  type PdfBytes = string | URL | Blob | ArrayBuffer | Uint8Array;
@@ -31,11 +33,321 @@ declare function PdfPages({ load, zoom, pages, overlay, withText, onLoaded, onEr
31
33
  onError?: (err: unknown) => void;
32
34
  }): react.JSX.Element;
33
35
 
36
+ type Selection = {
37
+ kind: 'field' | 'element';
38
+ id: string;
39
+ } | null;
34
40
  /** The editor's document: fields + layout (null = the original pages untouched, nothing drawn). */
35
41
  type FormDoc = {
36
42
  fields: FieldDef[];
37
43
  layout: Layout | null;
38
44
  };
45
+ interface UseFormEditorOptions {
46
+ /** Saved fields + layout to start from (omit for a new form). Remount (`key`) to load another. */
47
+ initialValue?: FormDoc;
48
+ /** Called by save() with the document. A rejected promise keeps the changes unsaved. */
49
+ onSave: (doc: FormDoc) => unknown;
50
+ /** Every change (also undo / redo). */
51
+ onChange?: (doc: FormDoc) => void;
52
+ /** Run auto-detect when the PDF opens. Default: only when there are no fields and no layout yet. */
53
+ autoDetect?: boolean;
54
+ /** Called with the suggested fields after a detect that found some. */
55
+ onDetected?: (found: FieldDef[]) => void;
56
+ /** Delete, Ctrl+Z / Ctrl+Y, Ctrl+D, arrows, Escape on window. Default true. */
57
+ keyboard?: boolean;
58
+ /** Field types that can be added. Default: all (FIELD_TYPES). */
59
+ fieldTypes?: readonly FieldType[];
60
+ /** Layout tools that can be added. Default: all. */
61
+ elementTools?: readonly ElementTool[];
62
+ /** Browser warning when leaving with unsaved changes. Default true. */
63
+ warnUnsaved?: boolean;
64
+ notify?: (message: string) => void;
65
+ onError?: (error: unknown) => void;
66
+ }
67
+ /**
68
+ * Everything FormEditor does, without its UI: the document with undo / redo, selection, placing / moving / resizing
69
+ * fields and layout items, pages, auto-detect, preview answers, validation and save. Render the pages with
70
+ * PdfPages (`onLoaded={ed.onPdfLoaded}`) and draw your own boxes, panels and toolbars around it.
71
+ */
72
+ declare function useFormEditor({ initialValue, onSave, onChange, autoDetect, onDetected, keyboard, warnUnsaved, fieldTypes, elementTools, notify, onError: notifyError, }: UseFormEditorOptions): {
73
+ doc: FormDoc;
74
+ fields: FieldDef[];
75
+ /** The layout as shown (the original pages until changed); null until the PDF has loaded. */
76
+ layout: Layout | null;
77
+ pageCount: number;
78
+ /** Page size in PDF points, for drawing layout text at the right size. */
79
+ pageHeight: (page: number) => number;
80
+ /** Fields + layout items per page. */
81
+ counts: Record<number, number>;
82
+ dirty: boolean;
83
+ /** Validation messages; save() refuses while there are any. */
84
+ problems: string[];
85
+ busy: boolean;
86
+ save: () => Promise<boolean>;
87
+ undo: () => void;
88
+ redo: () => void;
89
+ canUndo: boolean;
90
+ canRedo: boolean;
91
+ /** Changes the document; `record: false` = no undo step (live drags). */
92
+ setFields: (fn: (f: FieldDef[]) => FieldDef[], record?: boolean) => void;
93
+ setElements: (fn: (e: LayoutElement[]) => LayoutElement[], record?: boolean) => void;
94
+ updateField: (f: FieldDef) => void;
95
+ updateElement: (e: LayoutElement) => void;
96
+ reorderField: (id: string, step: -1 | 1) => void;
97
+ movePage: (n: number, step: -1 | 1) => void;
98
+ insertBlankPage: (n: number) => void;
99
+ deletePage: (n: number) => void;
100
+ selection: Selection;
101
+ setSelection: react.Dispatch<react.SetStateAction<Selection>>;
102
+ selectedField: FieldDef | null;
103
+ selectedElement: LayoutElement | null;
104
+ removeSelected: () => void;
105
+ duplicateSelected: () => void;
106
+ nudge: (dx: number, dy: number) => void;
107
+ startDrag: (e: PointerEvent, sel: NonNullable<Selection>, start: Position, mode: "move" | "resize") => void;
108
+ onDrag: (e: PointerEvent) => void;
109
+ endDrag: () => void;
110
+ /** Tools for a palette: drag with `dataTransfer` / tap to arm, then placeNew. */
111
+ fieldTools: string[];
112
+ elementTools: string[];
113
+ armed: string | null;
114
+ setArmed: react.Dispatch<react.SetStateAction<string | null>>;
115
+ placeNew: (key: string, page: number, clientX: number, clientY: number, rect: DOMRect) => boolean;
116
+ editText: boolean;
117
+ setEditText: react.Dispatch<react.SetStateAction<boolean>>;
118
+ editLine: (line: TextLine, page: number) => void;
119
+ snapOn: boolean;
120
+ setSnapOn: react.Dispatch<react.SetStateAction<boolean>>;
121
+ preview: boolean;
122
+ /** Preview starts with empty answers; leaving it ends Edit text. */
123
+ setPreview: (on: boolean) => void;
124
+ previewAnswers: Answers;
125
+ setPreviewAnswers: react.Dispatch<react.SetStateAction<Answers>>;
126
+ previewStates: Record<string, {
127
+ visible: boolean;
128
+ required: boolean;
129
+ readOnly: boolean;
130
+ }>;
131
+ pdf: pdfjs_dist_types_src_display_api.PDFDocumentProxy | null;
132
+ /** Pass to PdfPages' onLoaded. */
133
+ onPdfLoaded: (s: PageSize[], d: PDFDocumentProxy) => void;
134
+ pageSizes: PageSize[] | null;
135
+ detect: (auto?: boolean) => Promise<void>;
136
+ /** Progress while detecting; `stop.abort()` keeps what was found so far. */
137
+ detecting: {
138
+ done: number;
139
+ total: number;
140
+ stop: AbortController;
141
+ } | null;
142
+ };
143
+ type FormEditorState = ReturnType<typeof useFormEditor>;
144
+
145
+ declare const variants: {
146
+ primary: string;
147
+ secondary: string;
148
+ outline: string;
149
+ light: string;
150
+ heading: string;
151
+ success: string;
152
+ brand: string;
153
+ danger: string;
154
+ };
155
+ declare const sizes: {
156
+ sm: string;
157
+ md: string;
158
+ };
159
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
160
+ variant?: keyof typeof variants;
161
+ size?: keyof typeof sizes;
162
+ /** Icon before the label. */
163
+ icon?: LucideIcon;
164
+ }
165
+ /** The app's button. `type` defaults to "button" so it never submits a form by accident. */
166
+ declare function Button({ variant, size, icon: Icon, className, type, children, ...rest }: ButtonProps): react.JSX.Element;
167
+
168
+ /** The app's checkbox (theme colour). `label` = accessible name when there is no visible text. */
169
+ declare function Checkbox({ label, className, ...rest }: InputHTMLAttributes<HTMLInputElement> & {
170
+ label: string;
171
+ }): react.JSX.Element;
172
+
173
+ interface ChipOption {
174
+ value: string;
175
+ label: string;
176
+ /** How many items have it, shown after the label. */
177
+ count?: number;
178
+ }
179
+ /** A pill that can be switched on and off (a filter value). */
180
+ declare function Chip({ label, count, active, onClick, }: {
181
+ label: string;
182
+ count?: number;
183
+ active?: boolean;
184
+ onClick: () => void;
185
+ }): react.JSX.Element;
186
+ /** One pick out of a few pills; a click on the picked one clears it. Optional title before them. */
187
+ declare function ChipGroup({ title, options, value, onChange, }: {
188
+ title?: string;
189
+ options: ChipOption[];
190
+ value: string | null;
191
+ onChange: (value: string | null) => void;
192
+ }): react.JSX.Element;
193
+ /** A filter in use, with × to take it off. */
194
+ declare function RemovableChip({ label, onRemove }: {
195
+ label: string;
196
+ onRemove: () => void;
197
+ }): react.JSX.Element;
198
+
199
+ /** Label, the control, and its error under it. */
200
+ declare function FormField({ label, required, error, children, }: {
201
+ label: string;
202
+ required?: boolean;
203
+ error?: string;
204
+ children: ReactNode;
205
+ }): react.JSX.Element;
206
+
207
+ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
208
+ /** Tooltip and accessible name. */
209
+ title: string;
210
+ icon: LucideIcon;
211
+ /** Red on hover (delete). */
212
+ danger?: boolean;
213
+ iconSize?: number;
214
+ /** On a coloured background (panel headings): white icon. */
215
+ light?: boolean;
216
+ }
217
+ /** Icon-only button: view, download, delete, edit, expand… */
218
+ declare function IconButton({ title, icon: Icon, danger, iconSize, light, className, ...rest }: IconButtonProps): react.JSX.Element;
219
+
220
+ interface ModalProps {
221
+ children: ReactNode;
222
+ onClose: () => void;
223
+ /** Tailwind width class, e.g. 'w-[560px]' */
224
+ width: string;
225
+ }
226
+ declare function Modal({ children, onClose, width }: ModalProps): react.JSX.Element;
227
+ /** "Are you sure…?" with Yes / No. */
228
+ declare function ConfirmModal({ children, confirmLabel, cancelLabel, danger, onConfirm, onCancel, }: {
229
+ children: ReactNode;
230
+ confirmLabel?: string;
231
+ cancelLabel?: string;
232
+ /** Red confirm button (deletes). */
233
+ danger?: boolean;
234
+ onConfirm: () => void;
235
+ onCancel: () => void;
236
+ }): react.JSX.Element;
237
+
238
+ /** A button that opens a panel under it (closes on a click outside or Escape). `badge`: a count on the button. */
239
+ declare function Popover({ label, icon: Icon, badge, align, children, }: {
240
+ label: string;
241
+ icon?: LucideIcon;
242
+ badge?: number;
243
+ align?: 'left' | 'right';
244
+ children: ReactNode;
245
+ }): react.JSX.Element;
246
+
247
+ /** Pill toggle between a few views ("Chats | New Chats"). `sm`: a small one inside a panel. */
248
+ declare function SegmentedTabs<T extends string>({ options, value, onChange, size, }: {
249
+ options: {
250
+ value: T;
251
+ label: string;
252
+ }[];
253
+ value: T;
254
+ onChange: (value: T) => void;
255
+ size?: 'sm' | 'md';
256
+ }): react.JSX.Element;
257
+
258
+ /**
259
+ * Draw a signature with mouse, pen or finger. onSave gets a PNG data URL (transparent background).
260
+ * Scaled down to at most `maxWidth` pixels wide so it stays small enough to send.
261
+ */
262
+ declare function SignaturePad({ onSave, onCancel, maxWidth, }: {
263
+ onSave: (dataUrl: string) => void;
264
+ onCancel: () => void;
265
+ maxWidth?: number;
266
+ }): react.JSX.Element;
267
+
268
+ interface FieldControlProps {
269
+ field: FieldDef;
270
+ value: AnswerValue | undefined;
271
+ onChange: (v: AnswerValue) => void;
272
+ readOnly?: boolean;
273
+ error?: string;
274
+ fileName?: string;
275
+ onFile?: (file: File) => void;
276
+ onOpenFile?: () => void;
277
+ /** Image of a stored signature (the answer is its file id); signatures from before are data URLs. */
278
+ signatureSrc?: string;
279
+ /** Phone / touch: typed answers open in a full-size box (the page's boxes are too small to type in). */
280
+ compact?: boolean;
281
+ }
282
+ /**
283
+ * One answer control drawn inside its box on the page. `error` outlines it red; the message shows as a tooltip
284
+ * and in the page's error list.
285
+ */
286
+ declare function FieldControl({ field, value, onChange, readOnly, error, fileName, onFile, onOpenFile, signatureSrc, compact, }: FieldControlProps): react.JSX.Element;
287
+ /** FieldControl, or the replacement from `components` (per field type first, then FieldControl). */
288
+ declare function AnswerControl({ components, ...props }: FieldControlProps & {
289
+ components?: FormliftComponents;
290
+ }): react.JSX.Element;
291
+
292
+ declare const base: {
293
+ Button: typeof Button;
294
+ IconButton: typeof IconButton;
295
+ Checkbox: typeof Checkbox;
296
+ FormField: typeof FormField;
297
+ TextInput: ({ className, ...rest }: react.InputHTMLAttributes<HTMLInputElement>) => react.JSX.Element;
298
+ Select: ({ className, ...rest }: react.SelectHTMLAttributes<HTMLSelectElement>) => react.JSX.Element;
299
+ TextArea: ({ className, ...rest }: react.TextareaHTMLAttributes<HTMLTextAreaElement>) => react.JSX.Element;
300
+ Modal: typeof Modal;
301
+ ModalHeader: ({ title, onClose, disabled, }: {
302
+ title: string;
303
+ onClose: () => void;
304
+ disabled?: boolean;
305
+ }) => react.JSX.Element;
306
+ ModalBody: ({ children }: {
307
+ children: ReactNode;
308
+ }) => react.JSX.Element;
309
+ ModalFooter: ({ children }: {
310
+ children: ReactNode;
311
+ }) => react.JSX.Element;
312
+ ConfirmModal: typeof ConfirmModal;
313
+ PanelHeading: ({ children, actions }: {
314
+ children: ReactNode;
315
+ actions?: ReactNode;
316
+ }) => react.JSX.Element;
317
+ EmptyText: ({ children, className }: {
318
+ children: ReactNode;
319
+ className?: string;
320
+ }) => react.JSX.Element;
321
+ SegmentedTabs: typeof SegmentedTabs;
322
+ Chip: typeof Chip;
323
+ ChipGroup: typeof ChipGroup;
324
+ RemovableChip: typeof RemovableChip;
325
+ Popover: typeof Popover;
326
+ Tag: ({ tone, children }: {
327
+ tone?: "success" | "info" | "muted";
328
+ children: ReactNode;
329
+ }) => react.JSX.Element;
330
+ SignaturePad: typeof SignaturePad;
331
+ };
332
+ /** The built-in UI primitives. A replacement takes the same props: `ComponentProps<UIComponents['Button']>`. */
333
+ type UIComponents = typeof base;
334
+ declare const defaultComponents: UIComponents;
335
+ /** Replaces formlift's UI primitives for everything under it. Nested providers merge (inner wins). */
336
+ declare function FormliftProvider({ components, children, }: {
337
+ components?: Partial<UIComponents>;
338
+ children: ReactNode;
339
+ }): react.JSX.Element;
340
+ /**
341
+ * What `components` on FormEditor / FormFiller takes: UI primitives, the answer control for every field
342
+ * (`FieldControl`), or one per field type (`fields: { date: MyDatePicker }`; wins over FieldControl).
343
+ */
344
+ type FormliftComponents = Partial<UIComponents> & {
345
+ FieldControl?: ComponentType<FieldControlProps>;
346
+ fields?: Partial<Record<FieldType, ComponentType<FieldControlProps>>>;
347
+ };
348
+
349
+ /** Parts of the FormEditor screen that can be hidden with `hide`. */
350
+ type EditorPart = 'back' | 'title' | 'status' | 'undo' | 'snap' | 'zoom' | 'editText' | 'autoDetect' | 'preview' | 'save' | 'tools' | 'fieldTools' | 'layoutTools' | 'panel' | 'propertiesTab' | 'fieldsTab' | 'pagesTab' | 'problems';
39
351
  /** Imperative handle (`ref`): save from outside, e.g. before a workflow step. */
40
352
  interface FormEditorHandle {
41
353
  /** Saves if there are changes. false = invalid or onSave failed. */
@@ -43,15 +355,9 @@ interface FormEditorHandle {
43
355
  readonly dirty: boolean;
44
356
  readonly value: FormDoc;
45
357
  }
46
- interface FormEditorProps {
358
+ interface FormEditorProps extends UseFormEditorOptions {
47
359
  /** The PDF: URL, Blob / File, bytes, or a function returning one. Never modified. */
48
360
  pdf: PdfSource;
49
- /** Saved fields + layout to start from (omit for a new form). Remount (`key`) to load another. */
50
- initialValue?: FormDoc;
51
- /** Called by Save with the document. A rejected promise keeps the changes unsaved. */
52
- onSave: (doc: FormDoc) => unknown;
53
- /** Every change (also undo / redo). */
54
- onChange?: (doc: FormDoc) => void;
55
361
  title?: ReactNode;
56
362
  /** Back button in the toolbar (hidden when omitted). */
57
363
  onBack?: () => void;
@@ -65,10 +371,10 @@ interface FormEditorProps {
65
371
  label: string;
66
372
  content: ReactNode;
67
373
  }[];
68
- /** Run auto-detect when the PDF opens. Default: only when there are no fields and no layout yet. */
69
- autoDetect?: boolean;
70
- notify?: (message: string) => void;
71
- onError?: (error: unknown) => void;
374
+ /** Replace any UI primitive (Button, Modal, …), or the answer controls used in preview (FieldControl / `fields`). */
375
+ components?: FormliftComponents;
376
+ /** Parts of the screen to leave out (the logic still runs: hide 'save' and call `ref.save()` yourself). */
377
+ hide?: EditorPart[];
72
378
  /** Extra classes on the root. It fills its container's height (`h-full`) by default: give the container a height. */
73
379
  className?: string;
74
380
  ref?: Ref<FormEditorHandle>;
@@ -79,8 +385,9 @@ interface FormEditorProps {
79
385
  * clickable: a click covers it with an editable copy. Right: properties of the selection, the field list, pages
80
386
  * (reorder, delete, add blank) and history. Nothing changes the uploaded PDF; all of it is saved in the version.
81
387
  * Keyboard: Delete, Ctrl+Z / Ctrl+Y, Ctrl+D duplicate, arrows nudge (Shift = bigger steps).
388
+ * All the logic lives in useFormEditor: use that hook directly for a completely custom UI.
82
389
  */
83
- declare function FormEditor({ pdf: pdfSource, initialValue, onSave, onChange, title, onBack, toolbar, banner, tabs, autoDetect, notify, onError: notifyError, className, ref, }: FormEditorProps): react.JSX.Element;
390
+ declare function FormEditor({ pdf: pdfSource, title, onBack, toolbar, banner, tabs, components, hide, className, ref, ...options }: FormEditorProps): react.JSX.Element;
84
391
 
85
392
  /** A file answer (upload or signature PNG) as your storage knows it. The answer's value is its `id`. */
86
393
  interface AnswerFileInfo {
@@ -90,17 +397,15 @@ interface AnswerFileInfo {
90
397
  /** Bytes, for the size shown next to the name. */
91
398
  fileSize?: number;
92
399
  }
93
- interface FormFillerProps {
94
- /** The PDF: URL, Blob / File, bytes, or a function returning one. */
95
- pdf: PdfSource;
400
+ type SaveState = 'saved' | 'dirty' | 'saving' | 'error';
401
+ interface UseFormFillerOptions {
96
402
  fields: FieldDef[];
97
403
  layout?: Layout | null;
98
- title?: ReactNode;
99
404
  /** A draft to continue. Default: each field's default value. */
100
405
  initialAnswers?: Answers;
101
406
  /** Files already attached to these answers. */
102
407
  files?: AnswerFileInfo[];
103
- /** View only (a submitted form): no Save / Submit. */
408
+ /** View only: no saving, no autosave. */
104
409
  readOnly?: boolean;
105
410
  /**
106
411
  * Save a draft (`submit: false`, also autosave) or submit (answers already pass validateAnswers — check again on
@@ -113,42 +418,164 @@ interface FormFillerProps {
113
418
  onSubmitted?: (answers: Answers) => void;
114
419
  /** Autosave delay in ms after the last change; false = off. Default 2500. */
115
420
  autosave?: number | false;
116
- /** Store a file / signature PNG; resolve with its id. Without it, file fields are read-only and signatures are kept as PNG data URLs in the answer. */
421
+ /** Store a file / signature PNG; resolve with its id. */
117
422
  onUpload?: (field: FieldDef, file: File) => Promise<AnswerFileInfo>;
118
- /** The bytes of a stored file (shows stored signatures, opens / downloads attachments). */
423
+ /** The bytes of a stored file (stored signatures, opening / downloading attachments). */
119
424
  loadFile?: (id: string) => Promise<Blob>;
425
+ /**
426
+ * Groups the visible fields into questions for the list. Default: formlift's toQuestions. Wrap it to change
427
+ * them, e.g. `(f, t) => toQuestions(f, t, { categoryCode: false }).map(setMyCategory)`.
428
+ */
429
+ toQuestions?: (fields: FieldDef[], textOf: PageText) => Question[];
430
+ notify?: (message: string) => void;
431
+ onError?: (error: unknown) => void;
432
+ }
433
+ /**
434
+ * Everything FormFiller does, without its UI: answers, conditions, validation, queued saves + autosave, uploads,
435
+ * signatures and the question list. Draw the fields yourself (`boxStyle(field.position)` over each page, or a
436
+ * plain list of `questions`) and call `change`, `save`, `submit`.
437
+ */
438
+ declare function useFormFiller({ fields, layout, initialAnswers, files: initialFiles, readOnly, onSave, onSubmitted, autosave, onUpload, loadFile, toQuestions, notify, onError: notifyError, }: UseFormFillerOptions): {
439
+ answers: Answers;
440
+ /** Visible / required / readOnly per field key, from the conditions and current answers. */
441
+ states: Record<string, {
442
+ visible: boolean;
443
+ required: boolean;
444
+ readOnly: boolean;
445
+ }>;
446
+ /** Field key → message. */
447
+ errors: Record<string, string>;
448
+ setErrors: react.Dispatch<react.SetStateAction<Record<string, string>>>;
449
+ /** Fields with an error, in form order. */
450
+ errorList: FieldDef[];
451
+ change: (f: FieldDef, v: AnswerValue) => void;
452
+ save: (submit?: boolean) => Promise<boolean>;
453
+ submit: () => Promise<boolean>;
454
+ saveState: SaveState;
455
+ hasSaved: boolean;
456
+ readOnly: boolean;
457
+ files: AnswerFileInfo[];
458
+ fileOf: (id: AnswerValue | undefined) => AnswerFileInfo | undefined;
459
+ /** Files of upload fields that are answers now. */
460
+ attachments: AnswerFileInfo[];
461
+ attach: (f: FieldDef, file: File) => Promise<void>;
462
+ canUpload: boolean;
463
+ canLoadFiles: boolean;
464
+ openFile: (id: string) => Promise<void> | undefined;
465
+ downloadFile: (id: string, name: string) => Promise<void> | undefined;
466
+ /** Image src of a signature answer (stored file or data URL). */
467
+ signatureSrc: (f: FieldDef) => string;
468
+ /** Visible fields grouped as questions (tables, option groups), titled from the PDF text once loaded. */
469
+ questions: Question[];
470
+ /** Reads the PDF text so `questions` get real titles (call when you first show them). */
471
+ loadPageText: () => void;
472
+ pdf: pdfjs_dist_types_src_display_api.PDFDocumentProxy | null;
473
+ pdfSizes: PageSize[];
474
+ /** Pass to PdfPages' onLoaded. */
475
+ onPdfLoaded: (sizes: PageSize[], doc: PDFDocumentProxy) => void;
476
+ };
477
+ type FormFillerState = ReturnType<typeof useFormFiller>;
478
+
479
+ /** The built-in filters (labels from formlift's UI). Pick from it by id, reorder it, add your own. */
480
+ declare const defaultFilters: FilterSource[];
481
+ /** One filter, ready to draw: its options for this form, the picked value, and set(). */
482
+ interface FilterGroup {
483
+ id: string;
484
+ title: string;
485
+ section?: string;
486
+ options: FilterOption[];
487
+ /** false = this form has nothing to filter on here (no options). Show it or not: your call. */
488
+ available: boolean;
489
+ value: string | null;
490
+ /** Picks a value; null clears it. */
491
+ set: (value: string | null) => void;
492
+ }
493
+ interface UseQuestionFilterOptions {
494
+ /**
495
+ * The filters, in order: filter objects / sources (see `formlift` core: categoryFilter, partFilter,
496
+ * customFilter…) or ids of defaultFilters ('answered', 'remarks', 'comments', 'choices', 'picked', 'tags',
497
+ * 'type', 'category'). Default: defaultFilters.
498
+ */
499
+ filters?: (FilterSource | string)[];
500
+ /** Text search. Default true. */
501
+ search?: boolean;
502
+ /** Starting state (e.g. restored from the URL). */
503
+ initial?: {
504
+ search?: string;
505
+ values?: FilterValues;
506
+ };
507
+ /** Your own rule, applied after the filters. */
508
+ predicate?: (q: Question, answers: Answers) => boolean;
509
+ /** Every change (e.g. to keep it in the URL). */
510
+ onChange?: (state: {
511
+ search: string;
512
+ values: FilterValues;
513
+ }) => void;
514
+ }
515
+ /**
516
+ * Search and filters over questions, as data. formlift decides nothing about what to show: every filter you
517
+ * pass comes back as a group with `available` (does this form have anything to filter on?); you draw the ones
518
+ * you want, the way you want. QuestionList uses the same hook.
519
+ * While the filter stays the same, answers typed meanwhile do not make questions vanish mid-word.
520
+ */
521
+ declare function useQuestionFilter(questions: Question[], answers: Answers, { filters, search: searchOn, initial, predicate, onChange }?: UseQuestionFilterOptions): {
522
+ groups: FilterGroup[];
523
+ /** One group by id (undefined when not in `filters`). */
524
+ group: (id: string) => FilterGroup | undefined;
525
+ /** The search text; null when search is off. */
526
+ search: string | null;
527
+ setSearch: (text: string) => void;
528
+ values: FilterValues;
529
+ /** Filters in use, as chips to take off one by one. */
530
+ inUse: {
531
+ id: string;
532
+ label: string;
533
+ clear: () => void;
534
+ }[];
535
+ active: boolean;
536
+ clear: () => void;
537
+ /** The questions that match, in form order. */
538
+ list: Question[];
539
+ total: number;
540
+ /** Bumps on every change (reset paging / scroll on it). */
541
+ version: number;
542
+ };
543
+ type QuestionFilterState = ReturnType<typeof useQuestionFilter>;
544
+
545
+ /** Parts of the FormFiller screen that can be hidden with `hide`. */
546
+ type FillerPart = 'back' | 'title' | 'views' | 'zoom' | 'saveStatus' | 'saveDraft' | 'submit' | 'attachments' | 'errors' | 'requiredMark';
547
+ interface FormFillerProps extends UseFormFillerOptions {
548
+ /** The PDF: URL, Blob / File, bytes, or a function returning one. */
549
+ pdf: PdfSource;
550
+ title?: ReactNode;
120
551
  onBack?: () => void;
121
552
  /** Your own items at the end of the toolbar (e.g. "Download completed PDF"). */
122
553
  toolbar?: ReactNode;
123
- notify?: (message: string) => void;
124
- onError?: (error: unknown) => void;
554
+ /** Replace any UI primitive (Button, Modal, …), the answer control (FieldControl) or one per type (`fields`). */
555
+ components?: FormliftComponents;
556
+ /** Parts of the screen to leave out. */
557
+ hide?: FillerPart[];
558
+ /** The views offered, the first one shown first. Default ['document', 'list']. */
559
+ views?: ('document' | 'list')[];
560
+ /** The list view's filters, in order: filter objects or ids of defaultFilters. Default: defaultFilters. */
561
+ listFilters?: UseQuestionFilterOptions['filters'];
562
+ /** Search box over the list. Default true. */
563
+ listSearch?: boolean;
564
+ /** Leave out filters this form has nothing for (e.g. Category on a form without category codes). Default true. */
565
+ hideUnavailableFilters?: boolean;
566
+ /** Your own search / filter bar over the list instead of the built-in one; `fs` has every filter as data. */
567
+ renderFilters?: (fs: QuestionFilterState) => ReactNode;
568
+ /** Ask "Submit this form?" first. Default true. */
569
+ confirmSubmit?: boolean;
125
570
  /** Extra classes on the root. It fills its container's height (`h-full`) by default: give the container a height. */
126
571
  className?: string;
127
572
  }
128
573
  /**
129
574
  * The PDF with the form's fields on it. The user fills them in place (or as a searchable list); answers autosave
130
575
  * through onSave, Submit checks them with the same rules as validateAnswers on your server.
576
+ * All the logic lives in useFormFiller: use that hook directly for a completely custom UI.
131
577
  */
132
- declare function FormFiller({ pdf: pdfSource, fields, layout, title, initialAnswers, files: initialFiles, readOnly, onSave, onSubmitted, autosave, onUpload, loadFile, onBack, toolbar, notify, onError: notifyError, className, }: FormFillerProps): react.JSX.Element;
133
-
134
- /**
135
- * One answer control drawn inside its box on the page. `error` outlines it red; the message shows as a tooltip
136
- * and in the page's error list.
137
- */
138
- declare function FieldControl({ field, value, onChange, readOnly, error, fileName, onFile, onOpenFile, signatureSrc, compact, }: {
139
- field: FieldDef;
140
- value: AnswerValue | undefined;
141
- onChange: (v: AnswerValue) => void;
142
- readOnly?: boolean;
143
- error?: string;
144
- fileName?: string;
145
- onFile?: (file: File) => void;
146
- onOpenFile?: () => void;
147
- /** Image of a stored signature (the answer is its file id); signatures from before are data URLs. */
148
- signatureSrc?: string;
149
- /** Phone / touch: typed answers open in a full-size box (the page's boxes are too small to type in). */
150
- compact?: boolean;
151
- }): react.JSX.Element;
578
+ declare function FormFiller({ pdf: pdfSource, title, onBack, toolbar, components, hide, views, listFilters, listSearch, hideUnavailableFilters, renderFilters, confirmSubmit: askFirst, className, ...options }: FormFillerProps): react.JSX.Element;
152
579
 
153
580
  /**
154
581
  * Where pdf.js loads its worker from. Call once before the first document opens, e.g. with Vite:
@@ -159,4 +586,60 @@ declare function setPdfWorker(src: string | Worker): void;
159
586
 
160
587
  declare const TYPE_LABEL: Record<FieldType, string>;
161
588
 
162
- export { type AnswerFileInfo, FieldControl, type FormDoc, FormEditor, type FormEditorHandle, type FormEditorProps, FormFiller, type FormFillerProps, type PageSize, type PdfBytes, PdfPages, type PdfSource, TYPE_LABEL, readPdf, setPdfWorker };
589
+ /**
590
+ * Undo / redo over one value. `set(next, false)` changes it without a history step (during a drag; call
591
+ * `checkpoint()` when the drag starts so the whole drag undoes in one step).
592
+ */
593
+ declare function useUndo<T>(initial: T): {
594
+ value: T;
595
+ set: (next: T | ((v: T) => T), record?: boolean) => void;
596
+ checkpoint: () => void;
597
+ undo: () => void;
598
+ redo: () => void;
599
+ reset: (v: T) => void;
600
+ canUndo: boolean;
601
+ canRedo: boolean;
602
+ };
603
+
604
+ /**
605
+ * Zoom that fits a page `pageWidthPt` points wide into the scroll panel (a big screen gets a big page, up to the
606
+ * highest zoom), following the
607
+ * panel's size (phone, tablet, a resized window) until the user picks a zoom. Pass `ref` to the scroll panel.
608
+ */
609
+ declare function useFitZoom(pageWidthPt: number): {
610
+ zoom: number;
611
+ setZoom: react.Dispatch<react.SetStateAction<number | null>>;
612
+ ref: react.Dispatch<react.SetStateAction<HTMLElement | null>>;
613
+ };
614
+
615
+ /**
616
+ * One of the admin's items drawn on its page: text (with optional background that hides the original under it),
617
+ * box, or line. Purely visual; the editor wraps it for selecting and dragging.
618
+ */
619
+ declare function LayoutElementView({ element: e, pageHeight, className, style, }: {
620
+ element: LayoutElement;
621
+ pageHeight: number;
622
+ className?: string;
623
+ style?: CSSProperties;
624
+ }): react.JSX.Element;
625
+
626
+ /**
627
+ * The form as questions with search and filters: only the questions that match are shown. Answers are the
628
+ * same as on the document; `control(field)` draws a field's answer control. Filters come from useQuestionFilter:
629
+ * `filters` / `search` choose them, `filter` drives the list from your own useQuestionFilter, `renderToolbar`
630
+ * draws your own bar, `toolbar={false}` none. Filters with nothing to offer in this form are left out unless
631
+ * `hideUnavailable={false}`.
632
+ */
633
+ declare function QuestionList({ questions, answers, control, filter: external, filters, search, hideUnavailable, toolbar, renderToolbar, }: {
634
+ questions: Question[];
635
+ answers: Answers;
636
+ control: (f: FieldDef) => ReactNode;
637
+ filter?: QuestionFilterState;
638
+ filters?: UseQuestionFilterOptions['filters'];
639
+ search?: boolean;
640
+ hideUnavailable?: boolean;
641
+ toolbar?: boolean;
642
+ renderToolbar?: (fs: QuestionFilterState) => ReactNode;
643
+ }): react.JSX.Element;
644
+
645
+ export { AnswerControl, type AnswerFileInfo, type EditorPart, FieldControl, type FieldControlProps, type FillerPart, type FilterGroup, type FormDoc, FormEditor, type FormEditorHandle, type FormEditorProps, type FormEditorState, FormFiller, type FormFillerProps, type FormFillerState, type FormliftComponents, FormliftProvider, LayoutElementView, type PageSize, type PdfBytes, PdfPages, type PdfSource, type QuestionFilterState, QuestionList, type SaveState, type Selection, TYPE_LABEL, type UIComponents, type UseFormEditorOptions, type UseFormFillerOptions, type UseQuestionFilterOptions, defaultComponents, defaultFilters, readPdf, setPdfWorker, useFitZoom, useFormEditor, useFormFiller, useQuestionFilter, useUndo };