@usefillo/react 0.9.0 → 0.11.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,19 +1,31 @@
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 useRef8,
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,
18
+ isField as isField2,
9
19
  isFilloError as isFilloError2,
20
+ isLikelyDevEnv,
10
21
  needsExplicitSubmit,
11
22
  normalizeFormSchema,
12
23
  normalizeFormTheme,
13
24
  reachableFields,
14
- reachablePageSequence
25
+ reachablePageSequence,
26
+ resolveThemeAppearance
15
27
  } from "@usefillo/core";
16
- import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
28
+ import { REQUIRED_FIELD_MESSAGE as REQUIRED_FIELD_MESSAGE2, resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
17
29
 
18
30
  // src/appearance.tsx
19
31
  import { createContext, useContext } from "react";
@@ -61,11 +73,96 @@ function useStrings() {
61
73
  return useContext(FilloStringsContext);
62
74
  }
63
75
 
76
+ // src/chrome.tsx
77
+ import { isBuildTimeDevEnv } from "@usefillo/core";
78
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
79
+ function safeHttpUrl(href) {
80
+ try {
81
+ if (!href) return null;
82
+ const url = new URL(href);
83
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
84
+ return url.toString();
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+ var warnedPreviewInProduction = false;
90
+ function warnPreviewInProduction() {
91
+ if (warnedPreviewInProduction || isBuildTimeDevEnv()) return;
92
+ warnedPreviewInProduction = true;
93
+ console.warn(
94
+ "[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."
95
+ );
96
+ }
97
+ function PreviewBadge() {
98
+ return /* @__PURE__ */ jsx("span", { className: "fillo-preview-badge", "data-fillo": "preview-badge", children: "Preview" });
99
+ }
100
+ function DevNoClientNotice() {
101
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
102
+ /* @__PURE__ */ jsx("strong", { children: "No Fillo client connected." }),
103
+ " This form is render-only and won\u2019t save responses. Pass a ",
104
+ /* @__PURE__ */ jsx("code", { children: "client" }),
105
+ " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development and preview.)"
106
+ ] });
107
+ }
108
+ function DevDraftNotice({ warningUrl }) {
109
+ const safeUrl = safeHttpUrl(warningUrl);
110
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
111
+ /* @__PURE__ */ jsx("strong", { children: "Draft form." }),
112
+ " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard.",
113
+ " ",
114
+ safeUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
115
+ "Connect storage to publish:",
116
+ " ",
117
+ /* @__PURE__ */ jsx("a", { href: safeUrl, target: "_blank", rel: "noopener noreferrer", children: safeUrl }),
118
+ ".",
119
+ " "
120
+ ] }),
121
+ "(This notice only appears in development and preview.)"
122
+ ] });
123
+ }
124
+ function DevStagedNotice() {
125
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
126
+ /* @__PURE__ */ jsx("strong", { children: "Code changes are staged." }),
127
+ " This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
128
+ ] });
129
+ }
130
+ function DevSyncErrorNotice({ error }) {
131
+ return /* @__PURE__ */ jsxs("div", { className: "fillo-devwarning", role: "alert", children: [
132
+ /* @__PURE__ */ jsxs("strong", { children: [
133
+ "Form sync needs attention",
134
+ error.code ? ` (${error.code})` : "",
135
+ "."
136
+ ] }),
137
+ " ",
138
+ error.message,
139
+ " The safe form remains available for development."
140
+ ] });
141
+ }
142
+ function DevChrome(props) {
143
+ 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;
144
+ if (!props.preview && !notice) return null;
145
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
146
+ props.preview && /* @__PURE__ */ jsx(PreviewBadge, {}),
147
+ notice
148
+ ] });
149
+ }
150
+
64
151
  // src/context.ts
65
152
  import { allFields } from "@usefillo/core";
66
153
  import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
67
154
  var FilloContext = createContext2(null);
68
155
  var FilloInstanceIdContext = createContext2(null);
156
+ var FilloChromeContext = createContext2(null);
157
+ function useFilloChrome() {
158
+ return useContext2(FilloChromeContext);
159
+ }
160
+ var noopAnnounce = () => {
161
+ };
162
+ var FilloAnnounceContext = createContext2(noopAnnounce);
163
+ function useFilloAnnounce() {
164
+ return useContext2(FilloAnnounceContext);
165
+ }
69
166
  function useFilloInstanceId() {
70
167
  const instanceId = useContext2(FilloInstanceIdContext);
71
168
  const fallbackId = useId().replace(/:/g, "");
@@ -128,6 +225,12 @@ function useFilloController(options) {
128
225
  surface: options.surface ?? "headless",
129
226
  // Wrapped so the engine (created once) always calls the latest resolver.
130
227
  resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0,
228
+ // A getter, not a captured boolean: the dev-chrome gate upgrades right
229
+ // after hydration (build-time snapshot → hostname-aware check), but the
230
+ // engine is created once at mount and reads this at submit time.
231
+ get verboseResolutionErrors() {
232
+ return optsRef.current.verboseResolutionErrors;
233
+ },
131
234
  respondent: options.respondent,
132
235
  // Challenge wiring read through optsRef so the engine (created once) sees
133
236
  // the latest token/reset each submit — the widget solves after mount.
@@ -164,11 +267,11 @@ function useFilloController(options) {
164
267
  [store, state, options.form, options.formId, options.client]
165
268
  );
166
269
  }
167
- function useDraftFlush(api) {
168
- const enabled = Boolean(api.form.settings.saveProgress);
270
+ function useDraftFlush(api, enabled = true) {
271
+ const active = enabled && Boolean(api.form.settings.saveProgress);
169
272
  const flush = api.flushDraft;
170
273
  useEffect(() => {
171
- if (!enabled || typeof window === "undefined") return;
274
+ if (!active || typeof window === "undefined") return;
172
275
  const onPageHide = () => flush();
173
276
  const onVisibility = () => {
174
277
  if (document.visibilityState === "hidden") flush();
@@ -179,7 +282,7 @@ function useDraftFlush(api) {
179
282
  window.removeEventListener("pagehide", onPageHide);
180
283
  document.removeEventListener("visibilitychange", onVisibility);
181
284
  };
182
- }, [enabled, flush]);
285
+ }, [active, flush]);
183
286
  }
184
287
 
185
288
  // src/define.ts
@@ -230,8 +333,15 @@ function useCodeFormSync(form, client, onError, options) {
230
333
  error: null,
231
334
  noticeError: null,
232
335
  staged: false,
336
+ accepting: void 0,
337
+ acceptingReason: void 0,
338
+ uploadsAvailable: void 0,
339
+ uploadFileSizeLimitMb: void 0,
233
340
  resolvedSchema: void 0,
234
- resolvedTheme: void 0
341
+ resolvedTheme: void 0,
342
+ warning: void 0,
343
+ warningCode: void 0,
344
+ warningUrl: void 0
235
345
  } : {
236
346
  key: syncKey,
237
347
  client,
@@ -286,11 +396,22 @@ function useCodeFormSync(form, client, onError, options) {
286
396
  error: null,
287
397
  noticeError,
288
398
  staged: Boolean(r.staged),
399
+ accepting: r.accepting,
400
+ acceptingReason: r.acceptingReason,
401
+ uploadsAvailable: r.uploadsAvailable,
402
+ uploadFileSizeLimitMb: r.uploadFileSizeLimitMb,
289
403
  resolvedSchema: r.resolvedSchema,
290
- resolvedTheme: r.resolvedTheme
404
+ resolvedTheme: r.resolvedTheme,
405
+ warning: r.warning,
406
+ warningCode: r.warningCode,
407
+ warningUrl: r.warningUrl
291
408
  });
292
409
  const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
293
- if (r.warning) console.warn(`[fillo] ${r.warning}`);
410
+ if (r.warning) {
411
+ console.warn(
412
+ `[fillo] ${r.warning}${r.warningUrl ? ` Connect storage to publish: ${r.warningUrl}` : ""}`
413
+ );
414
+ }
294
415
  if (noticeError) {
295
416
  console.warn(
296
417
  `[fillo] form sync needs attention (${noticeError.code}): ${noticeError.message}`
@@ -337,8 +458,15 @@ function useCodeFormSync(form, client, onError, options) {
337
458
  noticeError: synced.noticeError,
338
459
  syncing: Boolean(syncKey) && !synced.settled,
339
460
  staged: synced.staged,
461
+ accepting: synced.accepting,
462
+ acceptingReason: synced.acceptingReason,
463
+ uploadsAvailable: synced.uploadsAvailable,
464
+ uploadFileSizeLimitMb: synced.uploadFileSizeLimitMb,
340
465
  resolvedSchema: synced.resolvedSchema,
341
- resolvedTheme: synced.resolvedTheme
466
+ resolvedTheme: synced.resolvedTheme,
467
+ warning: synced.warning,
468
+ warningCode: synced.warningCode,
469
+ warningUrl: synced.warningUrl
342
470
  } : {
343
471
  formId: null,
344
472
  status: null,
@@ -350,12 +478,25 @@ function useCodeFormSync(form, client, onError, options) {
350
478
  }
351
479
 
352
480
  // src/fields.tsx
353
- import { isField, pipeBlock, REQUIRED_FIELD_MESSAGE, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
354
- import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
481
+ import {
482
+ formatAnswer,
483
+ formatGroupedNumber,
484
+ isField,
485
+ isValidPartialNumberText,
486
+ parseGroupedNumber,
487
+ pipeBlock,
488
+ radioGroupStep,
489
+ REQUIRED_FIELD_MESSAGE,
490
+ shouldAutoSubmit,
491
+ slotClass as slotClass2,
492
+ visibleFields
493
+ } from "@usefillo/core";
494
+ import { memo, useEffect as useEffect6, useLayoutEffect as useLayoutEffect2, useMemo as useMemo4, useRef as useRef6, useState as useState5 } from "react";
355
495
 
356
496
  // src/phone.tsx
357
497
  import {
358
498
  PHONE_COUNTRIES,
499
+ PHONE_PICKER_COUNTRIES,
359
500
  countryByIso,
360
501
  countryByTimeZone,
361
502
  digitsOnly,
@@ -366,7 +507,7 @@ import {
366
507
  toE164
367
508
  } from "@usefillo/core";
368
509
  import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
369
- import { jsx, jsxs } from "react/jsx-runtime";
510
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
370
511
  function localeCountry() {
371
512
  if (typeof navigator === "undefined") return void 0;
372
513
  const tag = navigator.languages?.[0] || navigator.language || "";
@@ -407,8 +548,9 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
407
548
  const guess = countryByTimeZone(browserTimeZone()) ?? countryByIso(localeCountry());
408
549
  if (guess) setCountry(guess);
409
550
  }, []);
551
+ const [pendingRaw, setPendingRaw] = useState2(null);
410
552
  const national = nationalFor(stored, country);
411
- const display = formatNational(country, national);
553
+ const display = pendingRaw ?? formatNational(country, national);
412
554
  const inputRef = useRef3(null);
413
555
  const caretDigits = useRef3(null);
414
556
  useLayoutEffect(() => {
@@ -435,29 +577,37 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
435
577
  const caret = el.selectionStart ?? raw.length;
436
578
  caretDigits.current = digitsOnly(raw.slice(0, caret)).length;
437
579
  if (raw.trimStart().startsWith("+")) {
438
- const p = parsePhone(raw);
580
+ const p = parsePhone(raw, country, { previousDigits: national });
581
+ if (p.pending) {
582
+ setPendingRaw(p.raw ?? raw);
583
+ setValue(p.raw ?? raw);
584
+ return;
585
+ }
586
+ setPendingRaw(null);
439
587
  if (p.country) setCountry(p.country);
440
588
  commit(p.country ?? country, p.country ? p.national : digitsOnly(raw));
441
589
  return;
442
590
  }
591
+ setPendingRaw(null);
443
592
  commit(country, digitsOnly(raw));
444
593
  }
445
594
  function pickCountry(c) {
446
595
  touched.current = true;
596
+ setPendingRaw(null);
447
597
  setCountry(c);
448
598
  commit(c, national);
449
599
  inputRef.current?.focus();
450
600
  }
451
601
  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: [
602
+ return /* @__PURE__ */ jsxs2("div", { ...slots.wrapperProps(`fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`), children: [
603
+ /* @__PURE__ */ jsxs2("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
454
604
  field.label,
455
- !field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: strings.optional })
605
+ !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
456
606
  ] }),
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(
607
+ field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
608
+ /* @__PURE__ */ jsxs2("div", { className: "fillo-phone", children: [
609
+ /* @__PURE__ */ jsx2(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
610
+ /* @__PURE__ */ jsx2(
461
611
  "input",
462
612
  {
463
613
  ref: inputRef,
@@ -476,7 +626,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
476
626
  }
477
627
  )
478
628
  ] }),
479
- error && /* @__PURE__ */ jsx("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
629
+ error && /* @__PURE__ */ jsx2("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
480
630
  ] });
481
631
  }
482
632
  function CountrySelect({
@@ -484,6 +634,8 @@ function CountrySelect({
484
634
  onPick,
485
635
  labelledBy
486
636
  }) {
637
+ const strings = useStrings();
638
+ const announce = useFilloAnnounce();
487
639
  const [open, setOpen] = useState2(false);
488
640
  const [query, setQuery] = useState2("");
489
641
  const [active, setActive] = useState2(0);
@@ -492,19 +644,22 @@ function CountrySelect({
492
644
  const triggerRef = useRef3(null);
493
645
  const popoverRef = useRef3(null);
494
646
  const searchRef = useRef3(null);
647
+ const seedRef = useRef3(null);
495
648
  const listId = `${labelledBy}-countries`;
496
649
  const matches = useMemo3(() => {
497
650
  const q = query.trim().toLowerCase();
498
- if (!q) return PHONE_COUNTRIES;
651
+ if (!q) return PHONE_PICKER_COUNTRIES;
499
652
  const qDigits = q.replace(/[^\d]/g, "");
500
- return PHONE_COUNTRIES.filter(
653
+ return PHONE_PICKER_COUNTRIES.filter(
501
654
  (c) => c.name.toLowerCase().includes(q) || c.iso2.toLowerCase() === q || qDigits.length > 0 && c.dialCode.startsWith(qDigits)
502
655
  );
503
656
  }, [query]);
504
657
  useEffect3(() => {
505
658
  if (!open) return;
506
- setQuery("");
507
- setActive(Math.max(0, PHONE_COUNTRIES.indexOf(country)));
659
+ const seed = seedRef.current ?? "";
660
+ seedRef.current = null;
661
+ setQuery(seed);
662
+ setActive(seed ? 0 : Math.max(0, PHONE_PICKER_COUNTRIES.indexOf(country)));
508
663
  const id = requestAnimationFrame(() => searchRef.current?.focus());
509
664
  return () => cancelAnimationFrame(id);
510
665
  }, [open, country]);
@@ -531,8 +686,15 @@ function CountrySelect({
531
686
  if (!open) return;
532
687
  document.getElementById(`${listId}-opt-${active}`)?.scrollIntoView({ block: "nearest" });
533
688
  }, [active, open, listId]);
689
+ const resultsAnnouncement = strings.phoneResultsCount(matches.length);
690
+ useEffect3(() => {
691
+ if (!open || !query.trim()) return;
692
+ const id = setTimeout(() => announce(resultsAnnouncement), 300);
693
+ return () => clearTimeout(id);
694
+ }, [open, query, resultsAnnouncement, announce]);
534
695
  function choose(c) {
535
696
  if (!c) return;
697
+ announce(strings.phoneCountrySelected(c.name));
536
698
  onPick(c);
537
699
  setOpen(false);
538
700
  }
@@ -552,8 +714,31 @@ function CountrySelect({
552
714
  triggerRef.current?.focus();
553
715
  }
554
716
  }
555
- return /* @__PURE__ */ jsxs("div", { className: "fillo-phone-country", ref: rootRef, children: [
556
- /* @__PURE__ */ jsxs(
717
+ function onTriggerKeyDown(e) {
718
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
719
+ e.preventDefault();
720
+ setOpen(true);
721
+ return;
722
+ }
723
+ if (e.key.length === 1 && e.key !== " " && !e.ctrlKey && !e.altKey && !e.metaKey) {
724
+ e.preventDefault();
725
+ seedRef.current = e.key;
726
+ setOpen(true);
727
+ }
728
+ }
729
+ function onCompositeBlur(e) {
730
+ if (!open) return;
731
+ const next = e.relatedTarget;
732
+ if (next) {
733
+ if (!rootRef.current?.contains(next)) setOpen(false);
734
+ return;
735
+ }
736
+ queueMicrotask(() => {
737
+ if (!rootRef.current?.contains(document.activeElement)) setOpen(false);
738
+ });
739
+ }
740
+ return /* @__PURE__ */ jsxs2("div", { className: "fillo-phone-country", ref: rootRef, onBlur: onCompositeBlur, children: [
741
+ /* @__PURE__ */ jsxs2(
557
742
  "button",
558
743
  {
559
744
  ref: triggerRef,
@@ -564,18 +749,19 @@ function CountrySelect({
564
749
  "aria-controls": open ? listId : void 0,
565
750
  "aria-label": `Country: ${country.name} (+${country.dialCode})`,
566
751
  onClick: () => setOpen((o) => !o),
752
+ onKeyDown: onTriggerKeyDown,
567
753
  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: [
754
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(country.iso2) }),
755
+ /* @__PURE__ */ jsxs2("span", { className: "fillo-phone-dial", children: [
570
756
  "+",
571
757
  country.dialCode
572
758
  ] }),
573
- /* @__PURE__ */ jsx("span", { className: "fillo-phone-caret", "aria-hidden": "true", children: "\u25BE" })
759
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-caret", "aria-hidden": "true", children: "\u25BE" })
574
760
  ]
575
761
  }
576
762
  ),
577
- open && /* @__PURE__ */ jsxs("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
578
- /* @__PURE__ */ jsx(
763
+ open && /* @__PURE__ */ jsxs2("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
764
+ /* @__PURE__ */ jsx2(
579
765
  "input",
580
766
  {
581
767
  ref: searchRef,
@@ -596,8 +782,8 @@ function CountrySelect({
596
782
  onKeyDown: onSearchKey
597
783
  }
598
784
  ),
599
- /* @__PURE__ */ jsxs("ul", { className: "fillo-phone-list", role: "listbox", id: listId, "aria-label": "Country", children: [
600
- matches.map((c, i) => /* @__PURE__ */ jsxs(
785
+ /* @__PURE__ */ jsxs2("ul", { className: "fillo-phone-list", role: "listbox", id: listId, "aria-label": "Country", children: [
786
+ matches.map((c, i) => /* @__PURE__ */ jsxs2(
601
787
  "li",
602
788
  {
603
789
  id: `${listId}-opt-${i}`,
@@ -610,9 +796,9 @@ function CountrySelect({
610
796
  choose(c);
611
797
  },
612
798
  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: [
799
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(c.iso2) }),
800
+ /* @__PURE__ */ jsx2("span", { className: "fillo-phone-option-name", children: c.name }),
801
+ /* @__PURE__ */ jsxs2("span", { className: "fillo-phone-option-dial", children: [
616
802
  "+",
617
803
  c.dialCode
618
804
  ] })
@@ -620,7 +806,7 @@ function CountrySelect({
620
806
  },
621
807
  c.iso2
622
808
  )),
623
- matches.length === 0 && /* @__PURE__ */ jsx("li", { className: "fillo-phone-empty", children: "No matches" })
809
+ matches.length === 0 && /* @__PURE__ */ jsx2("li", { className: "fillo-phone-empty", role: "option", "aria-disabled": "true", children: "No matches" })
624
810
  ] })
625
811
  ] })
626
812
  ] });
@@ -628,13 +814,14 @@ function CountrySelect({
628
814
 
629
815
  // src/signature.tsx
630
816
  import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
631
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
817
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
632
818
  function SignatureField({ field, value, error, setValue, ids: providedIds }) {
633
819
  const ids = useFilloFieldIds(field.id, providedIds);
634
820
  const slots = useFieldSlots(field, error);
635
821
  const strings = useStrings();
636
822
  const typedInputId = `${ids.inputId}-typed`;
637
823
  const canvasRef = useRef4(null);
824
+ const typedInputRef = useRef4(null);
638
825
  const drawing = useRef4(false);
639
826
  const [hasInk, setHasInk] = useState3(Boolean(value));
640
827
  const [typedName, setTypedName] = useState3("");
@@ -731,28 +918,30 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
731
918
  setHasInk(false);
732
919
  setTypedName("");
733
920
  setValue(null);
921
+ typedInputRef.current?.focus();
734
922
  }
735
923
  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: [
924
+ return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`), children: [
925
+ /* @__PURE__ */ jsxs3("span", { className: slots.label, "data-fillo": "label", id: ids.labelId, children: [
738
926
  field.label,
739
- !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
927
+ !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
740
928
  ] }),
741
- field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
742
- /* @__PURE__ */ jsxs2(
929
+ field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
930
+ /* @__PURE__ */ jsxs3(
743
931
  "div",
744
932
  {
745
933
  role: "group",
746
934
  "aria-labelledby": ids.labelId,
747
935
  "aria-describedby": describedBy,
748
936
  children: [
749
- /* @__PURE__ */ jsxs2("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
750
- /* @__PURE__ */ jsx2(
937
+ /* @__PURE__ */ jsxs3("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
938
+ /* @__PURE__ */ jsx3(
751
939
  "canvas",
752
940
  {
753
941
  ref: canvasRef,
754
942
  className: "fillo-signature-canvas",
755
- "aria-hidden": "true",
943
+ role: "img",
944
+ "aria-label": hasInk ? strings.signatureSigned : strings.signatureEmpty,
756
945
  onPointerDown: start,
757
946
  onPointerMove: move,
758
947
  onPointerUp: end,
@@ -760,15 +949,16 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
760
949
  onPointerLeave: end
761
950
  }
762
951
  ),
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" })
952
+ hasInk && /* @__PURE__ */ jsx3("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
953
+ !hasInk && /* @__PURE__ */ jsx3("span", { className: "fillo-signature-hint", "aria-hidden": "true", children: "Sign here" })
765
954
  ] }),
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(
955
+ /* @__PURE__ */ jsxs3("div", { className: "fillo-signature-type", children: [
956
+ /* @__PURE__ */ jsx3("label", { className: "fillo-signature-type-label", htmlFor: typedInputId, children: "Or type your full name to sign" }),
957
+ /* @__PURE__ */ jsx3(
769
958
  "input",
770
959
  {
771
960
  id: typedInputId,
961
+ ref: typedInputRef,
772
962
  type: "text",
773
963
  className: "fillo-input fillo-signature-type-input",
774
964
  autoComplete: "name",
@@ -787,13 +977,13 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
787
977
  ]
788
978
  }
789
979
  ),
790
- error && /* @__PURE__ */ jsx2("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
980
+ error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
791
981
  ] });
792
982
  }
793
983
 
794
984
  // src/upload.tsx
795
985
  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";
986
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
797
987
  function uploadStatus(inFlight, doneCount, strings) {
798
988
  const failed = inFlight.filter((f) => f.error).length;
799
989
  const active = inFlight.length - failed;
@@ -809,10 +999,19 @@ function formatBytes(bytes) {
809
999
  if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
810
1000
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
811
1001
  }
1002
+ function uploadFailureMessage(error, strings) {
1003
+ if (error instanceof Error && error.name === "FilloError") {
1004
+ const status = error.status;
1005
+ if (status !== void 0 && status >= 500) return strings.uploadUnavailable;
1006
+ return error.message || strings.uploadFailed;
1007
+ }
1008
+ return strings.uploadFailed;
1009
+ }
812
1010
  function FileUploadField({ field, value, error, setValue, api, ids: providedIds }) {
813
1011
  const ids = useFilloFieldIds(field.id, providedIds);
814
1012
  const slots = useFieldSlots(field, error);
815
1013
  const strings = useStrings();
1014
+ const chrome = useFilloChrome();
816
1015
  const schema = field;
817
1016
  const inputRef = useRef5(null);
818
1017
  const [inFlight, setInFlight] = useState4([]);
@@ -834,9 +1033,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
834
1033
  const filesRef = useRef5(files);
835
1034
  filesRef.current = files;
836
1035
  const maxFiles = schema.maxFiles ?? 1;
837
- const maxMb = schema.maxFileSizeMb ?? 500;
1036
+ const configuredMaxMb = schema.maxFileSizeMb ?? 500;
1037
+ const maxMb = chrome?.uploadFileSizeLimitMb ? Math.min(configuredMaxMb, chrome.uploadFileSizeLimitMb) : configuredMaxMb;
838
1038
  const maxBytes = maxMb * 1024 * 1024;
839
- const canUpload = Boolean(api.client && api.formId);
1039
+ const storageBlocked = chrome?.uploadsAvailable === false;
1040
+ const storageUrl = storageBlocked && chrome?.devChrome ? safeHttpUrl(chrome.warningUrl) : null;
1041
+ const canUpload = Boolean(api.client && api.formId) && !storageBlocked;
840
1042
  const activeInFlight = inFlight.filter((f) => !f.error).length;
841
1043
  const remaining = maxFiles - files.length - activeInFlight;
842
1044
  async function startUpload(file) {
@@ -871,7 +1073,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
871
1073
  setValue(next);
872
1074
  } catch (err) {
873
1075
  if (mountedRef.current) {
874
- const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message : strings.uploadFailed;
1076
+ const message = uploadFailureMessage(err, strings);
875
1077
  setInFlight(
876
1078
  (prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
877
1079
  );
@@ -898,13 +1100,13 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
898
1100
  setValue(next);
899
1101
  }
900
1102
  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: [
1103
+ return /* @__PURE__ */ jsxs4("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
1104
+ /* @__PURE__ */ jsxs4("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
903
1105
  field.label,
904
- !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
1106
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
905
1107
  ] }),
906
- field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
907
- remaining > 0 && /* @__PURE__ */ jsxs3(
1108
+ field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1109
+ remaining > 0 && /* @__PURE__ */ jsxs4(
908
1110
  "div",
909
1111
  {
910
1112
  "data-fillo": "control",
@@ -935,7 +1137,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
935
1137
  }
936
1138
  },
937
1139
  children: [
938
- /* @__PURE__ */ jsx3(
1140
+ /* @__PURE__ */ jsx4(
939
1141
  "input",
940
1142
  {
941
1143
  ref: inputRef,
@@ -948,14 +1150,20 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
948
1150
  onChange: (e) => canUpload && handleFiles(e.target.files)
949
1151
  }
950
1152
  ),
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 })
1153
+ canUpload ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1154
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
1155
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
1156
+ ] }) : storageBlocked && chrome?.devChrome ? /* @__PURE__ */ jsxs4("span", { className: "fillo-dropzone-hint", children: [
1157
+ "Connect file storage to enable uploads",
1158
+ storageUrl && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1159
+ " \u2014 ",
1160
+ /* @__PURE__ */ jsx4("a", { href: storageUrl, target: "_blank", rel: "noopener noreferrer", children: storageUrl })
1161
+ ] })
1162
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: storageBlocked ? strings.uploadsUnavailable : strings.uploadsDisabled })
955
1163
  ]
956
1164
  }
957
1165
  ),
958
- /* @__PURE__ */ jsx3(
1166
+ /* @__PURE__ */ jsx4(
959
1167
  "div",
960
1168
  {
961
1169
  role: "status",
@@ -965,11 +1173,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
965
1173
  children: uploadStatus(inFlight, files.length, strings)
966
1174
  }
967
1175
  ),
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(
1176
+ (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs4("ul", { className: "fillo-files", children: [
1177
+ files.map((f) => /* @__PURE__ */ jsxs4("li", { className: "fillo-file fillo-file--done", children: [
1178
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1179
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
1180
+ /* @__PURE__ */ jsx4("span", { className: "fillo-sr-only", children: "Uploaded" }),
1181
+ /* @__PURE__ */ jsx4(
973
1182
  "button",
974
1183
  {
975
1184
  type: "button",
@@ -980,7 +1189,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
980
1189
  }
981
1190
  )
982
1191
  ] }, f.fileId)),
983
- inFlight.map((f) => /* @__PURE__ */ jsxs3(
1192
+ inFlight.map((f) => /* @__PURE__ */ jsxs4(
984
1193
  "li",
985
1194
  {
986
1195
  className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
@@ -992,42 +1201,44 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
992
1201
  "aria-valuenow": Math.round(f.fraction * 100)
993
1202
  },
994
1203
  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(
999
- "button",
1000
- {
1001
- type: "button",
1002
- className: "fillo-file-retry",
1003
- "aria-label": `${strings.uploadRetry} ${f.name}`,
1004
- onClick: () => {
1005
- const retryFile = f.file;
1006
- if (!retryFile) return;
1007
- setInFlight((prev) => prev.filter((x) => x.key !== f.key));
1008
- void startUpload(retryFile);
1009
- },
1010
- children: strings.uploadRetry
1011
- }
1012
- ),
1013
- /* @__PURE__ */ jsx3(
1014
- "button",
1015
- {
1016
- type: "button",
1017
- className: "fillo-file-remove",
1018
- "aria-label": "Dismiss",
1019
- onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
1020
- children: "\xD7"
1021
- }
1022
- )
1023
- ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
1024
- /* @__PURE__ */ jsxs3("span", { className: "fillo-file-meta", children: [
1204
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1205
+ f.error ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1206
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-actions", children: [
1207
+ f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx4(
1208
+ "button",
1209
+ {
1210
+ type: "button",
1211
+ className: "fillo-file-retry",
1212
+ "aria-label": `${strings.uploadRetry} ${f.name}`,
1213
+ onClick: () => {
1214
+ const retryFile = f.file;
1215
+ if (!retryFile) return;
1216
+ setInFlight((prev) => prev.filter((x) => x.key !== f.key));
1217
+ void startUpload(retryFile);
1218
+ },
1219
+ children: strings.uploadRetry
1220
+ }
1221
+ ),
1222
+ /* @__PURE__ */ jsx4(
1223
+ "button",
1224
+ {
1225
+ type: "button",
1226
+ className: "fillo-file-remove",
1227
+ "aria-label": "Dismiss",
1228
+ onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
1229
+ children: "\xD7"
1230
+ }
1231
+ )
1232
+ ] }),
1233
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-error", role: "alert", children: f.error })
1234
+ ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1235
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-meta", children: [
1025
1236
  Math.round(f.fraction * 100),
1026
1237
  "% of ",
1027
1238
  formatBytes(f.size)
1028
1239
  ] }),
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(
1240
+ /* @__PURE__ */ jsx4("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
1241
+ /* @__PURE__ */ jsx4(
1031
1242
  "button",
1032
1243
  {
1033
1244
  type: "button",
@@ -1043,12 +1254,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1043
1254
  f.key
1044
1255
  ))
1045
1256
  ] }),
1046
- error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1257
+ error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1047
1258
  ] });
1048
1259
  }
1049
1260
 
1050
1261
  // src/fields.tsx
1051
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1262
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1052
1263
  function fieldAria(field, error, ids) {
1053
1264
  const describedBy = [
1054
1265
  field.description ? ids.descriptionId : null,
@@ -1072,9 +1283,6 @@ function useControl(base, field, error) {
1072
1283
  "data-fillo": "control"
1073
1284
  };
1074
1285
  }
1075
- function isNativeControlTarget(target) {
1076
- return target instanceof HTMLElement && Boolean(target.closest("input, textarea, select, button"));
1077
- }
1078
1286
  function FieldShell({
1079
1287
  field,
1080
1288
  error,
@@ -1095,7 +1303,7 @@ function FieldShell({
1095
1303
  const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
1096
1304
  const errorCls = useSlotClass("fillo-error", { slot: "error", ...state });
1097
1305
  const strings = useStrings();
1098
- return /* @__PURE__ */ jsxs4(
1306
+ return /* @__PURE__ */ jsxs5(
1099
1307
  "div",
1100
1308
  {
1101
1309
  className: fieldCls,
@@ -1105,13 +1313,13 @@ function FieldShell({
1105
1313
  "data-invalid": error ? "" : void 0,
1106
1314
  "data-required": field.required ? "" : void 0,
1107
1315
  children: [
1108
- /* @__PURE__ */ jsxs4("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1316
+ /* @__PURE__ */ jsxs5("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1109
1317
  field.label,
1110
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1318
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1111
1319
  ] }),
1112
- field.description && /* @__PURE__ */ jsx4("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1320
+ field.description && /* @__PURE__ */ jsx5("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1113
1321
  children,
1114
- error && /* @__PURE__ */ jsx4("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1322
+ error && /* @__PURE__ */ jsx5("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, children: error })
1115
1323
  ]
1116
1324
  }
1117
1325
  );
@@ -1119,13 +1327,15 @@ function FieldShell({
1119
1327
  function TextInput({ field, value, error, setValue, ids: providedIds }) {
1120
1328
  const ids = useFilloFieldIds(field.id, providedIds);
1121
1329
  const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
1330
+ const autoComplete = field.kind === "email" ? "email" : field.kind === "url" ? "url" : void 0;
1122
1331
  const control = useControl("fillo-input", field, error);
1123
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1332
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1124
1333
  "input",
1125
1334
  {
1126
1335
  id: ids.inputId,
1127
1336
  ...control,
1128
1337
  type,
1338
+ autoComplete,
1129
1339
  value: typeof value === "string" || typeof value === "number" ? String(value) : "",
1130
1340
  placeholder: field.placeholder,
1131
1341
  maxLength: "maxLength" in field ? field.maxLength : void 0,
@@ -1136,10 +1346,75 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
1136
1346
  }
1137
1347
  ) });
1138
1348
  }
1349
+ function NumberInput({ field, value, error, setValue, api, ids: providedIds }) {
1350
+ const ids = useFilloFieldIds(field.id, providedIds);
1351
+ const control = useControl("fillo-input", field, error);
1352
+ const [draft, setDraft] = useState5(null);
1353
+ const inputRef = useRef6(null);
1354
+ const pendingCaret = useRef6(null);
1355
+ const [, setRevertTick] = useState5(0);
1356
+ useLayoutEffect2(() => {
1357
+ if (pendingCaret.current === null) return;
1358
+ const pos = pendingCaret.current;
1359
+ pendingCaret.current = null;
1360
+ inputRef.current?.setSelectionRange(pos, pos);
1361
+ });
1362
+ const num = field;
1363
+ const grouped = num.notation === "grouped";
1364
+ const hasAffix = Boolean(num.prefix) || Boolean(num.suffix);
1365
+ if (!grouped && !hasAffix) {
1366
+ return /* @__PURE__ */ jsx5(
1367
+ TextInput,
1368
+ {
1369
+ field,
1370
+ value,
1371
+ error,
1372
+ setValue,
1373
+ api,
1374
+ ids: providedIds
1375
+ }
1376
+ );
1377
+ }
1378
+ const stored = typeof value === "string" || typeof value === "number" ? String(value) : "";
1379
+ const n = Number(stored);
1380
+ const display = draft !== null ? draft : grouped && stored !== "" && Number.isFinite(n) ? formatGroupedNumber(n, { decimals: num.decimals }) : stored;
1381
+ const input = /* @__PURE__ */ jsx5(
1382
+ "input",
1383
+ {
1384
+ ref: inputRef,
1385
+ id: ids.inputId,
1386
+ ...control,
1387
+ type: "text",
1388
+ inputMode: "decimal",
1389
+ value: display,
1390
+ placeholder: field.placeholder,
1391
+ onBlur: grouped ? () => setDraft(null) : void 0,
1392
+ onChange: (e) => {
1393
+ if (!isValidPartialNumberText(e.target.value)) {
1394
+ pendingCaret.current = e.target.selectionStart ?? e.target.value.length;
1395
+ setRevertTick((t) => t + 1);
1396
+ return;
1397
+ }
1398
+ if (grouped) {
1399
+ setDraft(e.target.value);
1400
+ setValue(parseGroupedNumber(e.target.value));
1401
+ } else {
1402
+ setValue(e.target.value);
1403
+ }
1404
+ },
1405
+ ...fieldAria(field, error, ids)
1406
+ }
1407
+ );
1408
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: hasAffix ? /* @__PURE__ */ jsxs5("div", { className: "fillo-number", children: [
1409
+ num.prefix && /* @__PURE__ */ jsx5("span", { className: "fillo-number-prefix", children: num.prefix }),
1410
+ input,
1411
+ num.suffix && /* @__PURE__ */ jsx5("span", { className: "fillo-number-suffix", children: num.suffix })
1412
+ ] }) : input });
1413
+ }
1139
1414
  function LongText({ field, value, error, setValue, ids: providedIds }) {
1140
1415
  const ids = useFilloFieldIds(field.id, providedIds);
1141
1416
  const control = useControl("fillo-input fillo-textarea", field, error);
1142
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1417
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1143
1418
  "textarea",
1144
1419
  {
1145
1420
  id: ids.inputId,
@@ -1176,7 +1451,7 @@ var OPTION_ICON_PATHS = {
1176
1451
  };
1177
1452
  function ChoiceIcon({ icon }) {
1178
1453
  if (!icon) return null;
1179
- return /* @__PURE__ */ jsx4(
1454
+ return /* @__PURE__ */ jsx5(
1180
1455
  "svg",
1181
1456
  {
1182
1457
  className: "fillo-option-icon",
@@ -1187,7 +1462,7 @@ function ChoiceIcon({ icon }) {
1187
1462
  strokeLinecap: "round",
1188
1463
  strokeLinejoin: "round",
1189
1464
  "aria-hidden": "true",
1190
- children: /* @__PURE__ */ jsx4("path", { d: OPTION_ICON_PATHS[icon] })
1465
+ children: /* @__PURE__ */ jsx5("path", { d: OPTION_ICON_PATHS[icon] })
1191
1466
  }
1192
1467
  );
1193
1468
  }
@@ -1201,7 +1476,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1201
1476
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
1202
1477
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1203
1478
  const otherActive = isOtherValue || value === "" && otherOn;
1204
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
1479
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs5(
1205
1480
  "div",
1206
1481
  {
1207
1482
  className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
@@ -1210,7 +1485,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1210
1485
  "aria-labelledby": ids.labelId,
1211
1486
  ...fieldAria(field, error, ids),
1212
1487
  children: [
1213
- options.map((opt) => /* @__PURE__ */ jsxs4(
1488
+ options.map((opt) => /* @__PURE__ */ jsxs5(
1214
1489
  "label",
1215
1490
  {
1216
1491
  "data-option": opt.id,
@@ -1221,14 +1496,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1221
1496
  opt.id,
1222
1497
  value === opt.id
1223
1498
  ),
1224
- onClick: (event) => {
1225
- if (isNativeControlTarget(event.target)) return;
1226
- event.preventDefault();
1227
- setOtherOn(false);
1228
- setValue(opt.id);
1229
- },
1230
1499
  children: [
1231
- /* @__PURE__ */ jsx4(
1500
+ /* @__PURE__ */ jsx5(
1232
1501
  "input",
1233
1502
  {
1234
1503
  type: "radio",
@@ -1241,16 +1510,22 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1241
1510
  }
1242
1511
  }
1243
1512
  ),
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 })
1513
+ /* @__PURE__ */ jsx5(ChoiceIcon, { icon: opt.icon }),
1514
+ opt.icon && value === opt.id && // Icon-mode hides the native input, so its check state is
1515
+ // otherwise color-only (forfeits forced-colors' own indicator).
1516
+ /* @__PURE__ */ jsx5("span", { className: "fillo-option-check", "aria-hidden": "true", children: "\u2713" }),
1517
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1246
1518
  ]
1247
1519
  },
1248
1520
  opt.id
1249
1521
  )),
1250
- choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
1251
- // toggle the radio. The radio + "Other" text keep their own <label>.
1252
- /* @__PURE__ */ jsxs4(
1253
- "div",
1522
+ choice.allowOther && // The outer element is a <label> so the whole row is a pointer
1523
+ // target (native label click-forwarding), same as every other
1524
+ // option row above. The free-text input below is its own labelable
1525
+ // descendant, so it handles its own clicks without re-toggling the
1526
+ // radio (dom's proven pattern).
1527
+ /* @__PURE__ */ jsxs5(
1528
+ "label",
1254
1529
  {
1255
1530
  "data-fillo": "option",
1256
1531
  "data-selected": otherActive ? "" : void 0,
@@ -1260,8 +1535,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1260
1535
  otherActive
1261
1536
  ),
1262
1537
  children: [
1263
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1264
- /* @__PURE__ */ jsx4(
1538
+ /* @__PURE__ */ jsxs5("span", { className: "fillo-option-main", children: [
1539
+ /* @__PURE__ */ jsx5(
1265
1540
  "input",
1266
1541
  {
1267
1542
  type: "radio",
@@ -1274,9 +1549,9 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1274
1549
  }
1275
1550
  }
1276
1551
  ),
1277
- /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1552
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1278
1553
  ] }),
1279
- otherActive && /* @__PURE__ */ jsx4(
1554
+ otherActive && /* @__PURE__ */ jsx5(
1280
1555
  "input",
1281
1556
  {
1282
1557
  type: "text",
@@ -1308,7 +1583,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1308
1583
  const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
1309
1584
  const appearance = useFilloAppearance();
1310
1585
  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(
1586
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs5(
1312
1587
  "div",
1313
1588
  {
1314
1589
  className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
@@ -1317,7 +1592,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1317
1592
  "aria-labelledby": ids.labelId,
1318
1593
  ...fieldAria(field, error, ids),
1319
1594
  children: [
1320
- options.map((opt) => /* @__PURE__ */ jsxs4(
1595
+ options.map((opt) => /* @__PURE__ */ jsxs5(
1321
1596
  "label",
1322
1597
  {
1323
1598
  "data-option": opt.id,
@@ -1328,13 +1603,8 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1328
1603
  opt.id,
1329
1604
  selected.includes(opt.id)
1330
1605
  ),
1331
- onClick: (event) => {
1332
- if (isNativeControlTarget(event.target)) return;
1333
- event.preventDefault();
1334
- toggle(opt.id);
1335
- },
1336
1606
  children: [
1337
- /* @__PURE__ */ jsx4(
1607
+ /* @__PURE__ */ jsx5(
1338
1608
  "input",
1339
1609
  {
1340
1610
  type: "checkbox",
@@ -1343,16 +1613,22 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1343
1613
  onChange: () => toggle(opt.id)
1344
1614
  }
1345
1615
  ),
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 })
1616
+ /* @__PURE__ */ jsx5(ChoiceIcon, { icon: opt.icon }),
1617
+ opt.icon && selected.includes(opt.id) && // Icon-mode hides the native input, so its check state is
1618
+ // otherwise color-only (forfeits forced-colors' own indicator).
1619
+ /* @__PURE__ */ jsx5("span", { className: "fillo-option-check", "aria-hidden": "true", children: "\u2713" }),
1620
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1348
1621
  ]
1349
1622
  },
1350
1623
  opt.id
1351
1624
  )),
1352
- choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
1353
- // toggle the checkbox. The checkbox + "Other" text keep their own <label>.
1354
- /* @__PURE__ */ jsxs4(
1355
- "div",
1625
+ choice.allowOther && // The outer element is a <label> so the whole row is a pointer
1626
+ // target (native label click-forwarding), same as every other
1627
+ // option row above. The free-text input below is its own labelable
1628
+ // descendant, so it handles its own clicks without re-toggling the
1629
+ // checkbox (dom's proven pattern).
1630
+ /* @__PURE__ */ jsxs5(
1631
+ "label",
1356
1632
  {
1357
1633
  "data-fillo": "option",
1358
1634
  "data-selected": otherActive ? "" : void 0,
@@ -1362,8 +1638,8 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1362
1638
  otherActive
1363
1639
  ),
1364
1640
  children: [
1365
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
1366
- /* @__PURE__ */ jsx4(
1641
+ /* @__PURE__ */ jsxs5("span", { className: "fillo-option-main", children: [
1642
+ /* @__PURE__ */ jsx5(
1367
1643
  "input",
1368
1644
  {
1369
1645
  type: "checkbox",
@@ -1379,9 +1655,9 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1379
1655
  }
1380
1656
  }
1381
1657
  ),
1382
- /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1658
+ /* @__PURE__ */ jsx5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
1383
1659
  ] }),
1384
- otherActive && /* @__PURE__ */ jsx4(
1660
+ otherActive && /* @__PURE__ */ jsx5(
1385
1661
  "input",
1386
1662
  {
1387
1663
  type: "text",
@@ -1419,9 +1695,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1419
1695
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1420
1696
  const otherActive = isOtherValue || value === "" && otherOn;
1421
1697
  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(
1698
+ return /* @__PURE__ */ jsxs5(FieldShell, { field, error, ids, children: [
1699
+ /* @__PURE__ */ jsxs5("div", { className: "fillo-select-wrap", children: [
1700
+ /* @__PURE__ */ jsxs5(
1425
1701
  "select",
1426
1702
  {
1427
1703
  id: ids.inputId,
@@ -1438,15 +1714,15 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1438
1714
  }
1439
1715
  },
1440
1716
  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` })
1717
+ /* @__PURE__ */ jsx5("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1718
+ options.map((opt) => /* @__PURE__ */ jsx5("option", { value: opt.id, children: opt.label }, opt.id)),
1719
+ choice.allowOther && /* @__PURE__ */ jsx5("option", { value: otherValue, children: `${strings.other}\u2026` })
1444
1720
  ]
1445
1721
  }
1446
1722
  ),
1447
- /* @__PURE__ */ jsx4("span", { className: "fillo-select-icon", "aria-hidden": "true" })
1723
+ /* @__PURE__ */ jsx5("span", { className: "fillo-select-icon", "aria-hidden": "true" })
1448
1724
  ] }),
1449
- otherActive && /* @__PURE__ */ jsx4(
1725
+ otherActive && /* @__PURE__ */ jsx5(
1450
1726
  "input",
1451
1727
  {
1452
1728
  type: "text",
@@ -1467,48 +1743,46 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1467
1743
  const slots = useFieldSlots(field, error);
1468
1744
  const checkbox = field;
1469
1745
  if (checkbox.appearance === "toggle") {
1470
- return /* @__PURE__ */ jsxs4(
1746
+ return /* @__PURE__ */ jsxs5(
1471
1747
  "div",
1472
1748
  {
1473
1749
  ...slots.wrapperProps(
1474
1750
  `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`
1475
1751
  ),
1476
1752
  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: [
1753
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1754
+ /* @__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
1755
  field.label,
1480
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1756
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1481
1757
  ] }) }),
1482
- /* @__PURE__ */ jsx4(
1758
+ /* @__PURE__ */ jsx5(
1483
1759
  "input",
1484
1760
  {
1485
1761
  id: ids.inputId,
1486
1762
  type: "checkbox",
1487
- role: "switch",
1488
1763
  className: "fillo-toggle-input",
1489
1764
  checked: value === true,
1490
1765
  onChange: (e) => setValue(e.target.checked),
1491
- "aria-checked": value === true,
1492
1766
  ...fieldAria(field, error, ids)
1493
1767
  }
1494
1768
  ),
1495
- /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
1769
+ /* @__PURE__ */ jsx5("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx5("span", { className: "fillo-toggle-thumb" }) })
1496
1770
  ] }),
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 })
1771
+ field.description && /* @__PURE__ */ jsx5("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1772
+ error && /* @__PURE__ */ jsx5("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1499
1773
  ]
1500
1774
  }
1501
1775
  );
1502
1776
  }
1503
- return /* @__PURE__ */ jsxs4(
1777
+ return /* @__PURE__ */ jsxs5(
1504
1778
  "div",
1505
1779
  {
1506
1780
  ...slots.wrapperProps(
1507
1781
  `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`
1508
1782
  ),
1509
1783
  children: [
1510
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1511
- /* @__PURE__ */ jsx4(
1784
+ /* @__PURE__ */ jsxs5("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1785
+ /* @__PURE__ */ jsx5(
1512
1786
  "input",
1513
1787
  {
1514
1788
  id: ids.inputId,
@@ -1519,34 +1793,27 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1519
1793
  ...fieldAria(field, error, ids)
1520
1794
  }
1521
1795
  ),
1522
- /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1796
+ /* @__PURE__ */ jsxs5("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1523
1797
  field.label,
1524
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1798
+ !field.required && /* @__PURE__ */ jsx5("span", { className: "fillo-optional", children: strings.optional })
1525
1799
  ] })
1526
1800
  ] }),
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 })
1801
+ field.description && /* @__PURE__ */ jsx5("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1802
+ error && /* @__PURE__ */ jsx5("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1529
1803
  ]
1530
1804
  }
1531
1805
  );
1532
1806
  }
1533
1807
  function radiogroupKeyDown(e, steps, current, setValue) {
1534
- const forward = e.key === "ArrowRight" || e.key === "ArrowDown";
1535
- const backward = e.key === "ArrowLeft" || e.key === "ArrowUp";
1536
- if (!forward && !backward) return;
1808
+ const group = e.currentTarget;
1809
+ const rtl = getComputedStyle(group).direction === "rtl";
1810
+ const index = rovingIndex(steps, current);
1811
+ const next = radioGroupStep(e.key, index, steps.length, { rtl });
1812
+ if (next == null) return;
1537
1813
  e.preventDefault();
1538
- const index = current == null ? -1 : steps.indexOf(current);
1539
- let next;
1540
- if (index === -1) {
1541
- next = forward ? 0 : steps.length - 1;
1542
- } else {
1543
- next = forward ? index + 1 : index - 1;
1544
- next = (next + steps.length) % steps.length;
1545
- }
1546
1814
  const value = steps[next];
1547
1815
  if (value === void 0) return;
1548
1816
  setValue(value);
1549
- const group = e.currentTarget;
1550
1817
  const buttons = group.querySelectorAll('[role="radio"]');
1551
1818
  buttons[next]?.focus();
1552
1819
  }
@@ -1564,15 +1831,16 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1564
1831
  const tabbable = rovingIndex(steps, selected);
1565
1832
  const appearance = useFilloAppearance();
1566
1833
  const control = useControl("fillo-rating", field, error);
1567
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1834
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1568
1835
  "div",
1569
1836
  {
1837
+ id: ids.inputId,
1570
1838
  ...control,
1571
1839
  role: "radiogroup",
1572
1840
  "aria-labelledby": ids.labelId,
1573
1841
  ...fieldAria(field, error, ids),
1574
1842
  onKeyDown: (e) => radiogroupKeyDown(e, steps, selected, (n) => setValue(n)),
1575
- children: steps.map((n, i) => /* @__PURE__ */ jsx4(
1843
+ children: steps.map((n, i) => /* @__PURE__ */ jsx5(
1576
1844
  "button",
1577
1845
  {
1578
1846
  type: "button",
@@ -1588,8 +1856,11 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1588
1856
  "aria-label": `${n} of ${max}`,
1589
1857
  "aria-checked": n === current,
1590
1858
  tabIndex: i === tabbable ? 0 : -1,
1591
- onClick: () => setValue(n === current ? null : n),
1592
- children: "\u2605"
1859
+ onClick: (event) => {
1860
+ if (event.detail === 0 && n === current) return;
1861
+ setValue(n === current ? null : n);
1862
+ },
1863
+ children: n <= current ? "\u2605" : "\u2606"
1593
1864
  },
1594
1865
  n
1595
1866
  ))
@@ -1615,10 +1886,11 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1615
1886
  ].filter(Boolean).join(" ") || void 0;
1616
1887
  const appearance = useFilloAppearance();
1617
1888
  const control = useControl("fillo-scale", field, error);
1618
- return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1619
- /* @__PURE__ */ jsx4(
1889
+ return /* @__PURE__ */ jsxs5(FieldShell, { field, error, ids, children: [
1890
+ /* @__PURE__ */ jsx5(
1620
1891
  "div",
1621
1892
  {
1893
+ id: ids.inputId,
1622
1894
  ...control,
1623
1895
  role: "radiogroup",
1624
1896
  "aria-labelledby": ids.labelId,
@@ -1626,7 +1898,7 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1626
1898
  "aria-required": field.required ? true : void 0,
1627
1899
  "aria-describedby": describedBy,
1628
1900
  onKeyDown: (e) => radiogroupKeyDown(e, steps, current, (n) => setValue(n)),
1629
- children: steps.map((n, i) => /* @__PURE__ */ jsx4(
1901
+ children: steps.map((n, i) => /* @__PURE__ */ jsx5(
1630
1902
  "button",
1631
1903
  {
1632
1904
  type: "button",
@@ -1642,68 +1914,86 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1642
1914
  "aria-label": String(n),
1643
1915
  "aria-checked": value === n,
1644
1916
  tabIndex: i === tabbable ? 0 : -1,
1645
- onClick: () => setValue(value === n ? null : n),
1917
+ onClick: (event) => {
1918
+ if (event.detail === 0 && n === current) return;
1919
+ setValue(value === n ? null : n);
1920
+ },
1646
1921
  children: n
1647
1922
  },
1648
1923
  n
1649
1924
  ))
1650
1925
  }
1651
1926
  ),
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 })
1927
+ hasLabels && /* @__PURE__ */ jsxs5("div", { className: "fillo-scale-labels", children: [
1928
+ /* @__PURE__ */ jsx5("span", { id: minLabelId, children: scale.minLabel }),
1929
+ /* @__PURE__ */ jsx5("span", { id: maxLabelId, children: scale.maxLabel })
1655
1930
  ] })
1656
1931
  ] });
1657
1932
  }
1658
1933
  function Ranking({ field, value, error, setValue, ids: providedIds }) {
1659
1934
  const ids = useFilloFieldIds(field.id, providedIds);
1935
+ const strings = useStrings();
1936
+ const announce = useFilloAnnounce();
1660
1937
  const ranking = field;
1661
1938
  const answered = Array.isArray(value) ? value : [];
1662
1939
  const order = [
1663
1940
  ...answered.filter((id) => ranking.options.some((o) => o.id === id)),
1664
1941
  ...ranking.options.map((o) => o.id).filter((id) => !answered.includes(id))
1665
1942
  ];
1666
- function move(id, delta) {
1943
+ const pressedRef = useRef6(null);
1944
+ useEffect6(() => {
1945
+ const pressed = pressedRef.current;
1946
+ pressedRef.current = null;
1947
+ if (!pressed || !pressed.disabled) return;
1948
+ const sibling = pressed.parentElement?.querySelector(
1949
+ ".fillo-ranking-move:not(:disabled)"
1950
+ );
1951
+ sibling?.focus();
1952
+ }, [value]);
1953
+ function move(id, delta, button) {
1667
1954
  const index = order.indexOf(id);
1668
1955
  const target = index + delta;
1669
1956
  if (target < 0 || target >= order.length) return;
1957
+ pressedRef.current = button;
1670
1958
  const next = [...order];
1671
1959
  [next[index], next[target]] = [next[target], next[index]];
1672
1960
  setValue(next);
1961
+ const moved = ranking.options.find((o) => o.id === id);
1962
+ if (moved) announce(strings.rankingPosition(moved.label, target + 1, order.length));
1673
1963
  }
1674
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1964
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1675
1965
  "div",
1676
1966
  {
1677
1967
  id: ids.inputId,
1678
1968
  role: "group",
1679
1969
  "aria-labelledby": ids.labelId,
1680
1970
  ...fieldAria(field, error, ids),
1681
- children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1971
+ children: /* @__PURE__ */ jsx5("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1682
1972
  const option = ranking.options.find((o) => o.id === id);
1683
1973
  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(
1974
+ return /* @__PURE__ */ jsxs5("li", { className: "fillo-ranking-item", children: [
1975
+ /* @__PURE__ */ jsx5("span", { className: "fillo-ranking-index", children: index + 1 }),
1976
+ /* @__PURE__ */ jsx5("span", { className: "fillo-ranking-label", children: option.label }),
1977
+ /* @__PURE__ */ jsxs5("span", { className: "fillo-ranking-controls", children: [
1978
+ /* @__PURE__ */ jsx5(
1689
1979
  "button",
1690
1980
  {
1691
1981
  type: "button",
1692
1982
  className: "fillo-ranking-move",
1693
1983
  "aria-label": `Move ${option.label} up`,
1694
1984
  disabled: index === 0,
1695
- onClick: () => move(id, -1),
1985
+ onClick: (e) => move(id, -1, e.currentTarget),
1696
1986
  children: "\u2191"
1697
1987
  }
1698
1988
  ),
1699
- /* @__PURE__ */ jsx4(
1989
+ /* @__PURE__ */ jsx5(
1700
1990
  "button",
1701
1991
  {
1702
1992
  type: "button",
1703
1993
  className: "fillo-ranking-move",
1704
1994
  "aria-label": `Move ${option.label} down`,
1705
1995
  disabled: index === order.length - 1,
1706
- onClick: () => move(id, 1),
1996
+ onClick: (e) => move(id, 1, e.currentTarget),
1707
1997
  children: "\u2193"
1708
1998
  }
1709
1999
  )
@@ -1717,7 +2007,7 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1717
2007
  const ids = useFilloFieldIds(field.id, providedIds);
1718
2008
  const matrix = field;
1719
2009
  const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
1720
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
2010
+ return /* @__PURE__ */ jsx5(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx5(
1721
2011
  "div",
1722
2012
  {
1723
2013
  id: ids.inputId,
@@ -1725,12 +2015,12 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1725
2015
  role: "group",
1726
2016
  "aria-labelledby": ids.labelId,
1727
2017
  ...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))
2018
+ children: /* @__PURE__ */ jsxs5("table", { className: "fillo-matrix", children: [
2019
+ /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
2020
+ /* @__PURE__ */ jsx5("th", {}),
2021
+ matrix.columns.map((col) => /* @__PURE__ */ jsx5("th", { scope: "col", children: col.label }, col.id))
1732
2022
  ] }) }),
1733
- /* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
2023
+ /* @__PURE__ */ jsx5("tbody", { children: matrix.rows.map((row) => {
1734
2024
  const rowLabelId = `${ids.inputId}-${row.id}`;
1735
2025
  const answered = answers[row.id] !== void 0;
1736
2026
  const rowInvalid = Boolean(error) && field.required && !answered;
@@ -1741,17 +2031,17 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1741
2031
  // radiogroup lives on an inner element that aria-owns the radios
1742
2032
  // scattered across the cells (they can't be DOM-nested inside a
1743
2033
  // single element within a table row).
1744
- /* @__PURE__ */ jsxs4("tr", { children: [
1745
- /* @__PURE__ */ jsx4("th", { scope: "row", children: /* @__PURE__ */ jsx4(
2034
+ /* @__PURE__ */ jsxs5("tr", { children: [
2035
+ /* @__PURE__ */ jsx5("th", { scope: "row", children: /* @__PURE__ */ jsx5(
1746
2036
  "div",
1747
2037
  {
1748
2038
  role: "radiogroup",
1749
- "aria-labelledby": rowLabelId,
2039
+ "aria-labelledby": `${ids.labelId} ${rowLabelId}`,
1750
2040
  "aria-owns": matrix.columns.map(radioId).join(" "),
1751
- children: /* @__PURE__ */ jsx4("span", { id: rowLabelId, children: row.label })
2041
+ children: /* @__PURE__ */ jsx5("span", { id: rowLabelId, children: row.label })
1752
2042
  }
1753
2043
  ) }),
1754
- matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
2044
+ matrix.columns.map((col) => /* @__PURE__ */ jsx5("td", { "data-label": col.label, children: /* @__PURE__ */ jsx5("label", { className: "fillo-matrix-cell", children: /* @__PURE__ */ jsx5(
1755
2045
  "input",
1756
2046
  {
1757
2047
  id: radioId(col),
@@ -1764,7 +2054,7 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1764
2054
  checked: answers[row.id] === col.id,
1765
2055
  onChange: () => setValue({ ...answers, [row.id]: col.id })
1766
2056
  }
1767
- ) }, col.id))
2057
+ ) }) }, col.id))
1768
2058
  ] }, row.id)
1769
2059
  );
1770
2060
  }) })
@@ -1775,12 +2065,45 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
1775
2065
  function Hidden() {
1776
2066
  return null;
1777
2067
  }
2068
+ function Calculated({ field, value, ids: providedIds }) {
2069
+ const ids = useFilloFieldIds(field.id, providedIds);
2070
+ const state = { kind: field.kind, fieldId: field.id, invalid: false, required: false };
2071
+ const rootCls = useSlotClass("fillo-field fillo-field--calculated fillo-calculated", {
2072
+ slot: "calculated",
2073
+ ...state
2074
+ });
2075
+ const labelCls = useSlotClass("fillo-label", { slot: "label", ...state });
2076
+ const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
2077
+ const answered = value !== void 0 && value !== null && value !== "";
2078
+ return /* @__PURE__ */ jsxs5(
2079
+ "div",
2080
+ {
2081
+ className: rootCls,
2082
+ "data-fillo": "calculated",
2083
+ "data-field": field.id,
2084
+ "data-kind": field.kind,
2085
+ children: [
2086
+ /* @__PURE__ */ jsx5("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: field.label }),
2087
+ field.description && /* @__PURE__ */ jsx5("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
2088
+ /* @__PURE__ */ jsx5(
2089
+ "output",
2090
+ {
2091
+ id: ids.inputId,
2092
+ className: `fillo-calculated-value${answered ? "" : " fillo-calculated-value--empty"}`,
2093
+ "aria-describedby": field.description ? ids.descriptionId : void 0,
2094
+ children: answered ? formatAnswer(field, value) : "\u2014"
2095
+ }
2096
+ )
2097
+ ]
2098
+ }
2099
+ );
2100
+ }
1778
2101
  var DEFAULT_COMPONENTS = {
1779
2102
  short_text: TextInput,
1780
2103
  email: TextInput,
1781
2104
  url: TextInput,
1782
2105
  phone: PhoneField,
1783
- number: TextInput,
2106
+ number: NumberInput,
1784
2107
  date: TextInput,
1785
2108
  long_text: LongText,
1786
2109
  select: SingleChoice,
@@ -1793,16 +2116,17 @@ var DEFAULT_COMPONENTS = {
1793
2116
  matrix: Matrix,
1794
2117
  signature: SignatureField,
1795
2118
  file_upload: FileUploadField,
1796
- hidden: Hidden
2119
+ hidden: Hidden,
2120
+ calculated: Calculated
1797
2121
  };
1798
2122
  function ContentRenderer({ block }) {
1799
2123
  switch (block.kind) {
1800
2124
  case "heading":
1801
- return /* @__PURE__ */ jsx4("h3", { className: "fillo-heading", children: block.text });
2125
+ return /* @__PURE__ */ jsx5("h3", { className: "fillo-heading", children: block.text });
1802
2126
  case "paragraph":
1803
- return /* @__PURE__ */ jsx4("p", { className: "fillo-paragraph", children: block.text });
2127
+ return /* @__PURE__ */ jsx5("p", { className: "fillo-paragraph", children: block.text });
1804
2128
  case "divider":
1805
- return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
2129
+ return /* @__PURE__ */ jsx5("hr", { className: "fillo-divider" });
1806
2130
  }
1807
2131
  }
1808
2132
  function blockHasPiping(block) {
@@ -1841,7 +2165,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1841
2165
  block = pipeBlock(block, api.data, api.form);
1842
2166
  const ids = useFilloFieldIds(block.id);
1843
2167
  const strings = useStrings();
1844
- if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
2168
+ if (!isField(block)) return /* @__PURE__ */ jsx5(ContentRenderer, { block });
1845
2169
  let Component;
1846
2170
  if (block.kind === "custom") {
1847
2171
  Component = customComponents?.[block.component] ?? components?.custom;
@@ -1859,7 +2183,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1859
2183
  }
1860
2184
  const rawError = api.errors[block.id];
1861
2185
  const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
1862
- return /* @__PURE__ */ jsx4(
2186
+ return /* @__PURE__ */ jsx5(
1863
2187
  Component,
1864
2188
  {
1865
2189
  field: block,
@@ -1896,7 +2220,7 @@ function FormField({
1896
2220
  const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
1897
2221
  if (!block) return null;
1898
2222
  if (isField(block) && !visibleIds.has(block.id)) return null;
1899
- return /* @__PURE__ */ jsx4(
2223
+ return /* @__PURE__ */ jsx5(
1900
2224
  BlockRenderer,
1901
2225
  {
1902
2226
  block,
@@ -1908,8 +2232,8 @@ function FormField({
1908
2232
  }
1909
2233
 
1910
2234
  // src/turnstile.tsx
1911
- import { useEffect as useEffect7, useRef as useRef6 } from "react";
1912
- import { jsx as jsx5 } from "react/jsx-runtime";
2235
+ import { useEffect as useEffect7, useRef as useRef7 } from "react";
2236
+ import { jsx as jsx6 } from "react/jsx-runtime";
1913
2237
  var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
1914
2238
  var SCRIPT_ID = "fillo-turnstile-script";
1915
2239
  var scriptPromise = null;
@@ -1967,13 +2291,13 @@ function TurnstileWidget({
1967
2291
  registerReset,
1968
2292
  className
1969
2293
  }) {
1970
- const containerRef = useRef6(null);
1971
- const widgetIdRef = useRef6(null);
1972
- const onTokenRef = useRef6(onToken);
2294
+ const containerRef = useRef7(null);
2295
+ const widgetIdRef = useRef7(null);
2296
+ const onTokenRef = useRef7(onToken);
1973
2297
  onTokenRef.current = onToken;
1974
- const onErrorRef = useRef6(onError);
2298
+ const onErrorRef = useRef7(onError);
1975
2299
  onErrorRef.current = onError;
1976
- const registerResetRef = useRef6(registerReset);
2300
+ const registerResetRef = useRef7(registerReset);
1977
2301
  registerResetRef.current = registerReset;
1978
2302
  useEffect7(() => {
1979
2303
  let cancelled = false;
@@ -2016,7 +2340,7 @@ function TurnstileWidget({
2016
2340
  onTokenRef.current(void 0);
2017
2341
  };
2018
2342
  }, [siteKey]);
2019
- return /* @__PURE__ */ jsx5(
2343
+ return /* @__PURE__ */ jsx6(
2020
2344
  "div",
2021
2345
  {
2022
2346
  ref: containerRef,
@@ -2027,9 +2351,9 @@ function TurnstileWidget({
2027
2351
  }
2028
2352
 
2029
2353
  // src/FilloForm.tsx
2030
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2354
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2031
2355
  function themeProps(theme) {
2032
- const safeTheme = normalizeFormTheme(theme);
2356
+ const safeTheme = resolveThemeAppearance(normalizeFormTheme(theme));
2033
2357
  if (!safeTheme) return {};
2034
2358
  const style = {};
2035
2359
  if (safeTheme.primary) style["--fillo-primary"] = safeTheme.primary;
@@ -2044,32 +2368,30 @@ function themeProps(theme) {
2044
2368
  ...hasColorTheme ? { "data-fillo-has-color-theme": "" } : {}
2045
2369
  };
2046
2370
  }
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
2371
  function badgeUrl(href) {
2058
2372
  const url = new URL(safeHttpUrl(href) ?? "https://fillo.so");
2059
2373
  url.searchParams.set("utm_source", "form_badge");
2060
2374
  return url.toString();
2061
2375
  }
2062
- function focusFirstInvalid(formEl) {
2063
- if (!formEl || typeof window === "undefined") return;
2064
- const run = () => {
2065
- const invalid = formEl.querySelector('[aria-invalid="true"]');
2066
- const focusable = 'input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
2067
- const target = invalid?.matches(focusable) ? invalid : invalid?.querySelector(focusable);
2068
- target?.focus?.();
2069
- };
2376
+ var FOCUSABLE_SELECTOR = 'input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
2377
+ function resolveFocusable(el) {
2378
+ if (!el) return null;
2379
+ return el.matches(FOCUSABLE_SELECTOR) ? el : el.querySelector(FOCUSABLE_SELECTOR);
2380
+ }
2381
+ function nextFrame(run) {
2070
2382
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
2071
2383
  else setTimeout(run, 0);
2072
2384
  }
2385
+ function focusErrorSummary(formEl) {
2386
+ if (!formEl || typeof window === "undefined") return;
2387
+ nextFrame(() => {
2388
+ formEl.querySelector(".fillo-error-summary")?.focus();
2389
+ });
2390
+ }
2391
+ function focusField(inputId) {
2392
+ if (typeof document === "undefined") return;
2393
+ resolveFocusable(document.getElementById(inputId))?.focus();
2394
+ }
2073
2395
  var HP_STYLE = {
2074
2396
  position: "absolute",
2075
2397
  left: "-9999px",
@@ -2094,6 +2416,9 @@ function FilloForm(props) {
2094
2416
  );
2095
2417
  const [loaded, setLoaded] = useState6(null);
2096
2418
  const [loadFailure, setLoadFailure] = useState6(null);
2419
+ const devEnv = useIsDevEnv();
2420
+ const devChrome = props.preview === true || devEnv;
2421
+ if (props.preview === true) warnPreviewInProduction();
2097
2422
  const codeForm = isCodeForm2(form) ? form : null;
2098
2423
  const onError = props.onError;
2099
2424
  const {
@@ -2105,16 +2430,22 @@ function FilloForm(props) {
2105
2430
  noticeError: syncNoticeError,
2106
2431
  syncing: codeFormSyncing,
2107
2432
  staged: codeChangesStaged,
2433
+ accepting: syncedAccepting,
2434
+ acceptingReason: syncedAcceptingReason,
2435
+ uploadsAvailable: syncedUploadsAvailable,
2436
+ uploadFileSizeLimitMb: syncedUploadFileSizeLimitMb,
2108
2437
  resolvedSchema,
2109
- resolvedTheme
2438
+ resolvedTheme,
2439
+ warningCode: syncedWarningCode,
2440
+ warningUrl: syncedWarningUrl
2110
2441
  } = useCodeFormSync(form, client, onError, {
2111
2442
  // An absent client is intentional render-only use. Builder previews and an
2112
2443
  // explicit canonical id also do not need publishable-key resolution.
2113
2444
  allowUnsynced: Boolean(!client || props.skipValidation || formId)
2114
2445
  });
2115
- const renderResolvedSnapshot = Boolean(resolvedSchema && !isDevEnv());
2446
+ const renderResolvedSnapshot = Boolean(resolvedSchema && !devChrome);
2116
2447
  const inlineSchema = codeForm ? renderResolvedSnapshot ? resolvedSchema : codeForm.schema : form;
2117
- const onErrorRef = useRef7(onError);
2448
+ const onErrorRef = useRef8(onError);
2118
2449
  useEffect8(() => {
2119
2450
  onErrorRef.current = onError;
2120
2451
  });
@@ -2135,6 +2466,10 @@ function FilloForm(props) {
2135
2466
  schema: published.schema,
2136
2467
  theme: published.theme,
2137
2468
  closed: Boolean(published.closed),
2469
+ accepting: published.accepting,
2470
+ acceptingReason: published.acceptingReason,
2471
+ uploadsAvailable: published.uploadsAvailable,
2472
+ uploadFileSizeLimitMb: published.uploadFileSizeLimitMb,
2138
2473
  branding: published.branding ?? null,
2139
2474
  challenge: published.challenge ?? null
2140
2475
  });
@@ -2158,25 +2493,52 @@ function FilloForm(props) {
2158
2493
  const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
2159
2494
  if (normalized && !normalized.ok) {
2160
2495
  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 });
2496
+ if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
2497
+ return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
2163
2498
  }
2164
2499
  if (loadError) {
2165
2500
  if (props.renderError) {
2166
- return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(loadError) });
2501
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(loadError) });
2167
2502
  }
2168
2503
  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 });
2504
+ return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
2505
+ }
2506
+ if (currentLoaded?.accepting === false && schema) {
2507
+ const variant = overlayVariant(currentLoaded.acceptingReason);
2508
+ if (props.renderError) {
2509
+ const error = variant === "closed" ? new FilloError2(
2510
+ "This form is no longer accepting responses.",
2511
+ 403,
2512
+ void 0,
2513
+ currentLoaded.acceptingReason
2514
+ ) : new FilloError2(
2515
+ "This form isn't accepting responses yet.",
2516
+ 403,
2517
+ void 0,
2518
+ currentLoaded.acceptingReason
2519
+ );
2520
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
2521
+ }
2522
+ return /* @__PURE__ */ jsx7(
2523
+ NotOpenOverlay,
2524
+ {
2525
+ ...props,
2526
+ variant,
2527
+ schema,
2528
+ theme,
2529
+ branding: currentLoaded.branding
2530
+ }
2531
+ );
2170
2532
  }
2171
2533
  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 }) });
2534
+ 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
2535
  }
2174
2536
  const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
2175
2537
  if (canonicalSyncError && props.renderError) {
2176
- return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(canonicalSyncError) });
2538
+ return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(canonicalSyncError) });
2177
2539
  }
2178
- if (canonicalSyncError && !isDevEnv()) {
2179
- return /* @__PURE__ */ jsx6(
2540
+ if (canonicalSyncError && !devChrome) {
2541
+ return /* @__PURE__ */ jsx7(
2180
2542
  "div",
2181
2543
  {
2182
2544
  className: `fillo-form fillo-form--error ${props.className ?? ""}`,
@@ -2189,23 +2551,42 @@ function FilloForm(props) {
2189
2551
  );
2190
2552
  }
2191
2553
  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 }) });
2554
+ const syncNotAccepting = Boolean(codeForm) && !props.skipValidation && syncedStatus === "published" && syncedAccepting === false;
2555
+ if ((notPublished || syncNotAccepting) && !devChrome) {
2556
+ const variant = notPublished ? "notOpen" : overlayVariant(syncedAcceptingReason);
2557
+ const error = variant === "closed" ? new FilloError2(
2558
+ "This form is no longer accepting responses.",
2559
+ 403,
2560
+ void 0,
2561
+ syncedAcceptingReason
2562
+ ) : new FilloError2("This form isn't published yet.", 403);
2563
+ if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
2564
+ if (schema) {
2565
+ return /* @__PURE__ */ jsx7(
2566
+ NotOpenOverlay,
2567
+ {
2568
+ ...props,
2569
+ variant,
2570
+ schema,
2571
+ theme,
2572
+ branding: syncedBranding
2573
+ }
2574
+ );
2575
+ }
2576
+ 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
2577
  }
2197
2578
  const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
2198
- if (!isDevEnv() && awaitingCanonical || !schema) {
2199
- return /* @__PURE__ */ jsxs5(
2579
+ if (!devChrome && awaitingCanonical || !schema) {
2580
+ return /* @__PURE__ */ jsxs6(
2200
2581
  "div",
2201
2582
  {
2202
2583
  className: `fillo-form fillo-form--loading ${props.className ?? ""}`,
2203
2584
  "aria-busy": "true",
2204
2585
  ...themeProps(theme),
2205
2586
  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" })
2587
+ /* @__PURE__ */ jsx7("div", { className: "fillo-skeleton" }),
2588
+ /* @__PURE__ */ jsx7("div", { className: "fillo-skeleton" }),
2589
+ /* @__PURE__ */ jsx7("div", { className: "fillo-skeleton fillo-skeleton--short" })
2209
2590
  ]
2210
2591
  }
2211
2592
  );
@@ -2213,7 +2594,7 @@ function FilloForm(props) {
2213
2594
  const branding = codeForm ? syncedBranding : currentLoaded?.branding;
2214
2595
  const poweredBy = branding?.poweredBy ?? true;
2215
2596
  const challenge = props.challenge ?? (codeForm ? syncedChallenge : currentLoaded?.challenge) ?? void 0;
2216
- return /* @__PURE__ */ jsx6(
2597
+ return /* @__PURE__ */ jsx7(
2217
2598
  ResolvedForm,
2218
2599
  {
2219
2600
  ...props,
@@ -2227,60 +2608,84 @@ function FilloForm(props) {
2227
2608
  draftNotice: notPublished,
2228
2609
  stagedNotice: codeChangesStaged,
2229
2610
  syncErrorNotice: syncError ?? syncNoticeError ?? void 0,
2611
+ uploadsAvailable: codeForm ? syncedUploadsAvailable : currentLoaded?.uploadsAvailable,
2612
+ uploadFileSizeLimitMb: codeForm ? syncedUploadFileSizeLimitMb : currentLoaded?.uploadFileSizeLimitMb ?? props.uploadFileSizeLimitMb,
2613
+ syncWarningCode: codeForm ? syncedWarningCode : void 0,
2614
+ syncWarningUrl: syncedWarningUrl,
2230
2615
  challenge
2231
2616
  }
2232
2617
  );
2233
2618
  }
2234
2619
  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
- ] });
2620
+ return /* @__PURE__ */ jsx7("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
2247
2621
  }
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
- ] });
2622
+ function overlayVariant(reason) {
2623
+ return reason === "expired" || reason === "capped" || reason === "storage_full" ? "closed" : "notOpen";
2253
2624
  }
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
- ] });
2625
+ function NotOpenOverlay(props) {
2626
+ const strings = resolveStrings(props.strings);
2627
+ const closedFlavor = props.variant === "closed";
2628
+ return /* @__PURE__ */ jsxs6(
2629
+ "div",
2630
+ {
2631
+ className: `fillo-form fillo-form--not-open ${props.className ?? ""}`,
2632
+ "data-fillo": "root",
2633
+ "data-state": "closed",
2634
+ ...themeProps(props.theme),
2635
+ children: [
2636
+ /* @__PURE__ */ jsx7(
2637
+ "div",
2638
+ {
2639
+ className: "fillo-not-open-preview",
2640
+ "aria-hidden": "true",
2641
+ ref: (node) => node?.setAttribute("inert", ""),
2642
+ children: /* @__PURE__ */ jsx7(
2643
+ ResolvedForm,
2644
+ {
2645
+ schema: props.schema,
2646
+ theme: props.theme,
2647
+ appearance: props.appearance,
2648
+ strings: props.strings,
2649
+ components: props.components,
2650
+ customComponents: props.customComponents,
2651
+ initialData: props.initialData,
2652
+ showTitle: props.showTitle,
2653
+ displayOnly: true
2654
+ }
2655
+ )
2656
+ }
2657
+ ),
2658
+ /* @__PURE__ */ jsxs6("div", { className: "fillo-not-open-card", "data-fillo": "not-open-card", role: "status", children: [
2659
+ /* @__PURE__ */ jsx7("h2", { className: "fillo-not-open-title", children: closedFlavor ? strings.closedTitle : strings.notOpenTitle }),
2660
+ /* @__PURE__ */ jsx7("p", { className: "fillo-not-open-body", children: closedFlavor ? strings.closed : strings.notOpenBody })
2661
+ ] }),
2662
+ (props.branding?.poweredBy ?? true) && /* @__PURE__ */ jsx7(PoweredBy, { label: props.branding?.label, href: props.branding?.href })
2663
+ ]
2664
+ }
2665
+ );
2259
2666
  }
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
- ] });
2667
+ var noopSubscribe = () => () => {
2668
+ };
2669
+ function useIsDevEnv() {
2670
+ return useSyncExternalStore2(noopSubscribe, isLikelyDevEnv, isBuildTimeDevEnv2);
2271
2671
  }
2272
2672
  function ResolvedForm(props) {
2273
2673
  const { schema, theme } = props;
2674
+ const displayOnly = props.displayOnly === true;
2274
2675
  const instanceId = useId2().replace(/:/g, "");
2275
- const hpRef = useRef7(null);
2676
+ const devEnv = useIsDevEnv();
2677
+ const devChrome = !displayOnly && (props.preview === true || devEnv);
2678
+ const hpRef = useRef8(null);
2276
2679
  const codeForm = isCodeForm2(props.form) ? props.form : null;
2277
2680
  const client = props.client;
2278
2681
  const challenge = props.challenge;
2279
2682
  const challengeRequired = Boolean(challenge);
2280
- const challengeTokenRef = useRef7(void 0);
2683
+ const challengeTokenRef = useRef8(void 0);
2684
+ const [resyncWarning, setResyncWarning] = useState6(null);
2685
+ const [resolutionFailureCode, setResolutionFailureCode] = useState6(null);
2281
2686
  const [challengeToken, setChallengeToken] = useState6(void 0);
2282
2687
  const [challengeError, setChallengeError] = useState6(false);
2283
- const resetChallengeRef = useRef7(void 0);
2688
+ const resetChallengeRef = useRef8(void 0);
2284
2689
  const handleChallengeToken = useCallback((token) => {
2285
2690
  challengeTokenRef.current = token;
2286
2691
  setChallengeToken(token);
@@ -2295,6 +2700,12 @@ function ResolvedForm(props) {
2295
2700
  async () => {
2296
2701
  try {
2297
2702
  const result = await syncCodeForm2(client, codeForm, { bypassCache: true });
2703
+ setResyncWarning({
2704
+ code: result.warningCode,
2705
+ url: result.warningUrl,
2706
+ uploadsAvailable: result.uploadsAvailable,
2707
+ uploadFileSizeLimitMb: result.uploadFileSizeLimitMb
2708
+ });
2298
2709
  if (result.status === "draft") {
2299
2710
  throw new FilloError2(
2300
2711
  "This form is no longer published. Submission was stopped before sending answers.",
@@ -2303,6 +2714,14 @@ function ResolvedForm(props) {
2303
2714
  "form_not_published"
2304
2715
  );
2305
2716
  }
2717
+ if (result.accepting === false) {
2718
+ throw new FilloError2(
2719
+ "This form is no longer accepting responses. Submission was stopped before sending answers.",
2720
+ 403,
2721
+ void 0,
2722
+ result.acceptingReason ?? "form_not_accepting"
2723
+ );
2724
+ }
2306
2725
  const authoritative = normalizeFormSchema(result.resolvedSchema ?? codeForm.schema);
2307
2726
  if (!authoritative.ok) {
2308
2727
  throw new FilloError2(
@@ -2332,9 +2751,11 @@ function ResolvedForm(props) {
2332
2751
  );
2333
2752
  props.onError?.(notice);
2334
2753
  }
2754
+ setResolutionFailureCode(null);
2335
2755
  return result.formId;
2336
2756
  } catch (error) {
2337
2757
  const failure = isFilloError2(error) ? error : new FilloError2(error instanceof Error ? error.message : String(error), 0);
2758
+ setResolutionFailureCode(failure.code ?? "sync_failed");
2338
2759
  console.warn(
2339
2760
  `[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
2340
2761
  );
@@ -2358,6 +2779,9 @@ function ResolvedForm(props) {
2358
2779
  // "default", never the bare-engine "headless".
2359
2780
  surface: "default",
2360
2781
  resolveFormId,
2782
+ // Dev chrome gets the developer-grade resolution failure (message + code)
2783
+ // instead of the respondent-safe "unavailable" fallback.
2784
+ verboseResolutionErrors: devChrome,
2361
2785
  respondent: props.respondent,
2362
2786
  challengeRequired,
2363
2787
  getChallengeToken: () => challengeTokenRef.current,
@@ -2366,8 +2790,44 @@ function ResolvedForm(props) {
2366
2790
  resetChallengeRef.current?.();
2367
2791
  }
2368
2792
  });
2369
- useDraftFlush(api);
2793
+ useDraftFlush(api, !displayOnly);
2794
+ const strings = resolveStrings(props.strings);
2795
+ const announceRef = useRef8(null);
2796
+ const announce = useCallback((text) => {
2797
+ const el = announceRef.current;
2798
+ if (!el) return;
2799
+ if (el.textContent === text) {
2800
+ el.textContent = "";
2801
+ nextFrame(() => {
2802
+ if (announceRef.current) announceRef.current.textContent = text;
2803
+ });
2804
+ return;
2805
+ }
2806
+ el.textContent = text;
2807
+ }, []);
2808
+ useEffect8(() => {
2809
+ if (api.status === "submitting") announce(strings.submittingAnnouncement);
2810
+ }, [api.status, announce, strings.submittingAnnouncement]);
2811
+ const resuming = api.resumedDraft || api.editingPrevious;
2812
+ const resumeAnnouncement = api.editingPrevious ? strings.editNotice : strings.resumeNotice;
2813
+ useEffect8(() => {
2814
+ if (resuming) announce(resumeAnnouncement);
2815
+ }, [resuming, resumeAnnouncement, announce]);
2370
2816
  const themed = useMemo5(() => themeProps(theme), [theme]);
2817
+ const warningCode = resyncWarning ? resyncWarning.code : props.syncWarningCode;
2818
+ const warningUrl = resyncWarning ? resyncWarning.url : props.syncWarningUrl;
2819
+ const uploadsAvailable = resyncWarning ? resyncWarning.uploadsAvailable : props.uploadsAvailable;
2820
+ const uploadFileSizeLimitMb = resyncWarning ? resyncWarning.uploadFileSizeLimitMb : props.uploadFileSizeLimitMb;
2821
+ const chromeValue = useMemo5(
2822
+ () => ({
2823
+ devChrome,
2824
+ uploadsAvailable,
2825
+ uploadFileSizeLimitMb,
2826
+ warningCode,
2827
+ warningUrl
2828
+ }),
2829
+ [devChrome, uploadsAvailable, uploadFileSizeLimitMb, warningCode, warningUrl]
2830
+ );
2371
2831
  const settings = schema.settings;
2372
2832
  const submitted = api.status === "submitted";
2373
2833
  const liveSubmit = submitted && !api.restoredSubmission;
@@ -2376,21 +2836,25 @@ function ResolvedForm(props) {
2376
2836
  const url = safeHttpUrl(settings.redirectUrl);
2377
2837
  if (url) window.location.assign(url);
2378
2838
  }, [liveSubmit, settings.redirectUrl]);
2379
- const successRef = useRef7(null);
2839
+ const successRef = useRef8(null);
2380
2840
  useEffect8(() => {
2381
2841
  if (liveSubmit) successRef.current?.focus();
2382
2842
  }, [liveSubmit]);
2383
- const blocksRef = useRef7(null);
2384
- const prevPageIndex = useRef7(api.pageIndex);
2843
+ const blocksRef = useRef8(null);
2844
+ const prevPageIndex = useRef8(api.pageIndex);
2385
2845
  useEffect8(() => {
2386
2846
  if (prevPageIndex.current === api.pageIndex) return;
2387
2847
  prevPageIndex.current = api.pageIndex;
2388
- blocksRef.current?.focus();
2848
+ const container = blocksRef.current?.parentElement;
2849
+ const heading = container?.querySelector(".fillo-page-title, .fillo-title");
2850
+ const target = heading ?? blocksRef.current;
2851
+ if (!target) return;
2852
+ if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1");
2853
+ target.focus();
2389
2854
  }, [api.pageIndex]);
2390
2855
  const appearance = props.appearance;
2391
- const strings = resolveStrings(props.strings);
2392
2856
  if (api.status === "submitted") {
2393
- return /* @__PURE__ */ jsxs5(
2857
+ return /* @__PURE__ */ jsxs6(
2394
2858
  "div",
2395
2859
  {
2396
2860
  ref: successRef,
@@ -2406,19 +2870,19 @@ function ResolvedForm(props) {
2406
2870
  "data-state": "submitted",
2407
2871
  ...themed,
2408
2872
  children: [
2409
- props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5(
2873
+ props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs6(
2410
2874
  "div",
2411
2875
  {
2412
2876
  className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
2413
2877
  "data-fillo": "success",
2414
2878
  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 })
2879
+ /* @__PURE__ */ jsx7("div", { className: "fillo-success-mark", "aria-hidden": "true" }),
2880
+ /* @__PURE__ */ jsx7("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
2881
+ /* @__PURE__ */ jsx7("p", { className: "fillo-success-message", children: api.duplicateSubmission ? strings.alreadyAnswered : settings.successMessage ?? strings.successMessage })
2418
2882
  ]
2419
2883
  }
2420
2884
  ),
2421
- props.poweredBy && /* @__PURE__ */ jsx6(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2885
+ props.poweredBy && /* @__PURE__ */ jsx7(PoweredBy, { label: props.brandLabel, href: props.brandHref })
2422
2886
  ]
2423
2887
  }
2424
2888
  );
@@ -2434,13 +2898,200 @@ function ResolvedForm(props) {
2434
2898
  const progressTotal = Math.max(progressSeq.length, 1);
2435
2899
  const progressPos = progressSeq.indexOf(api.page.id);
2436
2900
  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(
2901
+ const submitFixUrl = devChrome && resolutionFailureCode === "form_not_published" && warningCode === "storage_required" ? safeHttpUrl(warningUrl) : null;
2902
+ const invalidFields = [];
2903
+ for (const block of api.blocks) {
2904
+ if (!isField2(block)) continue;
2905
+ const raw = api.errors[block.id];
2906
+ if (!raw) continue;
2907
+ invalidFields.push({
2908
+ field: block,
2909
+ error: raw === REQUIRED_FIELD_MESSAGE2 ? strings.required : raw,
2910
+ inputId: createFilloFieldIds(instanceId, block.id).inputId
2911
+ });
2912
+ }
2913
+ const rootClass = slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
2914
+ slot: "root",
2915
+ status: api.status
2916
+ });
2917
+ const formBody = /* @__PURE__ */ jsxs6(Fragment3, { children: [
2918
+ /* @__PURE__ */ jsx7(
2919
+ "span",
2920
+ {
2921
+ ref: announceRef,
2922
+ className: "fillo-sr-only",
2923
+ role: "status",
2924
+ "aria-live": "polite",
2925
+ "data-fillo": "announce"
2926
+ }
2927
+ ),
2928
+ devChrome && /* @__PURE__ */ jsx7(
2929
+ DevChrome,
2930
+ {
2931
+ preview: props.preview,
2932
+ devNotices: props.devNotices,
2933
+ syncError: props.syncErrorNotice,
2934
+ staged: props.stagedNotice,
2935
+ draft: props.draftNotice,
2936
+ warningUrl: props.syncWarningUrl,
2937
+ noClient: !props.client && !props.skipValidation
2938
+ }
2939
+ ),
2940
+ resuming && /* @__PURE__ */ jsxs6(
2941
+ "div",
2942
+ {
2943
+ className: slotClass3("fillo-resume", appearance, { slot: "resume" }),
2944
+ "data-fillo": "resume",
2945
+ children: [
2946
+ /* @__PURE__ */ jsx7("span", { className: "fillo-resume-text", children: api.editingPrevious ? strings.editNotice : strings.resumeNotice }),
2947
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "fillo-resume-clear", onClick: api.resetDraft, children: strings.resumeStartOver })
2948
+ ]
2949
+ }
2950
+ ),
2951
+ api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx7(
2952
+ "div",
2953
+ {
2954
+ className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
2955
+ "data-fillo": "resume",
2956
+ role: "status",
2957
+ children: /* @__PURE__ */ jsx7("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
2958
+ }
2959
+ ),
2960
+ multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx7(
2961
+ "div",
2962
+ {
2963
+ className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
2964
+ "data-fillo": "progress",
2965
+ role: "progressbar",
2966
+ "aria-label": schema.title || "Form progress",
2967
+ "aria-valuemin": 1,
2968
+ "aria-valuemax": progressTotal,
2969
+ "aria-valuenow": progressStep,
2970
+ children: /* @__PURE__ */ jsx7(
2971
+ "div",
2972
+ {
2973
+ className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
2974
+ "data-fillo": "progressFill",
2975
+ style: { "--fillo-progress-value": `${progressStep / progressTotal * 100}%` }
2976
+ }
2977
+ )
2978
+ }
2979
+ ),
2980
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs6("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
2981
+ schema.title && /* @__PURE__ */ jsx7("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2982
+ schema.description && /* @__PURE__ */ jsx7(
2983
+ "p",
2984
+ {
2985
+ className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2986
+ "data-fillo": "description",
2987
+ children: schema.description
2988
+ }
2989
+ )
2990
+ ] }),
2991
+ 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 }),
2992
+ invalidFields.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "fillo-error-summary", role: "alert", tabIndex: -1, "data-fillo": "error-summary", children: [
2993
+ /* @__PURE__ */ jsx7("p", { className: "fillo-error-summary-title", children: strings.errorSummaryTitle }),
2994
+ /* @__PURE__ */ jsx7("ul", { children: invalidFields.map(({ field, error, inputId }) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsxs6(
2995
+ "a",
2996
+ {
2997
+ href: `#${inputId}`,
2998
+ onClick: (e) => {
2999
+ e.preventDefault();
3000
+ focusField(inputId);
3001
+ },
3002
+ children: [
3003
+ field.label,
3004
+ ": ",
3005
+ error
3006
+ ]
3007
+ }
3008
+ ) }, field.id)) })
3009
+ ] }),
3010
+ /* @__PURE__ */ jsx7("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx7(
3011
+ BlockRenderer,
3012
+ {
3013
+ block,
3014
+ api,
3015
+ components: props.components,
3016
+ customComponents: props.customComponents
3017
+ },
3018
+ block.id
3019
+ )) }),
3020
+ !displayOnly && /* @__PURE__ */ jsx7(
3021
+ "input",
3022
+ {
3023
+ type: "text",
3024
+ name: "fillo_hp_field",
3025
+ className: "fillo-hp",
3026
+ tabIndex: -1,
3027
+ autoComplete: "off",
3028
+ "aria-hidden": "true",
3029
+ defaultValue: "",
3030
+ style: HP_STYLE,
3031
+ ref: (element) => {
3032
+ hpRef.current = element;
3033
+ assertHpHidden(element);
3034
+ }
3035
+ }
3036
+ ),
3037
+ challenge && showChallenge && /* @__PURE__ */ jsxs6("div", { className: "fillo-turnstile-slot", "data-fillo": "turnstile-slot", children: [
3038
+ /* @__PURE__ */ jsx7(
3039
+ TurnstileWidget,
3040
+ {
3041
+ siteKey: challenge.siteKey,
3042
+ className: slotClass3("fillo-turnstile", appearance, { slot: "turnstile" }),
3043
+ onToken: handleChallengeToken,
3044
+ onError: () => setChallengeError(true),
3045
+ registerReset: registerChallengeReset
3046
+ }
3047
+ ),
3048
+ challengeError && /* @__PURE__ */ jsx7("p", { className: "fillo-turnstile-error", role: "alert", children: strings.challengeUnavailable })
3049
+ ] }),
3050
+ (api.status === "error" || api.submitError) && /* @__PURE__ */ jsxs6("p", { className: "fillo-submit-error", role: "alert", children: [
3051
+ api.submitError ?? strings.submitFailed,
3052
+ submitFixUrl && /* @__PURE__ */ jsxs6(Fragment3, { children: [
3053
+ " ",
3054
+ "Connect storage:",
3055
+ " ",
3056
+ /* @__PURE__ */ jsx7("a", { href: submitFixUrl, target: "_blank", rel: "noopener noreferrer", children: submitFixUrl })
3057
+ ] })
3058
+ ] }),
3059
+ showFooter && /* @__PURE__ */ jsxs6("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
3060
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx7(
3061
+ "button",
3062
+ {
3063
+ type: "button",
3064
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
3065
+ "data-fillo": "button",
3066
+ onClick: api.back,
3067
+ children: strings.back
3068
+ }
3069
+ ),
3070
+ showPrimaryButton && /* @__PURE__ */ jsx7(
3071
+ "button",
3072
+ {
3073
+ type: "submit",
3074
+ className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
3075
+ "data-fillo": "button",
3076
+ disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
3077
+ // "Next"). The server rejects a tokenless submit regardless —
3078
+ // this is UX only.
3079
+ showChallenge && !challengeToken,
3080
+ children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
3081
+ }
3082
+ )
3083
+ ] }),
3084
+ props.poweredBy && /* @__PURE__ */ jsx7(PoweredBy, { label: props.brandLabel, href: props.brandHref })
3085
+ ] });
3086
+ return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx7(FilloAnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children: displayOnly ? (
3087
+ // Display-only preview: a plain div (no form element to submit) with
3088
+ // every control inside a natively disabled fieldset — unfocusable
3089
+ // and unfillable even without the stylesheet or inert support.
3090
+ /* @__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 }) })
3091
+ ) : /* @__PURE__ */ jsx7(
2438
3092
  "form",
2439
3093
  {
2440
- className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
2441
- slot: "root",
2442
- status: api.status
2443
- }),
3094
+ className: rootClass,
2444
3095
  "data-fillo": "root",
2445
3096
  "data-state": api.status,
2446
3097
  "data-page": api.pageIndex + 1,
@@ -2451,159 +3102,35 @@ function ResolvedForm(props) {
2451
3102
  e.preventDefault();
2452
3103
  const formEl = e.currentTarget;
2453
3104
  if (api.isLastPage) {
2454
- void api.submit().then(() => focusFirstInvalid(formEl)).catch(() => {
3105
+ void api.submit().then(() => focusErrorSummary(formEl)).catch(() => {
2455
3106
  });
2456
3107
  } else {
2457
3108
  api.next();
2458
- focusFirstInvalid(formEl);
3109
+ focusErrorSummary(formEl);
2459
3110
  }
2460
3111
  },
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
- ]
3112
+ children: formBody
2587
3113
  }
2588
- ) }) }) }) });
3114
+ ) }) }) }) }) }) });
2589
3115
  }
2590
3116
 
2591
3117
  // src/jsx.tsx
2592
- import { useRef as useRef8 } from "react";
3118
+ import { useRef as useRef9 } from "react";
2593
3119
  import {
2594
3120
  codeFormFromJsx,
2595
3121
  FilloJsxError,
3122
+ isLikelyDevEnv as isLikelyDevEnv2,
2596
3123
  JSX_BLOCK_COMPONENTS
2597
3124
  } from "@usefillo/core";
2598
- import { jsx as jsx7 } from "react/jsx-runtime";
3125
+ import { jsx as jsx8 } from "react/jsx-runtime";
2599
3126
  var B = JSX_BLOCK_COMPONENTS;
2600
- var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
3127
+ var isDevEnv = () => isLikelyDevEnv2();
2601
3128
  function JsxForm(props) {
2602
3129
  const { id, title, description, settings, theme, client, children, ...rest } = props;
2603
3130
  const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
2604
3131
  const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
2605
- const memo2 = useRef8(null);
2606
- if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
3132
+ const memo2 = useRef9(null);
3133
+ if (memo2.current && memo2.current.json !== json && isDevEnv()) {
2607
3134
  console.warn(
2608
3135
  `[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
3136
  );
@@ -2611,7 +3138,7 @@ function JsxForm(props) {
2611
3138
  if (!memo2.current || memo2.current.json !== json) {
2612
3139
  memo2.current = { json, form: compiled };
2613
3140
  }
2614
- return /* @__PURE__ */ jsx7(FilloForm, { form: memo2.current.form, client, ...rest });
3141
+ return /* @__PURE__ */ jsx8(FilloForm, { form: memo2.current.form, client, ...rest });
2615
3142
  }
2616
3143
  function defineFormFromJsx(element) {
2617
3144
  if (!element || element.type !== JsxForm) {
@@ -2646,6 +3173,7 @@ var Fillo = {
2646
3173
  Date: B.Date,
2647
3174
  FileUpload: B.FileUpload,
2648
3175
  Hidden: B.Hidden,
3176
+ Calculated: B.Calculated,
2649
3177
  Custom: B.Custom,
2650
3178
  Heading: B.Heading,
2651
3179
  Paragraph: B.Paragraph,
@@ -2655,7 +3183,7 @@ var Fillo = {
2655
3183
  };
2656
3184
 
2657
3185
  // src/provider.tsx
2658
- import { useId as useId3, useMemo as useMemo6 } from "react";
3186
+ import { useId as useId3, useMemo as useMemo6, useState as useState7 } from "react";
2659
3187
  import {
2660
3188
  FilloError as FilloError3,
2661
3189
  formSchemasEqual as formSchemasEqual2,
@@ -2664,7 +3192,7 @@ import {
2664
3192
  resolveStrings as resolveStrings2,
2665
3193
  syncCodeForm as syncCodeForm3
2666
3194
  } from "@usefillo/core";
2667
- import { Fragment as Fragment3, jsx as jsx8 } from "react/jsx-runtime";
3195
+ import { Fragment as Fragment4, jsx as jsx9 } from "react/jsx-runtime";
2668
3196
  function FilloProvider({
2669
3197
  children,
2670
3198
  form,
@@ -2674,20 +3202,31 @@ function FilloProvider({
2674
3202
  strings,
2675
3203
  onError,
2676
3204
  renderError,
3205
+ preview,
2677
3206
  ...options
2678
3207
  }) {
2679
3208
  const instanceId = useId3().replace(/:/g, "");
3209
+ const devEnv = useIsDevEnv();
3210
+ const devChrome = preview === true || devEnv;
3211
+ if (preview === true) warnPreviewInProduction();
2680
3212
  const codeForm = isCodeForm2(form) ? form : null;
2681
3213
  const {
2682
3214
  formId: syncedFormId,
2683
3215
  status: syncedStatus,
2684
3216
  error: syncError,
2685
3217
  syncing: codeFormSyncing,
2686
- resolvedSchema
3218
+ resolvedSchema,
3219
+ accepting: syncedAccepting,
3220
+ acceptingReason: syncedAcceptingReason,
3221
+ uploadsAvailable,
3222
+ uploadFileSizeLimitMb,
3223
+ warningCode,
3224
+ warningUrl
2687
3225
  } = useCodeFormSync(form, client, onError, {
2688
3226
  allowUnsynced: Boolean(!client || formId || options.skipValidation)
2689
3227
  });
2690
- const rawSchema = codeForm ? resolvedSchema && !isDevEnv3() ? resolvedSchema : codeForm.schema : form;
3228
+ const [resyncWarning, setResyncWarning] = useState7(null);
3229
+ const rawSchema = codeForm ? resolvedSchema && !devChrome ? resolvedSchema : codeForm.schema : form;
2691
3230
  const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema]);
2692
3231
  if (!normalized.ok) {
2693
3232
  throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
@@ -2697,6 +3236,12 @@ function FilloProvider({
2697
3236
  () => codeForm && client?.key ? async () => {
2698
3237
  try {
2699
3238
  const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
3239
+ setResyncWarning({
3240
+ code: result.warningCode,
3241
+ url: result.warningUrl,
3242
+ uploadsAvailable: result.uploadsAvailable,
3243
+ uploadFileSizeLimitMb: result.uploadFileSizeLimitMb
3244
+ });
2700
3245
  if (result.status === "draft") {
2701
3246
  throw new FilloError3(
2702
3247
  "This form is no longer published. Submission was stopped before sending answers.",
@@ -2705,6 +3250,14 @@ function FilloProvider({
2705
3250
  "form_not_published"
2706
3251
  );
2707
3252
  }
3253
+ if (result.accepting === false) {
3254
+ throw new FilloError3(
3255
+ "This form is no longer accepting responses. Submission was stopped before sending answers.",
3256
+ 403,
3257
+ void 0,
3258
+ result.acceptingReason ?? "form_not_accepting"
3259
+ );
3260
+ }
2708
3261
  const authoritative = normalizeFormSchema2(result.resolvedSchema ?? codeForm.schema);
2709
3262
  if (!authoritative.ok) {
2710
3263
  throw new FilloError3(
@@ -2752,25 +3305,45 @@ function FilloProvider({
2752
3305
  form: schema,
2753
3306
  formId: syncedFormId ?? formId,
2754
3307
  resolveFormId,
3308
+ // Dev chrome (preview or dev env) surfaces the developer-grade resolution
3309
+ // failure (message + code); an explicit option wins.
3310
+ verboseResolutionErrors: options.verboseResolutionErrors ?? devChrome,
2755
3311
  // Headless = your markup, no Fillo-rendered layout. Free like every embed
2756
3312
  // method; recorded per response so usage stays measurable.
2757
3313
  surface: "headless"
2758
3314
  });
2759
3315
  useDraftFlush(api);
3316
+ const freshWarningCode = resyncWarning ? resyncWarning.code : warningCode;
3317
+ const freshWarningUrl = resyncWarning ? resyncWarning.url : warningUrl;
3318
+ const freshUploadsAvailable = resyncWarning ? resyncWarning.uploadsAvailable : uploadsAvailable;
3319
+ const freshUploadFileSizeLimitMb = resyncWarning ? resyncWarning.uploadFileSizeLimitMb : uploadFileSizeLimitMb;
3320
+ const chromeValue = useMemo6(
3321
+ () => ({
3322
+ devChrome,
3323
+ uploadsAvailable: freshUploadsAvailable,
3324
+ uploadFileSizeLimitMb: freshUploadFileSizeLimitMb,
3325
+ warningCode: freshWarningCode,
3326
+ warningUrl: freshWarningUrl
3327
+ }),
3328
+ [devChrome, freshUploadsAvailable, freshUploadFileSizeLimitMb, freshWarningCode, freshWarningUrl]
3329
+ );
2760
3330
  const resolvedStrings = resolveStrings2(strings);
2761
3331
  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;
3332
+ if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx9(Fragment4, { children: renderError(canonicalSyncError) });
3333
+ if (canonicalSyncError && !devChrome) return null;
3334
+ if (codeFormSyncing && !options.skipValidation && !devChrome) return null;
2765
3335
  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;
3336
+ const notAccepting = Boolean(codeForm) && syncedStatus === "published" && syncedAccepting === false && !options.skipValidation;
3337
+ if ((notPublished || notAccepting) && !devChrome) {
3338
+ const error = notPublished ? new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published") : new FilloError3(
3339
+ "This form is no longer accepting responses.",
3340
+ 403,
3341
+ void 0,
3342
+ syncedAcceptingReason ?? "form_not_accepting"
3343
+ );
3344
+ return renderError ? /* @__PURE__ */ jsx9(Fragment4, { children: renderError(error) }) : null;
2769
3345
  }
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";
3346
+ 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
3347
  }
2775
3348
 
2776
3349
  // src/index.ts