@usefillo/react 0.8.0 → 0.10.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,16 +1,27 @@
1
1
  "use client";
2
2
 
3
3
  // src/FilloForm.tsx
4
- import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as useRef6, useState as useState6 } from "react";
4
+ import {
5
+ useCallback,
6
+ useEffect as useEffect8,
7
+ useId as useId2,
8
+ useMemo as useMemo5,
9
+ useRef as useRef7,
10
+ useState as useState6,
11
+ useSyncExternalStore as useSyncExternalStore2
12
+ } from "react";
5
13
  import {
6
14
  FilloError as FilloError2,
7
15
  createClient,
8
16
  formSchemasEqual,
17
+ isBuildTimeDevEnv as isBuildTimeDevEnv2,
9
18
  isFilloError as isFilloError2,
19
+ isLikelyDevEnv,
10
20
  needsExplicitSubmit,
11
21
  normalizeFormSchema,
12
22
  normalizeFormTheme,
13
- visibleFields as visibleFields2
23
+ reachableFields,
24
+ reachablePageSequence
14
25
  } from "@usefillo/core";
15
26
  import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
16
27
 
@@ -60,11 +71,90 @@ function useStrings() {
60
71
  return useContext(FilloStringsContext);
61
72
  }
62
73
 
74
+ // src/chrome.tsx
75
+ import { isBuildTimeDevEnv } from "@usefillo/core";
76
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
77
+ function safeHttpUrl(href) {
78
+ try {
79
+ if (!href) return null;
80
+ const url = new URL(href);
81
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
82
+ return url.toString();
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ var warnedPreviewInProduction = false;
88
+ function warnPreviewInProduction() {
89
+ if (warnedPreviewInProduction || isBuildTimeDevEnv()) return;
90
+ warnedPreviewInProduction = true;
91
+ console.warn(
92
+ "[fillo] `preview` is enabled in a production build. Preview is cosmetic only \u2014 it shows developer chrome and never changes where submissions go or whether they are accepted \u2014 but remove it before respondents see this page."
93
+ );
94
+ }
95
+ function PreviewBadge() {
96
+ return /* @__PURE__ */ jsx("span", { className: "fillo-preview-badge", "data-fillo": "preview-badge", children: "Preview" });
97
+ }
98
+ function DevNoClientNotice() {
99
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
100
+ /* @__PURE__ */ jsx("strong", { children: "No Fillo client connected." }),
101
+ " This form is render-only and won\u2019t save responses. Pass a ",
102
+ /* @__PURE__ */ jsx("code", { children: "client" }),
103
+ " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development and preview.)"
104
+ ] });
105
+ }
106
+ function DevDraftNotice({ warningUrl }) {
107
+ const safeUrl = safeHttpUrl(warningUrl);
108
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
109
+ /* @__PURE__ */ jsx("strong", { children: "Draft form." }),
110
+ " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard.",
111
+ " ",
112
+ safeUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
113
+ "Connect storage to publish:",
114
+ " ",
115
+ /* @__PURE__ */ jsx("a", { href: safeUrl, target: "_blank", rel: "noopener noreferrer", children: safeUrl }),
116
+ ".",
117
+ " "
118
+ ] }),
119
+ "(This notice only appears in development and preview.)"
120
+ ] });
121
+ }
122
+ function DevStagedNotice() {
123
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
124
+ /* @__PURE__ */ jsx("strong", { children: "Code changes are staged." }),
125
+ " This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
126
+ ] });
127
+ }
128
+ function DevSyncErrorNotice({ error }) {
129
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
130
+ /* @__PURE__ */ jsxs("strong", { children: [
131
+ "Form sync needs attention",
132
+ error.code ? ` (${error.code})` : "",
133
+ "."
134
+ ] }),
135
+ " ",
136
+ error.message,
137
+ " The safe form remains available for development."
138
+ ] });
139
+ }
140
+ function DevChrome(props) {
141
+ const notice = props.devNotices === false ? null : props.syncError ? /* @__PURE__ */ jsx(DevSyncErrorNotice, { error: props.syncError }) : props.staged ? /* @__PURE__ */ jsx(DevStagedNotice, {}) : props.draft ? /* @__PURE__ */ jsx(DevDraftNotice, { warningUrl: props.warningUrl }) : props.noClient ? /* @__PURE__ */ jsx(DevNoClientNotice, {}) : null;
142
+ if (!props.preview && !notice) return null;
143
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
144
+ props.preview && /* @__PURE__ */ jsx(PreviewBadge, {}),
145
+ notice
146
+ ] });
147
+ }
148
+
63
149
  // src/context.ts
64
150
  import { allFields } from "@usefillo/core";
65
151
  import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
66
152
  var FilloContext = createContext2(null);
67
153
  var FilloInstanceIdContext = createContext2(null);
154
+ var FilloChromeContext = createContext2(null);
155
+ function useFilloChrome() {
156
+ return useContext2(FilloChromeContext);
157
+ }
68
158
  function useFilloInstanceId() {
69
159
  const instanceId = useContext2(FilloInstanceIdContext);
70
160
  const fallbackId = useId().replace(/:/g, "");
@@ -127,7 +217,18 @@ function useFilloController(options) {
127
217
  surface: options.surface ?? "headless",
128
218
  // Wrapped so the engine (created once) always calls the latest resolver.
129
219
  resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0,
130
- respondent: options.respondent
220
+ // A getter, not a captured boolean: the dev-chrome gate upgrades right
221
+ // after hydration (build-time snapshot → hostname-aware check), but the
222
+ // engine is created once at mount and reads this at submit time.
223
+ get verboseResolutionErrors() {
224
+ return optsRef.current.verboseResolutionErrors;
225
+ },
226
+ respondent: options.respondent,
227
+ // Challenge wiring read through optsRef so the engine (created once) sees
228
+ // the latest token/reset each submit — the widget solves after mount.
229
+ challengeRequired: options.challengeRequired,
230
+ getChallengeToken: () => optsRef.current.getChallengeToken?.(),
231
+ onChallengeFailed: () => optsRef.current.onChallengeFailed?.()
131
232
  });
132
233
  }
133
234
  const store = storeRef.current;
@@ -158,11 +259,11 @@ function useFilloController(options) {
158
259
  [store, state, options.form, options.formId, options.client]
159
260
  );
160
261
  }
161
- function useDraftFlush(api) {
162
- const enabled = Boolean(api.form.settings.saveProgress);
262
+ function useDraftFlush(api, enabled = true) {
263
+ const active = enabled && Boolean(api.form.settings.saveProgress);
163
264
  const flush = api.flushDraft;
164
265
  useEffect(() => {
165
- if (!enabled || typeof window === "undefined") return;
266
+ if (!active || typeof window === "undefined") return;
166
267
  const onPageHide = () => flush();
167
268
  const onVisibility = () => {
168
269
  if (document.visibilityState === "hidden") flush();
@@ -173,7 +274,7 @@ function useDraftFlush(api) {
173
274
  window.removeEventListener("pagehide", onPageHide);
174
275
  document.removeEventListener("visibilitychange", onVisibility);
175
276
  };
176
- }, [enabled, flush]);
277
+ }, [active, flush]);
177
278
  }
178
279
 
179
280
  // src/define.ts
@@ -224,8 +325,14 @@ function useCodeFormSync(form, client, onError, options) {
224
325
  error: null,
225
326
  noticeError: null,
226
327
  staged: false,
328
+ accepting: void 0,
329
+ acceptingReason: void 0,
330
+ uploadsAvailable: void 0,
227
331
  resolvedSchema: void 0,
228
- resolvedTheme: void 0
332
+ resolvedTheme: void 0,
333
+ warning: void 0,
334
+ warningCode: void 0,
335
+ warningUrl: void 0
229
336
  } : {
230
337
  key: syncKey,
231
338
  client,
@@ -276,14 +383,25 @@ function useCodeFormSync(form, client, onError, options) {
276
383
  formId: r.formId,
277
384
  status: r.status ?? null,
278
385
  branding: r.branding,
386
+ challenge: r.challenge,
279
387
  error: null,
280
388
  noticeError,
281
389
  staged: Boolean(r.staged),
390
+ accepting: r.accepting,
391
+ acceptingReason: r.acceptingReason,
392
+ uploadsAvailable: r.uploadsAvailable,
282
393
  resolvedSchema: r.resolvedSchema,
283
- resolvedTheme: r.resolvedTheme
394
+ resolvedTheme: r.resolvedTheme,
395
+ warning: r.warning,
396
+ warningCode: r.warningCode,
397
+ warningUrl: r.warningUrl
284
398
  });
285
399
  const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
286
- if (r.warning) console.warn(`[fillo] ${r.warning}`);
400
+ if (r.warning) {
401
+ console.warn(
402
+ `[fillo] ${r.warning}${r.warningUrl ? ` Connect storage to publish: ${r.warningUrl}` : ""}`
403
+ );
404
+ }
287
405
  if (noticeError) {
288
406
  console.warn(
289
407
  `[fillo] form sync needs attention (${noticeError.code}): ${noticeError.message}`
@@ -325,12 +443,19 @@ function useCodeFormSync(form, client, onError, options) {
325
443
  formId: synced.formId,
326
444
  status: synced.status,
327
445
  branding: synced.branding,
446
+ challenge: synced.challenge,
328
447
  error: synced.error,
329
448
  noticeError: synced.noticeError,
330
449
  syncing: Boolean(syncKey) && !synced.settled,
331
450
  staged: synced.staged,
451
+ accepting: synced.accepting,
452
+ acceptingReason: synced.acceptingReason,
453
+ uploadsAvailable: synced.uploadsAvailable,
332
454
  resolvedSchema: synced.resolvedSchema,
333
- resolvedTheme: synced.resolvedTheme
455
+ resolvedTheme: synced.resolvedTheme,
456
+ warning: synced.warning,
457
+ warningCode: synced.warningCode,
458
+ warningUrl: synced.warningUrl
334
459
  } : {
335
460
  formId: null,
336
461
  status: null,
@@ -358,7 +483,7 @@ import {
358
483
  toE164
359
484
  } from "@usefillo/core";
360
485
  import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
361
- import { jsx, jsxs } from "react/jsx-runtime";
486
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
362
487
  function localeCountry() {
363
488
  if (typeof navigator === "undefined") return void 0;
364
489
  const tag = navigator.languages?.[0] || navigator.language || "";
@@ -441,15 +566,15 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
441
566
  inputRef.current?.focus();
442
567
  }
443
568
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
444
- return /* @__PURE__ */ jsxs("div", { ...slots.wrapperProps(`fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`), children: [
445
- /* @__PURE__ */ jsxs("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
569
+ return /* @__PURE__ */ jsxs2("div", { ...slots.wrapperProps(`fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`), children: [
570
+ /* @__PURE__ */ jsxs2("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
446
571
  field.label,
447
- !field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: strings.optional })
572
+ !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
448
573
  ] }),
449
- field.description && /* @__PURE__ */ jsx("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
450
- /* @__PURE__ */ jsxs("div", { className: "fillo-phone", children: [
451
- /* @__PURE__ */ jsx(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
452
- /* @__PURE__ */ jsx(
574
+ field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
575
+ /* @__PURE__ */ jsxs2("div", { className: "fillo-phone", children: [
576
+ /* @__PURE__ */ jsx2(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
577
+ /* @__PURE__ */ jsx2(
453
578
  "input",
454
579
  {
455
580
  ref: inputRef,
@@ -468,7 +593,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
468
593
  }
469
594
  )
470
595
  ] }),
471
- error && /* @__PURE__ */ jsx("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
596
+ error && /* @__PURE__ */ jsx2("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
472
597
  ] });
473
598
  }
474
599
  function CountrySelect({
@@ -544,8 +669,8 @@ function CountrySelect({
544
669
  triggerRef.current?.focus();
545
670
  }
546
671
  }
547
- return /* @__PURE__ */ jsxs("div", { className: "fillo-phone-country", ref: rootRef, children: [
548
- /* @__PURE__ */ jsxs(
672
+ return /* @__PURE__ */ jsxs2("div", { className: "fillo-phone-country", ref: rootRef, children: [
673
+ /* @__PURE__ */ jsxs2(
549
674
  "button",
550
675
  {
551
676
  ref: triggerRef,
@@ -557,17 +682,17 @@ function CountrySelect({
557
682
  "aria-label": `Country: ${country.name} (+${country.dialCode})`,
558
683
  onClick: () => setOpen((o) => !o),
559
684
  children: [
560
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(country.iso2) }),
561
- /* @__PURE__ */ jsxs("span", { className: "fillo-phone-dial", children: [
685
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(country.iso2) }),
686
+ /* @__PURE__ */ jsxs2("span", { className: "fillo-phone-dial", children: [
562
687
  "+",
563
688
  country.dialCode
564
689
  ] }),
565
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-caret", "aria-hidden": "true", children: "\u25BE" })
690
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-caret", "aria-hidden": "true", children: "\u25BE" })
566
691
  ]
567
692
  }
568
693
  ),
569
- open && /* @__PURE__ */ jsxs("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
570
- /* @__PURE__ */ jsx(
694
+ open && /* @__PURE__ */ jsxs2("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
695
+ /* @__PURE__ */ jsx2(
571
696
  "input",
572
697
  {
573
698
  ref: searchRef,
@@ -588,8 +713,8 @@ function CountrySelect({
588
713
  onKeyDown: onSearchKey
589
714
  }
590
715
  ),
591
- /* @__PURE__ */ jsxs("ul", { className: "fillo-phone-list", role: "listbox", id: listId, "aria-label": "Country", children: [
592
- matches.map((c, i) => /* @__PURE__ */ jsxs(
716
+ /* @__PURE__ */ jsxs2("ul", { className: "fillo-phone-list", role: "listbox", id: listId, "aria-label": "Country", children: [
717
+ matches.map((c, i) => /* @__PURE__ */ jsxs2(
593
718
  "li",
594
719
  {
595
720
  id: `${listId}-opt-${i}`,
@@ -602,9 +727,9 @@ function CountrySelect({
602
727
  choose(c);
603
728
  },
604
729
  children: [
605
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(c.iso2) }),
606
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-option-name", children: c.name }),
607
- /* @__PURE__ */ jsxs("span", { className: "fillo-phone-option-dial", children: [
730
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(c.iso2) }),
731
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-option-name", children: c.name }),
732
+ /* @__PURE__ */ jsxs2("span", { className: "fillo-phone-option-dial", children: [
608
733
  "+",
609
734
  c.dialCode
610
735
  ] })
@@ -612,7 +737,7 @@ function CountrySelect({
612
737
  },
613
738
  c.iso2
614
739
  )),
615
- matches.length === 0 && /* @__PURE__ */ jsx("li", { className: "fillo-phone-empty", children: "No matches" })
740
+ matches.length === 0 && /* @__PURE__ */ jsx2("li", { className: "fillo-phone-empty", children: "No matches" })
616
741
  ] })
617
742
  ] })
618
743
  ] });
@@ -620,7 +745,7 @@ function CountrySelect({
620
745
 
621
746
  // src/signature.tsx
622
747
  import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
623
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
748
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
624
749
  function SignatureField({ field, value, error, setValue, ids: providedIds }) {
625
750
  const ids = useFilloFieldIds(field.id, providedIds);
626
751
  const slots = useFieldSlots(field, error);
@@ -725,21 +850,21 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
725
850
  setValue(null);
726
851
  }
727
852
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
728
- return /* @__PURE__ */ jsxs2("div", { ...slots.wrapperProps(`fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`), children: [
729
- /* @__PURE__ */ jsxs2("span", { className: slots.label, "data-fillo": "label", id: ids.labelId, children: [
853
+ return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`), children: [
854
+ /* @__PURE__ */ jsxs3("span", { className: slots.label, "data-fillo": "label", id: ids.labelId, children: [
730
855
  field.label,
731
- !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
856
+ !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
732
857
  ] }),
733
- field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
734
- /* @__PURE__ */ jsxs2(
858
+ field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
859
+ /* @__PURE__ */ jsxs3(
735
860
  "div",
736
861
  {
737
862
  role: "group",
738
863
  "aria-labelledby": ids.labelId,
739
864
  "aria-describedby": describedBy,
740
865
  children: [
741
- /* @__PURE__ */ jsxs2("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
742
- /* @__PURE__ */ jsx2(
866
+ /* @__PURE__ */ jsxs3("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
867
+ /* @__PURE__ */ jsx3(
743
868
  "canvas",
744
869
  {
745
870
  ref: canvasRef,
@@ -752,12 +877,12 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
752
877
  onPointerLeave: end
753
878
  }
754
879
  ),
755
- hasInk && /* @__PURE__ */ jsx2("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
756
- !hasInk && /* @__PURE__ */ jsx2("span", { className: "fillo-signature-hint", "aria-hidden": "true", children: "Sign here" })
880
+ hasInk && /* @__PURE__ */ jsx3("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
881
+ !hasInk && /* @__PURE__ */ jsx3("span", { className: "fillo-signature-hint", "aria-hidden": "true", children: "Sign here" })
757
882
  ] }),
758
- /* @__PURE__ */ jsxs2("div", { className: "fillo-signature-type", children: [
759
- /* @__PURE__ */ jsx2("label", { className: "fillo-signature-type-label", htmlFor: typedInputId, children: "Or type your full name to sign" }),
760
- /* @__PURE__ */ jsx2(
883
+ /* @__PURE__ */ jsxs3("div", { className: "fillo-signature-type", children: [
884
+ /* @__PURE__ */ jsx3("label", { className: "fillo-signature-type-label", htmlFor: typedInputId, children: "Or type your full name to sign" }),
885
+ /* @__PURE__ */ jsx3(
761
886
  "input",
762
887
  {
763
888
  id: typedInputId,
@@ -779,13 +904,13 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
779
904
  ]
780
905
  }
781
906
  ),
782
- error && /* @__PURE__ */ jsx2("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
907
+ error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
783
908
  ] });
784
909
  }
785
910
 
786
911
  // src/upload.tsx
787
912
  import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
788
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
913
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
789
914
  function uploadStatus(inFlight, doneCount, strings) {
790
915
  const failed = inFlight.filter((f) => f.error).length;
791
916
  const active = inFlight.length - failed;
@@ -805,6 +930,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
805
930
  const ids = useFilloFieldIds(field.id, providedIds);
806
931
  const slots = useFieldSlots(field, error);
807
932
  const strings = useStrings();
933
+ const chrome = useFilloChrome();
808
934
  const schema = field;
809
935
  const inputRef = useRef5(null);
810
936
  const [inFlight, setInFlight] = useState4([]);
@@ -828,7 +954,9 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
828
954
  const maxFiles = schema.maxFiles ?? 1;
829
955
  const maxMb = schema.maxFileSizeMb ?? 500;
830
956
  const maxBytes = maxMb * 1024 * 1024;
831
- const canUpload = Boolean(api.client && api.formId);
957
+ const storageBlocked = chrome?.uploadsAvailable === false;
958
+ const storageUrl = storageBlocked && chrome?.devChrome ? safeHttpUrl(chrome.warningUrl) : null;
959
+ const canUpload = Boolean(api.client && api.formId) && !storageBlocked;
832
960
  const activeInFlight = inFlight.filter((f) => !f.error).length;
833
961
  const remaining = maxFiles - files.length - activeInFlight;
834
962
  async function startUpload(file) {
@@ -890,13 +1018,13 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
890
1018
  setValue(next);
891
1019
  }
892
1020
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
893
- return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
894
- /* @__PURE__ */ jsxs3("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1021
+ return /* @__PURE__ */ jsxs4("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
1022
+ /* @__PURE__ */ jsxs4("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
895
1023
  field.label,
896
- !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
1024
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
897
1025
  ] }),
898
- field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
899
- remaining > 0 && /* @__PURE__ */ jsxs3(
1026
+ field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1027
+ remaining > 0 && /* @__PURE__ */ jsxs4(
900
1028
  "div",
901
1029
  {
902
1030
  "data-fillo": "control",
@@ -927,7 +1055,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
927
1055
  }
928
1056
  },
929
1057
  children: [
930
- /* @__PURE__ */ jsx3(
1058
+ /* @__PURE__ */ jsx4(
931
1059
  "input",
932
1060
  {
933
1061
  ref: inputRef,
@@ -940,14 +1068,20 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
940
1068
  onChange: (e) => canUpload && handleFiles(e.target.files)
941
1069
  }
942
1070
  ),
943
- canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
944
- /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
945
- /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
946
- ] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.uploadsDisabled })
1071
+ canUpload ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1072
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
1073
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
1074
+ ] }) : storageBlocked && chrome?.devChrome ? /* @__PURE__ */ jsxs4("span", { className: "fillo-dropzone-hint", children: [
1075
+ "Connect file storage to enable uploads",
1076
+ storageUrl && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1077
+ " \u2014 ",
1078
+ /* @__PURE__ */ jsx4("a", { href: storageUrl, target: "_blank", rel: "noopener noreferrer", children: storageUrl })
1079
+ ] })
1080
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: storageBlocked ? strings.uploadsUnavailable : strings.uploadsDisabled })
947
1081
  ]
948
1082
  }
949
1083
  ),
950
- /* @__PURE__ */ jsx3(
1084
+ /* @__PURE__ */ jsx4(
951
1085
  "div",
952
1086
  {
953
1087
  role: "status",
@@ -957,11 +1091,11 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
957
1091
  children: uploadStatus(inFlight, files.length, strings)
958
1092
  }
959
1093
  ),
960
- (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
961
- files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
962
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
963
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
964
- /* @__PURE__ */ jsx3(
1094
+ (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs4("ul", { className: "fillo-files", children: [
1095
+ files.map((f) => /* @__PURE__ */ jsxs4("li", { className: "fillo-file fillo-file--done", children: [
1096
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1097
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
1098
+ /* @__PURE__ */ jsx4(
965
1099
  "button",
966
1100
  {
967
1101
  type: "button",
@@ -972,7 +1106,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
972
1106
  }
973
1107
  )
974
1108
  ] }, f.fileId)),
975
- inFlight.map((f) => /* @__PURE__ */ jsxs3(
1109
+ inFlight.map((f) => /* @__PURE__ */ jsxs4(
976
1110
  "li",
977
1111
  {
978
1112
  className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
@@ -984,10 +1118,10 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
984
1118
  "aria-valuenow": Math.round(f.fraction * 100)
985
1119
  },
986
1120
  children: [
987
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
988
- f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
989
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
990
- f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx3(
1121
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1122
+ f.error ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1123
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: f.error }),
1124
+ f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx4(
991
1125
  "button",
992
1126
  {
993
1127
  type: "button",
@@ -1002,7 +1136,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1002
1136
  children: strings.uploadRetry
1003
1137
  }
1004
1138
  ),
1005
- /* @__PURE__ */ jsx3(
1139
+ /* @__PURE__ */ jsx4(
1006
1140
  "button",
1007
1141
  {
1008
1142
  type: "button",
@@ -1012,14 +1146,14 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1012
1146
  children: "\xD7"
1013
1147
  }
1014
1148
  )
1015
- ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
1016
- /* @__PURE__ */ jsxs3("span", { className: "fillo-file-meta", children: [
1149
+ ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1150
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-meta", children: [
1017
1151
  Math.round(f.fraction * 100),
1018
1152
  "% of ",
1019
1153
  formatBytes(f.size)
1020
1154
  ] }),
1021
- /* @__PURE__ */ jsx3("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
1022
- /* @__PURE__ */ jsx3(
1155
+ /* @__PURE__ */ jsx4("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
1156
+ /* @__PURE__ */ jsx4(
1023
1157
  "button",
1024
1158
  {
1025
1159
  type: "button",
@@ -1035,12 +1169,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1035
1169
  f.key
1036
1170
  ))
1037
1171
  ] }),
1038
- error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1172
+ error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1039
1173
  ] });
1040
1174
  }
1041
1175
 
1042
1176
  // src/fields.tsx
1043
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1177
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1044
1178
  function fieldAria(field, error, ids) {
1045
1179
  const describedBy = [
1046
1180
  field.description ? ids.descriptionId : null,
@@ -1087,7 +1221,7 @@ function FieldShell({
1087
1221
  const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
1088
1222
  const errorCls = useSlotClass("fillo-error", { slot: "error", ...state });
1089
1223
  const strings = useStrings();
1090
- return /* @__PURE__ */ jsxs4(
1224
+ return /* @__PURE__ */ jsxs5(
1091
1225
  "div",
1092
1226
  {
1093
1227
  className: fieldCls,
@@ -1097,13 +1231,13 @@ function FieldShell({
1097
1231
  "data-invalid": error ? "" : void 0,
1098
1232
  "data-required": field.required ? "" : void 0,
1099
1233
  children: [
1100
- /* @__PURE__ */ jsxs4("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1234
+ /* @__PURE__ */ jsxs5("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1101
1235
  field.label,
1102
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1236
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1103
1237
  ] }),
1104
- field.description && /* @__PURE__ */ jsx4("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1238
+ field.description && /* @__PURE__ */ jsx5("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1105
1239
  children,
1106
- error && /* @__PURE__ */ jsx4("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1240
+ error && /* @__PURE__ */ jsx5("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1107
1241
  ]
1108
1242
  }
1109
1243
  );
@@ -1112,7 +1246,7 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
1112
1246
  const ids = useFilloFieldIds(field.id, providedIds);
1113
1247
  const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
1114
1248
  const control = useControl("fillo-input", field, error);
1115
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1249
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1116
1250
  "input",
1117
1251
  {
1118
1252
  id: ids.inputId,
@@ -1131,7 +1265,7 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
1131
1265
  function LongText({ field, value, error, setValue, ids: providedIds }) {
1132
1266
  const ids = useFilloFieldIds(field.id, providedIds);
1133
1267
  const control = useControl("fillo-input fillo-textarea", field, error);
1134
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1268
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1135
1269
  "textarea",
1136
1270
  {
1137
1271
  id: ids.inputId,
@@ -1168,7 +1302,7 @@ var OPTION_ICON_PATHS = {
1168
1302
  };
1169
1303
  function ChoiceIcon({ icon }) {
1170
1304
  if (!icon) return null;
1171
- return /* @__PURE__ */ jsx4(
1305
+ return /* @__PURE__ */ jsx5(
1172
1306
  "svg",
1173
1307
  {
1174
1308
  className: "fillo-option-icon",
@@ -1179,7 +1313,7 @@ function ChoiceIcon({ icon }) {
1179
1313
  strokeLinecap: "round",
1180
1314
  strokeLinejoin: "round",
1181
1315
  "aria-hidden": "true",
1182
- children: /* @__PURE__ */ jsx4("path", { d: OPTION_ICON_PATHS[icon] })
1316
+ children: /* @__PURE__ */ jsx5("path", { d: OPTION_ICON_PATHS[icon] })
1183
1317
  }
1184
1318
  );
1185
1319
  }
@@ -1193,7 +1327,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1193
1327
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
1194
1328
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1195
1329
  const otherActive = isOtherValue || value === "" && otherOn;
1196
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
1330
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs5(
1197
1331
  "div",
1198
1332
  {
1199
1333
  className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
@@ -1202,7 +1336,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1202
1336
  "aria-labelledby": ids.labelId,
1203
1337
  ...fieldAria(field, error, ids),
1204
1338
  children: [
1205
- options.map((opt) => /* @__PURE__ */ jsxs4(
1339
+ options.map((opt) => /* @__PURE__ */ jsxs5(
1206
1340
  "label",
1207
1341
  {
1208
1342
  "data-option": opt.id,
@@ -1220,7 +1354,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1220
1354
  setValue(opt.id);
1221
1355
  },
1222
1356
  children: [
1223
- /* @__PURE__ */ jsx4(
1357
+ /* @__PURE__ */ jsx5(
1224
1358
  "input",
1225
1359
  {
1226
1360
  type: "radio",
@@ -1233,15 +1367,15 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1233
1367
  }
1234
1368
  }
1235
1369
  ),
1236
- /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
1237
- /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1370
+ /* @__PURE__ */ jsx5(ChoiceIcon, { icon: opt.icon }),
1371
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1238
1372
  ]
1239
1373
  },
1240
1374
  opt.id
1241
1375
  )),
1242
1376
  choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
1243
1377
  // toggle the radio. The radio + "Other" text keep their own <label>.
1244
- /* @__PURE__ */ jsxs4(
1378
+ /* @__PURE__ */ jsxs5(
1245
1379
  "div",
1246
1380
  {
1247
1381
  "data-fillo": "option",
@@ -1252,8 +1386,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1252
1386
  otherActive
1253
1387
  ),
1254
1388
  children: [
1255
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1256
- /* @__PURE__ */ jsx4(
1389
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-option-main", children: [
1390
+ /* @__PURE__ */ jsx5(
1257
1391
  "input",
1258
1392
  {
1259
1393
  type: "radio",
@@ -1266,9 +1400,9 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1266
1400
  }
1267
1401
  }
1268
1402
  ),
1269
- /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1403
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1270
1404
  ] }),
1271
- otherActive && /* @__PURE__ */ jsx4(
1405
+ otherActive && /* @__PURE__ */ jsx5(
1272
1406
  "input",
1273
1407
  {
1274
1408
  type: "text",
@@ -1300,7 +1434,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1300
1434
  const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
1301
1435
  const appearance = useFilloAppearance();
1302
1436
  const optionCls = (base, optionId, isSelected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected: isSelected });
1303
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
1437
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs5(
1304
1438
  "div",
1305
1439
  {
1306
1440
  className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
@@ -1309,7 +1443,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1309
1443
  "aria-labelledby": ids.labelId,
1310
1444
  ...fieldAria(field, error, ids),
1311
1445
  children: [
1312
- options.map((opt) => /* @__PURE__ */ jsxs4(
1446
+ options.map((opt) => /* @__PURE__ */ jsxs5(
1313
1447
  "label",
1314
1448
  {
1315
1449
  "data-option": opt.id,
@@ -1326,7 +1460,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1326
1460
  toggle(opt.id);
1327
1461
  },
1328
1462
  children: [
1329
- /* @__PURE__ */ jsx4(
1463
+ /* @__PURE__ */ jsx5(
1330
1464
  "input",
1331
1465
  {
1332
1466
  type: "checkbox",
@@ -1335,15 +1469,15 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1335
1469
  onChange: () => toggle(opt.id)
1336
1470
  }
1337
1471
  ),
1338
- /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
1339
- /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1472
+ /* @__PURE__ */ jsx5(ChoiceIcon, { icon: opt.icon }),
1473
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1340
1474
  ]
1341
1475
  },
1342
1476
  opt.id
1343
1477
  )),
1344
1478
  choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
1345
1479
  // toggle the checkbox. The checkbox + "Other" text keep their own <label>.
1346
- /* @__PURE__ */ jsxs4(
1480
+ /* @__PURE__ */ jsxs5(
1347
1481
  "div",
1348
1482
  {
1349
1483
  "data-fillo": "option",
@@ -1354,8 +1488,8 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1354
1488
  otherActive
1355
1489
  ),
1356
1490
  children: [
1357
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1358
- /* @__PURE__ */ jsx4(
1491
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-option-main", children: [
1492
+ /* @__PURE__ */ jsx5(
1359
1493
  "input",
1360
1494
  {
1361
1495
  type: "checkbox",
@@ -1371,9 +1505,9 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1371
1505
  }
1372
1506
  }
1373
1507
  ),
1374
- /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1508
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1375
1509
  ] }),
1376
- otherActive && /* @__PURE__ */ jsx4(
1510
+ otherActive && /* @__PURE__ */ jsx5(
1377
1511
  "input",
1378
1512
  {
1379
1513
  type: "text",
@@ -1411,9 +1545,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1411
1545
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1412
1546
  const otherActive = isOtherValue || value === "" && otherOn;
1413
1547
  const control = useControl("fillo-input fillo-select", field, error);
1414
- return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1415
- /* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
1416
- /* @__PURE__ */ jsxs4(
1548
+ return /* @__PURE__ */ jsxs5(FieldShell, { field, error, ids, children: [
1549
+ /* @__PURE__ */ jsxs5("div", { className: "fillo-select-wrap", children: [
1550
+ /* @__PURE__ */ jsxs5(
1417
1551
  "select",
1418
1552
  {
1419
1553
  id: ids.inputId,
@@ -1430,15 +1564,15 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1430
1564
  }
1431
1565
  },
1432
1566
  children: [
1433
- /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1434
- options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
1435
- choice.allowOther && /* @__PURE__ */ jsx4("option", { value: otherValue, children: `${strings.other}\u2026` })
1567
+ /* @__PURE__ */ jsx5("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1568
+ options.map((opt) => /* @__PURE__ */ jsx5("option", { value: opt.id, children: opt.label }, opt.id)),
1569
+ choice.allowOther && /* @__PURE__ */ jsx5("option", { value: otherValue, children: `${strings.other}\u2026` })
1436
1570
  ]
1437
1571
  }
1438
1572
  ),
1439
- /* @__PURE__ */ jsx4("span", { className: "fillo-select-icon", "aria-hidden": "true" })
1573
+ /* @__PURE__ */ jsx5("span", { className: "fillo-select-icon", "aria-hidden": "true" })
1440
1574
  ] }),
1441
- otherActive && /* @__PURE__ */ jsx4(
1575
+ otherActive && /* @__PURE__ */ jsx5(
1442
1576
  "input",
1443
1577
  {
1444
1578
  type: "text",
@@ -1459,19 +1593,19 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1459
1593
  const slots = useFieldSlots(field, error);
1460
1594
  const checkbox = field;
1461
1595
  if (checkbox.appearance === "toggle") {
1462
- return /* @__PURE__ */ jsxs4(
1596
+ return /* @__PURE__ */ jsxs5(
1463
1597
  "div",
1464
1598
  {
1465
1599
  ...slots.wrapperProps(
1466
1600
  `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`
1467
1601
  ),
1468
1602
  children: [
1469
- /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1470
- /* @__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: [
1603
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1604
+ /* @__PURE__ */ jsx5("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1471
1605
  field.label,
1472
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1606
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1473
1607
  ] }) }),
1474
- /* @__PURE__ */ jsx4(
1608
+ /* @__PURE__ */ jsx5(
1475
1609
  "input",
1476
1610
  {
1477
1611
  id: ids.inputId,
@@ -1484,23 +1618,23 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1484
1618
  ...fieldAria(field, error, ids)
1485
1619
  }
1486
1620
  ),
1487
- /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
1621
+ /* @__PURE__ */ jsx5("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx5("span", { className: "fillo-toggle-thumb" }) })
1488
1622
  ] }),
1489
- field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1490
- error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1623
+ field.description && /* @__PURE__ */ jsx5("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1624
+ error && /* @__PURE__ */ jsx5("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1491
1625
  ]
1492
1626
  }
1493
1627
  );
1494
1628
  }
1495
- return /* @__PURE__ */ jsxs4(
1629
+ return /* @__PURE__ */ jsxs5(
1496
1630
  "div",
1497
1631
  {
1498
1632
  ...slots.wrapperProps(
1499
1633
  `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`
1500
1634
  ),
1501
1635
  children: [
1502
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1503
- /* @__PURE__ */ jsx4(
1636
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1637
+ /* @__PURE__ */ jsx5(
1504
1638
  "input",
1505
1639
  {
1506
1640
  id: ids.inputId,
@@ -1511,13 +1645,13 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1511
1645
  ...fieldAria(field, error, ids)
1512
1646
  }
1513
1647
  ),
1514
- /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1648
+ /* @__PURE__ */ jsxs5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1515
1649
  field.label,
1516
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1650
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1517
1651
  ] })
1518
1652
  ] }),
1519
- field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1520
- error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1653
+ field.description && /* @__PURE__ */ jsx5("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1654
+ error && /* @__PURE__ */ jsx5("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1521
1655
  ]
1522
1656
  }
1523
1657
  );
@@ -1556,7 +1690,7 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1556
1690
  const tabbable = rovingIndex(steps, selected);
1557
1691
  const appearance = useFilloAppearance();
1558
1692
  const control = useControl("fillo-rating", field, error);
1559
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1693
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1560
1694
  "div",
1561
1695
  {
1562
1696
  ...control,
@@ -1564,7 +1698,7 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1564
1698
  "aria-labelledby": ids.labelId,
1565
1699
  ...fieldAria(field, error, ids),
1566
1700
  onKeyDown: (e) => radiogroupKeyDown(e, steps, selected, (n) => setValue(n)),
1567
- children: steps.map((n, i) => /* @__PURE__ */ jsx4(
1701
+ children: steps.map((n, i) => /* @__PURE__ */ jsx5(
1568
1702
  "button",
1569
1703
  {
1570
1704
  type: "button",
@@ -1607,8 +1741,8 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1607
1741
  ].filter(Boolean).join(" ") || void 0;
1608
1742
  const appearance = useFilloAppearance();
1609
1743
  const control = useControl("fillo-scale", field, error);
1610
- return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1611
- /* @__PURE__ */ jsx4(
1744
+ return /* @__PURE__ */ jsxs5(FieldShell, { field, error, ids, children: [
1745
+ /* @__PURE__ */ jsx5(
1612
1746
  "div",
1613
1747
  {
1614
1748
  ...control,
@@ -1618,7 +1752,7 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1618
1752
  "aria-required": field.required ? true : void 0,
1619
1753
  "aria-describedby": describedBy,
1620
1754
  onKeyDown: (e) => radiogroupKeyDown(e, steps, current, (n) => setValue(n)),
1621
- children: steps.map((n, i) => /* @__PURE__ */ jsx4(
1755
+ children: steps.map((n, i) => /* @__PURE__ */ jsx5(
1622
1756
  "button",
1623
1757
  {
1624
1758
  type: "button",
@@ -1641,9 +1775,9 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1641
1775
  ))
1642
1776
  }
1643
1777
  ),
1644
- hasLabels && /* @__PURE__ */ jsxs4("div", { className: "fillo-scale-labels", children: [
1645
- /* @__PURE__ */ jsx4("span", { id: minLabelId, children: scale.minLabel }),
1646
- /* @__PURE__ */ jsx4("span", { id: maxLabelId, children: scale.maxLabel })
1778
+ hasLabels && /* @__PURE__ */ jsxs5("div", { className: "fillo-scale-labels", children: [
1779
+ /* @__PURE__ */ jsx5("span", { id: minLabelId, children: scale.minLabel }),
1780
+ /* @__PURE__ */ jsx5("span", { id: maxLabelId, children: scale.maxLabel })
1647
1781
  ] })
1648
1782
  ] });
1649
1783
  }
@@ -1663,21 +1797,21 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
1663
1797
  [next[index], next[target]] = [next[target], next[index]];
1664
1798
  setValue(next);
1665
1799
  }
1666
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1800
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1667
1801
  "div",
1668
1802
  {
1669
1803
  id: ids.inputId,
1670
1804
  role: "group",
1671
1805
  "aria-labelledby": ids.labelId,
1672
1806
  ...fieldAria(field, error, ids),
1673
- children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1807
+ children: /* @__PURE__ */ jsx5("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1674
1808
  const option = ranking.options.find((o) => o.id === id);
1675
1809
  if (!option) return null;
1676
- return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
1677
- /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
1678
- /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
1679
- /* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
1680
- /* @__PURE__ */ jsx4(
1810
+ return /* @__PURE__ */ jsxs5("li", { className: "fillo-ranking-item", children: [
1811
+ /* @__PURE__ */ jsx5("span", { className: "fillo-ranking-index", children: index + 1 }),
1812
+ /* @__PURE__ */ jsx5("span", { className: "fillo-ranking-label", children: option.label }),
1813
+ /* @__PURE__ */ jsxs5("span", { className: "fillo-ranking-controls", children: [
1814
+ /* @__PURE__ */ jsx5(
1681
1815
  "button",
1682
1816
  {
1683
1817
  type: "button",
@@ -1688,7 +1822,7 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
1688
1822
  children: "\u2191"
1689
1823
  }
1690
1824
  ),
1691
- /* @__PURE__ */ jsx4(
1825
+ /* @__PURE__ */ jsx5(
1692
1826
  "button",
1693
1827
  {
1694
1828
  type: "button",
@@ -1709,7 +1843,7 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1709
1843
  const ids = useFilloFieldIds(field.id, providedIds);
1710
1844
  const matrix = field;
1711
1845
  const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
1712
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1846
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1713
1847
  "div",
1714
1848
  {
1715
1849
  id: ids.inputId,
@@ -1717,12 +1851,12 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1717
1851
  role: "group",
1718
1852
  "aria-labelledby": ids.labelId,
1719
1853
  ...fieldAria(field, error, ids),
1720
- children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
1721
- /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
1722
- /* @__PURE__ */ jsx4("th", {}),
1723
- matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
1854
+ children: /* @__PURE__ */ jsxs5("table", { className: "fillo-matrix", children: [
1855
+ /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
1856
+ /* @__PURE__ */ jsx5("th", {}),
1857
+ matrix.columns.map((col) => /* @__PURE__ */ jsx5("th", { scope: "col", children: col.label }, col.id))
1724
1858
  ] }) }),
1725
- /* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
1859
+ /* @__PURE__ */ jsx5("tbody", { children: matrix.rows.map((row) => {
1726
1860
  const rowLabelId = `${ids.inputId}-${row.id}`;
1727
1861
  const answered = answers[row.id] !== void 0;
1728
1862
  const rowInvalid = Boolean(error) && field.required && !answered;
@@ -1733,17 +1867,17 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1733
1867
  // radiogroup lives on an inner element that aria-owns the radios
1734
1868
  // scattered across the cells (they can't be DOM-nested inside a
1735
1869
  // single element within a table row).
1736
- /* @__PURE__ */ jsxs4("tr", { children: [
1737
- /* @__PURE__ */ jsx4("th", { scope: "row", children: /* @__PURE__ */ jsx4(
1870
+ /* @__PURE__ */ jsxs5("tr", { children: [
1871
+ /* @__PURE__ */ jsx5("th", { scope: "row", children: /* @__PURE__ */ jsx5(
1738
1872
  "div",
1739
1873
  {
1740
1874
  role: "radiogroup",
1741
1875
  "aria-labelledby": rowLabelId,
1742
1876
  "aria-owns": matrix.columns.map(radioId).join(" "),
1743
- children: /* @__PURE__ */ jsx4("span", { id: rowLabelId, children: row.label })
1877
+ children: /* @__PURE__ */ jsx5("span", { id: rowLabelId, children: row.label })
1744
1878
  }
1745
1879
  ) }),
1746
- matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
1880
+ matrix.columns.map((col) => /* @__PURE__ */ jsx5("td", { children: /* @__PURE__ */ jsx5(
1747
1881
  "input",
1748
1882
  {
1749
1883
  id: radioId(col),
@@ -1790,11 +1924,11 @@ var DEFAULT_COMPONENTS = {
1790
1924
  function ContentRenderer({ block }) {
1791
1925
  switch (block.kind) {
1792
1926
  case "heading":
1793
- return /* @__PURE__ */ jsx4("h3", { className: "fillo-heading", children: block.text });
1927
+ return /* @__PURE__ */ jsx5("h3", { className: "fillo-heading", children: block.text });
1794
1928
  case "paragraph":
1795
- return /* @__PURE__ */ jsx4("p", { className: "fillo-paragraph", children: block.text });
1929
+ return /* @__PURE__ */ jsx5("p", { className: "fillo-paragraph", children: block.text });
1796
1930
  case "divider":
1797
- return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
1931
+ return /* @__PURE__ */ jsx5("hr", { className: "fillo-divider" });
1798
1932
  }
1799
1933
  }
1800
1934
  function blockHasPiping(block) {
@@ -1833,7 +1967,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1833
1967
  block = pipeBlock(block, api.data, api.form);
1834
1968
  const ids = useFilloFieldIds(block.id);
1835
1969
  const strings = useStrings();
1836
- if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
1970
+ if (!isField(block)) return /* @__PURE__ */ jsx5(ContentRenderer, { block });
1837
1971
  let Component;
1838
1972
  if (block.kind === "custom") {
1839
1973
  Component = customComponents?.[block.component] ?? components?.custom;
@@ -1851,7 +1985,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1851
1985
  }
1852
1986
  const rawError = api.errors[block.id];
1853
1987
  const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
1854
- return /* @__PURE__ */ jsx4(
1988
+ return /* @__PURE__ */ jsx5(
1855
1989
  Component,
1856
1990
  {
1857
1991
  field: block,
@@ -1888,7 +2022,7 @@ function FormField({
1888
2022
  const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
1889
2023
  if (!block) return null;
1890
2024
  if (isField(block) && !visibleIds.has(block.id)) return null;
1891
- return /* @__PURE__ */ jsx4(
2025
+ return /* @__PURE__ */ jsx5(
1892
2026
  BlockRenderer,
1893
2027
  {
1894
2028
  block,
@@ -1899,8 +2033,127 @@ function FormField({
1899
2033
  );
1900
2034
  }
1901
2035
 
2036
+ // src/turnstile.tsx
2037
+ import { useEffect as useEffect7, useRef as useRef6 } from "react";
2038
+ import { jsx as jsx6 } from "react/jsx-runtime";
2039
+ var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
2040
+ var SCRIPT_ID = "fillo-turnstile-script";
2041
+ var scriptPromise = null;
2042
+ function turnstileGlobal() {
2043
+ return globalThis.turnstile;
2044
+ }
2045
+ function loadTurnstileScript() {
2046
+ if (typeof window === "undefined" || typeof document === "undefined") {
2047
+ return Promise.reject(new Error("Turnstile needs a browser"));
2048
+ }
2049
+ if (turnstileGlobal()) return Promise.resolve();
2050
+ if (scriptPromise) return scriptPromise;
2051
+ scriptPromise = new Promise((resolve, reject) => {
2052
+ const onReady = () => {
2053
+ if (turnstileGlobal()) resolve();
2054
+ else reject(new Error("Turnstile loaded without its global"));
2055
+ };
2056
+ const existing = document.getElementById(SCRIPT_ID);
2057
+ if (existing) {
2058
+ if (turnstileGlobal()) {
2059
+ resolve();
2060
+ return;
2061
+ }
2062
+ existing.addEventListener("load", onReady, { once: true });
2063
+ existing.addEventListener(
2064
+ "error",
2065
+ () => reject(new Error("Turnstile script failed to load")),
2066
+ { once: true }
2067
+ );
2068
+ return;
2069
+ }
2070
+ const script = document.createElement("script");
2071
+ script.id = SCRIPT_ID;
2072
+ script.src = SCRIPT_SRC;
2073
+ script.async = true;
2074
+ script.defer = true;
2075
+ script.addEventListener("load", onReady, { once: true });
2076
+ script.addEventListener(
2077
+ "error",
2078
+ () => {
2079
+ script.remove();
2080
+ scriptPromise = null;
2081
+ reject(new Error("Turnstile script failed to load"));
2082
+ },
2083
+ { once: true }
2084
+ );
2085
+ document.head.appendChild(script);
2086
+ });
2087
+ return scriptPromise;
2088
+ }
2089
+ function TurnstileWidget({
2090
+ siteKey,
2091
+ onToken,
2092
+ onError,
2093
+ registerReset,
2094
+ className
2095
+ }) {
2096
+ const containerRef = useRef6(null);
2097
+ const widgetIdRef = useRef6(null);
2098
+ const onTokenRef = useRef6(onToken);
2099
+ onTokenRef.current = onToken;
2100
+ const onErrorRef = useRef6(onError);
2101
+ onErrorRef.current = onError;
2102
+ const registerResetRef = useRef6(registerReset);
2103
+ registerResetRef.current = registerReset;
2104
+ useEffect7(() => {
2105
+ let cancelled = false;
2106
+ loadTurnstileScript().then(() => {
2107
+ if (cancelled || !containerRef.current) return;
2108
+ const api = turnstileGlobal();
2109
+ if (!api) {
2110
+ onErrorRef.current?.();
2111
+ return;
2112
+ }
2113
+ const clearAndReset = () => {
2114
+ onTokenRef.current(void 0);
2115
+ if (widgetIdRef.current) api.reset(widgetIdRef.current);
2116
+ };
2117
+ widgetIdRef.current = api.render(containerRef.current, {
2118
+ sitekey: siteKey,
2119
+ theme: "auto",
2120
+ callback: (token) => onTokenRef.current(token),
2121
+ "error-callback": () => {
2122
+ onTokenRef.current(void 0);
2123
+ onErrorRef.current?.();
2124
+ },
2125
+ "expired-callback": clearAndReset,
2126
+ "timeout-callback": clearAndReset
2127
+ });
2128
+ registerResetRef.current?.(clearAndReset);
2129
+ }).catch(() => {
2130
+ if (!cancelled) onErrorRef.current?.();
2131
+ });
2132
+ return () => {
2133
+ cancelled = true;
2134
+ const api = turnstileGlobal();
2135
+ if (widgetIdRef.current && api) {
2136
+ try {
2137
+ api.remove(widgetIdRef.current);
2138
+ } catch {
2139
+ }
2140
+ }
2141
+ widgetIdRef.current = null;
2142
+ onTokenRef.current(void 0);
2143
+ };
2144
+ }, [siteKey]);
2145
+ return /* @__PURE__ */ jsx6(
2146
+ "div",
2147
+ {
2148
+ ref: containerRef,
2149
+ className,
2150
+ "data-fillo": "turnstile"
2151
+ }
2152
+ );
2153
+ }
2154
+
1902
2155
  // src/FilloForm.tsx
1903
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2156
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1904
2157
  function themeProps(theme) {
1905
2158
  const safeTheme = normalizeFormTheme(theme);
1906
2159
  if (!safeTheme) return {};
@@ -1917,16 +2170,6 @@ function themeProps(theme) {
1917
2170
  ...hasColorTheme ? { "data-fillo-has-color-theme": "" } : {}
1918
2171
  };
1919
2172
  }
1920
- function safeHttpUrl(href) {
1921
- try {
1922
- if (!href) return null;
1923
- const url = new URL(href);
1924
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
1925
- return url.toString();
1926
- } catch {
1927
- return null;
1928
- }
1929
- }
1930
2173
  function badgeUrl(href) {
1931
2174
  const url = new URL(safeHttpUrl(href) ?? "https://fillo.so");
1932
2175
  url.searchParams.set("utm_source", "form_badge");
@@ -1967,31 +2210,40 @@ function FilloForm(props) {
1967
2210
  );
1968
2211
  const [loaded, setLoaded] = useState6(null);
1969
2212
  const [loadFailure, setLoadFailure] = useState6(null);
2213
+ const devEnv = useIsDevEnv();
2214
+ const devChrome = props.preview === true || devEnv;
2215
+ if (props.preview === true) warnPreviewInProduction();
1970
2216
  const codeForm = isCodeForm2(form) ? form : null;
1971
2217
  const onError = props.onError;
1972
2218
  const {
1973
2219
  formId: syncedFormId,
1974
2220
  status: syncedStatus,
1975
2221
  branding: syncedBranding,
2222
+ challenge: syncedChallenge,
1976
2223
  error: syncError,
1977
2224
  noticeError: syncNoticeError,
1978
2225
  syncing: codeFormSyncing,
1979
2226
  staged: codeChangesStaged,
2227
+ accepting: syncedAccepting,
2228
+ acceptingReason: syncedAcceptingReason,
2229
+ uploadsAvailable: syncedUploadsAvailable,
1980
2230
  resolvedSchema,
1981
- resolvedTheme
2231
+ resolvedTheme,
2232
+ warningCode: syncedWarningCode,
2233
+ warningUrl: syncedWarningUrl
1982
2234
  } = useCodeFormSync(form, client, onError, {
1983
2235
  // An absent client is intentional render-only use. Builder previews and an
1984
2236
  // explicit canonical id also do not need publishable-key resolution.
1985
2237
  allowUnsynced: Boolean(!client || props.skipValidation || formId)
1986
2238
  });
1987
- const renderResolvedSnapshot = Boolean(resolvedSchema && !isDevEnv());
2239
+ const renderResolvedSnapshot = Boolean(resolvedSchema && !devChrome);
1988
2240
  const inlineSchema = codeForm ? renderResolvedSnapshot ? resolvedSchema : codeForm.schema : form;
1989
- const onErrorRef = useRef6(onError);
1990
- useEffect7(() => {
2241
+ const onErrorRef = useRef7(onError);
2242
+ useEffect8(() => {
1991
2243
  onErrorRef.current = onError;
1992
2244
  });
1993
2245
  const needsFetch = !inlineSchema && Boolean(client && formId);
1994
- useEffect7(() => {
2246
+ useEffect8(() => {
1995
2247
  if (!needsFetch || !client || !formId) {
1996
2248
  setLoaded(null);
1997
2249
  setLoadFailure(null);
@@ -2007,7 +2259,11 @@ function FilloForm(props) {
2007
2259
  schema: published.schema,
2008
2260
  theme: published.theme,
2009
2261
  closed: Boolean(published.closed),
2010
- branding: published.branding ?? null
2262
+ accepting: published.accepting,
2263
+ acceptingReason: published.acceptingReason,
2264
+ uploadsAvailable: published.uploadsAvailable,
2265
+ branding: published.branding ?? null,
2266
+ challenge: published.challenge ?? null
2011
2267
  });
2012
2268
  }).catch((err) => {
2013
2269
  if (cancelled) return;
@@ -2029,25 +2285,52 @@ function FilloForm(props) {
2029
2285
  const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
2030
2286
  if (normalized && !normalized.ok) {
2031
2287
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
2032
- if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
2033
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
2288
+ if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
2289
+ return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
2034
2290
  }
2035
2291
  if (loadError) {
2036
2292
  if (props.renderError) {
2037
- return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(loadError) });
2293
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(loadError) });
2038
2294
  }
2039
2295
  const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
2040
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
2296
+ return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
2297
+ }
2298
+ if (currentLoaded?.accepting === false && schema) {
2299
+ const variant = overlayVariant(currentLoaded.acceptingReason);
2300
+ if (props.renderError) {
2301
+ const error = variant === "closed" ? new FilloError2(
2302
+ "This form is no longer accepting responses.",
2303
+ 403,
2304
+ void 0,
2305
+ currentLoaded.acceptingReason
2306
+ ) : new FilloError2(
2307
+ "This form isn't accepting responses yet.",
2308
+ 403,
2309
+ void 0,
2310
+ currentLoaded.acceptingReason
2311
+ );
2312
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
2313
+ }
2314
+ return /* @__PURE__ */ jsx7(
2315
+ NotOpenOverlay,
2316
+ {
2317
+ ...props,
2318
+ variant,
2319
+ schema,
2320
+ theme,
2321
+ branding: currentLoaded.branding
2322
+ }
2323
+ );
2041
2324
  }
2042
2325
  if (currentLoaded?.closed) {
2043
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.closed }) });
2326
+ return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: strings.closed }) });
2044
2327
  }
2045
2328
  const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
2046
2329
  if (canonicalSyncError && props.renderError) {
2047
- return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(canonicalSyncError) });
2330
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(canonicalSyncError) });
2048
2331
  }
2049
- if (canonicalSyncError && !isDevEnv()) {
2050
- return /* @__PURE__ */ jsx5(
2332
+ if (canonicalSyncError && !devChrome) {
2333
+ return /* @__PURE__ */ jsx7(
2051
2334
  "div",
2052
2335
  {
2053
2336
  className: `fillo-form fillo-form--error ${props.className ?? ""}`,
@@ -2060,30 +2343,50 @@ function FilloForm(props) {
2060
2343
  );
2061
2344
  }
2062
2345
  const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
2063
- if (notPublished && !isDevEnv()) {
2064
- const error = new FilloError2("This form isn't published yet.", 403);
2065
- if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
2066
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.notLive }) });
2346
+ const syncNotAccepting = Boolean(codeForm) && !props.skipValidation && syncedStatus === "published" && syncedAccepting === false;
2347
+ if ((notPublished || syncNotAccepting) && !devChrome) {
2348
+ const variant = notPublished ? "notOpen" : overlayVariant(syncedAcceptingReason);
2349
+ const error = variant === "closed" ? new FilloError2(
2350
+ "This form is no longer accepting responses.",
2351
+ 403,
2352
+ void 0,
2353
+ syncedAcceptingReason
2354
+ ) : new FilloError2("This form isn't published yet.", 403);
2355
+ if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
2356
+ if (schema) {
2357
+ return /* @__PURE__ */ jsx7(
2358
+ NotOpenOverlay,
2359
+ {
2360
+ ...props,
2361
+ variant,
2362
+ schema,
2363
+ theme,
2364
+ branding: syncedBranding
2365
+ }
2366
+ );
2367
+ }
2368
+ return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: variant === "closed" ? strings.closed : strings.notLive }) });
2067
2369
  }
2068
2370
  const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
2069
- if (!isDevEnv() && awaitingCanonical || !schema) {
2070
- return /* @__PURE__ */ jsxs5(
2371
+ if (!devChrome && awaitingCanonical || !schema) {
2372
+ return /* @__PURE__ */ jsxs6(
2071
2373
  "div",
2072
2374
  {
2073
2375
  className: `fillo-form fillo-form--loading ${props.className ?? ""}`,
2074
2376
  "aria-busy": "true",
2075
2377
  ...themeProps(theme),
2076
2378
  children: [
2077
- /* @__PURE__ */ jsx5("div", { className: "fillo-skeleton" }),
2078
- /* @__PURE__ */ jsx5("div", { className: "fillo-skeleton" }),
2079
- /* @__PURE__ */ jsx5("div", { className: "fillo-skeleton fillo-skeleton--short" })
2379
+ /* @__PURE__ */ jsx7("div", { className: "fillo-skeleton" }),
2380
+ /* @__PURE__ */ jsx7("div", { className: "fillo-skeleton" }),
2381
+ /* @__PURE__ */ jsx7("div", { className: "fillo-skeleton fillo-skeleton--short" })
2080
2382
  ]
2081
2383
  }
2082
2384
  );
2083
2385
  }
2084
2386
  const branding = codeForm ? syncedBranding : currentLoaded?.branding;
2085
2387
  const poweredBy = branding?.poweredBy ?? true;
2086
- return /* @__PURE__ */ jsx5(
2388
+ const challenge = props.challenge ?? (codeForm ? syncedChallenge : currentLoaded?.challenge) ?? void 0;
2389
+ return /* @__PURE__ */ jsx7(
2087
2390
  ResolvedForm,
2088
2391
  {
2089
2392
  ...props,
@@ -2096,60 +2399,103 @@ function FilloForm(props) {
2096
2399
  brandHref: branding?.href,
2097
2400
  draftNotice: notPublished,
2098
2401
  stagedNotice: codeChangesStaged,
2099
- syncErrorNotice: syncError ?? syncNoticeError ?? void 0
2402
+ syncErrorNotice: syncError ?? syncNoticeError ?? void 0,
2403
+ uploadsAvailable: codeForm ? syncedUploadsAvailable : currentLoaded?.uploadsAvailable,
2404
+ syncWarningCode: codeForm ? syncedWarningCode : void 0,
2405
+ syncWarningUrl: syncedWarningUrl,
2406
+ challenge
2100
2407
  }
2101
2408
  );
2102
2409
  }
2103
2410
  function PoweredBy({ label, href }) {
2104
- return /* @__PURE__ */ jsx5("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
2105
- }
2106
- function isDevEnv() {
2107
- return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2411
+ return /* @__PURE__ */ jsx7("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
2108
2412
  }
2109
- function DevNoClientNotice() {
2110
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2111
- /* @__PURE__ */ jsx5("strong", { children: "No Fillo client connected." }),
2112
- " This form is render-only and won\u2019t save responses. Pass a ",
2113
- /* @__PURE__ */ jsx5("code", { children: "client" }),
2114
- " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
2115
- ] });
2116
- }
2117
- function DevDraftNotice() {
2118
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2119
- /* @__PURE__ */ jsx5("strong", { children: "Draft form." }),
2120
- " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
2121
- ] });
2413
+ function overlayVariant(reason) {
2414
+ return reason === "expired" || reason === "capped" || reason === "storage_full" ? "closed" : "notOpen";
2122
2415
  }
2123
- function DevStagedNotice() {
2124
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2125
- /* @__PURE__ */ jsx5("strong", { children: "Code changes are staged." }),
2126
- " This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
2127
- ] });
2416
+ function NotOpenOverlay(props) {
2417
+ const strings = resolveStrings(props.strings);
2418
+ const closedFlavor = props.variant === "closed";
2419
+ return /* @__PURE__ */ jsxs6(
2420
+ "div",
2421
+ {
2422
+ className: `fillo-form fillo-form--not-open ${props.className ?? ""}`,
2423
+ "data-fillo": "root",
2424
+ "data-state": "closed",
2425
+ ...themeProps(props.theme),
2426
+ children: [
2427
+ /* @__PURE__ */ jsx7(
2428
+ "div",
2429
+ {
2430
+ className: "fillo-not-open-preview",
2431
+ "aria-hidden": "true",
2432
+ ref: (node) => node?.setAttribute("inert", ""),
2433
+ children: /* @__PURE__ */ jsx7(
2434
+ ResolvedForm,
2435
+ {
2436
+ schema: props.schema,
2437
+ theme: props.theme,
2438
+ appearance: props.appearance,
2439
+ strings: props.strings,
2440
+ components: props.components,
2441
+ customComponents: props.customComponents,
2442
+ initialData: props.initialData,
2443
+ showTitle: props.showTitle,
2444
+ displayOnly: true
2445
+ }
2446
+ )
2447
+ }
2448
+ ),
2449
+ /* @__PURE__ */ jsxs6("div", { className: "fillo-not-open-card", "data-fillo": "not-open-card", role: "status", children: [
2450
+ /* @__PURE__ */ jsx7("h2", { className: "fillo-not-open-title", children: closedFlavor ? strings.closedTitle : strings.notOpenTitle }),
2451
+ /* @__PURE__ */ jsx7("p", { className: "fillo-not-open-body", children: closedFlavor ? strings.closed : strings.notOpenBody })
2452
+ ] }),
2453
+ (props.branding?.poweredBy ?? true) && /* @__PURE__ */ jsx7(PoweredBy, { label: props.branding?.label, href: props.branding?.href })
2454
+ ]
2455
+ }
2456
+ );
2128
2457
  }
2129
- function DevSyncErrorNotice({ error }) {
2130
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2131
- /* @__PURE__ */ jsxs5("strong", { children: [
2132
- "Form sync needs attention",
2133
- error.code ? ` (${error.code})` : "",
2134
- "."
2135
- ] }),
2136
- " ",
2137
- error.message,
2138
- " The safe form remains available for development."
2139
- ] });
2458
+ var noopSubscribe = () => () => {
2459
+ };
2460
+ function useIsDevEnv() {
2461
+ return useSyncExternalStore2(noopSubscribe, isLikelyDevEnv, isBuildTimeDevEnv2);
2140
2462
  }
2141
2463
  function ResolvedForm(props) {
2142
2464
  const { schema, theme } = props;
2465
+ const displayOnly = props.displayOnly === true;
2143
2466
  const instanceId = useId2().replace(/:/g, "");
2144
- const hpRef = useRef6(null);
2467
+ const devEnv = useIsDevEnv();
2468
+ const devChrome = !displayOnly && (props.preview === true || devEnv);
2469
+ const hpRef = useRef7(null);
2145
2470
  const codeForm = isCodeForm2(props.form) ? props.form : null;
2146
2471
  const client = props.client;
2472
+ const challenge = props.challenge;
2473
+ const challengeRequired = Boolean(challenge);
2474
+ const challengeTokenRef = useRef7(void 0);
2475
+ const [resyncWarning, setResyncWarning] = useState6(null);
2476
+ const [resolutionFailureCode, setResolutionFailureCode] = useState6(null);
2477
+ const [challengeToken, setChallengeToken] = useState6(void 0);
2478
+ const [challengeError, setChallengeError] = useState6(false);
2479
+ const resetChallengeRef = useRef7(void 0);
2480
+ const handleChallengeToken = useCallback((token) => {
2481
+ challengeTokenRef.current = token;
2482
+ setChallengeToken(token);
2483
+ if (token) setChallengeError(false);
2484
+ }, []);
2485
+ const registerChallengeReset = useCallback((reset) => {
2486
+ resetChallengeRef.current = reset;
2487
+ }, []);
2147
2488
  const resolveFormId = useMemo5(
2148
2489
  () => codeForm && client?.key ? (
2149
2490
  // bypassCache: a stale cached formId must not be trusted at submit time.
2150
2491
  async () => {
2151
2492
  try {
2152
2493
  const result = await syncCodeForm2(client, codeForm, { bypassCache: true });
2494
+ setResyncWarning({
2495
+ code: result.warningCode,
2496
+ url: result.warningUrl,
2497
+ uploadsAvailable: result.uploadsAvailable
2498
+ });
2153
2499
  if (result.status === "draft") {
2154
2500
  throw new FilloError2(
2155
2501
  "This form is no longer published. Submission was stopped before sending answers.",
@@ -2158,6 +2504,14 @@ function ResolvedForm(props) {
2158
2504
  "form_not_published"
2159
2505
  );
2160
2506
  }
2507
+ if (result.accepting === false) {
2508
+ throw new FilloError2(
2509
+ "This form is no longer accepting responses. Submission was stopped before sending answers.",
2510
+ 403,
2511
+ void 0,
2512
+ result.acceptingReason ?? "form_not_accepting"
2513
+ );
2514
+ }
2161
2515
  const authoritative = normalizeFormSchema(result.resolvedSchema ?? codeForm.schema);
2162
2516
  if (!authoritative.ok) {
2163
2517
  throw new FilloError2(
@@ -2187,9 +2541,11 @@ function ResolvedForm(props) {
2187
2541
  );
2188
2542
  props.onError?.(notice);
2189
2543
  }
2544
+ setResolutionFailureCode(null);
2190
2545
  return result.formId;
2191
2546
  } catch (error) {
2192
2547
  const failure = isFilloError2(error) ? error : new FilloError2(error instanceof Error ? error.message : String(error), 0);
2548
+ setResolutionFailureCode(failure.code ?? "sync_failed");
2193
2549
  console.warn(
2194
2550
  `[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
2195
2551
  );
@@ -2213,25 +2569,46 @@ function ResolvedForm(props) {
2213
2569
  // "default", never the bare-engine "headless".
2214
2570
  surface: "default",
2215
2571
  resolveFormId,
2216
- respondent: props.respondent
2572
+ // Dev chrome gets the developer-grade resolution failure (message + code)
2573
+ // instead of the respondent-safe "unavailable" fallback.
2574
+ verboseResolutionErrors: devChrome,
2575
+ respondent: props.respondent,
2576
+ challengeRequired,
2577
+ getChallengeToken: () => challengeTokenRef.current,
2578
+ onChallengeFailed: () => {
2579
+ handleChallengeToken(void 0);
2580
+ resetChallengeRef.current?.();
2581
+ }
2217
2582
  });
2218
- useDraftFlush(api);
2583
+ useDraftFlush(api, !displayOnly);
2219
2584
  const themed = useMemo5(() => themeProps(theme), [theme]);
2585
+ const warningCode = resyncWarning ? resyncWarning.code : props.syncWarningCode;
2586
+ const warningUrl = resyncWarning ? resyncWarning.url : props.syncWarningUrl;
2587
+ const uploadsAvailable = resyncWarning ? resyncWarning.uploadsAvailable : props.uploadsAvailable;
2588
+ const chromeValue = useMemo5(
2589
+ () => ({
2590
+ devChrome,
2591
+ uploadsAvailable,
2592
+ warningCode,
2593
+ warningUrl
2594
+ }),
2595
+ [devChrome, uploadsAvailable, warningCode, warningUrl]
2596
+ );
2220
2597
  const settings = schema.settings;
2221
2598
  const submitted = api.status === "submitted";
2222
2599
  const liveSubmit = submitted && !api.restoredSubmission;
2223
- useEffect7(() => {
2600
+ useEffect8(() => {
2224
2601
  if (!liveSubmit || !settings.redirectUrl || typeof window === "undefined") return;
2225
2602
  const url = safeHttpUrl(settings.redirectUrl);
2226
2603
  if (url) window.location.assign(url);
2227
2604
  }, [liveSubmit, settings.redirectUrl]);
2228
- const successRef = useRef6(null);
2229
- useEffect7(() => {
2605
+ const successRef = useRef7(null);
2606
+ useEffect8(() => {
2230
2607
  if (liveSubmit) successRef.current?.focus();
2231
2608
  }, [liveSubmit]);
2232
- const blocksRef = useRef6(null);
2233
- const prevPageIndex = useRef6(api.pageIndex);
2234
- useEffect7(() => {
2609
+ const blocksRef = useRef7(null);
2610
+ const prevPageIndex = useRef7(api.pageIndex);
2611
+ useEffect8(() => {
2235
2612
  if (prevPageIndex.current === api.pageIndex) return;
2236
2613
  prevPageIndex.current = api.pageIndex;
2237
2614
  blocksRef.current?.focus();
@@ -2239,7 +2616,7 @@ function ResolvedForm(props) {
2239
2616
  const appearance = props.appearance;
2240
2617
  const strings = resolveStrings(props.strings);
2241
2618
  if (api.status === "submitted") {
2242
- return /* @__PURE__ */ jsxs5(
2619
+ return /* @__PURE__ */ jsxs6(
2243
2620
  "div",
2244
2621
  {
2245
2622
  ref: successRef,
@@ -2255,34 +2632,190 @@ function ResolvedForm(props) {
2255
2632
  "data-state": "submitted",
2256
2633
  ...themed,
2257
2634
  children: [
2258
- props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5(
2635
+ props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs6(
2259
2636
  "div",
2260
2637
  {
2261
2638
  className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
2262
2639
  "data-fillo": "success",
2263
2640
  children: [
2264
- /* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
2265
- /* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
2266
- /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: api.duplicateSubmission ? strings.alreadyAnswered : settings.successMessage ?? strings.successMessage })
2641
+ /* @__PURE__ */ jsx7("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
2642
+ /* @__PURE__ */ jsx7("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
2643
+ /* @__PURE__ */ jsx7("p", { className: "fillo-success-message", children: api.duplicateSubmission ? strings.alreadyAnswered : settings.successMessage ?? strings.successMessage })
2267
2644
  ]
2268
2645
  }
2269
2646
  ),
2270
- props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2647
+ props.poweredBy && /* @__PURE__ */ jsx7(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2271
2648
  ]
2272
2649
  }
2273
2650
  );
2274
2651
  }
2275
2652
  const multiPage = api.pageCount > 1;
2276
2653
  const autoSubmit = settings.submitMode === "auto";
2277
- const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields2(api.form, api.data));
2654
+ const showChallenge = challengeRequired && api.isLastPage;
2655
+ const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(reachableFields(api.form, api.data)) || // A challenge needs an explicit Submit even on an otherwise button-less
2656
+ // auto-submit form — you can't one-tap past a human check.
2657
+ showChallenge;
2278
2658
  const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
2279
- 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(
2659
+ const progressSeq = reachablePageSequence(api.form, api.data);
2660
+ const progressTotal = Math.max(progressSeq.length, 1);
2661
+ const progressPos = progressSeq.indexOf(api.page.id);
2662
+ const progressStep = progressPos >= 0 ? progressPos + 1 : Math.min(api.pageIndex + 1, progressTotal);
2663
+ const submitFixUrl = devChrome && resolutionFailureCode === "form_not_published" && warningCode === "storage_required" ? safeHttpUrl(warningUrl) : null;
2664
+ const rootClass = slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
2665
+ slot: "root",
2666
+ status: api.status
2667
+ });
2668
+ const formBody = /* @__PURE__ */ jsxs6(Fragment3, { children: [
2669
+ devChrome && /* @__PURE__ */ jsx7(
2670
+ DevChrome,
2671
+ {
2672
+ preview: props.preview,
2673
+ devNotices: props.devNotices,
2674
+ syncError: props.syncErrorNotice,
2675
+ staged: props.stagedNotice,
2676
+ draft: props.draftNotice,
2677
+ warningUrl: props.syncWarningUrl,
2678
+ noClient: !props.client && !props.skipValidation
2679
+ }
2680
+ ),
2681
+ (api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs6(
2682
+ "div",
2683
+ {
2684
+ className: slotClass3("fillo-resume", appearance, { slot: "resume" }),
2685
+ "data-fillo": "resume",
2686
+ role: "status",
2687
+ children: [
2688
+ /* @__PURE__ */ jsx7("span", { className: "fillo-resume-text", children: api.editingPrevious ? strings.editNotice : strings.resumeNotice }),
2689
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "fillo-resume-clear", onClick: api.resetDraft, children: strings.resumeStartOver })
2690
+ ]
2691
+ }
2692
+ ),
2693
+ api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx7(
2694
+ "div",
2695
+ {
2696
+ className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
2697
+ "data-fillo": "resume",
2698
+ role: "status",
2699
+ children: /* @__PURE__ */ jsx7("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
2700
+ }
2701
+ ),
2702
+ multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx7(
2703
+ "div",
2704
+ {
2705
+ className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
2706
+ "data-fillo": "progress",
2707
+ role: "progressbar",
2708
+ "aria-label": schema.title || "Form progress",
2709
+ "aria-valuemin": 1,
2710
+ "aria-valuemax": progressTotal,
2711
+ "aria-valuenow": progressStep,
2712
+ children: /* @__PURE__ */ jsx7(
2713
+ "div",
2714
+ {
2715
+ className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
2716
+ "data-fillo": "progressFill",
2717
+ style: { "--fillo-progress-value": `${progressStep / progressTotal * 100}%` }
2718
+ }
2719
+ )
2720
+ }
2721
+ ),
2722
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs6("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
2723
+ schema.title && /* @__PURE__ */ jsx7("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2724
+ schema.description && /* @__PURE__ */ jsx7(
2725
+ "p",
2726
+ {
2727
+ className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2728
+ "data-fillo": "description",
2729
+ children: schema.description
2730
+ }
2731
+ )
2732
+ ] }),
2733
+ multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx7("h2", { className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }), "data-fillo": "pageTitle", children: api.page.title }),
2734
+ /* @__PURE__ */ jsx7("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx7(
2735
+ BlockRenderer,
2736
+ {
2737
+ block,
2738
+ api,
2739
+ components: props.components,
2740
+ customComponents: props.customComponents
2741
+ },
2742
+ block.id
2743
+ )) }),
2744
+ !displayOnly && /* @__PURE__ */ jsx7(
2745
+ "input",
2746
+ {
2747
+ type: "text",
2748
+ name: "fillo_hp_field",
2749
+ className: "fillo-hp",
2750
+ tabIndex: -1,
2751
+ autoComplete: "off",
2752
+ "aria-hidden": "true",
2753
+ defaultValue: "",
2754
+ style: HP_STYLE,
2755
+ ref: (element) => {
2756
+ hpRef.current = element;
2757
+ assertHpHidden(element);
2758
+ }
2759
+ }
2760
+ ),
2761
+ challenge && showChallenge && /* @__PURE__ */ jsxs6("div", { className: "fillo-turnstile-slot", "data-fillo": "turnstile-slot", children: [
2762
+ /* @__PURE__ */ jsx7(
2763
+ TurnstileWidget,
2764
+ {
2765
+ siteKey: challenge.siteKey,
2766
+ className: slotClass3("fillo-turnstile", appearance, { slot: "turnstile" }),
2767
+ onToken: handleChallengeToken,
2768
+ onError: () => setChallengeError(true),
2769
+ registerReset: registerChallengeReset
2770
+ }
2771
+ ),
2772
+ challengeError && /* @__PURE__ */ jsx7("p", { className: "fillo-turnstile-error", role: "alert", children: strings.challengeUnavailable })
2773
+ ] }),
2774
+ (api.status === "error" || api.submitError) && /* @__PURE__ */ jsxs6("p", { className: "fillo-submit-error", role: "alert", children: [
2775
+ api.submitError ?? strings.submitFailed,
2776
+ submitFixUrl && /* @__PURE__ */ jsxs6(Fragment3, { children: [
2777
+ " ",
2778
+ "Connect storage:",
2779
+ " ",
2780
+ /* @__PURE__ */ jsx7("a", { href: submitFixUrl, target: "_blank", rel: "noopener noreferrer", children: submitFixUrl })
2781
+ ] })
2782
+ ] }),
2783
+ showFooter && /* @__PURE__ */ jsxs6("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
2784
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx7(
2785
+ "button",
2786
+ {
2787
+ type: "button",
2788
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
2789
+ "data-fillo": "button",
2790
+ onClick: api.back,
2791
+ children: strings.back
2792
+ }
2793
+ ),
2794
+ showPrimaryButton && /* @__PURE__ */ jsx7(
2795
+ "button",
2796
+ {
2797
+ type: "submit",
2798
+ className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
2799
+ "data-fillo": "button",
2800
+ disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
2801
+ // "Next"). The server rejects a tokenless submit regardless —
2802
+ // this is UX only.
2803
+ showChallenge && !challengeToken,
2804
+ children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
2805
+ }
2806
+ )
2807
+ ] }),
2808
+ props.poweredBy && /* @__PURE__ */ jsx7(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2809
+ ] });
2810
+ return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children: displayOnly ? (
2811
+ // Display-only preview: a plain div (no form element to submit) with
2812
+ // every control inside a natively disabled fieldset — unfocusable
2813
+ // and unfillable even without the stylesheet or inert support.
2814
+ /* @__PURE__ */ jsx7("div", { className: rootClass, "data-fillo": "preview-form", "data-state": "preview", ...themed, children: /* @__PURE__ */ jsx7("fieldset", { disabled: true, className: "fillo-not-open-fields", children: formBody }) })
2815
+ ) : /* @__PURE__ */ jsx7(
2280
2816
  "form",
2281
2817
  {
2282
- className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
2283
- slot: "root",
2284
- status: api.status
2285
- }),
2818
+ className: rootClass,
2286
2819
  "data-fillo": "root",
2287
2820
  "data-state": api.status,
2288
2821
  "data-page": api.pageIndex + 1,
@@ -2300,136 +2833,28 @@ function ResolvedForm(props) {
2300
2833
  focusFirstInvalid(formEl);
2301
2834
  }
2302
2835
  },
2303
- children: [
2304
- !props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
2305
- props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
2306
- props.stagedNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevStagedNotice, {}),
2307
- props.syncErrorNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevSyncErrorNotice, { error: props.syncErrorNotice }),
2308
- (api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs5(
2309
- "div",
2310
- {
2311
- className: slotClass3("fillo-resume", appearance, { slot: "resume" }),
2312
- "data-fillo": "resume",
2313
- role: "status",
2314
- children: [
2315
- /* @__PURE__ */ jsx5("span", { className: "fillo-resume-text", children: api.editingPrevious ? strings.editNotice : strings.resumeNotice }),
2316
- /* @__PURE__ */ jsx5("button", { type: "button", className: "fillo-resume-clear", onClick: api.resetDraft, children: strings.resumeStartOver })
2317
- ]
2318
- }
2319
- ),
2320
- api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx5(
2321
- "div",
2322
- {
2323
- className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
2324
- "data-fillo": "resume",
2325
- role: "status",
2326
- children: /* @__PURE__ */ jsx5("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
2327
- }
2328
- ),
2329
- multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
2330
- "div",
2331
- {
2332
- className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
2333
- "data-fillo": "progress",
2334
- role: "progressbar",
2335
- "aria-label": schema.title || "Form progress",
2336
- "aria-valuemin": 1,
2337
- "aria-valuemax": api.pageCount,
2338
- "aria-valuenow": api.pageIndex + 1,
2339
- children: /* @__PURE__ */ jsx5(
2340
- "div",
2341
- {
2342
- className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
2343
- "data-fillo": "progressFill",
2344
- style: { "--fillo-progress-value": `${(api.pageIndex + 1) / api.pageCount * 100}%` }
2345
- }
2346
- )
2347
- }
2348
- ),
2349
- api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
2350
- schema.title && /* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2351
- schema.description && /* @__PURE__ */ jsx5(
2352
- "p",
2353
- {
2354
- className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2355
- "data-fillo": "description",
2356
- children: schema.description
2357
- }
2358
- )
2359
- ] }),
2360
- 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 }),
2361
- /* @__PURE__ */ jsx5("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
2362
- BlockRenderer,
2363
- {
2364
- block,
2365
- api,
2366
- components: props.components,
2367
- customComponents: props.customComponents
2368
- },
2369
- block.id
2370
- )) }),
2371
- /* @__PURE__ */ jsx5(
2372
- "input",
2373
- {
2374
- type: "text",
2375
- name: "fillo_hp_field",
2376
- className: "fillo-hp",
2377
- tabIndex: -1,
2378
- autoComplete: "off",
2379
- "aria-hidden": "true",
2380
- defaultValue: "",
2381
- style: HP_STYLE,
2382
- ref: (element) => {
2383
- hpRef.current = element;
2384
- assertHpHidden(element);
2385
- }
2386
- }
2387
- ),
2388
- (api.status === "error" || api.submitError) && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: api.submitError ?? strings.submitFailed }),
2389
- showFooter && /* @__PURE__ */ jsxs5("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
2390
- multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5(
2391
- "button",
2392
- {
2393
- type: "button",
2394
- className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
2395
- "data-fillo": "button",
2396
- onClick: api.back,
2397
- children: strings.back
2398
- }
2399
- ),
2400
- showPrimaryButton && /* @__PURE__ */ jsx5(
2401
- "button",
2402
- {
2403
- type: "submit",
2404
- className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
2405
- "data-fillo": "button",
2406
- disabled: api.status === "submitting" || api.uploading,
2407
- children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
2408
- }
2409
- )
2410
- ] }),
2411
- props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2412
- ]
2836
+ children: formBody
2413
2837
  }
2414
- ) }) }) }) });
2838
+ ) }) }) }) }) });
2415
2839
  }
2416
2840
 
2417
2841
  // src/jsx.tsx
2418
- import { useRef as useRef7 } from "react";
2842
+ import { useRef as useRef8 } from "react";
2419
2843
  import {
2420
2844
  codeFormFromJsx,
2421
2845
  FilloJsxError,
2846
+ isLikelyDevEnv as isLikelyDevEnv2,
2422
2847
  JSX_BLOCK_COMPONENTS
2423
2848
  } from "@usefillo/core";
2424
- import { jsx as jsx6 } from "react/jsx-runtime";
2849
+ import { jsx as jsx8 } from "react/jsx-runtime";
2425
2850
  var B = JSX_BLOCK_COMPONENTS;
2426
- var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2851
+ var isDevEnv = () => isLikelyDevEnv2();
2427
2852
  function JsxForm(props) {
2428
2853
  const { id, title, description, settings, theme, client, children, ...rest } = props;
2429
2854
  const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
2430
2855
  const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
2431
- const memo2 = useRef7(null);
2432
- if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
2856
+ const memo2 = useRef8(null);
2857
+ if (memo2.current && memo2.current.json !== json && isDevEnv()) {
2433
2858
  console.warn(
2434
2859
  `[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.`
2435
2860
  );
@@ -2437,7 +2862,7 @@ function JsxForm(props) {
2437
2862
  if (!memo2.current || memo2.current.json !== json) {
2438
2863
  memo2.current = { json, form: compiled };
2439
2864
  }
2440
- return /* @__PURE__ */ jsx6(FilloForm, { form: memo2.current.form, client, ...rest });
2865
+ return /* @__PURE__ */ jsx8(FilloForm, { form: memo2.current.form, client, ...rest });
2441
2866
  }
2442
2867
  function defineFormFromJsx(element) {
2443
2868
  if (!element || element.type !== JsxForm) {
@@ -2481,7 +2906,7 @@ var Fillo = {
2481
2906
  };
2482
2907
 
2483
2908
  // src/provider.tsx
2484
- import { useId as useId3, useMemo as useMemo6 } from "react";
2909
+ import { useId as useId3, useMemo as useMemo6, useState as useState7 } from "react";
2485
2910
  import {
2486
2911
  FilloError as FilloError3,
2487
2912
  formSchemasEqual as formSchemasEqual2,
@@ -2490,7 +2915,7 @@ import {
2490
2915
  resolveStrings as resolveStrings2,
2491
2916
  syncCodeForm as syncCodeForm3
2492
2917
  } from "@usefillo/core";
2493
- import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
2918
+ import { Fragment as Fragment4, jsx as jsx9 } from "react/jsx-runtime";
2494
2919
  function FilloProvider({
2495
2920
  children,
2496
2921
  form,
@@ -2500,20 +2925,30 @@ function FilloProvider({
2500
2925
  strings,
2501
2926
  onError,
2502
2927
  renderError,
2928
+ preview,
2503
2929
  ...options
2504
2930
  }) {
2505
2931
  const instanceId = useId3().replace(/:/g, "");
2932
+ const devEnv = useIsDevEnv();
2933
+ const devChrome = preview === true || devEnv;
2934
+ if (preview === true) warnPreviewInProduction();
2506
2935
  const codeForm = isCodeForm2(form) ? form : null;
2507
2936
  const {
2508
2937
  formId: syncedFormId,
2509
2938
  status: syncedStatus,
2510
2939
  error: syncError,
2511
2940
  syncing: codeFormSyncing,
2512
- resolvedSchema
2941
+ resolvedSchema,
2942
+ accepting: syncedAccepting,
2943
+ acceptingReason: syncedAcceptingReason,
2944
+ uploadsAvailable,
2945
+ warningCode,
2946
+ warningUrl
2513
2947
  } = useCodeFormSync(form, client, onError, {
2514
2948
  allowUnsynced: Boolean(!client || formId || options.skipValidation)
2515
2949
  });
2516
- const rawSchema = codeForm ? resolvedSchema && !isDevEnv3() ? resolvedSchema : codeForm.schema : form;
2950
+ const [resyncWarning, setResyncWarning] = useState7(null);
2951
+ const rawSchema = codeForm ? resolvedSchema && !devChrome ? resolvedSchema : codeForm.schema : form;
2517
2952
  const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema]);
2518
2953
  if (!normalized.ok) {
2519
2954
  throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
@@ -2523,6 +2958,11 @@ function FilloProvider({
2523
2958
  () => codeForm && client?.key ? async () => {
2524
2959
  try {
2525
2960
  const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
2961
+ setResyncWarning({
2962
+ code: result.warningCode,
2963
+ url: result.warningUrl,
2964
+ uploadsAvailable: result.uploadsAvailable
2965
+ });
2526
2966
  if (result.status === "draft") {
2527
2967
  throw new FilloError3(
2528
2968
  "This form is no longer published. Submission was stopped before sending answers.",
@@ -2531,6 +2971,14 @@ function FilloProvider({
2531
2971
  "form_not_published"
2532
2972
  );
2533
2973
  }
2974
+ if (result.accepting === false) {
2975
+ throw new FilloError3(
2976
+ "This form is no longer accepting responses. Submission was stopped before sending answers.",
2977
+ 403,
2978
+ void 0,
2979
+ result.acceptingReason ?? "form_not_accepting"
2980
+ );
2981
+ }
2534
2982
  const authoritative = normalizeFormSchema2(result.resolvedSchema ?? codeForm.schema);
2535
2983
  if (!authoritative.ok) {
2536
2984
  throw new FilloError3(
@@ -2578,25 +3026,43 @@ function FilloProvider({
2578
3026
  form: schema,
2579
3027
  formId: syncedFormId ?? formId,
2580
3028
  resolveFormId,
3029
+ // Dev chrome (preview or dev env) surfaces the developer-grade resolution
3030
+ // failure (message + code); an explicit option wins.
3031
+ verboseResolutionErrors: options.verboseResolutionErrors ?? devChrome,
2581
3032
  // Headless = your markup, no Fillo-rendered layout. Free like every embed
2582
3033
  // method; recorded per response so usage stays measurable.
2583
3034
  surface: "headless"
2584
3035
  });
2585
3036
  useDraftFlush(api);
3037
+ const freshWarningCode = resyncWarning ? resyncWarning.code : warningCode;
3038
+ const freshWarningUrl = resyncWarning ? resyncWarning.url : warningUrl;
3039
+ const freshUploadsAvailable = resyncWarning ? resyncWarning.uploadsAvailable : uploadsAvailable;
3040
+ const chromeValue = useMemo6(
3041
+ () => ({
3042
+ devChrome,
3043
+ uploadsAvailable: freshUploadsAvailable,
3044
+ warningCode: freshWarningCode,
3045
+ warningUrl: freshWarningUrl
3046
+ }),
3047
+ [devChrome, freshUploadsAvailable, freshWarningCode, freshWarningUrl]
3048
+ );
2586
3049
  const resolvedStrings = resolveStrings2(strings);
2587
3050
  const canonicalSyncError = syncError && !options.skipValidation ? syncError : null;
2588
- if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: renderError(canonicalSyncError) });
2589
- if (canonicalSyncError && !isDevEnv3()) return null;
2590
- if (codeFormSyncing && !options.skipValidation && !isDevEnv3()) return null;
3051
+ if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx9(Fragment4, { children: renderError(canonicalSyncError) });
3052
+ if (canonicalSyncError && !devChrome) return null;
3053
+ if (codeFormSyncing && !options.skipValidation && !devChrome) return null;
2591
3054
  const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !options.skipValidation;
2592
- if (notPublished && !isDevEnv3()) {
2593
- const error = new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published");
2594
- return renderError ? /* @__PURE__ */ jsx7(Fragment3, { children: renderError(error) }) : null;
3055
+ const notAccepting = Boolean(codeForm) && syncedStatus === "published" && syncedAccepting === false && !options.skipValidation;
3056
+ if ((notPublished || notAccepting) && !devChrome) {
3057
+ const error = notPublished ? new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published") : new FilloError3(
3058
+ "This form is no longer accepting responses.",
3059
+ 403,
3060
+ void 0,
3061
+ syncedAcceptingReason ?? "form_not_accepting"
3062
+ );
3063
+ return renderError ? /* @__PURE__ */ jsx9(Fragment4, { children: renderError(error) }) : null;
2595
3064
  }
2596
- return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolvedStrings, children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
2597
- }
2598
- function isDevEnv3() {
2599
- return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
3065
+ return /* @__PURE__ */ jsx9(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx9(FilloChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx9(FilloStringsContext.Provider, { value: resolvedStrings, children: /* @__PURE__ */ jsx9(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx9(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) }) });
2600
3066
  }
2601
3067
 
2602
3068
  // src/index.ts