@usefillo/react 0.3.0

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