@usefillo/react 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,23 +1,66 @@
1
1
  "use client";
2
2
 
3
3
  // src/FilloForm.tsx
4
- import { useEffect as useEffect7, useId as useId2, useMemo as useMemo4, useRef as useRef5, useState as useState6 } from "react";
4
+ import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
5
5
  import {
6
6
  FilloError as FilloError2,
7
7
  createClient,
8
- isField as isField2,
8
+ needsExplicitSubmit,
9
9
  normalizeFormSchema,
10
10
  normalizeFormTheme,
11
11
  visibleFields as visibleFields2
12
12
  } from "@usefillo/core";
13
+ import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
14
+
15
+ // src/appearance.tsx
16
+ import { createContext, useContext } from "react";
17
+ import {
18
+ DEFAULT_STRINGS,
19
+ slotClass
20
+ } from "@usefillo/core";
21
+ var FilloAppearanceContext = createContext(void 0);
22
+ function useSlotClass(base, state) {
23
+ const appearance = useContext(FilloAppearanceContext);
24
+ return slotClass(base, appearance, state);
25
+ }
26
+ function useFilloAppearance() {
27
+ return useContext(FilloAppearanceContext);
28
+ }
29
+ function useFieldSlots(field, error) {
30
+ const appearance = useContext(FilloAppearanceContext);
31
+ const state = {
32
+ kind: field.kind,
33
+ fieldId: field.id,
34
+ invalid: Boolean(error),
35
+ required: Boolean(field.required)
36
+ };
37
+ return {
38
+ wrapperProps: (base) => ({
39
+ className: slotClass(base, appearance, { slot: "field", ...state }),
40
+ "data-fillo": "field",
41
+ "data-field": field.id,
42
+ "data-kind": field.kind,
43
+ "data-invalid": error ? "" : void 0,
44
+ "data-required": field.required ? "" : void 0
45
+ }),
46
+ label: slotClass("fillo-label", appearance, { slot: "label", ...state }),
47
+ description: slotClass("fillo-description", appearance, { slot: "fieldDescription", ...state }),
48
+ error: slotClass("fillo-error", appearance, { slot: "error", ...state }),
49
+ control: (base) => slotClass(base, appearance, { slot: "control", ...state })
50
+ };
51
+ }
52
+ var FilloStringsContext = createContext(DEFAULT_STRINGS);
53
+ function useStrings() {
54
+ return useContext(FilloStringsContext);
55
+ }
13
56
 
14
57
  // src/context.ts
15
58
  import { allFields } from "@usefillo/core";
16
- import { createContext, useContext, useId, useMemo } from "react";
17
- var FilloContext = createContext(null);
18
- var FilloInstanceIdContext = createContext(null);
59
+ import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
60
+ var FilloContext = createContext2(null);
61
+ var FilloInstanceIdContext = createContext2(null);
19
62
  function useFilloInstanceId() {
20
- const instanceId = useContext(FilloInstanceIdContext);
63
+ const instanceId = useContext2(FilloInstanceIdContext);
21
64
  const fallbackId = useId().replace(/:/g, "");
22
65
  return instanceId ?? fallbackId;
23
66
  }
@@ -32,7 +75,7 @@ function createFilloFieldIds(instanceId, fieldId) {
32
75
  };
33
76
  }
34
77
  function useFillo() {
35
- const api = useContext(FilloContext);
78
+ const api = useContext2(FilloContext);
36
79
  if (!api) throw new Error("useFillo must be used inside <FilloForm> or <FilloProvider>");
37
80
  return api;
38
81
  }
@@ -75,7 +118,9 @@ function useFilloController(options) {
75
118
  onSubmitted: (id, d) => optsRef.current.onSubmitted?.(id, d),
76
119
  getHoneypot: () => optsRef.current.getHoneypot?.() ?? "",
77
120
  skipValidation: options.skipValidation,
78
- surface: options.surface ?? "default"
121
+ surface: options.surface ?? "headless",
122
+ // Wrapped so the engine (created once) always calls the latest resolver.
123
+ resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0
79
124
  });
80
125
  }
81
126
  const store = storeRef.current;
@@ -109,16 +154,33 @@ import {
109
154
  } from "@usefillo/core";
110
155
  import { useEffect as useEffect2, useState } from "react";
111
156
  import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
157
+ var noticedSync = /* @__PURE__ */ new Set();
158
+ function noticeSync(handle, kind, message) {
159
+ const key = `${handle}:${kind}`;
160
+ if (noticedSync.has(key)) return;
161
+ noticedSync.add(key);
162
+ console.info(message);
163
+ }
112
164
  function useCodeFormSync(form, client, onError) {
113
165
  const codeForm = isCodeForm(form) ? form : null;
114
- const [syncedFormId, setSyncedFormId] = useState(null);
166
+ const [synced, setSynced] = useState({ formId: null, status: null });
115
167
  const hasKey = Boolean(client?.key);
116
168
  const handle = codeForm?.id;
117
169
  const contentKey = codeForm ? contentHash(JSON.stringify(codeForm.schema) + JSON.stringify(codeForm.theme ?? null)) : null;
118
170
  useEffect2(() => {
119
171
  if (!codeForm || !client?.key) return;
120
172
  let cancelled = false;
121
- syncCodeForm(client, codeForm).then((r) => !cancelled && setSyncedFormId(r.formId)).catch((err) => {
173
+ syncCodeForm(client, codeForm).then((r) => {
174
+ if (cancelled) return;
175
+ setSynced({ formId: r.formId, status: r.status ?? null });
176
+ const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
177
+ if (r.warning) console.warn(`[fillo] ${r.warning}`);
178
+ if (r.staged) {
179
+ noticeSync(codeForm.id, "staged", `[fillo] "${codeForm.id}": changes staged as a draft \u2014 publish them at ${dashboardUrl}`);
180
+ } else if (r.status === "draft") {
181
+ noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
182
+ }
183
+ }).catch((err) => {
122
184
  const error = err instanceof FilloError ? err : new FilloError(String(err), 0);
123
185
  console.warn("[fillo] form sync failed:", error.message);
124
186
  if (!cancelled) onError?.(error);
@@ -127,17 +189,18 @@ function useCodeFormSync(form, client, onError) {
127
189
  cancelled = true;
128
190
  };
129
191
  }, [hasKey, handle, contentKey, client, onError]);
130
- return syncedFormId;
192
+ return synced;
131
193
  }
132
194
 
133
195
  // src/fields.tsx
134
- import { isField, pipeBlock, validateResponse, visibleFields } from "@usefillo/core";
135
- import { memo, useEffect as useEffect6, useState as useState5 } from "react";
196
+ import { isField, pipeBlock, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
197
+ import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
136
198
 
137
199
  // src/phone.tsx
138
200
  import {
139
201
  PHONE_COUNTRIES,
140
202
  countryByIso,
203
+ countryByTimeZone,
141
204
  digitsOnly,
142
205
  flagEmoji,
143
206
  formatNational,
@@ -152,17 +215,60 @@ function localeCountry() {
152
215
  const region = tag.split("-")[1];
153
216
  return region && /^[A-Za-z]{2}$/.test(region) ? region.toUpperCase() : void 0;
154
217
  }
218
+ function browserTimeZone() {
219
+ if (typeof Intl === "undefined") return void 0;
220
+ try {
221
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
222
+ return typeof timeZone === "string" && timeZone ? timeZone : void 0;
223
+ } catch {
224
+ return void 0;
225
+ }
226
+ }
155
227
  function nationalFor(stored, country) {
156
228
  const digits = digitsOnly(stored);
157
229
  return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
158
230
  }
231
+ var PHONE_POPOVER_VIEWPORT_GAP = 8;
232
+ function positionPhonePopover(anchor, popover) {
233
+ if (!anchor || !popover || typeof window === "undefined") return "below";
234
+ const gap = PHONE_POPOVER_VIEWPORT_GAP;
235
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
236
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
237
+ const anchorRect = anchor.getBoundingClientRect();
238
+ const spaceBelow = Math.max(0, viewportHeight - anchorRect.bottom - gap);
239
+ const spaceAbove = Math.max(0, anchorRect.top - gap);
240
+ const placement = spaceBelow < popover.scrollHeight && spaceAbove > spaceBelow ? "above" : "below";
241
+ const availableHeight = placement === "above" ? spaceAbove : spaceBelow;
242
+ const maxHeight = Math.max(0, Math.min(availableHeight, viewportHeight - gap * 2));
243
+ const maxWidth = Math.max(0, viewportWidth - gap * 2);
244
+ popover.style.setProperty("--fillo-phone-popover-max-height", `${Math.floor(maxHeight)}px`);
245
+ popover.style.setProperty("--fillo-phone-popover-max-width", `${Math.floor(maxWidth)}px`);
246
+ popover.style.setProperty("--fillo-phone-popover-offset-x", "0px");
247
+ const width = Math.min(popover.offsetWidth, maxWidth);
248
+ let offsetX = 0;
249
+ const rightOverflow = anchorRect.left + width - (viewportWidth - gap);
250
+ if (rightOverflow > 0) offsetX -= rightOverflow;
251
+ const leftOverflow = gap - (anchorRect.left + offsetX);
252
+ if (leftOverflow > 0) offsetX += leftOverflow;
253
+ popover.style.setProperty("--fillo-phone-popover-offset-x", `${Math.round(offsetX)}px`);
254
+ return placement;
255
+ }
159
256
  function PhoneField({ field, value, error, setValue, ids: providedIds }) {
160
257
  const ids = useFilloFieldIds(field.id, providedIds);
258
+ const slots = useFieldSlots(field, error);
259
+ const strings = useStrings();
161
260
  const schema = field;
162
261
  const stored = typeof value === "string" ? value : "";
163
262
  const [country, setCountry] = useState2(
164
- () => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? countryByIso(localeCountry()) ?? PHONE_COUNTRIES[0]
263
+ () => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? PHONE_COUNTRIES[0]
165
264
  );
265
+ const touched = useRef2(false);
266
+ useEffect3(() => {
267
+ if (touched.current) return;
268
+ if (parsePhone(stored).country || countryByIso(schema.defaultCountry)) return;
269
+ const guess = countryByTimeZone(browserTimeZone()) ?? countryByIso(localeCountry());
270
+ if (guess) setCountry(guess);
271
+ }, []);
166
272
  const national = nationalFor(stored, country);
167
273
  const display = formatNational(country, national);
168
274
  const inputRef = useRef2(null);
@@ -185,6 +291,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
185
291
  });
186
292
  const commit = (c, nat) => setValue(nat ? toE164(c, nat) : "");
187
293
  function onInput(e) {
294
+ touched.current = true;
188
295
  const el = e.target;
189
296
  const raw = el.value;
190
297
  const caret = el.selectionStart ?? raw.length;
@@ -198,17 +305,18 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
198
305
  commit(country, digitsOnly(raw));
199
306
  }
200
307
  function pickCountry(c) {
308
+ touched.current = true;
201
309
  setCountry(c);
202
310
  commit(c, national);
203
311
  inputRef.current?.focus();
204
312
  }
205
313
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
206
- return /* @__PURE__ */ jsxs("div", { className: `fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`, "data-field": field.id, children: [
207
- /* @__PURE__ */ jsxs("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
314
+ return /* @__PURE__ */ jsxs("div", { ...slots.wrapperProps(`fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`), children: [
315
+ /* @__PURE__ */ jsxs("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
208
316
  field.label,
209
- !field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: " (optional)" })
317
+ !field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: strings.optional })
210
318
  ] }),
211
- field.description && /* @__PURE__ */ jsx("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
319
+ field.description && /* @__PURE__ */ jsx("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
212
320
  /* @__PURE__ */ jsxs("div", { className: "fillo-phone", children: [
213
321
  /* @__PURE__ */ jsx(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
214
322
  /* @__PURE__ */ jsx(
@@ -216,7 +324,8 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
216
324
  {
217
325
  ref: inputRef,
218
326
  id: ids.inputId,
219
- className: "fillo-input fillo-phone-input",
327
+ className: slots.control("fillo-input fillo-phone-input"),
328
+ "data-fillo": "control",
220
329
  type: "tel",
221
330
  inputMode: "tel",
222
331
  autoComplete: "tel-national",
@@ -229,7 +338,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
229
338
  }
230
339
  )
231
340
  ] }),
232
- error && /* @__PURE__ */ jsx("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
341
+ error && /* @__PURE__ */ jsx("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
233
342
  ] });
234
343
  }
235
344
  function CountrySelect({
@@ -240,7 +349,10 @@ function CountrySelect({
240
349
  const [open, setOpen] = useState2(false);
241
350
  const [query, setQuery] = useState2("");
242
351
  const [active, setActive] = useState2(0);
352
+ const [placement, setPlacement] = useState2("below");
243
353
  const rootRef = useRef2(null);
354
+ const triggerRef = useRef2(null);
355
+ const popoverRef = useRef2(null);
244
356
  const searchRef = useRef2(null);
245
357
  const listId = `${labelledBy}-countries`;
246
358
  const matches = useMemo3(() => {
@@ -266,6 +378,17 @@ function CountrySelect({
266
378
  document.addEventListener("mousedown", onDown);
267
379
  return () => document.removeEventListener("mousedown", onDown);
268
380
  }, [open]);
381
+ useLayoutEffect(() => {
382
+ if (!open) return;
383
+ const update = () => setPlacement(positionPhonePopover(rootRef.current, popoverRef.current));
384
+ update();
385
+ window.addEventListener("resize", update);
386
+ window.addEventListener("scroll", update, true);
387
+ return () => {
388
+ window.removeEventListener("resize", update);
389
+ window.removeEventListener("scroll", update, true);
390
+ };
391
+ }, [open, matches.length]);
269
392
  useEffect3(() => {
270
393
  if (!open) return;
271
394
  document.getElementById(`${listId}-opt-${active}`)?.scrollIntoView({ block: "nearest" });
@@ -288,12 +411,14 @@ function CountrySelect({
288
411
  } else if (e.key === "Escape") {
289
412
  e.preventDefault();
290
413
  setOpen(false);
414
+ triggerRef.current?.focus();
291
415
  }
292
416
  }
293
417
  return /* @__PURE__ */ jsxs("div", { className: "fillo-phone-country", ref: rootRef, children: [
294
418
  /* @__PURE__ */ jsxs(
295
419
  "button",
296
420
  {
421
+ ref: triggerRef,
297
422
  type: "button",
298
423
  className: "fillo-phone-flag",
299
424
  "aria-haspopup": "listbox",
@@ -310,7 +435,7 @@ function CountrySelect({
310
435
  ]
311
436
  }
312
437
  ),
313
- open && /* @__PURE__ */ jsxs("div", { className: "fillo-phone-popover", children: [
438
+ open && /* @__PURE__ */ jsxs("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
314
439
  /* @__PURE__ */ jsx(
315
440
  "input",
316
441
  {
@@ -366,6 +491,8 @@ import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } fro
366
491
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
367
492
  function SignatureField({ field, value, error, setValue, ids: providedIds }) {
368
493
  const ids = useFilloFieldIds(field.id, providedIds);
494
+ const slots = useFieldSlots(field, error);
495
+ const strings = useStrings();
369
496
  const typedInputId = `${ids.inputId}-typed`;
370
497
  const canvasRef = useRef3(null);
371
498
  const drawing = useRef3(false);
@@ -447,12 +574,12 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
447
574
  setValue(null);
448
575
  }
449
576
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
450
- return /* @__PURE__ */ jsxs2("div", { className: `fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`, children: [
451
- /* @__PURE__ */ jsxs2("span", { className: "fillo-label", id: ids.labelId, children: [
577
+ return /* @__PURE__ */ jsxs2("div", { ...slots.wrapperProps(`fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`), children: [
578
+ /* @__PURE__ */ jsxs2("span", { className: slots.label, "data-fillo": "label", id: ids.labelId, children: [
452
579
  field.label,
453
- !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: " (optional)" })
580
+ !field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
454
581
  ] }),
455
- field.description && /* @__PURE__ */ jsx2("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
582
+ field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
456
583
  /* @__PURE__ */ jsxs2(
457
584
  "div",
458
585
  {
@@ -460,7 +587,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
460
587
  "aria-labelledby": ids.labelId,
461
588
  "aria-describedby": describedBy,
462
589
  children: [
463
- /* @__PURE__ */ jsxs2("div", { className: "fillo-signature", children: [
590
+ /* @__PURE__ */ jsxs2("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
464
591
  /* @__PURE__ */ jsx2(
465
592
  "canvas",
466
593
  {
@@ -500,7 +627,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
500
627
  ]
501
628
  }
502
629
  ),
503
- error && /* @__PURE__ */ jsx2("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
630
+ error && /* @__PURE__ */ jsx2("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
504
631
  ] });
505
632
  }
506
633
 
@@ -530,6 +657,8 @@ function formatBytes(bytes) {
530
657
  }
531
658
  function FileUploadField({ field, value, error, setValue, api, ids: providedIds }) {
532
659
  const ids = useFilloFieldIds(field.id, providedIds);
660
+ const slots = useFieldSlots(field, error);
661
+ const strings = useStrings();
533
662
  const schema = field;
534
663
  const inputRef = useRef4(null);
535
664
  const [inFlight, setInFlight] = useState4([]);
@@ -602,16 +731,18 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
602
731
  setValue(files.filter((f) => f.fileId !== fileId));
603
732
  }
604
733
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
605
- return /* @__PURE__ */ jsxs3("div", { className: `fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`, children: [
606
- /* @__PURE__ */ jsxs3("label", { className: "fillo-label", htmlFor: ids.inputId, children: [
734
+ return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
735
+ /* @__PURE__ */ jsxs3("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
607
736
  field.label,
608
- !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: " (optional)" })
737
+ !field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
609
738
  ] }),
610
- field.description && /* @__PURE__ */ jsx3("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
739
+ field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
611
740
  remaining > 0 && /* @__PURE__ */ jsxs3(
612
741
  "div",
613
742
  {
614
- className: `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`,
743
+ "data-fillo": "control",
744
+ "data-drag-over": dragOver ? "" : void 0,
745
+ className: slots.control(`fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`),
615
746
  onDragOver: (e) => {
616
747
  e.preventDefault();
617
748
  setDragOver(true);
@@ -625,6 +756,10 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
625
756
  onClick: () => canUpload && inputRef.current?.click(),
626
757
  role: "button",
627
758
  tabIndex: 0,
759
+ "aria-labelledby": ids.labelId,
760
+ "aria-required": field.required ? true : void 0,
761
+ "aria-invalid": error ? true : void 0,
762
+ "aria-describedby": describedBy,
628
763
  onKeyDown: (e) => {
629
764
  if ((e.key === "Enter" || e.key === " ") && canUpload) inputRef.current?.click();
630
765
  },
@@ -636,9 +771,6 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
636
771
  id: ids.inputId,
637
772
  type: "file",
638
773
  hidden: true,
639
- "aria-invalid": error ? true : void 0,
640
- "aria-required": field.required ? true : void 0,
641
- "aria-describedby": describedBy,
642
774
  multiple: maxFiles > 1,
643
775
  accept: schema.accept?.join(","),
644
776
  onChange: (e) => handleFiles(e.target.files)
@@ -659,7 +791,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
659
791
  ]
660
792
  }
661
793
  ),
662
- /* @__PURE__ */ jsx3("div", { role: "status", "aria-live": "polite", className: "fillo-sr-only", children: uploadStatus(inFlight, files.length) }),
794
+ /* @__PURE__ */ jsx3(
795
+ "div",
796
+ {
797
+ role: "status",
798
+ "aria-live": "polite",
799
+ className: "fillo-sr-only",
800
+ style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
801
+ children: uploadStatus(inFlight, files.length)
802
+ }
803
+ ),
663
804
  (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
664
805
  files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
665
806
  /* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
@@ -723,7 +864,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
723
864
  f.key
724
865
  ))
725
866
  ] }),
726
- error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
867
+ error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
727
868
  ] });
728
869
  }
729
870
 
@@ -740,25 +881,58 @@ function fieldAria(field, error, ids) {
740
881
  "aria-describedby": describedBy.length ? describedBy.join(" ") : void 0
741
882
  };
742
883
  }
884
+ function useControl(base, field, error) {
885
+ return {
886
+ className: useSlotClass(base, {
887
+ slot: "control",
888
+ kind: field.kind,
889
+ fieldId: field.id,
890
+ invalid: Boolean(error),
891
+ required: Boolean(field.required)
892
+ }),
893
+ "data-fillo": "control"
894
+ };
895
+ }
896
+ function isNativeControlTarget(target) {
897
+ return target instanceof HTMLElement && Boolean(target.closest("input, textarea, select, button"));
898
+ }
743
899
  function FieldShell({
744
900
  field,
745
901
  error,
746
902
  ids,
747
903
  children
748
904
  }) {
905
+ const state = {
906
+ kind: field.kind,
907
+ fieldId: field.id,
908
+ invalid: Boolean(error),
909
+ required: Boolean(field.required)
910
+ };
911
+ const fieldCls = useSlotClass(
912
+ `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
913
+ { slot: "field", ...state }
914
+ );
915
+ const labelCls = useSlotClass("fillo-label", { slot: "label", ...state });
916
+ const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
917
+ const errorCls = useSlotClass("fillo-error", { slot: "error", ...state });
918
+ const strings = useStrings();
749
919
  return /* @__PURE__ */ jsxs4(
750
920
  "div",
751
921
  {
752
- className: `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
922
+ className: fieldCls,
923
+ "data-fillo": "field",
753
924
  "data-field": field.id,
925
+ "data-kind": field.kind,
926
+ "data-invalid": error ? "" : void 0,
927
+ "data-required": field.required ? "" : void 0,
754
928
  children: [
755
- /* @__PURE__ */ jsxs4("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
929
+ /* @__PURE__ */ jsxs4("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
756
930
  field.label,
757
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
931
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
758
932
  ] }),
759
- field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
933
+ field.description && /* @__PURE__ */ jsx4("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
760
934
  children,
761
- error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
935
+ error && /* @__PURE__ */ jsx4("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
762
936
  ]
763
937
  }
764
938
  );
@@ -766,11 +940,12 @@ function FieldShell({
766
940
  function TextInput({ field, value, error, setValue, ids: providedIds }) {
767
941
  const ids = useFilloFieldIds(field.id, providedIds);
768
942
  const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
943
+ const control = useControl("fillo-input", field, error);
769
944
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
770
945
  "input",
771
946
  {
772
947
  id: ids.inputId,
773
- className: "fillo-input",
948
+ ...control,
774
949
  type,
775
950
  value: typeof value === "string" || typeof value === "number" ? String(value) : "",
776
951
  placeholder: field.placeholder,
@@ -781,11 +956,12 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
781
956
  }
782
957
  function LongText({ field, value, error, setValue, ids: providedIds }) {
783
958
  const ids = useFilloFieldIds(field.id, providedIds);
959
+ const control = useControl("fillo-input fillo-textarea", field, error);
784
960
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
785
961
  "textarea",
786
962
  {
787
963
  id: ids.inputId,
788
- className: "fillo-input fillo-textarea",
964
+ ...control,
789
965
  rows: 4,
790
966
  value: typeof value === "string" ? value : "",
791
967
  placeholder: field.placeholder,
@@ -834,6 +1010,9 @@ function ChoiceIcon({ icon }) {
834
1010
  }
835
1011
  function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
836
1012
  const ids = useFilloFieldIds(field.id, providedIds);
1013
+ const appearance = useFilloAppearance();
1014
+ const strings = useStrings();
1015
+ const optionCls = (base, optionId, selected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected });
837
1016
  const choice = field;
838
1017
  const options = useDisplayOptions(choice);
839
1018
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
@@ -842,7 +1021,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
842
1021
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
843
1022
  "div",
844
1023
  {
845
- className: "fillo-options",
1024
+ className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
1025
+ "data-fillo": "options",
846
1026
  role: "radiogroup",
847
1027
  "aria-labelledby": ids.labelId,
848
1028
  ...fieldAria(field, error, ids),
@@ -851,7 +1031,19 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
851
1031
  "label",
852
1032
  {
853
1033
  "data-option": opt.id,
854
- className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
1034
+ "data-fillo": "option",
1035
+ "data-selected": value === opt.id ? "" : void 0,
1036
+ className: optionCls(
1037
+ `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
1038
+ opt.id,
1039
+ value === opt.id
1040
+ ),
1041
+ onClick: (event) => {
1042
+ if (isNativeControlTarget(event.target)) return;
1043
+ event.preventDefault();
1044
+ setOtherOn(false);
1045
+ setValue(opt.id);
1046
+ },
855
1047
  children: [
856
1048
  /* @__PURE__ */ jsx4(
857
1049
  "input",
@@ -867,7 +1059,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
867
1059
  }
868
1060
  ),
869
1061
  /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
870
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
1062
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
871
1063
  ]
872
1064
  },
873
1065
  opt.id
@@ -877,7 +1069,13 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
877
1069
  /* @__PURE__ */ jsxs4(
878
1070
  "div",
879
1071
  {
880
- className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1072
+ "data-fillo": "option",
1073
+ "data-selected": otherActive ? "" : void 0,
1074
+ className: optionCls(
1075
+ `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1076
+ "__other",
1077
+ otherActive
1078
+ ),
881
1079
  children: [
882
1080
  /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
883
1081
  /* @__PURE__ */ jsx4(
@@ -893,15 +1091,15 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
893
1091
  }
894
1092
  }
895
1093
  ),
896
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
1094
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
897
1095
  ] }),
898
1096
  otherActive && /* @__PURE__ */ jsx4(
899
1097
  "input",
900
1098
  {
901
1099
  type: "text",
902
1100
  className: "fillo-input fillo-other-input",
903
- "aria-label": "Other \u2014 please specify",
904
- placeholder: "Your answer",
1101
+ "aria-label": strings.otherPrompt,
1102
+ placeholder: strings.otherPlaceholder,
905
1103
  autoFocus: !isOtherValue,
906
1104
  value: isOtherValue ? String(value) : "",
907
1105
  onChange: (e) => setValue(e.target.value)
@@ -918,16 +1116,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
918
1116
  const ids = useFilloFieldIds(field.id, providedIds);
919
1117
  const choice = field;
920
1118
  const options = useDisplayOptions(choice);
1119
+ const strings = useStrings();
921
1120
  const optionIds = new Set(choice.options.map((o) => o.id));
922
1121
  const selected = Array.isArray(value) ? value : [];
923
1122
  const otherText = selected.find((v) => !optionIds.has(v));
924
1123
  const [otherOn, setOtherOn] = useState5(otherText !== void 0);
925
1124
  const otherActive = otherText !== void 0 || otherOn;
926
1125
  const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
1126
+ const appearance = useFilloAppearance();
1127
+ const optionCls = (base, optionId, isSelected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected: isSelected });
927
1128
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
928
1129
  "div",
929
1130
  {
930
- className: "fillo-options",
1131
+ className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
1132
+ "data-fillo": "options",
931
1133
  role: "group",
932
1134
  "aria-labelledby": ids.labelId,
933
1135
  ...fieldAria(field, error, ids),
@@ -936,7 +1138,18 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
936
1138
  "label",
937
1139
  {
938
1140
  "data-option": opt.id,
939
- className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
1141
+ "data-fillo": "option",
1142
+ "data-selected": selected.includes(opt.id) ? "" : void 0,
1143
+ className: optionCls(
1144
+ `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
1145
+ opt.id,
1146
+ selected.includes(opt.id)
1147
+ ),
1148
+ onClick: (event) => {
1149
+ if (isNativeControlTarget(event.target)) return;
1150
+ event.preventDefault();
1151
+ toggle(opt.id);
1152
+ },
940
1153
  children: [
941
1154
  /* @__PURE__ */ jsx4(
942
1155
  "input",
@@ -948,7 +1161,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
948
1161
  }
949
1162
  ),
950
1163
  /* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
951
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
1164
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
952
1165
  ]
953
1166
  },
954
1167
  opt.id
@@ -958,7 +1171,13 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
958
1171
  /* @__PURE__ */ jsxs4(
959
1172
  "div",
960
1173
  {
961
- className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1174
+ "data-fillo": "option",
1175
+ "data-selected": otherActive ? "" : void 0,
1176
+ className: optionCls(
1177
+ `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
1178
+ "__other",
1179
+ otherActive
1180
+ ),
962
1181
  children: [
963
1182
  /* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
964
1183
  /* @__PURE__ */ jsx4(
@@ -977,15 +1196,15 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
977
1196
  }
978
1197
  }
979
1198
  ),
980
- /* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
1199
+ /* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
981
1200
  ] }),
982
1201
  otherActive && /* @__PURE__ */ jsx4(
983
1202
  "input",
984
1203
  {
985
1204
  type: "text",
986
1205
  className: "fillo-input fillo-other-input",
987
- "aria-label": "Other \u2014 please specify",
988
- placeholder: "Your answer",
1206
+ "aria-label": strings.otherPrompt,
1207
+ placeholder: strings.otherPlaceholder,
989
1208
  autoFocus: otherText === void 0,
990
1209
  value: otherText ?? "",
991
1210
  onChange: (e) => {
@@ -1004,18 +1223,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1004
1223
  var OTHER_SENTINEL = "__fw_other__";
1005
1224
  function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1006
1225
  const ids = useFilloFieldIds(field.id, providedIds);
1226
+ const strings = useStrings();
1007
1227
  const choice = field;
1008
1228
  const options = useDisplayOptions(choice);
1009
1229
  const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
1010
1230
  const [otherOn, setOtherOn] = useState5(isOtherValue);
1011
1231
  const otherActive = isOtherValue || value === "" && otherOn;
1232
+ const control = useControl("fillo-input fillo-select", field, error);
1012
1233
  return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1013
1234
  /* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
1014
1235
  /* @__PURE__ */ jsxs4(
1015
1236
  "select",
1016
1237
  {
1017
1238
  id: ids.inputId,
1018
- className: "fillo-input fillo-select",
1239
+ ...control,
1019
1240
  ...fieldAria(field, error, ids),
1020
1241
  value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
1021
1242
  onChange: (e) => {
@@ -1028,9 +1249,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1028
1249
  }
1029
1250
  },
1030
1251
  children: [
1031
- /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? "Choose\u2026" }),
1252
+ /* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
1032
1253
  options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
1033
- choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: "Other\u2026" })
1254
+ choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: `${strings.other}\u2026` })
1034
1255
  ]
1035
1256
  }
1036
1257
  ),
@@ -1041,8 +1262,8 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1041
1262
  {
1042
1263
  type: "text",
1043
1264
  className: "fillo-input fillo-other-input fillo-other-input--block",
1044
- "aria-label": "Other \u2014 please specify",
1045
- placeholder: "Your answer",
1265
+ "aria-label": strings.otherPrompt,
1266
+ placeholder: strings.otherPlaceholder,
1046
1267
  autoFocus: !isOtherValue,
1047
1268
  value: isOtherValue ? String(value) : "",
1048
1269
  onChange: (e) => setValue(e.target.value)
@@ -1052,18 +1273,24 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
1052
1273
  }
1053
1274
  function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1054
1275
  const ids = useFilloFieldIds(field.id, providedIds);
1276
+ const strings = useStrings();
1277
+ const appearance = useFilloAppearance();
1055
1278
  const checkbox = field;
1056
1279
  if (checkbox.appearance === "toggle") {
1057
1280
  return /* @__PURE__ */ jsxs4(
1058
1281
  "div",
1059
1282
  {
1060
1283
  className: `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`,
1284
+ "data-fillo": "field",
1061
1285
  "data-field": field.id,
1286
+ "data-kind": field.kind,
1287
+ "data-invalid": error ? "" : void 0,
1288
+ "data-required": field.required ? "" : void 0,
1062
1289
  children: [
1063
- /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", children: [
1064
- /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
1290
+ /* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1291
+ /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1065
1292
  field.label,
1066
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
1293
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1067
1294
  ] }) }),
1068
1295
  /* @__PURE__ */ jsx4(
1069
1296
  "input",
@@ -1090,9 +1317,13 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1090
1317
  "div",
1091
1318
  {
1092
1319
  className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
1320
+ "data-fillo": "field",
1093
1321
  "data-field": field.id,
1322
+ "data-kind": field.kind,
1323
+ "data-invalid": error ? "" : void 0,
1324
+ "data-required": field.required ? "" : void 0,
1094
1325
  children: [
1095
- /* @__PURE__ */ jsxs4("label", { className: "fillo-option", children: [
1326
+ /* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
1096
1327
  /* @__PURE__ */ jsx4(
1097
1328
  "input",
1098
1329
  {
@@ -1104,9 +1335,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
1104
1335
  ...fieldAria(field, error, ids)
1105
1336
  }
1106
1337
  ),
1107
- /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
1338
+ /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
1108
1339
  field.label,
1109
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
1340
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1110
1341
  ] })
1111
1342
  ] }),
1112
1343
  field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
@@ -1147,10 +1378,12 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1147
1378
  const steps = Array.from({ length: max }, (_, i) => i + 1);
1148
1379
  const selected = current > 0 ? current : null;
1149
1380
  const tabbable = rovingIndex(steps, selected);
1381
+ const appearance = useFilloAppearance();
1382
+ const control = useControl("fillo-rating", field, error);
1150
1383
  return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
1151
1384
  "div",
1152
1385
  {
1153
- className: "fillo-rating",
1386
+ ...control,
1154
1387
  role: "radiogroup",
1155
1388
  "aria-labelledby": ids.labelId,
1156
1389
  ...fieldAria(field, error, ids),
@@ -1159,7 +1392,14 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
1159
1392
  "button",
1160
1393
  {
1161
1394
  type: "button",
1162
- className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
1395
+ "data-fillo": "option",
1396
+ "data-selected": n <= current ? "" : void 0,
1397
+ className: slotClass2(`fillo-star${n <= current ? " fillo-star--active" : ""}`, appearance, {
1398
+ slot: "option",
1399
+ kind: field.kind,
1400
+ fieldId: field.id,
1401
+ selected: n <= current
1402
+ }),
1163
1403
  role: "radio",
1164
1404
  "aria-label": `${n} of ${max}`,
1165
1405
  "aria-checked": n === current,
@@ -1189,11 +1429,13 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1189
1429
  field.description ? ids.descriptionId : null,
1190
1430
  error ? ids.errorId : null
1191
1431
  ].filter(Boolean).join(" ") || void 0;
1432
+ const appearance = useFilloAppearance();
1433
+ const control = useControl("fillo-scale", field, error);
1192
1434
  return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
1193
1435
  /* @__PURE__ */ jsx4(
1194
1436
  "div",
1195
1437
  {
1196
- className: "fillo-scale",
1438
+ ...control,
1197
1439
  role: "radiogroup",
1198
1440
  "aria-labelledby": ids.labelId,
1199
1441
  "aria-invalid": error ? true : void 0,
@@ -1204,7 +1446,14 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
1204
1446
  "button",
1205
1447
  {
1206
1448
  type: "button",
1207
- className: `fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`,
1449
+ "data-fillo": "option",
1450
+ "data-selected": value === n ? "" : void 0,
1451
+ className: slotClass2(`fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`, appearance, {
1452
+ slot: "option",
1453
+ kind: field.kind,
1454
+ fieldId: field.id,
1455
+ selected: value === n
1456
+ }),
1208
1457
  role: "radio",
1209
1458
  "aria-label": String(n),
1210
1459
  "aria-checked": value === n,
@@ -1339,30 +1588,6 @@ function ContentRenderer({ block }) {
1339
1588
  return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
1340
1589
  }
1341
1590
  }
1342
- function shouldAutoSubmit(field, value, api) {
1343
- if (api.form.settings.submitMode !== "auto") return false;
1344
- if (!api.isLastPage || api.status !== "idle" || api.uploading) return false;
1345
- const hasDiscreteAnswer = (() => {
1346
- switch (field.kind) {
1347
- case "select":
1348
- case "dropdown":
1349
- return typeof value === "string" && field.options.some((option) => option.id === value);
1350
- case "checkbox":
1351
- return value === true;
1352
- case "rating":
1353
- case "linear_scale":
1354
- return typeof value === "number";
1355
- default:
1356
- return false;
1357
- }
1358
- })();
1359
- if (!hasDiscreteAnswer) return false;
1360
- if (value === api.data[field.id]) return false;
1361
- const nextData = { ...api.data, [field.id]: value };
1362
- const interactive = visibleFields(api.form, nextData).filter((f) => f.kind !== "hidden");
1363
- if (interactive.length !== 1 || interactive[0]?.id !== field.id) return false;
1364
- return validateResponse(api.form, nextData).ok;
1365
- }
1366
1591
  function blockHasPiping(block) {
1367
1592
  const has = (s) => typeof s === "string" && s.includes("{{");
1368
1593
  switch (block.kind) {
@@ -1382,8 +1607,12 @@ function blockPropsEqual(prev, next) {
1382
1607
  const id = next.block.id;
1383
1608
  if (prev.api.data[id] !== next.api.data[id]) return false;
1384
1609
  if (prev.api.errors[id] !== next.api.errors[id]) return false;
1385
- const needsWiderState = next.block.kind === "custom" || blockHasPiping(prev.block) || blockHasPiping(next.block);
1386
- if (needsWiderState && prev.api.data !== next.api.data) return false;
1610
+ if (next.block.kind === "custom") {
1611
+ 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;
1612
+ }
1613
+ if ((blockHasPiping(prev.block) || blockHasPiping(next.block)) && prev.api.data !== next.api.data) {
1614
+ return false;
1615
+ }
1387
1616
  if (next.api.form.settings.submitMode === "auto") {
1388
1617
  if (prev.api.data !== next.api.data || prev.api.status !== next.api.status || prev.api.uploading !== next.api.uploading || prev.api.isLastPage !== next.api.isLastPage) {
1389
1618
  return false;
@@ -1418,7 +1647,15 @@ function BlockRendererInner({ block, api, components, customComponents }) {
1418
1647
  error: api.errors[block.id],
1419
1648
  setValue: (v) => {
1420
1649
  api.setValue(block.id, v);
1421
- if (shouldAutoSubmit(block, v, api)) void api.submit();
1650
+ const ctx = {
1651
+ form: api.form,
1652
+ data: api.data,
1653
+ status: api.status,
1654
+ isLastPage: api.isLastPage,
1655
+ uploading: api.uploading
1656
+ };
1657
+ if (shouldAutoSubmit(block, v, ctx)) void api.submit().catch(() => {
1658
+ });
1422
1659
  },
1423
1660
  api,
1424
1661
  ids
@@ -1432,8 +1669,13 @@ function FormField({
1432
1669
  customComponents
1433
1670
  }) {
1434
1671
  const api = useFillo();
1672
+ const visibleIds = useMemo4(
1673
+ () => new Set(visibleFields(api.form, api.data).map((f) => f.id)),
1674
+ [api.form, api.data]
1675
+ );
1435
1676
  const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
1436
1677
  if (!block) return null;
1678
+ if (isField(block) && !visibleIds.has(block.id)) return null;
1437
1679
  return /* @__PURE__ */ jsx4(
1438
1680
  BlockRenderer,
1439
1681
  {
@@ -1487,18 +1729,25 @@ function focusFirstInvalid(formEl) {
1487
1729
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
1488
1730
  else setTimeout(run, 0);
1489
1731
  }
1490
- function isAutoSubmitBlock(block) {
1491
- return isField2(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
1492
- }
1493
- function needsExplicitSubmit(visible) {
1494
- const interactive = visible.filter((b) => !(isField2(b) && b.kind === "hidden"));
1495
- if (interactive.length === 0) return false;
1496
- if (interactive.length > 1) return true;
1497
- return !isAutoSubmitBlock(interactive[0]);
1732
+ var HP_STYLE = {
1733
+ position: "absolute",
1734
+ left: "-9999px",
1735
+ top: "auto",
1736
+ width: "1px",
1737
+ height: "1px",
1738
+ opacity: 0,
1739
+ overflow: "hidden",
1740
+ pointerEvents: "none"
1741
+ };
1742
+ function assertHpHidden(el) {
1743
+ if (!el) return;
1744
+ for (const [prop, value] of Object.entries(HP_STYLE)) {
1745
+ el.style[prop] = String(value);
1746
+ }
1498
1747
  }
1499
1748
  function FilloForm(props) {
1500
1749
  const { form, formId } = props;
1501
- const client = useMemo4(
1750
+ const client = useMemo5(
1502
1751
  () => props.client ?? (formId ? createClient() : void 0),
1503
1752
  [props.client, formId]
1504
1753
  );
@@ -1510,7 +1759,11 @@ function FilloForm(props) {
1510
1759
  const codeForm = isCodeForm2(form) ? form : null;
1511
1760
  const inlineSchema = codeForm ? codeForm.schema : form;
1512
1761
  const onError = props.onError;
1513
- const syncedFormId = useCodeFormSync(form, client, onError);
1762
+ const { formId: syncedFormId, status: syncedStatus } = useCodeFormSync(form, client, onError);
1763
+ const onErrorRef = useRef5(onError);
1764
+ useEffect7(() => {
1765
+ onErrorRef.current = onError;
1766
+ });
1514
1767
  const needsFetch = !inlineSchema && Boolean(client && formId);
1515
1768
  useEffect7(() => {
1516
1769
  if (!needsFetch || !client || !formId) return;
@@ -1527,30 +1780,37 @@ function FilloForm(props) {
1527
1780
  if (cancelled) return;
1528
1781
  const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
1529
1782
  setLoadError(error);
1530
- onError?.(error);
1783
+ onErrorRef.current?.(error);
1531
1784
  });
1532
1785
  return () => {
1533
1786
  cancelled = true;
1534
1787
  };
1535
- }, [needsFetch, client, formId, onError]);
1788
+ }, [needsFetch, client, formId]);
1789
+ const strings = resolveStrings(props.strings);
1536
1790
  const rawSchema = inlineSchema ?? fetched;
1537
- const normalized = useMemo4(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
1791
+ const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
1538
1792
  const schema = normalized?.ok ? normalized.schema : null;
1539
- const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
1793
+ const theme = props.appearance?.theme ?? props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
1540
1794
  if (normalized && !normalized.ok) {
1541
1795
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
1542
1796
  if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
1543
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: "This form could not be rendered." });
1797
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
1544
1798
  }
1545
1799
  if (loadError) {
1546
1800
  if (props.renderError) {
1547
1801
  return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(loadError) });
1548
1802
  }
1549
- const message = loadError.status === 404 ? "Form not found \u2014 check the form id and that it's published." : loadError.status === 0 ? "Couldn't reach the server \u2014 check your connection or CORS." : "This form could not be loaded.";
1803
+ const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
1550
1804
  return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
1551
1805
  }
1552
1806
  if (closed) {
1553
- return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: "This form is no longer accepting responses." }) });
1807
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.closed }) });
1808
+ }
1809
+ const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
1810
+ if (notPublished && !isDevEnv()) {
1811
+ const error = new FilloError2("This form isn't published yet.", 403);
1812
+ if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
1813
+ return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.notLive }) });
1554
1814
  }
1555
1815
  if (!schema) {
1556
1816
  return /* @__PURE__ */ jsxs5(
@@ -1578,7 +1838,8 @@ function FilloForm(props) {
1578
1838
  formId: syncedFormId ?? formId,
1579
1839
  poweredBy,
1580
1840
  brandLabel: fetchedBranding?.label,
1581
- brandHref: fetchedBranding?.href
1841
+ brandHref: fetchedBranding?.href,
1842
+ draftNotice: notPublished
1582
1843
  }
1583
1844
  );
1584
1845
  }
@@ -1596,10 +1857,25 @@ function DevNoClientNotice() {
1596
1857
  " to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
1597
1858
  ] });
1598
1859
  }
1860
+ function DevDraftNotice() {
1861
+ return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
1862
+ /* @__PURE__ */ jsx5("strong", { children: "Draft form." }),
1863
+ " It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
1864
+ ] });
1865
+ }
1599
1866
  function ResolvedForm(props) {
1600
1867
  const { schema, theme } = props;
1601
1868
  const instanceId = useId2().replace(/:/g, "");
1602
1869
  const hpRef = useRef5("");
1870
+ const codeForm = isCodeForm2(props.form) ? props.form : null;
1871
+ const client = props.client;
1872
+ const resolveFormId = useMemo5(
1873
+ () => codeForm && client?.key ? (
1874
+ // bypassCache: a stale cached formId must not be trusted at submit time.
1875
+ () => syncCodeForm2(client, codeForm, { bypassCache: true }).then((r) => r.formId)
1876
+ ) : void 0,
1877
+ [codeForm, client]
1878
+ );
1603
1879
  const api = useFilloController({
1604
1880
  form: schema,
1605
1881
  formId: props.formId,
@@ -1608,9 +1884,13 @@ function ResolvedForm(props) {
1608
1884
  onChange: props.onChange,
1609
1885
  onSubmitted: props.onSubmitted,
1610
1886
  skipValidation: props.skipValidation,
1611
- getHoneypot: () => hpRef.current
1887
+ getHoneypot: () => hpRef.current,
1888
+ // Framed renderer: Fillo draws the layout (and badge), so the surface is
1889
+ // "default", never the bare-engine "headless".
1890
+ surface: "default",
1891
+ resolveFormId
1612
1892
  });
1613
- const themed = useMemo4(() => themeProps(theme), [theme]);
1893
+ const themed = useMemo5(() => themeProps(theme), [theme]);
1614
1894
  const settings = schema.settings;
1615
1895
  const submitted = api.status === "submitted";
1616
1896
  useEffect7(() => {
@@ -1618,41 +1898,89 @@ function ResolvedForm(props) {
1618
1898
  const url = safeHttpUrl(settings.redirectUrl);
1619
1899
  if (url) window.location.assign(url);
1620
1900
  }, [submitted, settings.redirectUrl]);
1901
+ const successRef = useRef5(null);
1902
+ useEffect7(() => {
1903
+ if (submitted) successRef.current?.focus();
1904
+ }, [submitted]);
1905
+ const blocksRef = useRef5(null);
1906
+ const prevPageIndex = useRef5(api.pageIndex);
1907
+ useEffect7(() => {
1908
+ if (prevPageIndex.current === api.pageIndex) return;
1909
+ prevPageIndex.current = api.pageIndex;
1910
+ blocksRef.current?.focus();
1911
+ }, [api.pageIndex]);
1912
+ const appearance = props.appearance;
1913
+ const strings = resolveStrings(props.strings);
1621
1914
  if (api.status === "submitted") {
1622
- return /* @__PURE__ */ jsxs5("div", { className: `fillo-form fillo-form--success ${props.className ?? ""}`, ...themed, children: [
1623
- props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5("div", { className: "fillo-success", children: [
1624
- /* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
1625
- /* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? "Thanks!" }),
1626
- /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? "Your response has been recorded." })
1627
- ] }),
1628
- props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
1629
- ] });
1915
+ return /* @__PURE__ */ jsxs5(
1916
+ "div",
1917
+ {
1918
+ ref: successRef,
1919
+ tabIndex: -1,
1920
+ role: "status",
1921
+ "aria-live": "polite",
1922
+ className: slotClass3(
1923
+ `fillo-form fillo-form--success ${props.className ?? ""}`,
1924
+ appearance,
1925
+ { slot: "root", status: "submitted" }
1926
+ ),
1927
+ "data-fillo": "root",
1928
+ "data-state": "submitted",
1929
+ ...themed,
1930
+ children: [
1931
+ props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5(
1932
+ "div",
1933
+ {
1934
+ className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
1935
+ "data-fillo": "success",
1936
+ children: [
1937
+ /* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
1938
+ /* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
1939
+ /* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? strings.successMessage })
1940
+ ]
1941
+ }
1942
+ ),
1943
+ props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
1944
+ ]
1945
+ }
1946
+ );
1630
1947
  }
1631
1948
  const multiPage = api.pageCount > 1;
1632
1949
  const autoSubmit = settings.submitMode === "auto";
1633
1950
  const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields2(api.form, api.data));
1634
1951
  const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
1635
- return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
1952
+ return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx5(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
1636
1953
  "form",
1637
1954
  {
1638
- className: `fillo-form ${props.className ?? ""}`,
1955
+ className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
1956
+ slot: "root",
1957
+ status: api.status
1958
+ }),
1959
+ "data-fillo": "root",
1960
+ "data-state": api.status,
1961
+ "data-page": api.pageIndex + 1,
1962
+ "data-last-page": api.isLastPage ? "" : void 0,
1639
1963
  ...themed,
1640
1964
  noValidate: true,
1641
1965
  onSubmit: (e) => {
1642
1966
  e.preventDefault();
1643
1967
  const formEl = e.currentTarget;
1644
- if (api.isLastPage) void api.submit().then(() => focusFirstInvalid(formEl));
1645
- else {
1968
+ if (api.isLastPage) {
1969
+ void api.submit().then(() => focusFirstInvalid(formEl)).catch(() => {
1970
+ });
1971
+ } else {
1646
1972
  api.next();
1647
1973
  focusFirstInvalid(formEl);
1648
1974
  }
1649
1975
  },
1650
1976
  children: [
1651
1977
  !props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
1978
+ props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
1652
1979
  multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
1653
1980
  "div",
1654
1981
  {
1655
- className: "fillo-progress-track",
1982
+ className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
1983
+ "data-fillo": "progress",
1656
1984
  role: "progressbar",
1657
1985
  "aria-valuemin": 0,
1658
1986
  "aria-valuemax": api.pageCount,
@@ -1660,18 +1988,26 @@ function ResolvedForm(props) {
1660
1988
  children: /* @__PURE__ */ jsx5(
1661
1989
  "div",
1662
1990
  {
1663
- className: "fillo-progress-fill",
1664
- style: { width: `${(api.pageIndex + 1) / api.pageCount * 100}%` }
1991
+ className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
1992
+ "data-fillo": "progressFill",
1993
+ style: { "--fillo-progress-value": `${(api.pageIndex + 1) / api.pageCount * 100}%` }
1665
1994
  }
1666
1995
  )
1667
1996
  }
1668
1997
  ),
1669
- api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: "fillo-header", children: [
1670
- /* @__PURE__ */ jsx5("h1", { className: "fillo-title", children: schema.title }),
1671
- schema.description && /* @__PURE__ */ jsx5("p", { className: "fillo-form-description", children: schema.description })
1998
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
1999
+ /* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2000
+ schema.description && /* @__PURE__ */ jsx5(
2001
+ "p",
2002
+ {
2003
+ className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2004
+ "data-fillo": "description",
2005
+ children: schema.description
2006
+ }
2007
+ )
1672
2008
  ] }),
1673
- multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: "fillo-page-title", children: api.page.title }),
1674
- /* @__PURE__ */ jsx5("div", { className: "fillo-blocks", children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
2009
+ multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }), "data-fillo": "pageTitle", children: api.page.title }),
2010
+ /* @__PURE__ */ jsx5("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
1675
2011
  BlockRenderer,
1676
2012
  {
1677
2013
  block,
@@ -1691,45 +2027,123 @@ function ResolvedForm(props) {
1691
2027
  autoComplete: "off",
1692
2028
  "aria-hidden": "true",
1693
2029
  defaultValue: "",
2030
+ style: HP_STYLE,
2031
+ ref: assertHpHidden,
1694
2032
  onChange: (e) => {
1695
2033
  hpRef.current = e.target.value;
1696
2034
  }
1697
2035
  }
1698
2036
  ),
1699
- api.status === "error" && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: "This form can't submit right now. Please try again in a moment." }),
1700
- showFooter && /* @__PURE__ */ jsxs5("footer", { className: "fillo-footer", children: [
1701
- multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5("button", { type: "button", className: "fillo-button fillo-button--ghost", onClick: api.back, children: "Back" }),
2037
+ (api.status === "error" || api.submitError) && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: api.submitError ?? strings.submitFailed }),
2038
+ showFooter && /* @__PURE__ */ jsxs5("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
2039
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5(
2040
+ "button",
2041
+ {
2042
+ type: "button",
2043
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
2044
+ "data-fillo": "button",
2045
+ onClick: api.back,
2046
+ children: strings.back
2047
+ }
2048
+ ),
1702
2049
  showPrimaryButton && /* @__PURE__ */ jsx5(
1703
2050
  "button",
1704
2051
  {
1705
2052
  type: "submit",
1706
- className: "fillo-button fillo-button--primary",
2053
+ className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
2054
+ "data-fillo": "button",
1707
2055
  disabled: api.status === "submitting" || api.uploading,
1708
- children: api.uploading ? "Uploading\u2026" : api.status === "submitting" ? "Submitting\u2026" : api.isLastPage ? settings.submitLabel ?? "Submit" : "Next"
2056
+ children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
1709
2057
  }
1710
2058
  )
1711
2059
  ] }),
1712
2060
  props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
1713
2061
  ]
1714
2062
  }
1715
- ) }) });
2063
+ ) }) }) }) });
1716
2064
  }
1717
2065
 
1718
- // src/provider.tsx
1719
- import { useId as useId3, useMemo as useMemo5 } from "react";
1720
- import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2 } from "@usefillo/core";
2066
+ // src/jsx.tsx
2067
+ import { useRef as useRef6 } from "react";
2068
+ import {
2069
+ codeFormFromJsx,
2070
+ FilloJsxError,
2071
+ JSX_BLOCK_COMPONENTS
2072
+ } from "@usefillo/core";
1721
2073
  import { jsx as jsx6 } from "react/jsx-runtime";
1722
- function FilloProvider({ children, form, formId, client, ...options }) {
2074
+ var B = JSX_BLOCK_COMPONENTS;
2075
+ var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
2076
+ function JsxForm(props) {
2077
+ const { id, title, description, settings, theme, client, children, ...rest } = props;
2078
+ const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
2079
+ const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
2080
+ const memo2 = useRef6(null);
2081
+ if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
2082
+ console.warn(
2083
+ `[fillo] "${id}": form structure changed between renders. If you just edited the form, ignore this. Otherwise: conditional questions are visibleIf={when(\u2026)\u2026}, never {cond && <Fillo.\u2026/>} \u2014 a per-visitor schema churns drafts in your workspace.`
2084
+ );
2085
+ }
2086
+ if (!memo2.current || memo2.current.json !== json) {
2087
+ memo2.current = { json, form: compiled };
2088
+ }
2089
+ return /* @__PURE__ */ jsx6(FilloForm, { form: memo2.current.form, client, ...rest });
2090
+ }
2091
+ function defineFormFromJsx(element) {
2092
+ if (!element || element.type !== JsxForm) {
2093
+ throw new FilloJsxError(
2094
+ "NON_FILLO_CHILD",
2095
+ "Fillo.defineForm() takes a <Fillo.Form> element: Fillo.defineForm(<Fillo.Form id=\u2026>\u2026</Fillo.Form>)"
2096
+ );
2097
+ }
2098
+ const { id, title, description, settings, theme, children } = element.props;
2099
+ return codeFormFromJsx({ id, title, description, settings, theme }, children);
2100
+ }
2101
+ var Fillo = {
2102
+ Form: JsxForm,
2103
+ /** Compile a <Fillo.Form> element to a CodeForm at module scope — the same
2104
+ * value FilloProvider (headless) and the CLI consume. */
2105
+ defineForm: defineFormFromJsx,
2106
+ Text: B.Text,
2107
+ LongText: B.LongText,
2108
+ Email: B.Email,
2109
+ Url: B.Url,
2110
+ Phone: B.Phone,
2111
+ Number: B.Number,
2112
+ Select: B.Select,
2113
+ MultiSelect: B.MultiSelect,
2114
+ Dropdown: B.Dropdown,
2115
+ Checkbox: B.Checkbox,
2116
+ Rating: B.Rating,
2117
+ Scale: B.Scale,
2118
+ Ranking: B.Ranking,
2119
+ Matrix: B.Matrix,
2120
+ Signature: B.Signature,
2121
+ Date: B.Date,
2122
+ FileUpload: B.FileUpload,
2123
+ Hidden: B.Hidden,
2124
+ Custom: B.Custom,
2125
+ Heading: B.Heading,
2126
+ Paragraph: B.Paragraph,
2127
+ Divider: B.Divider,
2128
+ Page: B.Page,
2129
+ Option: B.Option
2130
+ };
2131
+
2132
+ // src/provider.tsx
2133
+ import { useId as useId3, useMemo as useMemo6 } from "react";
2134
+ import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2, resolveStrings as resolveStrings2 } from "@usefillo/core";
2135
+ import { jsx as jsx7 } from "react/jsx-runtime";
2136
+ function FilloProvider({ children, form, formId, client, appearance, strings, ...options }) {
1723
2137
  const instanceId = useId3().replace(/:/g, "");
1724
2138
  const codeForm = isCodeForm2(form) ? form : null;
1725
2139
  const rawSchema = codeForm ? codeForm.schema : form;
1726
- const schemaKey = useMemo5(() => JSON.stringify(rawSchema), [rawSchema]);
1727
- const normalized = useMemo5(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
2140
+ const schemaKey = useMemo6(() => JSON.stringify(rawSchema), [rawSchema]);
2141
+ const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
1728
2142
  if (!normalized.ok) {
1729
2143
  throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
1730
2144
  }
1731
2145
  const schema = normalized.schema;
1732
- const syncedFormId = useCodeFormSync(form, client);
2146
+ const { formId: syncedFormId } = useCodeFormSync(form, client);
1733
2147
  const api = useFilloController({
1734
2148
  ...options,
1735
2149
  client,
@@ -1739,22 +2153,36 @@ function FilloProvider({ children, form, formId, client, ...options }) {
1739
2153
  // a paid capability the server enforces when these responses are submitted.
1740
2154
  surface: "headless"
1741
2155
  });
1742
- return /* @__PURE__ */ jsx6(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx6(FilloInstanceIdContext.Provider, { value: instanceId, children }) });
2156
+ return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolveStrings2(strings), children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
1743
2157
  }
1744
2158
 
1745
2159
  // src/index.ts
1746
2160
  import {
1747
2161
  createClient as createClient2,
1748
2162
  provisionWorkspace,
2163
+ when,
2164
+ FilloJsxError as FilloJsxError2,
1749
2165
  FilloClient,
1750
2166
  FilloError as FilloError4
1751
2167
  } from "@usefillo/core";
2168
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
2169
+ const key = /* @__PURE__ */ Symbol.for("usefillo.react.instance");
2170
+ const g = globalThis;
2171
+ if (g[key]) {
2172
+ console.error(
2173
+ "[fillo] Two copies of @usefillo/react are loaded \u2014 React context does not cross package copies, so forms will misbehave. Dedupe the dependency (npm dedupe / pnpm why @usefillo/react)."
2174
+ );
2175
+ }
2176
+ g[key] = true;
2177
+ }
1752
2178
  export {
1753
2179
  BlockRenderer,
2180
+ Fillo,
1754
2181
  FilloClient,
1755
2182
  FilloError4 as FilloError,
1756
2183
  FileUploadField as FilloFileUpload,
1757
2184
  FilloForm,
2185
+ FilloJsxError2 as FilloJsxError,
1758
2186
  FilloProvider,
1759
2187
  FormField,
1760
2188
  createClient2 as createClient,
@@ -1762,5 +2190,7 @@ export {
1762
2190
  provisionWorkspace,
1763
2191
  useField,
1764
2192
  useFillo,
1765
- useFilloController
2193
+ useFilloAppearance,
2194
+ useFilloController,
2195
+ when
1766
2196
  };