@usefillo/react 0.5.4 → 0.6.1

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 CHANGED
@@ -1,23 +1,66 @@
1
1
  "use client";
2
2
 
3
3
  // src/FilloForm.tsx
4
- import { useEffect as useEffect7, useId as useId2, useMemo as useMemo4, useRef as useRef5, useState as useState6 } from "react";
4
+ import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
5
5
  import {
6
6
  FilloError as FilloError2,
7
7
  createClient,
8
- isField as isField2,
8
+ needsExplicitSubmit,
9
9
  normalizeFormSchema,
10
10
  normalizeFormTheme,
11
11
  visibleFields as visibleFields2
12
12
  } from "@usefillo/core";
13
+ import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
14
+
15
+ // src/appearance.tsx
16
+ import { createContext, useContext } from "react";
17
+ import {
18
+ DEFAULT_STRINGS,
19
+ slotClass
20
+ } from "@usefillo/core";
21
+ var FilloAppearanceContext = createContext(void 0);
22
+ function useSlotClass(base, state) {
23
+ const appearance = useContext(FilloAppearanceContext);
24
+ return slotClass(base, appearance, state);
25
+ }
26
+ function useFilloAppearance() {
27
+ return useContext(FilloAppearanceContext);
28
+ }
29
+ function useFieldSlots(field, error) {
30
+ const appearance = useContext(FilloAppearanceContext);
31
+ const state = {
32
+ kind: field.kind,
33
+ fieldId: field.id,
34
+ invalid: Boolean(error),
35
+ required: Boolean(field.required)
36
+ };
37
+ return {
38
+ wrapperProps: (base) => ({
39
+ className: slotClass(base, appearance, { slot: "field", ...state }),
40
+ "data-fillo": "field",
41
+ "data-field": field.id,
42
+ "data-kind": field.kind,
43
+ "data-invalid": error ? "" : void 0,
44
+ "data-required": field.required ? "" : void 0
45
+ }),
46
+ label: slotClass("fillo-label", appearance, { slot: "label", ...state }),
47
+ description: slotClass("fillo-description", appearance, { slot: "fieldDescription", ...state }),
48
+ error: slotClass("fillo-error", appearance, { slot: "error", ...state }),
49
+ control: (base) => slotClass(base, appearance, { slot: "control", ...state })
50
+ };
51
+ }
52
+ var FilloStringsContext = createContext(DEFAULT_STRINGS);
53
+ function useStrings() {
54
+ return useContext(FilloStringsContext);
55
+ }
13
56
 
14
57
  // src/context.ts
15
58
  import { allFields } from "@usefillo/core";
16
- import { createContext, useContext, useId, useMemo } from "react";
17
- var FilloContext = createContext(null);
18
- var FilloInstanceIdContext = createContext(null);
59
+ import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
60
+ var FilloContext = createContext2(null);
61
+ var FilloInstanceIdContext = createContext2(null);
19
62
  function useFilloInstanceId() {
20
- const instanceId = useContext(FilloInstanceIdContext);
63
+ const instanceId = useContext2(FilloInstanceIdContext);
21
64
  const fallbackId = useId().replace(/:/g, "");
22
65
  return instanceId ?? fallbackId;
23
66
  }
@@ -32,7 +75,7 @@ function createFilloFieldIds(instanceId, fieldId) {
32
75
  };
33
76
  }
34
77
  function useFillo() {
35
- const api = useContext(FilloContext);
78
+ const api = useContext2(FilloContext);
36
79
  if (!api) throw new Error("useFillo must be used inside <FilloForm> or <FilloProvider>");
37
80
  return api;
38
81
  }
@@ -75,7 +118,9 @@ function useFilloController(options) {
75
118
  onSubmitted: (id, d) => optsRef.current.onSubmitted?.(id, d),
76
119
  getHoneypot: () => optsRef.current.getHoneypot?.() ?? "",
77
120
  skipValidation: options.skipValidation,
78
- surface: options.surface ?? "default"
121
+ surface: options.surface ?? "headless",
122
+ // Wrapped so the engine (created once) always calls the latest resolver.
123
+ resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0
79
124
  });
80
125
  }
81
126
  const store = storeRef.current;
@@ -109,16 +154,33 @@ import {
109
154
  } from "@usefillo/core";
110
155
  import { useEffect as useEffect2, useState } from "react";
111
156
  import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
157
+ var noticedSync = /* @__PURE__ */ new Set();
158
+ function noticeSync(handle, kind, message) {
159
+ const key = `${handle}:${kind}`;
160
+ if (noticedSync.has(key)) return;
161
+ noticedSync.add(key);
162
+ console.info(message);
163
+ }
112
164
  function useCodeFormSync(form, client, onError) {
113
165
  const codeForm = isCodeForm(form) ? form : null;
114
- const [syncedFormId, setSyncedFormId] = useState(null);
166
+ const [synced, setSynced] = useState({ formId: null, status: null });
115
167
  const hasKey = Boolean(client?.key);
116
168
  const handle = codeForm?.id;
117
169
  const contentKey = codeForm ? contentHash(JSON.stringify(codeForm.schema) + JSON.stringify(codeForm.theme ?? null)) : null;
118
170
  useEffect2(() => {
119
171
  if (!codeForm || !client?.key) return;
120
172
  let cancelled = false;
121
- syncCodeForm(client, codeForm).then((r) => !cancelled && setSyncedFormId(r.formId)).catch((err) => {
173
+ syncCodeForm(client, codeForm).then((r) => {
174
+ if (cancelled) return;
175
+ setSynced({ formId: r.formId, status: r.status ?? null });
176
+ const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
177
+ if (r.warning) console.warn(`[fillo] ${r.warning}`);
178
+ if (r.staged) {
179
+ noticeSync(codeForm.id, "staged", `[fillo] "${codeForm.id}": changes staged as a draft \u2014 publish them at ${dashboardUrl}`);
180
+ } else if (r.status === "draft") {
181
+ noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
182
+ }
183
+ }).catch((err) => {
122
184
  const error = err instanceof FilloError ? err : new FilloError(String(err), 0);
123
185
  console.warn("[fillo] form sync failed:", error.message);
124
186
  if (!cancelled) onError?.(error);
@@ -127,12 +189,12 @@ function useCodeFormSync(form, client, onError) {
127
189
  cancelled = true;
128
190
  };
129
191
  }, [hasKey, handle, contentKey, client, onError]);
130
- return syncedFormId;
192
+ return synced;
131
193
  }
132
194
 
133
195
  // src/fields.tsx
134
- import { isField, pipeBlock, validateResponse, visibleFields } from "@usefillo/core";
135
- import { memo, useEffect as useEffect6, useState as useState5 } from "react";
196
+ import { isField, pipeBlock, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
197
+ import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
136
198
 
137
199
  // src/phone.tsx
138
200
  import {
@@ -193,6 +255,8 @@ function positionPhonePopover(anchor, popover) {
193
255
  }
194
256
  function PhoneField({ field, value, error, setValue, ids: providedIds }) {
195
257
  const ids = useFilloFieldIds(field.id, providedIds);
258
+ const slots = useFieldSlots(field, error);
259
+ const strings = useStrings();
196
260
  const schema = field;
197
261
  const stored = typeof value === "string" ? value : "";
198
262
  const [country, setCountry] = useState2(
@@ -247,12 +311,12 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
247
311
  inputRef.current?.focus();
248
312
  }
249
313
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
250
- return /* @__PURE__ */ jsxs("div", { className: `fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`, "data-field": field.id, children: [
251
- /* @__PURE__ */ jsxs("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
314
+ return /* @__PURE__ */ jsxs("div", { ...slots.wrapperProps(`fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`), children: [
315
+ /* @__PURE__ */ jsxs("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
252
316
  field.label,
253
- !field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: " (optional)" })
317
+ !field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: strings.optional })
254
318
  ] }),
255
- field.description && /* @__PURE__ */ jsx("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
319
+ field.description && /* @__PURE__ */ jsx("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
256
320
  /* @__PURE__ */ jsxs("div", { className: "fillo-phone", children: [
257
321
  /* @__PURE__ */ jsx(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
258
322
  /* @__PURE__ */ jsx(
@@ -260,7 +324,8 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
260
324
  {
261
325
  ref: inputRef,
262
326
  id: ids.inputId,
263
- className: "fillo-input fillo-phone-input",
327
+ className: slots.control("fillo-input fillo-phone-input"),
328
+ "data-fillo": "control",
264
329
  type: "tel",
265
330
  inputMode: "tel",
266
331
  autoComplete: "tel-national",
@@ -273,7 +338,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
273
338
  }
274
339
  )
275
340
  ] }),
276
- error && /* @__PURE__ */ jsx("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
341
+ error && /* @__PURE__ */ jsx("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
277
342
  ] });
278
343
  }
279
344
  function CountrySelect({
@@ -426,6 +491,8 @@ import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } fro
426
491
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
427
492
  function SignatureField({ field, value, error, setValue, ids: providedIds }) {
428
493
  const ids = useFilloFieldIds(field.id, providedIds);
494
+ const slots = useFieldSlots(field, error);
495
+ const strings = useStrings();
429
496
  const typedInputId = `${ids.inputId}-typed`;
430
497
  const canvasRef = useRef3(null);
431
498
  const drawing = useRef3(false);
@@ -507,12 +574,12 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
507
574
  setValue(null);
508
575
  }
509
576
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
510
- return /* @__PURE__ */ jsxs2("div", { className: `fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`, children: [
511
- /* @__PURE__ */ jsxs2("span", { className: "fillo-label", id: ids.labelId, children: [
577
+ return /* @__PURE__ */ jsxs2("div", { ...slots.wrapperProps(`fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`), children: [
578
+ /* @__PURE__ */ jsxs2("span", { className: slots.label, "data-fillo": "label", id: ids.labelId, children: [
512
579
  field.label,
513
- !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: " (optional)" })
580
+ !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
514
581
  ] }),
515
- field.description && /* @__PURE__ */ jsx2("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
582
+ field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
516
583
  /* @__PURE__ */ jsxs2(
517
584
  "div",
518
585
  {
@@ -520,7 +587,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
520
587
  "aria-labelledby": ids.labelId,
521
588
  "aria-describedby": describedBy,
522
589
  children: [
523
- /* @__PURE__ */ jsxs2("div", { className: "fillo-signature", children: [
590
+ /* @__PURE__ */ jsxs2("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
524
591
  /* @__PURE__ */ jsx2(
525
592
  "canvas",
526
593
  {
@@ -560,7 +627,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
560
627
  ]
561
628
  }
562
629
  ),
563
- error && /* @__PURE__ */ jsx2("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
630
+ error && /* @__PURE__ */ jsx2("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
564
631
  ] });
565
632
  }
566
633
 
@@ -590,6 +657,8 @@ function formatBytes(bytes) {
590
657
  }
591
658
  function FileUploadField({ field, value, error, setValue, api, ids: providedIds }) {
592
659
  const ids = useFilloFieldIds(field.id, providedIds);
660
+ const slots = useFieldSlots(field, error);
661
+ const strings = useStrings();
593
662
  const schema = field;
594
663
  const inputRef = useRef4(null);
595
664
  const [inFlight, setInFlight] = useState4([]);
@@ -637,10 +706,11 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
637
706
  setInFlight((prev) => prev.filter((f) => f.key !== key));
638
707
  const current = Array.isArray(api.data[field.id]) ? api.data[field.id] : [];
639
708
  setValue([...current, uploaded]);
640
- } catch {
709
+ } catch (err) {
641
710
  if (mountedRef.current) {
711
+ const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message : "Upload failed \u2014 try again";
642
712
  setInFlight(
643
- (prev) => prev.map((f) => f.key === key ? { ...f, error: "Upload failed \u2014 try again" } : f)
713
+ (prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
644
714
  );
645
715
  }
646
716
  } finally {
@@ -662,16 +732,18 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
662
732
  setValue(files.filter((f) => f.fileId !== fileId));
663
733
  }
664
734
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
665
- return /* @__PURE__ */ jsxs3("div", { className: `fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`, children: [
666
- /* @__PURE__ */ jsxs3("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
735
+ return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
736
+ /* @__PURE__ */ jsxs3("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
667
737
  field.label,
668
- !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: " (optional)" })
738
+ !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
669
739
  ] }),
670
- field.description && /* @__PURE__ */ jsx3("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
740
+ field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
671
741
  remaining > 0 && /* @__PURE__ */ jsxs3(
672
742
  "div",
673
743
  {
674
- className: `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`,
744
+ "data-fillo": "control",
745
+ "data-drag-over": dragOver ? "" : void 0,
746
+ className: slots.control(`fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`),
675
747
  onDragOver: (e) => {
676
748
  e.preventDefault();
677
749
  setDragOver(true);
@@ -702,7 +774,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
702
774
  hidden: true,
703
775
  multiple: maxFiles > 1,
704
776
  accept: schema.accept?.join(","),
705
- onChange: (e) => handleFiles(e.target.files)
777
+ onChange: (e) => canUpload && handleFiles(e.target.files)
706
778
  }
707
779
  ),
708
780
  canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
@@ -720,7 +792,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
720
792
  ]
721
793
  }
722
794
  ),
723
- /* @__PURE__ */ jsx3("div", { role: "status", "aria-live": "polite", className: "fillo-sr-only", children: uploadStatus(inFlight, files.length) }),
795
+ /* @__PURE__ */ jsx3(
796
+ "div",
797
+ {
798
+ role: "status",
799
+ "aria-live": "polite",
800
+ className: "fillo-sr-only",
801
+ style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
802
+ children: uploadStatus(inFlight, files.length)
803
+ }
804
+ ),
724
805
  (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
725
806
  files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
726
807
  /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
@@ -784,7 +865,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
784
865
  f.key
785
866
  ))
786
867
  ] }),
787
- error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
868
+ error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
788
869
  ] });
789
870
  }
790
871
 
@@ -801,6 +882,18 @@ function fieldAria(field, error, ids) {
801
882
  "aria-describedby": describedBy.length ? describedBy.join(" ") : void 0
802
883
  };
803
884
  }
885
+ function useControl(base, field, error) {
886
+ return {
887
+ className: useSlotClass(base, {
888
+ slot: "control",
889
+ kind: field.kind,
890
+ fieldId: field.id,
891
+ invalid: Boolean(error),
892
+ required: Boolean(field.required)
893
+ }),
894
+ "data-fillo": "control"
895
+ };
896
+ }
804
897
  function isNativeControlTarget(target) {
805
898
  return target instanceof HTMLElement && Boolean(target.closest("input, textarea, select, button"));
806
899
  }
@@ -810,19 +903,37 @@ function FieldShell({
810
903
  ids,
811
904
  children
812
905
  }) {
906
+ const state = {
907
+ kind: field.kind,
908
+ fieldId: field.id,
909
+ invalid: Boolean(error),
910
+ required: Boolean(field.required)
911
+ };
912
+ const fieldCls = useSlotClass(
913
+ `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
914
+ { slot: "field", ...state }
915
+ );
916
+ const labelCls = useSlotClass("fillo-label", { slot: "label", ...state });
917
+ const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
918
+ const errorCls = useSlotClass("fillo-error", { slot: "error", ...state });
919
+ const strings = useStrings();
813
920
  return /* @__PURE__ */ jsxs4(
814
921
  "div",
815
922
  {
816
- className: `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
923
+ className: fieldCls,
924
+ "data-fillo": "field",
817
925
  "data-field": field.id,
926
+ "data-kind": field.kind,
927
+ "data-invalid": error ? "" : void 0,
928
+ "data-required": field.required ? "" : void 0,
818
929
  children: [
819
- /* @__PURE__ */ jsxs4("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
930
+ /* @__PURE__ */ jsxs4("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
820
931
  field.label,
821
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
932
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
822
933
  ] }),
823
- field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
934
+ field.description && /* @__PURE__ */ jsx4("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
824
935
  children,
825
- error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
936
+ error && /* @__PURE__ */ jsx4("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
826
937
  ]
827
938
  }
828
939
  );
@@ -830,11 +941,12 @@ function FieldShell({
830
941
  function TextInput({ field, value, error, setValue, ids: providedIds }) {
831
942
  const ids = useFilloFieldIds(field.id, providedIds);
832
943
  const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
944
+ const control = useControl("fillo-input", field, error);
833
945
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
834
946
  "input",
835
947
  {
836
948
  id: ids.inputId,
837
- className: "fillo-input",
949
+ ...control,
838
950
  type,
839
951
  value: typeof value === "string" || typeof value === "number" ? String(value) : "",
840
952
  placeholder: field.placeholder,
@@ -845,11 +957,12 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
845
957
  }
846
958
  function LongText({ field, value, error, setValue, ids: providedIds }) {
847
959
  const ids = useFilloFieldIds(field.id, providedIds);
960
+ const control = useControl("fillo-input fillo-textarea", field, error);
848
961
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
849
962
  "textarea",
850
963
  {
851
964
  id: ids.inputId,
852
- className: "fillo-input fillo-textarea",
965
+ ...control,
853
966
  rows: 4,
854
967
  value: typeof value === "string" ? value : "",
855
968
  placeholder: field.placeholder,
@@ -898,6 +1011,9 @@ function ChoiceIcon({ icon }) {
898
1011
  }
899
1012
  function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
900
1013
  const ids = useFilloFieldIds(field.id, providedIds);
1014
+ const appearance = useFilloAppearance();
1015
+ const strings = useStrings();
1016
+ const optionCls = (base, optionId, selected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected });
901
1017
  const choice = field;
902
1018
  const options = useDisplayOptions(choice);
903
1019
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
@@ -906,7 +1022,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
906
1022
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
907
1023
  "div",
908
1024
  {
909
- className: "fillo-options",
1025
+ className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
1026
+ "data-fillo": "options",
910
1027
  role: "radiogroup",
911
1028
  "aria-labelledby": ids.labelId,
912
1029
  ...fieldAria(field, error, ids),
@@ -915,7 +1032,13 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
915
1032
  "label",
916
1033
  {
917
1034
  "data-option": opt.id,
918
- className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
1035
+ "data-fillo": "option",
1036
+ "data-selected": value === opt.id ? "" : void 0,
1037
+ className: optionCls(
1038
+ `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
1039
+ opt.id,
1040
+ value === opt.id
1041
+ ),
919
1042
  onClick: (event) => {
920
1043
  if (isNativeControlTarget(event.target)) return;
921
1044
  event.preventDefault();
@@ -937,7 +1060,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
937
1060
  }
938
1061
  ),
939
1062
  /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
940
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
1063
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
941
1064
  ]
942
1065
  },
943
1066
  opt.id
@@ -947,7 +1070,13 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
947
1070
  /* @__PURE__ */ jsxs4(
948
1071
  "div",
949
1072
  {
950
- className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1073
+ "data-fillo": "option",
1074
+ "data-selected": otherActive ? "" : void 0,
1075
+ className: optionCls(
1076
+ `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1077
+ "__other",
1078
+ otherActive
1079
+ ),
951
1080
  children: [
952
1081
  /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
953
1082
  /* @__PURE__ */ jsx4(
@@ -963,15 +1092,15 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
963
1092
  }
964
1093
  }
965
1094
  ),
966
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
1095
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
967
1096
  ] }),
968
1097
  otherActive && /* @__PURE__ */ jsx4(
969
1098
  "input",
970
1099
  {
971
1100
  type: "text",
972
1101
  className: "fillo-input fillo-other-input",
973
- "aria-label": "Other \u2014 please specify",
974
- placeholder: "Your answer",
1102
+ "aria-label": strings.otherPrompt,
1103
+ placeholder: strings.otherPlaceholder,
975
1104
  autoFocus: !isOtherValue,
976
1105
  value: isOtherValue ? String(value) : "",
977
1106
  onChange: (e) => setValue(e.target.value)
@@ -988,16 +1117,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
988
1117
  const ids = useFilloFieldIds(field.id, providedIds);
989
1118
  const choice = field;
990
1119
  const options = useDisplayOptions(choice);
1120
+ const strings = useStrings();
991
1121
  const optionIds = new Set(choice.options.map((o) => o.id));
992
1122
  const selected = Array.isArray(value) ? value : [];
993
1123
  const otherText = selected.find((v) => !optionIds.has(v));
994
1124
  const [otherOn, setOtherOn] = useState5(otherText !== void 0);
995
1125
  const otherActive = otherText !== void 0 || otherOn;
996
1126
  const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
1127
+ const appearance = useFilloAppearance();
1128
+ const optionCls = (base, optionId, isSelected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected: isSelected });
997
1129
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
998
1130
  "div",
999
1131
  {
1000
- className: "fillo-options",
1132
+ className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
1133
+ "data-fillo": "options",
1001
1134
  role: "group",
1002
1135
  "aria-labelledby": ids.labelId,
1003
1136
  ...fieldAria(field, error, ids),
@@ -1006,7 +1139,13 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1006
1139
  "label",
1007
1140
  {
1008
1141
  "data-option": opt.id,
1009
- className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
1142
+ "data-fillo": "option",
1143
+ "data-selected": selected.includes(opt.id) ? "" : void 0,
1144
+ className: optionCls(
1145
+ `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
1146
+ opt.id,
1147
+ selected.includes(opt.id)
1148
+ ),
1010
1149
  onClick: (event) => {
1011
1150
  if (isNativeControlTarget(event.target)) return;
1012
1151
  event.preventDefault();
@@ -1023,7 +1162,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1023
1162
  }
1024
1163
  ),
1025
1164
  /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
1026
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
1165
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1027
1166
  ]
1028
1167
  },
1029
1168
  opt.id
@@ -1033,7 +1172,13 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1033
1172
  /* @__PURE__ */ jsxs4(
1034
1173
  "div",
1035
1174
  {
1036
- className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1175
+ "data-fillo": "option",
1176
+ "data-selected": otherActive ? "" : void 0,
1177
+ className: optionCls(
1178
+ `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1179
+ "__other",
1180
+ otherActive
1181
+ ),
1037
1182
  children: [
1038
1183
  /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1039
1184
  /* @__PURE__ */ jsx4(
@@ -1052,15 +1197,15 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1052
1197
  }
1053
1198
  }
1054
1199
  ),
1055
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
1200
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1056
1201
  ] }),
1057
1202
  otherActive && /* @__PURE__ */ jsx4(
1058
1203
  "input",
1059
1204
  {
1060
1205
  type: "text",
1061
1206
  className: "fillo-input fillo-other-input",
1062
- "aria-label": "Other \u2014 please specify",
1063
- placeholder: "Your answer",
1207
+ "aria-label": strings.otherPrompt,
1208
+ placeholder: strings.otherPlaceholder,
1064
1209
  autoFocus: otherText === void 0,
1065
1210
  value: otherText ?? "",
1066
1211
  onChange: (e) => {
@@ -1079,18 +1224,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1079
1224
  var OTHER_SENTINEL = "__fw_other__";
1080
1225
  function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1081
1226
  const ids = useFilloFieldIds(field.id, providedIds);
1227
+ const strings = useStrings();
1082
1228
  const choice = field;
1083
1229
  const options = useDisplayOptions(choice);
1084
1230
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
1085
1231
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1086
1232
  const otherActive = isOtherValue || value === "" && otherOn;
1233
+ const control = useControl("fillo-input fillo-select", field, error);
1087
1234
  return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1088
1235
  /* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
1089
1236
  /* @__PURE__ */ jsxs4(
1090
1237
  "select",
1091
1238
  {
1092
1239
  id: ids.inputId,
1093
- className: "fillo-input fillo-select",
1240
+ ...control,
1094
1241
  ...fieldAria(field, error, ids),
1095
1242
  value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
1096
1243
  onChange: (e) => {
@@ -1103,9 +1250,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1103
1250
  }
1104
1251
  },
1105
1252
  children: [
1106
- /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? "Choose\u2026" }),
1253
+ /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1107
1254
  options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
1108
- choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: "Other\u2026" })
1255
+ choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: `${strings.other}\u2026` })
1109
1256
  ]
1110
1257
  }
1111
1258
  ),
@@ -1116,8 +1263,8 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1116
1263
  {
1117
1264
  type: "text",
1118
1265
  className: "fillo-input fillo-other-input fillo-other-input--block",
1119
- "aria-label": "Other \u2014 please specify",
1120
- placeholder: "Your answer",
1266
+ "aria-label": strings.otherPrompt,
1267
+ placeholder: strings.otherPlaceholder,
1121
1268
  autoFocus: !isOtherValue,
1122
1269
  value: isOtherValue ? String(value) : "",
1123
1270
  onChange: (e) => setValue(e.target.value)
@@ -1127,18 +1274,24 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1127
1274
  }
1128
1275
  function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1129
1276
  const ids = useFilloFieldIds(field.id, providedIds);
1277
+ const strings = useStrings();
1278
+ const appearance = useFilloAppearance();
1130
1279
  const checkbox = field;
1131
1280
  if (checkbox.appearance === "toggle") {
1132
1281
  return /* @__PURE__ */ jsxs4(
1133
1282
  "div",
1134
1283
  {
1135
1284
  className: `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`,
1285
+ "data-fillo": "field",
1136
1286
  "data-field": field.id,
1287
+ "data-kind": field.kind,
1288
+ "data-invalid": error ? "" : void 0,
1289
+ "data-required": field.required ? "" : void 0,
1137
1290
  children: [
1138
- /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", children: [
1139
- /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
1291
+ /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1292
+ /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1140
1293
  field.label,
1141
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
1294
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1142
1295
  ] }) }),
1143
1296
  /* @__PURE__ */ jsx4(
1144
1297
  "input",
@@ -1165,9 +1318,13 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1165
1318
  "div",
1166
1319
  {
1167
1320
  className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
1321
+ "data-fillo": "field",
1168
1322
  "data-field": field.id,
1323
+ "data-kind": field.kind,
1324
+ "data-invalid": error ? "" : void 0,
1325
+ "data-required": field.required ? "" : void 0,
1169
1326
  children: [
1170
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option", children: [
1327
+ /* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1171
1328
  /* @__PURE__ */ jsx4(
1172
1329
  "input",
1173
1330
  {
@@ -1179,9 +1336,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1179
1336
  ...fieldAria(field, error, ids)
1180
1337
  }
1181
1338
  ),
1182
- /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
1339
+ /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1183
1340
  field.label,
1184
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
1341
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1185
1342
  ] })
1186
1343
  ] }),
1187
1344
  field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
@@ -1222,10 +1379,12 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1222
1379
  const steps = Array.from({ length: max }, (_, i) => i + 1);
1223
1380
  const selected = current > 0 ? current : null;
1224
1381
  const tabbable = rovingIndex(steps, selected);
1382
+ const appearance = useFilloAppearance();
1383
+ const control = useControl("fillo-rating", field, error);
1225
1384
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1226
1385
  "div",
1227
1386
  {
1228
- className: "fillo-rating",
1387
+ ...control,
1229
1388
  role: "radiogroup",
1230
1389
  "aria-labelledby": ids.labelId,
1231
1390
  ...fieldAria(field, error, ids),
@@ -1234,7 +1393,14 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1234
1393
  "button",
1235
1394
  {
1236
1395
  type: "button",
1237
- className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
1396
+ "data-fillo": "option",
1397
+ "data-selected": n <= current ? "" : void 0,
1398
+ className: slotClass2(`fillo-star${n <= current ? " fillo-star--active" : ""}`, appearance, {
1399
+ slot: "option",
1400
+ kind: field.kind,
1401
+ fieldId: field.id,
1402
+ selected: n <= current
1403
+ }),
1238
1404
  role: "radio",
1239
1405
  "aria-label": `${n} of ${max}`,
1240
1406
  "aria-checked": n === current,
@@ -1264,11 +1430,13 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1264
1430
  field.description ? ids.descriptionId : null,
1265
1431
  error ? ids.errorId : null
1266
1432
  ].filter(Boolean).join(" ") || void 0;
1433
+ const appearance = useFilloAppearance();
1434
+ const control = useControl("fillo-scale", field, error);
1267
1435
  return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1268
1436
  /* @__PURE__ */ jsx4(
1269
1437
  "div",
1270
1438
  {
1271
- className: "fillo-scale",
1439
+ ...control,
1272
1440
  role: "radiogroup",
1273
1441
  "aria-labelledby": ids.labelId,
1274
1442
  "aria-invalid": error ? true : void 0,
@@ -1279,7 +1447,14 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1279
1447
  "button",
1280
1448
  {
1281
1449
  type: "button",
1282
- className: `fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`,
1450
+ "data-fillo": "option",
1451
+ "data-selected": value === n ? "" : void 0,
1452
+ className: slotClass2(`fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`, appearance, {
1453
+ slot: "option",
1454
+ kind: field.kind,
1455
+ fieldId: field.id,
1456
+ selected: value === n
1457
+ }),
1283
1458
  role: "radio",
1284
1459
  "aria-label": String(n),
1285
1460
  "aria-checked": value === n,
@@ -1414,30 +1589,6 @@ function ContentRenderer({ block }) {
1414
1589
  return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
1415
1590
  }
1416
1591
  }
1417
- function shouldAutoSubmit(field, value, api) {
1418
- if (api.form.settings.submitMode !== "auto") return false;
1419
- if (!api.isLastPage || api.status !== "idle" || api.uploading) return false;
1420
- const hasDiscreteAnswer = (() => {
1421
- switch (field.kind) {
1422
- case "select":
1423
- case "dropdown":
1424
- return typeof value === "string" && field.options.some((option) => option.id === value);
1425
- case "checkbox":
1426
- return value === true;
1427
- case "rating":
1428
- case "linear_scale":
1429
- return typeof value === "number";
1430
- default:
1431
- return false;
1432
- }
1433
- })();
1434
- if (!hasDiscreteAnswer) return false;
1435
- if (value === api.data[field.id]) return false;
1436
- const nextData = { ...api.data, [field.id]: value };
1437
- const interactive = visibleFields(api.form, nextData).filter((f) => f.kind !== "hidden");
1438
- if (interactive.length !== 1 || interactive[0]?.id !== field.id) return false;
1439
- return validateResponse(api.form, nextData).ok;
1440
- }
1441
1592
  function blockHasPiping(block) {
1442
1593
  const has = (s) => typeof s === "string" && s.includes("{{");
1443
1594
  switch (block.kind) {
@@ -1497,7 +1648,15 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1497
1648
  error: api.errors[block.id],
1498
1649
  setValue: (v) => {
1499
1650
  api.setValue(block.id, v);
1500
- if (shouldAutoSubmit(block, v, api)) void api.submit();
1651
+ const ctx = {
1652
+ form: api.form,
1653
+ data: api.data,
1654
+ status: api.status,
1655
+ isLastPage: api.isLastPage,
1656
+ uploading: api.uploading
1657
+ };
1658
+ if (shouldAutoSubmit(block, v, ctx)) void api.submit().catch(() => {
1659
+ });
1501
1660
  },
1502
1661
  api,
1503
1662
  ids
@@ -1511,8 +1670,13 @@ function FormField({
1511
1670
  customComponents
1512
1671
  }) {
1513
1672
  const api = useFillo();
1673
+ const visibleIds = useMemo4(
1674
+ () => new Set(visibleFields(api.form, api.data).map((f) => f.id)),
1675
+ [api.form, api.data]
1676
+ );
1514
1677
  const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
1515
1678
  if (!block) return null;
1679
+ if (isField(block) && !visibleIds.has(block.id)) return null;
1516
1680
  return /* @__PURE__ */ jsx4(
1517
1681
  BlockRenderer,
1518
1682
  {
@@ -1566,18 +1730,25 @@ function focusFirstInvalid(formEl) {
1566
1730
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
1567
1731
  else setTimeout(run, 0);
1568
1732
  }
1569
- function isAutoSubmitBlock(block) {
1570
- return isField2(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
1571
- }
1572
- function needsExplicitSubmit(visible) {
1573
- const interactive = visible.filter((b) => !(isField2(b) && b.kind === "hidden"));
1574
- if (interactive.length === 0) return false;
1575
- if (interactive.length > 1) return true;
1576
- return !isAutoSubmitBlock(interactive[0]);
1733
+ var HP_STYLE = {
1734
+ position: "absolute",
1735
+ left: "-9999px",
1736
+ top: "auto",
1737
+ width: "1px",
1738
+ height: "1px",
1739
+ opacity: 0,
1740
+ overflow: "hidden",
1741
+ pointerEvents: "none"
1742
+ };
1743
+ function assertHpHidden(el) {
1744
+ if (!el) return;
1745
+ for (const [prop, value] of Object.entries(HP_STYLE)) {
1746
+ el.style[prop] = String(value);
1747
+ }
1577
1748
  }
1578
1749
  function FilloForm(props) {
1579
1750
  const { form, formId } = props;
1580
- const client = useMemo4(
1751
+ const client = useMemo5(
1581
1752
  () => props.client ?? (formId ? createClient() : void 0),
1582
1753
  [props.client, formId]
1583
1754
  );
@@ -1589,7 +1760,7 @@ function FilloForm(props) {
1589
1760
  const codeForm = isCodeForm2(form) ? form : null;
1590
1761
  const inlineSchema = codeForm ? codeForm.schema : form;
1591
1762
  const onError = props.onError;
1592
- const syncedFormId = useCodeFormSync(form, client, onError);
1763
+ const { formId: syncedFormId, status: syncedStatus } = useCodeFormSync(form, client, onError);
1593
1764
  const onErrorRef = useRef5(onError);
1594
1765
  useEffect7(() => {
1595
1766
  onErrorRef.current = onError;
@@ -1616,24 +1787,31 @@ function FilloForm(props) {
1616
1787
  cancelled = true;
1617
1788
  };
1618
1789
  }, [needsFetch, client, formId]);
1790
+ const strings = resolveStrings(props.strings);
1619
1791
  const rawSchema = inlineSchema ?? fetched;
1620
- const normalized = useMemo4(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
1792
+ const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
1621
1793
  const schema = normalized?.ok ? normalized.schema : null;
1622
- const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
1794
+ const theme = props.appearance?.theme ?? props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
1623
1795
  if (normalized && !normalized.ok) {
1624
1796
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
1625
1797
  if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
1626
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: "This form could not be rendered." });
1798
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
1627
1799
  }
1628
1800
  if (loadError) {
1629
1801
  if (props.renderError) {
1630
1802
  return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(loadError) });
1631
1803
  }
1632
- 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.";
1804
+ const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
1633
1805
  return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
1634
1806
  }
1635
1807
  if (closed) {
1636
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: "This form is no longer accepting responses." }) });
1808
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.closed }) });
1809
+ }
1810
+ const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
1811
+ if (notPublished && !isDevEnv()) {
1812
+ const error = new FilloError2("This form isn't published yet.", 403);
1813
+ if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
1814
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.notLive }) });
1637
1815
  }
1638
1816
  if (!schema) {
1639
1817
  return /* @__PURE__ */ jsxs5(
@@ -1661,7 +1839,8 @@ function FilloForm(props) {
1661
1839
  formId: syncedFormId ?? formId,
1662
1840
  poweredBy,
1663
1841
  brandLabel: fetchedBranding?.label,
1664
- brandHref: fetchedBranding?.href
1842
+ brandHref: fetchedBranding?.href,
1843
+ draftNotice: notPublished
1665
1844
  }
1666
1845
  );
1667
1846
  }
@@ -1679,10 +1858,25 @@ function DevNoClientNotice() {
1679
1858
  " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
1680
1859
  ] });
1681
1860
  }
1861
+ function DevDraftNotice() {
1862
+ return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
1863
+ /* @__PURE__ */ jsx5("strong", { children: "Draft form." }),
1864
+ " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
1865
+ ] });
1866
+ }
1682
1867
  function ResolvedForm(props) {
1683
1868
  const { schema, theme } = props;
1684
1869
  const instanceId = useId2().replace(/:/g, "");
1685
1870
  const hpRef = useRef5("");
1871
+ const codeForm = isCodeForm2(props.form) ? props.form : null;
1872
+ const client = props.client;
1873
+ const resolveFormId = useMemo5(
1874
+ () => codeForm && client?.key ? (
1875
+ // bypassCache: a stale cached formId must not be trusted at submit time.
1876
+ () => syncCodeForm2(client, codeForm, { bypassCache: true }).then((r) => r.formId)
1877
+ ) : void 0,
1878
+ [codeForm, client]
1879
+ );
1686
1880
  const api = useFilloController({
1687
1881
  form: schema,
1688
1882
  formId: props.formId,
@@ -1691,9 +1885,13 @@ function ResolvedForm(props) {
1691
1885
  onChange: props.onChange,
1692
1886
  onSubmitted: props.onSubmitted,
1693
1887
  skipValidation: props.skipValidation,
1694
- getHoneypot: () => hpRef.current
1888
+ getHoneypot: () => hpRef.current,
1889
+ // Framed renderer: Fillo draws the layout (and badge), so the surface is
1890
+ // "default", never the bare-engine "headless".
1891
+ surface: "default",
1892
+ resolveFormId
1695
1893
  });
1696
- const themed = useMemo4(() => themeProps(theme), [theme]);
1894
+ const themed = useMemo5(() => themeProps(theme), [theme]);
1697
1895
  const settings = schema.settings;
1698
1896
  const submitted = api.status === "submitted";
1699
1897
  useEffect7(() => {
@@ -1712,6 +1910,8 @@ function ResolvedForm(props) {
1712
1910
  prevPageIndex.current = api.pageIndex;
1713
1911
  blocksRef.current?.focus();
1714
1912
  }, [api.pageIndex]);
1913
+ const appearance = props.appearance;
1914
+ const strings = resolveStrings(props.strings);
1715
1915
  if (api.status === "submitted") {
1716
1916
  return /* @__PURE__ */ jsxs5(
1717
1917
  "div",
@@ -1720,14 +1920,27 @@ function ResolvedForm(props) {
1720
1920
  tabIndex: -1,
1721
1921
  role: "status",
1722
1922
  "aria-live": "polite",
1723
- className: `fillo-form fillo-form--success ${props.className ?? ""}`,
1923
+ className: slotClass3(
1924
+ `fillo-form fillo-form--success ${props.className ?? ""}`,
1925
+ appearance,
1926
+ { slot: "root", status: "submitted" }
1927
+ ),
1928
+ "data-fillo": "root",
1929
+ "data-state": "submitted",
1724
1930
  ...themed,
1725
1931
  children: [
1726
- props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5("div", { className: "fillo-success", children: [
1727
- /* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
1728
- /* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? "Thanks!" }),
1729
- /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? "Your response has been recorded." })
1730
- ] }),
1932
+ props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5(
1933
+ "div",
1934
+ {
1935
+ className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
1936
+ "data-fillo": "success",
1937
+ children: [
1938
+ /* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
1939
+ /* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
1940
+ /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? strings.successMessage })
1941
+ ]
1942
+ }
1943
+ ),
1731
1944
  props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
1732
1945
  ]
1733
1946
  }
@@ -1737,27 +1950,38 @@ function ResolvedForm(props) {
1737
1950
  const autoSubmit = settings.submitMode === "auto";
1738
1951
  const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields2(api.form, api.data));
1739
1952
  const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
1740
- return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
1953
+ return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx5(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
1741
1954
  "form",
1742
1955
  {
1743
- className: `fillo-form ${props.className ?? ""}`,
1956
+ className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
1957
+ slot: "root",
1958
+ status: api.status
1959
+ }),
1960
+ "data-fillo": "root",
1961
+ "data-state": api.status,
1962
+ "data-page": api.pageIndex + 1,
1963
+ "data-last-page": api.isLastPage ? "" : void 0,
1744
1964
  ...themed,
1745
1965
  noValidate: true,
1746
1966
  onSubmit: (e) => {
1747
1967
  e.preventDefault();
1748
1968
  const formEl = e.currentTarget;
1749
- if (api.isLastPage) void api.submit().then(() => focusFirstInvalid(formEl));
1750
- else {
1969
+ if (api.isLastPage) {
1970
+ void api.submit().then(() => focusFirstInvalid(formEl)).catch(() => {
1971
+ });
1972
+ } else {
1751
1973
  api.next();
1752
1974
  focusFirstInvalid(formEl);
1753
1975
  }
1754
1976
  },
1755
1977
  children: [
1756
1978
  !props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
1979
+ props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
1757
1980
  multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
1758
1981
  "div",
1759
1982
  {
1760
- className: "fillo-progress-track",
1983
+ className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
1984
+ "data-fillo": "progress",
1761
1985
  role: "progressbar",
1762
1986
  "aria-valuemin": 0,
1763
1987
  "aria-valuemax": api.pageCount,
@@ -1765,18 +1989,26 @@ function ResolvedForm(props) {
1765
1989
  children: /* @__PURE__ */ jsx5(
1766
1990
  "div",
1767
1991
  {
1768
- className: "fillo-progress-fill",
1769
- style: { width: `${(api.pageIndex + 1) / api.pageCount * 100}%` }
1992
+ className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
1993
+ "data-fillo": "progressFill",
1994
+ style: { "--fillo-progress-value": `${(api.pageIndex + 1) / api.pageCount * 100}%` }
1770
1995
  }
1771
1996
  )
1772
1997
  }
1773
1998
  ),
1774
- api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: "fillo-header", children: [
1775
- /* @__PURE__ */ jsx5("h1", { className: "fillo-title", children: schema.title }),
1776
- schema.description && /* @__PURE__ */ jsx5("p", { className: "fillo-form-description", children: schema.description })
1999
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
2000
+ /* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2001
+ schema.description && /* @__PURE__ */ jsx5(
2002
+ "p",
2003
+ {
2004
+ className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2005
+ "data-fillo": "description",
2006
+ children: schema.description
2007
+ }
2008
+ )
1777
2009
  ] }),
1778
- multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: "fillo-page-title", children: api.page.title }),
1779
- /* @__PURE__ */ jsx5("div", { className: "fillo-blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
2010
+ multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }), "data-fillo": "pageTitle", children: api.page.title }),
2011
+ /* @__PURE__ */ jsx5("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
1780
2012
  BlockRenderer,
1781
2013
  {
1782
2014
  block,
@@ -1796,70 +2028,162 @@ function ResolvedForm(props) {
1796
2028
  autoComplete: "off",
1797
2029
  "aria-hidden": "true",
1798
2030
  defaultValue: "",
2031
+ style: HP_STYLE,
2032
+ ref: assertHpHidden,
1799
2033
  onChange: (e) => {
1800
2034
  hpRef.current = e.target.value;
1801
2035
  }
1802
2036
  }
1803
2037
  ),
1804
- api.status === "error" && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: "This form can't submit right now. Please try again in a moment." }),
1805
- showFooter && /* @__PURE__ */ jsxs5("footer", { className: "fillo-footer", children: [
1806
- multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5("button", { type: "button", className: "fillo-button fillo-button--ghost", onClick: api.back, children: "Back" }),
2038
+ (api.status === "error" || api.submitError) && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: api.submitError ?? strings.submitFailed }),
2039
+ showFooter && /* @__PURE__ */ jsxs5("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
2040
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5(
2041
+ "button",
2042
+ {
2043
+ type: "button",
2044
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
2045
+ "data-fillo": "button",
2046
+ onClick: api.back,
2047
+ children: strings.back
2048
+ }
2049
+ ),
1807
2050
  showPrimaryButton && /* @__PURE__ */ jsx5(
1808
2051
  "button",
1809
2052
  {
1810
2053
  type: "submit",
1811
- className: "fillo-button fillo-button--primary",
2054
+ className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
2055
+ "data-fillo": "button",
1812
2056
  disabled: api.status === "submitting" || api.uploading,
1813
- children: api.uploading ? "Uploading\u2026" : api.status === "submitting" ? "Submitting\u2026" : api.isLastPage ? settings.submitLabel ?? "Submit" : "Next"
2057
+ children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
1814
2058
  }
1815
2059
  )
1816
2060
  ] }),
1817
2061
  props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
1818
2062
  ]
1819
2063
  }
1820
- ) }) });
2064
+ ) }) }) }) });
1821
2065
  }
1822
2066
 
1823
- // src/provider.tsx
1824
- import { useId as useId3, useMemo as useMemo5 } from "react";
1825
- import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2 } from "@usefillo/core";
2067
+ // src/jsx.tsx
2068
+ import { useRef as useRef6 } from "react";
2069
+ import {
2070
+ codeFormFromJsx,
2071
+ FilloJsxError,
2072
+ JSX_BLOCK_COMPONENTS
2073
+ } from "@usefillo/core";
1826
2074
  import { jsx as jsx6 } from "react/jsx-runtime";
1827
- function FilloProvider({ children, form, formId, client, ...options }) {
2075
+ var B = JSX_BLOCK_COMPONENTS;
2076
+ var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2077
+ function JsxForm(props) {
2078
+ const { id, title, description, settings, theme, client, children, ...rest } = props;
2079
+ const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
2080
+ const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
2081
+ const memo2 = useRef6(null);
2082
+ if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
2083
+ console.warn(
2084
+ `[fillo] "${id}": form structure changed between renders. If you just edited the form, ignore this. Otherwise: conditional questions are visibleIf={when(\u2026)\u2026}, never {cond && <Fillo.\u2026/>} \u2014 a per-visitor schema churns drafts in your workspace.`
2085
+ );
2086
+ }
2087
+ if (!memo2.current || memo2.current.json !== json) {
2088
+ memo2.current = { json, form: compiled };
2089
+ }
2090
+ return /* @__PURE__ */ jsx6(FilloForm, { form: memo2.current.form, client, ...rest });
2091
+ }
2092
+ function defineFormFromJsx(element) {
2093
+ if (!element || element.type !== JsxForm) {
2094
+ throw new FilloJsxError(
2095
+ "NON_FILLO_CHILD",
2096
+ "Fillo.defineForm() takes a <Fillo.Form> element: Fillo.defineForm(<Fillo.Form id=\u2026>\u2026</Fillo.Form>)"
2097
+ );
2098
+ }
2099
+ const { id, title, description, settings, theme, children } = element.props;
2100
+ return codeFormFromJsx({ id, title, description, settings, theme }, children);
2101
+ }
2102
+ var Fillo = {
2103
+ Form: JsxForm,
2104
+ /** Compile a <Fillo.Form> element to a CodeForm at module scope — the same
2105
+ * value FilloProvider (headless) and the CLI consume. */
2106
+ defineForm: defineFormFromJsx,
2107
+ Text: B.Text,
2108
+ LongText: B.LongText,
2109
+ Email: B.Email,
2110
+ Url: B.Url,
2111
+ Phone: B.Phone,
2112
+ Number: B.Number,
2113
+ Select: B.Select,
2114
+ MultiSelect: B.MultiSelect,
2115
+ Dropdown: B.Dropdown,
2116
+ Checkbox: B.Checkbox,
2117
+ Rating: B.Rating,
2118
+ Scale: B.Scale,
2119
+ Ranking: B.Ranking,
2120
+ Matrix: B.Matrix,
2121
+ Signature: B.Signature,
2122
+ Date: B.Date,
2123
+ FileUpload: B.FileUpload,
2124
+ Hidden: B.Hidden,
2125
+ Custom: B.Custom,
2126
+ Heading: B.Heading,
2127
+ Paragraph: B.Paragraph,
2128
+ Divider: B.Divider,
2129
+ Page: B.Page,
2130
+ Option: B.Option
2131
+ };
2132
+
2133
+ // src/provider.tsx
2134
+ import { useId as useId3, useMemo as useMemo6 } from "react";
2135
+ import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2, resolveStrings as resolveStrings2 } from "@usefillo/core";
2136
+ import { jsx as jsx7 } from "react/jsx-runtime";
2137
+ function FilloProvider({ children, form, formId, client, appearance, strings, ...options }) {
1828
2138
  const instanceId = useId3().replace(/:/g, "");
1829
2139
  const codeForm = isCodeForm2(form) ? form : null;
1830
2140
  const rawSchema = codeForm ? codeForm.schema : form;
1831
- const schemaKey = useMemo5(() => JSON.stringify(rawSchema), [rawSchema]);
1832
- const normalized = useMemo5(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
2141
+ const schemaKey = useMemo6(() => JSON.stringify(rawSchema), [rawSchema]);
2142
+ const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
1833
2143
  if (!normalized.ok) {
1834
2144
  throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
1835
2145
  }
1836
2146
  const schema = normalized.schema;
1837
- const syncedFormId = useCodeFormSync(form, client);
2147
+ const { formId: syncedFormId } = useCodeFormSync(form, client);
1838
2148
  const api = useFilloController({
1839
2149
  ...options,
1840
2150
  client,
1841
2151
  form: schema,
1842
2152
  formId: syncedFormId ?? formId,
1843
- // Headless embedding renders no Fillo layout (so it carries no branding) —
1844
- // a paid capability the server enforces when these responses are submitted.
2153
+ // Headless = your markup, no Fillo-rendered layout. Free like every embed
2154
+ // method; recorded per response so usage stays measurable.
1845
2155
  surface: "headless"
1846
2156
  });
1847
- return /* @__PURE__ */ jsx6(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx6(FilloInstanceIdContext.Provider, { value: instanceId, children }) });
2157
+ return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolveStrings2(strings), children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
1848
2158
  }
1849
2159
 
1850
2160
  // src/index.ts
1851
2161
  import {
1852
2162
  createClient as createClient2,
1853
2163
  provisionWorkspace,
2164
+ when,
2165
+ FilloJsxError as FilloJsxError2,
1854
2166
  FilloClient,
1855
2167
  FilloError as FilloError4
1856
2168
  } from "@usefillo/core";
2169
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
2170
+ const key = /* @__PURE__ */ Symbol.for("usefillo.react.instance");
2171
+ const g = globalThis;
2172
+ if (g[key]) {
2173
+ console.error(
2174
+ "[fillo] Two copies of @usefillo/react are loaded \u2014 React context does not cross package copies, so forms will misbehave. Dedupe the dependency (npm dedupe / pnpm why @usefillo/react)."
2175
+ );
2176
+ }
2177
+ g[key] = true;
2178
+ }
1857
2179
  export {
1858
2180
  BlockRenderer,
2181
+ Fillo,
1859
2182
  FilloClient,
1860
2183
  FilloError4 as FilloError,
1861
2184
  FileUploadField as FilloFileUpload,
1862
2185
  FilloForm,
2186
+ FilloJsxError2 as FilloJsxError,
1863
2187
  FilloProvider,
1864
2188
  FormField,
1865
2189
  createClient2 as createClient,
@@ -1867,5 +2191,7 @@ export {
1867
2191
  provisionWorkspace,
1868
2192
  useField,
1869
2193
  useFillo,
1870
- useFilloController
2194
+ useFilloAppearance,
2195
+ useFilloController,
2196
+ when
1871
2197
  };