@usefillo/react 0.7.0 → 0.8.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,10 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  // src/FilloForm.tsx
4
- import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
4
+ import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as useRef6, useState as useState6 } from "react";
5
5
  import {
6
6
  FilloError as FilloError2,
7
7
  createClient,
8
+ formSchemasEqual,
9
+ isFilloError as isFilloError2,
8
10
  needsExplicitSubmit,
9
11
  normalizeFormSchema,
10
12
  normalizeFormTheme,
@@ -15,6 +17,7 @@ import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
15
17
  // src/appearance.tsx
16
18
  import { createContext, useContext } from "react";
17
19
  import {
20
+ DEFAULT_FIELD_STRINGS,
18
21
  DEFAULT_STRINGS,
19
22
  slotClass
20
23
  } from "@usefillo/core";
@@ -49,7 +52,10 @@ function useFieldSlots(field, error) {
49
52
  control: (base) => slotClass(base, appearance, { slot: "control", ...state })
50
53
  };
51
54
  }
52
- var FilloStringsContext = createContext(DEFAULT_STRINGS);
55
+ var FilloStringsContext = createContext({
56
+ ...DEFAULT_STRINGS,
57
+ ...DEFAULT_FIELD_STRINGS
58
+ });
53
59
  function useStrings() {
54
60
  return useContext(FilloStringsContext);
55
61
  }
@@ -175,9 +181,10 @@ import {
175
181
  contentHash,
176
182
  FilloError,
177
183
  isCodeForm,
184
+ isFilloError,
178
185
  syncCodeForm
179
186
  } from "@usefillo/core";
180
- import { useEffect as useEffect2, useState } from "react";
187
+ import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
181
188
  import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
182
189
  var noticedSync = /* @__PURE__ */ new Set();
183
190
  function noticeSync(handle, kind, message) {
@@ -186,39 +193,156 @@ function noticeSync(handle, kind, message) {
186
193
  noticedSync.add(key);
187
194
  console.info(message);
188
195
  }
189
- function useCodeFormSync(form, client, onError) {
196
+ function useCodeFormSync(form, client, onError, options) {
190
197
  const codeForm = isCodeForm(form) ? form : null;
191
- const [synced, setSynced] = useState({ formId: null, status: null });
198
+ const [synced, setSynced] = useState({
199
+ key: null,
200
+ client: void 0,
201
+ settled: false,
202
+ formId: null,
203
+ status: null,
204
+ error: null,
205
+ noticeError: null,
206
+ staged: false
207
+ });
192
208
  const hasKey = Boolean(client?.key);
209
+ const allowUnsynced = Boolean(options?.allowUnsynced);
193
210
  const handle = codeForm?.id;
194
211
  const contentKey = codeForm ? contentHash(JSON.stringify(codeForm.schema) + JSON.stringify(codeForm.theme ?? null)) : null;
212
+ const syncKey = codeForm ? client?.key ? `${client.baseUrl}|${client.key}|${codeForm.id}|${contentKey}` : allowUnsynced ? null : `${client?.baseUrl ?? ""}|<missing-key>|${codeForm.id}|${contentKey}` : null;
213
+ const onErrorRef = useRef2(onError);
214
+ onErrorRef.current = onError;
195
215
  useEffect2(() => {
196
- if (!codeForm || !client?.key) return;
216
+ if (!codeForm || !syncKey) return;
197
217
  let cancelled = false;
218
+ setSynced(
219
+ (previous) => previous.key === syncKey && previous.client === client ? {
220
+ ...previous,
221
+ settled: false,
222
+ formId: null,
223
+ status: null,
224
+ error: null,
225
+ noticeError: null,
226
+ staged: false,
227
+ resolvedSchema: void 0,
228
+ resolvedTheme: void 0
229
+ } : {
230
+ key: syncKey,
231
+ client,
232
+ settled: false,
233
+ formId: null,
234
+ status: null,
235
+ error: null,
236
+ noticeError: null,
237
+ staged: false
238
+ }
239
+ );
240
+ if (!client?.key) {
241
+ const error = new FilloError(
242
+ 'Code forms need createClient({ key: "pk_\u2026" }) to resolve the live form. Pass a publishable key or a published formId.',
243
+ 401,
244
+ void 0,
245
+ "sync_key_required"
246
+ );
247
+ console.warn(`[fillo] form sync failed (${error.code}): ${error.message}`);
248
+ setSynced({
249
+ key: syncKey,
250
+ client,
251
+ settled: true,
252
+ formId: null,
253
+ status: null,
254
+ error,
255
+ noticeError: null,
256
+ staged: false
257
+ });
258
+ onErrorRef.current?.(error);
259
+ return;
260
+ }
198
261
  syncCodeForm(client, codeForm).then((r) => {
199
262
  if (cancelled) return;
200
- setSynced({ formId: r.formId, status: r.status ?? null });
263
+ if ((r.staged || r.syncError) && !r.resolvedSchema) {
264
+ throw new FilloError(
265
+ "Sync did not return the live schema needed to render safely. Publish changes before deploy, and update @usefillo/* or your API.",
266
+ 409,
267
+ void 0,
268
+ "sync_snapshot_required"
269
+ );
270
+ }
271
+ const noticeError = r.syncError ? new FilloError(r.syncError.message, 403, void 0, r.syncError.code) : null;
272
+ setSynced({
273
+ key: syncKey,
274
+ client,
275
+ settled: true,
276
+ formId: r.formId,
277
+ status: r.status ?? null,
278
+ branding: r.branding,
279
+ error: null,
280
+ noticeError,
281
+ staged: Boolean(r.staged),
282
+ resolvedSchema: r.resolvedSchema,
283
+ resolvedTheme: r.resolvedTheme
284
+ });
201
285
  const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
202
286
  if (r.warning) console.warn(`[fillo] ${r.warning}`);
287
+ if (noticeError) {
288
+ console.warn(
289
+ `[fillo] form sync needs attention (${noticeError.code}): ${noticeError.message}`
290
+ );
291
+ onErrorRef.current?.(noticeError);
292
+ }
203
293
  if (r.staged) {
204
- noticeSync(codeForm.id, "staged", `[fillo] "${codeForm.id}": changes staged as a draft \u2014 publish them at ${dashboardUrl}`);
294
+ noticeSync(
295
+ codeForm.id,
296
+ "staged",
297
+ `[fillo] "${codeForm.id}": changes staged as a draft \u2014 the live version remains rendered until you publish them at ${dashboardUrl}`
298
+ );
205
299
  } else if (r.status === "draft") {
206
300
  noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
207
301
  }
208
302
  }).catch((err) => {
209
- const error = err instanceof FilloError ? err : new FilloError(String(err), 0);
210
- console.warn("[fillo] form sync failed:", error.message);
211
- if (!cancelled) onError?.(error);
303
+ if (cancelled) return;
304
+ const error = isFilloError(err) ? err : new FilloError(String(err), 0);
305
+ console.warn(
306
+ `[fillo] form sync failed${error.code ? ` (${error.code})` : ""}: ${error.message}`
307
+ );
308
+ setSynced({
309
+ key: syncKey,
310
+ client,
311
+ settled: true,
312
+ formId: null,
313
+ status: null,
314
+ error,
315
+ noticeError: null,
316
+ staged: false
317
+ });
318
+ onErrorRef.current?.(error);
212
319
  });
213
320
  return () => {
214
321
  cancelled = true;
215
322
  };
216
- }, [hasKey, handle, contentKey, client, onError]);
217
- return synced;
323
+ }, [hasKey, handle, contentKey, client, syncKey, allowUnsynced]);
324
+ return synced.key === syncKey && synced.client === client ? {
325
+ formId: synced.formId,
326
+ status: synced.status,
327
+ branding: synced.branding,
328
+ error: synced.error,
329
+ noticeError: synced.noticeError,
330
+ syncing: Boolean(syncKey) && !synced.settled,
331
+ staged: synced.staged,
332
+ resolvedSchema: synced.resolvedSchema,
333
+ resolvedTheme: synced.resolvedTheme
334
+ } : {
335
+ formId: null,
336
+ status: null,
337
+ error: null,
338
+ noticeError: null,
339
+ syncing: Boolean(syncKey),
340
+ staged: false
341
+ };
218
342
  }
219
343
 
220
344
  // src/fields.tsx
221
- import { isField, pipeBlock, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
345
+ import { isField, pipeBlock, REQUIRED_FIELD_MESSAGE, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
222
346
  import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
223
347
 
224
348
  // src/phone.tsx
@@ -230,9 +354,10 @@ import {
230
354
  flagEmoji,
231
355
  formatNational,
232
356
  parsePhone,
357
+ positionPhonePopover,
233
358
  toE164
234
359
  } from "@usefillo/core";
235
- import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
360
+ import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
236
361
  import { jsx, jsxs } from "react/jsx-runtime";
237
362
  function localeCountry() {
238
363
  if (typeof navigator === "undefined") return void 0;
@@ -253,31 +378,6 @@ function nationalFor(stored, country) {
253
378
  const digits = digitsOnly(stored);
254
379
  return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
255
380
  }
256
- var PHONE_POPOVER_VIEWPORT_GAP = 8;
257
- function positionPhonePopover(anchor, popover) {
258
- if (!anchor || !popover || typeof window === "undefined") return "below";
259
- const gap = PHONE_POPOVER_VIEWPORT_GAP;
260
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
261
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
262
- const anchorRect = anchor.getBoundingClientRect();
263
- const spaceBelow = Math.max(0, viewportHeight - anchorRect.bottom - gap);
264
- const spaceAbove = Math.max(0, anchorRect.top - gap);
265
- const placement = spaceBelow < popover.scrollHeight && spaceAbove > spaceBelow ? "above" : "below";
266
- const availableHeight = placement === "above" ? spaceAbove : spaceBelow;
267
- const maxHeight = Math.max(0, Math.min(availableHeight, viewportHeight - gap * 2));
268
- const maxWidth = Math.max(0, viewportWidth - gap * 2);
269
- popover.style.setProperty("--fillo-phone-popover-max-height", `${Math.floor(maxHeight)}px`);
270
- popover.style.setProperty("--fillo-phone-popover-max-width", `${Math.floor(maxWidth)}px`);
271
- popover.style.setProperty("--fillo-phone-popover-offset-x", "0px");
272
- const width = Math.min(popover.offsetWidth, maxWidth);
273
- let offsetX = 0;
274
- const rightOverflow = anchorRect.left + width - (viewportWidth - gap);
275
- if (rightOverflow > 0) offsetX -= rightOverflow;
276
- const leftOverflow = gap - (anchorRect.left + offsetX);
277
- if (leftOverflow > 0) offsetX += leftOverflow;
278
- popover.style.setProperty("--fillo-phone-popover-offset-x", `${Math.round(offsetX)}px`);
279
- return placement;
280
- }
281
381
  function PhoneField({ field, value, error, setValue, ids: providedIds }) {
282
382
  const ids = useFilloFieldIds(field.id, providedIds);
283
383
  const slots = useFieldSlots(field, error);
@@ -287,7 +387,12 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
287
387
  const [country, setCountry] = useState2(
288
388
  () => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? PHONE_COUNTRIES[0]
289
389
  );
290
- const touched = useRef2(false);
390
+ const touched = useRef3(false);
391
+ useEffect3(() => {
392
+ if (touched.current || !stored) return;
393
+ const restoredCountry = parsePhone(stored).country;
394
+ if (restoredCountry) setCountry(restoredCountry);
395
+ }, [stored]);
291
396
  useEffect3(() => {
292
397
  if (touched.current) return;
293
398
  if (parsePhone(stored).country || countryByIso(schema.defaultCountry)) return;
@@ -296,8 +401,8 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
296
401
  }, []);
297
402
  const national = nationalFor(stored, country);
298
403
  const display = formatNational(country, national);
299
- const inputRef = useRef2(null);
300
- const caretDigits = useRef2(null);
404
+ const inputRef = useRef3(null);
405
+ const caretDigits = useRef3(null);
301
406
  useLayoutEffect(() => {
302
407
  const el = inputRef.current;
303
408
  if (!el || caretDigits.current == null) return;
@@ -375,10 +480,10 @@ function CountrySelect({
375
480
  const [query, setQuery] = useState2("");
376
481
  const [active, setActive] = useState2(0);
377
482
  const [placement, setPlacement] = useState2("below");
378
- const rootRef = useRef2(null);
379
- const triggerRef = useRef2(null);
380
- const popoverRef = useRef2(null);
381
- const searchRef = useRef2(null);
483
+ const rootRef = useRef3(null);
484
+ const triggerRef = useRef3(null);
485
+ const popoverRef = useRef3(null);
486
+ const searchRef = useRef3(null);
382
487
  const listId = `${labelledBy}-countries`;
383
488
  const matches = useMemo3(() => {
384
489
  const q = query.trim().toLowerCase();
@@ -448,6 +553,7 @@ function CountrySelect({
448
553
  className: "fillo-phone-flag",
449
554
  "aria-haspopup": "listbox",
450
555
  "aria-expanded": open,
556
+ "aria-controls": open ? listId : void 0,
451
557
  "aria-label": `Country: ${country.name} (+${country.dialCode})`,
452
558
  onClick: () => setOpen((o) => !o),
453
559
  children: [
@@ -471,6 +577,7 @@ function CountrySelect({
471
577
  "aria-expanded": "true",
472
578
  "aria-controls": listId,
473
579
  "aria-autocomplete": "list",
580
+ "aria-label": "Search country or code",
474
581
  "aria-activedescendant": matches[active] ? `${listId}-opt-${active}` : void 0,
475
582
  placeholder: "Search country or code",
476
583
  value: query,
@@ -512,15 +619,15 @@ function CountrySelect({
512
619
  }
513
620
 
514
621
  // src/signature.tsx
515
- import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "react";
622
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
516
623
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
517
624
  function SignatureField({ field, value, error, setValue, ids: providedIds }) {
518
625
  const ids = useFilloFieldIds(field.id, providedIds);
519
626
  const slots = useFieldSlots(field, error);
520
627
  const strings = useStrings();
521
628
  const typedInputId = `${ids.inputId}-typed`;
522
- const canvasRef = useRef3(null);
523
- const drawing = useRef3(false);
629
+ const canvasRef = useRef4(null);
630
+ const drawing = useRef4(false);
524
631
  const [hasInk, setHasInk] = useState3(Boolean(value));
525
632
  const [typedName, setTypedName] = useState3("");
526
633
  useEffect4(() => {
@@ -537,12 +644,31 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
537
644
  ctx.lineCap = "round";
538
645
  ctx.lineJoin = "round";
539
646
  ctx.strokeStyle = getComputedStyle(canvas).color;
647
+ }, []);
648
+ useEffect4(() => {
649
+ const canvas = canvasRef.current;
650
+ const ctx = canvas?.getContext("2d");
651
+ if (!canvas || !ctx) return;
652
+ let cancelled = false;
653
+ const rect = canvas.getBoundingClientRect();
540
654
  if (typeof value === "string" && value.startsWith("data:image/")) {
655
+ setHasInk(true);
541
656
  const img = new Image();
542
- img.onload = () => ctx.drawImage(img, 0, 0, rect.width, rect.height);
657
+ img.onload = () => {
658
+ if (cancelled) return;
659
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
660
+ ctx.drawImage(img, 0, 0, rect.width, rect.height);
661
+ };
543
662
  img.src = value;
663
+ } else {
664
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
665
+ setHasInk(false);
666
+ setTypedName("");
544
667
  }
545
- }, []);
668
+ return () => {
669
+ cancelled = true;
670
+ };
671
+ }, [value]);
546
672
  function point(e) {
547
673
  const rect = e.currentTarget.getBoundingClientRect();
548
674
  return { x: e.clientX - rect.left, y: e.clientY - rect.top };
@@ -622,6 +748,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
622
748
  onPointerDown: start,
623
749
  onPointerMove: move,
624
750
  onPointerUp: end,
751
+ onPointerCancel: end,
625
752
  onPointerLeave: end
626
753
  }
627
754
  ),
@@ -657,21 +784,15 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
657
784
  }
658
785
 
659
786
  // src/upload.tsx
660
- import { useEffect as useEffect5, useRef as useRef4, useState as useState4 } from "react";
787
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
661
788
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
662
- function uploadStatus(inFlight, doneCount) {
789
+ function uploadStatus(inFlight, doneCount, strings) {
663
790
  const failed = inFlight.filter((f) => f.error).length;
664
791
  const active = inFlight.length - failed;
665
792
  const parts = [];
666
- if (active > 0) {
667
- parts.push(`Uploading ${active} ${active === 1 ? "file" : "files"}, please wait\u2026`);
668
- }
669
- if (failed > 0) {
670
- parts.push(`${failed} ${failed === 1 ? "upload" : "uploads"} failed.`);
671
- }
672
- if (active === 0 && doneCount > 0) {
673
- parts.push(`${doneCount} ${doneCount === 1 ? "file" : "files"} uploaded.`);
674
- }
793
+ if (active > 0) parts.push(strings.filesUploading(active));
794
+ if (failed > 0) parts.push(strings.uploadsFailed(failed));
795
+ if (active === 0 && doneCount > 0) parts.push(strings.filesUploaded(doneCount));
675
796
  return parts.join(" ");
676
797
  }
677
798
  function formatBytes(bytes) {
@@ -685,41 +806,49 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
685
806
  const slots = useFieldSlots(field, error);
686
807
  const strings = useStrings();
687
808
  const schema = field;
688
- const inputRef = useRef4(null);
809
+ const inputRef = useRef5(null);
689
810
  const [inFlight, setInFlight] = useState4([]);
690
811
  const [dragOver, setDragOver] = useState4(false);
691
- const mountedRef = useRef4(true);
692
- const controllers = useRef4(/* @__PURE__ */ new Map());
693
- const inFlightCount = useRef4(0);
694
- useEffect5(
695
- () => () => {
812
+ const mountedRef = useRef5(true);
813
+ const controllers = useRef5(/* @__PURE__ */ new Map());
814
+ const inFlightCount = useRef5(0);
815
+ const setUploading = api.setUploading;
816
+ const fieldId = field.id;
817
+ useEffect5(() => {
818
+ mountedRef.current = true;
819
+ return () => {
696
820
  mountedRef.current = false;
697
821
  controllers.current.forEach((c) => c.abort());
698
- },
699
- []
700
- );
822
+ if (inFlightCount.current > 0) setUploading(fieldId, false);
823
+ };
824
+ }, [fieldId, setUploading]);
701
825
  const files = Array.isArray(value) ? value : [];
826
+ const filesRef = useRef5(files);
827
+ filesRef.current = files;
702
828
  const maxFiles = schema.maxFiles ?? 1;
703
- const maxBytes = (schema.maxFileSizeMb ?? 500) * 1024 * 1024;
829
+ const maxMb = schema.maxFileSizeMb ?? 500;
830
+ const maxBytes = maxMb * 1024 * 1024;
704
831
  const canUpload = Boolean(api.client && api.formId);
705
- const remaining = maxFiles - files.length - inFlight.length;
832
+ const activeInFlight = inFlight.filter((f) => !f.error).length;
833
+ const remaining = maxFiles - files.length - activeInFlight;
706
834
  async function startUpload(file) {
835
+ if (maxFiles - filesRef.current.length - inFlightCount.current <= 0) return;
707
836
  const key = `${file.name}-${file.size}-${Math.floor(performance.now())}`;
708
837
  if (file.size > maxBytes) {
709
838
  setInFlight((prev) => [
710
839
  ...prev,
711
- { key, name: file.name, size: file.size, fraction: 0, error: `Larger than ${schema.maxFileSizeMb ?? 500} MB limit` }
840
+ { key, name: file.name, size: file.size, fraction: 0, file, tooLarge: true, error: strings.fileTooLarge(maxMb) }
712
841
  ]);
713
842
  return;
714
843
  }
715
- setInFlight((prev) => [...prev, { key, name: file.name, size: file.size, fraction: 0 }]);
844
+ setInFlight((prev) => [...prev, { key, name: file.name, size: file.size, fraction: 0, file }]);
716
845
  inFlightCount.current += 1;
717
- api.setUploading(field.id, true);
846
+ setUploading(fieldId, true);
718
847
  const controller = new AbortController();
719
848
  controllers.current.set(key, controller);
720
849
  try {
721
850
  const uploaded = await api.client.uploadFile(api.formId, file, {
722
- fieldId: field.id,
851
+ fieldId,
723
852
  signal: controller.signal,
724
853
  onProgress: ({ fraction }) => {
725
854
  if (mountedRef.current) {
@@ -729,11 +858,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
729
858
  });
730
859
  if (!mountedRef.current) return;
731
860
  setInFlight((prev) => prev.filter((f) => f.key !== key));
732
- const current = Array.isArray(api.data[field.id]) ? api.data[field.id] : [];
733
- setValue([...current, uploaded]);
861
+ const next = [...filesRef.current, uploaded].slice(0, maxFiles);
862
+ filesRef.current = next;
863
+ setValue(next);
734
864
  } catch (err) {
735
865
  if (mountedRef.current) {
736
- const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message : "Upload failed \u2014 try again";
866
+ const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message : strings.uploadFailed;
737
867
  setInFlight(
738
868
  (prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
739
869
  );
@@ -741,7 +871,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
741
871
  } finally {
742
872
  controllers.current.delete(key);
743
873
  inFlightCount.current -= 1;
744
- if (mountedRef.current && inFlightCount.current === 0) api.setUploading(field.id, false);
874
+ if (mountedRef.current && inFlightCount.current === 0) setUploading(fieldId, false);
745
875
  }
746
876
  }
747
877
  function cancelUpload(key) {
@@ -750,11 +880,14 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
750
880
  }
751
881
  function handleFiles(list) {
752
882
  if (!list) return;
753
- Array.from(list).slice(0, Math.max(0, remaining)).forEach((file) => void startUpload(file));
883
+ const currentRoom = Math.max(0, maxFiles - filesRef.current.length - inFlightCount.current);
884
+ Array.from(list).slice(0, currentRoom).forEach((file) => void startUpload(file));
754
885
  if (inputRef.current) inputRef.current.value = "";
755
886
  }
756
887
  function removeFile(fileId) {
757
- setValue(files.filter((f) => f.fileId !== fileId));
888
+ const next = filesRef.current.filter((f) => f.fileId !== fileId);
889
+ filesRef.current = next;
890
+ setValue(next);
758
891
  }
759
892
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
760
893
  return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
@@ -782,12 +915,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
782
915
  onClick: () => canUpload && inputRef.current?.click(),
783
916
  role: "button",
784
917
  tabIndex: 0,
918
+ "aria-disabled": !canUpload || void 0,
785
919
  "aria-labelledby": ids.labelId,
786
920
  "aria-required": field.required ? true : void 0,
787
921
  "aria-invalid": error ? true : void 0,
788
922
  "aria-describedby": describedBy,
789
923
  onKeyDown: (e) => {
790
- if ((e.key === "Enter" || e.key === " ") && canUpload) inputRef.current?.click();
924
+ if (e.key === "Enter" || e.key === " ") {
925
+ e.preventDefault();
926
+ if (canUpload) inputRef.current?.click();
927
+ }
791
928
  },
792
929
  children: [
793
930
  /* @__PURE__ */ jsx3(
@@ -797,23 +934,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
797
934
  id: ids.inputId,
798
935
  type: "file",
799
936
  hidden: true,
937
+ disabled: !canUpload,
800
938
  multiple: maxFiles > 1,
801
939
  accept: schema.accept?.join(","),
802
940
  onChange: (e) => canUpload && handleFiles(e.target.files)
803
941
  }
804
942
  ),
805
943
  canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
806
- /* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-title", children: [
807
- "Drop ",
808
- maxFiles > 1 ? "files" : "a file",
809
- " here or click to browse"
810
- ] }),
811
- /* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-hint", children: [
812
- "Up to ",
813
- schema.maxFileSizeMb ?? 500,
814
- " MB per file \xB7 resumable"
815
- ] })
816
- ] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: "Uploads are disabled in preview" })
944
+ /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
945
+ /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
946
+ ] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.uploadsDisabled })
817
947
  ]
818
948
  }
819
949
  ),
@@ -824,7 +954,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
824
954
  "aria-live": "polite",
825
955
  className: "fillo-sr-only",
826
956
  style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
827
- children: uploadStatus(inFlight, files.length)
957
+ children: uploadStatus(inFlight, files.length, strings)
828
958
  }
829
959
  ),
830
960
  (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
@@ -857,6 +987,21 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
857
987
  /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
858
988
  f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
859
989
  /* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
990
+ f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx3(
991
+ "button",
992
+ {
993
+ type: "button",
994
+ className: "fillo-file-retry",
995
+ "aria-label": `${strings.uploadRetry} ${f.name}`,
996
+ onClick: () => {
997
+ const retryFile = f.file;
998
+ if (!retryFile) return;
999
+ setInFlight((prev) => prev.filter((x) => x.key !== f.key));
1000
+ void startUpload(retryFile);
1001
+ },
1002
+ children: strings.uploadRetry
1003
+ }
1004
+ ),
860
1005
  /* @__PURE__ */ jsx3(
861
1006
  "button",
862
1007
  {
@@ -975,6 +1120,9 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
975
1120
  type,
976
1121
  value: typeof value === "string" || typeof value === "number" ? String(value) : "",
977
1122
  placeholder: field.placeholder,
1123
+ maxLength: "maxLength" in field ? field.maxLength : void 0,
1124
+ min: field.kind === "number" ? field.min : void 0,
1125
+ max: field.kind === "number" ? field.max : void 0,
978
1126
  onChange: (e) => setValue(e.target.value),
979
1127
  ...fieldAria(field, error, ids)
980
1128
  }
@@ -991,6 +1139,7 @@ function LongText({ field, value, error, setValue, ids: providedIds }) {
991
1139
  rows: 4,
992
1140
  value: typeof value === "string" ? value : "",
993
1141
  placeholder: field.placeholder,
1142
+ maxLength: "maxLength" in field ? field.maxLength : void 0,
994
1143
  onChange: (e) => setValue(e.target.value),
995
1144
  ...fieldAria(field, error, ids)
996
1145
  }
@@ -1246,12 +1395,18 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1246
1395
  }
1247
1396
  ) });
1248
1397
  }
1249
- var OTHER_SENTINEL = "__fw_other__";
1398
+ function otherSentinel(field) {
1399
+ let value = "__fillo_other__";
1400
+ const ids = new Set(field.options.map((option) => option.id));
1401
+ while (ids.has(value)) value += "_";
1402
+ return value;
1403
+ }
1250
1404
  function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1251
1405
  const ids = useFilloFieldIds(field.id, providedIds);
1252
1406
  const strings = useStrings();
1253
1407
  const choice = field;
1254
1408
  const options = useDisplayOptions(choice);
1409
+ const otherValue = otherSentinel(choice);
1255
1410
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
1256
1411
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1257
1412
  const otherActive = isOtherValue || value === "" && otherOn;
@@ -1264,9 +1419,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1264
1419
  id: ids.inputId,
1265
1420
  ...control,
1266
1421
  ...fieldAria(field, error, ids),
1267
- value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
1422
+ value: otherActive ? otherValue : typeof value === "string" ? value : "",
1268
1423
  onChange: (e) => {
1269
- if (e.target.value === OTHER_SENTINEL) {
1424
+ if (e.target.value === otherValue) {
1270
1425
  setOtherOn(true);
1271
1426
  setValue("");
1272
1427
  } else {
@@ -1277,7 +1432,7 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1277
1432
  children: [
1278
1433
  /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1279
1434
  options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
1280
- choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: `${strings.other}\u2026` })
1435
+ choice.allowOther && /* @__PURE__ */ jsx4("option", { value: otherValue, children: `${strings.other}\u2026` })
1281
1436
  ]
1282
1437
  }
1283
1438
  ),
@@ -1301,17 +1456,15 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1301
1456
  const ids = useFilloFieldIds(field.id, providedIds);
1302
1457
  const strings = useStrings();
1303
1458
  const appearance = useFilloAppearance();
1459
+ const slots = useFieldSlots(field, error);
1304
1460
  const checkbox = field;
1305
1461
  if (checkbox.appearance === "toggle") {
1306
1462
  return /* @__PURE__ */ jsxs4(
1307
1463
  "div",
1308
1464
  {
1309
- className: `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`,
1310
- "data-fillo": "field",
1311
- "data-field": field.id,
1312
- "data-kind": field.kind,
1313
- "data-invalid": error ? "" : void 0,
1314
- "data-required": field.required ? "" : void 0,
1465
+ ...slots.wrapperProps(
1466
+ `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`
1467
+ ),
1315
1468
  children: [
1316
1469
  /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1317
1470
  /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
@@ -1333,8 +1486,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1333
1486
  ),
1334
1487
  /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
1335
1488
  ] }),
1336
- field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
1337
- error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
1489
+ field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1490
+ error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1338
1491
  ]
1339
1492
  }
1340
1493
  );
@@ -1342,12 +1495,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1342
1495
  return /* @__PURE__ */ jsxs4(
1343
1496
  "div",
1344
1497
  {
1345
- className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
1346
- "data-fillo": "field",
1347
- "data-field": field.id,
1348
- "data-kind": field.kind,
1349
- "data-invalid": error ? "" : void 0,
1350
- "data-required": field.required ? "" : void 0,
1498
+ ...slots.wrapperProps(
1499
+ `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`
1500
+ ),
1351
1501
  children: [
1352
1502
  /* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1353
1503
  /* @__PURE__ */ jsx4(
@@ -1366,8 +1516,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1366
1516
  !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1367
1517
  ] })
1368
1518
  ] }),
1369
- field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
1370
- error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
1519
+ field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1520
+ error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
1371
1521
  ]
1372
1522
  }
1373
1523
  );
@@ -1513,73 +1663,106 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
1513
1663
  [next[index], next[target]] = [next[target], next[index]];
1514
1664
  setValue(next);
1515
1665
  }
1516
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1517
- const option = ranking.options.find((o) => o.id === id);
1518
- if (!option) return null;
1519
- return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
1520
- /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
1521
- /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
1522
- /* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
1523
- /* @__PURE__ */ jsx4(
1524
- "button",
1525
- {
1526
- type: "button",
1527
- className: "fillo-ranking-move",
1528
- "aria-label": `Move ${option.label} up`,
1529
- disabled: index === 0,
1530
- onClick: () => move(id, -1),
1531
- children: "\u2191"
1532
- }
1533
- ),
1534
- /* @__PURE__ */ jsx4(
1535
- "button",
1536
- {
1537
- type: "button",
1538
- className: "fillo-ranking-move",
1539
- "aria-label": `Move ${option.label} down`,
1540
- disabled: index === order.length - 1,
1541
- onClick: () => move(id, 1),
1542
- children: "\u2193"
1543
- }
1544
- )
1545
- ] })
1546
- ] }, id);
1547
- }) }) });
1666
+ return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1667
+ "div",
1668
+ {
1669
+ id: ids.inputId,
1670
+ role: "group",
1671
+ "aria-labelledby": ids.labelId,
1672
+ ...fieldAria(field, error, ids),
1673
+ children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1674
+ const option = ranking.options.find((o) => o.id === id);
1675
+ if (!option) return null;
1676
+ return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
1677
+ /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
1678
+ /* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
1679
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
1680
+ /* @__PURE__ */ jsx4(
1681
+ "button",
1682
+ {
1683
+ type: "button",
1684
+ className: "fillo-ranking-move",
1685
+ "aria-label": `Move ${option.label} up`,
1686
+ disabled: index === 0,
1687
+ onClick: () => move(id, -1),
1688
+ children: "\u2191"
1689
+ }
1690
+ ),
1691
+ /* @__PURE__ */ jsx4(
1692
+ "button",
1693
+ {
1694
+ type: "button",
1695
+ className: "fillo-ranking-move",
1696
+ "aria-label": `Move ${option.label} down`,
1697
+ disabled: index === order.length - 1,
1698
+ onClick: () => move(id, 1),
1699
+ children: "\u2193"
1700
+ }
1701
+ )
1702
+ ] })
1703
+ ] }, id);
1704
+ }) })
1705
+ }
1706
+ ) });
1548
1707
  }
1549
1708
  function Matrix({ field, value, error, setValue, ids: providedIds }) {
1550
1709
  const ids = useFilloFieldIds(field.id, providedIds);
1551
1710
  const matrix = field;
1552
1711
  const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
1553
- return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4("div", { className: "fillo-matrix-wrap", children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
1554
- /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
1555
- /* @__PURE__ */ jsx4("th", {}),
1556
- matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
1557
- ] }) }),
1558
- /* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
1559
- const rowLabelId = `${ids.inputId}-${row.id}`;
1560
- const answered = answers[row.id] !== void 0;
1561
- const rowInvalid = Boolean(error) && field.required && !answered;
1562
- return (
1563
- // Each row is its own radiogroup, labelled by the row question.
1564
- /* @__PURE__ */ jsxs4("tr", { role: "radiogroup", "aria-labelledby": rowLabelId, children: [
1565
- /* @__PURE__ */ jsx4("th", { scope: "row", id: rowLabelId, children: row.label }),
1566
- matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
1567
- "input",
1568
- {
1569
- type: "radio",
1570
- className: "fillo-option-input",
1571
- name: `${ids.name}-${row.id}`,
1572
- "aria-label": `${row.label}: ${col.label}`,
1573
- "aria-required": field.required ? true : void 0,
1574
- "aria-invalid": rowInvalid ? true : void 0,
1575
- checked: answers[row.id] === col.id,
1576
- onChange: () => setValue({ ...answers, [row.id]: col.id })
1577
- }
1578
- ) }, col.id))
1579
- ] }, row.id)
1580
- );
1581
- }) })
1582
- ] }) }) });
1712
+ return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1713
+ "div",
1714
+ {
1715
+ id: ids.inputId,
1716
+ className: "fillo-matrix-wrap",
1717
+ role: "group",
1718
+ "aria-labelledby": ids.labelId,
1719
+ ...fieldAria(field, error, ids),
1720
+ children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
1721
+ /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
1722
+ /* @__PURE__ */ jsx4("th", {}),
1723
+ matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
1724
+ ] }) }),
1725
+ /* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
1726
+ const rowLabelId = `${ids.inputId}-${row.id}`;
1727
+ const answered = answers[row.id] !== void 0;
1728
+ const rowInvalid = Boolean(error) && field.required && !answered;
1729
+ const radioId = (col) => `${ids.inputId}-${row.id}-${col.id}`;
1730
+ return (
1731
+ // The <tr> keeps its native row role so the grid's row/column
1732
+ // header association survives for screen readers. The row's
1733
+ // radiogroup lives on an inner element that aria-owns the radios
1734
+ // scattered across the cells (they can't be DOM-nested inside a
1735
+ // single element within a table row).
1736
+ /* @__PURE__ */ jsxs4("tr", { children: [
1737
+ /* @__PURE__ */ jsx4("th", { scope: "row", children: /* @__PURE__ */ jsx4(
1738
+ "div",
1739
+ {
1740
+ role: "radiogroup",
1741
+ "aria-labelledby": rowLabelId,
1742
+ "aria-owns": matrix.columns.map(radioId).join(" "),
1743
+ children: /* @__PURE__ */ jsx4("span", { id: rowLabelId, children: row.label })
1744
+ }
1745
+ ) }),
1746
+ matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
1747
+ "input",
1748
+ {
1749
+ id: radioId(col),
1750
+ type: "radio",
1751
+ className: "fillo-option-input",
1752
+ name: `${ids.name}-${row.id}`,
1753
+ "aria-label": `${row.label}: ${col.label}`,
1754
+ "aria-required": field.required ? true : void 0,
1755
+ "aria-invalid": rowInvalid ? true : void 0,
1756
+ checked: answers[row.id] === col.id,
1757
+ onChange: () => setValue({ ...answers, [row.id]: col.id })
1758
+ }
1759
+ ) }, col.id))
1760
+ ] }, row.id)
1761
+ );
1762
+ }) })
1763
+ ] })
1764
+ }
1765
+ ) });
1583
1766
  }
1584
1767
  function Hidden() {
1585
1768
  return null;
@@ -1633,8 +1816,8 @@ function blockPropsEqual(prev, next) {
1633
1816
  const id = next.block.id;
1634
1817
  if (prev.api.data[id] !== next.api.data[id]) return false;
1635
1818
  if (prev.api.errors[id] !== next.api.errors[id]) return false;
1636
- if (next.block.kind === "custom") {
1637
- return prev.api.data === next.api.data && prev.api.status === next.api.status && prev.api.uploading === next.api.uploading && prev.api.isFirstPage === next.api.isFirstPage && prev.api.isLastPage === next.api.isLastPage && prev.api.pageIndex === next.api.pageIndex;
1819
+ if (next.block.kind === "custom" || isField(next.block) && Boolean(next.components?.[next.block.kind])) {
1820
+ return prev.api === next.api;
1638
1821
  }
1639
1822
  if ((blockHasPiping(prev.block) || blockHasPiping(next.block)) && prev.api.data !== next.api.data) {
1640
1823
  return false;
@@ -1649,6 +1832,7 @@ function blockPropsEqual(prev, next) {
1649
1832
  function BlockRendererInner({ block, api, components, customComponents }) {
1650
1833
  block = pipeBlock(block, api.data, api.form);
1651
1834
  const ids = useFilloFieldIds(block.id);
1835
+ const strings = useStrings();
1652
1836
  if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
1653
1837
  let Component;
1654
1838
  if (block.kind === "custom") {
@@ -1665,12 +1849,14 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1665
1849
  } else {
1666
1850
  Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
1667
1851
  }
1852
+ const rawError = api.errors[block.id];
1853
+ const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
1668
1854
  return /* @__PURE__ */ jsx4(
1669
1855
  Component,
1670
1856
  {
1671
1857
  field: block,
1672
1858
  value: api.data[block.id],
1673
- error: api.errors[block.id],
1859
+ error,
1674
1860
  setValue: (v) => {
1675
1861
  api.setValue(block.id, v);
1676
1862
  const ctx = {
@@ -1749,7 +1935,9 @@ function badgeUrl(href) {
1749
1935
  function focusFirstInvalid(formEl) {
1750
1936
  if (!formEl || typeof window === "undefined") return;
1751
1937
  const run = () => {
1752
- const target = formEl.querySelector('[aria-invalid="true"]');
1938
+ const invalid = formEl.querySelector('[aria-invalid="true"]');
1939
+ const focusable = 'input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
1940
+ const target = invalid?.matches(focusable) ? invalid : invalid?.querySelector(focusable);
1753
1941
  target?.focus?.();
1754
1942
  };
1755
1943
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
@@ -1777,35 +1965,54 @@ function FilloForm(props) {
1777
1965
  () => props.client ?? (formId ? createClient() : void 0),
1778
1966
  [props.client, formId]
1779
1967
  );
1780
- const [fetched, setFetched] = useState6(null);
1781
- const [fetchedTheme, setFetchedTheme] = useState6(null);
1782
- const [loadError, setLoadError] = useState6(null);
1783
- const [closed, setClosed] = useState6(false);
1784
- const [fetchedBranding, setFetchedBranding] = useState6(null);
1968
+ const [loaded, setLoaded] = useState6(null);
1969
+ const [loadFailure, setLoadFailure] = useState6(null);
1785
1970
  const codeForm = isCodeForm2(form) ? form : null;
1786
- const inlineSchema = codeForm ? codeForm.schema : form;
1787
1971
  const onError = props.onError;
1788
- const { formId: syncedFormId, status: syncedStatus } = useCodeFormSync(form, client, onError);
1789
- const onErrorRef = useRef5(onError);
1972
+ const {
1973
+ formId: syncedFormId,
1974
+ status: syncedStatus,
1975
+ branding: syncedBranding,
1976
+ error: syncError,
1977
+ noticeError: syncNoticeError,
1978
+ syncing: codeFormSyncing,
1979
+ staged: codeChangesStaged,
1980
+ resolvedSchema,
1981
+ resolvedTheme
1982
+ } = useCodeFormSync(form, client, onError, {
1983
+ // An absent client is intentional render-only use. Builder previews and an
1984
+ // explicit canonical id also do not need publishable-key resolution.
1985
+ allowUnsynced: Boolean(!client || props.skipValidation || formId)
1986
+ });
1987
+ const renderResolvedSnapshot = Boolean(resolvedSchema && !isDevEnv());
1988
+ const inlineSchema = codeForm ? renderResolvedSnapshot ? resolvedSchema : codeForm.schema : form;
1989
+ const onErrorRef = useRef6(onError);
1790
1990
  useEffect7(() => {
1791
1991
  onErrorRef.current = onError;
1792
1992
  });
1793
1993
  const needsFetch = !inlineSchema && Boolean(client && formId);
1794
1994
  useEffect7(() => {
1795
- if (!needsFetch || !client || !formId) return;
1995
+ if (!needsFetch || !client || !formId) {
1996
+ setLoaded(null);
1997
+ setLoadFailure(null);
1998
+ return;
1999
+ }
1796
2000
  let cancelled = false;
1797
- setFetched(null);
1798
- setLoadError(null);
2001
+ setLoaded(null);
2002
+ setLoadFailure(null);
1799
2003
  client.getForm(formId).then((published) => {
1800
2004
  if (cancelled) return;
1801
- setFetched(published.schema);
1802
- setFetchedTheme(published.theme);
1803
- setClosed(Boolean(published.closed));
1804
- setFetchedBranding(published.branding ?? null);
2005
+ setLoaded({
2006
+ formId,
2007
+ schema: published.schema,
2008
+ theme: published.theme,
2009
+ closed: Boolean(published.closed),
2010
+ branding: published.branding ?? null
2011
+ });
1805
2012
  }).catch((err) => {
1806
2013
  if (cancelled) return;
1807
2014
  const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
1808
- setLoadError(error);
2015
+ setLoadFailure({ formId, error });
1809
2016
  onErrorRef.current?.(error);
1810
2017
  });
1811
2018
  return () => {
@@ -1813,10 +2020,13 @@ function FilloForm(props) {
1813
2020
  };
1814
2021
  }, [needsFetch, client, formId]);
1815
2022
  const strings = resolveStrings(props.strings);
1816
- const rawSchema = inlineSchema ?? fetched;
2023
+ const currentLoaded = needsFetch && loaded?.formId === formId ? loaded : null;
2024
+ const loadError = needsFetch && loadFailure && loadFailure.formId === formId ? loadFailure.error : null;
2025
+ const rawSchema = inlineSchema ?? currentLoaded?.schema ?? null;
1817
2026
  const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
1818
2027
  const schema = normalized?.ok ? normalized.schema : null;
1819
- const theme = props.appearance?.theme ?? props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
2028
+ const resolvedCodeTheme = renderResolvedSnapshot ? resolvedTheme ?? void 0 : codeForm?.theme;
2029
+ const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
1820
2030
  if (normalized && !normalized.ok) {
1821
2031
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
1822
2032
  if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
@@ -1829,16 +2039,34 @@ function FilloForm(props) {
1829
2039
  const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
1830
2040
  return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
1831
2041
  }
1832
- if (closed) {
2042
+ if (currentLoaded?.closed) {
1833
2043
  return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.closed }) });
1834
2044
  }
2045
+ const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
2046
+ if (canonicalSyncError && props.renderError) {
2047
+ return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(canonicalSyncError) });
2048
+ }
2049
+ if (canonicalSyncError && !isDevEnv()) {
2050
+ return /* @__PURE__ */ jsx5(
2051
+ "div",
2052
+ {
2053
+ className: `fillo-form fillo-form--error ${props.className ?? ""}`,
2054
+ role: "alert",
2055
+ "data-fillo": "root",
2056
+ "data-state": "error",
2057
+ ...themeProps(theme),
2058
+ children: strings.loadFailed
2059
+ }
2060
+ );
2061
+ }
1835
2062
  const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
1836
2063
  if (notPublished && !isDevEnv()) {
1837
2064
  const error = new FilloError2("This form isn't published yet.", 403);
1838
2065
  if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
1839
2066
  return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.notLive }) });
1840
2067
  }
1841
- if (!schema) {
2068
+ const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
2069
+ if (!isDevEnv() && awaitingCanonical || !schema) {
1842
2070
  return /* @__PURE__ */ jsxs5(
1843
2071
  "div",
1844
2072
  {
@@ -1853,7 +2081,8 @@ function FilloForm(props) {
1853
2081
  }
1854
2082
  );
1855
2083
  }
1856
- const poweredBy = fetchedBranding?.poweredBy ?? true;
2084
+ const branding = codeForm ? syncedBranding : currentLoaded?.branding;
2085
+ const poweredBy = branding?.poweredBy ?? true;
1857
2086
  return /* @__PURE__ */ jsx5(
1858
2087
  ResolvedForm,
1859
2088
  {
@@ -1863,9 +2092,11 @@ function FilloForm(props) {
1863
2092
  theme,
1864
2093
  formId: syncedFormId ?? formId,
1865
2094
  poweredBy,
1866
- brandLabel: fetchedBranding?.label,
1867
- brandHref: fetchedBranding?.href,
1868
- draftNotice: notPublished
2095
+ brandLabel: branding?.label,
2096
+ brandHref: branding?.href,
2097
+ draftNotice: notPublished,
2098
+ stagedNotice: codeChangesStaged,
2099
+ syncErrorNotice: syncError ?? syncNoticeError ?? void 0
1869
2100
  }
1870
2101
  );
1871
2102
  }
@@ -1889,18 +2120,85 @@ function DevDraftNotice() {
1889
2120
  " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
1890
2121
  ] });
1891
2122
  }
2123
+ function DevStagedNotice() {
2124
+ return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2125
+ /* @__PURE__ */ jsx5("strong", { children: "Code changes are staged." }),
2126
+ " This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
2127
+ ] });
2128
+ }
2129
+ function DevSyncErrorNotice({ error }) {
2130
+ return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
2131
+ /* @__PURE__ */ jsxs5("strong", { children: [
2132
+ "Form sync needs attention",
2133
+ error.code ? ` (${error.code})` : "",
2134
+ "."
2135
+ ] }),
2136
+ " ",
2137
+ error.message,
2138
+ " The safe form remains available for development."
2139
+ ] });
2140
+ }
1892
2141
  function ResolvedForm(props) {
1893
2142
  const { schema, theme } = props;
1894
2143
  const instanceId = useId2().replace(/:/g, "");
1895
- const hpRef = useRef5("");
2144
+ const hpRef = useRef6(null);
1896
2145
  const codeForm = isCodeForm2(props.form) ? props.form : null;
1897
2146
  const client = props.client;
1898
2147
  const resolveFormId = useMemo5(
1899
2148
  () => codeForm && client?.key ? (
1900
2149
  // bypassCache: a stale cached formId must not be trusted at submit time.
1901
- () => syncCodeForm2(client, codeForm, { bypassCache: true }).then((r) => r.formId)
2150
+ async () => {
2151
+ try {
2152
+ const result = await syncCodeForm2(client, codeForm, { bypassCache: true });
2153
+ if (result.status === "draft") {
2154
+ throw new FilloError2(
2155
+ "This form is no longer published. Submission was stopped before sending answers.",
2156
+ 403,
2157
+ void 0,
2158
+ "form_not_published"
2159
+ );
2160
+ }
2161
+ const authoritative = normalizeFormSchema(result.resolvedSchema ?? codeForm.schema);
2162
+ if (!authoritative.ok) {
2163
+ throw new FilloError2(
2164
+ `The canonical form schema was invalid: ${authoritative.error}`,
2165
+ 502,
2166
+ void 0,
2167
+ "invalid_sync_schema"
2168
+ );
2169
+ }
2170
+ if (!formSchemasEqual(schema, authoritative.schema)) {
2171
+ throw new FilloError2(
2172
+ "The live form changed after this page loaded. Submission stopped before incompatible answers were sent. Reload to use the published schema.",
2173
+ 409,
2174
+ void 0,
2175
+ "form_schema_changed"
2176
+ );
2177
+ }
2178
+ if (result.syncError) {
2179
+ const notice = new FilloError2(
2180
+ result.syncError.message,
2181
+ 403,
2182
+ void 0,
2183
+ result.syncError.code
2184
+ );
2185
+ console.warn(
2186
+ `[fillo] form sync needs attention (${notice.code}): ${notice.message}`
2187
+ );
2188
+ props.onError?.(notice);
2189
+ }
2190
+ return result.formId;
2191
+ } catch (error) {
2192
+ const failure = isFilloError2(error) ? error : new FilloError2(error instanceof Error ? error.message : String(error), 0);
2193
+ console.warn(
2194
+ `[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
2195
+ );
2196
+ props.onError?.(failure);
2197
+ throw failure;
2198
+ }
2199
+ }
1902
2200
  ) : void 0,
1903
- [codeForm, client]
2201
+ [codeForm, client, props.onError, schema]
1904
2202
  );
1905
2203
  const api = useFilloController({
1906
2204
  form: schema,
@@ -1910,7 +2208,7 @@ function ResolvedForm(props) {
1910
2208
  onChange: props.onChange,
1911
2209
  onSubmitted: props.onSubmitted,
1912
2210
  skipValidation: props.skipValidation,
1913
- getHoneypot: () => hpRef.current,
2211
+ getHoneypot: () => hpRef.current?.value ?? "",
1914
2212
  // Framed renderer: Fillo draws the layout (and badge), so the surface is
1915
2213
  // "default", never the bare-engine "headless".
1916
2214
  surface: "default",
@@ -1927,12 +2225,12 @@ function ResolvedForm(props) {
1927
2225
  const url = safeHttpUrl(settings.redirectUrl);
1928
2226
  if (url) window.location.assign(url);
1929
2227
  }, [liveSubmit, settings.redirectUrl]);
1930
- const successRef = useRef5(null);
2228
+ const successRef = useRef6(null);
1931
2229
  useEffect7(() => {
1932
2230
  if (liveSubmit) successRef.current?.focus();
1933
2231
  }, [liveSubmit]);
1934
- const blocksRef = useRef5(null);
1935
- const prevPageIndex = useRef5(api.pageIndex);
2232
+ const blocksRef = useRef6(null);
2233
+ const prevPageIndex = useRef6(api.pageIndex);
1936
2234
  useEffect7(() => {
1937
2235
  if (prevPageIndex.current === api.pageIndex) return;
1938
2236
  prevPageIndex.current = api.pageIndex;
@@ -1965,7 +2263,7 @@ function ResolvedForm(props) {
1965
2263
  children: [
1966
2264
  /* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
1967
2265
  /* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
1968
- /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? strings.successMessage })
2266
+ /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: api.duplicateSubmission ? strings.alreadyAnswered : settings.successMessage ?? strings.successMessage })
1969
2267
  ]
1970
2268
  }
1971
2269
  ),
@@ -2005,6 +2303,8 @@ function ResolvedForm(props) {
2005
2303
  children: [
2006
2304
  !props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
2007
2305
  props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
2306
+ props.stagedNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevStagedNotice, {}),
2307
+ props.syncErrorNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevSyncErrorNotice, { error: props.syncErrorNotice }),
2008
2308
  (api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs5(
2009
2309
  "div",
2010
2310
  {
@@ -2017,13 +2317,23 @@ function ResolvedForm(props) {
2017
2317
  ]
2018
2318
  }
2019
2319
  ),
2320
+ api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx5(
2321
+ "div",
2322
+ {
2323
+ className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
2324
+ "data-fillo": "resume",
2325
+ role: "status",
2326
+ children: /* @__PURE__ */ jsx5("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
2327
+ }
2328
+ ),
2020
2329
  multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
2021
2330
  "div",
2022
2331
  {
2023
2332
  className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
2024
2333
  "data-fillo": "progress",
2025
2334
  role: "progressbar",
2026
- "aria-valuemin": 0,
2335
+ "aria-label": schema.title || "Form progress",
2336
+ "aria-valuemin": 1,
2027
2337
  "aria-valuemax": api.pageCount,
2028
2338
  "aria-valuenow": api.pageIndex + 1,
2029
2339
  children: /* @__PURE__ */ jsx5(
@@ -2069,9 +2379,9 @@ function ResolvedForm(props) {
2069
2379
  "aria-hidden": "true",
2070
2380
  defaultValue: "",
2071
2381
  style: HP_STYLE,
2072
- ref: assertHpHidden,
2073
- onChange: (e) => {
2074
- hpRef.current = e.target.value;
2382
+ ref: (element) => {
2383
+ hpRef.current = element;
2384
+ assertHpHidden(element);
2075
2385
  }
2076
2386
  }
2077
2387
  ),
@@ -2105,7 +2415,7 @@ function ResolvedForm(props) {
2105
2415
  }
2106
2416
 
2107
2417
  // src/jsx.tsx
2108
- import { useRef as useRef6 } from "react";
2418
+ import { useRef as useRef7 } from "react";
2109
2419
  import {
2110
2420
  codeFormFromJsx,
2111
2421
  FilloJsxError,
@@ -2118,7 +2428,7 @@ function JsxForm(props) {
2118
2428
  const { id, title, description, settings, theme, client, children, ...rest } = props;
2119
2429
  const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
2120
2430
  const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
2121
- const memo2 = useRef6(null);
2431
+ const memo2 = useRef7(null);
2122
2432
  if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
2123
2433
  console.warn(
2124
2434
  `[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.`
@@ -2172,30 +2482,121 @@ var Fillo = {
2172
2482
 
2173
2483
  // src/provider.tsx
2174
2484
  import { useId as useId3, useMemo as useMemo6 } from "react";
2175
- import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2, resolveStrings as resolveStrings2 } from "@usefillo/core";
2176
- import { jsx as jsx7 } from "react/jsx-runtime";
2177
- function FilloProvider({ children, form, formId, client, appearance, strings, ...options }) {
2485
+ import {
2486
+ FilloError as FilloError3,
2487
+ formSchemasEqual as formSchemasEqual2,
2488
+ isFilloError as isFilloError3,
2489
+ normalizeFormSchema as normalizeFormSchema2,
2490
+ resolveStrings as resolveStrings2,
2491
+ syncCodeForm as syncCodeForm3
2492
+ } from "@usefillo/core";
2493
+ import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
2494
+ function FilloProvider({
2495
+ children,
2496
+ form,
2497
+ formId,
2498
+ client,
2499
+ appearance,
2500
+ strings,
2501
+ onError,
2502
+ renderError,
2503
+ ...options
2504
+ }) {
2178
2505
  const instanceId = useId3().replace(/:/g, "");
2179
2506
  const codeForm = isCodeForm2(form) ? form : null;
2180
- const rawSchema = codeForm ? codeForm.schema : form;
2181
- const schemaKey = useMemo6(() => JSON.stringify(rawSchema), [rawSchema]);
2182
- const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
2507
+ const {
2508
+ formId: syncedFormId,
2509
+ status: syncedStatus,
2510
+ error: syncError,
2511
+ syncing: codeFormSyncing,
2512
+ resolvedSchema
2513
+ } = useCodeFormSync(form, client, onError, {
2514
+ allowUnsynced: Boolean(!client || formId || options.skipValidation)
2515
+ });
2516
+ const rawSchema = codeForm ? resolvedSchema && !isDevEnv3() ? resolvedSchema : codeForm.schema : form;
2517
+ const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema]);
2183
2518
  if (!normalized.ok) {
2184
2519
  throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
2185
2520
  }
2186
2521
  const schema = normalized.schema;
2187
- const { formId: syncedFormId } = useCodeFormSync(form, client);
2522
+ const resolveFormId = useMemo6(
2523
+ () => codeForm && client?.key ? async () => {
2524
+ try {
2525
+ const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
2526
+ if (result.status === "draft") {
2527
+ throw new FilloError3(
2528
+ "This form is no longer published. Submission was stopped before sending answers.",
2529
+ 403,
2530
+ void 0,
2531
+ "form_not_published"
2532
+ );
2533
+ }
2534
+ const authoritative = normalizeFormSchema2(result.resolvedSchema ?? codeForm.schema);
2535
+ if (!authoritative.ok) {
2536
+ throw new FilloError3(
2537
+ `The canonical form schema was invalid: ${authoritative.error}`,
2538
+ 502,
2539
+ void 0,
2540
+ "invalid_sync_schema"
2541
+ );
2542
+ }
2543
+ if (!formSchemasEqual2(schema, authoritative.schema)) {
2544
+ throw new FilloError3(
2545
+ "The live form changed after this page loaded. Submission stopped before incompatible answers were sent. Reload to use the published schema.",
2546
+ 409,
2547
+ void 0,
2548
+ "form_schema_changed"
2549
+ );
2550
+ }
2551
+ if (result.syncError) {
2552
+ const notice = new FilloError3(
2553
+ result.syncError.message,
2554
+ 403,
2555
+ void 0,
2556
+ result.syncError.code
2557
+ );
2558
+ console.warn(
2559
+ `[fillo] form sync needs attention (${notice.code}): ${notice.message}`
2560
+ );
2561
+ onError?.(notice);
2562
+ }
2563
+ return result.formId;
2564
+ } catch (error) {
2565
+ const failure = isFilloError3(error) ? error : new FilloError3(error instanceof Error ? error.message : String(error), 0);
2566
+ console.warn(
2567
+ `[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
2568
+ );
2569
+ onError?.(failure);
2570
+ throw failure;
2571
+ }
2572
+ } : void 0,
2573
+ [client, codeForm, onError, schema]
2574
+ );
2188
2575
  const api = useFilloController({
2189
2576
  ...options,
2190
2577
  client,
2191
2578
  form: schema,
2192
2579
  formId: syncedFormId ?? formId,
2580
+ resolveFormId,
2193
2581
  // Headless = your markup, no Fillo-rendered layout. Free like every embed
2194
2582
  // method; recorded per response so usage stays measurable.
2195
2583
  surface: "headless"
2196
2584
  });
2197
2585
  useDraftFlush(api);
2198
- return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolveStrings2(strings), children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
2586
+ const resolvedStrings = resolveStrings2(strings);
2587
+ const canonicalSyncError = syncError && !options.skipValidation ? syncError : null;
2588
+ if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: renderError(canonicalSyncError) });
2589
+ if (canonicalSyncError && !isDevEnv3()) return null;
2590
+ if (codeFormSyncing && !options.skipValidation && !isDevEnv3()) return null;
2591
+ const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !options.skipValidation;
2592
+ if (notPublished && !isDevEnv3()) {
2593
+ const error = new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published");
2594
+ return renderError ? /* @__PURE__ */ jsx7(Fragment3, { children: renderError(error) }) : null;
2595
+ }
2596
+ return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolvedStrings, children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
2597
+ }
2598
+ function isDevEnv3() {
2599
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2199
2600
  }
2200
2601
 
2201
2602
  // src/index.ts