@usefillo/react 0.5.4 → 0.6.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 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([]);
@@ -662,16 +731,18 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
662
731
  setValue(files.filter((f) => f.fileId !== fileId));
663
732
  }
664
733
  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: [
734
+ return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
735
+ /* @__PURE__ */ jsxs3("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
667
736
  field.label,
668
- !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: " (optional)" })
737
+ !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
669
738
  ] }),
670
- field.description && /* @__PURE__ */ jsx3("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
739
+ field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
671
740
  remaining > 0 && /* @__PURE__ */ jsxs3(
672
741
  "div",
673
742
  {
674
- className: `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`,
743
+ "data-fillo": "control",
744
+ "data-drag-over": dragOver ? "" : void 0,
745
+ className: slots.control(`fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`),
675
746
  onDragOver: (e) => {
676
747
  e.preventDefault();
677
748
  setDragOver(true);
@@ -720,7 +791,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
720
791
  ]
721
792
  }
722
793
  ),
723
- /* @__PURE__ */ jsx3("div", { role: "status", "aria-live": "polite", className: "fillo-sr-only", children: uploadStatus(inFlight, files.length) }),
794
+ /* @__PURE__ */ jsx3(
795
+ "div",
796
+ {
797
+ role: "status",
798
+ "aria-live": "polite",
799
+ className: "fillo-sr-only",
800
+ style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
801
+ children: uploadStatus(inFlight, files.length)
802
+ }
803
+ ),
724
804
  (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
725
805
  files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
726
806
  /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
@@ -784,7 +864,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
784
864
  f.key
785
865
  ))
786
866
  ] }),
787
- error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
867
+ error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
788
868
  ] });
789
869
  }
790
870
 
@@ -801,6 +881,18 @@ function fieldAria(field, error, ids) {
801
881
  "aria-describedby": describedBy.length ? describedBy.join(" ") : void 0
802
882
  };
803
883
  }
884
+ function useControl(base, field, error) {
885
+ return {
886
+ className: useSlotClass(base, {
887
+ slot: "control",
888
+ kind: field.kind,
889
+ fieldId: field.id,
890
+ invalid: Boolean(error),
891
+ required: Boolean(field.required)
892
+ }),
893
+ "data-fillo": "control"
894
+ };
895
+ }
804
896
  function isNativeControlTarget(target) {
805
897
  return target instanceof HTMLElement && Boolean(target.closest("input, textarea, select, button"));
806
898
  }
@@ -810,19 +902,37 @@ function FieldShell({
810
902
  ids,
811
903
  children
812
904
  }) {
905
+ const state = {
906
+ kind: field.kind,
907
+ fieldId: field.id,
908
+ invalid: Boolean(error),
909
+ required: Boolean(field.required)
910
+ };
911
+ const fieldCls = useSlotClass(
912
+ `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
913
+ { slot: "field", ...state }
914
+ );
915
+ const labelCls = useSlotClass("fillo-label", { slot: "label", ...state });
916
+ const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
917
+ const errorCls = useSlotClass("fillo-error", { slot: "error", ...state });
918
+ const strings = useStrings();
813
919
  return /* @__PURE__ */ jsxs4(
814
920
  "div",
815
921
  {
816
- className: `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
922
+ className: fieldCls,
923
+ "data-fillo": "field",
817
924
  "data-field": field.id,
925
+ "data-kind": field.kind,
926
+ "data-invalid": error ? "" : void 0,
927
+ "data-required": field.required ? "" : void 0,
818
928
  children: [
819
- /* @__PURE__ */ jsxs4("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
929
+ /* @__PURE__ */ jsxs4("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
820
930
  field.label,
821
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
931
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
822
932
  ] }),
823
- field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
933
+ field.description && /* @__PURE__ */ jsx4("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
824
934
  children,
825
- error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
935
+ error && /* @__PURE__ */ jsx4("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
826
936
  ]
827
937
  }
828
938
  );
@@ -830,11 +940,12 @@ function FieldShell({
830
940
  function TextInput({ field, value, error, setValue, ids: providedIds }) {
831
941
  const ids = useFilloFieldIds(field.id, providedIds);
832
942
  const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
943
+ const control = useControl("fillo-input", field, error);
833
944
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
834
945
  "input",
835
946
  {
836
947
  id: ids.inputId,
837
- className: "fillo-input",
948
+ ...control,
838
949
  type,
839
950
  value: typeof value === "string" || typeof value === "number" ? String(value) : "",
840
951
  placeholder: field.placeholder,
@@ -845,11 +956,12 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
845
956
  }
846
957
  function LongText({ field, value, error, setValue, ids: providedIds }) {
847
958
  const ids = useFilloFieldIds(field.id, providedIds);
959
+ const control = useControl("fillo-input fillo-textarea", field, error);
848
960
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
849
961
  "textarea",
850
962
  {
851
963
  id: ids.inputId,
852
- className: "fillo-input fillo-textarea",
964
+ ...control,
853
965
  rows: 4,
854
966
  value: typeof value === "string" ? value : "",
855
967
  placeholder: field.placeholder,
@@ -898,6 +1010,9 @@ function ChoiceIcon({ icon }) {
898
1010
  }
899
1011
  function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
900
1012
  const ids = useFilloFieldIds(field.id, providedIds);
1013
+ const appearance = useFilloAppearance();
1014
+ const strings = useStrings();
1015
+ const optionCls = (base, optionId, selected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected });
901
1016
  const choice = field;
902
1017
  const options = useDisplayOptions(choice);
903
1018
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
@@ -906,7 +1021,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
906
1021
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
907
1022
  "div",
908
1023
  {
909
- className: "fillo-options",
1024
+ className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
1025
+ "data-fillo": "options",
910
1026
  role: "radiogroup",
911
1027
  "aria-labelledby": ids.labelId,
912
1028
  ...fieldAria(field, error, ids),
@@ -915,7 +1031,13 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
915
1031
  "label",
916
1032
  {
917
1033
  "data-option": opt.id,
918
- className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
1034
+ "data-fillo": "option",
1035
+ "data-selected": value === opt.id ? "" : void 0,
1036
+ className: optionCls(
1037
+ `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
1038
+ opt.id,
1039
+ value === opt.id
1040
+ ),
919
1041
  onClick: (event) => {
920
1042
  if (isNativeControlTarget(event.target)) return;
921
1043
  event.preventDefault();
@@ -937,7 +1059,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
937
1059
  }
938
1060
  ),
939
1061
  /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
940
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
1062
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
941
1063
  ]
942
1064
  },
943
1065
  opt.id
@@ -947,7 +1069,13 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
947
1069
  /* @__PURE__ */ jsxs4(
948
1070
  "div",
949
1071
  {
950
- className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1072
+ "data-fillo": "option",
1073
+ "data-selected": otherActive ? "" : void 0,
1074
+ className: optionCls(
1075
+ `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1076
+ "__other",
1077
+ otherActive
1078
+ ),
951
1079
  children: [
952
1080
  /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
953
1081
  /* @__PURE__ */ jsx4(
@@ -963,15 +1091,15 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
963
1091
  }
964
1092
  }
965
1093
  ),
966
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
1094
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
967
1095
  ] }),
968
1096
  otherActive && /* @__PURE__ */ jsx4(
969
1097
  "input",
970
1098
  {
971
1099
  type: "text",
972
1100
  className: "fillo-input fillo-other-input",
973
- "aria-label": "Other \u2014 please specify",
974
- placeholder: "Your answer",
1101
+ "aria-label": strings.otherPrompt,
1102
+ placeholder: strings.otherPlaceholder,
975
1103
  autoFocus: !isOtherValue,
976
1104
  value: isOtherValue ? String(value) : "",
977
1105
  onChange: (e) => setValue(e.target.value)
@@ -988,16 +1116,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
988
1116
  const ids = useFilloFieldIds(field.id, providedIds);
989
1117
  const choice = field;
990
1118
  const options = useDisplayOptions(choice);
1119
+ const strings = useStrings();
991
1120
  const optionIds = new Set(choice.options.map((o) => o.id));
992
1121
  const selected = Array.isArray(value) ? value : [];
993
1122
  const otherText = selected.find((v) => !optionIds.has(v));
994
1123
  const [otherOn, setOtherOn] = useState5(otherText !== void 0);
995
1124
  const otherActive = otherText !== void 0 || otherOn;
996
1125
  const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
1126
+ const appearance = useFilloAppearance();
1127
+ const optionCls = (base, optionId, isSelected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected: isSelected });
997
1128
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
998
1129
  "div",
999
1130
  {
1000
- className: "fillo-options",
1131
+ className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
1132
+ "data-fillo": "options",
1001
1133
  role: "group",
1002
1134
  "aria-labelledby": ids.labelId,
1003
1135
  ...fieldAria(field, error, ids),
@@ -1006,7 +1138,13 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1006
1138
  "label",
1007
1139
  {
1008
1140
  "data-option": opt.id,
1009
- className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
1141
+ "data-fillo": "option",
1142
+ "data-selected": selected.includes(opt.id) ? "" : void 0,
1143
+ className: optionCls(
1144
+ `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
1145
+ opt.id,
1146
+ selected.includes(opt.id)
1147
+ ),
1010
1148
  onClick: (event) => {
1011
1149
  if (isNativeControlTarget(event.target)) return;
1012
1150
  event.preventDefault();
@@ -1023,7 +1161,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1023
1161
  }
1024
1162
  ),
1025
1163
  /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
1026
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
1164
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1027
1165
  ]
1028
1166
  },
1029
1167
  opt.id
@@ -1033,7 +1171,13 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1033
1171
  /* @__PURE__ */ jsxs4(
1034
1172
  "div",
1035
1173
  {
1036
- className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1174
+ "data-fillo": "option",
1175
+ "data-selected": otherActive ? "" : void 0,
1176
+ className: optionCls(
1177
+ `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1178
+ "__other",
1179
+ otherActive
1180
+ ),
1037
1181
  children: [
1038
1182
  /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1039
1183
  /* @__PURE__ */ jsx4(
@@ -1052,15 +1196,15 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1052
1196
  }
1053
1197
  }
1054
1198
  ),
1055
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
1199
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1056
1200
  ] }),
1057
1201
  otherActive && /* @__PURE__ */ jsx4(
1058
1202
  "input",
1059
1203
  {
1060
1204
  type: "text",
1061
1205
  className: "fillo-input fillo-other-input",
1062
- "aria-label": "Other \u2014 please specify",
1063
- placeholder: "Your answer",
1206
+ "aria-label": strings.otherPrompt,
1207
+ placeholder: strings.otherPlaceholder,
1064
1208
  autoFocus: otherText === void 0,
1065
1209
  value: otherText ?? "",
1066
1210
  onChange: (e) => {
@@ -1079,18 +1223,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1079
1223
  var OTHER_SENTINEL = "__fw_other__";
1080
1224
  function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1081
1225
  const ids = useFilloFieldIds(field.id, providedIds);
1226
+ const strings = useStrings();
1082
1227
  const choice = field;
1083
1228
  const options = useDisplayOptions(choice);
1084
1229
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
1085
1230
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1086
1231
  const otherActive = isOtherValue || value === "" && otherOn;
1232
+ const control = useControl("fillo-input fillo-select", field, error);
1087
1233
  return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1088
1234
  /* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
1089
1235
  /* @__PURE__ */ jsxs4(
1090
1236
  "select",
1091
1237
  {
1092
1238
  id: ids.inputId,
1093
- className: "fillo-input fillo-select",
1239
+ ...control,
1094
1240
  ...fieldAria(field, error, ids),
1095
1241
  value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
1096
1242
  onChange: (e) => {
@@ -1103,9 +1249,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1103
1249
  }
1104
1250
  },
1105
1251
  children: [
1106
- /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? "Choose\u2026" }),
1252
+ /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1107
1253
  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" })
1254
+ choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: `${strings.other}\u2026` })
1109
1255
  ]
1110
1256
  }
1111
1257
  ),
@@ -1116,8 +1262,8 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1116
1262
  {
1117
1263
  type: "text",
1118
1264
  className: "fillo-input fillo-other-input fillo-other-input--block",
1119
- "aria-label": "Other \u2014 please specify",
1120
- placeholder: "Your answer",
1265
+ "aria-label": strings.otherPrompt,
1266
+ placeholder: strings.otherPlaceholder,
1121
1267
  autoFocus: !isOtherValue,
1122
1268
  value: isOtherValue ? String(value) : "",
1123
1269
  onChange: (e) => setValue(e.target.value)
@@ -1127,18 +1273,24 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1127
1273
  }
1128
1274
  function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1129
1275
  const ids = useFilloFieldIds(field.id, providedIds);
1276
+ const strings = useStrings();
1277
+ const appearance = useFilloAppearance();
1130
1278
  const checkbox = field;
1131
1279
  if (checkbox.appearance === "toggle") {
1132
1280
  return /* @__PURE__ */ jsxs4(
1133
1281
  "div",
1134
1282
  {
1135
1283
  className: `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`,
1284
+ "data-fillo": "field",
1136
1285
  "data-field": field.id,
1286
+ "data-kind": field.kind,
1287
+ "data-invalid": error ? "" : void 0,
1288
+ "data-required": field.required ? "" : void 0,
1137
1289
  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: [
1290
+ /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1291
+ /* @__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
1292
  field.label,
1141
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
1293
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1142
1294
  ] }) }),
1143
1295
  /* @__PURE__ */ jsx4(
1144
1296
  "input",
@@ -1165,9 +1317,13 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1165
1317
  "div",
1166
1318
  {
1167
1319
  className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
1320
+ "data-fillo": "field",
1168
1321
  "data-field": field.id,
1322
+ "data-kind": field.kind,
1323
+ "data-invalid": error ? "" : void 0,
1324
+ "data-required": field.required ? "" : void 0,
1169
1325
  children: [
1170
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option", children: [
1326
+ /* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1171
1327
  /* @__PURE__ */ jsx4(
1172
1328
  "input",
1173
1329
  {
@@ -1179,9 +1335,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1179
1335
  ...fieldAria(field, error, ids)
1180
1336
  }
1181
1337
  ),
1182
- /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
1338
+ /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1183
1339
  field.label,
1184
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
1340
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1185
1341
  ] })
1186
1342
  ] }),
1187
1343
  field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
@@ -1222,10 +1378,12 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1222
1378
  const steps = Array.from({ length: max }, (_, i) => i + 1);
1223
1379
  const selected = current > 0 ? current : null;
1224
1380
  const tabbable = rovingIndex(steps, selected);
1381
+ const appearance = useFilloAppearance();
1382
+ const control = useControl("fillo-rating", field, error);
1225
1383
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1226
1384
  "div",
1227
1385
  {
1228
- className: "fillo-rating",
1386
+ ...control,
1229
1387
  role: "radiogroup",
1230
1388
  "aria-labelledby": ids.labelId,
1231
1389
  ...fieldAria(field, error, ids),
@@ -1234,7 +1392,14 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1234
1392
  "button",
1235
1393
  {
1236
1394
  type: "button",
1237
- className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
1395
+ "data-fillo": "option",
1396
+ "data-selected": n <= current ? "" : void 0,
1397
+ className: slotClass2(`fillo-star${n <= current ? " fillo-star--active" : ""}`, appearance, {
1398
+ slot: "option",
1399
+ kind: field.kind,
1400
+ fieldId: field.id,
1401
+ selected: n <= current
1402
+ }),
1238
1403
  role: "radio",
1239
1404
  "aria-label": `${n} of ${max}`,
1240
1405
  "aria-checked": n === current,
@@ -1264,11 +1429,13 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1264
1429
  field.description ? ids.descriptionId : null,
1265
1430
  error ? ids.errorId : null
1266
1431
  ].filter(Boolean).join(" ") || void 0;
1432
+ const appearance = useFilloAppearance();
1433
+ const control = useControl("fillo-scale", field, error);
1267
1434
  return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1268
1435
  /* @__PURE__ */ jsx4(
1269
1436
  "div",
1270
1437
  {
1271
- className: "fillo-scale",
1438
+ ...control,
1272
1439
  role: "radiogroup",
1273
1440
  "aria-labelledby": ids.labelId,
1274
1441
  "aria-invalid": error ? true : void 0,
@@ -1279,7 +1446,14 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1279
1446
  "button",
1280
1447
  {
1281
1448
  type: "button",
1282
- className: `fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`,
1449
+ "data-fillo": "option",
1450
+ "data-selected": value === n ? "" : void 0,
1451
+ className: slotClass2(`fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`, appearance, {
1452
+ slot: "option",
1453
+ kind: field.kind,
1454
+ fieldId: field.id,
1455
+ selected: value === n
1456
+ }),
1283
1457
  role: "radio",
1284
1458
  "aria-label": String(n),
1285
1459
  "aria-checked": value === n,
@@ -1414,30 +1588,6 @@ function ContentRenderer({ block }) {
1414
1588
  return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
1415
1589
  }
1416
1590
  }
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
1591
  function blockHasPiping(block) {
1442
1592
  const has = (s) => typeof s === "string" && s.includes("{{");
1443
1593
  switch (block.kind) {
@@ -1497,7 +1647,15 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1497
1647
  error: api.errors[block.id],
1498
1648
  setValue: (v) => {
1499
1649
  api.setValue(block.id, v);
1500
- if (shouldAutoSubmit(block, v, api)) void api.submit();
1650
+ const ctx = {
1651
+ form: api.form,
1652
+ data: api.data,
1653
+ status: api.status,
1654
+ isLastPage: api.isLastPage,
1655
+ uploading: api.uploading
1656
+ };
1657
+ if (shouldAutoSubmit(block, v, ctx)) void api.submit().catch(() => {
1658
+ });
1501
1659
  },
1502
1660
  api,
1503
1661
  ids
@@ -1511,8 +1669,13 @@ function FormField({
1511
1669
  customComponents
1512
1670
  }) {
1513
1671
  const api = useFillo();
1672
+ const visibleIds = useMemo4(
1673
+ () => new Set(visibleFields(api.form, api.data).map((f) => f.id)),
1674
+ [api.form, api.data]
1675
+ );
1514
1676
  const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
1515
1677
  if (!block) return null;
1678
+ if (isField(block) && !visibleIds.has(block.id)) return null;
1516
1679
  return /* @__PURE__ */ jsx4(
1517
1680
  BlockRenderer,
1518
1681
  {
@@ -1566,18 +1729,25 @@ function focusFirstInvalid(formEl) {
1566
1729
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
1567
1730
  else setTimeout(run, 0);
1568
1731
  }
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]);
1732
+ var HP_STYLE = {
1733
+ position: "absolute",
1734
+ left: "-9999px",
1735
+ top: "auto",
1736
+ width: "1px",
1737
+ height: "1px",
1738
+ opacity: 0,
1739
+ overflow: "hidden",
1740
+ pointerEvents: "none"
1741
+ };
1742
+ function assertHpHidden(el) {
1743
+ if (!el) return;
1744
+ for (const [prop, value] of Object.entries(HP_STYLE)) {
1745
+ el.style[prop] = String(value);
1746
+ }
1577
1747
  }
1578
1748
  function FilloForm(props) {
1579
1749
  const { form, formId } = props;
1580
- const client = useMemo4(
1750
+ const client = useMemo5(
1581
1751
  () => props.client ?? (formId ? createClient() : void 0),
1582
1752
  [props.client, formId]
1583
1753
  );
@@ -1589,7 +1759,7 @@ function FilloForm(props) {
1589
1759
  const codeForm = isCodeForm2(form) ? form : null;
1590
1760
  const inlineSchema = codeForm ? codeForm.schema : form;
1591
1761
  const onError = props.onError;
1592
- const syncedFormId = useCodeFormSync(form, client, onError);
1762
+ const { formId: syncedFormId, status: syncedStatus } = useCodeFormSync(form, client, onError);
1593
1763
  const onErrorRef = useRef5(onError);
1594
1764
  useEffect7(() => {
1595
1765
  onErrorRef.current = onError;
@@ -1616,24 +1786,31 @@ function FilloForm(props) {
1616
1786
  cancelled = true;
1617
1787
  };
1618
1788
  }, [needsFetch, client, formId]);
1789
+ const strings = resolveStrings(props.strings);
1619
1790
  const rawSchema = inlineSchema ?? fetched;
1620
- const normalized = useMemo4(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
1791
+ const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
1621
1792
  const schema = normalized?.ok ? normalized.schema : null;
1622
- const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
1793
+ const theme = props.appearance?.theme ?? props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
1623
1794
  if (normalized && !normalized.ok) {
1624
1795
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
1625
1796
  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." });
1797
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
1627
1798
  }
1628
1799
  if (loadError) {
1629
1800
  if (props.renderError) {
1630
1801
  return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(loadError) });
1631
1802
  }
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.";
1803
+ const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
1633
1804
  return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
1634
1805
  }
1635
1806
  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." }) });
1807
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.closed }) });
1808
+ }
1809
+ const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
1810
+ if (notPublished && !isDevEnv()) {
1811
+ const error = new FilloError2("This form isn't published yet.", 403);
1812
+ if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
1813
+ 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
1814
  }
1638
1815
  if (!schema) {
1639
1816
  return /* @__PURE__ */ jsxs5(
@@ -1661,7 +1838,8 @@ function FilloForm(props) {
1661
1838
  formId: syncedFormId ?? formId,
1662
1839
  poweredBy,
1663
1840
  brandLabel: fetchedBranding?.label,
1664
- brandHref: fetchedBranding?.href
1841
+ brandHref: fetchedBranding?.href,
1842
+ draftNotice: notPublished
1665
1843
  }
1666
1844
  );
1667
1845
  }
@@ -1679,10 +1857,25 @@ function DevNoClientNotice() {
1679
1857
  " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
1680
1858
  ] });
1681
1859
  }
1860
+ function DevDraftNotice() {
1861
+ return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
1862
+ /* @__PURE__ */ jsx5("strong", { children: "Draft form." }),
1863
+ " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
1864
+ ] });
1865
+ }
1682
1866
  function ResolvedForm(props) {
1683
1867
  const { schema, theme } = props;
1684
1868
  const instanceId = useId2().replace(/:/g, "");
1685
1869
  const hpRef = useRef5("");
1870
+ const codeForm = isCodeForm2(props.form) ? props.form : null;
1871
+ const client = props.client;
1872
+ const resolveFormId = useMemo5(
1873
+ () => codeForm && client?.key ? (
1874
+ // bypassCache: a stale cached formId must not be trusted at submit time.
1875
+ () => syncCodeForm2(client, codeForm, { bypassCache: true }).then((r) => r.formId)
1876
+ ) : void 0,
1877
+ [codeForm, client]
1878
+ );
1686
1879
  const api = useFilloController({
1687
1880
  form: schema,
1688
1881
  formId: props.formId,
@@ -1691,9 +1884,13 @@ function ResolvedForm(props) {
1691
1884
  onChange: props.onChange,
1692
1885
  onSubmitted: props.onSubmitted,
1693
1886
  skipValidation: props.skipValidation,
1694
- getHoneypot: () => hpRef.current
1887
+ getHoneypot: () => hpRef.current,
1888
+ // Framed renderer: Fillo draws the layout (and badge), so the surface is
1889
+ // "default", never the bare-engine "headless".
1890
+ surface: "default",
1891
+ resolveFormId
1695
1892
  });
1696
- const themed = useMemo4(() => themeProps(theme), [theme]);
1893
+ const themed = useMemo5(() => themeProps(theme), [theme]);
1697
1894
  const settings = schema.settings;
1698
1895
  const submitted = api.status === "submitted";
1699
1896
  useEffect7(() => {
@@ -1712,6 +1909,8 @@ function ResolvedForm(props) {
1712
1909
  prevPageIndex.current = api.pageIndex;
1713
1910
  blocksRef.current?.focus();
1714
1911
  }, [api.pageIndex]);
1912
+ const appearance = props.appearance;
1913
+ const strings = resolveStrings(props.strings);
1715
1914
  if (api.status === "submitted") {
1716
1915
  return /* @__PURE__ */ jsxs5(
1717
1916
  "div",
@@ -1720,14 +1919,27 @@ function ResolvedForm(props) {
1720
1919
  tabIndex: -1,
1721
1920
  role: "status",
1722
1921
  "aria-live": "polite",
1723
- className: `fillo-form fillo-form--success ${props.className ?? ""}`,
1922
+ className: slotClass3(
1923
+ `fillo-form fillo-form--success ${props.className ?? ""}`,
1924
+ appearance,
1925
+ { slot: "root", status: "submitted" }
1926
+ ),
1927
+ "data-fillo": "root",
1928
+ "data-state": "submitted",
1724
1929
  ...themed,
1725
1930
  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
- ] }),
1931
+ props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5(
1932
+ "div",
1933
+ {
1934
+ className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
1935
+ "data-fillo": "success",
1936
+ children: [
1937
+ /* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
1938
+ /* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
1939
+ /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? strings.successMessage })
1940
+ ]
1941
+ }
1942
+ ),
1731
1943
  props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
1732
1944
  ]
1733
1945
  }
@@ -1737,27 +1949,38 @@ function ResolvedForm(props) {
1737
1949
  const autoSubmit = settings.submitMode === "auto";
1738
1950
  const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields2(api.form, api.data));
1739
1951
  const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
1740
- return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
1952
+ 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
1953
  "form",
1742
1954
  {
1743
- className: `fillo-form ${props.className ?? ""}`,
1955
+ className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
1956
+ slot: "root",
1957
+ status: api.status
1958
+ }),
1959
+ "data-fillo": "root",
1960
+ "data-state": api.status,
1961
+ "data-page": api.pageIndex + 1,
1962
+ "data-last-page": api.isLastPage ? "" : void 0,
1744
1963
  ...themed,
1745
1964
  noValidate: true,
1746
1965
  onSubmit: (e) => {
1747
1966
  e.preventDefault();
1748
1967
  const formEl = e.currentTarget;
1749
- if (api.isLastPage) void api.submit().then(() => focusFirstInvalid(formEl));
1750
- else {
1968
+ if (api.isLastPage) {
1969
+ void api.submit().then(() => focusFirstInvalid(formEl)).catch(() => {
1970
+ });
1971
+ } else {
1751
1972
  api.next();
1752
1973
  focusFirstInvalid(formEl);
1753
1974
  }
1754
1975
  },
1755
1976
  children: [
1756
1977
  !props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
1978
+ props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
1757
1979
  multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
1758
1980
  "div",
1759
1981
  {
1760
- className: "fillo-progress-track",
1982
+ className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
1983
+ "data-fillo": "progress",
1761
1984
  role: "progressbar",
1762
1985
  "aria-valuemin": 0,
1763
1986
  "aria-valuemax": api.pageCount,
@@ -1765,18 +1988,26 @@ function ResolvedForm(props) {
1765
1988
  children: /* @__PURE__ */ jsx5(
1766
1989
  "div",
1767
1990
  {
1768
- className: "fillo-progress-fill",
1769
- style: { width: `${(api.pageIndex + 1) / api.pageCount * 100}%` }
1991
+ className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
1992
+ "data-fillo": "progressFill",
1993
+ style: { "--fillo-progress-value": `${(api.pageIndex + 1) / api.pageCount * 100}%` }
1770
1994
  }
1771
1995
  )
1772
1996
  }
1773
1997
  ),
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 })
1998
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
1999
+ /* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2000
+ schema.description && /* @__PURE__ */ jsx5(
2001
+ "p",
2002
+ {
2003
+ className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2004
+ "data-fillo": "description",
2005
+ children: schema.description
2006
+ }
2007
+ )
1777
2008
  ] }),
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(
2009
+ 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 }),
2010
+ /* @__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
2011
  BlockRenderer,
1781
2012
  {
1782
2013
  block,
@@ -1796,45 +2027,123 @@ function ResolvedForm(props) {
1796
2027
  autoComplete: "off",
1797
2028
  "aria-hidden": "true",
1798
2029
  defaultValue: "",
2030
+ style: HP_STYLE,
2031
+ ref: assertHpHidden,
1799
2032
  onChange: (e) => {
1800
2033
  hpRef.current = e.target.value;
1801
2034
  }
1802
2035
  }
1803
2036
  ),
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" }),
2037
+ (api.status === "error" || api.submitError) && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: api.submitError ?? strings.submitFailed }),
2038
+ showFooter && /* @__PURE__ */ jsxs5("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
2039
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5(
2040
+ "button",
2041
+ {
2042
+ type: "button",
2043
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
2044
+ "data-fillo": "button",
2045
+ onClick: api.back,
2046
+ children: strings.back
2047
+ }
2048
+ ),
1807
2049
  showPrimaryButton && /* @__PURE__ */ jsx5(
1808
2050
  "button",
1809
2051
  {
1810
2052
  type: "submit",
1811
- className: "fillo-button fillo-button--primary",
2053
+ className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
2054
+ "data-fillo": "button",
1812
2055
  disabled: api.status === "submitting" || api.uploading,
1813
- children: api.uploading ? "Uploading\u2026" : api.status === "submitting" ? "Submitting\u2026" : api.isLastPage ? settings.submitLabel ?? "Submit" : "Next"
2056
+ children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
1814
2057
  }
1815
2058
  )
1816
2059
  ] }),
1817
2060
  props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
1818
2061
  ]
1819
2062
  }
1820
- ) }) });
2063
+ ) }) }) }) });
1821
2064
  }
1822
2065
 
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";
2066
+ // src/jsx.tsx
2067
+ import { useRef as useRef6 } from "react";
2068
+ import {
2069
+ codeFormFromJsx,
2070
+ FilloJsxError,
2071
+ JSX_BLOCK_COMPONENTS
2072
+ } from "@usefillo/core";
1826
2073
  import { jsx as jsx6 } from "react/jsx-runtime";
1827
- function FilloProvider({ children, form, formId, client, ...options }) {
2074
+ var B = JSX_BLOCK_COMPONENTS;
2075
+ var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2076
+ function JsxForm(props) {
2077
+ const { id, title, description, settings, theme, client, children, ...rest } = props;
2078
+ const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
2079
+ const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
2080
+ const memo2 = useRef6(null);
2081
+ if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
2082
+ console.warn(
2083
+ `[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.`
2084
+ );
2085
+ }
2086
+ if (!memo2.current || memo2.current.json !== json) {
2087
+ memo2.current = { json, form: compiled };
2088
+ }
2089
+ return /* @__PURE__ */ jsx6(FilloForm, { form: memo2.current.form, client, ...rest });
2090
+ }
2091
+ function defineFormFromJsx(element) {
2092
+ if (!element || element.type !== JsxForm) {
2093
+ throw new FilloJsxError(
2094
+ "NON_FILLO_CHILD",
2095
+ "Fillo.defineForm() takes a <Fillo.Form> element: Fillo.defineForm(<Fillo.Form id=\u2026>\u2026</Fillo.Form>)"
2096
+ );
2097
+ }
2098
+ const { id, title, description, settings, theme, children } = element.props;
2099
+ return codeFormFromJsx({ id, title, description, settings, theme }, children);
2100
+ }
2101
+ var Fillo = {
2102
+ Form: JsxForm,
2103
+ /** Compile a <Fillo.Form> element to a CodeForm at module scope — the same
2104
+ * value FilloProvider (headless) and the CLI consume. */
2105
+ defineForm: defineFormFromJsx,
2106
+ Text: B.Text,
2107
+ LongText: B.LongText,
2108
+ Email: B.Email,
2109
+ Url: B.Url,
2110
+ Phone: B.Phone,
2111
+ Number: B.Number,
2112
+ Select: B.Select,
2113
+ MultiSelect: B.MultiSelect,
2114
+ Dropdown: B.Dropdown,
2115
+ Checkbox: B.Checkbox,
2116
+ Rating: B.Rating,
2117
+ Scale: B.Scale,
2118
+ Ranking: B.Ranking,
2119
+ Matrix: B.Matrix,
2120
+ Signature: B.Signature,
2121
+ Date: B.Date,
2122
+ FileUpload: B.FileUpload,
2123
+ Hidden: B.Hidden,
2124
+ Custom: B.Custom,
2125
+ Heading: B.Heading,
2126
+ Paragraph: B.Paragraph,
2127
+ Divider: B.Divider,
2128
+ Page: B.Page,
2129
+ Option: B.Option
2130
+ };
2131
+
2132
+ // src/provider.tsx
2133
+ import { useId as useId3, useMemo as useMemo6 } from "react";
2134
+ import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2, resolveStrings as resolveStrings2 } from "@usefillo/core";
2135
+ import { jsx as jsx7 } from "react/jsx-runtime";
2136
+ function FilloProvider({ children, form, formId, client, appearance, strings, ...options }) {
1828
2137
  const instanceId = useId3().replace(/:/g, "");
1829
2138
  const codeForm = isCodeForm2(form) ? form : null;
1830
2139
  const rawSchema = codeForm ? codeForm.schema : form;
1831
- const schemaKey = useMemo5(() => JSON.stringify(rawSchema), [rawSchema]);
1832
- const normalized = useMemo5(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
2140
+ const schemaKey = useMemo6(() => JSON.stringify(rawSchema), [rawSchema]);
2141
+ const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
1833
2142
  if (!normalized.ok) {
1834
2143
  throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
1835
2144
  }
1836
2145
  const schema = normalized.schema;
1837
- const syncedFormId = useCodeFormSync(form, client);
2146
+ const { formId: syncedFormId } = useCodeFormSync(form, client);
1838
2147
  const api = useFilloController({
1839
2148
  ...options,
1840
2149
  client,
@@ -1844,22 +2153,36 @@ function FilloProvider({ children, form, formId, client, ...options }) {
1844
2153
  // a paid capability the server enforces when these responses are submitted.
1845
2154
  surface: "headless"
1846
2155
  });
1847
- return /* @__PURE__ */ jsx6(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx6(FilloInstanceIdContext.Provider, { value: instanceId, children }) });
2156
+ 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
2157
  }
1849
2158
 
1850
2159
  // src/index.ts
1851
2160
  import {
1852
2161
  createClient as createClient2,
1853
2162
  provisionWorkspace,
2163
+ when,
2164
+ FilloJsxError as FilloJsxError2,
1854
2165
  FilloClient,
1855
2166
  FilloError as FilloError4
1856
2167
  } from "@usefillo/core";
2168
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
2169
+ const key = /* @__PURE__ */ Symbol.for("usefillo.react.instance");
2170
+ const g = globalThis;
2171
+ if (g[key]) {
2172
+ console.error(
2173
+ "[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)."
2174
+ );
2175
+ }
2176
+ g[key] = true;
2177
+ }
1857
2178
  export {
1858
2179
  BlockRenderer,
2180
+ Fillo,
1859
2181
  FilloClient,
1860
2182
  FilloError4 as FilloError,
1861
2183
  FileUploadField as FilloFileUpload,
1862
2184
  FilloForm,
2185
+ FilloJsxError2 as FilloJsxError,
1863
2186
  FilloProvider,
1864
2187
  FormField,
1865
2188
  createClient2 as createClient,
@@ -1867,5 +2190,7 @@ export {
1867
2190
  provisionWorkspace,
1868
2191
  useField,
1869
2192
  useFillo,
1870
- useFilloController
2193
+ useFilloAppearance,
2194
+ useFilloController,
2195
+ when
1871
2196
  };