@usefillo/cli 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1298,79 +1298,122 @@ var CONTENT_KINDS = ["heading", "paragraph", "divider"];
1298
1298
  function isField(block) {
1299
1299
  return !CONTENT_KINDS.includes(block.kind);
1300
1300
  }
1301
- var C = (iso2, name, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name, dialCode, lengths, groups, example });
1301
+ var regionNames;
1302
+ function regionDisplayNames() {
1303
+ if (regionNames !== void 0) return regionNames ?? void 0;
1304
+ if (typeof Intl === "undefined" || typeof Intl.DisplayNames === "undefined") {
1305
+ regionNames = null;
1306
+ return void 0;
1307
+ }
1308
+ try {
1309
+ regionNames = new Intl.DisplayNames(void 0, { type: "region" });
1310
+ } catch {
1311
+ regionNames = null;
1312
+ }
1313
+ return regionNames ?? void 0;
1314
+ }
1315
+ function regionName(iso2) {
1316
+ try {
1317
+ return regionDisplayNames()?.of(iso2) ?? iso2;
1318
+ } catch {
1319
+ return iso2;
1320
+ }
1321
+ }
1322
+ var C = (iso2, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name: regionName(iso2), dialCode, lengths, groups, example });
1323
+ var CURATED_COUNTRIES = [
1324
+ C("US", "1", [10], [3, 3, 4], "2015550123"),
1325
+ C("CA", "1", [10], [3, 3, 4], "5145550123"),
1326
+ C("GB", "44", [10], [4, 6], "7400123456"),
1327
+ C("AU", "61", [9], [3, 3, 3], "412345678"),
1328
+ C("DE", "49", [10, 11], [4, 7], "15123456789"),
1329
+ C("FR", "33", [9], [1, 2, 2, 2, 2], "612345678"),
1330
+ C("IN", "91", [10], [5, 5], "8123456789"),
1331
+ C("DK", "45", [8], [2, 2, 2, 2], "32123456"),
1332
+ C("SE", "46", [9], [2, 3, 4], "701234567"),
1333
+ C("NO", "47", [8], [3, 2, 3], "40612345"),
1334
+ C("FI", "358", [9, 10], [2, 3, 4], "412345678"),
1335
+ C("NL", "31", [9], [1, 4, 4], "612345678"),
1336
+ C("BE", "32", [9], [3, 2, 2, 2], "470123456"),
1337
+ C("ES", "34", [9], [3, 3, 3], "612345678"),
1338
+ C("IT", "39", [9, 10], [3, 3, 4], "3123456789"),
1339
+ C("PT", "351", [9], [3, 3, 3], "912345678"),
1340
+ C("IE", "353", [9], [2, 3, 4], "851234567"),
1341
+ C("CH", "41", [9], [2, 3, 2, 2], "781234567"),
1342
+ C("AT", "43", [10, 11], [3, 7], "6641234567"),
1343
+ C("PL", "48", [9], [3, 3, 3], "512345678"),
1344
+ C("CZ", "420", [9], [3, 3, 3], "601123456"),
1345
+ C("GR", "30", [10], [3, 3, 4], "6912345678"),
1346
+ C("RO", "40", [9], [3, 3, 3], "712345678"),
1347
+ C("HU", "36", [9], [2, 3, 4], "201234567"),
1348
+ C("UA", "380", [9], [2, 3, 2, 2], "501234567"),
1349
+ C("RU", "7", [10], [3, 3, 2, 2], "9123456789"),
1350
+ C("TR", "90", [10], [3, 3, 2, 2], "5012345678"),
1351
+ C("IL", "972", [9], [2, 3, 4], "501234567"),
1352
+ C("AE", "971", [9], [2, 3, 4], "501234567"),
1353
+ C("SA", "966", [9], [2, 3, 4], "512345678"),
1354
+ C("ZA", "27", [9], [2, 3, 4], "711234567"),
1355
+ C("NG", "234", [10], [3, 3, 4], "8021234567"),
1356
+ C("KE", "254", [9], [3, 3, 3], "712123456"),
1357
+ C("EG", "20", [10], [3, 3, 4], "1001234567"),
1358
+ C("BR", "55", [10, 11], [2, 5, 4], "11961234567"),
1359
+ C("MX", "52", [10], [3, 3, 4], "2221234567"),
1360
+ C("AR", "54", [10], [4, 6], "1123456789"),
1361
+ C("CL", "56", [9], [1, 4, 4], "221234567"),
1362
+ C("CO", "57", [10], [3, 3, 4], "3211234567"),
1363
+ C("PE", "51", [9], [3, 3, 3], "912345678"),
1364
+ C("CN", "86", [11], [3, 4, 4], "13123456789"),
1365
+ C("JP", "81", [10], [2, 4, 4], "9012345678"),
1366
+ C("KR", "82", [9, 10], [2, 4, 4], "1023456789"),
1367
+ C("SG", "65", [8], [4, 4], "81234567"),
1368
+ C("HK", "852", [8], [4, 4], "51234567"),
1369
+ C("TW", "886", [9], [3, 3, 3], "912345678"),
1370
+ C("MY", "60", [9, 10], [2, 3, 4], "123456789"),
1371
+ C("ID", "62", [10, 11], [3, 4, 4], "81234567890"),
1372
+ C("TH", "66", [9], [2, 3, 4], "812345678"),
1373
+ C("PH", "63", [10], [3, 3, 4], "9171234567"),
1374
+ C("VN", "84", [9], [3, 3, 3], "912345678"),
1375
+ C("PK", "92", [10], [3, 3, 4], "3012345678"),
1376
+ C("BD", "880", [10], [2, 4, 4], "1812345678"),
1377
+ C("NZ", "64", [9, 10], [2, 3, 4], "211234567"),
1378
+ C("SK", "421", [9], [3, 3, 3], "912123456"),
1379
+ C("BG", "359", [9], [2, 3, 4], "481234567"),
1380
+ C("HR", "385", [8, 9], [2, 3, 4], "921234567"),
1381
+ C("RS", "381", [8, 9], [2, 3, 4], "601234567"),
1382
+ C("SI", "386", [8], [2, 3, 3], "31234567"),
1383
+ C("LT", "370", [8], [3, 2, 3], "61234567"),
1384
+ C("LV", "371", [8], [2, 3, 3], "21234567"),
1385
+ C("EE", "372", [7, 8], [3, 4], "51234567"),
1386
+ C("IS", "354", [7], [3, 4], "6111234"),
1387
+ C("LU", "352", [9], [3, 3, 3], "628123456"),
1388
+ C("MA", "212", [9], [3, 3, 3], "650123456"),
1389
+ C("CR", "506", [8], [4, 4], "83123456"),
1390
+ C("EC", "593", [9], [2, 3, 4], "991234567"),
1391
+ C("UY", "598", [8], [1, 3, 4], "94231234"),
1392
+ C("QA", "974", [8], [4, 4], "33123456"),
1393
+ C("KW", "965", [8], [4, 4], "50012345")
1394
+ ];
1395
+ 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";
1396
+ function parsePackedCountries(packed) {
1397
+ const out = [];
1398
+ for (const token of packed.trim().split(/\s+/)) {
1399
+ const iso2 = token.slice(0, 2);
1400
+ const dialCode = token.slice(2);
1401
+ out.push({ iso2, name: regionName(iso2), dialCode, lengths: [], groups: [], example: "" });
1402
+ }
1403
+ return out;
1404
+ }
1302
1405
  var PHONE_COUNTRIES = [
1303
- C("US", "United States", "1", [10], [3, 3, 4], "2015550123"),
1304
- C("CA", "Canada", "1", [10], [3, 3, 4], "5145550123"),
1305
- C("GB", "United Kingdom", "44", [10], [4, 6], "7400123456"),
1306
- C("AU", "Australia", "61", [9], [3, 3, 3], "412345678"),
1307
- C("DE", "Germany", "49", [10, 11], [4, 7], "15123456789"),
1308
- C("FR", "France", "33", [9], [1, 2, 2, 2, 2], "612345678"),
1309
- C("IN", "India", "91", [10], [5, 5], "8123456789"),
1310
- C("DK", "Denmark", "45", [8], [2, 2, 2, 2], "32123456"),
1311
- C("SE", "Sweden", "46", [9], [2, 3, 4], "701234567"),
1312
- C("NO", "Norway", "47", [8], [3, 2, 3], "40612345"),
1313
- C("FI", "Finland", "358", [9, 10], [2, 3, 4], "412345678"),
1314
- C("NL", "Netherlands", "31", [9], [1, 4, 4], "612345678"),
1315
- C("BE", "Belgium", "32", [9], [3, 2, 2, 2], "470123456"),
1316
- C("ES", "Spain", "34", [9], [3, 3, 3], "612345678"),
1317
- C("IT", "Italy", "39", [9, 10], [3, 3, 4], "3123456789"),
1318
- C("PT", "Portugal", "351", [9], [3, 3, 3], "912345678"),
1319
- C("IE", "Ireland", "353", [9], [2, 3, 4], "851234567"),
1320
- C("CH", "Switzerland", "41", [9], [2, 3, 2, 2], "781234567"),
1321
- C("AT", "Austria", "43", [10, 11], [3, 7], "6641234567"),
1322
- C("PL", "Poland", "48", [9], [3, 3, 3], "512345678"),
1323
- C("CZ", "Czechia", "420", [9], [3, 3, 3], "601123456"),
1324
- C("GR", "Greece", "30", [10], [3, 3, 4], "6912345678"),
1325
- C("RO", "Romania", "40", [9], [3, 3, 3], "712345678"),
1326
- C("HU", "Hungary", "36", [9], [2, 3, 4], "201234567"),
1327
- C("UA", "Ukraine", "380", [9], [2, 3, 2, 2], "501234567"),
1328
- C("RU", "Russia", "7", [10], [3, 3, 2, 2], "9123456789"),
1329
- C("TR", "Turkey", "90", [10], [3, 3, 2, 2], "5012345678"),
1330
- C("IL", "Israel", "972", [9], [2, 3, 4], "501234567"),
1331
- C("AE", "United Arab Emirates", "971", [9], [2, 3, 4], "501234567"),
1332
- C("SA", "Saudi Arabia", "966", [9], [2, 3, 4], "512345678"),
1333
- C("ZA", "South Africa", "27", [9], [2, 3, 4], "711234567"),
1334
- C("NG", "Nigeria", "234", [10], [3, 3, 4], "8021234567"),
1335
- C("KE", "Kenya", "254", [9], [3, 3, 3], "712123456"),
1336
- C("EG", "Egypt", "20", [10], [3, 3, 4], "1001234567"),
1337
- C("BR", "Brazil", "55", [10, 11], [2, 5, 4], "11961234567"),
1338
- C("MX", "Mexico", "52", [10], [3, 3, 4], "2221234567"),
1339
- C("AR", "Argentina", "54", [10], [4, 6], "1123456789"),
1340
- C("CL", "Chile", "56", [9], [1, 4, 4], "221234567"),
1341
- C("CO", "Colombia", "57", [10], [3, 3, 4], "3211234567"),
1342
- C("PE", "Peru", "51", [9], [3, 3, 3], "912345678"),
1343
- C("CN", "China", "86", [11], [3, 4, 4], "13123456789"),
1344
- C("JP", "Japan", "81", [10], [2, 4, 4], "9012345678"),
1345
- C("KR", "South Korea", "82", [9, 10], [2, 4, 4], "1023456789"),
1346
- C("SG", "Singapore", "65", [8], [4, 4], "81234567"),
1347
- C("HK", "Hong Kong", "852", [8], [4, 4], "51234567"),
1348
- C("TW", "Taiwan", "886", [9], [3, 3, 3], "912345678"),
1349
- C("MY", "Malaysia", "60", [9, 10], [2, 3, 4], "123456789"),
1350
- C("ID", "Indonesia", "62", [10, 11], [3, 4, 4], "81234567890"),
1351
- C("TH", "Thailand", "66", [9], [2, 3, 4], "812345678"),
1352
- C("PH", "Philippines", "63", [10], [3, 3, 4], "9171234567"),
1353
- C("VN", "Vietnam", "84", [9], [3, 3, 3], "912345678"),
1354
- C("PK", "Pakistan", "92", [10], [3, 3, 4], "3012345678"),
1355
- C("BD", "Bangladesh", "880", [10], [2, 4, 4], "1812345678"),
1356
- C("NZ", "New Zealand", "64", [9, 10], [2, 3, 4], "211234567"),
1357
- C("SK", "Slovakia", "421", [9], [3, 3, 3], "912123456"),
1358
- C("BG", "Bulgaria", "359", [9], [2, 3, 4], "481234567"),
1359
- C("HR", "Croatia", "385", [8, 9], [2, 3, 4], "921234567"),
1360
- C("RS", "Serbia", "381", [8, 9], [2, 3, 4], "601234567"),
1361
- C("SI", "Slovenia", "386", [8], [2, 3, 3], "31234567"),
1362
- C("LT", "Lithuania", "370", [8], [3, 2, 3], "61234567"),
1363
- C("LV", "Latvia", "371", [8], [2, 3, 3], "21234567"),
1364
- C("EE", "Estonia", "372", [7, 8], [3, 4], "51234567"),
1365
- C("IS", "Iceland", "354", [7], [3, 4], "6111234"),
1366
- C("LU", "Luxembourg", "352", [9], [3, 3, 3], "628123456"),
1367
- C("MA", "Morocco", "212", [9], [3, 3, 3], "650123456"),
1368
- C("CR", "Costa Rica", "506", [8], [4, 4], "83123456"),
1369
- C("EC", "Ecuador", "593", [9], [2, 3, 4], "991234567"),
1370
- C("UY", "Uruguay", "598", [8], [1, 3, 4], "94231234"),
1371
- C("QA", "Qatar", "974", [8], [4, 4], "33123456"),
1372
- C("KW", "Kuwait", "965", [8], [4, 4], "50012345")
1406
+ ...CURATED_COUNTRIES,
1407
+ ...parsePackedCountries(PACKED_DIAL_CODES)
1373
1408
  ];
1409
+ var PHONE_PICKER_COUNTRIES = sortForPicker(PHONE_COUNTRIES);
1410
+ function sortForPicker(countries) {
1411
+ if (typeof Intl === "undefined" || typeof Intl.Collator === "undefined") {
1412
+ return [...countries].sort((a, b) => a.iso2 < b.iso2 ? -1 : a.iso2 > b.iso2 ? 1 : 0);
1413
+ }
1414
+ const collator = new Intl.Collator(void 0, { sensitivity: "base" });
1415
+ return [...countries].sort((a, b) => collator.compare(a.name, b.name));
1416
+ }
1374
1417
  var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
1375
1418
  var fileValueSchema = object({
1376
1419
  fileId: string2().check(_minLength(1)),
@@ -1398,6 +1441,7 @@ var FIELD_KINDS = [
1398
1441
  "date",
1399
1442
  "file_upload",
1400
1443
  "hidden",
1444
+ "calculated",
1401
1445
  "custom"
1402
1446
  ];
1403
1447
  var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
@@ -1434,7 +1478,7 @@ var schemaShape = object({
1434
1478
  });
1435
1479
  var MAX_SCHEMA_VERSION = 1;
1436
1480
  var FILLO_SCHEMA_VERSION = 1;
1437
- var FILLO_SDK_VERSION = true ? "0.10.0" : "0.0.0-dev";
1481
+ var FILLO_SDK_VERSION = true ? "0.12.0" : "0.0.0-dev";
1438
1482
  function str(value, max, fallback = "") {
1439
1483
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
1440
1484
  }
@@ -1442,6 +1486,11 @@ function optionalStr(value, max) {
1442
1486
  const s = str(value, max);
1443
1487
  return s === "" ? void 0 : s;
1444
1488
  }
1489
+ function optionalAffix(value, max) {
1490
+ if (typeof value !== "string") return void 0;
1491
+ const s = value.slice(0, max);
1492
+ return s.trim() === "" ? void 0 : s;
1493
+ }
1445
1494
  function bool(value) {
1446
1495
  return typeof value === "boolean" ? value : void 0;
1447
1496
  }
@@ -1489,7 +1538,17 @@ function conditions(value) {
1489
1538
  });
1490
1539
  return normalized.length ? normalized : void 0;
1491
1540
  }
1492
- function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
1541
+ function jumpDepsAllowed(fieldId, calcRefs, allowed, seen) {
1542
+ const refs = calcRefs.get(fieldId);
1543
+ if (refs === void 0) return allowed.has(fieldId);
1544
+ if (seen.has(fieldId)) return true;
1545
+ seen.add(fieldId);
1546
+ for (const ref of refs) {
1547
+ if (!jumpDepsAllowed(ref, calcRefs, allowed, seen)) return false;
1548
+ }
1549
+ return true;
1550
+ }
1551
+ function jumpRules(value, pageIds, sourceId, allowedFieldIds, calcRefs) {
1493
1552
  if (!Array.isArray(value)) return void 0;
1494
1553
  const rules = value.slice(0, 50).flatMap((item) => {
1495
1554
  if (!item || typeof item !== "object") return [];
@@ -1501,11 +1560,187 @@ function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
1501
1560
  if (rawWhen !== void 0 && !Array.isArray(rawWhen)) return [];
1502
1561
  const when2 = conditions(rawWhen);
1503
1562
  if (Array.isArray(rawWhen) && rawWhen.length > 0 && when2 === void 0) return [];
1504
- if (when2 && when2.some((c) => !allowedFieldIds.has(c.fieldId))) return [];
1563
+ if (when2 && when2.some((c) => !jumpDepsAllowed(c.fieldId, calcRefs, allowedFieldIds, /* @__PURE__ */ new Set())))
1564
+ return [];
1505
1565
  return [{ when: when2 ?? [], to }];
1506
1566
  });
1507
1567
  return rules.length ? rules : void 0;
1508
1568
  }
1569
+ var CALC_OPERAND_KINDS = ["number", "rating", "linear_scale", "calculated"];
1570
+ function normalizeCalc(value, depth = 0) {
1571
+ if (depth > 32) return null;
1572
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1573
+ const rec = value;
1574
+ switch (rec.op) {
1575
+ case "value": {
1576
+ const fieldId = str(rec.fieldId, 128);
1577
+ return fieldId ? { op: "value", fieldId } : null;
1578
+ }
1579
+ case "const": {
1580
+ const n = finite(rec.value);
1581
+ return n === void 0 ? null : { op: "const", value: n };
1582
+ }
1583
+ case "add":
1584
+ case "mul":
1585
+ case "min":
1586
+ case "max": {
1587
+ if (!Array.isArray(rec.args) || rec.args.length < 1 || rec.args.length > 50) return null;
1588
+ const args = [];
1589
+ for (const raw of rec.args) {
1590
+ const arg = normalizeCalc(raw, depth + 1);
1591
+ if (!arg) return null;
1592
+ args.push(arg);
1593
+ }
1594
+ return { op: rec.op, args };
1595
+ }
1596
+ case "sub":
1597
+ case "div": {
1598
+ const left = normalizeCalc(rec.left, depth + 1);
1599
+ const right = normalizeCalc(rec.right, depth + 1);
1600
+ return left && right ? { op: rec.op, left, right } : null;
1601
+ }
1602
+ case "round": {
1603
+ const arg = normalizeCalc(rec.arg, depth + 1);
1604
+ if (!arg) return null;
1605
+ return cleanObject({
1606
+ op: "round",
1607
+ arg,
1608
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0)
1609
+ });
1610
+ }
1611
+ case "if": {
1612
+ if (!Array.isArray(rec.when)) return null;
1613
+ const when2 = conditions(rec.when) ?? [];
1614
+ if (when2.length !== rec.when.length) return null;
1615
+ const then = normalizeCalc(rec.then, depth + 1);
1616
+ const els = normalizeCalc(rec.else, depth + 1);
1617
+ return then && els ? { op: "if", when: when2, then, else: els } : null;
1618
+ }
1619
+ default:
1620
+ return null;
1621
+ }
1622
+ }
1623
+ function collectCalcRefs(expr, valueRefs, whenRefs) {
1624
+ switch (expr.op) {
1625
+ case "value":
1626
+ valueRefs.add(expr.fieldId);
1627
+ return;
1628
+ case "const":
1629
+ return;
1630
+ case "add":
1631
+ case "mul":
1632
+ case "min":
1633
+ case "max":
1634
+ for (const arg of expr.args) collectCalcRefs(arg, valueRefs, whenRefs);
1635
+ return;
1636
+ case "sub":
1637
+ case "div":
1638
+ collectCalcRefs(expr.left, valueRefs, whenRefs);
1639
+ collectCalcRefs(expr.right, valueRefs, whenRefs);
1640
+ return;
1641
+ case "round":
1642
+ collectCalcRefs(expr.arg, valueRefs, whenRefs);
1643
+ return;
1644
+ case "if":
1645
+ for (const cond of expr.when) whenRefs.add(cond.fieldId);
1646
+ collectCalcRefs(expr.then, valueRefs, whenRefs);
1647
+ collectCalcRefs(expr.else, valueRefs, whenRefs);
1648
+ return;
1649
+ }
1650
+ }
1651
+ function validateCalculatedFields(pages) {
1652
+ const kinds = /* @__PURE__ */ new Map();
1653
+ for (const page of pages) {
1654
+ for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
1655
+ }
1656
+ const deps = /* @__PURE__ */ new Map();
1657
+ for (const page of pages) {
1658
+ for (const block of page.blocks) {
1659
+ if (block.kind !== "calculated") continue;
1660
+ const calc = normalizeCalc(block.calc);
1661
+ if (!calc) {
1662
+ return `Field ${block.id} has a malformed calc expression \u2014 build it from value/const/add/sub/mul/div/min/max/round/if nodes`;
1663
+ }
1664
+ block.calc = calc;
1665
+ const valueRefs = /* @__PURE__ */ new Set();
1666
+ const whenRefs = /* @__PURE__ */ new Set();
1667
+ collectCalcRefs(calc, valueRefs, whenRefs);
1668
+ for (const ref of valueRefs) {
1669
+ const kind = kinds.get(ref);
1670
+ if (kind === void 0) {
1671
+ return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
1672
+ }
1673
+ if (!CALC_OPERAND_KINDS.includes(kind)) {
1674
+ return `Calculated field ${block.id} references non-numeric field ${ref} (${kind}) \u2014 calculations can read number, rating, linear_scale, and calculated fields only`;
1675
+ }
1676
+ }
1677
+ deps.set(
1678
+ block.id,
1679
+ [...valueRefs, ...whenRefs].filter((ref) => kinds.get(ref) === "calculated")
1680
+ );
1681
+ }
1682
+ }
1683
+ const state = /* @__PURE__ */ new Map();
1684
+ const visit = (id, path) => {
1685
+ const mark = state.get(id);
1686
+ if (mark === "done") return null;
1687
+ if (mark === "visiting") {
1688
+ const chain = [...path.slice(path.indexOf(id)), id].join(" \u2192 ");
1689
+ return `Calculated field ${id} depends on its own result (${chain}) \u2014 break the cycle so the value can be computed`;
1690
+ }
1691
+ state.set(id, "visiting");
1692
+ for (const dep of deps.get(id) ?? []) {
1693
+ const error = visit(dep, [...path, id]);
1694
+ if (error) return error;
1695
+ }
1696
+ state.set(id, "done");
1697
+ return null;
1698
+ };
1699
+ for (const id of deps.keys()) {
1700
+ const error = visit(id, []);
1701
+ if (error) return error;
1702
+ }
1703
+ return null;
1704
+ }
1705
+ var MAX_CALC_FIELDS = 100;
1706
+ var MAX_CALC_TOTAL_NODES = 5e3;
1707
+ function countCalcNodes(expr) {
1708
+ switch (expr.op) {
1709
+ case "value":
1710
+ case "const":
1711
+ return 1;
1712
+ case "add":
1713
+ case "mul":
1714
+ case "min":
1715
+ case "max":
1716
+ return 1 + expr.args.reduce((n, arg) => n + countCalcNodes(arg), 0);
1717
+ case "sub":
1718
+ case "div":
1719
+ return 1 + countCalcNodes(expr.left) + countCalcNodes(expr.right);
1720
+ case "round":
1721
+ return 1 + countCalcNodes(expr.arg);
1722
+ case "if":
1723
+ return 1 + countCalcNodes(expr.then) + countCalcNodes(expr.else);
1724
+ }
1725
+ }
1726
+ function calcComplexityError(pages) {
1727
+ let calcFields = 0;
1728
+ let totalNodes = 0;
1729
+ for (const page of pages) {
1730
+ for (const block of page.blocks) {
1731
+ if (block.kind !== "calculated") continue;
1732
+ calcFields += 1;
1733
+ totalNodes += countCalcNodes(block.calc);
1734
+ }
1735
+ }
1736
+ if (calcFields > MAX_CALC_FIELDS) {
1737
+ return `Form has ${calcFields} calculated fields \u2014 the maximum is ${MAX_CALC_FIELDS}; remove some or consolidate them into fewer expressions`;
1738
+ }
1739
+ if (totalNodes > MAX_CALC_TOTAL_NODES) {
1740
+ return `Calculated expressions total ${totalNodes} operations \u2014 the maximum is ${MAX_CALC_TOTAL_NODES} across the form; simplify or split up the calculations`;
1741
+ }
1742
+ return null;
1743
+ }
1509
1744
  function baseBlock(rec) {
1510
1745
  return {
1511
1746
  id: str(rec.id, 128),
@@ -1617,7 +1852,20 @@ function normalizeBlock(input) {
1617
1852
  maxLength: finite(rec.maxLength) === void 0 ? void 0 : int(rec.maxLength, 1, 2e4, 2e4)
1618
1853
  });
1619
1854
  case "number":
1620
- return cleanObject({ ...baseField(rec, kind), min: finite(rec.min), max: finite(rec.max) });
1855
+ return cleanObject({
1856
+ ...baseField(rec, kind),
1857
+ min: finite(rec.min),
1858
+ max: finite(rec.max),
1859
+ // Display-only (decision 1 in the number-formatting contract) —
1860
+ // mirrors the calculated field's decimals/prefix/suffix carry-through
1861
+ // exactly, plus notation which calculated doesn't have.
1862
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
1863
+ prefix: optionalAffix(rec.prefix, 100),
1864
+ suffix: optionalAffix(rec.suffix, 100),
1865
+ // The only supported values; anything else drops silently rather than
1866
+ // erroring (an old/garbage value is just "no grouping").
1867
+ notation: rec.notation === "grouped" ? "grouped" : rec.notation === "grouped-comma" ? "grouped-comma" : rec.notation === "grouped-dot" ? "grouped-dot" : void 0
1868
+ });
1621
1869
  case "select":
1622
1870
  case "multi_select":
1623
1871
  case "dropdown": {
@@ -1681,6 +1929,21 @@ function normalizeBlock(input) {
1681
1929
  paramName: optionalStr(rec.paramName, 128),
1682
1930
  defaultValue: optionalStr(rec.defaultValue, 1e3)
1683
1931
  });
1932
+ case "calculated":
1933
+ return cleanObject({
1934
+ ...baseField(rec, kind),
1935
+ // A derived value isn't answerable, so it can't be required (the
1936
+ // hidden-field precedent) — a required one could never be satisfied.
1937
+ required: false,
1938
+ // Kept raw here on purpose: the calc AST is normalized and structurally
1939
+ // validated in normalizeFormSchema once the full field universe is
1940
+ // known (missing refs, operand kinds, cycles are HARD errors there — a
1941
+ // calculated field must never be silently dropped or degraded).
1942
+ calc: rec.calc,
1943
+ decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
1944
+ prefix: optionalAffix(rec.prefix, 100),
1945
+ suffix: optionalAffix(rec.suffix, 100)
1946
+ });
1684
1947
  case "file_upload":
1685
1948
  return cleanObject({
1686
1949
  ...baseField(rec, kind),
@@ -1719,6 +1982,22 @@ function normalizeFormSchema(input) {
1719
1982
  return { id: str(page.id, 128), title: optionalStr(page.title, 500), blocks, next: page.next };
1720
1983
  });
1721
1984
  const targetIds = new Set(rebuilt.map((p) => p.id));
1985
+ const calcRefs = /* @__PURE__ */ new Map();
1986
+ for (const page of rebuilt) {
1987
+ for (const block of page.blocks) {
1988
+ if (block.kind !== "calculated") continue;
1989
+ const refs = /* @__PURE__ */ new Set();
1990
+ const calc = normalizeCalc(block.calc);
1991
+ if (calc) {
1992
+ const valueRefs = /* @__PURE__ */ new Set();
1993
+ const whenRefs = /* @__PURE__ */ new Set();
1994
+ collectCalcRefs(calc, valueRefs, whenRefs);
1995
+ for (const ref of valueRefs) refs.add(ref);
1996
+ for (const ref of whenRefs) refs.add(ref);
1997
+ }
1998
+ calcRefs.set(block.id, refs);
1999
+ }
2000
+ }
1722
2001
  const priorFieldIds = /* @__PURE__ */ new Set();
1723
2002
  const pages = rebuilt.map((page) => {
1724
2003
  for (const block of page.blocks) {
@@ -1728,7 +2007,7 @@ function normalizeFormSchema(input) {
1728
2007
  id: page.id,
1729
2008
  title: page.title,
1730
2009
  blocks: page.blocks,
1731
- next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds))
2010
+ next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds), calcRefs)
1732
2011
  });
1733
2012
  });
1734
2013
  const pageIds = /* @__PURE__ */ new Set();
@@ -1763,6 +2042,10 @@ function normalizeFormSchema(input) {
1763
2042
  }
1764
2043
  }
1765
2044
  }
2045
+ const calcError = validateCalculatedFields(pages);
2046
+ if (calcError) return { ok: false, error: calcError };
2047
+ const calcComplexity = calcComplexityError(pages);
2048
+ if (calcComplexity) return { ok: false, error: calcComplexity };
1766
2049
  if (pages.every((page) => page.blocks.length === 0)) {
1767
2050
  return { ok: false, error: "Schema must contain at least one valid block" };
1768
2051
  }
@@ -1788,6 +2071,7 @@ var MASK = 256 - 256 % ALPHABET.length;
1788
2071
  var S3_PART_TIMEOUT_MS = 5 * 6e4;
1789
2072
  var UPLOAD_CONTROL_TIMEOUT_MS = 10 * 6e4;
1790
2073
  var CLIENT_HEADER = { "X-Fillo-Client": `@usefillo/core@${FILLO_SDK_VERSION}` };
2074
+ var DARK_LUMINANCE_THRESHOLD = Math.sqrt(1.05 * 0.05) - 0.05;
1791
2075
  var FilloJsxError = class extends Error {
1792
2076
  constructor(code, message) {
1793
2077
  super(`[fillo] ${message} \u2014 https://fillo.so/docs/troubleshooting#jsx-${code.toLowerCase()}`);
@@ -1818,7 +2102,7 @@ var JSX_BLOCK_SPECS = [
1818
2102
  field("Email", "email", ["maxLength"]),
1819
2103
  field("Url", "url", ["maxLength"]),
1820
2104
  field("Phone", "phone", ["defaultCountry"]),
1821
- field("Number", "number", ["min", "max"]),
2105
+ field("Number", "number", ["min", "max", "decimals", "prefix", "suffix", "notation"]),
1822
2106
  field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
1823
2107
  field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
1824
2108
  field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
@@ -1831,6 +2115,7 @@ var JSX_BLOCK_SPECS = [
1831
2115
  field("Date", "date", []),
1832
2116
  field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
1833
2117
  field("Hidden", "hidden", ["paramName", "defaultValue"]),
2118
+ field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
1834
2119
  field("Custom", "custom", ["component", "config"]),
1835
2120
  content("Heading", "heading", true),
1836
2121
  content("Paragraph", "paragraph", true),
@@ -2024,7 +2309,8 @@ Login failed (${r.status}).`);
2024
2309
  return;
2025
2310
  }
2026
2311
  if (body.error === "slow_down") interval += 2e3;
2027
- else if (body.error && body.error !== "authorization_pending") die(`
2312
+ else if (body.error && body.error !== "authorization_pending")
2313
+ die(`
2028
2314
  Login failed: ${body.error}`);
2029
2315
  }
2030
2316
  die("\nLogin timed out \u2014 run `fillo login` again.");
@@ -2087,9 +2373,7 @@ async function publish(handle, flags) {
2087
2373
  const res = await api(`/cli/forms/${encodeURIComponent(handle)}/publish`, {
2088
2374
  method: "POST",
2089
2375
  token: requireToken(),
2090
- body: JSON.stringify(
2091
- flags["allow-breaking"] === true ? { allowBreaking: true } : {}
2092
- )
2376
+ body: JSON.stringify(flags["allow-breaking"] === true ? { allowBreaking: true } : {})
2093
2377
  });
2094
2378
  if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2095
2379
  if (res.status === 404) {
@@ -2116,7 +2400,9 @@ async function publish(handle, flags) {
2116
2400
  process.exit(1);
2117
2401
  }
2118
2402
  if (res.status === 409 && typeof body.warningUrl === "string" && body.warningUrl) {
2119
- console.error(`\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `publish failed (${res.status}).`)}`);
2403
+ console.error(
2404
+ `\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `publish failed (${res.status}).`)}`
2405
+ );
2120
2406
  console.error(` Storage settings: ${terminalText(body.warningUrl)}`);
2121
2407
  process.exit(1);
2122
2408
  }
@@ -2284,6 +2570,8 @@ function printPushed(formId, url, updated, draft = false) {
2284
2570
  if (draft) console.log(` ${dim("Review and publish it from the Fillo dashboard.")}`);
2285
2571
  else if (url) console.log(` Live at ${terminalText(url)}`);
2286
2572
  console.log(` Embed: <FilloForm formId="${terminalText(formId)}" />
2573
+ `);
2574
+ console.log(` Verify: [data-fillo-form-id="${terminalText(formId)}"] on the actual app route
2287
2575
  `);
2288
2576
  }
2289
2577
  function syncError(response, body, credential) {
@@ -2329,6 +2617,8 @@ function printSynced(body, requestedStage) {
2329
2617
  if (body.warning) console.log(` ${dim(`Before publishing: ${body.warning}`)}`);
2330
2618
  if (body.warningUrl) console.log(` Storage settings: ${terminalText(body.warningUrl)}`);
2331
2619
  console.log(` Embed: <FilloForm formId="${terminalText(formId)}" />
2620
+ `);
2621
+ console.log(` Verify: [data-fillo-form-id="${terminalText(formId)}"] on the actual app route
2332
2622
  `);
2333
2623
  }
2334
2624
  async function pushWithSync(items, credential, requestedStage) {
@@ -2374,7 +2664,9 @@ async function push(file, flags) {
2374
2664
  );
2375
2665
  }
2376
2666
  if (!syncToken && !accountToken && stage) {
2377
- die(`To stage changes, run \`fillo login\` or set ${SYNC_TOKEN_ENV} from Settings > Developers.`);
2667
+ die(
2668
+ `To stage changes, run \`fillo login\` or set ${SYNC_TOKEN_ENV} from Settings > Developers.`
2669
+ );
2378
2670
  }
2379
2671
  const handle = typeof flags.handle === "string" ? flags.handle : void 0;
2380
2672
  const items = toPushItems(await loadSchema(file, flags["allow-code"] === true), handle);
@@ -2419,7 +2711,9 @@ async function push(file, flags) {
2419
2711
  const body = await readJson(res);
2420
2712
  if (!res.ok || !body.formId) {
2421
2713
  if (res.status === 409 && typeof body.warningUrl === "string" && body.warningUrl) {
2422
- console.error(`\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `push failed (${res.status}).`)}`);
2714
+ console.error(
2715
+ `\x1B[31m\u2717\x1B[0m ${terminalText(body.error ?? `push failed (${res.status}).`)}`
2716
+ );
2423
2717
  console.error(` Storage settings: ${terminalText(body.warningUrl)}`);
2424
2718
  process.exit(1);
2425
2719
  }
@@ -2438,7 +2732,9 @@ async function push(file, flags) {
2438
2732
  async function init(flags) {
2439
2733
  const email3 = typeof flags.email === "string" ? flags.email.trim() : "";
2440
2734
  if (!email3) {
2441
- die("Usage: fillo init --email you@company.com (or run `fillo login` for an existing account).");
2735
+ die(
2736
+ "Usage: fillo init --email you@company.com (or run `fillo login` for an existing account)."
2737
+ );
2442
2738
  }
2443
2739
  const res = await api("/workspaces/provision", {
2444
2740
  method: "POST",
@@ -2448,13 +2744,17 @@ async function init(flags) {
2448
2744
  if (!res.ok || !body.key) die(body.error ?? `init failed (${res.status}).`);
2449
2745
  const { claimUrl: _legacyWorkspaceLink, ...current } = readConfig();
2450
2746
  writeConfig({ ...current, pk: body.key });
2451
- console.log(`
2452
- \x1B[32m\u2713\x1B[0m Workspace ready. A workspace link was sent to ${bold(terminalText(email3))}.`);
2747
+ console.log(
2748
+ `
2749
+ \x1B[32m\u2713\x1B[0m Workspace ready. A workspace link was sent to ${bold(terminalText(email3))}.`
2750
+ );
2453
2751
  if (body.limits) {
2454
2752
  const expiry = Date.parse(body.limits.expiresAt);
2455
2753
  if (Number.isFinite(expiry)) {
2456
2754
  const days = Math.max(1, Math.round((expiry - Date.now()) / 864e5));
2457
- console.log(` ${dim(`Preview: up to ${body.limits.responses} responses for ~${days} days.`)}`);
2755
+ console.log(
2756
+ ` ${dim(`Preview: up to ${body.limits.responses} responses for ~${days} days.`)}`
2757
+ );
2458
2758
  }
2459
2759
  }
2460
2760
  console.log(`
@@ -2519,7 +2819,8 @@ async function agent(subcommand, flags) {
2519
2819
  if (subcommand === "event") {
2520
2820
  const status2 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
2521
2821
  const message = flagString(flags, "message");
2522
- if (!status2) die('Usage: fillo agent event --status <status> --message "<what the human does next>"');
2822
+ if (!status2)
2823
+ die('Usage: fillo agent event --status <status> --message "<what the human does next>"');
2523
2824
  const action = enumFlag(flags, "action", AGENT_ACTIONS);
2524
2825
  const formStatus = enumFlag(flags, "form-status", FORM_STATUSES);
2525
2826
  const formId = optionalStringFlag(flags, "form-id");
@@ -2645,9 +2946,7 @@ function pathEntryExists(path) {
2645
2946
  var MANAGED_SKILL_MARKER = ".fillo-managed.json";
2646
2947
  function isCliManagedFilloSkill(target) {
2647
2948
  try {
2648
- const marker = JSON.parse(
2649
- readFileSync(join(target, MANAGED_SKILL_MARKER), "utf8")
2650
- );
2949
+ const marker = JSON.parse(readFileSync(join(target, MANAGED_SKILL_MARKER), "utf8"));
2651
2950
  return marker.managedBy === "@usefillo/cli" && marker.skill === "build-with-fillo" && marker.format === 1 && marker.allowAutomaticUpdates === true;
2652
2951
  } catch {
2653
2952
  return false;
@@ -2702,19 +3001,14 @@ function installSkill(flags) {
2702
3001
  if (!existsSync(BUNDLED_SKILL_DIR)) {
2703
3002
  die("This CLI package is missing its bundled Fillo skill. Reinstall @usefillo/cli@latest.");
2704
3003
  }
2705
- const destinations = Array.from(
2706
- new Set(skillDirectories)
2707
- ).map((skillDirectory) => {
3004
+ const destinations = Array.from(new Set(skillDirectories)).map((skillDirectory) => {
2708
3005
  const skillsRoot = resolve(scopeRoot, skillDirectory);
2709
3006
  const target = join(skillsRoot, "build-with-fillo");
2710
3007
  const relativeSkillsRoot = relative(scopeRoot, skillsRoot);
2711
3008
  if (relativeSkillsRoot === ".." || relativeSkillsRoot.startsWith(`..${sep}`) || isAbsolute(relativeSkillsRoot)) {
2712
3009
  die("The skill destination must stay inside the selected project or home scope.");
2713
3010
  }
2714
- const destinationParts = [
2715
- ...relativeSkillsRoot.split(sep).filter(Boolean),
2716
- "build-with-fillo"
2717
- ];
3011
+ const destinationParts = [...relativeSkillsRoot.split(sep).filter(Boolean), "build-with-fillo"];
2718
3012
  assertSafeSkillDestination(scopeRoot, destinationParts);
2719
3013
  const targetExists = pathEntryExists(target);
2720
3014
  const current = targetExists && sameSkillBundle(BUNDLED_SKILL_DIR, target);
@@ -2735,10 +3029,7 @@ function installSkill(flags) {
2735
3029
  if (destination.current) continue;
2736
3030
  mkdirSync(destination.skillsRoot, { recursive: true });
2737
3031
  assertSafeSkillDestination(scopeRoot, destination.destinationParts.slice(0, 2));
2738
- const staging = join(
2739
- destination.skillsRoot,
2740
- `.build-with-fillo.install-${randomUUID()}`
2741
- );
3032
+ const staging = join(destination.skillsRoot, `.build-with-fillo.install-${randomUUID()}`);
2742
3033
  try {
2743
3034
  cpSync(BUNDLED_SKILL_DIR, staging, { recursive: true, errorOnExist: true });
2744
3035
  assertSafeSkillDestination(scopeRoot, destination.destinationParts);
@@ -2784,18 +3075,15 @@ async function postAgentEvent(apiBase, run, token, body) {
2784
3075
  }
2785
3076
  async function attachAgentAccount(apiBase, run, progressToken) {
2786
3077
  const accountToken = requireTokenFor(apiBase);
2787
- const res = await fetch(
2788
- `${apiBase}/api/v1/agent-runs/${encodeURIComponent(run)}/account`,
2789
- {
2790
- method: "POST",
2791
- redirect: "error",
2792
- headers: {
2793
- Authorization: `Bearer ${accountToken}`,
2794
- "X-Fillo-Agent-Run-Token": progressToken
2795
- },
2796
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
2797
- }
2798
- );
3078
+ const res = await fetch(`${apiBase}/api/v1/agent-runs/${encodeURIComponent(run)}/account`, {
3079
+ method: "POST",
3080
+ redirect: "error",
3081
+ headers: {
3082
+ Authorization: `Bearer ${accountToken}`,
3083
+ "X-Fillo-Agent-Run-Token": progressToken
3084
+ },
3085
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
3086
+ });
2799
3087
  const payload = await res.json().catch(() => ({}));
2800
3088
  if (!res.ok) {
2801
3089
  const hint = res.status === 401 ? " Run `fillo login` and try again." : "";
@@ -2925,10 +3213,7 @@ var FLAGS_BY_COMMAND = {
2925
3213
  version: []
2926
3214
  };
2927
3215
  function validateFlags(command, flags) {
2928
- const allowed = /* @__PURE__ */ new Set([
2929
- ...GLOBAL_FLAGS,
2930
- ...command ? FLAGS_BY_COMMAND[command] ?? [] : []
2931
- ]);
3216
+ const allowed = /* @__PURE__ */ new Set([...GLOBAL_FLAGS, ...command ? FLAGS_BY_COMMAND[command] ?? [] : []]);
2932
3217
  for (const [key, value] of Object.entries(flags)) {
2933
3218
  if (!allowed.has(key)) die(`Unknown flag for ${command ?? "fillo"}: --${key}`);
2934
3219
  if (BOOLEAN_FLAGS.has(key)) {
@@ -27,6 +27,11 @@ Never require a provider-specific agent command.
27
27
  `formId`.
28
28
  - Fully custom UI: use `FilloProvider` and hooks in React, or
29
29
  `createFormController()` elsewhere.
30
+ Every interactive embed must have exactly one submission identity: a
31
+ published `formId`, or a `defineForm()` / `<Fillo.Form>` value plus a
32
+ client. A plain `FormSchema` plus a client is not a code-defined form and
33
+ cannot resolve a target. Use explicit `renderOnly` only for a deliberately
34
+ non-submitting UI preview.
30
35
  3. Ask only for missing product decisions that change the result: purpose,
31
36
  placement, required questions or files, conditional behavior, and what
32
37
  happens after submit. Infer routine implementation details from the repo.
@@ -76,7 +81,9 @@ could not be verified.
76
81
  - Keep conditional questions in schema data with `visibleIf`; never vary the
77
82
  schema structure per visitor.
78
83
  - Pass a client to code-defined forms that must sync or collect responses.
79
- Without a client they are local render-only forms.
84
+ Never render a plain schema with only a client: add the actual returned
85
+ `formId`, convert the schema to `defineForm()`, or opt into `renderOnly` for
86
+ a deliberately transportless preview.
80
87
  - Import the default stylesheet unless the app deliberately owns every form
81
88
  style. Keep overrides local and preserve accessible labels, errors, focus,
82
89
  disabled states, and keyboard behavior.
@@ -93,7 +100,14 @@ identity secrets, workspace capability links, or short-lived run tokens.
93
100
  ## Verify and hand off
94
101
 
95
102
  1. Run the host repository's typecheck and proportionate build or tests.
96
- 2. Inspect desktop and mobile states: loading, validation, conditional paths,
103
+ A successful build, public API check, or hosted Fillo page does not prove
104
+ that the host app embedded the correct form.
105
+ 2. Open the actual host-app route in a browser. Confirm its active root has
106
+ `data-fillo-form-id="<actual returned formId>"`; do not accept a schema
107
+ handle, a hosted `/f/...` page, or a different form id as evidence. If the
108
+ form includes files, confirm the embedded picker is enabled and shows its
109
+ browse/drop affordance. Then inspect desktop and mobile states: loading,
110
+ validation, conditional paths,
97
111
  keyboard focus, error, success, and narrow text. Off localhost (tunnel,
98
112
  staging), the cosmetic-only `preview` prop/attribute shows the same
99
113
  developer chrome — see
@@ -42,6 +42,27 @@ Keep Fillo JSX schema authoring in a `"use client"` module. `onSubmitted` is
42
42
  for navigation, analytics, or another host-side follow-up after storage; it is
43
43
  not the response transport.
44
44
 
45
+ The identity combinations are intentional and exhaustive:
46
+
47
+ ```tsx
48
+ // Published/dashboard/CLI-owned schema.
49
+ <FilloForm formId={actualReturnedFormId} />
50
+
51
+ // Inline snapshot of that same published form.
52
+ <FilloForm form={schema} formId={actualReturnedFormId} />
53
+
54
+ // App-owned schema with a stable defineForm() identity.
55
+ <FilloForm form={codeForm} client={client} />
56
+
57
+ // Deliberately local UI preview: cannot submit, upload, or save progress.
58
+ <FilloForm form={schema} renderOnly />
59
+ ```
60
+
61
+ Never use `<FilloForm form={plainSchema} client={client} />`. A plain schema
62
+ has no stable Fillo identity, so the client cannot know which form owns its
63
+ responses or uploads. Current SDK types reject this combination and the
64
+ runtime reports `form_target_required` for JavaScript callers.
65
+
45
66
  ## Vite apps
46
67
 
47
68
  Vite exposes only `VITE_`-prefixed env vars to browser code, through
@@ -110,11 +131,11 @@ unconnected. On a tunnel, staging deploy, or local production build that
110
131
  chrome stays quiet; opt in with the cosmetic-only `preview` flag:
111
132
 
112
133
  ```tsx
113
- <FilloForm form={feedback} client={client}
134
+ <FilloForm form={feedbackCodeForm} client={client}
114
135
  preview={process.env.NEXT_PUBLIC_STAGE !== "production"} />
115
136
  ```
116
137
 
117
- DOM equivalents: `renderForm(el, { form, client, preview: true })` or
138
+ DOM equivalents: `renderForm(el, { form: feedbackCodeForm, client, preview: true })` or
118
139
  `<fillo-form data-preview>`. `data-preview="false"` and `data-preview="0"`
119
140
  count as off (frameworks stringify booleans onto data-* attributes); any other
120
141
  presence is on. `preview` renders a visible "Preview" badge and
@@ -123,6 +144,10 @@ submissions authenticate with a credential, never a prop. Remove it before
123
144
  respondents see the page. Pass `devNotices={false}` (`devNotices: false` in
124
145
  DOM) when the page provides its own context; the badge stays.
125
146
 
147
+ `preview` is only developer chrome; it does not make a transportless form
148
+ valid. Use `renderOnly` (or `<fillo-form data-render-only>`) when the surface is
149
+ intentionally local and must not submit or upload.
150
+
126
151
  ## Styling and custom UI
127
152
 
128
153
  Use the lowest-control surface that satisfies the request:
@@ -6,6 +6,7 @@ Confirm the exact error and installed package version before changing code.
6
6
  | --- | --- |
7
7
  | Published-id embed returns 404 | Confirm the id or slug and that the form is published. Do not reveal whether an inaccessible draft exists. |
8
8
  | Code-defined form renders but cannot save | Pass a client, keep a stable id, verify the key belongs to the intended workspace, and check expected-origin restrictions. |
9
+ | `form_target_required` or the embed renders unavailable while the hosted page works | The host app rendered a plain schema without a submission identity. Pass the actual returned `formId`, or use `defineForm()` plus a client. Use `renderOnly` only for a deliberate non-submitting preview. Verify the app route exposes the same id in `data-fillo-form-id`. |
9
10
  | Publishable key is `undefined` in a Vite app | Read `import.meta.env.VITE_FILLO_KEY`, not `process.env`; declare it in `src/vite-env.d.ts` under strict TypeScript and restart the dev server after `.env` changes. |
10
11
  | Schema write reports `trusted_sync_required` | Log in and use `fillo push --stage`, or use a server-held `FILLO_SYNC_TOKEN`. Do not weaken the workspace policy. |
11
12
  | `fillo push --stage` has nothing to stage | The published schema already matches; do not create another form. |
@@ -13,6 +14,7 @@ Confirm the exact error and installed package version before changing code.
13
14
  | File form cannot publish | Connect supported storage and verify the provider before retrying publish. |
14
15
  | Test submit only says "This form is unavailable." | Run on localhost, or set the cosmetic-only `preview` prop / `data-preview` attribute: dev chrome shows the real failure with its machine code (for example `form_not_published`) and the connect-storage link. |
15
16
  | Upload field says "Connect file storage to enable uploads" | Expected dev-chrome pre-emption: sync reported `storage_required`. Open the linked storage settings, connect a destination, then publish. |
17
+ | Upload field says uploads are unavailable in a render-only preview | The embed explicitly disabled transport. Remove `renderOnly` and provide a real identity path before testing uploads. |
16
18
  | DOM form duplicates after navigation | Mount after the target exists and call `destroy()` in cleanup. |
17
19
  | React context or hook error | Keep hooks inside `FilloForm` or `FilloProvider` and check for two installed copies of `@usefillo/react`. |
18
20
  | Fillo JSX fails in Next.js | Move schema JSX to a `"use client"` module; use object-form `defineForm()` for framework-neutral schema. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/cli",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Create and publish Fillo forms, and install the Fillo Agent Skill.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  "@types/node": "^22.10.0",
27
27
  "tsup": "^8.4.0",
28
28
  "typescript": "^5.8.3",
29
- "@usefillo/core": "0.10.0"
29
+ "@usefillo/core": "0.12.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup && node scripts/copy-skill.mjs",