@usefillo/react 0.11.0 → 0.12.1

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
@@ -15,7 +15,6 @@ import {
15
15
  createClient,
16
16
  formSchemasEqual,
17
17
  isBuildTimeDevEnv as isBuildTimeDevEnv2,
18
- isField as isField2,
19
18
  isFilloError as isFilloError2,
20
19
  isLikelyDevEnv,
21
20
  needsExplicitSubmit,
@@ -25,7 +24,7 @@ import {
25
24
  reachablePageSequence,
26
25
  resolveThemeAppearance
27
26
  } from "@usefillo/core";
28
- import { REQUIRED_FIELD_MESSAGE as REQUIRED_FIELD_MESSAGE2, resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
27
+ import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
29
28
 
30
29
  // src/appearance.tsx
31
30
  import { createContext, useContext } from "react";
@@ -483,10 +482,12 @@ import {
483
482
  formatGroupedNumber,
484
483
  isField,
485
484
  isValidPartialNumberText,
485
+ localeForNotation,
486
486
  parseGroupedNumber,
487
487
  pipeBlock,
488
488
  radioGroupStep,
489
489
  REQUIRED_FIELD_MESSAGE,
490
+ requiredFieldMessage,
490
491
  shouldAutoSubmit,
491
492
  slotClass as slotClass2,
492
493
  visibleFields
@@ -1007,7 +1008,84 @@ function uploadFailureMessage(error, strings) {
1007
1008
  }
1008
1009
  return strings.uploadFailed;
1009
1010
  }
1010
- function FileUploadField({ field, value, error, setValue, api, ids: providedIds }) {
1011
+ function FileStateIcon({ state }) {
1012
+ if (state === "done") {
1013
+ return /* @__PURE__ */ jsx4("span", { className: "fillo-file-state fillo-file-state--done", "aria-hidden": "true", children: /* @__PURE__ */ jsxs4(
1014
+ "svg",
1015
+ {
1016
+ className: "fillo-file-state-icon",
1017
+ viewBox: "0 0 24 24",
1018
+ width: "22",
1019
+ height: "22",
1020
+ fill: "none",
1021
+ "aria-hidden": "true",
1022
+ focusable: "false",
1023
+ children: [
1024
+ /* @__PURE__ */ jsx4("path", { d: "M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0" }),
1025
+ /* @__PURE__ */ jsx4("path", { d: "m8.5 12 2.25 2.25 4.75-5" })
1026
+ ]
1027
+ }
1028
+ ) });
1029
+ }
1030
+ if (state === "failed") {
1031
+ return /* @__PURE__ */ jsx4("span", { className: "fillo-file-state fillo-file-state--failed", "aria-hidden": "true", children: /* @__PURE__ */ jsxs4(
1032
+ "svg",
1033
+ {
1034
+ className: "fillo-file-state-icon",
1035
+ viewBox: "0 0 24 24",
1036
+ width: "22",
1037
+ height: "22",
1038
+ fill: "none",
1039
+ "aria-hidden": "true",
1040
+ focusable: "false",
1041
+ children: [
1042
+ /* @__PURE__ */ jsx4("path", { d: "M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0" }),
1043
+ /* @__PURE__ */ jsx4("path", { d: "M12 7.75v5.5" }),
1044
+ /* @__PURE__ */ jsx4("path", { d: "M12 16.5h.01" })
1045
+ ]
1046
+ }
1047
+ ) });
1048
+ }
1049
+ return /* @__PURE__ */ jsx4("span", { className: "fillo-file-state fillo-file-state--uploading", "aria-hidden": "true", children: /* @__PURE__ */ jsxs4(
1050
+ "svg",
1051
+ {
1052
+ className: "fillo-file-state-icon",
1053
+ viewBox: "0 0 24 24",
1054
+ width: "22",
1055
+ height: "22",
1056
+ fill: "none",
1057
+ "aria-hidden": "true",
1058
+ focusable: "false",
1059
+ children: [
1060
+ /* @__PURE__ */ jsx4("path", { d: "M7.25 3.75h6.5l3 3v13.5h-9.5z" }),
1061
+ /* @__PURE__ */ jsx4("path", { d: "M13.75 3.75v3h3" })
1062
+ ]
1063
+ }
1064
+ ) });
1065
+ }
1066
+ function CloseIcon() {
1067
+ return /* @__PURE__ */ jsx4(
1068
+ "svg",
1069
+ {
1070
+ className: "fillo-file-action-icon",
1071
+ viewBox: "0 0 24 24",
1072
+ width: "16",
1073
+ height: "16",
1074
+ fill: "none",
1075
+ "aria-hidden": "true",
1076
+ focusable: "false",
1077
+ children: /* @__PURE__ */ jsx4("path", { d: "m7 7 10 10M17 7 7 17" })
1078
+ }
1079
+ );
1080
+ }
1081
+ function FileUploadField({
1082
+ field,
1083
+ value,
1084
+ error,
1085
+ setValue,
1086
+ api,
1087
+ ids: providedIds
1088
+ }) {
1011
1089
  const ids = useFilloFieldIds(field.id, providedIds);
1012
1090
  const slots = useFieldSlots(field, error);
1013
1091
  const strings = useStrings();
@@ -1036,9 +1114,10 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1036
1114
  const configuredMaxMb = schema.maxFileSizeMb ?? 500;
1037
1115
  const maxMb = chrome?.uploadFileSizeLimitMb ? Math.min(configuredMaxMb, chrome.uploadFileSizeLimitMb) : configuredMaxMb;
1038
1116
  const maxBytes = maxMb * 1024 * 1024;
1117
+ const renderOnly = chrome?.renderOnly === true;
1039
1118
  const storageBlocked = chrome?.uploadsAvailable === false;
1040
1119
  const storageUrl = storageBlocked && chrome?.devChrome ? safeHttpUrl(chrome.warningUrl) : null;
1041
- const canUpload = Boolean(api.client && api.formId) && !storageBlocked;
1120
+ const canUpload = !renderOnly && Boolean(api.client && api.formId) && !storageBlocked;
1042
1121
  const activeInFlight = inFlight.filter((f) => !f.error).length;
1043
1122
  const remaining = maxFiles - files.length - activeInFlight;
1044
1123
  async function startUpload(file) {
@@ -1047,7 +1126,15 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1047
1126
  if (file.size > maxBytes) {
1048
1127
  setInFlight((prev) => [
1049
1128
  ...prev,
1050
- { key, name: file.name, size: file.size, fraction: 0, file, tooLarge: true, error: strings.fileTooLarge(maxMb) }
1129
+ {
1130
+ key,
1131
+ name: file.name,
1132
+ size: file.size,
1133
+ fraction: 0,
1134
+ file,
1135
+ tooLarge: true,
1136
+ error: strings.fileTooLarge(maxMb)
1137
+ }
1051
1138
  ]);
1052
1139
  return;
1053
1140
  }
@@ -1074,9 +1161,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1074
1161
  } catch (err) {
1075
1162
  if (mountedRef.current) {
1076
1163
  const message = uploadFailureMessage(err, strings);
1077
- setInFlight(
1078
- (prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
1079
- );
1164
+ setInFlight((prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f));
1080
1165
  }
1081
1166
  } finally {
1082
1167
  controllers.current.delete(key);
@@ -1100,109 +1185,111 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1100
1185
  setValue(next);
1101
1186
  }
1102
1187
  const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
1103
- return /* @__PURE__ */ jsxs4("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
1104
- /* @__PURE__ */ jsxs4("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1105
- field.label,
1106
- !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1107
- ] }),
1108
- field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1109
- remaining > 0 && /* @__PURE__ */ jsxs4(
1110
- "div",
1111
- {
1112
- "data-fillo": "control",
1113
- "data-drag-over": dragOver ? "" : void 0,
1114
- className: slots.control(`fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`),
1115
- onDragOver: (e) => {
1116
- e.preventDefault();
1117
- setDragOver(true);
1118
- },
1119
- onDragLeave: () => setDragOver(false),
1120
- onDrop: (e) => {
1121
- e.preventDefault();
1122
- setDragOver(false);
1123
- if (canUpload) handleFiles(e.dataTransfer.files);
1124
- },
1125
- onClick: () => canUpload && inputRef.current?.click(),
1126
- role: "button",
1127
- tabIndex: 0,
1128
- "aria-disabled": !canUpload || void 0,
1129
- "aria-labelledby": ids.labelId,
1130
- "aria-required": field.required ? true : void 0,
1131
- "aria-invalid": error ? true : void 0,
1132
- "aria-describedby": describedBy,
1133
- onKeyDown: (e) => {
1134
- if (e.key === "Enter" || e.key === " ") {
1135
- e.preventDefault();
1136
- if (canUpload) inputRef.current?.click();
1188
+ return /* @__PURE__ */ jsxs4(
1189
+ "div",
1190
+ {
1191
+ ...slots.wrapperProps(
1192
+ `fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`
1193
+ ),
1194
+ children: [
1195
+ /* @__PURE__ */ jsxs4("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1196
+ field.label,
1197
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1198
+ ] }),
1199
+ field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1200
+ remaining > 0 && /* @__PURE__ */ jsxs4(
1201
+ "div",
1202
+ {
1203
+ "data-fillo": "control",
1204
+ "data-drag-over": dragOver ? "" : void 0,
1205
+ className: slots.control(
1206
+ `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`
1207
+ ),
1208
+ onDragOver: (e) => {
1209
+ e.preventDefault();
1210
+ setDragOver(true);
1211
+ },
1212
+ onDragLeave: () => setDragOver(false),
1213
+ onDrop: (e) => {
1214
+ e.preventDefault();
1215
+ setDragOver(false);
1216
+ if (canUpload) handleFiles(e.dataTransfer.files);
1217
+ },
1218
+ onClick: () => canUpload && inputRef.current?.click(),
1219
+ role: "button",
1220
+ tabIndex: 0,
1221
+ "aria-disabled": !canUpload || void 0,
1222
+ "aria-labelledby": ids.labelId,
1223
+ "aria-invalid": error ? true : void 0,
1224
+ "aria-describedby": describedBy,
1225
+ onKeyDown: (e) => {
1226
+ if (e.key === "Enter" || e.key === " ") {
1227
+ e.preventDefault();
1228
+ if (canUpload) inputRef.current?.click();
1229
+ }
1230
+ },
1231
+ children: [
1232
+ /* @__PURE__ */ jsx4(
1233
+ "input",
1234
+ {
1235
+ ref: inputRef,
1236
+ id: ids.inputId,
1237
+ type: "file",
1238
+ hidden: true,
1239
+ disabled: !canUpload,
1240
+ multiple: maxFiles > 1,
1241
+ accept: schema.accept?.join(","),
1242
+ onChange: (e) => canUpload && handleFiles(e.target.files)
1243
+ }
1244
+ ),
1245
+ canUpload ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1246
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
1247
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
1248
+ ] }) : storageBlocked && chrome?.devChrome ? /* @__PURE__ */ jsxs4("span", { className: "fillo-dropzone-hint", children: [
1249
+ "Connect file storage to enable uploads",
1250
+ storageUrl && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1251
+ " \u2014 ",
1252
+ /* @__PURE__ */ jsx4("a", { href: storageUrl, target: "_blank", rel: "noopener noreferrer", children: storageUrl })
1253
+ ] })
1254
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: renderOnly ? strings.uploadsRenderOnly : storageBlocked ? strings.uploadsUnavailable : strings.uploadsDisabled })
1255
+ ]
1137
1256
  }
1138
- },
1139
- children: [
1140
- /* @__PURE__ */ jsx4(
1141
- "input",
1142
- {
1143
- ref: inputRef,
1144
- id: ids.inputId,
1145
- type: "file",
1146
- hidden: true,
1147
- disabled: !canUpload,
1148
- multiple: maxFiles > 1,
1149
- accept: schema.accept?.join(","),
1150
- onChange: (e) => canUpload && handleFiles(e.target.files)
1151
- }
1152
- ),
1153
- canUpload ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1154
- /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
1155
- /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
1156
- ] }) : storageBlocked && chrome?.devChrome ? /* @__PURE__ */ jsxs4("span", { className: "fillo-dropzone-hint", children: [
1157
- "Connect file storage to enable uploads",
1158
- storageUrl && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1159
- " \u2014 ",
1160
- /* @__PURE__ */ jsx4("a", { href: storageUrl, target: "_blank", rel: "noopener noreferrer", children: storageUrl })
1161
- ] })
1162
- ] }) : /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: storageBlocked ? strings.uploadsUnavailable : strings.uploadsDisabled })
1163
- ]
1164
- }
1165
- ),
1166
- /* @__PURE__ */ jsx4(
1167
- "div",
1168
- {
1169
- role: "status",
1170
- "aria-live": "polite",
1171
- className: "fillo-sr-only",
1172
- style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
1173
- children: uploadStatus(inFlight, files.length, strings)
1174
- }
1175
- ),
1176
- (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs4("ul", { className: "fillo-files", children: [
1177
- files.map((f) => /* @__PURE__ */ jsxs4("li", { className: "fillo-file fillo-file--done", children: [
1178
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1179
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
1180
- /* @__PURE__ */ jsx4("span", { className: "fillo-sr-only", children: "Uploaded" }),
1257
+ ),
1181
1258
  /* @__PURE__ */ jsx4(
1182
- "button",
1259
+ "div",
1183
1260
  {
1184
- type: "button",
1185
- className: "fillo-file-remove",
1186
- "aria-label": `Remove ${f.name}`,
1187
- onClick: () => removeFile(f.fileId),
1188
- children: "\xD7"
1261
+ role: "status",
1262
+ "aria-live": "polite",
1263
+ className: "fillo-sr-only",
1264
+ style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
1265
+ children: uploadStatus(inFlight, files.length, strings)
1189
1266
  }
1190
- )
1191
- ] }, f.fileId)),
1192
- inFlight.map((f) => /* @__PURE__ */ jsxs4(
1193
- "li",
1194
- {
1195
- className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
1196
- ...f.error ? {} : {
1197
- role: "progressbar",
1198
- "aria-label": `Uploading ${f.name}`,
1199
- "aria-valuemin": 0,
1200
- "aria-valuemax": 100,
1201
- "aria-valuenow": Math.round(f.fraction * 100)
1202
- },
1203
- children: [
1204
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1267
+ ),
1268
+ (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs4("ul", { className: "fillo-files", children: [
1269
+ files.map((f) => /* @__PURE__ */ jsxs4("li", { className: "fillo-file fillo-file--done", children: [
1270
+ /* @__PURE__ */ jsx4(FileStateIcon, { state: "done" }),
1271
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-content", children: [
1272
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1273
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: strings.uploadedFile(formatBytes(f.size)) })
1274
+ ] }),
1275
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-actions", children: /* @__PURE__ */ jsx4(
1276
+ "button",
1277
+ {
1278
+ type: "button",
1279
+ className: "fillo-file-remove",
1280
+ "aria-label": `${strings.uploadRemove} ${f.name}`,
1281
+ onClick: () => removeFile(f.fileId),
1282
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
1283
+ }
1284
+ ) })
1285
+ ] }, f.fileId)),
1286
+ inFlight.map((f) => /* @__PURE__ */ jsxs4("li", { className: `fillo-file${f.error ? " fillo-file--failed" : ""}`, children: [
1287
+ /* @__PURE__ */ jsx4(FileStateIcon, { state: f.error ? "failed" : "uploading" }),
1205
1288
  f.error ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1289
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-content", children: [
1290
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1291
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-error", role: "alert", children: f.error })
1292
+ ] }),
1206
1293
  /* @__PURE__ */ jsxs4("span", { className: "fillo-file-actions", children: [
1207
1294
  f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx4(
1208
1295
  "button",
@@ -1224,50 +1311,65 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1224
1311
  {
1225
1312
  type: "button",
1226
1313
  className: "fillo-file-remove",
1227
- "aria-label": "Dismiss",
1314
+ "aria-label": `${strings.uploadDismiss} ${f.name}`,
1228
1315
  onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
1229
- children: "\xD7"
1316
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
1230
1317
  }
1231
1318
  )
1232
- ] }),
1233
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-error", role: "alert", children: f.error })
1319
+ ] })
1234
1320
  ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1235
- /* @__PURE__ */ jsxs4("span", { className: "fillo-file-meta", children: [
1236
- Math.round(f.fraction * 100),
1237
- "% of ",
1238
- formatBytes(f.size)
1321
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-content", children: [
1322
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1323
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: strings.uploadingFile(Math.round(f.fraction * 100), formatBytes(f.size)) })
1239
1324
  ] }),
1240
- /* @__PURE__ */ jsx4("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
1241
- /* @__PURE__ */ jsx4(
1325
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-actions", children: /* @__PURE__ */ jsx4(
1242
1326
  "button",
1243
1327
  {
1244
1328
  type: "button",
1245
1329
  className: "fillo-file-remove",
1246
- "aria-label": `Cancel ${f.name}`,
1330
+ "aria-label": `${strings.uploadCancel} ${f.name}`,
1247
1331
  onClick: () => cancelUpload(f.key),
1248
- children: "\xD7"
1332
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
1333
+ }
1334
+ ) }),
1335
+ /* @__PURE__ */ jsx4(
1336
+ "span",
1337
+ {
1338
+ className: "fillo-progress",
1339
+ role: "progressbar",
1340
+ "aria-label": `Uploading ${f.name}`,
1341
+ "aria-valuemin": 0,
1342
+ "aria-valuemax": 100,
1343
+ "aria-valuenow": Math.round(f.fraction * 100),
1344
+ children: /* @__PURE__ */ jsx4(
1345
+ "span",
1346
+ {
1347
+ className: "fillo-progress-bar",
1348
+ style: { width: `${f.fraction * 100}%` }
1349
+ }
1350
+ )
1249
1351
  }
1250
1352
  )
1251
1353
  ] })
1252
- ]
1253
- },
1254
- f.key
1255
- ))
1256
- ] }),
1257
- error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1258
- ] });
1354
+ ] }, f.key))
1355
+ ] }),
1356
+ error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1357
+ ]
1358
+ }
1359
+ );
1259
1360
  }
1260
1361
 
1261
1362
  // src/fields.tsx
1262
1363
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1263
- function fieldAria(field, error, ids) {
1364
+ function fieldAria(field, error, ids, opts = {}) {
1264
1365
  const describedBy = [
1265
1366
  field.description ? ids.descriptionId : null,
1266
1367
  error ? ids.errorId : null
1267
1368
  ].filter(Boolean);
1369
+ const requiredSupported = opts.required ?? true;
1268
1370
  return {
1269
1371
  "aria-invalid": error ? true : void 0,
1270
- "aria-required": field.required ? true : void 0,
1372
+ "aria-required": requiredSupported && field.required ? true : void 0,
1271
1373
  "aria-describedby": describedBy.length ? describedBy.join(" ") : void 0
1272
1374
  };
1273
1375
  }
@@ -1360,7 +1462,8 @@ function NumberInput({ field, value, error, setValue, api, ids: providedIds }) {
1360
1462
  inputRef.current?.setSelectionRange(pos, pos);
1361
1463
  });
1362
1464
  const num = field;
1363
- const grouped = num.notation === "grouped";
1465
+ const grouped = num.notation !== void 0;
1466
+ const locale = localeForNotation(num.notation);
1364
1467
  const hasAffix = Boolean(num.prefix) || Boolean(num.suffix);
1365
1468
  if (!grouped && !hasAffix) {
1366
1469
  return /* @__PURE__ */ jsx5(
@@ -1377,7 +1480,7 @@ function NumberInput({ field, value, error, setValue, api, ids: providedIds }) {
1377
1480
  }
1378
1481
  const stored = typeof value === "string" || typeof value === "number" ? String(value) : "";
1379
1482
  const n = Number(stored);
1380
- const display = draft !== null ? draft : grouped && stored !== "" && Number.isFinite(n) ? formatGroupedNumber(n, { decimals: num.decimals }) : stored;
1483
+ const display = draft !== null ? draft : grouped && stored !== "" && Number.isFinite(n) ? formatGroupedNumber(n, { locale, decimals: num.decimals }) : stored;
1381
1484
  const input = /* @__PURE__ */ jsx5(
1382
1485
  "input",
1383
1486
  {
@@ -1390,14 +1493,14 @@ function NumberInput({ field, value, error, setValue, api, ids: providedIds }) {
1390
1493
  placeholder: field.placeholder,
1391
1494
  onBlur: grouped ? () => setDraft(null) : void 0,
1392
1495
  onChange: (e) => {
1393
- if (!isValidPartialNumberText(e.target.value)) {
1496
+ if (!isValidPartialNumberText(e.target.value, locale)) {
1394
1497
  pendingCaret.current = e.target.selectionStart ?? e.target.value.length;
1395
1498
  setRevertTick((t) => t + 1);
1396
1499
  return;
1397
1500
  }
1398
1501
  if (grouped) {
1399
1502
  setDraft(e.target.value);
1400
- setValue(parseGroupedNumber(e.target.value));
1503
+ setValue(parseGroupedNumber(e.target.value, locale));
1401
1504
  } else {
1402
1505
  setValue(e.target.value);
1403
1506
  }
@@ -1590,7 +1693,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1590
1693
  "data-fillo": "options",
1591
1694
  role: "group",
1592
1695
  "aria-labelledby": ids.labelId,
1593
- ...fieldAria(field, error, ids),
1696
+ ...fieldAria(field, error, ids, { required: false }),
1594
1697
  children: [
1595
1698
  options.map((opt) => /* @__PURE__ */ jsxs5(
1596
1699
  "label",
@@ -1967,7 +2070,7 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
1967
2070
  id: ids.inputId,
1968
2071
  role: "group",
1969
2072
  "aria-labelledby": ids.labelId,
1970
- ...fieldAria(field, error, ids),
2073
+ ...fieldAria(field, error, ids, { required: false }),
1971
2074
  children: /* @__PURE__ */ jsx5("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1972
2075
  const option = ranking.options.find((o) => o.id === id);
1973
2076
  if (!option) return null;
@@ -2014,10 +2117,10 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
2014
2117
  className: "fillo-matrix-wrap",
2015
2118
  role: "group",
2016
2119
  "aria-labelledby": ids.labelId,
2017
- ...fieldAria(field, error, ids),
2120
+ ...fieldAria(field, error, ids, { required: false }),
2018
2121
  children: /* @__PURE__ */ jsxs5("table", { className: "fillo-matrix", children: [
2019
2122
  /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
2020
- /* @__PURE__ */ jsx5("th", {}),
2123
+ /* @__PURE__ */ jsx5("td", {}),
2021
2124
  matrix.columns.map((col) => /* @__PURE__ */ jsx5("th", { scope: "col", children: col.label }, col.id))
2022
2125
  ] }) }),
2023
2126
  /* @__PURE__ */ jsx5("tbody", { children: matrix.rows.map((row) => {
@@ -2182,7 +2285,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
2182
2285
  Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
2183
2286
  }
2184
2287
  const rawError = api.errors[block.id];
2185
- const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
2288
+ const error = rawError === REQUIRED_FIELD_MESSAGE ? requiredFieldMessage(block, strings) : rawError;
2186
2289
  return /* @__PURE__ */ jsx5(
2187
2290
  Component,
2188
2291
  {
@@ -2382,16 +2485,13 @@ function nextFrame(run) {
2382
2485
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
2383
2486
  else setTimeout(run, 0);
2384
2487
  }
2385
- function focusErrorSummary(formEl) {
2488
+ function focusFirstInvalid(formEl) {
2386
2489
  if (!formEl || typeof window === "undefined") return;
2387
2490
  nextFrame(() => {
2388
- formEl.querySelector(".fillo-error-summary")?.focus();
2491
+ const invalid = formEl.querySelector('[aria-invalid="true"]');
2492
+ resolveFocusable(invalid)?.focus();
2389
2493
  });
2390
2494
  }
2391
- function focusField(inputId) {
2392
- if (typeof document === "undefined") return;
2393
- resolveFocusable(document.getElementById(inputId))?.focus();
2394
- }
2395
2495
  var HP_STYLE = {
2396
2496
  position: "absolute",
2397
2497
  left: "-9999px",
@@ -2410,12 +2510,15 @@ function assertHpHidden(el) {
2410
2510
  }
2411
2511
  function FilloForm(props) {
2412
2512
  const { form, formId } = props;
2513
+ const renderOnly = props.renderOnly === true || props.skipValidation === true;
2413
2514
  const client = useMemo5(
2414
- () => props.client ?? (formId ? createClient() : void 0),
2415
- [props.client, formId]
2515
+ () => renderOnly ? void 0 : props.client ?? (formId ? createClient() : void 0),
2516
+ [renderOnly, props.client, formId]
2416
2517
  );
2417
2518
  const [loaded, setLoaded] = useState6(null);
2418
- const [loadFailure, setLoadFailure] = useState6(null);
2519
+ const [loadFailure, setLoadFailure] = useState6(
2520
+ null
2521
+ );
2419
2522
  const devEnv = useIsDevEnv();
2420
2523
  const devChrome = props.preview === true || devEnv;
2421
2524
  if (props.preview === true) warnPreviewInProduction();
@@ -2449,6 +2552,18 @@ function FilloForm(props) {
2449
2552
  useEffect8(() => {
2450
2553
  onErrorRef.current = onError;
2451
2554
  });
2555
+ const targetError = useMemo5(
2556
+ () => form && !formId && !renderOnly && (!isCodeForm2(form) || !props.client) ? new FilloError2(
2557
+ "This form has no Fillo target. Pass formId with the schema, use a defineForm() value with client, or set renderOnly for a non-submitting preview.",
2558
+ 422,
2559
+ void 0,
2560
+ "form_target_required"
2561
+ ) : null,
2562
+ [form, formId, props.client, renderOnly]
2563
+ );
2564
+ useEffect8(() => {
2565
+ if (targetError) onErrorRef.current?.(targetError);
2566
+ }, [targetError]);
2452
2567
  const needsFetch = !inlineSchema && Boolean(client && formId);
2453
2568
  useEffect8(() => {
2454
2569
  if (!needsFetch || !client || !formId) {
@@ -2487,10 +2602,28 @@ function FilloForm(props) {
2487
2602
  const currentLoaded = needsFetch && loaded?.formId === formId ? loaded : null;
2488
2603
  const loadError = needsFetch && loadFailure && loadFailure.formId === formId ? loadFailure.error : null;
2489
2604
  const rawSchema = inlineSchema ?? currentLoaded?.schema ?? null;
2490
- const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
2605
+ const normalized = useMemo5(
2606
+ () => rawSchema ? normalizeFormSchema(rawSchema) : null,
2607
+ [rawSchema]
2608
+ );
2491
2609
  const schema = normalized?.ok ? normalized.schema : null;
2492
2610
  const resolvedCodeTheme = renderResolvedSnapshot ? resolvedTheme ?? void 0 : codeForm?.theme;
2493
2611
  const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
2612
+ if (targetError) {
2613
+ if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(targetError) });
2614
+ return /* @__PURE__ */ jsx7(
2615
+ "div",
2616
+ {
2617
+ className: `fillo-form fillo-form--error ${props.className ?? ""}`,
2618
+ role: "alert",
2619
+ "data-fillo": "root",
2620
+ "data-state": "error",
2621
+ "data-error-code": "form_target_required",
2622
+ ...themeProps(theme),
2623
+ children: devChrome ? `${targetError.message} (${targetError.code})` : strings.loadFailed
2624
+ }
2625
+ );
2626
+ }
2494
2627
  if (normalized && !normalized.ok) {
2495
2628
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
2496
2629
  if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
@@ -2531,7 +2664,14 @@ function FilloForm(props) {
2531
2664
  );
2532
2665
  }
2533
2666
  if (currentLoaded?.closed) {
2534
- return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: strings.closed }) });
2667
+ return /* @__PURE__ */ jsx7(
2668
+ "div",
2669
+ {
2670
+ className: `fillo-form fillo-form--closed ${props.className ?? ""}`,
2671
+ ...themeProps(theme),
2672
+ children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: strings.closed })
2673
+ }
2674
+ );
2535
2675
  }
2536
2676
  const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
2537
2677
  if (canonicalSyncError && props.renderError) {
@@ -2573,7 +2713,14 @@ function FilloForm(props) {
2573
2713
  }
2574
2714
  );
2575
2715
  }
2576
- return /* @__PURE__ */ jsx7("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: variant === "closed" ? strings.closed : strings.notLive }) });
2716
+ return /* @__PURE__ */ jsx7(
2717
+ "div",
2718
+ {
2719
+ className: `fillo-form fillo-form--closed ${props.className ?? ""}`,
2720
+ ...themeProps(theme),
2721
+ children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: variant === "closed" ? strings.closed : strings.notLive })
2722
+ }
2723
+ );
2577
2724
  }
2578
2725
  const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
2579
2726
  if (!devChrome && awaitingCanonical || !schema) {
@@ -2598,10 +2745,12 @@ function FilloForm(props) {
2598
2745
  ResolvedForm,
2599
2746
  {
2600
2747
  ...props,
2601
- client,
2748
+ client: renderOnly ? void 0 : client,
2602
2749
  schema,
2603
2750
  theme,
2604
- formId: syncedFormId ?? formId,
2751
+ formId: renderOnly ? void 0 : syncedFormId ?? formId,
2752
+ renderOnly,
2753
+ skipValidation: renderOnly ? true : props.skipValidation,
2605
2754
  poweredBy,
2606
2755
  brandLabel: branding?.label,
2607
2756
  brandHref: branding?.href,
@@ -2672,13 +2821,14 @@ function useIsDevEnv() {
2672
2821
  function ResolvedForm(props) {
2673
2822
  const { schema, theme } = props;
2674
2823
  const displayOnly = props.displayOnly === true;
2824
+ const renderOnly = props.renderOnly === true;
2675
2825
  const instanceId = useId2().replace(/:/g, "");
2676
2826
  const devEnv = useIsDevEnv();
2677
2827
  const devChrome = !displayOnly && (props.preview === true || devEnv);
2678
2828
  const hpRef = useRef8(null);
2679
2829
  const codeForm = isCodeForm2(props.form) ? props.form : null;
2680
2830
  const client = props.client;
2681
- const challenge = props.challenge;
2831
+ const challenge = renderOnly ? void 0 : props.challenge;
2682
2832
  const challengeRequired = Boolean(challenge);
2683
2833
  const challengeTokenRef = useRef8(void 0);
2684
2834
  const [resyncWarning, setResyncWarning] = useState6(null);
@@ -2790,7 +2940,7 @@ function ResolvedForm(props) {
2790
2940
  resetChallengeRef.current?.();
2791
2941
  }
2792
2942
  });
2793
- useDraftFlush(api, !displayOnly);
2943
+ useDraftFlush(api, !displayOnly && !renderOnly);
2794
2944
  const strings = resolveStrings(props.strings);
2795
2945
  const announceRef = useRef8(null);
2796
2946
  const announce = useCallback((text) => {
@@ -2821,12 +2971,13 @@ function ResolvedForm(props) {
2821
2971
  const chromeValue = useMemo5(
2822
2972
  () => ({
2823
2973
  devChrome,
2974
+ renderOnly,
2824
2975
  uploadsAvailable,
2825
2976
  uploadFileSizeLimitMb,
2826
2977
  warningCode,
2827
2978
  warningUrl
2828
2979
  }),
2829
- [devChrome, uploadsAvailable, uploadFileSizeLimitMb, warningCode, warningUrl]
2980
+ [devChrome, renderOnly, uploadsAvailable, uploadFileSizeLimitMb, warningCode, warningUrl]
2830
2981
  );
2831
2982
  const settings = schema.settings;
2832
2983
  const submitted = api.status === "submitted";
@@ -2867,13 +3018,17 @@ function ResolvedForm(props) {
2867
3018
  { slot: "root", status: "submitted" }
2868
3019
  ),
2869
3020
  "data-fillo": "root",
3021
+ "data-fillo-form-id": props.formId,
2870
3022
  "data-state": "submitted",
2871
3023
  ...themed,
2872
3024
  children: [
2873
3025
  props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs6(
2874
3026
  "div",
2875
3027
  {
2876
- className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
3028
+ className: slotClass3("fillo-success", appearance, {
3029
+ slot: "success",
3030
+ status: "submitted"
3031
+ }),
2877
3032
  "data-fillo": "success",
2878
3033
  children: [
2879
3034
  /* @__PURE__ */ jsx7("div", { className: "fillo-success-mark", "aria-hidden": "true" }),
@@ -2899,17 +3054,6 @@ function ResolvedForm(props) {
2899
3054
  const progressPos = progressSeq.indexOf(api.page.id);
2900
3055
  const progressStep = progressPos >= 0 ? progressPos + 1 : Math.min(api.pageIndex + 1, progressTotal);
2901
3056
  const submitFixUrl = devChrome && resolutionFailureCode === "form_not_published" && warningCode === "storage_required" ? safeHttpUrl(warningUrl) : null;
2902
- const invalidFields = [];
2903
- for (const block of api.blocks) {
2904
- if (!isField2(block)) continue;
2905
- const raw = api.errors[block.id];
2906
- if (!raw) continue;
2907
- invalidFields.push({
2908
- field: block,
2909
- error: raw === REQUIRED_FIELD_MESSAGE2 ? strings.required : raw,
2910
- inputId: createFilloFieldIds(instanceId, block.id).inputId
2911
- });
2912
- }
2913
3057
  const rootClass = slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
2914
3058
  slot: "root",
2915
3059
  status: api.status
@@ -2934,7 +3078,7 @@ function ResolvedForm(props) {
2934
3078
  staged: props.stagedNotice,
2935
3079
  draft: props.draftNotice,
2936
3080
  warningUrl: props.syncWarningUrl,
2937
- noClient: !props.client && !props.skipValidation
3081
+ noClient: !props.client && !props.skipValidation && !renderOnly
2938
3082
  }
2939
3083
  ),
2940
3084
  resuming && /* @__PURE__ */ jsxs6(
@@ -2951,7 +3095,9 @@ function ResolvedForm(props) {
2951
3095
  api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx7(
2952
3096
  "div",
2953
3097
  {
2954
- className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
3098
+ className: slotClass3("fillo-resume fillo-resume--expired", appearance, {
3099
+ slot: "resume"
3100
+ }),
2955
3101
  "data-fillo": "resume",
2956
3102
  role: "status",
2957
3103
  children: /* @__PURE__ */ jsx7("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
@@ -2972,51 +3118,65 @@ function ResolvedForm(props) {
2972
3118
  {
2973
3119
  className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
2974
3120
  "data-fillo": "progressFill",
2975
- style: { "--fillo-progress-value": `${progressStep / progressTotal * 100}%` }
3121
+ style: {
3122
+ "--fillo-progress-value": `${progressStep / progressTotal * 100}%`
3123
+ }
2976
3124
  }
2977
3125
  )
2978
3126
  }
2979
3127
  ),
2980
- api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs6("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
2981
- schema.title && /* @__PURE__ */ jsx7("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
2982
- schema.description && /* @__PURE__ */ jsx7(
2983
- "p",
2984
- {
2985
- className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
2986
- "data-fillo": "description",
2987
- children: schema.description
2988
- }
2989
- )
2990
- ] }),
2991
- multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx7("h2", { className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }), "data-fillo": "pageTitle", children: api.page.title }),
2992
- invalidFields.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "fillo-error-summary", role: "alert", tabIndex: -1, "data-fillo": "error-summary", children: [
2993
- /* @__PURE__ */ jsx7("p", { className: "fillo-error-summary-title", children: strings.errorSummaryTitle }),
2994
- /* @__PURE__ */ jsx7("ul", { children: invalidFields.map(({ field, error, inputId }) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsxs6(
2995
- "a",
2996
- {
2997
- href: `#${inputId}`,
2998
- onClick: (e) => {
2999
- e.preventDefault();
3000
- focusField(inputId);
3001
- },
3002
- children: [
3003
- field.label,
3004
- ": ",
3005
- error
3006
- ]
3007
- }
3008
- ) }, field.id)) })
3009
- ] }),
3010
- /* @__PURE__ */ jsx7("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx7(
3011
- BlockRenderer,
3128
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs6(
3129
+ "header",
3012
3130
  {
3013
- block,
3014
- api,
3015
- components: props.components,
3016
- customComponents: props.customComponents
3017
- },
3018
- block.id
3019
- )) }),
3131
+ className: slotClass3("fillo-header", appearance, { slot: "header" }),
3132
+ "data-fillo": "header",
3133
+ children: [
3134
+ schema.title && /* @__PURE__ */ jsx7(
3135
+ "h1",
3136
+ {
3137
+ className: slotClass3("fillo-title", appearance, { slot: "title" }),
3138
+ "data-fillo": "title",
3139
+ children: schema.title
3140
+ }
3141
+ ),
3142
+ schema.description && /* @__PURE__ */ jsx7(
3143
+ "p",
3144
+ {
3145
+ className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
3146
+ "data-fillo": "description",
3147
+ children: schema.description
3148
+ }
3149
+ )
3150
+ ]
3151
+ }
3152
+ ),
3153
+ multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx7(
3154
+ "h2",
3155
+ {
3156
+ className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }),
3157
+ "data-fillo": "pageTitle",
3158
+ children: api.page.title
3159
+ }
3160
+ ),
3161
+ /* @__PURE__ */ jsx7(
3162
+ "div",
3163
+ {
3164
+ className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }),
3165
+ "data-fillo": "blocks",
3166
+ ref: blocksRef,
3167
+ tabIndex: -1,
3168
+ children: api.blocks.map((block) => /* @__PURE__ */ jsx7(
3169
+ BlockRenderer,
3170
+ {
3171
+ block,
3172
+ api,
3173
+ components: props.components,
3174
+ customComponents: props.customComponents
3175
+ },
3176
+ block.id
3177
+ ))
3178
+ }
3179
+ ),
3020
3180
  !displayOnly && /* @__PURE__ */ jsx7(
3021
3181
  "input",
3022
3182
  {
@@ -3056,43 +3216,66 @@ function ResolvedForm(props) {
3056
3216
  /* @__PURE__ */ jsx7("a", { href: submitFixUrl, target: "_blank", rel: "noopener noreferrer", children: submitFixUrl })
3057
3217
  ] })
3058
3218
  ] }),
3059
- showFooter && /* @__PURE__ */ jsxs6("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
3060
- multiPage && !api.isFirstPage && /* @__PURE__ */ jsx7(
3061
- "button",
3062
- {
3063
- type: "button",
3064
- className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
3065
- "data-fillo": "button",
3066
- onClick: api.back,
3067
- children: strings.back
3068
- }
3069
- ),
3070
- showPrimaryButton && /* @__PURE__ */ jsx7(
3071
- "button",
3072
- {
3073
- type: "submit",
3074
- className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
3075
- "data-fillo": "button",
3076
- disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
3077
- // "Next"). The server rejects a tokenless submit regardless —
3078
- // this is UX only.
3079
- showChallenge && !challengeToken,
3080
- children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
3081
- }
3082
- )
3083
- ] }),
3219
+ showFooter && /* @__PURE__ */ jsxs6(
3220
+ "footer",
3221
+ {
3222
+ className: slotClass3("fillo-footer", appearance, { slot: "footer" }),
3223
+ "data-fillo": "footer",
3224
+ children: [
3225
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx7(
3226
+ "button",
3227
+ {
3228
+ type: "button",
3229
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, {
3230
+ slot: "button",
3231
+ variant: "ghost"
3232
+ }),
3233
+ "data-fillo": "button",
3234
+ onClick: api.back,
3235
+ children: strings.back
3236
+ }
3237
+ ),
3238
+ showPrimaryButton && /* @__PURE__ */ jsx7(
3239
+ "button",
3240
+ {
3241
+ type: "submit",
3242
+ className: slotClass3("fillo-button fillo-button--primary", appearance, {
3243
+ slot: "button",
3244
+ variant: "primary"
3245
+ }),
3246
+ "data-fillo": "button",
3247
+ disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
3248
+ // "Next"). The server rejects a tokenless submit regardless —
3249
+ // this is UX only.
3250
+ showChallenge && !challengeToken,
3251
+ children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
3252
+ }
3253
+ )
3254
+ ]
3255
+ }
3256
+ ),
3084
3257
  props.poweredBy && /* @__PURE__ */ jsx7(PoweredBy, { label: props.brandLabel, href: props.brandHref })
3085
3258
  ] });
3086
3259
  return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx7(FilloAnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children: displayOnly ? (
3087
3260
  // Display-only preview: a plain div (no form element to submit) with
3088
3261
  // every control inside a natively disabled fieldset — unfocusable
3089
3262
  // and unfillable even without the stylesheet or inert support.
3090
- /* @__PURE__ */ jsx7("div", { className: rootClass, "data-fillo": "preview-form", "data-state": "preview", ...themed, children: /* @__PURE__ */ jsx7("fieldset", { disabled: true, className: "fillo-not-open-fields", children: formBody }) })
3263
+ /* @__PURE__ */ jsx7(
3264
+ "div",
3265
+ {
3266
+ className: rootClass,
3267
+ "data-fillo": "preview-form",
3268
+ "data-state": "preview",
3269
+ ...themed,
3270
+ children: /* @__PURE__ */ jsx7("fieldset", { disabled: true, className: "fillo-not-open-fields", children: formBody })
3271
+ }
3272
+ )
3091
3273
  ) : /* @__PURE__ */ jsx7(
3092
3274
  "form",
3093
3275
  {
3094
3276
  className: rootClass,
3095
3277
  "data-fillo": "root",
3278
+ "data-fillo-form-id": props.formId,
3096
3279
  "data-state": api.status,
3097
3280
  "data-page": api.pageIndex + 1,
3098
3281
  "data-last-page": api.isLastPage ? "" : void 0,
@@ -3102,11 +3285,11 @@ function ResolvedForm(props) {
3102
3285
  e.preventDefault();
3103
3286
  const formEl = e.currentTarget;
3104
3287
  if (api.isLastPage) {
3105
- void api.submit().then(() => focusErrorSummary(formEl)).catch(() => {
3288
+ void api.submit().then(() => focusFirstInvalid(formEl)).catch(() => {
3106
3289
  });
3107
3290
  } else {
3108
3291
  api.next();
3109
- focusErrorSummary(formEl);
3292
+ focusFirstInvalid(formEl);
3110
3293
  }
3111
3294
  },
3112
3295
  children: formBody
@@ -3138,7 +3321,7 @@ function JsxForm(props) {
3138
3321
  if (!memo2.current || memo2.current.json !== json) {
3139
3322
  memo2.current = { json, form: compiled };
3140
3323
  }
3141
- return /* @__PURE__ */ jsx8(FilloForm, { form: memo2.current.form, client, ...rest });
3324
+ return /* @__PURE__ */ jsx8(FilloForm, { ...{ form: memo2.current.form, client, ...rest } });
3142
3325
  }
3143
3326
  function defineFormFromJsx(element) {
3144
3327
  if (!element || element.type !== JsxForm) {
@@ -3183,9 +3366,10 @@ var Fillo = {
3183
3366
  };
3184
3367
 
3185
3368
  // src/provider.tsx
3186
- import { useId as useId3, useMemo as useMemo6, useState as useState7 } from "react";
3369
+ import { useEffect as useEffect9, useId as useId3, useMemo as useMemo6, useState as useState7 } from "react";
3187
3370
  import {
3188
3371
  FilloError as FilloError3,
3372
+ createClient as createClient2,
3189
3373
  formSchemasEqual as formSchemasEqual2,
3190
3374
  isFilloError as isFilloError3,
3191
3375
  normalizeFormSchema as normalizeFormSchema2,
@@ -3203,13 +3387,33 @@ function FilloProvider({
3203
3387
  onError,
3204
3388
  renderError,
3205
3389
  preview,
3390
+ renderOnly,
3391
+ skipValidation,
3206
3392
  ...options
3207
3393
  }) {
3394
+ const localPreview = renderOnly === true || skipValidation === true;
3395
+ const activeClient = useMemo6(
3396
+ () => localPreview ? void 0 : client ?? (formId ? createClient2() : void 0),
3397
+ [client, formId, localPreview]
3398
+ );
3399
+ const activeFormId = localPreview ? void 0 : formId;
3208
3400
  const instanceId = useId3().replace(/:/g, "");
3209
3401
  const devEnv = useIsDevEnv();
3210
3402
  const devChrome = preview === true || devEnv;
3211
3403
  if (preview === true) warnPreviewInProduction();
3212
3404
  const codeForm = isCodeForm2(form) ? form : null;
3405
+ const targetError = useMemo6(
3406
+ () => !formId && !localPreview && (!codeForm || !client) ? new FilloError3(
3407
+ "This form has no Fillo target. Pass formId with the schema, use a defineForm() value with client, or set renderOnly for a non-submitting preview.",
3408
+ 422,
3409
+ void 0,
3410
+ "form_target_required"
3411
+ ) : null,
3412
+ [client, codeForm, formId, localPreview]
3413
+ );
3414
+ useEffect9(() => {
3415
+ if (targetError) onError?.(targetError);
3416
+ }, [onError, targetError]);
3213
3417
  const {
3214
3418
  formId: syncedFormId,
3215
3419
  status: syncedStatus,
@@ -3222,8 +3426,8 @@ function FilloProvider({
3222
3426
  uploadFileSizeLimitMb,
3223
3427
  warningCode,
3224
3428
  warningUrl
3225
- } = useCodeFormSync(form, client, onError, {
3226
- allowUnsynced: Boolean(!client || formId || options.skipValidation)
3429
+ } = useCodeFormSync(form, activeClient, onError, {
3430
+ allowUnsynced: Boolean(!activeClient || activeFormId || localPreview)
3227
3431
  });
3228
3432
  const [resyncWarning, setResyncWarning] = useState7(null);
3229
3433
  const rawSchema = codeForm ? resolvedSchema && !devChrome ? resolvedSchema : codeForm.schema : form;
@@ -3233,9 +3437,9 @@ function FilloProvider({
3233
3437
  }
3234
3438
  const schema = normalized.schema;
3235
3439
  const resolveFormId = useMemo6(
3236
- () => codeForm && client?.key ? async () => {
3440
+ () => codeForm && activeClient?.key ? async () => {
3237
3441
  try {
3238
- const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
3442
+ const result = await syncCodeForm3(activeClient, codeForm, { bypassCache: true });
3239
3443
  setResyncWarning({
3240
3444
  code: result.warningCode,
3241
3445
  url: result.warningUrl,
@@ -3297,13 +3501,14 @@ function FilloProvider({
3297
3501
  throw failure;
3298
3502
  }
3299
3503
  } : void 0,
3300
- [client, codeForm, onError, schema]
3504
+ [activeClient, codeForm, onError, schema]
3301
3505
  );
3302
3506
  const api = useFilloController({
3303
3507
  ...options,
3304
- client,
3508
+ client: activeClient,
3305
3509
  form: schema,
3306
- formId: syncedFormId ?? formId,
3510
+ formId: syncedFormId ?? activeFormId ?? void 0,
3511
+ skipValidation: localPreview,
3307
3512
  resolveFormId,
3308
3513
  // Dev chrome (preview or dev env) surfaces the developer-grade resolution
3309
3514
  // failure (message + code); an explicit option wins.
@@ -3312,7 +3517,7 @@ function FilloProvider({
3312
3517
  // method; recorded per response so usage stays measurable.
3313
3518
  surface: "headless"
3314
3519
  });
3315
- useDraftFlush(api);
3520
+ useDraftFlush(api, !localPreview);
3316
3521
  const freshWarningCode = resyncWarning ? resyncWarning.code : warningCode;
3317
3522
  const freshWarningUrl = resyncWarning ? resyncWarning.url : warningUrl;
3318
3523
  const freshUploadsAvailable = resyncWarning ? resyncWarning.uploadsAvailable : uploadsAvailable;
@@ -3320,20 +3525,32 @@ function FilloProvider({
3320
3525
  const chromeValue = useMemo6(
3321
3526
  () => ({
3322
3527
  devChrome,
3528
+ renderOnly: localPreview,
3323
3529
  uploadsAvailable: freshUploadsAvailable,
3324
3530
  uploadFileSizeLimitMb: freshUploadFileSizeLimitMb,
3325
3531
  warningCode: freshWarningCode,
3326
3532
  warningUrl: freshWarningUrl
3327
3533
  }),
3328
- [devChrome, freshUploadsAvailable, freshUploadFileSizeLimitMb, freshWarningCode, freshWarningUrl]
3534
+ [
3535
+ devChrome,
3536
+ localPreview,
3537
+ freshUploadsAvailable,
3538
+ freshUploadFileSizeLimitMb,
3539
+ freshWarningCode,
3540
+ freshWarningUrl
3541
+ ]
3329
3542
  );
3330
3543
  const resolvedStrings = resolveStrings2(strings);
3331
- const canonicalSyncError = syncError && !options.skipValidation ? syncError : null;
3544
+ if (targetError) {
3545
+ if (renderError) return /* @__PURE__ */ jsx9(Fragment4, { children: renderError(targetError) });
3546
+ throw targetError;
3547
+ }
3548
+ const canonicalSyncError = syncError && !localPreview ? syncError : null;
3332
3549
  if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx9(Fragment4, { children: renderError(canonicalSyncError) });
3333
3550
  if (canonicalSyncError && !devChrome) return null;
3334
- if (codeFormSyncing && !options.skipValidation && !devChrome) return null;
3335
- const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !options.skipValidation;
3336
- const notAccepting = Boolean(codeForm) && syncedStatus === "published" && syncedAccepting === false && !options.skipValidation;
3551
+ if (codeFormSyncing && !localPreview && !devChrome) return null;
3552
+ const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !localPreview;
3553
+ const notAccepting = Boolean(codeForm) && syncedStatus === "published" && syncedAccepting === false && !localPreview;
3337
3554
  if ((notPublished || notAccepting) && !devChrome) {
3338
3555
  const error = notPublished ? new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published") : new FilloError3(
3339
3556
  "This form is no longer accepting responses.",
@@ -3348,7 +3565,7 @@ function FilloProvider({
3348
3565
 
3349
3566
  // src/index.ts
3350
3567
  import {
3351
- createClient as createClient2,
3568
+ createClient as createClient3,
3352
3569
  provisionWorkspace,
3353
3570
  when,
3354
3571
  FilloJsxError as FilloJsxError2,
@@ -3375,7 +3592,7 @@ export {
3375
3592
  FilloJsxError2 as FilloJsxError,
3376
3593
  FilloProvider,
3377
3594
  FormField,
3378
- createClient2 as createClient,
3595
+ createClient3 as createClient,
3379
3596
  defineForm,
3380
3597
  provisionWorkspace,
3381
3598
  useField,