@usefillo/core 0.5.0 → 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 +259 -20
  2. package/dist/index.js +968 -172
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13,8 +13,16 @@ function isAnswered(value) {
13
13
  if (typeof value === "object") return Object.keys(value).length > 0;
14
14
  return true;
15
15
  }
16
- function evalCondition(cond, data) {
17
- const value = data[cond.fieldId];
16
+ function valueEquals(value, target) {
17
+ if (value === target) return true;
18
+ if (typeof value === "number" && typeof target === "string" && target.trim() !== "")
19
+ return Number(target) === value;
20
+ if (typeof target === "number" && typeof value === "string" && value.trim() !== "")
21
+ return Number(value) === target;
22
+ return false;
23
+ }
24
+ function evalCondition(cond, resolve) {
25
+ const value = resolve(cond.fieldId);
18
26
  switch (cond.op) {
19
27
  case "answered":
20
28
  return isAnswered(value);
@@ -22,10 +30,11 @@ function evalCondition(cond, data) {
22
30
  return !isAnswered(value);
23
31
  case "eq":
24
32
  if (Array.isArray(value)) return value.some((v) => v === cond.value);
25
- return value === cond.value;
33
+ return valueEquals(value, cond.value);
26
34
  case "neq":
35
+ if (!isAnswered(value)) return false;
27
36
  if (Array.isArray(value)) return !value.some((v) => v === cond.value);
28
- return value !== cond.value;
37
+ return !valueEquals(value, cond.value);
29
38
  case "contains":
30
39
  if (typeof value === "string")
31
40
  return value.toLowerCase().includes(String(cond.value ?? "").toLowerCase());
@@ -37,22 +46,45 @@ function evalCondition(cond, data) {
37
46
  return typeof value === "number" && value < Number(cond.value);
38
47
  }
39
48
  }
40
- function isBlockVisible(block, data) {
49
+ function blockVisibleWith(block, resolve) {
41
50
  if (!block.visibleIf || block.visibleIf.length === 0) return true;
42
- return block.visibleIf.every((cond) => evalCondition(cond, data));
51
+ return block.visibleIf.every((cond) => evalCondition(cond, resolve));
52
+ }
53
+ function makeResolver(data, scoped, visible) {
54
+ return (fieldId) => scoped.has(fieldId) && !visible.has(fieldId) ? void 0 : data[fieldId];
55
+ }
56
+ function visibleFieldIds(fields, data) {
57
+ const scoped = new Set(fields.map((f) => f.id));
58
+ let visible = scoped;
59
+ for (let i = 0; i <= fields.length; i++) {
60
+ const resolve = makeResolver(data, scoped, visible);
61
+ const next = /* @__PURE__ */ new Set();
62
+ for (const f of fields) if (blockVisibleWith(f, resolve)) next.add(f.id);
63
+ if (next.size === visible.size && [...next].every((id) => visible.has(id))) return next;
64
+ visible = next;
65
+ }
66
+ return visible;
67
+ }
68
+ function isBlockVisible(block, data) {
69
+ return blockVisibleWith(block, (fieldId) => data[fieldId]);
43
70
  }
44
71
  function visibleBlocks(page, data) {
45
- return page.blocks.filter((b) => isBlockVisible(b, data));
72
+ const fields = page.blocks.filter(isField);
73
+ const scoped = new Set(fields.map((f) => f.id));
74
+ const resolve = makeResolver(data, scoped, visibleFieldIds(fields, data));
75
+ return page.blocks.filter((b) => blockVisibleWith(b, resolve));
46
76
  }
47
77
  function allFields(form) {
48
78
  return form.pages.flatMap((p) => p.blocks.filter(isField));
49
79
  }
50
80
  function visibleFields(form, data) {
51
- return form.pages.flatMap((p) => visibleBlocks(p, data).filter(isField));
81
+ const fields = allFields(form);
82
+ const visible = visibleFieldIds(fields, data);
83
+ return fields.filter((f) => visible.has(f.id));
52
84
  }
53
85
 
54
86
  // src/validation.ts
55
- import { z } from "zod";
87
+ import * as z from "zod/mini";
56
88
 
57
89
  // src/phone.ts
58
90
  var C = (iso2, name, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name, dialCode, lengths, groups, example });
@@ -129,6 +161,138 @@ var PHONE_COUNTRIES = [
129
161
  C("KW", "Kuwait", "965", [8], [4, 4], "50012345")
130
162
  ];
131
163
  var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
164
+ var timeZoneIso = {
165
+ "Africa/Cairo": "EG",
166
+ "Africa/Casablanca": "MA",
167
+ "Africa/Johannesburg": "ZA",
168
+ "Africa/Lagos": "NG",
169
+ "Africa/Nairobi": "KE",
170
+ "America/Adak": "US",
171
+ "America/Anchorage": "US",
172
+ "America/Argentina/Buenos_Aires": "AR",
173
+ "America/Argentina/Cordoba": "AR",
174
+ "America/Argentina/Mendoza": "AR",
175
+ "America/Bahia": "BR",
176
+ "America/Belem": "BR",
177
+ "America/Bogota": "CO",
178
+ "America/Boise": "US",
179
+ "America/Boa_Vista": "BR",
180
+ "America/Cancun": "MX",
181
+ "America/Chicago": "US",
182
+ "America/Chihuahua": "MX",
183
+ "America/Campo_Grande": "BR",
184
+ "America/Costa_Rica": "CR",
185
+ "America/Cuiaba": "BR",
186
+ "America/Denver": "US",
187
+ "America/Detroit": "US",
188
+ "America/Edmonton": "CA",
189
+ "America/Fortaleza": "BR",
190
+ "America/Guayaquil": "EC",
191
+ "America/Halifax": "CA",
192
+ "America/Hermosillo": "MX",
193
+ "America/Indiana/Indianapolis": "US",
194
+ "America/Indianapolis": "US",
195
+ "America/Juneau": "US",
196
+ "America/Kentucky/Louisville": "US",
197
+ "America/Lima": "PE",
198
+ "America/Los_Angeles": "US",
199
+ "America/Maceio": "BR",
200
+ "America/Manaus": "BR",
201
+ "America/Matamoros": "MX",
202
+ "America/Mazatlan": "MX",
203
+ "America/Merida": "MX",
204
+ "America/Mexico_City": "MX",
205
+ "America/Moncton": "CA",
206
+ "America/Monterrey": "MX",
207
+ "America/Montevideo": "UY",
208
+ "America/New_York": "US",
209
+ "America/Noronha": "BR",
210
+ "America/Nome": "US",
211
+ "America/North_Dakota/Center": "US",
212
+ "America/Ojinaga": "MX",
213
+ "America/Phoenix": "US",
214
+ "America/Porto_Velho": "BR",
215
+ "America/Punta_Arenas": "CL",
216
+ "America/Recife": "BR",
217
+ "America/Regina": "CA",
218
+ "America/Rio_Branco": "BR",
219
+ "America/Santiago": "CL",
220
+ "America/Sao_Paulo": "BR",
221
+ "America/Sitka": "US",
222
+ "America/St_Johns": "CA",
223
+ "America/Tijuana": "MX",
224
+ "America/Toronto": "CA",
225
+ "America/Vancouver": "CA",
226
+ "America/Whitehorse": "CA",
227
+ "America/Winnipeg": "CA",
228
+ "America/Yakutat": "US",
229
+ "Asia/Bangkok": "TH",
230
+ "Asia/Calcutta": "IN",
231
+ "Asia/Dacca": "BD",
232
+ "Asia/Dhaka": "BD",
233
+ "Asia/Dubai": "AE",
234
+ "Asia/Hong_Kong": "HK",
235
+ "Asia/Ho_Chi_Minh": "VN",
236
+ "Asia/Jakarta": "ID",
237
+ "Asia/Jerusalem": "IL",
238
+ "Asia/Karachi": "PK",
239
+ "Asia/Kolkata": "IN",
240
+ "Asia/Kuala_Lumpur": "MY",
241
+ "Asia/Kuwait": "KW",
242
+ "Asia/Manila": "PH",
243
+ "Asia/Qatar": "QA",
244
+ "Asia/Riyadh": "SA",
245
+ "Asia/Seoul": "KR",
246
+ "Asia/Shanghai": "CN",
247
+ "Asia/Saigon": "VN",
248
+ "Asia/Singapore": "SG",
249
+ "Asia/Taipei": "TW",
250
+ "Asia/Tel_Aviv": "IL",
251
+ "Asia/Tokyo": "JP",
252
+ "Asia/Urumqi": "CN",
253
+ "Atlantic/Reykjavik": "IS",
254
+ "Australia/Adelaide": "AU",
255
+ "Australia/Brisbane": "AU",
256
+ "Australia/Darwin": "AU",
257
+ "Australia/Hobart": "AU",
258
+ "Australia/Melbourne": "AU",
259
+ "Australia/Perth": "AU",
260
+ "Australia/Sydney": "AU",
261
+ "Europe/Amsterdam": "NL",
262
+ "Europe/Athens": "GR",
263
+ "Europe/Belgrade": "RS",
264
+ "Europe/Berlin": "DE",
265
+ "Europe/Bratislava": "SK",
266
+ "Europe/Brussels": "BE",
267
+ "Europe/Bucharest": "RO",
268
+ "Europe/Budapest": "HU",
269
+ "Europe/Copenhagen": "DK",
270
+ "Europe/Dublin": "IE",
271
+ "Europe/Helsinki": "FI",
272
+ "Europe/Zagreb": "HR",
273
+ "Europe/Istanbul": "TR",
274
+ "Europe/Kiev": "UA",
275
+ "Europe/Kyiv": "UA",
276
+ "Europe/Lisbon": "PT",
277
+ "Europe/Ljubljana": "SI",
278
+ "Europe/London": "GB",
279
+ "Europe/Luxembourg": "LU",
280
+ "Europe/Madrid": "ES",
281
+ "Europe/Moscow": "RU",
282
+ "Europe/Oslo": "NO",
283
+ "Europe/Paris": "FR",
284
+ "Europe/Prague": "CZ",
285
+ "Europe/Riga": "LV",
286
+ "Europe/Rome": "IT",
287
+ "Europe/Sofia": "BG",
288
+ "Europe/Stockholm": "SE",
289
+ "Europe/Tallinn": "EE",
290
+ "Europe/Vienna": "AT",
291
+ "Europe/Vilnius": "LT",
292
+ "Europe/Warsaw": "PL",
293
+ "Europe/Zurich": "CH",
294
+ "Pacific/Auckland": "NZ"
295
+ };
132
296
  function flagEmoji(iso2) {
133
297
  const cc = iso2.toUpperCase();
134
298
  if (!/^[A-Z]{2}$/.test(cc)) return "\u{1F3F3}\uFE0F";
@@ -137,6 +301,9 @@ function flagEmoji(iso2) {
137
301
  function countryByIso(iso2) {
138
302
  return iso2 ? byIso.get(iso2.toUpperCase()) : void 0;
139
303
  }
304
+ function countryByTimeZone(timeZone) {
305
+ return countryByIso(timeZone ? timeZoneIso[timeZone] : void 0);
306
+ }
140
307
  function countryByDialCode(digits) {
141
308
  let best;
142
309
  for (const c of PHONE_COUNTRIES) {
@@ -200,11 +367,11 @@ function isPossiblePhone(value) {
200
367
 
201
368
  // src/validation.ts
202
369
  var fileValueSchema = z.object({
203
- fileId: z.string().min(1),
370
+ fileId: z.string().check(z.minLength(1)),
204
371
  name: z.string(),
205
- size: z.number().nonnegative(),
372
+ size: z.number().check(z.nonnegative()),
206
373
  mime: z.string(),
207
- url: z.string().optional()
374
+ url: z.optional(z.string())
208
375
  });
209
376
  function isEmpty(value) {
210
377
  if (value === null || value === void 0) return true;
@@ -214,18 +381,18 @@ function isEmpty(value) {
214
381
  if (typeof value === "object") return Object.keys(value).length === 0;
215
382
  return false;
216
383
  }
217
- function validateField(field, value) {
384
+ function validateField(field2, value) {
218
385
  if (isEmpty(value)) {
219
- return field.required ? "This field is required" : null;
386
+ return field2.required ? "This field is required" : null;
220
387
  }
221
- switch (field.kind) {
388
+ switch (field2.kind) {
222
389
  case "short_text":
223
390
  case "long_text": {
224
391
  if (typeof value !== "string") return "Expected text";
225
- if (field.maxLength && value.length > field.maxLength)
226
- return `Must be at most ${field.maxLength} characters`;
227
- const hardCap = field.kind === "long_text" ? 2e4 : 2e3;
228
- 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";
229
396
  return null;
230
397
  }
231
398
  case "email": {
@@ -251,26 +418,26 @@ function validateField(field, value) {
251
418
  }
252
419
  case "number": {
253
420
  const n = typeof value === "string" ? Number(value) : value;
254
- if (typeof n !== "number" || Number.isNaN(n)) return "Enter a number";
255
- if (field.min !== void 0 && n < field.min) return `Must be at least ${field.min}`;
256
- if (field.max !== void 0 && n > field.max) return `Must be at most ${field.max}`;
421
+ if (typeof n !== "number" || !Number.isFinite(n)) return "Enter a number";
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}`;
257
424
  return null;
258
425
  }
259
426
  case "select":
260
427
  case "dropdown": {
261
428
  if (typeof value !== "string" || value === "") return "Pick an option";
262
- if (field.options.some((o) => o.id === value)) return null;
263
- 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;
264
431
  return "Pick a valid option";
265
432
  }
266
433
  case "multi_select": {
267
434
  if (!Array.isArray(value)) return "Pick at least one option";
268
- const ids = new Set(field.options.map((o) => o.id));
435
+ const ids = new Set(field2.options.map((o) => o.id));
269
436
  const arr = value;
270
437
  let others = 0;
271
438
  for (const v of arr) {
272
439
  if (ids.has(v)) continue;
273
- 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)
274
441
  return "Pick valid options";
275
442
  if (++others > 1) return "Only one 'Other' answer is allowed";
276
443
  }
@@ -280,15 +447,15 @@ function validateField(field, value) {
280
447
  return typeof value === "boolean" ? null : "Invalid value";
281
448
  }
282
449
  case "rating": {
283
- const max = field.max ?? 5;
450
+ const max = field2.max ?? 5;
284
451
  const n = typeof value === "string" ? Number(value) : value;
285
452
  if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > max)
286
453
  return `Pick a rating between 1 and ${max}`;
287
454
  return null;
288
455
  }
289
456
  case "linear_scale": {
290
- const min = field.min ?? 1;
291
- const max = field.max ?? 10;
457
+ const min = field2.min ?? 1;
458
+ const max = field2.max ?? 10;
292
459
  const n = typeof value === "string" ? Number(value) : value;
293
460
  if (typeof n !== "number" || !Number.isInteger(n) || n < min || n > max)
294
461
  return `Pick a value between ${min} and ${max}`;
@@ -296,7 +463,7 @@ function validateField(field, value) {
296
463
  }
297
464
  case "ranking": {
298
465
  if (!Array.isArray(value)) return "Drag the options into order";
299
- const ids = field.options.map((o) => o.id).sort();
466
+ const ids = field2.options.map((o) => o.id).sort();
300
467
  const got = [...value].sort();
301
468
  if (ids.length !== got.length || ids.some((id, i) => id !== got[i]))
302
469
  return "Rank every option";
@@ -306,12 +473,12 @@ function validateField(field, value) {
306
473
  if (typeof value !== "object" || value === null || Array.isArray(value))
307
474
  return "Answer the grid";
308
475
  const answers = value;
309
- const columnIds = new Set(field.columns.map((c) => c.id));
310
- 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));
311
478
  for (const [rowId, columnId] of Object.entries(answers)) {
312
479
  if (!rowIds.has(rowId) || !columnIds.has(columnId)) return "Invalid grid answer";
313
480
  }
314
- if (field.required && Object.keys(answers).length < field.rows.length)
481
+ if (field2.required && Object.keys(answers).length < field2.rows.length)
315
482
  return "Answer every row";
316
483
  return null;
317
484
  }
@@ -329,12 +496,16 @@ function validateField(field, value) {
329
496
  case "date": {
330
497
  if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
331
498
  return "Enter a valid date";
499
+ const [y, m, d] = value.split("-").map(Number);
500
+ const dt = new Date(Date.UTC(y, m - 1, d));
501
+ if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d)
502
+ return "Enter a valid date";
332
503
  return null;
333
504
  }
334
505
  case "file_upload": {
335
506
  const r = z.array(fileValueSchema).safeParse(value);
336
507
  if (!r.success) return "Upload did not complete";
337
- const maxFiles = field.maxFiles ?? 1;
508
+ const maxFiles = field2.maxFiles ?? 1;
338
509
  if (r.data.length > maxFiles)
339
510
  return `At most ${maxFiles} file${maxFiles === 1 ? "" : "s"}`;
340
511
  return null;
@@ -345,24 +516,24 @@ function validateResponse(form, data) {
345
516
  const fields = visibleFields(form, data);
346
517
  const errors = {};
347
518
  const cleaned = {};
348
- for (const field of fields) {
349
- const value = normalizeValue(field, data[field.id]);
350
- 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);
351
522
  if (error) {
352
- errors[field.id] = error;
523
+ errors[field2.id] = error;
353
524
  } else if (!isEmpty(value)) {
354
- cleaned[field.id] = value;
525
+ cleaned[field2.id] = value;
355
526
  }
356
527
  }
357
528
  return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
358
529
  }
359
- function normalizeValue(field, value) {
530
+ function normalizeValue(field2, value) {
360
531
  if (value === void 0 || value === null) return value;
361
- if (field.kind === "number" && typeof value === "string" && value.trim() !== "") {
532
+ if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
362
533
  const n = Number(value);
363
534
  return Number.isNaN(n) ? value : n;
364
535
  }
365
- if (field.kind === "rating" && typeof value === "string") {
536
+ if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
366
537
  const n = Number(value);
367
538
  return Number.isNaN(n) ? value : n;
368
539
  }
@@ -371,7 +542,7 @@ function normalizeValue(field, value) {
371
542
  }
372
543
 
373
544
  // src/schema-validation.ts
374
- import { z as z2 } from "zod";
545
+ import * as z2 from "zod/mini";
375
546
  var FIELD_KINDS = [
376
547
  "short_text",
377
548
  "long_text",
@@ -395,27 +566,31 @@ var FIELD_KINDS = [
395
566
  ];
396
567
  var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
397
568
  var ALL_KINDS = [...FIELD_KINDS, ...CONTENT_KINDS2];
398
- var idSchema = z2.string().min(1).max(128);
399
- var optionSchema = z2.object({ id: idSchema, label: z2.string().max(1e3), icon: z2.unknown().optional() }).passthrough();
400
- var blockSchema = z2.object({
569
+ var idSchema = z2.string().check(z2.minLength(1), z2.maxLength(128));
570
+ var optionSchema = z2.looseObject({
571
+ id: idSchema,
572
+ label: z2.string().check(z2.maxLength(1e3)),
573
+ icon: z2.optional(z2.unknown())
574
+ });
575
+ var blockSchema = z2.looseObject({
401
576
  id: idSchema,
402
577
  kind: z2.enum(ALL_KINDS),
403
- label: z2.string().max(2e3).optional(),
404
- options: z2.array(optionSchema).max(200).optional(),
405
- rows: z2.array(optionSchema).max(200).optional(),
406
- columns: z2.array(optionSchema).max(200).optional()
407
- }).passthrough();
578
+ label: z2.optional(z2.string().check(z2.maxLength(2e3))),
579
+ options: z2.optional(z2.array(optionSchema).check(z2.maxLength(200))),
580
+ rows: z2.optional(z2.array(optionSchema).check(z2.maxLength(200))),
581
+ columns: z2.optional(z2.array(optionSchema).check(z2.maxLength(200)))
582
+ });
408
583
  var pageSchema = z2.object({
409
584
  id: idSchema,
410
- title: z2.string().max(500).optional(),
411
- blocks: z2.array(blockSchema).max(500)
585
+ title: z2.optional(z2.string().check(z2.maxLength(500))),
586
+ blocks: z2.array(blockSchema).check(z2.maxLength(500))
412
587
  });
413
588
  var schemaShape = z2.object({
414
589
  version: z2.literal(1),
415
- title: z2.string().max(2e3).optional().default(""),
416
- description: z2.string().max(5e3).optional(),
417
- pages: z2.array(pageSchema).min(1).max(50),
418
- settings: z2.record(z2.string(), z2.unknown()).optional().default({})
590
+ title: z2._default(z2.optional(z2.string().check(z2.maxLength(2e3))), ""),
591
+ description: z2.optional(z2.string().check(z2.maxLength(5e3))),
592
+ pages: z2.array(pageSchema).check(z2.minLength(1), z2.maxLength(50)),
593
+ settings: z2._default(z2.optional(z2.record(z2.string(), z2.unknown())), {})
419
594
  });
420
595
  var MAX_SCHEMA_VERSION = 1;
421
596
  var FILLO_SCHEMA_VERSION = 1;
@@ -505,7 +680,7 @@ function normalizeSettings(value) {
505
680
  redirectUrl: normalizeUrl(rec.redirectUrl),
506
681
  showProgress: bool(rec.showProgress),
507
682
  submissionLimit,
508
- notifyEmail: z2.email().max(254).safeParse(rec.notifyEmail).success ? rec.notifyEmail : void 0,
683
+ notifyEmail: z2.email().check(z2.maxLength(254)).safeParse(rec.notifyEmail).success ? rec.notifyEmail : void 0,
509
684
  sendReceipt: bool(rec.sendReceipt)
510
685
  };
511
686
  }
@@ -694,21 +869,26 @@ function normalizeFormTheme(input) {
694
869
  function assertNever(x) {
695
870
  throw new Error(`Unhandled field kind: ${JSON.stringify(x)}`);
696
871
  }
697
- function formatAnswer(field, value) {
872
+ function formatAnswer(field2, value) {
698
873
  if (value === null || value === void 0 || value === "") return "";
699
- switch (field.kind) {
874
+ switch (field2.kind) {
700
875
  case "file_upload":
876
+ if (!Array.isArray(value)) return String(value);
701
877
  return value.map((f) => f.name).join(", ");
702
878
  case "select":
703
879
  case "dropdown":
704
- return field.options.find((o) => o.id === value)?.label ?? String(value);
880
+ return field2.options.find((o) => o.id === value)?.label ?? String(value);
705
881
  case "multi_select":
706
- return value.map((id) => field.options.find((o) => o.id === id)?.label ?? id).join(", ");
882
+ if (!Array.isArray(value)) return String(value);
883
+ return value.map((id) => field2.options.find((o) => o.id === id)?.label ?? id).join(", ");
707
884
  case "ranking":
708
- return value.map((id, i) => `${i + 1}. ${field.options.find((o) => o.id === id)?.label ?? id}`).join(", ");
885
+ if (!Array.isArray(value)) return String(value);
886
+ return value.map((id, i) => `${i + 1}. ${field2.options.find((o) => o.id === id)?.label ?? id}`).join(", ");
709
887
  case "matrix": {
888
+ if (typeof value !== "object" || value === null || Array.isArray(value))
889
+ return String(value);
710
890
  const answers = value;
711
- 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(", ");
712
892
  }
713
893
  case "signature":
714
894
  return typeof value === "string" && value.startsWith("data:image/") ? "\u270D signed" : "";
@@ -735,7 +915,7 @@ function formatAnswer(field, value) {
735
915
  case "custom":
736
916
  return typeof value === "object" ? JSON.stringify(value) : String(value);
737
917
  default:
738
- return assertNever(field);
918
+ return assertNever(field2);
739
919
  }
740
920
  }
741
921
 
@@ -1075,14 +1255,40 @@ var sha1Base64 = (bytes) => bytesToBase64(new Sha1().update(bytes).digest());
1075
1255
 
1076
1256
  // src/client.ts
1077
1257
  var FilloError = class extends Error {
1078
- constructor(message, status) {
1258
+ constructor(message, status, retryAfterSec2) {
1079
1259
  super(message);
1080
1260
  this.status = status;
1261
+ this.retryAfterSec = retryAfterSec2;
1081
1262
  this.name = "FilloError";
1082
1263
  }
1083
1264
  status;
1265
+ retryAfterSec;
1084
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
+ }
1085
1282
  var MAX_CHUNK_RETRIES = 3;
1283
+ var DEFAULT_TIMEOUT_MS = 3e4;
1284
+ function isRetryableStatus(status) {
1285
+ return status >= 500 || status === 429 || status === 408;
1286
+ }
1287
+ function asRequestError(err) {
1288
+ if (err instanceof DOMException && err.name === "TimeoutError")
1289
+ return new FilloError("Request timed out", 0);
1290
+ return err;
1291
+ }
1086
1292
  function compareVersions(a, b) {
1087
1293
  const left = a.split(".").map((part) => Number(part) || 0);
1088
1294
  const right = b.split(".").map((part) => Number(part) || 0);
@@ -1122,6 +1328,7 @@ async function digestPartBase64(bytes) {
1122
1328
  return sha1Base64(bytes);
1123
1329
  }
1124
1330
  var DEFAULT_BASE_URL = "https://fillo.so";
1331
+ var CLIENT_HEADER = { "X-Fillo-Client": `@usefillo/core@${FILLO_SDK_VERSION}` };
1125
1332
  var FilloClient = class {
1126
1333
  /** Server origin this client targets, normalized (no trailing slash). */
1127
1334
  baseUrl;
@@ -1129,7 +1336,7 @@ var FilloClient = class {
1129
1336
  /** Publishable workspace key, when configured. */
1130
1337
  key;
1131
1338
  constructor(options2 = {}) {
1132
- this.baseUrl = options2.sameOrigin ? "" : DEFAULT_BASE_URL;
1339
+ this.baseUrl = options2.baseUrl ? options2.baseUrl.replace(/\/$/, "") : options2.sameOrigin ? "" : DEFAULT_BASE_URL;
1133
1340
  this.key = options2.key;
1134
1341
  this.fetch = options2.fetch ?? ((...args) => globalThis.fetch(...args));
1135
1342
  }
@@ -1137,13 +1344,20 @@ var FilloClient = class {
1137
1344
  return `${this.baseUrl}/api/v1${path}`;
1138
1345
  }
1139
1346
  async json(path, init) {
1140
- const res = await this.fetch(this.url(path), {
1141
- ...init,
1142
- headers: { "Content-Type": "application/json", ...init?.headers }
1143
- });
1347
+ const headers = init?.body != null ? { "Content-Type": "application/json", ...CLIENT_HEADER, ...init?.headers } : init?.headers;
1348
+ let res;
1349
+ try {
1350
+ res = await this.fetch(this.url(path), {
1351
+ ...init,
1352
+ headers,
1353
+ signal: init?.signal ?? AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
1354
+ });
1355
+ } catch (err) {
1356
+ throw asRequestError(err);
1357
+ }
1144
1358
  if (!res.ok) {
1145
- const body = await res.text().catch(() => "");
1146
- 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));
1147
1361
  }
1148
1362
  return await res.json();
1149
1363
  }
@@ -1179,17 +1393,24 @@ var FilloClient = class {
1179
1393
  }
1180
1394
  /** Submit a response. Returns per-field errors instead of throwing on validation failure. */
1181
1395
  async submit(formId, data, meta) {
1182
- const res = await this.fetch(this.url(`/forms/${encodeURIComponent(formId)}/responses`), {
1183
- method: "POST",
1184
- headers: { "Content-Type": "application/json" },
1185
- body: JSON.stringify({ data, meta })
1186
- });
1396
+ let res;
1397
+ try {
1398
+ res = await this.fetch(this.url(`/forms/${encodeURIComponent(formId)}/responses`), {
1399
+ method: "POST",
1400
+ headers: { "Content-Type": "application/json", ...CLIENT_HEADER },
1401
+ body: JSON.stringify({ data, meta }),
1402
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
1403
+ });
1404
+ } catch (err) {
1405
+ throw asRequestError(err);
1406
+ }
1187
1407
  if (res.status === 422) {
1188
1408
  const body2 = await res.json();
1189
1409
  return { ok: false, errors: body2.errors };
1190
1410
  }
1191
1411
  if (!res.ok) {
1192
- 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));
1193
1414
  }
1194
1415
  const body = await res.json();
1195
1416
  return { ok: true, responseId: body.id, duplicate: body.duplicate };
@@ -1203,7 +1424,8 @@ var FilloClient = class {
1203
1424
  const res = await this.fetch(this.url(`/forms/${encodeURIComponent(formId)}/sessions`), {
1204
1425
  method: "POST",
1205
1426
  headers: { "Content-Type": "application/json" },
1206
- body: JSON.stringify({ pageCount })
1427
+ body: JSON.stringify({ pageCount }),
1428
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
1207
1429
  });
1208
1430
  if (!res.ok) return null;
1209
1431
  const body = await res.json();
@@ -1219,7 +1441,8 @@ var FilloClient = class {
1219
1441
  method: "POST",
1220
1442
  headers: { "Content-Type": "application/json" },
1221
1443
  body: JSON.stringify(data),
1222
- keepalive: true
1444
+ keepalive: true,
1445
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
1223
1446
  }).catch(() => {
1224
1447
  });
1225
1448
  } catch {
@@ -1303,10 +1526,17 @@ var FilloClient = class {
1303
1526
  JSON.stringify({ name: transport.fileName, parent: { id: transport.folderId } })
1304
1527
  );
1305
1528
  form.append("file", file, transport.fileName);
1306
- const res = await this.retry(
1307
- options2,
1308
- () => this.fetch(transport.uploadUrl, { method: "POST", headers: auth, body: form, signal: options2.signal })
1309
- );
1529
+ const res = await this.retry(options2, async () => {
1530
+ const r = await this.fetch(transport.uploadUrl, {
1531
+ method: "POST",
1532
+ headers: auth,
1533
+ body: form,
1534
+ signal: this.uploadSignal(options2)
1535
+ });
1536
+ if (!r.ok && isRetryableStatus(r.status))
1537
+ throw new FilloError(`Box upload failed: ${r.status}`, r.status);
1538
+ return r;
1539
+ });
1310
1540
  if (!res.ok) throw new FilloError(`Box upload failed: ${res.status}`, res.status);
1311
1541
  const body = await res.json();
1312
1542
  report(file.size);
@@ -1318,9 +1548,8 @@ var FilloClient = class {
1318
1548
  400
1319
1549
  );
1320
1550
  }
1321
- const sessionRes = await this.retry(
1322
- options2,
1323
- () => this.fetch(transport.sessionUrl, {
1551
+ const sessionRes = await this.retry(options2, async () => {
1552
+ const r = await this.fetch(transport.sessionUrl, {
1324
1553
  method: "POST",
1325
1554
  headers: { ...auth, "Content-Type": "application/json" },
1326
1555
  body: JSON.stringify({
@@ -1328,9 +1557,12 @@ var FilloClient = class {
1328
1557
  file_size: transport.size,
1329
1558
  file_name: transport.fileName
1330
1559
  }),
1331
- signal: options2.signal
1332
- })
1333
- );
1560
+ signal: this.uploadSignal(options2)
1561
+ });
1562
+ if (!r.ok && isRetryableStatus(r.status))
1563
+ throw new FilloError(`Box session failed: ${r.status}`, r.status);
1564
+ return r;
1565
+ });
1334
1566
  if (!sessionRes.ok) throw new FilloError(`Box session failed: ${sessionRes.status}`, sessionRes.status);
1335
1567
  const session = await sessionRes.json();
1336
1568
  const whole = new Sha1();
@@ -1342,9 +1574,8 @@ var FilloClient = class {
1342
1574
  const bytes = new Uint8Array(await file.slice(offset, end).arrayBuffer());
1343
1575
  whole.update(bytes);
1344
1576
  const partDigest = await digestPartBase64(bytes);
1345
- const res = await this.retry(
1346
- options2,
1347
- () => this.fetch(session.session_endpoints.upload_part, {
1577
+ const res = await this.retry(options2, async () => {
1578
+ const r = await this.fetch(session.session_endpoints.upload_part, {
1348
1579
  method: "PUT",
1349
1580
  headers: {
1350
1581
  ...auth,
@@ -1353,27 +1584,41 @@ var FilloClient = class {
1353
1584
  "Content-Range": `bytes ${offset}-${end - 1}/${file.size}`
1354
1585
  },
1355
1586
  body: bytes,
1356
- signal: options2.signal
1357
- })
1358
- );
1587
+ signal: this.uploadSignal(options2)
1588
+ });
1589
+ if (!r.ok && isRetryableStatus(r.status))
1590
+ throw new FilloError(`Box part failed: ${r.status}`, r.status);
1591
+ return r;
1592
+ });
1359
1593
  if (!res.ok) throw new FilloError(`Box part failed: ${res.status}`, res.status);
1360
1594
  parts.push((await res.json()).part);
1361
1595
  offset = end;
1362
1596
  report(offset);
1363
1597
  }
1364
- const commitRes = await this.retry(
1365
- options2,
1366
- () => this.fetch(session.session_endpoints.commit, {
1598
+ const commitRes = await this.retry(options2, async () => {
1599
+ const r = await this.fetch(session.session_endpoints.commit, {
1367
1600
  method: "POST",
1368
1601
  headers: { ...auth, "Content-Type": "application/json", Digest: `sha=${bytesToBase64(whole.digest())}` },
1369
1602
  body: JSON.stringify({ parts }),
1370
- signal: options2.signal
1371
- })
1372
- );
1603
+ signal: this.uploadSignal(options2)
1604
+ });
1605
+ if (!r.ok && isRetryableStatus(r.status))
1606
+ throw new FilloError(`Box commit failed: ${r.status}`, r.status);
1607
+ return r;
1608
+ });
1373
1609
  if (!commitRes.ok) throw new FilloError(`Box commit failed: ${commitRes.status}`, commitRes.status);
1374
1610
  const committed = await commitRes.json();
1375
1611
  return { providerFileId: committed.entries?.[0]?.id };
1376
1612
  }
1613
+ /**
1614
+ * Caller signal (if any) combined with a fresh per-request timeout. Created per
1615
+ * fetch so each retry attempt gets its own deadline; the caller's own abort
1616
+ * still drives the resumable-upload cancel semantics.
1617
+ */
1618
+ uploadSignal(options2) {
1619
+ const timeout = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
1620
+ return options2.signal ? AbortSignal.any([options2.signal, timeout]) : timeout;
1621
+ }
1377
1622
  /** Retry a request with exponential backoff; never retries an aborted upload. */
1378
1623
  async retry(options2, fn) {
1379
1624
  let attempt = 0;
@@ -1398,10 +1643,12 @@ var FilloClient = class {
1398
1643
  let offset = session.uploadedBytes;
1399
1644
  const report = makeReporter(file.size, options2);
1400
1645
  report(offset);
1646
+ let stalls = 0;
1401
1647
  while (offset < file.size) {
1402
1648
  options2.signal?.throwIfAborted();
1403
- const end = Math.min(offset + session.chunkSize, file.size);
1404
- const chunk = file.slice(offset, end);
1649
+ const startedAt = offset;
1650
+ let end = Math.min(offset + session.chunkSize, file.size);
1651
+ let chunk = file.slice(offset, end);
1405
1652
  let attempt = 0;
1406
1653
  for (; ; ) {
1407
1654
  try {
@@ -1411,7 +1658,7 @@ var FilloClient = class {
1411
1658
  "Content-Range": `bytes ${offset}-${end - 1}/${file.size}`
1412
1659
  },
1413
1660
  body: chunk,
1414
- signal: options2.signal
1661
+ signal: this.uploadSignal(options2)
1415
1662
  });
1416
1663
  if (res.status === 308) {
1417
1664
  const range = res.headers.get("Range") ?? res.headers.get("range");
@@ -1433,9 +1680,19 @@ var FilloClient = class {
1433
1680
  await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
1434
1681
  const fresh = await this.getUploadSession(session.id, session.token);
1435
1682
  offset = fresh.uploadedBytes;
1683
+ if (offset >= file.size) break;
1684
+ end = Math.min(offset + session.chunkSize, file.size);
1685
+ chunk = file.slice(offset, end);
1436
1686
  }
1437
1687
  }
1438
1688
  report(offset);
1689
+ if (offset <= startedAt) {
1690
+ if (++stalls >= MAX_CHUNK_RETRIES) {
1691
+ throw new FilloError("Drive upload stalled \u2014 the storage endpoint stopped accepting progress", 0);
1692
+ }
1693
+ } else {
1694
+ stalls = 0;
1695
+ }
1439
1696
  }
1440
1697
  }
1441
1698
  /**
@@ -1452,7 +1709,7 @@ var FilloClient = class {
1452
1709
  method: "PUT",
1453
1710
  headers: { "Content-Type": file.type || "application/octet-stream" },
1454
1711
  body: file,
1455
- signal: options2.signal
1712
+ signal: this.uploadSignal(options2)
1456
1713
  });
1457
1714
  if (!res.ok) throw new FilloError(`S3 upload failed: ${res.status}`, res.status);
1458
1715
  report(file.size);
@@ -1484,6 +1741,68 @@ async function provisionWorkspace(options2) {
1484
1741
  return await res.json();
1485
1742
  }
1486
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
+
1487
1806
  // src/controller.ts
1488
1807
  var EMPTY_PAGE = { id: "empty", blocks: [] };
1489
1808
  var warnedNoClient = false;
@@ -1553,6 +1872,10 @@ function hasSubmittedOnce(form, formId) {
1553
1872
  if (form.settings.submissionLimit !== "once_per_visitor" || !formId) return false;
1554
1873
  return Boolean(readStoredSubmission(formId)?.submittedAt);
1555
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
+ }
1556
1879
  function createFormController(options2) {
1557
1880
  const { onChange, onSubmitted } = options2;
1558
1881
  const initial = normalizeFormSchema(options2.form);
@@ -1563,10 +1886,32 @@ function createFormController(options2) {
1563
1886
  let data = { ...options2.initialData ?? {} };
1564
1887
  let errors = {};
1565
1888
  let pageIndex = 0;
1566
- 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;
1567
1911
  const uploadingFields = /* @__PURE__ */ new Set();
1568
1912
  const listeners = /* @__PURE__ */ new Set();
1569
1913
  const mountedAt = Date.now();
1914
+ const idempotencyKey = randomSubmissionKey();
1570
1915
  let sessionId = null;
1571
1916
  let sessionStarted = false;
1572
1917
  function computeSnapshot() {
@@ -1583,7 +1928,8 @@ function createFormController(options2) {
1583
1928
  blocks: visibleBlocks(page, data),
1584
1929
  isFirstPage: clamped === 0,
1585
1930
  isLastPage: clamped === pageCount - 1,
1586
- uploading: uploadingFields.size > 0
1931
+ uploading: uploadingFields.size > 0,
1932
+ submitError
1587
1933
  };
1588
1934
  }
1589
1935
  let snapshot = computeSnapshot();
@@ -1616,6 +1962,7 @@ function createFormController(options2) {
1616
1962
  }
1617
1963
  data = { ...data, [fieldId]: value };
1618
1964
  onChange?.(data);
1965
+ submitError = void 0;
1619
1966
  if (fieldId in errors) {
1620
1967
  errors = Object.fromEntries(Object.entries(errors).filter(([k]) => k !== fieldId));
1621
1968
  }
@@ -1658,20 +2005,36 @@ function createFormController(options2) {
1658
2005
  notify();
1659
2006
  return;
1660
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
+ }
1661
2021
  if (client && formId) {
1662
- const submissionKey = form.settings.submissionLimit === "once_per_visitor" ? ensureSubmissionKey(formId) : void 0;
2022
+ const visitorKey = form.settings.submissionLimit === "once_per_visitor" ? ensureSubmissionKey(formId) : void 0;
2023
+ const submissionKey = visitorKey ?? idempotencyKey;
1663
2024
  status = "submitting";
2025
+ submitError = void 0;
1664
2026
  notify();
1665
2027
  let res;
1666
2028
  try {
1667
2029
  res = await client.submit(formId, result.data, {
1668
2030
  hp: options2.getHoneypot?.() ?? "",
1669
2031
  elapsedMs: Date.now() - mountedAt,
1670
- surface: options2.surface ?? "default",
2032
+ surface: options2.surface ?? "headless",
1671
2033
  submissionKey
1672
2034
  });
1673
2035
  } catch (err) {
1674
2036
  status = "idle";
2037
+ submitError = submitFailureMessage(err);
1675
2038
  notify();
1676
2039
  throw err;
1677
2040
  }
@@ -1682,7 +2045,7 @@ function createFormController(options2) {
1682
2045
  return;
1683
2046
  }
1684
2047
  if (sessionId) client.reportProgress(sessionId, { completed: true });
1685
- if (submissionKey) markSubmitted(formId, res.responseId);
2048
+ if (visitorKey) markSubmitted(formId, res.responseId);
1686
2049
  status = "submitted";
1687
2050
  notify();
1688
2051
  emitSubmitted(res.responseId, result.data);
@@ -1728,6 +2091,122 @@ function createFormController(options2) {
1728
2091
  };
1729
2092
  }
1730
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
+
1731
2210
  // src/define.ts
1732
2211
  function defineForm(def) {
1733
2212
  return {
@@ -1747,76 +2226,376 @@ function isCodeForm(form) {
1747
2226
  return typeof form === "object" && form !== null && "__filloCodeForm" in form;
1748
2227
  }
1749
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
+ }
1750
2245
  function contentHash(input) {
1751
2246
  let h = 5381;
1752
2247
  for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) | 0;
1753
2248
  return (h >>> 0).toString(36);
1754
2249
  }
1755
- function syncCodeForm(client, form) {
1756
- const content = JSON.stringify(form.schema) + JSON.stringify(form.theme ?? null);
1757
- const key = `${client.baseUrl}:${client.key}:${form.id}:${contentHash(content)}`;
1758
- let pending = syncCache.get(key);
1759
- if (!pending) {
1760
- pending = client.syncForm(form.id, form.schema, form.theme ?? null);
1761
- syncCache.set(key, pending);
1762
- 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;
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 {
1763
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));
1764
2302
  return pending;
1765
2303
  }
1766
2304
 
1767
- // src/prefill.ts
1768
- function prefillFromParams(form, params) {
1769
- const data = {};
1770
- for (const field of allFields(form)) {
1771
- const raw = field.kind === "hidden" ? params[field.paramName ?? field.id] ?? field.defaultValue : params[field.id];
1772
- if (raw === void 0 || raw === "") continue;
1773
- switch (field.kind) {
1774
- case "number": {
1775
- const n = Number(raw);
1776
- if (Number.isNaN(n)) break;
1777
- if (field.min !== void 0 && n < field.min) break;
1778
- if (field.max !== void 0 && n > field.max) break;
1779
- data[field.id] = n;
1780
- break;
1781
- }
1782
- case "rating": {
1783
- const n = Number(raw);
1784
- if (Number.isInteger(n) && n >= 1 && n <= (field.max ?? 5)) data[field.id] = n;
1785
- break;
1786
- }
1787
- case "linear_scale": {
1788
- const n = Number(raw);
1789
- const min = field.min ?? 1;
1790
- const max = field.max ?? 10;
1791
- if (Number.isInteger(n) && n >= min && n <= max) data[field.id] = n;
1792
- break;
1793
- }
1794
- case "checkbox":
1795
- data[field.id] = raw === "true" || raw === "1" || raw === "yes";
1796
- break;
1797
- case "select":
1798
- case "dropdown": {
1799
- if (field.options.some((o) => o.id === raw)) data[field.id] = raw;
1800
- break;
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
+ );
2389
+ }
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
+ );
1801
2542
  }
1802
- case "multi_select": {
1803
- const ids = new Set(field.options.map((o) => o.id));
1804
- const picked = raw.split(",").filter((v) => ids.has(v));
1805
- if (picked.length > 0) data[field.id] = picked;
1806
- 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
+ );
1807
2555
  }
1808
- case "file_upload":
1809
- case "ranking":
1810
- case "matrix":
1811
- case "signature":
1812
- case "custom":
1813
- break;
1814
- // not prefillable from a URL
1815
- default:
1816
- data[field.id] = raw;
2556
+ seen.add(block.id);
1817
2557
  }
1818
2558
  }
1819
- 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
+ };
1820
2599
  }
1821
2600
 
1822
2601
  // src/piping.ts
@@ -1825,10 +2604,10 @@ function resolveText(text, data, form) {
1825
2604
  if (!text.includes("{{")) return text;
1826
2605
  const fields = allFields(form);
1827
2606
  return text.replace(TOKEN, (_, id) => {
1828
- const field = fields.find((f) => f.id === id);
2607
+ const field2 = fields.find((f) => f.id === id);
1829
2608
  const value = data[id];
1830
2609
  if (value === null || value === void 0 || value === "") return "";
1831
- 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;
1832
2611
  if (Array.isArray(value)) return value.map((v) => label(String(v))).join(", ");
1833
2612
  if (typeof value === "string") return label(value);
1834
2613
  return String(value);
@@ -1849,18 +2628,26 @@ function pipeBlock(block, data, form) {
1849
2628
  export {
1850
2629
  BLOCK_KIND_META,
1851
2630
  CONTENT_KINDS,
2631
+ DEFAULT_STRINGS,
1852
2632
  DRAFT_KINDS,
2633
+ FILLO_DATA_ATTRS,
1853
2634
  FILLO_SCHEMA_VERSION,
1854
2635
  FILLO_SDK_VERSION,
2636
+ FILLO_SLOTS,
1855
2637
  FilloClient,
1856
2638
  FilloError,
2639
+ FilloJsxError,
2640
+ JSX_BLOCK_COMPONENTS,
2641
+ JSX_BLOCK_SPECS,
1857
2642
  PHONE_COUNTRIES,
1858
2643
  Sha1,
1859
2644
  allFields,
1860
2645
  assembleForm,
2646
+ codeFormFromJsx,
1861
2647
  contentHash,
1862
2648
  countryByDialCode,
1863
2649
  countryByIso,
2650
+ countryByTimeZone,
1864
2651
  createBlock,
1865
2652
  createClient,
1866
2653
  createEmptyForm,
@@ -1871,23 +2658,32 @@ export {
1871
2658
  flagEmoji,
1872
2659
  formatAnswer,
1873
2660
  formatNational,
2661
+ isAutoSubmitBlock,
1874
2662
  isBlockVisible,
1875
2663
  isCodeForm,
1876
2664
  isField,
2665
+ isFilloError,
1877
2666
  isPossiblePhone,
2667
+ needsExplicitSubmit,
1878
2668
  normalizeFormSchema,
1879
2669
  normalizeFormTheme,
1880
2670
  parsePhone,
1881
2671
  pipeBlock,
1882
2672
  prefillFromParams,
1883
2673
  provisionWorkspace,
2674
+ resolveSlotClass,
2675
+ resolveStrings,
1884
2676
  resolveText,
2677
+ schemaFromJsx,
1885
2678
  sha1Base64,
2679
+ shouldAutoSubmit,
2680
+ slotClass,
1886
2681
  syncCodeForm,
1887
2682
  toE164,
1888
2683
  validateField,
1889
2684
  validateFormSchema,
1890
2685
  validateResponse,
1891
2686
  visibleBlocks,
1892
- visibleFields
2687
+ visibleFields,
2688
+ when
1893
2689
  };