@usefillo/core 0.10.0 → 0.11.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 +344 -18
  2. package/dist/index.js +741 -138
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,117 @@ function isField(block) {
4
4
  return !CONTENT_KINDS.includes(block.kind);
5
5
  }
6
6
 
7
+ // src/canonical.ts
8
+ function isEmpty(value) {
9
+ if (value === null || value === void 0) return true;
10
+ if (typeof value === "string") return value.trim() === "";
11
+ if (Array.isArray(value)) return value.length === 0;
12
+ if (typeof value === "boolean") return value === false;
13
+ if (typeof value === "object") return Object.keys(value).length === 0;
14
+ return false;
15
+ }
16
+ function normalizeValue(field2, value) {
17
+ if (value === void 0 || value === null) return value;
18
+ if (field2.kind === "custom") return value;
19
+ if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
20
+ const n = Number(value);
21
+ return Number.isNaN(n) ? value : n;
22
+ }
23
+ if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
24
+ const n = Number(value);
25
+ return Number.isNaN(n) ? value : n;
26
+ }
27
+ if (typeof value === "string") return value.trim();
28
+ return value;
29
+ }
30
+ function canonicalValue(field2, value) {
31
+ const normalized = normalizeValue(field2, value);
32
+ return isEmpty(normalized) ? void 0 : normalized;
33
+ }
34
+
35
+ // src/calc.ts
36
+ function clampDecimals(decimals) {
37
+ if (decimals === void 0 || !Number.isFinite(decimals)) return 0;
38
+ return Math.max(0, Math.min(6, Math.round(decimals)));
39
+ }
40
+ function roundHalfAwayFromZero(value, decimals) {
41
+ const factor = 10 ** decimals;
42
+ const rounded = Math.round(Math.abs(value) * factor) / factor;
43
+ return value < 0 ? -rounded : rounded;
44
+ }
45
+ function finiteOrNull(value) {
46
+ return Number.isFinite(value) ? value : null;
47
+ }
48
+ function evaluateCalc(expr, resolve) {
49
+ switch (expr.op) {
50
+ case "value":
51
+ return conditionNumber(resolve(expr.fieldId));
52
+ case "const":
53
+ return finiteOrNull(expr.value);
54
+ case "add":
55
+ case "mul":
56
+ case "min":
57
+ case "max": {
58
+ if (expr.args.length === 0) return null;
59
+ const values = [];
60
+ for (const arg of expr.args) {
61
+ const value = evaluateCalc(arg, resolve);
62
+ if (value === null) return null;
63
+ values.push(value);
64
+ }
65
+ if (expr.op === "add") return finiteOrNull(values.reduce((a, b) => a + b, 0));
66
+ if (expr.op === "mul") return finiteOrNull(values.reduce((a, b) => a * b, 1));
67
+ return expr.op === "min" ? Math.min(...values) : Math.max(...values);
68
+ }
69
+ case "sub":
70
+ case "div": {
71
+ const left = evaluateCalc(expr.left, resolve);
72
+ if (left === null) return null;
73
+ const right = evaluateCalc(expr.right, resolve);
74
+ if (right === null) return null;
75
+ if (expr.op === "sub") return finiteOrNull(left - right);
76
+ return right === 0 ? null : finiteOrNull(left / right);
77
+ }
78
+ case "round": {
79
+ const value = evaluateCalc(expr.arg, resolve);
80
+ return value === null ? null : finiteOrNull(roundHalfAwayFromZero(value, clampDecimals(expr.decimals)));
81
+ }
82
+ case "if":
83
+ return evaluateCalc(conditionsMet(expr.when, resolve) ? expr.then : expr.else, resolve);
84
+ }
85
+ }
86
+ function evaluateCalculatedField(field2, resolve) {
87
+ const value = evaluateCalc(field2.calc, resolve);
88
+ if (value === null || field2.decimals === void 0) return value;
89
+ return finiteOrNull(roundHalfAwayFromZero(value, clampDecimals(field2.decimals)));
90
+ }
91
+ function computeCalculated(form, data) {
92
+ const fields = allFields(form);
93
+ if (!fields.some((field2) => field2.kind === "calculated")) return data;
94
+ let view = data;
95
+ let reachable = reachableFieldIds(form, view);
96
+ for (let i = 0; i <= fields.length; i++) {
97
+ const next = {};
98
+ let pruned = false;
99
+ for (const [key, value] of Object.entries(view)) {
100
+ if (reachable.has(key)) next[key] = value;
101
+ else pruned = true;
102
+ }
103
+ if (!pruned) break;
104
+ view = next;
105
+ reachable = reachableFieldIds(form, view);
106
+ }
107
+ const state = resolveLogicState(fields, view);
108
+ const out = {};
109
+ for (const [key, value] of Object.entries(data)) {
110
+ if (!state.calcIds.has(key)) out[key] = value;
111
+ }
112
+ for (const [key, value] of Object.entries(state.calc)) {
113
+ if (reachable.has(key)) out[key] = value;
114
+ }
115
+ return out;
116
+ }
117
+
7
118
  // src/logic.ts
8
119
  function isAnswered(value) {
9
120
  if (value === null || value === void 0) return false;
@@ -62,37 +173,71 @@ function blockVisibleWith(block, resolve) {
62
173
  if (!block.visibleIf || block.visibleIf.length === 0) return true;
63
174
  return conditionsMet(block.visibleIf, resolve);
64
175
  }
65
- function makeResolver(data, scoped, visible) {
66
- return (fieldId) => scoped.has(fieldId) && !visible.has(fieldId) ? void 0 : data[fieldId];
176
+ function makeResolver(canonical, raw, scoped, visible, calcIds, calc) {
177
+ return (fieldId) => {
178
+ if (scoped.has(fieldId)) {
179
+ if (!visible.has(fieldId)) return void 0;
180
+ return calcIds.has(fieldId) ? calc[fieldId] : canonical[fieldId];
181
+ }
182
+ return raw[fieldId];
183
+ };
67
184
  }
68
- function visibleFieldIds(fields, data) {
185
+ function resolveLogicState(fields, data) {
69
186
  const scoped = new Set(fields.map((f) => f.id));
187
+ const calcFields = fields.filter((f) => f.kind === "calculated");
188
+ const calcIds = new Set(calcFields.map((f) => f.id));
189
+ const canonical = {};
190
+ for (const f of fields) {
191
+ if (f.kind === "calculated") continue;
192
+ const value = canonicalValue(f, data[f.id]);
193
+ if (value !== void 0) canonical[f.id] = value;
194
+ }
70
195
  let visible = scoped;
196
+ let calc = {};
71
197
  for (let i = 0; i <= fields.length; i++) {
72
- const resolve = makeResolver(data, scoped, visible);
73
- const next = /* @__PURE__ */ new Set();
74
- for (const f of fields) if (blockVisibleWith(f, resolve)) next.add(f.id);
75
- if (next.size === visible.size && [...next].every((id) => visible.has(id))) return next;
76
- visible = next;
198
+ let nextCalc = calc;
199
+ if (calcFields.length > 0) {
200
+ nextCalc = { ...calc };
201
+ const resolveCalc = makeResolver(canonical, data, scoped, visible, calcIds, nextCalc);
202
+ for (const field2 of calcFields) {
203
+ const value = visible.has(field2.id) ? evaluateCalculatedField(field2, resolveCalc) : null;
204
+ if (value === null) delete nextCalc[field2.id];
205
+ else nextCalc[field2.id] = value;
206
+ }
207
+ }
208
+ const resolve = makeResolver(canonical, data, scoped, visible, calcIds, nextCalc);
209
+ const nextVisible = /* @__PURE__ */ new Set();
210
+ for (const f of fields) if (blockVisibleWith(f, resolve)) nextVisible.add(f.id);
211
+ const stable = nextVisible.size === visible.size && [...nextVisible].every((id) => visible.has(id)) && calcEquals(calc, nextCalc);
212
+ visible = nextVisible;
213
+ calc = nextCalc;
214
+ if (stable) break;
77
215
  }
78
- return visible;
216
+ return {
217
+ visible,
218
+ calc,
219
+ calcIds,
220
+ resolve: makeResolver(canonical, data, scoped, visible, calcIds, calc)
221
+ };
222
+ }
223
+ function calcEquals(a, b) {
224
+ const aKeys = Object.keys(a);
225
+ return aKeys.length === Object.keys(b).length && aKeys.every((key) => a[key] === b[key]);
226
+ }
227
+ function visibleFieldIds(fields, data) {
228
+ return resolveLogicState(fields, data).visible;
79
229
  }
80
230
  function isBlockVisible(block, data) {
81
231
  return blockVisibleWith(block, (fieldId) => data[fieldId]);
82
232
  }
83
233
  function visibleBlocks(page, data) {
84
- const fields = page.blocks.filter(isField);
85
- const scoped = new Set(fields.map((f) => f.id));
86
- const resolve = makeResolver(data, scoped, visibleFieldIds(fields, data));
87
- return page.blocks.filter((b) => blockVisibleWith(b, resolve));
234
+ const state = resolveLogicState(page.blocks.filter(isField), data);
235
+ return page.blocks.filter((b) => blockVisibleWith(b, state.resolve));
88
236
  }
89
237
  function visiblePageBlocks(form, page, data) {
90
- const fields = allFields(form);
91
- const scoped = new Set(fields.map((f) => f.id));
92
- const formVisible = visibleFieldIds(fields, data);
93
- const resolve = makeResolver(data, scoped, formVisible);
238
+ const state = resolveLogicState(allFields(form), data);
94
239
  return page.blocks.filter(
95
- (b) => isField(b) ? formVisible.has(b.id) : blockVisibleWith(b, resolve)
240
+ (b) => isField(b) ? state.visible.has(b.id) : blockVisibleWith(b, state.resolve)
96
241
  );
97
242
  }
98
243
  function allFields(form) {
@@ -113,9 +258,7 @@ function visibleFields(form, data) {
113
258
  return fields.filter((f) => visible.has(f.id));
114
259
  }
115
260
  function formResolver(form, data) {
116
- const fields = allFields(form);
117
- const scoped = new Set(fields.map((f) => f.id));
118
- return makeResolver(data, scoped, visibleFieldIds(fields, data));
261
+ return resolveLogicState(allFields(form), data).resolve;
119
262
  }
120
263
  function resolveNextPage(form, currentPageId, data) {
121
264
  const page = form.pages.find((p) => p.id === currentPageId);
@@ -181,79 +324,122 @@ function reachableFields(form, data) {
181
324
  import * as z from "zod/mini";
182
325
 
183
326
  // src/phone.ts
184
- var C = (iso2, name, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name, dialCode, lengths, groups, example });
327
+ var regionNames;
328
+ function regionDisplayNames() {
329
+ if (regionNames !== void 0) return regionNames ?? void 0;
330
+ if (typeof Intl === "undefined" || typeof Intl.DisplayNames === "undefined") {
331
+ regionNames = null;
332
+ return void 0;
333
+ }
334
+ try {
335
+ regionNames = new Intl.DisplayNames(void 0, { type: "region" });
336
+ } catch {
337
+ regionNames = null;
338
+ }
339
+ return regionNames ?? void 0;
340
+ }
341
+ function regionName(iso2) {
342
+ try {
343
+ return regionDisplayNames()?.of(iso2) ?? iso2;
344
+ } catch {
345
+ return iso2;
346
+ }
347
+ }
348
+ var C = (iso2, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name: regionName(iso2), dialCode, lengths, groups, example });
349
+ var CURATED_COUNTRIES = [
350
+ C("US", "1", [10], [3, 3, 4], "2015550123"),
351
+ C("CA", "1", [10], [3, 3, 4], "5145550123"),
352
+ C("GB", "44", [10], [4, 6], "7400123456"),
353
+ C("AU", "61", [9], [3, 3, 3], "412345678"),
354
+ C("DE", "49", [10, 11], [4, 7], "15123456789"),
355
+ C("FR", "33", [9], [1, 2, 2, 2, 2], "612345678"),
356
+ C("IN", "91", [10], [5, 5], "8123456789"),
357
+ C("DK", "45", [8], [2, 2, 2, 2], "32123456"),
358
+ C("SE", "46", [9], [2, 3, 4], "701234567"),
359
+ C("NO", "47", [8], [3, 2, 3], "40612345"),
360
+ C("FI", "358", [9, 10], [2, 3, 4], "412345678"),
361
+ C("NL", "31", [9], [1, 4, 4], "612345678"),
362
+ C("BE", "32", [9], [3, 2, 2, 2], "470123456"),
363
+ C("ES", "34", [9], [3, 3, 3], "612345678"),
364
+ C("IT", "39", [9, 10], [3, 3, 4], "3123456789"),
365
+ C("PT", "351", [9], [3, 3, 3], "912345678"),
366
+ C("IE", "353", [9], [2, 3, 4], "851234567"),
367
+ C("CH", "41", [9], [2, 3, 2, 2], "781234567"),
368
+ C("AT", "43", [10, 11], [3, 7], "6641234567"),
369
+ C("PL", "48", [9], [3, 3, 3], "512345678"),
370
+ C("CZ", "420", [9], [3, 3, 3], "601123456"),
371
+ C("GR", "30", [10], [3, 3, 4], "6912345678"),
372
+ C("RO", "40", [9], [3, 3, 3], "712345678"),
373
+ C("HU", "36", [9], [2, 3, 4], "201234567"),
374
+ C("UA", "380", [9], [2, 3, 2, 2], "501234567"),
375
+ C("RU", "7", [10], [3, 3, 2, 2], "9123456789"),
376
+ C("TR", "90", [10], [3, 3, 2, 2], "5012345678"),
377
+ C("IL", "972", [9], [2, 3, 4], "501234567"),
378
+ C("AE", "971", [9], [2, 3, 4], "501234567"),
379
+ C("SA", "966", [9], [2, 3, 4], "512345678"),
380
+ C("ZA", "27", [9], [2, 3, 4], "711234567"),
381
+ C("NG", "234", [10], [3, 3, 4], "8021234567"),
382
+ C("KE", "254", [9], [3, 3, 3], "712123456"),
383
+ C("EG", "20", [10], [3, 3, 4], "1001234567"),
384
+ C("BR", "55", [10, 11], [2, 5, 4], "11961234567"),
385
+ C("MX", "52", [10], [3, 3, 4], "2221234567"),
386
+ C("AR", "54", [10], [4, 6], "1123456789"),
387
+ C("CL", "56", [9], [1, 4, 4], "221234567"),
388
+ C("CO", "57", [10], [3, 3, 4], "3211234567"),
389
+ C("PE", "51", [9], [3, 3, 3], "912345678"),
390
+ C("CN", "86", [11], [3, 4, 4], "13123456789"),
391
+ C("JP", "81", [10], [2, 4, 4], "9012345678"),
392
+ C("KR", "82", [9, 10], [2, 4, 4], "1023456789"),
393
+ C("SG", "65", [8], [4, 4], "81234567"),
394
+ C("HK", "852", [8], [4, 4], "51234567"),
395
+ C("TW", "886", [9], [3, 3, 3], "912345678"),
396
+ C("MY", "60", [9, 10], [2, 3, 4], "123456789"),
397
+ C("ID", "62", [10, 11], [3, 4, 4], "81234567890"),
398
+ C("TH", "66", [9], [2, 3, 4], "812345678"),
399
+ C("PH", "63", [10], [3, 3, 4], "9171234567"),
400
+ C("VN", "84", [9], [3, 3, 3], "912345678"),
401
+ C("PK", "92", [10], [3, 3, 4], "3012345678"),
402
+ C("BD", "880", [10], [2, 4, 4], "1812345678"),
403
+ C("NZ", "64", [9, 10], [2, 3, 4], "211234567"),
404
+ C("SK", "421", [9], [3, 3, 3], "912123456"),
405
+ C("BG", "359", [9], [2, 3, 4], "481234567"),
406
+ C("HR", "385", [8, 9], [2, 3, 4], "921234567"),
407
+ C("RS", "381", [8, 9], [2, 3, 4], "601234567"),
408
+ C("SI", "386", [8], [2, 3, 3], "31234567"),
409
+ C("LT", "370", [8], [3, 2, 3], "61234567"),
410
+ C("LV", "371", [8], [2, 3, 3], "21234567"),
411
+ C("EE", "372", [7, 8], [3, 4], "51234567"),
412
+ C("IS", "354", [7], [3, 4], "6111234"),
413
+ C("LU", "352", [9], [3, 3, 3], "628123456"),
414
+ C("MA", "212", [9], [3, 3, 3], "650123456"),
415
+ C("CR", "506", [8], [4, 4], "83123456"),
416
+ C("EC", "593", [9], [2, 3, 4], "991234567"),
417
+ C("UY", "598", [8], [1, 3, 4], "94231234"),
418
+ C("QA", "974", [8], [4, 4], "33123456"),
419
+ C("KW", "965", [8], [4, 4], "50012345")
420
+ ];
421
+ var PACKED_DIAL_CODES = "AD376 AF93 AG1 AI1 AL355 AM374 AO244 AS1 AW297 AX358 AZ994 BA387 BB1 BF226 BH973 BI257 BJ229 BL590 BM1 BN673 BO591 BQ599 BS1 BT975 BV47 BW267 BY375 BZ501 CC61 CD243 CF236 CG242 CI225 CK682 CM237 CU53 CV238 CW599 CX61 CY357 DJ253 DM1 DO1 DZ213 EH212 ER291 ET251 FJ679 FK500 FM691 FO298 GA241 GD1 GE995 GF594 GG44 GH233 GI350 GL299 GM220 GN224 GP590 GQ240 GS500 GT502 GU1 GW245 GY592 HN504 HT509 IM44 IO246 IQ964 IR98 JE44 JM1 JO962 KG996 KH855 KI686 KM269 KN1 KP850 KY1 KZ7 LA856 LB961 LC1 LI423 LK94 LR231 LS266 LY218 MC377 MD373 ME382 MF590 MG261 MH692 MK389 ML223 MM95 MN976 MO853 MP1 MQ596 MR222 MS1 MT356 MU230 MV960 MW265 MZ258 NA264 NC687 NE227 NF672 NI505 NP977 NR674 NU683 OM968 PA507 PF689 PG675 PM508 PN64 PR1 PS970 PW680 PY595 RE262 RW250 SB677 SC248 SD249 SH290 SJ47 SL232 SM378 SN221 SO252 SR597 SS211 ST239 SV503 SX1 SY963 SZ268 TC1 TD235 TF262 TG228 TJ992 TK690 TL670 TM993 TN216 TO676 TT1 TV688 TZ255 UG256 UZ998 VA379 VC1 VE58 VG1 VI1 VU678 WF681 WS685 XK383 YE967 YT262 ZM260 ZW263";
422
+ function parsePackedCountries(packed) {
423
+ const out = [];
424
+ for (const token of packed.trim().split(/\s+/)) {
425
+ const iso2 = token.slice(0, 2);
426
+ const dialCode = token.slice(2);
427
+ out.push({ iso2, name: regionName(iso2), dialCode, lengths: [], groups: [], example: "" });
428
+ }
429
+ return out;
430
+ }
185
431
  var PHONE_COUNTRIES = [
186
- C("US", "United States", "1", [10], [3, 3, 4], "2015550123"),
187
- C("CA", "Canada", "1", [10], [3, 3, 4], "5145550123"),
188
- C("GB", "United Kingdom", "44", [10], [4, 6], "7400123456"),
189
- C("AU", "Australia", "61", [9], [3, 3, 3], "412345678"),
190
- C("DE", "Germany", "49", [10, 11], [4, 7], "15123456789"),
191
- C("FR", "France", "33", [9], [1, 2, 2, 2, 2], "612345678"),
192
- C("IN", "India", "91", [10], [5, 5], "8123456789"),
193
- C("DK", "Denmark", "45", [8], [2, 2, 2, 2], "32123456"),
194
- C("SE", "Sweden", "46", [9], [2, 3, 4], "701234567"),
195
- C("NO", "Norway", "47", [8], [3, 2, 3], "40612345"),
196
- C("FI", "Finland", "358", [9, 10], [2, 3, 4], "412345678"),
197
- C("NL", "Netherlands", "31", [9], [1, 4, 4], "612345678"),
198
- C("BE", "Belgium", "32", [9], [3, 2, 2, 2], "470123456"),
199
- C("ES", "Spain", "34", [9], [3, 3, 3], "612345678"),
200
- C("IT", "Italy", "39", [9, 10], [3, 3, 4], "3123456789"),
201
- C("PT", "Portugal", "351", [9], [3, 3, 3], "912345678"),
202
- C("IE", "Ireland", "353", [9], [2, 3, 4], "851234567"),
203
- C("CH", "Switzerland", "41", [9], [2, 3, 2, 2], "781234567"),
204
- C("AT", "Austria", "43", [10, 11], [3, 7], "6641234567"),
205
- C("PL", "Poland", "48", [9], [3, 3, 3], "512345678"),
206
- C("CZ", "Czechia", "420", [9], [3, 3, 3], "601123456"),
207
- C("GR", "Greece", "30", [10], [3, 3, 4], "6912345678"),
208
- C("RO", "Romania", "40", [9], [3, 3, 3], "712345678"),
209
- C("HU", "Hungary", "36", [9], [2, 3, 4], "201234567"),
210
- C("UA", "Ukraine", "380", [9], [2, 3, 2, 2], "501234567"),
211
- C("RU", "Russia", "7", [10], [3, 3, 2, 2], "9123456789"),
212
- C("TR", "Turkey", "90", [10], [3, 3, 2, 2], "5012345678"),
213
- C("IL", "Israel", "972", [9], [2, 3, 4], "501234567"),
214
- C("AE", "United Arab Emirates", "971", [9], [2, 3, 4], "501234567"),
215
- C("SA", "Saudi Arabia", "966", [9], [2, 3, 4], "512345678"),
216
- C("ZA", "South Africa", "27", [9], [2, 3, 4], "711234567"),
217
- C("NG", "Nigeria", "234", [10], [3, 3, 4], "8021234567"),
218
- C("KE", "Kenya", "254", [9], [3, 3, 3], "712123456"),
219
- C("EG", "Egypt", "20", [10], [3, 3, 4], "1001234567"),
220
- C("BR", "Brazil", "55", [10, 11], [2, 5, 4], "11961234567"),
221
- C("MX", "Mexico", "52", [10], [3, 3, 4], "2221234567"),
222
- C("AR", "Argentina", "54", [10], [4, 6], "1123456789"),
223
- C("CL", "Chile", "56", [9], [1, 4, 4], "221234567"),
224
- C("CO", "Colombia", "57", [10], [3, 3, 4], "3211234567"),
225
- C("PE", "Peru", "51", [9], [3, 3, 3], "912345678"),
226
- C("CN", "China", "86", [11], [3, 4, 4], "13123456789"),
227
- C("JP", "Japan", "81", [10], [2, 4, 4], "9012345678"),
228
- C("KR", "South Korea", "82", [9, 10], [2, 4, 4], "1023456789"),
229
- C("SG", "Singapore", "65", [8], [4, 4], "81234567"),
230
- C("HK", "Hong Kong", "852", [8], [4, 4], "51234567"),
231
- C("TW", "Taiwan", "886", [9], [3, 3, 3], "912345678"),
232
- C("MY", "Malaysia", "60", [9, 10], [2, 3, 4], "123456789"),
233
- C("ID", "Indonesia", "62", [10, 11], [3, 4, 4], "81234567890"),
234
- C("TH", "Thailand", "66", [9], [2, 3, 4], "812345678"),
235
- C("PH", "Philippines", "63", [10], [3, 3, 4], "9171234567"),
236
- C("VN", "Vietnam", "84", [9], [3, 3, 3], "912345678"),
237
- C("PK", "Pakistan", "92", [10], [3, 3, 4], "3012345678"),
238
- C("BD", "Bangladesh", "880", [10], [2, 4, 4], "1812345678"),
239
- C("NZ", "New Zealand", "64", [9, 10], [2, 3, 4], "211234567"),
240
- C("SK", "Slovakia", "421", [9], [3, 3, 3], "912123456"),
241
- C("BG", "Bulgaria", "359", [9], [2, 3, 4], "481234567"),
242
- C("HR", "Croatia", "385", [8, 9], [2, 3, 4], "921234567"),
243
- C("RS", "Serbia", "381", [8, 9], [2, 3, 4], "601234567"),
244
- C("SI", "Slovenia", "386", [8], [2, 3, 3], "31234567"),
245
- C("LT", "Lithuania", "370", [8], [3, 2, 3], "61234567"),
246
- C("LV", "Latvia", "371", [8], [2, 3, 3], "21234567"),
247
- C("EE", "Estonia", "372", [7, 8], [3, 4], "51234567"),
248
- C("IS", "Iceland", "354", [7], [3, 4], "6111234"),
249
- C("LU", "Luxembourg", "352", [9], [3, 3, 3], "628123456"),
250
- C("MA", "Morocco", "212", [9], [3, 3, 3], "650123456"),
251
- C("CR", "Costa Rica", "506", [8], [4, 4], "83123456"),
252
- C("EC", "Ecuador", "593", [9], [2, 3, 4], "991234567"),
253
- C("UY", "Uruguay", "598", [8], [1, 3, 4], "94231234"),
254
- C("QA", "Qatar", "974", [8], [4, 4], "33123456"),
255
- C("KW", "Kuwait", "965", [8], [4, 4], "50012345")
432
+ ...CURATED_COUNTRIES,
433
+ ...parsePackedCountries(PACKED_DIAL_CODES)
256
434
  ];
435
+ var PHONE_PICKER_COUNTRIES = sortForPicker(PHONE_COUNTRIES);
436
+ function sortForPicker(countries) {
437
+ if (typeof Intl === "undefined" || typeof Intl.Collator === "undefined") {
438
+ return [...countries].sort((a, b) => a.iso2 < b.iso2 ? -1 : a.iso2 > b.iso2 ? 1 : 0);
439
+ }
440
+ const collator = new Intl.Collator(void 0, { sensitivity: "base" });
441
+ return [...countries].sort((a, b) => collator.compare(a.name, b.name));
442
+ }
257
443
  var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
258
444
  var timeZoneIso = {
259
445
  "Africa/Cairo": "EG",
@@ -431,19 +617,24 @@ function toE164(country, national) {
431
617
  const digits = digitsOnly(national);
432
618
  return digits ? `+${country.dialCode}${digits}` : "";
433
619
  }
434
- function parsePhone(value, fallback) {
620
+ function parsePhone(value, fallback, opts) {
435
621
  const trimmed = (value ?? "").trim();
436
622
  const hasPlus = trimmed.startsWith("+");
437
623
  const digits = digitsOnly(trimmed);
438
- if (!digits) return { country: fallback, national: "", e164: "" };
439
624
  if (hasPlus) {
625
+ if (!digits) return { national: "", e164: "", pending: true, raw: trimmed };
626
+ const previous = opts?.previousDigits ? digitsOnly(opts.previousDigits) : "";
627
+ if (previous && digits === previous) {
628
+ return { national: digits, e164: `+${digits}`, pending: true, raw: trimmed };
629
+ }
440
630
  const country = countryByDialCode(digits);
441
631
  if (country) {
442
632
  const national = digits.slice(country.dialCode.length);
443
633
  return { country, national, e164: `+${digits}` };
444
634
  }
445
- return { national: digits, e164: `+${digits}` };
635
+ return { national: digits, e164: `+${digits}`, pending: true, raw: trimmed };
446
636
  }
637
+ if (!digits) return { country: fallback, national: "", e164: "" };
447
638
  if (fallback) {
448
639
  const national = digits.startsWith(fallback.dialCode) ? digits.slice(fallback.dialCode.length) : digits;
449
640
  return { country: fallback, national, e164: `+${fallback.dialCode}${national}` };
@@ -501,8 +692,8 @@ var DEFAULT_STRINGS = {
501
692
  successMessage: "Your response has been recorded.",
502
693
  closed: "This form is no longer accepting responses.",
503
694
  notLive: "This form isn't live yet.",
504
- notOpenTitle: "Not open just yet",
505
- notOpenBody: "This form is still being set up. Check back soon.",
695
+ notOpenTitle: "This form isn't open yet",
696
+ notOpenBody: "The form owner is still setting things up. Please check back soon.",
506
697
  closedTitle: "Responses are closed",
507
698
  submitFailed: "This form can't submit right now. Please try again in a moment.",
508
699
  loadFailedNotFound: "Form not found \u2014 check the form id and that it's published.",
@@ -521,13 +712,21 @@ var DEFAULT_FIELD_STRINGS = {
521
712
  uploadRetry: "Retry",
522
713
  uploadsDisabled: "Uploads are disabled in preview",
523
714
  uploadsUnavailable: "Uploads are temporarily unavailable",
715
+ uploadUnavailable: "We couldn't upload this file right now. Try again in a moment.",
524
716
  uploadFailed: "Upload failed \u2014 try again",
525
717
  dropzoneTitle: (multiple) => `Drop ${multiple ? "files" : "a file"} here or click to browse`,
526
718
  dropzoneHint: (maxMb) => `Up to ${maxMb} MB per file`,
527
719
  fileTooLarge: (maxMb) => `Larger than ${maxMb} MB limit`,
528
720
  filesUploading: (count) => `Uploading ${count} ${count === 1 ? "file" : "files"}, please wait\u2026`,
529
721
  uploadsFailed: (count) => `${count} ${count === 1 ? "upload" : "uploads"} failed.`,
530
- filesUploaded: (count) => `${count} ${count === 1 ? "file" : "files"} uploaded.`
722
+ filesUploaded: (count) => `${count} ${count === 1 ? "file" : "files"} uploaded.`,
723
+ submittingAnnouncement: "Submitting your response\u2026",
724
+ errorSummaryTitle: "Please fix the following:",
725
+ rankingPosition: (label, position, count) => `${label}, position ${position} of ${count}`,
726
+ phoneCountrySelected: (name) => `${name} selected`,
727
+ phoneResultsCount: (count) => `${count} ${count === 1 ? "result" : "results"}`,
728
+ signatureEmpty: "No signature yet",
729
+ signatureSigned: "Signature saved"
531
730
  };
532
731
  function resolveStrings(overrides) {
533
732
  return overrides ? { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS, ...overrides } : { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS };
@@ -541,16 +740,9 @@ var fileValueSchema = z.object({
541
740
  mime: z.string(),
542
741
  url: z.optional(z.string())
543
742
  });
544
- function isEmpty(value) {
545
- if (value === null || value === void 0) return true;
546
- if (typeof value === "string") return value.trim() === "";
547
- if (Array.isArray(value)) return value.length === 0;
548
- if (typeof value === "boolean") return value === false;
549
- if (typeof value === "object") return Object.keys(value).length === 0;
550
- return false;
551
- }
552
743
  function validateField(field2, value) {
553
744
  if (isEmpty(value)) {
745
+ if (field2.kind === "calculated") return null;
554
746
  return field2.required ? REQUIRED_FIELD_MESSAGE : null;
555
747
  }
556
748
  switch (field2.kind) {
@@ -667,6 +859,9 @@ function validateField(field2, value) {
667
859
  case "hidden": {
668
860
  return typeof value === "string" ? null : "Invalid value";
669
861
  }
862
+ case "calculated": {
863
+ return null;
864
+ }
670
865
  case "custom": {
671
866
  return null;
672
867
  }
@@ -694,6 +889,7 @@ function validateResponse(form, data) {
694
889
  const errors = {};
695
890
  const cleaned = {};
696
891
  for (const field2 of fields) {
892
+ if (field2.kind === "calculated") continue;
697
893
  const value = normalizeValue(field2, data[field2.id]);
698
894
  const error = validateField(field2, value);
699
895
  if (error) {
@@ -704,20 +900,6 @@ function validateResponse(form, data) {
704
900
  }
705
901
  return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
706
902
  }
707
- function normalizeValue(field2, value) {
708
- if (value === void 0 || value === null) return value;
709
- if (field2.kind === "custom") return value;
710
- if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
711
- const n = Number(value);
712
- return Number.isNaN(n) ? value : n;
713
- }
714
- if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
715
- const n = Number(value);
716
- return Number.isNaN(n) ? value : n;
717
- }
718
- if (typeof value === "string") return value.trim();
719
- return value;
720
- }
721
903
 
722
904
  // src/schema-validation.ts
723
905
  import * as z2 from "zod/mini";
@@ -740,6 +922,7 @@ var FIELD_KINDS = [
740
922
  "date",
741
923
  "file_upload",
742
924
  "hidden",
925
+ "calculated",
743
926
  "custom"
744
927
  ];
745
928
  var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
@@ -776,9 +959,10 @@ var schemaShape = z2.object({
776
959
  });
777
960
  var MAX_SCHEMA_VERSION = 1;
778
961
  var FILLO_SCHEMA_VERSION = 1;
779
- var FILLO_SDK_VERSION = true ? "0.10.0" : "0.0.0-dev";
962
+ var FILLO_SDK_VERSION = true ? "0.11.0" : "0.0.0-dev";
780
963
  var FILLO_MIN_SDK_VERSION = "0.4.0";
781
964
  var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
965
+ var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
782
966
  function str(value, max, fallback = "") {
783
967
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
784
968
  }
@@ -786,6 +970,11 @@ function optionalStr(value, max) {
786
970
  const s = str(value, max);
787
971
  return s === "" ? void 0 : s;
788
972
  }
973
+ function optionalAffix(value, max) {
974
+ if (typeof value !== "string") return void 0;
975
+ const s = value.slice(0, max);
976
+ return s.trim() === "" ? void 0 : s;
977
+ }
789
978
  function bool(value) {
790
979
  return typeof value === "boolean" ? value : void 0;
791
980
  }
@@ -833,7 +1022,17 @@ function conditions(value) {
833
1022
  });
834
1023
  return normalized.length ? normalized : void 0;
835
1024
  }
836
- function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
1025
+ function jumpDepsAllowed(fieldId, calcRefs, allowed, seen) {
1026
+ const refs = calcRefs.get(fieldId);
1027
+ if (refs === void 0) return allowed.has(fieldId);
1028
+ if (seen.has(fieldId)) return true;
1029
+ seen.add(fieldId);
1030
+ for (const ref of refs) {
1031
+ if (!jumpDepsAllowed(ref, calcRefs, allowed, seen)) return false;
1032
+ }
1033
+ return true;
1034
+ }
1035
+ function jumpRules(value, pageIds, sourceId, allowedFieldIds, calcRefs) {
837
1036
  if (!Array.isArray(value)) return void 0;
838
1037
  const rules = value.slice(0, 50).flatMap((item) => {
839
1038
  if (!item || typeof item !== "object") return [];
@@ -845,11 +1044,187 @@ function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
845
1044
  if (rawWhen !== void 0 && !Array.isArray(rawWhen)) return [];
846
1045
  const when2 = conditions(rawWhen);
847
1046
  if (Array.isArray(rawWhen) && rawWhen.length > 0 && when2 === void 0) return [];
848
- if (when2 && when2.some((c) => !allowedFieldIds.has(c.fieldId))) return [];
1047
+ if (when2 && when2.some((c) => !jumpDepsAllowed(c.fieldId, calcRefs, allowedFieldIds, /* @__PURE__ */ new Set())))
1048
+ return [];
849
1049
  return [{ when: when2 ?? [], to }];
850
1050
  });
851
1051
  return rules.length ? rules : void 0;
852
1052
  }
1053
+ var CALC_OPERAND_KINDS = ["number", "rating", "linear_scale", "calculated"];
1054
+ function normalizeCalc(value, depth = 0) {
1055
+ if (depth > 32) return null;
1056
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1057
+ const rec = value;
1058
+ switch (rec.op) {
1059
+ case "value": {
1060
+ const fieldId = str(rec.fieldId, 128);
1061
+ return fieldId ? { op: "value", fieldId } : null;
1062
+ }
1063
+ case "const": {
1064
+ const n = finite(rec.value);
1065
+ return n === void 0 ? null : { op: "const", value: n };
1066
+ }
1067
+ case "add":
1068
+ case "mul":
1069
+ case "min":
1070
+ case "max": {
1071
+ if (!Array.isArray(rec.args) || rec.args.length < 1 || rec.args.length > 50) return null;
1072
+ const args = [];
1073
+ for (const raw of rec.args) {
1074
+ const arg = normalizeCalc(raw, depth + 1);
1075
+ if (!arg) return null;
1076
+ args.push(arg);
1077
+ }
1078
+ return { op: rec.op, args };
1079
+ }
1080
+ case "sub":
1081
+ case "div": {
1082
+ const left = normalizeCalc(rec.left, depth + 1);
1083
+ const right = normalizeCalc(rec.right, depth + 1);
1084
+ return left && right ? { op: rec.op, left, right } : null;
1085
+ }
1086
+ case "round": {
1087
+ const arg = normalizeCalc(rec.arg, depth + 1);
1088
+ if (!arg) return null;
1089
+ return cleanObject({
1090
+ op: "round",
1091
+ arg,
1092
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0)
1093
+ });
1094
+ }
1095
+ case "if": {
1096
+ if (!Array.isArray(rec.when)) return null;
1097
+ const when2 = conditions(rec.when) ?? [];
1098
+ if (when2.length !== rec.when.length) return null;
1099
+ const then = normalizeCalc(rec.then, depth + 1);
1100
+ const els = normalizeCalc(rec.else, depth + 1);
1101
+ return then && els ? { op: "if", when: when2, then, else: els } : null;
1102
+ }
1103
+ default:
1104
+ return null;
1105
+ }
1106
+ }
1107
+ function collectCalcRefs(expr, valueRefs, whenRefs) {
1108
+ switch (expr.op) {
1109
+ case "value":
1110
+ valueRefs.add(expr.fieldId);
1111
+ return;
1112
+ case "const":
1113
+ return;
1114
+ case "add":
1115
+ case "mul":
1116
+ case "min":
1117
+ case "max":
1118
+ for (const arg of expr.args) collectCalcRefs(arg, valueRefs, whenRefs);
1119
+ return;
1120
+ case "sub":
1121
+ case "div":
1122
+ collectCalcRefs(expr.left, valueRefs, whenRefs);
1123
+ collectCalcRefs(expr.right, valueRefs, whenRefs);
1124
+ return;
1125
+ case "round":
1126
+ collectCalcRefs(expr.arg, valueRefs, whenRefs);
1127
+ return;
1128
+ case "if":
1129
+ for (const cond of expr.when) whenRefs.add(cond.fieldId);
1130
+ collectCalcRefs(expr.then, valueRefs, whenRefs);
1131
+ collectCalcRefs(expr.else, valueRefs, whenRefs);
1132
+ return;
1133
+ }
1134
+ }
1135
+ function validateCalculatedFields(pages) {
1136
+ const kinds = /* @__PURE__ */ new Map();
1137
+ for (const page of pages) {
1138
+ for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
1139
+ }
1140
+ const deps = /* @__PURE__ */ new Map();
1141
+ for (const page of pages) {
1142
+ for (const block of page.blocks) {
1143
+ if (block.kind !== "calculated") continue;
1144
+ const calc = normalizeCalc(block.calc);
1145
+ if (!calc) {
1146
+ return `Field ${block.id} has a malformed calc expression \u2014 build it from value/const/add/sub/mul/div/min/max/round/if nodes`;
1147
+ }
1148
+ block.calc = calc;
1149
+ const valueRefs = /* @__PURE__ */ new Set();
1150
+ const whenRefs = /* @__PURE__ */ new Set();
1151
+ collectCalcRefs(calc, valueRefs, whenRefs);
1152
+ for (const ref of valueRefs) {
1153
+ const kind = kinds.get(ref);
1154
+ if (kind === void 0) {
1155
+ return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
1156
+ }
1157
+ if (!CALC_OPERAND_KINDS.includes(kind)) {
1158
+ return `Calculated field ${block.id} references non-numeric field ${ref} (${kind}) \u2014 calculations can read number, rating, linear_scale, and calculated fields only`;
1159
+ }
1160
+ }
1161
+ deps.set(
1162
+ block.id,
1163
+ [...valueRefs, ...whenRefs].filter((ref) => kinds.get(ref) === "calculated")
1164
+ );
1165
+ }
1166
+ }
1167
+ const state = /* @__PURE__ */ new Map();
1168
+ const visit = (id, path) => {
1169
+ const mark = state.get(id);
1170
+ if (mark === "done") return null;
1171
+ if (mark === "visiting") {
1172
+ const chain = [...path.slice(path.indexOf(id)), id].join(" \u2192 ");
1173
+ return `Calculated field ${id} depends on its own result (${chain}) \u2014 break the cycle so the value can be computed`;
1174
+ }
1175
+ state.set(id, "visiting");
1176
+ for (const dep of deps.get(id) ?? []) {
1177
+ const error = visit(dep, [...path, id]);
1178
+ if (error) return error;
1179
+ }
1180
+ state.set(id, "done");
1181
+ return null;
1182
+ };
1183
+ for (const id of deps.keys()) {
1184
+ const error = visit(id, []);
1185
+ if (error) return error;
1186
+ }
1187
+ return null;
1188
+ }
1189
+ var MAX_CALC_FIELDS = 100;
1190
+ var MAX_CALC_TOTAL_NODES = 5e3;
1191
+ function countCalcNodes(expr) {
1192
+ switch (expr.op) {
1193
+ case "value":
1194
+ case "const":
1195
+ return 1;
1196
+ case "add":
1197
+ case "mul":
1198
+ case "min":
1199
+ case "max":
1200
+ return 1 + expr.args.reduce((n, arg) => n + countCalcNodes(arg), 0);
1201
+ case "sub":
1202
+ case "div":
1203
+ return 1 + countCalcNodes(expr.left) + countCalcNodes(expr.right);
1204
+ case "round":
1205
+ return 1 + countCalcNodes(expr.arg);
1206
+ case "if":
1207
+ return 1 + countCalcNodes(expr.then) + countCalcNodes(expr.else);
1208
+ }
1209
+ }
1210
+ function calcComplexityError(pages) {
1211
+ let calcFields = 0;
1212
+ let totalNodes = 0;
1213
+ for (const page of pages) {
1214
+ for (const block of page.blocks) {
1215
+ if (block.kind !== "calculated") continue;
1216
+ calcFields += 1;
1217
+ totalNodes += countCalcNodes(block.calc);
1218
+ }
1219
+ }
1220
+ if (calcFields > MAX_CALC_FIELDS) {
1221
+ return `Form has ${calcFields} calculated fields \u2014 the maximum is ${MAX_CALC_FIELDS}; remove some or consolidate them into fewer expressions`;
1222
+ }
1223
+ if (totalNodes > MAX_CALC_TOTAL_NODES) {
1224
+ return `Calculated expressions total ${totalNodes} operations \u2014 the maximum is ${MAX_CALC_TOTAL_NODES} across the form; simplify or split up the calculations`;
1225
+ }
1226
+ return null;
1227
+ }
853
1228
  function baseBlock(rec) {
854
1229
  return {
855
1230
  id: str(rec.id, 128),
@@ -961,7 +1336,20 @@ function normalizeBlock(input) {
961
1336
  maxLength: finite(rec.maxLength) === void 0 ? void 0 : int(rec.maxLength, 1, 2e4, 2e4)
962
1337
  });
963
1338
  case "number":
964
- return cleanObject({ ...baseField(rec, kind), min: finite(rec.min), max: finite(rec.max) });
1339
+ return cleanObject({
1340
+ ...baseField(rec, kind),
1341
+ min: finite(rec.min),
1342
+ max: finite(rec.max),
1343
+ // Display-only (decision 1 in the number-formatting contract) —
1344
+ // mirrors the calculated field's decimals/prefix/suffix carry-through
1345
+ // exactly, plus notation which calculated doesn't have.
1346
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
1347
+ prefix: optionalAffix(rec.prefix, 100),
1348
+ suffix: optionalAffix(rec.suffix, 100),
1349
+ // The only supported value; anything else drops silently rather than
1350
+ // erroring (an old/garbage value is just "no grouping").
1351
+ notation: rec.notation === "grouped" ? "grouped" : void 0
1352
+ });
965
1353
  case "select":
966
1354
  case "multi_select":
967
1355
  case "dropdown": {
@@ -1025,6 +1413,21 @@ function normalizeBlock(input) {
1025
1413
  paramName: optionalStr(rec.paramName, 128),
1026
1414
  defaultValue: optionalStr(rec.defaultValue, 1e3)
1027
1415
  });
1416
+ case "calculated":
1417
+ return cleanObject({
1418
+ ...baseField(rec, kind),
1419
+ // A derived value isn't answerable, so it can't be required (the
1420
+ // hidden-field precedent) — a required one could never be satisfied.
1421
+ required: false,
1422
+ // Kept raw here on purpose: the calc AST is normalized and structurally
1423
+ // validated in normalizeFormSchema once the full field universe is
1424
+ // known (missing refs, operand kinds, cycles are HARD errors there — a
1425
+ // calculated field must never be silently dropped or degraded).
1426
+ calc: rec.calc,
1427
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
1428
+ prefix: optionalAffix(rec.prefix, 100),
1429
+ suffix: optionalAffix(rec.suffix, 100)
1430
+ });
1028
1431
  case "file_upload":
1029
1432
  return cleanObject({
1030
1433
  ...baseField(rec, kind),
@@ -1063,6 +1466,22 @@ function normalizeFormSchema(input) {
1063
1466
  return { id: str(page.id, 128), title: optionalStr(page.title, 500), blocks, next: page.next };
1064
1467
  });
1065
1468
  const targetIds = new Set(rebuilt.map((p) => p.id));
1469
+ const calcRefs = /* @__PURE__ */ new Map();
1470
+ for (const page of rebuilt) {
1471
+ for (const block of page.blocks) {
1472
+ if (block.kind !== "calculated") continue;
1473
+ const refs = /* @__PURE__ */ new Set();
1474
+ const calc = normalizeCalc(block.calc);
1475
+ if (calc) {
1476
+ const valueRefs = /* @__PURE__ */ new Set();
1477
+ const whenRefs = /* @__PURE__ */ new Set();
1478
+ collectCalcRefs(calc, valueRefs, whenRefs);
1479
+ for (const ref of valueRefs) refs.add(ref);
1480
+ for (const ref of whenRefs) refs.add(ref);
1481
+ }
1482
+ calcRefs.set(block.id, refs);
1483
+ }
1484
+ }
1066
1485
  const priorFieldIds = /* @__PURE__ */ new Set();
1067
1486
  const pages = rebuilt.map((page) => {
1068
1487
  for (const block of page.blocks) {
@@ -1072,7 +1491,7 @@ function normalizeFormSchema(input) {
1072
1491
  id: page.id,
1073
1492
  title: page.title,
1074
1493
  blocks: page.blocks,
1075
- next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds))
1494
+ next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds), calcRefs)
1076
1495
  });
1077
1496
  });
1078
1497
  const pageIds = /* @__PURE__ */ new Set();
@@ -1107,6 +1526,10 @@ function normalizeFormSchema(input) {
1107
1526
  }
1108
1527
  }
1109
1528
  }
1529
+ const calcError = validateCalculatedFields(pages);
1530
+ if (calcError) return { ok: false, error: calcError };
1531
+ const calcComplexity = calcComplexityError(pages);
1532
+ if (calcComplexity) return { ok: false, error: calcComplexity };
1110
1533
  if (pages.every((page) => page.blocks.length === 0)) {
1111
1534
  return { ok: false, error: "Schema must contain at least one valid block" };
1112
1535
  }
@@ -1179,8 +1602,19 @@ function formatAnswer(field2, value) {
1179
1602
  return value === true ? "Yes" : "No";
1180
1603
  case "rating":
1181
1604
  case "linear_scale":
1182
- case "number":
1183
1605
  return typeof value === "number" ? String(value) : String(value);
1606
+ case "number": {
1607
+ const n = typeof value === "number" ? value : Number(value);
1608
+ if (!Number.isFinite(n)) return String(value);
1609
+ const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
1610
+ return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
1611
+ }
1612
+ case "calculated": {
1613
+ const n = typeof value === "number" ? value : Number(value);
1614
+ if (!Number.isFinite(n)) return String(value);
1615
+ const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
1616
+ return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
1617
+ }
1184
1618
  case "phone": {
1185
1619
  const { country, national, e164 } = parsePhone(String(value));
1186
1620
  if (!e164) return String(value);
@@ -1202,6 +1636,92 @@ function formatAnswer(field2, value) {
1202
1636
  }
1203
1637
  }
1204
1638
 
1639
+ // src/number-format.ts
1640
+ function clampDecimals2(decimals) {
1641
+ return Math.max(0, Math.min(6, Math.round(decimals)));
1642
+ }
1643
+ function formatGroupedNumber(value, opts) {
1644
+ if (!Number.isFinite(value)) return String(value);
1645
+ const decimals = opts?.decimals === void 0 ? void 0 : clampDecimals2(opts.decimals);
1646
+ if (typeof Intl === "undefined") {
1647
+ return decimals === void 0 ? String(value) : value.toFixed(decimals);
1648
+ }
1649
+ const format = { useGrouping: true };
1650
+ if (decimals === void 0) {
1651
+ format.maximumFractionDigits = 20;
1652
+ } else {
1653
+ format.minimumFractionDigits = decimals;
1654
+ format.maximumFractionDigits = decimals;
1655
+ }
1656
+ return new Intl.NumberFormat(opts?.locale, format).format(value);
1657
+ }
1658
+ function stripAll(text, separator) {
1659
+ return separator ? text.split(separator).join("") : text;
1660
+ }
1661
+ function toCanonicalDecimal(text, decimal) {
1662
+ return decimal === "." ? text : text.split(decimal).join(".");
1663
+ }
1664
+ function resolveDotGroup(body, decimal) {
1665
+ if (decimal !== "." && body.includes(decimal)) {
1666
+ const i = body.lastIndexOf(decimal);
1667
+ const intPart = stripAll(body.slice(0, i), ".");
1668
+ const fracPart = body.slice(i + decimal.length);
1669
+ return `${intPart}.${fracPart}`;
1670
+ }
1671
+ const segments = body.split(".");
1672
+ if (segments.length === 1) return body;
1673
+ const isPureGrouping = segments.every(
1674
+ (seg, i) => i === 0 ? /^\d{1,3}$/.test(seg) : /^\d{3}$/.test(seg)
1675
+ );
1676
+ if (isPureGrouping) return segments.join("");
1677
+ return `${segments.slice(0, -1).join("")}.${segments.at(-1) ?? ""}`;
1678
+ }
1679
+ function parseGroupedNumber(text, locale) {
1680
+ if (typeof Intl === "undefined") return text;
1681
+ if (text.trim() === "") return text;
1682
+ const parts = new Intl.NumberFormat(locale).formatToParts(12345678e-1);
1683
+ const group = parts.find((p) => p.type === "group")?.value ?? ",";
1684
+ const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
1685
+ let body = text.trim();
1686
+ const negative = body.startsWith("-");
1687
+ if (negative) body = body.slice(1);
1688
+ if (/^\s$/.test(group)) {
1689
+ body = toCanonicalDecimal(body.replace(/\s/g, ""), decimal);
1690
+ } else if (group === ".") {
1691
+ body = resolveDotGroup(body, decimal);
1692
+ } else {
1693
+ body = toCanonicalDecimal(stripAll(body, group), decimal);
1694
+ }
1695
+ if (body === "" || body === "." || !/^\d*\.?\d*$/.test(body)) return text;
1696
+ return negative ? `-${body}` : body;
1697
+ }
1698
+ function isValidPartialNumberText(text, locale) {
1699
+ const negative = text.startsWith("-");
1700
+ const body = negative ? text.slice(1) : text;
1701
+ if (body.includes("-")) return false;
1702
+ if (body === "") return true;
1703
+ if (typeof Intl === "undefined") {
1704
+ return /^[\d.,]*$/.test(body);
1705
+ }
1706
+ const parts = new Intl.NumberFormat(locale).formatToParts(12345678e-1);
1707
+ const group = parts.find((p) => p.type === "group")?.value ?? ",";
1708
+ const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
1709
+ const dotIsGroup = group === ".";
1710
+ const groupIsWhitespace = /^\s$/.test(group);
1711
+ const decimalMarks = dotIsGroup ? [decimal] : Array.from(/* @__PURE__ */ new Set([".", decimal]));
1712
+ let decimalSeen = 0;
1713
+ for (const ch of body) {
1714
+ if (/\d/.test(ch)) continue;
1715
+ if (decimalMarks.includes(ch)) {
1716
+ if (++decimalSeen > 1) return false;
1717
+ continue;
1718
+ }
1719
+ if (groupIsWhitespace ? /\s/.test(ch) : ch === group) continue;
1720
+ return false;
1721
+ }
1722
+ return true;
1723
+ }
1724
+
1205
1725
  // src/ids.ts
1206
1726
  var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1207
1727
  var MASK = 256 - 256 % ALPHABET.length;
@@ -1245,6 +1765,7 @@ var BLOCK_KIND_META = {
1245
1765
  date: { label: "Date", hint: "Date picker" },
1246
1766
  file_upload: { label: "File upload", hint: "Direct to connected storage" },
1247
1767
  hidden: { label: "Hidden field", hint: "Filled from the URL" },
1768
+ calculated: { label: "Calculated value", hint: "Computed from other answers" },
1248
1769
  custom: { label: "Custom", hint: "Your own component (code only)" },
1249
1770
  heading: { label: "Heading", hint: "Section title" },
1250
1771
  paragraph: { label: "Paragraph", hint: "Explanatory text" },
@@ -1314,6 +1835,8 @@ function createBlock(kind) {
1314
1835
  return { id, kind, label: "Sign here" };
1315
1836
  case "hidden":
1316
1837
  return { id, kind, label: "Hidden field" };
1838
+ case "calculated":
1839
+ return { id, kind, label: "Calculated value", calc: { op: "const", value: 0 } };
1317
1840
  case "custom":
1318
1841
  return { id, kind, label: "Custom field", component: "" };
1319
1842
  case "file_upload":
@@ -2344,6 +2867,9 @@ function prefillFromParams(form, params) {
2344
2867
  case "custom":
2345
2868
  break;
2346
2869
  // not prefillable from a URL
2870
+ case "calculated":
2871
+ break;
2872
+ // engine-computed, never prefillable — a crafted URL can't seed it
2347
2873
  default:
2348
2874
  data[field2.id] = raw;
2349
2875
  }
@@ -2494,7 +3020,7 @@ function createFormController(options2) {
2494
3020
  let formId = options2.formId;
2495
3021
  let client = options2.client;
2496
3022
  let respondent = options2.respondent;
2497
- let data = { ...options2.initialData ?? {} };
3023
+ let data = computeCalculated(form, { ...options2.initialData ?? {} });
2498
3024
  let errors = {};
2499
3025
  let pageIndex = 0;
2500
3026
  let status = "idle";
@@ -2511,7 +3037,7 @@ function createFormController(options2) {
2511
3037
  }
2512
3038
  }
2513
3039
  if (merged && status === "idle") {
2514
- data = merged;
3040
+ data = computeCalculated(form, merged);
2515
3041
  notify();
2516
3042
  }
2517
3043
  }
@@ -2639,7 +3165,7 @@ function createFormController(options2) {
2639
3165
  const target = typeof draft.page === "number" && Number.isFinite(draft.page) ? Math.max(0, Math.min(Math.floor(draft.page), form.pages.length - 1)) : 0;
2640
3166
  const movePage = target > 0 && pageIndex === 0;
2641
3167
  if (!merged && !movePage) return;
2642
- if (merged) data = merged;
3168
+ if (merged) data = computeCalculated(form, merged);
2643
3169
  if (movePage) pageIndex = target;
2644
3170
  resumedDraft = true;
2645
3171
  notify();
@@ -2769,7 +3295,7 @@ function createFormController(options2) {
2769
3295
  sessionStarted = true;
2770
3296
  void client.startSession(formId, form.pages.length).then((id) => sessionId = id);
2771
3297
  }
2772
- data = { ...data, [fieldId]: value };
3298
+ data = computeCalculated(form, { ...data, [fieldId]: value });
2773
3299
  onChange?.(data);
2774
3300
  submitError = void 0;
2775
3301
  if (fieldId in errors) {
@@ -2953,6 +3479,7 @@ function createFormController(options2) {
2953
3479
  const next2 = normalizeFormSchema(ctx.form);
2954
3480
  if (!next2.ok) throw new Error(`Invalid form schema: ${next2.error}`);
2955
3481
  form = next2.schema;
3482
+ data = computeCalculated(form, data);
2956
3483
  }
2957
3484
  if (status === "idle" && hasSubmittedOnce(form, formId, data)) {
2958
3485
  status = "submitted";
@@ -2977,6 +3504,7 @@ function createFormController(options2) {
2977
3504
  if (data[key] === void 0) data[key] = value;
2978
3505
  }
2979
3506
  }
3507
+ data = computeCalculated(form, data);
2980
3508
  errors = {};
2981
3509
  pageIndex = 0;
2982
3510
  submitError = void 0;
@@ -2999,7 +3527,9 @@ function isAutoSubmitBlock(block) {
2999
3527
  return isField(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
3000
3528
  }
3001
3529
  function needsExplicitSubmit(visible) {
3002
- const interactive = visible.filter((b) => isField(b) && b.kind !== "hidden");
3530
+ const interactive = visible.filter(
3531
+ (b) => isField(b) && b.kind !== "hidden" && b.kind !== "calculated"
3532
+ );
3003
3533
  if (interactive.length !== 1) return true;
3004
3534
  return !isAutoSubmitBlock(interactive[0]);
3005
3535
  }
@@ -3025,11 +3555,40 @@ function shouldAutoSubmit(field2, value, ctx) {
3025
3555
  const nextData = { ...ctx.data, [field2.id]: value };
3026
3556
  const page = ctx.form.pages.find((p) => p.blocks.some((b) => b.id === field2.id));
3027
3557
  if (!page || !isTerminalPage(ctx.form, page.id, nextData)) return false;
3028
- const interactive = reachableFields(ctx.form, nextData).filter((f) => f.kind !== "hidden");
3558
+ const interactive = reachableFields(ctx.form, nextData).filter(
3559
+ (f) => f.kind !== "hidden" && f.kind !== "calculated"
3560
+ );
3029
3561
  if (interactive.length !== 1 || interactive[0]?.id !== field2.id) return false;
3030
3562
  return validateResponse(ctx.form, nextData).ok;
3031
3563
  }
3032
3564
 
3565
+ // src/radio-nav.ts
3566
+ function radioGroupStep(key, index, length, opts) {
3567
+ if (length <= 0) return null;
3568
+ const rtl = opts?.rtl ?? false;
3569
+ if (key === "Home") return 0;
3570
+ if (key === "End") return length - 1;
3571
+ let delta;
3572
+ switch (key) {
3573
+ case "ArrowRight":
3574
+ delta = rtl ? -1 : 1;
3575
+ break;
3576
+ case "ArrowLeft":
3577
+ delta = rtl ? 1 : -1;
3578
+ break;
3579
+ case "ArrowDown":
3580
+ delta = 1;
3581
+ break;
3582
+ case "ArrowUp":
3583
+ delta = -1;
3584
+ break;
3585
+ default:
3586
+ return null;
3587
+ }
3588
+ const next = index + delta;
3589
+ return Math.min(length - 1, Math.max(0, next));
3590
+ }
3591
+
3033
3592
  // src/appearance.ts
3034
3593
  var FILLO_SLOTS = [
3035
3594
  "root",
@@ -3052,7 +3611,8 @@ var FILLO_SLOTS = [
3052
3611
  "button",
3053
3612
  "success",
3054
3613
  "resume",
3055
- "turnstile"
3614
+ "turnstile",
3615
+ "calculated"
3056
3616
  ];
3057
3617
  var FILLO_DATA_ATTRS = {
3058
3618
  /** Slot name, on every slot element: `data-fillo="control"`. */
@@ -3100,6 +3660,35 @@ var FILLO_THEME_VARS = [
3100
3660
  { var: "--fillo-error" },
3101
3661
  { var: "--fillo-primary-contrast" }
3102
3662
  ];
3663
+ var DARK_LUMINANCE_THRESHOLD = Math.sqrt(1.05 * 0.05) - 0.05;
3664
+ var HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
3665
+ var HEX3 = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i;
3666
+ function srgbToLinear(channel255) {
3667
+ const c = channel255 / 255;
3668
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
3669
+ }
3670
+ function relativeLuminance(token) {
3671
+ const trimmed = token.trim();
3672
+ let hex = null;
3673
+ const six = HEX6.exec(trimmed);
3674
+ if (six) {
3675
+ hex = `${six[1]}${six[2]}${six[3]}`;
3676
+ } else {
3677
+ const three = HEX3.exec(trimmed);
3678
+ if (three) hex = `${three[1]}${three[1]}${three[2]}${three[2]}${three[3]}${three[3]}`;
3679
+ }
3680
+ if (!hex) return null;
3681
+ const r = parseInt(hex.slice(0, 2), 16);
3682
+ const g = parseInt(hex.slice(2, 4), 16);
3683
+ const b = parseInt(hex.slice(4, 6), 16);
3684
+ return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
3685
+ }
3686
+ function resolveThemeAppearance(theme) {
3687
+ if (!theme || theme.colorScheme || !theme.background || !theme.text) return theme;
3688
+ const luminance = relativeLuminance(theme.background);
3689
+ if (luminance == null) return theme;
3690
+ return { ...theme, colorScheme: luminance > DARK_LUMINANCE_THRESHOLD ? "light" : "dark" };
3691
+ }
3103
3692
 
3104
3693
  // src/define.ts
3105
3694
  function defineForm(def) {
@@ -3158,16 +3747,18 @@ function formSchemasEqual(left, right) {
3158
3747
  return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
3159
3748
  }
3160
3749
  var syncTtlMs = (status) => status === "published" ? 36e5 : 6e4;
3161
- var isVolatileSync = (result) => Boolean(result.staged || result.resolvedSchema || result.syncError || result.accepting === false);
3750
+ var isVolatileSync = (result) => Boolean(
3751
+ result.staged || result.resolvedSchema || result.syncError || result.accepting === false || result.uploadsAvailable === false
3752
+ );
3162
3753
  function storageKey(client, handle) {
3163
- return `fillo:sync:v3:${client.baseUrl}|${client.key}|${handle}`;
3754
+ return `fillo:sync:v4:${client.baseUrl}|${client.key}|${handle}`;
3164
3755
  }
3165
3756
  function readStoredSync(client, handle, hash) {
3166
3757
  try {
3167
3758
  const raw = globalThis.localStorage?.getItem(storageKey(client, handle));
3168
3759
  if (!raw) return null;
3169
3760
  const parsed = JSON.parse(raw);
3170
- if (typeof parsed.formId !== "string" || typeof parsed.slug !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number" || parsed.status !== void 0 && parsed.status !== "draft" && parsed.status !== "published")
3761
+ if (typeof parsed.formId !== "string" || typeof parsed.slug !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number" || parsed.uploadFileSizeLimitMb !== void 0 && (typeof parsed.uploadFileSizeLimitMb !== "number" || !Number.isFinite(parsed.uploadFileSizeLimitMb) || parsed.uploadFileSizeLimitMb <= 0) || parsed.status !== void 0 && parsed.status !== "draft" && parsed.status !== "published")
3171
3762
  return null;
3172
3763
  const ttl = syncTtlMs(parsed.status);
3173
3764
  if (Date.now() - parsed.ts > ttl) return null;
@@ -3186,6 +3777,7 @@ function writeStoredSync(client, handle, hash, r) {
3186
3777
  formId: r.formId,
3187
3778
  slug: r.slug,
3188
3779
  status: r.status,
3780
+ uploadFileSizeLimitMb: r.uploadFileSizeLimitMb,
3189
3781
  hash,
3190
3782
  ts: Date.now()
3191
3783
  })
@@ -3208,7 +3800,8 @@ function syncCodeForm(client, form, opts) {
3208
3800
  const promise2 = Promise.resolve({
3209
3801
  formId: stored.formId,
3210
3802
  slug: stored.slug,
3211
- status: stored.status
3803
+ status: stored.status,
3804
+ uploadFileSizeLimitMb: stored.uploadFileSizeLimitMb
3212
3805
  });
3213
3806
  syncCache.set(key, {
3214
3807
  promise: promise2,
@@ -3269,7 +3862,7 @@ var JSX_BLOCK_SPECS = [
3269
3862
  field("Email", "email", ["maxLength"]),
3270
3863
  field("Url", "url", ["maxLength"]),
3271
3864
  field("Phone", "phone", ["defaultCountry"]),
3272
- field("Number", "number", ["min", "max"]),
3865
+ field("Number", "number", ["min", "max", "decimals", "prefix", "suffix", "notation"]),
3273
3866
  field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
3274
3867
  field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
3275
3868
  field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
@@ -3282,6 +3875,7 @@ var JSX_BLOCK_SPECS = [
3282
3875
  field("Date", "date", []),
3283
3876
  field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
3284
3877
  field("Hidden", "hidden", ["paramName", "defaultValue"]),
3878
+ field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
3285
3879
  field("Custom", "custom", ["component", "config"]),
3286
3880
  content("Heading", "heading", true),
3287
3881
  content("Paragraph", "paragraph", true),
@@ -3608,6 +4202,7 @@ export {
3608
4202
  DEFAULT_FIELD_STRINGS,
3609
4203
  DEFAULT_STRINGS,
3610
4204
  DRAFT_KINDS,
4205
+ FILLO_CALC_MIN_SDK_VERSION,
3611
4206
  FILLO_CHALLENGE_MIN_SDK_VERSION,
3612
4207
  FILLO_DATA_ATTRS,
3613
4208
  FILLO_MIN_SDK_VERSION,
@@ -3621,12 +4216,14 @@ export {
3621
4216
  JSX_BLOCK_COMPONENTS,
3622
4217
  JSX_BLOCK_SPECS,
3623
4218
  PHONE_COUNTRIES,
4219
+ PHONE_PICKER_COUNTRIES,
3624
4220
  PHONE_POPOVER_VIEWPORT_GAP,
3625
4221
  REQUIRED_FIELD_MESSAGE,
3626
4222
  Sha1,
3627
4223
  allFields,
3628
4224
  assembleForm,
3629
4225
  codeFormFromJsx,
4226
+ computeCalculated,
3630
4227
  conditionsMet,
3631
4228
  contentHash,
3632
4229
  countryByDialCode,
@@ -3639,9 +4236,11 @@ export {
3639
4236
  createId,
3640
4237
  defineForm,
3641
4238
  digitsOnly,
4239
+ evaluateCalc,
3642
4240
  flagEmoji,
3643
4241
  formSchemasEqual,
3644
4242
  formatAnswer,
4243
+ formatGroupedNumber,
3645
4244
  formatNational,
3646
4245
  isAutoSubmitBlock,
3647
4246
  isBlockVisible,
@@ -3652,15 +4251,18 @@ export {
3652
4251
  isLikelyDevEnv,
3653
4252
  isPossiblePhone,
3654
4253
  isTerminalPage,
4254
+ isValidPartialNumberText,
3655
4255
  needsExplicitSubmit,
3656
4256
  normalizeFormSchema,
3657
4257
  normalizeFormTheme,
3658
4258
  normalizeSettings,
4259
+ parseGroupedNumber,
3659
4260
  parsePhone,
3660
4261
  pipeBlock,
3661
4262
  positionPhonePopover,
3662
4263
  prefillFromParams,
3663
4264
  provisionWorkspace,
4265
+ radioGroupStep,
3664
4266
  reachableFieldIds,
3665
4267
  reachableFields,
3666
4268
  reachablePageIds,
@@ -3669,6 +4271,7 @@ export {
3669
4271
  resolveSlotClass,
3670
4272
  resolveStrings,
3671
4273
  resolveText,
4274
+ resolveThemeAppearance,
3672
4275
  responseScopeValue,
3673
4276
  schemaFromJsx,
3674
4277
  sha1Base64,