@usefillo/core 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +239 -14
  2. package/dist/index.js +659 -111
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -381,18 +381,18 @@ function isEmpty(value) {
381
381
  if (typeof value === "object") return Object.keys(value).length === 0;
382
382
  return false;
383
383
  }
384
- function validateField(field, value) {
384
+ function validateField(field2, value) {
385
385
  if (isEmpty(value)) {
386
- return field.required ? "This field is required" : null;
386
+ return field2.required ? "This field is required" : null;
387
387
  }
388
- switch (field.kind) {
388
+ switch (field2.kind) {
389
389
  case "short_text":
390
390
  case "long_text": {
391
391
  if (typeof value !== "string") return "Expected text";
392
- if (field.maxLength && value.length > field.maxLength)
393
- return `Must be at most ${field.maxLength} characters`;
394
- const hardCap = field.kind === "long_text" ? 2e4 : 2e3;
395
- if (!field.maxLength && value.length > hardCap) return "Answer is too long";
392
+ if (field2.maxLength && value.length > field2.maxLength)
393
+ return `Must be at most ${field2.maxLength} characters`;
394
+ const hardCap = field2.kind === "long_text" ? 2e4 : 2e3;
395
+ if (!field2.maxLength && value.length > hardCap) return "Answer is too long";
396
396
  return null;
397
397
  }
398
398
  case "email": {
@@ -419,25 +419,25 @@ function validateField(field, value) {
419
419
  case "number": {
420
420
  const n = typeof value === "string" ? Number(value) : value;
421
421
  if (typeof n !== "number" || !Number.isFinite(n)) return "Enter a number";
422
- if (field.min !== void 0 && n < field.min) return `Must be at least ${field.min}`;
423
- if (field.max !== void 0 && n > field.max) return `Must be at most ${field.max}`;
422
+ if (field2.min !== void 0 && n < field2.min) return `Must be at least ${field2.min}`;
423
+ if (field2.max !== void 0 && n > field2.max) return `Must be at most ${field2.max}`;
424
424
  return null;
425
425
  }
426
426
  case "select":
427
427
  case "dropdown": {
428
428
  if (typeof value !== "string" || value === "") return "Pick an option";
429
- if (field.options.some((o) => o.id === value)) return null;
430
- if (field.allowOther && value.length <= 500) return null;
429
+ if (field2.options.some((o) => o.id === value)) return null;
430
+ if (field2.allowOther && value.length <= 500) return null;
431
431
  return "Pick a valid option";
432
432
  }
433
433
  case "multi_select": {
434
434
  if (!Array.isArray(value)) return "Pick at least one option";
435
- const ids = new Set(field.options.map((o) => o.id));
435
+ const ids = new Set(field2.options.map((o) => o.id));
436
436
  const arr = value;
437
437
  let others = 0;
438
438
  for (const v of arr) {
439
439
  if (ids.has(v)) continue;
440
- if (field.allowOther !== true || typeof v !== "string" || v.length === 0 || v.length > 500)
440
+ if (field2.allowOther !== true || typeof v !== "string" || v.length === 0 || v.length > 500)
441
441
  return "Pick valid options";
442
442
  if (++others > 1) return "Only one 'Other' answer is allowed";
443
443
  }
@@ -447,15 +447,15 @@ function validateField(field, value) {
447
447
  return typeof value === "boolean" ? null : "Invalid value";
448
448
  }
449
449
  case "rating": {
450
- const max = field.max ?? 5;
450
+ const max = field2.max ?? 5;
451
451
  const n = typeof value === "string" ? Number(value) : value;
452
452
  if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > max)
453
453
  return `Pick a rating between 1 and ${max}`;
454
454
  return null;
455
455
  }
456
456
  case "linear_scale": {
457
- const min = field.min ?? 1;
458
- const max = field.max ?? 10;
457
+ const min = field2.min ?? 1;
458
+ const max = field2.max ?? 10;
459
459
  const n = typeof value === "string" ? Number(value) : value;
460
460
  if (typeof n !== "number" || !Number.isInteger(n) || n < min || n > max)
461
461
  return `Pick a value between ${min} and ${max}`;
@@ -463,7 +463,7 @@ function validateField(field, value) {
463
463
  }
464
464
  case "ranking": {
465
465
  if (!Array.isArray(value)) return "Drag the options into order";
466
- const ids = field.options.map((o) => o.id).sort();
466
+ const ids = field2.options.map((o) => o.id).sort();
467
467
  const got = [...value].sort();
468
468
  if (ids.length !== got.length || ids.some((id, i) => id !== got[i]))
469
469
  return "Rank every option";
@@ -473,12 +473,12 @@ function validateField(field, value) {
473
473
  if (typeof value !== "object" || value === null || Array.isArray(value))
474
474
  return "Answer the grid";
475
475
  const answers = value;
476
- const columnIds = new Set(field.columns.map((c) => c.id));
477
- const rowIds = new Set(field.rows.map((r) => r.id));
476
+ const columnIds = new Set(field2.columns.map((c) => c.id));
477
+ const rowIds = new Set(field2.rows.map((r) => r.id));
478
478
  for (const [rowId, columnId] of Object.entries(answers)) {
479
479
  if (!rowIds.has(rowId) || !columnIds.has(columnId)) return "Invalid grid answer";
480
480
  }
481
- if (field.required && Object.keys(answers).length < field.rows.length)
481
+ if (field2.required && Object.keys(answers).length < field2.rows.length)
482
482
  return "Answer every row";
483
483
  return null;
484
484
  }
@@ -505,7 +505,7 @@ function validateField(field, value) {
505
505
  case "file_upload": {
506
506
  const r = z.array(fileValueSchema).safeParse(value);
507
507
  if (!r.success) return "Upload did not complete";
508
- const maxFiles = field.maxFiles ?? 1;
508
+ const maxFiles = field2.maxFiles ?? 1;
509
509
  if (r.data.length > maxFiles)
510
510
  return `At most ${maxFiles} file${maxFiles === 1 ? "" : "s"}`;
511
511
  return null;
@@ -516,24 +516,24 @@ function validateResponse(form, data) {
516
516
  const fields = visibleFields(form, data);
517
517
  const errors = {};
518
518
  const cleaned = {};
519
- for (const field of fields) {
520
- const value = normalizeValue(field, data[field.id]);
521
- const error = validateField(field, value);
519
+ for (const field2 of fields) {
520
+ const value = normalizeValue(field2, data[field2.id]);
521
+ const error = validateField(field2, value);
522
522
  if (error) {
523
- errors[field.id] = error;
523
+ errors[field2.id] = error;
524
524
  } else if (!isEmpty(value)) {
525
- cleaned[field.id] = value;
525
+ cleaned[field2.id] = value;
526
526
  }
527
527
  }
528
528
  return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
529
529
  }
530
- function normalizeValue(field, value) {
530
+ function normalizeValue(field2, value) {
531
531
  if (value === void 0 || value === null) return value;
532
- if (field.kind === "number" && typeof value === "string" && value.trim() !== "") {
532
+ if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
533
533
  const n = Number(value);
534
534
  return Number.isNaN(n) ? value : n;
535
535
  }
536
- if ((field.kind === "rating" || field.kind === "linear_scale") && typeof value === "string") {
536
+ if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
537
537
  const n = Number(value);
538
538
  return Number.isNaN(n) ? value : n;
539
539
  }
@@ -869,26 +869,26 @@ function normalizeFormTheme(input) {
869
869
  function assertNever(x) {
870
870
  throw new Error(`Unhandled field kind: ${JSON.stringify(x)}`);
871
871
  }
872
- function formatAnswer(field, value) {
872
+ function formatAnswer(field2, value) {
873
873
  if (value === null || value === void 0 || value === "") return "";
874
- switch (field.kind) {
874
+ switch (field2.kind) {
875
875
  case "file_upload":
876
876
  if (!Array.isArray(value)) return String(value);
877
877
  return value.map((f) => f.name).join(", ");
878
878
  case "select":
879
879
  case "dropdown":
880
- return field.options.find((o) => o.id === value)?.label ?? String(value);
880
+ return field2.options.find((o) => o.id === value)?.label ?? String(value);
881
881
  case "multi_select":
882
882
  if (!Array.isArray(value)) return String(value);
883
- return value.map((id) => field.options.find((o) => o.id === id)?.label ?? id).join(", ");
883
+ return value.map((id) => field2.options.find((o) => o.id === id)?.label ?? id).join(", ");
884
884
  case "ranking":
885
885
  if (!Array.isArray(value)) return String(value);
886
- return value.map((id, i) => `${i + 1}. ${field.options.find((o) => o.id === id)?.label ?? id}`).join(", ");
886
+ return value.map((id, i) => `${i + 1}. ${field2.options.find((o) => o.id === id)?.label ?? id}`).join(", ");
887
887
  case "matrix": {
888
888
  if (typeof value !== "object" || value === null || Array.isArray(value))
889
889
  return String(value);
890
890
  const answers = value;
891
- return field.rows.filter((row) => answers[row.id]).map((row) => `${row.label}: ${field.columns.find((c) => c.id === answers[row.id])?.label ?? ""}`).join(", ");
891
+ return field2.rows.filter((row) => answers[row.id]).map((row) => `${row.label}: ${field2.columns.find((c) => c.id === answers[row.id])?.label ?? ""}`).join(", ");
892
892
  }
893
893
  case "signature":
894
894
  return typeof value === "string" && value.startsWith("data:image/") ? "\u270D signed" : "";
@@ -915,7 +915,7 @@ function formatAnswer(field, value) {
915
915
  case "custom":
916
916
  return typeof value === "object" ? JSON.stringify(value) : String(value);
917
917
  default:
918
- return assertNever(field);
918
+ return assertNever(field2);
919
919
  }
920
920
  }
921
921
 
@@ -1255,13 +1255,30 @@ var sha1Base64 = (bytes) => bytesToBase64(new Sha1().update(bytes).digest());
1255
1255
 
1256
1256
  // src/client.ts
1257
1257
  var FilloError = class extends Error {
1258
- constructor(message, status) {
1258
+ constructor(message, status, retryAfterSec2) {
1259
1259
  super(message);
1260
1260
  this.status = status;
1261
+ this.retryAfterSec = retryAfterSec2;
1261
1262
  this.name = "FilloError";
1262
1263
  }
1263
1264
  status;
1265
+ retryAfterSec;
1264
1266
  };
1267
+ function isFilloError(err) {
1268
+ return err instanceof Error && err.name === "FilloError";
1269
+ }
1270
+ async function errorMessage(res, fallback) {
1271
+ try {
1272
+ const body = await res.json();
1273
+ if (typeof body.error === "string" && body.error) return body.error;
1274
+ } catch {
1275
+ }
1276
+ return fallback;
1277
+ }
1278
+ function retryAfterSec(res) {
1279
+ const raw = Number(res.headers.get("retry-after"));
1280
+ return Number.isFinite(raw) && raw > 0 ? raw : void 0;
1281
+ }
1265
1282
  var MAX_CHUNK_RETRIES = 3;
1266
1283
  var DEFAULT_TIMEOUT_MS = 3e4;
1267
1284
  function isRetryableStatus(status) {
@@ -1311,6 +1328,7 @@ async function digestPartBase64(bytes) {
1311
1328
  return sha1Base64(bytes);
1312
1329
  }
1313
1330
  var DEFAULT_BASE_URL = "https://fillo.so";
1331
+ var CLIENT_HEADER = { "X-Fillo-Client": `@usefillo/core@${FILLO_SDK_VERSION}` };
1314
1332
  var FilloClient = class {
1315
1333
  /** Server origin this client targets, normalized (no trailing slash). */
1316
1334
  baseUrl;
@@ -1318,7 +1336,7 @@ var FilloClient = class {
1318
1336
  /** Publishable workspace key, when configured. */
1319
1337
  key;
1320
1338
  constructor(options2 = {}) {
1321
- this.baseUrl = options2.sameOrigin ? "" : DEFAULT_BASE_URL;
1339
+ this.baseUrl = options2.baseUrl ? options2.baseUrl.replace(/\/$/, "") : options2.sameOrigin ? "" : DEFAULT_BASE_URL;
1322
1340
  this.key = options2.key;
1323
1341
  this.fetch = options2.fetch ?? ((...args) => globalThis.fetch(...args));
1324
1342
  }
@@ -1326,7 +1344,7 @@ var FilloClient = class {
1326
1344
  return `${this.baseUrl}/api/v1${path}`;
1327
1345
  }
1328
1346
  async json(path, init) {
1329
- const headers = init?.body != null ? { "Content-Type": "application/json", ...init?.headers } : init?.headers;
1347
+ const headers = init?.body != null ? { "Content-Type": "application/json", ...CLIENT_HEADER, ...init?.headers } : init?.headers;
1330
1348
  let res;
1331
1349
  try {
1332
1350
  res = await this.fetch(this.url(path), {
@@ -1338,8 +1356,8 @@ var FilloClient = class {
1338
1356
  throw asRequestError(err);
1339
1357
  }
1340
1358
  if (!res.ok) {
1341
- const body = await res.text().catch(() => "");
1342
- throw new FilloError(body || `Request failed: ${res.status}`, res.status);
1359
+ const message = await errorMessage(res, `Request failed: ${res.status}`);
1360
+ throw new FilloError(message, res.status, retryAfterSec(res));
1343
1361
  }
1344
1362
  return await res.json();
1345
1363
  }
@@ -1379,7 +1397,7 @@ var FilloClient = class {
1379
1397
  try {
1380
1398
  res = await this.fetch(this.url(`/forms/${encodeURIComponent(formId)}/responses`), {
1381
1399
  method: "POST",
1382
- headers: { "Content-Type": "application/json" },
1400
+ headers: { "Content-Type": "application/json", ...CLIENT_HEADER },
1383
1401
  body: JSON.stringify({ data, meta }),
1384
1402
  signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
1385
1403
  });
@@ -1391,7 +1409,8 @@ var FilloClient = class {
1391
1409
  return { ok: false, errors: body2.errors };
1392
1410
  }
1393
1411
  if (!res.ok) {
1394
- throw new FilloError(`Submit failed: ${res.status}`, res.status);
1412
+ const message = await errorMessage(res, `Submit failed: ${res.status}`);
1413
+ throw new FilloError(message, res.status, retryAfterSec(res));
1395
1414
  }
1396
1415
  const body = await res.json();
1397
1416
  return { ok: true, responseId: body.id, duplicate: body.duplicate };
@@ -1722,6 +1741,68 @@ async function provisionWorkspace(options2) {
1722
1741
  return await res.json();
1723
1742
  }
1724
1743
 
1744
+ // src/prefill.ts
1745
+ function parseDecimal(raw) {
1746
+ const s = raw.trim();
1747
+ if (!/^-?\d*\.?\d+$/.test(s)) return null;
1748
+ const n = Number(s);
1749
+ return Number.isFinite(n) ? n : null;
1750
+ }
1751
+ function prefillFromParams(form, params) {
1752
+ const data = {};
1753
+ for (const field2 of allFields(form)) {
1754
+ const raw = field2.kind === "hidden" ? params[field2.paramName ?? field2.id] ?? field2.defaultValue : params[field2.id];
1755
+ if (raw === void 0 || raw === "") continue;
1756
+ switch (field2.kind) {
1757
+ case "number": {
1758
+ const n = parseDecimal(raw);
1759
+ if (n === null) break;
1760
+ if (field2.min !== void 0 && n < field2.min) break;
1761
+ if (field2.max !== void 0 && n > field2.max) break;
1762
+ data[field2.id] = n;
1763
+ break;
1764
+ }
1765
+ case "rating": {
1766
+ const n = parseDecimal(raw);
1767
+ if (n !== null && Number.isInteger(n) && n >= 1 && n <= (field2.max ?? 5))
1768
+ data[field2.id] = n;
1769
+ break;
1770
+ }
1771
+ case "linear_scale": {
1772
+ const n = parseDecimal(raw);
1773
+ const min = field2.min ?? 1;
1774
+ const max = field2.max ?? 10;
1775
+ if (n !== null && Number.isInteger(n) && n >= min && n <= max) data[field2.id] = n;
1776
+ break;
1777
+ }
1778
+ case "checkbox":
1779
+ data[field2.id] = raw === "true" || raw === "1" || raw === "yes";
1780
+ break;
1781
+ case "select":
1782
+ case "dropdown": {
1783
+ if (field2.options.some((o) => o.id === raw)) data[field2.id] = raw;
1784
+ break;
1785
+ }
1786
+ case "multi_select": {
1787
+ const ids = new Set(field2.options.map((o) => o.id));
1788
+ const picked = raw.split(",").filter((v) => ids.has(v));
1789
+ if (picked.length > 0) data[field2.id] = picked;
1790
+ break;
1791
+ }
1792
+ case "file_upload":
1793
+ case "ranking":
1794
+ case "matrix":
1795
+ case "signature":
1796
+ case "custom":
1797
+ break;
1798
+ // not prefillable from a URL
1799
+ default:
1800
+ data[field2.id] = raw;
1801
+ }
1802
+ }
1803
+ return data;
1804
+ }
1805
+
1725
1806
  // src/controller.ts
1726
1807
  var EMPTY_PAGE = { id: "empty", blocks: [] };
1727
1808
  var warnedNoClient = false;
@@ -1791,6 +1872,10 @@ function hasSubmittedOnce(form, formId) {
1791
1872
  if (form.settings.submissionLimit !== "once_per_visitor" || !formId) return false;
1792
1873
  return Boolean(readStoredSubmission(formId)?.submittedAt);
1793
1874
  }
1875
+ function submitFailureMessage(err) {
1876
+ if (isFilloError(err) && err.status && err.status > 0 && err.message) return err.message;
1877
+ return "Couldn't reach the server \u2014 check your connection and try again. If this keeps happening, a browser extension, firewall, or the site's Content-Security-Policy may be blocking the request.";
1878
+ }
1794
1879
  function createFormController(options2) {
1795
1880
  const { onChange, onSubmitted } = options2;
1796
1881
  const initial = normalizeFormSchema(options2.form);
@@ -1801,7 +1886,28 @@ function createFormController(options2) {
1801
1886
  let data = { ...options2.initialData ?? {} };
1802
1887
  let errors = {};
1803
1888
  let pageIndex = 0;
1804
- let status = hasSubmittedOnce(form, formId) ? "submitted" : "idle";
1889
+ let status = "idle";
1890
+ queueMicrotask(() => {
1891
+ if (status === "idle" && hasSubmittedOnce(form, formId)) {
1892
+ status = "submitted";
1893
+ notify();
1894
+ }
1895
+ if (typeof location !== "undefined" && location.search) {
1896
+ const params = Object.fromEntries(new URLSearchParams(location.search));
1897
+ let merged = null;
1898
+ for (const [key, value] of Object.entries(prefillFromParams(form, params))) {
1899
+ if (data[key] === void 0) {
1900
+ merged = merged ?? { ...data };
1901
+ merged[key] = value;
1902
+ }
1903
+ }
1904
+ if (merged && status === "idle") {
1905
+ data = merged;
1906
+ notify();
1907
+ }
1908
+ }
1909
+ });
1910
+ let submitError;
1805
1911
  const uploadingFields = /* @__PURE__ */ new Set();
1806
1912
  const listeners = /* @__PURE__ */ new Set();
1807
1913
  const mountedAt = Date.now();
@@ -1822,7 +1928,8 @@ function createFormController(options2) {
1822
1928
  blocks: visibleBlocks(page, data),
1823
1929
  isFirstPage: clamped === 0,
1824
1930
  isLastPage: clamped === pageCount - 1,
1825
- uploading: uploadingFields.size > 0
1931
+ uploading: uploadingFields.size > 0,
1932
+ submitError
1826
1933
  };
1827
1934
  }
1828
1935
  let snapshot = computeSnapshot();
@@ -1855,6 +1962,7 @@ function createFormController(options2) {
1855
1962
  }
1856
1963
  data = { ...data, [fieldId]: value };
1857
1964
  onChange?.(data);
1965
+ submitError = void 0;
1858
1966
  if (fieldId in errors) {
1859
1967
  errors = Object.fromEntries(Object.entries(errors).filter(([k]) => k !== fieldId));
1860
1968
  }
@@ -1897,10 +2005,24 @@ function createFormController(options2) {
1897
2005
  notify();
1898
2006
  return;
1899
2007
  }
2008
+ if (client && !formId && options2.resolveFormId) {
2009
+ status = "submitting";
2010
+ submitError = void 0;
2011
+ notify();
2012
+ try {
2013
+ formId = await options2.resolveFormId();
2014
+ } catch (err) {
2015
+ status = "idle";
2016
+ submitError = submitFailureMessage(err);
2017
+ notify();
2018
+ throw err;
2019
+ }
2020
+ }
1900
2021
  if (client && formId) {
1901
2022
  const visitorKey = form.settings.submissionLimit === "once_per_visitor" ? ensureSubmissionKey(formId) : void 0;
1902
2023
  const submissionKey = visitorKey ?? idempotencyKey;
1903
2024
  status = "submitting";
2025
+ submitError = void 0;
1904
2026
  notify();
1905
2027
  let res;
1906
2028
  try {
@@ -1912,6 +2034,7 @@ function createFormController(options2) {
1912
2034
  });
1913
2035
  } catch (err) {
1914
2036
  status = "idle";
2037
+ submitError = submitFailureMessage(err);
1915
2038
  notify();
1916
2039
  throw err;
1917
2040
  }
@@ -1968,6 +2091,122 @@ function createFormController(options2) {
1968
2091
  };
1969
2092
  }
1970
2093
 
2094
+ // src/auto-submit.ts
2095
+ function isAutoSubmitBlock(block) {
2096
+ return isField(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
2097
+ }
2098
+ function needsExplicitSubmit(visible) {
2099
+ const interactive = visible.filter((b) => !(isField(b) && b.kind === "hidden"));
2100
+ if (interactive.length === 0) return false;
2101
+ if (interactive.length > 1) return true;
2102
+ return !isAutoSubmitBlock(interactive[0]);
2103
+ }
2104
+ function shouldAutoSubmit(field2, value, ctx) {
2105
+ if (ctx.form.settings.submitMode !== "auto") return false;
2106
+ if (!ctx.isLastPage || ctx.status !== "idle" || ctx.uploading) return false;
2107
+ const hasDiscreteAnswer = (() => {
2108
+ switch (field2.kind) {
2109
+ case "select":
2110
+ case "dropdown":
2111
+ return typeof value === "string" && field2.options.some((option) => option.id === value);
2112
+ case "checkbox":
2113
+ return value === true;
2114
+ case "rating":
2115
+ case "linear_scale":
2116
+ return typeof value === "number";
2117
+ default:
2118
+ return false;
2119
+ }
2120
+ })();
2121
+ if (!hasDiscreteAnswer) return false;
2122
+ if (value === ctx.data[field2.id]) return false;
2123
+ const nextData = { ...ctx.data, [field2.id]: value };
2124
+ const interactive = visibleFields(ctx.form, nextData).filter((f) => f.kind !== "hidden");
2125
+ if (interactive.length !== 1 || interactive[0]?.id !== field2.id) return false;
2126
+ return validateResponse(ctx.form, nextData).ok;
2127
+ }
2128
+
2129
+ // src/appearance.ts
2130
+ var FILLO_SLOTS = [
2131
+ "root",
2132
+ "header",
2133
+ "title",
2134
+ "description",
2135
+ "pageTitle",
2136
+ "progress",
2137
+ "progressFill",
2138
+ "blocks",
2139
+ "field",
2140
+ "label",
2141
+ "fieldDescription",
2142
+ "control",
2143
+ "options",
2144
+ "option",
2145
+ "optionLabel",
2146
+ "error",
2147
+ "footer",
2148
+ "button",
2149
+ "success"
2150
+ ];
2151
+ var FILLO_DATA_ATTRS = {
2152
+ /** Slot name, on every slot element: `data-fillo="control"`. */
2153
+ slot: "data-fillo",
2154
+ /** Field kind, on the field wrapper: `data-kind="email"`. */
2155
+ kind: "data-kind",
2156
+ /** Field id, on the field wrapper (pre-existing). */
2157
+ field: "data-field",
2158
+ invalid: "data-invalid",
2159
+ required: "data-required",
2160
+ /** Selected option row / active scale step / active star. */
2161
+ selected: "data-selected",
2162
+ /** Checkbox / toggle checked state. */
2163
+ checked: "data-checked",
2164
+ dragOver: "data-drag-over",
2165
+ /** Engine status on the root: idle | submitting | submitted | error. */
2166
+ state: "data-state",
2167
+ page: "data-page",
2168
+ lastPage: "data-last-page"
2169
+ };
2170
+ function resolveSlotClass(appearance, state) {
2171
+ if (!appearance?.classNames && !appearance?.fields) return "";
2172
+ const parts = [];
2173
+ const general = appearance.classNames?.[state.slot];
2174
+ if (general) parts.push(typeof general === "function" ? general(state) : general);
2175
+ const field2 = state.fieldId ? appearance.fields?.[state.fieldId]?.[state.slot] : void 0;
2176
+ if (field2) parts.push(typeof field2 === "function" ? field2(state) : field2);
2177
+ return parts.filter(Boolean).join(" ");
2178
+ }
2179
+ function slotClass(base, appearance, state) {
2180
+ const extra = resolveSlotClass(appearance, state);
2181
+ return extra ? `${base} ${extra}` : base;
2182
+ }
2183
+
2184
+ // src/strings.ts
2185
+ var DEFAULT_STRINGS = {
2186
+ back: "Back",
2187
+ next: "Next",
2188
+ submit: "Submit",
2189
+ submitting: "Submitting\u2026",
2190
+ uploading: "Uploading\u2026",
2191
+ optional: " (optional)",
2192
+ other: "Other",
2193
+ otherPrompt: "Other \u2014 please specify",
2194
+ otherPlaceholder: "Your answer",
2195
+ choosePlaceholder: "Choose\u2026",
2196
+ successTitle: "Thanks!",
2197
+ successMessage: "Your response has been recorded.",
2198
+ closed: "This form is no longer accepting responses.",
2199
+ notLive: "This form isn't live yet.",
2200
+ submitFailed: "This form can't submit right now. Please try again in a moment.",
2201
+ loadFailedNotFound: "Form not found \u2014 check the form id and that it's published.",
2202
+ loadFailedNetwork: "Couldn't reach the server \u2014 check your connection or CORS.",
2203
+ loadFailed: "This form could not be loaded.",
2204
+ renderFailed: "This form could not be rendered."
2205
+ };
2206
+ function resolveStrings(overrides) {
2207
+ return overrides ? { ...DEFAULT_STRINGS, ...overrides } : DEFAULT_STRINGS;
2208
+ }
2209
+
1971
2210
  // src/define.ts
1972
2211
  function defineForm(def) {
1973
2212
  return {
@@ -1987,83 +2226,376 @@ function isCodeForm(form) {
1987
2226
  return typeof form === "object" && form !== null && "__filloCodeForm" in form;
1988
2227
  }
1989
2228
  var syncCache = /* @__PURE__ */ new Map();
2229
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2230
+ async function syncWithRetry(client, form) {
2231
+ let delay = 800;
2232
+ for (let attempt = 0; ; attempt++) {
2233
+ try {
2234
+ return await client.syncForm(form.id, form.schema, form.theme ?? null);
2235
+ } catch (err) {
2236
+ const status = isFilloError(err) ? err.status ?? 0 : 0;
2237
+ const retryable = !isFilloError(err) || status === 0 || status === 408 || status === 429 || status >= 500;
2238
+ if (!retryable || attempt >= 2) throw err;
2239
+ const hinted = isFilloError(err) ? err.retryAfterSec : void 0;
2240
+ await sleep(Math.min(hinted ? hinted * 1e3 : delay * (1 + Math.random() * 0.5), 15e3));
2241
+ delay *= 2;
2242
+ }
2243
+ }
2244
+ }
1990
2245
  function contentHash(input) {
1991
2246
  let h = 5381;
1992
2247
  for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) | 0;
1993
2248
  return (h >>> 0).toString(36);
1994
2249
  }
1995
- function syncCodeForm(client, form) {
1996
- const content = JSON.stringify(form.schema) + JSON.stringify(form.theme ?? null);
1997
- const key = `${client.baseUrl}:${client.key}:${form.id}:${contentHash(content)}`;
1998
- let pending = syncCache.get(key);
1999
- if (!pending) {
2000
- pending = client.syncForm(form.id, form.schema, form.theme ?? null);
2001
- syncCache.set(key, pending);
2002
- pending.catch(() => syncCache.delete(key));
2250
+ function storageKey(client, handle) {
2251
+ return `fillo:sync:${client.baseUrl}|${client.key}|${handle}`;
2252
+ }
2253
+ function readStoredSync(client, handle, hash) {
2254
+ try {
2255
+ const raw = globalThis.localStorage?.getItem(storageKey(client, handle));
2256
+ if (!raw) return null;
2257
+ const parsed = JSON.parse(raw);
2258
+ if (typeof parsed.formId !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number")
2259
+ return null;
2260
+ const ttl = parsed.status === "published" ? 36e5 : 6e4;
2261
+ if (Date.now() - parsed.ts > ttl) return null;
2262
+ return parsed;
2263
+ } catch {
2264
+ return null;
2003
2265
  }
2266
+ }
2267
+ function writeStoredSync(client, handle, hash, r) {
2268
+ try {
2269
+ globalThis.localStorage?.setItem(
2270
+ storageKey(client, handle),
2271
+ JSON.stringify({
2272
+ formId: r.formId,
2273
+ slug: r.slug,
2274
+ status: r.status,
2275
+ hash,
2276
+ ts: Date.now()
2277
+ })
2278
+ );
2279
+ } catch {
2280
+ }
2281
+ }
2282
+ function syncCodeForm(client, form, opts) {
2283
+ const content2 = JSON.stringify(form.schema) + JSON.stringify(form.theme ?? null);
2284
+ const hash = contentHash(content2);
2285
+ const key = `${client.baseUrl}:${client.key}:${form.id}:${hash}`;
2286
+ let pending = syncCache.get(key);
2287
+ if (pending && !opts?.bypassCache) return pending;
2288
+ if (!opts?.bypassCache) {
2289
+ const stored = readStoredSync(client, form.id, hash);
2290
+ if (stored) {
2291
+ pending = Promise.resolve({ formId: stored.formId, slug: stored.slug, status: stored.status });
2292
+ syncCache.set(key, pending);
2293
+ return pending;
2294
+ }
2295
+ }
2296
+ pending = syncWithRetry(client, form).then((r) => {
2297
+ writeStoredSync(client, form.id, hash, r);
2298
+ return r;
2299
+ });
2300
+ syncCache.set(key, pending);
2301
+ pending.catch(() => syncCache.delete(key));
2004
2302
  return pending;
2005
2303
  }
2006
2304
 
2007
- // src/prefill.ts
2008
- function parseDecimal(raw) {
2009
- const s = raw.trim();
2010
- if (!/^-?\d*\.?\d+$/.test(s)) return null;
2011
- const n = Number(s);
2012
- return Number.isFinite(n) ? n : null;
2305
+ // src/jsx.ts
2306
+ var FilloJsxError = class extends Error {
2307
+ constructor(code, message) {
2308
+ super(`[fillo] ${message} \u2014 https://fillo.so/docs/troubleshooting#jsx-${code.toLowerCase()}`);
2309
+ this.code = code;
2310
+ this.name = "FilloJsxError";
2311
+ }
2312
+ code;
2313
+ };
2314
+ var BRAND = /* @__PURE__ */ Symbol.for("fillo.block");
2315
+ var REACT_FRAGMENT = /* @__PURE__ */ Symbol.for("react.fragment");
2316
+ var FIELD_BASE = ["label", "description", "required", "placeholder"];
2317
+ var field = (name, kind, extra, opts = {}) => ({
2318
+ name,
2319
+ kind,
2320
+ props: [...FIELD_BASE, ...extra],
2321
+ ...opts
2322
+ });
2323
+ var content = (name, kind, textChildren) => ({
2324
+ name,
2325
+ kind,
2326
+ props: textChildren ? ["text"] : [],
2327
+ textChildren,
2328
+ content: true
2329
+ });
2330
+ var CHOICE_EXTRA = ["options", "allowOther", "shuffleOptions"];
2331
+ var JSX_BLOCK_SPECS = [
2332
+ field("Text", "short_text", ["maxLength"]),
2333
+ field("LongText", "long_text", ["maxLength"]),
2334
+ field("Email", "email", ["maxLength"]),
2335
+ field("Url", "url", ["maxLength"]),
2336
+ field("Phone", "phone", ["defaultCountry"]),
2337
+ field("Number", "number", ["min", "max"]),
2338
+ field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
2339
+ field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
2340
+ field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
2341
+ field("Checkbox", "checkbox", ["appearance"]),
2342
+ field("Rating", "rating", ["max"]),
2343
+ field("Scale", "linear_scale", ["min", "max", "minLabel", "maxLabel"]),
2344
+ field("Ranking", "ranking", ["options"], { optionChildren: true }),
2345
+ field("Matrix", "matrix", ["rows", "columns"]),
2346
+ field("Signature", "signature", []),
2347
+ field("Date", "date", []),
2348
+ field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
2349
+ field("Hidden", "hidden", ["paramName", "defaultValue"]),
2350
+ field("Custom", "custom", ["component", "config"]),
2351
+ content("Heading", "heading", true),
2352
+ content("Paragraph", "paragraph", true),
2353
+ { name: "Divider", kind: "divider", props: [], content: true }
2354
+ ];
2355
+ var PAGE_SPEC = { name: "Page", kind: "__page", props: ["title"] };
2356
+ var OPTION_SPEC = { name: "Option", kind: "__option", props: ["label", "icon"] };
2357
+ function makeInert(spec) {
2358
+ const component = () => {
2359
+ throw new FilloJsxError(
2360
+ "RENDERED_INERT",
2361
+ `<Fillo.${spec.name}> was rendered by React. Fillo fields are schema declarations, valid only inside <Fillo.Form> or Fillo.defineForm(). If this form lives in a server component, move it to a client module ("use client")`
2362
+ );
2363
+ };
2364
+ component[BRAND] = spec;
2365
+ return component;
2366
+ }
2367
+ var JSX_BLOCK_COMPONENTS = Object.fromEntries([
2368
+ ...JSX_BLOCK_SPECS.map((spec) => [spec.name, makeInert(spec)]),
2369
+ ["Page", makeInert(PAGE_SPEC)],
2370
+ ["Option", makeInert(OPTION_SPEC)]
2371
+ ]);
2372
+ var isElement = (v) => typeof v === "object" && v !== null && "type" in v && "props" in v;
2373
+ var brandOf = (type) => typeof type === "function" ? type[BRAND] : void 0;
2374
+ function flatten(children, out = []) {
2375
+ if (children === null || children === void 0 || typeof children === "boolean") return out;
2376
+ if (Array.isArray(children)) {
2377
+ for (const child of children) flatten(child, out);
2378
+ return out;
2379
+ }
2380
+ if (isElement(children)) {
2381
+ if (children.type === REACT_FRAGMENT) return flatten(children.props.children, out);
2382
+ out.push(children);
2383
+ return out;
2384
+ }
2385
+ throw new FilloJsxError(
2386
+ "NON_FILLO_CHILD",
2387
+ `Unexpected ${typeof children} child inside <Fillo.Form> \u2014 children define the schema; put text in <Fillo.Paragraph> and layout outside the form`
2388
+ );
2013
2389
  }
2014
- function prefillFromParams(form, params) {
2015
- const data = {};
2016
- for (const field of allFields(form)) {
2017
- const raw = field.kind === "hidden" ? params[field.paramName ?? field.id] ?? field.defaultValue : params[field.id];
2018
- if (raw === void 0 || raw === "") continue;
2019
- switch (field.kind) {
2020
- case "number": {
2021
- const n = parseDecimal(raw);
2022
- if (n === null) break;
2023
- if (field.min !== void 0 && n < field.min) break;
2024
- if (field.max !== void 0 && n > field.max) break;
2025
- data[field.id] = n;
2026
- break;
2027
- }
2028
- case "rating": {
2029
- const n = parseDecimal(raw);
2030
- if (n !== null && Number.isInteger(n) && n >= 1 && n <= (field.max ?? 5))
2031
- data[field.id] = n;
2032
- break;
2033
- }
2034
- case "linear_scale": {
2035
- const n = parseDecimal(raw);
2036
- const min = field.min ?? 1;
2037
- const max = field.max ?? 10;
2038
- if (n !== null && Number.isInteger(n) && n >= min && n <= max) data[field.id] = n;
2039
- break;
2040
- }
2041
- case "checkbox":
2042
- data[field.id] = raw === "true" || raw === "1" || raw === "yes";
2043
- break;
2044
- case "select":
2045
- case "dropdown": {
2046
- if (field.options.some((o) => o.id === raw)) data[field.id] = raw;
2047
- break;
2390
+ function classify(el) {
2391
+ const spec = brandOf(el.type);
2392
+ if (spec) return spec;
2393
+ const t = el.type;
2394
+ if (typeof t === "string") {
2395
+ throw new FilloJsxError(
2396
+ "NON_FILLO_CHILD",
2397
+ `<${t}> is layout, and <Fillo.Form> children define the schema only. Put markup outside the form, or compose your own layout with FilloProvider (headless)`
2398
+ );
2399
+ }
2400
+ if (typeof t === "object" && t !== null) {
2401
+ throw new FilloJsxError(
2402
+ "OPAQUE_TYPE",
2403
+ `A child of <Fillo.Form> has an opaque component type (lazy/memo or a server-component boundary). Define the form in a client module ("use client") and pass the form VALUE across boundaries instead`
2404
+ );
2405
+ }
2406
+ const name = typeof t === "function" && t.name ? ` (${t.name})` : "";
2407
+ throw new FilloJsxError(
2408
+ "WRAPPER_COMPONENT",
2409
+ `A plain component${name} is not a Fillo field \u2014 <Fillo.Form> never renders children, so wrappers are invisible to it. For reuse, call a plain function that RETURNS Fillo elements: {contactFields()}`
2410
+ );
2411
+ }
2412
+ function buildProps(spec, el) {
2413
+ const props = el.props;
2414
+ const allowed = /* @__PURE__ */ new Set([...spec.props, "id", "visibleIf", "children"]);
2415
+ for (const key of Object.keys(props)) {
2416
+ if (props[key] === void 0) continue;
2417
+ if (!allowed.has(key)) {
2418
+ const hint = key === "className" || key === "style" ? `styling lives on <Fillo.Form appearance={\u2026}>, never in the schema` : `schema props for Fillo.${spec.name}: ${[...spec.props].join(", ")}`;
2419
+ throw new FilloJsxError("UNKNOWN_PROP", `Fillo.${spec.name} has no prop "${key}" \u2014 ${hint}`);
2420
+ }
2421
+ }
2422
+ const id = props.id;
2423
+ if (typeof id !== "string" || id.length === 0 || id.length > 128) {
2424
+ throw new FilloJsxError(
2425
+ "MISSING_ID",
2426
+ `Every Fillo.${spec.name} needs a stable string id (1-128 chars) \u2014 ids key responses, logic, piping, and sync; never derive them from position`
2427
+ );
2428
+ }
2429
+ const out = { id, kind: spec.kind };
2430
+ if (spec.optionChildren && props.children !== void 0) {
2431
+ if (props.options !== void 0) {
2432
+ throw new FilloJsxError(
2433
+ "OPTION_PROP_CONFLICT",
2434
+ `Fillo.${spec.name} "${id}" has both an options prop and <Fillo.Option> children \u2014 pick one`
2435
+ );
2436
+ }
2437
+ out.__childOptions = flatten(props.children).map((child) => buildOption(child, spec, id));
2438
+ } else if (spec.textChildren && props.children !== void 0) {
2439
+ const parts = Array.isArray(props.children) ? props.children : [props.children];
2440
+ if (!parts.every((p) => typeof p === "string" || typeof p === "number")) {
2441
+ throw new FilloJsxError(
2442
+ "TEXT_CHILD_REQUIRED",
2443
+ `Fillo.${spec.name} children must be plain text (piping tokens like {{name}} included) \u2014 expressions and elements can't serialize into the schema`
2444
+ );
2445
+ }
2446
+ if (props.text !== void 0) {
2447
+ throw new FilloJsxError(
2448
+ "OPTION_PROP_CONFLICT",
2449
+ `Fillo.${spec.name} "${id}" has both a text prop and children \u2014 pick one`
2450
+ );
2451
+ }
2452
+ out.__childText = parts.join("");
2453
+ } else if (props.children !== void 0) {
2454
+ throw new FilloJsxError(
2455
+ "NON_FILLO_CHILD",
2456
+ `Fillo.${spec.name} takes no children \u2014 its configuration is props-only`
2457
+ );
2458
+ }
2459
+ for (const key of spec.props) {
2460
+ if (key === "options" && out.__childOptions !== void 0) {
2461
+ out.options = out.__childOptions;
2462
+ continue;
2463
+ }
2464
+ if (key === "text" && out.__childText !== void 0) {
2465
+ out.text = out.__childText;
2466
+ continue;
2467
+ }
2468
+ if (props[key] !== void 0) out[key] = props[key];
2469
+ }
2470
+ delete out.__childOptions;
2471
+ delete out.__childText;
2472
+ if (spec.textChildren && out.text === void 0) {
2473
+ throw new FilloJsxError("TEXT_CHILD_REQUIRED", `Fillo.${spec.name} "${id}" has no text`);
2474
+ }
2475
+ if (props.visibleIf !== void 0) {
2476
+ const conditions2 = Array.isArray(props.visibleIf) ? props.visibleIf : [props.visibleIf];
2477
+ out.visibleIf = conditions2;
2478
+ }
2479
+ return out;
2480
+ }
2481
+ function buildOption(el, parent, parentId) {
2482
+ const spec = classify(el);
2483
+ if (spec.kind !== "__option") {
2484
+ throw new FilloJsxError(
2485
+ "NON_FILLO_CHILD",
2486
+ `Fillo.${parent.name} "${parentId}" children must be <Fillo.Option> elements, found Fillo.${spec.name}`
2487
+ );
2488
+ }
2489
+ const props = el.props;
2490
+ for (const key of Object.keys(props)) {
2491
+ if (props[key] !== void 0 && key !== "id" && key !== "label" && key !== "icon") {
2492
+ throw new FilloJsxError("UNKNOWN_PROP", `Fillo.Option has no prop "${key}" \u2014 it takes id, label, icon`);
2493
+ }
2494
+ }
2495
+ if (typeof props.id !== "string" || props.id.length === 0) {
2496
+ throw new FilloJsxError(
2497
+ "MISSING_ID",
2498
+ `Every <Fillo.Option> in "${parentId}" needs a stable string id \u2014 it's the stored answer value`
2499
+ );
2500
+ }
2501
+ if (typeof props.label !== "string") {
2502
+ throw new FilloJsxError("MISSING_ID", `<Fillo.Option id="${props.id}"> needs a label`);
2503
+ }
2504
+ const option = { id: props.id, label: props.label };
2505
+ if (props.icon !== void 0) option.icon = props.icon;
2506
+ return option;
2507
+ }
2508
+ function schemaFromJsx(children, meta) {
2509
+ const elements = flatten(children);
2510
+ const pageSpecs = elements.map((el) => ({ el, spec: classify(el) }));
2511
+ const pages = [];
2512
+ const isPage = (s) => s.kind === "__page";
2513
+ if (pageSpecs.some(({ spec }) => isPage(spec))) {
2514
+ if (!pageSpecs.every(({ spec }) => isPage(spec))) {
2515
+ throw new FilloJsxError(
2516
+ "PAGE_MIX",
2517
+ `<Fillo.Form> children must be EITHER all <Fillo.Page> elements or all blocks \u2014 wrap the loose blocks in a page`
2518
+ );
2519
+ }
2520
+ for (const { el } of pageSpecs) {
2521
+ const built = buildProps(PAGE_SPEC, { type: el.type, props: { ...el.props, children: void 0 } });
2522
+ const blocks = flatten(el.props.children).map((child) => {
2523
+ const spec = classify(child);
2524
+ if (isPage(spec) || spec.kind === "__option") {
2525
+ throw new FilloJsxError("PAGE_MIX", `<Fillo.${spec.name}> can't appear inside <Fillo.Page>`);
2526
+ }
2527
+ return buildProps(spec, child);
2528
+ });
2529
+ pages.push({
2530
+ id: built.id,
2531
+ ...built.title !== void 0 ? { title: built.title } : {},
2532
+ blocks
2533
+ });
2534
+ }
2535
+ } else {
2536
+ const blocks = pageSpecs.map(({ el, spec }) => {
2537
+ if (spec.kind === "__option") {
2538
+ throw new FilloJsxError(
2539
+ "NON_FILLO_CHILD",
2540
+ `<Fillo.Option> only makes sense inside a choice field (Select/MultiSelect/Dropdown/Ranking)`
2541
+ );
2048
2542
  }
2049
- case "multi_select": {
2050
- const ids = new Set(field.options.map((o) => o.id));
2051
- const picked = raw.split(",").filter((v) => ids.has(v));
2052
- if (picked.length > 0) data[field.id] = picked;
2053
- break;
2543
+ return buildProps(spec, el);
2544
+ });
2545
+ pages.push({ id: "main", blocks });
2546
+ }
2547
+ const seen = /* @__PURE__ */ new Set();
2548
+ for (const page of pages) {
2549
+ for (const block of page.blocks) {
2550
+ if (seen.has(block.id)) {
2551
+ throw new FilloJsxError(
2552
+ "DUPLICATE_ID",
2553
+ `Two blocks share the id "${block.id}" \u2014 ids key responses and must be unique across the whole form`
2554
+ );
2054
2555
  }
2055
- case "file_upload":
2056
- case "ranking":
2057
- case "matrix":
2058
- case "signature":
2059
- case "custom":
2060
- break;
2061
- // not prefillable from a URL
2062
- default:
2063
- data[field.id] = raw;
2556
+ seen.add(block.id);
2064
2557
  }
2065
2558
  }
2066
- return data;
2559
+ return {
2560
+ version: 1,
2561
+ title: meta.title ?? "",
2562
+ ...meta.description !== void 0 ? { description: meta.description } : {},
2563
+ pages,
2564
+ settings: meta.settings ?? {}
2565
+ };
2566
+ }
2567
+ function codeFormFromJsx(meta, children) {
2568
+ if (typeof meta.id !== "string" || meta.id.length === 0) {
2569
+ throw new FilloJsxError("MISSING_ID", `<Fillo.Form> needs an id \u2014 it names the form in your workspace`);
2570
+ }
2571
+ const schema = schemaFromJsx(children, {
2572
+ id: meta.id,
2573
+ title: meta.title,
2574
+ description: meta.description,
2575
+ settings: meta.settings
2576
+ });
2577
+ return defineForm({
2578
+ id: meta.id,
2579
+ title: schema.title,
2580
+ description: schema.description,
2581
+ pages: schema.pages,
2582
+ settings: schema.settings,
2583
+ theme: meta.theme
2584
+ });
2585
+ }
2586
+
2587
+ // src/when.ts
2588
+ function when(fieldId) {
2589
+ const cond = (op, value) => Object.freeze(value === void 0 ? { fieldId, op } : { fieldId, op, value });
2590
+ return {
2591
+ eq: (value) => cond("eq", value),
2592
+ neq: (value) => cond("neq", value),
2593
+ contains: (value) => cond("contains", value),
2594
+ gt: (value) => cond("gt", value),
2595
+ lt: (value) => cond("lt", value),
2596
+ answered: () => cond("answered"),
2597
+ notAnswered: () => cond("not_answered")
2598
+ };
2067
2599
  }
2068
2600
 
2069
2601
  // src/piping.ts
@@ -2072,10 +2604,10 @@ function resolveText(text, data, form) {
2072
2604
  if (!text.includes("{{")) return text;
2073
2605
  const fields = allFields(form);
2074
2606
  return text.replace(TOKEN, (_, id) => {
2075
- const field = fields.find((f) => f.id === id);
2607
+ const field2 = fields.find((f) => f.id === id);
2076
2608
  const value = data[id];
2077
2609
  if (value === null || value === void 0 || value === "") return "";
2078
- const label = (optionId) => field && "options" in field ? field.options.find((o) => o.id === optionId)?.label ?? optionId : optionId;
2610
+ const label = (optionId) => field2 && "options" in field2 ? field2.options.find((o) => o.id === optionId)?.label ?? optionId : optionId;
2079
2611
  if (Array.isArray(value)) return value.map((v) => label(String(v))).join(", ");
2080
2612
  if (typeof value === "string") return label(value);
2081
2613
  return String(value);
@@ -2096,15 +2628,22 @@ function pipeBlock(block, data, form) {
2096
2628
  export {
2097
2629
  BLOCK_KIND_META,
2098
2630
  CONTENT_KINDS,
2631
+ DEFAULT_STRINGS,
2099
2632
  DRAFT_KINDS,
2633
+ FILLO_DATA_ATTRS,
2100
2634
  FILLO_SCHEMA_VERSION,
2101
2635
  FILLO_SDK_VERSION,
2636
+ FILLO_SLOTS,
2102
2637
  FilloClient,
2103
2638
  FilloError,
2639
+ FilloJsxError,
2640
+ JSX_BLOCK_COMPONENTS,
2641
+ JSX_BLOCK_SPECS,
2104
2642
  PHONE_COUNTRIES,
2105
2643
  Sha1,
2106
2644
  allFields,
2107
2645
  assembleForm,
2646
+ codeFormFromJsx,
2108
2647
  contentHash,
2109
2648
  countryByDialCode,
2110
2649
  countryByIso,
@@ -2119,23 +2658,32 @@ export {
2119
2658
  flagEmoji,
2120
2659
  formatAnswer,
2121
2660
  formatNational,
2661
+ isAutoSubmitBlock,
2122
2662
  isBlockVisible,
2123
2663
  isCodeForm,
2124
2664
  isField,
2665
+ isFilloError,
2125
2666
  isPossiblePhone,
2667
+ needsExplicitSubmit,
2126
2668
  normalizeFormSchema,
2127
2669
  normalizeFormTheme,
2128
2670
  parsePhone,
2129
2671
  pipeBlock,
2130
2672
  prefillFromParams,
2131
2673
  provisionWorkspace,
2674
+ resolveSlotClass,
2675
+ resolveStrings,
2132
2676
  resolveText,
2677
+ schemaFromJsx,
2133
2678
  sha1Base64,
2679
+ shouldAutoSubmit,
2680
+ slotClass,
2134
2681
  syncCodeForm,
2135
2682
  toE164,
2136
2683
  validateField,
2137
2684
  validateFormSchema,
2138
2685
  validateResponse,
2139
2686
  visibleBlocks,
2140
- visibleFields
2687
+ visibleFields,
2688
+ when
2141
2689
  };