@usefillo/react 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.
package/dist/index.js ADDED
@@ -0,0 +1,1094 @@
1
+ "use client";
2
+
3
+ // src/FilloForm.tsx
4
+ import { useEffect as useEffect6, useMemo, useRef as useRef4, useState as useState5 } from "react";
5
+ import { FilloError as FilloError2 } from "@usefillo/core";
6
+
7
+ // src/context.ts
8
+ import { allFields } from "@usefillo/core";
9
+ import { createContext, useContext } from "react";
10
+ var FilloContext = createContext(null);
11
+ function useFillo() {
12
+ const api = useContext(FilloContext);
13
+ if (!api) throw new Error("useFillo must be used inside <FilloForm> or <FilloProvider>");
14
+ return api;
15
+ }
16
+ function useField(fieldId) {
17
+ const api = useFillo();
18
+ const field = allFields(api.form).find((f) => f.id === fieldId);
19
+ return {
20
+ field,
21
+ value: api.data[fieldId],
22
+ error: api.errors[fieldId],
23
+ setValue: (value) => api.setValue(fieldId, value)
24
+ };
25
+ }
26
+
27
+ // src/controller.ts
28
+ import { useEffect, useRef, useSyncExternalStore } from "react";
29
+ import {
30
+ createFormController
31
+ } from "@usefillo/core";
32
+ function useFilloController(options) {
33
+ const optsRef = useRef(options);
34
+ optsRef.current = options;
35
+ const storeRef = useRef(null);
36
+ if (storeRef.current === null) {
37
+ storeRef.current = createFormController({
38
+ form: options.form,
39
+ formId: options.formId,
40
+ client: options.client,
41
+ initialData: options.initialData,
42
+ onChange: (d) => optsRef.current.onChange?.(d),
43
+ onSubmitted: (id, d) => optsRef.current.onSubmitted?.(id, d),
44
+ // Only take over submission when a handler was supplied at mount; otherwise
45
+ // the engine submits through the client.
46
+ onSubmit: options.onSubmit ? (d) => optsRef.current.onSubmit?.(d) : void 0,
47
+ getHoneypot: () => optsRef.current.getHoneypot?.() ?? ""
48
+ });
49
+ }
50
+ const store = storeRef.current;
51
+ useEffect(() => {
52
+ store.setContext({ form: options.form, formId: options.formId, client: options.client });
53
+ }, [store, options.form, options.formId, options.client]);
54
+ useEffect(() => () => store.destroy(), [store]);
55
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
56
+ return {
57
+ form: options.form,
58
+ formId: options.formId,
59
+ client: options.client,
60
+ ...state,
61
+ setValue: store.setValue,
62
+ setUploading: store.setUploading,
63
+ next: store.next,
64
+ back: store.back,
65
+ submit: store.submit
66
+ };
67
+ }
68
+
69
+ // src/define.ts
70
+ import {
71
+ FilloError,
72
+ isCodeForm,
73
+ syncCodeForm
74
+ } from "@usefillo/core";
75
+ import { useEffect as useEffect2, useState } from "react";
76
+ import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
77
+ function contentHash(input) {
78
+ let h = 5381;
79
+ for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) | 0;
80
+ return (h >>> 0).toString(36);
81
+ }
82
+ function useCodeFormSync(form, client, onError) {
83
+ const codeForm = isCodeForm(form) ? form : null;
84
+ const [syncedFormId, setSyncedFormId] = useState(null);
85
+ const hasKey = Boolean(client?.key);
86
+ const handle = codeForm?.id;
87
+ const contentKey = codeForm ? contentHash(JSON.stringify(codeForm.schema) + JSON.stringify(codeForm.theme ?? null)) : null;
88
+ useEffect2(() => {
89
+ if (!codeForm || !client?.key) return;
90
+ let cancelled = false;
91
+ syncCodeForm(client, codeForm).then((r) => !cancelled && setSyncedFormId(r.formId)).catch((err) => {
92
+ const error = err instanceof FilloError ? err : new FilloError(String(err), 0);
93
+ console.warn("[fillo] form sync failed:", error.message);
94
+ if (!cancelled) onError?.(error);
95
+ });
96
+ return () => {
97
+ cancelled = true;
98
+ };
99
+ }, [hasKey, handle, contentKey, client, onError]);
100
+ return syncedFormId;
101
+ }
102
+
103
+ // src/fields.tsx
104
+ import { isField, pipeBlock } from "@usefillo/core";
105
+ import { useEffect as useEffect5, useState as useState4 } from "react";
106
+
107
+ // src/signature.tsx
108
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
109
+ import { jsx, jsxs } from "react/jsx-runtime";
110
+ function SignatureField({ field, value, error, setValue }) {
111
+ const canvasRef = useRef2(null);
112
+ const drawing = useRef2(false);
113
+ const [hasInk, setHasInk] = useState2(Boolean(value));
114
+ useEffect3(() => {
115
+ const canvas = canvasRef.current;
116
+ if (!canvas) return;
117
+ const scale = window.devicePixelRatio || 1;
118
+ const rect = canvas.getBoundingClientRect();
119
+ canvas.width = rect.width * scale;
120
+ canvas.height = rect.height * scale;
121
+ const ctx = canvas.getContext("2d");
122
+ if (!ctx) return;
123
+ ctx.scale(scale, scale);
124
+ ctx.lineWidth = 2;
125
+ ctx.lineCap = "round";
126
+ ctx.lineJoin = "round";
127
+ ctx.strokeStyle = getComputedStyle(canvas).color;
128
+ if (typeof value === "string" && value.startsWith("data:image/")) {
129
+ const img = new Image();
130
+ img.onload = () => ctx.drawImage(img, 0, 0, rect.width, rect.height);
131
+ img.src = value;
132
+ }
133
+ }, []);
134
+ function point(e) {
135
+ const rect = e.currentTarget.getBoundingClientRect();
136
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
137
+ }
138
+ function start(e) {
139
+ drawing.current = true;
140
+ e.currentTarget.setPointerCapture(e.pointerId);
141
+ const ctx = e.currentTarget.getContext("2d");
142
+ if (!ctx) return;
143
+ const { x, y } = point(e);
144
+ ctx.beginPath();
145
+ ctx.moveTo(x, y);
146
+ }
147
+ function move(e) {
148
+ if (!drawing.current) return;
149
+ const ctx = e.currentTarget.getContext("2d");
150
+ if (!ctx) return;
151
+ const { x, y } = point(e);
152
+ ctx.lineTo(x, y);
153
+ ctx.stroke();
154
+ }
155
+ function end(e) {
156
+ if (!drawing.current) return;
157
+ drawing.current = false;
158
+ setHasInk(true);
159
+ setValue(e.currentTarget.toDataURL("image/png"));
160
+ }
161
+ function clear() {
162
+ const canvas = canvasRef.current;
163
+ const ctx = canvas?.getContext("2d");
164
+ if (!canvas || !ctx) return;
165
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
166
+ setHasInk(false);
167
+ setValue(null);
168
+ }
169
+ const describedBy = [field.description ? `fillo-${field.id}-desc` : null, error ? `fillo-${field.id}-error` : null].filter(Boolean).join(" ") || void 0;
170
+ return /* @__PURE__ */ jsxs("div", { className: `fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`, children: [
171
+ /* @__PURE__ */ jsxs("label", { className: "fillo-label", htmlFor: `fillo-${field.id}`, children: [
172
+ field.label,
173
+ field.required && /* @__PURE__ */ jsx("span", { className: "fillo-required", "aria-hidden": "true", children: " *" })
174
+ ] }),
175
+ field.description && /* @__PURE__ */ jsx("p", { className: "fillo-description", id: `fillo-${field.id}-desc`, children: field.description }),
176
+ /* @__PURE__ */ jsxs("div", { className: "fillo-signature", children: [
177
+ /* @__PURE__ */ jsx(
178
+ "canvas",
179
+ {
180
+ ref: canvasRef,
181
+ id: `fillo-${field.id}`,
182
+ className: "fillo-signature-canvas",
183
+ "aria-invalid": error ? true : void 0,
184
+ "aria-describedby": describedBy,
185
+ onPointerDown: start,
186
+ onPointerMove: move,
187
+ onPointerUp: end,
188
+ onPointerLeave: end
189
+ }
190
+ ),
191
+ hasInk && /* @__PURE__ */ jsx("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
192
+ !hasInk && /* @__PURE__ */ jsx("span", { className: "fillo-signature-hint", children: "Sign here" })
193
+ ] }),
194
+ error && /* @__PURE__ */ jsx("p", { className: "fillo-error", id: `fillo-${field.id}-error`, role: "alert", children: error })
195
+ ] });
196
+ }
197
+
198
+ // src/upload.tsx
199
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "react";
200
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
201
+ function formatBytes(bytes) {
202
+ if (bytes < 1024) return `${bytes} B`;
203
+ if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(0)} KB`;
204
+ if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
205
+ return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
206
+ }
207
+ function FileUploadField({ field, value, error, setValue, api }) {
208
+ const schema = field;
209
+ const inputRef = useRef3(null);
210
+ const [inFlight, setInFlight] = useState3([]);
211
+ const [dragOver, setDragOver] = useState3(false);
212
+ const mountedRef = useRef3(true);
213
+ const controllers = useRef3(/* @__PURE__ */ new Map());
214
+ useEffect4(
215
+ () => () => {
216
+ mountedRef.current = false;
217
+ controllers.current.forEach((c) => c.abort());
218
+ },
219
+ []
220
+ );
221
+ const files = Array.isArray(value) ? value : [];
222
+ const maxFiles = schema.maxFiles ?? 1;
223
+ const maxBytes = (schema.maxFileSizeMb ?? 500) * 1024 * 1024;
224
+ const canUpload = Boolean(api.client && api.formId);
225
+ const remaining = maxFiles - files.length - inFlight.length;
226
+ async function startUpload(file) {
227
+ const key = `${file.name}-${file.size}-${Math.floor(performance.now())}`;
228
+ if (file.size > maxBytes) {
229
+ setInFlight((prev) => [
230
+ ...prev,
231
+ { key, name: file.name, size: file.size, fraction: 0, error: `Larger than ${schema.maxFileSizeMb ?? 500} MB limit` }
232
+ ]);
233
+ return;
234
+ }
235
+ setInFlight((prev) => [...prev, { key, name: file.name, size: file.size, fraction: 0 }]);
236
+ api.setUploading(field.id, true);
237
+ const controller = new AbortController();
238
+ controllers.current.set(key, controller);
239
+ try {
240
+ const uploaded = await api.client.uploadFile(api.formId, file, {
241
+ fieldId: field.id,
242
+ signal: controller.signal,
243
+ onProgress: ({ fraction }) => {
244
+ if (mountedRef.current) {
245
+ setInFlight((prev) => prev.map((f) => f.key === key ? { ...f, fraction } : f));
246
+ }
247
+ }
248
+ });
249
+ if (!mountedRef.current) return;
250
+ setInFlight((prev) => prev.filter((f) => f.key !== key));
251
+ const current = Array.isArray(api.data[field.id]) ? api.data[field.id] : [];
252
+ setValue([...current, uploaded]);
253
+ } catch {
254
+ if (mountedRef.current) {
255
+ setInFlight(
256
+ (prev) => prev.map((f) => f.key === key ? { ...f, error: "Upload failed \u2014 try again" } : f)
257
+ );
258
+ }
259
+ } finally {
260
+ controllers.current.delete(key);
261
+ if (mountedRef.current) api.setUploading(field.id, false);
262
+ }
263
+ }
264
+ function cancelUpload(key) {
265
+ controllers.current.get(key)?.abort();
266
+ setInFlight((prev) => prev.filter((f) => f.key !== key));
267
+ }
268
+ function handleFiles(list) {
269
+ if (!list) return;
270
+ Array.from(list).slice(0, Math.max(0, remaining)).forEach((file) => void startUpload(file));
271
+ if (inputRef.current) inputRef.current.value = "";
272
+ }
273
+ function removeFile(fileId) {
274
+ setValue(files.filter((f) => f.fileId !== fileId));
275
+ }
276
+ const describedBy = [field.description ? `fillo-${field.id}-desc` : null, error ? `fillo-${field.id}-error` : null].filter(Boolean).join(" ") || void 0;
277
+ return /* @__PURE__ */ jsxs2("div", { className: `fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`, children: [
278
+ /* @__PURE__ */ jsxs2("label", { className: "fillo-label", htmlFor: `fillo-${field.id}`, children: [
279
+ field.label,
280
+ field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-required", "aria-hidden": "true", children: " *" })
281
+ ] }),
282
+ field.description && /* @__PURE__ */ jsx2("p", { className: "fillo-description", id: `fillo-${field.id}-desc`, children: field.description }),
283
+ remaining > 0 && /* @__PURE__ */ jsxs2(
284
+ "div",
285
+ {
286
+ className: `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`,
287
+ onDragOver: (e) => {
288
+ e.preventDefault();
289
+ setDragOver(true);
290
+ },
291
+ onDragLeave: () => setDragOver(false),
292
+ onDrop: (e) => {
293
+ e.preventDefault();
294
+ setDragOver(false);
295
+ if (canUpload) handleFiles(e.dataTransfer.files);
296
+ },
297
+ onClick: () => canUpload && inputRef.current?.click(),
298
+ role: "button",
299
+ tabIndex: 0,
300
+ onKeyDown: (e) => {
301
+ if ((e.key === "Enter" || e.key === " ") && canUpload) inputRef.current?.click();
302
+ },
303
+ children: [
304
+ /* @__PURE__ */ jsx2(
305
+ "input",
306
+ {
307
+ ref: inputRef,
308
+ id: `fillo-${field.id}`,
309
+ type: "file",
310
+ hidden: true,
311
+ "aria-invalid": error ? true : void 0,
312
+ "aria-required": field.required ? true : void 0,
313
+ "aria-describedby": describedBy,
314
+ multiple: maxFiles > 1,
315
+ accept: schema.accept?.join(","),
316
+ onChange: (e) => handleFiles(e.target.files)
317
+ }
318
+ ),
319
+ canUpload ? /* @__PURE__ */ jsxs2(Fragment, { children: [
320
+ /* @__PURE__ */ jsxs2("span", { className: "fillo-dropzone-title", children: [
321
+ "Drop ",
322
+ maxFiles > 1 ? "files" : "a file",
323
+ " here or click to browse"
324
+ ] }),
325
+ /* @__PURE__ */ jsxs2("span", { className: "fillo-dropzone-hint", children: [
326
+ "Up to ",
327
+ schema.maxFileSizeMb ?? 500,
328
+ " MB per file \xB7 resumable"
329
+ ] })
330
+ ] }) : /* @__PURE__ */ jsx2("span", { className: "fillo-dropzone-hint", children: "Uploads are disabled in preview" })
331
+ ]
332
+ }
333
+ ),
334
+ (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs2("ul", { className: "fillo-files", children: [
335
+ files.map((f) => /* @__PURE__ */ jsxs2("li", { className: "fillo-file fillo-file--done", children: [
336
+ /* @__PURE__ */ jsx2("span", { className: "fillo-file-name", children: f.name }),
337
+ /* @__PURE__ */ jsx2("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
338
+ /* @__PURE__ */ jsx2(
339
+ "button",
340
+ {
341
+ type: "button",
342
+ className: "fillo-file-remove",
343
+ "aria-label": `Remove ${f.name}`,
344
+ onClick: () => removeFile(f.fileId),
345
+ children: "\xD7"
346
+ }
347
+ )
348
+ ] }, f.fileId)),
349
+ inFlight.map((f) => /* @__PURE__ */ jsxs2("li", { className: `fillo-file${f.error ? " fillo-file--failed" : ""}`, children: [
350
+ /* @__PURE__ */ jsx2("span", { className: "fillo-file-name", children: f.name }),
351
+ f.error ? /* @__PURE__ */ jsxs2(Fragment, { children: [
352
+ /* @__PURE__ */ jsx2("span", { className: "fillo-file-meta", children: f.error }),
353
+ /* @__PURE__ */ jsx2(
354
+ "button",
355
+ {
356
+ type: "button",
357
+ className: "fillo-file-remove",
358
+ "aria-label": "Dismiss",
359
+ onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
360
+ children: "\xD7"
361
+ }
362
+ )
363
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
364
+ /* @__PURE__ */ jsxs2("span", { className: "fillo-file-meta", children: [
365
+ Math.round(f.fraction * 100),
366
+ "% of ",
367
+ formatBytes(f.size)
368
+ ] }),
369
+ /* @__PURE__ */ jsx2("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
370
+ /* @__PURE__ */ jsx2(
371
+ "button",
372
+ {
373
+ type: "button",
374
+ className: "fillo-file-remove",
375
+ "aria-label": `Cancel ${f.name}`,
376
+ onClick: () => cancelUpload(f.key),
377
+ children: "\xD7"
378
+ }
379
+ )
380
+ ] })
381
+ ] }, f.key))
382
+ ] }),
383
+ error && /* @__PURE__ */ jsx2("p", { className: "fillo-error", id: `fillo-${field.id}-error`, role: "alert", children: error })
384
+ ] });
385
+ }
386
+
387
+ // src/fields.tsx
388
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
389
+ function fieldAria(field, error) {
390
+ const describedBy = [
391
+ field.description ? `fillo-${field.id}-desc` : null,
392
+ error ? `fillo-${field.id}-error` : null
393
+ ].filter(Boolean);
394
+ return {
395
+ "aria-invalid": error ? true : void 0,
396
+ "aria-required": field.required ? true : void 0,
397
+ "aria-describedby": describedBy.length ? describedBy.join(" ") : void 0
398
+ };
399
+ }
400
+ function FieldShell({
401
+ field,
402
+ error,
403
+ children
404
+ }) {
405
+ return /* @__PURE__ */ jsxs3(
406
+ "div",
407
+ {
408
+ className: `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
409
+ "data-field": field.id,
410
+ children: [
411
+ /* @__PURE__ */ jsxs3("label", { className: "fillo-label", id: `fillo-${field.id}-label`, htmlFor: `fillo-${field.id}`, children: [
412
+ field.label,
413
+ field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-required", "aria-hidden": "true", children: " *" })
414
+ ] }),
415
+ field.description && /* @__PURE__ */ jsx3("p", { className: "fillo-description", id: `fillo-${field.id}-desc`, children: field.description }),
416
+ children,
417
+ error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: `fillo-${field.id}-error`, role: "alert", children: error })
418
+ ]
419
+ }
420
+ );
421
+ }
422
+ function TextInput({ field, value, error, setValue }) {
423
+ const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "phone" ? "tel" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
424
+ return /* @__PURE__ */ jsx3(FieldShell, { field, error, children: /* @__PURE__ */ jsx3(
425
+ "input",
426
+ {
427
+ id: `fillo-${field.id}`,
428
+ className: "fillo-input",
429
+ type,
430
+ value: typeof value === "string" || typeof value === "number" ? String(value) : "",
431
+ placeholder: field.placeholder,
432
+ onChange: (e) => setValue(e.target.value),
433
+ ...fieldAria(field, error)
434
+ }
435
+ ) });
436
+ }
437
+ function LongText({ field, value, error, setValue }) {
438
+ return /* @__PURE__ */ jsx3(FieldShell, { field, error, children: /* @__PURE__ */ jsx3(
439
+ "textarea",
440
+ {
441
+ id: `fillo-${field.id}`,
442
+ className: "fillo-input fillo-textarea",
443
+ rows: 4,
444
+ value: typeof value === "string" ? value : "",
445
+ placeholder: field.placeholder,
446
+ onChange: (e) => setValue(e.target.value),
447
+ ...fieldAria(field, error)
448
+ }
449
+ ) });
450
+ }
451
+ function useDisplayOptions(choice) {
452
+ const idsKey = choice.options.map((o) => o.id).join("\0");
453
+ const [order, setOrder] = useState4(null);
454
+ useEffect5(() => {
455
+ if (!choice.shuffleOptions) return;
456
+ const ids = idsKey === "" ? [] : idsKey.split("\0");
457
+ for (let i = ids.length - 1; i > 0; i--) {
458
+ const j = Math.floor(Math.random() * (i + 1));
459
+ [ids[i], ids[j]] = [ids[j], ids[i]];
460
+ }
461
+ setOrder(ids);
462
+ }, [choice.shuffleOptions, idsKey]);
463
+ if (!choice.shuffleOptions || !order) return choice.options;
464
+ const byId = new Map(choice.options.map((o) => [o.id, o]));
465
+ const shuffled = order.flatMap((id) => byId.get(id) ?? []);
466
+ return shuffled.length === choice.options.length ? shuffled : choice.options;
467
+ }
468
+ function SingleChoice({ field, value, error, setValue }) {
469
+ const choice = field;
470
+ const options = useDisplayOptions(choice);
471
+ const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
472
+ const [otherOn, setOtherOn] = useState4(isOtherValue);
473
+ const otherActive = otherOn || isOtherValue;
474
+ return /* @__PURE__ */ jsx3(FieldShell, { field, error, children: /* @__PURE__ */ jsxs3(
475
+ "div",
476
+ {
477
+ className: "fillo-options",
478
+ role: "radiogroup",
479
+ "aria-labelledby": `fillo-${field.id}-label`,
480
+ ...fieldAria(field, error),
481
+ children: [
482
+ options.map((opt) => /* @__PURE__ */ jsxs3(
483
+ "label",
484
+ {
485
+ "data-option": opt.id,
486
+ className: `fillo-option${value === opt.id ? " fillo-option--selected" : ""}`,
487
+ children: [
488
+ /* @__PURE__ */ jsx3(
489
+ "input",
490
+ {
491
+ type: "radio",
492
+ className: "fillo-option-input",
493
+ name: `fillo-${field.id}`,
494
+ checked: value === opt.id,
495
+ onChange: () => {
496
+ setOtherOn(false);
497
+ setValue(opt.id);
498
+ }
499
+ }
500
+ ),
501
+ /* @__PURE__ */ jsx3("span", { className: "fillo-option-label", children: opt.label })
502
+ ]
503
+ },
504
+ opt.id
505
+ )),
506
+ choice.allowOther && /* @__PURE__ */ jsxs3("label", { className: `fillo-option${otherActive ? " fillo-option--selected" : ""}`, children: [
507
+ /* @__PURE__ */ jsx3(
508
+ "input",
509
+ {
510
+ type: "radio",
511
+ className: "fillo-option-input",
512
+ name: `fillo-${field.id}`,
513
+ checked: otherActive,
514
+ onChange: () => {
515
+ setOtherOn(true);
516
+ setValue("");
517
+ }
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsx3("span", { className: "fillo-option-label", children: "Other" }),
521
+ otherActive && /* @__PURE__ */ jsx3(
522
+ "input",
523
+ {
524
+ type: "text",
525
+ className: "fillo-input fillo-other-input",
526
+ placeholder: "Your answer",
527
+ autoFocus: !isOtherValue,
528
+ value: isOtherValue ? String(value) : "",
529
+ onChange: (e) => setValue(e.target.value)
530
+ }
531
+ )
532
+ ] })
533
+ ]
534
+ }
535
+ ) });
536
+ }
537
+ function MultiChoice({ field, value, error, setValue }) {
538
+ const choice = field;
539
+ const options = useDisplayOptions(choice);
540
+ const ids = new Set(choice.options.map((o) => o.id));
541
+ const selected = Array.isArray(value) ? value : [];
542
+ const otherText = selected.find((v) => !ids.has(v));
543
+ const [otherOn, setOtherOn] = useState4(otherText !== void 0);
544
+ const otherActive = otherOn || otherText !== void 0;
545
+ const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
546
+ return /* @__PURE__ */ jsx3(FieldShell, { field, error, children: /* @__PURE__ */ jsxs3(
547
+ "div",
548
+ {
549
+ className: "fillo-options",
550
+ role: "group",
551
+ "aria-labelledby": `fillo-${field.id}-label`,
552
+ ...fieldAria(field, error),
553
+ children: [
554
+ options.map((opt) => /* @__PURE__ */ jsxs3(
555
+ "label",
556
+ {
557
+ "data-option": opt.id,
558
+ className: `fillo-option${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
559
+ children: [
560
+ /* @__PURE__ */ jsx3(
561
+ "input",
562
+ {
563
+ type: "checkbox",
564
+ className: "fillo-option-input",
565
+ checked: selected.includes(opt.id),
566
+ onChange: () => toggle(opt.id)
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsx3("span", { className: "fillo-option-label", children: opt.label })
570
+ ]
571
+ },
572
+ opt.id
573
+ )),
574
+ choice.allowOther && /* @__PURE__ */ jsxs3("label", { className: `fillo-option${otherActive ? " fillo-option--selected" : ""}`, children: [
575
+ /* @__PURE__ */ jsx3(
576
+ "input",
577
+ {
578
+ type: "checkbox",
579
+ className: "fillo-option-input",
580
+ checked: otherActive,
581
+ onChange: () => {
582
+ if (otherActive) {
583
+ setOtherOn(false);
584
+ setValue(selected.filter((v) => ids.has(v)));
585
+ } else {
586
+ setOtherOn(true);
587
+ }
588
+ }
589
+ }
590
+ ),
591
+ /* @__PURE__ */ jsx3("span", { className: "fillo-option-label", children: "Other" }),
592
+ otherActive && /* @__PURE__ */ jsx3(
593
+ "input",
594
+ {
595
+ type: "text",
596
+ className: "fillo-input fillo-other-input",
597
+ placeholder: "Your answer",
598
+ autoFocus: otherText === void 0,
599
+ value: otherText ?? "",
600
+ onChange: (e) => {
601
+ const rest = selected.filter((v) => ids.has(v));
602
+ setValue(e.target.value ? [...rest, e.target.value] : rest);
603
+ }
604
+ }
605
+ )
606
+ ] })
607
+ ]
608
+ }
609
+ ) });
610
+ }
611
+ var OTHER_SENTINEL = "__fw_other__";
612
+ function Dropdown({ field, value, error, setValue }) {
613
+ const choice = field;
614
+ const options = useDisplayOptions(choice);
615
+ const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
616
+ const [otherOn, setOtherOn] = useState4(isOtherValue);
617
+ const otherActive = otherOn || isOtherValue;
618
+ return /* @__PURE__ */ jsxs3(FieldShell, { field, error, children: [
619
+ /* @__PURE__ */ jsxs3(
620
+ "select",
621
+ {
622
+ id: `fillo-${field.id}`,
623
+ className: "fillo-input fillo-select",
624
+ ...fieldAria(field, error),
625
+ value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
626
+ onChange: (e) => {
627
+ if (e.target.value === OTHER_SENTINEL) {
628
+ setOtherOn(true);
629
+ setValue("");
630
+ } else {
631
+ setOtherOn(false);
632
+ setValue(e.target.value || null);
633
+ }
634
+ },
635
+ children: [
636
+ /* @__PURE__ */ jsx3("option", { value: "", children: field.placeholder ?? "Choose\u2026" }),
637
+ options.map((opt) => /* @__PURE__ */ jsx3("option", { value: opt.id, children: opt.label }, opt.id)),
638
+ choice.allowOther && /* @__PURE__ */ jsx3("option", { value: OTHER_SENTINEL, children: "Other\u2026" })
639
+ ]
640
+ }
641
+ ),
642
+ otherActive && /* @__PURE__ */ jsx3(
643
+ "input",
644
+ {
645
+ type: "text",
646
+ className: "fillo-input fillo-other-input fillo-other-input--block",
647
+ placeholder: "Your answer",
648
+ autoFocus: !isOtherValue,
649
+ value: isOtherValue ? String(value) : "",
650
+ onChange: (e) => setValue(e.target.value)
651
+ }
652
+ )
653
+ ] });
654
+ }
655
+ function Checkbox({ field, value, error, setValue }) {
656
+ return /* @__PURE__ */ jsxs3(
657
+ "div",
658
+ {
659
+ className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
660
+ "data-field": field.id,
661
+ children: [
662
+ /* @__PURE__ */ jsxs3("label", { className: "fillo-option", children: [
663
+ /* @__PURE__ */ jsx3(
664
+ "input",
665
+ {
666
+ id: `fillo-${field.id}`,
667
+ type: "checkbox",
668
+ className: "fillo-option-input",
669
+ checked: value === true,
670
+ onChange: (e) => setValue(e.target.checked),
671
+ ...fieldAria(field, error)
672
+ }
673
+ ),
674
+ /* @__PURE__ */ jsxs3("span", { className: "fillo-option-label", children: [
675
+ field.label,
676
+ field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-required", "aria-hidden": "true", children: " *" })
677
+ ] })
678
+ ] }),
679
+ field.description && /* @__PURE__ */ jsx3("p", { className: "fillo-description", id: `fillo-${field.id}-desc`, children: field.description }),
680
+ error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: `fillo-${field.id}-error`, role: "alert", children: error })
681
+ ]
682
+ }
683
+ );
684
+ }
685
+ function Rating({ field, value, error, setValue }) {
686
+ const max = field.max ?? 5;
687
+ const current = typeof value === "number" ? value : 0;
688
+ return /* @__PURE__ */ jsx3(FieldShell, { field, error, children: /* @__PURE__ */ jsx3("div", { className: "fillo-rating", role: "radiogroup", "aria-label": field.label, ...fieldAria(field, error), children: Array.from({ length: max }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsx3(
689
+ "button",
690
+ {
691
+ type: "button",
692
+ className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
693
+ "aria-label": `${n} of ${max}`,
694
+ "aria-pressed": n === current,
695
+ onClick: () => setValue(n === current ? null : n),
696
+ children: "\u2605"
697
+ },
698
+ n
699
+ )) }) });
700
+ }
701
+ function LinearScale({ field, value, error, setValue }) {
702
+ const scale = field;
703
+ const min = scale.min ?? 1;
704
+ const max = scale.max ?? 10;
705
+ const steps = Array.from({ length: max - min + 1 }, (_, i) => min + i);
706
+ return /* @__PURE__ */ jsxs3(FieldShell, { field, error, children: [
707
+ /* @__PURE__ */ jsx3("div", { className: "fillo-scale", role: "radiogroup", "aria-label": field.label, ...fieldAria(field, error), children: steps.map((n) => /* @__PURE__ */ jsx3(
708
+ "button",
709
+ {
710
+ type: "button",
711
+ className: `fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`,
712
+ "aria-pressed": value === n,
713
+ onClick: () => setValue(value === n ? null : n),
714
+ children: n
715
+ },
716
+ n
717
+ )) }),
718
+ (scale.minLabel || scale.maxLabel) && /* @__PURE__ */ jsxs3("div", { className: "fillo-scale-labels", children: [
719
+ /* @__PURE__ */ jsx3("span", { children: scale.minLabel }),
720
+ /* @__PURE__ */ jsx3("span", { children: scale.maxLabel })
721
+ ] })
722
+ ] });
723
+ }
724
+ function Ranking({ field, value, error, setValue }) {
725
+ const ranking = field;
726
+ const answered = Array.isArray(value) ? value : [];
727
+ const order = [
728
+ ...answered.filter((id) => ranking.options.some((o) => o.id === id)),
729
+ ...ranking.options.map((o) => o.id).filter((id) => !answered.includes(id))
730
+ ];
731
+ function move(id, delta) {
732
+ const index = order.indexOf(id);
733
+ const target = index + delta;
734
+ if (target < 0 || target >= order.length) return;
735
+ const next = [...order];
736
+ [next[index], next[target]] = [next[target], next[index]];
737
+ setValue(next);
738
+ }
739
+ return /* @__PURE__ */ jsx3(FieldShell, { field, error, children: /* @__PURE__ */ jsx3("ol", { className: "fillo-ranking", children: order.map((id, index) => {
740
+ const option = ranking.options.find((o) => o.id === id);
741
+ if (!option) return null;
742
+ return /* @__PURE__ */ jsxs3("li", { className: "fillo-ranking-item", children: [
743
+ /* @__PURE__ */ jsx3("span", { className: "fillo-ranking-index", children: index + 1 }),
744
+ /* @__PURE__ */ jsx3("span", { className: "fillo-ranking-label", children: option.label }),
745
+ /* @__PURE__ */ jsxs3("span", { className: "fillo-ranking-controls", children: [
746
+ /* @__PURE__ */ jsx3(
747
+ "button",
748
+ {
749
+ type: "button",
750
+ className: "fillo-ranking-move",
751
+ "aria-label": `Move ${option.label} up`,
752
+ disabled: index === 0,
753
+ onClick: () => move(id, -1),
754
+ children: "\u2191"
755
+ }
756
+ ),
757
+ /* @__PURE__ */ jsx3(
758
+ "button",
759
+ {
760
+ type: "button",
761
+ className: "fillo-ranking-move",
762
+ "aria-label": `Move ${option.label} down`,
763
+ disabled: index === order.length - 1,
764
+ onClick: () => move(id, 1),
765
+ children: "\u2193"
766
+ }
767
+ )
768
+ ] })
769
+ ] }, id);
770
+ }) }) });
771
+ }
772
+ function Matrix({ field, value, error, setValue }) {
773
+ const matrix = field;
774
+ const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
775
+ return /* @__PURE__ */ jsx3(FieldShell, { field, error, children: /* @__PURE__ */ jsx3("div", { className: "fillo-matrix-wrap", children: /* @__PURE__ */ jsxs3("table", { className: "fillo-matrix", children: [
776
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs3("tr", { children: [
777
+ /* @__PURE__ */ jsx3("th", {}),
778
+ matrix.columns.map((col) => /* @__PURE__ */ jsx3("th", { scope: "col", children: col.label }, col.id))
779
+ ] }) }),
780
+ /* @__PURE__ */ jsx3("tbody", { children: matrix.rows.map((row) => /* @__PURE__ */ jsxs3("tr", { children: [
781
+ /* @__PURE__ */ jsx3("th", { scope: "row", children: row.label }),
782
+ matrix.columns.map((col) => /* @__PURE__ */ jsx3("td", { children: /* @__PURE__ */ jsx3(
783
+ "input",
784
+ {
785
+ type: "radio",
786
+ className: "fillo-option-input",
787
+ name: `fillo-${field.id}-${row.id}`,
788
+ "aria-label": `${row.label}: ${col.label}`,
789
+ checked: answers[row.id] === col.id,
790
+ onChange: () => setValue({ ...answers, [row.id]: col.id })
791
+ }
792
+ ) }, col.id))
793
+ ] }, row.id)) })
794
+ ] }) }) });
795
+ }
796
+ function Hidden() {
797
+ return null;
798
+ }
799
+ var DEFAULT_COMPONENTS = {
800
+ short_text: TextInput,
801
+ email: TextInput,
802
+ url: TextInput,
803
+ phone: TextInput,
804
+ number: TextInput,
805
+ date: TextInput,
806
+ long_text: LongText,
807
+ select: SingleChoice,
808
+ multi_select: MultiChoice,
809
+ dropdown: Dropdown,
810
+ checkbox: Checkbox,
811
+ rating: Rating,
812
+ linear_scale: LinearScale,
813
+ ranking: Ranking,
814
+ matrix: Matrix,
815
+ signature: SignatureField,
816
+ file_upload: FileUploadField,
817
+ hidden: Hidden
818
+ };
819
+ function ContentRenderer({ block }) {
820
+ switch (block.kind) {
821
+ case "heading":
822
+ return /* @__PURE__ */ jsx3("h3", { className: "fillo-heading", children: block.text });
823
+ case "paragraph":
824
+ return /* @__PURE__ */ jsx3("p", { className: "fillo-paragraph", children: block.text });
825
+ case "divider":
826
+ return /* @__PURE__ */ jsx3("hr", { className: "fillo-divider" });
827
+ }
828
+ }
829
+ function BlockRenderer({
830
+ block,
831
+ api,
832
+ components,
833
+ customComponents
834
+ }) {
835
+ block = pipeBlock(block, api.data, api.form);
836
+ if (!isField(block)) return /* @__PURE__ */ jsx3(ContentRenderer, { block });
837
+ let Component;
838
+ if (block.kind === "custom") {
839
+ Component = customComponents?.[block.component] ?? components?.custom;
840
+ if (!Component) {
841
+ const proc = globalThis.process;
842
+ if (proc?.env?.NODE_ENV !== "production") {
843
+ console.warn(
844
+ `[fillo] No renderer for custom field "${block.id}" (component "${block.component}"). Pass it via the customComponents prop.`
845
+ );
846
+ }
847
+ return null;
848
+ }
849
+ } else {
850
+ Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
851
+ }
852
+ return /* @__PURE__ */ jsx3(
853
+ Component,
854
+ {
855
+ field: block,
856
+ value: api.data[block.id],
857
+ error: api.errors[block.id],
858
+ setValue: (v) => api.setValue(block.id, v),
859
+ api
860
+ }
861
+ );
862
+ }
863
+ function FormField({
864
+ id,
865
+ components,
866
+ customComponents
867
+ }) {
868
+ const api = useFillo();
869
+ const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
870
+ if (!block) return null;
871
+ return /* @__PURE__ */ jsx3(
872
+ BlockRenderer,
873
+ {
874
+ block,
875
+ api,
876
+ components,
877
+ customComponents
878
+ }
879
+ );
880
+ }
881
+
882
+ // src/FilloForm.tsx
883
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
884
+ function themeStyle(theme) {
885
+ if (!theme) return void 0;
886
+ const style = {};
887
+ if (theme.primary) style["--fillo-primary"] = theme.primary;
888
+ if (theme.background) style["--fillo-bg"] = theme.background;
889
+ if (theme.text) style["--fillo-text"] = theme.text;
890
+ if (theme.radius) style["--fillo-radius"] = theme.radius;
891
+ if (theme.fontFamily) style["--fillo-font"] = theme.fontFamily;
892
+ return style;
893
+ }
894
+ function FilloForm(props) {
895
+ const { form, client, formId } = props;
896
+ const [fetched, setFetched] = useState5(null);
897
+ const [fetchedTheme, setFetchedTheme] = useState5(null);
898
+ const [loadError, setLoadError] = useState5(null);
899
+ const [closed, setClosed] = useState5(false);
900
+ const codeForm = isCodeForm2(form) ? form : null;
901
+ const inlineSchema = codeForm ? codeForm.schema : form;
902
+ const onError = props.onError;
903
+ const syncedFormId = useCodeFormSync(form, client, onError);
904
+ const needsFetch = !inlineSchema && Boolean(client && formId);
905
+ useEffect6(() => {
906
+ if (!needsFetch || !client || !formId) return;
907
+ let cancelled = false;
908
+ setFetched(null);
909
+ setLoadError(null);
910
+ client.getForm(formId).then((published) => {
911
+ if (cancelled) return;
912
+ setFetched(published.schema);
913
+ setFetchedTheme(published.theme);
914
+ setClosed(Boolean(published.closed));
915
+ }).catch((err) => {
916
+ if (cancelled) return;
917
+ const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
918
+ setLoadError(error);
919
+ onError?.(error);
920
+ });
921
+ return () => {
922
+ cancelled = true;
923
+ };
924
+ }, [needsFetch, client, formId, onError]);
925
+ const schema = inlineSchema ?? fetched;
926
+ const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
927
+ if (loadError) {
928
+ if (props.renderError) {
929
+ return /* @__PURE__ */ jsx4(Fragment2, { children: props.renderError(loadError) });
930
+ }
931
+ const message = loadError.status === 404 ? "Form not found \u2014 check the form id and that it's published." : loadError.status === 0 ? "Couldn't reach the server \u2014 check your connection or CORS." : "This form could not be loaded.";
932
+ return /* @__PURE__ */ jsx4("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
933
+ }
934
+ if (closed) {
935
+ return /* @__PURE__ */ jsx4("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, style: themeStyle(theme), children: /* @__PURE__ */ jsx4("p", { className: "fillo-closed", children: "This form is no longer accepting responses." }) });
936
+ }
937
+ if (!schema) {
938
+ return /* @__PURE__ */ jsxs4(
939
+ "div",
940
+ {
941
+ className: `fillo-form fillo-form--loading ${props.className ?? ""}`,
942
+ "aria-busy": "true",
943
+ style: themeStyle(theme),
944
+ children: [
945
+ /* @__PURE__ */ jsx4("div", { className: "fillo-skeleton" }),
946
+ /* @__PURE__ */ jsx4("div", { className: "fillo-skeleton" }),
947
+ /* @__PURE__ */ jsx4("div", { className: "fillo-skeleton fillo-skeleton--short" })
948
+ ]
949
+ }
950
+ );
951
+ }
952
+ return /* @__PURE__ */ jsx4(ResolvedForm, { ...props, schema, theme, formId: syncedFormId ?? formId });
953
+ }
954
+ function ResolvedForm(props) {
955
+ const { schema, theme } = props;
956
+ const hpRef = useRef4("");
957
+ const api = useFilloController({
958
+ form: schema,
959
+ formId: props.formId,
960
+ client: props.client,
961
+ initialData: props.initialData,
962
+ onChange: props.onChange,
963
+ onSubmitted: props.onSubmitted,
964
+ onSubmit: props.onSubmit,
965
+ getHoneypot: () => hpRef.current
966
+ });
967
+ const style = useMemo(() => themeStyle(theme), [theme]);
968
+ const settings = schema.settings;
969
+ const submitted = api.status === "submitted";
970
+ useEffect6(() => {
971
+ if (!submitted || !settings.redirectUrl || typeof window === "undefined") return;
972
+ if (/^https?:\/\//i.test(settings.redirectUrl)) {
973
+ window.location.assign(settings.redirectUrl);
974
+ }
975
+ }, [submitted, settings.redirectUrl]);
976
+ if (api.status === "submitted") {
977
+ return /* @__PURE__ */ jsx4("div", { className: `fillo-form fillo-form--success ${props.className ?? ""}`, style, children: props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs4("div", { className: "fillo-success", children: [
978
+ /* @__PURE__ */ jsx4("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
979
+ /* @__PURE__ */ jsx4("h2", { className: "fillo-success-title", children: settings.successTitle ?? "Thanks!" }),
980
+ /* @__PURE__ */ jsx4("p", { className: "fillo-success-message", children: settings.successMessage ?? "Your response has been recorded." })
981
+ ] }) });
982
+ }
983
+ const multiPage = api.pageCount > 1;
984
+ return /* @__PURE__ */ jsx4(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsxs4(
985
+ "form",
986
+ {
987
+ className: `fillo-form ${props.className ?? ""}`,
988
+ style,
989
+ noValidate: true,
990
+ onSubmit: (e) => {
991
+ e.preventDefault();
992
+ if (api.isLastPage) void api.submit();
993
+ else api.next();
994
+ },
995
+ children: [
996
+ multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx4(
997
+ "div",
998
+ {
999
+ className: "fillo-progress-track",
1000
+ role: "progressbar",
1001
+ "aria-valuemin": 0,
1002
+ "aria-valuemax": api.pageCount,
1003
+ "aria-valuenow": api.pageIndex + 1,
1004
+ children: /* @__PURE__ */ jsx4(
1005
+ "div",
1006
+ {
1007
+ className: "fillo-progress-fill",
1008
+ style: { width: `${(api.pageIndex + 1) / api.pageCount * 100}%` }
1009
+ }
1010
+ )
1011
+ }
1012
+ ),
1013
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs4("header", { className: "fillo-header", children: [
1014
+ /* @__PURE__ */ jsx4("h1", { className: "fillo-title", children: schema.title }),
1015
+ schema.description && /* @__PURE__ */ jsx4("p", { className: "fillo-form-description", children: schema.description })
1016
+ ] }),
1017
+ multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx4("h2", { className: "fillo-page-title", children: api.page.title }),
1018
+ /* @__PURE__ */ jsx4("div", { className: "fillo-blocks", children: api.blocks.map((block) => /* @__PURE__ */ jsx4(
1019
+ BlockRenderer,
1020
+ {
1021
+ block,
1022
+ api,
1023
+ components: props.components,
1024
+ customComponents: props.customComponents
1025
+ },
1026
+ block.id
1027
+ )) }),
1028
+ /* @__PURE__ */ jsx4(
1029
+ "input",
1030
+ {
1031
+ type: "text",
1032
+ name: "fw_hp_field",
1033
+ className: "fillo-hp",
1034
+ tabIndex: -1,
1035
+ autoComplete: "off",
1036
+ "aria-hidden": "true",
1037
+ defaultValue: "",
1038
+ onChange: (e) => {
1039
+ hpRef.current = e.target.value;
1040
+ }
1041
+ }
1042
+ ),
1043
+ /* @__PURE__ */ jsxs4("footer", { className: "fillo-footer", children: [
1044
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx4("button", { type: "button", className: "fillo-button fillo-button--ghost", onClick: api.back, children: "Back" }),
1045
+ /* @__PURE__ */ jsx4(
1046
+ "button",
1047
+ {
1048
+ type: "submit",
1049
+ className: "fillo-button fillo-button--primary",
1050
+ disabled: api.status === "submitting" || api.uploading,
1051
+ children: api.uploading ? "Uploading\u2026" : api.status === "submitting" ? "Submitting\u2026" : api.isLastPage ? settings.submitLabel ?? "Submit" : "Next"
1052
+ }
1053
+ )
1054
+ ] })
1055
+ ]
1056
+ }
1057
+ ) });
1058
+ }
1059
+
1060
+ // src/provider.tsx
1061
+ import { jsx as jsx5 } from "react/jsx-runtime";
1062
+ function FilloProvider({ children, form, formId, client, ...options }) {
1063
+ const codeForm = isCodeForm2(form) ? form : null;
1064
+ const schema = codeForm ? codeForm.schema : form;
1065
+ const syncedFormId = useCodeFormSync(form, client);
1066
+ const api = useFilloController({
1067
+ ...options,
1068
+ client,
1069
+ form: schema,
1070
+ formId: syncedFormId ?? formId
1071
+ });
1072
+ return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children });
1073
+ }
1074
+
1075
+ // src/index.ts
1076
+ import {
1077
+ createClient,
1078
+ FilloClient,
1079
+ FilloError as FilloError3
1080
+ } from "@usefillo/core";
1081
+ export {
1082
+ BlockRenderer,
1083
+ FilloClient,
1084
+ FilloError3 as FilloError,
1085
+ FileUploadField as FilloFileUpload,
1086
+ FilloForm,
1087
+ FilloProvider,
1088
+ FormField,
1089
+ createClient,
1090
+ defineForm,
1091
+ useField,
1092
+ useFillo,
1093
+ useFilloController
1094
+ };