@usefillo/react 0.11.0 → 0.12.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
@@ -483,6 +483,7 @@ import {
483
483
  formatGroupedNumber,
484
484
  isField,
485
485
  isValidPartialNumberText,
486
+ localeForNotation,
486
487
  parseGroupedNumber,
487
488
  pipeBlock,
488
489
  radioGroupStep,
@@ -1007,7 +1008,14 @@ 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 FileUploadField({
1012
+ field,
1013
+ value,
1014
+ error,
1015
+ setValue,
1016
+ api,
1017
+ ids: providedIds
1018
+ }) {
1011
1019
  const ids = useFilloFieldIds(field.id, providedIds);
1012
1020
  const slots = useFieldSlots(field, error);
1013
1021
  const strings = useStrings();
@@ -1036,9 +1044,10 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1036
1044
  const configuredMaxMb = schema.maxFileSizeMb ?? 500;
1037
1045
  const maxMb = chrome?.uploadFileSizeLimitMb ? Math.min(configuredMaxMb, chrome.uploadFileSizeLimitMb) : configuredMaxMb;
1038
1046
  const maxBytes = maxMb * 1024 * 1024;
1047
+ const renderOnly = chrome?.renderOnly === true;
1039
1048
  const storageBlocked = chrome?.uploadsAvailable === false;
1040
1049
  const storageUrl = storageBlocked && chrome?.devChrome ? safeHttpUrl(chrome.warningUrl) : null;
1041
- const canUpload = Boolean(api.client && api.formId) && !storageBlocked;
1050
+ const canUpload = !renderOnly && Boolean(api.client && api.formId) && !storageBlocked;
1042
1051
  const activeInFlight = inFlight.filter((f) => !f.error).length;
1043
1052
  const remaining = maxFiles - files.length - activeInFlight;
1044
1053
  async function startUpload(file) {
@@ -1047,7 +1056,15 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1047
1056
  if (file.size > maxBytes) {
1048
1057
  setInFlight((prev) => [
1049
1058
  ...prev,
1050
- { key, name: file.name, size: file.size, fraction: 0, file, tooLarge: true, error: strings.fileTooLarge(maxMb) }
1059
+ {
1060
+ key,
1061
+ name: file.name,
1062
+ size: file.size,
1063
+ fraction: 0,
1064
+ file,
1065
+ tooLarge: true,
1066
+ error: strings.fileTooLarge(maxMb)
1067
+ }
1051
1068
  ]);
1052
1069
  return;
1053
1070
  }
@@ -1074,9 +1091,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1074
1091
  } catch (err) {
1075
1092
  if (mountedRef.current) {
1076
1093
  const message = uploadFailureMessage(err, strings);
1077
- setInFlight(
1078
- (prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
1079
- );
1094
+ setInFlight((prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f));
1080
1095
  }
1081
1096
  } finally {
1082
1097
  controllers.current.delete(key);
@@ -1100,174 +1115,190 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
1100
1115
  setValue(next);
1101
1116
  }
1102
1117
  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();
1118
+ return /* @__PURE__ */ jsxs4(
1119
+ "div",
1120
+ {
1121
+ ...slots.wrapperProps(
1122
+ `fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`
1123
+ ),
1124
+ children: [
1125
+ /* @__PURE__ */ jsxs4("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
1126
+ field.label,
1127
+ !field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
1128
+ ] }),
1129
+ field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
1130
+ remaining > 0 && /* @__PURE__ */ jsxs4(
1131
+ "div",
1132
+ {
1133
+ "data-fillo": "control",
1134
+ "data-drag-over": dragOver ? "" : void 0,
1135
+ className: slots.control(
1136
+ `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`
1137
+ ),
1138
+ onDragOver: (e) => {
1139
+ e.preventDefault();
1140
+ setDragOver(true);
1141
+ },
1142
+ onDragLeave: () => setDragOver(false),
1143
+ onDrop: (e) => {
1144
+ e.preventDefault();
1145
+ setDragOver(false);
1146
+ if (canUpload) handleFiles(e.dataTransfer.files);
1147
+ },
1148
+ onClick: () => canUpload && inputRef.current?.click(),
1149
+ role: "button",
1150
+ tabIndex: 0,
1151
+ "aria-disabled": !canUpload || void 0,
1152
+ "aria-labelledby": ids.labelId,
1153
+ "aria-invalid": error ? true : void 0,
1154
+ "aria-describedby": describedBy,
1155
+ onKeyDown: (e) => {
1156
+ if (e.key === "Enter" || e.key === " ") {
1157
+ e.preventDefault();
1158
+ if (canUpload) inputRef.current?.click();
1159
+ }
1160
+ },
1161
+ children: [
1162
+ /* @__PURE__ */ jsx4(
1163
+ "input",
1164
+ {
1165
+ ref: inputRef,
1166
+ id: ids.inputId,
1167
+ type: "file",
1168
+ hidden: true,
1169
+ disabled: !canUpload,
1170
+ multiple: maxFiles > 1,
1171
+ accept: schema.accept?.join(","),
1172
+ onChange: (e) => canUpload && handleFiles(e.target.files)
1173
+ }
1174
+ ),
1175
+ canUpload ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1176
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
1177
+ /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
1178
+ ] }) : storageBlocked && chrome?.devChrome ? /* @__PURE__ */ jsxs4("span", { className: "fillo-dropzone-hint", children: [
1179
+ "Connect file storage to enable uploads",
1180
+ storageUrl && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1181
+ " \u2014 ",
1182
+ /* @__PURE__ */ jsx4("a", { href: storageUrl, target: "_blank", rel: "noopener noreferrer", children: storageUrl })
1183
+ ] })
1184
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "fillo-dropzone-hint", children: renderOnly ? strings.uploadsRenderOnly : storageBlocked ? strings.uploadsUnavailable : strings.uploadsDisabled })
1185
+ ]
1137
1186
  }
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" }),
1187
+ ),
1181
1188
  /* @__PURE__ */ jsx4(
1182
- "button",
1189
+ "div",
1183
1190
  {
1184
- type: "button",
1185
- className: "fillo-file-remove",
1186
- "aria-label": `Remove ${f.name}`,
1187
- onClick: () => removeFile(f.fileId),
1188
- children: "\xD7"
1191
+ role: "status",
1192
+ "aria-live": "polite",
1193
+ className: "fillo-sr-only",
1194
+ style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
1195
+ children: uploadStatus(inFlight, files.length, strings)
1189
1196
  }
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: [
1197
+ ),
1198
+ (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs4("ul", { className: "fillo-files", children: [
1199
+ files.map((f) => /* @__PURE__ */ jsxs4("li", { className: "fillo-file fillo-file--done", children: [
1204
1200
  /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1205
- f.error ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1206
- /* @__PURE__ */ jsxs4("span", { className: "fillo-file-actions", children: [
1207
- f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx4(
1208
- "button",
1209
- {
1210
- type: "button",
1211
- className: "fillo-file-retry",
1212
- "aria-label": `${strings.uploadRetry} ${f.name}`,
1213
- onClick: () => {
1214
- const retryFile = f.file;
1215
- if (!retryFile) return;
1216
- setInFlight((prev) => prev.filter((x) => x.key !== f.key));
1217
- void startUpload(retryFile);
1218
- },
1219
- children: strings.uploadRetry
1220
- }
1221
- ),
1222
- /* @__PURE__ */ jsx4(
1223
- "button",
1224
- {
1225
- type: "button",
1226
- className: "fillo-file-remove",
1227
- "aria-label": "Dismiss",
1228
- onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
1229
- children: "\xD7"
1230
- }
1231
- )
1232
- ] }),
1233
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-error", role: "alert", children: f.error })
1234
- ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1235
- /* @__PURE__ */ jsxs4("span", { className: "fillo-file-meta", children: [
1236
- Math.round(f.fraction * 100),
1237
- "% of ",
1238
- formatBytes(f.size)
1239
- ] }),
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(
1242
- "button",
1243
- {
1244
- type: "button",
1245
- className: "fillo-file-remove",
1246
- "aria-label": `Cancel ${f.name}`,
1247
- onClick: () => cancelUpload(f.key),
1248
- children: "\xD7"
1249
- }
1250
- )
1251
- ] })
1252
- ]
1253
- },
1254
- f.key
1255
- ))
1256
- ] }),
1257
- error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1258
- ] });
1201
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
1202
+ /* @__PURE__ */ jsx4("span", { className: "fillo-sr-only", children: "Uploaded" }),
1203
+ /* @__PURE__ */ jsx4(
1204
+ "button",
1205
+ {
1206
+ type: "button",
1207
+ className: "fillo-file-remove",
1208
+ "aria-label": `Remove ${f.name}`,
1209
+ onClick: () => removeFile(f.fileId),
1210
+ children: "\xD7"
1211
+ }
1212
+ )
1213
+ ] }, f.fileId)),
1214
+ inFlight.map((f) => /* @__PURE__ */ jsxs4(
1215
+ "li",
1216
+ {
1217
+ className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
1218
+ ...f.error ? {} : {
1219
+ role: "progressbar",
1220
+ "aria-label": `Uploading ${f.name}`,
1221
+ "aria-valuemin": 0,
1222
+ "aria-valuemax": 100,
1223
+ "aria-valuenow": Math.round(f.fraction * 100)
1224
+ },
1225
+ children: [
1226
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1227
+ f.error ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1228
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-actions", children: [
1229
+ f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx4(
1230
+ "button",
1231
+ {
1232
+ type: "button",
1233
+ className: "fillo-file-retry",
1234
+ "aria-label": `${strings.uploadRetry} ${f.name}`,
1235
+ onClick: () => {
1236
+ const retryFile = f.file;
1237
+ if (!retryFile) return;
1238
+ setInFlight((prev) => prev.filter((x) => x.key !== f.key));
1239
+ void startUpload(retryFile);
1240
+ },
1241
+ children: strings.uploadRetry
1242
+ }
1243
+ ),
1244
+ /* @__PURE__ */ jsx4(
1245
+ "button",
1246
+ {
1247
+ type: "button",
1248
+ className: "fillo-file-remove",
1249
+ "aria-label": "Dismiss",
1250
+ onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
1251
+ children: "\xD7"
1252
+ }
1253
+ )
1254
+ ] }),
1255
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-error", role: "alert", children: f.error })
1256
+ ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1257
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-meta", children: [
1258
+ Math.round(f.fraction * 100),
1259
+ "% of ",
1260
+ formatBytes(f.size)
1261
+ ] }),
1262
+ /* @__PURE__ */ jsx4(
1263
+ "button",
1264
+ {
1265
+ type: "button",
1266
+ className: "fillo-file-remove",
1267
+ "aria-label": `Cancel ${f.name}`,
1268
+ onClick: () => cancelUpload(f.key),
1269
+ children: "\xD7"
1270
+ }
1271
+ ),
1272
+ /* @__PURE__ */ jsx4("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx4(
1273
+ "span",
1274
+ {
1275
+ className: "fillo-progress-bar",
1276
+ style: { width: `${f.fraction * 100}%` }
1277
+ }
1278
+ ) })
1279
+ ] })
1280
+ ]
1281
+ },
1282
+ f.key
1283
+ ))
1284
+ ] }),
1285
+ error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1286
+ ]
1287
+ }
1288
+ );
1259
1289
  }
1260
1290
 
1261
1291
  // src/fields.tsx
1262
1292
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1263
- function fieldAria(field, error, ids) {
1293
+ function fieldAria(field, error, ids, opts = {}) {
1264
1294
  const describedBy = [
1265
1295
  field.description ? ids.descriptionId : null,
1266
1296
  error ? ids.errorId : null
1267
1297
  ].filter(Boolean);
1298
+ const requiredSupported = opts.required ?? true;
1268
1299
  return {
1269
1300
  "aria-invalid": error ? true : void 0,
1270
- "aria-required": field.required ? true : void 0,
1301
+ "aria-required": requiredSupported && field.required ? true : void 0,
1271
1302
  "aria-describedby": describedBy.length ? describedBy.join(" ") : void 0
1272
1303
  };
1273
1304
  }
@@ -1360,7 +1391,8 @@ function NumberInput({ field, value, error, setValue, api, ids: providedIds }) {
1360
1391
  inputRef.current?.setSelectionRange(pos, pos);
1361
1392
  });
1362
1393
  const num = field;
1363
- const grouped = num.notation === "grouped";
1394
+ const grouped = num.notation !== void 0;
1395
+ const locale = localeForNotation(num.notation);
1364
1396
  const hasAffix = Boolean(num.prefix) || Boolean(num.suffix);
1365
1397
  if (!grouped && !hasAffix) {
1366
1398
  return /* @__PURE__ */ jsx5(
@@ -1377,7 +1409,7 @@ function NumberInput({ field, value, error, setValue, api, ids: providedIds }) {
1377
1409
  }
1378
1410
  const stored = typeof value === "string" || typeof value === "number" ? String(value) : "";
1379
1411
  const n = Number(stored);
1380
- const display = draft !== null ? draft : grouped && stored !== "" && Number.isFinite(n) ? formatGroupedNumber(n, { decimals: num.decimals }) : stored;
1412
+ const display = draft !== null ? draft : grouped && stored !== "" && Number.isFinite(n) ? formatGroupedNumber(n, { locale, decimals: num.decimals }) : stored;
1381
1413
  const input = /* @__PURE__ */ jsx5(
1382
1414
  "input",
1383
1415
  {
@@ -1390,14 +1422,14 @@ function NumberInput({ field, value, error, setValue, api, ids: providedIds }) {
1390
1422
  placeholder: field.placeholder,
1391
1423
  onBlur: grouped ? () => setDraft(null) : void 0,
1392
1424
  onChange: (e) => {
1393
- if (!isValidPartialNumberText(e.target.value)) {
1425
+ if (!isValidPartialNumberText(e.target.value, locale)) {
1394
1426
  pendingCaret.current = e.target.selectionStart ?? e.target.value.length;
1395
1427
  setRevertTick((t) => t + 1);
1396
1428
  return;
1397
1429
  }
1398
1430
  if (grouped) {
1399
1431
  setDraft(e.target.value);
1400
- setValue(parseGroupedNumber(e.target.value));
1432
+ setValue(parseGroupedNumber(e.target.value, locale));
1401
1433
  } else {
1402
1434
  setValue(e.target.value);
1403
1435
  }
@@ -1590,7 +1622,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1590
1622
  "data-fillo": "options",
1591
1623
  role: "group",
1592
1624
  "aria-labelledby": ids.labelId,
1593
- ...fieldAria(field, error, ids),
1625
+ ...fieldAria(field, error, ids, { required: false }),
1594
1626
  children: [
1595
1627
  options.map((opt) => /* @__PURE__ */ jsxs5(
1596
1628
  "label",
@@ -1967,7 +1999,7 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
1967
1999
  id: ids.inputId,
1968
2000
  role: "group",
1969
2001
  "aria-labelledby": ids.labelId,
1970
- ...fieldAria(field, error, ids),
2002
+ ...fieldAria(field, error, ids, { required: false }),
1971
2003
  children: /* @__PURE__ */ jsx5("ol", { className: "fillo-ranking", children: order.map((id, index) => {
1972
2004
  const option = ranking.options.find((o) => o.id === id);
1973
2005
  if (!option) return null;
@@ -2014,10 +2046,10 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
2014
2046
  className: "fillo-matrix-wrap",
2015
2047
  role: "group",
2016
2048
  "aria-labelledby": ids.labelId,
2017
- ...fieldAria(field, error, ids),
2049
+ ...fieldAria(field, error, ids, { required: false }),
2018
2050
  children: /* @__PURE__ */ jsxs5("table", { className: "fillo-matrix", children: [
2019
2051
  /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
2020
- /* @__PURE__ */ jsx5("th", {}),
2052
+ /* @__PURE__ */ jsx5("td", {}),
2021
2053
  matrix.columns.map((col) => /* @__PURE__ */ jsx5("th", { scope: "col", children: col.label }, col.id))
2022
2054
  ] }) }),
2023
2055
  /* @__PURE__ */ jsx5("tbody", { children: matrix.rows.map((row) => {
@@ -2410,12 +2442,15 @@ function assertHpHidden(el) {
2410
2442
  }
2411
2443
  function FilloForm(props) {
2412
2444
  const { form, formId } = props;
2445
+ const renderOnly = props.renderOnly === true || props.skipValidation === true;
2413
2446
  const client = useMemo5(
2414
- () => props.client ?? (formId ? createClient() : void 0),
2415
- [props.client, formId]
2447
+ () => renderOnly ? void 0 : props.client ?? (formId ? createClient() : void 0),
2448
+ [renderOnly, props.client, formId]
2416
2449
  );
2417
2450
  const [loaded, setLoaded] = useState6(null);
2418
- const [loadFailure, setLoadFailure] = useState6(null);
2451
+ const [loadFailure, setLoadFailure] = useState6(
2452
+ null
2453
+ );
2419
2454
  const devEnv = useIsDevEnv();
2420
2455
  const devChrome = props.preview === true || devEnv;
2421
2456
  if (props.preview === true) warnPreviewInProduction();
@@ -2449,6 +2484,18 @@ function FilloForm(props) {
2449
2484
  useEffect8(() => {
2450
2485
  onErrorRef.current = onError;
2451
2486
  });
2487
+ const targetError = useMemo5(
2488
+ () => form && !formId && !renderOnly && (!isCodeForm2(form) || !props.client) ? new FilloError2(
2489
+ "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.",
2490
+ 422,
2491
+ void 0,
2492
+ "form_target_required"
2493
+ ) : null,
2494
+ [form, formId, props.client, renderOnly]
2495
+ );
2496
+ useEffect8(() => {
2497
+ if (targetError) onErrorRef.current?.(targetError);
2498
+ }, [targetError]);
2452
2499
  const needsFetch = !inlineSchema && Boolean(client && formId);
2453
2500
  useEffect8(() => {
2454
2501
  if (!needsFetch || !client || !formId) {
@@ -2487,10 +2534,28 @@ function FilloForm(props) {
2487
2534
  const currentLoaded = needsFetch && loaded?.formId === formId ? loaded : null;
2488
2535
  const loadError = needsFetch && loadFailure && loadFailure.formId === formId ? loadFailure.error : null;
2489
2536
  const rawSchema = inlineSchema ?? currentLoaded?.schema ?? null;
2490
- const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
2537
+ const normalized = useMemo5(
2538
+ () => rawSchema ? normalizeFormSchema(rawSchema) : null,
2539
+ [rawSchema]
2540
+ );
2491
2541
  const schema = normalized?.ok ? normalized.schema : null;
2492
2542
  const resolvedCodeTheme = renderResolvedSnapshot ? resolvedTheme ?? void 0 : codeForm?.theme;
2493
2543
  const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
2544
+ if (targetError) {
2545
+ if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(targetError) });
2546
+ return /* @__PURE__ */ jsx7(
2547
+ "div",
2548
+ {
2549
+ className: `fillo-form fillo-form--error ${props.className ?? ""}`,
2550
+ role: "alert",
2551
+ "data-fillo": "root",
2552
+ "data-state": "error",
2553
+ "data-error-code": "form_target_required",
2554
+ ...themeProps(theme),
2555
+ children: devChrome ? `${targetError.message} (${targetError.code})` : strings.loadFailed
2556
+ }
2557
+ );
2558
+ }
2494
2559
  if (normalized && !normalized.ok) {
2495
2560
  const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
2496
2561
  if (props.renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: props.renderError(error) });
@@ -2531,7 +2596,14 @@ function FilloForm(props) {
2531
2596
  );
2532
2597
  }
2533
2598
  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 }) });
2599
+ return /* @__PURE__ */ jsx7(
2600
+ "div",
2601
+ {
2602
+ className: `fillo-form fillo-form--closed ${props.className ?? ""}`,
2603
+ ...themeProps(theme),
2604
+ children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: strings.closed })
2605
+ }
2606
+ );
2535
2607
  }
2536
2608
  const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
2537
2609
  if (canonicalSyncError && props.renderError) {
@@ -2573,7 +2645,14 @@ function FilloForm(props) {
2573
2645
  }
2574
2646
  );
2575
2647
  }
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 }) });
2648
+ return /* @__PURE__ */ jsx7(
2649
+ "div",
2650
+ {
2651
+ className: `fillo-form fillo-form--closed ${props.className ?? ""}`,
2652
+ ...themeProps(theme),
2653
+ children: /* @__PURE__ */ jsx7("p", { className: "fillo-closed", children: variant === "closed" ? strings.closed : strings.notLive })
2654
+ }
2655
+ );
2577
2656
  }
2578
2657
  const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
2579
2658
  if (!devChrome && awaitingCanonical || !schema) {
@@ -2598,10 +2677,12 @@ function FilloForm(props) {
2598
2677
  ResolvedForm,
2599
2678
  {
2600
2679
  ...props,
2601
- client,
2680
+ client: renderOnly ? void 0 : client,
2602
2681
  schema,
2603
2682
  theme,
2604
- formId: syncedFormId ?? formId,
2683
+ formId: renderOnly ? void 0 : syncedFormId ?? formId,
2684
+ renderOnly,
2685
+ skipValidation: renderOnly ? true : props.skipValidation,
2605
2686
  poweredBy,
2606
2687
  brandLabel: branding?.label,
2607
2688
  brandHref: branding?.href,
@@ -2672,13 +2753,14 @@ function useIsDevEnv() {
2672
2753
  function ResolvedForm(props) {
2673
2754
  const { schema, theme } = props;
2674
2755
  const displayOnly = props.displayOnly === true;
2756
+ const renderOnly = props.renderOnly === true;
2675
2757
  const instanceId = useId2().replace(/:/g, "");
2676
2758
  const devEnv = useIsDevEnv();
2677
2759
  const devChrome = !displayOnly && (props.preview === true || devEnv);
2678
2760
  const hpRef = useRef8(null);
2679
2761
  const codeForm = isCodeForm2(props.form) ? props.form : null;
2680
2762
  const client = props.client;
2681
- const challenge = props.challenge;
2763
+ const challenge = renderOnly ? void 0 : props.challenge;
2682
2764
  const challengeRequired = Boolean(challenge);
2683
2765
  const challengeTokenRef = useRef8(void 0);
2684
2766
  const [resyncWarning, setResyncWarning] = useState6(null);
@@ -2790,7 +2872,7 @@ function ResolvedForm(props) {
2790
2872
  resetChallengeRef.current?.();
2791
2873
  }
2792
2874
  });
2793
- useDraftFlush(api, !displayOnly);
2875
+ useDraftFlush(api, !displayOnly && !renderOnly);
2794
2876
  const strings = resolveStrings(props.strings);
2795
2877
  const announceRef = useRef8(null);
2796
2878
  const announce = useCallback((text) => {
@@ -2821,12 +2903,13 @@ function ResolvedForm(props) {
2821
2903
  const chromeValue = useMemo5(
2822
2904
  () => ({
2823
2905
  devChrome,
2906
+ renderOnly,
2824
2907
  uploadsAvailable,
2825
2908
  uploadFileSizeLimitMb,
2826
2909
  warningCode,
2827
2910
  warningUrl
2828
2911
  }),
2829
- [devChrome, uploadsAvailable, uploadFileSizeLimitMb, warningCode, warningUrl]
2912
+ [devChrome, renderOnly, uploadsAvailable, uploadFileSizeLimitMb, warningCode, warningUrl]
2830
2913
  );
2831
2914
  const settings = schema.settings;
2832
2915
  const submitted = api.status === "submitted";
@@ -2867,13 +2950,17 @@ function ResolvedForm(props) {
2867
2950
  { slot: "root", status: "submitted" }
2868
2951
  ),
2869
2952
  "data-fillo": "root",
2953
+ "data-fillo-form-id": props.formId,
2870
2954
  "data-state": "submitted",
2871
2955
  ...themed,
2872
2956
  children: [
2873
2957
  props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs6(
2874
2958
  "div",
2875
2959
  {
2876
- className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
2960
+ className: slotClass3("fillo-success", appearance, {
2961
+ slot: "success",
2962
+ status: "submitted"
2963
+ }),
2877
2964
  "data-fillo": "success",
2878
2965
  children: [
2879
2966
  /* @__PURE__ */ jsx7("div", { className: "fillo-success-mark", "aria-hidden": "true" }),
@@ -2934,7 +3021,7 @@ function ResolvedForm(props) {
2934
3021
  staged: props.stagedNotice,
2935
3022
  draft: props.draftNotice,
2936
3023
  warningUrl: props.syncWarningUrl,
2937
- noClient: !props.client && !props.skipValidation
3024
+ noClient: !props.client && !props.skipValidation && !renderOnly
2938
3025
  }
2939
3026
  ),
2940
3027
  resuming && /* @__PURE__ */ jsxs6(
@@ -2951,7 +3038,9 @@ function ResolvedForm(props) {
2951
3038
  api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx7(
2952
3039
  "div",
2953
3040
  {
2954
- className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
3041
+ className: slotClass3("fillo-resume fillo-resume--expired", appearance, {
3042
+ slot: "resume"
3043
+ }),
2955
3044
  "data-fillo": "resume",
2956
3045
  role: "status",
2957
3046
  children: /* @__PURE__ */ jsx7("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
@@ -2972,23 +3061,46 @@ function ResolvedForm(props) {
2972
3061
  {
2973
3062
  className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
2974
3063
  "data-fillo": "progressFill",
2975
- style: { "--fillo-progress-value": `${progressStep / progressTotal * 100}%` }
3064
+ style: {
3065
+ "--fillo-progress-value": `${progressStep / progressTotal * 100}%`
3066
+ }
2976
3067
  }
2977
3068
  )
2978
3069
  }
2979
3070
  ),
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 }),
3071
+ api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs6(
3072
+ "header",
3073
+ {
3074
+ className: slotClass3("fillo-header", appearance, { slot: "header" }),
3075
+ "data-fillo": "header",
3076
+ children: [
3077
+ schema.title && /* @__PURE__ */ jsx7(
3078
+ "h1",
3079
+ {
3080
+ className: slotClass3("fillo-title", appearance, { slot: "title" }),
3081
+ "data-fillo": "title",
3082
+ children: schema.title
3083
+ }
3084
+ ),
3085
+ schema.description && /* @__PURE__ */ jsx7(
3086
+ "p",
3087
+ {
3088
+ className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
3089
+ "data-fillo": "description",
3090
+ children: schema.description
3091
+ }
3092
+ )
3093
+ ]
3094
+ }
3095
+ ),
3096
+ multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx7(
3097
+ "h2",
3098
+ {
3099
+ className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }),
3100
+ "data-fillo": "pageTitle",
3101
+ children: api.page.title
3102
+ }
3103
+ ),
2992
3104
  invalidFields.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "fillo-error-summary", role: "alert", tabIndex: -1, "data-fillo": "error-summary", children: [
2993
3105
  /* @__PURE__ */ jsx7("p", { className: "fillo-error-summary-title", children: strings.errorSummaryTitle }),
2994
3106
  /* @__PURE__ */ jsx7("ul", { children: invalidFields.map(({ field, error, inputId }) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsxs6(
@@ -3007,16 +3119,25 @@ function ResolvedForm(props) {
3007
3119
  }
3008
3120
  ) }, field.id)) })
3009
3121
  ] }),
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,
3122
+ /* @__PURE__ */ jsx7(
3123
+ "div",
3012
3124
  {
3013
- block,
3014
- api,
3015
- components: props.components,
3016
- customComponents: props.customComponents
3017
- },
3018
- block.id
3019
- )) }),
3125
+ className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }),
3126
+ "data-fillo": "blocks",
3127
+ ref: blocksRef,
3128
+ tabIndex: -1,
3129
+ children: api.blocks.map((block) => /* @__PURE__ */ jsx7(
3130
+ BlockRenderer,
3131
+ {
3132
+ block,
3133
+ api,
3134
+ components: props.components,
3135
+ customComponents: props.customComponents
3136
+ },
3137
+ block.id
3138
+ ))
3139
+ }
3140
+ ),
3020
3141
  !displayOnly && /* @__PURE__ */ jsx7(
3021
3142
  "input",
3022
3143
  {
@@ -3056,43 +3177,66 @@ function ResolvedForm(props) {
3056
3177
  /* @__PURE__ */ jsx7("a", { href: submitFixUrl, target: "_blank", rel: "noopener noreferrer", children: submitFixUrl })
3057
3178
  ] })
3058
3179
  ] }),
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
- ] }),
3180
+ showFooter && /* @__PURE__ */ jsxs6(
3181
+ "footer",
3182
+ {
3183
+ className: slotClass3("fillo-footer", appearance, { slot: "footer" }),
3184
+ "data-fillo": "footer",
3185
+ children: [
3186
+ multiPage && !api.isFirstPage && /* @__PURE__ */ jsx7(
3187
+ "button",
3188
+ {
3189
+ type: "button",
3190
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, {
3191
+ slot: "button",
3192
+ variant: "ghost"
3193
+ }),
3194
+ "data-fillo": "button",
3195
+ onClick: api.back,
3196
+ children: strings.back
3197
+ }
3198
+ ),
3199
+ showPrimaryButton && /* @__PURE__ */ jsx7(
3200
+ "button",
3201
+ {
3202
+ type: "submit",
3203
+ className: slotClass3("fillo-button fillo-button--primary", appearance, {
3204
+ slot: "button",
3205
+ variant: "primary"
3206
+ }),
3207
+ "data-fillo": "button",
3208
+ disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
3209
+ // "Next"). The server rejects a tokenless submit regardless —
3210
+ // this is UX only.
3211
+ showChallenge && !challengeToken,
3212
+ children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
3213
+ }
3214
+ )
3215
+ ]
3216
+ }
3217
+ ),
3084
3218
  props.poweredBy && /* @__PURE__ */ jsx7(PoweredBy, { label: props.brandLabel, href: props.brandHref })
3085
3219
  ] });
3086
3220
  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
3221
  // Display-only preview: a plain div (no form element to submit) with
3088
3222
  // every control inside a natively disabled fieldset — unfocusable
3089
3223
  // 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 }) })
3224
+ /* @__PURE__ */ jsx7(
3225
+ "div",
3226
+ {
3227
+ className: rootClass,
3228
+ "data-fillo": "preview-form",
3229
+ "data-state": "preview",
3230
+ ...themed,
3231
+ children: /* @__PURE__ */ jsx7("fieldset", { disabled: true, className: "fillo-not-open-fields", children: formBody })
3232
+ }
3233
+ )
3091
3234
  ) : /* @__PURE__ */ jsx7(
3092
3235
  "form",
3093
3236
  {
3094
3237
  className: rootClass,
3095
3238
  "data-fillo": "root",
3239
+ "data-fillo-form-id": props.formId,
3096
3240
  "data-state": api.status,
3097
3241
  "data-page": api.pageIndex + 1,
3098
3242
  "data-last-page": api.isLastPage ? "" : void 0,
@@ -3138,7 +3282,7 @@ function JsxForm(props) {
3138
3282
  if (!memo2.current || memo2.current.json !== json) {
3139
3283
  memo2.current = { json, form: compiled };
3140
3284
  }
3141
- return /* @__PURE__ */ jsx8(FilloForm, { form: memo2.current.form, client, ...rest });
3285
+ return /* @__PURE__ */ jsx8(FilloForm, { ...{ form: memo2.current.form, client, ...rest } });
3142
3286
  }
3143
3287
  function defineFormFromJsx(element) {
3144
3288
  if (!element || element.type !== JsxForm) {
@@ -3183,9 +3327,10 @@ var Fillo = {
3183
3327
  };
3184
3328
 
3185
3329
  // src/provider.tsx
3186
- import { useId as useId3, useMemo as useMemo6, useState as useState7 } from "react";
3330
+ import { useEffect as useEffect9, useId as useId3, useMemo as useMemo6, useState as useState7 } from "react";
3187
3331
  import {
3188
3332
  FilloError as FilloError3,
3333
+ createClient as createClient2,
3189
3334
  formSchemasEqual as formSchemasEqual2,
3190
3335
  isFilloError as isFilloError3,
3191
3336
  normalizeFormSchema as normalizeFormSchema2,
@@ -3203,13 +3348,33 @@ function FilloProvider({
3203
3348
  onError,
3204
3349
  renderError,
3205
3350
  preview,
3351
+ renderOnly,
3352
+ skipValidation,
3206
3353
  ...options
3207
3354
  }) {
3355
+ const localPreview = renderOnly === true || skipValidation === true;
3356
+ const activeClient = useMemo6(
3357
+ () => localPreview ? void 0 : client ?? (formId ? createClient2() : void 0),
3358
+ [client, formId, localPreview]
3359
+ );
3360
+ const activeFormId = localPreview ? void 0 : formId;
3208
3361
  const instanceId = useId3().replace(/:/g, "");
3209
3362
  const devEnv = useIsDevEnv();
3210
3363
  const devChrome = preview === true || devEnv;
3211
3364
  if (preview === true) warnPreviewInProduction();
3212
3365
  const codeForm = isCodeForm2(form) ? form : null;
3366
+ const targetError = useMemo6(
3367
+ () => !formId && !localPreview && (!codeForm || !client) ? new FilloError3(
3368
+ "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.",
3369
+ 422,
3370
+ void 0,
3371
+ "form_target_required"
3372
+ ) : null,
3373
+ [client, codeForm, formId, localPreview]
3374
+ );
3375
+ useEffect9(() => {
3376
+ if (targetError) onError?.(targetError);
3377
+ }, [onError, targetError]);
3213
3378
  const {
3214
3379
  formId: syncedFormId,
3215
3380
  status: syncedStatus,
@@ -3222,8 +3387,8 @@ function FilloProvider({
3222
3387
  uploadFileSizeLimitMb,
3223
3388
  warningCode,
3224
3389
  warningUrl
3225
- } = useCodeFormSync(form, client, onError, {
3226
- allowUnsynced: Boolean(!client || formId || options.skipValidation)
3390
+ } = useCodeFormSync(form, activeClient, onError, {
3391
+ allowUnsynced: Boolean(!activeClient || activeFormId || localPreview)
3227
3392
  });
3228
3393
  const [resyncWarning, setResyncWarning] = useState7(null);
3229
3394
  const rawSchema = codeForm ? resolvedSchema && !devChrome ? resolvedSchema : codeForm.schema : form;
@@ -3233,9 +3398,9 @@ function FilloProvider({
3233
3398
  }
3234
3399
  const schema = normalized.schema;
3235
3400
  const resolveFormId = useMemo6(
3236
- () => codeForm && client?.key ? async () => {
3401
+ () => codeForm && activeClient?.key ? async () => {
3237
3402
  try {
3238
- const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
3403
+ const result = await syncCodeForm3(activeClient, codeForm, { bypassCache: true });
3239
3404
  setResyncWarning({
3240
3405
  code: result.warningCode,
3241
3406
  url: result.warningUrl,
@@ -3297,13 +3462,14 @@ function FilloProvider({
3297
3462
  throw failure;
3298
3463
  }
3299
3464
  } : void 0,
3300
- [client, codeForm, onError, schema]
3465
+ [activeClient, codeForm, onError, schema]
3301
3466
  );
3302
3467
  const api = useFilloController({
3303
3468
  ...options,
3304
- client,
3469
+ client: activeClient,
3305
3470
  form: schema,
3306
- formId: syncedFormId ?? formId,
3471
+ formId: syncedFormId ?? activeFormId ?? void 0,
3472
+ skipValidation: localPreview,
3307
3473
  resolveFormId,
3308
3474
  // Dev chrome (preview or dev env) surfaces the developer-grade resolution
3309
3475
  // failure (message + code); an explicit option wins.
@@ -3312,7 +3478,7 @@ function FilloProvider({
3312
3478
  // method; recorded per response so usage stays measurable.
3313
3479
  surface: "headless"
3314
3480
  });
3315
- useDraftFlush(api);
3481
+ useDraftFlush(api, !localPreview);
3316
3482
  const freshWarningCode = resyncWarning ? resyncWarning.code : warningCode;
3317
3483
  const freshWarningUrl = resyncWarning ? resyncWarning.url : warningUrl;
3318
3484
  const freshUploadsAvailable = resyncWarning ? resyncWarning.uploadsAvailable : uploadsAvailable;
@@ -3320,20 +3486,32 @@ function FilloProvider({
3320
3486
  const chromeValue = useMemo6(
3321
3487
  () => ({
3322
3488
  devChrome,
3489
+ renderOnly: localPreview,
3323
3490
  uploadsAvailable: freshUploadsAvailable,
3324
3491
  uploadFileSizeLimitMb: freshUploadFileSizeLimitMb,
3325
3492
  warningCode: freshWarningCode,
3326
3493
  warningUrl: freshWarningUrl
3327
3494
  }),
3328
- [devChrome, freshUploadsAvailable, freshUploadFileSizeLimitMb, freshWarningCode, freshWarningUrl]
3495
+ [
3496
+ devChrome,
3497
+ localPreview,
3498
+ freshUploadsAvailable,
3499
+ freshUploadFileSizeLimitMb,
3500
+ freshWarningCode,
3501
+ freshWarningUrl
3502
+ ]
3329
3503
  );
3330
3504
  const resolvedStrings = resolveStrings2(strings);
3331
- const canonicalSyncError = syncError && !options.skipValidation ? syncError : null;
3505
+ if (targetError) {
3506
+ if (renderError) return /* @__PURE__ */ jsx9(Fragment4, { children: renderError(targetError) });
3507
+ throw targetError;
3508
+ }
3509
+ const canonicalSyncError = syncError && !localPreview ? syncError : null;
3332
3510
  if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx9(Fragment4, { children: renderError(canonicalSyncError) });
3333
3511
  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;
3512
+ if (codeFormSyncing && !localPreview && !devChrome) return null;
3513
+ const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !localPreview;
3514
+ const notAccepting = Boolean(codeForm) && syncedStatus === "published" && syncedAccepting === false && !localPreview;
3337
3515
  if ((notPublished || notAccepting) && !devChrome) {
3338
3516
  const error = notPublished ? new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published") : new FilloError3(
3339
3517
  "This form is no longer accepting responses.",
@@ -3348,7 +3526,7 @@ function FilloProvider({
3348
3526
 
3349
3527
  // src/index.ts
3350
3528
  import {
3351
- createClient as createClient2,
3529
+ createClient as createClient3,
3352
3530
  provisionWorkspace,
3353
3531
  when,
3354
3532
  FilloJsxError as FilloJsxError2,
@@ -3375,7 +3553,7 @@ export {
3375
3553
  FilloJsxError2 as FilloJsxError,
3376
3554
  FilloProvider,
3377
3555
  FormField,
3378
- createClient2 as createClient,
3556
+ createClient3 as createClient,
3379
3557
  defineForm,
3380
3558
  provisionWorkspace,
3381
3559
  useField,