@usefillo/react 0.9.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,12 +1,22 @@
1
1
  "use client";
2
2
 
3
3
  // src/FilloForm.tsx
4
- import { useCallback, useEffect as useEffect8, useId as useId2, useMemo as useMemo5, useRef as useRef7, 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,
@@ -61,11 +71,90 @@ function useStrings() {
61
71
  return useContext(FilloStringsContext);
62
72
  }
63
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
+
64
149
  // src/context.ts
65
150
  import { allFields } from "@usefillo/core";
66
151
  import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
67
152
  var FilloContext = createContext2(null);
68
153
  var FilloInstanceIdContext = createContext2(null);
154
+ var FilloChromeContext = createContext2(null);
155
+ function useFilloChrome() {
156
+ return useContext2(FilloChromeContext);
157
+ }
69
158
  function useFilloInstanceId() {
70
159
  const instanceId = useContext2(FilloInstanceIdContext);
71
160
  const fallbackId = useId().replace(/:/g, "");
@@ -128,6 +217,12 @@ function useFilloController(options) {
128
217
  surface: options.surface ?? "headless",
129
218
  // Wrapped so the engine (created once) always calls the latest resolver.
130
219
  resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0,
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
+ },
131
226
  respondent: options.respondent,
132
227
  // Challenge wiring read through optsRef so the engine (created once) sees
133
228
  // the latest token/reset each submit — the widget solves after mount.
@@ -164,11 +259,11 @@ function useFilloController(options) {
164
259
  [store, state, options.form, options.formId, options.client]
165
260
  );
166
261
  }
167
- function useDraftFlush(api) {
168
- const enabled = Boolean(api.form.settings.saveProgress);
262
+ function useDraftFlush(api, enabled = true) {
263
+ const active = enabled && Boolean(api.form.settings.saveProgress);
169
264
  const flush = api.flushDraft;
170
265
  useEffect(() => {
171
- if (!enabled || typeof window === "undefined") return;
266
+ if (!active || typeof window === "undefined") return;
172
267
  const onPageHide = () => flush();
173
268
  const onVisibility = () => {
174
269
  if (document.visibilityState === "hidden") flush();
@@ -179,7 +274,7 @@ function useDraftFlush(api) {
179
274
  window.removeEventListener("pagehide", onPageHide);
180
275
  document.removeEventListener("visibilitychange", onVisibility);
181
276
  };
182
- }, [enabled, flush]);
277
+ }, [active, flush]);
183
278
  }
184
279
 
185
280
  // src/define.ts
@@ -230,8 +325,14 @@ function useCodeFormSync(form, client, onError, options) {
230
325
  error: null,
231
326
  noticeError: null,
232
327
  staged: false,
328
+ accepting: void 0,
329
+ acceptingReason: void 0,
330
+ uploadsAvailable: void 0,
233
331
  resolvedSchema: void 0,
234
- resolvedTheme: void 0
332
+ resolvedTheme: void 0,
333
+ warning: void 0,
334
+ warningCode: void 0,
335
+ warningUrl: void 0
235
336
  } : {
236
337
  key: syncKey,
237
338
  client,
@@ -286,11 +387,21 @@ function useCodeFormSync(form, client, onError, options) {
286
387
  error: null,
287
388
  noticeError,
288
389
  staged: Boolean(r.staged),
390
+ accepting: r.accepting,
391
+ acceptingReason: r.acceptingReason,
392
+ uploadsAvailable: r.uploadsAvailable,
289
393
  resolvedSchema: r.resolvedSchema,
290
- resolvedTheme: r.resolvedTheme
394
+ resolvedTheme: r.resolvedTheme,
395
+ warning: r.warning,
396
+ warningCode: r.warningCode,
397
+ warningUrl: r.warningUrl
291
398
  });
292
399
  const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
293
- 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
+ }
294
405
  if (noticeError) {
295
406
  console.warn(
296
407
  `[fillo] form sync needs attention (${noticeError.code}): ${noticeError.message}`
@@ -337,8 +448,14 @@ function useCodeFormSync(form, client, onError, options) {
337
448
  noticeError: synced.noticeError,
338
449
  syncing: Boolean(syncKey) && !synced.settled,
339
450
  staged: synced.staged,
451
+ accepting: synced.accepting,
452
+ acceptingReason: synced.acceptingReason,
453
+ uploadsAvailable: synced.uploadsAvailable,
340
454
  resolvedSchema: synced.resolvedSchema,
341
- resolvedTheme: synced.resolvedTheme
455
+ resolvedTheme: synced.resolvedTheme,
456
+ warning: synced.warning,
457
+ warningCode: synced.warningCode,
458
+ warningUrl: synced.warningUrl
342
459
  } : {
343
460
  formId: null,
344
461
  status: null,
@@ -366,7 +483,7 @@ import {
366
483
  toE164
367
484
  } from "@usefillo/core";
368
485
  import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
369
- import { jsx, jsxs } from "react/jsx-runtime";
486
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
370
487
  function localeCountry() {
371
488
  if (typeof navigator === "undefined") return void 0;
372
489
  const tag = navigator.languages?.[0] || navigator.language || "";
@@ -449,15 +566,15 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
449
566
  inputRef.current?.focus();
450
567
  }
451
568
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
452
- return /* @__PURE__ */ jsxs("div", { ...slots.wrapperProps(`fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`), children: [
453
- /* @__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: [
454
571
  field.label,
455
- !field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: strings.optional })
572
+ !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
456
573
  ] }),
457
- field.description && /* @__PURE__ */ jsx("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
458
- /* @__PURE__ */ jsxs("div", { className: "fillo-phone", children: [
459
- /* @__PURE__ */ jsx(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
460
- /* @__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(
461
578
  "input",
462
579
  {
463
580
  ref: inputRef,
@@ -476,7 +593,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
476
593
  }
477
594
  )
478
595
  ] }),
479
- 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 })
480
597
  ] });
481
598
  }
482
599
  function CountrySelect({
@@ -552,8 +669,8 @@ function CountrySelect({
552
669
  triggerRef.current?.focus();
553
670
  }
554
671
  }
555
- return /* @__PURE__ */ jsxs("div", { className: "fillo-phone-country", ref: rootRef, children: [
556
- /* @__PURE__ */ jsxs(
672
+ return /* @__PURE__ */ jsxs2("div", { className: "fillo-phone-country", ref: rootRef, children: [
673
+ /* @__PURE__ */ jsxs2(
557
674
  "button",
558
675
  {
559
676
  ref: triggerRef,
@@ -565,17 +682,17 @@ function CountrySelect({
565
682
  "aria-label": `Country: ${country.name} (+${country.dialCode})`,
566
683
  onClick: () => setOpen((o) => !o),
567
684
  children: [
568
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(country.iso2) }),
569
- /* @__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: [
570
687
  "+",
571
688
  country.dialCode
572
689
  ] }),
573
- /* @__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" })
574
691
  ]
575
692
  }
576
693
  ),
577
- open && /* @__PURE__ */ jsxs("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
578
- /* @__PURE__ */ jsx(
694
+ open && /* @__PURE__ */ jsxs2("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
695
+ /* @__PURE__ */ jsx2(
579
696
  "input",
580
697
  {
581
698
  ref: searchRef,
@@ -596,8 +713,8 @@ function CountrySelect({
596
713
  onKeyDown: onSearchKey
597
714
  }
598
715
  ),
599
- /* @__PURE__ */ jsxs("ul", { className: "fillo-phone-list", role: "listbox", id: listId, "aria-label": "Country", children: [
600
- 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(
601
718
  "li",
602
719
  {
603
720
  id: `${listId}-opt-${i}`,
@@ -610,9 +727,9 @@ function CountrySelect({
610
727
  choose(c);
611
728
  },
612
729
  children: [
613
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(c.iso2) }),
614
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-option-name", children: c.name }),
615
- /* @__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: [
616
733
  "+",
617
734
  c.dialCode
618
735
  ] })
@@ -620,7 +737,7 @@ function CountrySelect({
620
737
  },
621
738
  c.iso2
622
739
  )),
623
- 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" })
624
741
  ] })
625
742
  ] })
626
743
  ] });
@@ -628,7 +745,7 @@ function CountrySelect({
628
745
 
629
746
  // src/signature.tsx
630
747
  import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
631
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
748
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
632
749
  function SignatureField({ field, value, error, setValue, ids: providedIds }) {
633
750
  const ids = useFilloFieldIds(field.id, providedIds);
634
751
  const slots = useFieldSlots(field, error);
@@ -733,21 +850,21 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
733
850
  setValue(null);
734
851
  }
735
852
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
736
- return /* @__PURE__ */ jsxs2("div", { ...slots.wrapperProps(`fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`), children: [
737
- /* @__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: [
738
855
  field.label,
739
- !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
856
+ !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
740
857
  ] }),
741
- field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
742
- /* @__PURE__ */ jsxs2(
858
+ field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
859
+ /* @__PURE__ */ jsxs3(
743
860
  "div",
744
861
  {
745
862
  role: "group",
746
863
  "aria-labelledby": ids.labelId,
747
864
  "aria-describedby": describedBy,
748
865
  children: [
749
- /* @__PURE__ */ jsxs2("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
750
- /* @__PURE__ */ jsx2(
866
+ /* @__PURE__ */ jsxs3("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
867
+ /* @__PURE__ */ jsx3(
751
868
  "canvas",
752
869
  {
753
870
  ref: canvasRef,
@@ -760,12 +877,12 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
760
877
  onPointerLeave: end
761
878
  }
762
879
  ),
763
- hasInk && /* @__PURE__ */ jsx2("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
764
- !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" })
765
882
  ] }),
766
- /* @__PURE__ */ jsxs2("div", { className: "fillo-signature-type", children: [
767
- /* @__PURE__ */ jsx2("label", { className: "fillo-signature-type-label", htmlFor: typedInputId, children: "Or type your full name to sign" }),
768
- /* @__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(
769
886
  "input",
770
887
  {
771
888
  id: typedInputId,
@@ -787,13 +904,13 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
787
904
  ]
788
905
  }
789
906
  ),
790
- 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 })
791
908
  ] });
792
909
  }
793
910
 
794
911
  // src/upload.tsx
795
912
  import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
796
- 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";
797
914
  function uploadStatus(inFlight, doneCount, strings) {
798
915
  const failed = inFlight.filter((f) => f.error).length;
799
916
  const active = inFlight.length - failed;
@@ -813,6 +930,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
813
930
  const ids = useFilloFieldIds(field.id, providedIds);
814
931
  const slots = useFieldSlots(field, error);
815
932
  const strings = useStrings();
933
+ const chrome = useFilloChrome();
816
934
  const schema = field;
817
935
  const inputRef = useRef5(null);
818
936
  const [inFlight, setInFlight] = useState4([]);
@@ -836,7 +954,9 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
836
954
  const maxFiles = schema.maxFiles ?? 1;
837
955
  const maxMb = schema.maxFileSizeMb ?? 500;
838
956
  const maxBytes = maxMb * 1024 * 1024;
839
- 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;
840
960
  const activeInFlight = inFlight.filter((f) => !f.error).length;
841
961
  const remaining = maxFiles - files.length - activeInFlight;
842
962
  async function startUpload(file) {
@@ -898,13 +1018,13 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
898
1018
  setValue(next);
899
1019
  }
900
1020
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
901
- return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
902
- /* @__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: [
903
1023
  field.label,
904
- !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
1024
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
905
1025
  ] }),
906
- field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
907
- 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(
908
1028
  "div",
909
1029
  {
910
1030
  "data-fillo": "control",
@@ -935,7 +1055,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
935
1055
  }
936
1056
  },
937
1057
  children: [
938
- /* @__PURE__ */ jsx3(
1058
+ /* @__PURE__ */ jsx4(
939
1059
  "input",
940
1060
  {
941
1061
  ref: inputRef,
@@ -948,14 +1068,20 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
948
1068
  onChange: (e) => canUpload && handleFiles(e.target.files)
949
1069
  }
950
1070
  ),
951
- canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
952
- /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
953
- /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
954
- ] }) : /* @__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 })
955
1081
  ]
956
1082
  }
957
1083
  ),
958
- /* @__PURE__ */ jsx3(
1084
+ /* @__PURE__ */ jsx4(
959
1085
  "div",
960
1086
  {
961
1087
  role: "status",
@@ -965,11 +1091,11 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
965
1091
  children: uploadStatus(inFlight, files.length, strings)
966
1092
  }
967
1093
  ),
968
- (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
969
- files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
970
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
971
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
972
- /* @__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(
973
1099
  "button",
974
1100
  {
975
1101
  type: "button",
@@ -980,7 +1106,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
980
1106
  }
981
1107
  )
982
1108
  ] }, f.fileId)),
983
- inFlight.map((f) => /* @__PURE__ */ jsxs3(
1109
+ inFlight.map((f) => /* @__PURE__ */ jsxs4(
984
1110
  "li",
985
1111
  {
986
1112
  className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
@@ -992,10 +1118,10 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
992
1118
  "aria-valuenow": Math.round(f.fraction * 100)
993
1119
  },
994
1120
  children: [
995
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
996
- f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
997
- /* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
998
- 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(
999
1125
  "button",
1000
1126
  {
1001
1127
  type: "button",
@@ -1010,7 +1136,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1010
1136
  children: strings.uploadRetry
1011
1137
  }
1012
1138
  ),
1013
- /* @__PURE__ */ jsx3(
1139
+ /* @__PURE__ */ jsx4(
1014
1140
  "button",
1015
1141
  {
1016
1142
  type: "button",
@@ -1020,14 +1146,14 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1020
1146
  children: "\xD7"
1021
1147
  }
1022
1148
  )
1023
- ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
1024
- /* @__PURE__ */ jsxs3("span", { className: "fillo-file-meta", children: [
1149
+ ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1150
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-meta", children: [
1025
1151
  Math.round(f.fraction * 100),
1026
1152
  "% of ",
1027
1153
  formatBytes(f.size)
1028
1154
  ] }),
1029
- /* @__PURE__ */ jsx3("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
1030
- /* @__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(
1031
1157
  "button",
1032
1158
  {
1033
1159
  type: "button",
@@ -1043,12 +1169,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1043
1169
  f.key
1044
1170
  ))
1045
1171
  ] }),
1046
- 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 })
1047
1173
  ] });
1048
1174
  }
1049
1175
 
1050
1176
  // src/fields.tsx
1051
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1177
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1052
1178
  function fieldAria(field, error, ids) {
1053
1179
  const describedBy = [
1054
1180
  field.description ? ids.descriptionId : null,
@@ -1095,7 +1221,7 @@ function FieldShell({
1095
1221
  const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
1096
1222
  const errorCls = useSlotClass("fillo-error", { slot: "error", ...state });
1097
1223
  const strings = useStrings();
1098
- return /* @__PURE__ */ jsxs4(
1224
+ return /* @__PURE__ */ jsxs5(
1099
1225
  "div",
1100
1226
  {
1101
1227
  className: fieldCls,
@@ -1105,13 +1231,13 @@ function FieldShell({
1105
1231
  "data-invalid": error ? "" : void 0,
1106
1232
  "data-required": field.required ? "" : void 0,
1107
1233
  children: [
1108
- /* @__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: [
1109
1235
  field.label,
1110
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1236
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1111
1237
  ] }),
1112
- 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 }),
1113
1239
  children,
1114
- 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 })
1115
1241
  ]
1116
1242
  }
1117
1243
  );
@@ -1120,7 +1246,7 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
1120
1246
  const ids = useFilloFieldIds(field.id, providedIds);
1121
1247
  const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
1122
1248
  const control = useControl("fillo-input", field, error);
1123
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1249
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1124
1250
  "input",
1125
1251
  {
1126
1252
  id: ids.inputId,
@@ -1139,7 +1265,7 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
1139
1265
  function LongText({ field, value, error, setValue, ids: providedIds }) {
1140
1266
  const ids = useFilloFieldIds(field.id, providedIds);
1141
1267
  const control = useControl("fillo-input fillo-textarea", field, error);
1142
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1268
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1143
1269
  "textarea",
1144
1270
  {
1145
1271
  id: ids.inputId,
@@ -1176,7 +1302,7 @@ var OPTION_ICON_PATHS = {
1176
1302
  };
1177
1303
  function ChoiceIcon({ icon }) {
1178
1304
  if (!icon) return null;
1179
- return /* @__PURE__ */ jsx4(
1305
+ return /* @__PURE__ */ jsx5(
1180
1306
  "svg",
1181
1307
  {
1182
1308
  className: "fillo-option-icon",
@@ -1187,7 +1313,7 @@ function ChoiceIcon({ icon }) {
1187
1313
  strokeLinecap: "round",
1188
1314
  strokeLinejoin: "round",
1189
1315
  "aria-hidden": "true",
1190
- children: /* @__PURE__ */ jsx4("path", { d: OPTION_ICON_PATHS[icon] })
1316
+ children: /* @__PURE__ */ jsx5("path", { d: OPTION_ICON_PATHS[icon] })
1191
1317
  }
1192
1318
  );
1193
1319
  }
@@ -1201,7 +1327,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1201
1327
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
1202
1328
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1203
1329
  const otherActive = isOtherValue || value === "" && otherOn;
1204
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
1330
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs5(
1205
1331
  "div",
1206
1332
  {
1207
1333
  className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
@@ -1210,7 +1336,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1210
1336
  "aria-labelledby": ids.labelId,
1211
1337
  ...fieldAria(field, error, ids),
1212
1338
  children: [
1213
- options.map((opt) => /* @__PURE__ */ jsxs4(
1339
+ options.map((opt) => /* @__PURE__ */ jsxs5(
1214
1340
  "label",
1215
1341
  {
1216
1342
  "data-option": opt.id,
@@ -1228,7 +1354,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1228
1354
  setValue(opt.id);
1229
1355
  },
1230
1356
  children: [
1231
- /* @__PURE__ */ jsx4(
1357
+ /* @__PURE__ */ jsx5(
1232
1358
  "input",
1233
1359
  {
1234
1360
  type: "radio",
@@ -1241,15 +1367,15 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1241
1367
  }
1242
1368
  }
1243
1369
  ),
1244
- /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
1245
- /* @__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 })
1246
1372
  ]
1247
1373
  },
1248
1374
  opt.id
1249
1375
  )),
1250
1376
  choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
1251
1377
  // toggle the radio. The radio + "Other" text keep their own <label>.
1252
- /* @__PURE__ */ jsxs4(
1378
+ /* @__PURE__ */ jsxs5(
1253
1379
  "div",
1254
1380
  {
1255
1381
  "data-fillo": "option",
@@ -1260,8 +1386,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1260
1386
  otherActive
1261
1387
  ),
1262
1388
  children: [
1263
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1264
- /* @__PURE__ */ jsx4(
1389
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-option-main", children: [
1390
+ /* @__PURE__ */ jsx5(
1265
1391
  "input",
1266
1392
  {
1267
1393
  type: "radio",
@@ -1274,9 +1400,9 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1274
1400
  }
1275
1401
  }
1276
1402
  ),
1277
- /* @__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 })
1278
1404
  ] }),
1279
- otherActive && /* @__PURE__ */ jsx4(
1405
+ otherActive && /* @__PURE__ */ jsx5(
1280
1406
  "input",
1281
1407
  {
1282
1408
  type: "text",
@@ -1308,7 +1434,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1308
1434
  const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
1309
1435
  const appearance = useFilloAppearance();
1310
1436
  const optionCls = (base, optionId, isSelected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected: isSelected });
1311
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
1437
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs5(
1312
1438
  "div",
1313
1439
  {
1314
1440
  className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
@@ -1317,7 +1443,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1317
1443
  "aria-labelledby": ids.labelId,
1318
1444
  ...fieldAria(field, error, ids),
1319
1445
  children: [
1320
- options.map((opt) => /* @__PURE__ */ jsxs4(
1446
+ options.map((opt) => /* @__PURE__ */ jsxs5(
1321
1447
  "label",
1322
1448
  {
1323
1449
  "data-option": opt.id,
@@ -1334,7 +1460,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1334
1460
  toggle(opt.id);
1335
1461
  },
1336
1462
  children: [
1337
- /* @__PURE__ */ jsx4(
1463
+ /* @__PURE__ */ jsx5(
1338
1464
  "input",
1339
1465
  {
1340
1466
  type: "checkbox",
@@ -1343,15 +1469,15 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1343
1469
  onChange: () => toggle(opt.id)
1344
1470
  }
1345
1471
  ),
1346
- /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
1347
- /* @__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 })
1348
1474
  ]
1349
1475
  },
1350
1476
  opt.id
1351
1477
  )),
1352
1478
  choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
1353
1479
  // toggle the checkbox. The checkbox + "Other" text keep their own <label>.
1354
- /* @__PURE__ */ jsxs4(
1480
+ /* @__PURE__ */ jsxs5(
1355
1481
  "div",
1356
1482
  {
1357
1483
  "data-fillo": "option",
@@ -1362,8 +1488,8 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1362
1488
  otherActive
1363
1489
  ),
1364
1490
  children: [
1365
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1366
- /* @__PURE__ */ jsx4(
1491
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-option-main", children: [
1492
+ /* @__PURE__ */ jsx5(
1367
1493
  "input",
1368
1494
  {
1369
1495
  type: "checkbox",
@@ -1379,9 +1505,9 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1379
1505
  }
1380
1506
  }
1381
1507
  ),
1382
- /* @__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 })
1383
1509
  ] }),
1384
- otherActive && /* @__PURE__ */ jsx4(
1510
+ otherActive && /* @__PURE__ */ jsx5(
1385
1511
  "input",
1386
1512
  {
1387
1513
  type: "text",
@@ -1419,9 +1545,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1419
1545
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1420
1546
  const otherActive = isOtherValue || value === "" && otherOn;
1421
1547
  const control = useControl("fillo-input fillo-select", field, error);
1422
- return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1423
- /* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
1424
- /* @__PURE__ */ jsxs4(
1548
+ return /* @__PURE__ */ jsxs5(FieldShell, { field, error, ids, children: [
1549
+ /* @__PURE__ */ jsxs5("div", { className: "fillo-select-wrap", children: [
1550
+ /* @__PURE__ */ jsxs5(
1425
1551
  "select",
1426
1552
  {
1427
1553
  id: ids.inputId,
@@ -1438,15 +1564,15 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1438
1564
  }
1439
1565
  },
1440
1566
  children: [
1441
- /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1442
- options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
1443
- 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` })
1444
1570
  ]
1445
1571
  }
1446
1572
  ),
1447
- /* @__PURE__ */ jsx4("span", { className: "fillo-select-icon", "aria-hidden": "true" })
1573
+ /* @__PURE__ */ jsx5("span", { className: "fillo-select-icon", "aria-hidden": "true" })
1448
1574
  ] }),
1449
- otherActive && /* @__PURE__ */ jsx4(
1575
+ otherActive && /* @__PURE__ */ jsx5(
1450
1576
  "input",
1451
1577
  {
1452
1578
  type: "text",
@@ -1467,19 +1593,19 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1467
1593
  const slots = useFieldSlots(field, error);
1468
1594
  const checkbox = field;
1469
1595
  if (checkbox.appearance === "toggle") {
1470
- return /* @__PURE__ */ jsxs4(
1596
+ return /* @__PURE__ */ jsxs5(
1471
1597
  "div",
1472
1598
  {
1473
1599
  ...slots.wrapperProps(
1474
1600
  `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`
1475
1601
  ),
1476
1602
  children: [
1477
- /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1478
- /* @__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: [
1479
1605
  field.label,
1480
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1606
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1481
1607
  ] }) }),
1482
- /* @__PURE__ */ jsx4(
1608
+ /* @__PURE__ */ jsx5(
1483
1609
  "input",
1484
1610
  {
1485
1611
  id: ids.inputId,
@@ -1492,23 +1618,23 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1492
1618
  ...fieldAria(field, error, ids)
1493
1619
  }
1494
1620
  ),
1495
- /* @__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" }) })
1496
1622
  ] }),
1497
- field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1498
- 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 })
1499
1625
  ]
1500
1626
  }
1501
1627
  );
1502
1628
  }
1503
- return /* @__PURE__ */ jsxs4(
1629
+ return /* @__PURE__ */ jsxs5(
1504
1630
  "div",
1505
1631
  {
1506
1632
  ...slots.wrapperProps(
1507
1633
  `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`
1508
1634
  ),
1509
1635
  children: [
1510
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1511
- /* @__PURE__ */ jsx4(
1636
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1637
+ /* @__PURE__ */ jsx5(
1512
1638
  "input",
1513
1639
  {
1514
1640
  id: ids.inputId,
@@ -1519,13 +1645,13 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1519
1645
  ...fieldAria(field, error, ids)
1520
1646
  }
1521
1647
  ),
1522
- /* @__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: [
1523
1649
  field.label,
1524
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1650
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1525
1651
  ] })
1526
1652
  ] }),
1527
- field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1528
- 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 })
1529
1655
  ]
1530
1656
  }
1531
1657
  );
@@ -1564,7 +1690,7 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1564
1690
  const tabbable = rovingIndex(steps, selected);
1565
1691
  const appearance = useFilloAppearance();
1566
1692
  const control = useControl("fillo-rating", field, error);
1567
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1693
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1568
1694
  "div",
1569
1695
  {
1570
1696
  ...control,
@@ -1572,7 +1698,7 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1572
1698
  "aria-labelledby": ids.labelId,
1573
1699
  ...fieldAria(field, error, ids),
1574
1700
  onKeyDown: (e) => radiogroupKeyDown(e, steps, selected, (n) => setValue(n)),
1575
- children: steps.map((n, i) => /* @__PURE__ */ jsx4(
1701
+ children: steps.map((n, i) => /* @__PURE__ */ jsx5(
1576
1702
  "button",
1577
1703
  {
1578
1704
  type: "button",
@@ -1615,8 +1741,8 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1615
1741
  ].filter(Boolean).join(" ") || void 0;
1616
1742
  const appearance = useFilloAppearance();
1617
1743
  const control = useControl("fillo-scale", field, error);
1618
- return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1619
- /* @__PURE__ */ jsx4(
1744
+ return /* @__PURE__ */ jsxs5(FieldShell, { field, error, ids, children: [
1745
+ /* @__PURE__ */ jsx5(
1620
1746
  "div",
1621
1747
  {
1622
1748
  ...control,
@@ -1626,7 +1752,7 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1626
1752
  "aria-required": field.required ? true : void 0,
1627
1753
  "aria-describedby": describedBy,
1628
1754
  onKeyDown: (e) => radiogroupKeyDown(e, steps, current, (n) => setValue(n)),
1629
- children: steps.map((n, i) => /* @__PURE__ */ jsx4(
1755
+ children: steps.map((n, i) => /* @__PURE__ */ jsx5(
1630
1756
  "button",
1631
1757
  {
1632
1758
  type: "button",
@@ -1649,9 +1775,9 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1649
1775
  ))
1650
1776
  }
1651
1777
  ),
1652
- hasLabels && /* @__PURE__ */ jsxs4("div", { className: "fillo-scale-labels", children: [
1653
- /* @__PURE__ */ jsx4("span", { id: minLabelId, children: scale.minLabel }),
1654
- /* @__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 })
1655
1781
  ] })
1656
1782
  ] });
1657
1783
  }
@@ -1671,21 +1797,21 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
1671
1797
  [next[index], next[target]] = [next[target], next[index]];
1672
1798
  setValue(next);
1673
1799
  }
1674
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1800
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1675
1801
  "div",
1676
1802
  {
1677
1803
  id: ids.inputId,
1678
1804
  role: "group",
1679
1805
  "aria-labelledby": ids.labelId,
1680
1806
  ...fieldAria(field, error, ids),
1681
- 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) => {
1682
1808
  const option = ranking.options.find((o) => o.id === id);
1683
1809
  if (!option) return null;
1684
- return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
1685
- /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
1686
- /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
1687
- /* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
1688
- /* @__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(
1689
1815
  "button",
1690
1816
  {
1691
1817
  type: "button",
@@ -1696,7 +1822,7 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
1696
1822
  children: "\u2191"
1697
1823
  }
1698
1824
  ),
1699
- /* @__PURE__ */ jsx4(
1825
+ /* @__PURE__ */ jsx5(
1700
1826
  "button",
1701
1827
  {
1702
1828
  type: "button",
@@ -1717,7 +1843,7 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1717
1843
  const ids = useFilloFieldIds(field.id, providedIds);
1718
1844
  const matrix = field;
1719
1845
  const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
1720
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1846
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1721
1847
  "div",
1722
1848
  {
1723
1849
  id: ids.inputId,
@@ -1725,12 +1851,12 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1725
1851
  role: "group",
1726
1852
  "aria-labelledby": ids.labelId,
1727
1853
  ...fieldAria(field, error, ids),
1728
- children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
1729
- /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
1730
- /* @__PURE__ */ jsx4("th", {}),
1731
- 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))
1732
1858
  ] }) }),
1733
- /* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
1859
+ /* @__PURE__ */ jsx5("tbody", { children: matrix.rows.map((row) => {
1734
1860
  const rowLabelId = `${ids.inputId}-${row.id}`;
1735
1861
  const answered = answers[row.id] !== void 0;
1736
1862
  const rowInvalid = Boolean(error) && field.required && !answered;
@@ -1741,17 +1867,17 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1741
1867
  // radiogroup lives on an inner element that aria-owns the radios
1742
1868
  // scattered across the cells (they can't be DOM-nested inside a
1743
1869
  // single element within a table row).
1744
- /* @__PURE__ */ jsxs4("tr", { children: [
1745
- /* @__PURE__ */ jsx4("th", { scope: "row", children: /* @__PURE__ */ jsx4(
1870
+ /* @__PURE__ */ jsxs5("tr", { children: [
1871
+ /* @__PURE__ */ jsx5("th", { scope: "row", children: /* @__PURE__ */ jsx5(
1746
1872
  "div",
1747
1873
  {
1748
1874
  role: "radiogroup",
1749
1875
  "aria-labelledby": rowLabelId,
1750
1876
  "aria-owns": matrix.columns.map(radioId).join(" "),
1751
- children: /* @__PURE__ */ jsx4("span", { id: rowLabelId, children: row.label })
1877
+ children: /* @__PURE__ */ jsx5("span", { id: rowLabelId, children: row.label })
1752
1878
  }
1753
1879
  ) }),
1754
- matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
1880
+ matrix.columns.map((col) => /* @__PURE__ */ jsx5("td", { children: /* @__PURE__ */ jsx5(
1755
1881
  "input",
1756
1882
  {
1757
1883
  id: radioId(col),
@@ -1798,11 +1924,11 @@ var DEFAULT_COMPONENTS = {
1798
1924
  function ContentRenderer({ block }) {
1799
1925
  switch (block.kind) {
1800
1926
  case "heading":
1801
- return /* @__PURE__ */ jsx4("h3", { className: "fillo-heading", children: block.text });
1927
+ return /* @__PURE__ */ jsx5("h3", { className: "fillo-heading", children: block.text });
1802
1928
  case "paragraph":
1803
- return /* @__PURE__ */ jsx4("p", { className: "fillo-paragraph", children: block.text });
1929
+ return /* @__PURE__ */ jsx5("p", { className: "fillo-paragraph", children: block.text });
1804
1930
  case "divider":
1805
- return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
1931
+ return /* @__PURE__ */ jsx5("hr", { className: "fillo-divider" });
1806
1932
  }
1807
1933
  }
1808
1934
  function blockHasPiping(block) {
@@ -1841,7 +1967,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1841
1967
  block = pipeBlock(block, api.data, api.form);
1842
1968
  const ids = useFilloFieldIds(block.id);
1843
1969
  const strings = useStrings();
1844
- if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
1970
+ if (!isField(block)) return /* @__PURE__ */ jsx5(ContentRenderer, { block });
1845
1971
  let Component;
1846
1972
  if (block.kind === "custom") {
1847
1973
  Component = customComponents?.[block.component] ?? components?.custom;
@@ -1859,7 +1985,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1859
1985
  }
1860
1986
  const rawError = api.errors[block.id];
1861
1987
  const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
1862
- return /* @__PURE__ */ jsx4(
1988
+ return /* @__PURE__ */ jsx5(
1863
1989
  Component,
1864
1990
  {
1865
1991
  field: block,
@@ -1896,7 +2022,7 @@ function FormField({
1896
2022
  const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
1897
2023
  if (!block) return null;
1898
2024
  if (isField(block) && !visibleIds.has(block.id)) return null;
1899
- return /* @__PURE__ */ jsx4(
2025
+ return /* @__PURE__ */ jsx5(
1900
2026
  BlockRenderer,
1901
2027
  {
1902
2028
  block,
@@ -1909,7 +2035,7 @@ function FormField({
1909
2035
 
1910
2036
  // src/turnstile.tsx
1911
2037
  import { useEffect as useEffect7, useRef as useRef6 } from "react";
1912
- import { jsx as jsx5 } from "react/jsx-runtime";
2038
+ import { jsx as jsx6 } from "react/jsx-runtime";
1913
2039
  var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
1914
2040
  var SCRIPT_ID = "fillo-turnstile-script";
1915
2041
  var scriptPromise = null;
@@ -2016,7 +2142,7 @@ function TurnstileWidget({
2016
2142
  onTokenRef.current(void 0);
2017
2143
  };
2018
2144
  }, [siteKey]);
2019
- return /* @__PURE__ */ jsx5(
2145
+ return /* @__PURE__ */ jsx6(
2020
2146
  "div",
2021
2147
  {
2022
2148
  ref: containerRef,
@@ -2027,7 +2153,7 @@ function TurnstileWidget({
2027
2153
  }
2028
2154
 
2029
2155
  // src/FilloForm.tsx
2030
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2156
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2031
2157
  function themeProps(theme) {
2032
2158
  const safeTheme = normalizeFormTheme(theme);
2033
2159
  if (!safeTheme) return {};
@@ -2044,16 +2170,6 @@ function themeProps(theme) {
2044
2170
  ...hasColorTheme ? { "data-fillo-has-color-theme": "" } : {}
2045
2171
  };
2046
2172
  }
2047
- function safeHttpUrl(href) {
2048
- try {
2049
- if (!href) return null;
2050
- const url = new URL(href);
2051
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
2052
- return url.toString();
2053
- } catch {
2054
- return null;
2055
- }
2056
- }
2057
2173
  function badgeUrl(href) {
2058
2174
  const url = new URL(safeHttpUrl(href) ?? "https://fillo.so");
2059
2175
  url.searchParams.set("utm_source", "form_badge");
@@ -2094,6 +2210,9 @@ function FilloForm(props) {
2094
2210
  );
2095
2211
  const [loaded, setLoaded] = useState6(null);
2096
2212
  const [loadFailure, setLoadFailure] = useState6(null);
2213
+ const devEnv = useIsDevEnv();
2214
+ const devChrome = props.preview === true || devEnv;
2215
+ if (props.preview === true) warnPreviewInProduction();
2097
2216
  const codeForm = isCodeForm2(form) ? form : null;
2098
2217
  const onError = props.onError;
2099
2218
  const {
@@ -2105,14 +2224,19 @@ function FilloForm(props) {
2105
2224
  noticeError: syncNoticeError,
2106
2225
  syncing: codeFormSyncing,
2107
2226
  staged: codeChangesStaged,
2227
+ accepting: syncedAccepting,
2228
+ acceptingReason: syncedAcceptingReason,
2229
+ uploadsAvailable: syncedUploadsAvailable,
2108
2230
  resolvedSchema,
2109
- resolvedTheme
2231
+ resolvedTheme,
2232
+ warningCode: syncedWarningCode,
2233
+ warningUrl: syncedWarningUrl
2110
2234
  } = useCodeFormSync(form, client, onError, {
2111
2235
  // An absent client is intentional render-only use. Builder previews and an
2112
2236
  // explicit canonical id also do not need publishable-key resolution.
2113
2237
  allowUnsynced: Boolean(!client || props.skipValidation || formId)
2114
2238
  });
2115
- const renderResolvedSnapshot = Boolean(resolvedSchema && !isDevEnv());
2239
+ const renderResolvedSnapshot = Boolean(resolvedSchema && !devChrome);
2116
2240
  const inlineSchema = codeForm ? renderResolvedSnapshot ? resolvedSchema : codeForm.schema : form;
2117
2241
  const onErrorRef = useRef7(onError);
2118
2242
  useEffect8(() => {
@@ -2135,6 +2259,9 @@ function FilloForm(props) {
2135
2259
  schema: published.schema,
2136
2260
  theme: published.theme,
2137
2261
  closed: Boolean(published.closed),
2262
+ accepting: published.accepting,
2263
+ acceptingReason: published.acceptingReason,
2264
+ uploadsAvailable: published.uploadsAvailable,
2138
2265
  branding: published.branding ?? null,
2139
2266
  challenge: published.challenge ?? null
2140
2267
  });
@@ -2158,25 +2285,52 @@ function FilloForm(props) {
2158
2285
  const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
2159
2286
  if (normalized && !normalized.ok) {
2160
2287
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
2161
- if (props.renderError) return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(error) });
2162
- return /* @__PURE__ */ jsx6("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 });
2163
2290
  }
2164
2291
  if (loadError) {
2165
2292
  if (props.renderError) {
2166
- return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(loadError) });
2293
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(loadError) });
2167
2294
  }
2168
2295
  const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
2169
- return /* @__PURE__ */ jsx6("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
+ );
2170
2324
  }
2171
2325
  if (currentLoaded?.closed) {
2172
- return /* @__PURE__ */ jsx6("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx6("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 }) });
2173
2327
  }
2174
2328
  const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
2175
2329
  if (canonicalSyncError && props.renderError) {
2176
- return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(canonicalSyncError) });
2330
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(canonicalSyncError) });
2177
2331
  }
2178
- if (canonicalSyncError && !isDevEnv()) {
2179
- return /* @__PURE__ */ jsx6(
2332
+ if (canonicalSyncError && !devChrome) {
2333
+ return /* @__PURE__ */ jsx7(
2180
2334
  "div",
2181
2335
  {
2182
2336
  className: `fillo-form fillo-form--error ${props.className ?? ""}`,
@@ -2189,23 +2343,42 @@ function FilloForm(props) {
2189
2343
  );
2190
2344
  }
2191
2345
  const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
2192
- if (notPublished && !isDevEnv()) {
2193
- const error = new FilloError2("This form isn't published yet.", 403);
2194
- if (props.renderError) return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(error) });
2195
- return /* @__PURE__ */ jsx6("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx6("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 }) });
2196
2369
  }
2197
2370
  const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
2198
- if (!isDevEnv() && awaitingCanonical || !schema) {
2199
- return /* @__PURE__ */ jsxs5(
2371
+ if (!devChrome && awaitingCanonical || !schema) {
2372
+ return /* @__PURE__ */ jsxs6(
2200
2373
  "div",
2201
2374
  {
2202
2375
  className: `fillo-form fillo-form--loading ${props.className ?? ""}`,
2203
2376
  "aria-busy": "true",
2204
2377
  ...themeProps(theme),
2205
2378
  children: [
2206
- /* @__PURE__ */ jsx6("div", { className: "fillo-skeleton" }),
2207
- /* @__PURE__ */ jsx6("div", { className: "fillo-skeleton" }),
2208
- /* @__PURE__ */ jsx6("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" })
2209
2382
  ]
2210
2383
  }
2211
2384
  );
@@ -2213,7 +2386,7 @@ function FilloForm(props) {
2213
2386
  const branding = codeForm ? syncedBranding : currentLoaded?.branding;
2214
2387
  const poweredBy = branding?.poweredBy ?? true;
2215
2388
  const challenge = props.challenge ?? (codeForm ? syncedChallenge : currentLoaded?.challenge) ?? void 0;
2216
- return /* @__PURE__ */ jsx6(
2389
+ return /* @__PURE__ */ jsx7(
2217
2390
  ResolvedForm,
2218
2391
  {
2219
2392
  ...props,
@@ -2227,57 +2400,80 @@ function FilloForm(props) {
2227
2400
  draftNotice: notPublished,
2228
2401
  stagedNotice: codeChangesStaged,
2229
2402
  syncErrorNotice: syncError ?? syncNoticeError ?? void 0,
2403
+ uploadsAvailable: codeForm ? syncedUploadsAvailable : currentLoaded?.uploadsAvailable,
2404
+ syncWarningCode: codeForm ? syncedWarningCode : void 0,
2405
+ syncWarningUrl: syncedWarningUrl,
2230
2406
  challenge
2231
2407
  }
2232
2408
  );
2233
2409
  }
2234
2410
  function PoweredBy({ label, href }) {
2235
- return /* @__PURE__ */ jsx6("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
2236
- }
2237
- function isDevEnv() {
2238
- return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2239
- }
2240
- function DevNoClientNotice() {
2241
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2242
- /* @__PURE__ */ jsx6("strong", { children: "No Fillo client connected." }),
2243
- " This form is render-only and won\u2019t save responses. Pass a ",
2244
- /* @__PURE__ */ jsx6("code", { children: "client" }),
2245
- " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
2246
- ] });
2411
+ return /* @__PURE__ */ jsx7("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
2247
2412
  }
2248
- function DevDraftNotice() {
2249
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2250
- /* @__PURE__ */ jsx6("strong", { children: "Draft form." }),
2251
- " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
2252
- ] });
2413
+ function overlayVariant(reason) {
2414
+ return reason === "expired" || reason === "capped" || reason === "storage_full" ? "closed" : "notOpen";
2253
2415
  }
2254
- function DevStagedNotice() {
2255
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2256
- /* @__PURE__ */ jsx6("strong", { children: "Code changes are staged." }),
2257
- " This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
2258
- ] });
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
+ );
2259
2457
  }
2260
- function DevSyncErrorNotice({ error }) {
2261
- return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2262
- /* @__PURE__ */ jsxs5("strong", { children: [
2263
- "Form sync needs attention",
2264
- error.code ? ` (${error.code})` : "",
2265
- "."
2266
- ] }),
2267
- " ",
2268
- error.message,
2269
- " The safe form remains available for development."
2270
- ] });
2458
+ var noopSubscribe = () => () => {
2459
+ };
2460
+ function useIsDevEnv() {
2461
+ return useSyncExternalStore2(noopSubscribe, isLikelyDevEnv, isBuildTimeDevEnv2);
2271
2462
  }
2272
2463
  function ResolvedForm(props) {
2273
2464
  const { schema, theme } = props;
2465
+ const displayOnly = props.displayOnly === true;
2274
2466
  const instanceId = useId2().replace(/:/g, "");
2467
+ const devEnv = useIsDevEnv();
2468
+ const devChrome = !displayOnly && (props.preview === true || devEnv);
2275
2469
  const hpRef = useRef7(null);
2276
2470
  const codeForm = isCodeForm2(props.form) ? props.form : null;
2277
2471
  const client = props.client;
2278
2472
  const challenge = props.challenge;
2279
2473
  const challengeRequired = Boolean(challenge);
2280
2474
  const challengeTokenRef = useRef7(void 0);
2475
+ const [resyncWarning, setResyncWarning] = useState6(null);
2476
+ const [resolutionFailureCode, setResolutionFailureCode] = useState6(null);
2281
2477
  const [challengeToken, setChallengeToken] = useState6(void 0);
2282
2478
  const [challengeError, setChallengeError] = useState6(false);
2283
2479
  const resetChallengeRef = useRef7(void 0);
@@ -2295,6 +2491,11 @@ function ResolvedForm(props) {
2295
2491
  async () => {
2296
2492
  try {
2297
2493
  const result = await syncCodeForm2(client, codeForm, { bypassCache: true });
2494
+ setResyncWarning({
2495
+ code: result.warningCode,
2496
+ url: result.warningUrl,
2497
+ uploadsAvailable: result.uploadsAvailable
2498
+ });
2298
2499
  if (result.status === "draft") {
2299
2500
  throw new FilloError2(
2300
2501
  "This form is no longer published. Submission was stopped before sending answers.",
@@ -2303,6 +2504,14 @@ function ResolvedForm(props) {
2303
2504
  "form_not_published"
2304
2505
  );
2305
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
+ }
2306
2515
  const authoritative = normalizeFormSchema(result.resolvedSchema ?? codeForm.schema);
2307
2516
  if (!authoritative.ok) {
2308
2517
  throw new FilloError2(
@@ -2332,9 +2541,11 @@ function ResolvedForm(props) {
2332
2541
  );
2333
2542
  props.onError?.(notice);
2334
2543
  }
2544
+ setResolutionFailureCode(null);
2335
2545
  return result.formId;
2336
2546
  } catch (error) {
2337
2547
  const failure = isFilloError2(error) ? error : new FilloError2(error instanceof Error ? error.message : String(error), 0);
2548
+ setResolutionFailureCode(failure.code ?? "sync_failed");
2338
2549
  console.warn(
2339
2550
  `[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
2340
2551
  );
@@ -2358,6 +2569,9 @@ function ResolvedForm(props) {
2358
2569
  // "default", never the bare-engine "headless".
2359
2570
  surface: "default",
2360
2571
  resolveFormId,
2572
+ // Dev chrome gets the developer-grade resolution failure (message + code)
2573
+ // instead of the respondent-safe "unavailable" fallback.
2574
+ verboseResolutionErrors: devChrome,
2361
2575
  respondent: props.respondent,
2362
2576
  challengeRequired,
2363
2577
  getChallengeToken: () => challengeTokenRef.current,
@@ -2366,8 +2580,20 @@ function ResolvedForm(props) {
2366
2580
  resetChallengeRef.current?.();
2367
2581
  }
2368
2582
  });
2369
- useDraftFlush(api);
2583
+ useDraftFlush(api, !displayOnly);
2370
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
+ );
2371
2597
  const settings = schema.settings;
2372
2598
  const submitted = api.status === "submitted";
2373
2599
  const liveSubmit = submitted && !api.restoredSubmission;
@@ -2390,7 +2616,7 @@ function ResolvedForm(props) {
2390
2616
  const appearance = props.appearance;
2391
2617
  const strings = resolveStrings(props.strings);
2392
2618
  if (api.status === "submitted") {
2393
- return /* @__PURE__ */ jsxs5(
2619
+ return /* @__PURE__ */ jsxs6(
2394
2620
  "div",
2395
2621
  {
2396
2622
  ref: successRef,
@@ -2406,19 +2632,19 @@ function ResolvedForm(props) {
2406
2632
  "data-state": "submitted",
2407
2633
  ...themed,
2408
2634
  children: [
2409
- props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5(
2635
+ props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs6(
2410
2636
  "div",
2411
2637
  {
2412
2638
  className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
2413
2639
  "data-fillo": "success",
2414
2640
  children: [
2415
- /* @__PURE__ */ jsx6("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
2416
- /* @__PURE__ */ jsx6("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
2417
- /* @__PURE__ */ jsx6("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 })
2418
2644
  ]
2419
2645
  }
2420
2646
  ),
2421
- props.poweredBy && /* @__PURE__ */ jsx6(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2647
+ props.poweredBy && /* @__PURE__ */ jsx7(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2422
2648
  ]
2423
2649
  }
2424
2650
  );
@@ -2434,13 +2660,162 @@ function ResolvedForm(props) {
2434
2660
  const progressTotal = Math.max(progressSeq.length, 1);
2435
2661
  const progressPos = progressSeq.indexOf(api.page.id);
2436
2662
  const progressStep = progressPos >= 0 ? progressPos + 1 : Math.min(api.pageIndex + 1, progressTotal);
2437
- return /* @__PURE__ */ jsx6(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx6(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx6(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx6(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
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(
2438
2816
  "form",
2439
2817
  {
2440
- className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
2441
- slot: "root",
2442
- status: api.status
2443
- }),
2818
+ className: rootClass,
2444
2819
  "data-fillo": "root",
2445
2820
  "data-state": api.status,
2446
2821
  "data-page": api.pageIndex + 1,
@@ -2458,134 +2833,9 @@ function ResolvedForm(props) {
2458
2833
  focusFirstInvalid(formEl);
2459
2834
  }
2460
2835
  },
2461
- children: [
2462
- !props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx6(DevNoClientNotice, {}),
2463
- props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx6(DevDraftNotice, {}),
2464
- props.stagedNotice && isDevEnv() && /* @__PURE__ */ jsx6(DevStagedNotice, {}),
2465
- props.syncErrorNotice && isDevEnv() && /* @__PURE__ */ jsx6(DevSyncErrorNotice, { error: props.syncErrorNotice }),
2466
- (api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs5(
2467
- "div",
2468
- {
2469
- className: slotClass3("fillo-resume", appearance, { slot: "resume" }),
2470
- "data-fillo": "resume",
2471
- role: "status",
2472
- children: [
2473
- /* @__PURE__ */ jsx6("span", { className: "fillo-resume-text", children: api.editingPrevious ? strings.editNotice : strings.resumeNotice }),
2474
- /* @__PURE__ */ jsx6("button", { type: "button", className: "fillo-resume-clear", onClick: api.resetDraft, children: strings.resumeStartOver })
2475
- ]
2476
- }
2477
- ),
2478
- api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx6(
2479
- "div",
2480
- {
2481
- className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
2482
- "data-fillo": "resume",
2483
- role: "status",
2484
- children: /* @__PURE__ */ jsx6("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
2485
- }
2486
- ),
2487
- multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx6(
2488
- "div",
2489
- {
2490
- className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
2491
- "data-fillo": "progress",
2492
- role: "progressbar",
2493
- "aria-label": schema.title || "Form progress",
2494
- "aria-valuemin": 1,
2495
- "aria-valuemax": progressTotal,
2496
- "aria-valuenow": progressStep,
2497
- children: /* @__PURE__ */ jsx6(
2498
- "div",
2499
- {
2500
- className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
2501
- "data-fillo": "progressFill",
2502
- style: { "--fillo-progress-value": `${progressStep / progressTotal * 100}%` }
2503
- }
2504
- )
2505
- }
2506
- ),
2507
- api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
2508
- schema.title && /* @__PURE__ */ jsx6("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2509
- schema.description && /* @__PURE__ */ jsx6(
2510
- "p",
2511
- {
2512
- className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2513
- "data-fillo": "description",
2514
- children: schema.description
2515
- }
2516
- )
2517
- ] }),
2518
- multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx6("h2", { className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }), "data-fillo": "pageTitle", children: api.page.title }),
2519
- /* @__PURE__ */ jsx6("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx6(
2520
- BlockRenderer,
2521
- {
2522
- block,
2523
- api,
2524
- components: props.components,
2525
- customComponents: props.customComponents
2526
- },
2527
- block.id
2528
- )) }),
2529
- /* @__PURE__ */ jsx6(
2530
- "input",
2531
- {
2532
- type: "text",
2533
- name: "fillo_hp_field",
2534
- className: "fillo-hp",
2535
- tabIndex: -1,
2536
- autoComplete: "off",
2537
- "aria-hidden": "true",
2538
- defaultValue: "",
2539
- style: HP_STYLE,
2540
- ref: (element) => {
2541
- hpRef.current = element;
2542
- assertHpHidden(element);
2543
- }
2544
- }
2545
- ),
2546
- challenge && showChallenge && /* @__PURE__ */ jsxs5("div", { className: "fillo-turnstile-slot", "data-fillo": "turnstile-slot", children: [
2547
- /* @__PURE__ */ jsx6(
2548
- TurnstileWidget,
2549
- {
2550
- siteKey: challenge.siteKey,
2551
- className: slotClass3("fillo-turnstile", appearance, { slot: "turnstile" }),
2552
- onToken: handleChallengeToken,
2553
- onError: () => setChallengeError(true),
2554
- registerReset: registerChallengeReset
2555
- }
2556
- ),
2557
- challengeError && /* @__PURE__ */ jsx6("p", { className: "fillo-turnstile-error", role: "alert", children: strings.challengeUnavailable })
2558
- ] }),
2559
- (api.status === "error" || api.submitError) && /* @__PURE__ */ jsx6("p", { className: "fillo-submit-error", role: "alert", children: api.submitError ?? strings.submitFailed }),
2560
- showFooter && /* @__PURE__ */ jsxs5("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
2561
- multiPage && !api.isFirstPage && /* @__PURE__ */ jsx6(
2562
- "button",
2563
- {
2564
- type: "button",
2565
- className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
2566
- "data-fillo": "button",
2567
- onClick: api.back,
2568
- children: strings.back
2569
- }
2570
- ),
2571
- showPrimaryButton && /* @__PURE__ */ jsx6(
2572
- "button",
2573
- {
2574
- type: "submit",
2575
- className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
2576
- "data-fillo": "button",
2577
- disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
2578
- // "Next"). The server rejects a tokenless submit regardless —
2579
- // this is UX only.
2580
- showChallenge && !challengeToken,
2581
- children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
2582
- }
2583
- )
2584
- ] }),
2585
- props.poweredBy && /* @__PURE__ */ jsx6(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2586
- ]
2836
+ children: formBody
2587
2837
  }
2588
- ) }) }) }) });
2838
+ ) }) }) }) }) });
2589
2839
  }
2590
2840
 
2591
2841
  // src/jsx.tsx
@@ -2593,17 +2843,18 @@ import { useRef as useRef8 } from "react";
2593
2843
  import {
2594
2844
  codeFormFromJsx,
2595
2845
  FilloJsxError,
2846
+ isLikelyDevEnv as isLikelyDevEnv2,
2596
2847
  JSX_BLOCK_COMPONENTS
2597
2848
  } from "@usefillo/core";
2598
- import { jsx as jsx7 } from "react/jsx-runtime";
2849
+ import { jsx as jsx8 } from "react/jsx-runtime";
2599
2850
  var B = JSX_BLOCK_COMPONENTS;
2600
- var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2851
+ var isDevEnv = () => isLikelyDevEnv2();
2601
2852
  function JsxForm(props) {
2602
2853
  const { id, title, description, settings, theme, client, children, ...rest } = props;
2603
2854
  const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
2604
2855
  const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
2605
2856
  const memo2 = useRef8(null);
2606
- if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
2857
+ if (memo2.current && memo2.current.json !== json && isDevEnv()) {
2607
2858
  console.warn(
2608
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.`
2609
2860
  );
@@ -2611,7 +2862,7 @@ function JsxForm(props) {
2611
2862
  if (!memo2.current || memo2.current.json !== json) {
2612
2863
  memo2.current = { json, form: compiled };
2613
2864
  }
2614
- return /* @__PURE__ */ jsx7(FilloForm, { form: memo2.current.form, client, ...rest });
2865
+ return /* @__PURE__ */ jsx8(FilloForm, { form: memo2.current.form, client, ...rest });
2615
2866
  }
2616
2867
  function defineFormFromJsx(element) {
2617
2868
  if (!element || element.type !== JsxForm) {
@@ -2655,7 +2906,7 @@ var Fillo = {
2655
2906
  };
2656
2907
 
2657
2908
  // src/provider.tsx
2658
- import { useId as useId3, useMemo as useMemo6 } from "react";
2909
+ import { useId as useId3, useMemo as useMemo6, useState as useState7 } from "react";
2659
2910
  import {
2660
2911
  FilloError as FilloError3,
2661
2912
  formSchemasEqual as formSchemasEqual2,
@@ -2664,7 +2915,7 @@ import {
2664
2915
  resolveStrings as resolveStrings2,
2665
2916
  syncCodeForm as syncCodeForm3
2666
2917
  } from "@usefillo/core";
2667
- import { Fragment as Fragment3, jsx as jsx8 } from "react/jsx-runtime";
2918
+ import { Fragment as Fragment4, jsx as jsx9 } from "react/jsx-runtime";
2668
2919
  function FilloProvider({
2669
2920
  children,
2670
2921
  form,
@@ -2674,20 +2925,30 @@ function FilloProvider({
2674
2925
  strings,
2675
2926
  onError,
2676
2927
  renderError,
2928
+ preview,
2677
2929
  ...options
2678
2930
  }) {
2679
2931
  const instanceId = useId3().replace(/:/g, "");
2932
+ const devEnv = useIsDevEnv();
2933
+ const devChrome = preview === true || devEnv;
2934
+ if (preview === true) warnPreviewInProduction();
2680
2935
  const codeForm = isCodeForm2(form) ? form : null;
2681
2936
  const {
2682
2937
  formId: syncedFormId,
2683
2938
  status: syncedStatus,
2684
2939
  error: syncError,
2685
2940
  syncing: codeFormSyncing,
2686
- resolvedSchema
2941
+ resolvedSchema,
2942
+ accepting: syncedAccepting,
2943
+ acceptingReason: syncedAcceptingReason,
2944
+ uploadsAvailable,
2945
+ warningCode,
2946
+ warningUrl
2687
2947
  } = useCodeFormSync(form, client, onError, {
2688
2948
  allowUnsynced: Boolean(!client || formId || options.skipValidation)
2689
2949
  });
2690
- 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;
2691
2952
  const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema]);
2692
2953
  if (!normalized.ok) {
2693
2954
  throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
@@ -2697,6 +2958,11 @@ function FilloProvider({
2697
2958
  () => codeForm && client?.key ? async () => {
2698
2959
  try {
2699
2960
  const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
2961
+ setResyncWarning({
2962
+ code: result.warningCode,
2963
+ url: result.warningUrl,
2964
+ uploadsAvailable: result.uploadsAvailable
2965
+ });
2700
2966
  if (result.status === "draft") {
2701
2967
  throw new FilloError3(
2702
2968
  "This form is no longer published. Submission was stopped before sending answers.",
@@ -2705,6 +2971,14 @@ function FilloProvider({
2705
2971
  "form_not_published"
2706
2972
  );
2707
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
+ }
2708
2982
  const authoritative = normalizeFormSchema2(result.resolvedSchema ?? codeForm.schema);
2709
2983
  if (!authoritative.ok) {
2710
2984
  throw new FilloError3(
@@ -2752,25 +3026,43 @@ function FilloProvider({
2752
3026
  form: schema,
2753
3027
  formId: syncedFormId ?? formId,
2754
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,
2755
3032
  // Headless = your markup, no Fillo-rendered layout. Free like every embed
2756
3033
  // method; recorded per response so usage stays measurable.
2757
3034
  surface: "headless"
2758
3035
  });
2759
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
+ );
2760
3049
  const resolvedStrings = resolveStrings2(strings);
2761
3050
  const canonicalSyncError = syncError && !options.skipValidation ? syncError : null;
2762
- if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx8(Fragment3, { children: renderError(canonicalSyncError) });
2763
- if (canonicalSyncError && !isDevEnv3()) return null;
2764
- 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;
2765
3054
  const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !options.skipValidation;
2766
- if (notPublished && !isDevEnv3()) {
2767
- const error = new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published");
2768
- return renderError ? /* @__PURE__ */ jsx8(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;
2769
3064
  }
2770
- return /* @__PURE__ */ jsx8(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx8(FilloStringsContext.Provider, { value: resolvedStrings, children: /* @__PURE__ */ jsx8(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx8(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
2771
- }
2772
- function isDevEnv3() {
2773
- 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 }) }) }) }) });
2774
3066
  }
2775
3067
 
2776
3068
  // src/index.ts