form-builder-pro 1.3.9 → 1.4.1

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.mjs CHANGED
@@ -4097,7 +4097,8 @@ var FIELD_TYPES = [
4097
4097
  { type: "repeater", label: "Repeater", icon: "Copy" },
4098
4098
  { type: "file", label: "File Upload", icon: "Upload" },
4099
4099
  { type: "image", label: "Image", icon: "Image" },
4100
- { type: "name_generator", label: "Name Generator", icon: "Hash" }
4100
+ { type: "name_generator", label: "Name Generator", icon: "Hash" },
4101
+ { type: "formula", label: "Formula", icon: "Calculator" }
4101
4102
  ];
4102
4103
  var DEFAULT_FIELD_CONFIG = {
4103
4104
  text: { label: "Text Input", placeholder: "Enter text...", width: "100%", enabled: true, visible: true },
@@ -4154,6 +4155,19 @@ var DEFAULT_FIELD_CONFIG = {
4154
4155
  nameGeneratorFormat: "TEXT_ID",
4155
4156
  nameGeneratorText: "",
4156
4157
  nameGeneratorIdPadding: 4
4158
+ },
4159
+ formula: {
4160
+ label: "Formula Field",
4161
+ placeholder: "\u2014",
4162
+ width: "50%",
4163
+ enabled: true,
4164
+ visible: true,
4165
+ formulaConfig: {
4166
+ mode: "single",
4167
+ single: { expression: "" },
4168
+ multiple: { compareField: "", conditions: [], fallbackExpression: "" },
4169
+ decimalPlaces: 2
4170
+ }
4157
4171
  }
4158
4172
  };
4159
4173
  var VALIDATION_TYPE_PRESETS = {
@@ -4503,6 +4517,8 @@ function normalizeFieldType(type) {
4503
4517
  return "datetime";
4504
4518
  if (str === "NAME_GENERATOR" || normalized === "namegenerator")
4505
4519
  return "name_generator";
4520
+ if (str === "FORMULA" || normalized === "formula")
4521
+ return "formula";
4506
4522
  return str.toLowerCase();
4507
4523
  }
4508
4524
  function transformField(field) {
@@ -4731,6 +4747,8 @@ function transformField(field) {
4731
4747
  transformed.repeatIncrementEnabled = field.repeatIncrementEnabled;
4732
4748
  if (field.dateConstraints !== void 0)
4733
4749
  transformed.dateConstraints = field.dateConstraints;
4750
+ if (field.formulaConfig !== void 0)
4751
+ transformed.formulaConfig = field.formulaConfig;
4734
4752
  if (field.nameGeneratorFormat !== void 0)
4735
4753
  transformed.nameGeneratorFormat = field.nameGeneratorFormat;
4736
4754
  if (field.nameGeneratorText !== void 0)
@@ -5045,6 +5063,12 @@ function fieldToPayload(field, opts) {
5045
5063
  payload.showWhenValueOffFields = field.showWhenValueOffFields;
5046
5064
  }
5047
5065
  }
5066
+ if (field.type === "formula") {
5067
+ payload.fieldType = "FORMULA";
5068
+ payload.type = "formula";
5069
+ if (field.formulaConfig !== void 0)
5070
+ payload.formulaConfig = field.formulaConfig;
5071
+ }
5048
5072
  if (field.type === "repeater") {
5049
5073
  payload.fieldType = "REPEATER";
5050
5074
  payload.type = "number";
@@ -5992,7 +6016,8 @@ var ICONS = {
5992
6016
  "MapPin": '<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />',
5993
6017
  "Briefcase": '<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.67.38-1.053.33-2.177.58-3.345.726-.26.032-.52.05-.778.066m-13.882-6.626h.008v.008h-.008v-.008zm1.096-3.837a48.116 48.116 0 00-3.413.387c-1.069.16-1.837 1.094-1.837 2.175v4.784c0 .493.196.958.536 1.344m0 0a17.8 17.8 0 013.344.726c.25.085.476.215.67.38m13.784-5.32c-.34-.386-.536-.851-.536-1.344v-4.784c0-1.081-.768-2.015-1.837-2.175a48.041 48.041 0 01-3.413-.387m-4.5 8.006c.194.165.42.295.67.38 1.053.33 2.177.58 3.345.726.26.032.52.05.778.066m0-7.384V5.626a2.25 2.25 0 00-2.25-2.25h-4.5a2.25 2.25 0 00-2.25 2.25v2.247M16.5 6h-9" />',
5994
6018
  "Eye": '<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />',
5995
- "Cog": '<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />'
6019
+ "Cog": '<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />',
6020
+ "Calculator": '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 15.75V18m-7.5-6.75h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V13.5zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25V18zm2.498-6.75h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V13.5zm0 2.25h.007v.008h-.007v-.008zm0 2.25h.007v.008h-.007V18zm2.504-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V18zm2.498-6.75h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V13.5zM8.25 6h7.5v2.25h-7.5V6zM12 2.25c-1.892 0-3.758.11-5.593.322C5.307 2.7 4.5 3.65 4.5 4.757V19.5a2.25 2.25 0 002.25 2.25h10.5a2.25 2.25 0 002.25-2.25V4.757c0-1.108-.806-2.057-1.907-2.185A48.507 48.507 0 0012 2.25z" />'
5996
6021
  };
5997
6022
  function getIcon(name, size = 20) {
5998
6023
  const svgString = ICONS[name] || `<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />`;
@@ -6226,6 +6251,311 @@ function getNumericFieldsForFormula(schema, excludeFieldId) {
6226
6251
  }
6227
6252
  return result;
6228
6253
  }
6254
+ var _tokenCache = /* @__PURE__ */ new Map();
6255
+ function _tokenize(expr) {
6256
+ if (_tokenCache.has(expr))
6257
+ return _tokenCache.get(expr);
6258
+ const tokens = [];
6259
+ let i = 0;
6260
+ while (i < expr.length) {
6261
+ const c = expr[i];
6262
+ if (/\s/.test(c)) {
6263
+ i++;
6264
+ continue;
6265
+ }
6266
+ if (/[+\-*/(),]/.test(c)) {
6267
+ tokens.push(c);
6268
+ i++;
6269
+ continue;
6270
+ }
6271
+ if (/[0-9.]/.test(c)) {
6272
+ let num = "";
6273
+ while (i < expr.length && /[0-9.]/.test(expr[i])) {
6274
+ num += expr[i++];
6275
+ }
6276
+ tokens.push(num);
6277
+ continue;
6278
+ }
6279
+ if (/[a-zA-Z_]/.test(c)) {
6280
+ let ident = "";
6281
+ while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
6282
+ ident += expr[i++];
6283
+ }
6284
+ tokens.push(ident);
6285
+ continue;
6286
+ }
6287
+ i++;
6288
+ }
6289
+ _tokenCache.set(expr, tokens);
6290
+ return tokens;
6291
+ }
6292
+ function _evalResolved(expr) {
6293
+ const tokens = _tokenize(expr);
6294
+ let pos = 0;
6295
+ const parseExpr = () => {
6296
+ let left = parseTerm();
6297
+ while (pos < tokens.length) {
6298
+ if (tokens[pos] === "+") {
6299
+ pos++;
6300
+ left += parseTerm();
6301
+ } else if (tokens[pos] === "-") {
6302
+ pos++;
6303
+ left -= parseTerm();
6304
+ } else
6305
+ break;
6306
+ }
6307
+ return left;
6308
+ };
6309
+ const parseTerm = () => {
6310
+ let left = parseFactor();
6311
+ while (pos < tokens.length) {
6312
+ if (tokens[pos] === "*") {
6313
+ pos++;
6314
+ left *= parseFactor();
6315
+ } else if (tokens[pos] === "/") {
6316
+ pos++;
6317
+ const r = parseFactor();
6318
+ if (r === 0)
6319
+ return NaN;
6320
+ left /= r;
6321
+ } else
6322
+ break;
6323
+ }
6324
+ return left;
6325
+ };
6326
+ const parseArgs = () => {
6327
+ const args = [];
6328
+ if (pos < tokens.length && tokens[pos] !== ")") {
6329
+ args.push(parseExpr());
6330
+ while (pos < tokens.length && tokens[pos] === ",") {
6331
+ pos++;
6332
+ args.push(parseExpr());
6333
+ }
6334
+ }
6335
+ return args;
6336
+ };
6337
+ const parseFactor = () => {
6338
+ if (pos >= tokens.length)
6339
+ return NaN;
6340
+ const t = tokens[pos];
6341
+ if (t === "(") {
6342
+ pos++;
6343
+ const v = parseExpr();
6344
+ if (tokens[pos] === ")")
6345
+ pos++;
6346
+ return v;
6347
+ }
6348
+ if (t === "-") {
6349
+ pos++;
6350
+ return -parseFactor();
6351
+ }
6352
+ if (t === "+") {
6353
+ pos++;
6354
+ return parseFactor();
6355
+ }
6356
+ const n = parseFloat(t);
6357
+ if (!isNaN(n)) {
6358
+ pos++;
6359
+ return n;
6360
+ }
6361
+ if (/^[a-zA-Z_]/.test(t) && pos + 1 < tokens.length && tokens[pos + 1] === "(") {
6362
+ const fn = t.toUpperCase();
6363
+ pos += 2;
6364
+ const args = parseArgs();
6365
+ if (pos < tokens.length && tokens[pos] === ")")
6366
+ pos++;
6367
+ switch (fn) {
6368
+ case "ROUND":
6369
+ return args.length >= 2 ? Math.round(args[0] * Math.pow(10, args[1])) / Math.pow(10, args[1]) : Math.round(args[0] ?? 0);
6370
+ case "ABS":
6371
+ return Math.abs(args[0] ?? 0);
6372
+ case "MIN":
6373
+ return args.length ? Math.min(...args) : NaN;
6374
+ case "MAX":
6375
+ return args.length ? Math.max(...args) : NaN;
6376
+ case "FLOOR":
6377
+ return Math.floor(args[0] ?? 0);
6378
+ case "CEIL":
6379
+ return Math.ceil(args[0] ?? 0);
6380
+ case "SQRT":
6381
+ return Math.sqrt(args[0] ?? 0);
6382
+ case "POW":
6383
+ return Math.pow(args[0] ?? 0, args[1] ?? 2);
6384
+ default:
6385
+ return NaN;
6386
+ }
6387
+ }
6388
+ pos++;
6389
+ return 0;
6390
+ };
6391
+ try {
6392
+ const result = parseExpr();
6393
+ return isNaN(result) ? NaN : result;
6394
+ } catch {
6395
+ return NaN;
6396
+ }
6397
+ }
6398
+ function extractBracketFields(expression) {
6399
+ if (!expression)
6400
+ return [];
6401
+ const seen = /* @__PURE__ */ new Set();
6402
+ const out = [];
6403
+ for (const m of expression.matchAll(/\{([^}]+)\}/g)) {
6404
+ const name = m[1].trim();
6405
+ if (name && !seen.has(name)) {
6406
+ seen.add(name);
6407
+ out.push(name);
6408
+ }
6409
+ }
6410
+ return out;
6411
+ }
6412
+ function evaluateFormulaExpression(expression, values) {
6413
+ if (!expression?.trim())
6414
+ return NaN;
6415
+ const resolved = expression.replace(/\{([^}]+)\}/g, (_, name) => {
6416
+ const v = values[name.trim()];
6417
+ if (v === void 0 || v === null || v === "")
6418
+ return "0";
6419
+ const n = typeof v === "number" ? v : parseFloat(String(v));
6420
+ return isNaN(n) ? "0" : String(n);
6421
+ });
6422
+ _tokenCache.delete(resolved);
6423
+ return _evalResolved(resolved);
6424
+ }
6425
+ function evaluateFormulaConfig(config, values, compareValue) {
6426
+ if (!config)
6427
+ return { result: NaN, error: "No formula config" };
6428
+ const dp = config.decimalPlaces ?? 2;
6429
+ try {
6430
+ let expression;
6431
+ if (config.mode === "single") {
6432
+ expression = config.single?.expression ?? "";
6433
+ if (!expression.trim())
6434
+ return { result: NaN, error: "No expression defined" };
6435
+ } else {
6436
+ const cmpVal = compareValue ?? "";
6437
+ const matched = config.multiple?.conditions?.find((c) => c.value === cmpVal);
6438
+ expression = matched?.expression ?? config.multiple?.fallbackExpression ?? "";
6439
+ if (!expression.trim())
6440
+ return { result: NaN, error: "No matching condition and no fallback" };
6441
+ }
6442
+ const raw = evaluateFormulaExpression(expression, values);
6443
+ if (isNaN(raw))
6444
+ return { result: NaN, error: "Expression evaluation failed (check syntax or divide-by-zero)" };
6445
+ const result = parseFloat(raw.toFixed(dp));
6446
+ return { result };
6447
+ } catch (e) {
6448
+ return { result: NaN, error: String(e) };
6449
+ }
6450
+ }
6451
+ function validateFormulaExpression(expression, availableFieldNames) {
6452
+ if (!expression?.trim())
6453
+ return { valid: false, error: "Expression cannot be empty" };
6454
+ const refs = extractBracketFields(expression);
6455
+ const known = new Set(availableFieldNames);
6456
+ for (const ref of refs) {
6457
+ if (!known.has(ref)) {
6458
+ return { valid: false, error: `Unknown field reference: "{${ref}}"` };
6459
+ }
6460
+ }
6461
+ let open = 0;
6462
+ for (const c of expression) {
6463
+ if (c === "(")
6464
+ open++;
6465
+ else if (c === ")") {
6466
+ open--;
6467
+ if (open < 0)
6468
+ return { valid: false, error: "Unbalanced parentheses" };
6469
+ }
6470
+ }
6471
+ if (open !== 0)
6472
+ return { valid: false, error: "Unbalanced parentheses" };
6473
+ return { valid: true };
6474
+ }
6475
+ function getFieldsForFormula(schema, excludeFieldId) {
6476
+ const result = [];
6477
+ const NUMERIC_TYPES = /* @__PURE__ */ new Set(["number", "formula"]);
6478
+ for (const section of schema.sections) {
6479
+ for (const field of section.fields) {
6480
+ if (!NUMERIC_TYPES.has(field.type))
6481
+ continue;
6482
+ if (excludeFieldId && field.id === excludeFieldId)
6483
+ continue;
6484
+ const fieldName = field.fieldName ?? field.id;
6485
+ result.push({ id: field.id, fieldName, label: field.label || fieldName });
6486
+ }
6487
+ }
6488
+ return result;
6489
+ }
6490
+ function detectFormulaFieldCircularDependency(schema, formulaFieldId, config) {
6491
+ const resolveField = (ref) => {
6492
+ for (const s of schema.sections) {
6493
+ for (const f of s.fields) {
6494
+ if (f.fieldName === ref || f.id === ref)
6495
+ return f;
6496
+ }
6497
+ }
6498
+ return void 0;
6499
+ };
6500
+ const getDepsFromConfig = (f) => {
6501
+ if (f.type !== "formula" || !f.formulaConfig)
6502
+ return [];
6503
+ const cfg = f.formulaConfig;
6504
+ const exprs = [];
6505
+ if (cfg.mode === "single") {
6506
+ if (cfg.single?.expression)
6507
+ exprs.push(cfg.single.expression);
6508
+ } else {
6509
+ cfg.multiple?.conditions?.forEach((c) => {
6510
+ if (c.expression)
6511
+ exprs.push(c.expression);
6512
+ });
6513
+ if (cfg.multiple?.fallbackExpression)
6514
+ exprs.push(cfg.multiple.fallbackExpression);
6515
+ }
6516
+ return exprs.flatMap((e) => extractBracketFields(e));
6517
+ };
6518
+ const visited = /* @__PURE__ */ new Set();
6519
+ const hasCycle = (fieldId) => {
6520
+ if (visited.has(fieldId))
6521
+ return true;
6522
+ visited.add(fieldId);
6523
+ const field = resolveField(fieldId);
6524
+ if (!field || field.type !== "formula" || !field.formulaConfig) {
6525
+ visited.delete(fieldId);
6526
+ return false;
6527
+ }
6528
+ for (const dep of getDepsFromConfig(field)) {
6529
+ const depField = resolveField(dep);
6530
+ if (!depField)
6531
+ continue;
6532
+ if (depField.id === formulaFieldId) {
6533
+ visited.delete(fieldId);
6534
+ return true;
6535
+ }
6536
+ if (hasCycle(depField.id)) {
6537
+ visited.delete(fieldId);
6538
+ return true;
6539
+ }
6540
+ }
6541
+ visited.delete(fieldId);
6542
+ return false;
6543
+ };
6544
+ const thisField = resolveField(formulaFieldId);
6545
+ if (!thisField || !config)
6546
+ return false;
6547
+ const thisDeps = getDepsFromConfig({ ...thisField, formulaConfig: config });
6548
+ for (const dep of thisDeps) {
6549
+ const depField = resolveField(dep);
6550
+ if (!depField)
6551
+ continue;
6552
+ if (depField.id === formulaFieldId)
6553
+ return true;
6554
+ if (hasCycle(depField.id))
6555
+ return true;
6556
+ }
6557
+ return false;
6558
+ }
6229
6559
 
6230
6560
  // src/core/countryData.ts
6231
6561
  var COUNTRY_DATA = [
@@ -6875,6 +7205,19 @@ var FieldRenderer = class {
6875
7205
  });
6876
7206
  break;
6877
7207
  }
7208
+ case "formula": {
7209
+ const formulaDisplay = value !== void 0 && value !== null ? String(value) : "\u2014";
7210
+ input = createElement("input", {
7211
+ type: "text",
7212
+ className: "flex min-h-touch w-full rounded-md border border-input bg-gray-50 dark:bg-gray-800 px-3 py-2 text-sm sm:text-base font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
7213
+ placeholder: "\u2014",
7214
+ value: formulaDisplay,
7215
+ readonly: true,
7216
+ disabled: true,
7217
+ title: "Computed field \u2014 value is calculated from a formula"
7218
+ });
7219
+ break;
7220
+ }
6878
7221
  default:
6879
7222
  const rules = getValidationRules(field);
6880
7223
  const useNumericTextInput = field.type === "text" && isNumericTextField(field);
@@ -7299,6 +7642,21 @@ function buildFormulaValuesMap(schema, data) {
7299
7642
  values[field.fieldName] = newVal;
7300
7643
  }
7301
7644
  }
7645
+ const formulaTypeFields = allFields.filter((f) => f.type === "formula" && f.formulaConfig);
7646
+ for (let pass = 0; pass < Math.max(1, formulaTypeFields.length); pass++) {
7647
+ for (const field of formulaTypeFields) {
7648
+ const modelKey = getModelKey(field);
7649
+ const compareFieldName = field.formulaConfig.multiple?.compareField;
7650
+ const compareValue = compareFieldName ? String(values[compareFieldName] ?? "") : "";
7651
+ const evalResult = evaluateFormulaConfig(field.formulaConfig, values, compareValue);
7652
+ const dp = field.formulaConfig.decimalPlaces ?? 2;
7653
+ const newVal = !evalResult.error && !isNaN(evalResult.result) ? parseFloat(evalResult.result.toFixed(dp)) : void 0;
7654
+ values[modelKey] = newVal;
7655
+ values[field.id] = newVal;
7656
+ if (field.fieldName)
7657
+ values[field.fieldName] = newVal;
7658
+ }
7659
+ }
7302
7660
  return values;
7303
7661
  }
7304
7662
  function computeFormulaValue(field, schema, data) {
@@ -7322,6 +7680,31 @@ function isFormulaDependency(schema, modelKey, fieldId) {
7322
7680
  if (fieldId && field.dependencies.includes(fieldId))
7323
7681
  return true;
7324
7682
  }
7683
+ if (field.type === "formula" && field.formulaConfig) {
7684
+ const cfg = field.formulaConfig;
7685
+ const exprs = [];
7686
+ if (cfg.mode === "single") {
7687
+ if (cfg.single?.expression)
7688
+ exprs.push(cfg.single.expression);
7689
+ } else {
7690
+ cfg.multiple?.conditions?.forEach((c) => {
7691
+ if (c.expression)
7692
+ exprs.push(c.expression);
7693
+ });
7694
+ if (cfg.multiple?.fallbackExpression)
7695
+ exprs.push(cfg.multiple.fallbackExpression);
7696
+ if (cfg.multiple?.compareField) {
7697
+ if (cfg.multiple.compareField === modelKey || cfg.multiple.compareField === fieldId)
7698
+ return true;
7699
+ }
7700
+ }
7701
+ for (const expr of exprs) {
7702
+ for (const ref of extractBracketFields(expr)) {
7703
+ if (ref === modelKey || fieldId && ref === fieldId)
7704
+ return true;
7705
+ }
7706
+ }
7707
+ }
7325
7708
  }
7326
7709
  }
7327
7710
  return false;
@@ -7390,6 +7773,15 @@ var FormRenderer = class {
7390
7773
  const computed = computeFormulaValue(field, this.schema, this.data);
7391
7774
  fieldValue = computed;
7392
7775
  this.data[modelKey] = computed;
7776
+ } else if (field.type === "formula" && field.formulaConfig) {
7777
+ const allValues = buildFormulaValuesMap(this.schema, this.data);
7778
+ const compareFieldName = field.formulaConfig.multiple?.compareField;
7779
+ const compareValue = compareFieldName ? String(allValues[compareFieldName] ?? "") : "";
7780
+ const evalResult = evaluateFormulaConfig(field.formulaConfig, allValues, compareValue);
7781
+ const dp = field.formulaConfig.decimalPlaces ?? 2;
7782
+ const computed = !evalResult.error && !isNaN(evalResult.result) ? parseFloat(evalResult.result.toFixed(dp)) : void 0;
7783
+ fieldValue = computed;
7784
+ this.data[modelKey] = computed;
7393
7785
  } else if (field.type === "name_generator") {
7394
7786
  fieldValue = this.data[modelKey];
7395
7787
  if (!fieldValue) {
@@ -7401,7 +7793,7 @@ var FormRenderer = class {
7401
7793
  } else {
7402
7794
  fieldValue = this.data[modelKey];
7403
7795
  }
7404
- const isFormulaField = field.type === "number" && field.valueSource === "formula";
7796
+ const isFormulaField = field.type === "number" && field.valueSource === "formula" || field.type === "formula";
7405
7797
  const fieldEl = FieldRenderer.render(
7406
7798
  field,
7407
7799
  fieldValue,
@@ -7439,6 +7831,18 @@ var FormRenderer = class {
7439
7831
  grid.appendChild(fieldWrapper);
7440
7832
  });
7441
7833
  sectionEl.appendChild(grid);
7834
+ if (section.repeatable === true) {
7835
+ const addLabel = section.addButtonLabel && section.addButtonLabel.trim() || "+ Add";
7836
+ const addRow = createElement("div", { className: "mt-3 flex justify-start" });
7837
+ addRow.appendChild(
7838
+ createElement("button", {
7839
+ type: "button",
7840
+ className: "px-4 py-2 text-sm font-medium rounded-md border border-[#019FA2] text-[#019FA2] dark:text-[#4dd4d6] dark:border-[#019FA2] bg-transparent hover:bg-[#019FA2]/10 transition-colors",
7841
+ text: addLabel
7842
+ })
7843
+ );
7844
+ sectionEl.appendChild(addRow);
7845
+ }
7442
7846
  form.appendChild(sectionEl);
7443
7847
  });
7444
7848
  const submitBtn = createElement("button", {
@@ -9914,6 +10318,21 @@ var Section = class _Section {
9914
10318
  nestedWrap.appendChild(nestedList);
9915
10319
  sectionEl.appendChild(nestedWrap);
9916
10320
  }
10321
+ if (this.section.repeatable === true) {
10322
+ const addLabel = this.section.addButtonLabel && this.section.addButtonLabel.trim() || "+ Add";
10323
+ const footer = createElement("div", {
10324
+ className: "px-4 pb-4 pt-2 flex justify-start border-t border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/30"
10325
+ });
10326
+ footer.appendChild(
10327
+ createElement("button", {
10328
+ type: "button",
10329
+ className: "px-4 py-2 text-sm font-medium rounded-md border border-[#019FA2] text-[#019FA2] dark:text-[#4dd4d6] dark:border-[#019FA2] bg-white dark:bg-gray-900 hover:bg-[#019FA2]/10 transition-colors",
10330
+ text: addLabel,
10331
+ onclick: (e) => e.preventDefault()
10332
+ })
10333
+ );
10334
+ sectionEl.appendChild(footer);
10335
+ }
9917
10336
  this.initFieldSortable(fieldsGrid);
9918
10337
  return sectionEl;
9919
10338
  }
@@ -10088,6 +10507,7 @@ var SectionList = class {
10088
10507
 
10089
10508
  // src/builder/FormBuilder.ts
10090
10509
  var advancedCssPanelState = /* @__PURE__ */ new Map();
10510
+ var lastFocusedExprTextarea = null;
10091
10511
  var LABEL_DEBOUNCE_MS = 300;
10092
10512
  var labelUpdateTimeouts = /* @__PURE__ */ new Map();
10093
10513
  var FormBuilder = class {
@@ -10272,6 +10692,7 @@ var FormBuilder = class {
10272
10692
  collapsible: s.collapsible,
10273
10693
  parentGroupId: s.parentGroupId,
10274
10694
  repeatable: s.repeatable,
10695
+ addButtonLabel: s.addButtonLabel,
10275
10696
  minInstances: s.minInstances,
10276
10697
  maxInstances: s.maxInstances,
10277
10698
  css: s.css,
@@ -10561,6 +10982,36 @@ var FormBuilder = class {
10561
10982
  }
10562
10983
  }
10563
10984
  }
10985
+ for (const field of schema.sections.flatMap((s) => s.fields)) {
10986
+ if (field.type !== "formula" || !field.formulaConfig)
10987
+ continue;
10988
+ const fcfg = field.formulaConfig;
10989
+ const fAvailable = getFieldsForFormula(schema, field.id);
10990
+ const fNames = fAvailable.map((f) => f.fieldName);
10991
+ const exprs = [];
10992
+ if (fcfg.mode === "single") {
10993
+ if (fcfg.single?.expression)
10994
+ exprs.push(fcfg.single.expression);
10995
+ } else {
10996
+ fcfg.multiple?.conditions?.forEach((c) => {
10997
+ if (c.expression)
10998
+ exprs.push(c.expression);
10999
+ });
11000
+ if (fcfg.multiple?.fallbackExpression)
11001
+ exprs.push(fcfg.multiple.fallbackExpression);
11002
+ }
11003
+ for (const expr of exprs) {
11004
+ const result = validateFormulaExpression(expr, fNames);
11005
+ if (!result.valid) {
11006
+ alert(`Formula error in "${field.label}": ${result.error}`);
11007
+ return;
11008
+ }
11009
+ }
11010
+ if (detectFormulaFieldCircularDependency(schema, field.id, fcfg)) {
11011
+ alert(`Circular dependency detected in formula for "${field.label}"`);
11012
+ return;
11013
+ }
11014
+ }
10564
11015
  schema.sections.forEach((section) => {
10565
11016
  section.fields?.forEach((field) => {
10566
11017
  if (field.type === "number" && field.validations) {
@@ -11162,11 +11613,37 @@ var FormBuilder = class {
11162
11613
  this.createCheckboxField(
11163
11614
  "Allow multiple instances",
11164
11615
  section.repeatable === true,
11165
- (checked) => formStore.getState().updateSection(sectionId, { repeatable: checked }),
11616
+ (checked) => formStore.getState().updateSection(sectionId, {
11617
+ repeatable: checked,
11618
+ ...checked ? {} : { addButtonLabel: null }
11619
+ }),
11166
11620
  `group-repeatable-${sectionId}`
11167
11621
  )
11168
11622
  );
11169
11623
  if (section.repeatable === true) {
11624
+ const addLabelWrap = createElement("div", { className: "mb-2" });
11625
+ addLabelWrap.appendChild(
11626
+ createElement("label", {
11627
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
11628
+ text: "Add Button Label"
11629
+ })
11630
+ );
11631
+ addLabelWrap.appendChild(
11632
+ createElement("input", {
11633
+ type: "text",
11634
+ className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
11635
+ value: section.addButtonLabel ?? "",
11636
+ placeholder: "e.g. + Billing Address",
11637
+ "data-focus-id": `group-add-btn-label-${sectionId}`,
11638
+ oninput: (e) => {
11639
+ const raw = e.target.value;
11640
+ formStore.getState().updateSection(sectionId, {
11641
+ addButtonLabel: raw === "" ? null : raw
11642
+ });
11643
+ }
11644
+ })
11645
+ );
11646
+ body.appendChild(addLabelWrap);
11170
11647
  const minG = createElement("div", { className: "mb-2" });
11171
11648
  minG.appendChild(createElement("label", { className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1", text: "Min Instances" }));
11172
11649
  minG.appendChild(createElement("input", {
@@ -11347,7 +11824,327 @@ var FormBuilder = class {
11347
11824
  body.appendChild(hintEl);
11348
11825
  }
11349
11826
  }
11350
- if (selectedField.type !== "image") {
11827
+ if (selectedField.type === "formula") {
11828
+ const schema = formStore.getState().schema;
11829
+ const availableFields = getFieldsForFormula(schema, selectedField.id);
11830
+ const cfg = selectedField.formulaConfig ?? {
11831
+ mode: "single",
11832
+ single: { expression: "" },
11833
+ multiple: { compareField: "", conditions: [], fallbackExpression: "" },
11834
+ decimalPlaces: 2
11835
+ };
11836
+ const patchFormulaConfig = (patch) => {
11837
+ const fresh = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
11838
+ const current = fresh?.formulaConfig ?? cfg;
11839
+ formStore.getState().updateField(selectedField.id, { formulaConfig: { ...current, ...patch } });
11840
+ };
11841
+ const patchMultiple = (patch) => {
11842
+ const fresh = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id);
11843
+ const current = fresh?.formulaConfig ?? cfg;
11844
+ patchFormulaConfig({ multiple: { ...current.multiple, ...patch } });
11845
+ };
11846
+ const formulaHeader = createElement("h3", {
11847
+ className: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 mt-4",
11848
+ text: "Formula Configuration"
11849
+ });
11850
+ body.appendChild(formulaHeader);
11851
+ const modeGroup = createElement("div", { className: "mb-4" });
11852
+ modeGroup.appendChild(createElement("label", {
11853
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
11854
+ text: "Mode"
11855
+ }));
11856
+ const modeRow = createElement("div", { className: "flex gap-2" });
11857
+ const activeModeClass = "flex-1 px-3 py-1.5 text-sm rounded-md border bg-[#635bff] text-white border-[#635bff] font-medium";
11858
+ const inactiveModeClass = "flex-1 px-3 py-1.5 text-sm rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-[#635bff] transition-colors";
11859
+ const singleBtn = createElement("button", {
11860
+ type: "button",
11861
+ className: cfg.mode === "single" ? activeModeClass : inactiveModeClass,
11862
+ text: "Single Expression",
11863
+ onclick: () => {
11864
+ patchFormulaConfig({ mode: "single" });
11865
+ this.render();
11866
+ }
11867
+ });
11868
+ const multipleBtn = createElement("button", {
11869
+ type: "button",
11870
+ className: cfg.mode === "multiple" ? activeModeClass : inactiveModeClass,
11871
+ text: "Multiple Conditions",
11872
+ onclick: () => {
11873
+ patchFormulaConfig({ mode: "multiple" });
11874
+ this.render();
11875
+ }
11876
+ });
11877
+ modeRow.appendChild(singleBtn);
11878
+ modeRow.appendChild(multipleBtn);
11879
+ modeGroup.appendChild(modeRow);
11880
+ body.appendChild(modeGroup);
11881
+ if (cfg.mode === "single") {
11882
+ const exprGroup = createElement("div", { className: "mb-3" });
11883
+ exprGroup.appendChild(createElement("label", {
11884
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
11885
+ text: "Expression"
11886
+ }));
11887
+ const exprTextarea = createElement("textarea", {
11888
+ className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent font-mono text-sm resize-none",
11889
+ placeholder: "e.g. {fieldA} + {fieldB} * 1.18",
11890
+ rows: "3"
11891
+ });
11892
+ exprTextarea.value = cfg.single?.expression ?? "";
11893
+ exprTextarea.addEventListener("focus", () => {
11894
+ lastFocusedExprTextarea = exprTextarea;
11895
+ });
11896
+ const exprError = createElement("div", { className: "text-xs text-red-500 mt-1 hidden" });
11897
+ exprTextarea.addEventListener("input", () => {
11898
+ const expr = exprTextarea.value;
11899
+ const result = validateFormulaExpression(expr, availableFields.map((f) => f.fieldName));
11900
+ if (result.valid || !expr.trim()) {
11901
+ exprError.classList.add("hidden");
11902
+ } else {
11903
+ exprError.textContent = result.error;
11904
+ exprError.classList.remove("hidden");
11905
+ }
11906
+ const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
11907
+ formStore.getState().updateField(selectedField.id, { formulaConfig: { ...freshCfg, single: { expression: expr } } });
11908
+ });
11909
+ exprGroup.appendChild(exprTextarea);
11910
+ exprGroup.appendChild(exprError);
11911
+ body.appendChild(exprGroup);
11912
+ } else {
11913
+ const compareGroup = createElement("div", { className: "mb-3" });
11914
+ compareGroup.appendChild(createElement("label", {
11915
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
11916
+ text: "Compare Field"
11917
+ }));
11918
+ const compareSelect = createElement("select", {
11919
+ className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
11920
+ onchange: (e) => {
11921
+ const val = e.target.value;
11922
+ patchMultiple({ compareField: val });
11923
+ }
11924
+ });
11925
+ compareSelect.appendChild(createElement("option", { value: "", text: "Select field to compare\u2026", selected: !cfg.multiple?.compareField }));
11926
+ schema.sections.flatMap((s) => s.fields).filter((f) => f.id !== selectedField.id).forEach((f) => {
11927
+ const fn = f.fieldName ?? f.id;
11928
+ compareSelect.appendChild(createElement("option", {
11929
+ value: fn,
11930
+ text: `${f.label} (${fn})`,
11931
+ selected: cfg.multiple?.compareField === fn
11932
+ }));
11933
+ });
11934
+ compareGroup.appendChild(compareSelect);
11935
+ body.appendChild(compareGroup);
11936
+ const conditionsLabel = createElement("label", {
11937
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-2",
11938
+ text: "Conditions"
11939
+ });
11940
+ body.appendChild(conditionsLabel);
11941
+ const conditions = cfg.multiple?.conditions ?? [];
11942
+ conditions.forEach((cond, idx) => {
11943
+ const row = createElement("div", { className: "mb-2 p-2 rounded-md border border-gray-100 dark:border-gray-800 space-y-1" });
11944
+ const whenLabel = createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "When equals" });
11945
+ const valueInput = createElement("input", {
11946
+ type: "text",
11947
+ className: "w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
11948
+ value: cond.value,
11949
+ placeholder: "e.g. BHS 146",
11950
+ oninput: (e) => {
11951
+ const v = e.target.value;
11952
+ const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
11953
+ const newConds = [...freshCfg.multiple?.conditions ?? []];
11954
+ newConds[idx] = { ...newConds[idx], value: v };
11955
+ patchMultiple({ conditions: newConds });
11956
+ }
11957
+ });
11958
+ const exprLabel = createElement("div", { className: "text-xs text-gray-500 dark:text-gray-400", text: "\u2192 Expression" });
11959
+ const condTextarea = createElement("textarea", {
11960
+ className: "w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-700 rounded-md bg-transparent font-mono resize-none",
11961
+ placeholder: "e.g. {fieldA} + {fieldB}",
11962
+ rows: "2"
11963
+ });
11964
+ condTextarea.value = cond.expression ?? "";
11965
+ condTextarea.addEventListener("focus", () => {
11966
+ lastFocusedExprTextarea = condTextarea;
11967
+ });
11968
+ condTextarea.addEventListener("input", () => {
11969
+ const expr = condTextarea.value;
11970
+ const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
11971
+ const newConds = [...freshCfg.multiple?.conditions ?? []];
11972
+ newConds[idx] = { ...newConds[idx], expression: expr };
11973
+ patchMultiple({ conditions: newConds });
11974
+ });
11975
+ const removeBtn = createElement("button", {
11976
+ type: "button",
11977
+ className: "text-xs text-red-500 hover:text-red-700 dark:text-red-400",
11978
+ text: "\u2212 Remove",
11979
+ onclick: () => {
11980
+ const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
11981
+ const newConds = [...freshCfg.multiple?.conditions ?? []];
11982
+ newConds.splice(idx, 1);
11983
+ patchMultiple({ conditions: newConds });
11984
+ this.render();
11985
+ }
11986
+ });
11987
+ row.appendChild(whenLabel);
11988
+ row.appendChild(valueInput);
11989
+ row.appendChild(exprLabel);
11990
+ row.appendChild(condTextarea);
11991
+ row.appendChild(removeBtn);
11992
+ body.appendChild(row);
11993
+ });
11994
+ body.appendChild(createElement("button", {
11995
+ type: "button",
11996
+ className: "w-full mb-3 py-1.5 text-sm border border-dashed border-gray-300 dark:border-gray-600 rounded-md text-gray-500 dark:text-gray-400 hover:border-[#635bff] hover:text-[#635bff] transition-colors",
11997
+ text: "+ Add Condition",
11998
+ onclick: () => {
11999
+ const freshCfg = formStore.getState().schema.sections.flatMap((s) => s.fields).find((f) => f.id === selectedField.id)?.formulaConfig ?? cfg;
12000
+ const newConds = [...freshCfg.multiple?.conditions ?? [], { value: "", expression: "" }];
12001
+ patchMultiple({ conditions: newConds });
12002
+ this.render();
12003
+ }
12004
+ }));
12005
+ const fallbackGroup = createElement("div", { className: "mb-3" });
12006
+ fallbackGroup.appendChild(createElement("label", {
12007
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12008
+ text: "Fallback Expression"
12009
+ }));
12010
+ const fallbackTextarea = createElement("textarea", {
12011
+ className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent font-mono text-sm resize-none",
12012
+ placeholder: "e.g. 0",
12013
+ rows: "2"
12014
+ });
12015
+ fallbackTextarea.value = cfg.multiple?.fallbackExpression ?? "";
12016
+ fallbackTextarea.addEventListener("focus", () => {
12017
+ lastFocusedExprTextarea = fallbackTextarea;
12018
+ });
12019
+ fallbackTextarea.addEventListener("input", () => {
12020
+ patchMultiple({ fallbackExpression: fallbackTextarea.value });
12021
+ });
12022
+ fallbackGroup.appendChild(fallbackTextarea);
12023
+ body.appendChild(fallbackGroup);
12024
+ }
12025
+ if (availableFields.length > 0) {
12026
+ const insertGroup = createElement("div", { className: "mb-3" });
12027
+ insertGroup.appendChild(createElement("label", {
12028
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12029
+ text: "Insert Field Reference"
12030
+ }));
12031
+ const insertSelect = createElement("select", {
12032
+ className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
12033
+ onchange: (e) => {
12034
+ const sel = e.target;
12035
+ const ref = sel.value;
12036
+ if (!ref)
12037
+ return;
12038
+ const ta = lastFocusedExprTextarea;
12039
+ if (ta) {
12040
+ const start = ta.selectionStart ?? ta.value.length;
12041
+ const end = ta.selectionEnd ?? ta.value.length;
12042
+ const before = ta.value.slice(0, start);
12043
+ const after = ta.value.slice(end);
12044
+ const pad = before.length > 0 && !/\s$/.test(before) ? " " : "";
12045
+ ta.value = before + pad + ref + after;
12046
+ const newPos = before.length + pad.length + ref.length;
12047
+ ta.selectionStart = ta.selectionEnd = newPos;
12048
+ ta.dispatchEvent(new Event("input", { bubbles: true }));
12049
+ ta.focus();
12050
+ }
12051
+ sel.value = "";
12052
+ }
12053
+ });
12054
+ insertSelect.appendChild(createElement("option", { value: "", text: "Select field to insert\u2026", selected: true }));
12055
+ availableFields.forEach((f) => {
12056
+ insertSelect.appendChild(createElement("option", {
12057
+ value: `{${f.fieldName}}`,
12058
+ text: `${f.label} ({${f.fieldName}})`
12059
+ }));
12060
+ });
12061
+ insertGroup.appendChild(insertSelect);
12062
+ body.appendChild(insertGroup);
12063
+ }
12064
+ body.appendChild(createElement("label", {
12065
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12066
+ text: "Math Helpers"
12067
+ }));
12068
+ const mathWrap = createElement("div", { className: "flex flex-wrap gap-1 mb-3" });
12069
+ const mathOps = [
12070
+ { text: "+", insert: " + " },
12071
+ { text: "-", insert: " - " },
12072
+ { text: "*", insert: " * " },
12073
+ { text: "/", insert: " / " },
12074
+ { text: "(", insert: "(" },
12075
+ { text: ")", insert: ")" },
12076
+ { text: "ROUND", insert: "ROUND(" },
12077
+ { text: "ABS", insert: "ABS(" },
12078
+ { text: "MIN", insert: "MIN(" },
12079
+ { text: "MAX", insert: "MAX(" },
12080
+ { text: "FLOOR", insert: "FLOOR(" },
12081
+ { text: "CEIL", insert: "CEIL(" }
12082
+ ];
12083
+ mathOps.forEach((op) => {
12084
+ mathWrap.appendChild(createElement("button", {
12085
+ type: "button",
12086
+ className: "px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 rounded font-mono hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors",
12087
+ text: op.text,
12088
+ onclick: () => {
12089
+ const ta = lastFocusedExprTextarea;
12090
+ if (!ta)
12091
+ return;
12092
+ const start = ta.selectionStart ?? ta.value.length;
12093
+ const end = ta.selectionEnd ?? ta.value.length;
12094
+ ta.value = ta.value.slice(0, start) + op.insert + ta.value.slice(end);
12095
+ const newPos = start + op.insert.length;
12096
+ ta.selectionStart = ta.selectionEnd = newPos;
12097
+ ta.dispatchEvent(new Event("input", { bubbles: true }));
12098
+ ta.focus();
12099
+ }
12100
+ }));
12101
+ });
12102
+ body.appendChild(mathWrap);
12103
+ const dpGroup = createElement("div", { className: "mb-3" });
12104
+ dpGroup.appendChild(createElement("label", {
12105
+ className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1",
12106
+ text: "Decimal Places"
12107
+ }));
12108
+ dpGroup.appendChild(createElement("input", {
12109
+ type: "number",
12110
+ className: "w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md bg-transparent",
12111
+ value: String(cfg.decimalPlaces ?? 2),
12112
+ min: "0",
12113
+ max: "10",
12114
+ placeholder: "2",
12115
+ oninput: (e) => {
12116
+ const v = e.target.value;
12117
+ patchFormulaConfig({ decimalPlaces: v !== "" ? parseInt(v, 10) : 2 });
12118
+ }
12119
+ }));
12120
+ body.appendChild(dpGroup);
12121
+ const zeroValues = {};
12122
+ availableFields.forEach((f) => {
12123
+ zeroValues[f.fieldName] = 0;
12124
+ zeroValues[f.id] = 0;
12125
+ });
12126
+ let previewText = "\u2014";
12127
+ try {
12128
+ const previewResult = evaluateFormulaConfig(cfg, zeroValues);
12129
+ if (!previewResult.error && !isNaN(previewResult.result)) {
12130
+ previewText = previewResult.result.toFixed(cfg.decimalPlaces ?? 2);
12131
+ } else if (previewResult.error) {
12132
+ previewText = `Error: ${previewResult.error}`;
12133
+ }
12134
+ } catch {
12135
+ }
12136
+ const previewSection = createElement("div", { className: "mb-4" });
12137
+ previewSection.appendChild(createElement("label", {
12138
+ className: "block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1",
12139
+ text: "Preview (fields = 0)"
12140
+ }));
12141
+ previewSection.appendChild(createElement("div", {
12142
+ className: "px-3 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md text-sm font-mono text-gray-700 dark:text-gray-300",
12143
+ text: previewText
12144
+ }));
12145
+ body.appendChild(previewSection);
12146
+ }
12147
+ if (selectedField.type !== "image" && selectedField.type !== "formula") {
11351
12148
  const placeholderGroup = createElement("div");
11352
12149
  placeholderGroup.appendChild(createElement("label", { className: "block text-sm font-normal text-gray-700 dark:text-gray-300 mb-1", text: "Placeholder" }));
11353
12150
  placeholderGroup.appendChild(createElement("input", {
@@ -12821,6 +13618,6 @@ sortablejs/modular/sortable.esm.js:
12821
13618
  *)
12822
13619
  */
12823
13620
 
12824
- export { FormBuilder, FormRenderer, FormSchemaValidation, LOOKUP_SOURCE_TYPE_OPTIONS, builderToPlatform, cleanFormSchema, convertValidationObjectToArray, detectCircularDependency, evaluateFormula, formStore, generateName, getColSpanFromWidth, getNumericFieldsForFormula, getValidationConfigForAngular, initFormBuilder, parseFormulaDependencies, parseWidth, platformToBuilder, resetNameGeneratorCounter, validateFormula };
13621
+ export { FormBuilder, FormRenderer, FormSchemaValidation, LOOKUP_SOURCE_TYPE_OPTIONS, builderToPlatform, cleanFormSchema, convertValidationObjectToArray, detectCircularDependency, detectFormulaFieldCircularDependency, evaluateFormula, evaluateFormulaConfig, evaluateFormulaExpression, extractBracketFields, formStore, generateName, getColSpanFromWidth, getFieldsForFormula, getNumericFieldsForFormula, getValidationConfigForAngular, initFormBuilder, parseFormulaDependencies, parseWidth, platformToBuilder, resetNameGeneratorCounter, validateFormula, validateFormulaExpression };
12825
13622
  //# sourceMappingURL=out.js.map
12826
13623
  //# sourceMappingURL=index.mjs.map